"""Settlers of EMF After a long voyage and 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! Settlers of EMF is a pass-and-play game of ruthless strategy for up to 4 players of all ages.""" ___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, Buttons.BTN_0, ] # 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 RESOURCE_KINDS = [ SHEEP, WHEAT, WOOD, BRICK, ORE ] # Set this to true to enable a "cheat code" to get more resources for testing # When in the main game running state, press 5 five times to get five more of # every resource TEST_MODE = False 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, clear_title=True): self.question = question self.choices = choices self.clear_title = clear_title if self.question: if isinstance(self.question, list): self.menu_offset = 100 + (20 * len(self.question)) else: self.question = [question] self.menu_offset = 120 else: self.menu_offset = 100 self.back = -1 def is_choice_enabled(self, num): c = self.choices[num] return 'disabled' not in c or not c['disabled'] def get_selected_choice(self): return self.choices[self.selection].copy() def draw(self): # Draw the menu on screen if self.clear_title: ugfx.clear(ugfx.BLACK) ugfx.display_image(0, 0, 'settlers_of_emf/title.png') else: ugfx.area(0, 95, 240, 225, ugfx.BLACK) if self.question: for i in range(len(self.question)): ugfx.text(2, 95 + (i * 20), self.question[i], ugfx.WHITE) offset = 0 for i in range(len(self.choices)): self._draw_choice(i, offset) offset = offset + 20 if 'cost' in self.choices[i]: offset = offset + 20 # Set the initial selection if self.is_choice_enabled(self.selection): self._set_selection(self.selection) else: self._set_selection(self._next_valid_selection(self.selection)) def _draw_choice(self, i, offset): c = self.choices[i] col = ugfx.WHITE if 'colour' in c: col = c['colour'] if not self.is_choice_enabled(i): col = ugfx.html_color(0x676767) if len(self.choices) == 1: text = "{} ".format(c['name']) else: if c['name'] == "Back": text = "B - {} ".format(c['name']) self.back = i else: text = "{} - {} ".format(i + 1, c['name']) ugfx.text(18, offset + self.menu_offset, text, col) if 'cost' in c: for j in range(len(c['cost'])): cost = c['cost'][j] if 'cost_or' in c and c['cost_or']: cost_text = "x{} / ".format(cost['amount']) if j == len(c['cost']) - 1: cost_text = "x{} ".format(cost['amount']) ugfx.area((63 * j) + 45, 21 + offset + self.menu_offset, 16, 16, cost['resource']['col']) ugfx.text((63 * j) + 61, 20 + offset + self.menu_offset, cost_text, col) else: ugfx.area((42 * j) + 45, 21 + offset + self.menu_offset, 16, 16, cost['resource']['col']) ugfx.text((42 * j) + 61, 20 + offset + self.menu_offset, "x{} ".format(cost['amount']), col) def initialise(self): # Register callbacks Buttons.enable_interrupt(Buttons.BTN_A, self._button_callback) Buttons.enable_interrupt(Buttons.BTN_B, 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)): if self.is_choice_enabled(i): Buttons.enable_interrupt(BUTTONS[i], self._button_callback) def deinitialise(self): # Unregister callbacks Buttons.disable_interrupt(Buttons.BTN_A) Buttons.disable_interrupt(Buttons.BTN_B) Buttons.disable_interrupt(Buttons.JOY_Up) Buttons.disable_interrupt(Buttons.JOY_Down) for i in range(len(self.choices)): if self.is_choice_enabled(i): Buttons.disable_interrupt(BUTTONS[i]) def _button_callback(self, btn): if btn == Buttons.BTN_A: self.done = True elif btn == Buttons.BTN_B: if self.back > -1: self._set_selection(self.back) 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: if self.is_choice_enabled(next_sel): 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: if self.is_choice_enabled(next_sel): 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 size = 2 if 'cost' in self.choices[self.selection] and len(self.choices[self.selection]['cost']) > 0 else 1 ugfx.box(0, self._get_offset_for_selection(self.selection) + self.menu_offset, 240, 20 * size, ugfx.BLACK) self.selection = new_selection size = 2 if 'cost' in self.choices[self.selection] and len(self.choices[self.selection]['cost']) > 0 else 1 ugfx.box(0, self._get_offset_for_selection(self.selection) + self.menu_offset, 240, 20 * size, ugfx.WHITE) def _get_offset_for_selection(self, sel): # Menu items are double height if they need to show a cost, so iterate # through the choices to find out exactly what the offset should be offset = 0 for i in range(len(self.choices)): if i == sel: return offset offset = offset + 20 if 'cost' in self.choices[i]: offset = offset + 20 class MainMenu(Menu): options = [ {'name': "Start New Game"}, {'name': "Continue Game"}, {'name': "Exit"}, ] NEW_GAME = 0 CONTINUE_GAME = 1 EXIT = 2 def __init__(self, disable_continue_option=True, clear_title=True): MainMenu.options[MainMenu.CONTINUE_GAME]['disabled'] = disable_continue_option super().__init__('Welcome!', MainMenu.options, clear_title) class TeamMenu(Menu): options = [ {'name': "Scottish Consulate", 'colour': ugfx.html_color(0x0000ff)}, {'name': "Camp Holland", 'colour': ugfx.html_color(0xff8c00)}, {'name': "Sheffield Hackspace", '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), 'cost': []}, {'name': "Start Game"}, {'name': "Back"}, ] TEAM_MAX = len(options) - 3 START_GAME = len(options) - 2 BACK = len(options) - 1 def __init__(self, teams): # Disable team options based on which have already been chosen for option in TeamMenu.options: if 'colour' not in option: continue option['disabled'] = option['name'] in [team['name'] for team in teams] TeamMenu.options[TeamMenu.START_GAME]['disabled'] = len(teams) == 0 super().__init__('Player {}, choose a team:'.format(len(teams) + 1), TeamMenu.options, False) class ActionMenu(Menu): options = [ {'name': "Build"}, {'name': "Trade"}, {'name': "End Turn"}, {'name': "Exit Game"}, {'name': "Back"}, ] BUILD = 0 TRADE = 1 END_TURN = 2 EXIT_GAME = 3 BACK = 4 def __init__(self, dice_roll, clear_title=True): # Rolling the dice is mandatory, so don't let the turn end unless it happened ActionMenu.options[ActionMenu.END_TURN]['disabled'] = dice_roll == 0 super().__init__('Do a thing:', ActionMenu.options, clear_title) class BuildMenu(Menu): options = [ {'name': "Build Road (0 points)", 'cost': [{'resource': BRICK, 'amount': 1}, {'resource': WOOD, 'amount': 1}]}, {'name': "Build Town (1 point)", 'cost': [{'resource': BRICK, 'amount': 1}, {'resource': WOOD, 'amount': 1}, {'resource': SHEEP, 'amount': 1}, {'resource': WHEAT, 'amount': 1}]}, {'name': "Upgrade to City (2 points)", 'cost': [{'resource': WHEAT, 'amount': 2}, {'resource': ORE, 'amount': 3}]}, {'name': "Back"}, ] ROAD = 0 TOWN = 1 CITY = 2 BACK = 3 def __init__(self, resources): # Disable build options based on whether the player can afford them for option in BuildMenu.options: if 'cost' not in option: continue option['disabled'] = False for cost in option['cost']: for resource in resources: if resource.resource == cost['resource']: if resource.quantity < cost['amount']: option['disabled'] = True super().__init__(None, BuildMenu.options, False) class TradeMenu(Menu): BACK = len(RESOURCE_KINDS) def __init__(self, resources): # Disable trade options based on whether the player can afford them options = [] for i in range(len(RESOURCE_KINDS)): option = {'name': "Buy a", 'resource': RESOURCE_KINDS[i], 'cost': [], 'cost_or': True} option['disabled'] = True for resource_kind in RESOURCE_KINDS: if resource_kind['kind'] != i: for resource in resources: if resource.resource == resource_kind and resource.quantity >= 4: option['cost'].append({'resource': resource_kind, 'amount': 4}) option['disabled'] = False options.append(option) options.append({'name': "Back"}) super().__init__(None, options, False) def _draw_choice(self, i, offset): super()._draw_choice(i, offset) if i < len(RESOURCE_KINDS): ugfx.area(93, 1 + offset + self.menu_offset, 16, 16, RESOURCE_KINDS[i]['col']) class NextPlayer(Menu): def __init__(self, team): super().__init__('Pass the badge to next team:', [team], False) class GameOver(Menu): def __init__(self, team): super().__init__(['Game over!', 'Congrats to the winning team:'], [team], False) 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 != DESERT: ugfx.text(round(self.centre[0] - text_offset), round(self.centre[1] - text_offset), "{} ".format(self.number['roll']), text_colour) class Selectable: EMPTY = 0 def __init__(self, data): # Screen coords that define the selectable object self.data = data # The list of hexes next to which this selectable object is adjacent self.hexes = [] # What is built here and who owns it self.team = None self.contents = Selectable.EMPTY # Whether to draw selection indicator self.selected = False def is_empty(self): return self.contents == Selectable.EMPTY def set_selection(self, selected): self.selected = selected # Notify the surrounding hexes that they need to redraw themselves for h in self.hexes: h.changed = True class Settlement(Selectable): """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 TOWN = 1 CITY = 2 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 == Selectable.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.data[0], self.data[1], 4, self.team['colour']) ugfx.circle(self.data[0], self.data[1], 4, ugfx.WHITE) elif self.contents == Settlement.CITY: ugfx.fill_circle(self.data[0], self.data[1], 8, self.team['colour']) ugfx.circle(self.data[0], self.data[1], 8, ugfx.WHITE) # A selection highlight if self.selected: # We can't draw circle primitives with thick lines, so need to draw it twice ugfx.circle(self.data[0], self.data[1], 10, ugfx.WHITE) ugfx.circle(self.data[0], self.data[1], 9, ugfx.WHITE) class Road(Selectable): """An edge along which it is possible to build a road.""" ROAD = 1 def build_road(self, team): assert self.contents == Selectable.EMPTY, 'Road can only be built in empty location' self.team = team self.contents = Road.ROAD def draw(self): x0, y0 = (self.data[0][0], self.data[0][1]) x1, y1 = (self.data[1][0], self.data[1][1]) if self.contents == Road.ROAD: ugfx.thickline(x0, y0, x1, y1, ugfx.WHITE, 6, False) ugfx.thickline(x0, y0, x1, y1, self.team['colour'], 4, False) # A selection highlight if self.selected: # We can't draw a rectangle at an angle, so lets calculate the points for an # appropriate polygon manually for drawing the selection box... # Get the vector and a normal, we'll use the magnitude of the normal for the # width of the selection box, so make it half the size for a long narrow box vx, vy = (x1 - x0, y1 - y0) nx, ny = (int(vy / 2), -(int(vx / 2))) # Draw with an origin that is offset by half the width of the box to straddle # the two adjacent hexes x = x0 - int(nx / 2) y = y0 - int(ny / 2) # It would be more efficient to call polygon directly like this: # ugfx.polygon(x, y, [[0, 0], [nx, ny], [vx + nx, vy + ny], [vx, vy]], ugfx.WHITE) # But we can't draw polygon primitives with thick lines, so draw them individually ugfx.thickline(x, y, x + nx, y + ny, ugfx.WHITE, 2, False) ugfx.thickline(x + nx, y + ny, x + nx + vx, y + ny + vy, ugfx.WHITE, 2, False) ugfx.thickline(x + nx + vx, y + ny + vy, x + vx, y + vy, ugfx.WHITE, 2, False) ugfx.thickline(x + vx, y + vy, x, y, ugfx.WHITE, 2, False) class Resource(): def __init__(self, resource): self.resource = resource self.quantity = 0 def increment(self, num=1): self.quantity = self.quantity + num def decrement(self, num=1): self.quantity = self.quantity - num if self.quantity < 0: self.quantity = 0 class Player: """The player's hand of resource cards and their score and what not.""" STATUS_ACTIONS_DICE = "menu = actions / # = roll dice " STATUS_ACTIONS = "menu = actions " STATUS_MOVE_ROBBER = "Move the robber! " STATUS_MUST_MOVE_ROBBER = "Robber MUST be moved! " STATUS_BUILD = "Select a build location. " STATUS_NO_BUILD = "No valid build locations. " def __init__(self, team, roads, settlements): # Player team details self.team = team # All the buildable game board locations self.roads = roads self.settlements = settlements # Player's hand of resources self.resources = [] for kind in RESOURCE_KINDS: r = Resource(kind) self.resources.append(r) # Turn number self.turn = 0 self.status = Player.STATUS_ACTIONS_DICE 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 increment_turn(self): self.turn = self.turn + 1 self.status = Player.STATUS_ACTIONS_DICE def num_resources(self): """Total number of all resources the player has""" return sum([x.quantity for x in self.resources]) def collect_starting(self): """Execute resource collection for our starting towns""" # Find the hexes adjacent to our settlements for s in [x for x in self.settlements if x.team == self.team]: for h in s.hexes: # Increment the appropriate resource for r in self.resources: if r.resource == h.resource: r.increment() def collect(self, num): """Execute resource collection or loss for a given dice roll""" if num == 7: # If total number of resources is over 7, lose half of them (rounded down) total = self.num_resources() if total > 7: lose = int(total / 2) while self.num_resources() > total - lose: self.resources[random.randrange(0, len(self.resources))].decrement() else: # Collect resources for each hex adjacent to our settlements that corresponds # with the given dice roll for s in [x for x in self.settlements if x.team == self.team]: for h in s.hexes: if h.number['roll'] == num and not h.robber: for r in self.resources: if r.resource == h.resource: if s.contents == Settlement.TOWN: r.increment() elif s.contents == Settlement.CITY: r.increment(2) def trade(self, buy_kind, sell_kind, sell_amount): """Executes a simple resource trade""" for r in self.resources: if r.resource == buy_kind: r.increment() if r.resource == sell_kind: r.decrement(sell_amount) def pay(self, cost): for c in cost: for r in self.resources: if c['resource'] == r.resource: r.decrement(c['amount']) def build_road_candidates(self): """Return the list of all roads that are valid candidates for building""" candidates = [] # Road segments that belong to us for r in [x for x in self.roads if x.team == self.team]: # Settlement spaces that these road segments connect for s in [x for x in self.settlements if x.data in r.data]: # Empty road segments connecting those settlement spaces for road in [x for x in self.roads if x.is_empty() and s.data in x.data]: if road not in candidates: candidates.append(road) return candidates def can_build_town_at(self, settlement): """Determines whether a town can be built at the given settlement according to proximity rules""" # Find the road segments connecting the given settlement for road in [x for x in self.roads if settlement.data in x.data]: # Get adjacent settlements (those at the other end of the road segments) for s in [x for x in self.settlements if x.data in road.data and x != settlement]: # If all adjacent settlements are empty, it means that we are at least two road # segments from any other built settlement, which is the required distance if not s.is_empty(): return False return True def build_town_candidates(self): """Return the list of all settlements that are valid candidates for towns to be built""" candidates = [] # Road segments that belong to us for r in [x for x in self.roads if x.team == self.team]: # Empty settlement spaces at each end of the road segments for s in [x for x in self.settlements if x.is_empty() and x.data in r.data]: # Settlement is a candidate if we can build there if self.can_build_town_at(s) and s not in candidates: candidates.append(s) return candidates def build_city_candidates(self): """Return the list of all settlements that are valid candidates for being upgraded to city""" candidates = [] # Settlement spaces that belong to us and contain a town for s in [x for x in self.settlements if x.team == self.team and x.contents == Settlement.TOWN]: candidates.append(s) return candidates def draw(self): # Blank out the score in case it changed ugfx.area(60, 28, 25, 18, ugfx.BLACK) # 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) ugfx.text(5, 48, "Turn: {} ".format(self.turn), ugfx.WHITE) # Blank out the status/resource area ugfx.area(0, 275, 240, 50, ugfx.BLACK) # Status line ugfx.text(5, 275, self.status, ugfx.WHITE) # Player's resources offset = int(ugfx.width() / len(self.resources)) square = int(offset / 3) for i in range(len(self.resources)): ugfx.area((offset * i) + 1, 295, square, 20, self.resources[i].resource['col']) ugfx.text((offset * i) + 1 + square, 295, "{} ".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): MENU = 0 # 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] # Interactivity modes for allowing the the user to navigate and select things on the game board ROBBER_MODE = 1 ROAD_MODE = 2 TOWN_MODE = 3 CITY_MODE = 4 def __init__(self, teams): # 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)) # Note the initial location of the robber to ensure it moves when activated self.robber_hex = self.get_robber_hex() # The mode that dictates how we are interpreting button presses self.interactive_mode = None # Generate lists of unique valid locations for building settlements and roads self.roads = [] self.settlements = [] for h in self.hexes: for edge in h.edges: already_got = False for r in self.roads: if r.data == edge: already_got = True r.hexes.append(h) if not already_got: r = Road(edge) r.hexes.append(h) self.roads.append(r) for node in h.nodes: already_got = False for s in self.settlements: if s.data == node: already_got = True s.hexes.append(h) if not already_got: s = Settlement(node) s.hexes.append(h) self.settlements.append(s) # Create a player for each team and give the team starting towns in the two settlements # with the highest probability score that not already taken # Each team gets a settlement in player order, then again but in reverse, so the last # player gets the first pick of the second settlements self.players = [] for team in teams: self.players.append(Player(team, self.roads, self.settlements)) self.player = self.players[-1] for team in teams: self.pick_starting_settlement(team) teams.reverse() for team in teams: self.pick_starting_settlement(team) teams.reverse() # Each player can now collect their starting resources for p in self.players: p.collect_starting() # The dice roller self.dice = Dice() # Cheat code counter self.cheat = 0 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.data in road.data: 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: if s.is_empty() and self.player.can_build_town_at(s): 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_A, self._button_callback) Buttons.enable_interrupt(Buttons.BTN_B, self._button_callback) Buttons.enable_interrupt(Buttons.BTN_Hash, self._button_callback) # For moving the robber and building selections Buttons.enable_interrupt(Buttons.JOY_Up, self._button_callback) Buttons.enable_interrupt(Buttons.JOY_Down, self._button_callback) Buttons.enable_interrupt(Buttons.JOY_Left, self._button_callback) Buttons.enable_interrupt(Buttons.JOY_Right, self._button_callback) # Cheat code button Buttons.enable_interrupt(Buttons.BTN_5, self._button_callback) def deinitialise(self): # Unregister callbacks Buttons.disable_interrupt(Buttons.BTN_Menu) Buttons.disable_interrupt(Buttons.BTN_A) Buttons.disable_interrupt(Buttons.BTN_B) Buttons.disable_interrupt(Buttons.BTN_Hash) # For moving the robber and building selections Buttons.disable_interrupt(Buttons.JOY_Up) Buttons.disable_interrupt(Buttons.JOY_Down) Buttons.disable_interrupt(Buttons.JOY_Left) Buttons.disable_interrupt(Buttons.JOY_Right) # Cheat code button Buttons.disable_interrupt(Buttons.BTN_5) # 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 not self.interactive_mode: if btn == Buttons.BTN_Menu: self.selection = GameBoard.MENU self.done = True 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 and not h.robber) or (num == 7 and h.robber): h.set_highlight(True) else: h.set_highlight(False) # All players collect resources corresponding with the dice roll for p in self.players: p.collect(num) # Activate the robber on a seven if num == 7: self.interactive_mode = GameBoard.ROBBER_MODE self.player.status = Player.STATUS_MOVE_ROBBER else: self.player.status = Player.STATUS_ACTIONS self.redraw = True # Cheat code to get more resources for testing if btn == Buttons.BTN_5 and TEST_MODE: self.cheat = self.cheat + 1 if self.cheat == 5: self.cheat = 0 for r in self.player.resources: r.increment(5) self.redraw = True elif self.interactive_mode == GameBoard.ROBBER_MODE: h_current = self.get_robber_hex() if btn == Buttons.BTN_A: self.redraw = True # The robber may not stay in the same hex, ensure it moved if h_current != self.robber_hex: self.robber_hex = h_current self.robber_hex.set_highlight(False) self.interactive_mode = None self.player.status = Player.STATUS_ACTIONS # TODO: Steal a card from a player at this hex else: self.player.status = Player.STATUS_MUST_MOVE_ROBBER if btn == Buttons.JOY_Up: self._move_robber(h_current, 4) if btn == Buttons.JOY_Down: self._move_robber(h_current, 1) if btn == Buttons.JOY_Left: self._move_robber(h_current, 0 if h_current.coords[0] % 2 == 0 else 5) if btn == Buttons.JOY_Right: self._move_robber(h_current, 2 if h_current.coords[0] % 2 == 0 else 3) elif self.interactive_mode in (GameBoard.ROAD_MODE, GameBoard.TOWN_MODE, GameBoard.CITY_MODE): if btn == Buttons.BTN_A: for candidate in self.build_candidates: if candidate.selected: # Build a road on the selected road segment if self.interactive_mode == GameBoard.ROAD_MODE: candidate.build_road(self.player.team) # Build a town on the selected settlement if self.interactive_mode == GameBoard.TOWN_MODE: candidate.build_town(self.player.team) # Upgrade a town on the selected settlement to a city if self.interactive_mode == GameBoard.CITY_MODE: candidate.build_city(self.player.team) candidate.set_selection(False) self.player.pay(self.build_cost) self.interactive_mode = None self.redraw = True if self.dice.total(): self.player.status = Player.STATUS_ACTIONS else: self.player.status = Player.STATUS_ACTIONS_DICE if btn == Buttons.JOY_Left or btn == Buttons.JOY_Up: self._select_prev_build_candidate(self.build_candidates) if btn == Buttons.JOY_Right or btn == Buttons.JOY_Down: self._select_next_build_candidate(self.build_candidates) def _move_robber(self, h_current, direction): coords = Hex.get_neighbouring_hex_coords(h_current.coords, direction) h_next = self.get_hex_for_coords(coords) self.move_robber(h_current, h_next) self.redraw = True def _select_prev_build_candidate(self, candidates): if len(candidates) > 1: for i in range(len(candidates)): if candidates[i].selected: candidates[i].set_selection(False) if i == 0: candidates[len(candidates) - 1].set_selection(True) else: candidates[i - 1].set_selection(True) break self.redraw = True def _select_next_build_candidate(self, candidates): if len(candidates) > 1: for i in range(len(candidates)): if candidates[i].selected: candidates[i].set_selection(False) if i == len(candidates) - 1: candidates[0].set_selection(True) else: candidates[i + 1].set_selection(True) break self.redraw = True def get_robber_hex(self): for h in self.hexes: if h.robber: return h return None def get_hex_for_coords(self, coords): for h in self.hexes: if h.coords == coords: return h return None def move_robber(self, from_hex, to_hex): if to_hex: from_hex.robber = False from_hex.set_highlight(False) to_hex.robber = True to_hex.set_highlight(True) def next_player(self): """Called from the state machine to reset the board for the next player""" for i in range(len(self.players)): if self.player == self.players[i]: if i + 1 == len(self.players): self.player = self.players[0] else: self.player = self.players[i + 1] break self.player.increment_turn() self.dice.reset() for h in self.hexes: h.set_highlight(False) def build_mode(self, mode, cost): """Called from the state machine to enter building selection mode""" if mode == GameBoard.ROAD_MODE: self.build_candidates = self.player.build_road_candidates() if mode == GameBoard.TOWN_MODE: self.build_candidates = self.player.build_town_candidates() if mode == GameBoard.CITY_MODE: self.build_candidates = self.player.build_city_candidates() if self.build_candidates: self.build_candidates[0].set_selection(True) self.build_cost = cost self.interactive_mode = mode self.player.status = Player.STATUS_BUILD else: self.player.status = Player.STATUS_NO_BUILD class Settlers: """A lean mean state machine""" # Game states EXIT = 0 MAIN_MENU = 1 TEAM_MENU = 2 GAME = 3 ACTION_MENU = 4 ACTION_TRADE_MENU = 5 ACTION_BUILD_MENU = 6 ACTION_END_TURN = 7 def __init__(self): self.old_state = None self.state = Settlers.MAIN_MENU self.game = None self.teams = [] def enter_state(self, state): self.old_state = self.state self.state = state def run(self): while self.state != Settlers.EXIT: if self.state == Settlers.MAIN_MENU: menu = MainMenu(self.game is None, self.old_state != Settlers.TEAM_MENU and self.old_state != Settlers.ACTION_END_TURN) x = menu.run() if x == MainMenu.NEW_GAME: self.teams = [] self.enter_state(Settlers.TEAM_MENU) if x == MainMenu.CONTINUE_GAME: self.enter_state(Settlers.GAME) if x == MainMenu.EXIT: self.enter_state(Settlers.EXIT) elif self.state == Settlers.TEAM_MENU: menu = TeamMenu(self.teams) x = menu.run() if x <= TeamMenu.TEAM_MAX: self.teams.append(menu.get_selected_choice()) if len(self.teams) >= 4: x = TeamMenu.START_GAME if x == TeamMenu.BACK: self.enter_state(Settlers.MAIN_MENU) if x == TeamMenu.START_GAME: self.game = GameBoard(self.teams) self.game.next_player() self.enter_state(Settlers.GAME) elif self.state == Settlers.GAME: x = self.game.run() if x == GameBoard.MENU: self.enter_state(Settlers.ACTION_MENU) elif self.state == Settlers.ACTION_MENU: menu = ActionMenu(self.game.dice.total(), self.old_state != Settlers.ACTION_BUILD_MENU and self.old_state != Settlers.ACTION_TRADE_MENU) x = menu.run() if x == ActionMenu.BUILD: self.enter_state(Settlers.ACTION_BUILD_MENU) if x == ActionMenu.TRADE: self.enter_state(Settlers.ACTION_TRADE_MENU) if x == ActionMenu.END_TURN: self.enter_state(Settlers.ACTION_END_TURN) if x == ActionMenu.EXIT_GAME: self.enter_state(Settlers.MAIN_MENU) if x == ActionMenu.BACK: self.enter_state(Settlers.GAME) elif self.state == Settlers.ACTION_BUILD_MENU: menu = BuildMenu(self.game.player.resources) x = menu.run() if x == BuildMenu.BACK: self.enter_state(Settlers.ACTION_MENU) else: choice = menu.get_selected_choice() if x == BuildMenu.ROAD: self.game.build_mode(GameBoard.ROAD_MODE, choice['cost']) if x == BuildMenu.TOWN: self.game.build_mode(GameBoard.TOWN_MODE, choice['cost']) if x == BuildMenu.CITY: self.game.build_mode(GameBoard.CITY_MODE, choice['cost']) self.enter_state(Settlers.GAME) elif self.state == Settlers.ACTION_TRADE_MENU: menu = TradeMenu(self.game.player.resources) x = menu.run() if x == TradeMenu.BACK: self.enter_state(Settlers.ACTION_MENU) else: trade_choice = menu.get_selected_choice() # TODO: ask user which resource to trade when they have >= 4 of more than one kind of resource # TODO: for now, just trade the first one in the cost list cost = trade_choice['cost'][0] self.game.player.trade(trade_choice['resource'], cost['resource'], cost['amount']) self.enter_state(Settlers.GAME) elif self.state == Settlers.ACTION_END_TURN: if self.game.player.score() >= 10: menu = GameOver(self.game.player.team) menu.run() self.game = None self.enter_state(Settlers.MAIN_MENU) else: self.game.next_player() menu = NextPlayer(self.game.player.team) menu.run() self.enter_state(Settlers.GAME) # User chose exit, a machine reset is the easiest way :-) restart_to_default() game = Settlers() game.run()