8.9 KiB
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 finalfields forPlayerStats,PlayerAbilities,GameState,TalentTree, and twoPreferencesrefs.
Tests (9):
core/src/test/java/ru/project/tower/HarnessSanityTest.javacore/src/test/java/ru/project/tower/FakePreferences.java— in-memoryPreferencesimpl, reused by every downstream test.core/src/test/java/ru/project/tower/FakePreferencesTest.javacore/src/test/java/ru/project/tower/PlayerStatsTest.javacore/src/test/java/ru/project/tower/PlayerAbilitiesTest.javacore/src/test/java/ru/project/tower/GameStateTest.javacore/src/test/java/ru/project/tower/TalentTreeTest.javacore/src/test/java/ru/project/tower/TalentDataTest.javacore/src/test/java/ru/project/tower/GameContextTest.java
Modified files (10)
Production classes (singletons → DI):
PlayerStats.java— publicPlayerStats(Preferences)ctor,getInstance()deleted,instancefield deleted,PREFS_NAMEdeleted,Gdximport deleted,preferencesfield madefinal.PlayerAbilities.java— publicPlayerAbilities(Preferences)ctor,getInstance()deleted,instancefield deleted,PREFS_NAMEdeleted,preferencesfield madefinal.Gdximport kept (used byGdx.app.error(...)).GameState.java— ctor made public,getInstance()deleted,instancefield deleted.TalentTree.java— publicTalentTree(PlayerStats)ctor,getInstance()deleted,instancefield deleted, two internalPlayerStats.getInstance()calls replaced with injectedstatsfield,talents/rootsmadefinal.
Composition root + screens:
Main.java— rewritten; buildsGameContextfrom twoPreferences, passes toMainScreen.MainScreen.java— 2-arg ctor, 3 downstreamnew 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), 13getInstance()call-sites replaced, internalnew MainScreen(game)→new MainScreen(game, ctx), 2 redundant local shadows removed.AbilityShopScreen.java— 2-arg ctor, 2getInstance()replacements, internalnew MainScreen(game, ctx).AbilitySelectionScreen.java— 2-arg ctor, 2getInstance()replacements, internalnew MainScreen(gameInstance, ctx)+new FirstScreen(gameInstance, ctx).TalentTreeScreen.java— 2-arg ctor, 2getInstance()replacements, internalnew 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 onlyregen_1. Other aggregate getters sum two nodes. If aregen_2node is added later, this getter silently reports the wrong value. Add aregen_2node 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-memorytalentLevelsmap but not persisted (saveData iteratesTALENT_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, includingfreezeandexplosionwhich default totrueon 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 resetspawnedBossesor 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.javafield declarations near lines 429–435 are non-final while thectxfield isfinal. Consistency drift inside one file. (Final review.)Main.ctxispublicnon-final. Safe given libGDXGame.create()semantics, butpublic finalwould document intent. (Final review.)
Test-coverage gaps (not bugs)
TalentTreeTest.getStatBonus_*exercises only thedamage_1multiplier. A second multiplier (cooldown_1, health_1, etc.) would lock more of the formula. (Task 6 review.)TalentDataTest.enum_whenKeystone_thenEnumMatchesis a tautology (KEYSTONE != DAMAGE). Safe to delete. (Final review.)PlayerAbilitiesTest.purchaseAbility_whenUnknownType_thenNoChangedoesn't assert that the unknown key wasn't written to Preferences. (Task 4 review.)PlayerAbilitiesTest.construct_whenPreferencesPrePopulated_thenHonoursStoredseeds onlyfreezeandshield— doesn't verify that absentexplosionkey falls through to thetruedefault. (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/getLongand 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 (A–J from the parent Phase 3 plan) remain untouched and can proceed in parallel with K.2+ when desired.