From 7ac28fc73e5035d5282c92fecb15a30f38e87dc0 Mon Sep 17 00:00:00 2001 From: Mat Booth Date: Fri, 7 Sep 2018 14:41:42 +0100 Subject: [PATCH] Initial version of a Catan game board generator If you have 3D printed your own version of Settlers of Catan, with 3D terrain and little sheep and grain siloes and everything, it is fairly difficult to properly randomise the tile selection during the game setup. I assume most EMF-goers have encountered this problem, so this app provides a solution! --- settlers/main.py | 187 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 settlers/main.py diff --git a/settlers/main.py b/settlers/main.py new file mode 100644 index 0000000..5dc7372 --- /dev/null +++ b/settlers/main.py @@ -0,0 +1,187 @@ +"""Settlers of Catan game board generator""" + +___name___ = "settlers" +___license___ = "MIT" +___dependencies___ = ["ugfx_helper", "sleep"] +___categories___ = ["Games"] +___bootstrapped___ = False + +import random, ugfx, ugfx_helper, math, time, buttons +from app import App, restart_to_default +from tilda import Buttons + +ugfx_helper.init() +ugfx.clear(ugfx.BLACK) + +""" +This was an experiment in drawing hexagons. Some notes: + +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. + +""" + +class Hex: + # Constant 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 = 22 + + # Possible kinds of resource and the colour it should be rendered + kinds = { + 0: ugfx.html_color(0xd4e157), # Sheep + 1: ugfx.html_color(0xffc107), # Wheat + 2: ugfx.html_color(0x993300), # Wood + 3: ugfx.html_color(0xff0000), # Brick + 4: ugfx.html_color(0x757575), # Ore + 5: ugfx.html_color(0xffee55), # Desert (nothing) + } + + # 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, kind, 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.kind = kind + + # The dice roll required to win this resource + self.number = number + + # Whether this hex contains the robber + self.robber = robber + + # Compute the screen coordinates of the centre of the hex + self.centre = Hex.to_screen_coords(self.coords[0], self.coords[1]) + + # Generate screen coordinates for each of the corners of the hex + self.corners = [] + 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.corners.append([round(self.centre[0] + offset[0]), round(self.centre[1] + offset[1])]) + + @staticmethod + def to_screen_coords(x, y): + """Returns screen coordinates computed from the given hex coordinates""" + newX = (Hex.matrix[0] * x + Hex.matrix[1] * y) * Hex.size + newY = (Hex.matrix[2] * x + Hex.matrix[3] * y) * Hex.size + return [newX + Hex.origin[0], newY + Hex.origin[1]] + + @staticmethod + def get_neighbouring_hex_coords(coords, direction): + return [a + b for a, b in zip(coords, Hex.directions[direction])] + + def draw(self): + """Draw the hexagon to the screen""" + ugfx.fill_polygon(0, 0, self.corners, Hex.kinds[self.kind]) + text_offset = Hex.size * 0.5 + if self.robber: + ugfx.text(round(self.centre[0] - text_offset), round(self.centre[1] - text_offset), "Rb ", ugfx.BLACK) + else: + if self.kind != 5: + ugfx.text(round(self.centre[0] - text_offset), round(self.centre[1] - text_offset), "{} ".format(self.number), ugfx.BLACK) + + def clear(self): + ugfx.fill_polygon(0, 0, self.corners, ugfx.BLACK) + + +def board_setup(resources, numbers): + """Generate a random game board""" + + # 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 = resources.copy() + n_copy = numbers.copy() + + 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 j 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 + if resource == 5: + number = 7 + else: + number = n_copy.pop(0) + hexes.append(Hex(coords, resource, number, number == 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 + resource = r_copy.pop() + if resource == 5: + number = 7 + else: + number = n_copy.pop(0) + hexes.append(Hex(coords, resource, number, number == 7)) + return hexes + + +# List of resources (pre-randomised to combat the not-very random number +# generator) and dice rolls (these have a strict order) for 2-4 player games +resources = [4, 0, 1, 4, 4, 2, 5, 3, 2, 1, 2, 2, 1, 0, 3, 0, 3, 1, 0] +numbers = [5, 2, 6, 3, 8, 10, 9, 12, 11, 4, 8, 10, 9, 4, 5, 6, 3, 11] + +def draw(): + hexes = board_setup(resources, numbers) + for h in hexes: + h.clear() + time.sleep_ms(100) + h.draw() + +ugfx.text(5, 5, 'Press A to generate another ', ugfx.WHITE) +draw() + +# Main Loop +while True: + if buttons.is_triggered(tilda.Buttons.BTN_A): + draw() + elif buttons.is_triggered(tilda.Buttons.BTN_Menu): + break + time.sleep_ms(5) +restart_to_default()