mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-28 17:29:41 +00:00
Adding a GameId to allow moving through a list of games.
This commit is contained in:
parent
9314a411f9
commit
9a43be9b9c
7 changed files with 163 additions and 7 deletions
2
ai_animation/.gitignore
vendored
2
ai_animation/.gitignore
vendored
|
|
@ -25,3 +25,5 @@ dist-ssr
|
||||||
|
|
||||||
# AI things
|
# AI things
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
./public/games/
|
||||||
|
|
|
||||||
|
|
@ -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:
|
The turn advancement follows a carefully orchestrated sequence:
|
||||||
|
|
||||||
#### 1. Playback Initiation
|
#### 1. Playback Initiation
|
||||||
|
|
||||||
When the user clicks Play, `togglePlayback()` is triggered, which:
|
When the user clicks Play, `togglePlayback()` is triggered, which:
|
||||||
|
|
||||||
- Sets `gameState.isPlaying = true`
|
- Sets `gameState.isPlaying = true`
|
||||||
- Hides the standings board
|
- Hides the standings board
|
||||||
- Starts the camera pan animation
|
- Starts the camera pan animation
|
||||||
- Begins message display for the current phase
|
- Begins message display for the current phase
|
||||||
|
|
||||||
#### 2. Message Animation Phase
|
#### 2. Message Animation Phase
|
||||||
|
|
||||||
If the current phase has messages:
|
If the current phase has messages:
|
||||||
|
|
||||||
- `updateChatWindows()` displays messages word-by-word
|
- `updateChatWindows()` displays messages word-by-word
|
||||||
- Each message appears with typing animation
|
- Each message appears with typing animation
|
||||||
- `gameState.messagesPlaying` tracks this state
|
- `gameState.messagesPlaying` tracks this state
|
||||||
|
|
||||||
#### 3. Unit Animation Phase
|
#### 3. Unit Animation Phase
|
||||||
|
|
||||||
Once messages complete (or if there are no messages):
|
Once messages complete (or if there are no messages):
|
||||||
|
|
||||||
- `displayPhaseWithAnimation()` is called
|
- `displayPhaseWithAnimation()` is called
|
||||||
- `createAnimationsForNextPhase()` analyzes the previous phase's orders
|
- `createAnimationsForNextPhase()` analyzes the previous phase's orders
|
||||||
- Movement tweens are created for each unit based on order results
|
- Movement tweens are created for each unit based on order results
|
||||||
- Animations are added to `gameState.unitAnimations` array
|
- Animations are added to `gameState.unitAnimations` array
|
||||||
|
|
||||||
#### 4. Animation Monitoring
|
#### 4. Animation Monitoring
|
||||||
|
|
||||||
The main `animate()` loop continuously:
|
The main `animate()` loop continuously:
|
||||||
|
|
||||||
- Updates all active unit animations
|
- Updates all active unit animations
|
||||||
- Filters out completed animations
|
- Filters out completed animations
|
||||||
- Detects when `gameState.unitAnimations.length === 0`
|
- Detects when `gameState.unitAnimations.length === 0`
|
||||||
|
|
||||||
#### 5. Phase Transition
|
#### 5. Phase Transition
|
||||||
|
|
||||||
When all animations complete:
|
When all animations complete:
|
||||||
|
|
||||||
- `advanceToNextPhase()` is scheduled with a configurable delay
|
- `advanceToNextPhase()` is scheduled with a configurable delay
|
||||||
- If the phase has a summary, text-to-speech is triggered
|
- If the phase has a summary, text-to-speech is triggered
|
||||||
- After speech completes, `moveToNextPhase()` increments the phase index
|
- After speech completes, `moveToNextPhase()` increments the phase index
|
||||||
|
|
@ -125,3 +135,6 @@ This architecture ensures smooth, coordinated animations while maintaining clear
|
||||||
## Game Data
|
## 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.
|
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
</select>
|
</select>
|
||||||
<span id="phase-display">No game loaded</span>
|
<span id="phase-display">No game loaded</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span id="game-id-display">Game: --</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="map-view" class="map-view"></div>
|
<div id="map-view" class="map-view"></div>
|
||||||
<input type="file" id="file-input" accept=".json">
|
<input type="file" id="file-input" accept=".json">
|
||||||
|
|
|
||||||
64
ai_animation/show_conversation.sh
Executable file
64
ai_animation/show_conversation.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to show conversation between two powers in a specific phase
|
||||||
|
# Usage: ./show_conversation.sh <power1> <power2> <phase> [game_file]
|
||||||
|
|
||||||
|
if [ $# -lt 3 ]; then
|
||||||
|
echo "Usage: $0 <power1> <power2> <phase> [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
|
||||||
|
|
||||||
|
|
@ -15,6 +15,21 @@ export function updatePhaseDisplay() {
|
||||||
phaseDisplay.style.opacity = '1';
|
phaseDisplay.style.opacity = '1';
|
||||||
}, 300);
|
}, 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 ---
|
// --- LOADING & DISPLAYING GAME PHASES ---
|
||||||
export function loadGameBtnFunction(file) {
|
export function loadGameBtnFunction(file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
@ -27,16 +42,41 @@ export function loadGameBtnFunction(file) {
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}
|
}
|
||||||
export const loadBtn = document.getElementById('load-btn');
|
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');
|
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');
|
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');
|
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');
|
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');
|
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');
|
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');
|
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');
|
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');
|
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');
|
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
|
// Add roundRect polyfill for browsers that don't support it
|
||||||
if (!CanvasRenderingContext2D.prototype.roundRect) {
|
if (!CanvasRenderingContext2D.prototype.roundRect) {
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,16 @@ import { updateRelationshipPopup } from "./domElements/relationshipPopup";
|
||||||
import { type CoordinateData, CoordinateDataSchema, PowerENUM } from "./types/map"
|
import { type CoordinateData, CoordinateDataSchema, PowerENUM } from "./types/map"
|
||||||
import type { GameSchemaType } from "./types/gameState";
|
import type { GameSchemaType } from "./types/gameState";
|
||||||
import { GameSchema } 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 { createChatWindows } from "./domElements/chatWindows";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { OrbitControls } from "three/examples/jsm/Addons.js";
|
import { OrbitControls } from "three/examples/jsm/Addons.js";
|
||||||
import { displayInitialPhase } from "./phase";
|
import { displayInitialPhase } from "./phase";
|
||||||
import { Tween, Group as TweenGroup } from "@tweenjs/tween.js";
|
import { Tween, Group as TweenGroup } from "@tweenjs/tween.js";
|
||||||
import { hideStandingsBoard, } from "./domElements/standingsBoard";
|
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 {
|
enum AvailableMaps {
|
||||||
STANDARD = "standard"
|
STANDARD = "standard"
|
||||||
|
|
@ -34,7 +34,7 @@ class GameState {
|
||||||
boardState: CoordinateData
|
boardState: CoordinateData
|
||||||
gameId: number
|
gameId: number
|
||||||
gameData: GameSchemaType
|
gameData: GameSchemaType
|
||||||
momentsData: MomentSchemaType
|
momentsData: MomentsDataSchemaType
|
||||||
phaseIndex: number
|
phaseIndex: number
|
||||||
boardName: string
|
boardName: string
|
||||||
currentPower: PowerENUM
|
currentPower: PowerENUM
|
||||||
|
|
@ -133,6 +133,9 @@ class GameState {
|
||||||
// Display the initial phase
|
// Display the initial phase
|
||||||
displayInitialPhase()
|
displayInitialPhase()
|
||||||
|
|
||||||
|
// Update game ID display
|
||||||
|
updateGameIdDisplay();
|
||||||
|
|
||||||
this.loadMomentsFile()
|
this.loadMomentsFile()
|
||||||
resolve()
|
resolve()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -204,6 +207,18 @@ class GameState {
|
||||||
return false;
|
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
|
* Given a gameId, load that game's state into the GameState Object
|
||||||
*/
|
*/
|
||||||
|
|
@ -251,6 +266,7 @@ class GameState {
|
||||||
if (this.gameData) {
|
if (this.gameData) {
|
||||||
updateRotatingDisplay(this.gameData, this.phaseIndex, this.currentPower);
|
updateRotatingDisplay(this.gameData, this.phaseIndex, this.currentPower);
|
||||||
updateRelationshipPopup();
|
updateRelationshipPopup();
|
||||||
|
updateGameIdDisplay();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|
@ -304,7 +320,7 @@ class GameState {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
initScene = () => {
|
createThreeScene = () => {
|
||||||
if (mapView === null) {
|
if (mapView === null) {
|
||||||
throw Error("Cannot find mapView element, unable to continue.")
|
throw Error("Cannot find mapView element, unable to continue.")
|
||||||
}
|
}
|
||||||
|
|
@ -332,6 +348,14 @@ class GameState {
|
||||||
this.camControls.maxDistance = 2000;
|
this.camControls.maxDistance = 2000;
|
||||||
this.camControls.maxPolarAngle = Math.PI / 2; // Limit so you don't flip under the map
|
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
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
background: linear-gradient(90deg, #5a3e2b 0%, #382519 100%);
|
background: linear-gradient(90deg, #5a3e2b 0%, #382519 100%);
|
||||||
color: #f0e6d2;
|
color: #f0e6d2;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 3px solid #2e1c10;
|
border-bottom: 3px solid #2e1c10;
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +46,18 @@
|
||||||
font-size: 1rem;
|
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.) */
|
/* Buttons (Load Game, Next, Prev, etc.) */
|
||||||
button {
|
button {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue