# 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 (
);
};
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 (
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).
)}
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' && (
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 (
);
};
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
Rank
Model
Elo Rating
Games Played
Players Using
{modelRankings.map((model, index) => (
{index + 1}
{model.model}
{model.rating}
{model.games_played}
{model.player_count || 0}
))}
{modelRankings.length === 0 && (
No model Elo rankings available for this game yet.
)}
)}
{activeTab === 'players' && (
Player Elo Leaderboard
Individual player Elo ratings
Rank
Player
Model
Elo Rating
Games Played
{playerRankings.map((player, index) => (
{index + 1}
{player.player_name}
{player.player_model}
{player.rating}
{player.games_played}
))}
{playerRankings.length === 0 && (
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
);
};
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 (
`);
});
// 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