Skip to content

Commit

Permalink
Merge 9017a04 into 4176da8
Browse files Browse the repository at this point in the history
  • Loading branch information
bcollazo committed Feb 3, 2022
2 parents 4176da8 + 9017a04 commit 88570f2
Show file tree
Hide file tree
Showing 79 changed files with 608 additions and 14,929 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Coverage Status](https://coveralls.io/repos/github/bcollazo/catanatron/badge.svg?branch=master)](https://coveralls.io/github/bcollazo/catanatron?branch=master)
[![Documentation Status](https://readthedocs.org/projects/catanatron/badge/?version=latest)](https://catanatron.readthedocs.io/en/latest/?badge=latest)
[![Join the chat at https://gitter.im/bcollazo-catanatron/community](https://badges.gitter.im/bcollazo-catanatron/community.svg)](https://gitter.im/bcollazo-catanatron/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/bcollazo/catanatron/blob/master/catanatron_experimental/catanatron_experimental/notebooks/Overview.ipynb)
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/bcollazo/catanatron/blob/master/catanatron_experimental/catanatron_experimental/Overview.ipynb)

Settlers of Catan Bot Simulator. Test out bot strategies at scale (thousands of games per minutes). The goal of this project is to find the strongest Settlers of Catan bot possible.

Expand Down
43 changes: 30 additions & 13 deletions catanatron_core/catanatron/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from catanatron.models.enums import Action
from catanatron.state import State, apply_action
from catanatron.state_functions import player_key
from catanatron.models.map import BaseMap
from catanatron.models.map import CatanMap
from catanatron.models.player import Color, Player

# To timeout RandomRobots from getting stuck...
Expand All @@ -29,8 +29,11 @@ def initialize(self, game):
"""
pass

def step(self, game, action):
"""Called after each action taken by a player"""
def step(self, game_before_action, action):
"""Called after each action taken by a player.
Game should be right before action is taken.
"""
pass

def finalize(self, game):
Expand All @@ -53,23 +56,28 @@ def __init__(
self,
players: Iterable[Player],
seed: int = None,
catan_map: BaseMap = None,
discard_limit: int = 7,
vps_to_win: int = 10,
catan_map: CatanMap = None,
initialize: bool = True,
):
"""Creates a game (doesn't run it).
Args:
players (Iterable[Player]): list of players, should be at most 4.
seed (int, optional): Random seed to use (for reproducing games). Defaults to None.
catan_map (BaseMap, optional): Map configuration to use. Defaults to None.
discard_limit (int, optional): Discard limit to use. Defaults to 7.
vps_to_win (int, optional): Victory Points needed to win. Defaults to 10.
catan_map (CatanMap, optional): Map to use. Defaults to None.
initialize (bool, optional): Whether to initialize. Defaults to True.
"""
if initialize:
self.seed = seed or random.randrange(sys.maxsize)
random.seed(self.seed)

self.id = str(uuid.uuid4())
self.state = State(players, catan_map or BaseMap())
self.vps_to_win = vps_to_win
self.state = State(players, catan_map, discard_limit=discard_limit)

def play(self, accumulators=[], decide_fn=None):
"""Executes game until a player wins or exceeded TURNS_LIMIT.
Expand All @@ -84,17 +92,17 @@ def play(self, accumulators=[], decide_fn=None):
Returns:
Color: winning color or None if game exceeded TURNS_LIMIT
"""
initial_game_state = self.copy()
for accumulator in accumulators:
accumulator.initialize(self)
accumulator.initialize(initial_game_state)
while self.winning_color() is None and self.state.num_turns < TURNS_LIMIT:
action = self.play_tick(decide_fn=decide_fn)
for accumulator in accumulators:
accumulator.step(self, action)
self.play_tick(decide_fn=decide_fn, accumulators=accumulators)
final_game_state = self.copy()
for accumulator in accumulators:
accumulator.finalize(self)
accumulator.finalize(final_game_state)
return self.winning_color()

def play_tick(self, decide_fn=None):
def play_tick(self, decide_fn=None, accumulators=[]):
"""Advances game by one ply (player decision).
Args:
Expand All @@ -112,6 +120,11 @@ def play_tick(self, decide_fn=None):
if decide_fn is not None
else player.decide(self, actions)
)
# Call accumulator.step here, because we want game_before_action, action
if len(accumulators) > 0:
game_snapshot = self.copy()
for accumulator in accumulators:
accumulator.step(game_snapshot, action)
return self.execute(action)

def execute(self, action: Action, validate_action: bool = True) -> Action:
Expand All @@ -132,7 +145,10 @@ def winning_color(self) -> Union[Color, None]:
winning_player = None
for player in self.state.players:
key = player_key(self.state, player.color)
if self.state.player_state[f"{key}_ACTUAL_VICTORY_POINTS"] >= 10:
if (
self.state.player_state[f"{key}_ACTUAL_VICTORY_POINTS"]
>= self.vps_to_win
):
winning_player = player

return None if winning_player is None else winning_player.color
Expand All @@ -147,5 +163,6 @@ def copy(self) -> "Game":
game_copy = Game([], None, None, initialize=False)
game_copy.seed = self.seed
game_copy.id = self.id
game_copy.vps_to_win = self.vps_to_win
game_copy.state = self.state.copy()
return game_copy
4 changes: 2 additions & 2 deletions catanatron_core/catanatron/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from enum import Enum

from catanatron.game import Game
from catanatron.models.map import Water, Port, Tile
from catanatron.models.map import Water, Port, LandTile
from catanatron.models.player import Color
from catanatron.models.decks import Deck
from catanatron.models.enums import Resource, Action, ActionType
Expand Down Expand Up @@ -103,7 +103,7 @@ def default(self, obj):
"direction": self.default(obj.direction),
"resource": self.default(obj.resource),
}
if isinstance(obj, Tile):
if isinstance(obj, LandTile):
if obj.resource is None:
return {"id": obj.id, "type": "DESERT"}
return {
Expand Down
2 changes: 1 addition & 1 deletion catanatron_core/catanatron/models/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def city_possibilities(state, color) -> List[Action]:

def robber_possibilities(state, color) -> List[Action]:
actions = []
for coordinate, tile in state.board.map.resource_tiles:
for (coordinate, tile) in state.board.map.land_tiles.items():
if coordinate == state.board.robber_coordinate:
continue # ignore. must move robber.

Expand Down
41 changes: 22 additions & 19 deletions catanatron_core/catanatron/models/board.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,39 @@
import pickle
from collections import defaultdict
from typing import Set, Tuple
import functools

import networkx as nx

from catanatron.models.player import Color
from catanatron.models.map import BaseMap, NUM_NODES
from catanatron.models.map import (
BASE_MAP_TEMPLATE,
MINI_MAP_TEMPLATE,
NUM_NODES,
CatanMap,
)
from catanatron.models.enums import BuildingType


NODE_DISTANCES = None
EDGES = None

# Used to find relationships between nodes and edges
sample_map = BaseMap()
base_map = CatanMap(BASE_MAP_TEMPLATE)
mini_map = CatanMap(MINI_MAP_TEMPLATE)
STATIC_GRAPH = nx.Graph()
for tile in sample_map.tiles.values():
for tile in base_map.tiles.values():
STATIC_GRAPH.add_nodes_from(tile.nodes.values())
STATIC_GRAPH.add_edges_from(tile.edges.values())


@functools.lru_cache(1)
def get_node_distances():
global NODE_DISTANCES, STATIC_GRAPH
if NODE_DISTANCES is None:
NODE_DISTANCES = nx.floyd_warshall(STATIC_GRAPH)

return NODE_DISTANCES
global STATIC_GRAPH
return nx.floyd_warshall(STATIC_GRAPH)


def get_edges():
global EDGES, STATIC_GRAPH
if EDGES is None:
EDGES = list(STATIC_GRAPH.subgraph(range(NUM_NODES)).edges())
return EDGES
@functools.lru_cache(3) # None, range(54), range(24)
def get_edges(land_nodes=None):
global STATIC_GRAPH, NUM_NODES
return list(STATIC_GRAPH.subgraph(land_nodes or range(NUM_NODES)).edges())


EdgeId = Tuple[int, int]
Expand Down Expand Up @@ -60,15 +61,17 @@ class Board:

def __init__(self, catan_map=None, initialize=True):
if initialize:
self.map = catan_map or BaseMap() # Static State (no need to copy)
self.map = catan_map or CatanMap(
BASE_MAP_TEMPLATE
) # Static State (no need to copy)

self.buildings = dict() # node_id => (color, building_type)
self.roads = dict() # (node_id, node_id) => color

# color => int{}[] (list of node_id sets) one per component
# nodes in sets are incidental (might not be owned by player)
self.connected_components = defaultdict(list)
self.board_buildable_ids = set(range(NUM_NODES))
self.board_buildable_ids = set(self.map.land_nodes)
self.road_lengths = defaultdict(int)
self.road_color = None
self.road_length = 0
Expand Down Expand Up @@ -244,7 +247,7 @@ def buildable_node_ids(self, color: Color, initial_build_phase=False):
def buildable_edges(self, color: Color):
"""List of (n1,n2) tuples. Edges are in n1 < n2 order."""
global STATIC_GRAPH
buildable_subgraph = STATIC_GRAPH.subgraph(range(NUM_NODES))
buildable_subgraph = STATIC_GRAPH.subgraph(self.map.land_nodes)
expandable = set()

# non-enemy-nodes in your connected components
Expand Down
Loading

0 comments on commit 88570f2

Please sign in to comment.