Skip to content

Commit

Permalink
Merge pull request #23 from bcollazo/feature/robber
Browse files Browse the repository at this point in the history
Robber. Possible actions and moving/stealing logic
  • Loading branch information
bcollazo committed Sep 12, 2020
2 parents daf705e + 3dab0bb commit af5749a
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 30 deletions.
90 changes: 64 additions & 26 deletions catanatron/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,28 @@ def city_possible_actions(player, board):
return []


def playable_actions(player, has_roll, board):
if not has_roll:
actions = [Action(player, ActionType.ROLL, None)]
if player.has_knight_card(): # maybe knight
for coordinate in board.tiles.keys():
if coordinate != board.robber_tile.coordinate:
actions.append(
Action(player, ActionType.PLAY_KNIGHT_CARD, coordinate)
)
def robber_possibilities(player, board, players):
players_by_color = {p.color: p for p in players}
actions = []
for coordinate, tile in board.resource_tiles():
if coordinate == board.robber_coordinate:
continue # ignore. must move robber.

return actions
# each tile can yield a (move-but-cant-steal) action or
# several (move-and-steal-from-x) actions.
players_to_steal_from = set()
for node_ref, node in tile.nodes.items():
building = board.buildings.get(node)
if building is not None:
candidate = players_by_color[building.color]
if candidate.resource_decks.num_cards() >= 1:
players_to_steal_from.add(candidate)

actions = [Action(player, ActionType.END_TURN, None)]
for action in road_possible_actions(player, board):
actions.append(action)
for action in city_possible_actions(player, board):
actions.append(action)
if len(players_to_steal_from) == 0:
actions.append(Action(player, ActionType.MOVE_ROBBER, (coordinate, None)))
else:
for p in players_to_steal_from:
actions.append(Action(player, ActionType.MOVE_ROBBER, (coordinate, p)))

return actions

Expand Down Expand Up @@ -123,6 +128,7 @@ def __init__(self, players: Iterable[Player]):

self.current_player_index = 0
self.current_player_has_roll = False
self.moving_robber = False
random.shuffle(self.players)

def play(self):
Expand Down Expand Up @@ -179,14 +185,30 @@ def winning_player(self):

def play_tick(self):
current_player = self.players[self.current_player_index]

actions = playable_actions(
current_player, self.current_player_has_roll, self.board
)
actions = self.playable_actions(current_player)
action = current_player.decide(self.board, actions)

self.execute(action)

def playable_actions(self, player):
if self.moving_robber:
return robber_possibilities(player, self.board, self.players)

if not self.current_player_has_roll:
actions = [Action(player, ActionType.ROLL, None)]
if player.has_knight_card(): # maybe knight
# TODO: Change to PLAY_KNIGHT_CARD
actions.extend(robber_possibilities(player, self.board, self.players))

return actions

actions = [Action(player, ActionType.END_TURN, None)]
for action in road_possible_actions(player, self.board):
actions.append(action)
for action in city_possible_actions(player, self.board):
actions.append(action)

return actions

def execute(self, action, initial_build_phase=False):
if action.action_type == ActionType.END_TURN:
self.current_player_index = (self.current_player_index + 1) % len(
Expand Down Expand Up @@ -215,18 +237,34 @@ def execute(self, action, initial_build_phase=False):
dices = roll_dice()
number = dices[0] + dices[1]

payout, depleted = yield_resources(self.board, self.resource_decks, number)
for color, resource_decks in payout.items():
player = self.players_by_color[color]
if number == 7:
self.moving_robber = True
else:
payout, depleted = yield_resources(
self.board, self.resource_decks, number
)
for color, resource_decks in payout.items():
player = self.players_by_color[color]

# Atomically add to player's hand and remove from bank
player.resource_decks += resource_decks
self.resource_decks -= resource_decks
# Atomically add to player's hand and remove from bank
player.resource_decks += resource_decks
self.resource_decks -= resource_decks

action = Action(action.player, action.action_type, dices)
self.current_player_has_roll = True
elif action.action_type == ActionType.MOVE_ROBBER:
(coordinate, player_to_steal_from) = action.value
self.board.robber_coordinate = coordinate
if player_to_steal_from is not None:
hand = player_to_steal_from.resource_decks.to_array()
resource = random.choice(hand)
player_to_steal_from.resource_decks.draw(1, resource)
self.current_player().resource_decks.replenish(1, resource)
else:
raise RuntimeError("Unknown ActionType")

# TODO: Think about possible-action/idea vs finalized-action design
self.actions.append(action)

def current_player(self):
return self.players[self.current_player_index]
7 changes: 7 additions & 0 deletions catanatron/models/decks.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ def draw(self, count: int, resource: Resource):
def replenish(self, count: int, resource: Resource):
self.decks[resource] += count

def to_array(self):
"""Make it look like a deck of cards"""
array = []
for resource in Resource:
array.extend([resource] * self.count(resource))
return array

def __add__(self, other):
for resource in Resource:
self.replenish(other.count(resource), resource)
Expand Down
1 change: 1 addition & 0 deletions catanatron/models/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Resource(Enum):

class ActionType(Enum):
ROLL = "ROLL" # value is None or rolled value.
MOVE_ROBBER = "MOVE_ROBBER" # value is (coordinate, Player|None).

# Building/Buying
BUILD_ROAD = "BUILD_ROAD" # value is edge
Expand Down
58 changes: 54 additions & 4 deletions tests/test_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

from catanatron.game import (
Game,
playable_actions,
yield_resources,
city_possible_actions,
road_possible_actions,
robber_possibilities,
)
from catanatron.models.board import Board
from catanatron.models.board_initializer import NodeRef, EdgeRef
Expand Down Expand Up @@ -68,12 +68,34 @@ def test_buying_road_is_payed_for():
assert game.resource_decks.count(Resource.WOOD) == 20 # since we didnt yield


def test_moving_robber_steals_correctly():
players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)]
game = Game(players)

players[1].resource_decks.replenish(1, Resource.WHEAT)
game.board.build_settlement(
Color.BLUE,
game.board.nodes[((0, 0, 0), NodeRef.SOUTH)],
initial_build_phase=True,
)

action = Action(players[0], ActionType.MOVE_ROBBER, ((2, 0, -2), None))
game.execute(action)
assert players[0].resource_decks.num_cards() == 0
assert players[1].resource_decks.num_cards() == 1

action = Action(players[0], ActionType.MOVE_ROBBER, ((0, 0, 0), players[1]))
game.execute(action)
assert players[0].resource_decks.num_cards() == 1
assert players[1].resource_decks.num_cards() == 0


# ===== Playable Actions
def test_playable_actions():
board = Board()
player = Player(Color.RED)
players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)]
game = Game(players)

actions = playable_actions(player, False, board)
actions = game.playable_actions(players[0])
assert len(actions) == 1
assert actions[0].action_type == ActionType.ROLL

Expand Down Expand Up @@ -120,6 +142,34 @@ def test_city_playable_actions():
assert len(city_possible_actions(player, board)) == 2


def test_robber_possibilities():
board = Board()
red = Player(Color.RED)
blue = Player(Color.BLUE)
orange = Player(Color.ORANGE)
players = [red, blue, orange]

# one for each resource tile (excluding desert)
assert len(robber_possibilities(red, board, players)) == 18

# assert same number of possibilities, b.c. players have no cards.
board.build_settlement(
Color.BLUE, board.nodes[((0, 0, 0), NodeRef.SOUTH)], initial_build_phase=True
)
board.build_settlement(
Color.ORANGE, board.nodes[((0, 0, 0), NodeRef.NORTH)], initial_build_phase=True
)
assert len(robber_possibilities(red, board, players)) == 18

# assert same number of possibilities, b.c. only one player to rob in this tile
orange.resource_decks.replenish(1, Resource.WHEAT)
assert len(robber_possibilities(red, board, players)) == 18

# now possibilites increase by 1 b.c. we have to decide to steal from blue or green
blue.resource_decks.replenish(1, Resource.WHEAT)
assert len(robber_possibilities(red, board, players)) == 19


# ===== Yield Resources
def test_yield_resources():
board = Board()
Expand Down
10 changes: 10 additions & 0 deletions tests/test_resource_decks.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,13 @@ def test_can_subtract():
b -= a
assert a.count(Resource.SHEEP) == 13
assert b.count(Resource.SHEEP) == 2


def test_to_array():
a = ResourceDecks(empty=True)
assert len(a.to_array()) == 0

a.replenish(3, Resource.SHEEP)
a.replenish(2, Resource.BRICK)
assert len(a.to_array()) == 5
assert len(set(a.to_array())) == 2

0 comments on commit af5749a

Please sign in to comment.