# Codebase Dump (excluding other games) Created: Wed Mar 12 10:18:09 UTC 2025 ## Directory Structure ``` ./llm-olympics/CLAUDE.md ./llm-olympics/ELO_RATINGS.md ./llm-olympics/backup/client.env ./llm-olympics/backup/server.env ./llm-olympics/change.sh ./llm-olympics/client/package.json ./llm-olympics/client/public/manifest.json ./llm-olympics/client/src/App.js ./llm-olympics/client/src/apiConfig.js ./llm-olympics/client/src/components/GameBoard.jsx ./llm-olympics/client/src/components/MatchCard.jsx ./llm-olympics/client/src/components/MoveDetailsModal.jsx ./llm-olympics/client/src/components/MovesList.jsx ./llm-olympics/client/src/components/Navbar.jsx ./llm-olympics/client/src/gameRegistry.js ./llm-olympics/client/src/games/RockPaperScissors.css ./llm-olympics/client/src/games/RockPaperScissorsRenderer.jsx ./llm-olympics/client/src/index.css ./llm-olympics/client/src/index.js ./llm-olympics/client/src/pages/Admin.jsx ./llm-olympics/client/src/pages/CreateMatch.jsx ./llm-olympics/client/src/pages/Dashboard.jsx ./llm-olympics/client/src/pages/EloRankings.jsx ./llm-olympics/client/src/pages/GameDetail.jsx ./llm-olympics/client/src/pages/GameStats.jsx ./llm-olympics/client/src/pages/MatchDetail.jsx ./llm-olympics/client/src/pages/ModelEloOverview.jsx ./llm-olympics/client/src/setupProxy.js ./llm-olympics/client/src/socketConfig.js ./llm-olympics/client/src/styles/GameStats.css ./llm-olympics/client/tailwind.config.js ./llm-olympics/package.json ./llm-olympics/scripts/initialize_elo.sh ./llm-olympics/scripts/initialize_elo_tables.js ./llm-olympics/scripts/initialize_playername_elo.js ./llm-olympics/scripts/initialize_playername_elo.sh ./llm-olympics/scripts/package.json ./llm-olympics/scripts/schema.sql ./llm-olympics/server/database.js ./llm-olympics/server/games/rockpaperscissors.js ./llm-olympics/server/games/tictactoe.js ./llm-olympics/server/index.js ./llm-olympics/server/matchRunner.js ./llm-olympics/server/openaiService.js ./llm-olympics/server/package.json ./llm-olympics/server/routes/elo.js ./llm-olympics/server/routes/games.js ./llm-olympics/server/routes/matches.js ./llm-olympics/server/routes/moves.js ./llm-olympics/server/routes/players.js ./llm-olympics/server/routes/scheduler.js ./llm-olympics/server/routes/stats.js ./llm-olympics/server/services/eloService.js ./llm-olympics/server/services/matchScheduler.js ./llm-olympics/server/socket.js ./llm-olympics/server/utils.js ./llm-olympics/server/utils/elo.js ./llm-olympics/server/utils/elo_tables.sql ./llm-olympics/server/utils/elo_tables_updated.sql ./llm-olympics/server/utils/logger.js ./llm-olympics/start.sh ``` ## File Contents ### ./llm-olympics/CLAUDE.md ```md # LLM Olympics Development Guide ## Build Commands - Client: `cd client && npm start` (dev server) or `npm run build` (production) - Server: `cd server && npm run dev` (with auto-restart) or `npm start` - Full application: `./start.sh` (starts both client and server) ## Test Commands - Client: `cd client && npm test` - Server: `cd server && npm test` ## Code Style Guidelines - Variables/functions: camelCase - Components/Classes: PascalCase - Files: PascalCase for components (.jsx), camelCase for utilities (.js) - Async: Use async/await pattern consistently - Error handling: Try/catch blocks with proper logging - Logging: Use utils/logger.js with appropriate log levels - React components: Functional components with hooks preferred - State management: Local state with useState, complex state with useReducer - Database queries: Async/await with proper error handling ## Project Structure - `/client`: React frontend with components, pages, game renderers - `/server`: Express backend with game logic, API routes, services - `/scripts`: Database initialization scripts - Game implementation: Logic in server/games/, UI in client/games/ ## Environment - Server runs on port 3302, client on port 3006 (configurable via env vars)``` ### ./llm-olympics/ELO_RATINGS.md ```md # Elo Rating System for LLM Olympics This document describes the Elo rating system implementation for the LLM Olympics platform. ## Overview The Elo rating system is a method for calculating the relative skill levels of players in zero-sum games. In LLM Olympics, we use Elo ratings to rank both individual players and AI models across different games. ## Features - **Per-Game Elo Ratings**: Each player and model has a separate Elo rating for each game - **Model-Based Rankings**: Aggregate rankings across models, independent of specific player instances - **Historical Tracking**: Track Elo rating changes over time - **Visual Comparisons**: Compare performance across different games ## How Elo Works 1. All players and models start with a base rating of 1200 2. After each match, ratings are updated based on: - The outcome of the match (win, loss, or draw) - The rating difference between opponents 3. Winning against a higher-rated opponent results in a bigger rating increase 4. Losing against a lower-rated opponent results in a bigger rating decrease ## Implementation Details ### Database Tables - `player_game_elo`: Stores player Elo ratings per game - `model_elo`: Stores model Elo ratings per game - `elo_history`: Tracks player rating changes - `model_elo_history`: Tracks model rating changes ### Calculation The Elo rating calculation uses the standard formula: ``` Expected Score = 1 / (1 + 10^((Rating2 - Rating1) / 400)) New Rating = Old Rating + K * (Actual Score - Expected Score) ``` Where: - K-factor = 32 (determines how much ratings change after each match) - Actual Score = 1 for win, 0.5 for draw, 0 for loss ## Setup Instructions 1. Run the initialization script: ```bash cd llm-olympics/scripts chmod +x initialize_elo.sh ./initialize_elo.sh ``` 2. This will: - Create the necessary database tables - Initialize ratings for existing players and models - Process historical matches to calculate current ratings ## Viewing Elo Ratings You can view Elo ratings through the web interface: - **Per-Game Ratings**: Visit `/elo/:gameId` to see ratings for a specific game - **Model Comparisons**: Visit `/models/elo` to compare models across all games ## API Endpoints The following API endpoints are available: - GET `/api/elo/games/:gameId/players` - Get player Elo ratings for a game - GET `/api/elo/games/:gameId/models` - Get model Elo ratings for a game - GET `/api/elo/players/:playerId/history` - Get rating history for a player - GET `/api/elo/models/:model/history` - Get rating history for a model - POST `/api/elo/matches/:matchId/process` - Process Elo for a specific match ## Maintenance Elo ratings are automatically calculated after each match. If you need to recalculate ratings, you can run the initialization script again to process all historical matches. ## Future Improvements - Adjustable K-factor based on player experience - Rating confidence intervals - Season-based ratings and resets - Rating decay for inactive players/models ``` ### ./llm-olympics/backup/client.env ```env # Client Configuration PORT=3006 REACT_APP_SERVER_PORT=3302 REACT_APP_SERVER_URL=http://localhost:3302 # Other client settings BROWSER=none ``` ### ./llm-olympics/backup/server.env ```env # Server Configuration PORT=3302 NODE_ENV=development # MySQL Configuration DB_HOST=localhost DB_USER=root DB_PASSWORD=appleman1Q!a DB_NAME=llm_olympics MYSQL_ROOT_PASSWORD=appleman1Q!a # OpenAI Configuration OPENAI_API_KEY=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 # Client Configuration (for CORS and redirects) CLIENT_PORT=3006 ``` ### ./llm-olympics/change.sh File skipped (more than 3000 lines: 4984 lines) ### ./llm-olympics/client/package.json ```json { "name": "llm-olympics-client", "version": "1.0.0", "description": "LLM Olympics Game Platform Client", "private": true, "dependencies": { "@headlessui/react": "^1.7.17", "axios": "^1.6.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.21.0", "react-scripts": "5.0.1", "socket.io-client": "^4.7.2", "tailwindcss": "^3.3.6" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ``` ### ./llm-olympics/client/public/manifest.json ```json { "short_name": "LLM Olympics", "name": "LLM Olympics - AI Game Platform", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ``` ### ./llm-olympics/client/src/App.js ```js import React from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import Dashboard from './pages/Dashboard'; import MatchDetail from './pages/MatchDetail'; import Navbar from './components/Navbar'; import GameStats from './pages/GameStats'; import GameDetail from './pages/GameDetail'; import ModelEloOverview from './pages/ModelEloOverview'; import Admin from './pages/Admin'; function App() { return (
} /> } /> } /> } /> } /> } /> } />
); } export default App; ``` ### ./llm-olympics/client/src/apiConfig.js ```js // Read server URL from environment variable or use default const getApiBaseUrl = () => { // Get the protocol and host from the current URL const protocol = window.location.protocol; const hostname = window.location.hostname; // Default to localhost for development if (hostname === 'localhost' || hostname === '127.0.0.1') { // Read from .env if available, otherwise use default port 3001 const port = process.env.REACT_APP_SERVER_PORT || 3302; return `${protocol}//${hostname}:${port}`; } // For production, use the same host but different port if specified const port = process.env.REACT_APP_SERVER_PORT; return port ? `${protocol}//${hostname}:${port}` : `${protocol}//${hostname}`; }; export const API_BASE_URL = getApiBaseUrl(); ``` ### ./llm-olympics/client/src/components/GameBoard.jsx ```jsx import React, { useEffect, useState } from 'react'; import { getGameRenderer } from '../gameRegistry'; const GameBoard = ({ gameType, gameState, moveHistory = [] }) => { const [Renderer, setRenderer] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function loadRenderer() { if (!gameType) { setError('No game type specified'); setLoading(false); return; } try { setLoading(true); const RendererComponent = await getGameRenderer(gameType); if (RendererComponent) { setRenderer(() => RendererComponent); setError(null); } else { setError(`No renderer found for game type: ${gameType}`); } } catch (err) { console.error('Error loading game renderer:', err); setError(`Failed to load renderer: ${err.message}`); } finally { setLoading(false); } } loadRenderer(); }, [gameType]); if (loading) { return (
Loading game renderer...
); } if (error) { return (
Error:
{error}
); } if (!gameState && (!moveHistory || moveHistory.length === 0)) { return (
Loading game state...
Game type: {gameType || 'unknown'}
); } // If we have a renderer, use it if (Renderer) { return ; } // Fallback when something went wrong return (

Could not render game: {gameType}

        {JSON.stringify(gameState, null, 2)}
      
); }; export default GameBoard; ``` ### ./llm-olympics/client/src/components/MatchCard.jsx ```jsx import React from 'react'; import { Link } from 'react-router-dom'; const MatchCard = ({ match }) => { const getStatusColor = (status) => { switch (status) { case 'completed': return 'bg-green-100 text-green-800'; case 'running': return 'bg-blue-100 text-blue-800'; case 'queued': return 'bg-yellow-100 text-yellow-800'; case 'pending': return 'bg-gray-100 text-gray-800'; case 'error': return 'bg-red-100 text-red-800'; default: return 'bg-gray-100 text-gray-800'; } }; const formatDate = (dateString) => { if (!dateString) return 'N/A'; const date = new Date(dateString); return date.toLocaleString(); }; return (

{match.game_name} Match #{match.id}

Created: {formatDate(match.created_at)}

{match.status}
{match.results && (

{match.results.winner ? `Winner: ${match.results.winner}` : 'No winner'}

)}
); }; export default MatchCard; ``` ### ./llm-olympics/client/src/components/MoveDetailsModal.jsx ```jsx import React, { useState, useEffect } from 'react'; import { API_BASE_URL } from '../apiConfig'; const MoveDetailsModal = ({ moveId, onClose }) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [moveDetails, setMoveDetails] = useState(null); const [activeTab, setActiveTab] = useState('prompt'); useEffect(() => { // Update the error handling in MoveDetailsModal.jsx const fetchMoveDetails = async () => { try { setLoading(true); console.log(`Fetching move details for ID: ${moveId} from ${API_BASE_URL}/api/moves/${moveId}`); const response = await fetch(`${API_BASE_URL}/api/moves/${moveId}`); if (!response.ok) { // Add detailed error logging const errorText = await response.text(); console.error(`API response error (${response.status}): ${errorText}`); if (response.status === 404) { throw new Error(`Move #${moveId} not found in the database. This may be a client-side move that hasn't been saved yet.`); } else { throw new Error(`Failed to fetch move details: ${response.statusText}`); } } const data = await response.json(); console.log("Received move details:", data); setMoveDetails(data); setLoading(false); } catch (err) { console.error('Error fetching move details:', err); setError(err.message); setLoading(false); } }; if (moveId) { fetchMoveDetails(); } }, [moveId]); if (!moveId) return null; if (loading) { return (

Loading move details...

); } if (error) { return (

Error

{error}

); } if (!moveDetails) { return (

Move Not Found

The requested move details could not be found.

); } return (

Move Details: {moveDetails.move_type} - {moveDetails.move_data}

Player: {moveDetails.player_name}

Game: {moveDetails.game_name}

Match ID: {moveDetails.match_id}

Turn: {moveDetails.turn_number}

Move Type: {moveDetails.move_type}

Created: {new Date(moveDetails.created_at).toLocaleString()}

{activeTab === 'prompt' && (

Prompt Sent to AI

                  {moveDetails.prompt || 'No prompt data available'}
                
)} {activeTab === 'response' && (

AI Response

                  {moveDetails.response || 'No response data available'}
                
)} {activeTab === 'reasoning' && (

AI Reasoning

                  {moveDetails.reasoning || 'No reasoning data available'}
                
)} {activeTab === 'state' && (

Game State

                  {moveDetails.full_state 
                    ? JSON.stringify(moveDetails.full_state, null, 2) 
                    : 'No state data available'}
                
)}
); }; export default MoveDetailsModal; ``` ### ./llm-olympics/client/src/components/MovesList.jsx ```jsx // Update MovesList.jsx import React, { useState, useEffect } from 'react'; import MoveDetailsModal from './MoveDetailsModal'; import axios from 'axios'; import { API_BASE_URL } from '../apiConfig'; const MovesList = ({ moves, matchId, title = "Moves" }) => { const [selectedMoveId, setSelectedMoveId] = useState(null); const [databaseMoves, setDatabaseMoves] = useState([]); const [loading, setLoading] = useState(false); // Load database moves when matchId changes useEffect(() => { if (matchId) { setLoading(true); axios.get(`${API_BASE_URL}/api/moves/match/${matchId}`) .then(response => { console.log("Loaded moves from database:", response.data); setDatabaseMoves(response.data); setLoading(false); }) .catch(err => { console.error("Error loading moves from database:", err); setLoading(false); }); } }, [matchId]); // Merge client-side moves with database moves const allMoves = databaseMoves.length > 0 ? databaseMoves : moves; const getMoveTypeColor = (moveType) => { switch (moveType) { case 'move': return 'bg-blue-100 text-blue-800'; case 'collision': return 'bg-red-100 text-red-800'; case 'eat': return 'bg-green-100 text-green-800'; case 'place': return 'bg-purple-100 text-purple-800'; case 'initialize': return 'bg-gray-100 text-gray-800'; case 'pre_start': return 'bg-yellow-100 text-yellow-800'; case 'end': return 'bg-orange-100 text-orange-800'; case 'tick': return 'bg-gray-100 text-gray-500'; case 'auto_correct': return 'bg-yellow-100 text-yellow-800'; default: return 'bg-gray-100 text-gray-800'; } }; const formatTime = (dateString) => { if (!dateString) return 'N/A'; const date = new Date(dateString); return date.toLocaleTimeString(); }; // Check if a move has a database ID before showing modal const handleMoveClick = (move) => { if (move.id && typeof move.id === 'number') { setSelectedMoveId(move.id); } else { console.warn("This move doesn't have a valid database ID:", move); alert("Move details are only available for completed moves stored in the database."); } }; return (
{title && (

{title}

{loading && Loading...}
)} {allMoves.length === 0 ? (
No moves to display
) : (
{allMoves.map((move, index) => { // Handle different property names between client and database moves const turnNumber = move.turn_number !== undefined ? move.turn_number : (move.turnNumber || index); const playerName = move.player_name || move.player || 'System'; const moveType = move.move_type || 'move'; const moveData = move.move_data || move.move || '-'; return ( move.id && handleMoveClick(move)} > ); })}
Turn Player Type Move Time
{turnNumber}
{playerName}
{moveType} {moveData} {formatTime(move.created_at || move.timestamp || new Date())}
)} {selectedMoveId && ( setSelectedMoveId(null)} /> )}
); }; export default MovesList; ``` ### ./llm-olympics/client/src/components/Navbar.jsx ```jsx import React from 'react'; import { Link } from 'react-router-dom'; const Navbar = () => { return ( ); }; export default Navbar; ``` ### ./llm-olympics/client/src/gameRegistry.js ```js // Game registry for dynamic imports const gameRegistry = {}; // Register a game renderer export function registerGameRenderer(gameType, importFn) { const normalizedType = gameType.toLowerCase().replace(/[^a-z0-9]/g, ''); gameRegistry[normalizedType] = importFn; } // Get a game renderer component (dynamically imports when needed) export async function getGameRenderer(gameType) { const normalizedType = gameType?.toLowerCase().replace(/[^a-z0-9]/g, '') || ''; // Check if game type exists in registry if (gameRegistry[normalizedType]) { try { // Dynamically import the renderer const module = await gameRegistry[normalizedType](); return module.default; } catch (error) { console.error(`Error loading renderer for ${gameType}:`, error); return null; } } console.warn(`No renderer registered for game type: ${gameType}`); return null; } // Register available games - this is the only place with knowledge of specific games registerGameRenderer('tictactoe', () => import('./games/TicTacToeRenderer')); registerGameRenderer('connectfour', () => import('./games/ConnectFourRenderer')); registerGameRenderer('highlow', () => import('./games/HighLowRenderer')); registerGameRenderer('hangman', () => import('./games/HangmanRenderer')); registerGameRenderer('rockpaperscissors', () => import('./games/RockPaperScissorsRenderer')); registerGameRenderer('twenty-questions', () => import('./games/TwentyQuestionsRenderer')); registerGameRenderer('wordle', () => import('./games/WordleRenderer')); registerGameRenderer('poker', () => import('./games/PokerRenderer')); //registerGameRenderer('tetris', () => import('./games/TetrisRenderer')); //registerGameRenderer('snake', () => import('./games/SnakeRenderer')); // Export available game types for UI export function getAvailableGames() { return Object.keys(gameRegistry); } ``` ### ./llm-olympics/client/src/games/RockPaperScissors.css ```css .rps-container { display: flex; flex-direction: column; width: 100%; max-width: 800px; margin: 0 auto; padding: 1.5rem; } /* Player info and scores */ .player-info-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; } .player-card { flex: 1; padding: 1rem; background-color: #f5f5f5; border-radius: 8px; text-align: center; transition: all 0.3s ease; max-width: 200px; } .player-card.winner { box-shadow: 0 0 15px rgba(255, 215, 0, 0.6); transform: scale(1.05); background-color: #fff8e1; } .player-name { font-weight: bold; margin-bottom: 0.5rem; font-size: 1.1rem; } .player-score { font-size: 1.8rem; font-weight: bold; color: #2196f3; } .vs-text { margin: 0 1.5rem; font-weight: bold; font-size: 1.2rem; color: #9e9e9e; } /* Current round */ .current-round { display: flex; flex-direction: column; align-items: center; margin-bottom: 2rem; } .round-number { font-size: 1.2rem; font-weight: bold; margin-bottom: 1.5rem; color: #37474f; } .moves-container { display: flex; justify-content: center; gap: 3rem; margin-bottom: 1.5rem; } .move-card { display: flex; flex-direction: column; align-items: center; width: 120px; height: 160px; padding: 1rem; background-color: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); transition: all 0.3s ease; } .move-card.animate-move { animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both; } @keyframes shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 40%, 60% { transform: translate3d(4px, 0, 0); } } .move-label { font-size: 0.9rem; color: #757575; margin-bottom: 0.5rem; } .move-icon { font-size: 3rem; margin: 0.5rem 0; } .move-name { font-weight: bold; text-transform: capitalize; } .round-result { font-size: 1.2rem; font-weight: bold; padding: 0.75rem 1.5rem; border-radius: 4px; animation: fadeIn 0.5s ease; } .tie-result { color: #9e9e9e; background-color: #f5f5f5; } .win-result { color: #2e7d32; background-color: #e8f5e9; } /* Round history */ .round-history { margin-bottom: 2rem; } .history-title { font-size: 1.1rem; font-weight: bold; margin-bottom: 1rem; color: #37474f; } .history-container { display: flex; flex-direction: column; gap: 0.75rem; max-height: 300px; overflow-y: auto; padding: 0.5rem; border: 1px solid #e0e0e0; border-radius: 8px; } .history-round { display: flex; align-items: center; padding: 0.75rem; background-color: #f9f9f9; border-radius: 4px; } .history-round-number { font-weight: bold; width: 80px; } .history-moves { flex: 1; display: flex; justify-content: space-between; padding: 0 1rem; } .history-player-move { display: flex; gap: 0.5rem; align-items: center; } .history-player-name { font-weight: 500; } .history-move-icon { font-size: 1.2rem; } .history-result { width: 100px; text-align: right; font-weight: 500; font-style: italic; color: #555; } /* Game over */ .game-over { text-align: center; padding: 1.5rem; margin: 1rem 0; background-color: #e8f5e9; border-radius: 8px; animation: fadeIn 0.5s ease; } .game-over-title { font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem; color: #2e7d32; } .game-winner { font-size: 1.2rem; font-weight: 500; } .game-tie { font-size: 1.2rem; font-weight: 500; color: #616161; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } ``` ### ./llm-olympics/client/src/games/RockPaperScissorsRenderer.jsx ```jsx import React, { useState, useEffect, useMemo } from 'react'; import './RockPaperScissors.css'; // Helper function to create a deep copy const deepCopy = (obj) => JSON.parse(JSON.stringify(obj)); // Icons for rock, paper, scissors const MOVE_ICONS = { 'rock': '✊', 'paper': '✋', 'scissors': '✌️' }; const RockPaperScissorsRenderer = ({ gameState, moveHistory }) => { // Replay state const [currentTurn, setCurrentTurn] = useState(0); const [isAutoPlaying, setIsAutoPlaying] = useState(false); const [autoPlaySpeed, setAutoPlaySpeed] = useState(1000); const [showAnimation, setShowAnimation] = useState(false); // Process the game history to create a replayable sequence const { stateHistory, maxTurns } = useMemo(() => { // Initialize with empty history const history = []; // If we have a complete gameState but no move history if (gameState && !moveHistory?.length) { history.push(gameState); return { stateHistory: history, maxTurns: 0 }; } // If we have move history, reconstruct the game state at each step if (moveHistory && moveHistory.length > 0) { console.log('Reconstructing RPS game history from moves'); // Filter and sort relevant moves const relevantMoves = moveHistory.filter(move => move.move_type === 'initialize' || move.move_type === 'select' || move.move_type === 'resolve' || move.move_type === 'end' ).sort((a, b) => { if (a.move_type === 'initialize') return -1; if (b.move_type === 'initialize') return 1; return a.turn_number - b.turn_number; }); // Initial state let currentState = { players: [], currentRound: 0, totalRounds: 10, moves: {}, history: [], scores: {}, game_over: false, winner: null }; // Extract players from gameState if (gameState && gameState.players) { currentState.players = [...gameState.players]; currentState.players.forEach(player => { currentState.scores[player] = 0; }); } // Add initial state to history history.push(deepCopy(currentState)); // Group moves by rounds const movesByRound = {}; for (const move of relevantMoves) { if (move.move_type === 'initialize') { if (move.coordinates) { const coords = typeof move.coordinates === 'string' ? JSON.parse(move.coordinates) : move.coordinates; if (coords.totalRounds) { currentState.totalRounds = coords.totalRounds; } } continue; } // For select and resolve moves, group by round const round = move.turn_number; if (!movesByRound[round]) { movesByRound[round] = { select: [], resolve: null, end: null }; } if (move.move_type === 'select') { movesByRound[round].select.push(move); } else if (move.move_type === 'resolve') { movesByRound[round].resolve = move; } else if (move.move_type === 'end') { movesByRound[round].end = move; } } // Process each round Object.keys(movesByRound).sort((a, b) => parseInt(a) - parseInt(b)).forEach(round => { const roundMoves = movesByRound[round]; // Process player selections if (roundMoves.select && roundMoves.select.length > 0) { // Create new state for each player selection for (const move of roundMoves.select) { const newState = deepCopy(currentState); // Update the moves if (move.player_name && move.move_data) { newState.moves[move.player_name] = move.move_data; } // Add state to history history.push(deepCopy(newState)); currentState = newState; } } // Process round resolution if (roundMoves.resolve) { const newState = deepCopy(currentState); if (roundMoves.resolve.coordinates) { const coords = typeof roundMoves.resolve.coordinates === 'string' ? JSON.parse(roundMoves.resolve.coordinates) : roundMoves.resolve.coordinates; if (coords.moves) { newState.moves = coords.moves; } if (coords.scores) { newState.scores = coords.scores; } if (coords.round) { const roundResult = { round: coords.round, moves: coords.moves || {}, result: coords.result || null, winner: coords.winner || null }; newState.history.push(roundResult); newState.currentRound = coords.round; newState.moves = {}; // Reset moves for next round } } // Add state to history history.push(deepCopy(newState)); currentState = newState; } // Process game end if (roundMoves.end) { const newState = deepCopy(currentState); newState.game_over = true; if (roundMoves.end.coordinates) { const coords = typeof roundMoves.end.coordinates === 'string' ? JSON.parse(roundMoves.end.coordinates) : roundMoves.end.coordinates; if (coords.winner) { newState.winner = coords.winner; } if (coords.scores) { newState.scores = coords.scores; } } // Add state to history history.push(deepCopy(newState)); currentState = newState; } }); // Add final state if we have it if (gameState && gameState.game_over) { // Only add if it's different from the last state if (history.length === 0 || JSON.stringify(gameState) !== JSON.stringify(history[history.length - 1])) { history.push(deepCopy(gameState)); } } } return { stateHistory: history, maxTurns: Math.max(0, history.length - 1) }; }, [gameState, moveHistory]); // When move history or game state changes, reset to the final state useEffect(() => { if (maxTurns > 0) { setCurrentTurn(maxTurns); } }, [maxTurns, moveHistory, gameState]); // Handle turn animation useEffect(() => { if ( currentTurn > 0 && stateHistory[currentTurn] && stateHistory[currentTurn - 1] && Object.keys(stateHistory[currentTurn].moves).length > Object.keys(stateHistory[currentTurn - 1].moves).length ) { setShowAnimation(true); const timer = setTimeout(() => { setShowAnimation(false); }, 1000); return () => clearTimeout(timer); } }, [currentTurn, stateHistory]); // Auto-play functionality useEffect(() => { let autoPlayTimer; if (isAutoPlaying && currentTurn < maxTurns) { autoPlayTimer = setTimeout(() => { setCurrentTurn(prev => Math.min(prev + 1, maxTurns)); }, autoPlaySpeed); } else if (isAutoPlaying && currentTurn >= maxTurns) { // Stop when we reach the end setIsAutoPlaying(false); } return () => { if (autoPlayTimer) clearTimeout(autoPlayTimer); }; }, [isAutoPlaying, currentTurn, maxTurns, autoPlaySpeed]); // If we have no states to display if (!stateHistory || stateHistory.length === 0) { return (
Cannot display Rock Paper Scissors game - no state available
Try refreshing the page
); } // Get the current state to display const state = stateHistory[Math.min(currentTurn, stateHistory.length - 1)] || stateHistory[0]; // Get player information const players = state.players || []; if (players.length !== 2) { return (
Invalid game state - expected 2 players
); } const [player1, player2] = players; // Get current moves const player1Move = state.moves[player1]; const player2Move = state.moves[player2]; // Determine if a round was just completed const isRoundComplete = player1Move && player2Move; // Determine round winner const determineWinner = (move1, move2) => { if (!move1 || !move2) return null; if (move1 === move2) return 'tie'; if ( (move1 === 'rock' && move2 === 'scissors') || (move1 === 'paper' && move2 === 'rock') || (move1 === 'scissors' && move2 === 'paper') ) { return player1; } return player2; }; const currentRoundWinner = determineWinner(player1Move, player2Move); // Get round history from state const roundHistory = state.history || []; // Render player information and scores const renderPlayerInfo = () => { return (
{player1}
{state.scores[player1] || 0}
VS
{player2}
{state.scores[player2] || 0}
); }; // Render the current round moves const renderCurrentRound = () => { return (
Round {state.currentRound + 1} of {state.totalRounds}
{player1}
{player1Move ? MOVE_ICONS[player1Move] : '?'}
{player1Move ? player1Move : 'waiting...'}
{player2}
{player2Move ? MOVE_ICONS[player2Move] : '?'}
{player2Move ? player2Move : 'waiting...'}
{isRoundComplete && (
{currentRoundWinner === 'tie' ? (
Tie!
) : (
{currentRoundWinner} wins this round!
)}
)}
); }; // Render the round history const renderRoundHistory = () => { if (roundHistory.length === 0) return null; return (

Round History

{roundHistory.map((round, index) => (
Round {round.round}
{player1}: {round.moves[player1] ? MOVE_ICONS[round.moves[player1]] : '?'}
{player2}: {round.moves[player2] ? MOVE_ICONS[round.moves[player2]] : '?'}
{round.winner === 'tie' ? 'Tie' : (round.winner ? `${round.winner} won` : '')}
))}
); }; // Render game over message const renderGameOver = () => { if (!state.game_over) return null; return (

Game Over

{state.winner ? (
{state.winner} wins with a score of {state.scores[state.winner]}!
) : (
The game ended in a tie!
)}
); }; return (
{renderPlayerInfo()} {renderCurrentRound()} {renderRoundHistory()} {renderGameOver()} {/* Replay Controls */}

Game Replay

Turn {currentTurn} of {maxTurns}
{/* Slider for quick navigation */}
{ setCurrentTurn(parseInt(e.target.value)); setIsAutoPlaying(false); }} className="w-full" />
{/* Control buttons */}
{/* Auto-play speed control */} {isAutoPlaying && (
Slow { // Convert from 1-10 scale (inverted) to ms const speedValue = parseInt(e.target.value); const newSpeed = (11 - speedValue) * 200; setAutoPlaySpeed(newSpeed); }} className="flex-1" /> Fast
)}
); }; export default RockPaperScissorsRenderer; ``` ### ./llm-olympics/client/src/index.css ```css @tailwind base; @tailwind components; @tailwind utilities; body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } ``` ### ./llm-olympics/client/src/index.js ```js import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( ); ``` ### ./llm-olympics/client/src/pages/Admin.jsx ```jsx import React, { useState, useEffect } from 'react'; import axios from 'axios'; import { useNavigate, useLocation } from 'react-router-dom'; import { API_BASE_URL } from '../apiConfig'; const Admin = () => { const navigate = useNavigate(); const location = useLocation(); const [activeTab, setActiveTab] = useState('clearData'); const [games, setGames] = useState([]); const [players, setPlayers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Clear data state const [selectedGame, setSelectedGame] = useState(''); const [isClearing, setIsClearing] = useState(false); const [result, setResult] = useState(null); const [showConfirmation, setShowConfirmation] = useState(false); const [resetElo, setResetElo] = useState(false); // Start match state const [selectedMatchGame, setSelectedMatchGame] = useState(''); const [selectedPlayers, setSelectedPlayers] = useState([]); const [randomizePlayers, setRandomizePlayers] = useState(true); const [submittingMatch, setSubmittingMatch] = useState(false); const [matchError, setMatchError] = useState(null); // Scheduler state const [schedulerStatus, setSchedulerStatus] = useState({ isRunning: false, targetMatchCount: 10, gameWeights: {}, activeMatches: { running: 0, queued: 0 } }); const [loadingScheduler, setLoadingScheduler] = useState(true); const [schedulerError, setSchedulerError] = useState(null); // Set initial active tab based on URL query parameters useEffect(() => { const searchParams = new URLSearchParams(location.search); const tabParam = searchParams.get('tab'); if (tabParam && ['clearData', 'startMatch', 'scheduler'].includes(tabParam)) { setActiveTab(tabParam); } }, [location.search]); // Update URL when tab changes const handleTabChange = (tab) => { setActiveTab(tab); navigate(`/admin?tab=${tab}`, { replace: true }); }; useEffect(() => { fetchGames(); if (activeTab === 'startMatch') { fetchPlayers(); } else if (activeTab === 'scheduler') { fetchSchedulerStatus(); } }, [activeTab]); const fetchGames = async () => { try { const response = await axios.get(`${API_BASE_URL}/api/games`); setGames(response.data); // Set defaults if switching to start match tab if (activeTab === 'startMatch' && response.data.length > 0 && !selectedMatchGame) { setSelectedMatchGame(response.data[0].id); } setLoading(false); } catch (error) { console.error('Error fetching games:', error); setError('Failed to load games'); setLoading(false); } }; const fetchPlayers = async () => { try { const response = await axios.get(`${API_BASE_URL}/api/players`); setPlayers(response.data); } catch (error) { console.error('Error fetching players:', error); setMatchError('Failed to load players'); } }; // Function to fetch scheduler status const fetchSchedulerStatus = async () => { try { setLoadingScheduler(true); const response = await axios.get(`${API_BASE_URL}/api/scheduler/status`); setSchedulerStatus(response.data); setSchedulerError(null); setLoadingScheduler(false); } catch (error) { console.error('Error fetching scheduler status:', error); setSchedulerError('Failed to load scheduler status'); setLoadingScheduler(false); } }; // Function to start the scheduler const startScheduler = async () => { try { setLoadingScheduler(true); const response = await axios.post(`${API_BASE_URL}/api/scheduler/start`); setSchedulerStatus({ ...schedulerStatus, isRunning: response.data.isRunning }); setSchedulerError(null); setLoadingScheduler(false); } catch (error) { console.error('Error starting scheduler:', error); setSchedulerError('Failed to start scheduler'); setLoadingScheduler(false); } }; // Function to stop the scheduler const stopScheduler = async () => { try { setLoadingScheduler(true); const response = await axios.post(`${API_BASE_URL}/api/scheduler/stop`); setSchedulerStatus({ ...schedulerStatus, isRunning: response.data.isRunning }); setSchedulerError(null); setLoadingScheduler(false); } catch (error) { console.error('Error stopping scheduler:', error); setSchedulerError('Failed to stop scheduler'); setLoadingScheduler(false); } }; // Function to update scheduler config const updateSchedulerConfig = async (config) => { try { setLoadingScheduler(true); const response = await axios.put(`${API_BASE_URL}/api/scheduler/config`, config); setSchedulerStatus({ ...schedulerStatus, targetMatchCount: response.data.targetMatchCount, gameWeights: response.data.gameWeights }); setSchedulerError(null); setLoadingScheduler(false); } catch (error) { console.error('Error updating scheduler config:', error); setSchedulerError('Failed to update scheduler configuration'); setLoadingScheduler(false); } }; const handleClearData = async () => { if (!selectedGame) return; setIsClearing(true); setResult(null); try { const response = await axios.delete(`${API_BASE_URL}/api/games/${selectedGame}/data?resetElo=${resetElo}`); const details = []; details.push(`${response.data.matchesDeleted} matches, ${response.data.movesDeleted} moves, and ${response.data.playerEntriesDeleted || 0} player entries deleted.`); if (response.data.eloReset && response.data.eloStats) { const { eloHistoryDeleted, modelEloHistoryDeleted, playerEloReset, modelEloReset } = response.data.eloStats; details.push(`Elo ratings reset: ${playerEloReset} player ratings and ${modelEloReset} model ratings reset to default.`); details.push(`${eloHistoryDeleted} Elo history records and ${modelEloHistoryDeleted} model Elo history records deleted.`); } setResult({ success: true, message: response.data.message, details: details.join(' ') }); setShowConfirmation(false); } catch (error) { console.error('Error clearing game data:', error); // Get more detailed error message let errorMessage = error.response?.data?.error || error.message; let partialSuccess = false; // Check if it's a specific error related to missing tables if (errorMessage?.includes("doesn't exist") && resetElo) { partialSuccess = true; errorMessage = `Table structure mismatch: ${errorMessage}. Game data may have been partially cleared, but Elo ratings could not be fully reset.`; } setResult({ success: partialSuccess, message: partialSuccess ? 'Partial success' : 'Failed to clear game data', details: errorMessage, partialSuccess }); // Log more detailed debug information to console console.log('Error details:', { status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data, message: error.message }); } finally { setIsClearing(false); } }; const handleConfirmClear = () => { setShowConfirmation(true); }; const handlePlayerToggle = (playerId) => { if (selectedPlayers.includes(playerId)) { setSelectedPlayers(selectedPlayers.filter(id => id !== playerId)); } else { setSelectedPlayers([...selectedPlayers, playerId]); } }; const handleStartMatch = async (e) => { e.preventDefault(); if (!selectedMatchGame) { setMatchError('Please select a game'); return; } if (selectedPlayers.length < 1) { setMatchError('Please select at least one player'); return; } try { setSubmittingMatch(true); setMatchError(null); // Create the match const createResponse = await axios.post(`${API_BASE_URL}/api/matches`, { gameId: selectedMatchGame, players: selectedPlayers, randomize: randomizePlayers }); const matchId = createResponse.data.id; // Start the match await axios.post(`${API_BASE_URL}/api/matches/${matchId}/start`); // Navigate to the match detail page navigate(`/matches/${matchId}`); } catch (error) { console.error('Error creating match:', error); setMatchError(error.response?.data?.error || 'Failed to create match'); setSubmittingMatch(false); } }; if (loading) { return (
); } if (error) { return (
Error! {error}
); } return (

Admin Dashboard

{/* Tab navigation */}
{/* Clear Game Data Tab */} {activeTab === 'clearData' && (

Game Data Management

setResetElo(e.target.checked)} className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" disabled={isClearing} />

Warning: This will permanently delete all matches, moves, and related data for the selected game. {resetElo && ' All Elo ratings will be reset to default values (1200).'}

{/* Confirmation Modal */} {showConfirmation && (

Confirm Data Deletion

Are you sure you want to delete all data for the selected game? This action cannot be undone. {resetElo && ( Warning: All Elo ratings for this game will be reset to default values (1200). )}

)} {/* Result Message */} {result && (
{result.success && !result.partialSuccess && ( )} {result.partialSuccess && ( )} {!result.success && ( )}

{result.message}

{result.details}
{result.partialSuccess && (
Note: Match data was cleared, but there was an issue with resetting Elo ratings. This is likely because your database has a different Elo table structure than expected.
)}
)}
)} {/* Start New Match Tab */} {activeTab === 'startMatch' && (

Start New Match

{matchError && (
Error! {matchError}
)}
{/* Game Selection */}
{/* Player Selection */}
{players.length > 0 ? ( players.map(player => (
)) ) : (
No players available
)}
{/* Randomize Option */}

When enabled, player turn order will be randomized. This creates fair competitions when the same players play multiple matches.

{/* Submit Button */}
)} {/* Match Scheduler Tab */} {activeTab === 'scheduler' && (

Match Scheduler Control

{schedulerError && (
Error! {schedulerError}
)} {loadingScheduler ? (
) : ( <>

Scheduler Status

{schedulerStatus.isRunning ? 'Running' : 'Stopped'}

Active matches: {schedulerStatus.activeMatches.running} running, {schedulerStatus.activeMatches.queued} queued

{schedulerStatus.isRunning ? ( ) : ( )}

Configuration

setSchedulerStatus({ ...schedulerStatus, targetMatchCount: parseInt(e.target.value) })} className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" />

Number of matches to run concurrently

Determine how often each game is selected for automatic matches

{games.map(game => (
{game.name} { const newWeights = { ...schedulerStatus.gameWeights, [game.name]: parseInt(e.target.value) }; setSchedulerStatus({ ...schedulerStatus, gameWeights: newWeights }); }} className="w-1/2 mx-2" /> {schedulerStatus.gameWeights[game.name] || 1}
))}

About the Match Scheduler

The match scheduler automatically creates and runs matches between AI models. When active, it maintains the target number of concurrent matches, selecting games according to the weights configured above. Each model pair will play up to 3 matches per game before prioritizing other combinations.

)}
)}
); }; export default Admin; ``` ### ./llm-olympics/client/src/pages/CreateMatch.jsx ```jsx import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import axios from 'axios'; import { API_BASE_URL } from '../apiConfig'; const CreateMatch = () => { const navigate = useNavigate(); const [games, setGames] = useState([]); const [players, setPlayers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedGame, setSelectedGame] = useState(''); const [selectedPlayers, setSelectedPlayers] = useState([]); const [submitting, setSubmitting] = useState(false); const [randomizePlayers, setRandomizePlayers] = useState(true); useEffect(() => { // Fetch games and players const fetchData = async () => { try { const [gamesResponse, playersResponse] = await Promise.all([ axios.get(`${API_BASE_URL}/api/games`), axios.get(`${API_BASE_URL}/api/players`) ]); setGames(gamesResponse.data); setPlayers(playersResponse.data); // Set defaults if data is available if (gamesResponse.data.length > 0) { setSelectedGame(gamesResponse.data[0].id); } setLoading(false); } catch (error) { console.error('Error fetching data:', error); setError('Failed to load games or players'); setLoading(false); } }; fetchData(); }, []); const handlePlayerToggle = (playerId) => { if (selectedPlayers.includes(playerId)) { setSelectedPlayers(selectedPlayers.filter(id => id !== playerId)); } else { setSelectedPlayers([...selectedPlayers, playerId]); } }; const handleSubmit = async (e) => { e.preventDefault(); if (!selectedGame) { setError('Please select a game'); return; } if (selectedPlayers.length < 1) { setError('Please select at least one player'); return; } try { setSubmitting(true); // Create the match const createResponse = await axios.post(`${API_BASE_URL}/api/matches`, { gameId: selectedGame, players: selectedPlayers, randomize: randomizePlayers }); const matchId = createResponse.data.id; // Start the match await axios.post(`${API_BASE_URL}/api/matches/${matchId}/start`); // Navigate to the match detail page navigate(`/matches/${matchId}`); } catch (error) { console.error('Error creating match:', error); setError(error.response?.data?.error || 'Failed to create match'); setSubmitting(false); } }; if (loading) { return (
); } return (

Create New Match

{error && (
Error! {error}
)}
{/* Game Selection */}
{/* Player Selection */}
{players.length > 0 ? ( players.map(player => (
)) ) : (
No players available
)}
{/* Randomize Option */}

When enabled, player turn order will be randomized. This creates fair competitions when the same players play multiple matches.

{/* Submit Button */}
); }; export default CreateMatch; ``` ### ./llm-olympics/client/src/pages/Dashboard.jsx ```jsx import React, { useState, useEffect } from 'react'; import axios from 'axios'; import MatchCard from '../components/MatchCard'; import MovesList from '../components/MovesList'; import { API_BASE_URL } from '../apiConfig'; const Dashboard = () => { const [activeTab, setActiveTab] = useState('matches'); const [matches, setMatches] = useState([]); const [recentMoves, setRecentMoves] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [runningMatches, setRunningMatches] = useState({ running: [], queued: [] }); useEffect(() => { // Fetch data based on active tab if (activeTab === 'matches') { fetchMatches(); fetchRunningMatches(); } else if (activeTab === 'moves') { fetchRecentMoves(); } // Refresh running matches every 5 seconds const interval = setInterval(() => { if (activeTab === 'matches') { fetchRunningMatches(); } }, 5000); return () => clearInterval(interval); }, [activeTab]); const fetchMatches = async () => { try { const response = await axios.get(`${API_BASE_URL}/api/matches`); setMatches(response.data); setLoading(false); } catch (error) { console.error('Error fetching matches:', error); setError('Failed to load matches'); setLoading(false); } }; const fetchRunningMatches = async () => { try { const response = await axios.get(`${API_BASE_URL}/api/matches/status/running`); setRunningMatches(response.data); } catch (error) { console.error('Error fetching running matches:', error); } }; const fetchRecentMoves = async () => { try { const response = await axios.get(`${API_BASE_URL}/api/moves?limit=50`); setRecentMoves(response.data); setLoading(false); } catch (error) { console.error('Error fetching recent moves:', error); setError('Failed to load recent moves'); setLoading(false); } }; if (loading) { return (
); } if (error) { return (
Error! {error}
); } return (

LLM Olympics Dashboard

{activeTab === 'matches' && ( <> {/* Running Matches Section */} {(runningMatches.running?.length > 0 || runningMatches.queued?.length > 0) && (

Active Matches

{runningMatches.running?.length > 0 && (

Running:

    {runningMatches.running.map(match => (
  • {match.game} Match #{match.id}
    Running for {Math.round(match.runningFor / 1000)}s
    Players: {match.players.join(', ')}
  • ))}
)} {runningMatches.queued?.length > 0 && (

Queued:

    {runningMatches.queued.map(matchId => (
  • Match #{matchId} - Waiting to start
  • ))}
)}
)} {/* Recent Matches */}

Recent Matches

{matches.map(match => ( ))}
{matches.length === 0 && (

No matches found.

Create your first match
)}
)} {activeTab === 'moves' && (

Recent Game Moves

)}
); }; export default Dashboard; ``` ### ./llm-olympics/client/src/pages/EloRankings.jsx ```jsx import React, { useState, useEffect } from 'react'; import { Link, useParams } from 'react-router-dom'; import axios from 'axios'; const EloRankings = () => { const { gameId } = useParams(); const [games, setGames] = useState([]); const [selectedGame, setSelectedGame] = useState(gameId || null); const [playerRankings, setPlayerRankings] = useState([]); const [modelRankings, setModelRankings] = useState([]); const [activeTab, setActiveTab] = useState('models'); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchGames = async () => { try { const response = await axios.get('/api/stats/games'); setGames(response.data.games); // If no game is selected, select the first one if (!selectedGame && response.data.games.length > 0) { setSelectedGame(response.data.games[0].id.toString()); } } catch (err) { console.error('Error fetching games:', err); setError('Failed to load games'); } }; fetchGames(); }, [selectedGame]); useEffect(() => { const fetchEloRankings = async () => { if (!selectedGame) return; setLoading(true); try { // Fetch both player and model rankings in parallel const [playerResponse, modelResponse] = await Promise.all([ axios.get(`/api/elo/games/${selectedGame}/players`), axios.get(`/api/elo/games/${selectedGame}/models`) ]); setPlayerRankings(playerResponse.data.rankings); setModelRankings(modelResponse.data.rankings); setLoading(false); } catch (err) { console.error('Error fetching Elo rankings:', err); setError('Failed to load Elo rankings'); setLoading(false); } }; fetchEloRankings(); }, [selectedGame]); const handleGameChange = (e) => { setSelectedGame(e.target.value); }; if (error) { return
{error}
; } return (
← Back to Game Stats

Elo Rating Rankings

{loading ? (
Loading rankings...
) : ( <> {activeTab === 'models' && (

Model Elo Leaderboard

Elo ratings by model type

{modelRankings.map((model, index) => ( ))} {modelRankings.length === 0 && ( )}
Rank Model Elo Rating Games Played Players Using
{index + 1}
{model.model}
{model.rating}
{model.games_played}
{model.player_count || 0}
No model Elo rankings available for this game yet.
)} {activeTab === 'players' && (

Player Elo Leaderboard

Individual player Elo ratings

{playerRankings.map((player, index) => ( ))} {playerRankings.length === 0 && ( )}
Rank Player Model Elo Rating Games Played
{index + 1}
{player.player_name}
{player.player_model}
{player.rating}
{player.games_played}
No player Elo rankings available for this game yet.
)} )}

About Elo Ratings

The Elo rating system is a method for calculating the relative skill levels of players in zero-sum games. All players start with a rating of 1200. After each game, ratings are updated based on the outcome and the difference between the players' ratings. Winning against a higher-rated opponent will result in a bigger rating increase than winning against a lower-rated opponent.

); }; export default EloRankings; ``` ### ./llm-olympics/client/src/pages/GameDetail.jsx ```jsx import React, { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import axios from 'axios'; const GameDetail = () => { const { gameId } = useParams(); const [gameData, setGameData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState('rankings'); const [selectedModel, setSelectedModel] = useState(null); const [modelMatches, setModelMatches] = useState([]); const [loadingModelMatches, setLoadingModelMatches] = useState(false); const [eloRankings, setEloRankings] = useState({ players: [], models: [] }); const [loadingElo, setLoadingElo] = useState(false); useEffect(() => { const fetchGameData = async () => { try { setLoading(true); // Fetch regular game stats const response = await axios.get(`/api/stats/games/${gameId}`); setGameData(response.data); // Also fetch Elo ratings setLoadingElo(true); try { const [playerEloResponse, modelEloResponse] = await Promise.all([ axios.get(`/api/elo/games/${gameId}/players`), axios.get(`/api/elo/games/${gameId}/models`) ]); setEloRankings({ players: playerEloResponse.data.rankings || [], models: modelEloResponse.data.rankings || [] }); } catch (eloErr) { console.error('Error fetching Elo rankings:', eloErr); // Continue even if Elo fetch fails } finally { setLoadingElo(false); } setLoading(false); } catch (err) { console.error('Error fetching game data:', err); setError('Failed to load game data'); setLoading(false); } }; fetchGameData(); }, [gameId]); // Fetch matches for a specific model const fetchModelMatches = async (playerId, playerName) => { if (!playerId) return; try { setLoadingModelMatches(true); const response = await axios.get(`/api/stats/games/${gameId}/players/${playerId}/matches`); setModelMatches(response.data.matches); setSelectedModel(playerName); setActiveTab('modelMatches'); setLoadingModelMatches(false); } catch (err) { console.error(`Error fetching matches for model ${playerName}:`, err); setLoadingModelMatches(false); } }; // Helper function to get Elo rating for a player const getPlayerElo = (playerId) => { if (!eloRankings.players.length) return null; const playerElo = eloRankings.players.find(p => p.player_id === playerId); return playerElo ? playerElo.rating : null; }; // Helper function to get Elo rating for a model const getModelElo = (modelName) => { if (!eloRankings.models.length) return null; const modelElo = eloRankings.models.find(m => m.model === modelName); return modelElo ? modelElo.rating : null; }; if (loading) return
Loading game data...
; if (error) return
{error}
; if (!gameData) return
No game data found
; const { game, matches, rankings } = gameData; // Clean up winner name (remove quotes from JSON extraction) const cleanMatches = matches.map(match => ({ ...match, winner: match.winner ? match.winner.replace(/"/g, '') : null })); return (
← Back to Games

{game.name}

{game.description && (

{game.description}

)}
{activeTab === 'rankings' && (

Model Performance Leaderboard

Rankings based on completed matches and Elo ratings

{loadingElo &&

Loading Elo ratings...

}
{rankings.map((model, index) => { const eloRating = getPlayerElo(model.id); const modelEloRating = getModelElo(model.model); return ( fetchModelMatches(model.id, model.name)} > ); })} {rankings.length === 0 && ( )}
Rank Model Name Games Wins Win Rate Elo Rating
{index + 1}
{model.model} {model.name} {model.games_played} {model.wins}
75 ? 'bg-green-600' : model.win_rate > 50 ? 'bg-blue-600' : model.win_rate > 25 ? 'bg-yellow-500' : 'bg-red-500' }`} style={{ width: `${model.win_rate}%` }} >
{model.win_rate}%
{eloRating ? (
= 1400 ? 'text-green-600' : eloRating >= 1300 ? 'text-blue-600' : eloRating >= 1200 ? 'text-gray-600' : 'text-red-600' }`}> {eloRating} {modelEloRating && modelEloRating !== eloRating && ( Model: {modelEloRating} )}
) : ( )}
No rankings available yet
Click on any model to see their match history
)} {activeTab === 'matches' && (

Recent Matches

Last 50 matches for this game

{cleanMatches.map(match => ( ))} {cleanMatches.length === 0 && ( )}
Match ID Players Status Winner Date Actions
{match.id}
{match.player1_name} vs {match.player2_name}
{match.status} {match.winner ? {match.winner} : match.status === 'completed' ? 'Draw' : '-'} {new Date(match.created_at).toLocaleString()} View Match
No matches found for this game.
)} {activeTab === 'modelMatches' && (

{selectedModel}'s Match History

All matches for this model in {game.name}

{loadingModelMatches ? (

Loading matches...

) : ( {modelMatches.map(match => ( ))} {modelMatches.length === 0 && ( )}
Match ID Opponent Result Date Actions
{match.id} {match.opponent_name} {match.result === 'win' ? 'Won' : match.result === 'loss' ? 'Lost' : 'Draw'} {new Date(match.created_at).toLocaleString()} View Match
No matches found for {selectedModel}.
)}
)}
); }; export default GameDetail; ``` ### ./llm-olympics/client/src/pages/GameStats.jsx ```jsx import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import axios from 'axios'; const GameStats = () => { const [games, setGames] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchGames = async () => { try { setLoading(true); const response = await axios.get('/api/stats/games'); setGames(response.data.games); setLoading(false); } catch (err) { console.error('Error fetching games:', err); setError('Failed to load games'); setLoading(false); } }; fetchGames(); }, []); if (loading) return
Loading games...
; if (error) return
{error}
; return (

Game Statistics & Leaderboards

View Model Elo Comparisons
{games.map(game => (

{game.name}

Total Matches
{game.total_matches}
Completed
{game.completed_matches}
View Details & Rankings
))}
); }; export default GameStats; ``` ### ./llm-olympics/client/src/pages/MatchDetail.jsx ```jsx import React, { useState, useEffect, useCallback } from 'react'; import { useParams } from 'react-router-dom'; import axios from 'axios'; import socket from '../socketConfig'; import GameBoard from '../components/GameBoard'; import MovesList from '../components/MovesList'; import { API_BASE_URL } from '../apiConfig'; const MatchDetail = () => { const { id } = useParams(); const [match, setMatch] = useState(null); const [gameState, setGameState] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [moveHistory, setMoveHistory] = useState([]); // Define fetchMatch with useCallback to memoize it const fetchMatch = useCallback(async () => { try { const response = await axios.get(`${API_BASE_URL}/api/matches/${id}`); setMatch(response.data); // Try to set initial state from results if available if (response.data.results && response.data.results.state) { console.log('Setting initial state from results'); setGameState(response.data.results.state); } // Get moves for this match try { const movesResponse = await axios.get(`${API_BASE_URL}/api/moves/match/${id}`); setMoveHistory(movesResponse.data || []); // If there are move records, we can use that for state if (movesResponse.data && movesResponse.data.length > 0) { // We'll use the database moves instead of match history console.log('Using move data from database'); } } catch (movesError) { console.error('Error fetching match moves:', movesError); // Fallback to old history API if moves API fails try { const historyResponse = await axios.get(`${API_BASE_URL}/api/matches/${id}/history`); setMoveHistory(historyResponse.data || []); // Try to get state from history if (historyResponse.data && historyResponse.data.length > 0) { // Get the last entry with a state const entriesWithState = historyResponse.data.filter(entry => entry.state); if (entriesWithState.length > 0) { const lastState = entriesWithState[entriesWithState.length - 1].state; console.log('Setting game state from history:', lastState); setGameState(lastState); } } } catch (historyError) { console.error('Error fetching match history:', historyError); } } setLoading(false); } catch (error) { console.error('Error fetching match:', error); setError('Failed to load match details'); setLoading(false); } }, [id]); // Define handleMatchUpdate with useCallback // Enhanced handleMatchUpdate with debug info const handleMatchUpdate = useCallback((data) => { console.log('Match update received:', data); if (!data) { console.error('Received empty update data'); return; } // Check if this update is for our match const updateMatchId = parseInt(data.matchId); const currentMatchId = parseInt(id); if (updateMatchId !== currentMatchId) { console.log(`Ignoring update for different match (${updateMatchId} vs ${currentMatchId})`); return; } // Debug: Dump the entire data structure console.log('Full update structure:', JSON.stringify(data, null, 2)); // CRITICAL FIX: Handle different state locations if (data.data && data.data.state) { // Most common format console.log('Found state in data.data.state'); setGameState(prevState => { // Compare with previous state to see if it's actually changing console.log('Previous state:', prevState); console.log('New state:', data.data.state); return data.data.state; }); } else if (data.state) { // Alternate format console.log('Found state in data.state'); setGameState(data.state); } else { // Try to find state in other locations console.warn('State not found in expected locations, searching deeper...'); // Look for any property that might contain state if (data.data) { if (typeof data.data === 'object') { // If data.data has a board property, it might be the state if (data.data.board) { console.log('Found potential state with board property'); setGameState(data.data); } } } } // Track moves for history if (data.type === 'move') { // Try different possible locations for player/move data let player = null; let move = null; if (data.data && data.data.player && data.data.move) { player = data.data.player; move = data.data.move; } else if (data.player && data.move) { player = data.player; move = data.move; } if (player && move) { console.log(`Adding move to history: ${player} made move ${move}`); setMoveHistory(prev => { const newHistory = [...prev, { id: Date.now(), // Add unique ID for react keys player_name: player, move_data: move, timestamp: Date.now(), created_at: new Date().toISOString() }]; console.log('Updated move history:', newHistory); return newHistory; }); } } // If the game has ended, refresh all data if (data.type === 'ended') { console.log('Game ended, fetching final state'); fetchMatch(); } }, [id, fetchMatch]); // Now use the memoized functions in useEffect with proper dependencies useEffect(() => { // Fetch match details fetchMatch(); // Join the match room console.log('Joining match room:', id); socket.emit('join-match', id); // Set up socket event listener socket.on('match-update', handleMatchUpdate); return () => { // Clean up socket event listener socket.off('match-update', handleMatchUpdate); console.log('Leaving match room:', id); socket.emit('leave-match', id); }; }, [id, fetchMatch, handleMatchUpdate]); // All dependencies included if (loading) { return (
); } if (error) { return (
Error! {error}
); } if (!match) { return (
Not Found! Match not found.
); } const formatDate = (dateString) => { if (!dateString) return 'N/A'; const date = new Date(dateString); return date.toLocaleString(); }; const getStatusBadge = (status) => { const colors = { completed: 'bg-green-100 text-green-800', running: 'bg-blue-100 text-blue-800', queued: 'bg-yellow-100 text-yellow-800', pending: 'bg-gray-100 text-gray-800', error: 'bg-red-100 text-red-800', }; return ( {status} ); }; return (

{match.game_name} Match #{match.id}

{getStatusBadge(match.status)}
Created: {formatDate(match.created_at)}
{match.completed_at && (
Completed: {formatDate(match.completed_at)}
)}
{/* Main game board */}

Game Board

{/* Debug State Display */}
Game State:
{gameState ? JSON.stringify(gameState, null, 2) : 'No state'}
{/* Results section (for completed matches) */} {match.status === 'completed' && match.results && (

Match Results

{match.results.winner ? (
Winner: {match.results.winner}
) : (
No winner (Draw)
)} {match.results.scores && (

Final Scores:

{Object.entries(match.results.scores).map(([player, data]) => (
{player}
{typeof data === 'object' ? ( Object.entries(data).map(([key, value]) => (
{key}: {value}
)) ) : (
Score: {data}
)}
))}
)}
)}
{/* Sidebar with info and move history */}
{/* Players section */}

Players

    {match.players.map((player) => (
  • {player.player_name}
    Position {player.position}
    {match.results?.scores?.[player.player_name] && (
    {typeof match.results.scores[player.player_name] === 'object' ? match.results.scores[player.player_name].score : match.results.scores[player.player_name]}
    )}
  • ))}
{/* Move history */}

Move History

{moveHistory.length > 0 ? ( ) : (
No moves yet
)}
); }; export default MatchDetail; ``` ### ./llm-olympics/client/src/pages/ModelEloOverview.jsx ```jsx import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import axios from 'axios'; const ModelEloOverview = () => { const [players, setPlayers] = useState([]); const [playerDetails, setPlayerDetails] = useState({}); const [selectedPlayer, setSelectedPlayer] = useState(null); const [games, setGames] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [matchStats, setMatchStats] = useState({}); useEffect(() => { const fetchData = async () => { try { setLoading(true); // Fetch all games const gamesResponse = await axios.get('/api/stats/games'); setGames(gamesResponse.data.games); // Create a map to store player name data const playerMap = {}; const statsMap = {}; // For each game, fetch player name Elo ratings and aggregate them for (const game of gamesResponse.data.games) { // Using the player name endpoint that replaced the model endpoint const playerResponse = await axios.get(`/api/elo/games/${game.id}/models`); const gamePlayers = playerResponse.data.rankings; // Also fetch all completed matches for this game to get statistics const matchesResponse = await axios.get(`/api/stats/games/${game.id}`); const matches = matchesResponse.data.matches || []; // Process match results to get stats per player if (!statsMap[game.id]) { statsMap[game.id] = {}; } matches.forEach(match => { if (match.status !== 'completed') return; // Parse winner (handle JSON string if needed) let winner = match.winner; if (typeof winner === 'string' && winner.startsWith('"') && winner.endsWith('"')) { winner = winner.substring(1, winner.length - 1); } // Record stats for player1 if (!statsMap[game.id][match.player1_name]) { statsMap[game.id][match.player1_name] = { played: 0, wins: 0, losses: 0, ties: 0 }; } statsMap[game.id][match.player1_name].played += 1; // Record stats for player2 if (!statsMap[game.id][match.player2_name]) { statsMap[game.id][match.player2_name] = { played: 0, wins: 0, losses: 0, ties: 0 }; } statsMap[game.id][match.player2_name].played += 1; // Record outcome if (!winner) { // It's a tie statsMap[game.id][match.player1_name].ties += 1; statsMap[game.id][match.player2_name].ties += 1; } else if (winner === match.player1_name) { statsMap[game.id][match.player1_name].wins += 1; statsMap[game.id][match.player2_name].losses += 1; } else if (winner === match.player2_name) { statsMap[game.id][match.player1_name].losses += 1; statsMap[game.id][match.player2_name].wins += 1; } }); gamePlayers.forEach(playerData => { // Using player_name instead of model const playerName = playerData.player_name; const rating = playerData.rating; const games_played = playerData.games_played; if (!playerName) return; // Skip if player name is undefined if (!playerMap[playerName]) { playerMap[playerName] = { name: playerName, totalGames: 0, ratings: {}, averageRating: 0, gamesPlayed: 0, stats: {} // Store game stats here }; } playerMap[playerName].ratings[game.name] = rating; playerMap[playerName].totalGames += games_played; playerMap[playerName].gamesPlayed += games_played; // Add match statistics if available if (statsMap[game.id] && statsMap[game.id][playerName]) { if (!playerMap[playerName].stats) { playerMap[playerName].stats = {}; } playerMap[playerName].stats[game.name] = statsMap[game.id][playerName]; } }); } // Calculate average ratings Object.keys(playerMap).forEach(playerName => { const ratingValues = Object.values(playerMap[playerName].ratings); const sum = ratingValues.reduce((acc, curr) => acc + curr, 0); playerMap[playerName].averageRating = ratingValues.length ? Math.round(sum / ratingValues.length) : 0; }); // Convert map to array and sort by average rating const playerArray = Object.values(playerMap).sort((a, b) => b.averageRating - a.averageRating); setPlayers(playerArray); setPlayerDetails(playerMap); setMatchStats(statsMap); // Select first player by default if any exist if (playerArray.length > 0) { setSelectedPlayer(playerArray[0].name); } setLoading(false); } catch (err) { console.error('Error fetching player data:', err); setError('Failed to load player data'); setLoading(false); } }; fetchData(); }, []); const handlePlayerSelect = (playerName) => { setSelectedPlayer(playerName); }; if (loading) { return
Loading player data...
; } if (error) { return
{error}
; } const selectedPlayerData = selectedPlayer ? playerDetails[selectedPlayer] : null; return (
← Back to Game Stats

Player Elo Ratings Overview

Players

    {players.map((player, index) => (
  • ))} {players.length === 0 && (
  • No players found
  • )}
{selectedPlayerData ? (

{selectedPlayerData.name}

Average Elo: {selectedPlayerData.averageRating} • Games played: {selectedPlayerData.gamesPlayed}

Elo Ratings by Game

{games.map((game, index) => { const rating = selectedPlayerData.ratings[game.name] || 1200; const diff = rating - selectedPlayerData.averageRating; // Get game stats const gameStats = selectedPlayerData.stats && selectedPlayerData.stats[game.name] ? selectedPlayerData.stats[game.name] : { played: 0, wins: 0, losses: 0, ties: 0 }; // Calculate win rate const winRate = gameStats.played > 0 ? Math.round((gameStats.wins / gameStats.played) * 100) : 0; return ( ); })}
Game Elo Rating Difference Played W-T-L Win Rate
{game.name}
{rating}
0 ? 'text-green-600' : diff < 0 ? 'text-red-600' : 'text-gray-600' }`}> {diff > 0 ? '+' : ''}{diff}
{gameStats.played}
{gameStats.wins}- {gameStats.ties}- {gameStats.losses}
= 60 ? 'bg-green-500' : winRate >= 45 ? 'bg-blue-500' : winRate >= 30 ? 'bg-yellow-500' : 'bg-red-500' }`} style={{ width: `${Math.min(100, winRate)}%` }} >
{winRate}%
{/* Visual representation of ratings */}

Rating Comparison

{games.map((game, index) => { const rating = selectedPlayerData.ratings[game.name] || 1200; const percentWidth = Math.min(100, Math.max(0, (rating - 1000) / 10)); // Scale between 1000-2000 // Get game stats const gameStats = selectedPlayerData.stats && selectedPlayerData.stats[game.name] ? selectedPlayerData.stats[game.name] : { played: 0, wins: 0, losses: 0, ties: 0 }; return (
{game.name} ({gameStats.wins}-{gameStats.ties}-{gameStats.losses})
{rating}
= 1400 ? 'bg-green-500' : rating >= 1300 ? 'bg-blue-500' : rating >= 1200 ? 'bg-yellow-500' : 'bg-red-500' }`} style={{ width: `${percentWidth}%` }} >
); })}
) : (

Select a player to view details

)}
); }; export default ModelEloOverview; ``` ### ./llm-olympics/client/src/setupProxy.js ```js const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function(app) { // Read the API URL from environment variable with fallback const serverUrl = process.env.REACT_APP_SERVER_URL || 'http://localhost:3302'; app.use( '/socket.io', createProxyMiddleware({ target: serverUrl, ws: true, changeOrigin: true }) ); app.use( '/api', createProxyMiddleware({ target: serverUrl, changeOrigin: true }) ); }; ``` ### ./llm-olympics/client/src/socketConfig.js ```js import { io } from 'socket.io-client'; // Read server URL from environment variable or use default const getServerUrl = () => { // Get the protocol and host from the current URL const protocol = window.location.protocol; const hostname = window.location.hostname; // Default to localhost for development if (hostname === 'localhost' || hostname === '127.0.0.1') { // Read from .env if available, otherwise use default port 3001 const port = process.env.REACT_APP_SERVER_PORT || 3302; return `${protocol}//${hostname}:${port}`; } // For production, use the same host but different port if specified const port = process.env.REACT_APP_SERVER_PORT; return port ? `${protocol}//${hostname}:${port}` : `${protocol}//${hostname}`; }; const SOCKET_URL = getServerUrl(); console.log('Connecting to Socket.io server at:', SOCKET_URL); const socket = io(SOCKET_URL, { transports: ['websocket', 'polling'], reconnection: true, reconnectionAttempts: 10, reconnectionDelay: 1000, timeout: 20000 }); socket.on('connect', () => { console.log('Socket connected!', socket.id); }); socket.on('connect_error', (err) => { console.error('Socket connection error:', err); }); socket.on('disconnect', (reason) => { console.log('Socket disconnected:', reason); }); export default socket; ``` ### ./llm-olympics/client/src/styles/GameStats.css ```css /* Game Stats specific styles */ .leaderboard-medal { display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; width: 30px; height: 30px; font-weight: bold; } .medal-gold { background-color: #FFD700; color: #5D4037; } .medal-silver { background-color: #C0C0C0; color: #424242; } .medal-bronze { background-color: #CD7F32; color: white; } .progress-bar { height: 8px; border-radius: 4px; background-color: #e5e7eb; overflow: hidden; } .progress-fill { height: 100%; border-radius: 4px; transition: width 0.5s ease-out; } .win-rate-high { background-color: #10B981; } .win-rate-medium { background-color: #3B82F6; } .win-rate-low { background-color: #F59E0B; } .win-rate-poor { background-color: #EF4444; } /* Cursor pointer for clickable rows */ .clickable-row { cursor: pointer; } .clickable-row:hover { background-color: rgba(59, 130, 246, 0.05); } /* Loading spinner */ .spinner { border: 3px solid rgba(59, 130, 246, 0.3); border-radius: 50%; border-top: 3px solid #3b82f6; width: 24px; height: 24px; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Match result badges */ .result-badge { display: inline-flex; align-items: center; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; } .result-win { background-color: #d1fae5; color: #065f46; } .result-loss { background-color: #fee2e2; color: #b91c1c; } .result-draw { background-color: #e5e7eb; color: #4b5563; } ``` ### ./llm-olympics/client/tailwind.config.js ```js /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './src/**/*.{js,jsx,ts,tsx}', './public/index.html' ], theme: { extend: {}, }, plugins: [], } ``` ### ./llm-olympics/package.json ```json { "dependencies": { "canvas": "^3.1.0", "gif-encoder-2": "^1.0.5" } } ``` ### ./llm-olympics/scripts/initialize_elo.sh File skipped (binary or too large) ### ./llm-olympics/scripts/initialize_elo_tables.js ```js /** * Initialize Elo Rating Tables * * This script creates tables for Elo ratings and initializes ratings for existing * players and models in the LLM Olympics database. */ // Load environment variables require('dotenv').config({ path: '../server/.env' }); const mysql = require('mysql2/promise'); const fs = require('fs'); const path = require('path'); // Read the SQL file const sqlFile = path.join(__dirname, '../server/utils/elo_tables.sql'); const sqlCommands = fs.readFileSync(sqlFile, 'utf8'); async function main() { console.log('Initializing Elo rating tables...'); // Connect to the database const connection = await mysql.createConnection({ host: process.env.DB_HOST || 'localhost', user: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD, database: process.env.DB_NAME || 'llm_olympics', multipleStatements: true }); try { // Execute SQL commands to create tables console.log('Creating Elo tables...'); await connection.query(sqlCommands); console.log('Elo tables created successfully!'); // Get all games const [games] = await connection.query('SELECT id, name FROM games'); console.log(`Found ${games.length} games`); // Get all players const [players] = await connection.query('SELECT id, name, model FROM players'); console.log(`Found ${players.length} players`); // Get unique models const models = [...new Set(players.map(player => player.model))]; console.log(`Found ${models.length} unique models`); // Initialize player game Elo ratings for (const game of games) { console.log(`Initializing player ratings for ${game.name}...`); for (const player of players) { // Check if rating already exists const [existingRating] = await connection.query( 'SELECT id FROM player_game_elo WHERE player_id = ? AND game_id = ?', [player.id, game.id] ); if (existingRating.length === 0) { // Create initial rating await connection.query( 'INSERT INTO player_game_elo (player_id, game_id, rating, games_played) VALUES (?, ?, 1200, 0)', [player.id, game.id] ); } } } // Initialize model Elo ratings for (const game of games) { console.log(`Initializing model ratings for ${game.name}...`); for (const model of models) { // Check if rating already exists const [existingRating] = await connection.query( 'SELECT id FROM model_elo WHERE model = ? AND game_id = ?', [model, game.id] ); if (existingRating.length === 0) { // Create initial rating await connection.query( 'INSERT INTO model_elo (model, game_id, rating, games_played) VALUES (?, ?, 1200, 0)', [model, game.id] ); } } } // Calculate Elo ratings based on past matches console.log('Calculating Elo ratings based on completed matches...'); // Get completed matches const [matches] = await connection.query(` SELECT m.id, m.game_id, m.results, mp1.player_id as player1_id, mp2.player_id as player2_id, p1.name as player1_name, p2.name as player2_name, p1.model as player1_model, p2.model as player2_model FROM matches m JOIN match_players mp1 ON m.id = mp1.match_id AND mp1.position = 1 JOIN match_players mp2 ON m.id = mp2.match_id AND mp2.position = 2 JOIN players p1 ON mp1.player_id = p1.id JOIN players p2 ON mp2.player_id = p2.id WHERE m.status = 'completed' ORDER BY m.created_at ASC `); console.log(`Found ${matches.length} completed matches`); // Process matches const elo = require('../server/utils/elo'); for (const match of matches) { // Parse results const results = typeof match.results === 'string' ? JSON.parse(match.results) : match.results; if (!results || !results.winner) { continue; // Skip matches without a winner } // Determine result from player1's perspective let result = 'draw'; if (results.winner === match.player1_name) { result = 'win'; } else if (results.winner === match.player2_name) { result = 'loss'; } // Update player Elo ratings await updatePlayerElo( connection, match.player1_id, match.player2_id, match.game_id, match.id, result ); // Update model Elo ratings if different models if (match.player1_model !== match.player2_model) { await updateModelElo( connection, match.player1_model, match.player2_model, match.game_id, match.id, result ); } } console.log('Elo ratings initialization completed!'); } catch (error) { console.error('Error initializing Elo ratings:', error); } finally { await connection.end(); } } async function getPlayerElo(connection, playerId, gameId) { const [results] = await connection.query( 'SELECT * FROM player_game_elo WHERE player_id = ? AND game_id = ?', [playerId, gameId] ); if (results.length === 0) { return { rating: 1200, games_played: 0 }; } return results[0]; } async function getModelElo(connection, model, gameId) { const [results] = await connection.query( 'SELECT * FROM model_elo WHERE model = ? AND game_id = ?', [model, gameId] ); if (results.length === 0) { return { rating: 1200, games_played: 0 }; } return results[0]; } async function updatePlayerElo(connection, playerId, opponentId, gameId, matchId, result) { // Get current ratings const playerElo = await getPlayerElo(connection, playerId, gameId); const opponentElo = await getPlayerElo(connection, opponentId, gameId); // Calculate new ratings const { newRatingA, newRatingB } = require('../server/utils/elo').updateRatings( playerElo.rating, opponentElo.rating, result ); // Update player rating await connection.query( 'UPDATE player_game_elo SET rating = ?, games_played = games_played + 1 WHERE player_id = ? AND game_id = ?', [newRatingA, playerId, gameId] ); // Update opponent rating await connection.query( 'UPDATE player_game_elo SET rating = ?, games_played = games_played + 1 WHERE player_id = ? AND game_id = ?', [newRatingB, opponentId, gameId] ); // Record history await connection.query( 'INSERT INTO elo_history (match_id, game_id, player_id, old_rating, new_rating, rating_change) VALUES (?, ?, ?, ?, ?, ?)', [matchId, gameId, playerId, playerElo.rating, newRatingA, newRatingA - playerElo.rating] ); await connection.query( 'INSERT INTO elo_history (match_id, game_id, player_id, old_rating, new_rating, rating_change) VALUES (?, ?, ?, ?, ?, ?)', [matchId, gameId, opponentId, opponentElo.rating, newRatingB, newRatingB - opponentElo.rating] ); } async function updateModelElo(connection, playerModel, opponentModel, gameId, matchId, result) { // Get current ratings const playerModelElo = await getModelElo(connection, playerModel, gameId); const opponentModelElo = await getModelElo(connection, opponentModel, gameId); // Calculate new ratings const { newRatingA, newRatingB } = require('../server/utils/elo').updateRatings( playerModelElo.rating, opponentModelElo.rating, result ); // Update player model rating await connection.query( 'UPDATE model_elo SET rating = ?, games_played = games_played + 1 WHERE model = ? AND game_id = ?', [newRatingA, playerModel, gameId] ); // Update opponent model rating await connection.query( 'UPDATE model_elo SET rating = ?, games_played = games_played + 1 WHERE model = ? AND game_id = ?', [newRatingB, opponentModel, gameId] ); // Record history await connection.query( 'INSERT INTO model_elo_history (match_id, game_id, model, old_rating, new_rating, rating_change) VALUES (?, ?, ?, ?, ?, ?)', [matchId, gameId, playerModel, playerModelElo.rating, newRatingA, newRatingA - playerModelElo.rating] ); await connection.query( 'INSERT INTO model_elo_history (match_id, game_id, model, old_rating, new_rating, rating_change) VALUES (?, ?, ?, ?, ?, ?)', [matchId, gameId, opponentModel, opponentModelElo.rating, newRatingB, newRatingB - opponentModelElo.rating] ); } // Run the script main() .then(() => process.exit(0)) .catch(err => { console.error('Initialization failed:', err); process.exit(1); }); ``` ### ./llm-olympics/scripts/initialize_playername_elo.js ```js /** * Initialize Player Name Elo Rating Tables * * This script creates tables for Elo ratings based on player names instead of models */ // Load environment variables require('dotenv').config({ path: '../server/.env' }); const mysql = require('mysql2/promise'); const fs = require('fs'); const path = require('path'); // Read the SQL file const sqlFile = path.join(__dirname, '../server/utils/elo_tables_updated.sql'); const sqlCommands = fs.readFileSync(sqlFile, 'utf8'); async function main() { console.log('Initializing player name Elo rating tables...'); // Connect to the database const connection = await mysql.createConnection({ host: process.env.DB_HOST || 'localhost', user: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD, database: process.env.DB_NAME || 'llm_olympics', multipleStatements: true }); try { // Execute SQL commands to drop old tables and create new ones console.log('Creating player name Elo tables...'); await connection.query(sqlCommands); console.log('Player name Elo tables created successfully!'); // Get all games const [games] = await connection.query('SELECT id, name FROM games'); console.log(`Found ${games.length} games`); // Get all players const [players] = await connection.query('SELECT id, name FROM players'); console.log(`Found ${players.length} players`); // Get unique player names const playerNames = [...new Set(players.map(player => player.name))]; console.log(`Found ${playerNames.length} unique player names`); // Initialize player game Elo ratings for (const game of games) { console.log(`Initializing player ratings for ${game.name}...`); for (const player of players) { // Check if rating already exists const [existingRating] = await connection.query( 'SELECT id FROM player_game_elo WHERE player_id = ? AND game_id = ?', [player.id, game.id] ); if (existingRating.length === 0) { // Create initial rating await connection.query( 'INSERT INTO player_game_elo (player_id, game_id, rating, games_played) VALUES (?, ?, 1200, 0)', [player.id, game.id] ); } } } // Initialize player name Elo ratings for (const game of games) { console.log(`Initializing player name ratings for ${game.name}...`); for (const playerName of playerNames) { // Check if rating already exists const [existingRating] = await connection.query( 'SELECT id FROM player_name_elo WHERE player_name = ? AND game_id = ?', [playerName, game.id] ); if (existingRating.length === 0) { // Create initial rating await connection.query( 'INSERT INTO player_name_elo (player_name, game_id, rating, games_played) VALUES (?, ?, 1200, 0)', [playerName, game.id] ); } } } // Calculate Elo ratings based on past matches console.log('Calculating Elo ratings based on completed matches...'); // Get completed matches const [matches] = await connection.query(` SELECT m.id, m.game_id, m.results, mp1.player_id as player1_id, mp2.player_id as player2_id, p1.name as player1_name, p2.name as player2_name FROM matches m JOIN match_players mp1 ON m.id = mp1.match_id AND mp1.position = 1 JOIN match_players mp2 ON m.id = mp2.match_id AND mp2.position = 2 JOIN players p1 ON mp1.player_id = p1.id JOIN players p2 ON mp2.player_id = p2.id WHERE m.status = 'completed' ORDER BY m.created_at ASC `); console.log(`Found ${matches.length} completed matches`); // Process matches const elo = require('../server/utils/elo'); for (const match of matches) { // Parse results const results = typeof match.results === 'string' ? JSON.parse(match.results) : match.results; if (!results || !results.winner) { continue; // Skip matches without a winner } // Determine result from player1's perspective let result = 'draw'; if (results.winner === match.player1_name) { result = 'win'; } else if (results.winner === match.player2_name) { result = 'loss'; } // Update player Elo ratings await updatePlayerElo( connection, match.player1_id, match.player2_id, match.game_id, match.id, result ); // Update player name Elo ratings await updatePlayerNameElo( connection, match.player1_name, match.player2_name, match.game_id, match.id, result ); } console.log('Player name Elo ratings initialization completed!'); } catch (error) { console.error('Error initializing Elo ratings:', error); } finally { await connection.end(); } } async function getPlayerElo(connection, playerId, gameId) { const [results] = await connection.query( 'SELECT * FROM player_game_elo WHERE player_id = ? AND game_id = ?', [playerId, gameId] ); if (results.length === 0) { return { rating: 1200, games_played: 0 }; } return results[0]; } async function getPlayerNameElo(connection, playerName, gameId) { const [results] = await connection.query( 'SELECT * FROM player_name_elo WHERE player_name = ? AND game_id = ?', [playerName, gameId] ); if (results.length === 0) { return { rating: 1200, games_played: 0 }; } return results[0]; } async function updatePlayerElo(connection, playerId, opponentId, gameId, matchId, result) { // Get current ratings const playerElo = await getPlayerElo(connection, playerId, gameId); const opponentElo = await getPlayerElo(connection, opponentId, gameId); // Calculate new ratings const { newRatingA, newRatingB } = require('../server/utils/elo').updateRatings( playerElo.rating, opponentElo.rating, result ); // Update player rating await connection.query( 'UPDATE player_game_elo SET rating = ?, games_played = games_played + 1 WHERE player_id = ? AND game_id = ?', [newRatingA, playerId, gameId] ); // Update opponent rating await connection.query( 'UPDATE player_game_elo SET rating = ?, games_played = games_played + 1 WHERE player_id = ? AND game_id = ?', [newRatingB, opponentId, gameId] ); // Record history await connection.query( 'INSERT INTO elo_history (match_id, game_id, player_id, old_rating, new_rating, rating_change) VALUES (?, ?, ?, ?, ?, ?)', [matchId, gameId, playerId, playerElo.rating, newRatingA, newRatingA - playerElo.rating] ); await connection.query( 'INSERT INTO elo_history (match_id, game_id, player_id, old_rating, new_rating, rating_change) VALUES (?, ?, ?, ?, ?, ?)', [matchId, gameId, opponentId, opponentElo.rating, newRatingB, newRatingB - opponentElo.rating] ); } async function updatePlayerNameElo(connection, playerName, opponentName, gameId, matchId, result) { // Skip if both player names are the same if (playerName === opponentName) { return; } // Get current ratings const playerNameElo = await getPlayerNameElo(connection, playerName, gameId); const opponentNameElo = await getPlayerNameElo(connection, opponentName, gameId); // Calculate new ratings const { newRatingA, newRatingB } = require('../server/utils/elo').updateRatings( playerNameElo.rating, opponentNameElo.rating, result ); // Update player name rating await connection.query( 'UPDATE player_name_elo SET rating = ?, games_played = games_played + 1 WHERE player_name = ? AND game_id = ?', [newRatingA, playerName, gameId] ); // Update opponent name rating await connection.query( 'UPDATE player_name_elo SET rating = ?, games_played = games_played + 1 WHERE player_name = ? AND game_id = ?', [newRatingB, opponentName, gameId] ); // Record history await connection.query( 'INSERT INTO player_name_elo_history (match_id, game_id, player_name, old_rating, new_rating, rating_change) VALUES (?, ?, ?, ?, ?, ?)', [matchId, gameId, playerName, playerNameElo.rating, newRatingA, newRatingA - playerNameElo.rating] ); await connection.query( 'INSERT INTO player_name_elo_history (match_id, game_id, player_name, old_rating, new_rating, rating_change) VALUES (?, ?, ?, ?, ?, ?)', [matchId, gameId, opponentName, opponentNameElo.rating, newRatingB, newRatingB - opponentNameElo.rating] ); } // Run the script main() .then(() => process.exit(0)) .catch(err => { console.error('Initialization failed:', err); process.exit(1); }); ``` ### ./llm-olympics/scripts/initialize_playername_elo.sh File skipped (binary or too large) ### ./llm-olympics/scripts/package.json ```json { "dependencies": { "dotenv": "^16.4.7", "mysql2": "^3.13.0" } } ``` ### ./llm-olympics/scripts/schema.sql ```sql -- Set foreign key checks off at the beginning for clean setup SET FOREIGN_KEY_CHECKS=0; -- Drop existing tables if they exist DROP TABLE IF EXISTS moves; DROP TABLE IF EXISTS match_players; DROP TABLE IF EXISTS matches; DROP TABLE IF EXISTS games; DROP TABLE IF EXISTS players; -- Re-enable foreign key checks SET FOREIGN_KEY_CHECKS=1; -- Create games table CREATE TABLE games ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT, rules TEXT, parameters JSON, active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Create players table CREATE TABLE players ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, model VARCHAR(255) NOT NULL, parameters JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Create matches table CREATE TABLE matches ( id INT AUTO_INCREMENT PRIMARY KEY, game_id INT NOT NULL, status ENUM('pending', 'queued', 'running', 'completed', 'error') DEFAULT 'pending', parameters JSON, results JSON, history JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, completed_at TIMESTAMP NULL, FOREIGN KEY (game_id) REFERENCES games(id) ); -- Create match_players table CREATE TABLE match_players ( id INT AUTO_INCREMENT PRIMARY KEY, match_id INT NOT NULL, player_id INT NOT NULL, position INT NOT NULL, score FLOAT DEFAULT 0, FOREIGN KEY (match_id) REFERENCES matches(id), FOREIGN KEY (player_id) REFERENCES players(id) ); -- Create moves table to track all game moves CREATE TABLE moves ( id INT AUTO_INCREMENT PRIMARY KEY, match_id INT NOT NULL, game_id INT NOT NULL, player_id INT, turn_number INT NOT NULL, move_type VARCHAR(50) NOT NULL, move_data VARCHAR(255) NOT NULL, reasoning TEXT, prompt TEXT, response TEXT, coordinates JSON, full_state JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (match_id) REFERENCES matches(id), FOREIGN KEY (game_id) REFERENCES games(id), FOREIGN KEY (player_id) REFERENCES players(id) ); -- Insert initial games INSERT INTO games (name, description, rules, parameters, active) VALUES ('Tic-Tac-Toe', 'Classic 3x3 grid game where players take turns placing X or O', 'First player to get 3 in a row wins', '{}', 1), ('Tetris', 'Classic block stacking game', 'Clear lines to score points. Game ends when blocks reach the top', '{"width": 10, "height": 20, "tickRate": 1000}', 1), ('Snake', 'Classic Snake game where you grow by eating food', 'Eat food to grow and avoid hitting walls or yourself', '{"width": 10, "height": 10, "tickRate": 3000, "initialLength": 1, "foodCount": 5, "maxTurns": 100, "aiMoveInterval": 1, "startDelay": 5000}', 1); -- Insert initial players INSERT INTO players (name, model, parameters) VALUES ('gpt-4o', 'gpt-4o', '{"temperature": 0.7}'), ('gpt-4o-mini', 'gpt-4o-mini', '{"temperature": 0.7}'); ``` ### ./llm-olympics/server/database.js ```js const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: process.env.DB_HOST || 'localhost', user: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || 'appleman1Q!a', database: process.env.DB_NAME || 'llm_olympics', waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); async function testConnection() { try { const connection = await pool.getConnection(); console.log('Database connection successful!'); connection.release(); } catch (error) { console.error('Database connection failed:', error); process.exit(1); } } async function query(sql, params) { try { // Ensure no undefined values in parameters if (params) { params = params.map(p => p === undefined ? null : p); } const [results] = await pool.execute(sql, params); return results; } catch (error) { console.error('Database query error:', error); throw error; } } // Game-specific methods async function getGames() { return query('SELECT * FROM games WHERE active = 1'); } async function getGame(id) { const results = await query('SELECT * FROM games WHERE id = ?', [id]); return results[0]; } // Match-specific methods async function createMatch(gameId, players, parameters) { // Handle undefined parameters by defaulting to empty object const paramsJson = JSON.stringify(parameters || {}); const matchResult = await query( 'INSERT INTO matches (game_id, status, parameters, created_at) VALUES (?, ?, ?, NOW())', [gameId, 'pending', paramsJson] ); const matchId = matchResult.insertId; // Add players to the match for (let i = 0; i < players.length; i++) { if (!players[i]) { console.warn(`Skipping undefined player at position ${i+1}`); continue; // Skip undefined players } await query( 'INSERT INTO match_players (match_id, player_id, position) VALUES (?, ?, ?)', [matchId, players[i], i + 1] ); } return matchId; } async function getMatch(id) { const match = await query(` SELECT m.*, g.name as game_name, g.id as game_id FROM matches m JOIN games g ON m.game_id = g.id WHERE m.id = ?`, [id] ); if (match.length === 0) return null; // Parse parameters if they're stored as JSON string if (match[0].parameters && typeof match[0].parameters === 'string') { try { match[0].parameters = JSON.parse(match[0].parameters); } catch (e) { console.error('Error parsing match parameters:', e); match[0].parameters = {}; // Default to empty object if parsing fails } } // Get players for this match const players = await query(` SELECT mp.*, p.name as player_name, p.id as player_id FROM match_players mp JOIN players p ON mp.player_id = p.id WHERE mp.match_id = ? ORDER BY mp.position`, [id] ); match[0].players = players; return match[0]; } async function updateMatchStatus(matchId, status) { return query('UPDATE matches SET status = ? WHERE id = ?', [status, matchId]); } async function saveMatchResults(matchId, results, history) { return query( 'UPDATE matches SET status = ?, results = ?, history = ?, completed_at = NOW() WHERE id = ?', ['completed', JSON.stringify(results), JSON.stringify(history), matchId] ); } // Enhanced recordMove function to include prompt and response async function recordMove(matchId, gameId, playerId, turnNumber, moveType, moveData, reasoning = '', coordinates = null, prompt = null, response = null, gameState = null) { try { console.log('Debug - Recording move to database:'); console.log('- matchId:', matchId); console.log('- gameId:', gameId); console.log('- playerId:', playerId); console.log('- moveType:', moveType); // Check if moveData is too long for the VARCHAR(255) field if (moveData && moveData.length > 250) { console.warn(`Move data too long (${moveData.length} chars), truncating to 250 chars`); moveData = moveData.substring(0, 250); } const coordJson = coordinates ? JSON.stringify(coordinates) : null; const stateJson = gameState ? JSON.stringify(gameState) : null; const result = await query( `INSERT INTO moves (match_id, game_id, player_id, turn_number, move_type, move_data, reasoning, prompt, response, coordinates, full_state, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`, [matchId, gameId, playerId, turnNumber, moveType, moveData, reasoning, prompt, response, coordJson, stateJson] ); console.log(`Recorded ${moveType} move for player ${playerId} in match ${matchId}. Insert ID: ${result.insertId}`); return true; } catch (error) { console.error('Error recording move:', error); console.error('Debug - Failed with parameters:', { matchId, gameId, playerId, turnNumber, moveType }); return false; } } // Get moves for a match async function getMatchMoves(matchId) { return query(` SELECT m.*, p.name as player_name FROM moves m LEFT JOIN players p ON m.player_id = p.id WHERE m.match_id = ? ORDER BY m.turn_number, m.created_at`, [matchId] ); } // Get a specific move by ID with full details async function getMoveById(moveId) { const results = await query(` SELECT m.*, p.name as player_name, g.name as game_name FROM moves m LEFT JOIN players p ON m.player_id = p.id LEFT JOIN games g ON m.game_id = g.id WHERE m.id = ?`, [moveId] ); if (results.length === 0) return null; return results[0]; } module.exports = { testConnection, query, getGames, getGame, createMatch, getMatch, updateMatchStatus, saveMatchResults, recordMove, getMatchMoves, getMoveById }; ``` ### ./llm-olympics/server/games/rockpaperscissors.js ```js /** * Rock Paper Scissors Game Implementation */ class RockPaperScissorsGame { constructor(gameId, name, parameters = {}) { this.gameId = gameId; this.name = name; // Game parameters this.totalRounds = parameters.totalRounds || 10; // Game state this.players = []; this.currentRound = 0; this.moves = {}; // Stores moves for the current round this.history = []; // Stores history of all rounds this.scores = {}; // Player scores this.gameOver = false; this.winner = null; // Match data for database this.matchId = null; this.gameDbId = null; this.playerDbIds = {}; } // Set match data for database integration setMatchData(matchId, gameDbId, players) { this.matchId = matchId; this.gameDbId = gameDbId; // Store player IDs for database records if (players && players.length) { players.forEach(player => { if (player.player_name && player.player_id) { this.playerDbIds[player.player_name] = player.player_id; } }); } console.log(`Set match data: Match ID ${matchId}, Game ID ${gameDbId}, Players:`, this.playerDbIds); } initialize_game(players) { if (players.length !== 2) { throw new Error("Rock Paper Scissors requires exactly 2 players"); } this.players = players; this.currentRound = 0; this.moves = {}; this.history = []; this.scores = {}; this.gameOver = false; this.winner = null; // Initialize scores players.forEach(player => { this.scores[player] = 0; }); // Record initial state this.history.push({ round: 0, moves: {}, result: null, state: this.get_state_for_renderer() }); // Record initial state to database if (this.matchId && this.gameDbId) { const db = require('../database'); db.recordMove( this.matchId, this.gameDbId, null, 0, 'initialize', 'game_start', 'Game initialized', { totalRounds: this.totalRounds }, null, null, this.get_state_for_renderer() ); } } make_move(player, move, reasoning = '', prompt = null, response = null) { // Game already over if (this.gameOver) { return false; } // Check if player is valid if (!this.players.includes(player)) { return false; } // Validate move move = move.toLowerCase(); if (!['rock', 'paper', 'scissors'].includes(move)) { console.log(`Invalid move: ${move}`); return false; } // Check if player has already moved this round if (this.moves[player]) { console.log(`Player ${player} has already moved this round`); return false; } // Record the move this.moves[player] = move; // Record move to database if (this.matchId && this.gameDbId && this.playerDbIds[player]) { const db = require('../database'); db.recordMove( this.matchId, this.gameDbId, this.playerDbIds[player], this.currentRound, 'select', move, reasoning || 'No reasoning provided', { round: this.currentRound + 1 }, prompt, response, this.get_state_for_renderer() ); } // Check if both players have moved if (Object.keys(this.moves).length === 2) { this._resolveRound(); } return true; } // Resolve the current round _resolveRound() { const roundResult = { round: this.currentRound + 1, moves: { ...this.moves }, winner: null, state: null }; // Get player moves const [player1, player2] = this.players; const move1 = this.moves[player1]; const move2 = this.moves[player2]; // Determine the winner if (move1 === move2) { // Tie roundResult.result = 'tie'; } else if ( (move1 === 'rock' && move2 === 'scissors') || (move1 === 'paper' && move2 === 'rock') || (move1 === 'scissors' && move2 === 'paper') ) { // Player 1 wins roundResult.result = 'win'; roundResult.winner = player1; this.scores[player1]++; } else { // Player 2 wins roundResult.result = 'win'; roundResult.winner = player2; this.scores[player2]++; } // Record round result to database if (this.matchId && this.gameDbId) { const db = require('../database'); const winnerDbId = roundResult.winner ? this.playerDbIds[roundResult.winner] : null; db.recordMove( this.matchId, this.gameDbId, winnerDbId, this.currentRound, 'resolve', roundResult.result, `Round ${this.currentRound + 1} resolved`, { round: this.currentRound + 1, moves: roundResult.moves, result: roundResult.result, winner: roundResult.winner, scores: this.scores }, null, null, this.get_state_for_renderer() ); } // Update state and history this.currentRound++; roundResult.state = this.get_state_for_renderer(); this.history.push(roundResult); // Reset moves for next round this.moves = {}; // Check if game is over if (this.currentRound >= this.totalRounds) { this._endGame(); } } _endGame() { this.gameOver = true; // Determine the winner const [player1, player2] = this.players; if (this.scores[player1] > this.scores[player2]) { this.winner = player1; } else if (this.scores[player2] > this.scores[player1]) { this.winner = player2; } else { // Tie this.winner = null; } // Record game end to database if (this.matchId && this.gameDbId) { const db = require('../database'); const winnerDbId = this.winner ? this.playerDbIds[this.winner] : null; db.recordMove( this.matchId, this.gameDbId, winnerDbId, this.currentRound, 'end', 'game_over', this.winner ? `Game ended. Winner: ${this.winner}` : 'Game ended in a tie', { scores: this.scores, winner: this.winner }, null, null, this.get_state_for_renderer() ); } } get_valid_moves() { if (this.gameOver) { return []; } return ['rock', 'paper', 'scissors']; } get_next_player() { if (this.gameOver) { return null; } // Return the first player who hasn't moved this round for (const player of this.players) { if (!this.moves[player]) { return player; } } return null; } advance_player() { // No-op since both players move in each round } is_game_over() { return this.gameOver; } get_winner() { return this.winner; } get_state_for_renderer() { return { players: this.players, currentRound: this.currentRound, totalRounds: this.totalRounds, moves: this.moves, history: this.history.map(round => ({ round: round.round, moves: round.moves, result: round.result, winner: round.winner })), scores: this.scores, game_over: this.gameOver, winner: this.winner }; } get_state_description() { const player = this.get_next_player(); if (!player) return "Game is over."; const opponent = this.players.find(p => p !== player); // Create description of the current state let description = `You are playing Rock Paper Scissors against ${opponent}.\n\n`; // Current round and scores description += `Current round: ${this.currentRound + 1} of ${this.totalRounds}\n`; description += `Your score: ${this.scores[player]} | Opponent score: ${this.scores[opponent]}\n\n`; // History of previous rounds if (this.currentRound > 0) { description += `Previous rounds:\n`; for (let i = 0; i < this.history.length; i++) { const round = this.history[i]; if (round.moves && Object.keys(round.moves).length === 2) { const yourMove = round.moves[player]; const opponentMove = round.moves[opponent]; if (yourMove && opponentMove) { const roundResult = round.winner === player ? "You won" : (round.winner === opponent ? "Opponent won" : "Tie"); description += `Round ${round.round}: You chose ${yourMove}, opponent chose ${opponentMove}. ${roundResult}.\n`; } } } description += '\n'; } // Explain rules description += `Rules:\n`; description += `1. Choose rock, paper, or scissors.\n`; description += `2. Rock beats scissors, scissors beats paper, paper beats rock.\n`; description += `3. The player with the most wins after ${this.totalRounds} rounds wins the game.\n\n`; description += `What is your choice for round ${this.currentRound + 1}? (rock, paper, or scissors)`; return description; } get_function_definition() { return { "name": "make_move", "description": "Choose rock, paper, or scissors for the current round", "parameters": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Your reasoning for choosing this option" }, "move": { "type": "string", "enum": ["rock", "paper", "scissors"], "description": "Your choice for this round" } }, "required": ["reasoning", "move"] } }; } } module.exports = RockPaperScissorsGame; ``` ### ./llm-olympics/server/games/tictactoe.js ```js /** * TicTacToe Game Implementation */ class TicTacToeGame { constructor(gameId, name, parameters = {}) { this.gameId = gameId; this.name = name; this.parameters = parameters; // Game state this.board = [ [' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' '] ]; this.players = []; this.playerSymbols = {}; this.currentPlayerIdx = 0; this.currentTurn = 0; this.gameOver = false; this.winner = null; this.history = []; // Match data for database this.matchId = null; this.gameDbId = null; this.playerDbIds = {}; } // Set match data for database integration setMatchData(matchId, gameDbId, players) { this.matchId = matchId; this.gameDbId = gameDbId; // Store player IDs for database records if (players && players.length) { players.forEach(player => { if (player.player_name && player.player_id) { this.playerDbIds[player.player_name] = player.player_id; } }); } console.log(`Set match data for TicTacToe: Match ID ${matchId}, Game ID ${gameDbId}, Players:`, this.playerDbIds); } initialize_game(players) { if (players.length !== 2) { throw new Error("TicTacToe requires exactly 2 players"); } this.players = players; this.playerSymbols = { [players[0]]: 'X', [players[1]]: 'O' }; this.currentPlayerIdx = 0; this.gameOver = false; this.winner = null; this.currentTurn = 0; // Initialize board this.board = [ [' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' '] ]; // Record initial state this.history.push({ player: null, move: null, state: this.get_state_for_renderer() }); // Record game initialization to database if (this.matchId && this.gameDbId) { const db = require('../database'); db.recordMove( this.matchId, this.gameDbId, null, // No specific player this.currentTurn, 'initialize', 'game_start', 'Game initialized', { board: this.board, players: this.players }, null, null, this.get_state_for_renderer() ); } } get_state_description() { const currentPlayer = this.get_next_player(); const symbol = this.playerSymbols[currentPlayer]; let description = `You are playing Tic-Tac-Toe as player "${currentPlayer}" with symbol "${symbol}".\n\n`; description += "Current board state:\n\n"; // Create ASCII board for (let i = 0; i < 3; i++) { description += ' '; for (let j = 0; j < 3; j++) { description += this.board[i][j] === ' ' ? '.' : this.board[i][j]; if (j < 2) description += ' | '; } description += '\n'; if (i < 2) description += ' -----------\n'; } description += "\nValid moves are specified as 'row,column' with 0-indexed positions.\n"; description += "For example, the center position is '1,1' and the top-left is '0,0'.\n\n"; description += `Valid moves: ${this.get_valid_moves().join(', ')}\n\n`; description += "Your goal is to get three of your symbols in a row (horizontally, vertically, or diagonally).\n"; return description; } get_valid_moves() { const moves = []; for (let row = 0; row < 3; row++) { for (let col = 0; col < 3; col++) { if (this.board[row][col] === ' ') { moves.push(`${row},${col}`); } } } return moves; } get_next_player() { return this.players[this.currentPlayerIdx]; } advance_player() { this.currentPlayerIdx = (this.currentPlayerIdx + 1) % this.players.length; } make_move(player, move, reasoning = '', prompt = null, response = null) { if (this.gameOver) return false; if (player !== this.get_next_player()) { return false; } // Parse move const [row, col] = move.split(',').map(v => parseInt(v, 10)); // Validate move if (row < 0 || row > 2 || col < 0 || col > 2 || this.board[row][col] !== ' ') { // Record invalid move to database if (this.matchId && this.gameDbId && this.playerDbIds[player]) { const db = require('../database'); db.recordMove( this.matchId, this.gameDbId, this.playerDbIds[player], this.currentTurn, 'invalid_move', move, reasoning, { row, col, valid: false }, prompt, response, this.get_state_for_renderer() ); } return false; } // Apply move this.board[row][col] = this.playerSymbols[player]; this.currentTurn++; // Record valid move to database if (this.matchId && this.gameDbId && this.playerDbIds[player]) { const db = require('../database'); db.recordMove( this.matchId, this.gameDbId, this.playerDbIds[player], this.currentTurn, 'move', move, reasoning, { row, col, symbol: this.playerSymbols[player] }, prompt, response, this.get_state_for_renderer() ); } // Check for win this._check_game_over(); // Record state this.history.push({ player, move, reasoning, state: this.get_state_for_renderer() }); // Record game end to database if game is over if (this.gameOver && this.matchId && this.gameDbId) { const db = require('../database'); const winnerDbId = this.winner ? this.playerDbIds[this.winner] : null; db.recordMove( this.matchId, this.gameDbId, winnerDbId, this.currentTurn, 'end', 'game_over', this.winner ? `Game ended. Winner: ${this.winner}` : 'Game ended in a draw', { winner: this.winner, board: this.board }, null, null, this.get_state_for_renderer() ); } // Move to next player if game not over if (!this.gameOver) { this.advance_player(); } return true; } _check_game_over() { // Check rows for (let i = 0; i < 3; i++) { if (this.board[i][0] !== ' ' && this.board[i][0] === this.board[i][1] && this.board[i][1] === this.board[i][2]) { this.gameOver = true; this.winner = this._get_player_by_symbol(this.board[i][0]); return; } } // Check columns for (let i = 0; i < 3; i++) { if (this.board[0][i] !== ' ' && this.board[0][i] === this.board[1][i] && this.board[1][i] === this.board[2][i]) { this.gameOver = true; this.winner = this._get_player_by_symbol(this.board[0][i]); return; } } // Check diagonals if (this.board[0][0] !== ' ' && this.board[0][0] === this.board[1][1] && this.board[1][1] === this.board[2][2]) { this.gameOver = true; this.winner = this._get_player_by_symbol(this.board[0][0]); return; } if (this.board[0][2] !== ' ' && this.board[0][2] === this.board[1][1] && this.board[1][1] === this.board[2][0]) { this.gameOver = true; this.winner = this._get_player_by_symbol(this.board[0][2]); return; } // Check for draw (board full) let full = true; for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (this.board[i][j] === ' ') { full = false; break; } } if (!full) break; } if (full) { this.gameOver = true; this.winner = null; // Draw } } _get_player_by_symbol(symbol) { for (const [player, playerSymbol] of Object.entries(this.playerSymbols)) { if (playerSymbol === symbol) { return player; } } return null; } is_game_over() { return this.gameOver; } get_winner() { return this.winner; } get_scores() { const scores = {}; for (const player of this.players) { scores[player] = { score: player === this.winner ? 1.0 : 0.0 }; } return scores; } get_state_for_renderer() { const state = { board: this.board, turn: this.currentTurn, current_player: this.get_next_player(), players: { [this.players[0]]: { symbol: 'X', score: this._get_player_score(this.players[0]) }, [this.players[1]]: { symbol: 'O', score: this._get_player_score(this.players[1]) } }, game_over: this.gameOver, winner: this.winner }; return state; } _get_player_score(player) { if (!this.gameOver) return 0; return player === this.winner ? 1 : 0; } get_function_definition() { return { "name": "make_move", "description": "Make a move in the Tic-Tac-Toe game", "parameters": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Your detailed reasoning for making this move" }, "move": { "type": "string", "enum": this.get_valid_moves(), "description": "The position to place your symbol, formatted as 'row,column' (e.g., '1,1' for center)" } }, "required": ["reasoning", "move"] } }; } } module.exports = TicTacToeGame; ``` ### ./llm-olympics/server/index.js ```js // Load environment variables from .env require('dotenv').config(); console.log('Environment loaded. API Key status:', process.env.OPENAI_API_KEY ? 'SET' : 'NOT SET'); const express = require('express'); const http = require('http'); const cors = require('cors'); const { initSocketIO } = require('./socket'); const db = require('./database'); // Import matchRunner before routes const { initMatchRunner } = require('./matchRunner'); // Initialize Express app const app = express(); const server = http.createServer(app); // Initialize Socket.io const io = initSocketIO(server); // Initialize Match Runner with io - IMPORTANT! const matchRunner = initMatchRunner(io); console.log("Match runner initialized successfully"); // Middleware app.use(cors({ origin: "*", methods: ['GET', 'POST', 'PUT', 'DELETE'], credentials: true })); app.use(express.json()); // Pass io and matchRunner to routes app.use((req, res, next) => { req.io = io; req.matchRunner = matchRunner; req.scheduler = scheduler; // Add scheduler to request next(); }); // NOW import and use routes (AFTER initializing matchRunner) const matchRoutes = require('./routes/matches'); const gameRoutes = require('./routes/games'); const playerRoutes = require('./routes/players'); const movesRoutes = require('./routes/moves'); const MatchScheduler = require('./services/matchScheduler'); const statsRoutes = require('./routes/stats'); const eloRoutes = require('./routes/elo'); const schedulerRoutes = require('./routes/scheduler'); // Create scheduler with configuration const scheduler = new MatchScheduler({ targetMatchCount: 10, checkInterval: 60000, // 1 minute maxMatchesPerPair: 3, // Maximum of 3 matches between each model pair per game stalledMatchTimeout: 30 * 60 * 1000, // 30 minutes timeout for stalled matches gameWeights: { 'tictactoe': 1, 'connectfour': 1, 'hangman': 1, 'rockpaperscissors': 1 } }); app.use('/api/matches', matchRoutes); app.use('/api/games', gameRoutes); app.use('/api/players', playerRoutes); app.use('/api/moves', movesRoutes); // Add to your server.js or index.js file app.use('/api/stats', statsRoutes); app.use('/api/elo', eloRoutes); // Add with other app.use statements app.use('/api/scheduler', schedulerRoutes); // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'ok' }); }); // Root route for API info app.get('/', (req, res) => { // Get client URL from env or default to 3000 const clientPort = process.env.CLIENT_PORT || 3000; const clientUrl = `http://localhost:${clientPort}`; res.send(` LLM Olympics API

LLM Olympics API Server

This is the backend API server. To access the web interface, please visit:

${clientUrl}

Available API endpoints:

`); }); // Error handler app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Something went wrong!', message: process.env.NODE_ENV === 'development' ? err.message : undefined }); }); // Start the server const PORT = process.env.PORT || 3001; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); // Optional: Add a shutdown handler process.on('SIGTERM', () => { console.log('Shutting down match scheduler'); scheduler.stop(); }); });``` ### ./llm-olympics/server/matchRunner.js ```js const db = require('./database'); const { emitMatchUpdate } = require('./socket'); const openaiService = require('./openaiService'); const { v4: uuidv4 } = require('uuid'); class MatchRunner { constructor(io) { this.io = io; this.runningMatches = new Map(); this.matchQueue = []; this.maxParallelMatches = 10; // Run up to 10 matches simultaneously // Start periodic queue processing setInterval(() => this.processQueue(), 5000); // Start periodic inactive match checking setInterval(() => this.checkInactiveMatches(), 60000); // Check every minute } async queueMatch(matchId) { // Check if match is valid const match = await db.getMatch(matchId); if (!match) { throw new Error(`Match ${matchId} not found`); } // Add to queue if not already running if (!this.runningMatches.has(matchId) && !this.matchQueue.includes(matchId)) { this.matchQueue.push(matchId); await db.updateMatchStatus(matchId, 'queued'); console.log(`Match ${matchId} queued for execution`); } this.processQueue(); } processQueue() { const availableSlots = this.maxParallelMatches - this.runningMatches.size; if (availableSlots > 0 && this.matchQueue.length > 0) { // Start multiple matches in parallel const matchesToStart = this.matchQueue.splice(0, availableSlots); matchesToStart.forEach(matchId => this.startMatch(matchId)); } } async startMatch(matchId) { try { const match = await db.getMatch(matchId); if (!match) { console.error(`Match ${matchId} not found`); return; } console.log(`Starting match ${matchId}: ${match.game_name}`); // Update status await db.updateMatchStatus(matchId, 'running'); // Create game instance const game = this.createGameInstance(match); if (!game) { throw new Error(`Could not create game instance for ${match.game_name}`); } // Initialize the game with match data if supported if (game.setMatchData && typeof game.setMatchData === 'function') { console.log(`Setting match data for ${match.game_name}: Match ID ${matchId}, Game ID ${match.game_id}`); console.log('Debug - Match players data:', JSON.stringify(match.players, null, 2)); game.setMatchData(matchId, match.game_id, match.players); } // Store in running matches this.runningMatches.set(matchId, { game, match, history: [], startTime: Date.now() }); // Initialize the game with players const players = match.players.map(p => p.player_name); game.initialize_game(players); // Emit initial state emitMatchUpdate(this.io, matchId, 'started', { matchId, game: match.game_name, players, state: game.get_state_for_renderer ? game.get_state_for_renderer() : null }); // Run the match this.runMatchLoop(matchId); } catch (error) { console.error(`Error starting match ${matchId}:`, error); db.updateMatchStatus(matchId, 'error'); this.runningMatches.delete(matchId); } } async runMatchLoop(matchId) { const matchData = this.runningMatches.get(matchId); if (!matchData) return; const { game, match } = matchData; // Check if game is over if (game.is_game_over && game.is_game_over()) { await this.endMatch(matchId); return; } // Get next player const currentPlayer = game.get_next_player(); if (!currentPlayer) { console.error(`No current player for match ${matchId}`); await this.endMatch(matchId); return; } try { // Get player model from database const playerInfo = match.players.find(p => p.player_name === currentPlayer); if (!playerInfo) { throw new Error(`Player ${currentPlayer} not found in match`); } // Get AI model info const model = await db.query( 'SELECT model FROM players WHERE id = ?', [playerInfo.player_id] ); if (!model.length) { throw new Error(`No model found for player ${currentPlayer}`); } const aiModel = model[0].model; // Get game state description for AI const stateDescription = game.get_state_description(); // Save the prompt to pass to the game later const prompt = stateDescription; // Get function definition for this game const functionDef = game.get_function_definition(); // Maximum number of retry attempts const MAX_RETRIES = 3; let moveSuccess = false; let attempts = 0; while (!moveSuccess && attempts < MAX_RETRIES) { attempts++; // Call OpenAI API console.log(`Getting move from ${currentPlayer} (${aiModel}) - Attempt ${attempts}/${MAX_RETRIES}`); try { const response = await openaiService.callOpenAI( aiModel, [{ role: 'user', content: stateDescription }], [functionDef] ); // Store the full response for reference const fullResponse = JSON.stringify(response); // Process the AI's move if (response.function_call) { try { const moveData = JSON.parse(response.function_call.arguments); console.log(`${currentPlayer} move:`, moveData.move); // Apply the move to the game with reasoning, prompt and response const moveResult = game.make_move( currentPlayer, moveData.move, moveData.reasoning || response.content || '', prompt, fullResponse ); if (moveResult) { moveSuccess = true; // Record move in history matchData.history.push({ player: currentPlayer, move: moveData.move, reasoning: moveData.reasoning || response.content || '', timestamp: Date.now() }); // Emit update to clients emitMatchUpdate(this.io, matchId, 'move', { player: currentPlayer, move: moveData.move, state: game.get_state_for_renderer ? game.get_state_for_renderer() : null }); // Check if the game is over after this move if (game.is_game_over && game.is_game_over()) { console.log(`Game over detected after move from ${currentPlayer}`); await this.endMatch(matchId); return; // Exit the match loop } break; // Exit the retry loop on success } else { console.error(`Invalid move ${moveData.move} from ${currentPlayer}`); } } catch (parseError) { console.error(`Error parsing move: ${parseError.message}`); // Continue to next attempt } } else { console.error(`No function call in response for ${currentPlayer} (attempt ${attempts}/${MAX_RETRIES})`); } } catch (error) { console.error(`API Error in attempt ${attempts}/${MAX_RETRIES}:`, error.message); } // Wait a bit before retrying if (attempts < MAX_RETRIES) { await new Promise(resolve => setTimeout(resolve, 500)); } } // If all attempts failed, make a random move if (!moveSuccess) { console.log(`All ${MAX_RETRIES} attempts failed for ${currentPlayer}, using random move fallback`); const validMoves = game.get_valid_moves(); if (validMoves && validMoves.length > 0) { // Select a random move from valid moves const randomMove = validMoves[Math.floor(Math.random() * validMoves.length)]; console.log(`${currentPlayer} RANDOM MOVE: ${randomMove}`); // Apply the random move const moveResult = game.make_move( currentPlayer, randomMove, "Auto-generated random move after failed API responses", prompt, JSON.stringify({ content: "Random fallback move" }) ); if (moveResult) { // Record the random move in history matchData.history.push({ player: currentPlayer, move: randomMove, reasoning: "Random move selected after failed API responses", timestamp: Date.now(), random: true }); // Emit update to clients emitMatchUpdate(this.io, matchId, 'move', { player: currentPlayer, move: randomMove, state: game.get_state_for_renderer ? game.get_state_for_renderer() : null }); // Check if the game is over after this random move if (game.is_game_over && game.is_game_over()) { console.log(`Game over detected after random move from ${currentPlayer}`); await this.endMatch(matchId); return; // Exit the match loop } } else { console.error(`Random move ${randomMove} was invalid - advancing player`); game.advance_player(); // Manually advance player since no valid move was made } } else { console.error(`No valid moves available for ${currentPlayer} - advancing player`); game.advance_player(); // Manually advance player } } // Continue game loop after delay setTimeout(() => this.runMatchLoop(matchId), 1000); } catch (error) { console.error(`Error in match ${matchId}:`, error); // Log error but try to continue with next player game.advance_player(); setTimeout(() => this.runMatchLoop(matchId), 1000); } } async endMatch(matchId) { const matchData = this.runningMatches.get(matchId); if (!matchData) return; const { game, match, history } = matchData; console.log(`Ending match ${matchId}`); // Get final scores const scores = game.get_scores ? game.get_scores() : {}; const winner = game.get_winner ? game.get_winner() : null; // Prepare results const results = { scores, winner, duration: Date.now() - matchData.startTime, state: game.get_state_for_renderer ? game.get_state_for_renderer() : null }; // Save results to database await db.saveMatchResults(matchId, results, history); // Process Elo ratings try { const eloService = require('./services/eloService'); const eloResults = await eloService.processMatchElo(matchId); console.log(`Processed Elo ratings for match ${matchId}:`, eloResults); // Add Elo changes to the results results.eloChanges = { playerRatings: eloResults.playerEloUpdate, modelRatings: eloResults.modelEloUpdate }; // Update the results in the database with Elo information await db.query( 'UPDATE matches SET results = ? WHERE id = ?', [JSON.stringify(results), matchId] ); } catch (eloError) { console.error(`Failed to process Elo ratings for match ${matchId}:`, eloError); } // Emit end event emitMatchUpdate(this.io, matchId, 'ended', { matchId, results, state: game.get_state_for_renderer ? game.get_state_for_renderer() : null }); // Remove from running matches this.runningMatches.delete(matchId); // Process queue to start next match this.processQueue(); } createGameInstance(match) { // Factory method to create the appropriate game instance try { const gameName = match.game_name.toLowerCase().replace(/[^a-z0-9]/g, ""); const gameId = uuidv4(); // Fix parameters parsing let parameters = {}; if (match.parameters) { console.log("Parameters type:", typeof match.parameters); if (typeof match.parameters === "string") { try { // If it's a JSON string, parse it parameters = JSON.parse(match.parameters); } catch (e) { console.log("Not valid JSON:", match.parameters); // If it's not valid JSON but a string representation of an object if (match.parameters === "[object Object]") { parameters = {}; } } } else { // It's already an object parameters = match.parameters; } } console.log(`Creating ${match.game_name} with parameters:`, parameters); // Import the correct game module let GameClass; if (gameName === "tictactoe") { GameClass = require("./games/tictactoe"); } else if (gameName === "tetris") { GameClass = require("./games/tetris"); } else if (gameName === "snake") { GameClass = require("./games/snake"); } else { try { // Try to dynamically import game GameClass = require(`./games/${gameName}`); } catch (e) { console.error(`Error importing game ${gameName}:`, e); throw new Error(`Game ${match.game_name} is not implemented`); } } // Create and return game instance return new GameClass(gameId, match.game_name, parameters); } catch (error) { console.error("Error creating game instance:", error); return null; } } getRunningMatches() { // Return info about running matches return Array.from(this.runningMatches.keys()).map(id => { const { match, startTime } = this.runningMatches.get(id); return { id, game: match.game_name, players: match.players.map(p => p.player_name), runningFor: Date.now() - startTime }; }); } getQueuedMatches() { return this.matchQueue; } /** * Check for and terminate matches with no activity in the last 10 minutes */ async checkInactiveMatches() { console.log('Checking for inactive matches...'); const now = Date.now(); const inactiveThreshold = 10 * 60 * 1000; // 10 minutes in milliseconds const matchesToEnd = []; // Loop through running matches and check last activity for (const [matchId, matchData] of this.runningMatches.entries()) { // Skip newly started matches with no history yet if (!matchData.history || matchData.history.length === 0) { continue; } // Calculate time since last move const lastMove = matchData.history[matchData.history.length - 1]; const timeSinceLastMove = now - lastMove.timestamp; if (timeSinceLastMove > inactiveThreshold) { console.warn(`Match ${matchId} inactive for ${Math.round(timeSinceLastMove/60000)} minutes, aborting`); matchesToEnd.push(matchId); } } // End inactive matches for (const matchId of matchesToEnd) { try { // Update the database directly to mark as error await db.query(` UPDATE matches SET status = 'error', results = JSON_OBJECT('error', 'Match terminated due to inactivity - no moves in last 10 minutes') WHERE id = ? `, [matchId]); console.log(`Marked inactive match ${matchId} as error (no recent moves)`); // Remove from running matches this.runningMatches.delete(matchId); // Emit error event to clients emitMatchUpdate(this.io, matchId, 'error', { matchId, error: 'Match terminated due to inactivity - no moves in last 10 minutes' }); } catch (error) { console.error(`Error ending inactive match ${matchId}:`, error); } } // Process queue to start new matches if needed if (matchesToEnd.length > 0) { this.processQueue(); } } } async function enqueueMatch(matchId, config) { try { // Create match record (or update existing one) const match = { id: matchId, gameType: config.gameType, players: config.players, parameters: config.parameters || {} }; // Add to processing queue matchQueue.push(match); // Update match status in database await db.query( 'UPDATE matches SET status = ? WHERE id = ?', ['queued', matchId] ); // If queue processor isn't running, start it if (!isProcessing) { processNextMatch(); } return matchId; } catch (error) { console.error(`Error enqueueing match ${matchId}:`, error); throw error; } } // Export as singleton let matchRunner = null; module.exports = { initMatchRunner: (io) => { if (!matchRunner) { matchRunner = new MatchRunner(io); } return matchRunner; }, getMatchRunner: () => matchRunner, enqueueMatch }; ``` ### ./llm-olympics/server/openaiService.js ```js const axios = require('axios'); async function callOpenAI(model, messages, functions) { // Determine which service to use based on environment variables const useOpenRouter = process.env.USE_OPENROUTER === 'true'; const openRouterApiKey = process.env.OPENROUTER_API_KEY || "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"; const openAiApiKey = process.env.OPENAI_API_KEY; // Try OpenRouter first if configured, otherwise use OpenAI directly if (useOpenRouter) { try { return await callOpenRouter(model, messages, functions, openRouterApiKey); } catch (error) { // If OpenRouter fails specifically due to tool use not being supported if (error.message && ( error.message.includes('No endpoints found that support tool use') || error.message.includes('404') )) { console.warn('OpenRouter does not support tool use for this model. Falling back to OpenAI API...'); return await callOpenAIDirectly(model, messages, functions, openAiApiKey); } // For other errors, just rethrow throw error; } } else { // Direct OpenAI call return await callOpenAIDirectly(model, messages, functions, openAiApiKey); } } // Function to call OpenRouter async function callOpenRouter(model, messages, functions, apiKey) { if (!apiKey) { throw new Error('OpenRouter API key not configured. Please set OPENROUTER_API_KEY in .env file.'); } console.log(`✓ Using OpenRouter API key: ${apiKey.substring(0, 5)}...${apiKey.substring(apiKey.length - 4)}`); // Format the function properly for the API const formattedFunctions = functions.map(func => { // Remove the nesting if it exists if (func.type === "function" && func.function) { return func.function; } return func; }); console.log('Calling OpenRouter with function:', JSON.stringify(formattedFunctions[0].name)); // Map OpenAI model names to OpenRouter compatible model references let routerModel = model; // Configure for OpenRouter format const payload = { model: routerModel, messages, tools: formattedFunctions.map(fn => ({ type: 'function', function: fn })), tool_choice: { type: 'function', function: { name: formattedFunctions[0].name } } }; const response = await axios.post( 'https://openrouter.ai/api/v1/chat/completions', payload, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'HTTP-Referer': 'https://llm-olympics.com', 'X-Title': 'LLM Olympics' } } ); return processResponse(response.data.choices[0].message); } // Function to call OpenAI directly async function callOpenAIDirectly(model, messages, functions, apiKey) { if (!apiKey) { throw new Error('OpenAI API key not configured. Please set OPENAI_API_KEY in .env file.'); } console.log(`✓ Using OpenAI API key: ${apiKey.substring(0, 5)}...${apiKey.substring(apiKey.length - 4)}`); // Format the function properly for the API const formattedFunctions = functions.map(func => { // Remove the nesting if it exists if (func.type === "function" && func.function) { return func.function; } return func; }); console.log('Calling OpenAI with function:', JSON.stringify(formattedFunctions[0].name)); // Configure for OpenAI format const payload = { model, messages, tools: formattedFunctions.map(fn => ({ type: 'function', function: fn })), tool_choice: { type: 'function', function: { name: formattedFunctions[0].name } } }; const response = await axios.post( 'https://api.openai.com/v1/chat/completions', payload, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` } } ); return processResponse(response.data.choices[0].message); } // Process the LLM response in a consistent way function processResponse(message) { // Check for tool calls in the response (new format) if (message.tool_calls && message.tool_calls.length > 0) { const toolCall = message.tool_calls[0]; // Extract the reasoning from the message content if available let reasoning = message.content || ''; try { // Parse the arguments to extract the reasoning and move const argsObj = JSON.parse(toolCall.function.arguments); // For snake game, we want to capture the full reasoning if (argsObj.reasoning) { reasoning = argsObj.reasoning; } // Make sure we actually get a move value if (!argsObj.move) { console.warn("Missing 'move' in function arguments. Full arguments:", toolCall.function.arguments); } // Log found reasoning for debugging console.log(`Extracted reasoning (${reasoning.length} chars): ${reasoning.substring(0, 100)}...`); // Convert to old function_call format with added reasoning in the response return { content: reasoning, function_call: { name: toolCall.function.name, arguments: toolCall.function.arguments } }; } catch (e) { console.error("Error parsing tool call arguments:", e); // On error, return the original message with empty reasoning return { content: reasoning, function_call: { name: toolCall.function.name, arguments: toolCall.function.arguments } }; } } // Handle legacy/old format function calls if (message.function_call) { // Return as is, extracting reasoning from content if available return { content: message.content || '', function_call: message.function_call }; } // No function call found, return the message as is return message; } module.exports = { callOpenAI }; ``` ### ./llm-olympics/server/package.json ```json { "name": "llm-olympics-server", "version": "1.0.0", "description": "LLM Olympics Game Platform Server", "main": "index.js", "scripts": { "start": "node index.js", "dev": "nodemon index.js", "test": "jest" }, "dependencies": { "axios": "^1.6.2", "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.18.2", "mysql2": "^3.13.0", "socket.io": "^4.7.2", "uuid": "^9.0.1" }, "devDependencies": { "jest": "^29.7.0", "nodemon": "^3.0.2" } } ``` ### ./llm-olympics/server/routes/elo.js ```js /** * Elo Rating API Routes */ const express = require('express'); const router = express.Router(); const db = require('../database'); const eloService = require('../services/eloService'); // Get Elo ratings for all players in a game router.get('/games/:gameId/players', async (req, res) => { try { const gameId = req.params.gameId; // Get game details const gameResult = await db.query( 'SELECT id, name FROM games WHERE id = ?', [gameId] ); if (!gameResult.length) { return res.status(404).json({ error: 'Game not found' }); } const game = gameResult[0]; // Get player Elo rankings const rankings = await eloService.getPlayerEloRankings(gameId); res.json({ game, rankings }); } catch (error) { console.error(`Error fetching player Elo rankings for game ${req.params.gameId}:`, error); res.status(500).json({ error: 'Failed to retrieve Elo rankings' }); } }); // Get Elo ratings for all player names in a game router.get('/games/:gameId/models', async (req, res) => { try { const gameId = req.params.gameId; // Get game details const gameResult = await db.query( 'SELECT id, name FROM games WHERE id = ?', [gameId] ); if (!gameResult.length) { return res.status(404).json({ error: 'Game not found' }); } const game = gameResult[0]; // Get player name Elo rankings const rankings = await eloService.getPlayerNameEloRankings(gameId); res.json({ game, rankings }); } catch (error) { console.error(`Error fetching player name Elo rankings for game ${req.params.gameId}:`, error); res.status(500).json({ error: 'Failed to retrieve Elo rankings' }); } }); // Get individual player's Elo history router.get('/players/:playerId/history', async (req, res) => { try { const playerId = req.params.playerId; // Get player info const playerResult = await db.query( 'SELECT id, name, model FROM players WHERE id = ?', [playerId] ); if (!playerResult.length) { return res.status(404).json({ error: 'Player not found' }); } const player = playerResult[0]; // Get player's Elo history across all games const history = await db.query(` SELECT eh.*, g.name as game_name, m.created_at as match_date FROM elo_history eh JOIN games g ON eh.game_id = g.id JOIN matches m ON eh.match_id = m.id WHERE eh.player_id = ? ORDER BY eh.created_at DESC LIMIT 100 `, [playerId]); // Get player's current Elo ratings for all games const currentRatings = await db.query(` SELECT pge.*, g.name as game_name FROM player_game_elo pge JOIN games g ON pge.game_id = g.id WHERE pge.player_id = ? ORDER BY pge.rating DESC `, [playerId]); res.json({ player, currentRatings, history }); } catch (error) { console.error(`Error fetching Elo history for player ${req.params.playerId}:`, error); res.status(500).json({ error: 'Failed to retrieve Elo history' }); } }); // Get individual player name's Elo history router.get('/playernames/:playerName/history', async (req, res) => { try { const playerName = req.params.playerName; // Get player name's Elo history across all games const history = await db.query(` SELECT pneh.*, g.name as game_name, m.created_at as match_date FROM player_name_elo_history pneh JOIN games g ON pneh.game_id = g.id JOIN matches m ON pneh.match_id = m.id WHERE pneh.player_name = ? ORDER BY pneh.created_at DESC LIMIT 100 `, [playerName]); // Get player name's current Elo ratings for all games const currentRatings = await db.query(` SELECT pne.*, g.name as game_name FROM player_name_elo pne JOIN games g ON pne.game_id = g.id WHERE pne.player_name = ? ORDER BY pne.rating DESC `, [playerName]); // Get count of players using this name const playerCount = await db.query(` SELECT COUNT(*) as count FROM players WHERE name = ? `, [playerName]); res.json({ playerName, playerCount: playerCount[0].count, currentRatings, history }); } catch (error) { console.error(`Error fetching Elo history for player name ${req.params.playerName}:`, error); res.status(500).json({ error: 'Failed to retrieve Elo history' }); } }); // Process Elo for a completed match router.post('/matches/:matchId/process', async (req, res) => { try { const matchId = req.params.matchId; // Process Elo ratings for the match const result = await eloService.processMatchElo(matchId); res.json(result); } catch (error) { console.error(`Error processing Elo for match ${req.params.matchId}:`, error); res.status(500).json({ error: error.message }); } }); module.exports = router; ``` ### ./llm-olympics/server/routes/games.js ```js const express = require('express'); const router = express.Router(); const db = require('../database'); // Get all games router.get('/', async (req, res) => { try { const games = await db.getGames(); res.json(games); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get game by ID router.get('/:id', async (req, res) => { try { const game = await db.getGame(req.params.id); if (!game) { return res.status(404).json({ error: 'Game not found' }); } res.json(game); } catch (error) { res.status(500).json({ error: error.message }); } }); // Create a new game router.post('/', async (req, res) => { try { const { name, description, rules, parameters } = req.body; // Validate input if (!name || !description) { return res.status(400).json({ error: 'Name and description are required' }); } // Insert new game const result = await db.query( 'INSERT INTO games (name, description, rules, parameters, active, created_at) VALUES (?, ?, ?, ?, 1, NOW())', [name, description, rules || '', JSON.stringify(parameters || {})] ); res.status(201).json({ id: result.insertId }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Clear all data for a specific game (admin action) router.delete('/:id/data', async (req, res) => { try { const gameId = req.params.id; const resetElo = req.query.resetElo === 'true'; // Optional parameter to reset Elo ratings // Validate that the game exists const game = await db.getGame(gameId); if (!game) { return res.status(404).json({ error: 'Game not found' }); } console.log(`[ADMIN] Clearing all data for game: ${game.name} (ID: ${gameId}), Reset Elo: ${resetElo}`); // 1. Get all matches for this game const matches = await db.query( 'SELECT id FROM matches WHERE game_id = ?', [gameId] ); if (matches.length === 0 && !resetElo) { return res.json({ message: `No data found for game: ${game.name}`, matchesDeleted: 0, movesDeleted: 0, eloReset: false }); } const matchIds = matches.map(match => match.id); console.log(`[ADMIN] Found ${matchIds.length} matches to delete`); let eloHistoryDeleted = 0; let modelEloHistoryDeleted = 0; let playerEloReset = 0; let modelEloReset = 0; // If resetting Elo ratings, handle that first if (resetElo) { // 5a. Delete Elo history records for this game's matches if (matchIds.length > 0) { // Process in batches const BATCH_SIZE = 50; for (let i = 0; i < matchIds.length; i += BATCH_SIZE) { const batch = matchIds.slice(i, i + BATCH_SIZE); const placeholders = batch.map(() => '?').join(','); // Delete from elo_history try { const eloHistoryResult = await db.query( `DELETE FROM elo_history WHERE match_id IN (${placeholders})`, batch ); eloHistoryDeleted += eloHistoryResult.affectedRows; } catch (e) { console.warn('Error deleting from elo_history, table might not exist:', e.message); } // Try to delete from model_elo_history (might not exist) try { const modelEloHistoryResult = await db.query( `DELETE FROM model_elo_history WHERE match_id IN (${placeholders})`, batch ); modelEloHistoryDeleted += modelEloHistoryResult.affectedRows; } catch (e) { console.warn('Error deleting from model_elo_history, table might not exist:', e.message); } // Try to delete from player_name_elo_history (might not exist) try { const playerNameEloHistoryResult = await db.query( `DELETE FROM player_name_elo_history WHERE match_id IN (${placeholders})`, batch ); modelEloHistoryDeleted += playerNameEloHistoryResult.affectedRows; } catch (e) { console.warn('Error deleting from player_name_elo_history, table might not exist:', e.message); } } } // 5b. Reset all player Elo ratings for this game to default (1200) try { const playerEloResult = await db.query( 'UPDATE player_game_elo SET rating = 1200, games_played = 0 WHERE game_id = ?', [gameId] ); playerEloReset = playerEloResult.affectedRows; } catch (e) { console.warn('Error resetting player_game_elo, table might not exist:', e.message); } // 5c. Reset all model Elo ratings try { const modelEloResult = await db.query( 'UPDATE model_elo SET rating = 1200, games_played = 0 WHERE game_id = ?', [gameId] ); modelEloReset = modelEloResult.affectedRows; } catch (e) { console.warn('Error resetting model_elo, table might not exist:', e.message); } // 5d. Reset player_name_elo if it exists try { const playerNameEloResult = await db.query( 'UPDATE player_name_elo SET rating = 1200, games_played = 0 WHERE game_id = ?', [gameId] ); modelEloReset += playerNameEloResult.affectedRows; } catch (e) { console.warn('Error resetting player_name_elo, table might not exist:', e.message); } } // Only delete match data if there are matches let totalMovesDeleted = 0; let totalPlayersDeleted = 0; let matchesDeleted = 0; if (matchIds.length > 0) { // 2. Delete all moves for these matches - process in batches if needed // Process deletion in chunks if there are many matches const BATCH_SIZE = 50; for (let i = 0; i < matchIds.length; i += BATCH_SIZE) { const batch = matchIds.slice(i, i + BATCH_SIZE); const placeholders = batch.map(() => '?').join(','); const batchResult = await db.query( `DELETE FROM moves WHERE match_id IN (${placeholders})`, batch ); totalMovesDeleted += batchResult.affectedRows; console.log(`[ADMIN] Deleted ${batchResult.affectedRows} moves in batch ${i/BATCH_SIZE + 1}`); } // 3. Delete all match_players entries - also in batches for (let i = 0; i < matchIds.length; i += BATCH_SIZE) { const batch = matchIds.slice(i, i + BATCH_SIZE); const placeholders = batch.map(() => '?').join(','); const batchResult = await db.query( `DELETE FROM match_players WHERE match_id IN (${placeholders})`, batch ); totalPlayersDeleted += batchResult.affectedRows; } // 4. Delete all matches const matchesResult = await db.query( 'DELETE FROM matches WHERE game_id = ?', [gameId] ); matchesDeleted = matchesResult.affectedRows; } res.json({ message: `Successfully cleared data for game: ${game.name}`, matchesDeleted, movesDeleted: totalMovesDeleted, playerEntriesDeleted: totalPlayersDeleted, eloReset: resetElo, eloStats: resetElo ? { eloHistoryDeleted, modelEloHistoryDeleted, playerEloReset, modelEloReset } : null }); } catch (error) { console.error('Error clearing game data:', error); res.status(500).json({ error: error.message }); } }); module.exports = router; ``` ### ./llm-olympics/server/routes/matches.js ```js const express = require('express'); const router = express.Router(); const db = require('../database'); const { getMatchRunner } = require('../matchRunner'); // Get all matches router.get('/', async (req, res) => { try { const matches = await db.query(` SELECT m.*, g.name as game_name FROM matches m JOIN games g ON m.game_id = g.id ORDER BY m.created_at DESC LIMIT 100 `); res.json(matches); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get match by ID router.get('/:id', async (req, res) => { try { const match = await db.getMatch(req.params.id); if (!match) { return res.status(404).json({ error: 'Match not found' }); } res.json(match); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get match history router.get('/:id/history', async (req, res) => { try { const match = await db.query( 'SELECT history FROM matches WHERE id = ?', [req.params.id] ); if (match.length === 0) { return res.status(404).json({ error: 'Match not found' }); } // Handle null or undefined history if (!match[0].history) { console.log(`No history found for match ${req.params.id}`); return res.json([]); } // Handle different types of history data let history = []; try { if (typeof match[0].history === 'string') { history = JSON.parse(match[0].history); } else { history = match[0].history; } } catch (parseError) { console.error(`Error parsing history for match ${req.params.id}:`, parseError); console.log('Raw history data:', match[0].history); return res.json([]); } res.json(history); } catch (error) { console.error(`Error getting history for match ${req.params.id}:`, error); res.status(500).json({ error: error.message }); } }); // Create a new match router.post('/', async (req, res) => { try { const { gameId, players, parameters, randomize = true } = req.body; // Validate input if (!gameId || !players || !Array.isArray(players)) { return res.status(400).json({ error: 'Invalid request data' }); } // Filter out any undefined players let validPlayers = players.filter(p => p !== undefined && p !== null); if (validPlayers.length === 0) { return res.status(400).json({ error: 'At least one valid player is required' }); } // Randomize player order if requested if (randomize) { validPlayers = validPlayers.sort(() => Math.random() - 0.5); } // Create match with possibly randomized players const matchId = await db.createMatch(gameId, validPlayers, parameters); res.status(201).json({ id: matchId }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Start a match router.post('/:id/start', async (req, res) => { try { const matchId = req.params.id; // Use matchRunner from the request const matchRunner = req.matchRunner; if (!matchRunner) { return res.status(500).json({ error: 'Match runner not initialized' }); } await matchRunner.queueMatch(matchId); res.json({ message: 'Match queued for execution' }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get running matches router.get('/status/running', async (req, res) => { try { const matchRunner = req.matchRunner; if (!matchRunner) { return res.status(500).json({ error: 'Match runner not initialized' }); } const runningMatches = matchRunner.getRunningMatches(); const queuedMatches = matchRunner.getQueuedMatches(); res.json({ running: runningMatches, queued: queuedMatches }); } catch (error) { res.status(500).json({ error: error.message }); } }); module.exports = router; ``` ### ./llm-olympics/server/routes/moves.js ```js const express = require('express'); const router = express.Router(); const db = require('../database'); // Get all moves router.get('/', async (req, res) => { try { // Limit to most recent 1000 moves unless a limit is specified const limit = req.query.limit || 1000; const moves = await db.query(` SELECT m.id, m.match_id, m.player_id, m.turn_number, m.move_type, m.move_data, SUBSTRING(m.reasoning, 1, 100) as reasoning_preview, m.created_at, p.name as player_name, g.name as game_name FROM moves m LEFT JOIN players p ON m.player_id = p.id LEFT JOIN games g ON m.game_id = g.id ORDER BY m.created_at DESC LIMIT ? `, [parseInt(limit)]); res.json(moves); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get a specific move by ID with full details router.get('/:id', async (req, res) => { try { const move = await db.getMoveById(req.params.id); if (!move) { return res.status(404).json({ error: 'Move not found' }); } // Parse JSON fields if (move.coordinates && typeof move.coordinates === 'string') { try { move.coordinates = JSON.parse(move.coordinates); } catch (e) { console.error('Error parsing move coordinates:', e); } } if (move.full_state && typeof move.full_state === 'string') { try { move.full_state = JSON.parse(move.full_state); } catch (e) { console.error('Error parsing move state:', e); } } res.json(move); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get moves for a specific match router.get('/match/:id', async (req, res) => { try { const moves = await db.getMatchMoves(req.params.id); // If include_details is true, also get the full content const includeDetails = req.query.include_details === 'true'; if (!includeDetails) { // Remove large text fields to reduce response size moves.forEach(move => { if (move.reasoning) { move.reasoning = move.reasoning.substring(0, 100) + '...'; } delete move.prompt; delete move.response; delete move.full_state; }); } res.json(moves); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get the prompt and response for a specific move router.get('/:id/details', async (req, res) => { try { const move = await db.query(` SELECT id, match_id, player_id, turn_number, move_type, move_data, reasoning, prompt, response FROM moves WHERE id = ?`, [req.params.id] ); if (move.length === 0) { return res.status(404).json({ error: 'Move not found' }); } res.json(move[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get moves for a specific player router.get('/player/:id', async (req, res) => { try { const moves = await db.query(` SELECT m.id, m.match_id, m.move_type, m.move_data, SUBSTRING(m.reasoning, 1, 100) as reasoning_preview, m.turn_number, m.created_at, g.name as game_name, ma.id as match_id FROM moves m JOIN games g ON m.game_id = g.id JOIN matches ma ON m.match_id = ma.id WHERE m.player_id = ? ORDER BY m.created_at DESC LIMIT 1000 `, [req.params.id]); res.json(moves); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get moves stats for all players router.get('/stats/players', async (req, res) => { try { const stats = await db.query(` SELECT p.id as player_id, p.name as player_name, COUNT(m.id) as total_moves, COUNT(DISTINCT m.match_id) as matches_played, SUM(CASE WHEN m.move_type = 'collision' THEN 1 ELSE 0 END) as deaths, SUM(CASE WHEN m.move_type = 'eat' THEN 1 ELSE 0 END) as food_eaten FROM players p LEFT JOIN moves m ON p.id = m.player_id GROUP BY p.id, p.name ORDER BY total_moves DESC `); res.json(stats); } catch (error) { res.status(500).json({ error: error.message }); } }); module.exports = router; ``` ### ./llm-olympics/server/routes/players.js ```js const express = require('express'); const router = express.Router(); const db = require('../database'); // Get all players router.get('/', async (req, res) => { try { const players = await db.query('SELECT * FROM players'); res.json(players); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get player by ID router.get('/:id', async (req, res) => { try { const player = await db.query( 'SELECT * FROM players WHERE id = ?', [req.params.id] ); if (player.length === 0) { return res.status(404).json({ error: 'Player not found' }); } res.json(player[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); // Create a new player router.post('/', async (req, res) => { try { const { name, model, parameters } = req.body; // Validate input if (!name || !model) { return res.status(400).json({ error: 'Name and model are required' }); } // Insert new player const result = await db.query( 'INSERT INTO players (name, model, parameters, created_at) VALUES (?, ?, ?, NOW())', [name, model, JSON.stringify(parameters || {})] ); res.status(201).json({ id: result.insertId }); } catch (error) { res.status(500).json({ error: error.message }); } }); module.exports = router; ``` ### ./llm-olympics/server/routes/scheduler.js ```js const express = require('express'); const router = express.Router(); // Get scheduler status router.get('/status', (req, res) => { const scheduler = req.scheduler; if (!scheduler) { return res.status(500).json({ error: 'Scheduler not initialized' }); } res.json({ isRunning: scheduler.isRunning || false, targetMatchCount: scheduler.targetMatchCount || 10, gameWeights: scheduler.gameWeights || {}, activeMatches: req.matchRunner ? { running: req.matchRunner.runningMatches.size || 0, queued: req.matchRunner.matchQueue.length || 0 } : { running: 0, queued: 0 } }); }); // Start scheduler router.post('/start', (req, res) => { const scheduler = req.scheduler; if (!scheduler) { return res.status(500).json({ error: 'Scheduler not initialized' }); } if (scheduler.isRunning) { return res.json({ message: 'Scheduler is already running', isRunning: true }); } scheduler.start(); res.json({ message: 'Scheduler started successfully', isRunning: true }); }); // Stop scheduler router.post('/stop', (req, res) => { const scheduler = req.scheduler; if (!scheduler) { return res.status(500).json({ error: 'Scheduler not initialized' }); } if (!scheduler.isRunning) { return res.json({ message: 'Scheduler is already stopped', isRunning: false }); } scheduler.stop(); res.json({ message: 'Scheduler stopped successfully', isRunning: false }); }); // Update scheduler configuration router.put('/config', (req, res) => { const scheduler = req.scheduler; if (!scheduler) { return res.status(500).json({ error: 'Scheduler not initialized' }); } const { targetMatchCount, gameWeights } = req.body; if (targetMatchCount) { scheduler.targetMatchCount = parseInt(targetMatchCount, 10); } if (gameWeights) { scheduler.gameWeights = gameWeights; } res.json({ message: 'Scheduler configuration updated', isRunning: scheduler.isRunning || false, targetMatchCount: scheduler.targetMatchCount, gameWeights: scheduler.gameWeights }); }); module.exports = router; ``` ### ./llm-olympics/server/routes/stats.js ```js /** * Game Statistics API Routes */ const express = require('express'); const router = express.Router(); const db = require('../database'); // Get stats for all games router.get('/games', async (req, res) => { try { const games = await db.query(` SELECT g.id, g.name, COUNT(DISTINCT m.id) as total_matches, SUM(CASE WHEN m.status = 'completed' THEN 1 ELSE 0 END) as completed_matches FROM games g LEFT JOIN matches m ON g.id = m.game_id WHERE g.active = TRUE GROUP BY g.id, g.name ORDER BY g.name `); res.json({ games }); } catch (error) { console.error('Error fetching game stats:', error); res.status(500).json({ error: 'Failed to retrieve game statistics' }); } }); // Get matches for a specific player in a specific game router.get('/games/:gameId/players/:playerId/matches', async (req, res) => { try { const { gameId, playerId } = req.params; // Get player info const playerResult = await db.query( 'SELECT id, name FROM players WHERE id = ?', [playerId] ); if (!playerResult.length) { return res.status(404).json({ error: 'Player not found' }); } const player = playerResult[0]; // Get all matches for this player in this game const matches = await db.query(` SELECT m.id, m.status, m.created_at, m.completed_at, JSON_UNQUOTE(JSON_EXTRACT(m.results, '$.winner')) as winner, CASE WHEN mp.position = 1 THEN p2.name ELSE p1.name END as opponent_name, CASE WHEN m.status != 'completed' THEN 'pending' WHEN JSON_UNQUOTE(JSON_EXTRACT(m.results, '$.winner')) IS NULL THEN 'draw' WHEN JSON_UNQUOTE(JSON_EXTRACT(m.results, '$.winner')) = ? THEN 'win' ELSE 'loss' END as result FROM matches m JOIN match_players mp ON m.id = mp.match_id AND mp.player_id = ? JOIN match_players mp1 ON m.id = mp1.match_id AND mp1.position = 1 JOIN match_players mp2 ON m.id = mp2.match_id AND mp2.position = 2 JOIN players p1 ON mp1.player_id = p1.id JOIN players p2 ON mp2.player_id = p2.id WHERE m.game_id = ? ORDER BY m.created_at DESC `, [player.name, playerId, gameId]); res.json({ player, matches }); } catch (error) { console.error(`Error fetching matches for player ${req.params.playerId}:`, error); res.status(500).json({ error: 'Failed to retrieve player matches' }); } }); // Get detailed stats for a specific game router.get('/games/:gameId', async (req, res) => { try { const gameId = req.params.gameId; // Get game details const gameResult = await db.query( 'SELECT id, name, description FROM games WHERE id = ?', [gameId] ); if (!gameResult.length) { return res.status(404).json({ error: 'Game not found' }); } const game = gameResult[0]; // Get recent matches for this game const matches = await db.query(` SELECT m.id, m.status, m.created_at, m.completed_at, p1.name as player1_name, p2.name as player2_name, JSON_EXTRACT(m.results, '$.winner') as winner FROM matches m JOIN match_players mp1 ON m.id = mp1.match_id AND mp1.position = 1 JOIN match_players mp2 ON m.id = mp2.match_id AND mp2.position = 2 JOIN players p1 ON mp1.player_id = p1.id JOIN players p2 ON mp2.player_id = p2.id WHERE m.game_id = ? ORDER BY m.created_at DESC LIMIT 50 `, [gameId]); // Get model rankings for this game const rankings = await db.query(` SELECT p.id, p.name, p.model, COUNT(DISTINCT m.id) as games_played, SUM(CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(m.results, '$.winner')) = p.name THEN 1 ELSE 0 END) as wins, ROUND(SUM(CASE WHEN JSON_UNQUOTE(JSON_EXTRACT(m.results, '$.winner')) = p.name THEN 1 ELSE 0 END) / COUNT(DISTINCT m.id) * 100, 2) as win_rate FROM players p JOIN match_players mp ON p.id = mp.player_id JOIN matches m ON mp.match_id = m.id WHERE m.game_id = ? AND m.status = 'completed' GROUP BY p.id, p.name, p.model ORDER BY win_rate DESC, games_played DESC `, [gameId]); res.json({ game, matches, rankings }); } catch (error) { console.error(`Error fetching stats for game ${req.params.gameId}:`, error); res.status(500).json({ error: 'Failed to retrieve game statistics' }); } }); module.exports = router; ``` ### ./llm-olympics/server/services/eloService.js ```js /** * Elo Rating Service * * This service provides functions to manage Elo ratings for players and models * in the LLM Olympics system. */ const db = require('../database'); const elo = require('../utils/elo'); /** * Get player's Elo rating for a specific game * @param {number} playerId - ID of the player * @param {number} gameId - ID of the game * @returns {Promise} Player's Elo data for this game */ async function getPlayerGameElo(playerId, gameId) { const results = await db.query( 'SELECT * FROM player_game_elo WHERE player_id = ? AND game_id = ?', [playerId, gameId] ); if (results.length === 0) { // Create initial rating entry if not exists await db.query( 'INSERT INTO player_game_elo (player_id, game_id, rating, games_played) VALUES (?, ?, ?, ?)', [playerId, gameId, elo.DEFAULT_INITIAL_RATING, 0] ); return { player_id: playerId, game_id: gameId, rating: elo.DEFAULT_INITIAL_RATING, games_played: 0 }; } return results[0]; } /** * Get player name's Elo rating for a specific game * @param {string} playerName - Name of the player * @param {number} gameId - ID of the game * @returns {Promise} Player name's Elo data for this game */ async function getPlayerNameElo(playerName, gameId) { const results = await db.query( 'SELECT * FROM player_name_elo WHERE player_name = ? AND game_id = ?', [playerName, gameId] ); if (results.length === 0) { // Create initial rating entry if not exists await db.query( 'INSERT INTO player_name_elo (player_name, game_id, rating, games_played) VALUES (?, ?, ?, ?)', [playerName, gameId, elo.DEFAULT_INITIAL_RATING, 0] ); return { player_name: playerName, game_id: gameId, rating: elo.DEFAULT_INITIAL_RATING, games_played: 0 }; } return results[0]; } /** * Update player's Elo rating after a match * @param {number} playerId - ID of the player * @param {number} opponentId - ID of the opponent * @param {number} gameId - ID of the game * @param {number} matchId - ID of the match * @param {string} result - Match result from player's perspective: 'win', 'loss', or 'draw' * @returns {Promise} Object with old and new ratings */ async function updatePlayerElo(playerId, opponentId, gameId, matchId, result) { // Get current ratings const playerElo = await getPlayerGameElo(playerId, gameId); const opponentElo = await getPlayerGameElo(opponentId, gameId); // Calculate new ratings const { newRatingA, newRatingB } = elo.updateRatings( playerElo.rating, opponentElo.rating, result ); // Update player rating await db.query( 'UPDATE player_game_elo SET rating = ?, games_played = games_played + 1 WHERE player_id = ? AND game_id = ?', [newRatingA, playerId, gameId] ); // Update opponent rating await db.query( 'UPDATE player_game_elo SET rating = ?, games_played = games_played + 1 WHERE player_id = ? AND game_id = ?', [newRatingB, opponentId, gameId] ); // Record player rating history await db.query( 'INSERT INTO elo_history (match_id, game_id, player_id, old_rating, new_rating, rating_change) VALUES (?, ?, ?, ?, ?, ?)', [matchId, gameId, playerId, playerElo.rating, newRatingA, newRatingA - playerElo.rating] ); // Record opponent rating history await db.query( 'INSERT INTO elo_history (match_id, game_id, player_id, old_rating, new_rating, rating_change) VALUES (?, ?, ?, ?, ?, ?)', [matchId, gameId, opponentId, opponentElo.rating, newRatingB, newRatingB - opponentElo.rating] ); return { player: { oldRating: playerElo.rating, newRating: newRatingA, change: newRatingA - playerElo.rating }, opponent: { oldRating: opponentElo.rating, newRating: newRatingB, change: newRatingB - opponentElo.rating } }; } /** * Update player name's Elo rating after a match * @param {string} playerName - Name of the player * @param {string} opponentName - Name of the opponent * @param {number} gameId - ID of the game * @param {number} matchId - ID of the match * @param {string} result - Match result from player's perspective: 'win', 'loss', or 'draw' * @returns {Promise} Object with old and new ratings */ async function updatePlayerNameElo(playerName, opponentName, gameId, matchId, result) { // Skip if both player names are the same if (playerName === opponentName) { return null; } // Get current ratings const playerNameElo = await getPlayerNameElo(playerName, gameId); const opponentNameElo = await getPlayerNameElo(opponentName, gameId); // Calculate new ratings const { newRatingA, newRatingB } = elo.updateRatings( playerNameElo.rating, opponentNameElo.rating, result ); // Update player name rating await db.query( 'UPDATE player_name_elo SET rating = ?, games_played = games_played + 1 WHERE player_name = ? AND game_id = ?', [newRatingA, playerName, gameId] ); // Update opponent name rating await db.query( 'UPDATE player_name_elo SET rating = ?, games_played = games_played + 1 WHERE player_name = ? AND game_id = ?', [newRatingB, opponentName, gameId] ); // Record player name rating history await db.query( 'INSERT INTO player_name_elo_history (match_id, game_id, player_name, old_rating, new_rating, rating_change) VALUES (?, ?, ?, ?, ?, ?)', [matchId, gameId, playerName, playerNameElo.rating, newRatingA, newRatingA - playerNameElo.rating] ); // Record opponent name rating history await db.query( 'INSERT INTO player_name_elo_history (match_id, game_id, player_name, old_rating, new_rating, rating_change) VALUES (?, ?, ?, ?, ?, ?)', [matchId, gameId, opponentName, opponentNameElo.rating, newRatingB, newRatingB - opponentNameElo.rating] ); return { playerName: { oldRating: playerNameElo.rating, newRating: newRatingA, change: newRatingA - playerNameElo.rating }, opponentName: { oldRating: opponentNameElo.rating, newRating: newRatingB, change: newRatingB - opponentNameElo.rating } }; } /** * Process Elo updates for a completed match * @param {number} matchId - ID of the completed match * @returns {Promise} Object with updated ratings */ async function processMatchElo(matchId) { try { // Get match details const match = await db.getMatch(matchId); if (!match || match.status !== 'completed') { throw new Error(`Match ${matchId} is not completed`); } if (match.players.length !== 2) { throw new Error(`Match ${matchId} does not have exactly 2 players`); } // Parse match results const results = typeof match.results === 'string' ? JSON.parse(match.results) : match.results; // Determine winner and loser let result = 'draw'; if (results && results.winner) { if (results.winner === match.players[0].player_name) { result = 'win'; } else if (results.winner === match.players[1].player_name) { result = 'loss'; } } // Get player info const player1Id = match.players[0].player_id; const player2Id = match.players[1].player_id; const player1Name = match.players[0].player_name; const player2Name = match.players[1].player_name; // Update player Elo ratings (based on player IDs) const playerEloUpdate = await updatePlayerElo( player1Id, player2Id, match.game_id, matchId, result ); // Update player name Elo ratings const playerNameEloUpdate = await updatePlayerNameElo( player1Name, player2Name, match.game_id, matchId, result ); return { matchId, gameId: match.game_id, result, playerEloUpdate, playerNameEloUpdate }; } catch (error) { console.error(`Error processing Elo for match ${matchId}:`, error); throw error; } } /** * Get Elo rankings for all players in a game * @param {number} gameId - ID of the game * @param {number} limit - Maximum number of players to return * @returns {Promise} Array of player rankings */ async function getPlayerEloRankings(gameId, limit = 20) { // MySQL prepared statements require LIMIT to be an integer const limitNum = parseInt(limit, 10); return db.query(` SELECT pge.*, p.name as player_name, p.model as player_model FROM player_game_elo pge JOIN players p ON pge.player_id = p.id WHERE pge.game_id = ? ORDER BY pge.rating DESC LIMIT ${limitNum} `, [gameId]); // Remove limit from parameters array and use directly in query } /** * Get Elo rankings for all player names in a game * @param {number} gameId - ID of the game * @param {number} limit - Maximum number of player names to return * @returns {Promise} Array of player name rankings */ async function getPlayerNameEloRankings(gameId, limit = 20) { // MySQL prepared statements require LIMIT to be an integer const limitNum = parseInt(limit, 10); return db.query(` SELECT pne.*, (SELECT COUNT(DISTINCT id) FROM players WHERE name = pne.player_name) as player_count FROM player_name_elo pne WHERE pne.game_id = ? ORDER BY pne.rating DESC LIMIT ${limitNum} `, [gameId]); } module.exports = { getPlayerGameElo, getPlayerNameElo, updatePlayerElo, updatePlayerNameElo, processMatchElo, getPlayerEloRankings, getPlayerNameEloRankings }; ``` ### ./llm-olympics/server/services/matchScheduler.js ```js /** * Background Match Scheduler * Maintains a pool of active AI vs AI matches running concurrently */ const db = require('../database'); const logger = require('../utils/logger'); const { getMatchRunner } = require('../matchRunner'); const { v4: uuidv4 } = require('uuid'); class MatchScheduler { constructor(config = {}) { // Configuration this.targetMatchCount = config.targetMatchCount || 10; this.checkInterval = config.checkInterval || 60000; // 1 minute this.gameWeights = config.gameWeights || null; // For weighted game selection this.maxMatchesPerPair = config.maxMatchesPerPair || 3; // Maximum matches between same model pair per game this.stalledMatchTimeout = config.stalledMatchTimeout || 30 * 60 * 1000; // 30 minutes in ms // Internal state this.isRunning = false; this.schedulerInterval = null; this.activeSchedulerPromise = null; } /** * Start the scheduler service */ start() { if (this.isRunning) { logger.info('Match scheduler is already running'); return; } logger.info(`Starting match scheduler with target of ${this.targetMatchCount} concurrent matches`); this.isRunning = true; // Check for stalled matches first this.cleanupStalledMatches().then(() => { // Then proceed with regular scheduling this.checkAndCreateMatches(); // Setup interval for subsequent runs this.schedulerInterval = setInterval(() => { this.checkAndCreateMatches(); }, this.checkInterval); }).catch(error => { logger.error('Error during stalled match cleanup:', error); // Continue with scheduler even if cleanup fails this.checkAndCreateMatches(); this.schedulerInterval = setInterval(() => { this.checkAndCreateMatches(); }, this.checkInterval); }); } /** * Stop the scheduler service */ stop() { if (!this.isRunning) return; logger.info('Stopping match scheduler'); clearInterval(this.schedulerInterval); this.isRunning = false; } /** * Find and mark stalled matches as failed */ async cleanupStalledMatches() { logger.info('Checking for stalled matches...'); try { // Find matches that are in running or queued state but haven't completed in a long time const stalledMatches = await db.query(` SELECT id, game_id, status, created_at FROM matches WHERE status IN ('running', 'queued') AND created_at < DATE_SUB(NOW(), INTERVAL ? SECOND) `, [this.stalledMatchTimeout / 1000]); if (stalledMatches.length === 0) { logger.info('No stalled matches found based on creation time'); } else { logger.warn(`Found ${stalledMatches.length} stalled matches based on creation time, marking as error`); // Mark stalled matches as error for (const match of stalledMatches) { await db.query(` UPDATE matches SET status = 'error', results = JSON_OBJECT('error', 'Match timed out or stalled') WHERE id = ? `, [match.id]); logger.info(`Marked stalled match ${match.id} as error (exceeded maximum duration)`); } } // Find matches with no moves in the last 10 minutes const noRecentMoveMatches = await db.query(` SELECT m.id, m.game_id, m.status FROM matches m WHERE m.status = 'running' AND NOT EXISTS ( SELECT 1 FROM moves WHERE match_id = m.id AND created_at > DATE_SUB(NOW(), INTERVAL 10 MINUTE) ) AND EXISTS ( SELECT 1 FROM moves WHERE match_id = m.id ) `); if (noRecentMoveMatches.length === 0) { logger.info('No matches found with inactive moves'); } else { logger.warn(`Found ${noRecentMoveMatches.length} matches with no moves in the last 10 minutes, marking as error`); // Mark inactive matches as error for (const match of noRecentMoveMatches) { await db.query(` UPDATE matches SET status = 'error', results = JSON_OBJECT('error', 'Match inactive - no moves in last 10 minutes') WHERE id = ? `, [match.id]); logger.info(`Marked inactive match ${match.id} as error (no recent moves)`); } } } catch (error) { logger.error('Error checking for stalled/inactive matches:', error); } } /** * Main scheduler logic - checks active matches and creates new ones if needed */ async checkAndCreateMatches() { // If there's already a check running, don't start another if (this.activeSchedulerPromise) { logger.debug('Scheduler check already in progress, skipping'); return; } try { this.activeSchedulerPromise = this._performSchedulerCheck(); await this.activeSchedulerPromise; } catch (error) { logger.error('Error in match scheduler:', error); } finally { this.activeSchedulerPromise = null; } } /** * Internal method to perform the actual scheduler check */ async _performSchedulerCheck() { // Get the matchRunner instance const matchRunner = getMatchRunner(); if (!matchRunner) { logger.error('Match runner not initialized'); return; } // 1. Count active matches (running + queued) const runningCount = matchRunner.runningMatches.size; const queuedCount = matchRunner.matchQueue.length; const activeMatches = runningCount + queuedCount; logger.info(`Active matches: ${activeMatches}/${this.targetMatchCount} (${runningCount} running, ${queuedCount} queued)`); // 2. Calculate how many new matches needed const neededMatches = Math.max(0, this.targetMatchCount - activeMatches); if (neededMatches === 0) { logger.debug('No new matches needed'); return; } // 3. Create needed matches logger.info(`Creating ${neededMatches} new matches`); for (let i = 0; i < neededMatches; i++) { try { // Generate match configuration const matchConfig = await this._generateMatchConfiguration(); if (!matchConfig) { logger.error('Failed to generate valid match configuration'); continue; } // Create the match in database and queue it const matchId = await this._createMatch(matchConfig, matchRunner); logger.info(`Created new match ${matchId} - Game ID ${matchConfig.gameId} with ${matchConfig.players.join(' vs ')}`); // Small delay between match creation to prevent resource spikes if (i < neededMatches - 1) { await new Promise(resolve => setTimeout(resolve, 1000)); } } catch (error) { logger.error('Error creating match:', error); } } } /** * Generate a balanced configuration for a new match */ async _generateMatchConfiguration() { try { // 1. Get available game types const games = await db.query( 'SELECT id, name FROM games WHERE active = TRUE' ); if (!games || games.length === 0) { logger.error('No active games found'); return null; } // 2. Get available AI models const models = await db.query( 'SELECT id, name, model FROM players WHERE active = TRUE' ); if (!models || models.length < 2) { logger.error('Not enough active AI models'); return null; } // 3. Select a game type (either random or weighted) let selectedGame; if (this.gameWeights) { // Use weighted selection if weights are configured selectedGame = this._weightedGameSelection(games); } else { // Random selection selectedGame = games[Math.floor(Math.random() * games.length)]; } // 4. Get match count for all model pairs in the selected game const pairMatchCounts = await db.query(` SELECT mp1.player_id as player1_id, mp2.player_id as player2_id, COUNT(*) as match_count FROM matches m JOIN match_players mp1 ON m.id = mp1.match_id AND mp1.position = 1 JOIN match_players mp2 ON m.id = mp2.match_id AND mp2.position = 2 WHERE m.game_id = ? GROUP BY mp1.player_id, mp2.player_id `, [selectedGame.id]); // Create a map for easy lookup const pairCountMap = new Map(); pairMatchCounts.forEach(pair => { const key = `${pair.player1_id}-${pair.player2_id}`; pairCountMap.set(key, pair.match_count); }); // 5. Find valid model pairs that haven't reached the maximum match count const validPairs = []; for (let i = 0; i < models.length; i++) { for (let j = i + 1; j < models.length; j++) { const player1 = models[i]; const player2 = models[j]; // Check match count for this pair const key = `${player1.id}-${player2.id}`; const reverseKey = `${player2.id}-${player1.id}`; const count = (pairCountMap.get(key) || 0) + (pairCountMap.get(reverseKey) || 0); if (count < this.maxMatchesPerPair) { validPairs.push([player1, player2]); } } } if (validPairs.length === 0) { logger.warn(`No valid model pairs found for game ${selectedGame.name} - all pairs have reached the maximum of ${this.maxMatchesPerPair} matches`); return null; } // 6. Select a random valid pair const selectedPair = validPairs[Math.floor(Math.random() * validPairs.length)]; // 7. Create the match configuration return { gameId: selectedGame.id, gameName: selectedGame.name, players: selectedPair.map(model => model.name), playerIds: selectedPair.map(model => model.id) }; } catch (error) { logger.error('Error generating match configuration:', error); throw error; } } /** * Create a new match in the database and enqueue it for execution */ async _createMatch(config, matchRunner) { try { // 1. Insert into matches table const matchResult = await db.query( 'INSERT INTO matches (game_id, status, created_at) VALUES (?, ?, NOW())', [config.gameId, 'pending'] ); const matchId = matchResult.insertId; try { // 2. Insert player records into match_players table // First player (position 1) await db.query( 'INSERT INTO match_players (match_id, player_id, position) VALUES (?, ?, ?)', [matchId, config.playerIds[0], 1] ); // Second player (position 2) await db.query( 'INSERT INTO match_players (match_id, player_id, position) VALUES (?, ?, ?)', [matchId, config.playerIds[1], 2] ); // 3. Queue the match for execution await matchRunner.queueMatch(matchId); return matchId; } catch (error) { // If there's an error with player insertion, try to clean up the match logger.error(`Error inserting players for match ${matchId}, attempting cleanup`, error); try { await db.query('DELETE FROM matches WHERE id = ?', [matchId]); } catch (cleanupError) { logger.error(`Failed to clean up match ${matchId}`, cleanupError); } throw error; } } catch (error) { logger.error('Error creating match in database:', error); throw error; } } /** * Select a game based on configured weights */ _weightedGameSelection(games) { if (!this.gameWeights) { return games[Math.floor(Math.random() * games.length)]; } // Get weights for available games const gameEntries = games.map(game => ({ game: game, weight: this.gameWeights[game.name] || 1 })); // Calculate total weight const totalWeight = gameEntries.reduce((sum, entry) => sum + entry.weight, 0); // Random selection based on weight let random = Math.random() * totalWeight; for (const entry of gameEntries) { random -= entry.weight; if (random <= 0) { return entry.game; } } // Fallback return games[0]; } /** * Fisher-Yates shuffle algorithm */ _shuffleArray(array) { const result = [...array]; for (let i = result.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [result[i], result[j]] = [result[j], result[i]]; } return result; } } module.exports = MatchScheduler; ``` ### ./llm-olympics/server/socket.js ```js const socketIO = require('socket.io'); function initSocketIO(server) { const io = socketIO(server, { cors: { origin: "*", // Allow all origins methods: ["GET", "POST"], credentials: true }, transports: ['websocket', 'polling'], pingTimeout: 60000, pingInterval: 25000 }); io.on('connection', (socket) => { console.log(`Client connected: ${socket.id}`); // Join match room to receive updates socket.on('join-match', (matchId) => { socket.join(`match-${matchId}`); console.log(`Client ${socket.id} joined match-${matchId}`); }); // Leave match room socket.on('leave-match', (matchId) => { socket.leave(`match-${matchId}`); console.log(`Client ${socket.id} left match-${matchId}`); }); // Handle disconnection socket.on('disconnect', (reason) => { console.log(`Client disconnected: ${socket.id}, reason: ${reason}`); }); }); return io; } function emitMatchUpdate(io, matchId, updateType, data) { io.to(`match-${matchId}`).emit('match-update', { type: updateType, matchId, data }); } module.exports = { initSocketIO, emitMatchUpdate }; ``` ### ./llm-olympics/server/utils.js ```js /** * Utility functions for the server */ /** * Deep copy an object * @param {*} obj The object to copy * @returns A deep copy of the object */ function deepCopy(obj) { return JSON.parse(JSON.stringify(obj)); } module.exports = { deepCopy }; ``` ### ./llm-olympics/server/utils/elo.js ```js /** * Elo Rating System Utilities * * This file provides functions to calculate Elo ratings for players and models * across different games in the LLM Olympics platform. */ // Default K-factor (determines how much ratings change after each game) const DEFAULT_K_FACTOR = 32; // Default initial rating for new players/models const DEFAULT_INITIAL_RATING = 1200; /** * Calculate the expected score (win probability) for a player * @param {number} ratingA - Rating of player A * @param {number} ratingB - Rating of player B * @returns {number} Expected score between 0 and 1 */ function calculateExpectedScore(ratingA, ratingB) { return 1 / (1 + Math.pow(10, (ratingB - ratingA) / 400)); } /** * Calculate new Elo rating * @param {number} currentRating - Current rating * @param {number} expectedScore - Expected score (win probability) * @param {number} actualScore - Actual score (1 for win, 0.5 for draw, 0 for loss) * @param {number} kFactor - K-factor that determines rating change magnitude * @returns {number} New rating */ function calculateNewRating(currentRating, expectedScore, actualScore, kFactor = DEFAULT_K_FACTOR) { return Math.round(currentRating + kFactor * (actualScore - expectedScore)); } /** * Convert match result to score * @param {string} result - Result: 'win', 'loss', or 'draw' * @returns {number} Score (1 for win, 0.5 for draw, 0 for loss) */ function resultToScore(result) { switch(result) { case 'win': return 1; case 'draw': return 0.5; case 'loss': return 0; default: return 0; } } /** * Update Elo ratings for both players in a match * @param {number} ratingA - Rating of player A * @param {number} ratingB - Rating of player B * @param {string} result - Result from player A's perspective: 'win', 'loss', or 'draw' * @param {number} kFactor - K-factor that determines rating change magnitude * @returns {Object} Object containing new ratings for both players */ function updateRatings(ratingA, ratingB, result, kFactor = DEFAULT_K_FACTOR) { const expectedScoreA = calculateExpectedScore(ratingA, ratingB); const expectedScoreB = calculateExpectedScore(ratingB, ratingA); const actualScoreA = resultToScore(result); const actualScoreB = resultToScore(result === 'win' ? 'loss' : result === 'loss' ? 'win' : 'draw'); return { newRatingA: calculateNewRating(ratingA, expectedScoreA, actualScoreA, kFactor), newRatingB: calculateNewRating(ratingB, expectedScoreB, actualScoreB, kFactor) }; } module.exports = { DEFAULT_K_FACTOR, DEFAULT_INITIAL_RATING, calculateExpectedScore, calculateNewRating, resultToScore, updateRatings }; ``` ### ./llm-olympics/server/utils/elo_tables.sql ```sql -- Elo ratings tables for LLM Olympics -- Table for per-game player Elo ratings CREATE TABLE IF NOT EXISTS player_game_elo ( id INT AUTO_INCREMENT PRIMARY KEY, player_id INT NOT NULL, game_id INT NOT NULL, rating INT NOT NULL DEFAULT 1200, games_played INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY player_game_unique (player_id, game_id), FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE, FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE ); -- Table for per-model Elo ratings CREATE TABLE IF NOT EXISTS model_elo ( id INT AUTO_INCREMENT PRIMARY KEY, model VARCHAR(255) NOT NULL, game_id INT NOT NULL, rating INT NOT NULL DEFAULT 1200, games_played INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY model_game_unique (model, game_id), FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE ); -- Table for Elo rating history to track changes over time CREATE TABLE IF NOT EXISTS elo_history ( id INT AUTO_INCREMENT PRIMARY KEY, match_id INT NOT NULL, game_id INT NOT NULL, player_id INT NOT NULL, old_rating INT NOT NULL, new_rating INT NOT NULL, rating_change INT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE, FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE, FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE ); -- Table for model Elo rating history CREATE TABLE IF NOT EXISTS model_elo_history ( id INT AUTO_INCREMENT PRIMARY KEY, match_id INT NOT NULL, game_id INT NOT NULL, model VARCHAR(255) NOT NULL, old_rating INT NOT NULL, new_rating INT NOT NULL, rating_change INT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE, FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE ); ``` ### ./llm-olympics/server/utils/elo_tables_updated.sql ```sql -- Updated Elo ratings tables for LLM Olympics -- Table for per-game player Elo ratings CREATE TABLE IF NOT EXISTS player_game_elo ( id INT AUTO_INCREMENT PRIMARY KEY, player_id INT NOT NULL, game_id INT NOT NULL, rating INT NOT NULL DEFAULT 1200, games_played INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY player_game_unique (player_id, game_id), FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE, FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE ); -- Table for per-player name Elo ratings (instead of model) CREATE TABLE IF NOT EXISTS player_name_elo ( id INT AUTO_INCREMENT PRIMARY KEY, player_name VARCHAR(255) NOT NULL, game_id INT NOT NULL, rating INT NOT NULL DEFAULT 1200, games_played INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY player_name_game_unique (player_name, game_id), FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE ); -- Table for Elo rating history to track changes over time CREATE TABLE IF NOT EXISTS elo_history ( id INT AUTO_INCREMENT PRIMARY KEY, match_id INT NOT NULL, game_id INT NOT NULL, player_id INT NOT NULL, old_rating INT NOT NULL, new_rating INT NOT NULL, rating_change INT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE, FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE, FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE ); -- Table for player name Elo rating history CREATE TABLE IF NOT EXISTS player_name_elo_history ( id INT AUTO_INCREMENT PRIMARY KEY, match_id INT NOT NULL, game_id INT NOT NULL, player_name VARCHAR(255) NOT NULL, old_rating INT NOT NULL, new_rating INT NOT NULL, rating_change INT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE, FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE ); -- Drop old model-based tables (since we're not migrating data) DROP TABLE IF EXISTS model_elo; DROP TABLE IF EXISTS model_elo_history; ``` ### ./llm-olympics/server/utils/logger.js ```js /** * Simple logger utility */ const fs = require('fs'); const path = require('path'); // Create logs directory if it doesn't exist const logsDir = path.join(__dirname, '..', 'logs'); if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir, { recursive: true }); } // Log file paths const infoLogPath = path.join(logsDir, 'scheduler-info.log'); const errorLogPath = path.join(logsDir, 'scheduler-error.log'); // Stream writers const infoStream = fs.createWriteStream(infoLogPath, { flags: 'a' }); const errorStream = fs.createWriteStream(errorLogPath, { flags: 'a' }); /** * Format log message with timestamp */ function formatMessage(message) { const timestamp = new Date().toISOString(); return `[${timestamp}] ${message}\n`; } /** * Logger implementation */ const logger = { debug: (message) => { if (process.env.NODE_ENV !== 'production') { console.log(`[DEBUG] ${message}`); } }, info: (message) => { console.log(`[INFO] ${message}`); infoStream.write(formatMessage(`[INFO] ${message}`)); }, warn: (message) => { console.warn(`[WARN] ${message}`); infoStream.write(formatMessage(`[WARN] ${message}`)); }, error: (message, error) => { const errorMessage = error ? `${message} ${error.stack || error}` : message; console.error(`[ERROR] ${errorMessage}`); errorStream.write(formatMessage(`[ERROR] ${errorMessage}`)); } }; module.exports = logger; ``` ### ./llm-olympics/start.sh File skipped (binary or too large)