diff --git a/ai_animation/.gitignore b/ai_animation/.gitignore index f62b80a..639ee0d 100644 --- a/ai_animation/.gitignore +++ b/ai_animation/.gitignore @@ -26,4 +26,4 @@ dist-ssr # AI things .claude/ -./public/games/ +public/games/ diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts index 26f17ea..eabb631 100644 --- a/ai_animation/src/gameState.ts +++ b/ai_animation/src/gameState.ts @@ -13,7 +13,7 @@ import { Tween, Group as TweenGroup } from "@tweenjs/tween.js"; import { hideStandingsBoard, } from "./domElements/standingsBoard"; import { MomentsDataSchema, MomentsDataSchemaType } from "./types/moments"; -//FIXME: This whole file is a mess. Need to organkze and format +//FIXME: This whole file is a mess. Need to organize and format enum AvailableMaps { STANDARD = "standard" diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index 50ac468..56472bd 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -7,7 +7,7 @@ import { loadBtn, prevBtn, nextBtn, speedSelector, fileInput, playBtn, mapView, import { updateChatWindows } from "./domElements/chatWindows"; import { initStandingsBoard, hideStandingsBoard, showStandingsBoard } from "./domElements/standingsBoard"; import { initRelationshipPopup, hideRelationshipPopup, updateRelationshipPopup } from "./domElements/relationshipPopup"; -import { displayPhaseWithAnimation, advanceToNextPhase, resetToPhase } from "./phase"; +import { displayPhaseWithAnimation, advanceToNextPhase, resetToPhase, nextPhase, previousPhase } from "./phase"; import { config } from "./config"; import { Tween, Group, Easing } from "@tweenjs/tween.js"; import { initRotatingDisplay, updateRotatingDisplay } from "./components/rotatingDisplay"; @@ -23,15 +23,8 @@ let prevPos // --- INITIALIZE SCENE --- function initScene() { - gameState.initScene() + gameState.createThreeScene() - // Lighting (keep it simple) - const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); - gameState.scene.add(ambientLight); - - const dirLight = new THREE.DirectionalLight(0xffffff, 0.6); - dirLight.position.set(300, 400, 300); - gameState.scene.add(dirLight); // Initialize standings board initStandingsBoard(); @@ -267,12 +260,10 @@ fileInput.addEventListener('change', e => { }); prevBtn.addEventListener('click', () => { - if (gameState.phaseIndex > 0) { - resetToPhase(gameState.phaseIndex - 1) - } + previousPhase() }); nextBtn.addEventListener('click', () => { - advanceToNextPhase() + nextPhase() }); playBtn.addEventListener('click', togglePlayback); diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index f93a35a..276821c 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -11,6 +11,42 @@ import { config } from "./config"; +function _setPhase(phaseIndex: number) { + const gameLength = gameState.gameData.phases.length + // Validate that the phaseIndex is within the bounds of the game length. + if (phaseIndex >= gameLength || phaseIndex < 0) { + throw new Error(`Provided invalid phaseIndex, cannot setPhase to ${phaseIndex} - game has ${gameState.gameData.phases.length} phases`) + } + if (Math.abs(phaseIndex - gameState.phaseIndex) > 1) { + // We're moving more than one Phase, to do so clear the board and reInit the units on the correct phase + gameState.unitAnimations = []; + initUnits(phaseIndex) + updateMapOwnership() + } else { + + } + + + + // Finally, update the gameState with the current phaseIndex + gameState.phaseIndex = phaseIndex + // If we're at the end of the game, don't attempt to animate. + if (phaseIndex === gameLength - 1) { + + } else { + + displayPhase() + } +} + +export function nextPhase() { + _setPhase(gameState.phaseIndex + 1) +} + +export function previousPhase() { + _setPhase(gameState.phaseIndex - 1) +} + /** * Unified function to display a phase with proper transitions * Handles both initial display and animated transitions between phases @@ -86,8 +122,8 @@ export function displayPhase(skipMessages = false) { * Used when first loading a game */ export function displayInitialPhase() { - initUnits(); gameState.phaseIndex = 0; + initUnits(0); displayPhase(true); } @@ -99,17 +135,6 @@ export function displayPhaseWithAnimation() { displayPhase(false); } -// Explicityly sets the phase to a given index, -// Removes and recreates all units. -export function resetToPhase(index: number) { - gameState.phaseIndex = index - gameState.unitAnimations = []; - gameState.unitMeshes.map(unitMesh => gameState.scene.remove(unitMesh)) - - updateMapOwnership() - initUnits() - -} /** * Advances to the next phase in the game sequence @@ -178,7 +203,7 @@ function displayFinalPhase() { // Get the final phase to determine the winner const finalPhase = gameState.gameData.phases[gameState.gameData.phases.length - 1]; - + if (!finalPhase.state?.centers) { logger.log("No supply center data available to determine winner"); return; @@ -187,7 +212,7 @@ function displayFinalPhase() { // Find the power with the most supply centers let winner = ''; let maxCenters = 0; - + for (const [power, centers] of Object.entries(finalPhase.state.centers)) { const centerCount = Array.isArray(centers) ? centers.length : 0; if (centerCount > maxCenters) { @@ -199,13 +224,13 @@ function displayFinalPhase() { // Display victory message if (winner && maxCenters > 0) { const victoryMessage = `🏆 GAME OVER - ${winner} WINS with ${maxCenters} supply centers! 🏆`; - + // Add victory message to news banner with dramatic styling addToNewsBanner(victoryMessage); - + // Log the victory logger.log(`Victory! ${winner} wins the game with ${maxCenters} supply centers.`); - + // Display final standings in console const standings = Object.entries(finalPhase.state.centers) .map(([power, centers]) => ({ @@ -213,7 +238,7 @@ function displayFinalPhase() { centers: Array.isArray(centers) ? centers.length : 0 })) .sort((a, b) => b.centers - a.centers); - + console.log("Final Standings:"); standings.forEach((entry, index) => { const medal = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : " "; diff --git a/ai_animation/src/types/gameState.ts b/ai_animation/src/types/gameState.ts index cd58788..052b3d8 100644 --- a/ai_animation/src/types/gameState.ts +++ b/ai_animation/src/types/gameState.ts @@ -21,7 +21,27 @@ const PhaseSchema = z.object({ messages: z.array(z.any()), name: z.string(), orders: z.record(PowerENUMSchema, z.array(OrderFromString).nullable()), - results: z.record(z.string(), z.array(z.any())), + results: z.record(z.string(), z.array(z.any())).transform((originalResults) => { + // Transform results from {"A BUD": [results]} to {A: {"BUD": [results]}, F: {"BUD": [results]}} + const transformed: { A: Record, F: Record } = { A: {}, F: {} }; + + for (const [key, value] of Object.entries(originalResults)) { + const tokens = key.split(' '); + if (tokens.length >= 2) { + const unitType = tokens[0]; // "A" or "F" + const province = tokens[1].split('/')[0]; // Remove coast specification if present + + if (unitType === 'A' || unitType === 'F') { + if (!transformed[unitType as 'A' | 'F']) { + transformed[unitType as 'A' | 'F'] = {}; + } + transformed[unitType as 'A' | 'F'][province] = value; + } + } + } + + return transformed; + }), state: z.object({ units: z.record(PowerENUMSchema, z.array(z.string())), centers: z.record(PowerENUMSchema, z.array(ProvinceENUMSchema)), diff --git a/ai_animation/src/units/animate.ts b/ai_animation/src/units/animate.ts index 03dc03c..f890229 100644 --- a/ai_animation/src/units/animate.ts +++ b/ai_animation/src/units/animate.ts @@ -94,25 +94,26 @@ export function createAnimationsForNextPhase() { } for (const order of orders) { // Check if unit bounced - let lastPhaseResultMatches = Object.entries(previousPhase.results).filter(([key, _]) => { - return key.split(" ")[1] == order.unit.origin - }).map(val => { - // in the form "A BER" (unitType origin) - let orderSplit = val[0].split(" ") - return { origin: orderSplit[1], unitType: orderSplit[0], result: val[1][0] } - }) - // This should always exist. If we don't have a match here, that means something went wrong with our order parsing - if (!lastPhaseResultMatches) { - throw new Error("No result present in current phase for previous phase order. Cannot continue") + // With new format: {A: {"BUD": [results]}, F: {"BUD": [results]}} + const unitType = order.unit.type; + const unitOrigin = order.unit.origin; + + let result = undefined; + if (previousPhase.results && previousPhase.results[unitType] && previousPhase.results[unitType][unitOrigin]) { + const resultArray = previousPhase.results[unitType][unitOrigin]; + result = resultArray.length > 0 ? resultArray[0] : null; + } - if (lastPhaseResultMatches.length > 1) { - throw new Error("Multiple matching results from last phase. Should only ever be 1.") + + if (result === undefined) { + throw new Error(`No result present in current phase for previous phase order: ${unitType} ${unitOrigin}. Cannot continue`); } - if (lastPhaseResultMatches[0].result === "bounce") { + + if (result === "bounce") { order.type = "bounce" } // If the result is void, that means the move was not valid? - if (lastPhaseResultMatches[0].result === "void") continue; + if (result === "void") continue; let unitIndex = -1 switch (order.type) { @@ -163,10 +164,6 @@ export function createAnimationsForNextPhase() { case "support": break - - - default: - break; } } } diff --git a/ai_animation/src/units/create.ts b/ai_animation/src/units/create.ts index d1c4880..9432416 100644 --- a/ai_animation/src/units/create.ts +++ b/ai_animation/src/units/create.ts @@ -144,11 +144,17 @@ export function createUnitMesh(unitData: UnitData): THREE.Group { return group; } +function _removeUnitsFromBoard() { -// Creates the units for the current gameState.phaseIndex. -export function initUnits() { - createSupplyCenters() - for (const [power, unitArr] of Object.entries(gameState.gameData.phases[gameState.phaseIndex].state.units)) { + gameState.unitMeshes.map((mesh) => gameState.scene.remove(mesh)) +} + +/* + * Given a phaseIndex, Add the units for that phase to the board, in the province specified in the game.json file. + */ +function _addUnitsToBoard(phaseIndex: number) { + _removeUnitsFromBoard() + for (const [power, unitArr] of Object.entries(gameState.gameData.phases[phaseIndex].state.units)) { unitArr.forEach(unitStr => { const match = unitStr.match(/^([AF])\s+(.+)$/); if (match) { @@ -163,3 +169,9 @@ export function initUnits() { }); } } +// Creates the units for the current gameState.phaseIndex. +export function initUnits(phaseIndex: number) { + if (phaseIndex === undefined) throw new Error("Cannot pass undefined phaseIndex"); + createSupplyCenters() + _addUnitsToBoard(phaseIndex) +}