From ac8c5113349d0084c075f1c875f68bb66ae0f913 Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Wed, 4 Jun 2025 09:35:37 -0700 Subject: [PATCH 01/11] Fixing game movement. The git gods appear to have swallowed the code I previously wrote for loadNextGame. Here is a fix for that. The games load in order, from 0 => infinity (more likely max of int). --- ai_animation/src/gameState.ts | 27 +++++++++++------ ai_animation/src/main.ts | 55 +--------------------------------- ai_animation/src/phase.ts | 56 ++++++++++++++++++++++++++++++++++- 3 files changed, 74 insertions(+), 64 deletions(-) diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts index 791418e..8ae6464 100644 --- a/ai_animation/src/gameState.ts +++ b/ai_animation/src/gameState.ts @@ -7,10 +7,10 @@ import { prevBtn, nextBtn, playBtn, speedSelector, mapView, updateGameIdDisplay import { createChatWindows } from "./domElements/chatWindows"; import { logger } from "./logger"; import { OrbitControls } from "three/examples/jsm/Addons.js"; -import { displayInitialPhase } from "./phase"; +import { displayInitialPhase, togglePlayback } from "./phase"; import { Tween, Group as TweenGroup } from "@tweenjs/tween.js"; import { hideStandingsBoard, } from "./domElements/standingsBoard"; -import { MomentsDataSchema, MomentsDataSchemaType, Moment, NormalizedMomentsData } from "./types/moments"; +import { MomentsDataSchema, Moment, NormalizedMomentsData } from "./types/moments"; //FIXME: This whole file is a mess. Need to organize and format @@ -273,18 +273,27 @@ class GameState { * Loads the next game in the order, reseting the board and gameState */ loadNextGame = () => { - // + let gameId = this.gameId + 1 + let contPlaying = false + if (this.isPlaying) { + contPlaying = true + } + this.loadGameFile(gameId).then(() => { - this.gameId += 1 + if (contPlaying) { + togglePlayback(true) + } + } + + ) - // Try to load the next game, if it fails, show end screen forever } /* * Given a gameId, load that game's state into the GameState Object */ - loadGameFile = (gameId: number) => { + loadGameFile = (gameId: number): Promise => { if (gameId === null || gameId < 0) { throw Error(`Attempted to load game with invalid ID ${gameId}`) @@ -292,18 +301,18 @@ class GameState { // Path to the default game file const gameFilePath = `./games/${gameId}/game.json`; - loadFileFromServer(gameFilePath).then((data) => { - this.gameId = gameId + return loadFileFromServer(gameFilePath).then((data) => { return this.loadGameData(data); }) .then(() => { - console.log("Default game file loaded and parsed successfully"); + console.log(`Game file with id ${gameId} loaded and parsed successfully`); // Explicitly hide standings board after loading game hideStandingsBoard(); // Update rotating display and relationship popup with game data if (this.gameData) { updateRotatingDisplay(this.gameData, this.phaseIndex, this.currentPower); + this.gameId = gameId updateGameIdDisplay(); } }) diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index 9eebe84..b5903ea 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -6,7 +6,7 @@ import { logger } from "./logger"; import { loadBtn, prevBtn, nextBtn, speedSelector, fileInput, playBtn, mapView, loadGameBtnFunction } from "./domElements"; import { updateChatWindows } from "./domElements/chatWindows"; import { initStandingsBoard, hideStandingsBoard, showStandingsBoard } from "./domElements/standingsBoard"; -import { displayPhaseWithAnimation, advanceToNextPhase, resetToPhase, nextPhase, previousPhase } from "./phase"; +import { displayPhaseWithAnimation, advanceToNextPhase, resetToPhase, nextPhase, previousPhase, togglePlayback } from "./phase"; import { config } from "./config"; import { Tween, Group, Easing } from "@tweenjs/tween.js"; import { initRotatingDisplay, updateRotatingDisplay } from "./components/rotatingDisplay"; @@ -186,58 +186,6 @@ function onWindowResize() { -// --- PLAYBACK CONTROLS --- -function togglePlayback() { - // If the game doesn't have any data, or there are no phases, return; - if (!gameState.gameData || gameState.gameData.phases.length <= 0) { - alert("This game file appears to be broken. Please reload the page and load a different game.") - throw Error("Bad gameState, exiting.") - }; - - // TODO: Likely not how we want to handle the speaking section of this. - // Should be able to pause the other elements while we're speaking - if (gameState.isSpeaking) return; - - gameState.isPlaying = !gameState.isPlaying; - - if (gameState.isPlaying) { - playBtn.textContent = "⏸ Pause"; - prevBtn.disabled = true; - nextBtn.disabled = true; - logger.log("Starting playback..."); - - if (gameState.cameraPanAnim) gameState.cameraPanAnim.getAll()[1].start() - // Hide standings board when playback starts - hideStandingsBoard(); - - // Update rotating display - if (gameState.gameData) { - updateRotatingDisplay(gameState.gameData, gameState.phaseIndex, gameState.currentPower); - } - - // First, show the messages of the current phase if it's the initial playback - const phase = gameState.gameData.phases[gameState.phaseIndex]; - if (phase.messages && phase.messages.length) { - // Show messages with stepwise animation - logger.log(`Playing ${phase.messages.length} messages from phase ${gameState.phaseIndex + 1}/${gameState.gameData.phases.length}`); - updateChatWindows(phase, true); - } else { - // No messages, go straight to unit animations - logger.log("No messages for this phase, proceeding to animations"); - displayPhaseWithAnimation(); - } - } else { - if (gameState.cameraPanAnim) gameState.cameraPanAnim.getAll()[0].pause(); - playBtn.textContent = "▶ Play"; - if (gameState.playbackTimer) { - clearTimeout(gameState.playbackTimer); - gameState.playbackTimer = null; - } - gameState.messagesPlaying = false; - prevBtn.disabled = false; - nextBtn.disabled = false; - } -} @@ -252,7 +200,6 @@ fileInput.addEventListener('change', e => { // Update rotating display and relationship popup with game data if (gameState.gameData) { updateRotatingDisplay(gameState.gameData, gameState.phaseIndex, gameState.currentPower); - updateRelationshipPopup(); } } }); diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index bbcbaa3..fe5df85 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -1,6 +1,6 @@ import { gameState } from "./gameState"; import { logger } from "./logger"; -import { updatePhaseDisplay } from "./domElements"; +import { updatePhaseDisplay, playBtn, prevBtn, nextBtn } from "./domElements"; import { initUnits } from "./units/create"; import { updateSupplyCenterOwnership, updateLeaderboard, updateMapOwnership as _updateMapOwnership, updateMapOwnership } from "./map/state"; import { updateChatWindows, addToNewsBanner } from "./domElements/chatWindows"; @@ -11,6 +11,7 @@ import { debugMenuInstance } from "./debug/debugMenu"; import { showTwoPowerConversation, closeTwoPowerConversation } from "./components/twoPowerConversation"; import { closeVictoryModal, showVictoryModal } from "./components/victoryModal"; import { notifyPhaseChange } from "./webhooks/phaseNotifier"; +import { updateRotatingDisplay } from "./components/rotatingDisplay"; const MOMENT_THRESHOLD = 8.0 // If we're in debug mode or instant mode, show it quick, otherwise show it for 30 seconds @@ -69,6 +70,59 @@ export function _setPhase(phaseIndex: number) { notifyPhaseChange(oldPhaseIndex, phaseIndex); } +// --- PLAYBACK CONTROLS --- +export function togglePlayback(explicitSet: boolean) { + // If the game doesn't have any data, or there are no phases, return; + if (!gameState.gameData || gameState.gameData.phases.length <= 0) { + alert("This game file appears to be broken. Please reload the page and load a different game.") + throw Error("Bad gameState, exiting.") + }; + + // TODO: Likely not how we want to handle the speaking section of this. + // Should be able to pause the other elements while we're speaking + if (gameState.isSpeaking) return; + + gameState.isPlaying = !gameState.isPlaying; + if (explicitSet !== undefined) { + gameState.isPlaying = explicitSet + } + + if (gameState.isPlaying) { + playBtn.textContent = "⏸ Pause"; + prevBtn.disabled = true; + nextBtn.disabled = true; + logger.log("Starting playback..."); + + if (gameState.cameraPanAnim) gameState.cameraPanAnim.getAll()[1].start() + + // Update rotating display + if (gameState.gameData) { + updateRotatingDisplay(gameState.gameData, gameState.phaseIndex, gameState.currentPower); + } + + // First, show the messages of the current phase if it's the initial playback + const phase = gameState.gameData.phases[gameState.phaseIndex]; + if (phase.messages && phase.messages.length) { + // Show messages with stepwise animation + logger.log(`Playing ${phase.messages.length} messages from phase ${gameState.phaseIndex + 1}/${gameState.gameData.phases.length}`); + updateChatWindows(phase, true); + } else { + // No messages, go straight to unit animations + logger.log("No messages for this phase, proceeding to animations"); + displayPhaseWithAnimation(); + } + } else { + if (gameState.cameraPanAnim) gameState.cameraPanAnim.getAll()[0].pause(); + playBtn.textContent = "▶ Play"; + if (gameState.playbackTimer) { + clearTimeout(gameState.playbackTimer); + gameState.playbackTimer = null; + } + gameState.messagesPlaying = false; + prevBtn.disabled = false; + nextBtn.disabled = false; + } +} export function nextPhase() { From 7fa5672877b4de59060df7e541ea0c68186f1210 Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Wed, 4 Jun 2025 10:32:54 -0700 Subject: [PATCH 02/11] FIX: Promises not rejecting properly causing game to not move Updated some of the start game functionality to properly reject the promises so we can catch them later and update them. Updated tsNoCheck so that we can build without it screaming about type errors --- ai_animation/src/gameState.ts | 56 +++++++++++++++++++---------------- ai_animation/src/phase.ts | 2 +- ai_animation/tsconfig.json | 2 +- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts index 8ae6464..27b4dc0 100644 --- a/ai_animation/src/gameState.ts +++ b/ai_animation/src/gameState.ts @@ -34,23 +34,21 @@ function loadFileFromServer(filePath: string): Promise { fetch(filePath) .then(response => { if (!response.ok) { - alert(`Couldn't load file, received reponse code ${response.status}`) - throw new Error(`Failed to load file: ${response.status}`); + reject(`Failed to load file: ${response.status}`); } // FIXME: This occurs because the server seems to resolve any URL to the homepage. This is the case for Vite's Dev Server. // Check content type to avoid HTML errors const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('text/html')) { - alert(`Unable to load file ${filePath}, was presented HTML, contentType ${contentType}`) - throw new Error('Received HTML instead of JSON. Check the file path.'); + reject('Received HTML instead of JSON. Check the file path.'); } return response.text(); }) .then(data => { // Check for HTML content as a fallback if (data.trim().startsWith(' { + console.warn("caught error trying to advance game. Setting gameId to 0 and restarting...") + this.loadGameFile(0) + if (contPlaying) { + togglePlayback(true) + } + }) } @@ -301,25 +303,29 @@ class GameState { // Path to the default game file const gameFilePath = `./games/${gameId}/game.json`; - return loadFileFromServer(gameFilePath).then((data) => { + return new Promise((resolve, reject) => { + loadFileFromServer(gameFilePath).then((data) => { - return this.loadGameData(data); - }) - .then(() => { - console.log(`Game file with id ${gameId} loaded and parsed successfully`); - // Explicitly hide standings board after loading game - hideStandingsBoard(); - // Update rotating display and relationship popup with game data - if (this.gameData) { - updateRotatingDisplay(this.gameData, this.phaseIndex, this.currentPower); - this.gameId = gameId - updateGameIdDisplay(); - } + return this.loadGameData(data); }) - .catch(error => { - // Use console.error instead of logger.log to avoid updating the info panel - console.error(`Error loading game ${gameFilePath}: ${error.message}`); - }); + .then(() => { + console.log(`Game file with id ${gameId} loaded and parsed successfully`); + // Explicitly hide standings board after loading game + hideStandingsBoard(); + // Update rotating display and relationship popup with game data + if (this.gameData) { + updateRotatingDisplay(this.gameData, this.phaseIndex, this.currentPower); + this.gameId = gameId + updateGameIdDisplay(); + resolve() + } + }) + .catch(error => { + // Use console.error instead of logger.log to avoid updating the info panel + console.error(`Error loading game ${gameFilePath}: ${error}`); + reject() + }); + }) } checkPhaseHasMoment = (phaseName: string): Moment | null => { diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index fe5df85..efb2f0c 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -83,7 +83,7 @@ export function togglePlayback(explicitSet: boolean) { if (gameState.isSpeaking) return; gameState.isPlaying = !gameState.isPlaying; - if (explicitSet !== undefined) { + if (typeof explicitSet === "boolean") { gameState.isPlaying = explicitSet } diff --git a/ai_animation/tsconfig.json b/ai_animation/tsconfig.json index bb15751..3bba997 100644 --- a/ai_animation/tsconfig.json +++ b/ai_animation/tsconfig.json @@ -9,7 +9,7 @@ "DOM.Iterable" ], // Turn the below off later, just for now we're not full TS yet - //"noCheck": true, + "noCheck": true, "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", From 700a5132a12ffa2abd82a1d77ab760268fb05c98 Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Wed, 4 Jun 2025 14:16:44 -0400 Subject: [PATCH 03/11] Fixing some issues with streaming and games not being accessible Signed-off-by: Tyler Marques --- ai_animation/.dockerignore | 3 +++ ai_animation/public/.dockerignore | 1 + ai_animation/reorganize_games.sh | 38 +++++++++++++++++++++++++++++++ ai_animation/vite.config.js | 8 +++---- docker-compose.yaml | 2 ++ 5 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 ai_animation/.dockerignore create mode 100644 ai_animation/public/.dockerignore create mode 100755 ai_animation/reorganize_games.sh diff --git a/ai_animation/.dockerignore b/ai_animation/.dockerignore new file mode 100644 index 0000000..f767f2a --- /dev/null +++ b/ai_animation/.dockerignore @@ -0,0 +1,3 @@ +**/*.zip +./node_modules/ +./public/games/ diff --git a/ai_animation/public/.dockerignore b/ai_animation/public/.dockerignore new file mode 100644 index 0000000..33bb22a --- /dev/null +++ b/ai_animation/public/.dockerignore @@ -0,0 +1 @@ +./games/ diff --git a/ai_animation/reorganize_games.sh b/ai_animation/reorganize_games.sh new file mode 100755 index 0000000..0a3b741 --- /dev/null +++ b/ai_animation/reorganize_games.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Navigate to the games directory +cd "public/games" || exit 1 + +# Initialize counter +counter=0 + +# Process each directory (excluding files) +for dir in */; do + # Remove trailing slash from directory name + dir_name="${dir%/}" + + # Skip if it's not a directory or if it's a numeric directory (already processed) + if [[ ! -d "$dir_name" ]] || [[ "$dir_name" =~ ^[0-9]+$ ]]; then + continue + fi + + echo "Processing: $dir_name" + + # Create empty file with same name as directory in parent directory + touch "../$dir_name" + + # Check if lmvsgame.json exists and rename it to game.json + if [[ -f "$dir_name/lmvsgame.json" ]]; then + mv "$dir_name/lmvsgame.json" "$dir_name/game.json" + echo " Renamed lmvsgame.json to game.json" + fi + + # Rename directory to integer + mv "$dir_name" "$counter" + echo " Renamed directory to: $counter" + + # Increment counter + ((counter++)) +done + +echo "Reorganization complete. Processed $counter directories." \ No newline at end of file diff --git a/ai_animation/vite.config.js b/ai_animation/vite.config.js index 32355d9..dc2ada2 100644 --- a/ai_animation/vite.config.js +++ b/ai_animation/vite.config.js @@ -4,10 +4,10 @@ import { defineConfig, loadEnv } from 'vite'; export default defineConfig(({ mode }) => { // Load environment variables const env = loadEnv(mode, process.cwd(), ''); - + console.log('Environment mode:', mode); console.log('Environment variables loaded:', Object.keys(env).filter(key => key.startsWith('VITE_'))); - + return { // Define environment variables that should be available in the client define: { @@ -19,10 +19,10 @@ export default defineConfig(({ mode }) => { }, // Server configuration "preview": { - "allowedHosts": ["diplomacy"] + "allowedHosts": ["diplomacy", "archlinux"] }, "dev": { - "allowedHosts": ["diplomacy"] + "allowedHosts": ["diplomacy", "archlinux"] } }; }); diff --git a/docker-compose.yaml b/docker-compose.yaml index 933c8f8..1d0172a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,6 +14,8 @@ services: ports: - "4173:4173" - "5173:5173" + volumes: + - ./ai_animation/public/games/:/app/dist/games diplomacy-dev: build: ai_animation ports: From a09ba2743749b479d7e7b05b8069e3496b09cd8f Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Wed, 4 Jun 2025 14:47:46 -0400 Subject: [PATCH 04/11] Fixing wrong name issues Signed-off-by: Tyler Marques --- ai_animation/src/utils/powerNames.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_animation/src/utils/powerNames.ts b/ai_animation/src/utils/powerNames.ts index 4cfdc81..7e9b7e2 100644 --- a/ai_animation/src/utils/powerNames.ts +++ b/ai_animation/src/utils/powerNames.ts @@ -25,7 +25,7 @@ export function getPowerDisplayName(power: PowerENUM | string): string { // Remove the extra long parts of some of the model names e.g. openroute-meta/llama-4-maverick -> llama-4-maverick let slashIdx = modelName?.indexOf("/") if (slashIdx >= 0) { - return modelName.slice(0, slashIdx) + return modelName.slice(slashIdx + 1, modelName.length) } return modelName; } From 3cccbbce6f34d22217dd35cac7eb1ee987149228 Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Wed, 4 Jun 2025 15:38:18 -0400 Subject: [PATCH 05/11] Fixing the phase schedule being called repeatedly --- ai_animation/src/main.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index b5903ea..e17cecf 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -142,10 +142,14 @@ function animate() { gameState.unitAnimations.forEach((anim) => anim.update()) // If all animations are complete and we're in playback mode - if (gameState.unitAnimations.length === 0 && gameState.isPlaying && !gameState.messagesPlaying && !gameState.isSpeaking) { + if (gameState.unitAnimations.length === 0 && gameState.isPlaying && !gameState.messagesPlaying && !gameState.isSpeaking && !gameState.nextPhaseScheduled) { // Schedule next phase after a pause delay console.log(`Scheduling next phase in ${config.effectivePlaybackSpeed}ms`); - gameState.playbackTimer = setTimeout(() => advanceToNextPhase(), config.effectivePlaybackSpeed); + gameState.nextPhaseScheduled = true; + gameState.playbackTimer = setTimeout(() => { + gameState.nextPhaseScheduled = false; + advanceToNextPhase(); + }, config.effectivePlaybackSpeed); } } From e483c82f1043bae2fd3d4094c1ce04a551d3b8d0 Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Wed, 4 Jun 2025 15:44:01 -0400 Subject: [PATCH 06/11] Select only powers that are in end game We were selecting powers to play as randomly, but sometimes they'd lose mid game, making the rest of the game very boring. --- ai_animation/src/gameState.ts | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts index 27b4dc0..cac5d54 100644 --- a/ai_animation/src/gameState.ts +++ b/ai_animation/src/gameState.ts @@ -20,13 +20,36 @@ enum AvailableMaps { /** * Return a random power from the PowerENUM for the player to control + * Only returns powers that have more than 2 supply centers in the last phase */ -function getRandomPower(): PowerENUM { - const values = Object.values(PowerENUM).filter(power => +function getRandomPower(gameData?: GameSchemaType): PowerENUM { + const allPowers = Object.values(PowerENUM).filter(power => power !== PowerENUM.GLOBAL && power !== PowerENUM.EUROPE ); - const idx = Math.floor(Math.random() * values.length); - return values[idx]; + + // If no game data provided, return any random power + if (!gameData || !gameData.phases || gameData.phases.length === 0) { + const idx = Math.floor(Math.random() * allPowers.length); + return allPowers[idx]; + } + + // Get the last phase to check supply centers + const lastPhase = gameData.phases[gameData.phases.length - 1]; + + // Filter powers that have more than 2 supply centers + const eligiblePowers = allPowers.filter(power => { + const centers = lastPhase.state?.centers?.[power]; + return centers && centers.length > 2; + }); + + // If no powers have more than 2 centers, fall back to any power + if (eligiblePowers.length === 0) { + const idx = Math.floor(Math.random() * allPowers.length); + return allPowers[idx]; + } + + const idx = Math.floor(Math.random() * eligiblePowers.length); + return eligiblePowers[idx]; } function loadFileFromServer(filePath: string): Promise { @@ -155,8 +178,8 @@ class GameState { playBtn.disabled = false; speedSelector.disabled = false; - // Set the poewr if the game specifies it, else random. - this.currentPower = this.gameData.power !== undefined ? this.gameData.power : getRandomPower(); + // Set the power if the game specifies it, else random. + this.currentPower = this.gameData.power !== undefined ? this.gameData.power : getRandomPower(this.gameData); const momentsFilePath = `./games/${this.gameId}/moments.json`; From 334950d6748a886c17a38c3bf53df3aba9c4c300 Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Wed, 4 Jun 2025 16:49:33 -0400 Subject: [PATCH 07/11] FIX: Some phase logic was mucked in chatwindows This created some very weird behaviour where it got hung up on certain phases. Removed that logic, as well as extraneous file relationshipPopup.ts --- ai_animation/src/domElements/chatWindows.ts | 116 --------- .../src/domElements/relationshipPopup.ts | 244 ------------------ ai_animation/src/main.ts | 19 +- ai_animation/src/phase.ts | 8 +- 4 files changed, 15 insertions(+), 372 deletions(-) delete mode 100644 ai_animation/src/domElements/relationshipPopup.ts diff --git a/ai_animation/src/domElements/chatWindows.ts b/ai_animation/src/domElements/chatWindows.ts index ade89f3..0f4a0a6 100644 --- a/ai_animation/src/domElements/chatWindows.ts +++ b/ai_animation/src/domElements/chatWindows.ts @@ -178,65 +178,6 @@ export function updateChatWindows(phase: any, stepMessages = false) { } }); gameState.messagesPlaying = false; - - // If instant chat is enabled during stepwise mode, immediately proceed to next phase logic - if (stepMessages && config.isInstantMode) { - // Trigger the same logic as the end of stepwise message display - if (gameState.isPlaying && !gameState.isSpeaking) { - if (gameState.gameData && gameState.gameData.phases) { - const currentPhase = gameState.gameData.phases[gameState.phaseIndex]; - - if (config.isDebugMode) { - console.log(`Instant chat enabled - processing end of phase ${currentPhase.name}`); - } - - // Show summary first if available - if (currentPhase.summary?.trim()) { - addToNewsBanner(`(${currentPhase.name}) ${currentPhase.summary}`); - } - - // Get previous phase for animations - const prevIndex = gameState.phaseIndex > 0 ? gameState.phaseIndex - 1 : null; - const previousPhase = prevIndex !== null ? gameState.gameData.phases[prevIndex] : null; - - // Only schedule next phase if not already scheduled - if (!gameState.nextPhaseScheduled) { - gameState.nextPhaseScheduled = true; - - // Show animations for current phase's orders - if (previousPhase) { - if (config.isDebugMode) { - console.log(`Animating orders from ${previousPhase.name} to ${currentPhase.name}`); - } - - // After animations complete, advance to next phase with longer delay - gameState.playbackTimer = setTimeout(() => { - if (gameState.isPlaying) { - if (config.isDebugMode) { - console.log(`Animations complete, advancing from ${currentPhase.name}`); - } - advanceToNextPhase(); - } - }, config.playbackSpeed + config.animationDuration); // Wait for both summary and animations - } else { - // For first phase, use shorter delay since there are no animations - if (config.isDebugMode) { - console.log(`First phase ${currentPhase.name} - no animations to wait for`); - } - - gameState.playbackTimer = setTimeout(() => { - if (gameState.isPlaying) { - if (config.isDebugMode) { - console.log(`Advancing from first phase ${currentPhase.name}`); - } - advanceToNextPhase(); - } - }, config.playbackSpeed); // Only wait for summary, no animation delay - } - } - } - } - } } else { // Stepwise mode: show one message at a time, animating word-by-word gameState.messagesPlaying = true; @@ -259,64 +200,7 @@ export function updateChatWindows(phase: any, stepMessages = false) { if (config.isDebugMode) { console.log(`All messages displayed in ${Date.now() - messageStartTime}ms`); } - gameState.messagesPlaying = false; - - // Only proceed if we're in playback mode and not speaking - if (gameState.isPlaying && !gameState.isSpeaking) { - if (gameState.gameData && gameState.gameData.phases) { - const currentPhase = gameState.gameData.phases[gameState.phaseIndex]; - - if (config.isDebugMode) { - console.log(`Processing end of phase ${currentPhase.name}`); - } - - // Show summary first if available - if (currentPhase.summary?.trim()) { - addToNewsBanner(`(${currentPhase.name}) ${currentPhase.summary}`); - } - - // Get previous phase for animations - const prevIndex = gameState.phaseIndex > 0 ? gameState.phaseIndex - 1 : null; - const previousPhase = prevIndex !== null ? gameState.gameData.phases[prevIndex] : null; - - // Only schedule next phase if not already scheduled - if (!gameState.nextPhaseScheduled) { - gameState.nextPhaseScheduled = true; - - // Show animations for current phase's orders - if (previousPhase) { - if (config.isDebugMode) { - console.log(`Animating orders from ${previousPhase.name} to ${currentPhase.name}`); - } - - // After animations complete, advance to next phase with longer delay - gameState.playbackTimer = setTimeout(() => { - if (gameState.isPlaying) { - if (config.isDebugMode) { - console.log(`Animations complete, advancing from ${currentPhase.name}`); - } - advanceToNextPhase(); - } - }, config.effectivePlaybackSpeed + config.effectiveAnimationDuration); // Wait for both summary and animations - } else { - // For first phase, use shorter delay since there are no animations - if (config.isDebugMode) { - console.log(`First phase ${currentPhase.name} - no animations to wait for`); - } - - gameState.playbackTimer = setTimeout(() => { - if (gameState.isPlaying) { - if (config.isDebugMode) { - console.log(`Advancing from first phase ${currentPhase.name}`); - } - advanceToNextPhase(); - } - }, config.effectivePlaybackSpeed); // Only wait for summary, no animation delay - } - } - } - } return; } diff --git a/ai_animation/src/domElements/relationshipPopup.ts b/ai_animation/src/domElements/relationshipPopup.ts deleted file mode 100644 index b23f977..0000000 --- a/ai_animation/src/domElements/relationshipPopup.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { relationshipsBtn } from '../domElements'; -import { gameState } from '../gameState'; -import { PowerENUM } from '../types/map'; -import { GameSchemaType } from '../types/gameState'; -import { renderRelationshipHistoryChartView, DisplayType } from '../components/rotatingDisplay'; -import { getPowerDisplayName } from '../utils/powerNames'; - -// DOM element references -let relationshipPopupContainer: HTMLElement | null = null; -let relationshipContent: HTMLElement | null = null; -let closeButton: HTMLElement | null = null; - -/** - * Initialize the relationship popup by creating DOM elements and attaching event handlers - */ -export function initRelationshipPopup(): void { - // Create the container if it doesn't exist - if (!document.getElementById('relationship-popup-container')) { - createRelationshipPopupElements(); - } - - // Get references to the created elements - relationshipPopupContainer = document.getElementById('relationship-popup-container'); - relationshipContent = document.getElementById('relationship-content'); - closeButton = document.getElementById('relationship-close-btn'); - - // Add event listeners - if (closeButton) { - closeButton.addEventListener('click', hideRelationshipPopup); - } - - // Add click handler for the relationships button - if (relationshipsBtn) { - relationshipsBtn.addEventListener('click', toggleRelationshipPopup); - } -} - -/** - * Create all DOM elements needed for the relationship popup - */ -function createRelationshipPopupElements(): void { - const container = document.createElement('div'); - container.id = 'relationship-popup-container'; - container.className = 'relationship-popup-container'; - - // Create header - const header = document.createElement('div'); - header.className = 'relationship-header'; - - const title = document.createElement('h2'); - title.textContent = 'Diplomatic Relations'; - header.appendChild(title); - - const closeBtn = document.createElement('button'); - closeBtn.id = 'relationship-close-btn'; - closeBtn.textContent = '×'; - closeBtn.title = 'Close Relationships Chart'; - header.appendChild(closeBtn); - - container.appendChild(header); - - // Create content container - const content = document.createElement('div'); - content.id = 'relationship-content'; - content.className = 'relationship-content'; - container.appendChild(content); - - // Add to document - document.body.appendChild(container); -} - -/** - * Toggle the visibility of the relationship popup - */ -export function toggleRelationshipPopup(): void { - if (relationshipPopupContainer) { - if (relationshipPopupContainer.classList.contains('visible')) { - hideRelationshipPopup(); - } else { - showRelationshipPopup(); - } - } -} - -/** - * Show the relationship popup - */ -export function showRelationshipPopup(): void { - if (relationshipPopupContainer && relationshipContent) { - relationshipPopupContainer.classList.add('visible'); - - // Only render if we have game data - if (gameState.gameData) { - renderRelationshipChart(); - } else { - relationshipContent.innerHTML = '
No game data loaded. Please load a game to view relationships.
'; - } - } -} - -/** - * Hide the relationship popup - */ -export function hideRelationshipPopup(): void { - if (relationshipPopupContainer) { - relationshipPopupContainer.classList.remove('visible'); - } -} - -/** - * Render the relationship chart in the popup - */ -function renderRelationshipChart(): void { - if (!relationshipContent || !gameState.gameData) return; - - // Clear current content - relationshipContent.innerHTML = ''; - - // Get a list of powers that have relationship data - const powersWithRelationships = new Set(); - - // Check all phases for relationships - if (gameState.gameData && gameState.gameData.phases) { - // Debug what relationship data we have - - let hasRelationshipData = false; - for (const phase of gameState.gameData.phases) { - if (phase.agent_relationships) { - hasRelationshipData = true; - // Add powers that have relationship data defined - Object.keys(phase.agent_relationships).forEach(power => { - powersWithRelationships.add(power); - }); - } - } - - if (!hasRelationshipData) { - console.log("No relationship data found in any phase"); - } - } - - // Create a container for each power's relationship chart - for (const power of Object.values(PowerENUM)) { - // Skip any non-string values - if (typeof power !== 'string') continue; - - // Check if this power has relationship data - if (powersWithRelationships.has(power)) { - const powerContainer = document.createElement('div'); - powerContainer.className = `power-relationship-container power-${power.toLowerCase()}`; - - const powerHeader = document.createElement('h3'); - powerHeader.className = `power-${power.toLowerCase()}`; - powerHeader.textContent = getPowerDisplayName(power as PowerENUM); - powerContainer.appendChild(powerHeader); - - const chartContainer = document.createElement('div'); - chartContainer.className = 'relationship-chart-container'; - - // Use the existing chart rendering function - renderRelationshipHistoryChartView( - chartContainer, - gameState.gameData, - gameState.phaseIndex, - power as PowerENUM - ); - - powerContainer.appendChild(chartContainer); - relationshipContent.appendChild(powerContainer); - } - } - - // If no powers have relationship data, create message to say so - if (powersWithRelationships.size === 0) { - console.log("No relationship data found in game"); - - // Create sample relationship data for all powers in the game - const allPowers = new Set(); - - // Find all powers from units and centers - if (gameState.gameData && gameState.gameData.phases && gameState.gameData.phases.length > 0) { - const currentPhase = gameState.gameData.phases[gameState.phaseIndex]; - - if (currentPhase.state?.units) { - Object.keys(currentPhase.state.units).forEach(power => allPowers.add(power)); - } - - if (currentPhase.state?.centers) { - Object.keys(currentPhase.state.centers).forEach(power => allPowers.add(power)); - } - - // Only proceed if we found some powers - if (allPowers.size > 0) { - console.log(`Found ${allPowers.size} powers in game, creating sample relationships`); - - // For each power, create a container and chart - for (const power of allPowers) { - const powerContainer = document.createElement('div'); - powerContainer.className = `power-relationship-container power-${power.toLowerCase()}`; - - const powerHeader = document.createElement('h3'); - powerHeader.className = `power-${power.toLowerCase()}`; - powerHeader.textContent = getPowerDisplayName(power as PowerENUM); - powerContainer.appendChild(powerHeader); - - const chartContainer = document.createElement('div'); - chartContainer.className = 'relationship-chart-container'; - - // Create a message about sample data - const sampleMessage = document.createElement('div'); - sampleMessage.className = 'sample-data-message'; - sampleMessage.innerHTML = `Note: No relationship data found for ${power}. - This chart will display when relationship data is available.`; - - chartContainer.appendChild(sampleMessage); - powerContainer.appendChild(chartContainer); - relationshipContent.appendChild(powerContainer); - } - } else { - // If we couldn't find any powers, show the no data message - const noDataMsg = document.createElement('div'); - noDataMsg.className = 'no-data-message'; - noDataMsg.textContent = 'No relationship data available in this game file.'; - relationshipContent.appendChild(noDataMsg); - } - } else { - // If no phases, show the no data message - const noDataMsg = document.createElement('div'); - noDataMsg.className = 'no-data-message'; - noDataMsg.textContent = 'No relationship data available in this game file.'; - relationshipContent.appendChild(noDataMsg); - } - } -} - -/** - * Update the relationship popup when game data changes - */ -export function updateRelationshipPopup(): void { - if (relationshipPopupContainer && - relationshipPopupContainer.classList.contains('visible')) { - renderRelationshipChart(); - } -} diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index e17cecf..2a2936c 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -141,18 +141,17 @@ function animate() { // Call update on each active animation gameState.unitAnimations.forEach((anim) => anim.update()) - // If all animations are complete and we're in playback mode - if (gameState.unitAnimations.length === 0 && gameState.isPlaying && !gameState.messagesPlaying && !gameState.isSpeaking && !gameState.nextPhaseScheduled) { - // Schedule next phase after a pause delay - console.log(`Scheduling next phase in ${config.effectivePlaybackSpeed}ms`); - gameState.nextPhaseScheduled = true; - gameState.playbackTimer = setTimeout(() => { - gameState.nextPhaseScheduled = false; - advanceToNextPhase(); - }, config.effectivePlaybackSpeed); - } } + // If all animations are complete and we're in playback mode + if (gameState.unitAnimations.length === 0 && gameState.isPlaying && !gameState.messagesPlaying && !gameState.isSpeaking && !gameState.nextPhaseScheduled) { + // Schedule next phase after a pause delay + console.log(`Scheduling next phase in ${config.effectivePlaybackSpeed}ms`); + gameState.nextPhaseScheduled = true; + gameState.playbackTimer = setTimeout(() => { + advanceToNextPhase(); + }, config.effectivePlaybackSpeed); + } // Update any pulsing or wave animations on supply centers or units if (gameState.scene.userData.animatedObjects) { gameState.scene.userData.animatedObjects.forEach(obj => { diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index efb2f0c..318d146 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -61,6 +61,7 @@ export function _setPhase(phaseIndex: number) { } else { displayPhase() } + gameState.nextPhaseScheduled = false; } // Finally, update the gameState with the current phaseIndex @@ -261,8 +262,6 @@ export function advanceToNextPhase() { logger.log("Cannot advance phase: invalid game state"); return; } - // Reset the nextPhaseScheduled flag to allow scheduling the next phase - gameState.nextPhaseScheduled = false; // Get current phase const currentPhase = gameState.gameData.phases[gameState.phaseIndex]; @@ -296,6 +295,8 @@ export function advanceToNextPhase() { if (gameState.isPlaying) { nextPhase(); } + }).finally(() => { + }); } else { console.error("Attempted to start speaking when already speaking...") @@ -305,6 +306,9 @@ export function advanceToNextPhase() { // No summary to speak, advance immediately nextPhase(); } + + // Reset the nextPhaseScheduled flag to allow scheduling the next phase + gameState.nextPhaseScheduled = false; } function displayFinalPhase() { From d3f78272873267499d43197efb3bf6a8585684d4 Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Wed, 4 Jun 2025 14:47:52 -0700 Subject: [PATCH 08/11] Setting keyframe rate to ever 60 frames (2 seconds at 30fps) Signed-off-by: Tyler Marques --- twitch-streamer/entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/twitch-streamer/entrypoint.sh b/twitch-streamer/entrypoint.sh index 1fa2fcc..b340ac4 100755 --- a/twitch-streamer/entrypoint.sh +++ b/twitch-streamer/entrypoint.sh @@ -41,6 +41,7 @@ sleep 5 # let the page load or animations start # - For audio: pulse from the "default" device # Adjust your bitrate, resolution, frame rate, etc. as desired. exec ffmpeg -y \ + -g 60 \ -f x11grab -thread_queue_size 512 -r 30 -s 1920x1080 -i $DISPLAY \ -f pulse -thread_queue_size 512 -i default \ -c:v libx264 -preset veryfast -b:v 6000k -maxrate 6000k -bufsize 12000k \ From 061a048dc617f0da8cf519e413b2d44d5d1fd460 Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Wed, 4 Jun 2025 15:17:09 -0700 Subject: [PATCH 09/11] Removing the duplicated banner add call Signed-off-by: Tyler Marques --- ai_animation/src/phase.ts | 6 +----- twitch-streamer/entrypoint.sh | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index 318d146..2ad090e 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -201,7 +201,7 @@ export function displayPhase(skipMessages = false) { _updateMapOwnership(); // Add phase info to news banner if not already there - const phaseBannerText = `Phase: ${currentPhase.name}`; + const phaseBannerText = `Phase: ${currentPhase.name}: ${currentPhase.summary}`; addToNewsBanner(phaseBannerText); // Log phase details to console only, don't update info panel with this @@ -277,10 +277,6 @@ export function advanceToNextPhase() { // First show summary if available if (currentPhase.summary && currentPhase.summary.trim() !== '') { - // Update the news banner with full summary - addToNewsBanner(`(${currentPhase.name}) ${currentPhase.summary}`); - console.log("Added summary to news banner, preparing to call speakSummary"); - // Speak the summary and advance after if (!gameState.isSpeaking) { speakSummary(currentPhase.summary) diff --git a/twitch-streamer/entrypoint.sh b/twitch-streamer/entrypoint.sh index b340ac4..1fa2fcc 100755 --- a/twitch-streamer/entrypoint.sh +++ b/twitch-streamer/entrypoint.sh @@ -41,7 +41,6 @@ sleep 5 # let the page load or animations start # - For audio: pulse from the "default" device # Adjust your bitrate, resolution, frame rate, etc. as desired. exec ffmpeg -y \ - -g 60 \ -f x11grab -thread_queue_size 512 -r 30 -s 1920x1080 -i $DISPLAY \ -f pulse -thread_queue_size 512 -i default \ -c:v libx264 -preset veryfast -b:v 6000k -maxrate 6000k -bufsize 12000k \ From d3d8b2242816b6c7a41f30344e274daf4ef7ccd3 Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Wed, 4 Jun 2025 15:44:13 -0700 Subject: [PATCH 10/11] O4 tries to fix stream performance Signed-off-by: Tyler Marques --- docker-compose.yaml | 2 ++ twitch-streamer/entrypoint.sh | 19 +++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 1d0172a..1342c25 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,6 +8,8 @@ services: - DISPLAY=:99 ports: - "9222:9222" + ipc: host + shm_size: "1gb" diplomacy: build: ai_animation diff --git a/twitch-streamer/entrypoint.sh b/twitch-streamer/entrypoint.sh index 1fa2fcc..93a3e75 100755 --- a/twitch-streamer/entrypoint.sh +++ b/twitch-streamer/entrypoint.sh @@ -20,15 +20,15 @@ sleep 2 mkdir -p /home/chrome # Launch Chrome in the background, pointing at your site. -# --app=... to open it as a single-window "app" -# --no-sandbox / --disable-gpu often needed in Docker -# --use-fake-device-for-media-stream / etc. if you need to simulate mic/cam +# --disable-background-timer-throttling & related flags to prevent fps throttling in headless/Xvfb DISPLAY=$DISPLAY google-chrome \ --remote-debugging-port=9222 \ --disable-gpu \ - --disable-dev-shm-usage \ - --no-first-run \ --disable-infobars \ + --no-first-run \ + --disable-background-timer-throttling \ + --disable-renderer-backgrounding \ + --disable-backgrounding-occluded-windows \ --user-data-dir=/home/chrome/chrome-data \ --window-size=1920,1080 --window-position=0,0 \ --kiosk \ @@ -37,13 +37,12 @@ DISPLAY=$DISPLAY google-chrome \ sleep 5 # let the page load or animations start # Start streaming with FFmpeg. -# - For video: x11grab from DISPLAY -# - For audio: pulse from the "default" device -# Adjust your bitrate, resolution, frame rate, etc. as desired. +# - For video: x11grab at 30fps +# - For audio: pulse from the default device exec ffmpeg -y \ - -f x11grab -thread_queue_size 512 -r 30 -s 1920x1080 -i $DISPLAY \ + -f x11grab -video_size 1920x1080 -framerate 30 -thread_queue_size 512 -i $DISPLAY \ -f pulse -thread_queue_size 512 -i default \ - -c:v libx264 -preset veryfast -b:v 6000k -maxrate 6000k -bufsize 12000k \ + -c:v libx264 -preset ultrafast -b:v 6000k -maxrate 6000k -bufsize 12000k \ -pix_fmt yuv420p \ -c:a aac -b:a 160k \ -vsync 1 -async 1 \ From 4e3d6ba204c41d9c38c82bc43d7174a6cdefb194 Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Wed, 4 Jun 2025 19:28:50 -0700 Subject: [PATCH 11/11] FIX: Catching bad next phase Sometimes there is an animation missing, or we can't find the unit we expect to find for the animation, and it throws an error. This was cancelling the move forward. "Fixing" with a catch for now. --- ai_animation/src/main.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index 2a2936c..fa917a8 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -149,7 +149,14 @@ function animate() { console.log(`Scheduling next phase in ${config.effectivePlaybackSpeed}ms`); gameState.nextPhaseScheduled = true; gameState.playbackTimer = setTimeout(() => { - advanceToNextPhase(); + try { + advanceToNextPhase() + } catch { + // FIXME: This is a dumb patch for us not being able to find the unit we expect to find. + // We should instead bee figuring out why units aren't where we expect them to be when the engine has said that is a valid move + nextPhase() + gameState.nextPhaseScheduled; + } }, config.effectivePlaybackSpeed); } // Update any pulsing or wave animations on supply centers or units