Huge AI Improvements #12
No reviewers
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
pokebedrock/pokebedrock-showdown!12
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "huge-ai-improvements"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stronger Strategic AI: ~65% winrate at d5 vs d3, fix randombattle crash
Summary
This PR overhauls the
strategic-aibattle engine so higher difficulty levels are measurably stronger than lower ones, fixes a randombattle generation crash caused by host-side cosmetic skins, and adds a self-play harness so future AI changes can be validated empirically instead of by feel.Headline numbers (1000-game seeded series,
gen9randombattle, side-swapped):A clean monotonic difficulty gradient, with the top tier now clearly separating from the heuristic core.
What changed
1. Tracker is no longer starving the engines (the biggest single bug)
Before:
BattleStateTrackeronly saw protocol log lines that flowed throughPlayerAI.receiveLine. The PokeBedrock host bypasses that path (it has its own log interpreter), so engines were running blind to weather, hazards, boosts, status, revealed moves, andlastMoveFailed.PlayerAI.ingestLogLine(line)API: feed the tracker without dispatching choices.Battle.forwardLogLineToAIhook inpokebedrock-beh: routes every protocol line to its actor (with|split|privacy preserved).EngineContext.lastMoveFailedByMonnow mirrors tracker state, so e.g. Stomping Tantrum doubles BP correctly after a fizzled Earthquake into Levitate.2. MCTS tier (d5) does real multi-turn search
MctsEnginewas a one-ply sampler in disguise. It now:BattleviatoJSON()/fromJSON()and plays out a few greedy turns against a determinized foe team.3. MCTS now searches over switches, not just moves
MctsEngine.maybeSwitchOutwraps the heuristic switch gate with rollout verification:This was the missing structural piece — switch decisions used to be 100% heuristic regardless of difficulty.
4. Disciplined play at high difficulty
A diagnostic run (d5 with search disabled) only scored 52.4% vs d3, proving the heuristic core was injecting handicaps even at max difficulty:
0.12→0(fully greedy).Lower tiers keep all three for human-feeling variety.
5. Pokerogue-style switch margins replacing a coin flip
The old switch gate was a
(difficulty - 1) / 4coin flip — even an "elite" trainer ignored half their correct switches at d3 and added pure noise at d4–d5. Now: a voluntary switch must beat the current matchup by a difficulty-scaled margin, raised further by aswitchCountcounter that climbs each time we switch and floors back down each time we commit to a move.6. Move selection ladder replacing epsilon-uniform noise
selectByScoreLadderwalks the descending-sorted move list and advances to the next-best with probability proportional to closeness, capped per difficulty. Variety is now strength-preserving: a close second is a frequent pick, a distant third is rare, a negative-scored move is never reached from a positive run.7.
OpponentInferenceupgrades9L36/9M), so e.g. a level-50 Garchomp's prior pool no longer includes moves it can't yet have learned.inferredImmunitiesfield onTrackedPokemon: when a foe's attack is logged as|-immune|against a type the dex didn't predict, the inference layer now uses that to upgrade ability/item guesses (Levitate, Storm Drain, Air Balloon, etc.).lastMoveFailedflag.8. Damage calc gaps closed
9. Mega/Tera matchup-aware transforms
TransformPolicy.considerMeganow compares the current matchup score with the projected mega forme's score and defers transformation when significantly worse — no more "Mega evolved into a 4× weakness because the move score said attack."10. Random-team generator no longer crashes on host-side skins
PokeBedrock event skins (
Torterra-StPatrick,Sableye-Sculk,Mimikyu-Raichu, etc.) are registered ascosmeticFormesbut have no dex entries/aliases. Vanilla Showdown's random team generator deliberately rolls a random cosmetic forme for variety, which previously crashed battle creation:getFormeingen8,gen9, andgen9baby/teams.tsnow filters cosmetic forme samples throughdex.species.get(forme).exists, so unresolvable host skins are silently dropped while real cosmetic formes (e.g.Pikachu-Original,Gastrodon-East) still rotate as before. Errored game count in self-play went from ~6% to 0%.11. New: AI-vs-AI self-play harness
Added
sim/tools/selfplay.ts. Pits twoPlayerAIconfigurations against each other, side-swapped per game, with aworker_threadspool for parallelism (default: cores − 1).Usage:
A 1000-game series now finishes in ~7 min on an M-series MacBook (was ~2h single-threaded). Per-game seeds are pre-derived from the run seed, so results are reproducible regardless of pool size or completion order. Includes a per-game wall-clock watchdog so a single deadlocked battle can't kill the series silently.
What players in
pokebedrock-behwill noticeOnce
pokebedrock-behpicks up the new package, in-game AI behaviour should change visibly:Unidentified speciescrashes when a randombattle-format trainer rolls into a battle that would have previously selected a host skin.The lower tiers were intentionally not hardened; players choosing easier opponents should still see plenty of suboptimal play, just suboptimal in a more intentional way.
Test plan
node buildclean.node dist/sim/tools/selfplay.js --a 5 --b 3 --games 1000 --quiet --seed 1234,5678,9012,3456→ 65.2% (target was ≥65%).Unidentified specieserrors across 1000+ games ofgen9randombattle.pokebedrock-behagainst an Elite Four trainer (visual) — recommended before merge.Risks / follow-ups
Battle.toJSON()/fromJSON()and a host that exposes the live battle to the AI; falls back to the tracker-only sampler when unavailable. (Bedrock host wires this up; the self-play harness does too.)searchBudgetMsstill defaults to 200ms at d5. Increasing to 500ms didn't change winrate within noise — the ceiling is rollout quality, not quantity. Future work: smarter rollout policy (use the real evaluator inside the fork instead of greedy max-BP).Summary by CodeRabbit
Bug Fixes
New Features
Improvements
ℹ️ Recent review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID:
9104939c-9012-43c2-a8d7-ac3f10fd0b9b📥 Commits
Reviewing files that changed from the base of the PR and between 479d35216b09d6658646e6b4b7a65146a3f940d3 and
05d5879e43.⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json📒 Files selected for processing (15)
data/random-battles/gen8/teams.tsdata/random-battles/gen9/teams.tsdata/random-battles/gen9baby/teams.tspackage.jsonsim/tools/player-ai.tssim/tools/selfplay.tssim/tools/strategic-ai/engines/Engine.tssim/tools/strategic-ai/engines/HeuristicEngine.tssim/tools/strategic-ai/engines/MctsEngine.tssim/tools/strategic-ai/mechanics/DamageCalc.tssim/tools/strategic-ai/mechanics/MoveEvaluator.tssim/tools/strategic-ai/mechanics/TransformPolicy.tssim/tools/strategic-ai/policy/DifficultyPolicy.tssim/tools/strategic-ai/state/BattleStateTracker.tssim/tools/strategic-ai/state/OpponentInference.ts📝 Walkthrough
Walkthrough
This PR filters random-battle cosmetic formes, updates strategic AI state, inference, damage, transformation, heuristic, and MCTS behavior, adds PlayerAI log ingestion, and introduces deterministic self-play execution with worker and CLI support.
Changes
Strategic AI runtime, inference, and self-play
data/random-battles/gen8/teams.ts,data/random-battles/gen9/teams.ts,data/random-battles/gen9baby/teams.ts,package.json0.1.21.sim/tools/strategic-ai/engines/Engine.ts,sim/tools/strategic-ai/policy/DifficultyPolicy.tssim/tools/strategic-ai/state/BattleStateTracker.ts,sim/tools/strategic-ai/state/OpponentInference.tssim/tools/player-ai.tssim/tools/strategic-ai/mechanics/MoveEvaluator.ts,sim/tools/strategic-ai/mechanics/DamageCalc.ts,sim/tools/strategic-ai/mechanics/TransformPolicy.tssim/tools/strategic-ai/engines/HeuristicEngine.tssim/tools/strategic-ai/engines/MctsEngine.tssim/tools/selfplay.tssim/tools/selfplay.tsEstimated code review effort: 5 (Critical) | ~120 minutes
Sequence Diagram(s)
Possibly related PRs
lastMoveFailedstate used by this PR’s tracker and decision changes.🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches
📝 Generate docstrings
🧪 Generate unit tests (beta)
huge-ai-improvementsThanks 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 helpto get the list of available commands.@ -604,0 +753,4 @@return;}case "item": {const mon = this.resolveByRef(event.pokemon);CodeQL / Prototype-polluting assignment
This assignment may alter Object.prototype if a malicious 'proto' string is injected from library input.
This assignment may alter Object.prototype if a malicious 'proto' string is injected from library input.
Show more details
@ -604,0 +773,4 @@return;}case "primal": {const mon = this.resolveByRef(event.pokemon);CodeQL / Prototype-polluting assignment
This assignment may alter Object.prototype if a malicious 'proto' string is injected from library input.
This assignment may alter Object.prototype if a malicious 'proto' string is injected from library input.
Show more details
Pull request overview
This PR substantially strengthens the
sim/tools/strategic-aisubsystem by improving battle-state tracking fidelity, making high-difficulty play more consistently “best-move/best-switch” (including deeper MCTS via battle forking), and adding a parallel self-play harness to measure difficulty gradients. It also hardens random-battle team generation against unresolved host cosmetic formes that previously caused battle creation crashes.Changes:
Battleis available.sim/tools/selfplay.tsto run reproducible, parallel AI-vs-AI winrate series; and filtered random cosmetic formes to preventUnidentified speciescrashes.Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
selectByScoreLadderand extendsEngineContextknobs (plus optionalgetBattle).ingestLogLineand tracker-to-engineCtx synchronization, and wires optional live-battle access for MCTS forking.💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
@ -202,6 +416,341 @@ function sampleRollout(return reward;determinizeMovescurrently consultsfoeActive.moveSlots(which contains the battle’s real hidden moves) viapresent = new Set(foeActive.moveSlots.map(...)), and it stops filling whenfillsruns out. Both behaviors can leak full-information into fork rollouts: the determinization is influenced by the true hidden moves, and any remaining unrevealed slots keep their real moves. This breaks the “no full-info cheating” guarantee of the fork search.@ -2,19 +2,25 @@* Strategic-AI difficulty policy.The difficulty/knobs mapping table is now out of sync with the actual knobs: difficulty 5 is documented as ladder cap 0.12, but
knobsForDifficulty(5)returnsladderAdvanceCap: 0(fully greedy). This can mislead future tuning/benchmarking since the comment is treated as the authoritative tier spec.Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID:
08196a42-9d63-4fe4-8622-66e97ddb5e82📥 Commits
Reviewing files that changed from the base of the PR and between
5d45910c73and af190cbba06609c299f18d6dd3a6128e1e140de9.📒 Files selected for processing (14)
data/random-battles/gen8/teams.tsdata/random-battles/gen9/teams.tsdata/random-battles/gen9baby/teams.tssim/tools/player-ai.tssim/tools/selfplay.tssim/tools/strategic-ai/engines/Engine.tssim/tools/strategic-ai/engines/HeuristicEngine.tssim/tools/strategic-ai/engines/MctsEngine.tssim/tools/strategic-ai/mechanics/DamageCalc.tssim/tools/strategic-ai/mechanics/MoveEvaluator.tssim/tools/strategic-ai/mechanics/TransformPolicy.tssim/tools/strategic-ai/policy/DifficultyPolicy.tssim/tools/strategic-ai/state/BattleStateTracker.tssim/tools/strategic-ai/state/OpponentInference.ts@ -0,0 +1,558 @@/**⚠️ Potential issue | 🟠 Major | ⚡ Quick win
Surface
PlayerAI.start()failures instead of converting them into timeout hangs.These are the only promises that can tell this harness an AI loop crashed. Swallowing both rejections means a deterministic failure just sits until the force-tie/hung timers fire, and
playGameWithRetries()can then burn three more full retries on the same broken setup.Suggested direction
🤖 Prompt for AI Agents
✅ Addressed in commits f4568cf to d768907
@ -0,0 +327,4 @@worker.on("message", onMessage);worker.on("error", onError);worker.on("exit", onExit);worker.postMessage(task);⚠️ Potential issue | 🟠 Major | ⚡ Quick win
Handle worker
exitas well aserrorfor in-flight tasks.runOne()never settles if a worker dies before replying but doesn't emiterrorfirst. In that case the lane hangs forever instead of marking the game errored and respawning the worker as intended.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents
✅ Addressed in commits f4568cf to d768907
@ -0,0 +541,4 @@maxTurns: intArg(args["max-turns"], "max-turns", 1),gameTimeoutMs: intArg(args["game-timeout"], "game-timeout", 0),jobs: intArg(args.jobs, "jobs", 1),verbose: !args.quiet,⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
Reject invalid numeric CLI flags up front.
parseIntcan yieldNaNor out-of-range values here (--games foo,--a 9,--max-turns -1, etc.), and those flow straight intorunSelfPlay(). The result is confusing behavior—empty runs, immediate timeouts, or invalid difficulty configs—instead of a clear usage error.🤖 Prompt for AI Agents
✅ Addressed in commits f4568cf to d768907
@ -99,0 +124,4 @@*/interface TurnPlan {/** An earlier slot chose Helping Hand this turn. */helpingHand: boolean;⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift
partnerAttacksis too coarse for Assurance.This boolean only says that an earlier ally picked some damaging move. Assurance doubles only if the same target has already taken damage, so later-slot scoring is inflated whenever the partner attacks the other foe, gets redirected, or commits to a non-matching hit pattern. This needs target-aware turn-plan state, not a single flag.
Also applies to: 330-335, 360-361
🤖 Prompt for AI Agents
⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift
Fork rollouts still read the opponent’s hidden bench.
determinizeMovesonly rewrites the current foe active, but the fork keeps the real unrevealed backline andevaluateForkStatescores everyside.pokemon. After a KO or forced switch, these rollouts can therefore branch on and score the opponent’s actual hidden switch-in/species/HP, which turns the d5 fork path into a full-information search outside self-play. Redact/determinize unrevealed foe bench state before forking, or keep fork evaluation constrained to tracker-visible state only.Also applies to: 711-729
✅ Addressed in commits f4568cf to d768907
@ -503,22 +505,22 @@ function abilityImmunityBlocks(moveType: string, move: Move, input: DamageCalcInconst ability = toID(input.defender.ability);if (move.category === "Status") return false;⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
Normalizenever converts non-Normal moves here.Because this switch only runs inside
if (type === "Normal"), aNormalizeuser firing a non-Normal move keeps its original type. That breaks both effectiveness and the later Normalize BP boost path.Suggested fix
🤖 Prompt for AI Agents
✅ Addressed in commits f4568cf to d768907
⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
Gyro Ballbase power is off by one.The formula is
floor(25 * targetSpe / userSpe)with a minimum of 1, not... + 1. This overstates everyGyro Ballroll.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents
⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
Fix the difficulty-5 tuning table.
Line 17 still documents a
0.12ladder cap and Lines 20-23 describe tier 5 as “near-greedy”, butknobsForDifficulty(5)is now fully greedy withladderAdvanceCap: 0. Leaving the table stale makes future tuning harder to reason about.🤖 Prompt for AI Agents
✅ Addressed in commits f4568cf to d768907
@ -102,7 +109,11 @@ function autoFor(difficulty: number): Exclude<EngineName, "auto"> {*/⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
Overwrite
searchBudgetMsinstead of conditionally preserving it.Line 115 only writes truthy budgets. If this helper is reapplied to a reused
EngineContext, tiers without a budget inherit the previous tier’s value, and a legitimate0budget could never be applied.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents
✅ Addressed in commits f4568cf to d768907
⚠️ Potential issue | 🟠 Major | ⚡ Quick win
Normalize the immunity-ability prior when there is no remainder bucket.
If every legal ability is in
candidates, this branch returns a distribution summing to0.9instead of1.0. That breaks theDistributioncontract and underweights the only legal outcomes for single-ability species.Suggested fix
🤖 Prompt for AI Agents
✅ Addressed in commits f4568cf to d768907
⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift
Key learnset legality off the battle’s actual gen, not
Dex.gen.Line 286 uses the global default dex generation, but the tracker already records the real battle generation. In Gen 8 or older formats this builds Gen 9 move pools and level gates, so the rollout priors are wrong before search even starts.
Also applies to: 329-334
🤖 Prompt for AI Agents
✅ Addressed in commits f4568cf to d768907
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID:
7184dd53-0f60-4f61-946f-d21060dd901f📥 Commits
Reviewing files that changed from the base of the PR and between af190cbba06609c299f18d6dd3a6128e1e140de9 and 479d35216b09d6658646e6b4b7a65146a3f940d3.
📒 Files selected for processing (14)
data/random-battles/gen8/teams.tsdata/random-battles/gen9/teams.tsdata/random-battles/gen9baby/teams.tssim/tools/player-ai.tssim/tools/selfplay.tssim/tools/strategic-ai/engines/Engine.tssim/tools/strategic-ai/engines/HeuristicEngine.tssim/tools/strategic-ai/engines/MctsEngine.tssim/tools/strategic-ai/mechanics/DamageCalc.tssim/tools/strategic-ai/mechanics/MoveEvaluator.tssim/tools/strategic-ai/mechanics/TransformPolicy.tssim/tools/strategic-ai/policy/DifficultyPolicy.tssim/tools/strategic-ai/state/BattleStateTracker.tssim/tools/strategic-ai/state/OpponentInference.ts🚧 Files skipped from review as they are similar to previous changes (11)
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: smell-of-curry/pokebedrock-showdown
Length of output: 3981
🏁 Script executed:
Repository: smell-of-curry/pokebedrock-showdown
Length of output: 5753
Normalize only boosts already-Normal moves
moveType === "Normal"is already the whole check here; the|| trueis dead code. The real issue is thatresolveMoveTypestill leaves non-Normal moves unchanged under Normalize, so the 20% boost never reaches the converted moves. Fix the resolver and drop the tautology.🤖 Prompt for AI Agents
✅ Addressed in commits f4568cf to d768907
@ -604,0 +753,4 @@return;}case "item": {const mon = this.resolveByRef(event.pokemon);False positive.
this.activeis typedRecord<SideId, MonId[]>, so this is an array-element write with an integer index (isValidActivePositionrestricts it to0..2) into the array at aSideIdthatisSideIdvalidates against a strict"p1" | "p2" | "p3" | "p4"allowlist. The written key is a small bounded integer, not an attacker-controlled string, so__proto__/constructorcan never reach the assignment target. No prototype-pollution vector here — resolving.@ -604,0 +773,4 @@return;}case "primal": {const mon = this.resolveByRef(event.pokemon);False positive.
this.activeis typedRecord<SideId, MonId[]>, so this is an array-element write with an integer index (isValidActivePositionrestricts it to0..2) into the array at aSideIdthatisSideIdvalidates against a strict"p1" | "p2" | "p3" | "p4"allowlist. The written key is a small bounded integer, not an attacker-controlled string, so__proto__/constructorcan never reach the assignment target. No prototype-pollution vector here — resolving.@ -2,19 +2,25 @@* Strategic-AI difficulty policy.Fixed in d768907a1 — the tier-5 row now reads ladder cap
0.00and "fully greedy", matchingknobsForDifficulty(5)(ladderAdvanceCap: 0).@ -202,6 +416,341 @@ function sampleRollout(return reward;Partially addressed in d768907a1:
determinizeMovesnow seeds its exclusion set from revealed moves only, so the sampled fills are no longer biased away from the foe's real (hidden) moveset. The deeper concern — fully redacting the unrevealed backline — is tracked on the related thread as a follow-up (see reply there).@ -0,0 +1,558 @@/**Fixed in d768907a1 — a crashed
PlayerAI.start()now surfaces through the gamePromise.raceand is mapped to an erroredGameResult, instead of being swallowed and left to the force-tie/hang timers. A successfulstart()(which resolves at battle end) is deliberately mapped to a never-settling promise so it can't win the race beforeconsume()reads|win|.@ -0,0 +327,4 @@worker.on("message", onMessage);worker.on("error", onError);worker.on("exit", onExit);worker.postMessage(task);Fixed in d768907a1 — added an
exitlistener alongsideerrorthat rejects the in-flight task with a clear message, so a worker dying without emittingerrorerrors the game and lets the lane respawn instead of hanging forever.@ -0,0 +541,4 @@maxTurns: intArg(args["max-turns"], "max-turns", 1),gameTimeoutMs: intArg(args["game-timeout"], "game-timeout", 0),jobs: intArg(args.jobs, "jobs", 1),verbose: !args.quiet,Fixed in d768907a1 — numeric flags now go through a validating
intArg(raw, name, min, max)helper that rejectsNaN/non-integer/out-of-range values with a clear usage error andprocess.exit(1)(difficulty0..5, games/max-turns>= 1, budgets/timeout>= 0).@ -99,0 +124,4 @@*/interface TurnPlan {/** An earlier slot chose Helping Hand this turn. */helpingHand: boolean;Won't fix — this is an intentional heuristic approximation, not a correctness bug. The heuristic engine models the foe side as a single
tracker.foeActivedefender for every slot (defender: foeMon = tracker.foeActive); doubles support here is deliberately coarse, and the precise multi-target search (MCTS) doesn't run doubles at all. Within that single-foe model,partnerAttackscorrectly means "the modeled foe already took damage this turn." A target-awarepartnerAttacksByTargetwould require per-foe-slot modeling this engine doesn't do, and the only impact today is a minor over-valuation of Assurance in doubles. Resolving as a documented limitation.Valid, but a known limitation rather than a quick fix. d768907a1 stops
determinizeMovesfrom leaking the foe active's real moves, but fully honest rollouts require redacting/resampling the entire unrevealed foe team (species/sets/HP), which is a substantial redesign of the fork/eval path. That would change rollout outcomes and invalidate the empirically-tuned win-rate, so it needs its own self-play re-validation. In self-play the leak is symmetric, and MCTS (difficulty 5) is the intended hardest tier. Tracking as a follow-up instead of churning the validated engine in this PR — resolving the thread.@ -503,22 +505,22 @@ function abilityImmunityBlocks(moveType: string, move: Move, input: DamageCalcInconst ability = toID(input.defender.ability);if (move.category === "Status") return false;Fixed in d768907a1 —
resolveMoveTypenow handles Normalize before thetype === "Normal"block (if (ability === "normalize") return "Normal";), so a Normalize user's non-Normal move is correctly converted to Normal for both effectiveness and the BP-boost path.Not a bug — the
+1is correct. Showdown's owngyroballbasePowerCallback(data/moves.ts) isMath.floor(25 * target.getStat('spe') / pokemon.getStat('spe')) + 1, capped at 150, which is exactly what this code computes (userSpeis floored at 1 so the min-1 is implicit). Resolving without change.Fixed in d768907a1 — the tier-5 row now reads ladder cap
0.00and "fully greedy", matchingknobsForDifficulty(5)(ladderAdvanceCap: 0).@ -102,7 +109,11 @@ function autoFor(difficulty: number): Exclude<EngineName, "auto"> {*/Fixed in d768907a1 —
applyKnobsnow unconditionally assignsctx.searchBudgetMs = knobs.searchBudgetMs. The explicitoptions.searchBudgetMsoverride is re-applied by the caller on the next line, and search engines readctx.searchBudgetMs ?? DEFAULT_BUDGET_MS, so a reusedEngineContextcan no longer inherit a prior tier's budget and a legitimate0is applied.Fixed in d768907a1 — when every legal ability is an immunity candidate (empty remainder bucket), the full probability mass is now spread over the candidates (
1 / candidates.length) so the distribution sums to 1 instead of 0.9.Fixed in d768907a1 —
learnableMoves/legalMovesnow take the battle generation, threaded fromtracker.gen, instead of the globalDex.gen. So current-gen source gating and level gates match the actual battle gen. The learnset cache key already includes gen, so per-gen caching stays correct. Applies to both the 285-287 and 329-334 sites.Fixed in d768907a1 — dropped the
|| truetautology; the 1.2x now applies only to moves Normalize actually converted (if (move.type !== "Normal")), matching Showdown'stypeChangerBoostedrule so already-Normal moves aren't double-counted. Paired with theresolveMoveTypefix above so non-Normal moves are converted to Normal first.@ -0,0 +1,558 @@/**@smell-of-curryThanks for the detailed fix — mapping successfulstart()resolutions to a never-settling promise is a good touch, since it keepsconsume()as the authoritative source for the actual|win|outcome while still letting a crash in either AI short-circuit the race immediately. That addresses the concern about failures silently degrading into timeout hangs.@ -0,0 +327,4 @@worker.on("message", onMessage);worker.on("error", onError);worker.on("exit", onExit);worker.postMessage(task);@smell-of-curry, thanks for confirming — that resolves the hang scenario cleanly. Appreciate the quick turnaround!(ᵔ◡ᵔ)
@ -0,0 +541,4 @@maxTurns: intArg(args["max-turns"], "max-turns", 1),gameTimeoutMs: intArg(args["game-timeout"], "game-timeout", 0),jobs: intArg(args.jobs, "jobs", 1),verbose: !args.quiet,@smell-of-curry, that closes the gap nicely — theintArghelper with explicit bounds and clear usage errors is exactly what was needed here. Thanks for the fix!ヽ(•‿•)ノ
@ -503,22 +505,22 @@ function abilityImmunityBlocks(moveType: string, move: Move, input: DamageCalcInconst ability = toID(input.defender.ability);if (move.category === "Status") return false;@smell-of-curryConfirmed, that fix correctly handles the ordering issue — checkingability === "normalize"before thetype === "Normal"guard ensures non-Normal moves are properly converted for both type effectiveness and the BP-boost logic. Thanks for the quick fix!(‿ˠ‿)
🧠 Learnings used
@smell-of-curry, that's a reasonable tradeoff — re-validating win-rates after a rollout-behavior change shouldn't happen incidentally inside this PR. The partial fix in d768907a1 (redacting the foe active's unrevealed moves) already closes the highest-leverage leak, and the symmetric-leak argument for self-play is fair since both sides get the same treatment during evaluation.Would you like me to open a follow-up issue to track the full foe-bench redaction (species/sets/HP) for the fork rollout path, so it doesn't get lost?
(°ヘ°) noted — tracking it as a follow-up sounds good to me.
🧠 Learnings used
@smell-of-curryConfirmed — the file now shows the tier-5 row as0.00ladder cap with "fully greedy" wording, matchingknobsForDifficulty(5)'sladderAdvanceCap: 0. Thanks for the fix!(=^・ω・^=)
🧠 Learnings used
@ -99,0 +124,4 @@*/interface TurnPlan {/** An earlier slot chose Helping Hand this turn. */helpingHand: boolean;@smell-of-curry, that makes sense — sinceevalCtx.defenderis alwaystracker.foeActivefor every slot in this engine, there's no per-foe-slot state to keypartnerAttacksagainst in the first place, so a target-aware map wouldn't add real precision here. Thanks for the clarification on the doubles model scope; withdrawing this one.✏️ Learnings added
🧠 Learnings used
@ -102,7 +109,11 @@ function autoFor(difficulty: number): Exclude<EngineName, "auto"> {*/@smell-of-curryConfirmed — the file content showsapplyKnobsnow unconditionally assignsctx.searchBudgetMs = knobs.searchBudgetMs, which resolves the stale-budget/legitimate-0issue I flagged. Thanks for the fix!(´。• ᵕ •。`) ♡
🧠 Learnings used
@smell-of-curryThanks for the correction — you're right, Showdown's actualbasePowerCallbackfor Gyro Ball does include the+ 1, and theMath.max(1, ...)floor onuserSpealready prevents division issues. My original suggestion was incorrect. Withdrawing that comment.(๑•̀ㅁ•́๑)✧ my bad, thanks for double-checking against the source!
🧠 Learnings used
@smell-of-curryConfirmed — the current file showslearnableMoves(species, gen)andlegalMoves(mon, gen)both threadinggenthrough (sourced fromtracker.genviainferMon), and the cache key${toID(species)}|${gen}correctly scopes the cache per generation. This resolves the issue at both call sites. Thanks for the fix!🧠 Learnings used
@smell-of-curryConfirmed — the updated code insim/tools/strategic-ai/state/OpponentInference.tsnow branches onrest.length === 0and spreads full mass (1 / candidates.length) across candidates in that case, otherwise keeping the0.9/0.1split. This correctly normalizes the distribution to sum to 1.0 in both cases. Thanks for the quick fix!🧠 Learnings used
@smell-of-curry, confirmed — that resolves the issue correctly and aligns with Showdown'stypeChangerBoostedsemantics. Thanks for the fix!🐇✨
🧠 Learnings used