diff --git a/environments/dynastai/ATROPOS_INTEGRATION.md b/environments/dynastai/ATROPOS_INTEGRATION.md
new file mode 100644
index 00000000..63079285
--- /dev/null
+++ b/environments/dynastai/ATROPOS_INTEGRATION.md
@@ -0,0 +1,73 @@
+# DynastAI Integration with Atropos
+
+This document provides instructions on how to use DynastAI with Atropos.
+
+## Quick Start Guide
+
+### Option 1: Using the Web Interface (No Atropos Required)
+
+This is the simplest way to test the game mechanics:
+
+```bash
+# Navigate to the dynastai directory
+cd environments/dynastai
+
+# Run the quick start script
+python run_dynastai.py
+```
+
+Your browser will open automatically to http://localhost:3000.
+
+### Option 2: Using with Atropos
+
+```bash
+# From the atropos root directory
+python environments/dynastai_environment.py serve --web-ui
+```
+
+The environment will start and be available for Atropos trainers.
+
+## Testing Components
+
+You can test individual components:
+
+1. **Card Generation:**
+ ```bash
+ cd environments/dynastai
+ python test_card_generation.py
+ ```
+
+2. **API Endpoints:**
+ ```bash
+ cd environments/dynastai
+ # Start the server in one terminal
+ python dynastai_server.py
+ # In another terminal
+ python test_dynastai_api.py
+ ```
+
+3. **Environment Integration:**
+ ```bash
+ cd environments/dynastai
+ python test_dynastai_env.py
+ ```
+
+## Directory Structure
+
+- `dynastai_environment.py`: Main entry point for Atropos integration
+- `dynastai/`: Environment package
+ - `src/dynastai_env.py`: Atropos environment implementation
+ - `src/game_logic.py`: Core game mechanics
+ - `src/web/`: Web interface
+ - `src/data/`: Game data including cards.json
+
+## Troubleshooting
+
+- **Missing cards.json**: Run `test_card_generation.py` to generate it
+- **API errors**: Ensure server is running on port 9001
+- **Import errors**: Make sure you're in the correct directory and have installed dependencies
+- **Web UI not loading**: Check that the server is running and the ports are correct
+
+## Feedback and Issues
+
+Report any issues on GitHub or contact the maintainer directly.
diff --git a/environments/dynastai/INSTALL_AND_RUN.md b/environments/dynastai/INSTALL_AND_RUN.md
new file mode 100644
index 00000000..42160e67
--- /dev/null
+++ b/environments/dynastai/INSTALL_AND_RUN.md
@@ -0,0 +1,107 @@
+# DynastAI - Installation and Running Guide
+
+This guide provides step-by-step instructions to install and run the DynastAI game.
+
+## Prerequisites
+
+- Python 3.8 or higher
+- pip (Python package manager)
+- Git (optional, for cloning the repository)
+
+## Installation
+
+### Step 1: Get the Code
+
+If using git:
+```bash
+git clone https://github.com/torinvdb/atropos.git
+cd atropos
+```
+
+Or download and unzip the project, then navigate to the project folder.
+
+### Step 2: Install Dependencies
+
+Option 1 - Using the setup script (recommended):
+```bash
+cd environments/dynastai
+python setup.py
+```
+
+Option 2 - Manual installation:
+```bash
+cd environments/dynastai
+pip install --upgrade pip
+pip install -r requirements.txt
+```
+
+This installs all required packages including:
+- FastAPI and Uvicorn for the backend server
+- Pydantic for data validation
+- Requests for API calls
+- Python-dotenv for environment variable management
+
+Note: If you're using Python 3.13+, the setup script handles compatibility issues automatically.
+
+### Step 3 (Optional): Add OpenRouter API Key
+
+For dynamic card generation using AI, create a `.env` file in the `environments/dynastai` directory:
+
+```bash
+echo "OPENROUTER_API_KEY=your_api_key_here" > .env
+```
+
+If you don't have an OpenRouter API key, the game will use pre-defined cards from the cards.json file.
+
+## Running the Game
+
+### Option 1: Web Interface (Simple)
+
+This is the easiest way to play the game directly:
+
+```bash
+python run_dynastai.py
+```
+
+Your default browser will open automatically to http://localhost:3000, and you can begin playing.
+
+Command options:
+- `--no-browser`: Don't open the browser automatically
+- `--api-port 9001`: Use a different API port (default: 9001)
+- `--web-port 3000`: Use a different web port (default: 3000)
+
+Example:
+```bash
+python run_dynastai.py --api-port 8080 --web-port 8000
+```
+
+### Option 2: Atropos Integration (Advanced)
+
+For integration with the Atropos reinforcement learning framework:
+
+```bash
+# From the atropos root directory
+python environments/dynastai_environment.py serve --web-ui
+```
+
+## Troubleshooting
+
+- **"Missing cards.json" error**: Run `python test_card_generation.py` to generate it
+- **API connection error**: Ensure the API server is running on the specified port
+- **Import errors**: Verify that all dependencies are installed
+- **Web UI not loading**: Check that both API and web servers are running correctly
+- **Python 3.13+ compatibility issues**: Some packages may need manual installation:
+ ```bash
+ pip install --force-reinstall --no-binary aiohttp aiohttp>=3.9.0
+ ```
+
+## Playing the Game
+
+- The game presents you with scenario cards that impact your kingdom
+- Make choices (Yes/No) to affect your kingdom's metrics:
+ - Power: Royal authority and military strength
+ - Stability: Population happiness and civic order
+ - Piety: Religious influence and moral standing
+ - Wealth: Kingdom finances and economic prosperity
+- Your reign ends when any metric reaches 0 or 100, or after 30 years
+- Each decision affects the adaptive reward system that evolves gameplay based on your choices
diff --git a/environments/dynastai/README.md b/environments/dynastai/README.md
index f22b50b7..ac1a89a4 100644
--- a/environments/dynastai/README.md
+++ b/environments/dynastai/README.md
@@ -19,7 +19,8 @@ Each turn, players are presented with scenario cards generated using Qwen 1.7B v
- **FastAPI Backend**: REST endpoints for game state management
- **HTML/CSS/JS Frontend**: Modern, responsive web interface
- **Adaptive Rewards**: Reward calculation that adapts to player choices and outcomes
-- **OpenRouter Integration**: Dynamic card generation using Qwen 1.7B language model
+- **Card Generation**: Uses both pre-defined cards from JSON and dynamic generation via Qwen 1.7B
+- **OpenRouter Integration**: Dynamic card generation using Qwen 1.7B language model (optional)
## Project Structure
@@ -33,6 +34,8 @@ dynastai/
│ ├── game_logic.py # Core game mechanics
│ ├── util.py # Utility functions
│ ├── data/ # Game data storage
+│ │ ├── game_data.json # Game configuration data
+│ │ └── cards.json # Card templates
│ └── web/ # Web interface
│ ├── __init__.py
│ ├── api.py # FastAPI endpoints
@@ -44,6 +47,8 @@ dynastai/
│
├── dynastai_server.py # Main server entry point
├── dynastai_local_server.py # Local development server
+├── test_dynastai_env.py # Environment test script
+├── test_dynastai_api.py # API test script
├── requirements.txt # Dependencies
└── README.md # Documentation
```
@@ -87,6 +92,8 @@ This creates a dynamic reward system that adapts to each player's style and deci
OPENROUTER_API_KEY=your_api_key_here
```
+4. The system includes a pre-configured `cards.json` file with 400+ scenario cards. If you don't set an OpenRouter API key, the game will exclusively use these pre-defined cards.
+
### Running the Server
To run the full server with API endpoints:
@@ -101,6 +108,30 @@ For local development with both API and web server:
python dynastai_local_server.py
```
+### Testing the Installation
+
+DynastAI includes several test scripts to verify that everything is working correctly:
+
+1. **Installation Verification**: Check if all required packages are installed properly
+ ```bash
+ python verify_install.py
+ ```
+
+2. **Quick Test**: Run the game with the web interface
+ ```bash
+ python run_dynastai.py
+ ```
+
+3. **Card Generation Test**: Test that cards are generated correctly
+ ```bash
+ python test_card_generation.py
+ ```
+
+4. **Environment Test**: Test the Atropos environment integration
+ ```bash
+ python test_dynastai_env.py
+ ```
+
Then access the web interface at http://localhost:3000
## API Endpoints
@@ -116,17 +147,34 @@ The game exposes the following REST API endpoints:
## Integration with Atropos
-The `DynastAIEnv` class implements Atropos's `BaseEnv` interface, making it compatible with Atropos reinforcement learning workflows:
+DynastAI fully integrates with the Atropos RL framework. There are two ways to use it:
+
+### Method 1: Using the Environment Entry Point
+
+```bash
+# From the atropos root directory
+python environments/dynastai_environment.py serve --slurm False
+```
+
+### Method 2: Direct Integration in Trainers
```python
-from atroposlib.envs.base import BaseEnv
-from src.dynastai_env import DynastAIEnv
+from atroposlib.envs.base import BaseEnv, ServerBaseline
+from environments.dynastai.src.dynastai_env import DynastAIEnv, DynastAIEnvConfig
# Create and configure environment
+config = DynastAIEnvConfig(
+ api_host="localhost",
+ api_port=9001,
+ web_enabled=True,
+ web_port=3000
+)
+server_configs = ServerBaseline()
env = DynastAIEnv(config, server_configs)
# Use with Atropos training
observation = await env.reset()
+action = {"session_id": observation["session_id"], "choice": "yes"}
observation, reward, done, info = await env.step(action)
```
diff --git a/environments/dynastai/dynastai_atropos.py b/environments/dynastai/dynastai_atropos.py
new file mode 100755
index 00000000..d5751d30
--- /dev/null
+++ b/environments/dynastai/dynastai_atropos.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+"""
+DynastAI Environment - Direct entry point for Atropos integration
+
+This script provides a direct entry point for running the DynastAI environment
+with the Atropos framework. It serves as a simple wrapper around the dynastai_environment.py
+script in the parent directory.
+"""
+
+import os
+import sys
+import asyncio
+
+# Add parent directory to path to find dynastai_environment.py
+parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, parent_dir)
+
+# Import the main environment module
+import environments.dynastai_environment as dynastai_environment
+
+if __name__ == "__main__":
+ # Pass all arguments to the main entry point
+ sys.exit(dynastai_environment.__file__)
diff --git a/environments/dynastai/dynastai_local_server.py b/environments/dynastai/dynastai_local_server.py
index 6491d617..306c2cad 100755
--- a/environments/dynastai/dynastai_local_server.py
+++ b/environments/dynastai/dynastai_local_server.py
@@ -16,6 +16,11 @@ from threading import Thread
# Ensure src directory is in path
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
+# Add parent directory to path to allow standalone execution without atroposlib
+parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
+if parent_dir not in sys.path:
+ sys.path.insert(0, parent_dir)
+
from src.web.server import run_server
from src.config import get_config
@@ -133,7 +138,12 @@ def main():
Your Reign Has Ended
-
+
+
+
+
+
+
@@ -164,13 +174,10 @@ def main():
body {
font-family: 'Georgia', serif;
- background-color: #f5f5f5;
- color: #333;
+ background-color: #2c3e50;
+ color: #ecf0f1;
line-height: 1.6;
- background-image: url('https://images.unsplash.com/photo-1534196511436-921a4e99f297?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1920&q=80');
- background-size: cover;
- background-position: center;
- background-attachment: fixed;
+ /* Removed stock photo background */
}
header, footer {
@@ -305,6 +312,28 @@ footer {
.hidden {
display: none !important;
}
+
+.detailed-ending {
+ background-color: rgba(30, 30, 30, 0.7);
+ padding: 15px;
+ border-radius: 8px;
+ margin: 15px 0;
+ line-height: 1.7;
+ max-width: 800px;
+ text-align: justify;
+}
+
+.legacy-message {
+ font-style: italic;
+ margin-bottom: 20px;
+}
+
+#reign-options {
+ display: flex;
+ gap: 15px;
+ justify-content: center;
+ margin-top: 20px;
+}
""")
if not os.path.exists(js_file):
@@ -337,6 +366,9 @@ const startGameButton = document.getElementById('start-game');
const gameOverScreen = document.getElementById('game-over');
const gameOverReason = document.getElementById('game-over-reason');
const reignSummary = document.getElementById('reign-summary');
+const detailedEnding = document.getElementById('detailed-ending');
+const legacyMessage = document.getElementById('legacy-message');
+const continueGameButton = document.getElementById('continue-game');
const newGameButton = document.getElementById('new-game');
// Game state
@@ -354,6 +386,7 @@ let trajectory = [];
startGameButton.addEventListener('click', startGame);
yesButton.addEventListener('click', () => makeChoice('yes'));
noButton.addEventListener('click', () => makeChoice('no'));
+continueGameButton.addEventListener('click', continueGame);
newGameButton.addEventListener('click', startGame);
// Helper functions
@@ -462,12 +495,17 @@ async function makeChoice(choice) {
const data = await response.json();
- // Record this move in trajectory
+ // Record this move in trajectory with all required fields
trajectory.push({
- card_id: currentCard.id,
- category: currentCard.category,
+ card_id: currentCard.id || "unknown",
+ category: currentCard.category || "unknown",
choice: choice,
- effects: currentCard.effects[choice],
+ effects: {
+ power: currentCard.effects[choice].power || 0,
+ stability: currentCard.effects[choice].stability || 0,
+ piety: currentCard.effects[choice].piety || 0,
+ wealth: currentCard.effects[choice].wealth || 0
+ },
post_metrics: data.metrics
});
@@ -475,9 +513,19 @@ async function makeChoice(choice) {
metrics = data.metrics;
updateMeters();
- // Check for game over
- if (data.game_over) {
- endReign();
+ // Check for game over conditions
+ const reignEnded = (
+ data.game_over ||
+ metrics.power <= 0 || metrics.power >= 100 ||
+ metrics.stability <= 0 || metrics.stability >= 100 ||
+ metrics.piety <= 0 || metrics.piety >= 100 ||
+ metrics.wealth <= 0 || metrics.wealth >= 100
+ );
+
+ console.log("Checking game over conditions:", reignEnded, metrics);
+
+ if (reignEnded) {
+ await endReign();
return;
}
@@ -490,50 +538,134 @@ async function makeChoice(choice) {
}
}
+async function continueGame() {
+ try {
+ // Reset game state but keep session ID for continuity
+ gameOver = false;
+ trajectory = [];
+
+ // Create new game session with the same session ID to maintain reign history
+ const response = await fetch(`${API_URL}/new_game`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ session_id: sessionId })
+ });
+
+ const data = await response.json();
+ metrics = data.metrics;
+
+ updateMeters();
+
+ // Hide game over screen, show game screen
+ gameOverScreen.classList.add('hidden');
+ cardContainer.classList.remove('hidden');
+
+ // Generate first card
+ await generateCard();
+ } catch (error) {
+ console.error("Error continuing game:", error);
+ gameOverReason.textContent = "Something went wrong when starting a new reign.";
+ }
+}
+
async function endReign() {
try {
// Determine cause of end
let cause = null;
- if (metrics.power <= 0) cause = "power";
- else if (metrics.power >= 100) cause = "power";
- else if (metrics.stability <= 0) cause = "stability";
- else if (metrics.stability >= 100) cause = "stability";
- else if (metrics.piety <= 0) cause = "piety";
- else if (metrics.piety >= 100) cause = "piety";
- else if (metrics.wealth <= 0) cause = "wealth";
- else if (metrics.wealth >= 100) cause = "wealth";
- // Send end reign data to server
+ // Log current metrics for debugging
+ console.log("End reign metrics:", metrics);
+
+ if (metrics.power <= 0) cause = "power_low";
+ else if (metrics.power >= 100) cause = "power_high";
+ else if (metrics.stability <= 0) cause = "stability_low";
+ else if (metrics.stability >= 100) cause = "stability_high";
+ else if (metrics.piety <= 0) cause = "piety_low";
+ else if (metrics.piety >= 100) cause = "piety_high";
+ else if (metrics.wealth <= 0) cause = "wealth_low";
+ else if (metrics.wealth >= 100) cause = "wealth_high";
+ else cause = "old_age";
+
+ console.log("Determined cause of end:", cause);
+
+ // Send end reign data to server with complete information
const response = await fetch(`${API_URL}/end_reign`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
trajectory: trajectory,
- final_metrics: metrics,
+ final_metrics: {
+ power: metrics.power,
+ stability: metrics.stability,
+ piety: metrics.piety,
+ wealth: metrics.wealth,
+ reign_year: metrics.reign_year
+ },
reign_length: metrics.reign_year,
cause_of_end: cause
})
});
- const data = await response.json();
+ let data = { reward: 0 };
+
+ if (response.ok) {
+ data = await response.json();
+ } else {
+ console.error("End reign failed with status:", response.status);
+ }
// Show game over screen
cardContainer.classList.add('hidden');
gameOverScreen.classList.remove('hidden');
- // Set reason based on metrics
+ // Set reason based on metrics and get detailed ending
let reason = "";
- if (metrics.power <= 0) reason = "You lost all authority. The nobles overthrew you!";
- else if (metrics.power >= 100) reason = "Your absolute power made you a tyrant. You were assassinated!";
- else if (metrics.stability <= 0) reason = "The people revolted against your rule!";
- else if (metrics.stability >= 100) reason = "The people loved you so much they established a republic!";
- else if (metrics.piety <= 0) reason = "The church declared you a heretic and had you executed!";
- else if (metrics.piety >= 100) reason = "The church became too powerful and took control of your kingdom!";
- else if (metrics.wealth <= 0) reason = "Your kingdom went bankrupt and you were deposed!";
- else if (metrics.wealth >= 100) reason = "Your vast wealth attracted invaders who conquered your kingdom!";
+ let detailedText = "";
+ let legacyText = "";
+
+ // Set reason based on cause
+ if (cause === "power_low") {
+ reason = "You lost all authority. The nobles overthrew you!";
+ detailedText = "Years of concessions and weak leadership eroded your authority. The nobles, seeing your weakness, formed a coalition against you. After a brief struggle, you were deposed and exiled, remembered as a ruler who couldn't maintain the respect of the nobility.";
+ legacyText = determineRulerLegacy("weak");
+ } else if (cause === "power_high") {
+ reason = "Your absolute power made you a tyrant. You were assassinated!";
+ detailedText = "Your iron-fisted rule and consolidation of power bred resentment among the nobility. As your authority grew unchecked, many feared for their own positions. A conspiracy formed in the shadows, and despite your vigilance, an assassin's blade found its mark. You died as you ruled - alone and feared.";
+ legacyText = determineRulerLegacy("tyrant");
+ } else if (cause === "stability_low") {
+ reason = "The people revolted against your rule!";
+ detailedText = "The cries of the hungry and oppressed grew too loud to ignore. Years of neglect and harsh policies turned the populace against you. What began as isolated protests quickly spread across the kingdom. The uprising was swift and merciless, with angry mobs storming the palace. Your reign ended at the hands of those you failed to serve.";
+ legacyText = determineRulerLegacy("hated");
+ } else if (cause === "stability_high") {
+ reason = "The people loved you so much they established a republic!";
+ detailedText = "The common folk adored you for your generosity and fairness. However, your popularity threatened the traditional power structure. As people began calling for democratic reforms and greater representation, the nobles and church became alarmed. They orchestrated your removal, claiming the kingdom needed 'proper governance, not popularity.' The republic that followed bore your name, though you did not live to see it flourish.";
+ legacyText = determineRulerLegacy("beloved");
+ } else if (cause === "piety_low") {
+ reason = "The church declared you a heretic and had you executed!";
+ detailedText = "Your dismissal of religious traditions and constant conflicts with church authorities were deemed heretical. The Grand Inquisitor publicly denounced you, turning religious sentiment against the crown. Priests preached against you from every pulpit until the faithful rose up in a holy crusade. Declared a heretic, you faced the ultimate punishment for challenging divine authority.";
+ legacyText = determineRulerLegacy("heretic");
+ } else if (cause === "piety_high") {
+ reason = "The church became too powerful and took control of your kingdom!";
+ detailedText = "You allowed religious authorities too much influence, and the church's power grew unchecked. Gradually, religious law superseded royal edicts, and church officials began overruling your decisions. Eventually, the Archbishop declared divine right to rule, and with popular support, established a theocracy. You were permitted to retain your title in name only - a figurehead in a kingdom ruled by the cloth.";
+ legacyText = determineRulerLegacy("pious");
+ } else if (cause === "wealth_low") {
+ reason = "Your kingdom went bankrupt and you were deposed!";
+ detailedText = "Years of extravagance and financial mismanagement emptied the royal coffers. Unable to pay the army or maintain the kingdom's infrastructure, your rule collapsed under mounting debts. Foreign creditors seized royal assets, while unpaid servants and soldiers abandoned their posts. With nothing left to rule, you were quietly removed from the throne, your name becoming synonymous with fiscal irresponsibility.";
+ legacyText = determineRulerLegacy("poor");
+ } else if (cause === "wealth_high") {
+ reason = "Your vast wealth attracted invaders who conquered your kingdom!";
+ detailedText = "Your kingdom's legendary wealth attracted unwanted attention. Neighboring rulers looked upon your treasuries with envy, and despite your diplomatic efforts, greed won out. A coalition of foreign powers, using your hoarding of wealth as justification, invaded with overwhelming force. Your vast riches funded your enemies' armies, and your kingdom was divided among the victors.";
+ legacyText = determineRulerLegacy("wealthy");
+ } else {
+ reason = "You died of natural causes after a long reign.";
+ detailedText = `After ${metrics.reign_year} years of rule, age finally caught up with you. Your legacy secured, you passed peacefully in your sleep, surrounded by generations of family. The kingdom mourned for forty days, and your achievements were recorded in detail by royal historians. Few monarchs are fortunate enough to meet such a natural end, a testament to your balanced approach to leadership.`;
+ legacyText = determineRulerLegacy("balanced");
+ }
gameOverReason.textContent = reason;
+ detailedEnding.textContent = detailedText;
+ legacyMessage.textContent = legacyText;
reignSummary.textContent = `You ruled for ${metrics.reign_year} years. Final reward: ${data.reward.toFixed(2)}`;
} catch (error) {
@@ -542,6 +674,64 @@ async function endReign() {
}
}
+function determineRulerLegacy(rulerType) {
+ // Generate a legacy message based on reign length and ruler type
+ const reignLength = metrics.reign_year;
+ let legacy = "";
+
+ if (reignLength < 5) {
+ legacy = "Your brief rule will be barely a footnote in the kingdom's history.";
+ } else if (reignLength > 30) {
+ switch (rulerType) {
+ case "balanced":
+ legacy = "Your long and balanced reign will be remembered as a golden age of prosperity and peace.";
+ break;
+ case "tyrant":
+ legacy = "Your decades of tyrannical rule have left a permanent scar on the kingdom's history. Your name will be used to frighten children for generations.";
+ break;
+ case "beloved":
+ legacy = "Your generous and fair leadership established a cultural renaissance that will be studied for centuries to come.";
+ break;
+ default:
+ legacy = "Your long reign, despite its end, has made an indelible mark on the kingdom's history.";
+ }
+ } else {
+ switch (rulerType) {
+ case "weak":
+ legacy = "History will remember you as a monarch who failed to maintain control of their own court.";
+ break;
+ case "tyrant":
+ legacy = "You will be remembered as a harsh and unforgiving ruler who sought power above all else.";
+ break;
+ case "hated":
+ legacy = "Your name will be spoken with contempt by commoners for generations to come.";
+ break;
+ case "beloved":
+ legacy = "The people will sing songs of your kindness and fairness for many years.";
+ break;
+ case "heretic":
+ legacy = "Religious texts will cite you as an example of the dangers of straying from the faith.";
+ break;
+ case "pious":
+ legacy = "You will be remembered as a devout ruler who perhaps trusted the clergy too much.";
+ break;
+ case "poor":
+ legacy = "Future monarchs will study your reign as a cautionary tale of financial mismanagement.";
+ break;
+ case "wealthy":
+ legacy = "Tales of your kingdom's riches will become legendary, though they ultimately led to your downfall.";
+ break;
+ case "balanced":
+ legacy = "Your rule will be remembered as a time of reasonable balance and steady progress.";
+ break;
+ default:
+ legacy = `You ruled for ${reignLength} years, leaving behind a mixed legacy of successes and failures.`;
+ }
+ }
+
+ return legacy;
+}
+
// Check if API is available when page loads
window.addEventListener('load', async () => {
try {
diff --git a/environments/dynastai/requirements.txt b/environments/dynastai/requirements.txt
index f9cffcf6..af399628 100644
--- a/environments/dynastai/requirements.txt
+++ b/environments/dynastai/requirements.txt
@@ -1,10 +1,21 @@
- # filepath: /Users/torinvandenbulk/Documents/GitHub/atropos/environments/dynastai/requirements.txt
-fastapi==0.104.1
-uvicorn==0.23.2
-pydantic==2.4.2
-python-dotenv==1.0.0
-requests==2.31.0
-httpx==0.25.0
-python-multipart==0.0.6
-uuid==1.30
-aiohttp==3.8.5
\ No newline at end of file
+# DynastAI Requirements
+fastapi>=0.105.0
+uvicorn>=0.28.0
+pydantic>=2.6.0
+python-dotenv>=1.0.0
+requests>=2.31.0
+httpx>=0.27.0
+python-multipart>=0.0.7
+aiohttp~=3.9.5
+jinja2>=3.1.3
+tqdm>=4.66.2
+numpy>=1.26.0
+wandb>=0.16.0
+
+# Atropos dependencies
+datasets>=2.16.0
+
+# Additional dependencies for compatibility
+typing-extensions>=4.9.0
+# For Python 3.13+
+setuptools>=68.0.0
\ No newline at end of file
diff --git a/environments/dynastai/run_dynastai.py b/environments/dynastai/run_dynastai.py
new file mode 100755
index 00000000..2376fbbe
--- /dev/null
+++ b/environments/dynastai/run_dynastai.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+"""
+DynastAI Quick Test Script
+
+This script provides a simple way to test the DynastAI environment without requiring the full Atropos setup.
+It will start a local server and open the web UI in your default browser.
+"""
+
+import os
+import sys
+import subprocess
+import webbrowser
+import time
+import argparse
+
+# Ensure the script works from any directory
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+os.chdir(SCRIPT_DIR)
+
+# Add the parent directory to sys.path to allow importing atroposlib if available
+parent_dir = os.path.abspath(os.path.join(SCRIPT_DIR, '..', '..'))
+if parent_dir not in sys.path:
+ sys.path.insert(0, parent_dir)
+
+def main():
+ # Parse arguments
+ parser = argparse.ArgumentParser(description="Run DynastAI test environment")
+ parser.add_argument("--no-browser", action="store_true", help="Don't open browser automatically")
+ parser.add_argument("--api-port", type=int, default=9001, help="API port (default: 9001)")
+ parser.add_argument("--web-port", type=int, default=3000, help="Web UI port (default: 3000)")
+ args = parser.parse_args()
+
+ # Install dependencies if needed
+ try:
+ import fastapi
+ import uvicorn
+ import pydantic
+ print("Dependencies already installed.")
+ except ImportError:
+ print("Installing dependencies...")
+ try:
+ # First try using the setup script
+ if os.path.exists(os.path.join(SCRIPT_DIR, "setup.py")):
+ print("Running setup script...")
+ subprocess.run([sys.executable, os.path.join(SCRIPT_DIR, "setup.py")])
+ else:
+ # Fall back to direct installation
+ subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", "pip"])
+ subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
+ except Exception as e:
+ print(f"Warning: Error installing dependencies: {e}")
+ print("Please run 'python setup.py' manually before continuing.")
+
+ # Create data directory if it doesn't exist
+ os.makedirs(os.path.join(SCRIPT_DIR, "src/data"), exist_ok=True)
+
+ # Start the local server
+ print(f"Starting DynastAI server on http://localhost:{args.web_port}")
+ server_process = subprocess.Popen(
+ [sys.executable, os.path.join(SCRIPT_DIR, "dynastai_local_server.py"), "--api-port", str(args.api_port), "--web-port", str(args.web_port)]
+ )
+
+ try:
+ # Give the server time to start
+ time.sleep(2)
+
+ # Open the browser if requested
+ if not args.no_browser:
+ print("Opening web browser...")
+ webbrowser.open(f"http://localhost:{args.web_port}")
+
+ print("Press Ctrl+C to stop the server")
+ server_process.wait()
+ except KeyboardInterrupt:
+ print("\nShutting down server...")
+ server_process.terminate()
+ server_process.wait()
+ print("Server stopped.")
+
+if __name__ == "__main__":
+ main()
diff --git a/environments/dynastai/setup.py b/environments/dynastai/setup.py
new file mode 100755
index 00000000..f022b37c
--- /dev/null
+++ b/environments/dynastai/setup.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+"""
+DynastAI Setup Script
+
+This script ensures that all dependencies for DynastAI are properly installed
+and compatible with your Python version.
+"""
+
+import sys
+import subprocess
+import os
+import platform
+
+def main():
+ """Run the setup process"""
+ print("DynastAI Setup")
+ print("=============")
+
+ # Check Python version
+ python_version = tuple(map(int, platform.python_version_tuple()))
+ print(f"Python version: {platform.python_version()}")
+
+ if python_version < (3, 8):
+ print("Error: Python 3.8 or higher is required")
+ sys.exit(1)
+
+ # Check built-in uuid module
+ print("Checking UUID module...")
+ try:
+ import uuid
+ print(f"UUID module version: {uuid.__version__ if hasattr(uuid, '__version__') else 'built-in'}")
+ except ImportError:
+ print("Warning: UUID module not found. This is unexpected as it should be built into Python.")
+
+ # Get script directory
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ requirements_file = os.path.join(script_dir, "requirements.txt")
+
+ # Ensure pip is up-to-date
+ print("\nUpdating pip...")
+ subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", "pip"])
+
+ # Install dependencies with special handling
+ print("\nInstalling dependencies...")
+
+ # First, install key packages that others might depend on
+ print("Installing core dependencies...")
+ subprocess.run([
+ sys.executable, "-m", "pip", "install",
+ "wheel", "setuptools>=68.0.0", "typing-extensions>=4.9.0"
+ ])
+
+ # For Python 3.13+, special handling for aiohttp
+ if python_version >= (3, 13):
+ print("\nDetected Python 3.13+, installing compatible versions of packages...")
+ try:
+ # For Python 3.13, use the newest compatible version or install from source if needed
+ subprocess.run([
+ sys.executable, "-m", "pip", "install",
+ "--force-reinstall", "--no-binary", "aiohttp", "aiohttp>=3.9.0"
+ ])
+ print("Successfully installed aiohttp from source.")
+ except Exception as e:
+ print(f"Warning: Failed to install aiohttp from source: {e}")
+ print("Continuing with installation, but some features might not work.")
+
+ # Install main requirements
+ print("\nInstalling main requirements...")
+ result = subprocess.run([
+ sys.executable, "-m", "pip", "install", "-r", requirements_file
+ ])
+
+ if result.returncode != 0:
+ print("\nTrying an alternative installation method for problematic packages...")
+
+ # Read requirements file
+ with open(requirements_file, 'r') as f:
+ requirements = [line.strip() for line in f if line.strip() and not line.startswith('#')]
+
+ # Install packages one by one
+ for req in requirements:
+ print(f"Installing {req}...")
+ try:
+ subprocess.run([sys.executable, "-m", "pip", "install", req], check=True)
+ except subprocess.CalledProcessError:
+ pkg_name = req.split('>=')[0] if '>=' in req else req.split('==')[0] if '==' in req else req
+ print(f"Warning: Failed to install {pkg_name}, trying without version constraint...")
+ subprocess.run([sys.executable, "-m", "pip", "install", pkg_name])
+
+ print("\nSetup complete! You can now run DynastAI.")
+ print("To start the game with web interface, run: python run_dynastai.py")
+
+if __name__ == "__main__":
+ main()
diff --git a/environments/dynastai/src/config.py b/environments/dynastai/src/config.py
index c4a8624b..ab6f7f89 100644
--- a/environments/dynastai/src/config.py
+++ b/environments/dynastai/src/config.py
@@ -63,6 +63,22 @@ class DynastAIConfig(BaseModel):
description="Directory for storing game data"
)
+ # Card configuration
+ cards_file: str = Field(
+ default="cards.json",
+ description="Filename for storing cards"
+ )
+
+ use_local_cards: bool = Field(
+ default=True,
+ description="Whether to use cards from local JSON file"
+ )
+
+ use_api_cards: bool = Field(
+ default=True,
+ description="Whether to generate cards using OpenRouter API"
+ )
+
# Game difficulty settings
min_effect_value: int = Field(default=-20, description="Minimum effect value")
max_effect_value: int = Field(default=20, description="Maximum effect value")
diff --git a/environments/dynastai/src/dynastai_env.py b/environments/dynastai/src/dynastai_env.py
index e1f42fad..677727a4 100644
--- a/environments/dynastai/src/dynastai_env.py
+++ b/environments/dynastai/src/dynastai_env.py
@@ -10,14 +10,35 @@ import time
import json
import random
import uuid
+import sys
from typing import Dict, List, Tuple, Any, Optional, Union
import requests
from dotenv import load_dotenv
-from atroposlib.envs.base import BaseEnv, BaseEnvConfig
-from atroposlib.envs.server_handling.server_baseline import ServerBaseline
-from atroposlib.envs.server_handling.server_manager import APIServerConfig
+# Try to import from atroposlib, but provide fallbacks for standalone mode
+try:
+ from atroposlib.envs.base import BaseEnv, BaseEnvConfig
+ from atroposlib.envs.server_handling.server_baseline import ServerBaseline
+ from atroposlib.envs.server_handling.server_manager import APIServerConfig
+ HAS_ATROPOSLIB = True
+except ImportError:
+ # Create minimal stub classes for standalone mode
+ class BaseEnvConfig:
+ pass
+
+ class BaseEnv:
+ def __init__(self, *args, **kwargs):
+ pass
+
+ class ServerBaseline:
+ pass
+
+ class APIServerConfig:
+ pass
+
+ HAS_ATROPOSLIB = False
+ print("Running in standalone mode without atroposlib")
from .game_logic import GameState, generate_card, apply_choice_effects
@@ -54,12 +75,18 @@ class DynastAIEnv(BaseEnv):
def __init__(
self,
- config: DynastAIEnvConfig,
- server_configs: Union[ServerBaseline, List[APIServerConfig]],
+ config: DynastAIEnvConfig = None,
+ server_configs: Union[ServerBaseline, List[APIServerConfig]] = None,
slurm=False,
testing=False,
):
- super().__init__(config, server_configs, slurm, testing)
+ if HAS_ATROPOSLIB:
+ super().__init__(config, server_configs, slurm, testing)
+
+ # In standalone mode, initialize with default config if none provided
+ if config is None:
+ config = DynastAIEnvConfig()
+
self.config = config
# Game state storage (in-memory keyed by session_id)
diff --git a/environments/dynastai/src/game_logic.py b/environments/dynastai/src/game_logic.py
index eb1bdfb3..a3bee6e0 100644
--- a/environments/dynastai/src/game_logic.py
+++ b/environments/dynastai/src/game_logic.py
@@ -11,12 +11,24 @@ This module handles the core game mechanics:
import os
import json
import random
-import uuid
import time
from typing import Dict, List, Tuple, Any, Optional
import requests
from dotenv import load_dotenv
+# Import UUID - it's a built-in module in Python
+try:
+ import uuid
+except ImportError:
+ # If somehow uuid is not available, create a simple UUID generator
+ import random
+ class FallbackUUID:
+ @staticmethod
+ def uuid4():
+ # Simple fallback for uuid4 (not as good but functional)
+ return f"uuid-{random.randint(10000000, 99999999)}"
+ uuid = FallbackUUID
+
# Load environment variables
load_dotenv()
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
@@ -37,6 +49,7 @@ class GameState:
self.current_card = None
self.card_history = [] # List of played cards
self.choice_history = [] # List of yes/no choices made
+ self.previous_reigns = [] # Track previous reigns in this session for continuity
# Category counts for adaptive reward calculation
self.category_counts = {"power": 0, "stability": 0, "piety": 0, "wealth": 0}
@@ -71,17 +84,243 @@ class GameState:
self.reign_year += 1
-def generate_card(metrics: Dict[str, int], category_weights: Dict[str, int]) -> Dict:
+def generate_card(metrics: Dict[str, int], category_weights: Dict[str, int], previous_reigns=None) -> Dict:
"""
- Generate a new card using the OpenRouter API (Qwen 1.7B)
+ Generate a new card using either the cards.json file or the OpenRouter API (Qwen 1.7B)
Parameters:
- metrics: Current game metrics
- category_weights: Weights for selecting card categories
+ - previous_reigns: Optional list of previous reign outcomes to influence card generation
Returns:
- card: A card object with text, options and effects
"""
+ # Try to load cards from the cards.json file first
+ try:
+ if previous_reigns and len(previous_reigns) > 0:
+ # If there are previous reigns, occasionally generate continuity cards
+ if random.random() < 0.2: # 20% chance of generating a continuity card
+ return generate_continuity_card(metrics, category_weights, previous_reigns)
+
+ return select_card_from_file(metrics, category_weights)
+ except Exception as e:
+ print(f"Error selecting card from file: {e}")
+ # Fall back to OpenRouter API or mock cards
+ return generate_api_card(metrics, category_weights, previous_reigns)
+
+
+def select_card_from_file(metrics: Dict[str, int], category_weights: Dict[str, int]) -> Dict:
+ """
+ Select a card from the cards.json file based on category weights
+ """
+ # Path to cards.json file
+ cards_file = os.path.join(os.path.dirname(__file__), "data", "cards.json")
+
+ if not os.path.exists(cards_file):
+ raise FileNotFoundError(f"Cards file not found: {cards_file}")
+
+ with open(cards_file, "r") as f:
+ cards_data = json.load(f)
+
+ # Check if it's the new format (direct array) or old format (with "cards" key)
+ if isinstance(cards_data, list):
+ cards = cards_data
+ elif "cards" in cards_data and cards_data["cards"]:
+ cards = cards_data["cards"]
+ else:
+ raise ValueError("No valid cards found in cards.json")
+
+ if not cards:
+ raise ValueError("Empty card list in cards.json")
+
+ # Map the new format fields to the expected format
+ formatted_cards = []
+ for card in cards:
+ # Check if it's already in the expected format
+ if all(key in card for key in ["text", "yes_option", "no_option", "category"]):
+ formatted_cards.append(card)
+ else:
+ # Convert new format to expected format
+ formatted_card = {
+ "id": card.get("ID", f"card_{str(uuid.uuid4())[:8]}"),
+ "text": card.get("Prompt", ""),
+ "yes_option": card.get("Left_Choice", "Yes"),
+ "no_option": card.get("Right_Choice", "No"),
+ "category": determine_category_from_effects(card),
+ "effects": {
+ "yes": {
+ "power": int(card.get("Left_Power", 0)),
+ "stability": int(card.get("Left_Stability", 0)),
+ "piety": int(card.get("Left_Piety", 0)),
+ "wealth": int(card.get("Left_Wealth", 0))
+ },
+ "no": {
+ "power": int(card.get("Right_Power", 0)),
+ "stability": int(card.get("Right_Stability", 0)),
+ "piety": int(card.get("Right_Piety", 0)),
+ "wealth": int(card.get("Right_Wealth", 0))
+ }
+ },
+ "character_name": card.get("Character", "Royal Advisor")
+ }
+ formatted_cards.append(formatted_card)
+
+ # Select a category based on weights
+ categories = list(category_weights.keys())
+ weights = [category_weights[cat] for cat in categories]
+ total_weight = sum(weights)
+
+ # Normalize weights to avoid issues if weights are too small
+ if total_weight > 0:
+ normalized_weights = [w/total_weight for w in weights]
+ else:
+ normalized_weights = [1/len(categories)] * len(categories)
+
+ category = random.choices(categories, weights=normalized_weights, k=1)[0]
+
+ # Filter cards by selected category if using the new format
+ category_cards = [card for card in formatted_cards if determine_category_from_card(card) == category]
+
+ # If no cards for this category, use all cards
+ if not category_cards:
+ category_cards = formatted_cards
+
+ # Select a random card
+ selected_card = random.choice(category_cards)
+
+ return selected_card
+
+def determine_category_from_effects(card):
+ """Determine the category from a card's effects"""
+ metrics = {
+ "power": abs(int(card.get("Left_Power", 0))) + abs(int(card.get("Right_Power", 0))),
+ "stability": abs(int(card.get("Left_Stability", 0))) + abs(int(card.get("Right_Stability", 0))),
+ "piety": abs(int(card.get("Left_Piety", 0))) + abs(int(card.get("Right_Piety", 0))),
+ "wealth": abs(int(card.get("Left_Wealth", 0))) + abs(int(card.get("Right_Wealth", 0))),
+ }
+
+ # Return the metric with the highest absolute effect
+ return max(metrics.items(), key=lambda x: x[1])[0]
+
+def determine_category_from_card(card):
+ """Determine the category from a card object"""
+ if "category" in card:
+ return card["category"]
+
+ if "effects" in card:
+ effects = card["effects"]
+ total_effects = {}
+
+ for choice in ["yes", "no"]:
+ if choice in effects:
+ for metric, value in effects[choice].items():
+ if metric not in total_effects:
+ total_effects[metric] = 0
+ total_effects[metric] += abs(value)
+
+ if total_effects:
+ return max(total_effects.items(), key=lambda x: x[1])[0]
+
+ return "stability" # Default category
+
+
+def generate_continuity_card(metrics: Dict[str, int], category_weights: Dict[str, int], previous_reigns) -> Dict:
+ """
+ Generate a card that references the previous reign's outcome for continuity
+ """
+ # Get the most recent reign
+ last_reign = previous_reigns[-1]
+ cause_of_end = last_reign.get("cause_of_end", "unknown")
+ reign_length = last_reign.get("reign_length", 0)
+
+ # Generate a unique card ID
+ card_id = f"continuity_card_{str(uuid.uuid4())[:8]}"
+
+ # Create a continuity event based on how the previous reign ended
+ if OPENROUTER_API_KEY:
+ try:
+ # Create a prompt with previous reign details
+ prompt = f"""System: "You are generating JSON event cards for a medieval kingdom management game that reference past gameplay."
+
+User: "Create a card that references the previous ruler's downfall. The previous ruler reigned for {reign_length} years and was ended due to '{cause_of_end}'.
+Output ONLY a JSON event card object where the scenario references the previous ruler's fate."
+"""
+ # Send to OpenRouter API
+ response = call_openrouter(prompt)
+ try:
+ card_data = json.loads(response)
+ if validate_card(card_data):
+ return card_data
+ except:
+ # Fall back to mock continuity card on error
+ pass
+ except Exception as e:
+ print(f"Error generating continuity card: {e}")
+
+ # If API fails or isn't available, use mock continuity card
+ return generate_mock_continuity_card(metrics, cause_of_end, reign_length)
+
+def generate_mock_continuity_card(metrics, cause_of_end, reign_length):
+ """Generate a mock continuity card based on previous reign"""
+ category = "stability" # Default category
+
+ # Create text based on previous reign's end cause
+ if cause_of_end == "power_low":
+ text = f"Advisors remind you that the previous ruler was overthrown by nobles after {reign_length} years of weak leadership."
+ category = "power"
+ elif cause_of_end == "power_high":
+ text = f"You visit the tomb of your predecessor, an infamous tyrant who was assassinated after {reign_length} years of iron-fisted rule."
+ category = "power"
+ elif cause_of_end == "stability_low":
+ text = f"The kingdom still bears scars from the peasant revolt that deposed the previous monarch after {reign_length} years."
+ category = "stability"
+ elif cause_of_end == "stability_high":
+ text = f"Citizens talk fondly of your predecessor who was so loved they established a republic after {reign_length} years."
+ category = "stability"
+ elif cause_of_end == "piety_low":
+ text = f"The church reminds you that the previous ruler was declared a heretic and executed after {reign_length} years."
+ category = "piety"
+ elif cause_of_end == "piety_high":
+ text = f"The Archbishop speaks of reclaiming authority that the church gained under the previous ruler's {reign_length}-year reign."
+ category = "piety"
+ elif cause_of_end == "wealth_low":
+ text = f"The treasury still suffers from the bankruptcy that ended the previous {reign_length}-year reign."
+ category = "wealth"
+ elif cause_of_end == "wealth_high":
+ text = f"Neighboring kingdoms remain wary after invading to seize the vast wealth accumulated during the previous {reign_length}-year reign."
+ category = "wealth"
+ else:
+ text = f"Your predecessor ruled for {reign_length} years before their demise. Their decisions still affect the kingdom."
+
+ # Generate effect values
+ effects = {
+ "yes": {category: 5, "stability": -2},
+ "no": {category: -2, "stability": 2}
+ }
+
+ # Character names for continuity cards
+ continuity_characters = {
+ "power": "Royal Historian",
+ "stability": "Elder Villager",
+ "piety": "Ancient Priest",
+ "wealth": "Treasury Keeper",
+ }
+
+ return {
+ "id": f"continuity_{str(uuid.uuid4())[:8]}",
+ "text": text,
+ "yes_option": "Learn from their mistakes",
+ "no_option": "Forge your own path",
+ "effects": effects,
+ "category": category,
+ "character_name": continuity_characters.get(category, "Court Advisor")
+ }
+
+def generate_api_card(metrics: Dict[str, int], category_weights: Dict[str, int], previous_reigns=None) -> Dict:
+ """
+ Generate a new card using the OpenRouter API (Qwen 1.7B)
+ """
# Select a category based on weights
categories = list(category_weights.keys())
weights = [category_weights[cat] for cat in categories]
@@ -98,11 +337,19 @@ def generate_card(metrics: Dict[str, int], category_weights: Dict[str, int]) ->
# Generate a unique card ID
card_id = f"card_{str(uuid.uuid4())[:8]}"
+ # Add context from previous reigns if available
+ previous_reign_context = ""
+ if previous_reigns and len(previous_reigns) > 0:
+ last_reign = previous_reigns[-1]
+ reign_length = last_reign.get("reign_length", 0)
+ cause = last_reign.get("cause_of_end", "unknown")
+ previous_reign_context = f"Note: The previous ruler reigned for {reign_length} years before falling due to {cause}. "
+
# Create a card prompt
prompt = f"""System: "You are generating JSON event cards for a medieval kingdom management game."
User: "Create a {category} focused event card for a medieval ruler.
-Current metrics: Power:{metrics['power']}, Stability:{metrics['stability']}, Piety:{metrics['piety']}, Wealth:{metrics['wealth']}.
+{previous_reign_context}Current metrics: Power:{metrics['power']}, Stability:{metrics['stability']}, Piety:{metrics['piety']}, Wealth:{metrics['wealth']}.
Output ONLY a JSON event card object like this:
{{
'id': '{card_id}',
@@ -190,8 +437,72 @@ def validate_card(card: Dict) -> bool:
def generate_mock_card(metrics: Dict[str, int], category: str) -> Dict:
"""
- Generate a mock card for testing when OpenRouter API is unavailable
+ Generate a mock card for testing when other card sources are unavailable
+ First tries to select from the built-in templates, then generates a new card
"""
+ # Try to use the card file first if it exists
+ try:
+ # Path to cards.json file
+ cards_file = os.path.join(os.path.dirname(__file__), "data", "cards.json")
+
+ if os.path.exists(cards_file):
+ with open(cards_file, "r") as f:
+ cards_data = json.load(f)
+
+ # Handle both new format (direct array) and old format (with "cards" key)
+ cards = []
+ if isinstance(cards_data, list):
+ cards = cards_data
+ elif "cards" in cards_data and cards_data["cards"]:
+ cards = cards_data["cards"]
+
+ if cards:
+ # Determine category for each card (if needed)
+ formatted_cards = []
+ for card in cards:
+ # Convert card if needed
+ if all(key in card for key in ["text", "yes_option", "no_option", "category"]):
+ formatted_cards.append(card)
+ else:
+ # Convert new format to expected format
+ formatted_card = {
+ "id": card.get("ID", f"card_{str(uuid.uuid4())[:8]}"),
+ "text": card.get("Prompt", ""),
+ "yes_option": card.get("Left_Choice", "Yes"),
+ "no_option": card.get("Right_Choice", "No"),
+ "category": determine_category_from_effects(card),
+ "effects": {
+ "yes": {
+ "power": int(card.get("Left_Power", 0)),
+ "stability": int(card.get("Left_Stability", 0)),
+ "piety": int(card.get("Left_Piety", 0)),
+ "wealth": int(card.get("Left_Wealth", 0))
+ },
+ "no": {
+ "power": int(card.get("Right_Power", 0)),
+ "stability": int(card.get("Right_Stability", 0)),
+ "piety": int(card.get("Right_Piety", 0)),
+ "wealth": int(card.get("Right_Wealth", 0))
+ }
+ },
+ "character_name": card.get("Character", "Royal Advisor")
+ }
+ formatted_cards.append(formatted_card)
+
+ # Filter cards by selected category
+ category_cards = [card for card in formatted_cards if determine_category_from_card(card) == category]
+
+ # If no cards for this category, use all cards
+ if not category_cards:
+ category_cards = formatted_cards
+
+ # Select a random card
+ if category_cards:
+ return random.choice(category_cards)
+ except Exception as e:
+ print(f"Error selecting from card file: {e}")
+
+ # If we can't use the card file, generate a random card
effect_range = (-10, 10)
# Create effects for yes and no choices
@@ -202,6 +513,17 @@ def generate_mock_card(metrics: Dict[str, int], category: str) -> Dict:
yes_effects[category] = random.randint(5, 15)
no_effects[category] = random.randint(-15, -5)
+ # Character names for each category
+ character_names = {
+ "power": ["General Blackstone", "Captain of the Guard", "Lord Commander", "Duke of Westbridge"],
+ "stability": ["Village Elder", "Town Crier", "Guild Master", "Court Jester"],
+ "piety": ["High Priest", "Bishop Aurelius", "Sister Margaery", "Oracle of the Temple"],
+ "wealth": ["Royal Treasurer", "Master of Coin", "Merchant Guild Leader", "Foreign Diplomat"]
+ }
+
+ # Pick a random character name for the category
+ character_name = random.choice(character_names.get(category, ["Royal Advisor"]))
+
# Create mock scenarios based on category
scenarios = {
"power": "The Royal General requests funds to expand the army.",
@@ -233,7 +555,8 @@ def generate_mock_card(metrics: Dict[str, int], category: str) -> Dict:
"yes": yes_effects,
"no": no_effects
},
- "category": category
+ "category": category,
+ "character_name": character_name
}
@@ -265,7 +588,7 @@ def apply_choice_effects(game_state: GameState, choice: str) -> Tuple[bool, Dict
game_state.piety = max(0, min(100, game_state.piety + effects["piety"]))
game_state.wealth = max(0, min(100, game_state.wealth + effects["wealth"]))
- # Record the card play
+ # Record the card play and update category counts
game_state.record_card_play(game_state.current_card, choice)
# Check for game over conditions
diff --git a/environments/dynastai/src/web/__init__.py b/environments/dynastai/src/web/__init__.py
index 62d49400..9a5ac8be 100644
--- a/environments/dynastai/src/web/__init__.py
+++ b/environments/dynastai/src/web/__init__.py
@@ -1,2 +1,5 @@
- mkdir dynastai
- cd dynastai
\ No newline at end of file
+ # DynastAI web interface
+from .api import api
+from .server import run_server
+
+__all__ = ["api", "run_server"]
\ No newline at end of file
diff --git a/environments/dynastai/src/web/api.py b/environments/dynastai/src/web/api.py
index 61167df8..3cbca331 100644
--- a/environments/dynastai/src/web/api.py
+++ b/environments/dynastai/src/web/api.py
@@ -1,6 +1,5 @@
- """
-FastAPI endpoints for DynastAI game
-
+# FastAPI endpoints for DynastAI game
+"""
This module provides the REST API endpoints for the DynastAI game:
- GET /state: Get current game state
- POST /generate_card: Generate a new card
@@ -19,11 +18,26 @@ from pydantic import BaseModel
# Import the game logic
from ..game_logic import GameState, generate_card, apply_choice_effects
-# In-memory store for game sessions
-game_sessions: Dict[str, GameState] = {}
-
-# In-memory store for category weights across reigns
-category_weights: Dict[str, float] = {"power": 50, "stability": 50, "piety": 50, "wealth": 50}
+# Try to import the environment if running in standalone mode
+try:
+ from ..dynastai_env import DynastAIEnv, HAS_ATROPOSLIB
+
+ # Create a standalone environment instance if running without atroposlib
+ standalone_env = None
+ if not HAS_ATROPOSLIB:
+ standalone_env = DynastAIEnv()
+ # Use the environment's game states and category weights
+ game_sessions = standalone_env.game_states
+ category_weights = standalone_env.category_weights
+ else:
+ # In-memory store for game sessions
+ game_sessions: Dict[str, GameState] = {}
+ # In-memory store for category weights across reigns
+ category_weights: Dict[str, float] = {"power": 50, "stability": 50, "piety": 50, "wealth": 50}
+except ImportError:
+ # Fallback if import fails
+ game_sessions: Dict[str, GameState] = {}
+ category_weights: Dict[str, float] = {"power": 50, "stability": 50, "piety": 50, "wealth": 50}
# Path to save reign trajectories
trajectories_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "trajectories.json")
@@ -58,6 +72,9 @@ class TrajectoryItem(BaseModel):
choice: str
effects: Dict[str, Any]
post_metrics: Dict[str, int]
+
+ class Config:
+ extra = "ignore" # Allow extra fields
class EndReignRequest(BaseModel):
"""Request model for ending a reign"""
@@ -66,6 +83,9 @@ class EndReignRequest(BaseModel):
final_metrics: Dict[str, int]
reign_length: int
cause_of_end: Optional[str] = None
+
+ class Config:
+ extra = "ignore" # Allow extra fields
class EndReignResponse(BaseModel):
"""Response model for ending a reign"""
@@ -128,8 +148,16 @@ async def generate_new_card(request: GenerateCardRequest):
game_state = game_sessions[request.session_id]
- # Generate a new card using the current metrics and category weights
- card = generate_card(game_state.get_metrics(), category_weights)
+ # Check if this session has a history of previous reigns
+ # This would be used to adapt card generation based on previous outcomes
+ previous_reigns = game_state.previous_reigns if hasattr(game_state, 'previous_reigns') else []
+
+ # Generate a new card using the current metrics, category weights, and reign history
+ card = generate_card(
+ game_state.get_metrics(),
+ category_weights,
+ previous_reigns=previous_reigns
+ )
# Store the card in the game state
game_state.current_card = card
@@ -170,76 +198,136 @@ async def end_reign(request: EndReignRequest):
"""
if request.session_id not in game_sessions:
raise HTTPException(status_code=404, detail="Session not found")
+
+ # Log the received trajectory for debugging
+ print(f"Received trajectory with {len(request.trajectory)} items")
+
+ try:
+ # Calculate the adaptive reward
+ reward = calculate_adaptive_reward(request.final_metrics, request.trajectory)
- # Calculate the adaptive reward
- reward = calculate_adaptive_reward(request.final_metrics, request.trajectory)
+ # Update category weights
+ update_category_weights(request.final_metrics, request.trajectory)
+
+ # Log the trajectory
+ log_trajectory(request, reward)
+
+ # Store previous reign data before resetting
+ previous_reign = {
+ "final_metrics": request.final_metrics,
+ "reign_length": request.reign_length,
+ "cause_of_end": request.cause_of_end,
+ "reward": reward
+ }
+
+ # Create new game state while preserving reign history
+ new_state = GameState()
+
+ # Initialize previous_reigns if needed
+ new_state.previous_reigns = []
+
+ # If session already exists, get previous reigns history
+ if request.session_id in game_sessions:
+ if hasattr(game_sessions[request.session_id], 'previous_reigns'):
+ new_state.previous_reigns = game_sessions[request.session_id].previous_reigns
+
+ # Add this reign to history
+ new_state.previous_reigns.append(previous_reign)
+
+ # Update the session with new state
+ game_sessions[request.session_id] = new_state
+
+ return EndReignResponse(
+ reward=reward,
+ session_id=request.session_id,
+ new_weights=category_weights
+ )
- # Update category weights
- update_category_weights(request.final_metrics, request.trajectory)
-
- # Log the trajectory
- log_trajectory(request, reward)
-
- # Clean up the session
- # Note: We don't delete it in case the client wants to start a new reign with same session
- game_sessions[request.session_id] = GameState() # Reset the game state
-
- return EndReignResponse(
- reward=reward,
- session_id=request.session_id,
- new_weights=category_weights
- )
+ except Exception as e:
+ # Log the error for debugging
+ print(f"Error processing end_reign: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ raise HTTPException(status_code=500, detail=f"Error calculating reward: {str(e)}")
def calculate_adaptive_reward(final_metrics: Dict[str, int], trajectory: List[TrajectoryItem]) -> float:
"""
- Calculate the adaptive reward based on the final metrics and trajectory
+ Calculate the adaptive reward based on the final metrics and trajectory.
- R = power_final * P + stability_final * S + piety_final * Pi + wealth_final * W
+ Following the formula: R = power_final * P + stability_final * S + piety_final * Pi + wealth_final * W
+ Where P, S, Pi, W are the counts of cards played in each category
"""
- # Count the number of cards in each category
+ # Initialize category counts
category_counts = {"power": 0, "stability": 0, "piety": 0, "wealth": 0}
+ # Count cards played in each category
for item in trajectory:
- if item.category in category_counts:
- category_counts[item.category] += 1
+ try:
+ category = item.category.lower()
+ if category in category_counts:
+ category_counts[category] += 1
+ except Exception as e:
+ print(f"Error processing trajectory item: {str(e)}, item: {item}")
+ continue
- # Calculate the reward
- reward = (
- final_metrics["power"] * category_counts["power"] +
- final_metrics["stability"] * category_counts["stability"] +
- final_metrics["piety"] * category_counts["piety"] +
- final_metrics["wealth"] * category_counts["wealth"]
- )
-
- return reward
+ # Calculate reward using the formula from README.md
+ try:
+ # For each category, multiply final metric value by the count of cards in that category
+ reward = 0.0
+ reward += final_metrics.get("power", 50) * category_counts["power"]
+ reward += final_metrics.get("stability", 50) * category_counts["stability"]
+ reward += final_metrics.get("piety", 50) * category_counts["piety"]
+ reward += final_metrics.get("wealth", 50) * category_counts["wealth"]
+
+ # If no cards were played, use the average of final metrics as reward
+ if sum(category_counts.values()) == 0:
+ total = sum(final_metrics.get(key, 50) for key in ["power", "stability", "piety", "wealth"])
+ reward = total / 4.0
+
+ print(f"Calculated reward: {reward} based on:")
+ print(f"Final metrics: {final_metrics}")
+ print(f"Category counts: {category_counts}")
+
+ return float(reward)
+
+ except Exception as e:
+ print(f"Error in adaptive reward calculation: {str(e)}")
+ # Provide a fallback reward calculation
+ return float(sum(final_metrics.get(key, 50) for key in ["power", "stability", "piety", "wealth"]) / 4)
def update_category_weights(final_metrics: Dict[str, int], trajectory: List[TrajectoryItem]):
"""
- Update category weights using exponential moving average (EMA)
-
- weights["power"] = 0.9 * weights["power"] + 0.1 * (power_final * P_last)
- weights["stability"] = 0.9 * weights["stability"] + 0.1 * (stability_final * S_last)
- weights["piety"] = 0.9 * weights["piety"] + 0.1 * (piety_final * Pi_last)
- weights["wealth"] = 0.9 * weights["wealth"] + 0.1 * (wealth_final * W_last)
+ Update category weights using exponential moving average (EMA) based on
+ the average per-card adaptive rewards value of its associated metric.
"""
- global category_weights
-
- # Count the number of cards in each category
+ # Initialize tracking variables
+ category_totals = {"power": 0, "stability": 0, "piety": 0, "wealth": 0}
category_counts = {"power": 0, "stability": 0, "piety": 0, "wealth": 0}
+ # Calculate total reward for each category
for item in trajectory:
- if item.category in category_counts:
- category_counts[item.category] += 1
+ try:
+ category = item.category.lower()
+ if category in category_totals:
+ category_totals[category] += final_metrics.get(category, 50)
+ category_counts[category] += 1
+ except Exception as e:
+ print(f"Error processing category weight for item: {e}")
+ continue
# Update weights using EMA
alpha = 0.9 # Weight for the old value
beta = 0.1 # Weight for the new value
for category in category_weights:
- category_weights[category] = (
- alpha * category_weights[category] +
- beta * (final_metrics[category] * category_counts[category])
- )
+ # Calculate average reward for this category (use current weight if no cards in this category)
+ avg_reward = final_metrics.get(category, 50)
+ if category_counts[category] > 0:
+ avg_reward = category_totals[category] / category_counts[category]
+
+ # Update weight using EMA
+ category_weights[category] = alpha * category_weights[category] + beta * avg_reward
+
# Ensure weights stay in a reasonable range
category_weights[category] = max(1, min(100, category_weights[category]))
diff --git a/environments/dynastai/src/web/server.py b/environments/dynastai/src/web/server.py
index 923af2e9..de0afed3 100644
--- a/environments/dynastai/src/web/server.py
+++ b/environments/dynastai/src/web/server.py
@@ -1,6 +1,5 @@
+# FastAPI server for DynastAI game
"""
-FastAPI server for DynastAI game
-
This module initializes and runs the FastAPI server for the DynastAI game.
"""
diff --git a/environments/dynastai/src/web/static/css/main.css b/environments/dynastai/src/web/static/css/main.css
index f04a4422..e4a139de 100644
--- a/environments/dynastai/src/web/static/css/main.css
+++ b/environments/dynastai/src/web/static/css/main.css
@@ -1,13 +1,13 @@
- /*
+/*
DynastAI - Main CSS Styles
Medieval kingdom management game
*/
:root {
- --power-color: #e74c3c;
- --stability-color: #2ecc71;
- --piety-color: #f1c40f;
- --wealth-color: #3498db;
+ --power-color: #e74c3c; /* Red for power */
+ --stability-color: #2ecc71; /* Green for stability */
+ --piety-color: #9b59b6; /* Purple for piety */
+ --wealth-color: #f1c40f; /* Yellow for wealth */
--primary-bg: #2c3e50;
--secondary-bg: #34495e;
--card-bg: rgba(255, 255, 255, 0.95);
@@ -16,6 +16,10 @@
--shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
--header-font: 'Cinzel', serif;
--body-font: 'Lato', sans-serif;
+ --parchment: #f5e7c1;
+ --aged-parchment: #e8d8b0;
+ --dark-ink: #3a2921;
+ --border-color: #916726;
}
* {
@@ -26,29 +30,33 @@
body {
font-family: var(--body-font);
- background-color: #f5f5f5;
+ background-color: #3a2921; /* Dark woody background */
color: var(--text-color);
line-height: 1.6;
- background-image: url('https://images.unsplash.com/photo-1534196511436-921a4e99f297?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1920&q=80');
- background-size: cover;
- background-position: center;
- background-attachment: fixed;
+ position: relative;
}
header, footer {
text-align: center;
padding: 1rem;
- background-color: rgba(0, 0, 0, 0.8);
+ background-color: #28180c;
color: var(--light-text);
+ border-bottom: 3px solid var(--border-color);
}
header h1 {
font-family: var(--header-font);
- font-size: 2.5rem;
+ font-size: 3rem;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 3px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
+ color: #d4af37; /* Gold */
+}
+
+header p {
+ font-style: italic;
+ color: #c0c0c0; /* Silver */
}
main {
@@ -65,10 +73,13 @@ main {
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 2rem;
- background-color: rgba(255, 255, 255, 0.9);
+ background-color: var(--aged-parchment);
padding: 1rem;
border-radius: 10px;
box-shadow: var(--shadow);
+ border: 2px solid var(--border-color);
+ /* Create parchment texture with gradient */
+ background-image: linear-gradient(to right, rgba(222, 208, 173, 0.2) 0%, rgba(255, 255, 255, 0.1) 20%, rgba(222, 208, 173, 0.2) 40%, rgba(255, 255, 255, 0.1) 60%, rgba(222, 208, 173, 0.2) 80%);
}
.metric {
@@ -78,6 +89,14 @@ main {
text-align: center;
}
+.metric h3 {
+ font-family: var(--header-font);
+ color: var(--dark-ink);
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: 5px;
+ margin-bottom: 10px;
+}
+
.meter-container {
background-color: #ddd;
height: 20px;
@@ -85,6 +104,7 @@ main {
margin: 0.5rem 0;
overflow: hidden;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
+ border: 1px solid var(--border-color);
}
.meter {
@@ -98,6 +118,27 @@ main {
.piety { background-color: var(--piety-color); width: 50%; }
.wealth { background-color: var(--wealth-color); width: 50%; }
+.stat-change {
+ font-size: 0.9em;
+ font-weight: bold;
+ margin-left: 4px;
+ animation: pulse 1.5s infinite;
+}
+
+.positive {
+ color: var(--stability-color);
+}
+
+.negative {
+ color: var(--power-color);
+}
+
+@keyframes pulse {
+ 0% { opacity: 1; }
+ 50% { opacity: 0.5; }
+ 100% { opacity: 1; }
+}
+
#card-container {
display: flex;
justify-content: center;
@@ -105,14 +146,18 @@ main {
}
#card {
- background-color: var(--card-bg);
+ background-color: var(--parchment);
padding: 2rem;
border-radius: 10px;
box-shadow: var(--shadow);
width: 100%;
max-width: 600px;
position: relative;
- border: 1px solid #ddd;
+ border: 2px solid var(--border-color);
+ /* Create parchment texture with subtle patterns */
+ background-image:
+ repeating-linear-gradient(45deg, rgba(222, 208, 173, 0.2) 0px, rgba(222, 208, 173, 0.2) 1px, transparent 1px, transparent 3px),
+ repeating-linear-gradient(-45deg, rgba(222, 208, 173, 0.2) 0px, rgba(222, 208, 173, 0.2) 1px, transparent 1px, transparent 3px);
}
#category-indicator {
@@ -124,6 +169,7 @@ main {
border-radius: 50%;
background-color: var(--power-color);
border: 2px solid white;
+ box-shadow: var(--shadow);
}
#category-indicator.power { background-color: var(--power-color); }
@@ -135,6 +181,17 @@ main {
font-size: 1.2rem;
margin-bottom: 2rem;
line-height: 1.6;
+ color: var(--dark-ink);
+}
+
+.character-name {
+ font-weight: bold;
+ color: var(--border-color);
+ display: block;
+ margin-bottom: 8px;
+ font-style: italic;
+ font-size: 1.3rem;
+ font-family: var(--header-font);
}
#card-options {
@@ -150,32 +207,46 @@ button {
font-size: 1rem;
font-weight: bold;
transition: all 0.2s ease;
+ font-family: var(--header-font);
+ letter-spacing: 1px;
}
.choice-btn {
min-width: 150px;
+ border: 2px solid #7d5614;
}
#yes-button {
- background-color: #2ecc71;
+ background-color: #2e7d32;
color: white;
}
#no-button {
- background-color: #e74c3c;
+ background-color: #c62828;
color: white;
}
.primary-btn {
- background-color: #3498db;
+ background-color: #916726;
color: white;
padding: 1rem 2rem;
font-size: 1.1rem;
+ border: 2px solid #7d5614;
+}
+
+.secondary-btn {
+ background-color: #5f4520;
+ color: white;
+ padding: 0.8rem 1.5rem;
+ font-size: 1rem;
+ margin-right: 10px;
+ border: 2px solid #493814;
}
button:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
+ filter: brightness(110%);
}
#effects-display {
@@ -183,10 +254,11 @@ button:hover {
justify-content: space-around;
flex-wrap: wrap;
margin: 1rem 0;
- background-color: rgba(255, 255, 255, 0.8);
+ background-color: var(--parchment);
padding: 1rem;
border-radius: 5px;
box-shadow: var(--shadow);
+ border: 2px solid var(--border-color);
}
.effect-item {
@@ -204,23 +276,29 @@ button:hover {
.wealth-effect { color: var(--wealth-color); }
#start-screen, #game-over {
- background-color: var(--card-bg);
+ background-color: var(--parchment);
padding: 2rem;
border-radius: 10px;
box-shadow: var(--shadow);
text-align: center;
margin: 2rem auto;
max-width: 500px;
+ border: 2px solid var(--border-color);
+ /* Create parchment texture with subtle patterns */
+ background-image:
+ repeating-linear-gradient(45deg, rgba(222, 208, 173, 0.2) 0px, rgba(222, 208, 173, 0.2) 1px, transparent 1px, transparent 3px),
+ repeating-linear-gradient(-45deg, rgba(222, 208, 173, 0.2) 0px, rgba(222, 208, 173, 0.2) 1px, transparent 1px, transparent 3px);
}
#start-screen h2, #game-over h2 {
font-family: var(--header-font);
margin-bottom: 1rem;
- color: var(--primary-bg);
+ color: #916726;
}
#start-screen p {
margin-bottom: 1rem;
+ color: var(--dark-ink);
}
#game-over #final-metrics {
@@ -230,19 +308,76 @@ button:hover {
margin: 1.5rem 0;
text-align: left;
padding: 1rem;
- background-color: rgba(0, 0, 0, 0.05);
+ background-color: rgba(255, 255, 255, 0.5);
border-radius: 5px;
+ border: 1px solid var(--border-color);
+ color: var(--dark-ink);
}
#game-over #reign-summary {
margin: 1.5rem 0;
font-weight: bold;
font-size: 1.1rem;
+ color: #916726;
+}
+
+.detailed-ending {
+ background-color: rgba(30, 30, 30, 0.7);
+ color: var(--light-text);
+ padding: 15px;
+ border-radius: 8px;
+ margin: 15px 0;
+ line-height: 1.7;
+ max-width: 600px;
+ text-align: justify;
+ margin-left: auto;
+ margin-right: auto;
+ border: 1px solid var(--border-color);
+}
+
+.legacy-message {
+ font-style: italic;
+ margin: 20px 0;
+ color: #493814;
+}
+
+#reign-options {
+ display: flex;
+ gap: 15px;
+ justify-content: center;
+ margin-top: 20px;
+}
+
+.name-input {
+ margin: 15px 0;
+}
+
+.name-input label {
+ display: block;
+ margin-bottom: 5px;
+ font-weight: bold;
+ font-family: var(--header-font);
+ color: #916726;
+}
+
+.name-input input {
+ padding: 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 5px;
+ width: 100%;
+ font-size: 16px;
+ max-width: 300px;
+ margin: 0 auto 15px;
+ display: block;
+ background-color: rgba(255, 255, 255, 0.7);
+ font-family: var(--body-font);
}
footer {
margin-top: auto;
font-size: 0.9rem;
+ border-top: 3px solid var(--border-color);
+ border-bottom: none;
}
.api-status {
diff --git a/environments/dynastai/src/web/static/index.html b/environments/dynastai/src/web/static/index.html
index 9c15c1bb..7db23537 100644
--- a/environments/dynastai/src/web/static/index.html
+++ b/environments/dynastai/src/web/static/index.html
@@ -69,6 +69,10 @@
Begin Your Reign
As the new monarch, you must balance the needs of the realm while keeping your metrics in check.
If any metric reaches 0 or 100, your reign will end.
+
+
+
+
@@ -82,12 +86,17 @@
Wealth:
-
+
+
+
+
+
+
diff --git a/environments/dynastai/src/web/static/js/game.js b/environments/dynastai/src/web/static/js/game.js
index c93ab6e6..d3c74b21 100644
--- a/environments/dynastai/src/web/static/js/game.js
+++ b/environments/dynastai/src/web/static/js/game.js
@@ -8,7 +8,7 @@
*/
// Configuration
-const API_URL = 'http://localhost:9001/api';
+const API_URL = 'http://localhost:9001/api/'; // Added trailing slash
let sessionId = null;
let currentCard = null;
let gameOver = false;
@@ -22,6 +22,9 @@ let metrics = {
reign_year: 1
};
+// Track dynasty timeline
+let dynastyYear = 0;
+let rulerName = "Anonymous Ruler";
let trajectory = [];
// DOM Elements
@@ -40,6 +43,9 @@ const stabilityEffect = document.getElementById('stability-effect');
const pietyEffect = document.getElementById('piety-effect');
const wealthEffect = document.getElementById('wealth-effect');
+const dynastyYearSpan = document.getElementById('dynasty-year');
+const rulerNameInput = document.getElementById('ruler-name');
+
const effectsDisplay = document.getElementById('effects-display');
const categoryIndicator = document.getElementById('category-indicator');
const reignYear = document.getElementById('reign-year');
@@ -56,6 +62,9 @@ const finalStability = document.getElementById('final-stability');
const finalPiety = document.getElementById('final-piety');
const finalWealth = document.getElementById('final-wealth');
const reignSummary = document.getElementById('reign-summary');
+const detailedEnding = document.getElementById('detailed-ending');
+const legacyMessage = document.getElementById('legacy-message');
+const continueGameButton = document.getElementById('continue-game');
const newGameButton = document.getElementById('new-game');
const apiStatus = document.getElementById('api-status');
@@ -63,6 +72,7 @@ const apiStatus = document.getElementById('api-status');
startGameButton.addEventListener('click', startGame);
yesButton.addEventListener('click', () => makeChoice('yes'));
noButton.addEventListener('click', () => makeChoice('no'));
+continueGameButton.addEventListener('click', continueGame);
newGameButton.addEventListener('click', startGame);
/**
@@ -100,6 +110,12 @@ function updateMeters() {
wealthValue.textContent = metrics.wealth;
reignYear.textContent = metrics.reign_year;
+ // Update dynasty year (base year + reign year)
+ const dynastyYearElement = document.getElementById('dynasty-year');
+ if (dynastyYearElement) {
+ dynastyYearElement.textContent = dynastyYear + metrics.reign_year;
+ }
+
// Change colors when values get dangerous
if (metrics.power <= 20 || metrics.power >= 80) {
powerMeter.style.backgroundColor = '#ff5252';
@@ -130,18 +146,27 @@ function updateMeters() {
* Display choice effects on the UI
*/
function displayEffects(effects) {
+ // Check if the elements exist
+ if (!powerEffect || !stabilityEffect || !pietyEffect || !wealthEffect || !effectsDisplay) {
+ console.error("Effect display elements not found");
+ return;
+ }
+
// Update effect values
- powerEffect.textContent = formatEffect(effects.power);
- stabilityEffect.textContent = formatEffect(effects.stability);
- pietyEffect.textContent = formatEffect(effects.piety);
- wealthEffect.textContent = formatEffect(effects.wealth);
+ powerValue.innerHTML = `${metrics.power} ${formatEffect(effects.power)}`;
+ stabilityValue.innerHTML = `${metrics.stability} ${formatEffect(effects.stability)}`;
+ pietyValue.innerHTML = `${metrics.piety} ${formatEffect(effects.piety)}`;
+ wealthValue.innerHTML = `${metrics.wealth} ${formatEffect(effects.wealth)}`;
- // Show effects display
- effectsDisplay.classList.remove('hidden');
+ // Hide effects display since we're showing them inline
+ effectsDisplay.classList.add('hidden');
- // Hide after 3 seconds
+ // Hide the inline effects after 3 seconds
setTimeout(() => {
- effectsDisplay.classList.add('hidden');
+ powerValue.textContent = metrics.power;
+ stabilityValue.textContent = metrics.stability;
+ pietyValue.textContent = metrics.piety;
+ wealthValue.textContent = metrics.wealth;
}, 3000);
}
@@ -169,10 +194,21 @@ async function startGame() {
gameOver = false;
trajectory = [];
+ // Get ruler name from input
+ rulerName = rulerNameInput.value.trim() || "Anonymous Ruler";
+
+ // Reset dynasty year only if this is a new game (button text check)
+ if (startGameButton.textContent !== "Continue Dynasty") {
+ dynastyYear = 0;
+ }
+
// Create new game session
- const response = await fetch(`${API_URL}/new_game`, {
+ const response = await fetch(`${API_URL}new_game`, { // Removed the / for consistency
method: 'POST',
- headers: { 'Content-Type': 'application/json' }
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ session_id: sessionId // This will be null for a brand new game
+ })
});
const data = await response.json();
@@ -186,9 +222,11 @@ async function startGame() {
gameOverScreen.classList.add('hidden');
cardContainer.classList.remove('hidden');
+ // Reset the start button text for future new games
+ startGameButton.textContent = "Start New Game";
+
// Generate first card
await generateCard();
-
} catch (error) {
console.error("Error starting game:", error);
alert("Failed to start game. Please check your connection to the game server.");
@@ -200,7 +238,7 @@ async function startGame() {
*/
async function generateCard() {
try {
- const response = await fetch(`${API_URL}/generate_card`, {
+ const response = await fetch(`${API_URL}generate_card`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId })
@@ -209,7 +247,14 @@ async function generateCard() {
currentCard = await response.json();
// Update card UI
- cardText.textContent = currentCard.text;
+ let cardContent = currentCard.text;
+
+ // Always use character name from the Character field if available
+ if (currentCard.character_name) {
+ cardContent = `${currentCard.character_name}: ${cardContent}`;
+ }
+
+ cardText.innerHTML = cardContent;
yesButton.textContent = currentCard.yes_option;
noButton.textContent = currentCard.no_option;
@@ -228,7 +273,7 @@ async function generateCard() {
*/
async function makeChoice(choice) {
try {
- const response = await fetch(`${API_URL}/card_choice`, {
+ const response = await fetch(`${API_URL}card_choice`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -237,6 +282,10 @@ async function makeChoice(choice) {
})
});
+ if (!response.ok) {
+ throw new Error(`Server error: ${response.status}`);
+ }
+
const data = await response.json();
// Display choice effects
@@ -244,20 +293,50 @@ async function makeChoice(choice) {
// Record this move in trajectory
trajectory.push({
- card_id: currentCard.id,
- category: currentCard.category,
+ card_id: String(currentCard.id || "unknown"), // Ensure card_id is always a string
+ category: String(currentCard.category || "unknown"), // Ensure category is always a string
choice: choice,
- effects: currentCard.effects[choice],
- post_metrics: data.metrics
+ effects: {
+ power: Number(currentCard.effects[choice].power || 0),
+ stability: Number(currentCard.effects[choice].stability || 0),
+ piety: Number(currentCard.effects[choice].piety || 0),
+ wealth: Number(currentCard.effects[choice].wealth || 0)
+ },
+ post_metrics: {
+ power: Number(data.metrics.power),
+ stability: Number(data.metrics.stability),
+ piety: Number(data.metrics.piety),
+ wealth: Number(data.metrics.wealth),
+ reign_year: Number(data.metrics.reign_year)
+ }
});
// Update game state
metrics = data.metrics;
updateMeters();
- // Check for game over
- if (data.game_over) {
- endReign();
+ // Debug log metrics
+ console.log("Current metrics after choice:", metrics);
+
+ // Check for game over conditions
+ let reignEnded = false;
+
+ if (data.game_over === true) {
+ console.log("Game over signal from server");
+ reignEnded = true;
+ }
+
+ if (metrics.power <= 0 || metrics.power >= 100 ||
+ metrics.stability <= 0 || metrics.stability >= 100 ||
+ metrics.piety <= 0 || metrics.piety >= 100 ||
+ metrics.wealth <= 0 || metrics.wealth >= 100) {
+ console.log("Game over due to metrics limit reached");
+ reignEnded = true;
+ }
+
+ if (reignEnded) {
+ console.log("Ending reign due to game over condition");
+ await endReign();
return;
}
@@ -276,7 +355,11 @@ async function makeChoice(choice) {
async function endReign() {
try {
// Determine cause of end
- let cause = null;
+ let cause = "old_age"; // Default cause
+
+ // Log current metrics for debugging
+ console.log("End reign metrics:", metrics);
+
if (metrics.power <= 0) cause = "power_low";
else if (metrics.power >= 100) cause = "power_high";
else if (metrics.stability <= 0) cause = "stability_low";
@@ -286,20 +369,65 @@ async function endReign() {
else if (metrics.wealth <= 0) cause = "wealth_low";
else if (metrics.wealth >= 100) cause = "wealth_high";
- // Send end reign data to server
- const response = await fetch(`${API_URL}/end_reign`, {
+ console.log("Determined cause of end:", cause);
+
+ // Debug log trajectory data
+ console.log("Trajectory data:", JSON.stringify(trajectory));
+
+ // Ensure trajectory data has the correct structure
+ const cleanTrajectory = trajectory.map(item => ({
+ card_id: String(item.card_id),
+ category: String(item.category),
+ choice: String(item.choice),
+ effects: {
+ power: Number(item.effects.power || 0),
+ stability: Number(item.effects.stability || 0),
+ piety: Number(item.effects.piety || 0),
+ wealth: Number(item.effects.wealth || 0)
+ },
+ post_metrics: {
+ power: Number(item.post_metrics.power || 0),
+ stability: Number(item.post_metrics.stability || 0),
+ piety: Number(item.post_metrics.piety || 0),
+ wealth: Number(item.post_metrics.wealth || 0),
+ reign_year: Number(item.post_metrics.reign_year || 1)
+ }
+ }));
+
+ // Send end reign data to server with all required fields
+ const response = await fetch(`${API_URL}end_reign`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
- trajectory: trajectory,
- final_metrics: metrics,
- reign_length: metrics.reign_year,
+ trajectory: cleanTrajectory,
+ final_metrics: {
+ power: Number(metrics.power),
+ stability: Number(metrics.stability),
+ piety: Number(metrics.piety),
+ wealth: Number(metrics.wealth),
+ reign_year: Number(metrics.reign_year)
+ },
+ reign_length: Number(metrics.reign_year),
cause_of_end: cause
})
});
- const data = await response.json();
+ // Debug log for API response
+ console.log("End reign response status:", response.status);
+
+ let data = { reward: 0 };
+
+ if (response.ok) {
+ data = await response.json();
+ console.log("End reign response data:", data);
+ } else {
+ console.error("End reign failed with status:", response.status);
+ // Even if the API call fails, we should still show the game over screen
+ }
+
+ // Force game over state
+ gameOver = true;
// Show game over screen
cardContainer.classList.add('hidden');
@@ -311,26 +439,239 @@ async function endReign() {
finalPiety.textContent = metrics.piety;
finalWealth.textContent = metrics.wealth;
- // Set reason based on metrics
+ // Set reason based on metrics and get detailed ending
let reason = "";
- if (metrics.power <= 0) reason = "You lost all authority. The nobles overthrew you!";
- else if (metrics.power >= 100) reason = "Your absolute power made you a tyrant. You were assassinated!";
- else if (metrics.stability <= 0) reason = "The people revolted against your rule!";
- else if (metrics.stability >= 100) reason = "The people loved you so much they established a republic!";
- else if (metrics.piety <= 0) reason = "The church declared you a heretic and had you executed!";
- else if (metrics.piety >= 100) reason = "The church became too powerful and took control of your kingdom!";
- else if (metrics.wealth <= 0) reason = "Your kingdom went bankrupt and you were deposed!";
- else if (metrics.wealth >= 100) reason = "Your vast wealth attracted invaders who conquered your kingdom!";
+ let detailedText = "";
+ let legacyText = "";
+ // Set reason based on cause
+ if (cause === "power_low") {
+ reason = "You lost all authority. The nobles overthrew you!";
+ detailedText = "Years of concessions and weak leadership eroded your authority. The nobles, seeing your weakness, formed a coalition against you. After a brief struggle, you were deposed and exiled, remembered as a ruler who couldn't maintain the respect of the nobility.";
+ legacyText = determineRulerLegacy("weak");
+ } else if (cause === "power_high") {
+ reason = "Your absolute power made you a tyrant. You were assassinated!";
+ detailedText = "Your iron-fisted rule and consolidation of power bred resentment among the nobility. As your authority grew unchecked, many feared for their own positions. A conspiracy formed in the shadows, and despite your vigilance, an assassin's blade found its mark. You died as you ruled - alone and feared.";
+ legacyText = determineRulerLegacy("tyrant");
+ } else if (cause === "stability_low") {
+ reason = "The people revolted against your rule!";
+ detailedText = "The cries of the hungry and oppressed grew too loud to ignore. Years of neglect and harsh policies turned the populace against you. What began as isolated protests quickly spread across the kingdom. The uprising was swift and merciless, with angry mobs storming the palace. Your reign ended at the hands of those you failed to serve.";
+ legacyText = determineRulerLegacy("hated");
+ } else if (cause === "stability_high") {
+ reason = "The people loved you so much they established a republic!";
+ detailedText = "The common folk adored you for your generosity and fairness. However, your popularity threatened the traditional power structure. As people began calling for democratic reforms and greater representation, the nobles and church became alarmed. They orchestrated your removal, claiming the kingdom needed 'proper governance, not popularity.' The republic that followed bore your name, though you did not live to see it flourish.";
+ legacyText = determineRulerLegacy("beloved");
+ } else if (cause === "piety_low") {
+ reason = "The church declared you a heretic and had you executed!";
+ detailedText = "Your dismissal of religious traditions and constant conflicts with church authorities were deemed heretical. The Grand Inquisitor publicly denounced you, turning religious sentiment against the crown. Priests preached against you from every pulpit until the faithful rose up in a holy crusade. Declared a heretic, you faced the ultimate punishment for challenging divine authority.";
+ legacyText = determineRulerLegacy("heretic");
+ } else if (cause === "piety_high") {
+ reason = "The church became too powerful and took control of your kingdom!";
+ detailedText = "You allowed religious authorities too much influence, and the church's power grew unchecked. Gradually, religious law superseded royal edicts, and church officials began overruling your decisions. Eventually, the Archbishop declared divine right to rule, and with popular support, established a theocracy. You were permitted to retain your title in name only - a figurehead in a kingdom ruled by the cloth.";
+ legacyText = determineRulerLegacy("pious");
+ } else if (cause === "wealth_low") {
+ reason = "Your kingdom went bankrupt and you were deposed!";
+ detailedText = "Years of extravagance and financial mismanagement emptied the royal coffers. Unable to pay the army or maintain the kingdom's infrastructure, your rule collapsed under mounting debts. Foreign creditors seized royal assets, while unpaid servants and soldiers abandoned their posts. With nothing left to rule, you were quietly removed from the throne, your name becoming synonymous with fiscal irresponsibility.";
+ legacyText = determineRulerLegacy("poor");
+ } else if (cause === "wealth_high") {
+ reason = "Your vast wealth attracted invaders who conquered your kingdom!";
+ detailedText = "Your kingdom's legendary wealth attracted unwanted attention. Neighboring rulers looked upon your treasuries with envy, and despite your diplomatic efforts, greed won out. A coalition of foreign powers, using your hoarding of wealth as justification, invaded with overwhelming force. Your vast riches funded your enemies' armies, and your kingdom was divided among the victors.";
+ legacyText = determineRulerLegacy("wealthy");
+ } else {
+ reason = "You died of natural causes after a long reign.";
+ detailedText = `After ${metrics.reign_year} years of rule, age finally caught up with you. Your legacy secured, you passed peacefully in your sleep, surrounded by generations of family. The kingdom mourned for forty days, and your achievements were recorded in detail by royal historians. Few monarchs are fortunate enough to meet such a natural end, a testament to your balanced approach to leadership.`;
+ legacyText = determineRulerLegacy("balanced");
+ }
+
+ // Generate epithet
+ const epithet = generateEpithet(cause, metrics);
+
+ // Make sure to display reward information
gameOverReason.textContent = reason;
- reignSummary.textContent = `You ruled for ${metrics.reign_year} years. Final reward: ${data.reward.toFixed(2)}`;
+ detailedEnding.textContent = detailedText;
+ legacyMessage.textContent = legacyText;
+
+ // Format the reward nicely
+ const formattedReward = data.reward !== undefined ? data.reward.toFixed(2) : "0.00";
+ reignSummary.textContent = `${rulerName} "${epithet}" ruled for ${metrics.reign_year} years. Final reward: ${formattedReward}`;
+
+ // Display the adaptive weights if available
+ if (data.new_weights) {
+ console.log("New category weights:", data.new_weights);
+ // You could display these weights in the UI if desired
+ }
+
+ // Clean up for next game
+ currentCard = null;
} catch (error) {
console.error("Error ending reign:", error);
gameOverReason.textContent = "Something went wrong when calculating your legacy.";
+
+ // Force display of game over screen even if there was an error
+ cardContainer.classList.add('hidden');
+ gameOverScreen.classList.remove('hidden');
}
}
+/**
+ * Continue the game with a new ruler in the same dynasty
+ */
+async function continueGame() {
+ try {
+ // Reset game state but keep session ID for continuity
+ gameOver = false;
+ trajectory = [];
+
+ // Update dynasty year before starting new reign
+ dynastyYear += metrics.reign_year;
+
+ // Clear the ruler name input to allow entering a new name
+ rulerNameInput.value = '';
+
+ // Create new game session with the same session ID to maintain reign history
+ const response = await fetch(`${API_URL}new_game`, { // Removed the / for consistency
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ session_id: sessionId })
+ });
+
+ const data = await response.json();
+ metrics = data.metrics;
+
+ updateMeters();
+
+ // Hide game over screen, show name input and start screen
+ gameOverScreen.classList.add('hidden');
+ startScreen.classList.remove('hidden');
+
+ // Focus the name input for convenience
+ rulerNameInput.focus();
+
+ // Update start screen text for a new ruler
+ const startHeader = document.querySelector('#start-screen h2');
+ if (startHeader) {
+ startHeader.textContent = "Begin Your New Reign";
+ }
+ startGameButton.textContent = "Continue Dynasty";
+ } catch (error) {
+ console.error("Error continuing game:", error);
+ gameOverReason.textContent = "Something went wrong when starting a new reign.";
+ }
+}
+
+/**
+ * Generate a legacy message based on reign length and ruler type
+ */
+function determineRulerLegacy(rulerType) {
+ // Generate a legacy message based on reign length and ruler type
+ const reignLength = metrics.reign_year;
+ let legacy = "";
+
+ if (reignLength < 5) {
+ legacy = "Your brief rule will be barely a footnote in the kingdom's history.";
+ } else if (reignLength > 30) {
+ switch (rulerType) {
+ case "balanced":
+ legacy = "Your long and balanced reign will be remembered as a golden age of prosperity and peace.";
+ break;
+ case "tyrant":
+ legacy = "Your decades of tyrannical rule have left a permanent scar on the kingdom's history. Your name will be used to frighten children for generations.";
+ break;
+ case "beloved":
+ legacy = "Your generous and fair leadership established a cultural renaissance that will be studied for centuries to come.";
+ break;
+ default:
+ legacy = "Your long reign, despite its end, has made an indelible mark on the kingdom's history.";
+ }
+ } else {
+ switch (rulerType) {
+ case "weak":
+ legacy = "History will remember you as a monarch who failed to maintain control of their own court.";
+ break;
+ case "tyrant":
+ legacy = "You will be remembered as a harsh and unforgiving ruler who sought power above all else.";
+ break;
+ case "hated":
+ legacy = "Your name will be spoken with contempt by commoners for generations to come.";
+ break;
+ case "beloved":
+ legacy = "The people will sing songs of your kindness and fairness for many years.";
+ break;
+ case "heretic":
+ legacy = "Religious texts will cite you as an example of the dangers of straying from the faith.";
+ break;
+ case "pious":
+ legacy = "You will be remembered as a devout ruler who perhaps trusted the clergy too much.";
+ break;
+ case "poor":
+ legacy = "Future monarchs will study your reign as a cautionary tale of financial mismanagement.";
+ break;
+ case "wealthy":
+ legacy = "Tales of your kingdom's riches will become legendary, though they ultimately led to your downfall.";
+ break;
+ case "balanced":
+ legacy = "Your rule will be remembered as a time of reasonable balance and steady progress.";
+ break;
+ default:
+ legacy = `You ruled for ${reignLength} years, leaving behind a mixed legacy of successes and failures.`;
+ }
+ }
+
+ return legacy;
+}
+
+/**
+ * Generate a fitting epithet for the ruler based on reign outcomes
+ */
+function generateEpithet(cause, metrics) {
+ // Generate epithets based on cause of end and metrics
+ if (metrics.reign_year <= 3) {
+ return "the Brief";
+ }
+
+ if (metrics.reign_year >= 30) {
+ return "the Ancient";
+ }
+
+ // Causes of death
+ switch(cause) {
+ case "power_low":
+ return "the Weak";
+ case "power_high":
+ return "the Tyrant";
+ case "stability_low":
+ return "the Cruel";
+ case "stability_high":
+ return "the Beloved";
+ case "piety_low":
+ return "the Heretic";
+ case "piety_high":
+ return "the Pious";
+ case "wealth_low":
+ return "the Bankrupt";
+ case "wealth_high":
+ return "the Opulent";
+ case "old_age":
+ // For natural death, base epithet on highest stat
+ const stats = [
+ { name: "the Just", value: metrics.stability },
+ { name: "the Mighty", value: metrics.power },
+ { name: "the Wise", value: metrics.piety },
+ { name: "the Wealthy", value: metrics.wealth }
+ ];
+
+ // Sort stats by value (highest first)
+ stats.sort((a, b) => b.value - a.value);
+
+ // Return epithet based on highest stat
+ return stats[0].name;
+ }
+
+ // Default epithet if no specific condition is met
+ return "the Monarch";
+}
+
// Check if API is available when page loads
window.addEventListener('load', async () => {
await checkApiStatus();
diff --git a/environments/dynastai/test_dynastai_api.py b/environments/dynastai/test_dynastai_api.py
deleted file mode 100755
index 3372bfcb..00000000
--- a/environments/dynastai/test_dynastai_api.py
+++ /dev/null
@@ -1,110 +0,0 @@
-#!/usr/bin/env python3
-"""
-DynastAI API Test Script
-
-This script tests the FastAPI endpoints of the DynastAI game:
-- Creating a new game
-- Getting game state
-- Generating cards
-- Processing choices
-- Ending reigns
-"""
-
-import os
-import sys
-import asyncio
-import json
-import random
-import httpx
-from dotenv import load_dotenv
-
-# Ensure the src directory is in path
-sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
-
-# Load environment variables
-load_dotenv()
-
-# API configuration
-API_URL = "http://localhost:9001/api"
-
-async def test_api():
- """Test the DynastAI API endpoints"""
- print("Testing DynastAI API...")
-
- async with httpx.AsyncClient() as client:
- # Test root endpoint
- print("\nTesting root endpoint...")
- response = await client.get(f"{API_URL}/")
- print(f"Status: {response.status_code}")
- print(f"Response: {response.json()}")
-
- # Create a new game
- print("\nCreating new game...")
- response = await client.post(f"{API_URL}/new_game")
- game_data = response.json()
- session_id = game_data["session_id"]
- print(f"Session ID: {session_id}")
- print(f"Initial metrics: {game_data['metrics']}")
-
- # Generate a card
- print("\nGenerating card...")
- response = await client.post(
- f"{API_URL}/generate_card",
- json={"session_id": session_id}
- )
- card = response.json()
- print(f"Card: {card['text']}")
- print(f"Option Yes: {card['yes_option']}")
- print(f"Option No: {card['no_option']}")
-
- # Make a choice
- print("\nMaking choice...")
- choice = random.choice(["yes", "no"])
- response = await client.post(
- f"{API_URL}/card_choice",
- json={"session_id": session_id, "choice": choice}
- )
- result = response.json()
- print(f"Choice: {choice}")
- print(f"New metrics: {result['metrics']}")
- print(f"Game over: {result.get('game_over', False)}")
-
- # End the reign
- print("\nEnding reign...")
- trajectory = [{
- "card_id": card["id"],
- "category": card["category"],
- "choice": choice,
- "effects": card["effects"][choice],
- "post_metrics": result["metrics"]
- }]
-
- response = await client.post(
- f"{API_URL}/end_reign",
- json={
- "session_id": session_id,
- "trajectory": trajectory,
- "final_metrics": result["metrics"],
- "reign_length": result["metrics"]["reign_year"],
- "cause_of_end": "test_termination"
- }
- )
- end_data = response.json()
- print(f"Reward: {end_data['reward']}")
- print(f"New weights: {end_data['new_weights']}")
-
- print("\nAPI tests completed successfully!")
- return True
-
-if __name__ == "__main__":
- print("DynastAI API Test")
- print("================")
-
- # Check if server is running in a separate process
- print("NOTE: This test assumes the DynastAI server is running.")
- print("Please start the server with 'python dynastai_server.py' before running this test.")
-
- input("Press Enter to continue...")
-
- # Run the test
- asyncio.run(test_api())
diff --git a/environments/dynastai/test_dynastai_env.py b/environments/dynastai/test_dynastai_env.py
index 93d09afb..0a6b42e0 100755
--- a/environments/dynastai/test_dynastai_env.py
+++ b/environments/dynastai/test_dynastai_env.py
@@ -86,23 +86,10 @@ async def test_environment():
def _generate_card_internal(self, metrics, category_weights):
"""Internal method for card generation during testing"""
- from src.game_logic import generate_mock_card
+ from src.game_logic import generate_card
- # Select a category based on weights
- categories = list(category_weights.keys())
- weights = [category_weights[cat] for cat in categories]
- total_weight = sum(weights)
-
- # Normalize weights
- if total_weight > 0:
- normalized_weights = [w/total_weight for w in weights]
- else:
- normalized_weights = [1/len(categories)] * len(categories)
-
- category = random.choices(categories, weights=normalized_weights, k=1)[0]
-
- # Use the mock generator for testing
- return generate_mock_card(metrics, category)
+ # Use the main card generator which now checks cards.json first
+ return generate_card(metrics, category_weights)
# Add the test method to the environment class
DynastAIEnv._generate_card_internal = _generate_card_internal
diff --git a/environments/dynastai/verify_install.py b/environments/dynastai/verify_install.py
new file mode 100755
index 00000000..f84f378e
--- /dev/null
+++ b/environments/dynastai/verify_install.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+"""
+DynastAI Installation Verification Script
+
+This script checks if all required packages are installed properly.
+"""
+
+import sys
+import platform
+import importlib.util
+
+def check_module(module_name):
+ """Check if a module is installed and report its version if available"""
+ try:
+ spec = importlib.util.find_spec(module_name)
+ if spec is None:
+ return False, None
+
+ module = importlib.import_module(module_name)
+ version = getattr(module, "__version__", "Unknown")
+ return True, version
+ except ImportError:
+ return False, None
+
+def main():
+ print(f"Python version: {platform.python_version()}")
+ print(f"Platform: {platform.platform()}")
+ print("\nChecking required modules:")
+
+ required_modules = [
+ "fastapi", "uvicorn", "pydantic", "requests", "httpx",
+ "python_multipart", "uuid", "aiohttp", "jinja2",
+ "tqdm", "numpy", "wandb", "datasets"
+ ]
+
+ all_found = True
+ for module in required_modules:
+ found, version = check_module(module)
+ status = f"✓ Found (version: {version})" if found else "✗ Not found"
+ print(f"- {module}: {status}")
+ if not found:
+ all_found = False
+
+ # Special check for built-in uuid module which is critical
+ if not check_module("uuid")[0]:
+ print("\nWARNING: The UUID module is missing. This is a built-in Python module and should be available.")
+ all_found = False
+
+ if all_found:
+ print("\nAll required packages are installed! You should be able to run DynastAI successfully.")
+ else:
+ print("\nSome packages are missing. Run 'python setup.py' to install them.")
+
+if __name__ == "__main__":
+ main()