Files
square-defense/docs/superpowers/plans/2026-04-17-phase3-k1-test-harness-completion.md
T
2026-06-16 10:57:01 +03:00

128 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 3.K.1 — Completion Report
**Date completed:** 2026-04-17
**Source plan:** `2026-04-17-phase3-k1-test-harness.md`
**Source spec:** `../specs/2026-04-17-phase3-k1-test-harness-design.md`
**Execution mode:** subagent-driven-development (fresh subagent per task, two-stage review).
## Status
- **Code:** ✅ Shipped. All 11 tasks complete.
- **Automated verification:** ✅ `./gradlew core:test` → 58/58 green. `./gradlew :core:build :lwjgl3:build` → BUILD SUCCESSFUL.
- **Manual smoke test:** ⏳ Pending user — see checklist below.
- **Git:** project is not a git repository; no commits were made. All work lives in the working tree.
## What was delivered
### New files (10)
Production (1):
- `core/src/main/java/ru/project/tower/GameContext.java` — composition-root holder for shared persistent-state services. `public final` fields for `PlayerStats`, `PlayerAbilities`, `GameState`, `TalentTree`, and two `Preferences` refs.
Tests (9):
- `core/src/test/java/ru/project/tower/HarnessSanityTest.java`
- `core/src/test/java/ru/project/tower/FakePreferences.java` — in-memory `Preferences` impl, reused by every downstream test.
- `core/src/test/java/ru/project/tower/FakePreferencesTest.java`
- `core/src/test/java/ru/project/tower/PlayerStatsTest.java`
- `core/src/test/java/ru/project/tower/PlayerAbilitiesTest.java`
- `core/src/test/java/ru/project/tower/GameStateTest.java`
- `core/src/test/java/ru/project/tower/TalentTreeTest.java`
- `core/src/test/java/ru/project/tower/TalentDataTest.java`
- `core/src/test/java/ru/project/tower/GameContextTest.java`
### Modified files (10)
Production classes (singletons → DI):
- `PlayerStats.java` — public `PlayerStats(Preferences)` ctor, `getInstance()` deleted, `instance` field deleted, `PREFS_NAME` deleted, `Gdx` import deleted, `preferences` field made `final`.
- `PlayerAbilities.java` — public `PlayerAbilities(Preferences)` ctor, `getInstance()` deleted, `instance` field deleted, `PREFS_NAME` deleted, `preferences` field made `final`. `Gdx` import kept (used by `Gdx.app.error(...)`).
- `GameState.java` — ctor made public, `getInstance()` deleted, `instance` field deleted.
- `TalentTree.java` — public `TalentTree(PlayerStats)` ctor, `getInstance()` deleted, `instance` field deleted, two internal `PlayerStats.getInstance()` calls replaced with injected `stats` field, `talents`/`roots` made `final`.
Composition root + screens:
- `Main.java` — rewritten; builds `GameContext` from two `Preferences`, passes to `MainScreen`.
- `MainScreen.java` — 2-arg ctor, 3 downstream `new X(game)``new X(game, ctx)`.
- `FirstScreen.java` — 2-arg ctor, 3 new cached fields (`ctx`, `gameState`, `talentTree`), 4 cached services assigned at ctor top (closed an NPE window), 13 `getInstance()` call-sites replaced, internal `new MainScreen(game)``new MainScreen(game, ctx)`, 2 redundant local shadows removed.
- `AbilityShopScreen.java` — 2-arg ctor, 2 `getInstance()` replacements, internal `new MainScreen(game, ctx)`.
- `AbilitySelectionScreen.java` — 2-arg ctor, 2 `getInstance()` replacements, internal `new MainScreen(gameInstance, ctx)` + `new FirstScreen(gameInstance, ctx)`.
- `TalentTreeScreen.java` — 2-arg ctor, 2 `getInstance()` replacements, internal `new MainScreen(game, ctx)`.
Build:
- `core/build.gradle``testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'`, `testImplementation 'org.assertj:assertj-core:3.26.3'`, `test { useJUnitPlatform() }`.
### Test counts (by class)
| Test class | Tests |
|---|---|
| HarnessSanityTest | 1 |
| FakePreferencesTest | 7 |
| PlayerStatsTest | 14 |
| PlayerAbilitiesTest | 8 |
| GameStateTest | 6 |
| TalentTreeTest | 12 |
| TalentDataTest | 6 |
| GameContextTest | 4 |
| **Total** | **58** |
### Invariants verified at completion
- `grep "getInstance(" core/src/main/java/ru/project/tower -r` → 0 matches.
- `grep "private static \w+ instance;" core/src/main/java/ru/project/tower -r` → 0 matches.
- `./gradlew core:test` → 58 passed, 0 failed.
- `./gradlew :core:build :lwjgl3:build` → BUILD SUCCESSFUL.
## Manual smoke test — ⏳ pending
Run `lwjgl3/src/main/java/ru/project/tower/lwjgl3/Lwjgl3Launcher` and verify:
- [ ] Main menu renders with persisted currency + max-wave.
- [ ] Talent Tree: purchased levels display, new purchase deducts currency, back returns to menu.
- [ ] Ability Shop: owned abilities display correctly, new purchase (e.g., Shield) persists after a restart.
- [ ] Ability Selection → Start Game → wave 1 plays, firing kills enemies, death returns to menu, currency reflects loot.
- [ ] Mid-run shop flow: save/restore works (upgrade survives shop round-trip).
- [ ] Close and relaunch: all persisted state (currency, max wave, unlocked abilities, talent levels) survives.
If any check fails, report back — most likely a missed call-site or a `ctx` threading bug; fix scope is minutes, not hours.
## Carry-over items (non-blocking)
These were flagged by reviewers during K.1 execution but are either pre-existing (not regressions introduced by K.1) or are tiny cleanups better folded into the next spec. They do not block K.2.
### Pre-existing design issues surfaced by the new test harness
- **`PlayerStats.getRegenTalentLevel()` sums only `regen_1`.** Other aggregate getters sum two nodes. If a `regen_2` node is added later, this getter silently reports the wrong value. Add a `regen_2` node or a comment explaining the omission. (Flagged in Task 3 review.)
- **`PlayerStats.incrementTalentNode(String)` accepts any string, including typos.** Phantom nodes are created in the in-memory `talentLevels` map but not persisted (saveData iterates `TALENT_NODE_IDS`). Consider a validation guard. (Flagged in Task 3 review.)
- **`PlayerStats.resetData()` Javadoc says "для тестирования"** but the test suite doesn't call it — callers live elsewhere. Clarify the comment or remove the method. (Task 3 review.)
- **`PlayerAbilities.resetData()` clears everything, including `freeze` and `explosion` which default to `true` on construction.** Intentional per the existing code, but the name implies "factory defaults" which it isn't. Rename or add a comment. (Task 4 review.)
- **`GameState.resetGameState()` doesn't reset `spawnedBosses` or the gameplay stat fields** (money, squareHealth, bulletSpeed, etc.). If this method is the "start new run" boundary, that's a latent bug. (Task 5 review.)
- **`FirstScreen.java` field declarations near lines 429435 are non-final** while the `ctx` field is `final`. Consistency drift inside one file. (Final review.)
- **`Main.ctx` is `public` non-final.** Safe given libGDX `Game.create()` semantics, but `public final` would document intent. (Final review.)
### Test-coverage gaps (not bugs)
- `TalentTreeTest.getStatBonus_*` exercises only the `damage_1` multiplier. A second multiplier (cooldown_1, health_1, etc.) would lock more of the formula. (Task 6 review.)
- `TalentDataTest.enum_whenKeystone_thenEnumMatches` is a tautology (`KEYSTONE != DAMAGE`). Safe to delete. (Final review.)
- `PlayerAbilitiesTest.purchaseAbility_whenUnknownType_thenNoChange` doesn't assert that the unknown key wasn't written to Preferences. (Task 4 review.)
- `PlayerAbilitiesTest.construct_whenPreferencesPrePopulated_thenHonoursStored` seeds only `freeze` and `shield` — doesn't verify that absent `explosion` key falls through to the `true` default. (Task 4 review.)
### FakePreferences polish
- `FakePreferences.put(Map<String,?>)` bulk-put isn't guarded — values inserted through this path could be types no getter recognises (e.g., `Double`, `Short`). No test exercises this. (Task 2 review.)
- `FakePreferences.getFloat`/`getLong` and the no-default single-arg getters have no tests. (Task 2 review.)
## What's next
K.1 is the foundation. The remaining specs in the K-series each handle one subsystem extraction out of `FirstScreen`'s god-class. Each follows the same brainstorm → spec → plan → subagent-driven-implementation cycle.
| Spec | Scope | Dependency |
|---|---|---|
| **K.2** | `BulletSystem` extraction — bullet tick, bullet-vs-circle collision, homing/pierce/aoe/chain resolution | K.1 |
| **K.3** | `SpawnController` extraction — `spawnCircle()` cascade | K.1 |
| **K.4** | `WaveController` extraction — `startNextWave` / `endWave` / scaling | K.1 |
| **K.5** | `EnemyBehaviour` tests — `tick()` for `HealerCircleData`, `DasherCircleData`, `SniperCircleData` (and others) | K.1 + Phase 3 A1/A2/A3 |
| **K.6** | `AbilitySystem` extraction — cooldowns, activations, effects | K.1 |
| **K.7** | `UpgradeSystem` extraction — mid-run upgrade pick, money, apply-effect | K.1 |
Recommended next step: brainstorm **K.2** (`BulletSystem`) — highest-value extraction because bullet logic is central to the game, has the most flags (`piercesLeft`, `homing`, `aoeRadius`, `chainJumps`), and is the first place Phase 3's `homing` keystone + C2 chain work will land.
Phase 3 gameplay tasks (AJ from the parent Phase 3 plan) remain untouched and can proceed in parallel with K.2+ when desired.