diff --git a/server/bin/create_game b/server/bin/create_game index e3507f5..d437f70 100755 --- a/server/bin/create_game +++ b/server/bin/create_game @@ -9,12 +9,12 @@ from sqlalchemy import exc, update ses = db.get_session() mode = "occupy" -teams_count = 6 +teams_count = 16 configuration = { - "teams_width": 2, - "teams_height": 3, - "width_per_team": 10, - "height_per_team": 10, + "teams_width": 4, + "teams_height": 4, + "width_per_team": 30, + "height_per_team": 30, } g = db.Game(game_mode=mode, configuration=configuration, teams_count=teams_count) diff --git a/server/hra/db.py b/server/hra/db.py index 8467c58..bb1afaf 100644 --- a/server/hra/db.py +++ b/server/hra/db.py @@ -59,6 +59,9 @@ class User(Base): def __repr__(self): return '' % self.username + def print(self): + return self.username + (" (org)" if self.org else "") + class Game(Base): __tablename__ = 'games' @@ -69,8 +72,8 @@ class Game(Base): 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: + def current_state(self, none_if_working=True) -> Optional['State']: + if none_if_working and 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() @@ -82,6 +85,9 @@ class Game(Base): ses.expire_all() return ses.query(Game).filter_by(game_id=self.game_id).with_for_update().first() + def print(self): + return f"{self.game_id}: " + class Team(Base): __tablename__ = 'base' diff --git a/server/hra/util.py b/server/hra/util.py index c578a72..76aec67 100644 --- a/server/hra/util.py +++ b/server/hra/util.py @@ -1,7 +1,16 @@ import bcrypt +from time import time def hash_passwd(a): salt = b'$2b$12$V2aIKSJC/uozaodwYnQX3e' hashed = bcrypt.hashpw(a.encode('utf-8'), salt) return hashed.decode('us-ascii') +def timer_func(func): + def wrap_func(*args, **kwargs): + t1 = time() + result = func(*args, **kwargs) + t2 = time() + print(f'Function {func.__name__!r} executed in {(t2-t1):.4f}s') + return result + return wrap_func diff --git a/server/hra/web/api.py b/server/hra/web/api.py index 12e7449..5059925 100644 --- a/server/hra/web/api.py +++ b/server/hra/web/api.py @@ -70,7 +70,7 @@ def api_action(): except Exception as e: return json.dumps({ "error": "error", - "description": str(e) + "description": f"{type(e).__name__}: {e}" }) db.get_session().expire_all() diff --git a/server/hra/web/game.py b/server/hra/web/game.py new file mode 100644 index 0000000..ec008c9 --- /dev/null +++ b/server/hra/web/game.py @@ -0,0 +1,101 @@ +from flask import Flask, redirect, flash, session, g, request, get_flashed_messages, Markup +from wtforms import Form, BooleanField, StringField, PasswordField, validators, SubmitField, IntegerField, DateTimeField +from wtforms.validators import ValidationError +from flask_wtf import FlaskForm +from flask_bootstrap import Bootstrap +import time +from datetime import datetime +from sqlalchemy import exc, update +import werkzeug.exceptions +import wtforms +from wtforms.fields import EmailField +from wtforms.widgets import NumberInput +from typing import Optional, Any + +import hra.config as config +import hra.web.html as html +import hra.db as db +from hra.web import app +import hra.web.jinja_mac as jinja_mac +from hra.util import hash_passwd +from hra.web.pages import BasePage + + +wlogic_by_mode = {} + +def add_wlogic(cls): + wlogic_by_mode[cls.__name__.lower()] = cls + +def get_wlogic(game): + if game.game_mode in wlogic_by_mode: + cls = wlogic_by_mode[game.game_mode] + else: + cls = WLogic + return cls(game) + + +class WLogic: + def __init__(self, game): + self.game = game + self.logic = game.get_logic() + + +@add_wlogic +class Occupy(WLogic): + def view(self, state: db.State, team: Optional[db.Team]): + s = state.state + if team is not None: + s = self.logic.personalize_state(s, team.team_id, state.round) + b = BasePage() + b.h2(f"Hra {self.game.print()} kolo {state.round}") + with b.table(_class="game_table"): + for i, row in enumerate(s["map"]): + with b.tr(): + for j, x in enumerate(row): + occupied_by_team = x["occupied_by_team"] + home_for_team = x["home_for_team"] + members = x["members"] + with b.td(): + classes = [] + with b.a(href=f"#cell_{i}_{j}"): + if x["hill"]: + classes.append("game_hill") + b(Markup(" ")) + else: + if home_for_team is not None: + classes.append(f'game_home') + if occupied_by_team is not None: + classes.append(f'game_occupied') + classes.append(f'game_occupied_by_{occupied_by_team}') + if len(members): + b(len(members)) + else: + b(Markup(" ")) + b(_class=" ".join(classes)) + + for i, row in enumerate(s["map"]): + for j, x in enumerate(row): + occupied_by_team = x["occupied_by_team"] + home_for_team = x["home_for_team"] + members = x["members"] + with b.div(id=f"cell_{i}_{j}", _class="game_tab"): + b.h4(f"Políčko {i} {j}") + if x["hill"]: + b.p().b("Pohoří") + else: + if occupied_by_team is not None: + b.p(_class=f"game_team_{occupied_by_team}").b(f"Obsazeno týmem: {occupied_by_team}") + if home_for_team is not None: + b.p(_class=f"game_team_{home_for_team}").b(f"Domov týmu: {home_for_team}") + b.p().b(f"Počet osob: {len(members)}") + with b.ul(): + for m in members: + b.li(_class=f"game_team_{home_for_team}")(f"Voják {m['id']} týmu {m['team']}") + + + b.wrap( + limited_size=False, + sticky_head=False, + head=lambda x:x.link(rel="stylesheet", href=app.url_for('static', filename='occupy.css'), type='text/css', media="all") + ) + return b.print_file() diff --git a/server/hra/web/html.py b/server/hra/web/html.py index 4a9e26e..dffd8ba 100644 --- a/server/hra/web/html.py +++ b/server/hra/web/html.py @@ -116,7 +116,7 @@ class Tag(Bucket): self.name = name self.attributes = attributes - def add_attribute(k, v): + def add_attribute(self, k, v): self.attributes.append((k, v)) def format_attributes(self): @@ -202,24 +202,25 @@ def remove_leading_underscore(s): class WrapAfterBuilder(Builder): def __init__(self, f): super().__init__(Bucket(self)) - self._wrap_done = False - self._wrap_function = f + self.wrap_done = False + self.wrap_function = f - def _wrap(self, *arg, **kvarg): - if self._wrap_done: + def wrap(self, *arg, **kvarg): + if self.wrap_done: return - self._wrap_done = True + self.wrap_done = True content = self.root_tag.content self.root_tag = None self.current_tag = None - self.root_tag = self._wrap_function(self, content, *arg, **kvarg) or self.root_tag + self.root_tag = self.wrap_function(self, content, *arg, **kvarg) or self.root_tag + return self def print(self, *arg, **kvarg): - self._wrap() + self.wrap() return super().print(*arg, **kvarg) def print_file(self, *arg, **kvarg): - self._wrap() + self.wrap() return super().print_file(*arg, **kvarg) diff --git a/server/hra/web/pages.py b/server/hra/web/pages.py index df11bb4..e9a6fc1 100644 --- a/server/hra/web/pages.py +++ b/server/hra/web/pages.py @@ -5,11 +5,7 @@ from flask_wtf import FlaskForm from flask_bootstrap import Bootstrap import time from datetime import datetime -from flask_sqlalchemy import SQLAlchemy from sqlalchemy import exc, update -import hashlib -import bcrypt -import os import werkzeug.exceptions import wtforms from wtforms.fields import EmailField @@ -22,8 +18,9 @@ from hra.web import app import hra.web.jinja_mac as jinja_mac from hra.util import hash_passwd + @html.WrapAfterBuilder_decorator -def BasePage(b, content): +def BasePage(b, content, head=lambda x:None, limited_size=True, sticky_head=True): b.current_tag = html.Tag(b, "html", []) b.root_tag = b.current_tag with b.head(): @@ -33,13 +30,14 @@ def BasePage(b, content): b.link(rel="stylesheet", href=app.url_for('static', filename='ksp-mhd.css'), type='text/css', media="all") b.link(rel="icon", type="image/png", sizes="16x16", href="static/favicon.ico") b.link(rel="shortcut icon", href=app.url_for('static', filename='img/favicon.ico')) + b(head) with b.body() as body: with b.header(_class=f"flavor-{config.WEB_FLAVOR}"): - with b.div(_class="content"): + with b.div(_class="content content_limited" if limited_size else "content"): with b.a(href="/", title="Na hlavní stránku"): b.img(src=app.url_for('static', filename='hippo.png'), style="width: 60px;height: auto;", alt="KSP") b.h1()("Hra na soustředění KSP") - with b.div(id="nav-wrapper"): + with b.div(id="nav-wrapper", _class="nav-wrapper-sticky" if sticky_head else ""): with b.nav(id="main-menu", _class="content"): for item in g.menu: b.a(href=item.url)(item.name) @@ -78,11 +76,36 @@ def user_link(user): b("-") else: if g.org: - b.a(href=app.url_for(web_org_user.__name__, user_id=user.id))(user.username) + b.a(href=app.url_for(web_org_user.__name__, user_id=user.id))(user.print()) else: - b(user.username) + b(user.print()) + return b.root_tag + + +def right_for_game(game): + if g.org: + return True + if g.user is None: + return False + return db.get_session().query(db.Team).filter_by(game_id=game_id, user=g.user.id).one_or_none() is not None + + +def right_for_team(team): + if g.org: + return True + if g.user is None: + return False + return team.user_id == g.user.id + + +def game_link(game): + b = html.Builder() + with b.line(): + if game is None: + b("-") + else: + b.a(href=app.url_for(web_game.__name__, game_id=game.game_id))(game.print()) return b.root_tag - class RegistrationForm(FlaskForm): @@ -175,9 +198,22 @@ def print_time(t): @app.route("/", methods=['GET', 'POST']) def web_index(): b = BasePage() + if g.user: - with b.p(): - b(f"Váš token je: {g.user.token}") + teams = db.get_session().query(db.Team).filter_by(user_id=g.user.id).order_by(db.Team.game_id).all() + games = [] + for t in teams: + if len(games) == 0 or games[-1].game_id != t.game_id: + games.append(t.game) + + b.line().h3("Token") + b.line().p().b(f"Váš token je: {g.user.token}") + b.line().h3("Vaše hry") + for game in games: + b.line().p(game_link(game)) + else: + b.line().p("Přihlaste se, prosím.") + return b.print_file() @app.route("/game/", methods=['GET']) @@ -187,30 +223,66 @@ def web_game(game_id): teams = ses.query(db.Team).filter_by(game_id=game_id).order_by(db.Team.team_id).all() if game is None: raise werkzeug.exceptions.NotFound() + if not right_for_game(game): + raise werkzeug.exceptions.Forbidden() b = BasePage() + b.h2("Hra ", game.print()) with b.p().table(_class="data full"): with b.thead(): b.line().th()("Id") b.line().th()("User") - if g.org: - b.line().th()("Akce") + b.line().th()("Akce") for team in teams: with b.tr(): b.line().td()(team.team_id) b.line().td()(user_link(team.user),": ", team.name) - if g.org: - with b.td(): - with b.div(_class="btn-group", role="group"): - b.a(_class="btn btn-xs btn-primary", href=app.url_for(web_org_game_userchange.__name__, game_id=game.game_id, team_id=team.team_id))("Změnit uživatele") - #b.a(_class="btn btn-xs btn-primary", href=app.url_for(web_game.__name__, game_id=g.game_id))("Detail") + with b.td(): + with b.div(_class="btn-group", role="group"): + if right_for_team(team): + b.a(_class="btn btn-xs btn-primary", href=app.url_for(web_game_view.__name__, game_id=game.game_id, team_id=team.team_id))("Zobrazit hru") + if g.org: + b.a(_class="btn btn-xs btn-default", href=app.url_for(web_org_game_userchange.__name__, game_id=game.game_id, team_id=team.team_id))("Změnit uživatele") return b.print_file() +@app.route("/game//view", methods=['GET']) +@app.route("/game//view/", methods=['GET']) +def web_game_view(game_id, team_id=None): + ses = db.get_session() + game = ses.query(db.Game).filter_by(game_id=game_id).one_or_none() + if game is None: + raise werkzeug.exceptions.NotFound() + if not right_for_game(game): + raise werkzeug.exceptions.Forbidden() + if team_id is not None: + team = ses.query(db.Team).filter_by(game_id=game_id, team_id=team_id).one_or_none() + if game is None: + raise werkzeug.exceptions.NotFound() + if not right_for_team(team): + raise werkzeug.exceptions.Forbidden() + else: + team = None + + wl = get_wlogic(game) + state = game.current_state() + + if not hasattr(wl, "view"): + raise werkzeug.exceptions.NotFound() + + if state is None: + raise werkzeug.exceptions.NotFound() + + return wl.view(state, team) + + + + @app.route("/org/games") def web_org_games(): games = db.get_session().query(db.Game).order_by(db.Game.game_id).all() b = BasePage() + b.h2("Hry") with b.p().table(_class="data full"): with b.thead(): b.line().th()("Id") @@ -294,7 +366,13 @@ def web_org_game_userchange(game_id, team_id): @app.route("/org/users") def web_org_users(): users = db.get_session().query(db.User).order_by(db.User.id).all() + teams = db.get_session().query(db.Team).all() + games_for_user = {u.id: set() for u in users} + for t in teams: + if t.user_id is not None: + games_for_user[t.user_id].add(t.game) b = BasePage() + b.h2("Uživatelé") with b.p().table(_class="data full"): with b.thead(): b.line().th()("Username") @@ -302,8 +380,10 @@ def web_org_users(): b.line().th()("Akce") for u in users: with b.tr(): - b.line().td()(u.username) - b.line().td() + b.line().td()(user_link(u)) + with b.td().ul(): + for g in games_for_user[u.id]: + b.line().li(game_link(g)) with b.td(): with b.div(_class="btn-group", role="group"): b.a(_class="btn btn-xs btn-primary", href=app.url_for(web_org_user.__name__, user_id=u.id))("Detail") @@ -314,11 +394,23 @@ def web_org_user(user_id): user = db.get_session().query(db.User).filter_by(id=user_id).one_or_none() if not user: raise werkzeug.exceptions.NotFound() + teams = db.get_session().query(db.Team).filter_by(user_id=user_id).order_by(db.Team.game_id).all() b = BasePage() - b.line().h2("Uživatel ", user.username) + b.line().h2("Uživatel ", user.print()) with b.div(_class="btn-group", role="group"): with b.form(method="POST", _class="btn-group", action=app.url_for(web_org_user_su.__name__, user_id=user.id)): b.button(_class="btn btn-default", type="submit", name="su", value="yes")("Převtělit") + b.h3("Týmy") + with b.p().table(_class="data full"): + with b.thead(): + b.line().th()("Hra") + b.line().th()("Jméno z pohledu týmu") + b.line().th()("Číslo týmu") + for team in teams: + with b.tr(): + b.line().td()(game_link(team.game)) + b.line().td()(team.name) + b.line().td()(team.team_id) return b.print_file() @app.route("/org/user//su", methods=['POST']) @@ -327,3 +419,5 @@ def web_org_user_su(user_id): session['uid'] = user.id flash("Uživatel vtělen!") return redirect('/') + +from hra.web.game import get_wlogic, wlogic_by_mode diff --git a/server/static/ksp-mhd.css b/server/static/ksp-mhd.css index 8101897..c794054 100644 --- a/server/static/ksp-mhd.css +++ b/server/static/ksp-mhd.css @@ -11,10 +11,13 @@ body { display: block; margin: 0 auto; padding: 0 1em; - max-width: 1000px; width: 100%; } +.content_limited, main { + max-width: 1000px; +} + header { padding: 10px 0px; } @@ -39,8 +42,10 @@ header img { height: 60px; } header h1 { margin: auto 20px 0px; color: #222; } -#nav-wrapper { +.nav-wrapper-sticky { position: sticky; +} +#nav-wrapper { top: 0; background-color: #222; border: 1px #222 solid; diff --git a/server/static/occupy.css b/server/static/occupy.css new file mode 100644 index 0000000..0af694c --- /dev/null +++ b/server/static/occupy.css @@ -0,0 +1,135 @@ + + +table.game_table, table.game_table tr, table.game_table tr td { + border: thin solid black; + border-collapse: collapse; + table-layout: fixed; +} + +table.game_table tr td { + width: 10pt; + height: 10px; + font-size: 8px; + text-align: end; +} + +td.game_home { + border: 4pt solid black; +} +td.game_hill { + background-color: black; +} +td.game_occupied { + color: white; +} +td.game_occupied_by_0 { + background-color: #A50000; +} +td.game_occupied_by_1 { + background-color: #00A6A6; +} +td.game_occupied_by_2 { + background-color: #53A600; +} +td.game_occupied_by_3 { + background-color: #5300A6; +} +td.game_occupied_by_4 { + background-color: #A67D00; +} +td.game_occupied_by_5 { + background-color: #002AA6; +} +td.game_occupied_by_6 { + background-color: #00A62A; +} +td.game_occupied_by_7 { + background-color: #A6007D; +} +td.game_occupied_by_8 { + background-color: #A63D00; +} +td.game_occupied_by_9 { + background-color: #0069A6; +} +td.game_occupied_by_10 { + background-color: #16A600; +} +td.game_occupied_by_11 { + background-color: #9000A6; +} +td.game_occupied_by_12 { + background-color: #93A600; +} +td.game_occupied_by_13 { + background-color: #1300A6; +} +td.game_occupied_by_14 { + background-color: #00A667; +} +td.game_occupied_by_15 { + background-color: #A60040; +} + +.game_team_0 { + color: #A50000; +} +.game_team_1 { + color: #00A6A6; +} +.game_team_2 { + color: #53A600; +} +.game_team_3 { + color: #5300A6; +} +.game_team_4 { + color: #A67D00; +} +.game_team_5 { + color: #002AA6; +} +.game_team_6 { + color: #00A62A; +} +.game_team_7 { + color: #A6007D; +} +.game_team_8 { + color: #A63D00; +} +.game_team_9 { + color: #0069A6; +} +.game_team_10 { + color: #16A600; +} +.game_team_11 { + color: #9000A6; +} +.game_team_12 { + color: #93A600; +} +.game_team_13 { + color: #1300A6; +} +.game_team_14 { + color: #00A667; +} +.game_team_15 { + color: #A60040; +} + +.game_tab { + display: none; +} +.game_tab:target { + display: block; +} + +table.game_table tr td a, table.game_table tr td a:hover, table.game_table tr td a:visited, table.game_table tr td a:active { + display:block; + text-decoration:none; + color: inherit; + text-decoration: none; +}