Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@ A horizontal bar chart ranking each model by mean words per message. Identifies

---

## Web Interface

A web-based interface is now available that allows you to run the game with any LLM from the Pollinations.ai API. Features include:

- Visual game board showing player positions
- Real-time conversation display
- Character type selection (strategist, diplomat, trickster, etc.)
- Auto-advance mode for continuous play
- Win tracking and scoreboard

To run the web interface:
1. Open `index.html` in your browser
2. Select 3 LLM players and their character types
3. Click "Start Game" to begin

The game uses the free Pollinations.ai API to simulate LLM responses.

---

## Method Summary

1. **Players & Setup**
Expand Down
92 changes: 92 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pump and Dump Game</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="game-container">
<h1>Pump and Dump Game</h1>

<div class="setup" id="setup">
<h2>Select 3 LLMs to compete:</h2>
<div class="player-setup">
<div class="player-select-group">
<label>Player 1:</label>
<select class="player-select" id="player1">
<option value="">Loading models...</option>
</select>
<select class="character-select" id="character1">
<option value="">Select character type...</option>
</select>
<div class="reasoning-toggle">
<input type="checkbox" id="reasoning1" checked>
<label for="reasoning1">Enable step-by-step reasoning</label>
</div>
</div>
<div class="player-select-group">
<label>Player 2:</label>
<select class="player-select" id="player2">
<option value="">Loading models...</option>
</select>
<select class="character-select" id="character2">
<option value="">Select character type...</option>
</select>
<div class="reasoning-toggle">
<input type="checkbox" id="reasoning2" checked>
<label for="reasoning2">Enable step-by-step reasoning</label>
</div>
</div>
<div class="player-select-group">
<label>Player 3:</label>
<select class="player-select" id="player3">
<option value="">Loading models...</option>
</select>
<select class="character-select" id="character3">
<option value="">Select character type...</option>
</select>
<div class="reasoning-toggle">
<input type="checkbox" id="reasoning3" checked>
<label for="reasoning3">Enable step-by-step reasoning</label>
</div>
</div>
</div>
<button onclick="startGame()" id="startButton">Start Game</button>
</div>

<div id="game">
<div class="status" id="status">Game starting...</div>
<div class="scoreboard" id="scoreboard">
<h3>Win Count</h3>
<div class="scores" id="scores"></div>
</div>
<div class="track">
<div class="track-label track-label-1" id="track-label-1"></div>
<div class="track-label track-label-2" id="track-label-2"></div>
<div class="track-label track-label-3" id="track-label-3"></div>
<div class="player player-1" id="player1-marker"></div>
<div class="player player-2" id="player2-marker"></div>
<div class="player player-3" id="player3-marker"></div>
</div>
<div class="conversation" id="conversation"></div>
<div class="controls">
<button onclick="nextTurn()" id="nextButton" disabled>Next Turn</button>
<label style="margin-left: 10px;">
<input type="checkbox" id="autoAdvance" checked> Auto-advance mode
</label>
</div>
<div class="player-thoughts" id="player-thoughts">
<h3>Last Thoughts</h3>
<div class="thoughts"></div>
</div>
</div>
</div>
<script src="js/constants.js"></script>
<script src="js/utils.js"></script>
<script src="js/dom.js"></script>
<script src="js/api.js"></script>
<script src="js/game.js"></script>
</body>
</html>
190 changes: 190 additions & 0 deletions js/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// const API_ENDPOINT = 'https://text.pollinations.ai';

const API_ENDPOINT = 'http://localhost:16385';

function removeThink(text, playerIdx, playerName) {
// Check for content after last </think>
const thinkMatch = text.split('</think>');
if (thinkMatch.length > 1) {
// Get everything before the last </think> tag
const thinkContent = thinkMatch.slice(0, -1).join('</think>');
console.log('%cThink content:', 'color: blue', thinkContent);

// Update the thoughts display
const thoughtsDiv = document.querySelector('#player-thoughts .thoughts');
const playerThoughtId = `player-${playerIdx + 1}-thought`;
let playerThought = document.getElementById(playerThoughtId);

if (!playerThought) {
playerThought = document.createElement('div');
playerThought.id = playerThoughtId;
playerThought.className = `thought player-${playerIdx + 1}-thought`;
thoughtsDiv.appendChild(playerThought);
}

if (thinkContent) {
playerThought.innerHTML = `<strong>${playerName}:</strong> ${thinkContent}`;
}

// Return everything after the last </think> tag
return thinkMatch[thinkMatch.length - 1].trim();
}
return text;
}

async function fetchApiResponse(url, model, messages) {
const maxRetries = 3;
const initialDelay = 1000; // Initial delay of 1 second

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Calculate exponential backoff delay
const backoffDelay = initialDelay * Math.pow(2, attempt);
await delay(backoffDelay);

const response = await fetch(url+'/openai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
messages,
temperature: 0.5,
seed: Math.floor(Math.random() * 1000000), // Generate a random seed for each request, between 0 and 999999Math.random()
referrer: "roblox"
})
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

return response;
} catch (error) {
if (attempt === maxRetries - 1) {
// If this was the last attempt, throw the error
throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
}
console.log(`Attempt ${attempt + 1} failed, retrying... Error: ${error.message}`);
}
}
}

async function getConversationResponse(playerIdx, gameState, getPlayerDisplayName) {
const playerNames = gameState.players.map(getPlayerDisplayName);

try {
const [commonPrompt, conversationPrompt] = await Promise.all([
loadPrompt('common'),
loadPrompt('conversation')
]);

const fullTemplate = commonPrompt + '\n\n' + conversationPrompt;
const prompt = replaceTemplateVariables(fullTemplate, playerNames, playerIdx, gameState);

console.log('[API Request]', `Getting conversation response for ${getPlayerDisplayName(gameState.players[playerIdx])}`, { model: gameState.players[playerIdx].model, prompt, seed: Math.random() });

try {
const response = await fetchApiResponse(API_ENDPOINT, gameState.players[playerIdx].model, [
{ role: 'user', content: prompt }
]);
if (!response.ok) {
const errorText = await response.text();
console.log('[API Error]', `Error getting conversation response for ${getPlayerDisplayName(gameState.players[playerIdx])}`, errorText);
return '<stop>';
}
const result = await response.json();
console.log('[API Debug]', `Raw API response:`, result);

// Add null check for content
const content = result?.choices?.[0]?.message?.content;
if (!content) {
console.log('[API Error]', `No content in response for ${getPlayerDisplayName(gameState.players[playerIdx])}`, result);
return '<stop>';
}

const trimmedContent = removeThink(content.trim(), playerIdx, getPlayerDisplayName(gameState.players[playerIdx]));
console.log('[API Response]', `Got conversation response for ${getPlayerDisplayName(gameState.players[playerIdx])}`, {
originalContent: content,
trimmedContent
});
return trimmedContent;
} catch (error) {
console.log('[API Error]', `Exception getting conversation response for ${getPlayerDisplayName(gameState.players[playerIdx])}`, error);
return '<stop>';
}
} catch (error) {
console.log('[API Error]', `Exception getting conversation response for ${getPlayerDisplayName(gameState.players[playerIdx])}`, error);
return '<stop>';
}
}

async function getMoveResponse(playerIdx, gameState, getPlayerDisplayName) {
const playerNames = gameState.players.map(getPlayerDisplayName);

try {
const [commonPrompt, movePrompt] = await Promise.all([
loadPrompt('common'),
loadPrompt('move')
]);

const fullTemplate = commonPrompt + '\n\n' + movePrompt;
const prompt = replaceTemplateVariables(fullTemplate, playerNames, playerIdx, gameState);

console.log('[API Request]', `Getting move response for ${getPlayerDisplayName(gameState.players[playerIdx])}`, { model: gameState.players[playerIdx].model, prompt, seed: Math.random() });

try {
const response = await fetchApiResponse(API_ENDPOINT, gameState.players[playerIdx].model, [
{ role: 'user', content: prompt }
]);
if (!response.ok) {
const errorText = await response.text();
console.log('[API Error]', `Error getting move response for ${getPlayerDisplayName(gameState.players[playerIdx])}`, errorText);
return null;
}
const result = await response.json();

// Add null check for content
const content = result?.choices?.[0]?.message?.content;
if (!content) {
console.log('[API Error]', `No content in response for ${getPlayerDisplayName(gameState.players[playerIdx])}`, result);
return null;
}

const moveText = removeThink(content.trim(), playerIdx, getPlayerDisplayName(gameState.players[playerIdx]));

console.log('[API Response]', `Got move response for ${getPlayerDisplayName(gameState.players[playerIdx])}`, { rawResponse: moveText });
return moveText;
} catch (error) {
console.log('[API Error]', `Exception getting move response for ${getPlayerDisplayName(gameState.players[playerIdx])}`, error);
return null;
}
} catch (error) {
console.log('[API Error]', `Exception getting move response for ${getPlayerDisplayName(gameState.players[playerIdx])}`, error);
return null;
}
}

async function fetchModels() {
return fetch(`${API_ENDPOINT}/models`)
.then(response => response.json());
}

function replaceTemplateVariables(template, playerNames, playerIdx, gameState) {
const characterType = gameState.players[playerIdx].characterType;
const result = template
.replace(/{{PLAYER_NAME}}/g, playerNames[playerIdx])
.replace(/{{PLAYERS_LIST}}/g, playerNames.join(', '))
.replace(/{{CHARACTER_TYPE}}/g, characterType)
.replace(/{{CHARACTER_DESCRIPTION}}/g, CHARACTER_TYPES[characterType])
.replace(/{{MAX_SUB_ROUND}}/g, MAX_SUB_ROUNDS)
.replace(/{{WIN_STEPS}}/g, FINISH_LINE)
.replace(/{{CONVERSATION_HISTORY}}/g, gameState.conversation.join('\n'))
.replace(/{{GAME_STATE}}/g, `Scores: ${gameState.scores.map((score, idx) =>
`${playerNames[idx]}: ${score}`).join(', ')}`);

// Add reasoning rules if enabled for this player
if (gameState.reasoningEnabled[playerIdx]) {
return result + '\n' + (promptCache.reasoning || '');
}
return result;
}
40 changes: 40 additions & 0 deletions js/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Game constants
const FINISH_LINE = 5; // Reduced to make games shorter
const MAX_SUB_ROUNDS = 2;
const DEFAULT_CHARACTER_TYPE = 'competitive';

const CHARACTER_TYPES = {
'diplomat': 'Negotiates, starts friendly, pushes back if needed',
'strategist': 'Plans carefully, adapts strategy based on results',
'cautious': 'Prefers to pump unless threatened',
'aggressive': 'More likely to dump for higher gains',
'trickster': 'Unpredictable, keeps others guessing',
'competitive': 'You want to win above all. Ties or losses are inconceivable.',
'selective-ally': 'You choose one ally to trust and coordinate with to make the other player lose. Communicate at the start of the round which ally you want to cooperate with and make sure they agree.',
};

const CHARACTER_NAMES = {
'default': [
'Emma', 'Frank', 'Grace', 'Henry', 'Ivy',
'Jack', 'Kate', 'Liam', 'Mia', 'Noah'
]
};

const PAYOFFS = {
ALL_PUMP: {
pumpers: 1,
dumpers: 0
},
LONE_DUMPER: {
dumpers: 2,
pumpers: 0
},
TWO_DUMPERS: {
dumpers: 0,
pumpers: 1.5
},
ALL_DUMP: {
dumpers: 0.5,
pumpers: 0
}
};
Loading