diff --git a/ai_animation/.gitignore b/ai_animation/.gitignore
index 789ec75..f62b80a 100644
--- a/ai_animation/.gitignore
+++ b/ai_animation/.gitignore
@@ -25,3 +25,5 @@ dist-ssr
# AI things
.claude/
+
+./public/games/
diff --git a/ai_animation/README.md b/ai_animation/README.md
index acdc328..7c78a6d 100644
--- a/ai_animation/README.md
+++ b/ai_animation/README.md
@@ -20,33 +20,43 @@ The turn animation system is built around several key components that work toget
The turn advancement follows a carefully orchestrated sequence:
#### 1. Playback Initiation
+
When the user clicks Play, `togglePlayback()` is triggered, which:
+
- Sets `gameState.isPlaying = true`
- Hides the standings board
- Starts the camera pan animation
- Begins message display for the current phase
#### 2. Message Animation Phase
+
If the current phase has messages:
+
- `updateChatWindows()` displays messages word-by-word
- Each message appears with typing animation
- `gameState.messagesPlaying` tracks this state
#### 3. Unit Animation Phase
+
Once messages complete (or if there are no messages):
+
- `displayPhaseWithAnimation()` is called
- `createAnimationsForNextPhase()` analyzes the previous phase's orders
- Movement tweens are created for each unit based on order results
- Animations are added to `gameState.unitAnimations` array
#### 4. Animation Monitoring
+
The main `animate()` loop continuously:
+
- Updates all active unit animations
- Filters out completed animations
- Detects when `gameState.unitAnimations.length === 0`
#### 5. Phase Transition
+
When all animations complete:
+
- `advanceToNextPhase()` is scheduled with a configurable delay
- If the phase has a summary, text-to-speech is triggered
- After speech completes, `moveToNextPhase()` increments the phase index
@@ -124,4 +134,7 @@ This architecture ensures smooth, coordinated animations while maintaining clear
## Game Data
-Game data is loaded from JSON files in the `public/games/` directory. The expected format includes phases with messages, orders, and state information for each turn of the Diplomacy game.
\ No newline at end of file
+Game data is loaded from JSON files in the `public/games/` directory. The expected format includes phases with messages, orders, and state information for each turn of the Diplomacy game.
+
+--TODO: Create something to combine the game data into a simpler place, Diary is in csv and I need that to display the thoughts of the LLM during the betrayal scenes
+
diff --git a/ai_animation/index.html b/ai_animation/index.html
index 3f22307..ef84066 100644
--- a/ai_animation/index.html
+++ b/ai_animation/index.html
@@ -27,6 +27,7 @@
No game loaded
+ Game: --
diff --git a/ai_animation/show_conversation.sh b/ai_animation/show_conversation.sh
new file mode 100755
index 0000000..f0632ad
--- /dev/null
+++ b/ai_animation/show_conversation.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+
+# Script to show conversation between two powers in a specific phase
+# Usage: ./show_conversation.sh [game_file]
+
+if [ $# -lt 3 ]; then
+ echo "Usage: $0 [game_file]"
+ echo "Example: $0 FRANCE ENGLAND S1901M"
+ echo "Example: $0 FRANCE ENGLAND S1901M public/games/0/game.json"
+ exit 1
+fi
+
+POWER1="$1"
+POWER2="$2"
+PHASE="$3"
+GAME_FILE="${4:-public/default_game_formatted.json}"
+
+# Check if game file exists
+if [ ! -f "$GAME_FILE" ]; then
+ echo "Error: Game file '$GAME_FILE' not found"
+ exit 1
+fi
+
+# Check if jq is installed
+if ! command -v jq &>/dev/null; then
+ echo "Error: jq is required but not installed"
+ exit 1
+fi
+
+echo "=== Conversation between $POWER1 and $POWER2 in phase $PHASE ==="
+echo "Game file: $GAME_FILE"
+echo ""
+
+# Extract messages between the two powers in the specified phase, sorted by time
+jq -r --arg power1 "$POWER1" --arg power2 "$POWER2" --arg phase "$PHASE" '
+ .phases[] |
+ select(.name == $phase) |
+ .messages[] |
+ select(
+ (.sender == $power1 and .recipient == $power2) or
+ (.sender == $power2 and .recipient == $power1)
+ ) |
+ . as $msg |
+ "\($msg.time_sent) \($msg.sender) -> \($msg.recipient): \($msg.message)\n"
+' "$GAME_FILE" | sort -n #| sed 's/^[0-9]* //'
+
+# Check if any messages were found
+if [ ${PIPESTATUS[0]} -eq 0 ]; then
+ message_count=$(jq --arg power1 "$POWER1" --arg power2 "$POWER2" --arg phase "$PHASE" '
+ .phases[] |
+ select(.name == $phase) |
+ .messages[] |
+ select(
+ (.sender == $power1 and .recipient == $power2) or
+ (.sender == $power2 and .recipient == $power1)
+ )
+ ' "$GAME_FILE" | jq -s 'length')
+
+ echo ""
+ echo "Found $message_count messages in phase $PHASE between $POWER1 and $POWER2"
+else
+ echo "No messages found between $POWER1 and $POWER2 in phase $PHASE"
+fi
+
diff --git a/ai_animation/src/domElements.ts b/ai_animation/src/domElements.ts
index e3b4445..630a935 100644
--- a/ai_animation/src/domElements.ts
+++ b/ai_animation/src/domElements.ts
@@ -15,6 +15,21 @@ export function updatePhaseDisplay() {
phaseDisplay.style.opacity = '1';
}, 300);
}
+
+export function updateGameIdDisplay() {
+ if (!gameIdDisplay) return;
+
+ // Add fade-out effect
+ gameIdDisplay.style.transition = 'opacity 0.3s ease-out';
+ gameIdDisplay.style.opacity = '0';
+
+ // Update text after fade-out
+ setTimeout(() => {
+ gameIdDisplay.textContent = `Game: ${gameState.gameId}`;
+ // Fade back in
+ gameIdDisplay.style.opacity = '1';
+ }, 300);
+}
// --- LOADING & DISPLAYING GAME PHASES ---
export function loadGameBtnFunction(file) {
const reader = new FileReader();
@@ -27,16 +42,41 @@ export function loadGameBtnFunction(file) {
reader.readAsText(file);
}
export const loadBtn = document.getElementById('load-btn');
+if (null === loadBtn) throw new Error("Element with ID 'load-btn' not found");
+
export const fileInput = document.getElementById('file-input');
+if (null === fileInput) throw new Error("Element with ID 'file-input' not found");
+
export const prevBtn = document.getElementById('prev-btn');
+if (null === prevBtn) throw new Error("Element with ID 'prev-btn' not found");
+
export const nextBtn = document.getElementById('next-btn');
+if (null === nextBtn) throw new Error("Element with ID 'next-btn' not found");
+
export const playBtn = document.getElementById('play-btn');
+if (null === playBtn) throw new Error("Element with ID 'play-btn' not found");
+
export const speedSelector = document.getElementById('speed-selector');
+if (null === speedSelector) throw new Error("Element with ID 'speed-selector' not found");
+
export const phaseDisplay = document.getElementById('phase-display');
+if (null === phaseDisplay) throw new Error("Element with ID 'phase-display' not found");
+
+export const gameIdDisplay = document.getElementById('game-id-display');
+if (null === gameIdDisplay) throw new Error("Element with ID 'game-id-display' not found");
+
export const mapView = document.getElementById('map-view');
+if (null === mapView) throw new Error("Element with ID 'map-view' not found");
+
export const leaderboard = document.getElementById('leaderboard');
+if (null === leaderboard) throw new Error("Element with ID 'leaderboard' not found");
+
export const standingsBtn = document.getElementById('standings-btn');
+if (null === standingsBtn) throw new Error("Element with ID 'standings-btn' not found");
+
export const relationshipsBtn = document.getElementById('relationships-btn');
+if (null === relationshipsBtn) throw new Error("Element with ID 'relationships-btn' not found");
+
// Add roundRect polyfill for browsers that don't support it
if (!CanvasRenderingContext2D.prototype.roundRect) {
diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts
index aa99944..26f17ea 100644
--- a/ai_animation/src/gameState.ts
+++ b/ai_animation/src/gameState.ts
@@ -4,16 +4,16 @@ import { updateRelationshipPopup } from "./domElements/relationshipPopup";
import { type CoordinateData, CoordinateDataSchema, PowerENUM } from "./types/map"
import type { GameSchemaType } from "./types/gameState";
import { GameSchema } from "./types/gameState";
-import { prevBtn, nextBtn, playBtn, speedSelector, mapView } from "./domElements";
+import { prevBtn, nextBtn, playBtn, speedSelector, mapView, updateGameIdDisplay } from "./domElements";
import { createChatWindows } from "./domElements/chatWindows";
import { logger } from "./logger";
import { OrbitControls } from "three/examples/jsm/Addons.js";
import { displayInitialPhase } from "./phase";
import { Tween, Group as TweenGroup } from "@tweenjs/tween.js";
import { hideStandingsBoard, } from "./domElements/standingsBoard";
-import { MomentsDataSchema, MomentsMetadataSchema } from "./types/moments";
+import { MomentsDataSchema, MomentsDataSchemaType } from "./types/moments";
-//FIXME: This whole file is a mess. Need to organize and format
+//FIXME: This whole file is a mess. Need to organkze and format
enum AvailableMaps {
STANDARD = "standard"
@@ -34,7 +34,7 @@ class GameState {
boardState: CoordinateData
gameId: number
gameData: GameSchemaType
- momentsData: MomentSchemaType
+ momentsData: MomentsDataSchemaType
phaseIndex: number
boardName: string
currentPower: PowerENUM
@@ -133,6 +133,9 @@ class GameState {
// Display the initial phase
displayInitialPhase()
+ // Update game ID display
+ updateGameIdDisplay();
+
this.loadMomentsFile()
resolve()
} else {
@@ -204,6 +207,18 @@ class GameState {
return false;
}
+ /*
+ * Loads the next game in the order, reseting the board and gameState
+ */
+ loadNextGame = () => {
+ //
+
+ this.gameId += 1
+
+ // 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
*/
@@ -251,6 +266,7 @@ class GameState {
if (this.gameData) {
updateRotatingDisplay(this.gameData, this.phaseIndex, this.currentPower);
updateRelationshipPopup();
+ updateGameIdDisplay();
}
})
.catch(error => {
@@ -304,7 +320,7 @@ class GameState {
})
}
- initScene = () => {
+ createThreeScene = () => {
if (mapView === null) {
throw Error("Cannot find mapView element, unable to continue.")
}
@@ -332,6 +348,14 @@ class GameState {
this.camControls.maxDistance = 2000;
this.camControls.maxPolarAngle = Math.PI / 2; // Limit so you don't flip under the map
this.camControls.target.set(0, 0, 100); // ADDED: Set control target to new map center
+
+
+ // Lighting (keep it simple)
+ this.scene.add(new THREE.AmbientLight(0xffffff, 0.6));
+
+ const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
+ dirLight.position.set(300, 400, 300);
+ this.scene.add(dirLight);
}
}
diff --git a/ai_animation/src/style.css b/ai_animation/src/style.css
index 16726b4..88140de 100644
--- a/ai_animation/src/style.css
+++ b/ai_animation/src/style.css
@@ -28,7 +28,7 @@
background: linear-gradient(90deg, #5a3e2b 0%, #382519 100%);
color: #f0e6d2;
display: flex;
- justify-content: space-between;
+ justify-content: flex-start;
align-items: center;
border-bottom: 3px solid #2e1c10;
}
@@ -46,6 +46,18 @@
font-size: 1rem;
}
+ #game-id-display {
+ font-weight: bold;
+ margin-left: auto;
+ padding: 4px 8px;
+ border: 2px solid #2e1c10;
+ border-radius: 6px;
+ background-color: rgba(0, 0, 0, 0.3);
+ color: #ffd;
+ font-family: "Book Antiqua", Palatino, serif;
+ font-size: 1rem;
+ }
+
/* Buttons (Load Game, Next, Prev, etc.) */
button {
padding: 8px 16px;