Skip to content

Commit

Permalink
Make initial_phase part of execute. Replay games.
Browse files Browse the repository at this point in the history
  • Loading branch information
bcollazo committed Oct 4, 2020
1 parent 6497e75 commit ae9448f
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 155 deletions.
232 changes: 119 additions & 113 deletions catanatron/game.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import uuid
import random
import sys
import copy
from enum import Enum
from typing import Iterable
from collections import namedtuple, defaultdict

Expand All @@ -9,6 +12,7 @@
from catanatron.models.board_initializer import Node
from catanatron.models.enums import Resource, DevelopmentCard
from catanatron.models.actions import (
ActionPrompt,
Action,
ActionType,
road_possible_actions,
Expand All @@ -17,6 +21,9 @@
robber_possibilities,
year_of_plenty_possible_actions,
monopoly_possible_actions,
initial_road_possibilities,
initial_settlement_possibilites,
discard_possibilities,
)
from catanatron.models.player import Player
from catanatron.models.decks import ResourceDeck, DevelopmentDeck
Expand Down Expand Up @@ -73,86 +80,47 @@ def yield_resources(board, resource_deck, number):
return payout, depleted


def initialize_tick_queue(players):
"""First player goes, settlement and road, ..."""
tick_queue = []
for player in players:
tick_queue.append((player, ActionPrompt.BUILD_FIRST_SETTLEMENT))
tick_queue.append((player, ActionPrompt.BUILD_INITIAL_ROAD))
for player in reversed(players):
tick_queue.append((player, ActionPrompt.BUILD_SECOND_SETTLEMENT))
tick_queue.append((player, ActionPrompt.BUILD_INITIAL_ROAD))
tick_queue.append((players[0], ActionPrompt.ROLL))
return tick_queue


class Game:
"""
This contains the complete state of the game (board + players) and the
core turn-by-turn controlling logic.
"""

def __init__(self, players: Iterable[Player]):
def __init__(self, players: Iterable[Player], seed=None):
self.seed = seed or random.randrange(sys.maxsize)
random.seed(self.seed)

self.id = str(uuid.uuid4())
self.players = players
self.players = random.sample(players, len(players))
self.players_by_color = {p.color: p for p in players}
self.map = BaseMap()
self.board = Board(self.map)
self.actions = [] # log of all action taken by players
self.resource_deck = ResourceDeck.starting_bank()
self.development_deck = DevelopmentDeck.starting_bank()

# variables to keep track of what to do next
self.tick_queue = initialize_tick_queue(players)
self.current_player_index = 0
self.current_player_has_roll = False
self.moving_robber = False
self.tick_queue = []
random.shuffle(self.players)

self.num_turns = 0

def play(self, action_callback=None):
"""Runs the game until the end"""
self.play_initial_build_phase(action_callback)
while self.winning_player() == None and self.num_turns < TURNS_LIMIT:
self.play_tick(action_callback)

def play_initial_build_phase(self, action_callback=None):
"""First player goes, settlement and road, ..."""
for player in self.players + list(reversed(self.players)):
# Place a settlement first
buildable_nodes = self.board.buildable_nodes(
player.color, initial_build_phase=True
)
actions = map(
lambda node: Action(player, ActionType.BUILD_SETTLEMENT, node),
buildable_nodes,
)
action = player.decide(self, list(actions))
self.execute(
action, initial_build_phase=True, action_callback=action_callback
)

# Then a road, ensure its connected to this last settlement
buildable_edges = filter(
lambda e: action.value in e.nodes,
self.board.buildable_edges(player.color),
)
actions = map(
lambda edge: Action(player, ActionType.BUILD_ROAD, edge),
buildable_edges,
)
action = player.decide(self, list(actions))
self.execute(
action, initial_build_phase=True, action_callback=action_callback
)

# yield resources of second settlement
second_settlements = map(
lambda a: (a.player, a.value),
filter(
lambda a: a.action_type == ActionType.BUILD_SETTLEMENT,
self.actions[len(self.players) * 2 :],
),
)
for (player, node) in second_settlements:
for tile in self.board.get_adjacent_tiles(node):
if tile.resource != None:
player.resource_deck.replenish(1, tile.resource)

# TODO: For the robot to better learn, refactor this method to use .execute via
# ActionType.BUILD_FIRST_SETTLEMENT and ActionType.BUILD_SECOND_SETTLEMENT.
# So that it learns second settlement yields.
# if action_callback:
# action_callback()

def winning_player(self):
for player in self.players:
if player.actual_victory_points >= 10:
Expand All @@ -166,70 +134,91 @@ def play_tick(self, action_callback=None):
If nothing there, fall back to (current, playable()) for current-turn.
"""
if len(self.tick_queue) > 0:
(player, actions) = self.tick_queue.pop()
(player, action_prompt) = self.tick_queue.pop(0)
else:
player = self.players[self.current_player_index]
actions = self.playable_actions(player)
(player, action_prompt) = (self.current_player(), ActionPrompt.PLAY_TURN)

actions = self.playable_actions(player, action_prompt)
action = player.decide(self.board, actions)
self.execute(action, action_callback=action_callback)

def playable_actions(self, player):
if self.moving_robber:
def playable_actions(self, player, action_prompt):
if action_prompt == ActionPrompt.BUILD_FIRST_SETTLEMENT:
return initial_settlement_possibilites(player, self.board, True)
elif action_prompt == ActionPrompt.BUILD_SECOND_SETTLEMENT:
return initial_settlement_possibilites(player, self.board, False)
elif action_prompt == ActionPrompt.BUILD_INITIAL_ROAD:
return initial_road_possibilities(player, self.board, self.actions)
elif action_prompt == ActionPrompt.MOVE_ROBBER:
return robber_possibilities(player, self.board, self.players)

if not self.current_player_has_roll:
elif action_prompt == ActionPrompt.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 settlement_possible_actions(player, self.board):
actions.append(action)
for action in city_possible_actions(player, self.board):
actions.append(action)

# Can only do if the player has not already played a development card
if player.has_year_of_plenty_card():
for action in year_of_plenty_possible_actions(player, self.resource_deck):
elif action_prompt == ActionPrompt.DISCARD:
return discard_possibilities(player)
elif action_prompt == ActionPrompt.PLAY_TURN:
actions = [Action(player, ActionType.END_TURN, None)]
for action in road_possible_actions(player, self.board):
actions.append(action)
if player.has_monopoly_card():
for action in monopoly_possible_actions(player):
for action in settlement_possible_actions(player, self.board):
actions.append(action)
for action in city_possible_actions(player, self.board):
actions.append(action)

if (
player.resource_deck.includes(ResourceDeck.development_card_cost())
and self.development_deck.num_cards() > 0
):
actions.append(Action(player, ActionType.BUY_DEVELOPMENT_CARD, None))
# TODO: Can only do if the player has not already played a development card
if player.has_year_of_plenty_card():
for action in year_of_plenty_possible_actions(
player, self.resource_deck
):
actions.append(action)
if player.has_monopoly_card():
for action in monopoly_possible_actions(player):
actions.append(action)

return actions
if (
player.resource_deck.includes(ResourceDeck.development_card_cost())
and self.development_deck.num_cards() > 0
):
actions.append(Action(player, ActionType.BUY_DEVELOPMENT_CARD, None))

def execute(self, action, initial_build_phase=False, action_callback=None):
return actions
else:
raise RuntimeError("Unknown ActionPrompt")

def execute(self, action, action_callback=None):
if action.action_type == ActionType.END_TURN:
self.current_player_index = (self.current_player_index + 1) % len(
self.players
)
self.current_player_has_roll = False
next_player_index = (self.current_player_index + 1) % len(self.players)
self.current_player_index = next_player_index
self.tick_queue.append((self.players[next_player_index], ActionPrompt.ROLL))
self.num_turns += 1
elif action.action_type == ActionType.BUILD_FIRST_SETTLEMENT:
self.board.build_settlement(
action.player.color, action.value, initial_build_phase=True
)
elif action.action_type == ActionType.BUILD_SECOND_SETTLEMENT:
self.board.build_settlement(
action.player.color, action.value, initial_build_phase=True
)
# yield resources of second settlement
for tile in self.board.get_adjacent_tiles(action.value):
if tile.resource != None:
action.player.resource_deck.replenish(1, tile.resource)
elif action.action_type == ActionType.BUILD_SETTLEMENT:
self.board.build_settlement(
action.player.color,
action.value,
initial_build_phase=initial_build_phase,
action.player.color, action.value, initial_build_phase=False
)
if not initial_build_phase:
action.player.resource_deck -= ResourceDeck.settlement_cost()
self.resource_deck += ResourceDeck.settlement_cost() # replenish bank
action.player.resource_deck -= ResourceDeck.settlement_cost()
self.resource_deck += ResourceDeck.settlement_cost() # replenish bank
elif action.action_type == ActionType.BUILD_INITIAL_ROAD:
self.board.build_road(action.player.color, action.value)
elif action.action_type == ActionType.BUILD_ROAD:
self.board.build_road(action.player.color, action.value)
if not initial_build_phase:
action.player.resource_deck -= ResourceDeck.road_cost()
self.resource_deck += ResourceDeck.road_cost() # replenish bank
action.player.resource_deck -= ResourceDeck.road_cost()
self.resource_deck += ResourceDeck.road_cost() # replenish bank
elif action.action_type == ActionType.BUILD_CITY:
self.board.build_city(action.player.color, action.value)
action.player.resource_deck -= ResourceDeck.city_cost()
Expand All @@ -243,12 +232,9 @@ def execute(self, action, initial_build_phase=False, action_callback=None):
p for p in self.players if p.resource_deck.num_cards() > 7
]
self.tick_queue.extend(
[
(p, [Action(p, ActionType.DISCARD, None)])
for p in players_to_discard
]
[(p, ActionPrompt.DISCARD) for p in players_to_discard]
)
self.moving_robber = True
self.tick_queue.append((action.player, ActionPrompt.MOVE_ROBBER))
else:
payout, depleted = yield_resources(
self.board, self.resource_deck, number
Expand All @@ -261,27 +247,22 @@ def execute(self, action, initial_build_phase=False, action_callback=None):
self.resource_deck -= resource_deck

action = Action(action.player, action.action_type, dices)
self.current_player_has_roll = True
self.tick_queue.append((action.player, ActionPrompt.PLAY_TURN))
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:
resource = player_to_steal_from.resource_deck.random_draw()
self.current_player().resource_deck.replenish(1, resource)

self.moving_robber = False
action.player.resource_deck.replenish(1, resource)
elif action.action_type == ActionType.DISCARD:
num_cards = action.player.resource_deck.num_cards()
discarded = action.player.discard()
assert len(discarded) == num_cards // 2
discarded = action.value

to_discard = ResourceDeck()
for resource in discarded:
to_discard.replenish(1, resource)

action.player.resource_deck -= to_discard
self.resource_deck += to_discard

action = Action(action.player, action.action_type, discarded)
elif action.action_type == ActionType.BUY_DEVELOPMENT_CARD:
if self.development_deck.num_cards() == 0:
raise ValueError("No more development cards")
Expand Down Expand Up @@ -361,3 +342,28 @@ def count_victory_points(self):
player.actual_victory_points = public_vps + player.development_deck.count(
DevelopmentCard.VICTORY_POINT
)


def replay_game(game):
game_copy = copy.deepcopy(game)

for player in game_copy.players:
player.public_victory_points = 0
player.actual_victory_points = 0
player.resource_deck = ResourceDeck()
player.development_deck = DevelopmentDeck()

tmp_game = Game(game_copy.players, seed=game.seed)
tmp_game.id = game_copy.id
tmp_game.players = game_copy.players # use same seating order
tmp_game.map = game_copy.map
tmp_game.board = game_copy.board
tmp_game.board.buildings = {}
tmp_game.board.robber_coordinate = filter(
lambda coordinate: tmp_game.board.tiles[coordinate].resource == None,
tmp_game.board.tiles.keys(),
).__next__()

for action in game_copy.actions:
tmp_game.execute(action)
yield tmp_game
Loading

0 comments on commit ae9448f

Please sign in to comment.