Created
April 20, 2025 01:16
-
-
Save mgroves/b7bd3085b508b0b2e950e3918ab4cdf7 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
<!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