From 57ee3129acfa684f0f3eabcca1a196e6086c7387 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Sat, 15 Sep 2018 15:49:48 +0100 Subject: [PATCH 01/15] 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 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 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 + + From dac613c19fd0ce78c4974dbedea1c53bdecdbf8b Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Sat, 22 Sep 2018 16:04:14 +0100 Subject: [PATCH 02/15] Settlers of EMF Refactor resources and collect resources on dice roll --- settlers_of_emf/main.py | 73 ++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index 2fd6628..6f71747 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -318,6 +318,9 @@ class Hex: self.highlight = highlight self.changed = True + def kind(self): + return self.resource['kind'] + @staticmethod def get_neighbouring_hex_coords(coords, direction): return [a + b for a, b in zip(coords, Hex.directions[direction])] @@ -334,7 +337,7 @@ class Hex: 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: + if self.kind() != 5: ugfx.text(round(self.centre[0] - text_offset), round(self.centre[1] - text_offset), "{} ".format(self.number['roll']), text_colour) @@ -414,10 +417,31 @@ class Road: ugfx.thickline(self.edge[0][0], self.edge[0][1], self.edge[1][0], self.edge[1][1], self.team['colour'], 4, False) +class Resource(): + + def __init__(self, resource): + self.resource = resource + self.quantity = 0 + + def kind(self): + return self.resource['kind'] + + def colour(self): + return self.resource['col'] + + 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.""" - def __init__(self, team, roads, settlements, resources): + def __init__(self, team, roads, settlements): # Player team details self.team = team @@ -425,17 +449,17 @@ class Player: self.roads = roads self.settlements = settlements - # Player resources + # Player's hand of resources self.resources = [] - for r in resources.copy(): - r['quantity'] = 0 + for kind in [GameBoard.SHEEP, GameBoard.WHEAT, GameBoard.WOOD, GameBoard.BRICK, GameBoard.ORE]: + r = Resource(kind) 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 + if r.kind() == h.kind(): + r.increment() def score(self): points = 0 @@ -443,18 +467,42 @@ class Player: points = points + s.contents return points + def num_resources(self): + return sum([x.quantity for x in self.resources]) + + def collect(self, num): + 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.kind() == h.kind(): + if s.contents == Settlement.TOWN: + r.increment() + elif s.contents == Settlement.CITY: + r.increment(2) + 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) + ugfx.area(0, 290, 240, 30, 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) + ugfx.area((offset * i) + 1, 295, square, 20, self.resources[i].colour()) + ugfx.text((offset * i) + 1 + square, 295, "{} ".format(self.resources[i].quantity), ugfx.WHITE) class Dice: @@ -623,7 +671,7 @@ class GameBoard(State): self.dice = Dice() # The player details - self.player = Player(team, self.roads, self.settlements, [GameBoard.SHEEP, GameBoard.WHEAT, GameBoard.WOOD, GameBoard.BRICK, GameBoard.ORE]) + self.player = Player(team, self.roads, self.settlements) def get_roads_for_settlement(self, settlement): """Return a list of roads that connect to the given settlement""" @@ -704,7 +752,8 @@ class GameBoard(State): h.set_highlight(True) else: h.set_highlight(False) - # TODO collect resources + self.player.collect(num) + # TODO: Move the robber on a seven self.redraw = True From b8a49997e7a35107a737df77426913976b255c63 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Sat, 22 Sep 2018 23:09:57 +0100 Subject: [PATCH 03/15] Settlers of EMF Allow moving the robber around when a 7 is rolled --- settlers_of_emf/main.py | 119 +++++++++++++++++++++++++++++++--------- 1 file changed, 92 insertions(+), 27 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index 6f71747..5025590 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -26,6 +26,7 @@ BUTTONS = [ Buttons.BTN_7, Buttons.BTN_8, Buttons.BTN_9, + Buttons.BTN_0, ] @@ -74,7 +75,7 @@ class Menu(State): def draw(self): # Draw the menu on screen ugfx.clear(ugfx.BLACK) - ugfx.display_image(0, 0, 'settlers_game/title.png') + ugfx.display_image(0, 0, 'settlers_of_emf/title.png') ugfx.text(5, 95, self.question, ugfx.WHITE) i = 0 for c in self.choices: @@ -639,6 +640,10 @@ class GameBoard(State): 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_mode = False + self.robber_hex = self.get_robber_hex() + # Generate lists of unique valid locations for building self.roads = [] self.settlements = [] @@ -712,49 +717,109 @@ class GameBoard(State): 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_Star, self._button_callback) Buttons.enable_interrupt(Buttons.BTN_Hash, self._button_callback) + # For moving the robber + 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) 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_Star) Buttons.disable_interrupt(Buttons.BTN_Hash) + # For moving the robber + Buttons.disable_interrupt(Buttons.JOY_Up) + Buttons.disable_interrupt(Buttons.JOY_Down) + Buttons.disable_interrupt(Buttons.JOY_Left) + Buttons.disable_interrupt(Buttons.JOY_Right) # 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: + if not self.robber_mode: + 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: + # Can end the turn if dice were rolled + if self.dice.total() != 0: + self.selection = GameBoard.END_TURN + self.done = True + self.dice.reset() + for h in self.hexes: h.set_highlight(False) - self.player.collect(num) - # TODO: Move the robber on a seven - self.redraw = 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) + # Collect resources corresponding with the dice roll + self.player.collect(num) + # Activate the robber on a seven + if num == 7: + self.robber_mode = True + self.redraw = True + else: + h_current = self.get_robber_hex() + if btn == Buttons.BTN_A: + # 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.robber_mode = False + self.redraw = True + # TODO: Steal a card from a player at this hex + 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) + + 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 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) class Settlers: From c75357269fb3c0b3b87f1772a67482e071976404 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Sun, 23 Sep 2018 11:54:50 +0100 Subject: [PATCH 04/15] Settlers of EMF Show cost of building stuff in the build menu and only enable options that the player can afford to build --- settlers_of_emf/main.py | 115 ++++++++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 35 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index 5025590..7981a5f 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -29,6 +29,14 @@ BUTTONS = [ 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 + class State: @@ -72,23 +80,38 @@ class Menu(State): self.question = question self.choices = choices + def is_choice_enabled(self, num): + c = self.choices[num] + return 'disabled' not in c or not c['disabled'] + def draw(self): # Draw the menu on screen ugfx.clear(ugfx.BLACK) ugfx.display_image(0, 0, 'settlers_of_emf/title.png') - ugfx.text(5, 95, self.question, ugfx.WHITE) - i = 0 - for c in self.choices: + ugfx.text(5, 100, self.question, ugfx.WHITE) + offset = 0 + for i in range(len(self.choices)): + c = self.choices[i] col = ugfx.WHITE if 'colour' in c: col = c['colour'] - if 'disabled' in c and c['disabled']: + if not self.is_choice_enabled(i): col = ugfx.html_color(0x676767) - ugfx.text(20, (20 * i) + 125, "{} - {} ".format(i + 1, c['name']), col) - i = i + 1 + text = "{} - {} ".format(i + 1, c['name']) + ugfx.text(20, offset + 125, text, col) + offset = offset + 20 + if 'cost' in c: + for j in range(len(c['cost'])): + cost = c['cost'][j] + ugfx.area((42 * j) + 48, offset + 125, 18, 18, cost['resource']['col']) + ugfx.text((42 * j) + 66, offset + 125, "x{} ".format(cost['amount']), col) + offset = offset + 20 # Set the initial selection - self._set_selection(self.selection) + if self.is_choice_enabled(self.selection): + self._set_selection(self.selection) + else: + self._set_selection(self._next_valid_selection(self.selection)) def initialise(self): # Register callbacks @@ -96,8 +119,7 @@ class Menu(State): 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']: + if self.is_choice_enabled(i): Buttons.enable_interrupt(BUTTONS[i], self._button_callback) def deinitialise(self): @@ -106,8 +128,7 @@ class Menu(State): 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']: + if self.is_choice_enabled(i): Buttons.disable_interrupt(BUTTONS[i]) def _button_callback(self, btn): @@ -131,8 +152,7 @@ class Menu(State): else: next_sel = sel - 1 while True: - c = self.choices[next_sel] - if 'disabled' not in c or not c['disabled']: + if self.is_choice_enabled(next_sel): break if next_sel == 0: next_sel = len(self.choices) - 1 @@ -147,8 +167,7 @@ class Menu(State): else: next_sel = sel + 1 while True: - c = self.choices[next_sel] - if 'disabled' not in c or not c['disabled']: + if self.is_choice_enabled(next_sel): break if next_sel == len(self.choices) - 1: next_sel = 0 @@ -158,9 +177,22 @@ class Menu(State): def _set_selection(self, new_selection): # Redraws the selection box - ugfx.box(0, (20 * self.selection) + 125, 240, 20, ugfx.BLACK) + size = 2 if 'cost' in self.choices[self.selection] else 1 + ugfx.box(0, self._get_offset_for_selection(self.selection) + 125, 240, 20 * size, ugfx.BLACK) self.selection = new_selection - ugfx.box(0, (20 * self.selection) + 125, 240, 20, ugfx.WHITE) + size = 2 if 'cost' in self.choices[self.selection] else 1 + ugfx.box(0, self._get_offset_for_selection(self.selection) + 125, 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): @@ -180,7 +212,6 @@ class MainMenu(Menu): class TeamMenu(Menu): - BACK = 6 options = [ {'name': "Scottish Consulate", @@ -198,6 +229,8 @@ class TeamMenu(Menu): {'name': "Back"}, ] + BACK = len(options) - 1 + def __init__(self): super().__init__('Choose your team:', TeamMenu.options) @@ -206,19 +239,39 @@ class TeamMenu(Menu): class BuildMenu(Menu): - BACK = 0 options = [ - {'name': "Road"}, - {'name': "Town"}, - {'name': "City"}, + {'name': "Road (0 points)", + 'cost': [{'resource': BRICK, 'amount': 1}, + {'resource': WOOD, 'amount': 1}]}, + {'name': "Town (1 point)", + 'cost': [{'resource': BRICK, 'amount': 1}, + {'resource': WOOD, 'amount': 1}, + {'resource': SHEEP, 'amount': 1}, + {'resource': WHEAT, 'amount': 1}]}, + {'name': "City (2 points)", + 'cost': [{'resource': WHEAT, 'amount': 2}, + {'resource': ORE, 'amount': 3}]}, {'name': "Back"}, ] - def __init__(self): + BACK = len(options) - 1 + + def __init__(self, resources): + # Disable options based on whether the player can afford them + for option in BuildMenu.options: + option['disabled'] = False + if 'cost' not in option: + continue + for cost in option['cost']: + for resource in resources: + if resource.resource == cost['resource']: + if resource.quantity < cost['amount']: + option['disabled'] = True super().__init__('Build a thing:', BuildMenu.options) - # TODO: show the build cost - # TODO: enable options based on whether the player can afford them + + def get_selected_build(self): + return BuildMenu.options[self.selection].copy() class Hex: @@ -452,7 +505,7 @@ class Player: # Player's hand of resources self.resources = [] - for kind in [GameBoard.SHEEP, GameBoard.WHEAT, GameBoard.WOOD, GameBoard.BRICK, GameBoard.ORE]: + for kind in [SHEEP, WHEAT, WOOD, BRICK, ORE]: r = Resource(kind) self.resources.append(r) @@ -570,14 +623,6 @@ class GameBoard(State): 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, @@ -869,7 +914,7 @@ class Settlers: self.state = Settlers.END_TURN_MENU if self.state == Settlers.BUILD_MENU: - menu = BuildMenu() + menu = BuildMenu(self.game.player.resources) x = menu.run() if x == BuildMenu.BACK: self.state = Settlers.GAME From 01e0b9226d43c5eb93a5b527dbd185cc9ed61348 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Sun, 23 Sep 2018 22:49:18 +0100 Subject: [PATCH 05/15] Settlers of EMF Keep track of and show turn numbers, plus prevent starting settlements from being created too close to one another --- settlers_of_emf/main.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index 7981a5f..c70e75b 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -515,12 +515,18 @@ class Player: if r.kind() == h.kind(): r.increment() + # Turn number + self.turn = 0 + 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 + def num_resources(self): return sum([x.quantity for x in self.resources]) @@ -549,6 +555,7 @@ class Player: # 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) # Player's resources ugfx.area(0, 290, 240, 30, ugfx.BLACK) @@ -731,6 +738,17 @@ class GameBoard(State): roads.append(road) return roads + def can_build_settlement(self, settlement): + """Determines if a given settlement is at least two roads from any other settlement""" + for r in self.get_roads_for_settlement(settlement): + # Get coords for the settlement at the other end of the road + for coords in r.edge: + for s in self.settlements: + if s.node == coords and s != settlement: + if not s.is_empty(): + return False + return True + def pick_starting_settlement(self, team): """Choose a starting settlement for the given team, and place a town and a connecting road there""" @@ -740,8 +758,7 @@ class GameBoard(State): # 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(): + if s.is_empty() and self.can_build_settlement(s): s.build_town(team) s_roads = self.get_roads_for_settlement(s) s_roads[random.randrange(0, len(s_roads))].build_road(team) @@ -802,9 +819,6 @@ class GameBoard(State): if self.dice.total() != 0: 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: @@ -866,6 +880,12 @@ class GameBoard(State): to_hex.robber = True to_hex.set_highlight(True) + def next_player(self): + """ Call from the state machine to reset the board for the next player""" + self.player.increment_turn() + self.dice.reset() + for h in self.hexes: + h.set_highlight(False) class Settlers: """A lean mean state machine""" @@ -902,6 +922,7 @@ class Settlers: self.state = Settlers.MAIN_MENU else: self.game = GameBoard(menu.get_selected_team()) + self.game.next_player() self.state = Settlers.GAME if self.state == Settlers.GAME: @@ -923,6 +944,7 @@ class Settlers: self.state = Settlers.GAME if self.state == Settlers.END_TURN_MENU: + self.game.next_player() # TODO: Ask for confirmation self.state = Settlers.GAME From 846bab0ab1bf1ea3fd8c79970e089a2edeaade9a Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Mon, 24 Sep 2018 10:12:04 +0100 Subject: [PATCH 06/15] Settlers of EMF Implement pass-and-play multiplayer, with interstitial screen asking to pass the badge to the next player --- settlers_of_emf/main.py | 158 ++++++++++++++++++++++++++-------------- 1 file changed, 102 insertions(+), 56 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index c70e75b..520a08a 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -1,6 +1,10 @@ """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! """ +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" @@ -97,14 +101,17 @@ class Menu(State): col = c['colour'] if not self.is_choice_enabled(i): col = ugfx.html_color(0x676767) - text = "{} - {} ".format(i + 1, c['name']) - ugfx.text(20, offset + 125, text, col) + if len(self.choices) == 1: + text = "{} ".format(c['name']) + else: + text = "{} - {} ".format(i + 1, c['name']) + ugfx.text(18, offset + 125, text, col) offset = offset + 20 if 'cost' in c: for j in range(len(c['cost'])): cost = c['cost'][j] - ugfx.area((42 * j) + 48, offset + 125, 18, 18, cost['resource']['col']) - ugfx.text((42 * j) + 66, offset + 125, "x{} ".format(cost['amount']), col) + ugfx.area((42 * j) + 46, offset + 125, 18, 18, cost['resource']['col']) + ugfx.text((42 * j) + 64, offset + 125, "x{} ".format(cost['amount']), col) offset = offset + 20 # Set the initial selection @@ -207,8 +214,8 @@ class MainMenu(Menu): ] def __init__(self, disable_continue_option=True): - MainMenu.options[1]['disabled'] = disable_continue_option - super().__init__('Main menu:', MainMenu.options) + MainMenu.options[MainMenu.CONTINUE_GAME]['disabled'] = disable_continue_option + super().__init__('Welcome!', MainMenu.options) class TeamMenu(Menu): @@ -226,52 +233,76 @@ class TeamMenu(Menu): 'colour': ugfx.html_color(0xeaeaea)}, {'name': "Null Sector", 'colour': ugfx.html_color(0x9c27b0)}, + {'name': "Start Game"}, {'name': "Back"}, ] + TEAM_MAX = len(options) - 3 + START_GAME = len(options) - 2 BACK = len(options) - 1 - def __init__(self): - super().__init__('Choose your team:', TeamMenu.options) + 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) def get_selected_team(self): return TeamMenu.options[self.selection].copy() -class BuildMenu(Menu): +class ActionMenu(Menu): options = [ - {'name': "Road (0 points)", + {'name': "Build Road (0 points)", 'cost': [{'resource': BRICK, 'amount': 1}, {'resource': WOOD, 'amount': 1}]}, - {'name': "Town (1 point)", + {'name': "Build Town (1 point)", 'cost': [{'resource': BRICK, 'amount': 1}, {'resource': WOOD, 'amount': 1}, {'resource': SHEEP, 'amount': 1}, {'resource': WHEAT, 'amount': 1}]}, - {'name': "City (2 points)", + {'name': "Upgrade to City (2 points)", 'cost': [{'resource': WHEAT, 'amount': 2}, {'resource': ORE, 'amount': 3}]}, + # TODO Implement trading + {'name': "Trade", 'disabled': True}, + {'name': "End Turn"}, {'name': "Back"}, ] + TRADE = len(options) - 3 + END_TURN = len(options) - 2 BACK = len(options) - 1 - def __init__(self, resources): - # Disable options based on whether the player can afford them - for option in BuildMenu.options: - option['disabled'] = False + def __init__(self, resources, dice_roll): + # Disable build options based on whether the player can afford them + for option in ActionMenu.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__('Build a thing:', BuildMenu.options) + # 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) def get_selected_build(self): - return BuildMenu.options[self.selection].copy() + return ActionMenu.options[self.selection].copy() + + +class NextPlayer(Menu): + + START_TURN = 0 + + def __init__(self, team): + super().__init__('Pass the badge to next team:', [team]) class Hex: @@ -627,8 +658,7 @@ class Dice: class GameBoard(State): MAIN_MENU = 0 - BUILD_MENU = 1 - END_TURN = 2 + ACTION_MENU = 1 # 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 @@ -653,7 +683,7 @@ class GameBoard(State): numbers = [FIVE, TWO, SIX, THREE, EIGHT, TEN, NINE, TWELVE, ELEVEN, FOUR, EIGHT, TEN, NINE, FOUR, FIVE, SIX, THREE, ELEVEN] - def __init__(self, team): + def __init__(self, teams): # Two rings of hexes around the centre radius = 2 @@ -696,7 +726,7 @@ class GameBoard(State): self.robber_mode = False self.robber_hex = self.get_robber_hex() - # Generate lists of unique valid locations for building + # Generate lists of unique valid locations for building settlements and roads self.roads = [] self.settlements = [] for h in self.hexes: @@ -719,17 +749,24 @@ class GameBoard(State): 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) + # 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 + for team in teams: + self.pick_starting_settlement(team) + teams.reverse() + for team in teams: + self.pick_starting_settlement(team) + teams.reverse() + self.players = [] + for team in teams: + self.players.append(Player(team, self.roads, self.settlements)) + self.player = self.players[-1] # The dice roller self.dice = Dice() - # The player details - self.player = Player(team, self.roads, self.settlements) - def get_roads_for_settlement(self, settlement): """Return a list of roads that connect to the given settlement""" roads = [] @@ -811,14 +848,9 @@ class GameBoard(State): 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: - # Can end the turn if dice were rolled - if self.dice.total() != 0: - self.selection = GameBoard.END_TURN - self.done = True + self.selection = GameBoard.ACTION_MENU + self.done = True if btn == Buttons.BTN_Hash: # Only roll the dice if not already rolled if self.dice.total() == 0: @@ -830,8 +862,9 @@ class GameBoard(State): h.set_highlight(True) else: h.set_highlight(False) - # Collect resources corresponding with the dice roll - self.player.collect(num) + # 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.robber_mode = True @@ -882,11 +915,19 @@ class GameBoard(State): def next_player(self): """ Call 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) + class Settlers: """A lean mean state machine""" @@ -895,12 +936,13 @@ class Settlers: MAIN_MENU = 1 TEAM_MENU = 2 GAME = 3 - BUILD_MENU = 4 - END_TURN_MENU = 5 + ACTION_MENU = 4 + END_TURN = 5 def __init__(self): self.state = Settlers.MAIN_MENU self.game = None + self.teams = [] def run(self): while self.state != Settlers.EXIT: @@ -909,6 +951,7 @@ class Settlers: menu = MainMenu(self.game is None) x = menu.run() if x == MainMenu.NEW_GAME: + self.teams = [] self.state = Settlers.TEAM_MENU if x == MainMenu.CONTINUE_GAME: self.state = Settlers.GAME @@ -916,12 +959,16 @@ class Settlers: self.state = Settlers.EXIT if self.state == Settlers.TEAM_MENU: - menu = TeamMenu() + menu = TeamMenu(self.teams) x = menu.run() + if x <= TeamMenu.TEAM_MAX: + self.teams.append(menu.get_selected_team()) + if len(self.teams) >= 4: + x = TeamMenu.START_GAME if x == TeamMenu.BACK: self.state = Settlers.MAIN_MENU - else: - self.game = GameBoard(menu.get_selected_team()) + if x == TeamMenu.START_GAME: + self.game = GameBoard(self.teams) self.game.next_player() self.state = Settlers.GAME @@ -929,23 +976,22 @@ class Settlers: 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 x == GameBoard.ACTION_MENU: + self.state = Settlers.ACTION_MENU - if self.state == Settlers.BUILD_MENU: - menu = BuildMenu(self.game.player.resources) + if self.state == Settlers.ACTION_MENU: + menu = ActionMenu(self.game.player.resources, self.game.dice.total()) x = menu.run() - if x == BuildMenu.BACK: - self.state = Settlers.GAME - else: - # TODO initiate building a thing + if x == ActionMenu.BACK: self.state = Settlers.GAME + if x == ActionMenu.END_TURN: + self.state = Settlers.END_TURN + # TODO initiate building a thing - if self.state == Settlers.END_TURN_MENU: + if self.state == Settlers.END_TURN: self.game.next_player() - # TODO: Ask for confirmation + menu = NextPlayer(self.game.player.team) + x = menu.run() self.state = Settlers.GAME # User chose exit, a machine reset is the easiest way :-) From fcd537cdfdfe57f733aa8fcf43ea350439f1c2b8 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Tue, 30 Oct 2018 19:23:34 +0000 Subject: [PATCH 07/15] Settlers of EMF Allow menu screens to avoid redrawing the logo, for quicker response times when navigating menus. --- settlers_of_emf/main.py | 173 ++++++++++++++++++++++++++++------------ 1 file changed, 123 insertions(+), 50 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index 520a08a..33cab4f 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -80,19 +80,28 @@ class State: class Menu(State): - def __init__(self, question, choices): + MENU_ITEM_OFFSET = 120 + + def __init__(self, question, choices, clear_title=True): self.question = question self.choices = choices + self.clear_title = clear_title 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 - ugfx.clear(ugfx.BLACK) - ugfx.display_image(0, 0, 'settlers_of_emf/title.png') - ugfx.text(5, 100, self.question, ugfx.WHITE) + 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) + ugfx.text(5, 95, self.question, ugfx.WHITE) offset = 0 for i in range(len(self.choices)): c = self.choices[i] @@ -105,13 +114,13 @@ class Menu(State): text = "{} ".format(c['name']) else: text = "{} - {} ".format(i + 1, c['name']) - ugfx.text(18, offset + 125, text, col) + ugfx.text(18, offset + Menu.MENU_ITEM_OFFSET, text, col) offset = offset + 20 if 'cost' in c: for j in range(len(c['cost'])): cost = c['cost'][j] - ugfx.area((42 * j) + 46, offset + 125, 18, 18, cost['resource']['col']) - ugfx.text((42 * j) + 64, offset + 125, "x{} ".format(cost['amount']), col) + ugfx.area((42 * j) + 46, offset + Menu.MENU_ITEM_OFFSET, 18, 18, cost['resource']['col']) + ugfx.text((42 * j) + 64, offset + Menu.MENU_ITEM_OFFSET, "x{} ".format(cost['amount']), col) offset = offset + 20 # Set the initial selection @@ -185,10 +194,10 @@ class Menu(State): def _set_selection(self, new_selection): # Redraws the selection box size = 2 if 'cost' in self.choices[self.selection] else 1 - ugfx.box(0, self._get_offset_for_selection(self.selection) + 125, 240, 20 * size, ugfx.BLACK) + ugfx.box(0, self._get_offset_for_selection(self.selection) + Menu.MENU_ITEM_OFFSET, 240, 20 * size, ugfx.BLACK) self.selection = new_selection size = 2 if 'cost' in self.choices[self.selection] else 1 - ugfx.box(0, self._get_offset_for_selection(self.selection) + 125, 240, 20 * size, ugfx.WHITE) + ugfx.box(0, self._get_offset_for_selection(self.selection) + Menu.MENU_ITEM_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 @@ -203,9 +212,6 @@ class Menu(State): class MainMenu(Menu): - NEW_GAME = 0 - CONTINUE_GAME = 1 - EXIT = 2 options = [ {'name': "Start New Game"}, @@ -213,9 +219,13 @@ class MainMenu(Menu): {'name': "Exit"}, ] - def __init__(self, disable_continue_option=True): + 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) + super().__init__('Welcome!', MainMenu.options, clear_title) class TeamMenu(Menu): @@ -248,14 +258,31 @@ class TeamMenu(Menu): 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) - - def get_selected_team(self): - return TeamMenu.options[self.selection].copy() + 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': "Back"}, + ] + + BUILD = 0 + TRADE = 1 + END_TURN = 2 + BACK = 3 + + 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}, @@ -268,19 +295,14 @@ class ActionMenu(Menu): {'name': "Upgrade to City (2 points)", 'cost': [{'resource': WHEAT, 'amount': 2}, {'resource': ORE, 'amount': 3}]}, - # TODO Implement trading - {'name': "Trade", 'disabled': True}, - {'name': "End Turn"}, {'name': "Back"}, ] - TRADE = len(options) - 3 - END_TURN = len(options) - 2 BACK = len(options) - 1 - def __init__(self, resources, dice_roll): + def __init__(self, resources): # Disable build options based on whether the player can afford them - for option in ActionMenu.options: + for option in BuildMenu.options: if 'cost' not in option: continue option['disabled'] = False @@ -289,12 +311,39 @@ class ActionMenu(Menu): if resource.resource == cost['resource']: if resource.quantity < cost['amount']: option['disabled'] = 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) + super().__init__('Build:', BuildMenu.options, False) - def get_selected_build(self): - return ActionMenu.options[self.selection].copy() + +class TradeMenu(Menu): + + options = [ + {'name': "Buy a Resource", + 'cost': [{'resource': BRICK, 'amount': 4}]}, + {'name': "Buy a Resource", + 'cost': [{'resource': WOOD, 'amount': 4}]}, + {'name': "Buy a Resource", + 'cost': [{'resource': SHEEP, 'amount': 4}]}, + {'name': "Buy a Resource", + 'cost': [{'resource': WHEAT, 'amount': 4}]}, + {'name': "Buy a Resource", + 'cost': [{'resource': ORE, 'amount': 4}]}, + {'name': "Back"}, + ] + + BACK = len(options) - 1 + + def __init__(self, resources): + # Disable trade options based on whether the player can afford them + for option in TradeMenu.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__('Trade:', TradeMenu.options, False) class NextPlayer(Menu): @@ -302,7 +351,7 @@ class NextPlayer(Menu): START_TURN = 0 def __init__(self, team): - super().__init__('Pass the badge to next team:', [team]) + super().__init__('Pass the badge to next team:', [team], False) class Hex: @@ -937,62 +986,86 @@ class Settlers: TEAM_MENU = 2 GAME = 3 ACTION_MENU = 4 - END_TURN = 5 + 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) + menu = MainMenu(self.game is None, self.old_state != Settlers.TEAM_MENU) x = menu.run() if x == MainMenu.NEW_GAME: self.teams = [] - self.state = Settlers.TEAM_MENU + self.enter_state(Settlers.TEAM_MENU) if x == MainMenu.CONTINUE_GAME: - self.state = Settlers.GAME + self.enter_state(Settlers.GAME) if x == MainMenu.EXIT: - self.state = Settlers.EXIT + self.enter_state(Settlers.EXIT) - if self.state == Settlers.TEAM_MENU: + elif self.state == Settlers.TEAM_MENU: menu = TeamMenu(self.teams) x = menu.run() if x <= TeamMenu.TEAM_MAX: - self.teams.append(menu.get_selected_team()) + self.teams.append(menu.get_selected_choice()) if len(self.teams) >= 4: x = TeamMenu.START_GAME if x == TeamMenu.BACK: - self.state = Settlers.MAIN_MENU + self.enter_state(Settlers.MAIN_MENU) if x == TeamMenu.START_GAME: self.game = GameBoard(self.teams) self.game.next_player() - self.state = Settlers.GAME + self.enter_state(Settlers.GAME) - if self.state == Settlers.GAME: + elif self.state == Settlers.GAME: x = self.game.run() if x == GameBoard.MAIN_MENU: - self.state = Settlers.MAIN_MENU + self.enter_state(Settlers.MAIN_MENU) if x == GameBoard.ACTION_MENU: - self.state = Settlers.ACTION_MENU + self.enter_state(Settlers.ACTION_MENU) - if self.state == Settlers.ACTION_MENU: - menu = ActionMenu(self.game.player.resources, self.game.dice.total()) + 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.BACK: - self.state = Settlers.GAME + 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.state = Settlers.END_TURN + self.enter_state(Settlers.ACTION_END_TURN) + 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) # TODO initiate building a thing - if self.state == Settlers.END_TURN: + 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) + # TODO initiate trading a thing + + elif self.state == Settlers.ACTION_END_TURN: self.game.next_player() menu = NextPlayer(self.game.player.team) x = menu.run() - self.state = Settlers.GAME + self.enter_state(Settlers.GAME) # User chose exit, a machine reset is the easiest way :-) restart_to_default() From 25f35c05186dd740423a5ceed53e52851afd3663 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Sat, 3 Nov 2018 15:21:25 +0000 Subject: [PATCH 08/15] Settlers of EMF Ensure 'back' option is visible on trade menu. --- settlers_of_emf/main.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index 33cab4f..105e1c7 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -80,12 +80,14 @@ class State: class Menu(State): - MENU_ITEM_OFFSET = 120 - def __init__(self, question, choices, clear_title=True): self.question = question self.choices = choices self.clear_title = clear_title + if self.question: + self.menu_offset = 120 + else: + self.menu_offset = 100 def is_choice_enabled(self, num): c = self.choices[num] @@ -101,7 +103,8 @@ class Menu(State): ugfx.display_image(0, 0, 'settlers_of_emf/title.png') else: ugfx.area(0, 95, 240, 225, ugfx.BLACK) - ugfx.text(5, 95, self.question, ugfx.WHITE) + if self.question: + ugfx.text(5, 95, self.question, ugfx.WHITE) offset = 0 for i in range(len(self.choices)): c = self.choices[i] @@ -114,13 +117,13 @@ class Menu(State): text = "{} ".format(c['name']) else: text = "{} - {} ".format(i + 1, c['name']) - ugfx.text(18, offset + Menu.MENU_ITEM_OFFSET, text, col) + ugfx.text(18, offset + self.menu_offset, text, col) offset = offset + 20 if 'cost' in c: for j in range(len(c['cost'])): cost = c['cost'][j] - ugfx.area((42 * j) + 46, offset + Menu.MENU_ITEM_OFFSET, 18, 18, cost['resource']['col']) - ugfx.text((42 * j) + 64, offset + Menu.MENU_ITEM_OFFSET, "x{} ".format(cost['amount']), col) + ugfx.area((42 * j) + 46, offset + self.menu_offset, 18, 18, cost['resource']['col']) + ugfx.text((42 * j) + 64, offset + self.menu_offset, "x{} ".format(cost['amount']), col) offset = offset + 20 # Set the initial selection @@ -194,10 +197,10 @@ class Menu(State): def _set_selection(self, new_selection): # Redraws the selection box size = 2 if 'cost' in self.choices[self.selection] else 1 - ugfx.box(0, self._get_offset_for_selection(self.selection) + Menu.MENU_ITEM_OFFSET, 240, 20 * size, ugfx.BLACK) + 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] else 1 - ugfx.box(0, self._get_offset_for_selection(self.selection) + Menu.MENU_ITEM_OFFSET, 240, 20 * size, ugfx.WHITE) + 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 @@ -311,7 +314,7 @@ class BuildMenu(Menu): if resource.resource == cost['resource']: if resource.quantity < cost['amount']: option['disabled'] = True - super().__init__('Build:', BuildMenu.options, False) + super().__init__(None, BuildMenu.options, False) class TradeMenu(Menu): @@ -343,7 +346,7 @@ class TradeMenu(Menu): if resource.resource == cost['resource']: if resource.quantity < cost['amount']: option['disabled'] = True - super().__init__('Trade:', TradeMenu.options, False) + super().__init__(None, TradeMenu.options, False) class NextPlayer(Menu): From bdbaa2f62e4608c181411ac012cf04c9d2ff64dd Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Sat, 3 Nov 2018 16:20:05 +0000 Subject: [PATCH 09/15] Settlers of EMF Refactor menu drawing to allow menus to customise how choices are drawn. Make the trade menu more sensible to use, showing better the trade costs. Implement simple 4 for 1 trading with the bank --- settlers_of_emf/main.py | 133 +++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 62 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index 105e1c7..5850d5e 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -40,6 +40,7 @@ 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 ] class State: @@ -107,23 +108,9 @@ class Menu(State): ugfx.text(5, 95, self.question, ugfx.WHITE) offset = 0 for i in range(len(self.choices)): - 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: - text = "{} - {} ".format(i + 1, c['name']) - ugfx.text(18, offset + self.menu_offset, text, col) + self._draw_choice(i, offset) offset = offset + 20 - if 'cost' in c: - for j in range(len(c['cost'])): - cost = c['cost'][j] - ugfx.area((42 * j) + 46, offset + self.menu_offset, 18, 18, cost['resource']['col']) - ugfx.text((42 * j) + 64, offset + self.menu_offset, "x{} ".format(cost['amount']), col) + if 'cost' in self.choices[i]: offset = offset + 20 # Set the initial selection @@ -132,6 +119,31 @@ class Menu(State): 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: + 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) @@ -196,10 +208,10 @@ class Menu(State): def _set_selection(self, new_selection): # Redraws the selection box - size = 2 if 'cost' in self.choices[self.selection] else 1 + 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] else 1 + 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): @@ -245,7 +257,8 @@ class TeamMenu(Menu): {'name': "Robot Arms", 'colour': ugfx.html_color(0xeaeaea)}, {'name': "Null Sector", - 'colour': ugfx.html_color(0x9c27b0)}, + 'colour': ugfx.html_color(0x9c27b0), + 'cost': []}, {'name': "Start Game"}, {'name': "Back"}, ] @@ -319,40 +332,32 @@ class BuildMenu(Menu): class TradeMenu(Menu): - options = [ - {'name': "Buy a Resource", - 'cost': [{'resource': BRICK, 'amount': 4}]}, - {'name': "Buy a Resource", - 'cost': [{'resource': WOOD, 'amount': 4}]}, - {'name': "Buy a Resource", - 'cost': [{'resource': SHEEP, 'amount': 4}]}, - {'name': "Buy a Resource", - 'cost': [{'resource': WHEAT, 'amount': 4}]}, - {'name': "Buy a Resource", - 'cost': [{'resource': ORE, 'amount': 4}]}, - {'name': "Back"}, - ] - - BACK = len(options) - 1 + BACK = len(RESOURCE_KINDS) def __init__(self, resources): # Disable trade options based on whether the player can afford them - for option in TradeMenu.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, TradeMenu.options, False) + 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): - START_TURN = 0 - def __init__(self, team): super().__init__('Pass the badge to next team:', [team], False) @@ -455,9 +460,6 @@ class Hex: self.highlight = highlight self.changed = True - def kind(self): - return self.resource['kind'] - @staticmethod def get_neighbouring_hex_coords(coords, direction): return [a + b for a, b in zip(coords, Hex.directions[direction])] @@ -474,7 +476,7 @@ class Hex: 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.kind() != 5: + if self.resource != DESERT: ugfx.text(round(self.centre[0] - text_offset), round(self.centre[1] - text_offset), "{} ".format(self.number['roll']), text_colour) @@ -560,12 +562,6 @@ class Resource(): self.resource = resource self.quantity = 0 - def kind(self): - return self.resource['kind'] - - def colour(self): - return self.resource['col'] - def increment(self, num=1): self.quantity = self.quantity + num @@ -588,14 +584,14 @@ class Player: # Player's hand of resources self.resources = [] - for kind in [SHEEP, WHEAT, WOOD, BRICK, ORE]: + for kind in RESOURCE_KINDS: r = Resource(kind) 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.kind(): + if r.resource == h.resource: r.increment() # Turn number @@ -628,12 +624,19 @@ class Player: for h in s.hexes: if h.number['roll'] == num and not h.robber: for r in self.resources: - if r.kind() == h.kind(): + 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): + for r in self.resources: + if r.resource == buy_kind: + r.increment() + if r.resource == sell_kind: + r.decrement(sell_amount) + def draw(self): # Player's team and score ugfx.text(5, 8, "{} ".format(self.team['name']), self.team['colour']) @@ -645,7 +648,7 @@ class Player: 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].colour()) + 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) @@ -1062,12 +1065,18 @@ class Settlers: x = menu.run() if x == TradeMenu.BACK: self.enter_state(Settlers.ACTION_MENU) - # TODO initiate trading a thing + 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: self.game.next_player() menu = NextPlayer(self.game.player.team) - x = menu.run() + menu.run() self.enter_state(Settlers.GAME) # User chose exit, a machine reset is the easiest way :-) From 31d326f281ff8377cd1f4943b24067ec17a9b363 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Sun, 4 Nov 2018 14:41:16 +0000 Subject: [PATCH 10/15] Settlers of EMF Turn robber mode into a more general interactive selection mode, add a way to show which settlement is selected and and implement city building --- settlers_of_emf/main.py | 145 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 135 insertions(+), 10 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index 5850d5e..e9d678a 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -314,7 +314,10 @@ class BuildMenu(Menu): {'name': "Back"}, ] - BACK = len(options) - 1 + ROAD = 0 + TOWN = 1 + CITY = 2 + BACK = 3 def __init__(self, resources): # Disable build options based on whether the player can afford them @@ -500,6 +503,9 @@ class Settlement: self.team = None self.contents = Settlement.EMPTY + # Whether to draw selection indicator + self.selected = False + def is_empty(self): return self.contents == Settlement.EMPTY @@ -519,6 +525,12 @@ class Settlement: 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 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 + def draw(self): if self.contents == Settlement.TOWN: ugfx.fill_circle(self.node[0], self.node[1], 4, self.team['colour']) @@ -526,6 +538,10 @@ class Settlement: 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) + # A selection highlight + if self.selected: + ugfx.circle(self.node[0], self.node[1], 11, ugfx.WHITE) + ugfx.circle(self.node[0], self.node[1], 10, ugfx.WHITE) class Road: @@ -607,9 +623,11 @@ class Player: self.turn = self.turn + 1 def num_resources(self): + """Total number of all resources the player has""" return sum([x.quantity for x in self.resources]) 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() @@ -631,13 +649,43 @@ class Player: 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 = [] + # TODO + return candidates + + def build_town_candidates(self): + """Return the list of all settlements that are valid candidates for towns to be built""" + candidates = [] + # TODO + return candidates + + def build_city_candidates(self): + """Return the list of all settlements that are valid candidates for being upgraded to city""" + candidates = [] + for s in [x for x in self.settlements if x.team == self.team]: + if s.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) @@ -738,6 +786,12 @@ class GameBoard(State): 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 @@ -778,9 +832,11 @@ class GameBoard(State): 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_mode = False 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 = [] @@ -875,7 +931,7 @@ class GameBoard(State): 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) - # For moving the robber + # 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) @@ -888,7 +944,7 @@ class GameBoard(State): Buttons.disable_interrupt(Buttons.BTN_B) Buttons.disable_interrupt(Buttons.BTN_Star) Buttons.disable_interrupt(Buttons.BTN_Hash) - # For moving the robber + # 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) @@ -899,7 +955,7 @@ class GameBoard(State): h.changed = True def _button_callback(self, btn): - if not self.robber_mode: + if not self.interactive_mode: if btn == Buttons.BTN_Menu: self.selection = GameBoard.MAIN_MENU self.done = True @@ -922,18 +978,20 @@ class GameBoard(State): p.collect(num) # Activate the robber on a seven if num == 7: - self.robber_mode = True + self.interactive_mode = GameBoard.ROBBER_MODE + # TODO give user hint about moving the robber self.redraw = True - else: + elif self.interactive_mode == GameBoard.ROBBER_MODE: h_current = self.get_robber_hex() if btn == Buttons.BTN_A: # 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.robber_mode = False + self.interactive_mode = None self.redraw = True # TODO: Steal a card from a player at this hex + # TODO tell user that the robber must move if btn == Buttons.JOY_Up: self._move_robber(h_current, 4) if btn == Buttons.JOY_Down: @@ -942,6 +1000,27 @@ class GameBoard(State): 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 == GameBoard.ROAD_MODE: + # TODO implement road building + pass + elif self.interactive_mode == GameBoard.TOWN_MODE: + # TODO implement town building + pass + elif self.interactive_mode == GameBoard.CITY_MODE: + candidates = self.player.build_city_candidates() + if btn == Buttons.BTN_A: + # Upgrade the selected settlement to a city + for candidate in candidates: + if candidate.selected: + candidate.build_city(self.player.team) + candidate.set_selection(False) + self.player.pay(self.build_cost) + self.interactive_mode = None + self.redraw = True + if btn == Buttons.JOY_Left or btn == Buttons.JOY_Up: + self._select_prev_build_candidate(candidates) + if btn == Buttons.JOY_Right or btn == Buttons.JOY_Down: + self._select_next_build_candidate(candidates) def _move_robber(self, h_current, direction): coords = Hex.get_neighbouring_hex_coords(h_current.coords, direction) @@ -949,6 +1028,30 @@ class GameBoard(State): 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: @@ -969,7 +1072,7 @@ class GameBoard(State): to_hex.set_highlight(True) def next_player(self): - """ Call from the state machine to reset the board for the next player""" + """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): @@ -982,6 +1085,20 @@ class GameBoard(State): 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: + candidates = self.player.build_road_candidates() + if mode == GameBoard.TOWN_MODE: + candidates = self.player.build_town_candidates() + if mode == GameBoard.CITY_MODE: + candidates = self.player.build_city_candidates() + if candidates: + candidates[0].set_selection(True) + self.interactive_mode = mode + self.build_cost = cost + # TODO tell user there are no valid candidates + class Settlers: """A lean mean state machine""" @@ -1058,7 +1175,15 @@ class Settlers: x = menu.run() if x == BuildMenu.BACK: self.enter_state(Settlers.ACTION_MENU) - # TODO initiate building a thing + 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) From 1fde67e2f821694107e0591e428eb0328b1633d9 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Sun, 4 Nov 2018 17:59:03 +0000 Subject: [PATCH 11/15] Settlers of EMF Refactor roads and settlements to abstract out some common features and finish adding implementations of building roads and towns. Also implement drawing a selection box around hex edges when choosing where to build roads. --- settlers_of_emf/main.py | 174 +++++++++++++++++++++++----------------- 1 file changed, 101 insertions(+), 73 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index e9d678a..49b1fde 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -483,31 +483,41 @@ class Hex: 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.""" +class Selectable: - # 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 + def __init__(self, data): + # Screen coords that define the selectable object + self.data = data - # The list of hexes to which this settlement is adjacent + # 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 = Settlement.EMPTY + self.contents = Selectable.EMPTY # Whether to draw selection indicator self.selected = False def is_empty(self): - return self.contents == Settlement.EMPTY + 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""" @@ -517,7 +527,7 @@ class Settlement: return score def build_town(self, team): - assert self.contents == Settlement.EMPTY, 'Town can only be built in empty location' + assert self.contents == Selectable.EMPTY, 'Town can only be built in empty location' self.team = team self.contents = Settlement.TOWN @@ -525,51 +535,58 @@ class Settlement: 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 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 - 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) + 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.node[0], self.node[1], 8, self.team['colour']) - ugfx.circle(self.node[0], self.node[1], 8, ugfx.WHITE) + 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: - ugfx.circle(self.node[0], self.node[1], 11, ugfx.WHITE) - ugfx.circle(self.node[0], self.node[1], 10, ugfx.WHITE) + # 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: +class Road(Selectable): """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' + 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(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) + 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(): @@ -665,21 +682,30 @@ class Player: def build_road_candidates(self): """Return the list of all roads that are valid candidates for building""" candidates = [] - # TODO + # 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]: + candidates.append(road) return candidates def build_town_candidates(self): """Return the list of all settlements that are valid candidates for towns to be built""" candidates = [] - # TODO + for s in self.settlements: + # TODO it's way more complex than this... + if s.is_empty(): + 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 = [] - for s in [x for x in self.settlements if x.team == self.team]: - if s.contents == Settlement.TOWN: - candidates.append(s) + # 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): @@ -844,15 +870,17 @@ class GameBoard(State): for edge in h.edges: already_got = False for r in self.roads: - if r.edge == edge: + 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.node == node: + if s.data == node: already_got = True s.hexes.append(h) if not already_got: @@ -882,7 +910,7 @@ class GameBoard(State): """Return a list of roads that connect to the given settlement""" roads = [] for road in self.roads: - if settlement.node in road.edge: + if settlement.data in road.data: roads.append(road) return roads @@ -890,9 +918,9 @@ class GameBoard(State): """Determines if a given settlement is at least two roads from any other settlement""" for r in self.get_roads_for_settlement(settlement): # Get coords for the settlement at the other end of the road - for coords in r.edge: + for coords in r.data: for s in self.settlements: - if s.node == coords and s != settlement: + if s.data == coords and s != settlement: if not s.is_empty(): return False return True @@ -979,7 +1007,7 @@ class GameBoard(State): # Activate the robber on a seven if num == 7: self.interactive_mode = GameBoard.ROBBER_MODE - # TODO give user hint about moving the robber + # TODO: give user hint about moving the robber self.redraw = True elif self.interactive_mode == GameBoard.ROBBER_MODE: h_current = self.get_robber_hex() @@ -991,7 +1019,7 @@ class GameBoard(State): self.interactive_mode = None self.redraw = True # TODO: Steal a card from a player at this hex - # TODO tell user that the robber must move + # TODO: tell user that the robber must move if btn == Buttons.JOY_Up: self._move_robber(h_current, 4) if btn == Buttons.JOY_Down: @@ -1000,27 +1028,27 @@ class GameBoard(State): 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 == GameBoard.ROAD_MODE: - # TODO implement road building - pass - elif self.interactive_mode == GameBoard.TOWN_MODE: - # TODO implement town building - pass - elif self.interactive_mode == GameBoard.CITY_MODE: - candidates = self.player.build_city_candidates() + elif self.interactive_mode in (GameBoard.ROAD_MODE, GameBoard.TOWN_MODE, GameBoard.CITY_MODE): if btn == Buttons.BTN_A: - # Upgrade the selected settlement to a city - for candidate in candidates: + for candidate in self.build_candidates: if candidate.selected: - candidate.build_city(self.player.team) + # Build a town on the selected settlement + 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 btn == Buttons.JOY_Left or btn == Buttons.JOY_Up: - self._select_prev_build_candidate(candidates) + self._select_prev_build_candidate(self.build_candidates) if btn == Buttons.JOY_Right or btn == Buttons.JOY_Down: - self._select_next_build_candidate(candidates) + 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) @@ -1088,16 +1116,16 @@ class GameBoard(State): def build_mode(self, mode, cost): """Called from the state machine to enter building selection mode""" if mode == GameBoard.ROAD_MODE: - candidates = self.player.build_road_candidates() + self.build_candidates = self.player.build_road_candidates() if mode == GameBoard.TOWN_MODE: - candidates = self.player.build_town_candidates() + self.build_candidates = self.player.build_town_candidates() if mode == GameBoard.CITY_MODE: - candidates = self.player.build_city_candidates() - if candidates: - candidates[0].set_selection(True) - self.interactive_mode = mode + self.build_candidates = self.player.build_city_candidates() + if self.build_candidates: + self.build_candidates[0].set_selection(True) self.build_cost = cost - # TODO tell user there are no valid candidates + self.interactive_mode = mode + # TODO: tell user there are no valid candidates class Settlers: From 87457fccb43e6c0742fd0c0c6612ae4df0e1e04a Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Mon, 5 Nov 2018 23:49:00 +0000 Subject: [PATCH 12/15] Settlers of EMF Fix a problem with the road selection cursor getting stuck due to there being duplicate entries in the candidate list --- settlers_of_emf/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index 49b1fde..2d549ee 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -688,7 +688,8 @@ class Player: 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]: - candidates.append(road) + if road not in candidates: + candidates.append(road) return candidates def build_town_candidates(self): @@ -1032,7 +1033,7 @@ class GameBoard(State): if btn == Buttons.BTN_A: for candidate in self.build_candidates: if candidate.selected: - # Build a town on the selected settlement + # 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 From df0a13c1ab15c7688cba47b069816903c266b4d7 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Wed, 7 Nov 2018 23:26:22 +0000 Subject: [PATCH 13/15] Settlers of EMF Finish implementing town building selection mode -- all building modes now complete --- settlers_of_emf/main.py | 76 ++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index 2d549ee..25eb8d0 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -250,7 +250,7 @@ class TeamMenu(Menu): 'colour': ugfx.html_color(0x0000ff)}, {'name': "Camp Holland", 'colour': ugfx.html_color(0xff8c00)}, - {'name': "Sheffield Hackers", + {'name': "Sheffield Hackspace", 'colour': ugfx.html_color(0x26c6da)}, {'name': "Milliways", 'colour': ugfx.html_color(0xff00ff)}, @@ -621,12 +621,6 @@ class Player: r = Resource(kind) 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.resource == h.resource: - r.increment() - # Turn number self.turn = 0 @@ -643,6 +637,16 @@ class Player: """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: @@ -692,13 +696,28 @@ class Player: 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 = [] - for s in self.settlements: - # TODO it's way more complex than this... - if s.is_empty(): - candidates.append(s) + # 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): @@ -893,16 +912,20 @@ class GameBoard(State): # 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 - for team in teams: - self.pick_starting_settlement(team) - teams.reverse() - for team in teams: - self.pick_starting_settlement(team) - teams.reverse() 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() @@ -915,17 +938,6 @@ class GameBoard(State): roads.append(road) return roads - def can_build_settlement(self, settlement): - """Determines if a given settlement is at least two roads from any other settlement""" - for r in self.get_roads_for_settlement(settlement): - # Get coords for the settlement at the other end of the road - for coords in r.data: - for s in self.settlements: - if s.data == coords and s != settlement: - if not s.is_empty(): - return False - return True - def pick_starting_settlement(self, team): """Choose a starting settlement for the given team, and place a town and a connecting road there""" @@ -935,7 +947,7 @@ class GameBoard(State): # Build at the highest probability settlement that is still available for s in sorted_settlements: - if s.is_empty() and self.can_build_settlement(s): + 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) @@ -1221,8 +1233,8 @@ class Settlers: 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 + # 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) @@ -1233,6 +1245,8 @@ class Settlers: menu.run() self.enter_state(Settlers.GAME) + # TODO: Game over! + # User chose exit, a machine reset is the easiest way :-) restart_to_default() From 82c0cb378c56692f3d5758df9ad59e9e5cd089b4 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Sat, 24 Nov 2018 12:04:57 +0000 Subject: [PATCH 14/15] Settlers of EMF UI improvements: Always use menu button to summon a menu screen, add a status line that gives hints about controls etc, allow using the B button as a shortcut for "Back" in menus that have such an option Also add a kind of "cheat mode" for testing that grants extra resources --- settlers_of_emf/main.py | 87 ++++++++++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index 25eb8d0..7954355 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -42,6 +42,11 @@ 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: @@ -89,6 +94,7 @@ class Menu(State): self.menu_offset = 120 else: self.menu_offset = 100 + self.back = -1 def is_choice_enabled(self, num): c = self.choices[num] @@ -129,7 +135,11 @@ class Menu(State): if len(self.choices) == 1: text = "{} ".format(c['name']) else: - text = "{} - {} ".format(i + 1, c['name']) + 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'])): @@ -147,6 +157,7 @@ class Menu(State): 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)): @@ -156,6 +167,7 @@ class Menu(State): 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)): @@ -165,6 +177,10 @@ class Menu(State): 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) @@ -283,13 +299,15 @@ class ActionMenu(Menu): {'name': "Build"}, {'name': "Trade"}, {'name': "End Turn"}, + {'name': "Exit Game"}, {'name': "Back"}, ] BUILD = 0 TRADE = 1 END_TURN = 2 - BACK = 3 + 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 @@ -607,6 +625,13 @@ class Resource(): 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 @@ -623,6 +648,7 @@ class Player: # Turn number self.turn = 0 + self.status = Player.STATUS_ACTIONS_DICE def score(self): points = 0 @@ -632,6 +658,7 @@ class Player: 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""" @@ -737,8 +764,13 @@ class Player: 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 - ugfx.area(0, 290, 240, 30, ugfx.BLACK) offset = int(ugfx.width() / len(self.resources)) square = int(offset / 3) for i in range(len(self.resources)): @@ -806,8 +838,7 @@ class Dice: class GameBoard(State): - MAIN_MENU = 0 - ACTION_MENU = 1 + 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 @@ -930,6 +961,9 @@ class GameBoard(State): # 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 = [] @@ -970,26 +1004,28 @@ class GameBoard(State): 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_Star, 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_Star) 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: @@ -998,10 +1034,7 @@ class GameBoard(State): def _button_callback(self, btn): if not self.interactive_mode: if btn == Buttons.BTN_Menu: - self.selection = GameBoard.MAIN_MENU - self.done = True - if btn == Buttons.BTN_Star: - self.selection = GameBoard.ACTION_MENU + self.selection = GameBoard.MENU self.done = True if btn == Buttons.BTN_Hash: # Only roll the dice if not already rolled @@ -1020,19 +1053,31 @@ class GameBoard(State): # Activate the robber on a seven if num == 7: self.interactive_mode = GameBoard.ROBBER_MODE - # TODO: give user hint about moving the robber + 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.redraw = True + self.player.status = Player.STATUS_ACTIONS # TODO: Steal a card from a player at this hex - # TODO: tell user that the robber must move + 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: @@ -1058,6 +1103,10 @@ class GameBoard(State): 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: @@ -1138,7 +1187,9 @@ class GameBoard(State): self.build_candidates[0].set_selection(True) self.build_cost = cost self.interactive_mode = mode - # TODO: tell user there are no valid candidates + self.player.status = Player.STATUS_BUILD + else: + self.player.status = Player.STATUS_NO_BUILD class Settlers: @@ -1194,9 +1245,7 @@ class Settlers: elif self.state == Settlers.GAME: x = self.game.run() - if x == GameBoard.MAIN_MENU: - self.enter_state(Settlers.MAIN_MENU) - if x == GameBoard.ACTION_MENU: + if x == GameBoard.MENU: self.enter_state(Settlers.ACTION_MENU) elif self.state == Settlers.ACTION_MENU: @@ -1208,6 +1257,8 @@ class Settlers: 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) From a6747e79f23e043119033907cabd316005a7c98b Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Sat, 24 Nov 2018 14:29:56 +0000 Subject: [PATCH 15/15] Settlers of EMF Add a noddy game over screen that triggers when you end your turn with 10 points or more. Menu object was enhanced to allow multi- line questions to accomodate this. --- settlers_of_emf/main.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/settlers_of_emf/main.py b/settlers_of_emf/main.py index 7954355..82b308c 100644 --- a/settlers_of_emf/main.py +++ b/settlers_of_emf/main.py @@ -91,7 +91,11 @@ class Menu(State): self.choices = choices self.clear_title = clear_title if self.question: - self.menu_offset = 120 + 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 @@ -111,7 +115,8 @@ class Menu(State): else: ugfx.area(0, 95, 240, 225, ugfx.BLACK) if self.question: - ugfx.text(5, 95, self.question, ugfx.WHITE) + 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) @@ -383,6 +388,12 @@ class NextPlayer(Menu): 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.""" @@ -1219,7 +1230,7 @@ class Settlers: while self.state != Settlers.EXIT: if self.state == Settlers.MAIN_MENU: - menu = MainMenu(self.game is None, self.old_state != Settlers.TEAM_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 = [] @@ -1291,12 +1302,16 @@ class Settlers: self.enter_state(Settlers.GAME) elif self.state == Settlers.ACTION_END_TURN: - self.game.next_player() - menu = NextPlayer(self.game.player.team) - menu.run() - self.enter_state(Settlers.GAME) - - # TODO: Game over! + 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()