Created
February 7, 2025 14:07
-
-
Save pirate/e4573ea164de8163faaf6e1a3a76497e to your computer and use it in GitHub Desktop.
Mini JS web game allowing you to simulate iterated prisoners dillema rounds against a bot character with a few different strategies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- Iterated prisoners dillema mini JS web game by Nick Sweeting on 2025-02-07 https://blog.sweeting.me --> | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Iterated Prisoner's Dilemma Game</title> | |
<style> | |
/* Basic Styles */ | |
body { | |
font-family: sans-serif; | |
background-color: #f0f0f0; | |
margin: 0; | |
padding: 20px; | |
} | |
#game-container { | |
max-width: 700px; | |
margin: auto; | |
background-color: #fff; | |
padding: 20px; | |
border-radius: 10px; | |
box-shadow: 0 2px 8px rgba(0,0,0,0.2); | |
} | |
h1, h2 { | |
text-align: center; | |
} | |
p { | |
line-height: 1.6; | |
} | |
.button { | |
padding: 10px 20px; | |
font-size: 16px; | |
margin: 10px; | |
cursor: pointer; | |
} | |
#log { | |
border: 1px solid #ccc; | |
height: 250px; | |
overflow-y: scroll; | |
background: #eee; | |
padding: 10px; | |
margin-top: 20px; | |
} | |
#scoreboard p { | |
font-size: 18px; | |
margin: 5px 0; | |
} | |
.strategy-selector { | |
margin-bottom: 20px; | |
text-align: center; | |
} | |
/* Avatars and Animations */ | |
#avatars { | |
display: flex; | |
justify-content: space-around; | |
margin: 20px 0; | |
} | |
.avatar { | |
width: 100px; | |
height: 100px; | |
border-radius: 50%; | |
position: relative; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
font-size: 50px; | |
box-shadow: 0 2px 6px rgba(0,0,0,0.2); | |
} | |
/* Player avatar: light blue; AI avatar: light coral */ | |
#player-avatar { | |
background-color: #a8d0e6; | |
} | |
#ai-avatar { | |
background-color: #f76c6c; | |
} | |
.avatar-label { | |
position: absolute; | |
bottom: -22px; | |
width: 100%; | |
text-align: center; | |
font-weight: bold; | |
} | |
/* Animated move indicator */ | |
@keyframes moveAnimation { | |
0% { opacity: 0; transform: translateY(20px); } | |
50% { opacity: 1; transform: translateY(0px); } | |
100% { opacity: 0; transform: translateY(-20px); } | |
} | |
.move-indicator { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
font-size: 40px; | |
pointer-events: none; | |
animation: moveAnimation 1s ease-out; | |
} | |
/* Health Bars */ | |
#health-bars { | |
display: flex; | |
justify-content: space-around; | |
margin: 20px 0; | |
} | |
.health-bar { | |
width: 40%; | |
background-color: #ddd; | |
border-radius: 10px; | |
overflow: hidden; | |
position: relative; | |
height: 20px; | |
} | |
.health-label { | |
position: absolute; | |
width: 100%; | |
text-align: center; | |
top: 0; | |
font-size: 12px; | |
line-height: 20px; | |
color: #333; | |
z-index: 2; | |
pointer-events: none; | |
} | |
.health-fill { | |
height: 100%; | |
width: 0%; | |
transition: width 0.5s ease; | |
} | |
#player-health-fill { | |
background-color: #a8d0e6; | |
} | |
#ai-health-fill { | |
background-color: #f76c6c; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="game-container"> | |
<h1>Iterated Prisoner's Dilemma</h1> | |
<p> | |
In this game, you and the AI repeatedly choose to either <strong>Cooperate</strong> or <strong>Defect</strong>. | |
The payoffs per round are: | |
<ul> | |
<li><strong>Both Cooperate (C, C):</strong> You get 3 points, AI gets 3 points.</li> | |
<li><strong>You Cooperate, AI Defects (C, D):</strong> You get 0 points, AI gets 5 points.</li> | |
<li><strong>You Defect, AI Cooperates (D, C):</strong> You get 5 points, AI gets 0 points.</li> | |
<li><strong>Both Defect (D, D):</strong> You get 1 point, AI gets 1 point.</li> | |
</ul> | |
Choose your move each round and watch how the AI’s strategy unfolds! | |
</p> | |
<div class="strategy-selector"> | |
<label for="ai-strategy">Choose AI Strategy: </label> | |
<select id="ai-strategy"> | |
<option value="titfortat">Tit for Tat</option> | |
<option value="always_cooperate">Always Cooperate</option> | |
<option value="always_defect">Always Defect</option> | |
<option value="random">Random</option> | |
<option value="grim_trigger">Grim Trigger</option> | |
<option value="pavlov">Pavlov (Win–Stay, Lose–Shift)</option> | |
</select> | |
</div> | |
<!-- Avatars Container --> | |
<div id="avatars"> | |
<div id="player-avatar" class="avatar"> | |
<div class="avatar-label">You</div> | |
</div> | |
<div id="ai-avatar" class="avatar"> | |
<div class="avatar-label">AI</div> | |
</div> | |
</div> | |
<div id="scoreboard"> | |
<p>Your Score: <span id="player-score">0</span></p> | |
<p>AI Score: <span id="ai-score">0</span></p> | |
<p>Round: <span id="round-number">0</span></p> | |
</div> | |
<!-- Health Bars --> | |
<div id="health-bars"> | |
<div class="health-bar"> | |
<div class="health-label">Your Health</div> | |
<div class="health-fill" id="player-health-fill"></div> | |
</div> | |
<div class="health-bar"> | |
<div class="health-label">AI Health</div> | |
<div class="health-fill" id="ai-health-fill"></div> | |
</div> | |
</div> | |
<div style="text-align: center;"> | |
<button id="cooperate-btn" class="button">Cooperate</button> | |
<button id="defect-btn" class="button">Defect</button> | |
<button id="reset-btn" class="button">Reset Game</button> | |
</div> | |
<h2>Game Log</h2> | |
<div id="log"></div> | |
</div> | |
<script> | |
// The payoff matrix for a single round. | |
// Format: 'XY' where X is your move and Y is the AI move. | |
const PAYOFFS = { | |
'CC': { player: 3, ai: 3 }, | |
'CD': { player: 0, ai: 5 }, | |
'DC': { player: 5, ai: 0 }, | |
'DD': { player: 1, ai: 1 } | |
}; | |
// Global game state variables. | |
let playerScore = 0; | |
let aiScore = 0; | |
let round = 0; | |
let lastPlayerMove = 'C'; // (For strategies like Tit for Tat.) | |
let lastAIMove = 'C'; // (For strategies that depend on the AI's previous move.) | |
let aiLastPayoff = 0; // (For the Pavlov strategy.) | |
let playerHasDefected = false; // (For the Grim Trigger strategy.) | |
// Resets the game state and updates the UI. | |
function resetGame() { | |
playerScore = 0; | |
aiScore = 0; | |
round = 0; | |
lastPlayerMove = 'C'; | |
lastAIMove = 'C'; | |
aiLastPayoff = 0; | |
playerHasDefected = false; | |
document.getElementById("player-score").innerText = playerScore; | |
document.getElementById("ai-score").innerText = aiScore; | |
document.getElementById("round-number").innerText = round; | |
document.getElementById("log").innerHTML = ""; | |
appendLog("Game reset. Let the battle of wits begin!"); | |
updateHealthBars(); | |
} | |
// Appends text to the game log. | |
function appendLog(text) { | |
const logDiv = document.getElementById("log"); | |
logDiv.innerHTML += text + "<br/>"; | |
logDiv.scrollTop = logDiv.scrollHeight; | |
} | |
// Determine the AI's move based on the selected strategy. | |
function aiMove() { | |
const strategy = document.getElementById("ai-strategy").value; | |
let move; | |
switch(strategy) { | |
case "titfortat": | |
// Cooperate on the first move, then mimic your last move. | |
move = (round === 0) ? 'C' : lastPlayerMove; | |
break; | |
case "always_cooperate": | |
move = 'C'; | |
break; | |
case "always_defect": | |
move = 'D'; | |
break; | |
case "random": | |
move = (Math.random() < 0.5) ? 'C' : 'D'; | |
break; | |
case "grim_trigger": | |
// Cooperate until you ever defect; then defect forever. | |
move = playerHasDefected ? 'D' : 'C'; | |
break; | |
case "pavlov": | |
// Win–Stay, Lose–Shift: start with cooperation. | |
if (round === 0) { | |
move = 'C'; | |
} else { | |
// A "win" is defined as receiving 3 or 5 points. | |
if (aiLastPayoff >= 3) { | |
move = lastAIMove; | |
} else { | |
move = (lastAIMove === 'C') ? 'D' : 'C'; | |
} | |
} | |
break; | |
default: | |
move = 'C'; | |
} | |
return move; | |
} | |
// Animates the move for an avatar. | |
// side: "player" or "ai", move: 'C' or 'D' | |
function animateMove(side, move) { | |
const avatar = document.getElementById(side + "-avatar"); | |
const indicator = document.createElement("div"); | |
indicator.className = "move-indicator"; | |
// Map moves to an emoji: Cooperation shows a handshake, Defection shows a fist. | |
indicator.innerText = (move === 'C') ? "🤝" : "👊"; | |
avatar.appendChild(indicator); | |
// Remove the indicator after the animation completes (roughly 1s). | |
setTimeout(() => { | |
avatar.removeChild(indicator); | |
}, 1000); | |
} | |
// Updates the health bars based on the current scores. | |
// We use a dynamic maximum: at least 50, or the highest current score. | |
function updateHealthBars() { | |
const dynamicMax = Math.max(50, playerScore, aiScore); | |
const playerPercent = Math.min(100, (playerScore / dynamicMax) * 100); | |
const aiPercent = Math.min(100, (aiScore / dynamicMax) * 100); | |
document.getElementById("player-health-fill").style.width = playerPercent + "%"; | |
document.getElementById("ai-health-fill").style.width = aiPercent + "%"; | |
} | |
// Plays one round of the game. | |
function playRound(playerMove) { | |
// Update the Grim Trigger flag. | |
if (playerMove === 'D') { | |
playerHasDefected = true; | |
} | |
// Let the AI decide its move. | |
let aiChoice = aiMove(); | |
// Animate the moves on the avatars. | |
animateMove("player", playerMove); | |
animateMove("ai", aiChoice); | |
// Get the payoff for this round. | |
let key = playerMove + aiChoice; | |
let payoff = PAYOFFS[key]; | |
playerScore += payoff.player; | |
aiScore += payoff.ai; | |
round++; | |
// Update previous moves (for strategies that remember them). | |
lastPlayerMove = playerMove; | |
lastAIMove = aiChoice; | |
aiLastPayoff = payoff.ai; | |
// Update the scoreboard. | |
document.getElementById("player-score").innerText = playerScore; | |
document.getElementById("ai-score").innerText = aiScore; | |
document.getElementById("round-number").innerText = round; | |
// Update the health bars. | |
updateHealthBars(); | |
// Log the round’s results. | |
appendLog("Round " + round + ": You chose <strong>" + (playerMove === 'C' ? "Cooperate" : "Defect") + "</strong>, " + | |
"AI chose <strong>" + (aiChoice === 'C' ? "Cooperate" : "Defect") + "</strong>. " + | |
"(You: " + payoff.player + " | AI: " + payoff.ai + ")"); | |
} | |
// Attach event listeners to the buttons. | |
document.getElementById("cooperate-btn").addEventListener("click", function(){ | |
playRound('C'); | |
}); | |
document.getElementById("defect-btn").addEventListener("click", function(){ | |
playRound('D'); | |
}); | |
document.getElementById("reset-btn").addEventListener("click", resetGame); | |
// Start the game. | |
resetGame(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment