Refactor: decompose GameService god class, eliminate duplication across controllers and services#43
Refactor: decompose GameService god class, eliminate duplication across controllers and services#43
Conversation
…rvice, PolicyService) Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/6304afc1-8b8f-4e60-bd3b-eb43f2e4f568 Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
…t comments, fix imports Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/6304afc1-8b8f-4e60-bd3b-eb43f2e4f568 Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/6304afc1-8b8f-4e60-bd3b-eb43f2e4f568 Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Refactors the backend game loop by decomposing the former “god” GameService into focused internal services (AI, combat, policy) and reduces duplicated request/response boilerplate across backend + web-client controllers/services, while keeping public API behavior intact.
Changes:
- Split game logic into
AiService,CombatService, andPolicyService, withGameServiceas the orchestrator/facade. - Consolidated duplicated validation/error-handling in
GameControllerand proxy error-handling inWebController. - Centralized repeated header/entity construction in
web-clientBackendService, and moved snapshot/deep-copy logic intoGameState#createSnapshot().
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| web-client/src/main/java/com/barony/webclient/service/BackendService.java | Adds reusable JSON/session HttpEntity helpers and expands session-aware API methods. |
| web-client/src/main/java/com/barony/webclient/controller/WebController.java | Extracts shared backend error-handling wrapper and adds session decision endpoint handling via it. |
| backend/src/main/java/com/barony/backend/service/SessionService.java | Minor cleanup/formatting (no behavioral refactor in shown diff). |
| backend/src/main/java/com/barony/backend/service/PolicyService.java | New package-private service encapsulating policy-related tick effects and stats/income calculations. |
| backend/src/main/java/com/barony/backend/service/MapGenerator.java | Removes redundant comments/docs (no functional change). |
| backend/src/main/java/com/barony/backend/service/GameService.java | Becomes the orchestrator; delegates to new services; adds capture occupancy helper; uses GameState#createSnapshot(). |
| backend/src/main/java/com/barony/backend/service/CombatService.java | New package-private service for combat resolution and friendly-army merging. |
| backend/src/main/java/com/barony/backend/service/AiService.java | New package-private service for AI turn logic and garrison behavior. |
| backend/src/main/java/com/barony/backend/model/Tile.java | Removes inline explanatory comments (no functional change). |
| backend/src/main/java/com/barony/backend/model/RulerDecision.java | Removes inline explanatory comments (no functional change). |
| backend/src/main/java/com/barony/backend/model/GameState.java | Adds createSnapshot() to own deep-copy logic. |
| backend/src/main/java/com/barony/backend/model/Army.java | Removes inline explanatory comments (no functional change). |
| backend/src/main/java/com/barony/backend/controller/GameController.java | Extracts shared decision validation + policy-change execution helpers and reuses them for session/non-session endpoints. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (int i = 0; i < tickCount; i++) { | ||
| snapshot.incrementTick(); | ||
| } |
There was a problem hiding this comment.
createSnapshot() copies tickCount by calling incrementTick() in a loop. Since GameState has Lombok @Setter, this can be replaced with a direct setTickCount(tickCount) assignment to avoid O(tickCount) snapshot creation as games run longer.
| for (int i = 0; i < tickCount; i++) { | |
| snapshot.incrementTick(); | |
| } | |
| snapshot.setTickCount(tickCount); |
| public synchronized void setGameState(GameState state) { | ||
| this.gameState = state; | ||
| } |
There was a problem hiding this comment.
GameService is a singleton Spring bean whose mutable gameState reference is swapped via setGameState(). Session endpoints call setGameState(sessionState) and then call another synchronized method (e.g., tick() / executeCommand()), which allows another request to interleave between those calls and run the operation against the wrong session’s state. Consider making GameService stateless (pass GameState into operations), or add a single synchronized helper that sets the state and executes an action under the same lock, and update controllers to use it.
| private static final int PLAYER_1_ID = 1; | ||
| private static final int MORALE_BASELINE = 100; | ||
| private static final long ROUNDING_OFFSET = 50L; | ||
| private static final long MORALE_DIVISOR = 100L; |
There was a problem hiding this comment.
MORALE_BASELINE is declared but never used. Either remove it, or incorporate it into calculateEffectiveStrength() (e.g., to make the intent of morale-as-percentage explicit) to avoid dead code.
| } | ||
|
|
||
| private int[] findCastleAssaultTarget(Army army, GameState gameState) { | ||
| int enemyPlayerId = (AI_PLAYER_ID == 1) ? 2 : 1; |
There was a problem hiding this comment.
In findCastleAssaultTarget(), int enemyPlayerId = (AI_PLAYER_ID == 1) ? 2 : 1; is constant-folded (AI_PLAYER_ID is always 2), so this conditional adds noise without changing behavior. Consider replacing it with 1 (or derive from army.getPlayerId() if you want it to work for a different AI player in the future).
| int enemyPlayerId = (AI_PLAYER_ID == 1) ? 2 : 1; | |
| int enemyPlayerId = (army.getPlayerId() == 1) ? 2 : 1; |
|
@copilot You are a coding agent tasked with resolving all open comments and review threads Instructions1. Discover all comments
2. For each comment, do the following:
3. Types of issues to handle:
4. Do NOT:
After making changes
Context
|
…ead safety, remove dead MORALE_BASELINE, fix constant-folded AI enemy ID Agent-Logs-Url: https://github.com/Preponderous-Software/barony/sessions/4b986c0a-4ee4-4375-9741-f0a8c577d164 Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
All 4 review comments addressed in
All 137 tests pass, both modules compile. |
GameService.javawas a 930-line god class handling AI, combat, policy, movement, capture, and win conditions. Controllers and services had duplicated validation, error handling, and HTTP header construction.GameService decomposition (930 → 330 lines)
AiService— AI opponent decision-making (threat detection, target selection, garrison logic)CombatService— Combat resolution with morale modifiers, army mergingPolicyService— Stat recovery, desertion, soldier generation, ruler stats, income calculationnew GameService()directly)DRY consolidation
GameController: extractedvalidateDecision()+executePolicyChange()— two decision endpoints shared identical validation and error mappingBackendService: extractedjsonEntity(),sessionEntity(),sessionJsonEntity()— 10+ methods repeated header constructionWebController: extractedexecuteWithBackendErrorHandling(Supplier<GameState>)— two decision proxy endpoints had identical try/catch chainsGameService: extractedTileOccupancy determineTileOccupancy(x, y)— village and castle capture duplicated the same army-scanning loopcreateSnapshot()toGameState— the data class owns its own deep-copy logicThread safety fix
executeWithSessionState(GameState, Function<GameService, T>)toGameService— atomically sets the session's game state and executes the action under the same synchronized lock, preventing interleaved requests from operating on the wrong session's stateGameControllerto useexecuteWithSessionState()instead of separatesetGameState()+ operation callsNaming improvements
army1/army2→attacker/defender,ax/ay→armyX/armyYTHREAT_DETECTION_RANGE,CAPTURE_DANGER_RANGE,VILLAGE_ATTACK_FORCE_RATIO,POLICY_COOLDOWN_TICKSexecuteAI()→executeAiTurn(),makeAIDecision()→assignAiTarget()Cleanup
// Default,// Place initial armies)org.springframework.web.server.ResponseStatusException) → proper importsMORALE_BASELINEconstant fromCombatServiceincrementTick()loop increateSnapshot()with directsetTickCount()assignmentAiService.findCastleAssaultTarget()to usearmy.getPlayerId()for future-proof flexibilityAll 137 tests pass, both modules compile. No public API contracts changed.