Settlers of EMF
Beginnings of an EMF themed implementation of Settlers of Catanmaster
parent
4cbfd23e05
commit
57ee3129ac
|
@ -0,0 +1,775 @@
|
|||
"""Settlers of EMF
|
||||
|
||||
After a long voyage of great deprivation of wifi, your vehicles have finally reached the edge of an uncharted field at the foot of a great castle. The Electromagnetic Field! But you are not the only discoverer. Other fearless voyagers have also arrived at the foot the castle: the race to settle the Electricmagnetic Field has begun! """
|
||||
|
||||
___title___ = "Settlers of EMF"
|
||||
___license___ = "MIT"
|
||||
___dependencies___ = ["sleep"]
|
||||
___categories___ = ["Games"]
|
||||
___bootstrapped___ = False
|
||||
|
||||
import random, ugfx, math, time
|
||||
from app import restart_to_default
|
||||
from tilda import Buttons
|
||||
|
||||
ugfx.init()
|
||||
|
||||
# Because the button constants have no kind of order, it's hard to iterate
|
||||
# through them, so keep a global list here to help us keep the code concise
|
||||
BUTTONS = [
|
||||
Buttons.BTN_1,
|
||||
Buttons.BTN_2,
|
||||
Buttons.BTN_3,
|
||||
Buttons.BTN_4,
|
||||
Buttons.BTN_5,
|
||||
Buttons.BTN_6,
|
||||
Buttons.BTN_7,
|
||||
Buttons.BTN_8,
|
||||
Buttons.BTN_9,
|
||||
]
|
||||
|
||||
|
||||
class State:
|
||||
|
||||
def run(self):
|
||||
"""Runs the main loop for this state"""
|
||||
self.done = False
|
||||
self.redraw = False
|
||||
self.selection = 0
|
||||
|
||||
# Render the current screen
|
||||
self.draw()
|
||||
|
||||
# Enter the loop and spin until the user makes a choice
|
||||
self.initialise()
|
||||
while not self.done:
|
||||
time.sleep_ms(10)
|
||||
if self.redraw:
|
||||
self.draw()
|
||||
self.redraw = False
|
||||
self.deinitialise()
|
||||
|
||||
# Then drop back out into the state machine returning the selected choice
|
||||
return self.selection
|
||||
|
||||
def draw(self):
|
||||
"""Renders the current state to the screen"""
|
||||
pass
|
||||
|
||||
def initialise(self):
|
||||
"""Perform actions that need to happen before we enter the loop"""
|
||||
pass
|
||||
|
||||
def deinitialise(self):
|
||||
"""Perform actions that need to happen after we exit the loop"""
|
||||
pass
|
||||
|
||||
|
||||
class Menu(State):
|
||||
|
||||
def __init__(self, question, choices):
|
||||
self.question = question
|
||||
self.choices = choices
|
||||
|
||||
def draw(self):
|
||||
# Draw the menu on screen
|
||||
ugfx.clear(ugfx.BLACK)
|
||||
ugfx.display_image(0, 0, 'settlers_game/title.png')
|
||||
ugfx.text(5, 95, self.question, ugfx.WHITE)
|
||||
i = 0
|
||||
for c in self.choices:
|
||||
col = ugfx.WHITE
|
||||
if 'colour' in c:
|
||||
col = c['colour']
|
||||
if 'disabled' in c and c['disabled']:
|
||||
col = ugfx.html_color(0x676767)
|
||||
ugfx.text(20, (20 * i) + 125, "{} - {} ".format(i + 1, c['name']), col)
|
||||
i = i + 1
|
||||
|
||||
# Set the initial selection
|
||||
self._set_selection(self.selection)
|
||||
|
||||
def initialise(self):
|
||||
# Register callbacks
|
||||
Buttons.enable_interrupt(Buttons.BTN_A, self._button_callback)
|
||||
Buttons.enable_interrupt(Buttons.JOY_Up, self._button_callback)
|
||||
Buttons.enable_interrupt(Buttons.JOY_Down, self._button_callback)
|
||||
for i in range(len(self.choices)):
|
||||
c = self.choices[i]
|
||||
if 'disabled' not in c or not c['disabled']:
|
||||
Buttons.enable_interrupt(BUTTONS[i], self._button_callback)
|
||||
|
||||
def deinitialise(self):
|
||||
# Unregister callbacks
|
||||
Buttons.disable_interrupt(Buttons.BTN_A)
|
||||
Buttons.disable_interrupt(Buttons.JOY_Up)
|
||||
Buttons.disable_interrupt(Buttons.JOY_Down)
|
||||
for i in range(len(self.choices)):
|
||||
c = self.choices[i]
|
||||
if 'disabled' not in c or not c['disabled']:
|
||||
Buttons.disable_interrupt(BUTTONS[i])
|
||||
|
||||
def _button_callback(self, btn):
|
||||
if btn == Buttons.BTN_A:
|
||||
self.done = True
|
||||
else:
|
||||
if btn == Buttons.JOY_Up:
|
||||
new_selection = self._prev_valid_selection(self.selection)
|
||||
elif btn == Buttons.JOY_Down:
|
||||
new_selection = self._next_valid_selection(self.selection)
|
||||
else:
|
||||
for i in range(len(self.choices)):
|
||||
if btn == BUTTONS[i]:
|
||||
new_selection = i
|
||||
self._set_selection(new_selection)
|
||||
|
||||
def _prev_valid_selection(self, sel):
|
||||
# Calculate the next enabled option going upwards
|
||||
if sel == 0:
|
||||
next_sel = len(self.choices) - 1
|
||||
else:
|
||||
next_sel = sel - 1
|
||||
while True:
|
||||
c = self.choices[next_sel]
|
||||
if 'disabled' not in c or not c['disabled']:
|
||||
break
|
||||
if next_sel == 0:
|
||||
next_sel = len(self.choices) - 1
|
||||
else:
|
||||
next_sel = next_sel - 1
|
||||
return next_sel
|
||||
|
||||
def _next_valid_selection(self, sel):
|
||||
# Calculate the next enabled option going downwards
|
||||
if sel == len(self.choices) - 1:
|
||||
next_sel = 0
|
||||
else:
|
||||
next_sel = sel + 1
|
||||
while True:
|
||||
c = self.choices[next_sel]
|
||||
if 'disabled' not in c or not c['disabled']:
|
||||
break
|
||||
if next_sel == len(self.choices) - 1:
|
||||
next_sel = 0
|
||||
else:
|
||||
next_sel = next_sel + 1
|
||||
return next_sel
|
||||
|
||||
def _set_selection(self, new_selection):
|
||||
# Redraws the selection box
|
||||
ugfx.box(0, (20 * self.selection) + 125, 240, 20, ugfx.BLACK)
|
||||
self.selection = new_selection
|
||||
ugfx.box(0, (20 * self.selection) + 125, 240, 20, ugfx.WHITE)
|
||||
|
||||
|
||||
class MainMenu(Menu):
|
||||
NEW_GAME = 0
|
||||
CONTINUE_GAME = 1
|
||||
EXIT = 2
|
||||
|
||||
options = [
|
||||
{'name': "Start New Game"},
|
||||
{'name': "Continue Game"},
|
||||
{'name': "Exit"},
|
||||
]
|
||||
|
||||
def __init__(self, disable_continue_option=True):
|
||||
MainMenu.options[1]['disabled'] = disable_continue_option
|
||||
super().__init__('Main menu:', MainMenu.options)
|
||||
|
||||
|
||||
class TeamMenu(Menu):
|
||||
BACK = 6
|
||||
|
||||
options = [
|
||||
{'name': "Scottish Consulate",
|
||||
'colour': ugfx.html_color(0x0000ff)},
|
||||
{'name': "Camp Holland",
|
||||
'colour': ugfx.html_color(0xff8c00)},
|
||||
{'name': "Sheffield Hackers",
|
||||
'colour': ugfx.html_color(0x26c6da)},
|
||||
{'name': "Milliways",
|
||||
'colour': ugfx.html_color(0xff00ff)},
|
||||
{'name': "Robot Arms",
|
||||
'colour': ugfx.html_color(0xeaeaea)},
|
||||
{'name': "Null Sector",
|
||||
'colour': ugfx.html_color(0x9c27b0)},
|
||||
{'name': "Back"},
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('Choose your team:', TeamMenu.options)
|
||||
|
||||
def get_selected_team(self):
|
||||
return TeamMenu.options[self.selection].copy()
|
||||
|
||||
|
||||
class BuildMenu(Menu):
|
||||
BACK = 0
|
||||
|
||||
options = [
|
||||
{'name': "Road"},
|
||||
{'name': "Town"},
|
||||
{'name': "City"},
|
||||
{'name': "Back"},
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('Build a thing:', BuildMenu.options)
|
||||
# TODO: show the build cost
|
||||
# TODO: enable options based on whether the player can afford them
|
||||
|
||||
|
||||
class Hex:
|
||||
"""Hexes are the games tiles. They have a resource kind, correspond to the value
|
||||
of a roll of two D6 and may or may not contain the robber."""
|
||||
|
||||
# Screen coords are x,y values that locate pixels on the physical display:
|
||||
#
|
||||
# 0,0 → → 240,0
|
||||
# ↓ ↓
|
||||
# 0,320 → 240,320
|
||||
#
|
||||
# Hex coords are x,y,z values that locate the relative positions of hexagons:
|
||||
#
|
||||
# 0,1,-1
|
||||
# -1,1,0 ↖ ↑ ↗ 1,0,-1
|
||||
# 0,0,0
|
||||
# -1,0,1 ↙ ↓ ↘ 1,-1,0
|
||||
# 0,-1,1
|
||||
#
|
||||
# Converting between the two systems can be done by multiplying the x and y
|
||||
# coordinates against a matrix. When converting to hex coords, the z value
|
||||
# can be computed from the new x and y values because x + y + z must always
|
||||
# equal zero.
|
||||
#
|
||||
# This is the matrix used to convert from hex coords to screen coords
|
||||
matrix = [3.0 * 0.5, 0.0, math.sqrt(3.0) * 0.5, math.sqrt(3.0)]
|
||||
|
||||
# The screen coordinate to use as the origin for hex coordinates,
|
||||
# the centre of hex 0,0,0 will be at this coordinate
|
||||
origin = [math.ceil(ugfx.width() / 2), math.ceil(ugfx.height() / 2)]
|
||||
|
||||
# Size in pixels of the hex, from the centre point to each corner
|
||||
size = 25
|
||||
|
||||
# Transformations for how to get to the neighbouring hexes
|
||||
directions = {
|
||||
0: [-1, 1, 0], # South West
|
||||
1: [0, 1, -1], # South
|
||||
2: [1, 0, -1], # South East
|
||||
3: [1, -1, 0], # North East
|
||||
4: [0, -1, 1], # North
|
||||
5: [-1, 0, 1], # North West
|
||||
}
|
||||
|
||||
def __init__(self, coords, resource, number, robber):
|
||||
"""Create a new hex at the given hex coordinates, of the given kind of resource"""
|
||||
# Validate coords
|
||||
assert len(coords) == 3, 'Invalid number of hexagon coordinates'
|
||||
assert coords[0] + coords[1] + coords[2] == 0, 'Invalid hexagon coordinate values'
|
||||
self.coords = coords
|
||||
|
||||
# The kind of resource hosted by this hex
|
||||
self.resource = resource
|
||||
|
||||
# The dice roll required to win this resource
|
||||
self.number = number
|
||||
|
||||
# Whether this hex contains the robber
|
||||
self.robber = robber
|
||||
|
||||
# Whether this hex should be highlighted
|
||||
self.highlight = False
|
||||
|
||||
# A hex is quite expensive to draw, so keep track of whether the state changed recently
|
||||
# to avoid redrawing where possible
|
||||
self.changed = True
|
||||
|
||||
# Compute the screen coordinates of the centre of the hex
|
||||
x = self.coords[0]
|
||||
y = self.coords[1]
|
||||
newX = (Hex.matrix[0] * x + Hex.matrix[1] * y) * Hex.size
|
||||
newY = (Hex.matrix[2] * x + Hex.matrix[3] * y) * Hex.size
|
||||
self.centre = [newX + Hex.origin[0], newY + Hex.origin[1]]
|
||||
|
||||
# Generate the list of screen coordinates for each of the corners of the hex
|
||||
self.nodes = []
|
||||
for i in range(0, 6):
|
||||
angle = 2.0 * math.pi * (0 - i) / 6
|
||||
offset = [Hex.size * math.cos(angle), Hex.size * math.sin(angle)]
|
||||
self.nodes.append([int(self.centre[0] + offset[0]), int(self.centre[1] + offset[1])])
|
||||
|
||||
# Generate the list of pairs of screen coordinates for each of the sides of the hex
|
||||
self.edges = []
|
||||
for i in range(0, 6):
|
||||
node1 = self.nodes[i]
|
||||
if i < 5:
|
||||
node2 = self.nodes[i + 1]
|
||||
else:
|
||||
node2 = self.nodes[0]
|
||||
if node1[0] <= node2[0]:
|
||||
self.edges.append([node1, node2])
|
||||
else:
|
||||
self.edges.append([node2, node1])
|
||||
|
||||
def set_highlight(self, highlight):
|
||||
if self.highlight != highlight:
|
||||
self.highlight = highlight
|
||||
self.changed = True
|
||||
|
||||
@staticmethod
|
||||
def get_neighbouring_hex_coords(coords, direction):
|
||||
return [a + b for a, b in zip(coords, Hex.directions[direction])]
|
||||
|
||||
def draw(self):
|
||||
if self.changed:
|
||||
self.changed = False
|
||||
ugfx.fill_polygon(0, 0, self.nodes, self.resource['col'])
|
||||
if self.highlight:
|
||||
text_colour = ugfx.WHITE
|
||||
else:
|
||||
text_colour = ugfx.BLACK
|
||||
text_offset = Hex.size * 0.45
|
||||
if self.robber:
|
||||
ugfx.text(round(self.centre[0] - Hex.size * 0.75), round(self.centre[1] - text_offset), "Rob ", text_colour)
|
||||
else:
|
||||
if self.resource['kind'] != 5:
|
||||
ugfx.text(round(self.centre[0] - text_offset), round(self.centre[1] - text_offset), "{} ".format(self.number['roll']), text_colour)
|
||||
|
||||
|
||||
class Settlement:
|
||||
"""A node at which it is possible to build a settlement."""
|
||||
|
||||
# Possible things this location may contain, the values here are the number of
|
||||
# victory points that the building is worth to the player who built it
|
||||
EMPTY = 0
|
||||
TOWN = 1
|
||||
CITY = 2
|
||||
|
||||
def __init__(self, node):
|
||||
# Screen coords that define the settlement
|
||||
self.node = node
|
||||
|
||||
# The list of hexes to which this settlement is adjacent
|
||||
self.hexes = []
|
||||
|
||||
# What is built here and who owns it
|
||||
self.team = None
|
||||
self.contents = Settlement.EMPTY
|
||||
|
||||
def is_empty(self):
|
||||
return self.contents == Settlement.EMPTY
|
||||
|
||||
def prob_score(self):
|
||||
"""The probability score of the location is the sum of the probability of all adjacent hexes"""
|
||||
score = 0
|
||||
for h in self.hexes:
|
||||
score = score + h.number['prob']
|
||||
return score
|
||||
|
||||
def build_town(self, team):
|
||||
assert self.contents == Settlement.EMPTY, 'Town can only be built in empty location'
|
||||
self.team = team
|
||||
self.contents = Settlement.TOWN
|
||||
|
||||
def build_city(self, team):
|
||||
assert self.contents == Settlement.TOWN and self.team['name'] == team['name'], 'City can only be built in place of one of your own towns'
|
||||
self.contents = Settlement.CITY
|
||||
|
||||
def draw(self):
|
||||
if self.contents == Settlement.TOWN:
|
||||
ugfx.fill_circle(self.node[0], self.node[1], 4, self.team['colour'])
|
||||
ugfx.circle(self.node[0], self.node[1], 4, ugfx.WHITE)
|
||||
elif self.contents == Settlement.CITY:
|
||||
ugfx.fill_circle(self.node[0], self.node[1], 8, self.team['colour'])
|
||||
ugfx.circle(self.node[0], self.node[1], 8, ugfx.WHITE)
|
||||
|
||||
|
||||
class Road:
|
||||
"""An edge along which it is possible to build a road."""
|
||||
|
||||
EMPTY = 0
|
||||
ROAD = 1
|
||||
|
||||
def __init__(self, edge):
|
||||
# List of screen coords that define the road
|
||||
self.edge = edge
|
||||
|
||||
# What is built here and who owns it
|
||||
self.team = None
|
||||
self.contents = Road.EMPTY
|
||||
|
||||
def is_empty(self):
|
||||
return self.contents == Road.EMPTY
|
||||
|
||||
def build_road(self, team):
|
||||
assert self.contents == Road.EMPTY, 'Road can only be built in empty location'
|
||||
self.team = team
|
||||
self.contents = Road.ROAD
|
||||
|
||||
def draw(self):
|
||||
if self.contents == Road.ROAD:
|
||||
ugfx.thickline(self.edge[0][0], self.edge[0][1], self.edge[1][0], self.edge[1][1], ugfx.WHITE, 6, False)
|
||||
ugfx.thickline(self.edge[0][0], self.edge[0][1], self.edge[1][0], self.edge[1][1], self.team['colour'], 4, False)
|
||||
|
||||
|
||||
class Player:
|
||||
"""The player's hand of resource cards and their score and what not."""
|
||||
|
||||
def __init__(self, team, roads, settlements, resources):
|
||||
# Player team details
|
||||
self.team = team
|
||||
|
||||
# All the buildable game board locations
|
||||
self.roads = roads
|
||||
self.settlements = settlements
|
||||
|
||||
# Player resources
|
||||
self.resources = []
|
||||
for r in resources.copy():
|
||||
r['quantity'] = 0
|
||||
self.resources.append(r)
|
||||
|
||||
# Collect starting resources from the hexes adjacent to our starting settlements
|
||||
for s in [x for x in self.settlements if x.team == self.team]:
|
||||
for h in s.hexes:
|
||||
if r['kind'] == h.resource['kind']:
|
||||
r['quantity'] = r['quantity'] + 1
|
||||
|
||||
def score(self):
|
||||
points = 0
|
||||
for s in [x for x in self.settlements if x.team == self.team]:
|
||||
points = points + s.contents
|
||||
return points
|
||||
|
||||
def draw(self):
|
||||
# Player's team and score
|
||||
ugfx.text(5, 8, "{} ".format(self.team['name']), self.team['colour'])
|
||||
ugfx.text(5, 28, "Points: {} ".format(self.score()), ugfx.WHITE)
|
||||
|
||||
# Player's resources
|
||||
ugfx.area(0, 280, 120, 40, ugfx.BLACK)
|
||||
offset = int(ugfx.width() / len(self.resources))
|
||||
square = int(offset / 3)
|
||||
for i in range(len(self.resources)):
|
||||
ugfx.area((offset * i) + 1, 285, square, 22, self.resources[i]['col'])
|
||||
ugfx.text((offset * i) + 1 + square, 285, "{} ".format(self.resources[i]['quantity']), ugfx.WHITE)
|
||||
|
||||
|
||||
class Dice:
|
||||
|
||||
# Size in pixels that the dice will be drawn on screen
|
||||
size = 25
|
||||
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def roll(self):
|
||||
self.die1 = random.randint(1, 6)
|
||||
self.die2 = random.randint(1, 6)
|
||||
|
||||
def reset(self):
|
||||
self.die1 = 0
|
||||
self.die2 = 0
|
||||
|
||||
def total(self):
|
||||
return self.die1 + self.die2
|
||||
|
||||
def draw(self):
|
||||
self._draw_die(210, 5, self.die1)
|
||||
self._draw_die(210, 5 + Dice.size + 5, self.die2)
|
||||
|
||||
def _draw_die(self, x, y, num):
|
||||
ugfx.box(x, y, Dice.size, Dice.size, ugfx.html_color(0x676767))
|
||||
ugfx.area(x + 1, y + 1, Dice.size - 2, Dice.size - 2, ugfx.BLACK)
|
||||
if num == 1:
|
||||
self._draw_one_dot(x, y)
|
||||
if num == 2:
|
||||
self._draw_two_dot(x, y)
|
||||
if num == 3:
|
||||
self._draw_one_dot(x, y)
|
||||
self._draw_two_dot(x, y)
|
||||
if num == 4:
|
||||
self._draw_four_dot(x, y)
|
||||
if num == 5:
|
||||
self._draw_one_dot(x, y)
|
||||
self._draw_four_dot(x, y)
|
||||
if num == 6:
|
||||
self._draw_six_dot(x, y)
|
||||
|
||||
def _draw_one_dot(self, x, y):
|
||||
ugfx.fill_circle(x + int(Dice.size / 2), y + int(Dice.size / 2), 1, ugfx.WHITE)
|
||||
|
||||
def _draw_two_dot(self, x, y):
|
||||
ugfx.fill_circle(1 + x + int(Dice.size / 8), (y - 1) + (Dice.size - int(Dice.size / 8)), 1, ugfx.WHITE)
|
||||
ugfx.fill_circle((x - 2) + (Dice.size - int(Dice.size / 8)), 1 + y + int(Dice.size / 8), 1, ugfx.WHITE)
|
||||
|
||||
def _draw_four_dot(self, x, y):
|
||||
self._draw_two_dot(x, y)
|
||||
ugfx.fill_circle(1 + x + int(Dice.size / 8), 1 + y + int(Dice.size / 8), 1, ugfx.WHITE)
|
||||
ugfx.fill_circle((x - 2) + (Dice.size - int(Dice.size / 8)), (y - 1) + (Dice.size - int(Dice.size / 8)), 1, ugfx.WHITE)
|
||||
|
||||
def _draw_six_dot(self, x, y):
|
||||
self._draw_four_dot(x, y)
|
||||
ugfx.fill_circle(1 + x + int(Dice.size / 8), y + int(Dice.size / 2), 1, ugfx.WHITE)
|
||||
ugfx.fill_circle((x - 2) + (Dice.size - int(Dice.size / 8)), y + int(Dice.size / 2), 1, ugfx.WHITE)
|
||||
|
||||
|
||||
class GameBoard(State):
|
||||
MAIN_MENU = 0
|
||||
BUILD_MENU = 1
|
||||
END_TURN = 2
|
||||
|
||||
# Kinds of resource
|
||||
SHEEP = {'kind':0, 'col': ugfx.html_color(0xd4e157)}
|
||||
WHEAT = {'kind':1, 'col': ugfx.html_color(0xffc107)}
|
||||
WOOD = {'kind':2, 'col': ugfx.html_color(0x993300)}
|
||||
BRICK = {'kind':3, 'col': ugfx.html_color(0xff0000)}
|
||||
ORE = {'kind':4, 'col': ugfx.html_color(0x757575)}
|
||||
DESERT = {'kind':5, 'col': ugfx.html_color(0xffee55)} # Not really a resource
|
||||
|
||||
# List of resources (pre-randomised to combat the not-very random number
|
||||
# generator) that make up the hexes on the game board for 4 players
|
||||
resources = [ORE, SHEEP, WHEAT, ORE, ORE, WOOD, DESERT, BRICK, SHEEP, WOOD,
|
||||
WHEAT, WOOD, WOOD, WHEAT, SHEEP, BRICK, SHEEP, BRICK, WHEAT]
|
||||
|
||||
# Dice roll probabilities
|
||||
TWO = {'roll':2, 'prob':1}
|
||||
THREE = {'roll':3, 'prob':2}
|
||||
FOUR = {'roll':4, 'prob':3}
|
||||
FIVE = {'roll':5, 'prob':4}
|
||||
SIX = {'roll':6, 'prob':5}
|
||||
SEVEN = {'roll':7, 'prob':0} # Most probable, but zero because desert
|
||||
EIGHT = {'roll':8, 'prob':5}
|
||||
NINE = {'roll':9, 'prob':4}
|
||||
TEN = {'roll':10, 'prob':3}
|
||||
ELEVEN = {'roll':11, 'prob':2}
|
||||
TWELVE = {'roll':12, 'prob':1}
|
||||
|
||||
# Dice rolls for (these have a strict order) to be assigned to the resource
|
||||
# hexes for 4 player games
|
||||
numbers = [FIVE, TWO, SIX, THREE, EIGHT, TEN, NINE, TWELVE, ELEVEN, FOUR,
|
||||
EIGHT, TEN, NINE, FOUR, FIVE, SIX, THREE, ELEVEN]
|
||||
|
||||
def __init__(self, team):
|
||||
# Two rings of hexes around the centre
|
||||
radius = 2
|
||||
|
||||
# Choose a starting hex on the outermost ring of hexes
|
||||
choice = random.randrange(0, 6)
|
||||
coords = [0, 0, 0]
|
||||
for i in range(radius):
|
||||
coords = [a + b for a, b in zip(coords, Hex.directions[choice])]
|
||||
|
||||
# Copy lists so we can edit them with impunity
|
||||
r_copy = GameBoard.resources.copy()
|
||||
n_copy = GameBoard.numbers.copy()
|
||||
|
||||
self.hexes = []
|
||||
while radius > 0:
|
||||
# From the starting hex, go radius hexes in each of the 6 directions
|
||||
for i in list(range((choice + 2) % 6, 6)) + list(range(0, (choice + 2) % 6)):
|
||||
for _ in range(radius):
|
||||
# The resources are picked at random from the list
|
||||
resource = r_copy.pop(random.randrange(0, len(r_copy)))
|
||||
# But the dice roll numbers are picked in order, unless it's
|
||||
# the desert in which case that is always 7
|
||||
number = GameBoard.SEVEN
|
||||
if resource['kind'] != 5:
|
||||
number = n_copy.pop(0)
|
||||
self.hexes.append(Hex(coords, resource, number, number['roll'] == 7))
|
||||
coords = Hex.get_neighbouring_hex_coords(coords, i)
|
||||
|
||||
# Go into the next ring of hexes (opposite direction of starting choice)
|
||||
coords = Hex.get_neighbouring_hex_coords(coords, (choice + 3) % 6)
|
||||
radius = radius - 1
|
||||
# The final, centre hex
|
||||
resource = r_copy.pop()
|
||||
number = GameBoard.SEVEN
|
||||
if resource['kind'] != 5:
|
||||
number = n_copy.pop(0)
|
||||
self.hexes.append(Hex(coords, resource, number, number['roll'] == 7))
|
||||
|
||||
# Generate lists of unique valid locations for building
|
||||
self.roads = []
|
||||
self.settlements = []
|
||||
for h in self.hexes:
|
||||
for edge in h.edges:
|
||||
already_got = False
|
||||
for r in self.roads:
|
||||
if r.edge == edge:
|
||||
already_got = True
|
||||
if not already_got:
|
||||
r = Road(edge)
|
||||
self.roads.append(r)
|
||||
for node in h.nodes:
|
||||
already_got = False
|
||||
for s in self.settlements:
|
||||
if s.node == node:
|
||||
already_got = True
|
||||
s.hexes.append(h)
|
||||
if not already_got:
|
||||
s = Settlement(node)
|
||||
s.hexes.append(h)
|
||||
self.settlements.append(s)
|
||||
|
||||
# Give the team starting towns in the two settlements with the highest probability score
|
||||
# TODO interleave starting town choices for multi-player
|
||||
self.pick_starting_settlement(team)
|
||||
self.pick_starting_settlement(team)
|
||||
|
||||
# The dice roller
|
||||
self.dice = Dice()
|
||||
|
||||
# The player details
|
||||
self.player = Player(team, self.roads, self.settlements, [GameBoard.SHEEP, GameBoard.WHEAT, GameBoard.WOOD, GameBoard.BRICK, GameBoard.ORE])
|
||||
|
||||
def get_roads_for_settlement(self, settlement):
|
||||
"""Return a list of roads that connect to the given settlement"""
|
||||
roads = []
|
||||
for road in self.roads:
|
||||
if settlement.node in road.edge:
|
||||
roads.append(road)
|
||||
return roads
|
||||
|
||||
def pick_starting_settlement(self, team):
|
||||
"""Choose a starting settlement for the given team, and place a town and a connecting road there"""
|
||||
|
||||
# Sort the settlements by highest dice roll probability
|
||||
sorted_settlements = sorted(self.settlements, key=lambda s: s.prob_score())
|
||||
sorted_settlements.reverse()
|
||||
|
||||
# Build at the highest probability settlement that is still available
|
||||
for s in sorted_settlements:
|
||||
# TODO check the towns are not too close to one another
|
||||
if s.is_empty():
|
||||
s.build_town(team)
|
||||
s_roads = self.get_roads_for_settlement(s)
|
||||
s_roads[random.randrange(0, len(s_roads))].build_road(team)
|
||||
break
|
||||
|
||||
def draw(self):
|
||||
if not self.redraw:
|
||||
ugfx.clear(ugfx.BLACK)
|
||||
self.dice.draw()
|
||||
for h in self.hexes:
|
||||
h.draw()
|
||||
for r in self.roads:
|
||||
r.draw()
|
||||
for s in self.settlements:
|
||||
s.draw()
|
||||
self.player.draw()
|
||||
|
||||
def initialise(self):
|
||||
# Register callbacks
|
||||
Buttons.enable_interrupt(Buttons.BTN_Menu, self._button_callback)
|
||||
Buttons.enable_interrupt(Buttons.BTN_B, self._button_callback)
|
||||
Buttons.enable_interrupt(Buttons.BTN_Star, self._button_callback)
|
||||
Buttons.enable_interrupt(Buttons.BTN_Hash, self._button_callback)
|
||||
|
||||
def deinitialise(self):
|
||||
# Unregister callbacks
|
||||
Buttons.disable_interrupt(Buttons.BTN_Menu)
|
||||
Buttons.disable_interrupt(Buttons.BTN_B)
|
||||
Buttons.disable_interrupt(Buttons.BTN_Star)
|
||||
Buttons.disable_interrupt(Buttons.BTN_Hash)
|
||||
|
||||
# Ensure all hexes are drawn next time we enter this state
|
||||
for h in self.hexes:
|
||||
h.changed = True
|
||||
|
||||
def _button_callback(self, btn):
|
||||
if btn == Buttons.BTN_Menu:
|
||||
self.selection = GameBoard.MAIN_MENU
|
||||
self.done = True
|
||||
if btn == Buttons.BTN_B:
|
||||
self.selection = GameBoard.BUILD_MENU
|
||||
self.done = True
|
||||
if btn == Buttons.BTN_Star:
|
||||
# End the turn
|
||||
self.selection = GameBoard.END_TURN
|
||||
self.done = True
|
||||
self.dice.reset()
|
||||
for h in self.hexes:
|
||||
h.set_highlight(False)
|
||||
if btn == Buttons.BTN_Hash:
|
||||
# Only roll the dice if not already rolled
|
||||
if self.dice.total() == 0:
|
||||
self.dice.roll()
|
||||
# Highlight the hexes corresponding with the dice roll
|
||||
num = self.dice.total()
|
||||
for h in self.hexes:
|
||||
if h.number['roll'] == num or (num == 7 and h.robber):
|
||||
h.set_highlight(True)
|
||||
else:
|
||||
h.set_highlight(False)
|
||||
# TODO collect resources
|
||||
self.redraw = True
|
||||
|
||||
|
||||
class Settlers:
|
||||
"""A lean mean state machine"""
|
||||
|
||||
# Game states
|
||||
EXIT = 0
|
||||
MAIN_MENU = 1
|
||||
TEAM_MENU = 2
|
||||
GAME = 3
|
||||
BUILD_MENU = 4
|
||||
END_TURN_MENU = 5
|
||||
|
||||
def __init__(self):
|
||||
self.state = Settlers.MAIN_MENU
|
||||
self.game = None
|
||||
|
||||
def run(self):
|
||||
while self.state != Settlers.EXIT:
|
||||
|
||||
if self.state == Settlers.MAIN_MENU:
|
||||
menu = MainMenu(self.game is None)
|
||||
x = menu.run()
|
||||
if x == MainMenu.NEW_GAME:
|
||||
self.state = Settlers.TEAM_MENU
|
||||
if x == MainMenu.CONTINUE_GAME:
|
||||
self.state = Settlers.GAME
|
||||
if x == MainMenu.EXIT:
|
||||
self.state = Settlers.EXIT
|
||||
|
||||
if self.state == Settlers.TEAM_MENU:
|
||||
menu = TeamMenu()
|
||||
x = menu.run()
|
||||
if x == TeamMenu.BACK:
|
||||
self.state = Settlers.MAIN_MENU
|
||||
else:
|
||||
self.game = GameBoard(menu.get_selected_team())
|
||||
self.state = Settlers.GAME
|
||||
|
||||
if self.state == Settlers.GAME:
|
||||
x = self.game.run()
|
||||
if x == GameBoard.MAIN_MENU:
|
||||
self.state = Settlers.MAIN_MENU
|
||||
if x == GameBoard.BUILD_MENU:
|
||||
self.state = Settlers.BUILD_MENU
|
||||
if x == GameBoard.END_TURN:
|
||||
self.state = Settlers.END_TURN_MENU
|
||||
|
||||
if self.state == Settlers.BUILD_MENU:
|
||||
menu = BuildMenu()
|
||||
x = menu.run()
|
||||
if x == BuildMenu.BACK:
|
||||
self.state = Settlers.GAME
|
||||
else:
|
||||
# TODO initiate building a thing
|
||||
self.state = Settlers.GAME
|
||||
|
||||
if self.state == Settlers.END_TURN_MENU:
|
||||
# TODO: Ask for confirmation
|
||||
self.state = Settlers.GAME
|
||||
|
||||
# User chose exit, a machine reset is the easiest way :-)
|
||||
restart_to_default()
|
||||
|
||||
|
||||
game = Settlers()
|
||||
game.run()
|
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
|
@ -0,0 +1,153 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="210mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 210 297"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
sodipodi:docname="title.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
inkscape:export-filename="/home/mbooth/eclipse-workspace/Mk4-Apps/settlers_game/title.svg.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.62"
|
||||
inkscape:cx="143.29493"
|
||||
inkscape:cy="689.03226"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer2"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="BG"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
id="rect10"
|
||||
width="208.64285"
|
||||
height="101.29762"
|
||||
x="0.75595242"
|
||||
y="2.9345238"
|
||||
style="stroke-width:0.26458332" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Text">
|
||||
<image
|
||||
y="10.664136"
|
||||
x="16.271868"
|
||||
id="image21"
|
||||
xlink:href="data:image/png;base64,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
|
||||
QmCC
|
||||
"
|
||||
style="image-rendering:optimizeSpeed"
|
||||
preserveAspectRatio="none"
|
||||
height="66.939583"
|
||||
width="24.606251" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="47.795704"
|
||||
y="34.977154"
|
||||
id="text26"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan24"
|
||||
x="47.795704"
|
||||
y="34.977154"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:25.39999962px;font-family:'Source Han Serif CN';-inkscape-font-specification:'Source Han Serif CN Bold';fill:#ffffff;stroke-width:0.26458332">SETTLERS</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="83.304222"
|
||||
y="70.846184"
|
||||
id="text26-0"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan24-2"
|
||||
x="83.304222"
|
||||
y="70.846184"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:25.39999962px;font-family:'Source Han Serif CN';-inkscape-font-specification:'Source Han Serif CN Bold';fill:#ffffff;stroke-width:0.26458332">EMF</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:5.88926697px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.14723167"
|
||||
x="103.27212"
|
||||
y="48.41703"
|
||||
id="text26-0-7"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan24-2-6"
|
||||
x="103.27212"
|
||||
y="48.41703"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:14.1342411px;font-family:'Source Han Serif CN';-inkscape-font-specification:'Source Han Serif CN Bold';fill:#ffffff;stroke-width:0.14723167">OF</tspan></text>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 7.2 KiB |
Loading…
Reference in New Issue