Skip to content

Silent: 68 card effects#4

Open
JackSwitzer wants to merge 1 commit intomainfrom
work/cards-silent
Open

Silent: 68 card effects#4
JackSwitzer wants to merge 1 commit intomainfrom
work/cards-silent

Conversation

@JackSwitzer
Copy link
Owner

@JackSwitzer JackSwitzer commented Feb 4, 2026

Summary

  • Implement ~68 Silent card effects
  • Poison mechanics
  • Shiv generation and discard triggers
  • Intangible, retain effects
  • Power effects (Thousand Cuts, Accuracy, etc.)

Test Results

328 tests passing

Files Changed

  • packages/engine/effects/cards.py (+705 lines)
  • packages/engine/registry/powers.py (power triggers)
  • tests/test_silent_cards.py (NEW, 96 tests)

🤖 Generated with Claude Code


Note

Medium Risk
Large addition of combat/effect logic that changes core simulation behavior (new card effects and power triggers), though scoped to Silent mechanics and covered by new tests.

Overview
Adds a full set of Silent card effect implementations and wiring, including Poison, Shiv generation, discard/draw/energy/block conditionals, and X-cost/special behaviors via SILENT_CARD_EFFECTS + get_silent_card_effects() in packages/engine/effects/cards.py.

Extends the power trigger registry (packages/engine/registry/powers.py) with Silent-specific start/end-of-turn, on-card-play, discard-trigger, damage modifier, and on-death handlers (e.g., ToolsOfTheTrade, Burst, Accuracy, Reflex/Tactician, CorpseExplosion).

Adds a new tests/test_silent_cards.py suite asserting Silent card stats/effects and key mechanics interactions, and expands attack-card identification to include Silent attacks for playability/discard logic.

Written by Cursor Bugbot for commit b620e9d. This will update automatically on new commits. Configure here.

Batch 1 - Poison effects:
- apply_poison (Deadly Poison, Poisoned Stab)
- double_poison (Catalyst - 2x/3x multiplier)
- apply_poison_all, apply_weak_2_all (Crippling Poison)
- apply_poison_random_3_times (Bouncing Flask)
- apply_corpse_explosion (Corpse Explosion on death AoE)
- apply_poison_all_each_turn (Noxious Fumes power)

Batch 2 - Shiv mechanics:
- add_shivs_to_hand (Blade Dance, Cloak and Dagger)
- add_shiv_each_turn (Infinite Blades power)
- shivs_deal_more_damage (Accuracy power)
- add_shivs_equal_to_discarded (Storm of Steel)

Batch 3 - Discard triggers:
- discard_1, discard_x, discard_random_1 (selection-based)
- discard_hand, discard_hand_draw_same (Calculated Gamble)
- discard_non_attacks (Unload)
- when_discarded_draw (Reflex)
- when_discarded_gain_energy (Tactician)
- cost_reduces_per_discard (Eviscerate)
- refund_2_energy_if_discarded_this_turn (Sneaky Strike)

Batch 4 - Special effects:
- gain_intangible, lose_1_dexterity_each_turn (Wraith Form)
- no_draw_this_turn, cards_cost_0_this_turn (Bullet Time)
- double_damage_next_turn (Phantasmal Killer)
- double_next_skills (Burst)
- block_not_removed_next_turn (Blur)
- gain_1_block_per_card_played (After Image)
- deal_damage_per_card_played (A Thousand Cuts)

X-cost effects:
- damage_x_times_energy (Skewer)
- apply_weak_x, apply_strength_down_x (Malaise)
- draw_x_next_turn, gain_x_energy_next_turn (Doppelganger)

Power triggers added to powers.py:
- ToolsOfTheTrade, NextTurnDraw, NextTurnEnergy (start of turn)
- ThousandCuts, Burst, Accuracy (on card play)
- Reflex, Tactician on manual discard
- WellLaidPlans, NoDraw, ZeroCostCards (end of turn)
- CorpseExplosion (on enemy death)
- PhantasmalKiller, Blur (damage/block modifiers)

Tests: 96 new tests in test_silent_cards.py covering all card stats
and effect registrations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@JackSwitzer
Copy link
Owner Author

PR #4 Code Review: Silent Cards - Java Parity Audit

Critical Issues

1. Wraith Form - Incorrect Dexterity Loss

  • Python: Directly subtracts Dexterity
  • Java: Applies via ApplyPowerAction which respects Artifact
  • Impact: Artifact should block the Dexterity loss, Python bypasses this
  • Fix: Apply negative dexterity through power application system

2. Burst Power - Missing End of Turn Removal

  • Python: Only decrements when skill played, no end-of-turn cleanup
  • Java: Has atEndOfTurn() that calls RemoveSpecificPowerAction
  • Impact: If no skills played, Burst persists incorrectly
  • Fix: Add @power_trigger("atEndOfTurn", power="Burst") to remove

Medium Issues

  • Accuracy: Uses atDamageGive instead of modifying card baseDamage
  • Thousand Cuts: Uses onUseCard instead of onAfterCardPlayed (timing difference)
  • Bouncing Flask: Uses random.choice() instead of cardRandomRng (RNG parity)
  • Reflex/Tactician: Power triggers instead of card triggers (architecturally wrong)

Verified Correct

  • Infinite Blades trigger timing
  • Noxious Fumes trigger timing
  • Shiv base damage (cosmetic only with Accuracy)

Review by Claude Opus 4.5

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 5 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

This PR is being reviewed by Cursor Bugbot

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

"""Refund 2 energy if a card was discarded this turn (Sneaky Strike)."""
discarded_this_turn = ctx.extra_data.get("discarded_this_turn", 0)
if discarded_this_turn > 0:
ctx.gain_energy(2)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sneaky Strike energy refund checks wrong data location

High Severity

The refund_2_energy_if_discarded_this_turn effect reads from ctx.extra_data.get("discarded_this_turn", 0), but the SneakyStrike power trigger writes the discard count to ctx.state.discarded_this_turn. Since these are different storage locations, the effect will always see 0 discards and never trigger the energy refund for Sneaky Strike/Underhanded Strike.

Additional Locations (1)

Fix in Cursor Fix in Web

if card_id in ALL_CARDS:
card = ALL_CARDS[card_id]
energy_amount = card.magic_number if card.magic_number > 0 else 1
ctx.gain_energy(energy_amount)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reflex/Tactician discard triggers never fire

High Severity

The power triggers reflex_on_discard and tactician_on_discard are registered with power="Reflex" and power="Tactician", but these are card IDs not player statuses. The power trigger system only invokes handlers for powers that exist in owner.statuses.keys(). Since no code adds "Reflex" or "Tactician" as player statuses, these triggers will never execute. The corresponding card effects (when_discarded_draw, when_discarded_gain_energy) are also empty no-ops, meaning Reflex and Tactician's discard abilities are completely non-functional.

Additional Locations (1)

Fix in Cursor Fix in Web

if current > 1:
ctx.player.statuses["Burst"] = current - 1
else:
del ctx.player.statuses["Burst"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Burst power trigger consumes itself on play

High Severity

The burst_on_use power trigger fires after the Burst card's effects are applied, meaning the "Burst" status is already in the player's statuses when onUseCard triggers. Since Burst itself is a Skill card, the trigger checks pass and it immediately consumes the Burst status on the card that just created it. The trigger sets play_again = True for Burst (which is meaningless) and decrements the Burst counter before any subsequent skill can benefit from it. A check is needed to skip the card that created the Burst status.

Fix in Cursor Fix in Web


import pytest
import sys
sys.path.insert(0, '/Users/jackswitzer/Desktop/SlayTheSpireRL')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded absolute path in test file

Medium Severity

sys.path.insert(0, '/Users/jackswitzer/Desktop/SlayTheSpireRL') is a hardcoded absolute path specific to one developer's machine. This will break on CI systems or other developers' environments.

Fix in Cursor Fix in Web

def get_silent_card_effects(card_id: str) -> List[str]:
"""Get the effect names for a Silent card."""
base_id = card_id.rstrip("+")
return SILENT_CARD_EFFECTS.get(base_id, [])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exported function get_silent_card_effects is unused

Low Severity

The function get_silent_card_effects is defined but never imported or used anywhere in the codebase. The SILENT_CARD_EFFECTS dict and this accessor function appear to be dead code.

Fix in Cursor Fix in Web

JackSwitzer added a commit that referenced this pull request Feb 4, 2026
1. Wraith Form - Dexterity Loss Implementation (CRITICAL)
   - Changed from direct subtraction to using apply_power_to_player
   - Now properly respects Artifact (can block negative dex)
   - Updated apply_power to treat negative Strength/Dexterity as debuffs

2. Burst Power - Missing End of Turn Removal (CRITICAL)
   - Added atEndOfTurn handler for Burst power
   - Burst now properly removes at end of turn even if unused
   - Matches Java BurstPower.atEndOfTurn() behavior

3. Thousand Cuts - Trigger Timing (MEDIUM)
   - Added documentation noting timing difference
   - Java uses onAfterCardPlayed, we use onUseCard
   - Removed duplicate handler definition

4. Bouncing Flask - RNG Stream (MEDIUM-HIGH)
   - Changed from random.choice() to deterministic RNG
   - Uses card_rng_state from combat state for reproducibility
   - Ensures same seed produces same targeting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant