Skip to content

Phase 7: engine accuracy, abilities/items, BST-aware suggestions - #36

Merged
Marcogn merged 7 commits into
mainfrom
claude/pokemon-app-planning-oxssx3
Sep 6, 2026
Merged

Phase 7: engine accuracy, abilities/items, BST-aware suggestions#36
Marcogn merged 7 commits into
mainfrom
claude/pokemon-app-planning-oxssx3

Conversation

@Marcogn

@Marcogn Marcogn commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Full implementation of Phase 7 — docs/plan/phase-7-accuracy-and-customization.md.

Why

Four things were raised against the shipped app: the suggestion alternatives look irrelevant; abilities render as raw slugs and can't be picked freely for ROM hacks; the weakness/resistance and suggestion engines needed verifying; the number of suggestions should be configurable.

What changed

  • Dataset (§2, §8). Four new pinned CSVs (pokemon_stats, pokemon_stats_past, ability_names, move_names); dataset grows to ~12 requests / ~565 KB. Room bumped to schema v3 (baseStatTotal, per-form abilities, past-BST, item columns), with a real migration and MigrationTestHelper test.
  • Correct ability/move names (§3). Names now come from PokéAPI's own ability_names.csv / move_names.csv instead of a prettify() heuristic that mangled hyphenated proper names (Well-Baked Body, U-turn). New canonical-ability-or-custom-text picker (AbilityPicker) for ROM hack support, wired into both the team slot editor and the custom roster editor.
  • Ability-effect gaps closed (§7.1/§7.2). Ten previously-unmodelled defensive abilities (Heatproof, Water Bubble, Purifying Salt, Filter, Solid Rock, Prism Armor, Primordial Sea, Desolate Land, Delta Stream, Tera Shell), the offensive -ate/Normalize type-override gap, a Dry Skin fix (was immunity-only, now also takes 1.25x Fire), and a Wonder Guard bug (was a UI-only badge, not applied to the actual multiplier).
  • Held items, defensive subset (§4). Air Balloon, Iron Ball, Ring Target, and the 17 type-resist berries — modelled end to end: TeamMember/CustomPokemon, Room columns, the coverage engine's item+ability pipeline, Showdown import/export, and backup format v2.
  • BST tie-break in suggestions (§5). When the composite score ties (the actual root cause of "irrelevant" alternatives — a full-coverage team collapses every candidate to the same score, previously broken by Pokédex-id order), suggestions now break ties by base stat total, resolved for the analysis's selected generation via pokemon_stats_past.csv. The composite score formula itself is untouched, per an explicit scope decision. Also replaced a linear findEntry() scan (run once per candidate, every team edit) with a lookup map.
  • Configurable suggestion count (§6). Settings gained a 5–10 stepper (default 5), threading through AnalysisViewModel/AnalysisScreen.
  • Exhaustive engine verification (§7.3). New tests cover the complete 171-typing space and all 324 type-chart cells directly, not a sample, plus per-ability and per-item regression tests. This is what "verified, not just plausible" means for a coverage engine at this scale.
  • CI fix. A top-level forward-reference in AbilityEffects.kt (KNOWN_ABILITIES_WITH_EFFECTS reading ABILITY_EFFECTS before its declaration) broke every build from the first Phase 7 commit onward — this sandbox has no local Android SDK/JDK 17, so it went undetected until CI ran. Fixed by reordering the declarations; confirmed green after.

Five findings were deliberately deferred rather than folded into this phase — see docs/post-migration-review.md ("Phase 7 audit") and ROADMAP.md (generational type charts chief among them, since the pinned dataset already has the CSVs but no game/generation rule-set concept exists anywhere else in the app).

Test plan

  • ./gradlew testDebugUnitTest — green in CI (build-and-test) on the final commit.
  • ./gradlew lintDebug — green in CI.
  • On-device verification per docs/test-plan.md's new Phase 7 section (ability picker, item picker, BST-ordered suggestions, suggestion-count stepper) — not run in this sandbox, no device available.

🤖 Generated with Claude Code

https://claude.ai/code/session_011j5jieoE4nqU2ZBSM7USHc


Generated by Claude Code

Adds docs/plan/phase-7-accuracy-and-customization.md, the executable plan
for the next phase, plus its pointers in CLAUDE.md and docs/plan/README.md.

The plan opens with an audit measured against the pinned dataset revision,
not recalled:

- The suggestion ranking degenerates to "lowest Pokedex id" once a team's
  coverage is complete: every candidate ties on the composite score, so the
  entryId tie-break decides. That is exactly the reported
  Raticate/Persian/Kangaskhan output.
- PokemonEntry.defaultAbility carries the raw PokeAPI slug, so the same
  ability renders as "sap-sipper" in the field and "Sap Sipper" in the
  picker one row below.
- prettify() cannot produce correct English names (Double-Edge, U-turn,
  Well-Baked Body); only ability_names.csv / move_names.csv can.
- Ten defensive abilities that change type effectiveness are unmodelled,
  carried by 26 catalogue forms, 16 of them as the slot-1 default; dry-skin
  is modelled only halfway; AbilityEffectSide.OFFENSIVE is declared but
  never constructed or consumed.
- Held items are absent from the whole app, and Showdown import discards
  them.
- findEntry() is a linear scan run once per candidate.

The plan then specifies the dataset additions (four CSVs, 212,818 to
578,165 bytes, measured), the per-generation BST rule with worked examples
and the Gen-I five-stat decision, the canonical-plus-custom ability picker,
the defensive-item subset with an explicit application order, the BST
tie-break (score formula and its shared weights untouched), the 5-10
suggestion-count setting, Room v3, and the test suite that has to prove it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011j5jieoE4nqU2ZBSM7USHc
Implements docs/plan/phase-7-accuracy-and-customization.md steps 1-4 of its
task order (§9): the dataset layer, Room v3, and the ability picker/engine
fixes. BST-aware suggestions, held items, the suggestion-count setting,
docs and the exhaustive type-chart/dual-typing test sweep are follow-up
commits on this same PR.

Dataset (§2):
- 4 new pinned CSVs (pokemon_stats, pokemon_stats_past, ability_names,
  move_names), parsed and joined into the assembled catalogue.
- PokemonEntry.baseStatTotal (current BST) and ParsedDataset.pastBst
  (per-generation historical BST, five-stat Gen-I rule) per §2.2's worked
  examples.
- ParsedDataset.pokemonAbilities: every canonical (pokemonId, slot) ability
  row, for the picker's canonical list.
- Ability/move display names now come from ability_names.csv/move_names.csv
  instead of prettify(), which cannot produce a correct hyphenated name
  (Well-Baked Body, Double-Edge, U-turn, ...). defaultAbility now carries
  the display name, not the raw PokeAPI slug (§0.2's Sap Sipper/sap-sipper
  bug).

Room v3 (§8):
- poke_species.baseStatTotal, team_member.item, custom_pokemon.item
  (nullable, item wiring itself lands in the next commit).
- New cache tables poke_pokemon_ability, poke_species_bst_past, wired into
  PokedexDao.replaceCache()/clearCache() by name, never clearAllTables().
- MIGRATION_2_3 + schemas/3.json + DATASET_SCHEMA_VERSION bump to 2, so
  every existing install re-syncs and actually gets the new columns.
- Migration2To3Test mirrors Migration1To2Test's MigrationTestHelper
  coverage.

Ability picker (§3):
- abilityKey() (strip every symbol, not just spaces) replaces
  normalizeAbilityName(), so a slug and a display name with a mismatched
  hyphen still resolve to the same ABILITY_EFFECTS entry.
- ui/common/AbilityPicker.kt: a species' canonical abilities (normal, then
  hidden) plus a "Custom ability..." row that falls back to the existing
  free-text-with-suggestions picker — a ROM hack ability with no PokeAPI
  entry stays typeable. Canonical options with a coverage effect are
  marked. Wired into SlotEditorScreen and RosterEditorScreen.

Ability effect gaps (§7.1/§7.2), found by auditing every ability's
PokeAPI short_effect text:
- 10 previously-unmodelled defensive abilities (Heatproof, Water Bubble,
  Purifying Salt, Filter, Solid Rock, Prism Armor, Primordial Sea,
  Desolate Land, Delta Stream, Tera Shell) plus Dry Skin's missing Fire
  1.25x half.
- Wonder Guard promoted from a UI-only badge to a real effect: only a
  super-effective hit deals damage.
- The offensive gap: Scrappy/Mind's Eye (Normal/Fighting bypass Ghost
  immunity, grid-display only) and the -ate/Normalize abilities (rewrite a
  Normal move's type, gated on real moves being entered).
- CoverageEngineTest/AbilityEffectsTest extended accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011j5jieoE4nqU2ZBSM7USHc
Implements docs/plan/phase-7-accuracy-and-customization.md §4 — step 5 of
the task order (§9). BST-aware suggestions, the suggestion-count setting,
docs and the exhaustive type-chart sweep are still follow-up commits.

- domain/item/ItemEffects.kt: Air Balloon, Iron Ball, Ring Target, and one
  resist berry per type (18 items total: 17 types + Chilan for Normal,
  which applies unconditionally since nothing is super-effective against
  Normal). items.csv is deliberately not downloaded — this is a hardcoded
  table, same shape as ABILITY_EFFECTS, with the same symbol-insensitive
  itemKey() lookup.
- TeamMember.item (nullable, default null — a suggestion candidate never
  sets one). Room columns landed in the previous commit; this one wires
  them through TeamMappers, CustomPokemonDao's upsert (already fixed
  earlier) and BackupRepositoryImpl's restore path, which had its own
  direct CustomPokemonEntity(...) construction that bypassed the mapper
  and would have silently dropped the item on every restore — caught by a
  new round-trip test before it shipped.
- CoverageEngine.defensiveMultiplier gains an item parameter and applies
  ability+item effects in the order §4.2 specifies: item
  RemovesTypeImmunities/GroundsHolder cancel a type-chart 0, then ability
  immunity (skipped for a Ground attack under Iron Ball, which also
  neutralizes Levitate/Earth Eater), then item Immunity, then ability
  multiplier and super-effective reducer, then the resist berry. Threaded
  through defensiveProfile/sharedWeaknessCounts/mostVulnerableByType and
  Scoring.kt's weaknesses()/teamScoringContext()/computeCompositeScore().
- Showdown export/import: "Species @ Item" round-trips instead of being
  discarded on import. Backup format bumped to v2 (a v1 file has no `item`
  key at all, so it still decodes with item defaulting to null).
- UI: ui/common/ItemPicker.kt (free text + suggestions from the modelled
  set, same contract as the ability field), wired into SlotEditorScreen
  and RosterEditorScreen; shown on SlotSummaryCard ("@ Item") and
  PerPokemonCard (with its effect summary, mirroring the ability row).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011j5jieoE4nqU2ZBSM7USHc
Implements docs/plan/phase-7-accuracy-and-customization.md §5 and §6 —
steps 6-7 of the task order (§9). Docs, the exhaustive type-chart/dual-
typing test sweep, and a broader ability/item test pass are still
follow-up commits on this same PR.

Suggestion ranking (§5):
- Suggestion.baseStatTotal (null for a custom candidate); the ranking
  comparator gains a single new tie-break step — bestScore, then isFinal,
  then baseStatTotal descending, then catalogue id ascending. The
  composite-score formula and its 0.5/1.0 weights are untouched, so
  ScoringTest/TeamGeneratorTest keep passing unchanged.
- domain/model/PastBst.kt: bstResolverFor(pastBst, generation) — null
  means the current, latest-generation value; a real generation resolves
  to the smallest stored historical breakpoint at or after it (proven
  correct in the plan's own §2.2, and exercised here against every case:
  no history, before/at/after the only breakpoint, strictly between two
  independent breakpoints, and no cross-contamination between forms).
  AnalysisViewModel now fetches allPastBst() alongside the chart/pool and
  passes a generation-aware resolver into computeSuggestions.
- SuggestionCard shows the base stat total and a plain-language score
  hint ("coverage gained minus weaknesses introduced").
- SuggestionEngine.findEntry's per-candidate linear scan over the ~1351-
  entry pool (up to ~1.8M string comparisons per recomputation) replaced
  with two maps built once per computeSuggestions call, same displayName-
  first precedence.
- The old "secondary sort ... ascending catalogue id" test replaced with
  two regression tests against a hand-built pool: a higher baseStatTotal
  now outranks a lower one on a composite-score tie (reproducing the
  reported Raticate/Persian/Kangaskhan case), and id still decides when
  baseStatTotal also ties.

Suggestion count (§6):
- SettingsPreferences.suggestionCount, clamped to 5-10 on both read and
  write, default 5 (matching the value Phase 4 hardcoded).
- ui/common/StepperCounter.kt: the -/+ row promoted out of
  ui/surprise/SurpriseMeScreen.kt's private ConstraintCounter (0-floor,
  no shared ceiling) into a shared composable with explicit
  canDecrement/canIncrement bounds; Surprise Me's five constraint rows and
  the new Settings row both use it now.
- AnalysisUiState.suggestionCount flows from the setting through to
  AnalysisScreen's take(state.suggestionCount), replacing the hardcoded
  take(5); AnalysisUiState.suggestions itself stays unsliced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011j5jieoE4nqU2ZBSM7USHc
Implements docs/plan/phase-7-accuracy-and-customization.md §7.3.1-3 — the
point-3 ask ("verify the weakness/resistance engine is correct across all
1000+ Pokemon"), answered at the level where it's actually decidable: the
complete 171-typing space (18 single types + all 153 unordered dual-type
pairs), not a re-test of the same space 1351 times over with worse
failure messages.

- Every one of the 324 type-chart cells is one of {0, 0.5, 1, 2}.
- The eight classic asymmetric matchups (Ghost/Normal, Fighting/Ghost,
  Ground/Flying immunities; Fairy/Dragon, Steel/Fairy, Fire/Steel;
  Poison/Steel, Electric/Ground) assert their exact value.
- defensiveMultiplier for all 171 typings against all 18 attacking types
  equals the product of the two single-type lookups.
- defensiveProfile buckets every non-neutral type into exactly one of
  weaknesses/resistances/immunities and omits every neutral one, checked
  against the real multiplier for all 171 typings x 18 attacking types —
  this is the test that would have caught a mis-bucketing or
  double-counting bug anywhere in the whole type-effectiveness surface.
- A cheap structural guard in DatasetAssemblyTest that every assembled
  form's typing is non-null and has no duplicate second type.
- One more ability case (Filter turning a genuine x4 weakness into x3,
  the plan's own worked example) alongside the ones already covered when
  the ability-effect gaps were closed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011j5jieoE4nqU2ZBSM7USHc
Update CHANGELOG, native spec, reference dataset contract, architecture
docs, status snapshot, roadmap, implementation decisions, post-migration
review, and README to reflect Phase 7's shipped scope: base stat totals,
canonical ability names/picker, held item coverage effects, BST
tie-break in suggestions, configurable suggestion count, and the
exhaustive coverage-engine verification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011j5jieoE4nqU2ZBSM7USHc
KNOWN_ABILITIES_WITH_EFFECTS was declared before ABILITY_EFFECTS, which
it reads at initialization — Kotlin does not allow a top-level property
initializer to forward-reference another one declared later in the same
file ("Variable 'ABILITY_EFFECTS' must be initialized"). This broke
compilation on every commit since it was introduced; move the derived
list below the map it derives from.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011j5jieoE4nqU2ZBSM7USHc
@Marcogn Marcogn changed the title Plan Phase 7: engine accuracy, abilities/items, BST-aware suggestions Phase 7: engine accuracy, abilities/items, BST-aware suggestions Sep 6, 2026
@Marcogn
Marcogn marked this pull request as ready for review September 6, 2026 18:15
@Marcogn
Marcogn merged commit ee5ef61 into main Sep 6, 2026
1 check passed
@Marcogn
Marcogn deleted the claude/pokemon-app-planning-oxssx3 branch September 6, 2026 18:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants