Improve AI move & switch heuristics; add tests #11

Merged
smell-of-curry merged 4 commits from fix/ai-decision-issues into master 2026-06-03 19:09:15 +00:00
smell-of-curry commented 2026-05-15 15:40:33 +00:00 (Migrated from github.com)

Enhance strategic-AI scoring and add regression tests. - HeuristicEngine: add proactive-switch thresholds (PROACTIVE_SWITCH_DELTA/FLOOR) and trigger to avoid flip-flopping on small edges. - MoveEvaluator: special-case Counter/Mirror Coat/Metal Burst, refine Encore scoring, add Destiny Bond and Baton Pass scoring, and minor cleanup (recovery move formatting). - SwitchEvaluator: add entry-synergy bonuses (terrain seeds, Intimidate, weather setters) and improve bestAttackingDamage by seeding STAB proxy moves and conservative coverage probes. - Tests: add comprehensive move-evaluator tests and extend switch-evaluator tests to cover STAB-awareness and terrain/Intimidate synergies. These changes fix AI issues like DB spamming, Baton Pass being undervalued, missed Counter picks, terrain-seed blindspots, and unrevealed-STAB underestimation.

Summary by CodeRabbit

  • New Features

    • Smarter switching: proactive bench-synergy scoring, stronger survivability gating, emergency-switch override, and vetoes when the active benefits from consumables or setup windows.
    • Richer move scoring: improved priority/KO heuristics, dedicated handling for Counter/Mirror Coat/Metal Burst/Sucker Punch, Destiny Bond, Baton Pass, Encore, and hazard/yawn/endure heuristics.
    • Better damage/stat estimates: Paradox/Protosynthesis/Quark Drive and Solar Power now affect damage and speed estimations.
    • Improved AI context: engines now track recent failed moves to inform decisions.
  • Refactor

    • Targeting and evaluator helpers reorganized for clearer decision logic.
  • Tests

    • Extensive regression suites added for move, switch, and damage evaluation.

Note

Medium Risk
Changes only affect simulator AI heuristics and tests, but the logic is large and battle-decision-sensitive; wrong scoring could change competitive AI behavior noticeably.

Overview
This PR tightens strategic-AI move choice and switching so playtests stop cycling bad pivots, undervaluing key moves, and mis-estimating damage.

Switching (HeuristicEngine + SwitchEvaluator) adds proactive bench swaps when a teammate clears score floors and a large delta over the active mon, while new vetoes block flip-flops: wasted consumable/seed/Paradox boosts, Focus Sash/Sturdy/WP/Unburden setup windows, healthy outspeeders that already pressure the foe, usable priority KOs, sac-into-OHKO bench picks, and “stay and sac” when both active and switch-in would die to the same hit. Emergency speed checks now use exported scaledSpeed (weather abilities, Tailwind, Trick Room, Quark Drive on terrain). Matchup scoring gains entry synergies (seeds, Intimidate, weather speed, Booster Energy, absorb abilities, WP bait) and a survivability gate; foe threat modeling seeds STAB proxies (both categories) without inventing extra coverage on our full moveset.

Move scoring (MoveEvaluator + DamageCalc) special-cases Counter/Mirror Coat/Metal Burst, Sucker Punch, Destiny Bond, Baton Pass, Encore, Yawn, Endure, richer priority/hazard/setup logic, and threads attackerLastMoveFailed for Stomping Tantrum. Damage calc adds conditional BP (Bolt Beak/Payback/Revenge/etc.), Protosynthesis/Quark Drive and Solar Power, auto-crit for willCrit moves, and turn-order hints.

State: BattleStateTracker records lastMoveFailed from miss/immune/fail/cant; PlayerAI carries lastMoveFailedByMon on EngineContext. Large regression tests cover these behaviors.

Reviewed by Cursor Bugbot for commit 433f496ab0. Bugbot is set up for automated code reviews on this repo. Configure here.

Enhance strategic-AI scoring and add regression tests. - HeuristicEngine: add proactive-switch thresholds (PROACTIVE_SWITCH_DELTA/FLOOR) and trigger to avoid flip-flopping on small edges. - MoveEvaluator: special-case Counter/Mirror Coat/Metal Burst, refine Encore scoring, add Destiny Bond and Baton Pass scoring, and minor cleanup (recovery move formatting). - SwitchEvaluator: add entry-synergy bonuses (terrain seeds, Intimidate, weather setters) and improve bestAttackingDamage by seeding STAB proxy moves and conservative coverage probes. - Tests: add comprehensive move-evaluator tests and extend switch-evaluator tests to cover STAB-awareness and terrain/Intimidate synergies. These changes fix AI issues like DB spamming, Baton Pass being undervalued, missed Counter picks, terrain-seed blindspots, and unrevealed-STAB underestimation. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Smarter switching: proactive bench-synergy scoring, stronger survivability gating, emergency-switch override, and vetoes when the active benefits from consumables or setup windows. * Richer move scoring: improved priority/KO heuristics, dedicated handling for Counter/Mirror Coat/Metal Burst/Sucker Punch, Destiny Bond, Baton Pass, Encore, and hazard/yawn/endure heuristics. * Better damage/stat estimates: Paradox/Protosynthesis/Quark Drive and Solar Power now affect damage and speed estimations. * Improved AI context: engines now track recent failed moves to inform decisions. * **Refactor** * Targeting and evaluator helpers reorganized for clearer decision logic. * **Tests** * Extensive regression suites added for move, switch, and damage evaluation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes only affect simulator AI heuristics and tests, but the logic is large and battle-decision-sensitive; wrong scoring could change competitive AI behavior noticeably. > > **Overview** > This PR tightens **strategic-AI** move choice and switching so playtests stop cycling bad pivots, undervaluing key moves, and mis-estimating damage. > > **Switching (`HeuristicEngine` + `SwitchEvaluator`)** adds proactive bench swaps when a teammate clears score floors and a large delta over the active mon, while new **vetoes** block flip-flops: wasted consumable/seed/Paradox boosts, Focus Sash/Sturdy/WP/Unburden setup windows, healthy outspeeders that already pressure the foe, usable priority KOs, sac-into-OHKO bench picks, and “stay and sac” when both active and switch-in would die to the same hit. Emergency speed checks now use exported **`scaledSpeed`** (weather abilities, Tailwind, Trick Room, Quark Drive on terrain). Matchup scoring gains **entry synergies** (seeds, Intimidate, weather speed, Booster Energy, absorb abilities, WP bait) and a **survivability gate**; foe threat modeling seeds **STAB proxies** (both categories) without inventing extra coverage on our full moveset. > > **Move scoring (`MoveEvaluator` + `DamageCalc`)** special-cases Counter/Mirror Coat/Metal Burst, Sucker Punch, Destiny Bond, Baton Pass, Encore, Yawn, Endure, richer priority/hazard/setup logic, and threads **`attackerLastMoveFailed`** for Stomping Tantrum. Damage calc adds conditional BP (Bolt Beak/Payback/Revenge/etc.), **Protosynthesis/Quark Drive** and **Solar Power**, auto-crit for `willCrit` moves, and turn-order hints. > > **State**: `BattleStateTracker` records **`lastMoveFailed`** from miss/immune/fail/cant; `PlayerAI` carries **`lastMoveFailedByMon`** on `EngineContext`. Large **regression tests** cover these behaviors. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 433f496ab06adba6534ff4fa9beca34741746223. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
coderabbitai[bot] commented 2026-05-15 15:40:46 +00:00 (Migrated from github.com)

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Enhances Strategic-AI: records per-mon "last move failed", extends DamageCalc for Paradox/Solar boosts and conditional BP, enriches move/status scoring and setup-window logic, adds switch-in entry-synergy and survivability gating, rewrites proactive switching, and adds extensive regression tests.

Changes

Strategic AI Move & Switch Evaluation

Layer / File(s) Summary
State plumbing: last-move-failed tracking
sim/tools/strategic-ai/state/BattleStateTracker.ts, sim/tools/strategic-ai/engines/Engine.ts, sim/tools/player-ai.ts, test/sim/tools/strategic-ai/force-switch.js
Adds per-mon lastMoveFailed, _lastMover attribution, exposes lastMoveFailedByMon in EngineContext and test contexts so evaluators can use "previous move failed" signals.
DamageCalc: Paradox & Solar Power
sim/tools/strategic-ai/mechanics/DamageCalc.ts, test/sim/tools/strategic-ai/damage-calc.js
Adds DamageCalcInput hints for turn-order/previous-turn conditions, auto-applies forceCrit for guaranteed-crit moves, and applies Protosynthesis/Quark Drive and Solar Power offensive/defensive multipliers with helpers; tests cover Paradox abilities and Solar Power.
SwitchEvaluator: entry-synergy, move-pool, and scaledSpeed
sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts
Adds entrySynergyBonus (seeds, Intimidate, weather-setters, weather-speed tempo, Booster Energy for Paradox), absorb/Weakness Policy recognition, builds revealed-first move pools with STAB proxies and optional coverage proxies in bestAttackingDamage, and exports scaledSpeed with paradoxSpeedActive and terrain support.
SwitchEvaluator: regression tests
test/sim/tools/strategic-ai/switch-evaluator.js
Adds tests for STAB-from-species inference, Grassy Seed/terrain synergy, Intimidate preference, survivability/OHKO gating, weather/Booster Energy entry-synergy, absorb abilities, scaledSpeed multipliers, and Weakness Policy baiting.
MoveEvaluator: counter/priority/status & setup gating
sim/tools/strategic-ai/mechanics/MoveEvaluator.ts
Adds attackerLastMoveFailed to context; special-cases Counter-family and Sucker Punch; replaces simple priority-scoring with richer outspeed/KO/insurance/chip heuristics; restructures status handling (encore/destinybond/batonpass/yawn/endure); refactors boost scoring with setup-window helpers and updates hazard valuation.
MoveEvaluator: regression tests
test/sim/tools/strategic-ai/move-evaluator.js
Extensive evaluateMove tests: Destiny Bond, Baton Pass, Counter/Mirror Coat, Encore, priority/insurance moves, Sucker Punch, setup-window bonuses (Sash/Sturdy/Weakness Policy), hazard priorities, Yawn/stall combos, conditional-BP moves, always-crit interactions, and Endure/berry combos.
Heuristic engine: maybeSwitchOut rewrite & helpers
sim/tools/strategic-ai/engines/HeuristicEngine.ts
Passes active slot request into maybeSwitchOut; precomputes best bench candidate; uses scaledSpeed; adds proactive-switch thresholds and bench-survives-entry checks; early vetoes for consumable/field boosts and setup preservation; emergencySwitch override and priority-KO short-circuit; refined sacrifice/entry-safety gates; refactors doubles target formatting and adds setup-detection helpers.

🎯 4 (Complex) | ⏱️ ~45 minutes

"I hop through code with quiet cheer,
tracking fails and boosts that steer;
when suns or paradoxes make things bright,
I test each path by candlelight —
the rabbit nods: the logic's clear." 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly describes the main focus: improving AI move and switch heuristics, plus adding tests. It accurately captures the core changes across HeuristicEngine, MoveEvaluator, SwitchEvaluator, and test files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ai-decision-issues

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

<!-- This is an auto-generated comment: summarize by coderabbit.ai --> <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/smell-of-curry/pokebedrock-showdown/pull/11?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- This is an auto-generated comment: failure by coderabbit.ai --> > [!CAUTION] > ## Review failed > > Pull request was closed or merged during review <!-- end of auto-generated comment: failure by coderabbit.ai --> <!-- walkthrough_start --> <details> <summary>📝 Walkthrough</summary> ## Walkthrough Enhances Strategic-AI: records per-mon "last move failed", extends DamageCalc for Paradox/Solar boosts and conditional BP, enriches move/status scoring and setup-window logic, adds switch-in entry-synergy and survivability gating, rewrites proactive switching, and adds extensive regression tests. ## Changes **Strategic AI Move & Switch Evaluation** |Layer / File(s)|Summary| |---|---| |**State plumbing: last-move-failed tracking** <br> `sim/tools/strategic-ai/state/BattleStateTracker.ts`, `sim/tools/strategic-ai/engines/Engine.ts`, `sim/tools/player-ai.ts`, `test/sim/tools/strategic-ai/force-switch.js`|Adds per-mon `lastMoveFailed`, `_lastMover` attribution, exposes `lastMoveFailedByMon` in EngineContext and test contexts so evaluators can use "previous move failed" signals.| |**DamageCalc: Paradox & Solar Power** <br> `sim/tools/strategic-ai/mechanics/DamageCalc.ts`, `test/sim/tools/strategic-ai/damage-calc.js`|Adds DamageCalcInput hints for turn-order/previous-turn conditions, auto-applies `forceCrit` for guaranteed-crit moves, and applies Protosynthesis/Quark Drive and Solar Power offensive/defensive multipliers with helpers; tests cover Paradox abilities and Solar Power.| |**SwitchEvaluator: entry-synergy, move-pool, and scaledSpeed** <br> `sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts`|Adds `entrySynergyBonus` (seeds, Intimidate, weather-setters, weather-speed tempo, Booster Energy for Paradox), absorb/Weakness Policy recognition, builds revealed-first move pools with STAB proxies and optional coverage proxies in `bestAttackingDamage`, and exports `scaledSpeed` with `paradoxSpeedActive` and `terrain` support.| |**SwitchEvaluator: regression tests** <br> `test/sim/tools/strategic-ai/switch-evaluator.js`|Adds tests for STAB-from-species inference, Grassy Seed/terrain synergy, Intimidate preference, survivability/OHKO gating, weather/Booster Energy entry-synergy, absorb abilities, `scaledSpeed` multipliers, and Weakness Policy baiting.| |**MoveEvaluator: counter/priority/status & setup gating** <br> `sim/tools/strategic-ai/mechanics/MoveEvaluator.ts`|Adds `attackerLastMoveFailed` to context; special-cases Counter-family and Sucker Punch; replaces simple priority-scoring with richer outspeed/KO/insurance/chip heuristics; restructures status handling (encore/destinybond/batonpass/yawn/endure); refactors boost scoring with setup-window helpers and updates hazard valuation.| |**MoveEvaluator: regression tests** <br> `test/sim/tools/strategic-ai/move-evaluator.js`|Extensive `evaluateMove` tests: Destiny Bond, Baton Pass, Counter/Mirror Coat, Encore, priority/insurance moves, Sucker Punch, setup-window bonuses (Sash/Sturdy/Weakness Policy), hazard priorities, Yawn/stall combos, conditional-BP moves, always-crit interactions, and Endure/berry combos.| |**Heuristic engine: maybeSwitchOut rewrite & helpers** <br> `sim/tools/strategic-ai/engines/HeuristicEngine.ts`|Passes active slot request into `maybeSwitchOut`; precomputes best bench candidate; uses `scaledSpeed`; adds proactive-switch thresholds and bench-survives-entry checks; early vetoes for consumable/field boosts and setup preservation; `emergencySwitch` override and priority-KO short-circuit; refined sacrifice/entry-safety gates; refactors doubles target formatting and adds setup-detection helpers.| 🎯 4 (Complex) | ⏱️ ~45 minutes - [smell-of-curry/pokebedrock-showdown#10](https://github.com/smell-of-curry/pokebedrock-showdown/pull/10): Prior PR that modified Strategic-AI core modules; likely directly related to the current changes. > "I hop through code with quiet cheer, > tracking fails and boosts that steer; > when suns or paradoxes make things bright, > I test each path by candlelight — > the rabbit nods: the logic's clear." 🐇✨ </details> <!-- walkthrough_end --> <!-- pre_merge_checks_walkthrough_start --> <details> <summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary> ### ❌ Failed checks (1 warning) | Check name | Status | Explanation | Resolution | | :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- | | Docstring Coverage | ⚠️ Warning | Docstring coverage is 68.18% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. | <details> <summary>✅ Passed checks (4 passed)</summary> | Check name | Status | Explanation | | :------------------------: | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. | | Title check | ✅ Passed | The pull request title clearly describes the main focus: improving AI move and switch heuristics, plus adding tests. It accurately captures the core changes across HeuristicEngine, MoveEvaluator, SwitchEvaluator, and test files. | | Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. | | Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. | </details> <sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub> </details> <!-- pre_merge_checks_walkthrough_end --> <!-- finishing_touch_checkbox_start --> <details> <summary>✨ Finishing Touches</summary> <details> <summary>📝 Generate docstrings</summary> - [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> Create stacked PR - [ ] <!-- {"checkboxId": "3e1879ae-f29b-4d0d-8e06-d12b7ba33d98"} --> Commit on current branch </details> <details> <summary>🧪 Generate unit tests (beta)</summary> - [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Create PR with unit tests - [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Commit unit tests in branch `fix/ai-decision-issues` </details> </details> <!-- finishing_touch_checkbox_end --> <!-- tips_start --> --- Thanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=smell-of-curry/pokebedrock-showdown&utm_content=11)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. <details> <summary>❤️ Share</summary> - [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai) - [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai) - [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai) - [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code) </details> <sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub> <!-- tips_end --> <!-- internal state start --> <!-- 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 --> <!-- internal state end -->
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2026-05-15 15:47:00 +00:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull request overview

This pull request updates the Strategic AI’s move and switch scoring to better account for hidden STAB threats, entry-triggered synergies (terrain seeds, Intimidate, weather/terrain setters), and several previously mis-scored utility moves, and adds regression tests to lock in the intended behavior.

Changes:

  • SwitchEvaluator: add entry-synergy bonuses and expand bestAttackingDamage to seed STAB proxies (and broader coverage probes when allowed).
  • MoveEvaluator: add dedicated scoring for Counter/Mirror Coat/Metal Burst, refine Encore, and add Destiny Bond + Baton Pass scoring branches.
  • Tests: add MoveEvaluator regression coverage and extend SwitchEvaluator tests for STAB-awareness and entry-synergy cases.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts Adds entry-synergy scoring and broadens attack modeling via STAB/coverage proxies.
sim/tools/strategic-ai/mechanics/MoveEvaluator.ts Adds special-case scoring for counter-moves, Encore, Destiny Bond, and Baton Pass.
sim/tools/strategic-ai/engines/HeuristicEngine.ts Introduces proactive-switch thresholds and refactors switch-candidate selection.
test/sim/tools/strategic-ai/switch-evaluator.js Adds regression tests for unrevealed STAB risk and entry-synergy bonuses.
test/sim/tools/strategic-ai/move-evaluator.js Adds regression tests for Destiny Bond, Baton Pass, Counter/Mirror Coat, and Encore scoring.
Comments suppressed due to low confidence (2)

sim/tools/strategic-ai/mechanics/MoveEvaluator.ts:503

  • For Metal Burst, defender.choiceLocked alone isn’t enough to treat the move as "almost guaranteed": a Choice-locked foe can be locked into a Status move (or any non-damaging move), in which case Metal Burst will fail because it requires taking damage earlier in the same turn. The scoring should only apply the big Choice-lock bonus when the locked move is damaging (or otherwise strongly predicted to deal damage).
	const matches =
		requiredFoeCategory === "Any" ||
		(foeLastCategory && foeLastCategory === requiredFoeCategory);
	if (matches && slower) {
		// Foe is committed (e.g. Choice-locked) makes this almost guaranteed.
		score = defender.choiceLocked ? 55 : 35;
		rationale = `${moveId}:setup`;
	} else if (matches) {
		score = 12;
	} else if (foeLastCategory) {
		// Foe just used the opposite category — predicting they'll do it
		// again is a coin-flip; mildly negative so it's not the top pick
		// but still considered.
		score = -4;
	} else {
		// No info on the foe yet (turn 1, fresh switch-in). Modest baseline
		// so it can outscore truly useless moves but isn't a default pick.
		score = 4;
	}

test/sim/tools/strategic-ai/move-evaluator.js:187

  • The Counter/Mirror Coat tests currently hard-require weOutspeed: false, but Counter/Mirror Coat have priority -5 and usually act after the foe regardless of Speed. If the evaluator is updated to reflect priority instead of Speed tier, these tests should stop depending on weOutspeed for correctness.
	describe('Counter / Mirror Coat', () => {
		it('Counter scores high when the foe just used a Physical move and we are slower', () => {
			const tracker = freshTracker();
			const me = mkTracked('Wobbuffet', { ability: 'shadowtag' });
			const foe = mkTracked('Garchomp', {
				revealedMoves: ['earthquake'],
				lastMove: 'earthquake',
			});
			const result = evaluateMove(Dex.moves.get('counter'), ctxFor(me, foe, tracker, {
				weOutspeed: false,
			}));
			assert(result.score > 20,
				`Counter vs a foe that just used Physical should score >20 (got ${result.score})`);
		});

		it('Mirror Coat scores high when the foe just used a Special move and we are slower', () => {
			const tracker = freshTracker();
			const me = mkTracked('Wobbuffet', { ability: 'shadowtag' });
			const foe = mkTracked('Heatran', {
				revealedMoves: ['magmastorm'],
				lastMove: 'magmastorm',
			});
			const result = evaluateMove(Dex.moves.get('mirrorcoat'), ctxFor(me, foe, tracker, {
				weOutspeed: false,
			}));
			assert(result.score > 20,
				`Mirror Coat vs a foe that just used Special should score >20 (got ${result.score})`);
		});

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

## Pull request overview This pull request updates the Strategic AI’s move and switch scoring to better account for hidden STAB threats, entry-triggered synergies (terrain seeds, Intimidate, weather/terrain setters), and several previously mis-scored utility moves, and adds regression tests to lock in the intended behavior. **Changes:** - SwitchEvaluator: add entry-synergy bonuses and expand `bestAttackingDamage` to seed STAB proxies (and broader coverage probes when allowed). - MoveEvaluator: add dedicated scoring for Counter/Mirror Coat/Metal Burst, refine Encore, and add Destiny Bond + Baton Pass scoring branches. - Tests: add MoveEvaluator regression coverage and extend SwitchEvaluator tests for STAB-awareness and entry-synergy cases. ### Reviewed changes Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments. <details> <summary>Show a summary per file</summary> | File | Description | | ---- | ----------- | | `sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` | Adds entry-synergy scoring and broadens attack modeling via STAB/coverage proxies. | | `sim/tools/strategic-ai/mechanics/MoveEvaluator.ts` | Adds special-case scoring for counter-moves, Encore, Destiny Bond, and Baton Pass. | | `sim/tools/strategic-ai/engines/HeuristicEngine.ts` | Introduces proactive-switch thresholds and refactors switch-candidate selection. | | `test/sim/tools/strategic-ai/switch-evaluator.js` | Adds regression tests for unrevealed STAB risk and entry-synergy bonuses. | | `test/sim/tools/strategic-ai/move-evaluator.js` | Adds regression tests for Destiny Bond, Baton Pass, Counter/Mirror Coat, and Encore scoring. | </details> <details> <summary>Comments suppressed due to low confidence (2)</summary> **sim/tools/strategic-ai/mechanics/MoveEvaluator.ts:503** * For Metal Burst, `defender.choiceLocked` alone isn’t enough to treat the move as "almost guaranteed": a Choice-locked foe can be locked into a Status move (or any non-damaging move), in which case Metal Burst will fail because it requires taking damage earlier in the same turn. The scoring should only apply the big Choice-lock bonus when the locked move is damaging (or otherwise strongly predicted to deal damage). ``` const matches = requiredFoeCategory === "Any" || (foeLastCategory && foeLastCategory === requiredFoeCategory); if (matches && slower) { // Foe is committed (e.g. Choice-locked) makes this almost guaranteed. score = defender.choiceLocked ? 55 : 35; rationale = `${moveId}:setup`; } else if (matches) { score = 12; } else if (foeLastCategory) { // Foe just used the opposite category — predicting they'll do it // again is a coin-flip; mildly negative so it's not the top pick // but still considered. score = -4; } else { // No info on the foe yet (turn 1, fresh switch-in). Modest baseline // so it can outscore truly useless moves but isn't a default pick. score = 4; } ``` **test/sim/tools/strategic-ai/move-evaluator.js:187** * The Counter/Mirror Coat tests currently hard-require `weOutspeed: false`, but Counter/Mirror Coat have priority -5 and usually act after the foe regardless of Speed. If the evaluator is updated to reflect priority instead of Speed tier, these tests should stop depending on `weOutspeed` for correctness. ``` describe('Counter / Mirror Coat', () => { it('Counter scores high when the foe just used a Physical move and we are slower', () => { const tracker = freshTracker(); const me = mkTracked('Wobbuffet', { ability: 'shadowtag' }); const foe = mkTracked('Garchomp', { revealedMoves: ['earthquake'], lastMove: 'earthquake', }); const result = evaluateMove(Dex.moves.get('counter'), ctxFor(me, foe, tracker, { weOutspeed: false, })); assert(result.score > 20, `Counter vs a foe that just used Physical should score >20 (got ${result.score})`); }); it('Mirror Coat scores high when the foe just used a Special move and we are slower', () => { const tracker = freshTracker(); const me = mkTracked('Wobbuffet', { ability: 'shadowtag' }); const foe = mkTracked('Heatran', { revealedMoves: ['magmastorm'], lastMove: 'magmastorm', }); const result = evaluateMove(Dex.moves.get('mirrorcoat'), ctxFor(me, foe, tracker, { weOutspeed: false, })); assert(result.score > 20, `Mirror Coat vs a foe that just used Special should score >20 (got ${result.score})`); }); ``` </details> --- 💡 <a href="/smell-of-curry/pokebedrock-showdown/new/master?filename=.github/instructions/*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
@ -322,0 +423,4 @@
if (defender.status) return { moveId, score: -10, rationale: "yawnRedundant" };
// Electric/Misty Terrain only block sleep on *grounded* targets;
// an airborne / Levitate foe can still be put to sleep.
const yawnBlockedByTerrain =
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-15 15:46:59 +00:00

Counter and Mirror Coat have priority -5 (see move data), so they will usually move after the foe regardless of Speed. Gating the high score on !ctx.weOutspeed will undervalue them when we are faster, even though they still work; consider keying this on move.priority/move id (only Metal Burst needs the "must be slower" check).

This issue also appears on line 485 of the same file.

Counter and Mirror Coat have priority -5 (see move data), so they will usually move after the foe regardless of Speed. Gating the high score on `!ctx.weOutspeed` will undervalue them when we are faster, even though they still work; consider keying this on `move.priority`/move id (only Metal Burst needs the "must be slower" check). This issue also appears on line 485 of the same file.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-15 15:46:59 +00:00

Destiny Bond must be set before the user faints; if the foe is faster and can KO us this turn, Destiny Bond often won’t execute and won’t trade. The current logic rewards !ctx.weOutspeed (foe faster), which is backwards for the common "low HP desperation trade" use-case; consider favoring ctx.weOutspeed (or otherwise factoring in whether we’re expected to survive the incoming hit).

Destiny Bond must be set *before* the user faints; if the foe is faster and can KO us this turn, Destiny Bond often won’t execute and won’t trade. The current logic rewards `!ctx.weOutspeed` (foe faster), which is backwards for the common "low HP desperation trade" use-case; consider favoring `ctx.weOutspeed` (or otherwise factoring in whether we’re expected to survive the incoming hit).
@ -129,9 +129,340 @@ export function evaluateMatchup(
score -= foeBoosts * 4;
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-15 15:46:59 +00:00

Unaware does not negate Intimidate’s Attack drop on the Unaware Pokémon; it only ignores the opponent’s stat changes during damage calculation. Treating foe.ability === "unaware" as cancelling the Intimidate bonus will undervalue Intimidate vs Unaware physical attackers.

Unaware does not negate Intimidate’s Attack drop on the Unaware Pokémon; it only ignores the opponent’s stat changes during damage calculation. Treating `foe.ability === "unaware"` as cancelling the Intimidate bonus will undervalue Intimidate vs Unaware physical attackers.
@ -135,0 +274,4 @@
// triggers on entry instead, granting +30%/+50% to the highest
// stat for the duration of the stay.
if (item === "boosterenergy") {
const isProto = ability === "protosynthesis";
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-15 15:47:00 +00:00

bestAttackingDamage now always adds STAB proxies (and, when useKnownOnly is false, coverage proxies) even when the attacker already has revealed damaging moves. For our own side, revealedMoves is seeded from the request and should already represent the full moveset, so these extra proxies can overestimate our actual damage output and skew matchup scores/switching decisions.

`bestAttackingDamage` now always adds STAB proxies (and, when `useKnownOnly` is false, coverage proxies) even when the attacker already has revealed damaging moves. For our own side, `revealedMoves` is seeded from the request and should already represent the full moveset, so these extra proxies can overestimate our actual damage output and skew matchup scores/switching decisions.
@ -0,0 +110,4 @@
assert(result.score < 0,
`Destiny Bond into a faster KO should be negative — it whiffs ` +
`before resolving (got ${result.score})`);
});
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-15 15:47:00 +00:00

These Destiny Bond tests assume Destiny Bond is best when the foe is faster (weOutspeed: false), but Destiny Bond generally needs to execute before we faint to force a trade. If the scoring is corrected to prefer situations where we can actually get Destiny Bond up (often when we outspeed or can survive the hit), these test expectations should be updated accordingly.

This issue also appears on line 160 of the same file.

These Destiny Bond tests assume Destiny Bond is best when the foe is faster (`weOutspeed: false`), but Destiny Bond generally needs to execute before we faint to force a trade. If the scoring is corrected to prefer situations where we can actually get Destiny Bond up (often when we outspeed or can survive the hit), these test expectations should be updated accordingly. This issue also appears on line 160 of the same file.
coderabbitai[bot] (Migrated from github.com) reviewed 2026-05-15 15:48:08 +00:00
coderabbitai[bot] (Migrated from github.com) left a comment

Actionable comments posted: 3

🧹 Nitpick comments (2)
test/sim/tools/strategic-ai/move-evaluator.js (1)

160-204: ⚡ Quick win

Consider adding test coverage for Metal Burst.

The Counter/Mirror Coat tests cover the Physical and Special category paths, but Metal Burst (which accepts "Any" category) is not tested. Adding a Metal Burst test case would ensure complete coverage of the evaluateCounterMove logic.

Suggested test case
it('Metal Burst scores high when the foe just used any damaging move and we are slower', () => {
    const tracker = freshTracker();
    const me = mkTracked('Wobbuffet', { ability: 'shadowtag' });
    const foe = mkTracked('Garchomp', {
        revealedMoves: ['earthquake'],
        lastMove: 'earthquake',
    });
    const result = evaluateMove(Dex.moves.get('metalburst'), ctxFor(me, foe, tracker, {
        weOutspeed: false,
    }));
    assert(result.score > 20,
        `Metal Burst vs a foe that just used any damaging move should score >20 (got ${result.score})`);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/sim/tools/strategic-ai/move-evaluator.js` around lines 160 - 204, Add a
test for Metal Burst in the existing "Counter / Mirror Coat" suite: create the
same setup used by the Counter/Mirror Coat tests (freshTracker(), me via
mkTracked('Wobbuffet', { ability: 'shadowtag' }), foe via mkTracked('Garchomp',
{ revealedMoves: ['earthquake'], lastMove: 'earthquake' })), call
evaluateMove(Dex.moves.get('metalburst'), ctxFor(..., { weOutspeed: false })),
and assert the returned result.score > 20; place this new it(...) alongside the
other Counter/Mirror Coat tests so evaluateMove(Dex.moves.get('metalburst'))
exercises the evaluateCounterMove "Any" category path.
sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts (1)

201-212: ⚡ Quick win

Add missing terrain-surge abilities to entry-weather bonuses.

The helper rewards weather setters on entry but skips the four terrain-setting surges (grassysurge, electricsurge, mistysurge, psychicsurge)—the most common terrain installers in modern formats (Rillaboom, Tapu Koko/Lele/Fini, Indeedee, etc.). These pair directly with the Grassy/Electric/Misty/Psychic Seed bonuses already credited just above (lines 173–176), so omitting them causes the AI to undervalue key switch-ins that enable the seed synergy. The data structure already supports terrain values (hadronengine maps to electricterrain), so this is just adding four entries:

♻️ Proposed addition
 	const weatherFromAbility: Record<string, string> = {
 		drought: "sunnyday",
 		drizzle: "raindance",
 		sandstream: "sandstorm",
 		snowwarning: "snowscape",
 		orichalcumpulse: "sunnyday",
 		hadronengine: "electricterrain",
+		grassysurge: "grassyterrain",
+		electricsurge: "electricterrain",
+		mistysurge: "mistyterrain",
+		psychicsurge: "psychicterrain",
 	};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` around lines 201 - 212,
The weather/terrain mapping in weatherFromAbility is missing the four
terrain-surge abilities, so update the Record<string,string> weatherFromAbility
in SwitchEvaluator.ts to include grassysurge => "grassyterrain", electricsurge
=> "electricterrain", mistysurge => "mistyterrain", and psychicsurge =>
"psychicterrain" so that the broughtWeather lookup (broughtWeather) will detect
these terrain installers and correctly apply the entry bonus when comparing
against tracker.field.weather and tracker.field.terrain and adding to bonus.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sim/tools/strategic-ai/mechanics/MoveEvaluator.ts`:
- Around line 467-507: evaluateCounterMove undervalues counter/mirrorcoat
because it uses ctx.weOutspeed to compute `slower`, but those moves have -5
priority so they always act last; change the `slower` computation to force true
for moveId "counter" and "mirrorcoat" (e.g. `const slower = moveId === "counter"
|| moveId === "mirrorcoat" ? true : !ctx.weOutspeed`), leaving all other logic
(requiredFoeCategory, score/rationale calculation, health gate) unchanged so
Metal Burst and other moves still use the existing speed-based behavior.

In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts`:
- Around line 286-326: The proxy moves are always created with category
"Physical", which mis-scores special attackers; update both the STAB proxy
creation (where id is currently `proxystab${t.toLowerCase()}`) and the coverage
probe creation (`proxycov${t.toLowerCase()}`) to emit two proxy moves per type:
one with category "Physical" and one with category "Special" (use distinct ids,
e.g. append `_phys` and `_spec` or similar), reusing `tackleProto` spread and
the same type/basePower/accuracy fields; leave `useKnownOnly` logic intact so
the later max(avgDamage) selection (the `calculateDamage` path) will pick the
appropriate category automatically.
- Around line 190-195: The current conditional in SwitchEvaluator that skips
adding Intimidate's offensive bonus when toID(foe.ability) === "unaware" is
incorrect; change the logic so Intimidate's matchup bonus (the 7 * physicalLean
addition) is applied normally unless the foe has an ability that actually blocks
or inverts the drop (e.g. clearbody, hypercutter, innerfocus, owntempo, defiant,
competitive) or Mirror Armor which reflects effects; update the check around the
bonus calculation to remove the special-case for "unaware" and instead exclude
only those explicit abilities (use toID(foe.ability) and compare against a small
set or switch) so the bonus is applied for unaware but not for
blocking/inverting abilities or mirrorarmor.

---

Nitpick comments:
In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts`:
- Around line 201-212: The weather/terrain mapping in weatherFromAbility is
missing the four terrain-surge abilities, so update the Record<string,string>
weatherFromAbility in SwitchEvaluator.ts to include grassysurge =>
"grassyterrain", electricsurge => "electricterrain", mistysurge =>
"mistyterrain", and psychicsurge => "psychicterrain" so that the broughtWeather
lookup (broughtWeather) will detect these terrain installers and correctly apply
the entry bonus when comparing against tracker.field.weather and
tracker.field.terrain and adding to bonus.

In `@test/sim/tools/strategic-ai/move-evaluator.js`:
- Around line 160-204: Add a test for Metal Burst in the existing "Counter /
Mirror Coat" suite: create the same setup used by the Counter/Mirror Coat tests
(freshTracker(), me via mkTracked('Wobbuffet', { ability: 'shadowtag' }), foe
via mkTracked('Garchomp', { revealedMoves: ['earthquake'], lastMove:
'earthquake' })), call evaluateMove(Dex.moves.get('metalburst'), ctxFor(..., {
weOutspeed: false })), and assert the returned result.score > 20; place this new
it(...) alongside the other Counter/Mirror Coat tests so
evaluateMove(Dex.moves.get('metalburst')) exercises the evaluateCounterMove
"Any" category path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85155ef0-141a-4af7-bdcb-944e9f415d6a

📥 Commits

Reviewing files that changed from the base of the PR and between aece3520cf and 6f079bb206.

📒 Files selected for processing (5)
  • sim/tools/strategic-ai/engines/HeuristicEngine.ts
  • sim/tools/strategic-ai/mechanics/MoveEvaluator.ts
  • sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts
  • test/sim/tools/strategic-ai/move-evaluator.js
  • test/sim/tools/strategic-ai/switch-evaluator.js
**Actionable comments posted: 3** <details> <summary>🧹 Nitpick comments (2)</summary><blockquote> <details> <summary>test/sim/tools/strategic-ai/move-evaluator.js (1)</summary><blockquote> `160-204`: _⚡ Quick win_ **Consider adding test coverage for Metal Burst.** The Counter/Mirror Coat tests cover the Physical and Special category paths, but Metal Burst (which accepts "Any" category) is not tested. Adding a Metal Burst test case would ensure complete coverage of the `evaluateCounterMove` logic. <details> <summary>Suggested test case</summary> ```javascript it('Metal Burst scores high when the foe just used any damaging move and we are slower', () => { const tracker = freshTracker(); const me = mkTracked('Wobbuffet', { ability: 'shadowtag' }); const foe = mkTracked('Garchomp', { revealedMoves: ['earthquake'], lastMove: 'earthquake', }); const result = evaluateMove(Dex.moves.get('metalburst'), ctxFor(me, foe, tracker, { weOutspeed: false, })); assert(result.score > 20, `Metal Burst vs a foe that just used any damaging move should score >20 (got ${result.score})`); }); ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/sim/tools/strategic-ai/move-evaluator.js` around lines 160 - 204, Add a test for Metal Burst in the existing "Counter / Mirror Coat" suite: create the same setup used by the Counter/Mirror Coat tests (freshTracker(), me via mkTracked('Wobbuffet', { ability: 'shadowtag' }), foe via mkTracked('Garchomp', { revealedMoves: ['earthquake'], lastMove: 'earthquake' })), call evaluateMove(Dex.moves.get('metalburst'), ctxFor(..., { weOutspeed: false })), and assert the returned result.score > 20; place this new it(...) alongside the other Counter/Mirror Coat tests so evaluateMove(Dex.moves.get('metalburst')) exercises the evaluateCounterMove "Any" category path. ``` </details> </blockquote></details> <details> <summary>sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts (1)</summary><blockquote> `201-212`: _⚡ Quick win_ **Add missing terrain-surge abilities to entry-weather bonuses.** The helper rewards weather setters on entry but skips the four terrain-setting surges (`grassysurge`, `electricsurge`, `mistysurge`, `psychicsurge`)—the most common terrain installers in modern formats (Rillaboom, Tapu Koko/Lele/Fini, Indeedee, etc.). These pair directly with the Grassy/Electric/Misty/Psychic Seed bonuses already credited just above (lines 173–176), so omitting them causes the AI to undervalue key switch-ins that enable the seed synergy. The data structure already supports terrain values (`hadronengine` maps to `electricterrain`), so this is just adding four entries: <details> <summary>♻️ Proposed addition</summary> ```diff const weatherFromAbility: Record<string, string> = { drought: "sunnyday", drizzle: "raindance", sandstream: "sandstorm", snowwarning: "snowscape", orichalcumpulse: "sunnyday", hadronengine: "electricterrain", + grassysurge: "grassyterrain", + electricsurge: "electricterrain", + mistysurge: "mistyterrain", + psychicsurge: "psychicterrain", }; ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` around lines 201 - 212, The weather/terrain mapping in weatherFromAbility is missing the four terrain-surge abilities, so update the Record<string,string> weatherFromAbility in SwitchEvaluator.ts to include grassysurge => "grassyterrain", electricsurge => "electricterrain", mistysurge => "mistyterrain", and psychicsurge => "psychicterrain" so that the broughtWeather lookup (broughtWeather) will detect these terrain installers and correctly apply the entry bonus when comparing against tracker.field.weather and tracker.field.terrain and adding to bonus. ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. Inline comments: In `@sim/tools/strategic-ai/mechanics/MoveEvaluator.ts`: - Around line 467-507: evaluateCounterMove undervalues counter/mirrorcoat because it uses ctx.weOutspeed to compute `slower`, but those moves have -5 priority so they always act last; change the `slower` computation to force true for moveId "counter" and "mirrorcoat" (e.g. `const slower = moveId === "counter" || moveId === "mirrorcoat" ? true : !ctx.weOutspeed`), leaving all other logic (requiredFoeCategory, score/rationale calculation, health gate) unchanged so Metal Burst and other moves still use the existing speed-based behavior. In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts`: - Around line 286-326: The proxy moves are always created with category "Physical", which mis-scores special attackers; update both the STAB proxy creation (where id is currently `proxystab${t.toLowerCase()}`) and the coverage probe creation (`proxycov${t.toLowerCase()}`) to emit two proxy moves per type: one with category "Physical" and one with category "Special" (use distinct ids, e.g. append `_phys` and `_spec` or similar), reusing `tackleProto` spread and the same type/basePower/accuracy fields; leave `useKnownOnly` logic intact so the later max(avgDamage) selection (the `calculateDamage` path) will pick the appropriate category automatically. - Around line 190-195: The current conditional in SwitchEvaluator that skips adding Intimidate's offensive bonus when toID(foe.ability) === "unaware" is incorrect; change the logic so Intimidate's matchup bonus (the 7 * physicalLean addition) is applied normally unless the foe has an ability that actually blocks or inverts the drop (e.g. clearbody, hypercutter, innerfocus, owntempo, defiant, competitive) or Mirror Armor which reflects effects; update the check around the bonus calculation to remove the special-case for "unaware" and instead exclude only those explicit abilities (use toID(foe.ability) and compare against a small set or switch) so the bonus is applied for unaware but not for blocking/inverting abilities or mirrorarmor. --- Nitpick comments: In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts`: - Around line 201-212: The weather/terrain mapping in weatherFromAbility is missing the four terrain-surge abilities, so update the Record<string,string> weatherFromAbility in SwitchEvaluator.ts to include grassysurge => "grassyterrain", electricsurge => "electricterrain", mistysurge => "mistyterrain", and psychicsurge => "psychicterrain" so that the broughtWeather lookup (broughtWeather) will detect these terrain installers and correctly apply the entry bonus when comparing against tracker.field.weather and tracker.field.terrain and adding to bonus. In `@test/sim/tools/strategic-ai/move-evaluator.js`: - Around line 160-204: Add a test for Metal Burst in the existing "Counter / Mirror Coat" suite: create the same setup used by the Counter/Mirror Coat tests (freshTracker(), me via mkTracked('Wobbuffet', { ability: 'shadowtag' }), foe via mkTracked('Garchomp', { revealedMoves: ['earthquake'], lastMove: 'earthquake' })), call evaluateMove(Dex.moves.get('metalburst'), ctxFor(..., { weOutspeed: false })), and assert the returned result.score > 20; place this new it(...) alongside the other Counter/Mirror Coat tests so evaluateMove(Dex.moves.get('metalburst')) exercises the evaluateCounterMove "Any" category path. ``` </details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `85155ef0-141a-4af7-bdcb-944e9f415d6a` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between aece3520cfe4e45521d7e56065aaa9498aa365ce and 6f079bb206776aeac7056c81aeca51feb5e6cd1d. </details> <details> <summary>📒 Files selected for processing (5)</summary> * `sim/tools/strategic-ai/engines/HeuristicEngine.ts` * `sim/tools/strategic-ai/mechanics/MoveEvaluator.ts` * `sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` * `test/sim/tools/strategic-ai/move-evaluator.js` * `test/sim/tools/strategic-ai/switch-evaluator.js` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
coderabbitai[bot] (Migrated from github.com) commented 2026-05-15 15:48:07 +00:00

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check move priority for Counter, Mirror Coat, and Metal Burst
echo "=== Move priorities ==="
for move in counter mirrorcoat metalburst; do
  echo "--- $move ---"
  rg -nP "^\s*$move:\s*\{" data/moves.ts -A 20 | rg "priority:"
done

Repository: smell-of-curry/pokebedrock-showdown

Length of output: 221


🏁 Script executed:

# Check the MoveEvalContext definition and ctx.weOutspeed usage
rg -n "weOutspeed" sim/tools/strategic-ai/mechanics/MoveEvaluator.ts -B 5 -A 2 | head -50

Repository: smell-of-curry/pokebedrock-showdown

Length of output: 1956


🏁 Script executed:

# Check if there's priority handling elsewhere in the evaluation logic
rg -n "priority" sim/tools/strategic-ai/mechanics/MoveEvaluator.ts

Repository: smell-of-curry/pokebedrock-showdown

Length of output: 333


🏁 Script executed:

# Check the type definition of MoveEvalContext
rg -n "MoveEvalContext" sim/tools/strategic-ai/mechanics/MoveEvaluator.ts -A 10 | head -30

Repository: smell-of-curry/pokebedrock-showdown

Length of output: 1312


🏁 Script executed:

# Find all moves with -5 priority to understand scope of the issue
rg -n "priority: -5" data/moves.ts

Repository: smell-of-curry/pokebedrock-showdown

Length of output: 122


Counter and Mirror Coat are undervalued when the player outspeeds due to priority not being considered.

Counter and Mirror Coat both have -5 priority (confirmed in move data), which means they always move last regardless of Speed stats. Line 482 sets slower = !ctx.weOutspeed, but ctx.weOutspeed only reflects base Speed comparison, not move priority.

When the player outspeeds (e.g., Speed 100 vs foe Speed 50), the code sets slower = false and scores these moves at 12 (line 493). However, due to -5 priority, Counter/Mirror Coat will still move last, so they should be scored at 35+ (line 490), especially if the foe is choice-locked.

The comment at lines 479–481 is misleading—it states outspeeding makes them whiff, but that only applies if the move could move first. With -5 priority, this never happens.

Fix: For Counter and Mirror Coat specifically, override slower to always be true since priority -5 guarantees they move last. Metal Burst (priority 0) is unaffected and should use the current speed-based logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/mechanics/MoveEvaluator.ts` around lines 467 - 507,
evaluateCounterMove undervalues counter/mirrorcoat because it uses
ctx.weOutspeed to compute `slower`, but those moves have -5 priority so they
always act last; change the `slower` computation to force true for moveId
"counter" and "mirrorcoat" (e.g. `const slower = moveId === "counter" || moveId
=== "mirrorcoat" ? true : !ctx.weOutspeed`), leaving all other logic
(requiredFoeCategory, score/rationale calculation, health gate) unchanged so
Metal Burst and other moves still use the existing speed-based behavior.

✅ Confirmed as addressed by @smell-of-curry

_⚠️ Potential issue_ | _🟠 Major_ | _🏗️ Heavy lift_ <details> <summary>🧩 Analysis chain</summary> 🏁 Script executed: ```shell #!/bin/bash # Check move priority for Counter, Mirror Coat, and Metal Burst echo "=== Move priorities ===" for move in counter mirrorcoat metalburst; do echo "--- $move ---" rg -nP "^\s*$move:\s*\{" data/moves.ts -A 20 | rg "priority:" done ``` Repository: smell-of-curry/pokebedrock-showdown Length of output: 221 --- 🏁 Script executed: ```shell # Check the MoveEvalContext definition and ctx.weOutspeed usage rg -n "weOutspeed" sim/tools/strategic-ai/mechanics/MoveEvaluator.ts -B 5 -A 2 | head -50 ``` Repository: smell-of-curry/pokebedrock-showdown Length of output: 1956 --- 🏁 Script executed: ```shell # Check if there's priority handling elsewhere in the evaluation logic rg -n "priority" sim/tools/strategic-ai/mechanics/MoveEvaluator.ts ``` Repository: smell-of-curry/pokebedrock-showdown Length of output: 333 --- 🏁 Script executed: ```shell # Check the type definition of MoveEvalContext rg -n "MoveEvalContext" sim/tools/strategic-ai/mechanics/MoveEvaluator.ts -A 10 | head -30 ``` Repository: smell-of-curry/pokebedrock-showdown Length of output: 1312 --- 🏁 Script executed: ```shell # Find all moves with -5 priority to understand scope of the issue rg -n "priority: -5" data/moves.ts ``` Repository: smell-of-curry/pokebedrock-showdown Length of output: 122 --- </details> **Counter and Mirror Coat are undervalued when the player outspeeds due to priority not being considered.** Counter and Mirror Coat both have -5 priority (confirmed in move data), which means they always move last regardless of Speed stats. Line 482 sets `slower = !ctx.weOutspeed`, but `ctx.weOutspeed` only reflects base Speed comparison, not move priority. When the player outspeeds (e.g., Speed 100 vs foe Speed 50), the code sets `slower = false` and scores these moves at 12 (line 493). However, due to -5 priority, Counter/Mirror Coat will still move last, so they should be scored at 35+ (line 490), especially if the foe is choice-locked. The comment at lines 479–481 is misleading—it states outspeeding makes them whiff, but that only applies if the move could move first. With -5 priority, this never happens. **Fix:** For Counter and Mirror Coat specifically, override `slower` to always be `true` since priority -5 guarantees they move last. Metal Burst (priority 0) is unaffected and should use the current speed-based logic. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sim/tools/strategic-ai/mechanics/MoveEvaluator.ts` around lines 467 - 507, evaluateCounterMove undervalues counter/mirrorcoat because it uses ctx.weOutspeed to compute `slower`, but those moves have -5 priority so they always act last; change the `slower` computation to force true for moveId "counter" and "mirrorcoat" (e.g. `const slower = moveId === "counter" || moveId === "mirrorcoat" ? true : !ctx.weOutspeed`), leaving all other logic (requiredFoeCategory, score/rationale calculation, health gate) unchanged so Metal Burst and other moves still use the existing speed-based behavior. ``` </details> <!-- fingerprinting:phantom:poseidon:puma --> <!-- This is an auto-generated reply by CodeRabbit --> ✅ Confirmed as addressed by @smell-of-curry <!-- This is an auto-generated comment by CodeRabbit -->
@ -129,9 +129,340 @@ export function evaluateMatchup(
score -= foeBoosts * 4;
coderabbitai[bot] (Migrated from github.com) commented 2026-05-15 15:48:07 +00:00

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unaware on the foe does not negate Intimidate's offensive impact.

Unaware ignores the opponent's stat-stage changes when the Unaware holder is dealing or taking damage. It does not erase the holder's own stages. When the foe has Unaware and our Intimidate sets foe's Atk to −1, the foe still attacks with its own −1 Atk stage; that's what reduces incoming damage on us, and Unaware doesn't bypass it. So Intimidate's matchup bonus should still apply here.

The only real wrinkles are Clear Body / Hyper Cutter / Inner Focus / Own Tempo / Defiant / Competitive on the foe (block or invert the drop), and Mirror Armor (reflect onto us) — none of which is unaware.

🐛 Proposed fix
-		// Unaware on the foe ignores our Intimidate drop's offensive
-		// impact, though the stage is still applied; treat that as
-		// neutral.
-		if (toID(foe.ability) === "unaware") bonus += 0;
-		else bonus += 7 * physicalLean;
+		// Abilities that block, invert, or reflect Intimidate's drop.
+		const foeAbil = toID(foe.ability);
+		const blocksIntimidate =
+			foeAbil === "clearbody" || foeAbil === "whitesmoke" ||
+			foeAbil === "hypercutter" || foeAbil === "innerfocus" ||
+			foeAbil === "owntempo" || foeAbil === "fullmetalbody" ||
+			foeAbil === "scrappy" || foeAbil === "oblivious";
+		if (!blocksIntimidate) bonus += 7 * physicalLean;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

		// Abilities that block, invert, or reflect Intimidate's drop.
		const foeAbil = toID(foe.ability);
		const blocksIntimidate =
			foeAbil === "clearbody" || foeAbil === "whitesmoke" ||
			foeAbil === "hypercutter" || foeAbil === "innerfocus" ||
			foeAbil === "owntempo" || foeAbil === "fullmetalbody" ||
			foeAbil === "scrappy" || foeAbil === "oblivious";
		if (!blocksIntimidate) bonus += 7 * physicalLean;
	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` around lines 190 - 195,
The current conditional in SwitchEvaluator that skips adding Intimidate's
offensive bonus when toID(foe.ability) === "unaware" is incorrect; change the
logic so Intimidate's matchup bonus (the 7 * physicalLean addition) is applied
normally unless the foe has an ability that actually blocks or inverts the drop
(e.g. clearbody, hypercutter, innerfocus, owntempo, defiant, competitive) or
Mirror Armor which reflects effects; update the check around the bonus
calculation to remove the special-case for "unaware" and instead exclude only
those explicit abilities (use toID(foe.ability) and compare against a small set
or switch) so the bonus is applied for unaware but not for blocking/inverting
abilities or mirrorarmor.

✅ Confirmed as addressed by @smell-of-curry

_⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ **Unaware on the foe does not negate Intimidate's offensive impact.** Unaware ignores the *opponent's* stat-stage changes when the Unaware holder is dealing or taking damage. It does not erase the holder's own stages. When the foe has Unaware and our Intimidate sets foe's Atk to −1, the foe still attacks with its own −1 Atk stage; that's what reduces incoming damage on us, and Unaware doesn't bypass it. So Intimidate's matchup bonus should still apply here. The only real wrinkles are Clear Body / Hyper Cutter / Inner Focus / Own Tempo / Defiant / Competitive on the foe (block or invert the drop), and Mirror Armor (reflect onto us) — none of which is `unaware`. <details> <summary>🐛 Proposed fix</summary> ```diff - // Unaware on the foe ignores our Intimidate drop's offensive - // impact, though the stage is still applied; treat that as - // neutral. - if (toID(foe.ability) === "unaware") bonus += 0; - else bonus += 7 * physicalLean; + // Abilities that block, invert, or reflect Intimidate's drop. + const foeAbil = toID(foe.ability); + const blocksIntimidate = + foeAbil === "clearbody" || foeAbil === "whitesmoke" || + foeAbil === "hypercutter" || foeAbil === "innerfocus" || + foeAbil === "owntempo" || foeAbil === "fullmetalbody" || + foeAbil === "scrappy" || foeAbil === "oblivious"; + if (!blocksIntimidate) bonus += 7 * physicalLean; ``` </details> <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. ```suggestion // Abilities that block, invert, or reflect Intimidate's drop. const foeAbil = toID(foe.ability); const blocksIntimidate = foeAbil === "clearbody" || foeAbil === "whitesmoke" || foeAbil === "hypercutter" || foeAbil === "innerfocus" || foeAbil === "owntempo" || foeAbil === "fullmetalbody" || foeAbil === "scrappy" || foeAbil === "oblivious"; if (!blocksIntimidate) bonus += 7 * physicalLean; } ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` around lines 190 - 195, The current conditional in SwitchEvaluator that skips adding Intimidate's offensive bonus when toID(foe.ability) === "unaware" is incorrect; change the logic so Intimidate's matchup bonus (the 7 * physicalLean addition) is applied normally unless the foe has an ability that actually blocks or inverts the drop (e.g. clearbody, hypercutter, innerfocus, owntempo, defiant, competitive) or Mirror Armor which reflects effects; update the check around the bonus calculation to remove the special-case for "unaware" and instead exclude only those explicit abilities (use toID(foe.ability) and compare against a small set or switch) so the bonus is applied for unaware but not for blocking/inverting abilities or mirrorarmor. ``` </details> <!-- This is an auto-generated reply by CodeRabbit --> ✅ Confirmed as addressed by @smell-of-curry <!-- This is an auto-generated comment by CodeRabbit -->
@ -135,0 +274,4 @@
// triggers on entry instead, granting +30%/+50% to the highest
// stat for the duration of the stay.
if (item === "boosterenergy") {
const isProto = ability === "protosynthesis";
coderabbitai[bot] (Migrated from github.com) commented 2026-05-15 15:48:07 +00:00

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Proxy moves are hardcoded category: "Physical", underestimating special-leaning attackers.

Both the STAB proxy loop (293-311) and the coverage probe loop (312-326) emit Physical moves only. calculateDamage selects Atk-vs-Def for Physical and SpA-vs-SpD for Special, so any attacker whose offensive leaning is Special gets clamped to their (usually low) Atk stat. Concrete consequences:

  • Foe evaluation (useKnownOnly=true): a Magnezone that has only revealed Thunderbolt gets a Physical Steel STAB proxy computed off ~Atk 70 instead of SpA 130. The AI massively under-fears its unrevealed Flash Cannon — exactly the unrevealed-STAB blindspot this PR is trying to close.
  • Our evaluation (useKnownOnly=false): same problem for our own special-leaning candidates probing coverage types — the optimistic-probe rationale at lines 254-256 breaks for ~half of mons.

The new regression tests don't exercise this because every attacker used (Garchomp, Tyranitar) is Physical-leaning, so they pass with "Physical" baked in.

The cleanest fix is to emit both categories per type and let the max avgDamage selection at line 338 pick whichever side actually hurts — that way you don't need stats data (important for unrevealed foes) and you naturally model both unrevealed Iron Head and unrevealed Flash Cannon.

🐛 Proposed fix
-	const revealedTypes = new Set(moves.map(m => m.type));
-	const tackleProto = Dex.moves.get("tackle");
-	const seenProxyTypes = new Set<string>();
-	for (const t of attackerMon.types) {
-		if (seenProxyTypes.has(t) || revealedTypes.has(t)) continue;
-		seenProxyTypes.add(t);
-		// Slightly lower BP for the proxy (80) than a real STAB so the
-		// estimate is "the foe probably has *some* attack of this type"
-		// without claiming it's a guaranteed top-tier hit.
-		moves.push({
-			...tackleProto,
-			id: `proxystab${t.toLowerCase()}` as never,
-			name: `Proxy ${t}`,
-			type: t,
-			category: "Physical",
-			basePower: 80,
-			accuracy: 100,
-		});
-	}
-	if (!useKnownOnly) {
-		// Coverage moves: the most-feared off-type move. We probe a
-		// shortlist of common types; the best one wins.
-		for (const t of COMMON_COVERAGE_TYPES) {
-			if (seenProxyTypes.has(t) || revealedTypes.has(t)) continue;
-			moves.push({
-				...tackleProto,
-				id: `proxycov${t.toLowerCase()}` as never,
-				name: `Proxy ${t}`,
-				type: t,
-				category: "Physical",
-				basePower: 80,
-				accuracy: 100,
-			});
-		}
-	}
+	const revealedTypes = new Set(moves.map(m => m.type));
+	const tackleProto = Dex.moves.get("tackle");
+	const seenProxyTypes = new Set<string>();
+	const pushProxy = (idPrefix: string, t: string, category: "Physical" | "Special") => {
+		moves.push({
+			...tackleProto,
+			id: `${idPrefix}${category.toLowerCase()}${t.toLowerCase()}` as never,
+			name: `Proxy ${category} ${t}`,
+			type: t,
+			category,
+			basePower: 80,
+			accuracy: 100,
+		});
+	};
+	for (const t of attackerMon.types) {
+		if (seenProxyTypes.has(t) || revealedTypes.has(t)) continue;
+		seenProxyTypes.add(t);
+		// Emit both categories; max-damage selection downstream picks
+		// whichever side actually hurts. This avoids clamping special
+		// attackers (e.g. Magnezone) to their unused Atk stat.
+		pushProxy("proxystab", t, "Physical");
+		pushProxy("proxystab", t, "Special");
+	}
+	if (!useKnownOnly) {
+		for (const t of COMMON_COVERAGE_TYPES) {
+			if (seenProxyTypes.has(t) || revealedTypes.has(t)) continue;
+			pushProxy("proxycov", t, "Physical");
+			pushProxy("proxycov", t, "Special");
+		}
+	}

You'll probably also want a follow-up test that pits e.g. a Magnezone with only Thunderbolt revealed against a 4×-Steel-weak switch candidate to lock in the special-side regression.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

	// Always add STAB proxies for the attacker's effective types — but
	// only ones the attacker doesn't already cover with a revealed move
	// of the same type. Species typing is public information, so
	// assuming the mon has at least *one* damaging STAB option is safe
	// and prevents the AI from over-trusting an incomplete revealed
	// list. Skipping types we already see avoids double-counting
	// (revealed Earthquake + imagined "Proxy Ground" → over-estimate).
	const revealedTypes = new Set(moves.map(m => m.type));
	const tackleProto = Dex.moves.get("tackle");
	const seenProxyTypes = new Set<string>();
	const pushProxy = (idPrefix: string, t: string, category: "Physical" | "Special") => {
		moves.push({
			...tackleProto,
			id: `${idPrefix}${category.toLowerCase()}${t.toLowerCase()}` as never,
			name: `Proxy ${category} ${t}`,
			type: t,
			category,
			basePower: 80,
			accuracy: 100,
		});
	};
	for (const t of attackerMon.types) {
		if (seenProxyTypes.has(t) || revealedTypes.has(t)) continue;
		seenProxyTypes.add(t);
		// Emit both categories; max-damage selection downstream picks
		// whichever side actually hurts. This avoids clamping special
		// attackers (e.g. Magnezone) to their unused Atk stat.
		pushProxy("proxystab", t, "Physical");
		pushProxy("proxystab", t, "Special");
	}
	if (!useKnownOnly) {
		for (const t of COMMON_COVERAGE_TYPES) {
			if (seenProxyTypes.has(t) || revealedTypes.has(t)) continue;
			pushProxy("proxycov", t, "Physical");
			pushProxy("proxycov", t, "Special");
		}
	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` around lines 286 - 326,
The proxy moves are always created with category "Physical", which mis-scores
special attackers; update both the STAB proxy creation (where id is currently
`proxystab${t.toLowerCase()}`) and the coverage probe creation
(`proxycov${t.toLowerCase()}`) to emit two proxy moves per type: one with
category "Physical" and one with category "Special" (use distinct ids, e.g.
append `_phys` and `_spec` or similar), reusing `tackleProto` spread and the
same type/basePower/accuracy fields; leave `useKnownOnly` logic intact so the
later max(avgDamage) selection (the `calculateDamage` path) will pick the
appropriate category automatically.

✅ Confirmed as addressed by @smell-of-curry

_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Proxy moves are hardcoded `category: "Physical"`, underestimating special-leaning attackers.** Both the STAB proxy loop (293-311) and the coverage probe loop (312-326) emit Physical moves only. `calculateDamage` selects Atk-vs-Def for Physical and SpA-vs-SpD for Special, so any attacker whose offensive leaning is Special gets clamped to their (usually low) Atk stat. Concrete consequences: - Foe evaluation (`useKnownOnly=true`): a Magnezone that has only revealed Thunderbolt gets a Physical Steel STAB proxy computed off ~Atk 70 instead of SpA 130. The AI massively under-fears its unrevealed Flash Cannon — exactly the unrevealed-STAB blindspot this PR is trying to close. - Our evaluation (`useKnownOnly=false`): same problem for our own special-leaning candidates probing coverage types — the optimistic-probe rationale at lines 254-256 breaks for ~half of mons. The new regression tests don't exercise this because every attacker used (Garchomp, Tyranitar) is Physical-leaning, so they pass with `"Physical"` baked in. The cleanest fix is to emit both categories per type and let the `max avgDamage` selection at line 338 pick whichever side actually hurts — that way you don't need stats data (important for unrevealed foes) and you naturally model both unrevealed Iron Head and unrevealed Flash Cannon. <details> <summary>🐛 Proposed fix</summary> ```diff - const revealedTypes = new Set(moves.map(m => m.type)); - const tackleProto = Dex.moves.get("tackle"); - const seenProxyTypes = new Set<string>(); - for (const t of attackerMon.types) { - if (seenProxyTypes.has(t) || revealedTypes.has(t)) continue; - seenProxyTypes.add(t); - // Slightly lower BP for the proxy (80) than a real STAB so the - // estimate is "the foe probably has *some* attack of this type" - // without claiming it's a guaranteed top-tier hit. - moves.push({ - ...tackleProto, - id: `proxystab${t.toLowerCase()}` as never, - name: `Proxy ${t}`, - type: t, - category: "Physical", - basePower: 80, - accuracy: 100, - }); - } - if (!useKnownOnly) { - // Coverage moves: the most-feared off-type move. We probe a - // shortlist of common types; the best one wins. - for (const t of COMMON_COVERAGE_TYPES) { - if (seenProxyTypes.has(t) || revealedTypes.has(t)) continue; - moves.push({ - ...tackleProto, - id: `proxycov${t.toLowerCase()}` as never, - name: `Proxy ${t}`, - type: t, - category: "Physical", - basePower: 80, - accuracy: 100, - }); - } - } + const revealedTypes = new Set(moves.map(m => m.type)); + const tackleProto = Dex.moves.get("tackle"); + const seenProxyTypes = new Set<string>(); + const pushProxy = (idPrefix: string, t: string, category: "Physical" | "Special") => { + moves.push({ + ...tackleProto, + id: `${idPrefix}${category.toLowerCase()}${t.toLowerCase()}` as never, + name: `Proxy ${category} ${t}`, + type: t, + category, + basePower: 80, + accuracy: 100, + }); + }; + for (const t of attackerMon.types) { + if (seenProxyTypes.has(t) || revealedTypes.has(t)) continue; + seenProxyTypes.add(t); + // Emit both categories; max-damage selection downstream picks + // whichever side actually hurts. This avoids clamping special + // attackers (e.g. Magnezone) to their unused Atk stat. + pushProxy("proxystab", t, "Physical"); + pushProxy("proxystab", t, "Special"); + } + if (!useKnownOnly) { + for (const t of COMMON_COVERAGE_TYPES) { + if (seenProxyTypes.has(t) || revealedTypes.has(t)) continue; + pushProxy("proxycov", t, "Physical"); + pushProxy("proxycov", t, "Special"); + } + } ``` </details> You'll probably also want a follow-up test that pits e.g. a Magnezone with only Thunderbolt revealed against a 4×-Steel-weak switch candidate to lock in the special-side regression. <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. ```suggestion // Always add STAB proxies for the attacker's effective types — but // only ones the attacker doesn't already cover with a revealed move // of the same type. Species typing is public information, so // assuming the mon has at least *one* damaging STAB option is safe // and prevents the AI from over-trusting an incomplete revealed // list. Skipping types we already see avoids double-counting // (revealed Earthquake + imagined "Proxy Ground" → over-estimate). const revealedTypes = new Set(moves.map(m => m.type)); const tackleProto = Dex.moves.get("tackle"); const seenProxyTypes = new Set<string>(); const pushProxy = (idPrefix: string, t: string, category: "Physical" | "Special") => { moves.push({ ...tackleProto, id: `${idPrefix}${category.toLowerCase()}${t.toLowerCase()}` as never, name: `Proxy ${category} ${t}`, type: t, category, basePower: 80, accuracy: 100, }); }; for (const t of attackerMon.types) { if (seenProxyTypes.has(t) || revealedTypes.has(t)) continue; seenProxyTypes.add(t); // Emit both categories; max-damage selection downstream picks // whichever side actually hurts. This avoids clamping special // attackers (e.g. Magnezone) to their unused Atk stat. pushProxy("proxystab", t, "Physical"); pushProxy("proxystab", t, "Special"); } if (!useKnownOnly) { for (const t of COMMON_COVERAGE_TYPES) { if (seenProxyTypes.has(t) || revealedTypes.has(t)) continue; pushProxy("proxycov", t, "Physical"); pushProxy("proxycov", t, "Special"); } } ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` around lines 286 - 326, The proxy moves are always created with category "Physical", which mis-scores special attackers; update both the STAB proxy creation (where id is currently `proxystab${t.toLowerCase()}`) and the coverage probe creation (`proxycov${t.toLowerCase()}`) to emit two proxy moves per type: one with category "Physical" and one with category "Special" (use distinct ids, e.g. append `_phys` and `_spec` or similar), reusing `tackleProto` spread and the same type/basePower/accuracy fields; leave `useKnownOnly` logic intact so the later max(avgDamage) selection (the `calculateDamage` path) will pick the appropriate category automatically. ``` </details> <!-- This is an auto-generated reply by CodeRabbit --> ✅ Confirmed as addressed by @smell-of-curry <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-05-16 17:53:50 +00:00
coderabbitai[bot] (Migrated from github.com) left a comment

Actionable comments posted: 3

🧹 Nitpick comments (2)
test/sim/tools/strategic-ai/move-evaluator.js (1)

236-257: 💤 Low value

Consider adding a brief comment explaining the stats requirement.

The Bullet Punch and Extreme Speed tests provide explicit stats objects (lines 241-242, 245-246, 263, 267-268), while earlier tests do not. A one-line comment noting that damage calculation needs stats would help readers understand why these tests differ.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/sim/tools/strategic-ai/move-evaluator.js` around lines 236 - 257, Add a
one-line comment above the test setup for Bullet Punch / Extreme Speed
explaining why explicit stats are provided (damage calc needs concrete stats for
consistent evaluation), i.e. annotate the mkTracked calls used in the 'Bullet
Punch at low HP...' test and the Extreme Speed test to clarify that evaluateMove
/ ctxFor rely on the provided stats for deterministic damage/priority scoring;
reference the test function names and helpers evaluateMove, mkTracked and ctxFor
so reviewers see where to add the comment.
test/sim/tools/strategic-ai/switch-evaluator.js (1)

257-281: 💤 Low value

Intimidate test nested under wrong describe block.

This test validates Intimidate entry synergy but is nested under 'terrain-seed entry synergy'. Consider moving it to its own describe('Intimidate entry synergy', ...) block for clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/sim/tools/strategic-ai/switch-evaluator.js` around lines 257 - 281, The
Intimidate-focused test ("Intimidate bonus shifts the switch decision toward the
physical-foe-counter") is currently nested under the wrong describe block;
extract this it(...) and place it under a new describe('Intimidate entry
synergy', ...) block to clarify intent and grouping. Locate the test using the
it name and helper calls (freshTracker, mkTracked, evaluateMatchup, assert) and
move the entire test body into the new describe, keeping the test code unchanged
except for its wrapping describe so it remains grouped with other Intimidate
entry-synergy tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sim/tools/strategic-ai/mechanics/DamageCalc.ts`:
- Around line 759-767: This helper uses raw field.weather and thus ignores
weather suppression (Cloud Nine / Air Lock); replace the direct reads of
field.weather and field.terrain with the calc's effective values by calling
effectiveWeather(field) (and/or effectiveTerrain(field) if available) and use
that result to set sun and eTerrain so activeForProto and activeForQuark respect
suppressed weather/terrain; update references to weather/terrain, keep
item/booster logic and early return unchanged.
- Around line 625-629: The Paradox/Protosynthesis offensive multiplier is being
added even when id === "foulplay", which causes Foul Play to incorrectly include
the user's own Paradox boost; modify the Paradox offensive multiplier logic (the
block that applies the offensive multiplier around the existing code that
references offensiveStatKey/baseStat/effectiveStat) to skip applying that
multiplier when id === "foulplay" (i.e., treat Foul Play as exempt) so the move
uses only the defender's Attack read into baseStat via effectiveStat without
adding the user's Paradox/Quark Drive boost.

In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts`:
- Around line 460-500: paradoxSpeedActive currently only accepts weather and
treats Quark Drive as active solely when Booster Energy is present; change its
signature to accept terrain (e.g., add a terrain: string parameter) and update
the internal logic so the Quark Drive branch treats electricterrain the same as
hasBooster (i.e., const electric = terrain === "electricterrain"; use electric
|| hasBooster where ability === "quarkdrive"); then update callers named
scaledSpeed and computeSpeedDelta to pass the current terrain through to
paradoxSpeedActive so the Electric Terrain case is respected.

---

Nitpick comments:
In `@test/sim/tools/strategic-ai/move-evaluator.js`:
- Around line 236-257: Add a one-line comment above the test setup for Bullet
Punch / Extreme Speed explaining why explicit stats are provided (damage calc
needs concrete stats for consistent evaluation), i.e. annotate the mkTracked
calls used in the 'Bullet Punch at low HP...' test and the Extreme Speed test to
clarify that evaluateMove / ctxFor rely on the provided stats for deterministic
damage/priority scoring; reference the test function names and helpers
evaluateMove, mkTracked and ctxFor so reviewers see where to add the comment.

In `@test/sim/tools/strategic-ai/switch-evaluator.js`:
- Around line 257-281: The Intimidate-focused test ("Intimidate bonus shifts the
switch decision toward the physical-foe-counter") is currently nested under the
wrong describe block; extract this it(...) and place it under a new
describe('Intimidate entry synergy', ...) block to clarify intent and grouping.
Locate the test using the it name and helper calls (freshTracker, mkTracked,
evaluateMatchup, assert) and move the entire test body into the new describe,
keeping the test code unchanged except for its wrapping describe so it remains
grouped with other Intimidate entry-synergy tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8476ef2b-cdce-47bb-836c-805825b55a52

📥 Commits

Reviewing files that changed from the base of the PR and between 6f079bb206 and c987e24d24.

📒 Files selected for processing (7)
  • sim/tools/strategic-ai/engines/HeuristicEngine.ts
  • sim/tools/strategic-ai/mechanics/DamageCalc.ts
  • sim/tools/strategic-ai/mechanics/MoveEvaluator.ts
  • sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts
  • test/sim/tools/strategic-ai/damage-calc.js
  • test/sim/tools/strategic-ai/move-evaluator.js
  • test/sim/tools/strategic-ai/switch-evaluator.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • sim/tools/strategic-ai/engines/HeuristicEngine.ts
  • sim/tools/strategic-ai/mechanics/MoveEvaluator.ts
**Actionable comments posted: 3** <details> <summary>🧹 Nitpick comments (2)</summary><blockquote> <details> <summary>test/sim/tools/strategic-ai/move-evaluator.js (1)</summary><blockquote> `236-257`: _💤 Low value_ **Consider adding a brief comment explaining the stats requirement.** The Bullet Punch and Extreme Speed tests provide explicit `stats` objects (lines 241-242, 245-246, 263, 267-268), while earlier tests do not. A one-line comment noting that damage calculation needs stats would help readers understand why these tests differ. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/sim/tools/strategic-ai/move-evaluator.js` around lines 236 - 257, Add a one-line comment above the test setup for Bullet Punch / Extreme Speed explaining why explicit stats are provided (damage calc needs concrete stats for consistent evaluation), i.e. annotate the mkTracked calls used in the 'Bullet Punch at low HP...' test and the Extreme Speed test to clarify that evaluateMove / ctxFor rely on the provided stats for deterministic damage/priority scoring; reference the test function names and helpers evaluateMove, mkTracked and ctxFor so reviewers see where to add the comment. ``` </details> </blockquote></details> <details> <summary>test/sim/tools/strategic-ai/switch-evaluator.js (1)</summary><blockquote> `257-281`: _💤 Low value_ **Intimidate test nested under wrong describe block.** This test validates Intimidate entry synergy but is nested under `'terrain-seed entry synergy'`. Consider moving it to its own `describe('Intimidate entry synergy', ...)` block for clarity. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/sim/tools/strategic-ai/switch-evaluator.js` around lines 257 - 281, The Intimidate-focused test ("Intimidate bonus shifts the switch decision toward the physical-foe-counter") is currently nested under the wrong describe block; extract this it(...) and place it under a new describe('Intimidate entry synergy', ...) block to clarify intent and grouping. Locate the test using the it name and helper calls (freshTracker, mkTracked, evaluateMatchup, assert) and move the entire test body into the new describe, keeping the test code unchanged except for its wrapping describe so it remains grouped with other Intimidate entry-synergy tests. ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. Inline comments: In `@sim/tools/strategic-ai/mechanics/DamageCalc.ts`: - Around line 759-767: This helper uses raw field.weather and thus ignores weather suppression (Cloud Nine / Air Lock); replace the direct reads of field.weather and field.terrain with the calc's effective values by calling effectiveWeather(field) (and/or effectiveTerrain(field) if available) and use that result to set sun and eTerrain so activeForProto and activeForQuark respect suppressed weather/terrain; update references to weather/terrain, keep item/booster logic and early return unchanged. - Around line 625-629: The Paradox/Protosynthesis offensive multiplier is being added even when id === "foulplay", which causes Foul Play to incorrectly include the user's own Paradox boost; modify the Paradox offensive multiplier logic (the block that applies the offensive multiplier around the existing code that references offensiveStatKey/baseStat/effectiveStat) to skip applying that multiplier when id === "foulplay" (i.e., treat Foul Play as exempt) so the move uses only the defender's Attack read into baseStat via effectiveStat without adding the user's Paradox/Quark Drive boost. In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts`: - Around line 460-500: paradoxSpeedActive currently only accepts weather and treats Quark Drive as active solely when Booster Energy is present; change its signature to accept terrain (e.g., add a terrain: string parameter) and update the internal logic so the Quark Drive branch treats electricterrain the same as hasBooster (i.e., const electric = terrain === "electricterrain"; use electric || hasBooster where ability === "quarkdrive"); then update callers named scaledSpeed and computeSpeedDelta to pass the current terrain through to paradoxSpeedActive so the Electric Terrain case is respected. --- Nitpick comments: In `@test/sim/tools/strategic-ai/move-evaluator.js`: - Around line 236-257: Add a one-line comment above the test setup for Bullet Punch / Extreme Speed explaining why explicit stats are provided (damage calc needs concrete stats for consistent evaluation), i.e. annotate the mkTracked calls used in the 'Bullet Punch at low HP...' test and the Extreme Speed test to clarify that evaluateMove / ctxFor rely on the provided stats for deterministic damage/priority scoring; reference the test function names and helpers evaluateMove, mkTracked and ctxFor so reviewers see where to add the comment. In `@test/sim/tools/strategic-ai/switch-evaluator.js`: - Around line 257-281: The Intimidate-focused test ("Intimidate bonus shifts the switch decision toward the physical-foe-counter") is currently nested under the wrong describe block; extract this it(...) and place it under a new describe('Intimidate entry synergy', ...) block to clarify intent and grouping. Locate the test using the it name and helper calls (freshTracker, mkTracked, evaluateMatchup, assert) and move the entire test body into the new describe, keeping the test code unchanged except for its wrapping describe so it remains grouped with other Intimidate entry-synergy tests. ``` </details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `8476ef2b-cdce-47bb-836c-805825b55a52` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 6f079bb206776aeac7056c81aeca51feb5e6cd1d and c987e24d249feb0db6c4abf330567c76c16ee9eb. </details> <details> <summary>📒 Files selected for processing (7)</summary> * `sim/tools/strategic-ai/engines/HeuristicEngine.ts` * `sim/tools/strategic-ai/mechanics/DamageCalc.ts` * `sim/tools/strategic-ai/mechanics/MoveEvaluator.ts` * `sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` * `test/sim/tools/strategic-ai/damage-calc.js` * `test/sim/tools/strategic-ai/move-evaluator.js` * `test/sim/tools/strategic-ai/switch-evaluator.js` </details> <details> <summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary> * sim/tools/strategic-ai/engines/HeuristicEngine.ts * sim/tools/strategic-ai/mechanics/MoveEvaluator.ts </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
@ -621,3 +701,4 @@
if (id === "bodypress") {
offensiveStatKey = "def";
baseStat = effectiveStat(input.attacker, "def", /* ignoreNeg */ crit);
} else if (id === "foulplay") {
coderabbitai[bot] (Migrated from github.com) commented 2026-05-16 17:53:49 +00:00

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Respect weather suppression when activating Protosynthesis.

This helper reads raw field.weather, so Cloud Nine / Air Lock are ignored here even though the rest of the calc routes weather-dependent effects through effectiveWeather(...). In those matchups the calc will still apply the Protosynthesis stat boost under suppressed sun.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/mechanics/DamageCalc.ts` around lines 759 - 767, This
helper uses raw field.weather and thus ignores weather suppression (Cloud Nine /
Air Lock); replace the direct reads of field.weather and field.terrain with the
calc's effective values by calling effectiveWeather(field) (and/or
effectiveTerrain(field) if available) and use that result to set sun and
eTerrain so activeForProto and activeForQuark respect suppressed
weather/terrain; update references to weather/terrain, keep item/booster logic
and early return unchanged.
_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Respect weather suppression when activating `Protosynthesis`.** This helper reads raw `field.weather`, so `Cloud Nine` / `Air Lock` are ignored here even though the rest of the calc routes weather-dependent effects through `effectiveWeather(...)`. In those matchups the calc will still apply the Protosynthesis stat boost under suppressed sun. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sim/tools/strategic-ai/mechanics/DamageCalc.ts` around lines 759 - 767, This helper uses raw field.weather and thus ignores weather suppression (Cloud Nine / Air Lock); replace the direct reads of field.weather and field.terrain with the calc's effective values by calling effectiveWeather(field) (and/or effectiveTerrain(field) if available) and use that result to set sun and eTerrain so activeForProto and activeForQuark respect suppressed weather/terrain; update references to weather/terrain, keep item/booster logic and early return unchanged. ``` </details> <!-- fingerprinting:phantom:medusa:grasshopper --> <!-- This is an auto-generated reply by CodeRabbit -->
@ -644,0 +730,4 @@
if (
ability === "solarpower" &&
!physical &&
(offensiveWeather === "sunnyday" || offensiveWeather === "desolateland")
coderabbitai[bot] (Migrated from github.com) commented 2026-05-16 17:53:49 +00:00

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Skip the Paradox offensive multiplier for Foul Play.

On Line 629 the move intentionally reads the target's Attack, but Lines 664-667 can still add the user's own Protosynthesis/Quark Drive boost back on top. That makes Foul Play too strong whenever the user has an active Attack boost.

Suggested fix
-	if (paradoxBoosted && paradoxBoosted === offensiveStatKey) {
+	if (id !== "foulplay" && paradoxBoosted && paradoxBoosted === offensiveStatKey) {
 		baseStat *= paradoxBoostedMultiplier(paradoxBoosted);
 	}

Also applies to: 664-667

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/mechanics/DamageCalc.ts` around lines 625 - 629, The
Paradox/Protosynthesis offensive multiplier is being added even when id ===
"foulplay", which causes Foul Play to incorrectly include the user's own Paradox
boost; modify the Paradox offensive multiplier logic (the block that applies the
offensive multiplier around the existing code that references
offensiveStatKey/baseStat/effectiveStat) to skip applying that multiplier when
id === "foulplay" (i.e., treat Foul Play as exempt) so the move uses only the
defender's Attack read into baseStat via effectiveStat without adding the user's
Paradox/Quark Drive boost.

✅ Confirmed as addressed by @smell-of-curry

_⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ **Skip the Paradox offensive multiplier for `Foul Play`.** On Line 629 the move intentionally reads the target's Attack, but Lines 664-667 can still add the user's own Protosynthesis/Quark Drive boost back on top. That makes `Foul Play` too strong whenever the user has an active Attack boost. <details> <summary>Suggested fix</summary> ```diff - if (paradoxBoosted && paradoxBoosted === offensiveStatKey) { + if (id !== "foulplay" && paradoxBoosted && paradoxBoosted === offensiveStatKey) { baseStat *= paradoxBoostedMultiplier(paradoxBoosted); } ``` </details> Also applies to: 664-667 <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sim/tools/strategic-ai/mechanics/DamageCalc.ts` around lines 625 - 629, The Paradox/Protosynthesis offensive multiplier is being added even when id === "foulplay", which causes Foul Play to incorrectly include the user's own Paradox boost; modify the Paradox offensive multiplier logic (the block that applies the offensive multiplier around the existing code that references offensiveStatKey/baseStat/effectiveStat) to skip applying that multiplier when id === "foulplay" (i.e., treat Foul Play as exempt) so the move uses only the defender's Attack read into baseStat via effectiveStat without adding the user's Paradox/Quark Drive boost. ``` </details> <!-- fingerprinting:phantom:medusa:grasshopper --> <!-- This is an auto-generated reply by CodeRabbit --> ✅ Confirmed as addressed by @smell-of-curry <!-- This is an auto-generated comment by CodeRabbit -->
@ -135,0 +433,4 @@
* @param move The incoming move.
* @param target The Pokemon being hit.
* @returns true if the move is fully neutralized.
*/
coderabbitai[bot] (Migrated from github.com) commented 2026-05-16 17:53:49 +00:00

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Quark Drive activation ignores Electric Terrain.

paradoxSpeedActive receives only weather but Quark Drive should activate when Electric Terrain is up (not just with Booster Energy). At line 474, the Quark Drive branch only checks hasBooster, so a mon like Iron Valiant on Electric Terrain (set by another source) won't receive the 1.5× speed multiplier.

Pass terrain into this helper and check for it:

🐛 Proposed fix
-function paradoxSpeedActive(mon: TrackedPokemon, weather: string): boolean {
+function paradoxSpeedActive(mon: TrackedPokemon, weather: string, terrain: string): boolean {
 	const ability = toID(mon.ability);
 	if (ability !== "protosynthesis" && ability !== "quarkdrive") return false;
 	for (const v of mon.volatiles) {
 		if (v === "protosynthesisspe" || v === "quarkdrivespe") return true;
 	}
 	const item = toID(mon.item);
 	const hasBooster = item === "boosterenergy";
 	const sun = weather === "sunnyday" || weather === "desolateland";
+	const eTerrain = terrain === "electricterrain";
 	const isProto = ability === "protosynthesis";
-	if (!(isProto ? sun || hasBooster : hasBooster)) return false;
+	if (!(isProto ? sun || hasBooster : eTerrain || hasBooster)) return false;
 	// ... rest unchanged

And update scaledSpeed to pass terrain:

-function scaledSpeed(mon: TrackedPokemon, tailwind: boolean, weather: string): number {
+function scaledSpeed(mon: TrackedPokemon, tailwind: boolean, weather: string, terrain: string): number {
 	// ...
-	if (paradoxSpeedActive(mon, weather)) spe = Math.floor(spe * 1.5);
+	if (paradoxSpeedActive(mon, weather, terrain)) spe = Math.floor(spe * 1.5);

And update computeSpeedDelta to pass terrain:

 function computeSpeedDelta(...) {
 	const weather = tracker.field.weather;
-	const mySpe = scaledSpeed(mon, tracker.sides[tracker.mySide].tailwindTurns > 0, weather);
-	const foeSpe = scaledSpeed(foe, tracker.sides[tracker.foeSide].tailwindTurns > 0, weather);
+	const terrain = tracker.field.terrain;
+	const mySpe = scaledSpeed(mon, tracker.sides[tracker.mySide].tailwindTurns > 0, weather, terrain);
+	const foeSpe = scaledSpeed(foe, tracker.sides[tracker.foeSide].tailwindTurns > 0, weather, terrain);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` around lines 460 - 500,
paradoxSpeedActive currently only accepts weather and treats Quark Drive as
active solely when Booster Energy is present; change its signature to accept
terrain (e.g., add a terrain: string parameter) and update the internal logic so
the Quark Drive branch treats electricterrain the same as hasBooster (i.e.,
const electric = terrain === "electricterrain"; use electric || hasBooster where
ability === "quarkdrive"); then update callers named scaledSpeed and
computeSpeedDelta to pass the current terrain through to paradoxSpeedActive so
the Electric Terrain case is respected.

✅ Confirmed as addressed by @smell-of-curry

_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Quark Drive activation ignores Electric Terrain.** `paradoxSpeedActive` receives only `weather` but Quark Drive should activate when Electric Terrain is up (not just with Booster Energy). At line 474, the Quark Drive branch only checks `hasBooster`, so a mon like Iron Valiant on Electric Terrain (set by another source) won't receive the 1.5× speed multiplier. Pass terrain into this helper and check for it: <details> <summary>🐛 Proposed fix</summary> ```diff -function paradoxSpeedActive(mon: TrackedPokemon, weather: string): boolean { +function paradoxSpeedActive(mon: TrackedPokemon, weather: string, terrain: string): boolean { const ability = toID(mon.ability); if (ability !== "protosynthesis" && ability !== "quarkdrive") return false; for (const v of mon.volatiles) { if (v === "protosynthesisspe" || v === "quarkdrivespe") return true; } const item = toID(mon.item); const hasBooster = item === "boosterenergy"; const sun = weather === "sunnyday" || weather === "desolateland"; + const eTerrain = terrain === "electricterrain"; const isProto = ability === "protosynthesis"; - if (!(isProto ? sun || hasBooster : hasBooster)) return false; + if (!(isProto ? sun || hasBooster : eTerrain || hasBooster)) return false; // ... rest unchanged ``` And update `scaledSpeed` to pass terrain: ```diff -function scaledSpeed(mon: TrackedPokemon, tailwind: boolean, weather: string): number { +function scaledSpeed(mon: TrackedPokemon, tailwind: boolean, weather: string, terrain: string): number { // ... - if (paradoxSpeedActive(mon, weather)) spe = Math.floor(spe * 1.5); + if (paradoxSpeedActive(mon, weather, terrain)) spe = Math.floor(spe * 1.5); ``` And update `computeSpeedDelta` to pass terrain: ```diff function computeSpeedDelta(...) { const weather = tracker.field.weather; - const mySpe = scaledSpeed(mon, tracker.sides[tracker.mySide].tailwindTurns > 0, weather); - const foeSpe = scaledSpeed(foe, tracker.sides[tracker.foeSide].tailwindTurns > 0, weather); + const terrain = tracker.field.terrain; + const mySpe = scaledSpeed(mon, tracker.sides[tracker.mySide].tailwindTurns > 0, weather, terrain); + const foeSpe = scaledSpeed(foe, tracker.sides[tracker.foeSide].tailwindTurns > 0, weather, terrain); ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` around lines 460 - 500, paradoxSpeedActive currently only accepts weather and treats Quark Drive as active solely when Booster Energy is present; change its signature to accept terrain (e.g., add a terrain: string parameter) and update the internal logic so the Quark Drive branch treats electricterrain the same as hasBooster (i.e., const electric = terrain === "electricterrain"; use electric || hasBooster where ability === "quarkdrive"); then update callers named scaledSpeed and computeSpeedDelta to pass the current terrain through to paradoxSpeedActive so the Electric Terrain case is respected. ``` </details> <!-- fingerprinting:phantom:medusa:ocelot --> <!-- This is an auto-generated reply by CodeRabbit --> ✅ Confirmed as addressed by @smell-of-curry <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-05-19 12:05:15 +00:00
coderabbitai[bot] (Migrated from github.com) left a comment

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
sim/tools/strategic-ai/mechanics/DamageCalc.ts (1)

704-758: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Foul Play still picks up the attacker's own Attack multipliers.

Lines 704-708 correctly swap Foul Play to the target's Attack, but the attacker-side Attack modifiers later in this block still run. That means abilities/items like Huge Power, Choice Band, and the new Protosynthesis/Quark Drive branch can all inflate Foul Play, even though this move should be keyed off the defender's Attack stat instead.

💡 Suggested fix
 function computeOffensiveStat(move: Move, input: DamageCalcInput): number {
 	const id = toID(move.id || (move as { name?: string }).name || "");
 	const physical = isPhysicalForCalc(move, input);
 	const crit = !!input.forceCrit;
+	const usesTargetAttack = id === "foulplay";
 	let baseStat: number;
 	let offensiveStatKey: keyof StatBlock;
 	if (id === "bodypress") {
 		offensiveStatKey = "def";
 		baseStat = effectiveStat(input.attacker, "def", /* ignoreNeg */ crit);
 	} else if (id === "foulplay") {
@@
 	}
 	const ability = toID(input.attacker.ability);
-	if ((ability === "hugepower" || ability === "purepower") && physical) baseStat *= 2;
-	if (ability === "guts" && physical && input.attacker.status) baseStat *= 1.5;
+	if (!usesTargetAttack && (ability === "hugepower" || ability === "purepower") && physical) baseStat *= 2;
+	if (!usesTargetAttack && ability === "guts" && physical && input.attacker.status) baseStat *= 1.5;
@@
-	if (paradoxBoosted && paradoxBoosted === offensiveStatKey) {
+	if (!usesTargetAttack && paradoxBoosted && paradoxBoosted === offensiveStatKey) {
 		baseStat *= paradoxBoostedMultiplier(paradoxBoosted);
 	}
 	const item = toID(input.attacker.item);
-	if (item === "choiceband" && physical) baseStat *= 1.5;
+	if (!usesTargetAttack && item === "choiceband" && physical) baseStat *= 1.5;

Apply the same guard to any other attacker-side Attack-stat multipliers in this block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/mechanics/DamageCalc.ts` around lines 704 - 758, The
Foul Play branch sets baseStat from the defender, but subsequent attacker-side
multipliers (ability/item/weather/protosynthesis checks) still apply; modify the
multiplier checks (the blocks referencing offensiveStatKey/baseStat, ability,
paradoxBoosted/paradoxBoostedStat, item checks like
choiceband/choicespecs/thickclub/lightball/deepseatooth, and the
flashfire/solarpower branches) to skip applying those boosts when id ===
"foulplay" (or when offensiveStatKey was sourced from the defender) so that only
defender-derived stat and its modifiers are used for Foul Play.
sim/tools/strategic-ai/engines/HeuristicEngine.ts (1)

262-276: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move evaluation is still using the stale outspeed check.

maybeSwitchOut now uses scaledSpeed, but MoveEvalContext.weOutspeed still comes from the old raw-Speed helper. That leaves the new order-sensitive move heuristics wrong in real battles under Scarf, paralysis, stat boosts, Swift Swim/Chlorophyll, and Paradox speed boosts.

Also applies to: 323-333

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/engines/HeuristicEngine.ts` around lines 262 - 276,
Move evaluation is using the old raw-Speed outspeed check (weOutspeed) while
maybeSwitchOut and newer logic use scaledSpeed, causing incorrect heuristics
under speed modifiers; update the MoveEvalContext construction (the weOutspeed
field passed into the MoveEvalContext inside HeuristicEngine) to compute
outspeed using the scaledSpeed-aware helper (or call the updated
weOutspeedScaled helper) so the value reflects current scaledSpeed rules used by
maybeSwitchOut and other order-sensitive heuristics; ensure the same fix is
applied at both places referenced (around the MoveEvalContext at the shown block
and the similar block at lines 323-333) so valueOfBestSwitch and any logic
relying on weOutspeed use the new scaled-speed result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sim/tools/strategic-ai/engines/HeuristicEngine.ts`:
- Around line 459-474: The loop assumes every positive-priority move is reliably
usable; skip conditional-priority moves (e.g., Fake Out, Sucker Punch, First
Impression) before running calculateDamage so they don't veto switching. Add a
quick guard in the active.moves loop (where move is obtained via moveOf(m.id)
and before calling calculateDamage) that continues when move.id is in a small
set of conditional-priority move ids (e.g.,
'fakeout','suckerpunch','firstimpression') or when a helper
isConditionalPriorityMove(move) returns true, then proceed with the existing
logic and KO probability check.

In `@sim/tools/strategic-ai/mechanics/MoveEvaluator.ts`:
- Around line 422-425: The yawn/terrain check incorrectly treats Electric/Misty
Terrain as blocking Yawn for all targets; update the logic in the branch
handling moveId === "yawn" (and the similar block around lines handling other
sleep/paralysis moves at 697-718) to also verify the target is grounded (e.g.,
check defender.hasAbility('levitate')/defender.isGrounded or the existing
grounded flag) before treating tracker.field.terrain === "electricterrain" ||
"mistyterrain" as a blocker; only return the -10 score/rationale when the
defender is grounded AND the terrain blocks the status, otherwise allow the move
as legal.
- Around line 820-829: The current early return in the choice-locked branch
(defender.choiceLocked && last?.exists && last.category !== "Status")
unconditionally returns score:35 with rationale "suckerpunchLockedKO", which
overrides real damage scoring; instead remove this hard return and treat it as a
fallback/bonus: let the normal damage evaluation path compute the move's actual
score, and if that path cannot produce a meaningful damage score then synthesize
a fallback by adding the priority-KO baseline (≈30) to the computed damage score
or use it only when damage is zero; keep the same identifying checks
(defender.choiceLocked, last?.exists, last.category !== "Status") and preserve
moveId/toID(move.id) and rationale but change logic to augment or fallback
rather than replace the damage score.

In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts`:
- Around line 333-347: The current ability immunity check in SwitchEvaluator
(the conditional that checks ability, moveType, and move.flags?.wind)
incorrectly treats moveType === "Flying" as Wind Rider immunity; change the Wind
Rider branch so it only treats moves with the wind flag as free-turns (i.e.,
require move.flags?.wind to be truthy) and remove the moveType === "Flying"
clause so Brave Bird/Air Slash/Acrobatics are no longer considered blocked by
"windrider".
- Around line 389-401: anyRevealedFoeMoveIsSuperEffective currently only uses
type chart so it treats moves as "super-effective" even when the target is
effectively immune (Levitate, Flash Fire, Water Absorb, Bulletproof, Air
Balloon, etc.); update the loop in anyRevealedFoeMoveIsSuperEffective to skip
moves that would deal zero damage by checking the target TrackedPokemon's
immunities/ability/item interactions for that move before counting effectiveness
(use the move from Dex.moves.get(moveId) and consult the target's ability, item
and relevant flags/statuses to detect Levitate/Flash Fire/Water
Absorb/Bulletproof/Air Balloon immunities or other move-specific immunities);
only return true when a revealed move is both type-super-effective (eff > 0) and
not actually blocked by an immunity so the Policy could realistically trigger.

---

Outside diff comments:
In `@sim/tools/strategic-ai/engines/HeuristicEngine.ts`:
- Around line 262-276: Move evaluation is using the old raw-Speed outspeed check
(weOutspeed) while maybeSwitchOut and newer logic use scaledSpeed, causing
incorrect heuristics under speed modifiers; update the MoveEvalContext
construction (the weOutspeed field passed into the MoveEvalContext inside
HeuristicEngine) to compute outspeed using the scaledSpeed-aware helper (or call
the updated weOutspeedScaled helper) so the value reflects current scaledSpeed
rules used by maybeSwitchOut and other order-sensitive heuristics; ensure the
same fix is applied at both places referenced (around the MoveEvalContext at the
shown block and the similar block at lines 323-333) so valueOfBestSwitch and any
logic relying on weOutspeed use the new scaled-speed result.

In `@sim/tools/strategic-ai/mechanics/DamageCalc.ts`:
- Around line 704-758: The Foul Play branch sets baseStat from the defender, but
subsequent attacker-side multipliers (ability/item/weather/protosynthesis
checks) still apply; modify the multiplier checks (the blocks referencing
offensiveStatKey/baseStat, ability, paradoxBoosted/paradoxBoostedStat, item
checks like choiceband/choicespecs/thickclub/lightball/deepseatooth, and the
flashfire/solarpower branches) to skip applying those boosts when id ===
"foulplay" (or when offensiveStatKey was sourced from the defender) so that only
defender-derived stat and its modifiers are used for Foul Play.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 463c3a15-5702-48d2-ae8a-cdd6186ca67d

📥 Commits

Reviewing files that changed from the base of the PR and between c987e24d24 and 6a52f5117a.

📒 Files selected for processing (10)
  • sim/tools/player-ai.ts
  • sim/tools/strategic-ai/engines/Engine.ts
  • sim/tools/strategic-ai/engines/HeuristicEngine.ts
  • sim/tools/strategic-ai/mechanics/DamageCalc.ts
  • sim/tools/strategic-ai/mechanics/MoveEvaluator.ts
  • sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts
  • sim/tools/strategic-ai/state/BattleStateTracker.ts
  • test/sim/tools/strategic-ai/force-switch.js
  • test/sim/tools/strategic-ai/move-evaluator.js
  • test/sim/tools/strategic-ai/switch-evaluator.js
✅ Files skipped from review due to trivial changes (2)
  • sim/tools/player-ai.ts
  • test/sim/tools/strategic-ai/force-switch.js
**Actionable comments posted: 5** > [!CAUTION] > Some comments are outside the diff and can’t be posted inline due to platform limitations. > > > > <details> > <summary>⚠️ Outside diff range comments (2)</summary><blockquote> > > <details> > <summary>sim/tools/strategic-ai/mechanics/DamageCalc.ts (1)</summary><blockquote> > > `704-758`: _⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ > > **`Foul Play` still picks up the attacker's own Attack multipliers.** > > Lines 704-708 correctly swap `Foul Play` to the target's Attack, but the attacker-side Attack modifiers later in this block still run. That means abilities/items like Huge Power, Choice Band, and the new Protosynthesis/Quark Drive branch can all inflate `Foul Play`, even though this move should be keyed off the defender's Attack stat instead. > > > > <details> > <summary>💡 Suggested fix</summary> > > ```diff > function computeOffensiveStat(move: Move, input: DamageCalcInput): number { > const id = toID(move.id || (move as { name?: string }).name || ""); > const physical = isPhysicalForCalc(move, input); > const crit = !!input.forceCrit; > + const usesTargetAttack = id === "foulplay"; > let baseStat: number; > let offensiveStatKey: keyof StatBlock; > if (id === "bodypress") { > offensiveStatKey = "def"; > baseStat = effectiveStat(input.attacker, "def", /* ignoreNeg */ crit); > } else if (id === "foulplay") { > @@ > } > const ability = toID(input.attacker.ability); > - if ((ability === "hugepower" || ability === "purepower") && physical) baseStat *= 2; > - if (ability === "guts" && physical && input.attacker.status) baseStat *= 1.5; > + if (!usesTargetAttack && (ability === "hugepower" || ability === "purepower") && physical) baseStat *= 2; > + if (!usesTargetAttack && ability === "guts" && physical && input.attacker.status) baseStat *= 1.5; > @@ > - if (paradoxBoosted && paradoxBoosted === offensiveStatKey) { > + if (!usesTargetAttack && paradoxBoosted && paradoxBoosted === offensiveStatKey) { > baseStat *= paradoxBoostedMultiplier(paradoxBoosted); > } > const item = toID(input.attacker.item); > - if (item === "choiceband" && physical) baseStat *= 1.5; > + if (!usesTargetAttack && item === "choiceband" && physical) baseStat *= 1.5; > ``` > > Apply the same guard to any other attacker-side Attack-stat multipliers in this block. > </details> > > <details> > <summary>🤖 Prompt for AI Agents</summary> > > ``` > Verify each finding against current code. Fix only still-valid issues, skip the > rest with a brief reason, keep changes minimal, and validate. > > In `@sim/tools/strategic-ai/mechanics/DamageCalc.ts` around lines 704 - 758, The > Foul Play branch sets baseStat from the defender, but subsequent attacker-side > multipliers (ability/item/weather/protosynthesis checks) still apply; modify the > multiplier checks (the blocks referencing offensiveStatKey/baseStat, ability, > paradoxBoosted/paradoxBoostedStat, item checks like > choiceband/choicespecs/thickclub/lightball/deepseatooth, and the > flashfire/solarpower branches) to skip applying those boosts when id === > "foulplay" (or when offensiveStatKey was sourced from the defender) so that only > defender-derived stat and its modifiers are used for Foul Play. > ``` > > </details> > > </blockquote></details> > <details> > <summary>sim/tools/strategic-ai/engines/HeuristicEngine.ts (1)</summary><blockquote> > > `262-276`: _⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ > > **Move evaluation is still using the stale outspeed check.** > > `maybeSwitchOut` now uses `scaledSpeed`, but `MoveEvalContext.weOutspeed` still comes from the old raw-Speed helper. That leaves the new order-sensitive move heuristics wrong in real battles under Scarf, paralysis, stat boosts, Swift Swim/Chlorophyll, and Paradox speed boosts. > > > > Also applies to: 323-333 > > <details> > <summary>🤖 Prompt for AI Agents</summary> > > ``` > Verify each finding against current code. Fix only still-valid issues, skip the > rest with a brief reason, keep changes minimal, and validate. > > In `@sim/tools/strategic-ai/engines/HeuristicEngine.ts` around lines 262 - 276, > Move evaluation is using the old raw-Speed outspeed check (weOutspeed) while > maybeSwitchOut and newer logic use scaledSpeed, causing incorrect heuristics > under speed modifiers; update the MoveEvalContext construction (the weOutspeed > field passed into the MoveEvalContext inside HeuristicEngine) to compute > outspeed using the scaledSpeed-aware helper (or call the updated > weOutspeedScaled helper) so the value reflects current scaledSpeed rules used by > maybeSwitchOut and other order-sensitive heuristics; ensure the same fix is > applied at both places referenced (around the MoveEvalContext at the shown block > and the similar block at lines 323-333) so valueOfBestSwitch and any logic > relying on weOutspeed use the new scaled-speed result. > ``` > > </details> > > </blockquote></details> > > </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. Inline comments: In `@sim/tools/strategic-ai/engines/HeuristicEngine.ts`: - Around line 459-474: The loop assumes every positive-priority move is reliably usable; skip conditional-priority moves (e.g., Fake Out, Sucker Punch, First Impression) before running calculateDamage so they don't veto switching. Add a quick guard in the active.moves loop (where move is obtained via moveOf(m.id) and before calling calculateDamage) that continues when move.id is in a small set of conditional-priority move ids (e.g., 'fakeout','suckerpunch','firstimpression') or when a helper isConditionalPriorityMove(move) returns true, then proceed with the existing logic and KO probability check. In `@sim/tools/strategic-ai/mechanics/MoveEvaluator.ts`: - Around line 422-425: The yawn/terrain check incorrectly treats Electric/Misty Terrain as blocking Yawn for all targets; update the logic in the branch handling moveId === "yawn" (and the similar block around lines handling other sleep/paralysis moves at 697-718) to also verify the target is grounded (e.g., check defender.hasAbility('levitate')/defender.isGrounded or the existing grounded flag) before treating tracker.field.terrain === "electricterrain" || "mistyterrain" as a blocker; only return the -10 score/rationale when the defender is grounded AND the terrain blocks the status, otherwise allow the move as legal. - Around line 820-829: The current early return in the choice-locked branch (defender.choiceLocked && last?.exists && last.category !== "Status") unconditionally returns score:35 with rationale "suckerpunchLockedKO", which overrides real damage scoring; instead remove this hard return and treat it as a fallback/bonus: let the normal damage evaluation path compute the move's actual score, and if that path cannot produce a meaningful damage score then synthesize a fallback by adding the priority-KO baseline (≈30) to the computed damage score or use it only when damage is zero; keep the same identifying checks (defender.choiceLocked, last?.exists, last.category !== "Status") and preserve moveId/toID(move.id) and rationale but change logic to augment or fallback rather than replace the damage score. In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts`: - Around line 333-347: The current ability immunity check in SwitchEvaluator (the conditional that checks ability, moveType, and move.flags?.wind) incorrectly treats moveType === "Flying" as Wind Rider immunity; change the Wind Rider branch so it only treats moves with the wind flag as free-turns (i.e., require move.flags?.wind to be truthy) and remove the moveType === "Flying" clause so Brave Bird/Air Slash/Acrobatics are no longer considered blocked by "windrider". - Around line 389-401: anyRevealedFoeMoveIsSuperEffective currently only uses type chart so it treats moves as "super-effective" even when the target is effectively immune (Levitate, Flash Fire, Water Absorb, Bulletproof, Air Balloon, etc.); update the loop in anyRevealedFoeMoveIsSuperEffective to skip moves that would deal zero damage by checking the target TrackedPokemon's immunities/ability/item interactions for that move before counting effectiveness (use the move from Dex.moves.get(moveId) and consult the target's ability, item and relevant flags/statuses to detect Levitate/Flash Fire/Water Absorb/Bulletproof/Air Balloon immunities or other move-specific immunities); only return true when a revealed move is both type-super-effective (eff > 0) and not actually blocked by an immunity so the Policy could realistically trigger. --- Outside diff comments: In `@sim/tools/strategic-ai/engines/HeuristicEngine.ts`: - Around line 262-276: Move evaluation is using the old raw-Speed outspeed check (weOutspeed) while maybeSwitchOut and newer logic use scaledSpeed, causing incorrect heuristics under speed modifiers; update the MoveEvalContext construction (the weOutspeed field passed into the MoveEvalContext inside HeuristicEngine) to compute outspeed using the scaledSpeed-aware helper (or call the updated weOutspeedScaled helper) so the value reflects current scaledSpeed rules used by maybeSwitchOut and other order-sensitive heuristics; ensure the same fix is applied at both places referenced (around the MoveEvalContext at the shown block and the similar block at lines 323-333) so valueOfBestSwitch and any logic relying on weOutspeed use the new scaled-speed result. In `@sim/tools/strategic-ai/mechanics/DamageCalc.ts`: - Around line 704-758: The Foul Play branch sets baseStat from the defender, but subsequent attacker-side multipliers (ability/item/weather/protosynthesis checks) still apply; modify the multiplier checks (the blocks referencing offensiveStatKey/baseStat, ability, paradoxBoosted/paradoxBoostedStat, item checks like choiceband/choicespecs/thickclub/lightball/deepseatooth, and the flashfire/solarpower branches) to skip applying those boosts when id === "foulplay" (or when offensiveStatKey was sourced from the defender) so that only defender-derived stat and its modifiers are used for Foul Play. ``` </details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `463c3a15-5702-48d2-ae8a-cdd6186ca67d` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between c987e24d249feb0db6c4abf330567c76c16ee9eb and 6a52f5117adb33020c35e8b2421e5219873f7b1e. </details> <details> <summary>📒 Files selected for processing (10)</summary> * `sim/tools/player-ai.ts` * `sim/tools/strategic-ai/engines/Engine.ts` * `sim/tools/strategic-ai/engines/HeuristicEngine.ts` * `sim/tools/strategic-ai/mechanics/DamageCalc.ts` * `sim/tools/strategic-ai/mechanics/MoveEvaluator.ts` * `sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` * `sim/tools/strategic-ai/state/BattleStateTracker.ts` * `test/sim/tools/strategic-ai/force-switch.js` * `test/sim/tools/strategic-ai/move-evaluator.js` * `test/sim/tools/strategic-ai/switch-evaluator.js` </details> <details> <summary>✅ Files skipped from review due to trivial changes (2)</summary> * sim/tools/player-ai.ts * test/sim/tools/strategic-ai/force-switch.js </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
coderabbitai[bot] (Migrated from github.com) commented 2026-05-19 12:05:14 +00:00

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Exclude conditional priority moves from this switch veto.

This helper assumes every positive-priority attack is reliably usable. That makes the engine stay in on the strength of Sucker Punch / Fake Out / First Impression KOs that may not exist on the current turn.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/engines/HeuristicEngine.ts` around lines 459 - 474,
The loop assumes every positive-priority move is reliably usable; skip
conditional-priority moves (e.g., Fake Out, Sucker Punch, First Impression)
before running calculateDamage so they don't veto switching. Add a quick guard
in the active.moves loop (where move is obtained via moveOf(m.id) and before
calling calculateDamage) that continues when move.id is in a small set of
conditional-priority move ids (e.g., 'fakeout','suckerpunch','firstimpression')
or when a helper isConditionalPriorityMove(move) returns true, then proceed with
the existing logic and KO probability check.

✅ Confirmed as addressed by @smell-of-curry

_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Exclude conditional priority moves from this switch veto.** This helper assumes every positive-priority attack is reliably usable. That makes the engine stay in on the strength of Sucker Punch / Fake Out / First Impression KOs that may not exist on the current turn. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sim/tools/strategic-ai/engines/HeuristicEngine.ts` around lines 459 - 474, The loop assumes every positive-priority move is reliably usable; skip conditional-priority moves (e.g., Fake Out, Sucker Punch, First Impression) before running calculateDamage so they don't veto switching. Add a quick guard in the active.moves loop (where move is obtained via moveOf(m.id) and before calling calculateDamage) that continues when move.id is in a small set of conditional-priority move ids (e.g., 'fakeout','suckerpunch','firstimpression') or when a helper isConditionalPriorityMove(move) returns true, then proceed with the existing logic and KO probability check. ``` </details> <!-- fingerprinting:phantom:medusa:grasshopper --> <!-- This is an auto-generated reply by CodeRabbit --> ✅ Confirmed as addressed by @smell-of-curry <!-- This is an auto-generated comment by CodeRabbit -->
@ -377,0 +613,4 @@
*
* @param ctx Move-eval context (attacker, defender, tracker).
* @returns true when the attacker should be treated as "definitely
* here next turn", false otherwise.
coderabbitai[bot] (Migrated from github.com) commented 2026-05-19 12:05:14 +00:00

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Terrain status blockers need a grounded check.

Electric/Misty Terrain only prevent these statuses on grounded targets. These early returns currently make the AI give up legal Yawn / sleep / paralysis lines into airborne or Levitate targets whenever those terrains are up.

Also applies to: 697-718

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/mechanics/MoveEvaluator.ts` around lines 422 - 425,
The yawn/terrain check incorrectly treats Electric/Misty Terrain as blocking
Yawn for all targets; update the logic in the branch handling moveId === "yawn"
(and the similar block around lines handling other sleep/paralysis moves at
697-718) to also verify the target is grounded (e.g., check
defender.hasAbility('levitate')/defender.isGrounded or the existing grounded
flag) before treating tracker.field.terrain === "electricterrain" ||
"mistyterrain" as a blocker; only return the -10 score/rationale when the
defender is grounded AND the terrain blocks the status, otherwise allow the move
as legal.

✅ Confirmed as addressed by @smell-of-curry

_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Terrain status blockers need a grounded check.** Electric/Misty Terrain only prevent these statuses on grounded targets. These early returns currently make the AI give up legal Yawn / sleep / paralysis lines into airborne or Levitate targets whenever those terrains are up. Also applies to: 697-718 <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sim/tools/strategic-ai/mechanics/MoveEvaluator.ts` around lines 422 - 425, The yawn/terrain check incorrectly treats Electric/Misty Terrain as blocking Yawn for all targets; update the logic in the branch handling moveId === "yawn" (and the similar block around lines handling other sleep/paralysis moves at 697-718) to also verify the target is grounded (e.g., check defender.hasAbility('levitate')/defender.isGrounded or the existing grounded flag) before treating tracker.field.terrain === "electricterrain" || "mistyterrain" as a blocker; only return the -10 score/rationale when the defender is grounded AND the terrain blocks the status, otherwise allow the move as legal. ``` </details> <!-- fingerprinting:phantom:medusa:grasshopper --> <!-- This is an auto-generated reply by CodeRabbit --> ✅ Confirmed as addressed by @smell-of-curry <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) commented 2026-05-19 12:05:14 +00:00

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't replace damage scoring with a flat Sucker Punch score.

This 35 ignores whether the hit is actually threatening. A non-STAB or low-Attack Sucker Punch now looks just as good as a real KO as long as the foe is choice-locked into an attack. It would be safer to keep the fail-case override here and let the normal damage path score the success case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/mechanics/MoveEvaluator.ts` around lines 820 - 829,
The current early return in the choice-locked branch (defender.choiceLocked &&
last?.exists && last.category !== "Status") unconditionally returns score:35
with rationale "suckerpunchLockedKO", which overrides real damage scoring;
instead remove this hard return and treat it as a fallback/bonus: let the normal
damage evaluation path compute the move's actual score, and if that path cannot
produce a meaningful damage score then synthesize a fallback by adding the
priority-KO baseline (≈30) to the computed damage score or use it only when
damage is zero; keep the same identifying checks (defender.choiceLocked,
last?.exists, last.category !== "Status") and preserve moveId/toID(move.id) and
rationale but change logic to augment or fallback rather than replace the damage
score.

✅ Confirmed as addressed by @smell-of-curry

_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Don't replace damage scoring with a flat Sucker Punch score.** This `35` ignores whether the hit is actually threatening. A non-STAB or low-Attack Sucker Punch now looks just as good as a real KO as long as the foe is choice-locked into an attack. It would be safer to keep the fail-case override here and let the normal damage path score the success case. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sim/tools/strategic-ai/mechanics/MoveEvaluator.ts` around lines 820 - 829, The current early return in the choice-locked branch (defender.choiceLocked && last?.exists && last.category !== "Status") unconditionally returns score:35 with rationale "suckerpunchLockedKO", which overrides real damage scoring; instead remove this hard return and treat it as a fallback/bonus: let the normal damage evaluation path compute the move's actual score, and if that path cannot produce a meaningful damage score then synthesize a fallback by adding the priority-KO baseline (≈30) to the computed damage score or use it only when damage is zero; keep the same identifying checks (defender.choiceLocked, last?.exists, last.category !== "Status") and preserve moveId/toID(move.id) and rationale but change logic to augment or fallback rather than replace the damage score. ``` </details> <!-- fingerprinting:phantom:medusa:grasshopper --> <!-- This is an auto-generated reply by CodeRabbit --> ✅ Confirmed as addressed by @smell-of-curry <!-- This is an auto-generated comment by CodeRabbit -->
@ -135,0 +362,4 @@
// every Flying attack — Brave Bird / Air Slash / Acrobatics
// hit it normally.
(ability === "windrider" && !!move.flags?.wind)
) {
coderabbitai[bot] (Migrated from github.com) commented 2026-05-19 12:05:14 +00:00

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Limit Wind Rider to actual wind-flagged moves.

moveType === "Flying" makes Brave Bird / Air Slash / Acrobatics look like “free-turn” entries even though Wind Rider does not block them. That can materially over-rank Wind Rider switch-ins into normal Flying attackers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` around lines 333 - 347,
The current ability immunity check in SwitchEvaluator (the conditional that
checks ability, moveType, and move.flags?.wind) incorrectly treats moveType ===
"Flying" as Wind Rider immunity; change the Wind Rider branch so it only treats
moves with the wind flag as free-turns (i.e., require move.flags?.wind to be
truthy) and remove the moveType === "Flying" clause so Brave Bird/Air
Slash/Acrobatics are no longer considered blocked by "windrider".

✅ Confirmed as addressed by @smell-of-curry

_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Limit Wind Rider to actual wind-flagged moves.** `moveType === "Flying"` makes Brave Bird / Air Slash / Acrobatics look like “free-turn” entries even though Wind Rider does not block them. That can materially over-rank Wind Rider switch-ins into normal Flying attackers. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` around lines 333 - 347, The current ability immunity check in SwitchEvaluator (the conditional that checks ability, moveType, and move.flags?.wind) incorrectly treats moveType === "Flying" as Wind Rider immunity; change the Wind Rider branch so it only treats moves with the wind flag as free-turns (i.e., require move.flags?.wind to be truthy) and remove the moveType === "Flying" clause so Brave Bird/Air Slash/Acrobatics are no longer considered blocked by "windrider". ``` </details> <!-- fingerprinting:phantom:medusa:grasshopper --> <!-- This is an auto-generated reply by CodeRabbit --> ✅ Confirmed as addressed by @smell-of-curry <!-- This is an auto-generated comment by CodeRabbit -->
@ -135,0 +420,4 @@
if (isMoveNeutralizedBy(move, mon)) continue;
let eff = 0;
for (const t of mon.types) eff += Dex.getEffectiveness(move.type, t);
if (eff > 0) return true;
coderabbitai[bot] (Migrated from github.com) commented 2026-05-19 12:05:14 +00:00

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Weakness Policy baiting needs to respect actual immunities.

This helper only checks the type chart, so it will hand out the weaknesspolicy bonus for revealed moves that are super-effective on paper but deal zero in practice because of Levitate, Flash Fire, Water Absorb, Bulletproof, etc. That makes the AI switch in expecting a Policy proc that can never happen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` around lines 389 - 401,
anyRevealedFoeMoveIsSuperEffective currently only uses type chart so it treats
moves as "super-effective" even when the target is effectively immune (Levitate,
Flash Fire, Water Absorb, Bulletproof, Air Balloon, etc.); update the loop in
anyRevealedFoeMoveIsSuperEffective to skip moves that would deal zero damage by
checking the target TrackedPokemon's immunities/ability/item interactions for
that move before counting effectiveness (use the move from Dex.moves.get(moveId)
and consult the target's ability, item and relevant flags/statuses to detect
Levitate/Flash Fire/Water Absorb/Bulletproof/Air Balloon immunities or other
move-specific immunities); only return true when a revealed move is both
type-super-effective (eff > 0) and not actually blocked by an immunity so the
Policy could realistically trigger.

✅ Confirmed as addressed by @smell-of-curry

_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Weakness Policy baiting needs to respect actual immunities.** This helper only checks the type chart, so it will hand out the `weaknesspolicy` bonus for revealed moves that are super-effective on paper but deal zero in practice because of Levitate, Flash Fire, Water Absorb, Bulletproof, etc. That makes the AI switch in expecting a Policy proc that can never happen. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sim/tools/strategic-ai/mechanics/SwitchEvaluator.ts` around lines 389 - 401, anyRevealedFoeMoveIsSuperEffective currently only uses type chart so it treats moves as "super-effective" even when the target is effectively immune (Levitate, Flash Fire, Water Absorb, Bulletproof, Air Balloon, etc.); update the loop in anyRevealedFoeMoveIsSuperEffective to skip moves that would deal zero damage by checking the target TrackedPokemon's immunities/ability/item interactions for that move before counting effectiveness (use the move from Dex.moves.get(moveId) and consult the target's ability, item and relevant flags/statuses to detect Levitate/Flash Fire/Water Absorb/Bulletproof/Air Balloon immunities or other move-specific immunities); only return true when a revealed move is both type-super-effective (eff > 0) and not actually blocked by an immunity so the Policy could realistically trigger. ``` </details> <!-- fingerprinting:phantom:medusa:grasshopper --> <!-- This is an auto-generated reply by CodeRabbit --> ✅ Confirmed as addressed by @smell-of-curry <!-- This is an auto-generated comment by CodeRabbit -->
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:24 +00:00
@ -322,0 +423,4 @@
if (defender.status) return { moveId, score: -10, rationale: "yawnRedundant" };
// Electric/Misty Terrain only block sleep on *grounded* targets;
// an airborne / Levitate foe can still be put to sleep.
const yawnBlockedByTerrain =
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:24 +00:00

Fixed in 433f496 — evaluateCounterMove now forces slower = true for Counter/Mirror Coat (their -5 priority always moves them last); Metal Burst keeps the speed-based logic. Added a regression test covering the outspeed case.

Fixed in 433f496 — `evaluateCounterMove` now forces `slower = true` for Counter/Mirror Coat (their -5 priority always moves them last); Metal Burst keeps the speed-based logic. Added a regression test covering the outspeed case.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:27 +00:00
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:27 +00:00

Fixed in 433f496 — slower is now hardcoded to true for counter/mirrorcoat (priority -5), so they score on the slower path even when we outspeed. Metal Burst (+0 priority) still uses !ctx.weOutspeed.

Fixed in 433f496 — `slower` is now hardcoded to `true` for `counter`/`mirrorcoat` (priority -5), so they score on the slower path even when we outspeed. Metal Burst (+0 priority) still uses `!ctx.weOutspeed`.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:30 +00:00
@ -322,0 +423,4 @@
if (defender.status) return { moveId, score: -10, rationale: "yawnRedundant" };
// Electric/Misty Terrain only block sleep on *grounded* targets;
// an airborne / Levitate foe can still be put to sleep.
const yawnBlockedByTerrain =
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:30 +00:00

Fixed in 433f496 — scoreDestinyBond now rewards moving first (we outspeed → bond resolves before the incoming KO) instead of the foe being faster, which whiffed. Tests updated.

Fixed in 433f496 — `scoreDestinyBond` now rewards moving first (we outspeed → bond resolves before the incoming KO) instead of the foe being faster, which whiffed. Tests updated.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:32 +00:00
@ -0,0 +110,4 @@
assert(result.score < 0,
`Destiny Bond into a faster KO should be negative — it whiffs ` +
`before resolving (got ${result.score})`);
});
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:32 +00:00

Fixed in 433f496 — the Destiny Bond tests now expect the high score when we outspeed, plus a new case asserting it's negative when the foe outspeeds and KOs first.

Fixed in 433f496 — the Destiny Bond tests now expect the high score when we outspeed, plus a new case asserting it's negative when the foe outspeeds and KOs first.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:35 +00:00
@ -129,9 +129,340 @@ export function evaluateMatchup(
score -= foeBoosts * 4;
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:35 +00:00

Fixed in 433f496 — Unaware no longer cancels Intimidate's bonus (it only ignores the opponent's stages). The bonus is skipped only for real blockers (Clear Body, Hyper Cutter, Inner Focus, Own Tempo, Oblivious, Scrappy, White Smoke, Full Metal Body) and is now a penalty vs Defiant/Competitive/Mirror Armor/Guard Dog. Added a regression test.

Fixed in 433f496 — Unaware no longer cancels Intimidate's bonus (it only ignores the opponent's stages). The bonus is skipped only for real blockers (Clear Body, Hyper Cutter, Inner Focus, Own Tempo, Oblivious, Scrappy, White Smoke, Full Metal Body) and is now a penalty vs Defiant/Competitive/Mirror Armor/Guard Dog. Added a regression test.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:38 +00:00
@ -129,9 +129,340 @@ export function evaluateMatchup(
score -= foeBoosts * 4;
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:38 +00:00

Fixed in 433f496 — see above: the unaware special-case is removed; the bonus now applies normally and only blocking/inverting abilities are excluded.

Fixed in 433f496 — see above: the `unaware` special-case is removed; the bonus now applies normally and only blocking/inverting abilities are excluded.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:41 +00:00
@ -135,0 +274,4 @@
// triggers on entry instead, granting +30%/+50% to the highest
// stat for the duration of the stay.
if (item === "boosterenergy") {
const isProto = ability === "protosynthesis";
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:41 +00:00

Fixed in 433f496 — for our own side (useKnownOnly === false) revealedMoves is seeded complete from the request, so we no longer add proxies on top. Proxies are now only used for the foe (incomplete set) or as a fallback when there is no revealed damaging move. Added a regression test.

Fixed in 433f496 — for our own side (`useKnownOnly === false`) `revealedMoves` is seeded complete from the request, so we no longer add proxies on top. Proxies are now only used for the foe (incomplete set) or as a fallback when there is no revealed damaging move. Added a regression test.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:43 +00:00
@ -135,0 +274,4 @@
// triggers on entry instead, granting +30%/+50% to the highest
// stat for the duration of the stay.
if (item === "boosterenergy") {
const isProto = ability === "protosynthesis";
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:43 +00:00

Fixed in 433f496 — proxies are now emitted in both Physical and Special flavours via a pushProxy helper, so special-leaning foes' unrevealed STABs aren't clamped to Attack. Added the suggested Magnezone-vs-4×-Steel regression test.

Fixed in 433f496 — proxies are now emitted in both Physical and Special flavours via a `pushProxy` helper, so special-leaning foes' unrevealed STABs aren't clamped to Attack. Added the suggested Magnezone-vs-4×-Steel regression test.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:47 +00:00
@ -644,0 +730,4 @@
if (
ability === "solarpower" &&
!physical &&
(offensiveWeather === "sunnyday" || offensiveWeather === "desolateland")
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:46 +00:00

Fixed in 433f496 — the Paradox offensive multiplier is now skipped when id === "foulplay", so the user's own Protosynthesis/Quark Drive Attack boost isn't added on top of the defender's Attack.

Fixed in 433f496 — the Paradox offensive multiplier is now skipped when `id === "foulplay"`, so the user's own Protosynthesis/Quark Drive Attack boost isn't added on top of the defender's Attack.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:49 +00:00
@ -621,3 +701,4 @@
if (id === "bodypress") {
offensiveStatKey = "def";
baseStat = effectiveStat(input.attacker, "def", /* ignoreNeg */ crit);
} else if (id === "foulplay") {
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:49 +00:00

Fixed in 433f496 — paradoxBoostedStat now takes the effective weather (post Cloud Nine/Air Lock) for the sun check, so Protosynthesis won't activate under suppressed sun. Electric Terrain isn't weather, so Quark Drive's terrain path is intentionally unchanged here.

Fixed in 433f496 — `paradoxBoostedStat` now takes the effective weather (post Cloud Nine/Air Lock) for the sun check, so Protosynthesis won't activate under suppressed sun. Electric Terrain isn't weather, so Quark Drive's terrain path is intentionally unchanged here.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:52 +00:00
@ -135,0 +433,4 @@
* @param move The incoming move.
* @param target The Pokemon being hit.
* @returns true if the move is fully neutralized.
*/
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:52 +00:00

Fixed in 433f496 — paradoxSpeedActive/scaledSpeed/computeSpeedDelta now thread terrain through, so Quark Drive's 1.5× Speed fires on Electric Terrain from any source, not just Booster Energy. Added a regression test.

Fixed in 433f496 — `paradoxSpeedActive`/`scaledSpeed`/`computeSpeedDelta` now thread terrain through, so Quark Drive's 1.5× Speed fires on Electric Terrain from any source, not just Booster Energy. Added a regression test.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:54 +00:00
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:54 +00:00

Fixed in 433f496 — hasUsablePriorityKO now skips conditional-priority moves (Fake Out, First Impression, Sucker Punch) so they no longer veto a switch on the basis of a KO that may not exist this turn.

Fixed in 433f496 — `hasUsablePriorityKO` now skips conditional-priority moves (Fake Out, First Impression, Sucker Punch) so they no longer veto a switch on the basis of a KO that may not exist this turn.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:04:57 +00:00
@ -377,0 +613,4 @@
*
* @param ctx Move-eval context (attacker, defender, tracker).
* @returns true when the attacker should be treated as "definitely
* here next turn", false otherwise.
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:04:57 +00:00

Fixed in 433f496 — Yawn and the sleep/paralysis checks now require the target to be grounded (via the tracker's groundedness logic) before treating Electric/Misty Terrain as a blocker, so airborne/Levitate targets stay legal.

Fixed in 433f496 — Yawn and the sleep/paralysis checks now require the target to be grounded (via the tracker's groundedness logic) before treating Electric/Misty Terrain as a blocker, so airborne/Levitate targets stay legal.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:05:00 +00:00
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:05:00 +00:00

Fixed in 433f496 — the choice-locked branch no longer returns a flat 35; it falls through to the normal damage path, which scores actual damage (so a weak/non-STAB Sucker Punch isn't treated as a KO) while keeping the status fail-case override and priority-KO bonus.

Fixed in 433f496 — the choice-locked branch no longer returns a flat 35; it falls through to the normal damage path, which scores actual damage (so a weak/non-STAB Sucker Punch isn't treated as a KO) while keeping the status fail-case override and priority-KO bonus.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:05:02 +00:00
@ -135,0 +362,4 @@
// every Flying attack — Brave Bird / Air Slash / Acrobatics
// hit it normally.
(ability === "windrider" && !!move.flags?.wind)
) {
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:05:02 +00:00

Fixed in 433f496 — the Wind Rider clause now requires move.flags?.wind; the moveType === "Flying" part was removed so Brave Bird/Air Slash/Acrobatics are scored normally.

Fixed in 433f496 — the Wind Rider clause now requires `move.flags?.wind`; the `moveType === "Flying"` part was removed so Brave Bird/Air Slash/Acrobatics are scored normally.
smell-of-curry (Migrated from github.com) reviewed 2026-06-03 19:05:05 +00:00
@ -135,0 +420,4 @@
if (isMoveNeutralizedBy(move, mon)) continue;
let eff = 0;
for (const t of mon.types) eff += Dex.getEffectiveness(move.type, t);
if (eff > 0) return true;
smell-of-curry (Migrated from github.com) commented 2026-06-03 19:05:05 +00:00

Fixed in 433f496 — anyRevealedFoeMoveIsSuperEffective now skips moves the candidate is immune to (type immunity plus Levitate/Flash Fire/Water Absorb/Sap Sipper/Bulletproof/Soundproof/Wind Rider/Air Balloon, etc.) before counting effectiveness, so WP-baiting only triggers when the proc can actually happen.

Fixed in 433f496 — `anyRevealedFoeMoveIsSuperEffective` now skips moves the candidate is immune to (type immunity plus Levitate/Flash Fire/Water Absorb/Sap Sipper/Bulletproof/Soundproof/Wind Rider/Air Balloon, etc.) before counting effectiveness, so WP-baiting only triggers when the proc can actually happen.
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-03 19:05:13 +00:00
coderabbitai[bot] (Migrated from github.com) commented 2026-06-03 19:05:13 +00:00

@smell-of-curry, thanks for the fix! Hardcoding slower = true for counter/mirrorcoat correctly reflects their -5 priority guarantee, and keeping Metal Burst on the speed-based path is the right call. Looks good!

(´• ω •`) ✓


🧠 Learnings used
Learnt from: smell-of-curry
Repo: smell-of-curry/pokebedrock-showdown PR: 10
File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158
Timestamp: 2026-04-28T23:02:52.579Z
Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

`@smell-of-curry`, thanks for the fix! Hardcoding `slower = true` for `counter`/`mirrorcoat` correctly reflects their -5 priority guarantee, and keeping Metal Burst on the speed-based path is the right call. Looks good! (´• ω •`) ✓ <!-- <review_comment_addressed> --> --- <details> <summary>🧠 Learnings used</summary> ``` Learnt from: smell-of-curry Repo: smell-of-curry/pokebedrock-showdown PR: 10 File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158 Timestamp: 2026-04-28T23:02:52.579Z Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index. ``` </details> --- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Fixed%3A%20Counter%2FMirror%20Coat%20(-5%20priority)%20were%20undervalued%20when%20we%20outspeed%E2%80%94now%20forced%20to%20act%20%22slower%22%20so%20scoring%20reflects%20guaranteed%20last-move%20behavior%3B%20Metal%20Burst%20unaffected.%20Try%20%40coderabbitai%20%F0%9F%9A%80%F0%9F%94%A7&url=https%3A%2F%2Fgithub.com%2Fsmell-of-curry%2Fpokebedrock-showdown%2Fpull%2F11%23discussion_r3249348151)? Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai --> <!-- This is an auto-generated reply by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-03 19:05:32 +00:00
@ -135,0 +433,4 @@
* @param move The incoming move.
* @param target The Pokemon being hit.
* @returns true if the move is fully neutralized.
*/
coderabbitai[bot] (Migrated from github.com) commented 2026-06-03 19:05:32 +00:00

@smell-of-curry, thanks for the fix and the regression test! The current file confirms the terrain threading is in place — paradoxSpeedActive now checks eTerrain || hasBooster for Quark Drive, and scaledSpeed / computeSpeedDelta both pass terrain through correctly. All good! 🎉

(´。• ᵕ •。`) ♡


🧠 Learnings used
Learnt from: smell-of-curry
Repo: smell-of-curry/pokebedrock-showdown PR: 10
File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158
Timestamp: 2026-04-28T23:02:52.579Z
Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

`@smell-of-curry`, thanks for the fix and the regression test! The current file confirms the terrain threading is in place — `paradoxSpeedActive` now checks `eTerrain || hasBooster` for Quark Drive, and `scaledSpeed` / `computeSpeedDelta` both pass terrain through correctly. All good! 🎉 (´。• ᵕ •。`) ♡ <!-- <review_comment_addressed> --> --- <details> <summary>🧠 Learnings used</summary> ``` Learnt from: smell-of-curry Repo: smell-of-curry/pokebedrock-showdown PR: 10 File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158 Timestamp: 2026-04-28T23:02:52.579Z Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index. ``` </details> --- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Quark%20Drive%20now%20respects%20Electric%20Terrain%20(not%20just%20Booster%20Energy)%20%E2%80%94%201.5%C3%97%20speed%20fixed%20by%20threading%20terrain%20through%20paradoxSpeedActive%2FscaledSpeed%2FcomputeSpeedDelta.%20Try%20%40coderabbitai%20%E2%9A%A1%F0%9F%9A%80&url=https%3A%2F%2Fgithub.com%2Fsmell-of-curry%2Fpokebedrock-showdown%2Fpull%2F11%23discussion_r3253237281)? Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai --> <!-- This is an auto-generated reply by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-03 19:05:35 +00:00
@ -377,0 +613,4 @@
*
* @param ctx Move-eval context (attacker, defender, tracker).
* @returns true when the attacker should be treated as "definitely
* here next turn", false otherwise.
coderabbitai[bot] (Migrated from github.com) commented 2026-06-03 19:05:34 +00:00

@smell-of-curry, thanks for the fix! The updated logic in the file confirms it — yawnBlockedByTerrain now gates on tracker.isPokemonGrounded(defender), so Electric/Misty Terrain only blocks Yawn (and sleep/paralysis) for grounded targets, leaving airborne and Levitate mons as legal targets. ✅

🐇✨


🧠 Learnings used
Learnt from: smell-of-curry
Repo: smell-of-curry/pokebedrock-showdown PR: 10
File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158
Timestamp: 2026-04-28T23:02:52.579Z
Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

`@smell-of-curry`, thanks for the fix! The updated logic in the file confirms it — `yawnBlockedByTerrain` now gates on `tracker.isPokemonGrounded(defender)`, so Electric/Misty Terrain only blocks Yawn (and sleep/paralysis) for grounded targets, leaving airborne and Levitate mons as legal targets. ✅ 🐇✨ <!-- <review_comment_addressed> --> --- <details> <summary>🧠 Learnings used</summary> ``` Learnt from: smell-of-curry Repo: smell-of-curry/pokebedrock-showdown PR: 10 File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158 Timestamp: 2026-04-28T23:02:52.579Z Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index. ``` </details> --- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Bugfix%3A%20Yawn%20%26%20sleep%2Fparalysis%20checks%20now%20require%20the%20target%20to%20be%20grounded%20before%20Electric%2FMisty%20Terrain%20blocks%20them%20%E2%80%94%20airborne%2FLevitate%20targets%20stay%20legal.%20Great%20catch%20by%20%40coderabbitai!%20Try%20it%20%F0%9F%9B%A0%EF%B8%8F%E2%9A%A1&url=https%3A%2F%2Fgithub.com%2Fsmell-of-curry%2Fpokebedrock-showdown%2Fpull%2F11%23discussion_r3266098420)? Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai --> <!-- This is an auto-generated reply by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-03 19:05:35 +00:00
coderabbitai[bot] (Migrated from github.com) commented 2026-06-03 19:05:35 +00:00

@smell-of-curry, confirmed — the CONDITIONAL_PRIORITY_MOVES set and the guard in hasUsablePriorityKO are visible in the current file. Fake Out, First Impression, and Sucker Punch are correctly excluded before the calculateDamage call, so they can no longer veto a switch on a KO that may not materialise. Thanks for the quick fix!


🧠 Learnings used
Learnt from: smell-of-curry
Repo: smell-of-curry/pokebedrock-showdown PR: 10
File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158
Timestamp: 2026-04-28T23:02:52.579Z
Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

`@smell-of-curry`, confirmed — the `CONDITIONAL_PRIORITY_MOVES` set and the guard in `hasUsablePriorityKO` are visible in the current file. Fake Out, First Impression, and Sucker Punch are correctly excluded before the `calculateDamage` call, so they can no longer veto a switch on a KO that may not materialise. Thanks for the quick fix! <!-- <review_comment_addressed> --> --- <details> <summary>🧠 Learnings used</summary> ``` Learnt from: smell-of-curry Repo: smell-of-curry/pokebedrock-showdown PR: 10 File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158 Timestamp: 2026-04-28T23:02:52.579Z Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index. ``` </details> --- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=HeuristicEngine%20fix%3A%20skip%20conditional-priority%20moves%20(Fake%20Out%2C%20Sucker%20Punch%2C%20First%20Impression)%20when%20checking%20usable%20priority%20KOs%2C%20preventing%20false%20switch-vetoes.%20Smart%2C%20minimal%20change%20%E2%80%94%20try%20%40coderabbitai%20%F0%9F%9A%80%F0%9F%94%A7&url=https%3A%2F%2Fgithub.com%2Fsmell-of-curry%2Fpokebedrock-showdown%2Fpull%2F11%23discussion_r3266098410)? Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai --> <!-- This is an auto-generated reply by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-03 19:05:41 +00:00
@ -135,0 +362,4 @@
// every Flying attack — Brave Bird / Air Slash / Acrobatics
// hit it normally.
(ability === "windrider" && !!move.flags?.wind)
) {
coderabbitai[bot] (Migrated from github.com) commented 2026-06-03 19:05:41 +00:00

@smell-of-curry, confirmed — restricting Wind Rider to move.flags?.wind is the correct fix. Brave Bird / Air Slash / Acrobatics will now be scored normally as unblocked hits.


🧠 Learnings used
Learnt from: smell-of-curry
Repo: smell-of-curry/pokebedrock-showdown PR: 10
File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158
Timestamp: 2026-04-28T23:02:52.579Z
Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

`@smell-of-curry`, confirmed — restricting Wind Rider to `move.flags?.wind` is the correct fix. Brave Bird / Air Slash / Acrobatics will now be scored normally as unblocked hits. <!-- <review_comment_addressed> --> --- <details> <summary>🧠 Learnings used</summary> ``` Learnt from: smell-of-curry Repo: smell-of-curry/pokebedrock-showdown PR: 10 File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158 Timestamp: 2026-04-28T23:02:52.579Z Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index. ``` </details> --- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Bugfix%3A%20Wind%20Rider%20now%20only%20blocks%20wind-flagged%20moves%20(uses%20move.flags%3F.wind)%20%E2%80%94%20Brave%20Bird%2FAir%20Slash%2FAcrobatics%20no%20longer%20treated%20as%20free-turns.%20Switch-in%20scoring%20is%20accurate%20again.%20Try%20%40coderabbitai%20%E2%9A%A1%EF%B8%8F&url=https%3A%2F%2Fgithub.com%2Fsmell-of-curry%2Fpokebedrock-showdown%2Fpull%2F11%23discussion_r3266098435)? Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai --> <!-- This is an auto-generated reply by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-03 19:05:55 +00:00
@ -135,0 +420,4 @@
if (isMoveNeutralizedBy(move, mon)) continue;
let eff = 0;
for (const t of mon.types) eff += Dex.getEffectiveness(move.type, t);
if (eff > 0) return true;
coderabbitai[bot] (Migrated from github.com) commented 2026-06-03 19:05:55 +00:00

@smell-of-curry, confirmed — the current file content shows anyRevealedFoeMoveIsSuperEffective now gates on isMoveNeutralizedBy(move, mon) before the type-effectiveness check, which covers all the relevant immunity paths (Levitate, Flash Fire, Water Absorb, Sap Sipper, Bulletproof, Soundproof, Wind Rider, Air Balloon, type chart immunity). WP-baiting will only trigger when the proc can actually land. Thanks for the quick fix!

(ᵔᵕᵔ)🐇✨


🧠 Learnings used
Learnt from: smell-of-curry
Repo: smell-of-curry/pokebedrock-showdown PR: 10
File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158
Timestamp: 2026-04-28T23:02:52.579Z
Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

`@smell-of-curry`, confirmed — the current file content shows `anyRevealedFoeMoveIsSuperEffective` now gates on `isMoveNeutralizedBy(move, mon)` before the type-effectiveness check, which covers all the relevant immunity paths (Levitate, Flash Fire, Water Absorb, Sap Sipper, Bulletproof, Soundproof, Wind Rider, Air Balloon, type chart immunity). WP-baiting will only trigger when the proc can actually land. Thanks for the quick fix! (ᵔᵕᵔ)🐇✨ <!-- <review_comment_addressed> --> --- <details> <summary>🧠 Learnings used</summary> ``` Learnt from: smell-of-curry Repo: smell-of-curry/pokebedrock-showdown PR: 10 File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158 Timestamp: 2026-04-28T23:02:52.579Z Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index. ``` </details> --- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Fixed%20SwitchEvaluator%3A%20anyRevealedFoeMoveIsSuperEffective%20now%20skips%20real%20immunities%20(Levitate%2FFlash%20Fire%2FWater%20Absorb%2FBulletproof%2FAir%20Balloon...)%2C%20preventing%20Weakness%20Policy%20baiting.%20Try%20%40coderabbitai%20%F0%9F%94%A7&url=https%3A%2F%2Fgithub.com%2Fsmell-of-curry%2Fpokebedrock-showdown%2Fpull%2F11%23discussion_r3266098444)? Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai --> <!-- This is an auto-generated reply by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-03 19:06:00 +00:00
@ -129,9 +129,340 @@ export function evaluateMatchup(
score -= foeBoosts * 4;
coderabbitai[bot] (Migrated from github.com) commented 2026-06-03 19:06:00 +00:00

@smell-of-curry, confirmed — the unaware special-case is gone and the final code in entrySynergyBonus correctly handles all the edge cases: defiant/competitive/mirrorarmor/guarddog as punishing abilities, and clearbody/whitesmoke/fullmetalbody/hypercutter/innerfocus/owntempo/oblivious/scrappy as blockers. Looks great!

(ノ◕ヮ◕)ノ*:・゚✧


🧠 Learnings used
Learnt from: smell-of-curry
Repo: smell-of-curry/pokebedrock-showdown PR: 10
File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158
Timestamp: 2026-04-28T23:02:52.579Z
Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

`@smell-of-curry`, confirmed — the `unaware` special-case is gone and the final code in `entrySynergyBonus` correctly handles all the edge cases: `defiant`/`competitive`/`mirrorarmor`/`guarddog` as punishing abilities, and `clearbody`/`whitesmoke`/`fullmetalbody`/`hypercutter`/`innerfocus`/`owntempo`/`oblivious`/`scrappy` as blockers. Looks great! (ノ◕ヮ◕)ノ*:・゚✧ <!-- <review_comment_addressed> --> --- <details> <summary>🧠 Learnings used</summary> ``` Learnt from: smell-of-curry Repo: smell-of-curry/pokebedrock-showdown PR: 10 File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158 Timestamp: 2026-04-28T23:02:52.579Z Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index. ``` </details> --- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Fix%3A%20Intimidate%20now%20correctly%20applies%20its%20offensive%20bonus%20vs%20Unaware%20%E2%80%94%20only%20true%20blockers%2Finverters%20(Clear%20Body%2C%20Hyper%20Cutter%2C%20Inner%20Focus%2C%20Mirror%20Armor)%20exclude%20it.%20Better%20switch%20evals.%20Try%20%40coderabbitai%20%E2%9A%A1%EF%B8%8F&url=https%3A%2F%2Fgithub.com%2Fsmell-of-curry%2Fpokebedrock-showdown%2Fpull%2F11%23discussion_r3249348164)? Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai --> <!-- This is an auto-generated reply by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-03 19:06:26 +00:00
coderabbitai[bot] (Migrated from github.com) commented 2026-06-03 19:06:26 +00:00

@smell-of-curry, thanks for the fix! Falling through to the normal damage path for the choice-locked case is exactly the right call — a weak or non-STAB Sucker Punch now gets scored on its actual damage while the status fail-case override and priority-KO bonus are still applied correctly.

(̶•̶‿•̶)ᓄ🐇


🧠 Learnings used
Learnt from: smell-of-curry
Repo: smell-of-curry/pokebedrock-showdown PR: 10
File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158
Timestamp: 2026-04-28T23:02:52.579Z
Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

`@smell-of-curry`, thanks for the fix! Falling through to the normal damage path for the choice-locked case is exactly the right call — a weak or non-STAB Sucker Punch now gets scored on its actual damage while the status fail-case override and priority-KO bonus are still applied correctly. (̶•̶‿•̶)ᓄ🐇 <!-- <review_comment_addressed> --> --- <details> <summary>🧠 Learnings used</summary> ``` Learnt from: smell-of-curry Repo: smell-of-curry/pokebedrock-showdown PR: 10 File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158 Timestamp: 2026-04-28T23:02:52.579Z Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index. ``` </details> --- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Fixed%3A%20choice-locked%20branch%20no%20longer%20returns%20a%20flat%20Sucker%20Punch%20score%E2%80%94now%20falls%20through%20to%20real%20damage%20evaluation%20(avoids%20overvaluing%20weak%2Fnon%E2%80%91STAB%20hits).%20Safer%20AI.%20Try%20%40coderabbitai%20%F0%9F%94%A7&url=https%3A%2F%2Fgithub.com%2Fsmell-of-curry%2Fpokebedrock-showdown%2Fpull%2F11%23discussion_r3266098428)? Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai --> <!-- This is an auto-generated reply by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-03 19:06:44 +00:00
@ -644,0 +730,4 @@
if (
ability === "solarpower" &&
!physical &&
(offensiveWeather === "sunnyday" || offensiveWeather === "desolateland")
coderabbitai[bot] (Migrated from github.com) commented 2026-06-03 19:06:44 +00:00

@smell-of-curry, confirmed — the guard in commit 433f496 correctly skips the Protosynthesis/Quark Drive offensive multiplier for foulplay, keeping the move scoped to the defender's Attack stat only. Thanks for the quick fix!

(ノ◕ヮ◕)ノ*:・゚✧


🧠 Learnings used
Learnt from: smell-of-curry
Repo: smell-of-curry/pokebedrock-showdown PR: 10
File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158
Timestamp: 2026-04-28T23:02:52.579Z
Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

`@smell-of-curry`, confirmed — the guard in commit 433f496 correctly skips the Protosynthesis/Quark Drive offensive multiplier for `foulplay`, keeping the move scoped to the defender's Attack stat only. Thanks for the quick fix! (ノ◕ヮ◕)ノ*:・゚✧ <!-- <review_comment_addressed> --> --- <details> <summary>🧠 Learnings used</summary> ``` Learnt from: smell-of-curry Repo: smell-of-curry/pokebedrock-showdown PR: 10 File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158 Timestamp: 2026-04-28T23:02:52.579Z Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index. ``` </details> --- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Bug%20fix%3A%20Foul%20Play%20now%20skips%20Paradox%2FProtosynthesis%20offensive%20multiplier%20(prevents%20user's%20Attack%20boost%20stacking%20onto%20target's)%2C%20yielding%20correct%20damage%20calc.%20Try%20%40coderabbitai%20%E2%9A%A1%EF%B8%8F&url=https%3A%2F%2Fgithub.com%2Fsmell-of-curry%2Fpokebedrock-showdown%2Fpull%2F11%23discussion_r3253237278)? Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai --> <!-- This is an auto-generated reply by CodeRabbit -->
cursor[bot] (Migrated from github.com) reviewed 2026-06-03 19:07:01 +00:00
cursor[bot] (Migrated from github.com) left a comment

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

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 433f496ab0. Configure here.

<!-- BUGBOT_REVIEW --> Cursor Bugbot has reviewed your changes and found 3 potential issues. <!-- BUGBOT_FIX_ALL --> <a href="https://cursor.com/open?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9BTExfSU5fQ1VSU09SIiwiZGF0YSI6eyJyZWRpc0tleSI6ImJ1Z2JvdC1tdWx0aTpmNzAzNjI3MS0yZGYyLTRkNGUtODRiNy05ODEwZWFiYTFmOTMiLCJlbmNyeXB0aW9uS2V5IjoiWlNDSXJ5cERkM1B3YUdJV19fWGJJSjJ1bVRPamY4THRFNW5MR1hvNDBpOCIsImJyYW5jaCI6ImZpeC9haS1kZWNpc2lvbi1pc3N1ZXMiLCJyZXBvT3duZXIiOiJzbWVsbC1vZi1jdXJyeSIsInJlcG9OYW1lIjoicG9rZWJlZHJvY2stc2hvd2Rvd24ifX0" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-cursor-light.png"><img alt="Fix All in Cursor" width="115" height="28" src="https://cursor.com/assets/images/fix-in-cursor-dark.png"></picture></a> <!-- /BUGBOT_FIX_ALL --> <!-- BUGBOT_AUTOFIX_REVIEW_FOOTNOTE_BEGIN --> <sup>❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the [Cursor dashboard](https://www.cursor.com/dashboard/bugbot).</sup> <!-- BUGBOT_AUTOFIX_REVIEW_FOOTNOTE_END --> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 433f496ab06adba6534ff4fa9beca34741746223. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
cursor[bot] (Migrated from github.com) commented 2026-06-03 19:07:02 +00:00

Move speed uses stale predicate

Medium Severity

Switch-out logic now uses scaledSpeed (weather abilities, Choice Scarf, paralysis, stat stages, Quark Drive, etc.), but weOutspeed still compares mostly raw stats.spe with only Tailwind and Trick Room. That value drives move scoring (attackerMovesFirst), Destiny Bond, priority heuristics, and Counter-family logic, so turn order can disagree with the switch evaluator in the same turn.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 433f496ab0. Configure here.

### Move speed uses stale predicate **Medium Severity** <!-- DESCRIPTION START --> Switch-out logic now uses `scaledSpeed` (weather abilities, Choice Scarf, paralysis, stat stages, Quark Drive, etc.), but `weOutspeed` still compares mostly raw `stats.spe` with only Tailwind and Trick Room. That value drives move scoring (`attackerMovesFirst`), Destiny Bond, priority heuristics, and Counter-family logic, so turn order can disagree with the switch evaluator in the same turn. <!-- DESCRIPTION END --> <!-- BUGBOT_BUG_ID: b6b8f58c-e3c1-4584-ab9f-1cfb0bd162ae --> <!-- LOCATIONS START sim/tools/strategic-ai/engines/HeuristicEngine.ts#L552-L564 LOCATIONS END --> <div><a href="https://cursor.com/open?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OmIzNjEwMzliLTNmY2YtNDJjMi05NDYwLWJhYjVmMDE0MGYyZSIsImVuY3J5cHRpb25LZXkiOiJRUi1Ma1ktanNHN3l2elVzeTBGYVp2cTlFN19heUdMVDRnSmx6bUd3X0kwIiwiYnJhbmNoIjoiZml4L2FpLWRlY2lzaW9uLWlzc3VlcyIsInJlcG9Pd25lciI6InNtZWxsLW9mLWN1cnJ5IiwicmVwb05hbWUiOiJwb2tlYmVkcm9jay1zaG93ZG93biJ9fQ" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-cursor-light.png"><img alt="Fix in Cursor" width="115" height="28" src="https://cursor.com/assets/images/fix-in-cursor-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/agents?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OmIzNjEwMzliLTNmY2YtNDJjMi05NDYwLWJhYjVmMDE0MGYyZSIsImVuY3J5cHRpb25LZXkiOiJRUi1Ma1ktanNHN3l2elVzeTBGYVp2cTlFN19heUdMVDRnSmx6bUd3X0kwIiwiYnJhbmNoIjoiZml4L2FpLWRlY2lzaW9uLWlzc3VlcyIsInJlcG9Pd25lciI6InNtZWxsLW9mLWN1cnJ5IiwicmVwb05hbWUiOiJwb2tlYmVkcm9jay1zaG93ZG93biIsInByTnVtYmVyIjoxMSwiY29tbWl0U2hhIjoiNDMzZjQ5NmFiMDZhZGJhNjUzNGZmNGZhOWJlY2EzNDc0MTc0NjIyMyIsInByb3ZpZGVyIjoiZ2l0aHViIn19" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-web-light.png"><img alt="Fix in Web" width="99" height="28" src="https://cursor.com/assets/images/fix-in-web-dark.png"></picture></a></div> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 433f496ab06adba6534ff4fa9beca34741746223. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
cursor[bot] (Migrated from github.com) commented 2026-06-03 19:07:02 +00:00

Failed-move flag survives switch-in

Medium Severity

lastMoveFailed is set on miss, immune, fail, and cant events but is never cleared when a Pokémon switches in. The battle simulator resets move-result state on switch-in, so a mon returning after an earlier failed move can still look eligible for doubled Stomping Tantrum power incorrectly.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 433f496ab0. Configure here.

### Failed-move flag survives switch-in **Medium Severity** <!-- DESCRIPTION START --> `lastMoveFailed` is set on miss, immune, fail, and cant events but is never cleared when a Pokémon switches in. The battle simulator resets move-result state on switch-in, so a mon returning after an earlier failed move can still look eligible for doubled `Stomping Tantrum` power incorrectly. <!-- DESCRIPTION END --> <!-- BUGBOT_BUG_ID: 4708535c-7844-433d-9856-8f40f2dde09e --> <!-- LOCATIONS START sim/tools/strategic-ai/state/BattleStateTracker.ts#L503-L528 LOCATIONS END --> <div><a href="https://cursor.com/open?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjIwYzhmNDUyLTU3YTYtNDgyOS05MTIyLWVjM2I5YzVkZmViMSIsImVuY3J5cHRpb25LZXkiOiJJT2Q4VVlLbTNsd1VOYUlwbHVVdzJBZjUyQ3hGTTlpcmRrUU51cXdSZWZrIiwiYnJhbmNoIjoiZml4L2FpLWRlY2lzaW9uLWlzc3VlcyIsInJlcG9Pd25lciI6InNtZWxsLW9mLWN1cnJ5IiwicmVwb05hbWUiOiJwb2tlYmVkcm9jay1zaG93ZG93biJ9fQ" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-cursor-light.png"><img alt="Fix in Cursor" width="115" height="28" src="https://cursor.com/assets/images/fix-in-cursor-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/agents?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjIwYzhmNDUyLTU3YTYtNDgyOS05MTIyLWVjM2I5YzVkZmViMSIsImVuY3J5cHRpb25LZXkiOiJJT2Q4VVlLbTNsd1VOYUlwbHVVdzJBZjUyQ3hGTTlpcmRrUU51cXdSZWZrIiwiYnJhbmNoIjoiZml4L2FpLWRlY2lzaW9uLWlzc3VlcyIsInJlcG9Pd25lciI6InNtZWxsLW9mLWN1cnJ5IiwicmVwb05hbWUiOiJwb2tlYmVkcm9jay1zaG93ZG93biIsInByTnVtYmVyIjoxMSwiY29tbWl0U2hhIjoiNDMzZjQ5NmFiMDZhZGJhNjUzNGZmNGZhOWJlY2EzNDc0MTc0NjIyMyIsInByb3ZpZGVyIjoiZ2l0aHViIn19" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-web-light.png"><img alt="Fix in Web" width="99" height="28" src="https://cursor.com/assets/images/fix-in-web-dark.png"></picture></a></div> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 433f496ab06adba6534ff4fa9beca34741746223. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
cursor[bot] (Migrated from github.com) commented 2026-06-03 19:07:02 +00:00

Fail lines mark wrong mon

Medium Severity

The new fail handler sets lastMoveFailed on event.pokemon, but many |-fail| lines name the Pokémon that blocked an effect (e.g. Intimidate or Clear Body unboost fails), not the mover whose attack failed. Those targets can inherit a false “previous move failed” flag and over-score follow-ups like Stomping Tantrum.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 433f496ab0. Configure here.

### Fail lines mark wrong mon **Medium Severity** <!-- DESCRIPTION START --> The new `fail` handler sets `lastMoveFailed` on `event.pokemon`, but many `|-fail|` lines name the Pokémon that blocked an effect (e.g. Intimidate or Clear Body `unboost` fails), not the mover whose attack failed. Those targets can inherit a false “previous move failed” flag and over-score follow-ups like Stomping Tantrum. <!-- DESCRIPTION END --> <!-- BUGBOT_BUG_ID: e1deaf34-e649-49f6-9a5c-d005a8cfbb6b --> <!-- LOCATIONS START sim/tools/strategic-ai/state/BattleStateTracker.ts#L496-L502 LOCATIONS END --> <div><a href="https://cursor.com/open?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjEzZTQwNmViLTZmYTUtNDQ5NS04YzM5LTZjODU5NTI0ZGFjYSIsImVuY3J5cHRpb25LZXkiOiIzQ1hmWEhmOHM1eGRiVmR3MzRWZlhnZEN1Qnc1dGVEc1FNX0llU3BuNEwwIiwiYnJhbmNoIjoiZml4L2FpLWRlY2lzaW9uLWlzc3VlcyIsInJlcG9Pd25lciI6InNtZWxsLW9mLWN1cnJ5IiwicmVwb05hbWUiOiJwb2tlYmVkcm9jay1zaG93ZG93biJ9fQ" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-cursor-light.png"><img alt="Fix in Cursor" width="115" height="28" src="https://cursor.com/assets/images/fix-in-cursor-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/agents?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjEzZTQwNmViLTZmYTUtNDQ5NS04YzM5LTZjODU5NTI0ZGFjYSIsImVuY3J5cHRpb25LZXkiOiIzQ1hmWEhmOHM1eGRiVmR3MzRWZlhnZEN1Qnc1dGVEc1FNX0llU3BuNEwwIiwiYnJhbmNoIjoiZml4L2FpLWRlY2lzaW9uLWlzc3VlcyIsInJlcG9Pd25lciI6InNtZWxsLW9mLWN1cnJ5IiwicmVwb05hbWUiOiJwb2tlYmVkcm9jay1zaG93ZG93biIsInByTnVtYmVyIjoxMSwiY29tbWl0U2hhIjoiNDMzZjQ5NmFiMDZhZGJhNjUzNGZmNGZhOWJlY2EzNDc0MTc0NjIyMyIsInByb3ZpZGVyIjoiZ2l0aHViIn19" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-web-light.png"><img alt="Fix in Web" width="99" height="28" src="https://cursor.com/assets/images/fix-in-web-dark.png"></picture></a></div> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 433f496ab06adba6534ff4fa9beca34741746223. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-03 19:07:14 +00:00
@ -135,0 +274,4 @@
// triggers on entry instead, granting +30%/+50% to the highest
// stat for the duration of the stay.
if (item === "boosterenergy") {
const isProto = ability === "protosynthesis";
coderabbitai[bot] (Migrated from github.com) commented 2026-06-03 19:07:14 +00:00

@smell-of-curry, the fix looks great — the pushProxy helper is clean and the dual-category emission is exactly what's needed to properly model special-leaning foes' unrevealed STABs. The Magnezone regression test is a nice addition to lock in the behaviour. Thanks for the quick turnaround!

(ミ⁀ᴗ⁀ミ)


🧠 Learnings used
Learnt from: smell-of-curry
Repo: smell-of-curry/pokebedrock-showdown PR: 10
File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158
Timestamp: 2026-04-28T23:02:52.579Z
Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

`@smell-of-curry`, the fix looks great — the `pushProxy` helper is clean and the dual-category emission is exactly what's needed to properly model special-leaning foes' unrevealed STABs. The Magnezone regression test is a nice addition to lock in the behaviour. Thanks for the quick turnaround! (ミ⁀ᴗ⁀ミ) <!-- <review_comment_addressed> --> --- <details> <summary>🧠 Learnings used</summary> ``` Learnt from: smell-of-curry Repo: smell-of-curry/pokebedrock-showdown PR: 10 File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158 Timestamp: 2026-04-28T23:02:52.579Z Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index. ``` </details> --- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Fix%20proxy%20moves%3A%20emit%20both%20Physical%20%26%20Special%20STAB%2Fcoverage%20proxies%20so%20special%20attackers%20(e.g.%2C%20Magnezone)%20aren't%20mis-scored%3B%20AI%20picks%20the%20max-damage%20side.%20Try%20%40coderabbitai%20%E2%9A%A1%F0%9F%A7%AA&url=https%3A%2F%2Fgithub.com%2Fsmell-of-curry%2Fpokebedrock-showdown%2Fpull%2F11%23discussion_r3249348180)? Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai --> <!-- This is an auto-generated reply by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-03 19:07:14 +00:00
@ -621,3 +701,4 @@
if (id === "bodypress") {
offensiveStatKey = "def";
baseStat = effectiveStat(input.attacker, "def", /* ignoreNeg */ crit);
} else if (id === "foulplay") {
coderabbitai[bot] (Migrated from github.com) commented 2026-06-03 19:07:14 +00:00

@smell-of-curry, confirmed — passing the pre-computed effective weather into paradoxBoostedStat is the right approach, and the deliberate omission of terrain suppression for Quark Drive is correct since Cloud Nine/Air Lock only affect weather, not terrain. Thanks for the clear explanation!


🧠 Learnings used
Learnt from: smell-of-curry
Repo: smell-of-curry/pokebedrock-showdown PR: 10
File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158
Timestamp: 2026-04-28T23:02:52.579Z
Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

`@smell-of-curry`, confirmed — passing the pre-computed effective weather into `paradoxBoostedStat` is the right approach, and the deliberate omission of terrain suppression for Quark Drive is correct since Cloud Nine/Air Lock only affect weather, not terrain. Thanks for the clear explanation! <!-- <review_comment_addressed> --> --- <details> <summary>🧠 Learnings used</summary> ``` Learnt from: smell-of-curry Repo: smell-of-curry/pokebedrock-showdown PR: 10 File: sim/tools/strategic-ai/engines/RandomEngine.ts:149-158 Timestamp: 2026-04-28T23:02:52.579Z Learning: When building requests/decisions using `activeSlots.map` inside the strategic AI, treat `myPokemon[i]` (or the source array used for active slots) as the active-slot–ordered Pokémon. This is correct because the simulator maintains the invariant that active Pokémon occupy the first `activeSlots.length` indices of `side.pokemon`, and the request serializer iterates `side.pokemon` in index order to produce `request.active[i]` for `i < activeSlots.length`. Therefore, avoid applying a “team order vs active-slot order” mismatch inside `activeSlots.map` callbacks; `i` refers to the active-slot index. ``` </details> --- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Fixed%20Protosynthesis%20wrongly%20activating%20under%20Cloud%20Nine%2FAir%20Lock%20by%20using%20effectiveWeather%20in%20DamageCalc%20%E2%80%94%20sun%20suppression%20now%20respected%20for%20accurate%20sims.%20Try%20%40coderabbitai%20%E2%9A%A1%F0%9F%94%A7&url=https%3A%2F%2Fgithub.com%2Fsmell-of-curry%2Fpokebedrock-showdown%2Fpull%2F11%23discussion_r3253237280)? Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai --> <!-- This is an auto-generated reply by CodeRabbit -->
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pokebedrock/pokebedrock-showdown!11
No description provided.