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!
sammachin-gprs
Mat Booth 2018-09-07 14:41:42 +01:00
parent b44cc3596b
commit 7ac28fc73e
1 changed files with 187 additions and 0 deletions

187
settlers/main.py Normal file
View File

@ -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()