Skip to content

Instantly share code, notes, and snippets.

@mgroves
Created April 20, 2025 01:16
Show Gist options
  • Save mgroves/b7bd3085b508b0b2e950e3918ab4cdf7 to your computer and use it in GitHub Desktop.
Save mgroves/b7bd3085b508b0b2e950e3918ab4cdf7 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Yahtzee 🎲</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f0f0f0;
margin: 0;
padding: 10px;
touch-action: manipulation;
}
h1 {
font-size: 1.8em;
margin: 10px 0;
}
#game-container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 15px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
}
#dice {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 10px 0;
}
.die {
font-size: 2em;
margin: 5px;
padding: 10px;
border: 2px solid #333;
border-radius: 5px;
background: #fff;
cursor: pointer;
user-select: none;
touch-action: none;
}
.held {
background: #ffd700;
}
button {
font-size: 1.2em;
padding: 10px 20px;
margin: 5px;
border: none;
border-radius: 5px;
background: #28a745;
color: white;
cursor: pointer;
touch-action: manipulation;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
#scorecard {
margin: 10px 0;
font-size: 1em;
}
table {
width: 100%;
border-collapse: collapse;
margin: 0 auto;
}
th, td {
border: 1px solid #333;
padding: 5px;
text-align: left;
}
.selectable {
background: #e0f7fa;
cursor: pointer;
}
.scored {
background: #ccc;
}
#rules {
font-size: 0.9em;
text-align: left;
margin: 10px;
padding: 10px;
background: #e9ecef;
border-radius: 5px;
}
#message {
font-size: 1.1em;
margin: 10px 0;
}
#turn-counter {
font-size: 1.1em;
margin: 5px 0;
}
@media (max-width: 400px) {
h1 {
font-size: 1.5em;
}
.die {
font-size: 1.5em;
padding: 8px;
}
button {
font-size: 1em;
padding: 8px 15px;
}
table {
font-size: 0.8em;
}
}
</style>
</head>
<body>
<div id="game-container">
<h1>Yahtzee 🎲</h1>
<div id="turn-counter">Turn: 1 / 13</div>
<div id="message">Roll the dice to start!</div>
<div id="dice"></div>
<button id="roll-btn" onclick="rollDice()">Roll Dice (3 rolls left)</button>
<div id="scorecard">
<table>
<tr><th>Category</th><th>Score</th></tr>
<tr><td>Ones</td><td id="ones">-</td></tr>
<tr><td>Twos</td><td id="twos">-</td></tr>
<tr><td>Threes</td><td id="threes">-</td></tr>
<tr><td>Fours</td><td id="fours">-</td></tr>
<tr><td>Fives</td><td id="fives">-</td></tr>
<tr><td>Sixes</td><td id="sixes">-</td></tr>
<tr><td>Upper Total</td><td id="upper-total">0</td></tr>
<tr><td>Bonus</td><td id="bonus">0</td></tr>
<tr><td>Three of a Kind</td><td id="three-kind">-</td></tr>
<tr><td>Four of a Kind</td><td id="four-kind">-</td></tr>
<tr><td>Full House</td><td id="full-house">-</td></tr>
<tr><td>Small Straight</td><td id="small-straight">-</td></tr>
<tr><td>Large Straight</td><td id="large-straight">-</td></tr>
<tr><td>Yahtzee</td><td id="yahtzee">-</td></tr>
<tr><td>Chance</td><td id="chance">-</td></tr>
<tr><td>Yahtzee Bonus</td><td id="yahtzee-bonus">0</td></tr>
<tr><td>Grand Total</td><td id="grand-total">0</td></tr>
</table>
</div>
<div id="rules">
<h2>Yahtzee Rules</h2>
<p><strong>Objective:</strong> Score the highest points by rolling five dice to complete 13 categories.</p>
<p><strong>Gameplay:</strong></p>
<ul>
<li>Each turn, roll up to 3 times. Tap dice to hold them between rolls.</li>
<li>After rolling, tap a category in the scorecard to score it.</li>
<li>Each category can only be scored once.</li>
</ul>
<p><strong>Scoring Categories:</strong></p>
<ul>
<li><strong>Ones to Sixes:</strong> Sum of dice showing that number (e.g., Ones: sum of 1s).</li>
<li><strong>Upper Total Bonus:</strong> 35 points if Upper Total ≥ 63.</li>
<li><strong>Three of a Kind:</strong> Sum of all dice if ≥3 dice are the same.</li>
<li><strong>Four of a Kind:</strong> Sum of all dice if ≥4 dice are the same.</li>
<li><strong>Full House:</strong> 25 points for 3 of one number and 2 of another.</li>
<li><strong>Small Straight:</strong> 30 points for 4 consecutive numbers (e.g., 1-2-3-4).</li>
<li><strong>Large Straight:</strong> 40 points for 5 consecutive numbers (e.g., 2-3-4-5-6).</li>
<li><strong>Yahtzee:</strong> 50 points for 5 identical dice.</li>
<li><strong>Chance:</strong> Sum of all dice, no requirements.</li>
<li><strong>Yahtzee Bonus:</strong> 100 points per additional Yahtzee if the Yahtzee category is already scored.</li>
</ul>
<p><strong>Game End:</strong> After 13 turns, the Grand Total is your final score.</p>
</div>
</div>
<script>
const diceEmojis = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
let dice = [1, 1, 1, 1, 1];
let held = [false, false, false, false, false];
let rollsLeft = 3;
let turn = 1;
let scores = {
ones: null, twos: null, threes: null, fours: null, fives: null, sixes: null,
'three-kind': null, 'four-kind': null, 'full-house': null,
'small-straight': null, 'large-straight': null, yahtzee: null, chance: null
};
let yahtzeeBonus = 0;
// Sound effects using Web Audio API
const ctx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(freq, type = 'sine', duration = 0.1) {
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
oscillator.type = type;
oscillator.frequency.setValueAtTime(freq, ctx.currentTime);
gainNode.gain.setValueAtTime(0.5, ctx.currentTime);
oscillator.connect(gainNode);
gainNode.connect(ctx.currentTime ? ctx.destination : ctx.destination);
oscillator.start();
oscillator.stop(ctx.currentTime + duration);
}
function updateDiceDisplay() {
const diceDiv = document.getElementById('dice');
diceDiv.innerHTML = '';
dice.forEach((value, i) => {
const die = document.createElement('div');
die.className = `die ${held[i] ? 'held' : ''}`;
die.innerHTML = diceEmojis[value - 1];
die.onclick = () => toggleHold(i);
diceDiv.appendChild(die);
});
}
function toggleHold(index) {
if (rollsLeft < 3 && rollsLeft > 0) {
held[index] = !held[index];
updateDiceDisplay();
playSound(500, 'square', 0.05); // Click sound
}
}
function rollDice() {
if (rollsLeft > 0) {
for (let i = 0; i < 5; i++) {
if (!held[i]) {
dice[i] = Math.floor(Math.random() * 6) + 1;
}
}
rollsLeft--;
document.getElementById('roll-btn').textContent = `Roll Dice (${rollsLeft} rolls left)`;
document.getElementById('roll-btn').disabled = rollsLeft === 0;
updateDiceDisplay();
updateScorecard();
document.getElementById('message').textContent = rollsLeft > 0 ?
`Tap dice to hold, then roll again or select a category (${rollsLeft} rolls left).` :
`Select a category to score for turn ${turn}.`;
playSound(200, 'triangle', 0.2); // Roll sound
}
}
function calculateScores() {
const counts = Array(7).fill(0);
dice.forEach(d => counts[d]++);
const sortedDice = [...dice].sort();
return {
ones: counts[1] * 1,
twos: counts[2] * 2,
threes: counts[3] * 3,
fours: counts[4] * 4,
fives: counts[5] * 5,
sixes: counts[6] * 6,
'three-kind': counts.some(c => c >= 3) ? dice.reduce((a, b) => a + b, 0) : 0,
'four-kind': counts.some(c => c >= 4) ? dice.reduce((a, b) => a + b, 0) : 0,
'full-house': (counts.includes(3) && counts.includes(2)) ? 25 : 0,
'small-straight': (
(sortedDice.join('').includes('1234')) ||
(sortedDice.join('').includes('2345')) ||
(sortedDice.join('').includes('3456'))
) ? 30 : 0,
'large-straight': (
sortedDice.join('') === '12345' ||
sortedDice.join('') === '23456'
) ? 40 : 0,
yahtzee: counts.some(c => c === 5) ? 50 : 0,
chance: dice.reduce((a, b) => a + b, 0)
};
}
function updateScorecard() {
const possibleScores = calculateScores();
Object.keys(possibleScores).forEach(category => {
const cell = document.getElementById(category);
if (scores[category] === null) {
cell.textContent = possibleScores[category];
cell.className = 'selectable';
cell.onclick = () => selectCategory(category, possibleScores[category]);
} else {
cell.textContent = scores[category];
cell.className = 'scored';
cell.onclick = null;
}
});
}
function selectCategory(category, score) {
if (scores[category] === null && rollsLeft < 3) {
scores[category] = score;
document.getElementById(category).textContent = score;
document.getElementById(category).className = 'scored';
document.getElementById(category).onclick = null;
// Handle Yahtzee Bonus
if (category === 'yahtzee' && score === 50) {
// Yahtzee scored, future Yahtzees give bonuses
} else if (scores.yahtzee === 50 && calculateScores().yahtzee === 50 && category !== 'yahtzee') {
yahtzeeBonus += 100;
document.getElementById('yahtzee-bonus').textContent = yahtzeeBonus;
}
// Update totals
updateTotals();
// Reset for next turn
rollsLeft = 3;
held = [false, false, false, false, false];
document.getElementById('roll-btn').textContent = `Roll Dice (3 rolls left)`;
document.getElementById('roll-btn').disabled = false;
updateDiceDisplay();
turn++;
document.getElementById('turn-counter').textContent = `Turn: ${turn} / 13`;
document.getElementById('message').textContent = turn <= 13 ?
`Roll the dice to start turn ${turn}!` :
`Game Over! Final Score: ${document.getElementById('grand-total').textContent}. Refresh to play again.`;
playSound(700, 'sine', 0.1); // Score sound
if (turn > 13) {
document.getElementById('roll-btn').disabled = true;
Object.keys(scores).forEach(cat => {
const cell = document.getElementById(cat);
cell.onclick = null;
cell.className = 'scored';
});
}
}
}
function updateTotals() {
const upperScores = ['ones', 'twos', 'threes', 'fours', 'fives', 'sixes'];
let upperTotal = 0;
upperScores.forEach(cat => {
if (scores[cat] !== null) upperTotal += scores[cat];
});
document.getElementById('upper-total').textContent = upperTotal;
const bonus = upperTotal >= 63 ? 35 : 0;
document.getElementById('bonus').textContent = bonus;
let grandTotal = upperTotal + bonus + yahtzeeBonus;
['three-kind', 'four-kind', 'full-house', 'small-straight', 'large-straight', 'yahtzee', 'chance'].forEach(cat => {
if (scores[cat] !== null) grandTotal += scores[cat];
});
document.getElementById('grand-total').textContent = grandTotal;
}
// Initialize game
updateDiceDisplay();
updateScorecard();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment