From 749bef6ff12f5ab3d04be96dcc45487c70c9f7ee Mon Sep 17 00:00:00 2001 From: Tim McCarthy Date: Fri, 4 Sep 2026 19:21:35 -0700 Subject: [PATCH] Discard completed obstacles automatically and verify scene refresh rules --- TODO.md | 2 +- frontend/src/lib/changelog.js | 3 ++- src/pirats/crud_challenge.py | 5 +++++ src/pirats/crud_scene.py | 3 ++- tests/test_game.py | 41 +++++++++++++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index d11edd2..3c10f20 100644 --- a/TODO.md +++ b/TODO.md @@ -19,7 +19,7 @@ ## Rules -- [ ] Check against rules on correct quantity of of obstacles and obstacle refresh rules +- [x] Check against rules on correct quantity of of obstacles and obstacle refresh rules - [ ] Check against the rules on pi-rat hand size and conditions for drawing more cards - [ ] Assisting other pi-rats should only be available if there are multiple obstacles in the current challenge - [ ] Checking off personal objectives (gat/name/death) should be handled by group vote rather than adjudication by the deep diff --git a/frontend/src/lib/changelog.js b/frontend/src/lib/changelog.js index 9fd8c2a..e6b5810 100644 --- a/frontend/src/lib/changelog.js +++ b/frontend/src/lib/changelog.js @@ -6,10 +6,11 @@ // - Add a CHANGELOG entry only when a commit changes something players can // see. Skip refactors, tests, and tooling. Keep wording player-facing. -export const VERSION = 31; +export const VERSION = 32; // Newest first. Each entry: { version, date: 'YYYY-MM-DD', changes: [string, ...] }. export const CHANGELOG = [ + { version: 32, date: '2026-09-04', changes: ['Obstacles are automatically discarded with their columns once they reach one success per player. Unfinished obstacles carry over between scenes.'] }, { version: 31, date: '2026-09-04', changes: ['Change your rank-up vote until everyone has voted. The final vote now reveals the result and advances automatically, without a Ready button.', 'The vote result stays visible through upkeep and next-scene setup.'] }, { version: 30, date: '2026-09-04', changes: ['The crew roster shows voting progress with icons.', 'Your submitted vote now shows the crewmate you nominated.'] }, { version: 29, date: '2026-09-04', changes: ['Removed the remaining deck count from the table.'] }, diff --git a/src/pirats/crud_challenge.py b/src/pirats/crud_challenge.py index 9ae52c5..1b19c28 100644 --- a/src/pirats/crud_challenge.py +++ b/src/pirats/crud_challenge.py @@ -167,6 +167,11 @@ def play_challenge_card( role_note = "" if player.id == challenge.acting_player_id else " (assist)" add_game_event(db, game.id, f"{player.name} played {cards.parse_card(card_code)['display']} on '{obstacle.title}'{role_note}. {result['details']}", kind="card") + if obstacle.success_count >= len(game.players): + add_game_event(db, game.id, f"'{obstacle.title}' is complete and discarded with its column.", kind="obstacle") + db.delete(obstacle) + db.commit() + result["drew_card"] = drew_card result["is_joker"] = False return True, "Card played successfully.", result diff --git a/src/pirats/crud_scene.py b/src/pirats/crud_scene.py index f74aad4..72cd963 100644 --- a/src/pirats/crud_scene.py +++ b/src/pirats/crud_scene.py @@ -405,7 +405,8 @@ def grant_story_bonus_rank(db: Session, game: Game, granter: Player, target): def clear_completed_obstacle(db: Session, game_id: str, obstacle_id: str): obstacle = db.get(Obstacle, obstacle_id) - if obstacle: + game = get_game(db, game_id) + if obstacle and game and obstacle.game_id == game_id and obstacle.success_count >= len(game.players): add_game_event(db, game_id, f"The Deep cleared the completed obstacle '{obstacle.title}'.", kind="obstacle") db.delete(obstacle) db.commit() diff --git a/tests/test_game.py b/tests/test_game.py index 2abb0e9..09e4420 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -2651,3 +2651,44 @@ def test_kick_last_pending_voter_completes_voting(session): assert crud.kick_player(session, game, b)[0] assert game.phase == "deep_upkeep" assert game.last_rank_up_player_id == a.id + + +def test_completed_obstacle_discards_without_replacement(session): + game, deep, (rat,) = make_scene_game(session) + obs = game.obstacles[0] + oid = obs.id + obs.played_cards = json.dumps([{"card": "KC", "success": True}]) + rat.hand_cards = json.dumps(["QD"]) + session.add(obs) + session.add(rat) + session.commit() + assert crud.create_challenge(session, game.id, deep.id, rat.id, [oid])[0] + assert crud.play_challenge_card(session, rat.id, oid, "QD")[0] + assert session.get(Obstacle, oid) is None + session.refresh(game) + assert len(game.obstacles) == 1 + + +def test_cannot_clear_unfinished_obstacle(session): + game, deep, (rat,) = make_scene_game(session) + oid = game.obstacles[0].id + crud.clear_completed_obstacle(session, game.id, oid) + assert session.get(Obstacle, oid) is not None + + +def test_setup_joker_only_increases_following_scene(session, monkeypatch): + game, deep, (rat,) = make_scene_game(session) + for obs in list(game.obstacles): + session.delete(obs) + game.current_scene_number = 2 + deep.previous_role = "pirat" + rat.previous_role = "deep" + session.commit() + monkeypatch.setattr('pirats.crud_scene.reshuffle_discard_pile', lambda db, game: None) + game.deck_cards = json.dumps(["Joker1", "2H", "3H", "4H"]) + session.commit() + assert crud.confirm_scene_setup(session, game.id)[0] + session.refresh(game) + assert len(game.obstacles) == 2 + assert game.extra_obstacles == 1 + assert crud.get_game_deck(game) == ["4H"]