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

10
server/hra/db.py

@ -59,6 +59,9 @@ class User(Base):
def __repr__(self):
return '<User %r>' % 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}: <name>"
class Team(Base):
__tablename__ = 'base'

9
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

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

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

138
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/<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()
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/<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")
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/<int:user_id>/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

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

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