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. 76
      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 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')

9
server/hra/game.py

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

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

76
server/hra/web/api.py

@ -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({
"status": "working",
"wait": 1.0,
})
return json.dumps({ return json.dumps({
"status": "ok",
"round": state.round, "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 redirect('/')
return render_template("org_su.html", f=f) return render_template("org_su.html", f=f)

2
server/setup.py

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

Loading…
Cancel
Save