stats: síň slávy a kariérní statistiky
- rekordy: nejvyšší tah, nejvyšší konečné skóre, nejrychlejší výhra - kariéra: počet her, výher, úspěšnost, průměrné pořadí, průměr na tah a kolikrát koho postihlo vynulování - do statistik jdou jen dohrané hry, nedohrané a opuštěné se ignorují - celkové skóre počítá SQL stejným pravidlem jako core.Game.totals(), tedy součet tahů po posledním vynulování; testem ověřeno, že se obě cesty shodují - vědomě chybí celkový součet bodů napříč hrami — cílové skóre je u každé hry jiné, metrika by odměňovala jen toho, kdo hrál nejvíc - README se spuštěním a přehledem modulů Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bcbbede258
commit
d7c63cf98b
9 files changed
+474
-1
No files matched your search
@@ -0,0 +1,56 @@
|
|||||||
|
# Počitadlo v kostkách
|
||||||
|
|
||||||
|
Doprovod k fyzickým kostkám: **telefon zapisuje skóre, notebook ukazuje
|
||||||
|
zápisník** a hry se pamatují mezi večery.
|
||||||
|
|
||||||
|
Hráči se napříč hrami poznávají podle jména — Adam ze středečního večera je
|
||||||
|
ten samý Adam jako z minulého měsíce. Žádné účty, žádná hesla.
|
||||||
|
|
||||||
|
## Spuštění
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
.venv/bin/pip install -r requirements.txt
|
||||||
|
.venv/bin/python web.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Server vypíše adresu, na které poslouchá. Na notebooku otevři `/board`,
|
||||||
|
naskenuj QR kód telefonem a hraj.
|
||||||
|
|
||||||
|
| Proměnná | K čemu je | Výchozí |
|
||||||
|
|---|---|---|
|
||||||
|
| `DICE_DB` | soubor s databází | `dice.db` |
|
||||||
|
| `DICE_PORT` | port | `8000` |
|
||||||
|
| `DICE_HOST` | adresa v QR kódu, když ji nemá hledat sám | zjistí se za běhu |
|
||||||
|
|
||||||
|
Počítá se s domácí sítí — appka nemá přihlašování a kdokoli na stejné WiFi
|
||||||
|
může zapisovat.
|
||||||
|
|
||||||
|
## Pravidla
|
||||||
|
|
||||||
|
Skóre se zadává ručně po tazích, hází se doopravdy. Tři nulové tahy za sebou
|
||||||
|
vynulují hráči skóre; indikátor `x` / `xx` ukazuje, jak blízko k tomu je.
|
||||||
|
Vítěz se vyhlašuje až po dokončeném kole, aby měli všichni stejný počet tahů —
|
||||||
|
při remíze na prvním místě se hraje dál.
|
||||||
|
|
||||||
|
## Co kde je
|
||||||
|
|
||||||
|
| Soubor | Role |
|
||||||
|
|---|---|
|
||||||
|
| `core.py` | pravidla hry, žádný vstup ani výstup |
|
||||||
|
| `storage.py` | SQLite: hráči, hry, tahy |
|
||||||
|
| `stats.py` | síň slávy a kariérní statistiky |
|
||||||
|
| `web.py` | Flask aplikace |
|
||||||
|
| `dice.py` | totéž v terminálu, bez ukládání |
|
||||||
|
|
||||||
|
Terminálová verze zůstává funkční jako záloha:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python dice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python -m pytest tests/ -v
|
||||||
|
```
|
||||||
@@ -419,3 +419,45 @@ button {
|
|||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
* { transition: none !important; animation: none !important; }
|
* { transition: none !important; animation: none !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- síň slávy ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
.records {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
background: var(--rule);
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
margin-bottom: calc(var(--step) * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.record {
|
||||||
|
background: #FFFFFF;
|
||||||
|
padding: 1.2rem 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record .number {
|
||||||
|
font-family: var(--font-num);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-size: 2.6rem;
|
||||||
|
margin: 0.2rem 0 0.3rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record .number small {
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.record .who {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet .name {
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""Statistiky napříč hrami.
|
||||||
|
|
||||||
|
Dvě různé obrazovky, protože odpovídají na jiné otázky: síň slávy sbírá
|
||||||
|
rekordy, kariéra ukazuje, kdo je dlouhodobě dobrý.
|
||||||
|
|
||||||
|
Vědomě tu není celkový součet bodů napříč hrami — cílové skóre je u každé hry
|
||||||
|
jiné, hry nejsou srovnatelné a taková metrika by odměňovala jen toho, kdo hrál
|
||||||
|
nejvíc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
import storage
|
||||||
|
|
||||||
|
# Celkové skóre hráče ve hře: součet tahů po posledním vynulování.
|
||||||
|
# Stejné pravidlo jako v core.Game.totals(), jen vyjádřené v SQL.
|
||||||
|
TOTALS = """
|
||||||
|
WITH last_reset AS (
|
||||||
|
SELECT game_id, player_id, MAX(id) AS reset_id
|
||||||
|
FROM turns
|
||||||
|
WHERE reset = 1
|
||||||
|
GROUP BY game_id, player_id
|
||||||
|
),
|
||||||
|
totals AS (
|
||||||
|
SELECT gp.game_id,
|
||||||
|
gp.player_id,
|
||||||
|
COALESCE((
|
||||||
|
SELECT SUM(t.value)
|
||||||
|
FROM turns t
|
||||||
|
LEFT JOIN last_reset lr
|
||||||
|
ON lr.game_id = t.game_id AND lr.player_id = t.player_id
|
||||||
|
WHERE t.game_id = gp.game_id
|
||||||
|
AND t.player_id = gp.player_id
|
||||||
|
AND (lr.reset_id IS NULL OR t.id > lr.reset_id)
|
||||||
|
), 0) AS total
|
||||||
|
FROM game_players gp
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def records(conn: sqlite3.Connection) -> dict:
|
||||||
|
"""Síň slávy — tři rekordy, které jde překonat."""
|
||||||
|
best_turn = conn.execute("""
|
||||||
|
SELECT p.name, t.value, t.game_id, t.at
|
||||||
|
FROM turns t
|
||||||
|
JOIN players p ON p.id = t.player_id
|
||||||
|
ORDER BY t.value DESC, t.id ASC
|
||||||
|
LIMIT 1
|
||||||
|
""").fetchone()
|
||||||
|
|
||||||
|
best_score = conn.execute(TOTALS + """
|
||||||
|
SELECT p.name, totals.total, g.id AS game_id, g.finished_at
|
||||||
|
FROM totals
|
||||||
|
JOIN games g ON g.id = totals.game_id
|
||||||
|
JOIN players p ON p.id = totals.player_id
|
||||||
|
WHERE g.status = ?
|
||||||
|
ORDER BY totals.total DESC
|
||||||
|
LIMIT 1
|
||||||
|
""", (storage.STATUS_FINISHED,)).fetchone()
|
||||||
|
|
||||||
|
fastest = conn.execute("""
|
||||||
|
SELECT p.name, g.id AS game_id, MAX(t.round) AS rounds, g.finished_at
|
||||||
|
FROM games g
|
||||||
|
JOIN turns t ON t.game_id = g.id
|
||||||
|
JOIN players p ON p.id = g.winner_id
|
||||||
|
WHERE g.status = ?
|
||||||
|
GROUP BY g.id
|
||||||
|
ORDER BY rounds ASC, g.id ASC
|
||||||
|
LIMIT 1
|
||||||
|
""", (storage.STATUS_FINISHED,)).fetchone()
|
||||||
|
|
||||||
|
return {"best_turn": best_turn, "best_score": best_score, "fastest": fastest}
|
||||||
|
|
||||||
|
|
||||||
|
def careers(conn: sqlite3.Connection) -> list[dict]:
|
||||||
|
"""Kdo je dlouhodobě dobrý. Počítají se jen dohrané hry."""
|
||||||
|
placings = conn.execute(TOTALS + """
|
||||||
|
, ranked AS (
|
||||||
|
SELECT totals.game_id,
|
||||||
|
totals.player_id,
|
||||||
|
RANK() OVER (PARTITION BY totals.game_id
|
||||||
|
ORDER BY totals.total DESC) AS placing
|
||||||
|
FROM totals
|
||||||
|
JOIN games g ON g.id = totals.game_id
|
||||||
|
WHERE g.status = ?
|
||||||
|
)
|
||||||
|
SELECT r.player_id,
|
||||||
|
p.name,
|
||||||
|
COUNT(*) AS games,
|
||||||
|
SUM(CASE WHEN g.winner_id = r.player_id THEN 1 ELSE 0 END) AS wins,
|
||||||
|
AVG(r.placing) AS placing
|
||||||
|
FROM ranked r
|
||||||
|
JOIN players p ON p.id = r.player_id
|
||||||
|
JOIN games g ON g.id = r.game_id
|
||||||
|
GROUP BY r.player_id
|
||||||
|
""", (storage.STATUS_FINISHED,)).fetchall()
|
||||||
|
|
||||||
|
turn_stats = conn.execute("""
|
||||||
|
SELECT t.player_id,
|
||||||
|
AVG(t.value) AS per_turn,
|
||||||
|
SUM(t.reset) AS wipeouts
|
||||||
|
FROM turns t
|
||||||
|
JOIN games g ON g.id = t.game_id
|
||||||
|
WHERE g.status = ?
|
||||||
|
GROUP BY t.player_id
|
||||||
|
""", (storage.STATUS_FINISHED,)).fetchall()
|
||||||
|
|
||||||
|
by_player = {row["player_id"]: row for row in turn_stats}
|
||||||
|
|
||||||
|
table = []
|
||||||
|
for row in placings:
|
||||||
|
turns = by_player.get(row["player_id"])
|
||||||
|
table.append({
|
||||||
|
"name": row["name"],
|
||||||
|
"games": row["games"],
|
||||||
|
"wins": row["wins"],
|
||||||
|
"share": row["wins"] / row["games"] if row["games"] else 0,
|
||||||
|
"placing": row["placing"],
|
||||||
|
"per_turn": turns["per_turn"] if turns else 0,
|
||||||
|
"wipeouts": turns["wipeouts"] if turns else 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
table.sort(key=lambda r: (-r["share"], r["placing"], -r["games"]))
|
||||||
|
return table
|
||||||
@@ -72,6 +72,7 @@
|
|||||||
|
|
||||||
<p class="footer">
|
<p class="footer">
|
||||||
<a href="{{ url_for('board', game_id=game_id) }}">Zápisník</a>
|
<a href="{{ url_for('board', game_id=game_id) }}">Zápisník</a>
|
||||||
|
<a href="{{ url_for('hall') }}">Síň slávy</a>
|
||||||
<a href="{{ url_for('index') }}">Nová hra</a>
|
<a href="{{ url_for('index') }}">Nová hra</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,7 +47,10 @@
|
|||||||
<button class="btn" type="submit">Začít hru</button>
|
<button class="btn" type="submit">Začít hru</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="footer"><a href="{{ url_for('board_redirect') }}">Zápisník</a></p>
|
<p class="footer">
|
||||||
|
<a href="{{ url_for('board_redirect') }}">Zápisník</a>
|
||||||
|
<a href="{{ url_for('hall') }}">Síň slávy</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Síň slávy{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="board">
|
||||||
|
<header class="masthead">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Počitadlo</p>
|
||||||
|
<h1>Síň slávy</h1>
|
||||||
|
</div>
|
||||||
|
<p class="meta"><a href="{{ url_for('index') }}">Nová hra</a></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% if records.best_turn %}
|
||||||
|
<div class="records">
|
||||||
|
<article class="record">
|
||||||
|
<p class="eyebrow">Nejvyšší tah</p>
|
||||||
|
<p class="number">{{ records.best_turn.value|cislo }}</p>
|
||||||
|
<p class="who">{{ records.best_turn.name }} · hra {{ records.best_turn.game_id }}
|
||||||
|
· {{ records.best_turn.at|datum }}</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{% if records.best_score %}
|
||||||
|
<article class="record">
|
||||||
|
<p class="eyebrow">Nejvyšší konečné skóre</p>
|
||||||
|
<p class="number">{{ records.best_score.total|cislo }}</p>
|
||||||
|
<p class="who">{{ records.best_score.name }} · hra {{ records.best_score.game_id }}
|
||||||
|
· {{ records.best_score.finished_at|datum }}</p>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if records.fastest %}
|
||||||
|
<article class="record">
|
||||||
|
<p class="eyebrow">Nejrychlejší výhra</p>
|
||||||
|
<p class="number">{{ records.fastest.rounds }}<small> kol</small></p>
|
||||||
|
<p class="who">{{ records.fastest.name }} · hra {{ records.fastest.game_id }}
|
||||||
|
· {{ records.fastest.finished_at|datum }}</p>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="hint">Zatím se neodehrál ani jeden tah. Rekordy přibudou po první hře.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if careers %}
|
||||||
|
<p class="eyebrow">Kariéra</p>
|
||||||
|
<p class="hint">Počítají se jen dohrané hry.</p>
|
||||||
|
<div class="sheet-wrap">
|
||||||
|
<table class="sheet">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="label">Hráč</th>
|
||||||
|
<th>Her</th>
|
||||||
|
<th>Výher</th>
|
||||||
|
<th>Úspěšnost</th>
|
||||||
|
<th>Průměrné pořadí</th>
|
||||||
|
<th>Průměr na tah</th>
|
||||||
|
<th>Vynulování</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for player in careers %}
|
||||||
|
<tr>
|
||||||
|
<td class="label name">{{ player.name }}</td>
|
||||||
|
<td>{{ player.games }}</td>
|
||||||
|
<td>{{ player.wins }}</td>
|
||||||
|
<td>{{ (player.share * 100)|round|int }} %</td>
|
||||||
|
<td>{{ '%.1f'|format(player.placing) }}</td>
|
||||||
|
<td>{{ player.per_turn|round|int|cislo }}</td>
|
||||||
|
<td class="{{ 'wiped' if player.wipeouts }}">{{ player.wipeouts }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if games %}
|
||||||
|
<p class="eyebrow" style="margin-top: 2.5rem">Odehrané hry</p>
|
||||||
|
<div class="sheet-wrap">
|
||||||
|
<table class="sheet">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="label">Hra</th>
|
||||||
|
<th class="label">Datum</th>
|
||||||
|
<th>Hráčů</th>
|
||||||
|
<th>Kol</th>
|
||||||
|
<th>Cíl</th>
|
||||||
|
<th class="label">Výsledek</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for game in games %}
|
||||||
|
<tr>
|
||||||
|
<td class="label">{{ game.id }}</td>
|
||||||
|
<td class="label">{{ game.started_at|datum }}</td>
|
||||||
|
<td>{{ game.players }}</td>
|
||||||
|
<td>{{ (game.turns / game.players)|round(0, 'ceil')|int }}</td>
|
||||||
|
<td>{{ game.final_score|cislo }}</td>
|
||||||
|
<td class="label name">
|
||||||
|
{%- if game.winner_name %}Vyhrává {{ game.winner_name }}
|
||||||
|
{%- elif game.status == 'probíhá' %}<a href="{{ url_for('game_view', game_id=game.id) }}">Pokračovat</a>
|
||||||
|
{%- else %}{{ game.status }}{% endif -%}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
import stats
|
||||||
|
import storage
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def conn(tmp_path):
|
||||||
|
connection = storage.connect(tmp_path / "test.db")
|
||||||
|
yield connection
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def play(conn, names, final_score, values, finish=True):
|
||||||
|
game_id = storage.create_game(conn, names, final_score)
|
||||||
|
game = storage.load_game(conn, game_id)
|
||||||
|
for value in values:
|
||||||
|
storage.add_turn(conn, game_id, game.add_score(value).turn)
|
||||||
|
if finish and game.winner:
|
||||||
|
storage.finish_game(conn, game_id, game.winner)
|
||||||
|
return game_id
|
||||||
|
|
||||||
|
|
||||||
|
def by_name(table):
|
||||||
|
return {row["name"]: row for row in table}
|
||||||
|
|
||||||
|
|
||||||
|
def test_prazdna_sin_slavy(conn):
|
||||||
|
found = stats.records(conn)
|
||||||
|
|
||||||
|
assert found["best_turn"] is None
|
||||||
|
assert found["best_score"] is None
|
||||||
|
assert found["fastest"] is None
|
||||||
|
assert stats.careers(conn) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_rekordy(conn):
|
||||||
|
play(conn, ["Adam", "Eva"], 1000, [600, 200, 500, 100])
|
||||||
|
|
||||||
|
found = stats.records(conn)
|
||||||
|
|
||||||
|
assert found["best_turn"]["name"] == "Adam"
|
||||||
|
assert found["best_turn"]["value"] == 600
|
||||||
|
assert found["best_score"]["name"] == "Adam"
|
||||||
|
assert found["best_score"]["total"] == 1100
|
||||||
|
assert found["fastest"]["name"] == "Adam"
|
||||||
|
assert found["fastest"]["rounds"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_kariera_pocita_vyhry_a_uspesnost(conn):
|
||||||
|
play(conn, ["Adam", "Eva"], 1000, [600, 200, 500, 100])
|
||||||
|
play(conn, ["Adam", "Eva"], 1000, [100, 600, 200, 500])
|
||||||
|
|
||||||
|
table = by_name(stats.careers(conn))
|
||||||
|
|
||||||
|
assert table["Adam"]["games"] == 2
|
||||||
|
assert table["Adam"]["wins"] == 1
|
||||||
|
assert table["Adam"]["share"] == 0.5
|
||||||
|
assert table["Eva"]["wins"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_nedohrane_hry_se_nepocitaji(conn):
|
||||||
|
play(conn, ["Adam", "Eva"], 1000, [600, 200, 500, 100])
|
||||||
|
play(conn, ["Adam", "Eva"], 5000, [100, 100], finish=False)
|
||||||
|
|
||||||
|
table = by_name(stats.careers(conn))
|
||||||
|
|
||||||
|
assert table["Adam"]["games"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_vynulovani_se_pocita(conn):
|
||||||
|
# Eva vyhraje, Adam se cestou třikrát vynuluje.
|
||||||
|
play(conn, ["Adam", "Eva"], 1000, [300, 400, 0, 300, 0, 200, 0, 500])
|
||||||
|
|
||||||
|
table = by_name(stats.careers(conn))
|
||||||
|
|
||||||
|
assert table["Adam"]["wipeouts"] == 1
|
||||||
|
assert table["Eva"]["wipeouts"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_prumerne_poradi(conn):
|
||||||
|
play(conn, ["Adam", "Eva", "Petr"], 1000, [600, 300, 100, 500, 200, 50])
|
||||||
|
|
||||||
|
table = by_name(stats.careers(conn))
|
||||||
|
|
||||||
|
assert table["Adam"]["placing"] == 1
|
||||||
|
assert table["Eva"]["placing"] == 2
|
||||||
|
assert table["Petr"]["placing"] == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_soucty_v_sql_respektuji_vynulovani(conn):
|
||||||
|
"""SQL musí počítat celkové skóre stejně jako core.Game.totals()."""
|
||||||
|
game_id = play(conn, ["Adam", "Eva"], 1000, [300, 100, 0, 100, 0, 100, 0, 900])
|
||||||
|
|
||||||
|
game = storage.load_game(conn, game_id)
|
||||||
|
best = stats.records(conn)["best_score"]
|
||||||
|
|
||||||
|
assert game.totals()["Adam"] == 0
|
||||||
|
assert best["name"] == "Eva"
|
||||||
|
assert best["total"] == game.totals()["Eva"]
|
||||||
@@ -175,3 +175,19 @@ def test_tabule_presmeruje_na_rozehranou_hru(client):
|
|||||||
|
|
||||||
def test_neexistujici_hra(client):
|
def test_neexistujici_hra(client):
|
||||||
assert client.get("/game/999").status_code == 404
|
assert client.get("/game/999").status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_sin_slavy(client):
|
||||||
|
game_id = start_game(client, "Adam", "Eva", final_score=500)
|
||||||
|
score(client, game_id, 500)
|
||||||
|
score(client, game_id, 100)
|
||||||
|
|
||||||
|
page = text(client.get("/stats"))
|
||||||
|
|
||||||
|
assert "Nejvyšší tah" in page
|
||||||
|
assert "Kariéra" in page
|
||||||
|
assert "Vyhrává Adam" in page
|
||||||
|
|
||||||
|
|
||||||
|
def test_sin_slavy_bez_her(client):
|
||||||
|
assert "Rekordy přibudou po první hře" in text(client.get("/stats"))
|
||||||
@@ -12,6 +12,7 @@ from flask import (Flask, abort, g, redirect, render_template, request,
|
|||||||
url_for)
|
url_for)
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
import stats
|
||||||
import storage
|
import storage
|
||||||
from core import FINAL_SCORE, Game, GameOver
|
from core import FINAL_SCORE, Game, GameOver
|
||||||
|
|
||||||
@@ -77,6 +78,15 @@ def qr_svg(data: str) -> Markup:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.template_filter("datum")
|
||||||
|
def datum(value: str) -> str:
|
||||||
|
"""Z ISO textu udělá 12. 8. 2026."""
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
den = value[:10].split("-")
|
||||||
|
return f"{int(den[2])}. {int(den[1])}. {den[0]}"
|
||||||
|
|
||||||
|
|
||||||
@app.template_filter("cislo")
|
@app.template_filter("cislo")
|
||||||
def cislo(value: int) -> str:
|
def cislo(value: int) -> str:
|
||||||
"""Tisíce se v češtině oddělují mezerou."""
|
"""Tisíce se v češtině oddělují mezerou."""
|
||||||
@@ -232,6 +242,15 @@ def board_panel(game_id: int):
|
|||||||
return render_template("_panel.html", **view(game, game_id, row))
|
return render_template("_panel.html", **view(game, game_id, row))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/stats")
|
||||||
|
def hall():
|
||||||
|
conn = db()
|
||||||
|
return render_template("stats.html",
|
||||||
|
records=stats.records(conn),
|
||||||
|
careers=stats.careers(conn),
|
||||||
|
games=storage.recent_games(conn))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print(f"Počitadlo běží na {lan_address()}")
|
print(f"Počitadlo běží na {lan_address()}")
|
||||||
print(f"Tabule: {lan_address()}/board")
|
print(f"Tabule: {lan_address()}/board")
|
||||||
|
|||||||
Reference in new issue
Block a user