add WL to tss scoreboards (#1345)
This commit is contained in:
+51
-16
@@ -225,14 +225,20 @@ def _create_scoreboard_sync(model: dict[str, Any], output_path: str, bar_color:
|
|||||||
name_x = start_x if not flipped else start_x + section_width - name_w
|
name_x = start_x if not flipped else start_x + section_width - name_w
|
||||||
draw.text((name_x, header_y), team_name, font=fonts["team"], fill=header_fill)
|
draw.text((name_x, header_y), team_name, font=fonts["team"], fill=header_fill)
|
||||||
|
|
||||||
# W/L slot — only drawn once real per-tournament standings exist (stub returns
|
# W/L slot — per-tournament record under the team name. Coloured exactly like
|
||||||
# None today, so nothing renders under the team name).
|
# SREBOT: green W · grey dash · red L · grey dash · win-rate% on a red→green
|
||||||
standings = get_tss_standings(team.get("team_id"))
|
# gradient. Placed below the team name's full glyph box (descender-inclusive)
|
||||||
|
# so names with p/g/y descenders don't clip into the record line.
|
||||||
|
standings = get_tss_standings(team.get("team_id"), model.get("tournament_id"))
|
||||||
if standings:
|
if standings:
|
||||||
wl_text = f"{standings['wins']}W - {standings['losses']}L"
|
wl_segs = _winloss_segments(standings)
|
||||||
wb = draw.textbbox((0, 0), wl_text, font=fonts["sub"])
|
seg_w = _segments_width(draw, wl_segs, fonts["sub"])
|
||||||
wl_x = start_x if not flipped else start_x + section_width - (wb[2] - wb[0])
|
wl_x = start_x if not flipped else start_x + section_width - seg_w
|
||||||
draw.text((wl_x, header_y + (nb[3] - nb[1]) + 18), wl_text, font=fonts["sub"], fill=GREY_FILL)
|
# Sit halfway between the original (descender-clipping) baseline and a
|
||||||
|
# full descender-clear drop — enough to clear p/g/y, no more.
|
||||||
|
name_full_h = fonts["team"].getbbox("ApgjyQ")[3]
|
||||||
|
wl_y = header_y + ((nb[3] - nb[1] + 18) + (name_full_h + 10)) // 2
|
||||||
|
_draw_segments(draw, wl_x, wl_y, wl_segs, fonts["sub"])
|
||||||
|
|
||||||
# Stat columns (rating now lives inline next to the player name).
|
# Stat columns (rating now lives inline next to the player name).
|
||||||
col_labels = ["Air", "Ground", "Assists", "Deaths", "Caps"]
|
col_labels = ["Air", "Ground", "Assists", "Deaths", "Caps"]
|
||||||
@@ -270,7 +276,7 @@ def _create_scoreboard_sync(model: dict[str, Any], output_path: str, bar_color:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Player rows. Layout per row:
|
# Player rows. Layout per row:
|
||||||
# top line — player name + inline (rating ±delta) at the column edge; stats
|
# top line — player name + inline (rating) at the column edge; stats
|
||||||
# lower line — vehicle icons next to their translated names
|
# lower line — vehicle icons next to their translated names
|
||||||
body = fonts["body"]
|
body = fonts["body"]
|
||||||
small = fonts["veh"]
|
small = fonts["veh"]
|
||||||
@@ -287,7 +293,7 @@ def _create_scoreboard_sync(model: dict[str, Any], output_path: str, bar_color:
|
|||||||
veh_y = line_y + name_h + 12 # vehicle line (icons + names together)
|
veh_y = line_y + name_h + 12 # vehicle line (icons + names together)
|
||||||
vname_y = veh_y + (veh_icon - small_h) // 2
|
vname_y = veh_y + (veh_icon - small_h) // 2
|
||||||
name_w = draw.textbbox((0, 0), name, font=body)[2]
|
name_w = draw.textbbox((0, 0), name, font=body)[2]
|
||||||
rating_segs = _rating_segments(p.get("pvp_ratio"), p.get("pvp_ratio_delta"))
|
rating_segs = _rating_segments(p.get("pvp_ratio"))
|
||||||
rating_w = _segments_width(draw, rating_segs, body)
|
rating_w = _segments_width(draw, rating_segs, body)
|
||||||
|
|
||||||
if not flipped:
|
if not flipped:
|
||||||
@@ -342,16 +348,45 @@ def _create_scoreboard_sync(model: dict[str, Any], output_path: str, bar_color:
|
|||||||
RATING_FILL = (170, 200, 255, 255)
|
RATING_FILL = (170, 200, 255, 255)
|
||||||
|
|
||||||
|
|
||||||
def _rating_segments(rating, delta):
|
# W/L record colours — matched exactly to SREBOT's scoreboard.
|
||||||
"""Build colored (text, fill) segments for the inline ``(1468 +2)`` rating.
|
WL_WIN_FILL = (0, 255, 0, 255)
|
||||||
|
WL_LOSS_FILL = (255, 60, 60, 255)
|
||||||
|
WL_DASH_FILL = (200, 200, 200, 255)
|
||||||
|
|
||||||
``delta`` is a signed change vs the player's previous rating; it's ``None`` until
|
|
||||||
rating history is tracked (see wl.py), so for now only the current value shows.
|
def _winrate_gradient(win_rate: float) -> tuple:
|
||||||
|
"""Red→yellow→green gradient for a win-rate percentage (SREBOT's get_gradient_color).
|
||||||
|
|
||||||
|
0% = red (255,0,0), 50% = yellow (255,255,0), 100% = green (0,255,0).
|
||||||
"""
|
"""
|
||||||
|
win_rate = max(0.0, min(100.0, win_rate))
|
||||||
|
if win_rate <= 50:
|
||||||
|
return (255, int(255 * (win_rate / 50)), 0, 255)
|
||||||
|
return (int(255 * (1 - (win_rate - 50) / 50)), 255, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
|
def _winloss_segments(standings: dict[str, int]) -> list[tuple[str, tuple]]:
|
||||||
|
"""Colored ``3W - 2L - 60%`` segments for the per-tournament record line.
|
||||||
|
|
||||||
|
Win count + 'W' green, ' - ' grey, loss count + 'L' red, ' - ' grey, then the
|
||||||
|
win-rate% on the red→green gradient — mirroring SREBOT's _draw_winloss.
|
||||||
|
"""
|
||||||
|
wins = int(standings.get("wins") or 0)
|
||||||
|
losses = int(standings.get("losses") or 0)
|
||||||
|
total = wins + losses
|
||||||
|
pct = (wins / total) * 100 if total else 0.0
|
||||||
|
return [
|
||||||
|
(f"{wins}W", WL_WIN_FILL),
|
||||||
|
(" - ", WL_DASH_FILL),
|
||||||
|
(f"{losses}L", WL_LOSS_FILL),
|
||||||
|
(" - ", WL_DASH_FILL),
|
||||||
|
(f"{pct:.0f}%", _winrate_gradient(pct)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _rating_segments(rating):
|
||||||
|
"""Build colored (text, fill) segments for the inline ``(1468)`` rating."""
|
||||||
rating_str = f"{int(round(rating))}" if isinstance(rating, (int, float)) else "—"
|
rating_str = f"{int(round(rating))}" if isinstance(rating, (int, float)) else "—"
|
||||||
if isinstance(delta, (int, float)) and delta != 0:
|
|
||||||
dcol = WIN_FILL if delta > 0 else LOSS_FILL
|
|
||||||
return [(f"({rating_str} ", RATING_FILL), (f"{'+' if delta > 0 else ''}{int(delta)}", dcol), (")", RATING_FILL)]
|
|
||||||
return [(f"({rating_str})", RATING_FILL)]
|
return [(f"({rating_str})", RATING_FILL)]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ def build_scoreboard_model(game: dict[str, Any], lang_column: str = "<English>")
|
|||||||
"duration": game.get("duration"),
|
"duration": game.get("duration"),
|
||||||
"map": str(game.get("mission_name") or ""),
|
"map": str(game.get("mission_name") or ""),
|
||||||
"mission_mode": str(game.get("mission_mode") or ""),
|
"mission_mode": str(game.get("mission_mode") or ""),
|
||||||
|
"tournament_id": tss.get("tournament_id"),
|
||||||
"tournament_name": str(tss.get("tournament_name") or ""),
|
"tournament_name": str(tss.get("tournament_name") or ""),
|
||||||
"bracket": str(tss.get("bracket") or ""),
|
"bracket": str(tss.get("bracket") or ""),
|
||||||
"is_draw": is_draw,
|
"is_draw": is_draw,
|
||||||
|
|||||||
@@ -1,20 +1,99 @@
|
|||||||
"""TSS win/loss standings — STUB.
|
"""TSS win/loss standings, scoped per tournament.
|
||||||
|
|
||||||
SREBOT tracks squadron W/L globally. TSS needs the same idea but **scoped per
|
SREBOT tracks squadron W/L globally. TSS records only mean something inside a
|
||||||
tournament** (a team's record only means something inside its bracket), so the real
|
bracket, so standings are scoped to ``(tournament_id, team_id)`` and derived live
|
||||||
implementation is deferred. Until then ``get_tss_standings`` returns ``None`` for
|
from the rows already persisted per match — no separate counter table:
|
||||||
every team and the scoreboard renders a neutral placeholder in the W/L slot.
|
|
||||||
|
|
||||||
TODO(tss-wl): persist per-(tournament_id, team_id) wins/losses and resolve here.
|
* ``match_summary`` — one row per session, carries ``tournament_id``
|
||||||
|
* ``player_games_hist`` — one row per used vehicle per player, carries
|
||||||
|
``team_id``, ``session_id`` and ``victor_bool`` ('Win'/'Loss')
|
||||||
|
|
||||||
|
A draw is already written as ``victor_bool = 'Loss'`` for *both* teams (the winning
|
||||||
|
slot is empty), so counting over ``victor_bool`` treats a draw as a loss for both
|
||||||
|
sides with no special handling.
|
||||||
|
|
||||||
|
``insert_match``/``insert_player_games`` run before the scoreboard renders (see
|
||||||
|
``tss_ws._handle_game``), so the record returned here includes the match on the
|
||||||
|
board being drawn.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
log = logging.getLogger("tssbot.wl")
|
||||||
|
|
||||||
def get_tss_standings(team_id: Optional[str], tournament_id: Optional[Any] = None) -> Optional[dict[str, int]]:
|
# One outcome per session (vehicle rows are duplicated across a player's lineup, so
|
||||||
"""Return ``{"wins": int, "losses": int}`` for a team, or ``None`` if untracked.
|
# collapse to a single victor_bool per session before counting — mirrors the
|
||||||
|
# DISTINCT-first rule in storage.py's docstring), then tally wins vs losses.
|
||||||
Stub: always ``None`` until per-tournament W/L tracking lands.
|
_STANDINGS_SQL = """
|
||||||
|
SELECT
|
||||||
|
SUM(CASE WHEN victor = 'Win' THEN 1 ELSE 0 END) AS wins,
|
||||||
|
SUM(CASE WHEN victor = 'Loss' THEN 1 ELSE 0 END) AS losses
|
||||||
|
FROM (
|
||||||
|
SELECT pgh.session_id, MAX(pgh.victor_bool) AS victor
|
||||||
|
FROM player_games_hist pgh
|
||||||
|
JOIN match_summary ms ON ms.session_id = pgh.session_id
|
||||||
|
WHERE pgh.team_id = ? AND ms.tournament_id = ?
|
||||||
|
GROUP BY pgh.session_id
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_tss_standings(
|
||||||
|
team_id: Optional[Any], tournament_id: Optional[Any] = None
|
||||||
|
) -> Optional[dict[str, int]]:
|
||||||
|
"""Return ``{"wins": int, "losses": int}`` for a team within one tournament.
|
||||||
|
|
||||||
|
Returns ``None`` when either id is missing, the DB is unavailable, the query
|
||||||
|
errors, or the team has no recorded matches in that tournament — in every such
|
||||||
|
case the scoreboard leaves the W/L slot blank (matching offline/sample renders).
|
||||||
|
"""
|
||||||
|
if team_id is None or tournament_id is None:
|
||||||
return None
|
return None
|
||||||
|
try:
|
||||||
|
tid = int(team_id)
|
||||||
|
tournament = int(tournament_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Imported here so the module stays importable without STORAGE_VOL_PATH set
|
||||||
|
# (e.g. for unit tests that point _db_path at a temp database).
|
||||||
|
db_path = _db_path()
|
||||||
|
if db_path is None or not os.path.exists(db_path):
|
||||||
|
# No file → nothing to read (and don't let sqlite3.connect create a stray
|
||||||
|
# empty DB). Keeps offline/sample renders showing a blank slot.
|
||||||
|
return None
|
||||||
|
|
||||||
|
# NOT a ``mode=ro`` connection: the production DB is WAL (see storage._PRAGMAS),
|
||||||
|
# and a pure read-only open can fail when a -wal exists but the -shm wal-index
|
||||||
|
# does not (it can't be created read-only). A normal connection set to
|
||||||
|
# ``query_only`` reads WAL safely in every state while never writing. ``timeout``
|
||||||
|
# waits out a momentary write lock from the concurrent ingest path.
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(db_path, timeout=5.0)
|
||||||
|
conn.execute("PRAGMA query_only=ON")
|
||||||
|
except sqlite3.Error:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
row = conn.execute(_STANDINGS_SQL, (tid, tournament)).fetchone()
|
||||||
|
except sqlite3.Error as exc:
|
||||||
|
log.warning("[TSS-WL] standings query failed (team=%s tourn=%s): %s", tid, tournament, exc)
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not row or row[0] is None:
|
||||||
|
return None
|
||||||
|
return {"wins": int(row[0] or 0), "losses": int(row[1] or 0)}
|
||||||
|
|
||||||
|
|
||||||
|
def _db_path() -> Optional[str]:
|
||||||
|
"""Resolve the battles DB path, or ``None`` if storage isn't configured."""
|
||||||
|
try:
|
||||||
|
from .storage import TSS_BATTLES_DB_PATH
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return str(TSS_BATTLES_DB_PATH)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "SHARED"))
|
||||||
|
|
||||||
|
from BOT.transform import build_scoreboard_model
|
||||||
|
|
||||||
|
|
||||||
|
def _game():
|
||||||
|
return {
|
||||||
|
"_id": "abc",
|
||||||
|
"winner": "1",
|
||||||
|
"mission_name": "Kursk",
|
||||||
|
"tss": {
|
||||||
|
"tournament_id": 24965,
|
||||||
|
"tournament_name": "2x2 RBm Tanks",
|
||||||
|
"1": {"team_id": "111", "team_name": "A", "players": [{"uid": "1", "pvp_ratio": 1500}]},
|
||||||
|
"2": {"team_id": "222", "team_name": "B", "players": [{"uid": "2", "pvp_ratio": 1400}]},
|
||||||
|
},
|
||||||
|
"players": {
|
||||||
|
"1": {"name": "alice", "team": "1", "units": [{"unit": "t34", "used": True}]},
|
||||||
|
"2": {"name": "bob", "team": "2", "units": [{"unit": "pz", "used": True}]},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_carries_tournament_id():
|
||||||
|
model = build_scoreboard_model(_game())
|
||||||
|
assert model is not None
|
||||||
|
assert model["tournament_id"] == 24965
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import pathlib
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "SHARED"))
|
||||||
|
|
||||||
|
from BOT import wl
|
||||||
|
|
||||||
|
|
||||||
|
def _make_db(path: str, matches, players):
|
||||||
|
"""Seed the two real tables with the columns get_tss_standings reads.
|
||||||
|
|
||||||
|
matches: list of (session_id, tournament_id)
|
||||||
|
players: list of (session_id, team_id, vehicle_internal, victor_bool)
|
||||||
|
"""
|
||||||
|
conn = sqlite3.connect(path)
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE match_summary (session_id TEXT PRIMARY KEY, tournament_id INTEGER)"
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE player_games_hist ("
|
||||||
|
" UID TEXT, session_id TEXT, team_id INTEGER,"
|
||||||
|
" vehicle_internal TEXT, victor_bool TEXT)"
|
||||||
|
)
|
||||||
|
conn.executemany("INSERT INTO match_summary VALUES (?, ?)", matches)
|
||||||
|
conn.executemany(
|
||||||
|
"INSERT INTO player_games_hist (UID, session_id, team_id, vehicle_internal, victor_bool)"
|
||||||
|
" VALUES ('u', ?, ?, ?, ?)",
|
||||||
|
players,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _point_at(monkeypatch, path):
|
||||||
|
monkeypatch.setattr(wl, "_db_path", lambda: str(path))
|
||||||
|
|
||||||
|
|
||||||
|
def test_basic_wins_and_losses(tmp_path, monkeypatch):
|
||||||
|
db = tmp_path / "battles.db"
|
||||||
|
_make_db(
|
||||||
|
str(db),
|
||||||
|
matches=[("s1", 100), ("s2", 100), ("s3", 100)],
|
||||||
|
players=[
|
||||||
|
("s1", 7, "t34", "Win"),
|
||||||
|
("s2", 7, "t34", "Win"),
|
||||||
|
("s3", 7, "t34", "Loss"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
_point_at(monkeypatch, db)
|
||||||
|
assert wl.get_tss_standings(7, 100) == {"wins": 2, "losses": 1}
|
||||||
|
|
||||||
|
|
||||||
|
def test_per_tournament_isolation(tmp_path, monkeypatch):
|
||||||
|
db = tmp_path / "battles.db"
|
||||||
|
# Same team_id appears in two tournaments; records must not bleed across.
|
||||||
|
_make_db(
|
||||||
|
str(db),
|
||||||
|
matches=[("a", 100), ("b", 100), ("c", 200)],
|
||||||
|
players=[
|
||||||
|
("a", 7, "t34", "Win"),
|
||||||
|
("b", 7, "t34", "Loss"),
|
||||||
|
("c", 7, "t34", "Win"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
_point_at(monkeypatch, db)
|
||||||
|
assert wl.get_tss_standings(7, 100) == {"wins": 1, "losses": 1}
|
||||||
|
assert wl.get_tss_standings(7, 200) == {"wins": 1, "losses": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_is_loss_for_both_teams(tmp_path, monkeypatch):
|
||||||
|
db = tmp_path / "battles.db"
|
||||||
|
# A draw is persisted as victor_bool='Loss' for both participating teams.
|
||||||
|
_make_db(
|
||||||
|
str(db),
|
||||||
|
matches=[("s1", 100)],
|
||||||
|
players=[
|
||||||
|
("s1", 7, "t34", "Loss"),
|
||||||
|
("s1", 9, "pz", "Loss"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
_point_at(monkeypatch, db)
|
||||||
|
assert wl.get_tss_standings(7, 100) == {"wins": 0, "losses": 1}
|
||||||
|
assert wl.get_tss_standings(9, 100) == {"wins": 0, "losses": 1}
|
||||||
|
|
||||||
|
|
||||||
|
def test_vehicle_duplicate_rows_count_once(tmp_path, monkeypatch):
|
||||||
|
db = tmp_path / "battles.db"
|
||||||
|
# One player on the team flew two vehicles -> two rows, same session/outcome.
|
||||||
|
_make_db(
|
||||||
|
str(db),
|
||||||
|
matches=[("s1", 100)],
|
||||||
|
players=[
|
||||||
|
("s1", 7, "t34", "Win"),
|
||||||
|
("s1", 7, "is2", "Win"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
_point_at(monkeypatch, db)
|
||||||
|
assert wl.get_tss_standings(7, 100) == {"wins": 1, "losses": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_string_team_id_is_coerced(tmp_path, monkeypatch):
|
||||||
|
db = tmp_path / "battles.db"
|
||||||
|
_make_db(str(db), matches=[("s1", 100)], players=[("s1", 7, "t34", "Win")])
|
||||||
|
_point_at(monkeypatch, db)
|
||||||
|
# The render model carries team_id as a string.
|
||||||
|
assert wl.get_tss_standings("7", 100) == {"wins": 1, "losses": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_ids_return_none(tmp_path, monkeypatch):
|
||||||
|
db = tmp_path / "battles.db"
|
||||||
|
_make_db(str(db), matches=[("s1", 100)], players=[("s1", 7, "t34", "Win")])
|
||||||
|
_point_at(monkeypatch, db)
|
||||||
|
assert wl.get_tss_standings(None, 100) is None
|
||||||
|
assert wl.get_tss_standings(7, None) is None
|
||||||
|
assert wl.get_tss_standings("not-a-number", 100) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_team_returns_none(tmp_path, monkeypatch):
|
||||||
|
db = tmp_path / "battles.db"
|
||||||
|
_make_db(str(db), matches=[("s1", 100)], players=[("s1", 7, "t34", "Win")])
|
||||||
|
_point_at(monkeypatch, db)
|
||||||
|
assert wl.get_tss_standings(999, 100) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_db_returns_none(tmp_path, monkeypatch):
|
||||||
|
_point_at(monkeypatch, tmp_path / "does_not_exist.db")
|
||||||
|
assert wl.get_tss_standings(7, 100) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_reads_wal_mode_db_with_open_writer(tmp_path, monkeypatch):
|
||||||
|
# The production DB is WAL. Reproduce a live-ingest state: WAL journal, no
|
||||||
|
# autocheckpoint (so a non-empty -wal/-shm lingers), and a writer connection
|
||||||
|
# still open while we read — get_tss_standings must still return the record.
|
||||||
|
db = tmp_path / "battles.db"
|
||||||
|
_make_db(
|
||||||
|
str(db),
|
||||||
|
matches=[("s1", 100), ("s2", 100)],
|
||||||
|
players=[("s1", 7, "t34", "Win"), ("s2", 7, "t34", "Loss")],
|
||||||
|
)
|
||||||
|
writer = sqlite3.connect(str(db))
|
||||||
|
writer.execute("PRAGMA journal_mode=WAL")
|
||||||
|
writer.execute("PRAGMA wal_autocheckpoint=0")
|
||||||
|
writer.execute(
|
||||||
|
"INSERT INTO player_games_hist (UID, session_id, team_id, vehicle_internal, victor_bool)"
|
||||||
|
" VALUES ('u', 's3', 7, 't34', 'Win')"
|
||||||
|
)
|
||||||
|
writer.execute("INSERT INTO match_summary VALUES ('s3', 100)")
|
||||||
|
writer.commit() # committed to the WAL, not checkpointed; writer stays open
|
||||||
|
try:
|
||||||
|
_point_at(monkeypatch, db)
|
||||||
|
assert wl.get_tss_standings(7, 100) == {"wins": 2, "losses": 1}
|
||||||
|
finally:
|
||||||
|
writer.close()
|
||||||
Reference in New Issue
Block a user