From 57ee3129acfa684f0f3eabcca1a196e6086c7387 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Sat, 15 Sep 2018 15:49:48 +0100 Subject: [PATCH] Settlers of EMF Beginnings of an EMF themed implementation of Settlers of Catan --- settlers_of_emf/main.py | 775 ++++++++++++++++++++++++++++++++++++++ settlers_of_emf/title.png | Bin 0 -> 9766 bytes settlers_of_emf/title.svg | 153 ++++++++ 3 files changed, 928 insertions(+) create mode 100644 settlers_of_emf/main.py create mode 100644 settlers_of_emf/title.png create mode 100644 settlers_of_emf/title.svg diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py new file mode 100644 index 0000000..2fd6628 --- /dev/null +++ b/settlers_of_emf/main.py @@ -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() diff --git a/settlers_of_emf/title.png b/settlers_of_emf/title.png new file mode 100644 index 0000000000000000000000000000000000000000..89e1dab0323174898df44b6be3ac2dda2cb19b89 GIT binary patch literal 9766 zcmX9^2Rzm97rrDb0_=yX=vX5SL^{Rz}F)GdqdM-XUbKB72j)_s+`9$p75W zpYTby?|t9%p7WgNdCnWEt}0K6PmPa2AP5x|WHjLC9(v#>=bn0AP__?=r4@8|A=VdmpG1!DzZ3>ctqsaDGAsrYY~V$2t}Dknl6*u zp01jj^2b-XCpmSxwT}doo~t8e$TBnhvHaV_dF*);U-c~7^FPU4dVlI9bTguxIkPR# zzLR-BeDDEdq@UriPewH(i9H4u8SnnQW`1lx?m{?vx0oTCm@5_LI03Re~k#<;OBDI*;1!ZSvr-HJww3!*}yLaylxhWBd z*RNkwN_vS|+t?WKQx6UdH0J3slRjc5Eu9@RoBVJir`VGFy3T2H`qB_Aw6lvdRFd%L)h^nrBpsA^OM@)>CNiN(+yCAU1 z@Zr;^H=pCA_6O_S4br(QXJNrXdh_P~a%U|0oV=qW|Jl)&{G3|u5YJ|;Qg--u+p1^g zh~*>PM@~)xeJSF~b5?(rJNJ%{b5dj7yaSN9SeOdg8nr1oIjpe4%*smgm6a8CPEM)U zukRhYc@~%`vc@tnGh_Ms`X=yOkYW?l6NJiO`ZpmWbRlN^I7<&v$M0y=^n%PzpFH+rlx7|^vcT0kkHVkL;)+<|Nha~3zL~z zr;h&q+)hc4Q@aPk!jBkZ43|4%!p{!={INSfw!zkA|KsN3lIDH&58*RY_m)XO;Fd!T z?QM@ejJ60G97MJ20V})u4hIesi^QW2SW;;6=y(-!GU7?W2maCQi?dTXUN5l$;i zDn?Iy4V%EHpy0oy^EYdK>0iJ0Sz~IjlR+W|OQk>5)ul4rD*&@$aa3 z@90QYQ4w!(Y03U*Q|m)q9KhC{+BM8Z&8(=lL-S{F7G4Sn(JF#5_);v9Pguy_jM<6$}jMEh68}d5#tC zbaW^H1gNEmF%*A44pZK+mA5<7int8R|1QrR&t1gm zGkco+aY9vdGq)ZqDe)GV&o}xmzqmT1;GraNtl9m39(nkCVra49*;jqVv81FVWdqlh zTcZx5&cE*jAT!3cItz8nYR3eB*_x#<9q?EVv)5l9Pa%BR)KUyv-`%Qq+<5=VOEFN8j}^bD%5!LMZB4JPPS|fy%I1w-?H!TC*V57& zu&hAh2bNQ-R;<8U^NNZDV(F#Q2dpiOCMgsZ6&$*l~J@-BNpY>$9#p%31u}-(-)x|-AfED=++kel# zw!lf~PPwf1P$AN`|NDHamM&U>S*PkX-Q`s*o0{F(p+#6wq@N!qQfgp$7%N%T++M{B zzewAvlrZYHWZU0^HL6sB7ySkeZgH~|wqM^T`W1=C#l^WCu9K)3T5jhJZS3tWNN)|_ z|LA_})~)oy!X8FnmGj0|{E0CoT-=vj3|Xxe>zC&zPc${LN_Bda_$x)YMsmlt_FE}l z4D3d?FB$y|zzYcv=l@UCq>aGE#buzS-eo_dRnW}L?ALDLe-XMfiH*jklu$DzwI0sS zf|iz+cgXPd_(c+k+>}jmd;qLX&CQ3}h`&4sP#M@dA|HE~b7Dy2Q&CZ&pr#hw&>+6- zAPgOZmX=l>g?d-_b{8e9UD?l`nWD-0a^&9QDPw+WQPV7=tsft8KgY!Q!dd2Na*pB! zMKp3%s1=MXcE$a7cx7N-b99qznuUr+?Q{b-Q!3vyJ}#S%ITw=in$klPJ=?_w?!0{f*qzUwqDh)vu?keS3Ny z7qGBQiEnOg*{=^Uz&n$XlV_x-W8~-O=M@*5vvAWGaVefR2NP!&77{>+pWpLU*|Ji> zq#!3>47>l`CpGo9%`59-teljpZ{ObY>?hsRCBwgNLeV%oOK(|`sJ*5Fjmn2u@&ajD zF=FK!m_&*n#gJU2VqHlKw zOgXr?TFp~EvHhETGBOwovNBjYW1^#dAd(r?)sztD)8$yTw4sfj(Gi<`3dHxb&Hrd6 zy*`gHb_?P;n(mHJ|Ea5~Vc_J%uc@hlj@q0q9o)Efz@;==SuTH@Gm42?r~eGxHdr7K-(~^bFQ4y74isskKs1Z}9v@9H3G^i`D+mL&oL0t`Yz6o3Ez<@z!=N}vEOX{yRLv%a1dRYX29F#&C5 zckjCiVC~LyO|!_LlVIVKZD}c~68j~&aB6{ftPKXmRf)7*ab;Ci?Txd0Fua z%xGXQDzXXN%E~?ZRp)1y{QO6L*Vg5nq*~V2^SXil1GqyJ2 zYUHlqQWp0J-`mAtvo1@7lZF!`>&GcN&^M5w70 zbxZbU>?D2WF`VOEWIzBG)O?ktrkLY8Qb|c^uUBX;O+Jb~e0O?gW?^S1GCCUBlOj%o zUX{K+t^9ms1~82C<85hGRU++Tg9ym(q?m!`=4nDX-SWVdel_OCJBzu*5Qv@Sm%4?bKL0x9;quTH!ZcwfHLP11k<9L-8?ZEXVr z1C6#PE1sXL4n{oZP`Us1?OR@GGo%z0@^{no@1&;J1!758QYVCoxb>+Qq`_Kca4kfAZB z=jT-gDq{3eJxN0SJ#oMK`XYf#Ga_+u5xjuC(2#-NRD78QN{=kkRvvTxv#gk*k*@$d z3rR|%!6PJO+27v}KmxtBQ~)H_t#{!m(96m09vqB;eGI;iwazOnWLA#9x1!G>dP7!L z_Q9J~f*iHf$h*p)0t3iP`D0nN$QJ613Wff)|9UXP_^#$qQ+vVq&Hs~C> zWoV@=KiPh)2Sic|*XB;vXtJQ7p!V2Ay#bW}yLb2(f4c9p-@DgVq*qm9J;4hUdVF&7 zXMaEOo7JAC0KA)pqZ+`v>8mc-@oyiH*5vxo6|LK2FuLV0Ht-gsUz~fewA}@ z;GOk2P& zS~HaN{O8ZK*3;D_p`oG4qfo-E`rkc1*{j{YeS6fOY*@!qG4o47fi$r46z_koVs3}) z-a)MCNYAsw1>EUTjN4>n0RYs-Cp$AmA0=J?Fne9>%eR?x33pcs)@>cRovD8>D&o@q zsvnw_#mskmYEDXa+Y)a&v8{1wzr?(sJ}WD$vAsQa2KDqnnw5u#r`m1-6Ogv?i*{MX zAel&VB#*_@n<{p8CAt&95g6{>BbxKR5*HH}*R!?)VX=fHjA}1iFY6_#z77YK0eq6n zt&2Z3BI&$YgYS#LR{E$xB&w(l5N=_6TgBb1B91wk2QeNpO0SVbpTB(YmrIEikO>_O z`q->;x*@@eAKQF90=rO<2I z&&kPp+nKBX`ZWV?MK;=C3Ghu#PNG=;CK6{sHih9(Pb%G`prC-zN0%AUY=I(N(cJJCcmdR6A1S ztA&Y)N##{{(5iAENQ%$m?#C0X+)?e|B zP2Hf(I2%0_h5`iLLiO_U@^WL`NK-ddMrzKx>jMXWQ&Ky7=H?3k9(1Qktp8A87Qa_e zz)2n|1Lv*t!%N)2`_iec7Bu(J6T%To0Z0cVIe8F^^2d2V0G)59c(3QaM?e)3RV;MI zvUPNK``6Wpf?z3i-bT?%cqGggXM1GC-+vJ~FrW_N9BPdUdL6oNJdaY!{t1jL1ccn- z&P-C|LxmXQ#gB<>iH=K$9#+35nV8Wl*B< zz^3)V%!QQ|S>o64E@o$Ebs!R8PV{70y=yggD-n_uqNS&g3<<$?adR{NJznaV1R4m4 zZ^`Q!5lt1fIzXAr{~vbdjI>oMnX-E4?IMU#`lF9pXc9|c%Vk|uvwzp5~CDG4<4!H*q2asq7N&=V}qo^niWOzVq>`k-&ln8$u;tZglJIXjXIE4lc)S2>8 za=>uk0iSs_$f#fET(rfn=lICaHeEfWH%Z83w%*k-OBQ%pZLJ713(Mf(ooo#bx09U+ zP=+q9u1#%iLG+08lil_&S_L}&sLJag0f3LXY!y}Q{ZhU^ctjH+7b3$b^7uCh!4i{p zBItgTJ*7-wNZj^3B%zaS9=^jshR@X&z!)&_aR{DdvOC*Q7~FVuu_n26-}~UOz z6qm`LjEjPN?ow!0AdJ6j1;tSDnHjG3e99{*@K?=+KPCZj*+0+04I!hTFggA`QJ7nh z^Z1h=nr~4@1bq$u0>GXS2$XYPeZesPuCCwv_oR1G<+y``gXK=T#bbK6v_CRtgFmCd z{1e1)Fc8UyxkE-?5d%*!vIbq}Am_W{jgoOAQ6r=4N z{d@Ajt;p{igclaxg_Q&Z1ucR>(r#7U&^AyA8`*A&O-h2)JjDzqzAdMsq7r{!O?0H9 zyR$PCe3)NfO+hy>3kbx?$jeuq*3HiwIvlP)(LkY;!sVDi(Vw2WEUvAM?$pWWDP5hCT-gBx{=Y}8cVf|}wQF9$D4#p-XLzZ+7HJ^c=ggj2*_6@%+ znLfc@!_XH9i=nt6W; z)LW27zUfSHd1%oQddbz+}z3V}V*QML| z)YQ^qYZ$Zy{otP#wbj*zKnH=1;Uf$?-?NN=dwJueO#}n{BgovGDv{02&H3NI!+-vi z@_;h|9ho~80fvvu)#Zr=rjUR@dR7*eheT>0W4b0MTDQ@PI)`sf=lT$Z**|;s%mM5v zpi{6Zx_7_kE~o{qDLcEm7z2>+Q&ZI#nV5tt^8MqXC;UBKiUMBoL1gytbo_Ux**H~$}bjoc2q5` ztU?Wn*}!WG*Srr!!Uh)1{-SJ~EhN?7&4S{NwN&rEqun9yStm^oQ4tYFc6MAapaKH} zPmiZ;MeDt%XmeGQGl;1mv))eBbV*{Ko$N*_#2>vJ4=`@Lw&0)71Fq z;9#Ta5gqRQfP{>!Y!lFS$Y=8AR)BF#1bBV&KmP0L^qZ8-o!{M!vR`Uj05)+7tFI>V z!e1|8(^gkUHl!!V#kFqgbGseBBA^$egtKI4XK#$45whDFXOB)!wi(P&6Fvso1p6I&G{N3!WnyxTjg1YQ(r|il zYB;V3u76B}dHL*I-eDd8`QxXNF9R&7VC3)2+qu?Sr>&llxBo6&KZ`iwQ{2;kC@p;r zjU~hqrwHCp1kynS>7Kd1FPPXqeEj&cy*)URa9XRqtd=j&e^V3FR!}`(Gw-6AjNMP< z!2`pK(|u%E>-yXe$*PKz!kY>B2D9ViuPTq}z^AOVo$I3x7X`unXlpIE`Sb(60@_n2ZpGseqP=?*ndxb$YmAFTpYYi4+s!bDc%rXQP=T`Hsbz9{w+7}`I6?3**5pO` zV87(mbk3bqTCZTNd+h9J#iq%53%q)&7K2(x4&dx;hg)OBTVut!2g|Iitk72J!P=C4 z{FngrnElm1Pbgvn$Vpyhiw)qSG+n?bA7ED91pRin-vxX?|A*Ji?`0C%c@Q7G9m7~P=fEX03y2P%^Il`ZfHaV3x1&A{tVCqT!sK3;1>*PqUm*R#=^qF zF<==(O{38S;39eQp^%V}!^NpRRJFameaqBTlDh?6JvJe=3=pVJXxANJMRaD!hADt~ z%W?NETAgLHJ(U0$Vq^rbmpZ?wC|fP0wY@zSETZVG?zyHBj$kV1-Ps6eRumo*x8V(2 zVTvKTZQ3QeTkpR;Rn^?qhV6YmHCJLYRq1S=x3njz?wSUSziI6teP|;Z3~#h@e?sjl zMh|2Y&Bg(Unhd#p`q!lIk}tuLnPKz@i}`c%BNoBtEf{olbxmev!GT4CpS{sc@cC@| zZ+t`qVQE#B?SZe$&Zb;26(TxH;w(hFERtS)asdNSx6}6;&Z`QWiEl5b6T|AN3kq=W z#wm4zxLgnSEG;c{G^?pJ;Ea2RhVK?bZln~hFx2%1y!nYiw_{mnXp(hmx&Zfepx~b$ z*kgichw$Mu?=u9?#bs~acf{%R>}+w@`%2WneOmxz@^rQRwKDVmonIYZKPaHx(Wj)O zI39MbvU#Jrv>(3QzZoin9#%AGeHpR5*iiHK11lc*MWzI}Lf}~}fxTn%?HO3q1T=yK zUS3||6}-^drP9$56rGrepR5|Zw`WH{$&G-vBEagbykDOGDmEe_0t_>&AJ}x z3lE^=>V8V>)$g+Mk4)SOcR!`_(6#!OM}DfbO9%^5gb(X5C*Nre*f<2;F2M6 z0ftKuoHUAtRv0-^5@07rLso$4$Iiti3kaFaT?kD%0bB+@KR@W1*5nkmFd{jkA2z)c z-wnkYE?_lkhqa@`M4n`{ytmMNc&1e_@*Ys$VRM*+*SwFGl!WBb&!0a#-t>#hoArHG z1n&8mpPJCZ+B(bo&2k6HFOVjZS&tOi$=U|iWAt59r zJmgDqQxFgoM1}L{1Gk5LF@jyQ!10osl5)*!qH>N^EhQYXMQ`QF%KEy3c`>)vTLJ0_ zT?Z@hV+tl98_YNzWn}|fO+vs1GrN2HK}lw}Mgdx<)HxFZ2bYtP!6Kp)37DSNA08ea zz&T+d#e-1NOL&C9;h|d;Kmr(bi?<`|M-OvZ(3*hmANO?_h5FG*>{_V-o2Zo=_{1l|sgN6=={)a+@3h?8Xj-XyR% zZZN)JK?}jRy|~@`iyHWzZpN#2|+otP!D@pZ@>>-5SYNEef=6q{{c9- z_Q8Wi-tz)sR2bj!`XEREW*ve!X_kMSkf2IR_)8BvK>GDcQI#sroy@wU{Tl1I-yc~& zt@aO7`*wGC?}OKh^W);r4e;`-G4{*xdWy_N;s-QeXz0SI@ej9I59Q-zkr75;4h|1U zN3iz>=dNR6HI9vqE!F|ELKAf`aG{y?Mlzz9d{wl}lvna;)e&|u&4S4wA8CxeOBdGa zfGZaL1G_t5PnBAZa2BwsmgR;hz~i zK>zU;78b_q-rh%)ocyYu4@mdAVu%}S1*%uJ z+WK06K7|+!E&)NyyIcRQm&gJ}0ITFe@i2bB?uP^ z`mtanzG3Z0X5g}L9fW6vV&Ln@!$U_fK*bQqpzxd3J7DfvSXh|F`dVDt z)Of@25u1Rrk)0_Za|&_ z;{aVoT`jF!Fw76jf13NNMIYROWhSEats_L0_0-nYnSf#?&wi#%g@=LGb_7=C z2F2~54s=5;l_ac&pFhp-B}rhitNK`-GpkTkBnl}89Oy2oPUhf%m09JkvXT;#hDO;N zP0*Lu){^213JOfbr>v+W{B645*%0=p`z!zV^@i8&I{}8k2GHt#*`Y>vwGEgdjERHd zI$+`f3gGZ+*j^GoAcl=IGj!m{t)1M?kIvD7o6@bl$4b=>)5@o93-jQBjRo+1>{EgXYaaXWL-h!}381pl`+iVGG{{ zcTG%83_bmwG?WdT2X)%f)n(jLgXsfCFnan;9f2)*b-^cun9Qt6zqur(cMa+aW+!MC zQthyU4_8Kd8Un9~fx-eWLQ3h3bbqY$%VY9{D-tllW50hN_>m77Os5_anG%-wpXCmv zmy|@O&(_w~O0knI?k}~6|KW^7k6h(4V=8QhC#%1F!G~KaV4}gzBRe}g1bEMEdtS7tHcaumL= wu-*J#c)=caH9*MD61;-Rm#xsf0qQkrT_o{ literal 0 HcmV?d00001 diff --git a/settlers_of_emf/title.svg b/settlers_of_emf/title.svg new file mode 100644 index 0000000..b670071 --- /dev/null +++ b/settlers_of_emf/title.svg @@ -0,0 +1,153 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + SETTLERS + EMF + OF + +