Browse Source

Strategická: Dokončení API v0.1

master
Jiří Kalvoda 2 years ago
parent
commit
c9da8dca60
  1. 22
      server/bin/control_game
  2. 25
      server/bin/create_game
  3. 20
      server/hra/db.py
  4. 9
      server/hra/game.py
  5. 50
      server/hra/lib.py
  6. 78
      server/hra/web/api.py
  7. 2
      server/hra/web/pages.py
  8. 2
      server/setup.py

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

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

20
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')

9
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
# 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

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

78
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",
})

2
server/hra/web/pages.py

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

2
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,

Loading…
Cancel
Save