From 169ade276ed38e73e5da5b5cc9751100e4dfc50b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 05:01:36 +0000 Subject: [PATCH] refactor: consolidate turn production logic into TurnEngine Consolidated redundant production logic from LocalGameServer into the centralized TurnEngine system. This eliminates over 250 lines of duplicated code and ensures a single source of truth for the game loop across client-side simulation and authoritative server logic. Key changes: - Migrated construction helper methods to TurnEngine for better decomposition. - Updated TurnEngine to use the shared GameEffect protocol for notifications. - Removed dead code and unused imports from LocalGameServer. - Verified consistency with ProductionSystem.getInventoryCapacity. - All tests and lint checks passed. --- src/server/game/LocalGameServer.ts | 281 +------------------------- src/shared/game/systems/TurnEngine.ts | 153 +++++++++----- 2 files changed, 106 insertions(+), 328 deletions(-) diff --git a/src/server/game/LocalGameServer.ts b/src/server/game/LocalGameServer.ts index a31fdca..e3002fa 100644 --- a/src/server/game/LocalGameServer.ts +++ b/src/server/game/LocalGameServer.ts @@ -1,9 +1,8 @@ -import { BUILDING_COSTS, COLONY_CONSTANTS, RECRUITMENT_COSTS, UNIT_BUILD_COSTS } from '@shared/game/constants'; +import { RECRUITMENT_COSTS } from '@shared/game/constants'; import { calculatePopulation } from '@shared/game/entities/Settlement'; import { createUnit } from '@shared/game/entities/Unit'; import type { Player } from '@shared/game/entities/Player'; import type { Settlement } from '@shared/game/entities/Settlement'; -import type { Unit } from '@shared/game/entities/Unit'; import type { Position } from '@shared/game/entities/Position'; import { Attitude, @@ -16,8 +15,8 @@ import { type Occupation, } from '@shared/game/entities/types'; import { GameSystem } from '@shared/game/systems/GameSystem'; -import { NamingSystem, type NamingStats } from '@shared/game/systems/NamingSystem'; -import { ProductionSystem } from '@shared/game/systems/ProductionSystem'; +import { NamingSystem } from '@shared/game/systems/NamingSystem'; +import { TurnEngine } from '@shared/game/systems/TurnEngine'; import { MovementSystem } from '@shared/game/systems/MovementSystem'; import { UnitSystem } from '@shared/game/systems/UnitSystem'; import { SettlementSystem } from '@shared/game/systems/SettlementSystem'; @@ -701,7 +700,12 @@ export class LocalGameServer { } if (this.state.phase === TurnPhase.PRODUCTION) { - const result = this.runProduction(this.state.players, this.state.map, this.state.namingStats); + const result = TurnEngine.runProduction( + this.state.players, + this.state.map, + this.state.namingStats, + generateId + ); this.state.players = result.players; this.state.namingStats = result.namingStats; effects.push(...result.effects); @@ -740,273 +744,6 @@ export class LocalGameServer { } } - private runProduction( - players: Player[], - map: AuthoritativeGameState['map'], - namingStats: NamingStats, - ): { players: Player[]; namingStats: NamingStats; effects: GameEffect[] } { - let currentNamingStats = { ...namingStats }; - const effects: GameEffect[] = []; - - const updatedPlayers = players.map((player) => { - const newPlayerUnits = [...player.units]; - const newSettlements = player.settlements.map((settlement) => { - const nextSettlement: Settlement = { - ...settlement, - buildings: [...settlement.buildings], - productionQueue: [...settlement.productionQueue], - inventory: new Map(settlement.inventory), - units: settlement.units.map((unit) => ({ ...unit, cargo: new Map(unit.cargo) })), - goods: new Map(settlement.goods), - }; - - currentNamingStats = this.processSettlementTurn( - nextSettlement, - player, - newPlayerUnits, - map, - currentNamingStats, - effects, - ); - - return nextSettlement; - }); - - return { - ...player, - units: newPlayerUnits, - settlements: newSettlements, - }; - }); - - return { players: updatedPlayers, namingStats: currentNamingStats, effects }; - } - - private processSettlementTurn( - settlement: Settlement, - player: Player, - playerUnits: Unit[], - map: AuthoritativeGameState['map'], - namingStats: NamingStats, - effects: GameEffect[], - ): NamingStats { - let currentNamingStats = namingStats; - settlement.population = calculatePopulation(settlement); - - settlement.units.forEach((unit) => { - unit.turnsInJob += 1; - if (unit.turnsInJob >= COLONY_CONSTANTS.EXPERT_PROMOTION_TURNS && !unit.expertise) { - if (typeof unit.occupation === 'string') { - unit.expertise = unit.occupation; - effects.push({ - type: 'notification', - message: `${unit.type} has become an expert ${unit.expertise}!`, - }); - } - } - }); - - const { netProduction, hammersProduced } = ProductionSystem.calculateSettlementProduction( - settlement, - map, - true - ); - - netProduction.forEach((amount, good) => { - settlement.inventory.set(good, Math.max(0, (settlement.inventory.get(good) ?? 0) + amount)); - }); - settlement.hammers += hammersProduced; - - if (settlement.buildings.includes(BuildingType.PRINTING_PRESS)) { - const namingResult = NamingSystem.getNextName(player.nation, 'unit', currentNamingStats); - currentNamingStats = namingResult.updatedStats; - - const newUnit = createUnit( - generateId('unit'), - settlement.ownerId, - namingResult.name, - UnitType.COLONIST, - settlement.position.x, - settlement.position.y, - 3 - ); - - playerUnits.push(newUnit); - effects.push({ - type: 'notification', - message: `An intellectual has joined the cause in ${settlement.name}!`, - }); - } - - currentNamingStats = this.processConstruction( - settlement, - player, - playerUnits, - currentNamingStats, - effects, - ); - - currentNamingStats = this.processPopulationGrowth( - settlement, - player, - playerUnits, - currentNamingStats, - effects, - ); - - const cap = settlement.buildings.includes(BuildingType.WAREHOUSE) - ? COLONY_CONSTANTS.WAREHOUSE_CAPACITY - : COLONY_CONSTANTS.DEFAULT_CAPACITY; - - settlement.inventory.forEach((amount, good) => { - if (amount > cap) { - settlement.inventory.set(good, cap); - } - }); - - return currentNamingStats; - } - - private processConstruction( - settlement: Settlement, - player: Player, - playerUnits: Unit[], - namingStats: NamingStats, - effects: GameEffect[], - ): NamingStats { - let currentNamingStats = namingStats; - if (settlement.productionQueue.length === 0) { - return currentNamingStats; - } - - const currentItem = settlement.productionQueue[0]; - if (!currentItem) { - return currentNamingStats; - } - - const isBuilding = Object.values(BuildingType).includes(currentItem as BuildingType); - const isUnit = Object.values(UnitType).includes(currentItem as UnitType); - const cost = this.getProductionCost(currentItem, isBuilding); - - if (!this.canAffordConstruction(settlement, cost)) { - return currentNamingStats; - } - - this.deductConstructionResources(settlement, cost); - settlement.productionQueue.shift(); - - if (isBuilding) { - settlement.buildings.push(currentItem as BuildingType); - effects.push({ - type: 'notification', - message: `${settlement.name} completed ${currentItem as BuildingType}!`, - }); - } else if (isUnit) { - const namingResult = NamingSystem.getNextName( - player.nation, - (currentItem as UnitType) === UnitType.SHIP ? 'ship' : 'unit', - currentNamingStats - ); - currentNamingStats = namingResult.updatedStats; - - const newUnit = createUnit( - generateId('unit'), - settlement.ownerId, - namingResult.name, - currentItem as UnitType, - settlement.position.x, - settlement.position.y, - 3 - ); - settlement.units.push(newUnit); - playerUnits.push(newUnit); - effects.push({ - type: 'notification', - message: `${settlement.name} completed ${currentItem as UnitType}!`, - }); - } - - return currentNamingStats; - } - - private getProductionCost( - item: BuildingType | UnitType, - isBuilding: boolean - ): { hammers: number; tools: number; muskets: number } { - if (isBuilding) { - const cost = (BUILDING_COSTS as Record)[item as string] ?? { hammers: 40, tools: 0 }; - return { hammers: cost.hammers, tools: cost.tools ?? 0, muskets: 0 }; - } - const cost = (UNIT_BUILD_COSTS as Record)[item as string] ?? { hammers: 40, tools: 0, muskets: 0 }; - return { hammers: cost.hammers, tools: cost.tools ?? 0, muskets: cost.muskets ?? 0 }; - } - - private canAffordConstruction( - settlement: Settlement, - cost: { hammers: number; tools: number; muskets: number } - ): boolean { - const currentTools = settlement.inventory.get(GoodType.TOOLS) ?? 0; - const currentMuskets = settlement.inventory.get(GoodType.MUSKETS) ?? 0; - - return ( - settlement.hammers >= cost.hammers && - currentTools >= cost.tools && - currentMuskets >= cost.muskets - ); - } - - private deductConstructionResources( - settlement: Settlement, - cost: { hammers: number; tools: number; muskets: number } - ): void { - settlement.hammers -= cost.hammers; - - if (cost.tools > 0) { - const currentTools = settlement.inventory.get(GoodType.TOOLS) ?? 0; - settlement.inventory.set(GoodType.TOOLS, currentTools - cost.tools); - } - - if (cost.muskets > 0) { - const currentMuskets = settlement.inventory.get(GoodType.MUSKETS) ?? 0; - settlement.inventory.set(GoodType.MUSKETS, currentMuskets - cost.muskets); - } - } - - private processPopulationGrowth( - settlement: Settlement, - player: Player, - playerUnits: Unit[], - namingStats: NamingStats, - effects: GameEffect[], - ): NamingStats { - const currentFood = settlement.inventory.get(GoodType.FOOD) ?? 0; - if (currentFood < COLONY_CONSTANTS.FOOD_GROWTH_THRESHOLD) { - return namingStats; - } - - settlement.inventory.set( - GoodType.FOOD, - currentFood - COLONY_CONSTANTS.FOOD_GROWTH_THRESHOLD - ); - - const namingResult = NamingSystem.getNextName(player.nation, 'unit', namingStats); - const newColonist = createUnit( - generateId('unit'), - settlement.ownerId, - namingResult.name, - UnitType.COLONIST, - settlement.position.x, - settlement.position.y, - 3 - ); - playerUnits.push(newColonist); - effects.push({ - type: 'notification', - message: `A new colonist has been born in ${settlement.name}!`, - }); - - return namingResult.updatedStats; - } private selectCurrentPlayer(): Player | undefined { return TraversalUtils.findPlayerById(this.state.players, this.state.currentPlayerId); diff --git a/src/shared/game/systems/TurnEngine.ts b/src/shared/game/systems/TurnEngine.ts index 3324680..ddd64e8 100644 --- a/src/shared/game/systems/TurnEngine.ts +++ b/src/shared/game/systems/TurnEngine.ts @@ -8,16 +8,12 @@ import { createUnit } from '../entities/Unit'; import { calculatePopulation } from '../entities/Settlement'; import { ProductionSystem } from './ProductionSystem'; import { NamingSystem, type NamingStats } from './NamingSystem'; - -export interface TurnNotificationEffect { - readonly type: 'notification'; - readonly message: string; -} +import type { GameEffect } from '../protocol'; export interface TurnEngineResult { readonly players: Player[]; readonly namingStats: NamingStats; - readonly effects: readonly TurnNotificationEffect[]; + readonly effects: readonly GameEffect[]; } /* eslint-disable-next-line @typescript-eslint/no-extraneous-class */ @@ -33,7 +29,7 @@ export class TurnEngine { generateId: (prefix: string) => string ): TurnEngineResult { let currentNamingStats = { ...namingStats }; - const effects: TurnNotificationEffect[] = []; + const effects: GameEffect[] = []; const updatedPlayers = players.map((player) => { const newPlayerUnits = [...player.units]; @@ -77,7 +73,7 @@ export class TurnEngine { map: Tile[][], namingStats: NamingStats, generateId: (prefix: string) => string, - effects: TurnNotificationEffect[] + effects: GameEffect[] ): NamingStats { let currentNamingStats = namingStats; @@ -128,7 +124,7 @@ export class TurnEngine { return currentNamingStats; } - private static promoteExperts(settlement: Settlement, effects: TurnNotificationEffect[]): void { + private static promoteExperts(settlement: Settlement, effects: GameEffect[]): void { settlement.units.forEach((unit) => { unit.turnsInJob += 1; if (unit.turnsInJob >= COLONY_CONSTANTS.EXPERT_PROMOTION_TURNS && !unit.expertise) { @@ -146,68 +142,114 @@ export class TurnEngine { playerUnits: Unit[], namingStats: NamingStats, generateId: (prefix: string) => string, - effects: TurnNotificationEffect[] + effects: GameEffect[] ): NamingStats { let currentNamingStats = namingStats; - if (settlement.productionQueue.length > 0) { - const currentItem = settlement.productionQueue[0]; - if (!currentItem) return currentNamingStats; + if (settlement.productionQueue.length === 0) { + return currentNamingStats; + } - const isBuilding = Object.values(BuildingType).includes(currentItem as BuildingType); - const isUnit = Object.values(UnitType).includes(currentItem as UnitType); + const currentItem = settlement.productionQueue[0]; + if (!currentItem) { + return currentNamingStats; + } - const cost = isBuilding - ? (BUILDING_COSTS[currentItem as BuildingType] ?? { hammers: 40, tools: 0 }) - : (UNIT_BUILD_COSTS[currentItem as UnitType] ?? { hammers: 40, tools: 0, muskets: 0 }); + const isBuilding = Object.values(BuildingType).includes(currentItem as BuildingType); + const isUnit = Object.values(UnitType).includes(currentItem as UnitType); + const cost = this.getProductionCost(currentItem, isBuilding); - // Check resource availability - const currentTools = settlement.inventory.get(GoodType.TOOLS) ?? 0; - const currentMuskets = settlement.inventory.get(GoodType.MUSKETS) ?? 0; - const toolsNeeded = (cost as { tools?: number }).tools ?? 0; - const musketsNeeded = (cost as { muskets?: number }).muskets ?? 0; - - if (currentTools >= toolsNeeded && currentMuskets >= musketsNeeded) { - if (settlement.hammers >= cost.hammers) { - settlement.hammers -= cost.hammers; - settlement.inventory.set(GoodType.TOOLS, currentTools - toolsNeeded); - settlement.inventory.set(GoodType.MUSKETS, currentMuskets - musketsNeeded); - settlement.productionQueue.shift(); - - if (isBuilding) { - settlement.buildings.push(currentItem as BuildingType); - effects.push({ type: 'notification', message: `${settlement.name} completed ${(currentItem as BuildingType)}!` }); - } else if (isUnit) { - const namingResult = NamingSystem.getNextName(player.nation, (currentItem as UnitType) === UnitType.SHIP ? 'ship' : 'unit', currentNamingStats); - currentNamingStats = namingResult.updatedStats; - - const newUnit = createUnit( - generateId('unit'), - settlement.ownerId, - namingResult.name, - currentItem as UnitType, - settlement.position.x, - settlement.position.y, - 3 - ); - settlement.units.push(newUnit); - playerUnits.push(newUnit); - effects.push({ type: 'notification', message: `${settlement.name} completed ${(currentItem as UnitType)}!` }); - } - } - } + if (!this.canAffordConstruction(settlement, cost)) { + return currentNamingStats; + } + + this.deductConstructionResources(settlement, cost); + settlement.productionQueue.shift(); + + if (isBuilding) { + settlement.buildings.push(currentItem as BuildingType); + effects.push({ + type: 'notification', + message: `${settlement.name} completed ${currentItem as BuildingType}!`, + }); + } else if (isUnit) { + const namingResult = NamingSystem.getNextName( + player.nation, + (currentItem as UnitType) === UnitType.SHIP ? 'ship' : 'unit', + currentNamingStats + ); + currentNamingStats = namingResult.updatedStats; + + const newUnit = createUnit( + generateId('unit'), + settlement.ownerId, + namingResult.name, + currentItem as UnitType, + settlement.position.x, + settlement.position.y, + 3 + ); + settlement.units.push(newUnit); + playerUnits.push(newUnit); + effects.push({ + type: 'notification', + message: `${settlement.name} completed ${currentItem as UnitType}!`, + }); } return currentNamingStats; } + private static getProductionCost( + item: BuildingType | UnitType, + isBuilding: boolean + ): { hammers: number; tools: number; muskets: number } { + if (isBuilding) { + const cost = (BUILDING_COSTS as Record)[item as string] ?? { hammers: 40, tools: 0 }; + return { hammers: cost.hammers, tools: cost.tools ?? 0, muskets: 0 }; + } + const cost = (UNIT_BUILD_COSTS as Record)[item as string] ?? { hammers: 40, tools: 0, muskets: 0 }; + return { hammers: cost.hammers, tools: cost.tools ?? 0, muskets: cost.muskets ?? 0 }; + } + + private static canAffordConstruction( + settlement: Settlement, + cost: { hammers: number; tools: number; muskets: number } + ): boolean { + const currentTools = settlement.inventory.get(GoodType.TOOLS) ?? 0; + const currentMuskets = settlement.inventory.get(GoodType.MUSKETS) ?? 0; + + return ( + settlement.hammers >= cost.hammers && + currentTools >= cost.tools && + currentMuskets >= cost.muskets + ); + } + + private static deductConstructionResources( + settlement: Settlement, + cost: { hammers: number; tools: number; muskets: number } + ): void { + settlement.hammers -= cost.hammers; + + if (cost.tools > 0) { + const currentTools = settlement.inventory.get(GoodType.TOOLS) ?? 0; + settlement.inventory.set(GoodType.TOOLS, currentTools - cost.tools); + } + + if (cost.muskets > 0) { + const currentMuskets = settlement.inventory.get(GoodType.MUSKETS) ?? 0; + settlement.inventory.set(GoodType.MUSKETS, currentMuskets - cost.muskets); + } + } + private static processPopulationGrowth( settlement: Settlement, player: Player, playerUnits: Unit[], namingStats: NamingStats, generateId: (prefix: string) => string, - effects: TurnNotificationEffect[] + effects: GameEffect[] ): NamingStats { let currentNamingStats = namingStats; const currentFood = settlement.inventory.get(GoodType.FOOD) ?? 0; @@ -234,8 +276,7 @@ export class TurnEngine { } private static applyInventoryCap(settlement: Settlement): void { - const cap = settlement.buildings.includes(BuildingType.WAREHOUSE) ? - COLONY_CONSTANTS.WAREHOUSE_CAPACITY : COLONY_CONSTANTS.DEFAULT_CAPACITY; + const cap = ProductionSystem.getInventoryCapacity(settlement); settlement.inventory.forEach((amount, good) => { if (amount > cap) { settlement.inventory.set(good, cap);