Skip to content

Instantly share code, notes, and snippets.

@pirate
Created February 7, 2025 14:07
Show Gist options
  • Save pirate/e4573ea164de8163faaf6e1a3a76497e to your computer and use it in GitHub Desktop.
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
<!-- 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