diff --git a/server/bin/control_game b/server/bin/control_game new file mode 100755 index 0000000..5466d4d --- /dev/null +++ b/server/bin/control_game @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +import hra.db as db +import hra.lib as lib +from hra.util import hash_passwd + +import argparse +from sqlalchemy import exc, update +import sys + +g = db.Game(game_mode="", configuration={}, teams_count=1) + +parser = argparse.ArgumentParser() +parser.add_argument("game_id") +parser.add_argument("--step", action="store_true") +parser.add_argument("--restore", action="store_true") + +args = parser.parse_args() + +if args.restore: + lib.game_restore_broken(args.game_id) +if args.step: + lib.game_step(args.game_id) diff --git a/server/bin/create_game b/server/bin/create_game new file mode 100755 index 0000000..9e75e1d --- /dev/null +++ b/server/bin/create_game @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +from hra.game import logic_by_mode +import hra.db as db +import hra.lib as lib + +import sys +from sqlalchemy import exc, update + + +mode = "occupy" +teams_count = 6 +configuration = {} +g = db.Game(game_mode=mode, configuration=configuration, teams_count=teams_count) + +db.get_session().add(g) +db.get_session().commit() + +s = db.State(game_id=g.game_id, round=0, state=g.get_logic().zero_state()) + +db.get_session().add(s) +g.current_round = 0 +g.working_on_next_state = False +db.get_session().commit() + +print(f"Přidána hra {g.game_id}. ") diff --git a/server/hra/db.py b/server/hra/db.py index b782830..1e28fac 100644 --- a/server/hra/db.py +++ b/server/hra/db.py @@ -16,6 +16,7 @@ import secrets import string import hra.config as config +import hra.game Base = declarative_base() metadata = Base.metadata @@ -65,17 +66,26 @@ class Game(Base): configuration = Column(JSONB, nullable=False) game_mode = Column(String(80), nullable=False) teams_count = Column(Integer, nullable=False) + working_on_next_state = Column(Boolean, nullable=False, default=True) + current_round = Column(Integer, default=-1) + def current_state(self) -> Optional['State']: + if self.working_on_next_state is True: + return None + return get_session().query(State).filter_by(game_id=self.game_id, round=self.current_round).order_by(State.round.desc()).first() - def current_state(self) -> 'State': - return get_session().query(State).filter_by(game_id=self.game_id).order_by(State.round.desc()).first() + def get_logic(self) -> 'hra.game.Logic': + return hra.game.logic_by_mode[self.game_mode](self.teams_count, self.configuration) + + def lock(self) -> 'Game': + return get_session().query(Game).filter_by(game_id=self.game_id).with_for_update().first() class State(Base): __tablename__ = 'states' game_id = Column(Integer, ForeignKey('games.game_id'), primary_key=True, nullable=False) - round = Column(Integer) + round = Column(Integer, primary_key=True) state = Column(JSONB) game = relationship('Game', primaryjoin='State.game_id == Game.game_id') @@ -84,8 +94,8 @@ class Move(Base): __tablename__ = 'moves' game_id = Column(Integer, ForeignKey('games.game_id'), primary_key=True, nullable=False) - round = Column(Integer) - team_id = Column(Integer) + round = Column(Integer, primary_key=True) + team_id = Column(Integer, primary_key=True) move = Column(JSONB) game = relationship('Game', primaryjoin='Move.game_id == Game.game_id') diff --git a/server/hra/game.py b/server/hra/game.py index ea1db6e..aa9c26e 100644 --- a/server/hra/game.py +++ b/server/hra/game.py @@ -12,15 +12,18 @@ class Logic: def zero_state(self) -> Any: return {} - def step(self, state: Any, actions: List[Optional[Any]], round_id: int) -> Tuple[Any, List[int]]: - return {}, [0] * teams_count # new_state, add_points for each team + def step(self, state: Any, moves: List[Optional[Any]], round_id: int) -> Tuple[Any, List[int]]: + return {}, [0] * self.teams_count # new_state, add_points for each team - def validate_step(self, state: Any, team_id: int, action: Any, round_id: int) -> Union[None, Any]: + def validate_move(self, state: Any, team_id: int, move: Any, round_id: int) -> Union[None, Any]: return None # Bez chyby # return {"status": "warning", ... } # Drobná chyba, ale tah provedu - # throw Exception("Chybí povinná ...") # Zásadní chyba, tah neuznán + # throw Exception("Chybí povinná ...") # Zásadní chyba, tah neuznán # Když používají našeho klienta, tak by se toto nemělo stát + def personalize_state(self, state: Any, team_id: int, round_id: int) -> Any: + return state + @add_logic class Occupy(Logic): pass diff --git a/server/hra/lib.py b/server/hra/lib.py new file mode 100644 index 0000000..ae402aa --- /dev/null +++ b/server/hra/lib.py @@ -0,0 +1,50 @@ +import json +import hra.config as config +import hra.db as db + +class DuplicitMakeStep(Exception): + pass + +def game_step(game_id: int): + ses = db.get_session() + + ses.expire_all() + game = ses.query(db.Game).filter_by(game_id=game_id).with_for_update().one_or_none() + assert game is not None + if game.working_on_next_state: + ses.commit() + raise DuplicitMakeStep() + game.working_on_next_state = True + ses.commit() + + old_round_id = game.current_round + new_round_id = old_round_id + 1 + old_state = ses.query(db.State).filter_by(game_id=game.game_id, round=old_round_id).one_or_none() + + moves = [None for _ in range(game.teams_count)] + for i in ses.query(db.Move).filter_by(game_id=game.game_id, round=old_round_id).all(): + moves[i.team_id] = i.move + + ses.commit() + + x, points = game.get_logic().step(old_state.state, moves, old_round_id) + + new_state = db.State(game_id=game.game_id, round=new_round_id, state=x) + ses.add(new_state) + + ses.expire_all() + game = ses.query(db.Game).filter_by(game_id=game_id).with_for_update().one_or_none() + assert game is not None + assert game.working_on_next_state + game.current_round = new_round_id + game.working_on_next_state = False + ses.commit() + +def game_restore_broken(game_id: int) -> None: + ses = db.get_session() + ses.expire_all() + game = ses.query(db.Game).filter_by(game_id=game_id).with_for_update().one_or_none() + game.working_on_next_state = False + ses.commit() + + diff --git a/server/hra/web/api.py b/server/hra/web/api.py index 0a79a5d..d994d7a 100644 --- a/server/hra/web/api.py +++ b/server/hra/web/api.py @@ -1,32 +1,94 @@ from flask import Flask, redirect, flash, render_template, session, g, request, get_flashed_messages import werkzeug.exceptions -import time -from datetime import datetime import json import hra.config as config -import hra.web.html as html import hra.db as db from hra.web import app, NeedLoginError import hra.web.jinja_mac as jinja_mac +def args_get(name, type, optional=False, default=None): + v = request.args.get(name) + if v is None: + if optional: + return default + else: + raise werkzeug.exceptions.BadRequest(f"Missing mandatory option {name}.") + try: + return type(v) + except ValueError: + raise werkzeug.exceptions.BadRequest(f"Option {name} have wrong type.") + def get_context(): if g.user is None: raise NeedLoginError - game_id = request.args.get('game') or 1 - team_id = request.args.get('team') or 0 + game_id = args_get("game", int, True, 1) + team_id = args_get('team', int, True, 0) game = db.get_session().query(db.Game).filter_by(game_id=game_id).first() - print(game_id, game, team_id) if game is None: raise werkzeug.exceptions.NotFound("No such game") return game, team_id + @app.route("/api/state", methods=['GET']) def api_state(): - game, team_id = get_context() + game, team_id = get_context() state = game.current_state() + if state is None: + return json.dumps({ + "status": "working", + "wait": 1.0, + }) return json.dumps({ + "status": "ok", "round": state.round, - "state": state.state, + "team_id": team_id, + "state": game.get_logic().personalize_state(state.state, team_id, state.round), + }) + +@app.route("/api/action", methods=['POST']) +def api_action(): + def to_late(): + return json.dumps({ + "status": "too-late", + }) + + game, team_id = get_context() + j = request.get_json() + state = game.current_state() + round_id = args_get('round', int) + if round_id < 0 or round_id > game.current_round: + raise werkzeug.exceptions.BadRequest("Wrong round.") + if game.working_on_next_state or round_id < game.current_round: + return to_late() + + try: + warnings = game.get_logic().validate_move(state.state, team_id, j, round_id) + except Exception as e: + return json.dumps({ + "error": "error", + "description": str(e) }) + + db.get_session().expire_all() + game = game.lock() + + if game.working_on_next_state or round_id < game.current_round: + db.get_session().commit() + return to_late() + + move = db.get_session().query(db.Move).filter_by(game_id=game.game_id, team_id=team_id, round=round_id).one_or_none() + if move is None: + move = db.Move(game_id=game.game_id, team_id=team_id, round=round_id) + db.get_session().add(move) + + move.move = j + + db.get_session().commit() + + if warnings is not None: + return json.dumps(warnings) + return json.dumps({ + "status": "ok", + }) diff --git a/server/hra/web/pages.py b/server/hra/web/pages.py index 37c8f6b..d3faac2 100644 --- a/server/hra/web/pages.py +++ b/server/hra/web/pages.py @@ -248,5 +248,3 @@ def web_su(): return redirect('/') return render_template("org_su.html", f=f) - - diff --git a/server/setup.py b/server/setup.py index e560876..08793d9 100644 --- a/server/setup.py +++ b/server/setup.py @@ -10,6 +10,8 @@ setuptools.setup( scripts=[ "bin/db_init", "bin/create_root", + "bin/create_game", + "bin/control_game", ], include_package_data=True, zip_safe=False,