"""Scopone result-persistence tests: engine result to Postgres to API. The platform suite covers the generic machinery against a toy game; these tests pin the scopone-specific shape: the match ``result`` summary, per-player scores/teams, Elo deltas and what the history and leaderboard endpoints expose for a finished scopone match. """ from __future__ import annotations import unittest from httpx import ASGITransport, AsyncClient from tavolo.app import app, platform, tortoise_mixin from tavolo.platform import GameSession, Seat from tavolo.platform.elo import INITIAL_RATING from tavolo.platform.models import Match, MatchPlayer, PlayerRating from tavolo.platform.stats import save_match_result from tavolo.scopone import engine as rules from tests.helpers import async_test, oidc_user PLAYERS = ("alice", "bob", "carol", "dave") async def _finished_session(target_score: int = 1) -> GameSession: engine = platform.registry.require("scopone_scientifico") session = GameSession( id="stats-scope-1", game_type=engine.id, join_code="SS0001", creator_sub="alice", players=[Seat(user_sub="alice", display_name="Alice")], ) engine.create(session, {"target_score": target_score}) for name in PLAYERS[1:]: engine.join(session, name, name.capitalize()) moves = 0 while not engine.is_finished(session) and moves < 200000: state = session.state if state.phase == "hand_end": for player in state.players: engine.handle_action(session, player.sub, "ack", {}) continue player = next(p for p in state.players if p.seat == state.turn) played = player.hand[0] options = rules.legal_captures(state.table, played) payload = {"card": played.code} if options: payload["capture"] = [c.code for c in options[0]] engine.handle_action(session, player.sub, "play", payload) moves += 1 assert engine.is_finished(session) return session class ScoponeStatsTest(unittest.TestCase): @async_test async def test_finished_match_persisted_with_scopone_summary(self) -> None: await tortoise_mixin._bind() ctx = tortoise_mixin._ctx assert ctx is not None engine = platform.registry.require("scopone_scientifico") session = await _finished_session() with ctx: await save_match_result(session, engine) self.assertEqual(1, await Match.all().count()) self.assertEqual(4, await MatchPlayer.all().count()) match = await Match.all().first() assert match is not None self.assertEqual("scopone_scientifico", match.game_type) summary = match.result self.assertEqual(1, summary["target_score"]) self.assertIn("team_a_score", summary) self.assertIn("team_b_score", summary) self.assertIn("winner_team", summary) self.assertIn("hands_played", summary) self.assertIn("hand_scores", summary) players = {p.user_sub: p for p in await MatchPlayer.all()} winner_team = summary["winner_team"] for sub, row in players.items(): if row.won: self.assertEqual(winner_team, row.team) else: self.assertNotEqual(winner_team, row.team) self.assertEqual( summary["team_a_score"] if row.team == "A" else summary["team_b_score"], row.score, ) # Winners share a team, losers the other. winners = {sub for sub, row in players.items() if row.won} self.assertEqual(2, len(winners)) @async_test async def test_finished_match_updates_elo(self) -> None: await tortoise_mixin._bind() ctx = tortoise_mixin._ctx assert ctx is not None engine = platform.registry.require("scopone_scientifico") session = await _finished_session() with ctx: await save_match_result(session, engine) ratings = {r.user_sub: r.rating for r in await PlayerRating.all()} self.assertEqual(4, len(ratings)) self.assertEqual( {INITIAL_RATING + 16, INITIAL_RATING - 16}, set(ratings.values()) ) @async_test async def test_history_and_leaderboard_expose_scopone_result(self) -> None: await tortoise_mixin._bind() ctx = tortoise_mixin._ctx assert ctx is not None engine = platform.registry.require("scopone_scientifico") session = await _finished_session() with ctx: await save_match_result(session, engine) transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client: with oidc_user("alice"): history = await client.get("/api/me/matches") self.assertEqual(200, history.status_code) results = history.json()["results"] self.assertEqual(1, len(results)) self.assertEqual("scopone_scientifico", results[0]["game_type"]) self.assertIn("team_a_score", results[0]["result"]) self.assertIn("your_elo_delta", results[0]) self.assertEqual(4, len(results[0]["players"])) board = await client.get("/api/leaderboard") self.assertEqual(200, board.status_code) by_sub = {r["user_sub"]: r for r in board.json()["results"]} self.assertEqual(4, len(by_sub)) self.assertTrue(all(r["matches"] == 1 for r in by_sub.values())) if __name__ == "__main__": unittest.main()