295478254c
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
384 lines
12 KiB
HTML
384 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Cribbage Hand Practice</title>
|
|
<style>
|
|
body {
|
|
background-color: #006400; /* darkgreen */
|
|
color: white;
|
|
font-family: Arial, sans-serif;
|
|
text-align: center;
|
|
padding-top: 20px;
|
|
}
|
|
|
|
#app {
|
|
max-width: 600px;
|
|
margin: auto;
|
|
padding: 0 10px;
|
|
}
|
|
|
|
button {
|
|
font-size: 1.2em;
|
|
padding: 10px 20px;
|
|
margin: 10px;
|
|
cursor: pointer;
|
|
border-radius: 5px;
|
|
border: 1px solid #ccc;
|
|
background-color: #f0f0f0;
|
|
}
|
|
|
|
#cardsContainer {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
margin: 20px 0;
|
|
min-height: 120px;
|
|
}
|
|
|
|
#hand {
|
|
display: flex;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
}
|
|
|
|
#starter {
|
|
margin-left: 20px;
|
|
}
|
|
|
|
.card {
|
|
width: 70px;
|
|
height: 100px;
|
|
border: 1px solid black;
|
|
border-radius: 5px;
|
|
background-color: white;
|
|
color: black;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
padding: 5px;
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
box-shadow: 2px 2px 5px rgba(0,0,0,0.3);
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.card.red {
|
|
color: red;
|
|
}
|
|
|
|
.card .rank {
|
|
text-align: left;
|
|
}
|
|
|
|
.card .suit {
|
|
font-size: 36px;
|
|
text-align: center;
|
|
line-height: 0.8;
|
|
}
|
|
|
|
#starter .card {
|
|
border: 3px solid gold;
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
#cardsContainer {
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
#starter {
|
|
margin-left: 0;
|
|
}
|
|
}
|
|
|
|
#scoreContainer {
|
|
margin-top: 20px;
|
|
background-color: rgba(0,0,0,0.2);
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
#scoreBreakdown ul {
|
|
list-style-type: none;
|
|
padding: 0;
|
|
text-align: left;
|
|
display: inline-block;
|
|
margin: 0;
|
|
}
|
|
|
|
#scoreBreakdown li {
|
|
margin-bottom: 5px;
|
|
font-size: 1.1em;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<h1>Cribbage Hand Practice</h1>
|
|
<button id="dealButton">Deal New Hand</button>
|
|
<div id="cardsContainer">
|
|
<div id="hand"></div>
|
|
<div id="starter"></div>
|
|
</div>
|
|
<button id="revealButton" style="display: none;">Reveal Score</button>
|
|
<div id="scoreContainer" style="display: none;">
|
|
<h2>Total Score: <span id="totalScore"></span></h2>
|
|
<div id="scoreBreakdown"></div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
// --- Constants and Globals ---
|
|
const SUITS = ['♠', '♥', '♦', '♣'];
|
|
const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
|
|
const VALUES = { 'A': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '10': 10, 'J': 10, 'Q': 10, 'K': 10 };
|
|
const RANK_ORDER = { 'A': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '10': 10, 'J': 11, 'Q': 12, 'K': 13 };
|
|
|
|
let currentHand = [];
|
|
let starterCard = null;
|
|
|
|
// --- DOM Elements ---
|
|
const dealButton = document.getElementById('dealButton');
|
|
const revealButton = document.getElementById('revealButton');
|
|
const handContainer = document.getElementById('hand');
|
|
const starterContainer = document.getElementById('starter');
|
|
const scoreContainer = document.getElementById('scoreContainer');
|
|
const totalScoreEl = document.getElementById('totalScore');
|
|
const scoreBreakdownEl = document.getElementById('scoreBreakdown');
|
|
|
|
// --- Event Listeners ---
|
|
dealButton.addEventListener('click', dealNewHand);
|
|
revealButton.addEventListener('click', showScore);
|
|
|
|
// --- Core Functions ---
|
|
function createDeck() {
|
|
const deck = [];
|
|
for (const suit of SUITS) {
|
|
for (const rank of RANKS) {
|
|
deck.push({
|
|
rank: rank,
|
|
suit: suit,
|
|
value: VALUES[rank],
|
|
order: RANK_ORDER[rank]
|
|
});
|
|
}
|
|
}
|
|
return deck;
|
|
}
|
|
|
|
function shuffleDeck(deck) {
|
|
for (let i = deck.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[deck[i], deck[j]] = [deck[j], deck[i]];
|
|
}
|
|
}
|
|
|
|
function dealNewHand() {
|
|
const deck = createDeck();
|
|
shuffleDeck(deck);
|
|
|
|
currentHand = deck.slice(0, 4);
|
|
starterCard = deck[4];
|
|
|
|
displayCards(currentHand, starterCard);
|
|
|
|
scoreContainer.style.display = 'none';
|
|
revealButton.style.display = 'inline-block';
|
|
}
|
|
|
|
// --- UI Functions ---
|
|
function createCardElement(card) {
|
|
const cardEl = document.createElement('div');
|
|
cardEl.classList.add('card');
|
|
const isRed = card.suit === '♥' || card.suit === '♦';
|
|
if (isRed) {
|
|
cardEl.classList.add('red');
|
|
}
|
|
|
|
cardEl.innerHTML = `
|
|
<div class="rank">${card.rank}</div>
|
|
<div class="suit">${card.suit}</div>
|
|
<div class="rank" style="transform: rotate(180deg); text-align: right;">${card.rank}</div>
|
|
`;
|
|
return cardEl;
|
|
}
|
|
|
|
function displayCards(hand, starter) {
|
|
handContainer.innerHTML = '';
|
|
starterContainer.innerHTML = '';
|
|
|
|
hand.forEach(card => {
|
|
handContainer.appendChild(createCardElement(card));
|
|
});
|
|
|
|
starterContainer.appendChild(createCardElement(starter));
|
|
}
|
|
|
|
function showScore() {
|
|
const { score, breakdown } = calculateScore(currentHand, starterCard);
|
|
|
|
totalScoreEl.textContent = score;
|
|
|
|
let breakdownHtml = '<ul>';
|
|
if (breakdown.length > 0) {
|
|
for (const item of breakdown) {
|
|
breakdownHtml += `<li>${item.reason}: ${item.points} points</li>`;
|
|
}
|
|
} else {
|
|
breakdownHtml += `<li>No score</li>`;
|
|
}
|
|
breakdownHtml += '</ul>';
|
|
scoreBreakdownEl.innerHTML = breakdownHtml;
|
|
|
|
scoreContainer.style.display = 'block';
|
|
revealButton.style.display = 'none';
|
|
}
|
|
|
|
// --- Scoring Logic ---
|
|
function getCombinations(arr, k) {
|
|
const result = [];
|
|
function backtrack(combination, start) {
|
|
if (combination.length === k) {
|
|
result.push([...combination]);
|
|
return;
|
|
}
|
|
for (let i = start; i < arr.length; i++) {
|
|
combination.push(arr[i]);
|
|
backtrack(combination, i + 1);
|
|
combination.pop();
|
|
}
|
|
}
|
|
backtrack([], 0);
|
|
return result;
|
|
}
|
|
|
|
function calculateScore(hand, starter) {
|
|
let score = 0;
|
|
const breakdown = [];
|
|
const allCards = [...hand, starter];
|
|
|
|
// 1. Fifteens
|
|
let fifteensCount = 0;
|
|
for (let k = 2; k <= 5; k++) {
|
|
const combos = getCombinations(allCards, k);
|
|
for (const combo of combos) {
|
|
const sum = combo.reduce((acc, card) => acc + card.value, 0);
|
|
if (sum === 15) {
|
|
fifteensCount++;
|
|
}
|
|
}
|
|
}
|
|
if (fifteensCount > 0) {
|
|
const points = fifteensCount * 2;
|
|
score += points;
|
|
breakdown.push({ reason: `Fifteens`, points });
|
|
}
|
|
|
|
// 2. Pairs
|
|
let pairsCount = 0;
|
|
const pairCombos = getCombinations(allCards, 2);
|
|
for (const pair of pairCombos) {
|
|
if (pair[0].rank === pair[1].rank) {
|
|
pairsCount++;
|
|
}
|
|
}
|
|
if (pairsCount > 0) {
|
|
const points = pairsCount * 2;
|
|
score += points;
|
|
breakdown.push({ reason: `Pairs`, points });
|
|
}
|
|
|
|
// 3. Runs
|
|
const runsScore = scoreRuns(allCards);
|
|
if (runsScore.points > 0) {
|
|
score += runsScore.points;
|
|
breakdown.push({ reason: runsScore.reason, points: runsScore.points });
|
|
}
|
|
|
|
// 4. Flush
|
|
const flushScore = scoreFlush(hand, starter);
|
|
if (flushScore.points > 0) {
|
|
score += flushScore.points;
|
|
breakdown.push({ reason: flushScore.reason, points: flushScore.points });
|
|
}
|
|
|
|
// 5. Nobs
|
|
for (const card of hand) {
|
|
if (card.rank === 'J' && card.suit === starter.suit) {
|
|
score += 1;
|
|
breakdown.push({ reason: 'Nobs', points: 1 });
|
|
break;
|
|
}
|
|
}
|
|
|
|
return { score, breakdown };
|
|
}
|
|
|
|
function scoreRuns(allCards) {
|
|
const sortedUniqueRanks = [...new Set(allCards.map(c => c.order))].sort((a, b) => a - b);
|
|
|
|
if (sortedUniqueRanks.length < 3) return { points: 0, reason: '' };
|
|
|
|
let runs = [];
|
|
let currentRun = [sortedUniqueRanks[0]];
|
|
|
|
for (let i = 1; i < sortedUniqueRanks.length; i++) {
|
|
if (sortedUniqueRanks[i] === sortedUniqueRanks[i - 1] + 1) {
|
|
currentRun.push(sortedUniqueRanks[i]);
|
|
} else {
|
|
if (currentRun.length >= 3) {
|
|
runs.push(currentRun);
|
|
}
|
|
currentRun = [sortedUniqueRanks[i]];
|
|
}
|
|
}
|
|
if (currentRun.length >= 3) {
|
|
runs.push(currentRun);
|
|
}
|
|
|
|
if (runs.length === 0) return { points: 0, reason: '' };
|
|
|
|
const runToScore = runs.reduce((longest, current) => current.length > longest.length ? current : longest, []);
|
|
|
|
const runLength = runToScore.length;
|
|
let multiplier = 1;
|
|
const rankCounts = {};
|
|
allCards.forEach(c => {
|
|
rankCounts[c.order] = (rankCounts[c.order] || 0) + 1;
|
|
});
|
|
|
|
for (const rank of runToScore) {
|
|
multiplier *= rankCounts[rank];
|
|
}
|
|
|
|
const points = runLength * multiplier;
|
|
const prefix = multiplier > 1 ? (['', 'Double', 'Triple', 'Quadruple'][multiplier - 1] || `${multiplier}x`) : '';
|
|
const reason = `${prefix} Run of ${runLength}`.trim();
|
|
|
|
return { points, reason };
|
|
}
|
|
|
|
function scoreFlush(hand, starter) {
|
|
const firstSuit = hand[0].suit;
|
|
const isHandFlush = hand.every(card => card.suit === firstSuit);
|
|
|
|
if (!isHandFlush) {
|
|
return { points: 0, reason: '' };
|
|
}
|
|
|
|
if (starter.suit === firstSuit) {
|
|
return { points: 5, reason: 'Flush of 5' };
|
|
} else {
|
|
return { points: 4, reason: 'Flush of 4' };
|
|
}
|
|
}
|
|
|
|
// --- Initial Load ---
|
|
dealNewHand();
|
|
</script>
|
|
</body>
|
|
</html>
|