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.
master
Mat Booth 2018-11-04 17:59:03 +00:00
parent 31d326f281
commit 1fde67e2f8
1 changed files with 101 additions and 73 deletions

View File

@ -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: