diff --git a/TODO.md b/TODO.md index 359df7f..ebb4c61 100644 --- a/TODO.md +++ b/TODO.md @@ -21,7 +21,7 @@ - [x] Check against rules on correct quantity of of obstacles and obstacle refresh rules - [x] 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 +- [x] 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 - [ ] Double check if PvP obstacles should allow card re-draw diff --git a/frontend/src/components/scene/ChallengePanel.svelte b/frontend/src/components/scene/ChallengePanel.svelte index b2c714b..95fa436 100644 --- a/frontend/src/components/scene/ChallengePanel.svelte +++ b/frontend/src/components/scene/ChallengePanel.svelte @@ -126,6 +126,9 @@ Temporary Obstacle: {getCardDisplay(ch.temp_card)} — {playerName(ch.acting_player_id)} must answer it!

{:else} +

{state.obstacles.filter(o => chObstacleIds.includes(o.id)).length > 1 + ? 'Other Pi-Rats may assist. Assistants do not draw replacement cards.' + : 'Only the attempting Pi-Rat can play against this single Obstacle.'}

{#each chObstacleIds as oid} {@const obs = state.obstacles.find(o => o.id === oid)} diff --git a/frontend/src/components/scene/ObstacleItem.svelte b/frontend/src/components/scene/ObstacleItem.svelte index 4665e34..d528fd5 100644 --- a/frontend/src/components/scene/ObstacleItem.svelte +++ b/frontend/src/components/scene/ObstacleItem.svelte @@ -22,6 +22,11 @@ $: success_count = column_cards.filter(c => c.success).length; $: is_completed = success_count >= state.players.length; $: in_challenge = openChallenges.some(c => JSON.parse(c.obstacle_ids || '[]').includes(obs.id)); + $: challenge = openChallenges.find(c => JSON.parse(c.obstacle_ids || '[]').includes(obs.id)); + $: canPlay = state.player.role === 'pirat' && !state.player.is_dead && !state.player.needs_reroll + && challenge && challenge.tax_state !== 'requested' + && (challenge.acting_player_id === state.player.id + || state.obstacles.filter(o => JSON.parse(challenge.obstacle_ids || '[]').includes(o.id)).length > 1); $: cardCodeLabel = `${cardValue(obs.original_card)}${SUIT_CHAR[obs.suit] || ''}`; $: is_face_active = ['J', 'Q', 'K'].includes(active_card_code.slice(0, -1)); @@ -61,7 +66,7 @@ const cardCode = e.dataTransfer.getData('text/plain'); if (cardCode) { if (isJoker(cardCode)) playJoker(cardCode); - else playCard(cardCode); + else if (canPlay) playCard(cardCode); } }}> {#if error} diff --git a/frontend/src/lib/changelog.js b/frontend/src/lib/changelog.js index b8c1eb5..b102391 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 = 33; +export const VERSION = 34; // Newest first. Each entry: { version, date: 'YYYY-MM-DD', changes: [string, ...] }. export const CHANGELOG = [ + { version: 34, date: '2026-09-04', changes: ['Assistance is available only when a Challenge has multiple active Obstacles. Gat and Name Tax takeovers still work against a single Obstacle.'] }, { version: 33, date: '2026-09-04', changes: ['Hand sizes compare ranks across the crew, including Pi-Rats whose players are the Deep.', 'Discarded cards return to the deck at scene setup. Only the previous Deep can refresh their hand during upkeep, once per scene.'] }, { 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.'] }, diff --git a/src/pirats/crud_challenge.py b/src/pirats/crud_challenge.py index 1b19c28..e5b906d 100644 --- a/src/pirats/crud_challenge.py +++ b/src/pirats/crud_challenge.py @@ -120,6 +120,12 @@ def play_challenge_card( if challenge.tax_state == "requested": return False, "A Gat/Name Tax is pending. Wait for an answer before playing cards.", {} + if player.role != "pirat" or player.is_dead or player.needs_reroll: + return False, "Only participating Pi-Rats can play cards.", {} + active_ids = {o.id for o in game.obstacles} & set(json.loads(challenge.obstacle_ids)) + if player.id != challenge.acting_player_id and len(active_ids) < 2: + return False, "Assistance requires multiple Obstacles in the current Challenge.", {} + # An Obstacle beaten as many times as there are players is spent played_list = json.loads(obstacle.played_cards) current_successes = sum(1 for c in played_list if c.get("success") is True) diff --git a/tests/test_game.py b/tests/test_game.py index 2ccf2a9..86a44dc 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -242,7 +242,7 @@ def test_assistant_does_not_draw(session): session.add_all([obs, p3]) session.commit() - ok, msg = crud.create_challenge(session, game.id, deep.id, p2.id, [obs.id]) + ok, msg = crud.create_challenge(session, game.id, deep.id, p2.id, [o.id for o in game.obstacles]) assert ok session.refresh(game) @@ -2726,3 +2726,19 @@ def test_deep_refresh_does_not_recycle_discards_or_allow_pirats(session): assert "KC" not in crud.get_game_deck(game) crud.confirm_deep_refresh(session, deep.id, ["AH"]) assert "AH" in crud.get_player_hand(deep) + + +def test_single_obstacle_rejects_assistance_but_allows_tax_actor(session): + game, deep, (rat, helper) = make_scene_game(session, num_pirats=2) + obs = game.obstacles[0] + helper.hand_cards = '["KH"]' + helper.completed_personal_1 = True + session.commit() + assert crud.create_challenge(session, game.id, deep.id, rat.id, [obs.id])[0] + ok, msg, _ = crud.play_challenge_card(session, helper.id, obs.id, "KH") + assert not ok and 'multiple' in msg + assert crud.get_player_hand(helper) == ["KH"] + challenge = game.challenges[0] + assert crud.request_tax(session, challenge.id, rat.id, helper.id)[0] + assert crud.respond_tax(session, challenge.id, helper.id, True)[0] + assert crud.play_challenge_card(session, helper.id, obs.id, "KH")[0]