Browse Source

Strategická: Vizualizátko + drobnosti

master
Jiří Kalvoda 2 years ago
parent
commit
c76f21fc3b
  1. 10
      server/bin/create_game
  2. 10
      server/hra/db.py
  3. 9
      server/hra/util.py
  4. 2
      server/hra/web/api.py
  5. 101
      server/hra/web/game.py
  6. 19
      server/hra/web/html.py
  7. 138
      server/hra/web/pages.py
  8. 9
      server/static/ksp-mhd.css
  9. 135
      server/static/occupy.css

10
server/bin/create_game

@ -9,12 +9,12 @@ from sqlalchemy import exc, update
ses = db.get_session() ses = db.get_session()
mode = "occupy" mode = "occupy"
teams_count = 6 teams_count = 16
configuration = { configuration = {
"teams_width": 2, "teams_width": 4,
"teams_height": 3, "teams_height": 4,
"width_per_team": 10, "width_per_team": 30,
"height_per_team": 10, "height_per_team": 30,
} }
g = db.Game(game_mode=mode, configuration=configuration, teams_count=teams_count) g = db.Game(game_mode=mode, configuration=configuration, teams_count=teams_count)

10
server/hra/db.py

@ -59,6 +59,9 @@ class User(Base):
def __repr__(self): def __repr__(self):
return '<User %r>' % self.username return '<User %r>' % self.username
def print(self):
return self.username + (" (org)" if self.org else "")
class Game(Base): class Game(Base):
__tablename__ = 'games' __tablename__ = 'games'
@ -69,8 +72,8 @@ class Game(Base):
working_on_next_state = Column(Boolean, nullable=False, default=True) working_on_next_state = Column(Boolean, nullable=False, default=True)
current_round = Column(Integer, default=-1) current_round = Column(Integer, default=-1)
def current_state(self) -> Optional['State']: def current_state(self, none_if_working=True) -> Optional['State']:
if self.working_on_next_state is True: if none_if_working and self.working_on_next_state is True:
return None return None
return get_session().query(State).filter_by(game_id=self.game_id, round=self.current_round).order_by(State.round.desc()).first() 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() ses.expire_all()
return ses.query(Game).filter_by(game_id=self.game_id).with_for_update().first() return ses.query(Game).filter_by(game_id=self.game_id).with_for_update().first()
def print(self):
return f"{self.game_id}: <name>"
class Team(Base): class Team(Base):
__tablename__ = 'base' __tablename__ = 'base'

9
server/hra/util.py

@ -1,7 +1,16 @@
import bcrypt import bcrypt
from time import time
def hash_passwd(a): def hash_passwd(a):
salt = b'$2b$12$V2aIKSJC/uozaodwYnQX3e' salt = b'$2b$12$V2aIKSJC/uozaodwYnQX3e'
hashed = bcrypt.hashpw(a.encode('utf-8'), salt) hashed = bcrypt.hashpw(a.encode('utf-8'), salt)
return hashed.decode('us-ascii') 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

2
server/hra/web/api.py

@ -70,7 +70,7 @@ def api_action():
except Exception as e: except Exception as e:
return json.dumps({ return json.dumps({
"error": "error", "error": "error",
"description": str(e) "description": f"{type(e).__name__}: {e}"
}) })
db.get_session().expire_all() db.get_session().expire_all()

101
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("&nbsp;"))
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("&nbsp;"))
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()

19
server/hra/web/html.py

@ -116,7 +116,7 @@ class Tag(Bucket):
self.name = name self.name = name
self.attributes = attributes self.attributes = attributes
def add_attribute(k, v): def add_attribute(self, k, v):
self.attributes.append((k, v)) self.attributes.append((k, v))
def format_attributes(self): def format_attributes(self):
@ -202,24 +202,25 @@ def remove_leading_underscore(s):
class WrapAfterBuilder(Builder): class WrapAfterBuilder(Builder):
def __init__(self, f): def __init__(self, f):
super().__init__(Bucket(self)) super().__init__(Bucket(self))
self._wrap_done = False self.wrap_done = False
self._wrap_function = f self.wrap_function = f
def _wrap(self, *arg, **kvarg): def wrap(self, *arg, **kvarg):
if self._wrap_done: if self.wrap_done:
return return
self._wrap_done = True self.wrap_done = True
content = self.root_tag.content content = self.root_tag.content
self.root_tag = None self.root_tag = None
self.current_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): def print(self, *arg, **kvarg):
self._wrap() self.wrap()
return super().print(*arg, **kvarg) return super().print(*arg, **kvarg)
def print_file(self, *arg, **kvarg): def print_file(self, *arg, **kvarg):
self._wrap() self.wrap()
return super().print_file(*arg, **kvarg) return super().print_file(*arg, **kvarg)

138
server/hra/web/pages.py

@ -5,11 +5,7 @@ from flask_wtf import FlaskForm
from flask_bootstrap import Bootstrap from flask_bootstrap import Bootstrap
import time import time
from datetime import datetime from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import exc, update from sqlalchemy import exc, update
import hashlib
import bcrypt
import os
import werkzeug.exceptions import werkzeug.exceptions
import wtforms import wtforms
from wtforms.fields import EmailField from wtforms.fields import EmailField
@ -22,8 +18,9 @@ from hra.web import app
import hra.web.jinja_mac as jinja_mac import hra.web.jinja_mac as jinja_mac
from hra.util import hash_passwd from hra.util import hash_passwd
@html.WrapAfterBuilder_decorator @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.current_tag = html.Tag(b, "html", [])
b.root_tag = b.current_tag b.root_tag = b.current_tag
with b.head(): 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="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="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.link(rel="shortcut icon", href=app.url_for('static', filename='img/favicon.ico'))
b(head)
with b.body() as body: with b.body() as body:
with b.header(_class=f"flavor-{config.WEB_FLAVOR}"): 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"): 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.img(src=app.url_for('static', filename='hippo.png'), style="width: 60px;height: auto;", alt="KSP")
b.h1()("Hra na soustředění 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"): with b.nav(id="main-menu", _class="content"):
for item in g.menu: for item in g.menu:
b.a(href=item.url)(item.name) b.a(href=item.url)(item.name)
@ -78,11 +76,36 @@ def user_link(user):
b("-") b("-")
else: else:
if g.org: 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: 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 return b.root_tag
class RegistrationForm(FlaskForm): class RegistrationForm(FlaskForm):
@ -175,9 +198,22 @@ def print_time(t):
@app.route("/", methods=['GET', 'POST']) @app.route("/", methods=['GET', 'POST'])
def web_index(): def web_index():
b = BasePage() b = BasePage()
if g.user: if g.user:
with b.p(): teams = db.get_session().query(db.Team).filter_by(user_id=g.user.id).order_by(db.Team.game_id).all()
b(f"Váš token je: {g.user.token}") 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() return b.print_file()
@app.route("/game/<int:game_id>", methods=['GET']) @app.route("/game/<int:game_id>", 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() teams = ses.query(db.Team).filter_by(game_id=game_id).order_by(db.Team.team_id).all()
if game is None: if game is None:
raise werkzeug.exceptions.NotFound() raise werkzeug.exceptions.NotFound()
if not right_for_game(game):
raise werkzeug.exceptions.Forbidden()
b = BasePage() b = BasePage()
b.h2("Hra ", game.print())
with b.p().table(_class="data full"): with b.p().table(_class="data full"):
with b.thead(): with b.thead():
b.line().th()("Id") b.line().th()("Id")
b.line().th()("User") b.line().th()("User")
if g.org: b.line().th()("Akce")
b.line().th()("Akce")
for team in teams: for team in teams:
with b.tr(): with b.tr():
b.line().td()(team.team_id) b.line().td()(team.team_id)
b.line().td()(user_link(team.user),": ", team.name) b.line().td()(user_link(team.user),": ", team.name)
if g.org: with b.td():
with b.td(): with b.div(_class="btn-group", role="group"):
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_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_view.__name__, game_id=game.game_id, team_id=team.team_id))("Zobrazit hru")
#b.a(_class="btn btn-xs btn-primary", href=app.url_for(web_game.__name__, game_id=g.game_id))("Detail") 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() return b.print_file()
@app.route("/game/<int:game_id>/view", methods=['GET'])
@app.route("/game/<int:game_id>/view/<int:team_id>", 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") @app.route("/org/games")
def web_org_games(): def web_org_games():
games = db.get_session().query(db.Game).order_by(db.Game.game_id).all() games = db.get_session().query(db.Game).order_by(db.Game.game_id).all()
b = BasePage() b = BasePage()
b.h2("Hry")
with b.p().table(_class="data full"): with b.p().table(_class="data full"):
with b.thead(): with b.thead():
b.line().th()("Id") b.line().th()("Id")
@ -294,7 +366,13 @@ def web_org_game_userchange(game_id, team_id):
@app.route("/org/users") @app.route("/org/users")
def web_org_users(): def web_org_users():
users = db.get_session().query(db.User).order_by(db.User.id).all() 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 = BasePage()
b.h2("Uživatelé")
with b.p().table(_class="data full"): with b.p().table(_class="data full"):
with b.thead(): with b.thead():
b.line().th()("Username") b.line().th()("Username")
@ -302,8 +380,10 @@ def web_org_users():
b.line().th()("Akce") b.line().th()("Akce")
for u in users: for u in users:
with b.tr(): with b.tr():
b.line().td()(u.username) b.line().td()(user_link(u))
b.line().td() with b.td().ul():
for g in games_for_user[u.id]:
b.line().li(game_link(g))
with b.td(): with b.td():
with b.div(_class="btn-group", role="group"): 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") 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() user = db.get_session().query(db.User).filter_by(id=user_id).one_or_none()
if not user: if not user:
raise werkzeug.exceptions.NotFound() 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 = 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.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)): 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.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() return b.print_file()
@app.route("/org/user/<int:user_id>/su", methods=['POST']) @app.route("/org/user/<int:user_id>/su", methods=['POST'])
@ -327,3 +419,5 @@ def web_org_user_su(user_id):
session['uid'] = user.id session['uid'] = user.id
flash("Uživatel vtělen!") flash("Uživatel vtělen!")
return redirect('/') return redirect('/')
from hra.web.game import get_wlogic, wlogic_by_mode

9
server/static/ksp-mhd.css

@ -11,10 +11,13 @@ body {
display: block; display: block;
margin: 0 auto; margin: 0 auto;
padding: 0 1em; padding: 0 1em;
max-width: 1000px;
width: 100%; width: 100%;
} }
.content_limited, main {
max-width: 1000px;
}
header { header {
padding: 10px 0px; padding: 10px 0px;
} }
@ -39,8 +42,10 @@ header img { height: 60px; }
header h1 { margin: auto 20px 0px; color: #222; } header h1 { margin: auto 20px 0px; color: #222; }
#nav-wrapper { .nav-wrapper-sticky {
position: sticky; position: sticky;
}
#nav-wrapper {
top: 0; top: 0;
background-color: #222; background-color: #222;
border: 1px #222 solid; border: 1px #222 solid;

135
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;
}
Loading…
Cancel
Save