Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add States Navigator to UI #279

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,4 @@ dmypy.json

# Pyre type checker
.pyre/
game_states_pickle/
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ To use, ensure you have [Docker Compose](https://docs.docker.com/compose/install
docker-compose up
```

You can now use the `--db` flag to make the catanatron-play simulator save
the game in the database for inspection via the web server.
You can now use the `--pickle` flag to make the catanatron-play simulator save
the game as local pickle files for inspection via the web server.

```
catanatron-play --players=W,W,W,W --db --num=1
catanatron-play --players=W,W,W,W --pickle --num=1
```

NOTE: A great contribution would be to make the Web UI allow to step forwards and backwards in a game to inspect it (ala chess.com).
Expand Down Expand Up @@ -159,7 +159,7 @@ open_link(game) # opens game in browser

The code is divided in the following 5 components (folders):

- **catanatron**: A pure python implementation of the game logic. Uses `networkx` for fast graph operations. Is pip-installable (see `setup.py`) and can be used as a Python package. See the documentation for the package here: https://catanatron.readthedocs.io/.
- **catanatron**: A pure python implementation of the game logic. Uses `networkx` for fast graph operations. Is pip-installable (see `setup.py`) and can be used as a Python package. See the documentation for the package here: <https://catanatron.readthedocs.io/>.

- **catanatron_server**: Contains a Flask web server in order to serve
game states from a database to a Web UI. The idea of using a database, is to ease watching games played in a different process. It defaults to using an ephemeral in-memory sqlite database. Also pip-installable (not publised in PyPi however).
Expand All @@ -168,7 +168,7 @@ The code is divided in the following 5 components (folders):

- **catantron_experimental**: A collection of unorganized scripts with contain many failed attempts at finding the best possible bot. Its ok to break these scripts. Its pip-installable. Exposes a `catanatron-play` command-line script that can be used to play games in bulk, create machine learning datasets of games, and more!

- **ui**: A React web UI to render games. This is helpful for debugging the core implementation. We decided to use the browser as a randering engine (as opposed to the terminal or a desktop GUI) because of HTML/CSS's ubiquitousness and the ability to use modern animation libraries in the future (https://www.framer.com/motion/ or https://www.react-spring.io/).
- **ui**: A React web UI to render games. This is helpful for debugging the core implementation. We decided to use the browser as a randering engine (as opposed to the terminal or a desktop GUI) because of HTML/CSS's ubiquitousness and the ability to use modern animation libraries in the future (<https://www.framer.com/motion/> or <https://www.react-spring.io/>).

## AI Bots Leaderboard

Expand Down Expand Up @@ -264,7 +264,7 @@ docker run -it -p 5000:5000 bcollazo/catanatron-server

### PostgreSQL Database

Make sure you have `docker-compose` installed (https://docs.docker.com/compose/install/).
Make sure you have `docker-compose` installed (<https://docs.docker.com/compose/install/>).

```
docker-compose up
Expand Down
2 changes: 2 additions & 0 deletions catanatron_core/catanatron/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def __init__(
self.id = str(uuid.uuid4())
self.vps_to_win = vps_to_win
self.state = State(players, catan_map, discard_limit=discard_limit)
self.state_index = 0

def play(self, accumulators=[], decide_fn=None):
"""Executes game until a player wins or exceeded TURNS_LIMIT.
Expand All @@ -130,6 +131,7 @@ def play(self, accumulators=[], decide_fn=None):
accumulator.before(self)
while self.winning_color() is None and self.state.num_turns < TURNS_LIMIT:
self.play_tick(decide_fn=decide_fn, accumulators=accumulators)
self.state_index += 1
for accumulator in accumulators:
accumulator.after(self)
return self.winning_color()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
get_player_buildings,
)
from catanatron.models.enums import VICTORY_POINT, SETTLEMENT, CITY
from catanatron_server.models import database_session, upsert_game_state
from catanatron_server.utils import ensure_link
from catanatron_server.models import serialize_game_state, save_game_metadata
from catanatron_experimental.utils import formatSecs
from catanatron_experimental.machine_learning.utils import (
get_discounted_return,
Expand Down Expand Up @@ -137,26 +136,17 @@ def get_avg_duration(self):
return sum(self.durations) / len(self.durations)


class StepDatabaseAccumulator(GameAccumulator):
"""
Saves a game state to database for each tick.
Slows game ~1s per tick.
"""

def before(self, game):
with database_session() as session:
upsert_game_state(game, session)

def step(self, game):
with database_session() as session:
upsert_game_state(game, session)

class PickleAccumulator(GameAccumulator):
"""Saves each game state to local pickle file"""
def before(self, game_before):
serialize_game_state(game_before, game_before.state_index)

class DatabaseAccumulator(GameAccumulator):
"""Saves last game state to database"""
def step(self, game_before_action, action):
serialize_game_state(game_before_action, game_before_action.state_index)

def after(self, game):
self.link = ensure_link(game)
serialize_game_state(game, game.state_index)
self.link = f"http://localhost:3000/games/{game.id}/states/{game.state_index}"


class JsonDataAccumulator(GameAccumulator):
Expand Down
23 changes: 11 additions & 12 deletions catanatron_experimental/catanatron_experimental/play.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from catanatron_experimental.cli.accumulators import (
JsonDataAccumulator,
CsvDataAccumulator,
DatabaseAccumulator,
PickleAccumulator,
StatisticsAccumulator,
VpDistributionAccumulator,
)
Expand Down Expand Up @@ -90,12 +90,11 @@ def render(self, task):
"--csv", default=False, is_flag=True, help="Save game data in CSV format."
)
@click.option(
"--db",
"--pickle",
default=False,
is_flag=True,
help="""
Save game in PGSQL database.
Expects docker-compose provided database to be up and running.
Save game states as local pickle files.
This allows games to be watched.
""",
)
Expand Down Expand Up @@ -135,7 +134,7 @@ def simulate(
output,
json,
csv,
db,
pickle,
config_discard_limit,
config_vps_to_win,
config_map,
Expand All @@ -151,7 +150,7 @@ def simulate(
catanatron-play --players=R,R,R,R --num=1000\n
catanatron-play --players=W,W,R,R --num=50000 --output=data/ --csv\n
catanatron-play --players=VP,F --num=10 --output=data/ --json\n
catanatron-play --players=W,F,AB:3 --num=1 --csv --json --db --quiet
catanatron-play --players=W,F,AB:3 --num=1 --csv --json --pickle --quiet
"""
if code:
abspath = os.path.abspath(code)
Expand All @@ -178,7 +177,7 @@ def simulate(
players.append(player)
break

output_options = OutputOptions(output, csv, json, db)
output_options = OutputOptions(output, csv, json, pickle)
game_config = GameConfigOptions(config_discard_limit, config_vps_to_win, config_map)
play_batch(
num,
Expand All @@ -196,7 +195,7 @@ class OutputOptions:
output: Union[str, None] = None # path to store files
csv: bool = False
json: bool = False
db: bool = False
pickle: bool = False


@dataclass(frozen=True)
Expand Down Expand Up @@ -268,8 +267,8 @@ def play_batch(
accumulators.append(CsvDataAccumulator(output_options.output))
if output_options.output and output_options.json:
accumulators.append(JsonDataAccumulator(output_options.output))
if output_options.db:
accumulators.append(DatabaseAccumulator())
if output_options.pickle:
accumulators.append(PickleAccumulator())
for accumulator_class in CUSTOM_ACCUMULATORS:
accumulators.append(accumulator_class(players=players, game_config=game_config))

Expand All @@ -292,7 +291,7 @@ def play_batch(
for player in players:
table.add_column(f"{player.color.value} VP", justify="right")
table.add_column("WINNER")
if output_options.db:
if output_options.pickle:
table.add_column("LINK", overflow="fold")

with Progress(
Expand Down Expand Up @@ -326,7 +325,7 @@ def play_batch(
points = get_actual_victory_points(game.state, player.color)
row.append(str(points))
row.append(rich_color(winning_color))
if output_options.db:
if output_options.pickle:
row.append(accumulators[-1].link)

table.add_row(*row)
Expand Down
11 changes: 0 additions & 11 deletions catanatron_server/catanatron_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,14 @@ def create_app(test_config=None):
CORS(app)

# ===== Load base configuration
database_url = os.environ.get("DATABASE_URL", "sqlite:///:memory:")
if database_url.startswith("postgres://"):
database_url = database_url.replace("postgres://", "postgresql://", 1)
secret_key = os.environ.get("SECRET_KEY", "dev")
app.config.from_mapping(
SECRET_KEY=secret_key,
SQLALCHEMY_DATABASE_URI=database_url,
SQLALCHEMY_TRACK_MODIFICATIONS=False,
)
if test_config is not None:
app.config.update(test_config)

# ===== Initialize Database
from catanatron_server.models import db

with app.app_context():
db.init_app(app)
db.create_all()

# ===== Initialize Routes
from . import api

Expand Down
83 changes: 63 additions & 20 deletions catanatron_server/catanatron_server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

from flask import Response, Blueprint, jsonify, abort, request

from catanatron_server.models import upsert_game_state, get_game_state
from catanatron_server.models import get_game_metadata, get_games_info, serialize_game_state, load_game_state
from catanatron.json import GameEncoder, action_from_json
from catanatron.models.player import Color, RandomPlayer
from catanatron.game import Game
from catanatron_experimental.machine_learning.players.value import ValueFunctionPlayer
from catanatron_experimental.machine_learning.players.minimax import AlphaBetaPlayer

from catanatron_gym.features import (
create_sample,
# create_sample_vector,
# get_feature_ordering,
)

bp = Blueprint("api", __name__, url_prefix="/api")

Expand All @@ -30,14 +34,18 @@ def post_game_endpoint():
players = list(map(player_factory, zip(player_keys, Color)))

game = Game(players=players)
upsert_game_state(game)
return jsonify({"game_id": game.id})


serialize_game_state(game, game.state_index)

return jsonify({
"game_id": game.id,
"state_index": game.state_index
})

# get game state by uuid and state_index
@bp.route("/games/<string:game_id>/states/<string:state_index>", methods=("GET",))
def get_game_endpoint(game_id, state_index):
state_index = None if state_index == "latest" else int(state_index)
game = get_game_state(game_id, state_index)
game = load_game_state(game_id, state_index)
if game is None:
abort(404, description="Resource not found")

Expand All @@ -48,9 +56,11 @@ def get_game_endpoint(game_id, state_index):
)


@bp.route("/games/<string:game_id>/actions", methods=["POST"])
def post_action_endpoint(game_id):
game = get_game_state(game_id)
@bp.route("/games/<string:game_id>/states/<string:state_index>/actions", methods=["POST"])
def post_action_endpoint(game_id, state_index):
state_index = None if state_index == "latest" else int(state_index)

game = load_game_state(game_id, state_index)
if game is None:
abort(404, description="Resource not found")

Expand All @@ -65,11 +75,11 @@ def post_action_endpoint(game_id):
body_is_empty = (not request.data) or request.json is None
if game.state.current_player().is_bot or body_is_empty:
game.play_tick()
upsert_game_state(game)
serialize_game_state(game, game.state_index)
else:
action = action_from_json(request.json)
game.execute(action)
upsert_game_state(game)
serialize_game_state(game, game.state_index)

return Response(
response=json.dumps(game, cls=GameEncoder),
Expand All @@ -96,15 +106,48 @@ def stress_test_endpoint():


# ===== Debugging Routes
# @app.route(
# "/games/<string:game_id>/players/<int:player_index>/features", methods=["GET"]
# )
# def get_game_feature_vector(game_id, player_index):
# game = get_game_state(game_id)
# if game is None:
# abort(404, description="Resource not found")
@bp.route(
"/games/<string:game_id>/players/<int:player_index>/features", methods=["GET"]
)
def get_game_feature_vector(game_id, player_index):
# game = get_game(game_id)
game = load_game_state(game_id, None)
if game is None:
abort(404, description="Resource not found")

return create_sample(game, game.state.colors[player_index])


# get game info
@bp.route(
"/games/<string:game_id>/info", methods=["GET"]
)
def get_game_info(game_id):
game_metadata = get_game_metadata(game_id)

if game_metadata is None:
abort(404, description="Metadata not found")

return jsonify({
"game_states_count": game_metadata["game_states_count"],
"winner": game_metadata["winner"],
"players": game_metadata["players"]
})

# get general info
@bp.route(
"/info", methods=["GET"]
)
def get_info():
game_count, games_uuid = get_games_info()

# games_uuid = [str(game_id[0]) for game_id in games_uuid] # convert to string

# return create_sample(game, game.state.colors[player_index])
return jsonify({
# "game_states_count": game_states_count,
"games_count": game_count,
"games_uuid": games_uuid
})


# @app.route("/games/<string:game_id>/value-function", methods=["GET"])
Expand Down
Loading