Strategická: Dokončení API v0.1

This commit is contained in:
Jiří Kalvoda 2022-09-12 17:47:58 +02:00
parent 20a418e932
commit c9da8dca60
8 changed files with 193 additions and 21 deletions

22
server/bin/control_game Executable file
View file

@ -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)

25
server/bin/create_game Executable file
View file

@ -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}. ")

View file

@ -16,6 +16,7 @@ import secrets
import string import string
import hra.config as config import hra.config as config
import hra.game
Base = declarative_base() Base = declarative_base()
metadata = Base.metadata metadata = Base.metadata
@ -65,17 +66,26 @@ class Game(Base):
configuration = Column(JSONB, nullable=False) configuration = Column(JSONB, nullable=False)
game_mode = Column(String(80), nullable=False) game_mode = Column(String(80), nullable=False)
teams_count = Column(Integer, 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': def get_logic(self) -> 'hra.game.Logic':
return get_session().query(State).filter_by(game_id=self.game_id).order_by(State.round.desc()).first() 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): class State(Base):
__tablename__ = 'states' __tablename__ = 'states'
game_id = Column(Integer, ForeignKey('games.game_id'), primary_key=True, nullable=False) 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) state = Column(JSONB)
game = relationship('Game', primaryjoin='State.game_id == Game.game_id') game = relationship('Game', primaryjoin='State.game_id == Game.game_id')
@ -84,8 +94,8 @@ class Move(Base):
__tablename__ = 'moves' __tablename__ = 'moves'
game_id = Column(Integer, ForeignKey('games.game_id'), primary_key=True, nullable=False) game_id = Column(Integer, ForeignKey('games.game_id'), primary_key=True, nullable=False)
round = Column(Integer) round = Column(Integer, primary_key=True)
team_id = Column(Integer) team_id = Column(Integer, primary_key=True)
move = Column(JSONB) move = Column(JSONB)
game = relationship('Game', primaryjoin='Move.game_id == Game.game_id') game = relationship('Game', primaryjoin='Move.game_id == Game.game_id')

View file

@ -12,15 +12,18 @@ class Logic:
def zero_state(self) -> Any: def zero_state(self) -> Any:
return {} return {}
def step(self, state: Any, actions: List[Optional[Any]], round_id: int) -> Tuple[Any, List[int]]: def step(self, state: Any, moves: List[Optional[Any]], round_id: int) -> Tuple[Any, List[int]]:
return {}, [0] * teams_count # new_state, add_points for each team 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 None # Bez chyby
# return {"status": "warning", ... } # Drobná chyba, ale tah provedu # 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 # 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 @add_logic
class Occupy(Logic): class Occupy(Logic):
pass pass

50
server/hra/lib.py Normal file
View file

@ -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()

View file

@ -1,32 +1,94 @@
from flask import Flask, redirect, flash, render_template, session, g, request, get_flashed_messages from flask import Flask, redirect, flash, render_template, session, g, request, get_flashed_messages
import werkzeug.exceptions import werkzeug.exceptions
import time
from datetime import datetime
import json import json
import hra.config as config import hra.config as config
import hra.web.html as html
import hra.db as db import hra.db as db
from hra.web import app, NeedLoginError from hra.web import app, NeedLoginError
import hra.web.jinja_mac as jinja_mac 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(): def get_context():
if g.user is None: if g.user is None:
raise NeedLoginError raise NeedLoginError
game_id = request.args.get('game') or 1 game_id = args_get("game", int, True, 1)
team_id = request.args.get('team') or 0 team_id = args_get('team', int, True, 0)
game = db.get_session().query(db.Game).filter_by(game_id=game_id).first() game = db.get_session().query(db.Game).filter_by(game_id=game_id).first()
print(game_id, game, team_id)
if game is None: if game is None:
raise werkzeug.exceptions.NotFound("No such game") raise werkzeug.exceptions.NotFound("No such game")
return game, team_id return game, team_id
@app.route("/api/state", methods=['GET']) @app.route("/api/state", methods=['GET'])
def api_state(): def api_state():
game, team_id = get_context() game, team_id = get_context()
state = game.current_state() state = game.current_state()
if state is None:
return json.dumps({ return json.dumps({
"round": state.round, "status": "working",
"state": state.state, "wait": 1.0,
})
return json.dumps({
"status": "ok",
"round": state.round,
"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",
}) })

View file

@ -248,5 +248,3 @@ def web_su():
return redirect('/') return redirect('/')
return render_template("org_su.html", f=f) return render_template("org_su.html", f=f)

View file

@ -10,6 +10,8 @@ setuptools.setup(
scripts=[ scripts=[
"bin/db_init", "bin/db_init",
"bin/create_root", "bin/create_root",
"bin/create_game",
"bin/control_game",
], ],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,