Add briscola: classic two-player and briscola a quattro
CI / Build and push docker image (push) Successful in 1m45s
CI / Build and push docker image (push) Successful in 1m45s
New tavolo-briscola distribution implementing briscola on the platform: - pure rules (state.py, engine.py): 3-card deals, a face-up trump that is the last card drawn, trump-first trick resolution (A > 3 > K > H > J > 7 > 6 > 5 > 4 > 2), 120 points per deal, 60-60 patta re-deals, a configurable target score (default 61), deal-end acknowledgements and turn auto-play; - BriscolaEngine (briscola, 2 players) and BriscolaQuattroEngine (briscola_a_quattro, 4 players in fixed partnerships) adapters; - registered in tavolo-app, packaged in the Docker image, documented. Platform fixes surfaced by a two-player lobby: - the websocket state and snapshot payloads now carry seats_open (they were only in the REST lobby payload), so lobbies with player counts other than four render correctly; seats_open is clamped at zero; - the web client logs unparseable server messages instead of silently dropping them. Web client: briscola table for both variants (stock, face-up trump, trick, last-trick banner), deal summary modal and game-over overlay. Tests: tavolo-briscola rules/adapter suites, app integration tests (lobby, websocket play, turn timeout, deal-end acks, persistence), a platform seats_open test and frontend model parsing tests.
This commit is contained in:
@@ -136,5 +136,91 @@ class ScoponeStatsTest(unittest.TestCase):
|
||||
self.assertTrue(all(r["matches"] == 1 for r in by_sub.values()))
|
||||
|
||||
|
||||
async def _finished_briscola_session() -> GameSession:
|
||||
engine = platform.registry.require("briscola")
|
||||
session = GameSession(
|
||||
id="stats-briscola-1",
|
||||
game_type=engine.id,
|
||||
join_code="BS0001",
|
||||
creator_sub="alice",
|
||||
players=[Seat(user_sub="alice", display_name="Alice")],
|
||||
)
|
||||
engine.create(session, {})
|
||||
engine.join(session, "bob", "Bob")
|
||||
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)
|
||||
engine.handle_action(
|
||||
session, player.sub, "play", {"card": player.hand[0].code}
|
||||
)
|
||||
moves += 1
|
||||
assert engine.is_finished(session)
|
||||
return session
|
||||
|
||||
|
||||
class BriscolaStatsTest(unittest.TestCase):
|
||||
@async_test
|
||||
async def test_finished_briscola_match_persisted_with_summary(self) -> None:
|
||||
await tortoise_mixin._bind()
|
||||
ctx = tortoise_mixin._ctx
|
||||
assert ctx is not None
|
||||
engine = platform.registry.require("briscola")
|
||||
session = await _finished_briscola_session()
|
||||
with ctx:
|
||||
await save_match_result(session, engine)
|
||||
self.assertEqual(1, await Match.all().count())
|
||||
self.assertEqual(2, await MatchPlayer.all().count())
|
||||
|
||||
match = await Match.all().first()
|
||||
assert match is not None
|
||||
self.assertEqual("briscola", match.game_type)
|
||||
summary = match.result
|
||||
self.assertIn("team_a_score", summary)
|
||||
self.assertIn("team_b_score", summary)
|
||||
self.assertIn("winner_team", summary)
|
||||
self.assertIn("target_score", summary)
|
||||
self.assertIn("deals_played", summary)
|
||||
self.assertIn("deal_scores", summary)
|
||||
|
||||
winner_team = summary["winner_team"]
|
||||
players = {p.user_sub: p for p in await MatchPlayer.all()}
|
||||
for row in players.values():
|
||||
if row.won:
|
||||
self.assertEqual(winner_team, row.team)
|
||||
else:
|
||||
self.assertNotEqual(winner_team, row.team)
|
||||
# A two-player match moves Elo by 16 points each.
|
||||
ratings = {r.user_sub: r.rating for r in await PlayerRating.all()}
|
||||
self.assertEqual(2, len(ratings))
|
||||
self.assertEqual(
|
||||
{INITIAL_RATING + 16, INITIAL_RATING - 16}, set(ratings.values())
|
||||
)
|
||||
|
||||
@async_test
|
||||
async def test_history_exposes_briscola_result(self) -> None:
|
||||
await tortoise_mixin._bind()
|
||||
ctx = tortoise_mixin._ctx
|
||||
assert ctx is not None
|
||||
engine = platform.registry.require("briscola")
|
||||
session = await _finished_briscola_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("briscola", results[0]["game_type"])
|
||||
self.assertIn("team_a_score", results[0]["result"])
|
||||
self.assertEqual(2, len(results[0]["players"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user