Skip to content

Instantly share code, notes, and snippets.

@fromalexx
Created June 21, 2026 20:32
Show Gist options
  • Select an option

  • Save fromalexx/ea7a6881c74139d6cc1d3fffc09e50cd to your computer and use it in GitHub Desktop.

Select an option

Save fromalexx/ea7a6881c74139d6cc1d3fffc09e50cd to your computer and use it in GitHub Desktop.
Speed card game (dad & daughter edition)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#0b6b3a">
<title>Speed! 🃏</title>
<style>
:root{
--card-w: clamp(46px, 17vw, 62px);
--card-h: calc(var(--card-w) * 1.42);
}
*{ box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html,body{ margin:0; height:100%; }
body{
font-family: "Segoe UI", system-ui, -apple-system, Roboto, sans-serif;
background: radial-gradient(circle at 50% 20%, #0e8a4a 0%, #0b6b3a 55%, #073d22 100%);
color:#fff; user-select:none; -webkit-user-select:none;
touch-action: manipulation;
display:flex; justify-content:center;
min-height:100%;
}
#app{ width:100%; max-width:520px; padding:10px 12px 18px; display:flex; flex-direction:column; gap:8px; position:relative; }
header{ text-align:center; }
header h1{ margin:2px 0; font-size:26px; letter-spacing:1px; text-shadow:0 2px 6px rgba(0,0,0,.4); }
header .sub{ font-size:12px; opacity:.85; }
.row{ display:flex; align-items:center; justify-content:center; gap:6px; }
.row.spread{ justify-content:space-between; }
.seat{ display:flex; align-items:center; gap:10px; padding:6px 10px; border-radius:14px;
background:rgba(0,0,0,.18); width:100%; justify-content:space-between; }
.seat .label{ font-size:13px; font-weight:600; opacity:.9; }
.badge{ font-size:12px; background:rgba(255,255,255,.16); padding:3px 8px; border-radius:20px; white-space:nowrap; }
.hand{ display:flex; gap:6px; justify-content:center; flex-wrap:nowrap; min-height:var(--card-h); }
.card{
width:var(--card-w); height:var(--card-h);
background:#fff; color:#1a1a1a; border-radius:9px;
box-shadow:0 3px 6px rgba(0,0,0,.35);
display:flex; flex-direction:column; justify-content:space-between;
padding:5px 6px; font-weight:800; position:relative;
transition: transform .12s ease, box-shadow .12s ease;
line-height:1;
}
.card .r{ font-size: calc(var(--card-w) * .34); }
.card .s{ font-size: calc(var(--card-w) * .44); align-self:flex-end; }
.card.red{ color:#d23b3b; }
.card.back{
background: repeating-linear-gradient(45deg, #2554c7 0 8px, #1c44a8 8px 16px);
border:2px solid #cdd8ff;
}
.card.back::after{ content:"🃏"; position:absolute; inset:0; display:flex; align-items:center; justify-content:center; font-size:calc(var(--card-w)*.5); opacity:.85; }
.hand .card.playable{ cursor:pointer; outline:3px solid #ffe14d; transform:translateY(-8px); box-shadow:0 8px 14px rgba(0,0,0,.45), 0 0 14px rgba(255,225,77,.7); }
.hand .card{ cursor:pointer; }
.card.shake{ animation:shake .35s; }
@keyframes shake{ 0%,100%{transform:translateX(0)} 20%{transform:translateX(-6px)} 40%{transform:translateX(6px)} 60%{transform:translateX(-4px)} 80%{transform:translateX(4px)} }
.card.pop{ animation:pop .18s ease; }
@keyframes pop{ 0%{transform:scale(.7)} 60%{transform:scale(1.12)} 100%{transform:scale(1)} }
#center-row{ display:flex; align-items:center; justify-content:center; gap:18px; padding:10px 0; min-height:calc(var(--card-h) + 8px); }
.pile-wrap{ display:flex; flex-direction:column; align-items:center; gap:4px; }
.pile-wrap .mini{ font-size:11px; opacity:.75; }
#flipBtn{
display:none; border:none; cursor:pointer;
background:linear-gradient(#ffcf3f,#ff9d2e); color:#5a2a00; font-weight:900;
font-size:16px; padding:14px 22px; border-radius:14px; box-shadow:0 4px 0 #c96a00;
animation:pulse 1s infinite;
}
#flipBtn.show{ display:block; }
@keyframes pulse{ 0%,100%{transform:scale(1)} 50%{transform:scale(1.07)} }
#status{ text-align:center; min-height:20px; font-size:13px; opacity:.92; }
#toast{ position:fixed; left:50%; bottom:14px; transform:translateX(-50%);
background:rgba(0,0,0,.82); color:#fff; padding:10px 16px; border-radius:14px;
font-size:14px; max-width:90vw; text-align:center; box-shadow:0 4px 14px rgba(0,0,0,.5);
opacity:0; pointer-events:none; transition:opacity .3s, transform .3s; z-index:30; }
#toast.show{ opacity:1; transform:translateX(-50%) translateY(-4px); }
#toast .who{ display:block; font-size:11px; opacity:.7; margin-bottom:2px; }
.btn{ border:none; cursor:pointer; border-radius:12px; font-weight:700; padding:10px 16px; font-size:14px; }
.btn.primary{ background:#ffe14d; color:#3a2a00; }
.btn.ghost{ background:rgba(255,255,255,.16); color:#fff; }
#controls{ display:flex; gap:8px; justify-content:center; margin-top:4px; flex-wrap:wrap; }
.overlay{ position:fixed; inset:0; background:rgba(3,30,16,.92); display:flex; align-items:center; justify-content:center; z-index:40; padding:18px; }
.overlay.hide{ display:none; }
.panel{ background:#0b6b3a; border:2px solid rgba(255,255,255,.25); border-radius:20px; padding:22px; max-width:380px; width:100%; text-align:center; box-shadow:0 10px 40px rgba(0,0,0,.6); }
.panel h2{ margin:4px 0 8px; font-size:26px; }
.panel p{ font-size:15px; line-height:1.4; opacity:.95; }
.panel .joke{ background:rgba(0,0,0,.25); border-radius:12px; padding:12px; margin:14px 0; font-style:italic; }
.seg{ display:flex; gap:6px; justify-content:center; margin:14px 0; }
.seg button{ flex:1; padding:10px 4px; border-radius:10px; border:2px solid rgba(255,255,255,.25); background:rgba(255,255,255,.08); color:#fff; font-weight:700; cursor:pointer; }
.seg button.on{ background:#ffe14d; color:#3a2a00; border-color:#ffe14d; }
.how{ font-size:12px; opacity:.8; text-align:left; margin-top:10px; line-height:1.5; }
</style>
</head>
<body>
<div id="app">
<header>
<h1>Speed! 🃏</h1>
<div class="sub">Beat the computer — first to ditch all your cards wins.</div>
</header>
<div class="seat">
<span class="label">🤖 Computer</span>
<div id="ai-hand" class="hand" style="min-height:auto; flex:1; justify-content:center;"></div>
<span class="badge" id="ai-stock">Draw: 0</span>
</div>
<div id="center-row">
<div class="pile-wrap">
<div id="pile0" class="card"></div>
<div class="mini" id="flip0">Flip: 0</div>
</div>
<button id="flipBtn">FLIP! 🔄</button>
<div class="pile-wrap">
<div id="pile1" class="card"></div>
<div class="mini" id="flip1">Flip: 0</div>
</div>
</div>
<div id="status"></div>
<div class="seat">
<span class="label">🧒 You</span>
<span class="badge" id="player-stock">Draw: 0</span>
</div>
<div id="player-hand" class="hand"></div>
<div id="controls">
<button class="btn ghost" id="jokeBtn">🤣 Dad Joke</button>
<button class="btn primary" id="newBtn">New Game</button>
</div>
</div>
<div id="toast"></div>
<!-- Start / Win overlay -->
<div id="overlay" class="overlay">
<div class="panel">
<h2 id="ov-title">Ready to play Speed?</h2>
<p id="ov-text">Tap a glowing card to slap it onto a center pile. A card plays if it's one rank up or down from the top card. Aces wrap around Kings!</p>
<div class="joke" id="ov-joke"></div>
<div>
<div style="font-size:13px; opacity:.85; margin-bottom:6px;">Computer speed:</div>
<div class="seg" id="seg">
<button data-ms="1400">Easy</button>
<button data-ms="950" class="on">Medium</button>
<button data-ms="620">Fast</button>
</div>
</div>
<button class="btn primary" id="startBtn" style="width:100%; padding:14px; font-size:18px;">Deal! ▶️</button>
<div class="how">
• Tap a glowing card — it jumps onto a matching pile.<br>
• No one can move? Tap the big <b>FLIP!</b> button.<br>
• Empty your hand <i>and</i> draw pile to win. 🏆
</div>
</div>
</div>
<script>
const SUITS = ['♠','♥','♦','♣']; // 0 spade,1 heart,2 diamond,3 club
const isRed = s => s===1 || s===2;
const RANKL = {1:'A',11:'J',12:'Q',13:'K'};
const label = r => RANKL[r] || String(r);
const DAD_JOKES = [
"Why did the playing card go to the doctor? It was feeling a little off-suit.",
"I told my deck a joke. It was a real card.",
"Why can't your hand be 12 inches long? Because then it'd be a foot.",
"I only know 25 letters of the alphabet. I don't know y.",
"What do you call a deck of cards on the floor? A card-pet.",
"Why did the scarecrow win an award? He was outstanding in his field.",
"I'm reading a book about anti-gravity. It's impossible to put down!",
"What do you call fake spaghetti? An impasta.",
"Why don't eggs tell jokes? They'd crack each other up.",
"I used to play piano by ear. Now I use my hands.",
"What did the ocean say to the beach? Nothing, it just waved.",
"Why did the math book look sad? It had too many problems.",
"How do you organize a space party? You planet.",
"I made a belt out of watches. It was a waist of time.",
"Why can't you trust an atom? They make up everything.",
"What do you call cheese that isn't yours? Nacho cheese.",
"Did you hear about the cheese factory explosion? There was nothing left but de-brie.",
"Why did the bicycle fall over? It was two-tired.",
"What do you call a fish with no eyes? A fsh.",
"I'm afraid for the calendar. Its days are numbered.",
"Why did the golfer bring two pairs of pants? In case he got a hole in one.",
"What do you get when you cross a snowman and a vampire? Frostbite.",
"Why was the broom late? It over-swept.",
"How does a penguin build its house? Igloos it together.",
"What kind of music do balloons hate? Pop.",
"Want to hear a joke about paper? Never mind, it's tearable.",
"Why did the coffee file a police report? It got mugged.",
"What did one wall say to the other? I'll meet you at the corner.",
"Why don't skeletons fight each other? They don't have the guts.",
"What do you call a bear with no teeth? A gummy bear.",
"How do you make a tissue dance? Put a little boogie in it.",
"Why did the cookie go to the hospital? It was feeling crumby.",
"What do you call a dinosaur that crashes his car? Tyrannosaurus wrecks.",
"I would tell you a chemistry joke, but I know I wouldn't get a reaction.",
"Why did the picture go to jail? It was framed.",
"Speed tip: play fast! Unlike my jokes, which never get old.",
"I'm so good at sleeping I can do it with my eyes closed.",
"What's brown and sticky? A stick."
];
const $ = id => document.getElementById(id);
let state = null;
let jokeIdx = 0;
function shuffle(a){ for(let i=a.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; } return a; }
function matches(a,b){ const d=Math.abs(a-b); return d===1 || d===12; } // ±1, with Ace<->King wrap
function topOf(p){ return p[p.length-1]; }
function newDeck(){
const d=[];
for(let s=0;s<4;s++) for(let r=1;r<=13;r++) d.push({r,s});
return shuffle(d);
}
function refill(p){ while(p.hand.length<5 && p.stock.length>0) p.hand.push(p.stock.pop()); }
function deal(ms){
const d = newDeck();
const take = n => d.splice(0,n);
state = {
player: { stock: take(15), hand: take(5) },
ai: { stock: take(15), hand: take(5) },
flip: [ take(5), take(5) ],
center: [ take(1), take(1) ],
difficulty: ms,
running: true,
aiTimer: null,
flipScheduled: null,
jokeTimer: null
};
render();
scheduleAI();
state.jokeTimer = setInterval(()=>{ if(state && state.running) randomJoke(); }, 24000);
}
function scheduleAI(){
if(!state || !state.running) return;
const base = state.difficulty;
const wait = base * (0.75 + Math.random()*0.5);
state.aiTimer = setTimeout(aiTick, wait);
}
function aiTick(){
if(!state || !state.running) return;
const tops = state.center.map(topOf);
const moves = [];
state.ai.hand.forEach((c,i)=> state.center.forEach((p,pi)=>{ if(matches(c.r, tops[pi].r)) moves.push({i,pi}); }));
if(moves.length){
const m = moves[Math.floor(Math.random()*moves.length)];
const card = state.ai.hand.splice(m.i,1)[0];
state.center[m.pi].push(card);
refill(state.ai);
render(m.pi);
if(checkWin()) return;
}
scheduleAI();
}
function anyPlayable(hand){
const tops = state.center.map(topOf);
return hand.some(c => state.center.some((p,pi)=> matches(c.r, tops[pi].r)));
}
function playerPlay(i){
if(!state || !state.running) return;
const c = state.player.hand[i];
const tops = state.center.map(topOf);
let pile = -1;
for(let pi=0; pi<2; pi++){ if(matches(c.r, tops[pi].r)){ pile = pi; break; } }
if(pile<0){ shakeCard(i); return; }
state.player.hand.splice(i,1);
state.center[pile].push(c);
refill(state.player);
render(pile);
checkWin();
}
function shakeCard(i){
const el = $('player-hand').children[i];
if(!el) return;
el.classList.add('shake');
setTimeout(()=>el && el.classList.remove('shake'), 360);
}
function rebuildFlips(){
let pool = [];
state.center.forEach(p=>{ const t = p.pop(); pool = pool.concat(p); p.length=0; p.push(t); });
shuffle(pool);
const h = Math.ceil(pool.length/2);
state.flip[0] = pool.slice(0,h);
state.flip[1] = pool.slice(h);
}
function doFlip(){
if(state) state.flipScheduled = null;
if(!state || !state.running) return;
if(state.flip[0].length===0 || state.flip[1].length===0) rebuildFlips();
if(state.flip[0].length && state.flip[1].length){
state.center[0].push(state.flip[0].pop());
state.center[1].push(state.flip[1].pop());
toast("New cards down! GO!", "🔄 Flip");
}
render(0); render(1);
}
function updateStalemate(){
const stuck = state && state.running && !anyPlayable(state.player.hand) && !anyPlayable(state.ai.hand);
$('flipBtn').classList.toggle('show', !!stuck);
$('status').textContent = stuck ? "Nobody can move — tap FLIP!" : "";
if(stuck){
if(!state.flipScheduled) state.flipScheduled = setTimeout(doFlip, 2600);
} else if(state && state.flipScheduled){
clearTimeout(state.flipScheduled); state.flipScheduled = null;
}
}
function checkWin(){
const p = state.player, a = state.ai;
if(p.hand.length===0 && p.stock.length===0){ endGame(true); return true; }
if(a.hand.length===0 && a.stock.length===0){ endGame(false); return true; }
return false;
}
function endGame(playerWon){
state.running = false;
clearTimeout(state.aiTimer);
clearTimeout(state.flipScheduled);
clearInterval(state.jokeTimer);
$('flipBtn').classList.remove('show');
$('ov-title').textContent = playerWon ? "🏆 You win!" : "🤖 Computer wins!";
$('ov-text').textContent = playerWon
? "Lightning fingers! You cleared your cards first."
: "So close! The computer emptied out first. Run it back?";
$('ov-joke').textContent = nextJoke();
$('startBtn').textContent = "Play Again ▶️";
$('overlay').classList.remove('hide');
}
/* ---------- rendering ---------- */
function makeCard(card, faceUp){
const d = document.createElement('div');
d.className = 'card' + (faceUp ? '' : ' back');
if(faceUp){
if(isRed(card.s)) d.classList.add('red');
d.innerHTML = `<span class="r">${label(card.r)}</span><span class="s">${SUITS[card.s]}</span>`;
}
return d;
}
function renderPile(id, pile){
const el = $(id);
const top = topOf(pile);
el.className = 'card' + (isRed(top.s) ? ' red' : '');
el.innerHTML = `<span class="r">${label(top.r)}</span><span class="s">${SUITS[top.s]}</span>`;
}
function render(poppedPile){
// AI hand (face down) + counts
const ah = $('ai-hand'); ah.innerHTML='';
state.ai.hand.forEach(()=> ah.appendChild(makeCard(null,false)));
$('ai-stock').textContent = 'Draw: ' + state.ai.stock.length;
$('player-stock').textContent = 'Draw: ' + state.player.stock.length;
$('flip0').textContent = 'Flip: ' + state.flip[0].length;
$('flip1').textContent = 'Flip: ' + state.flip[1].length;
renderPile('pile0', state.center[0]);
renderPile('pile1', state.center[1]);
if(poppedPile===0) $('pile0').classList.add('pop');
if(poppedPile===1) $('pile1').classList.add('pop');
// Player hand
const tops = state.center.map(topOf);
const ph = $('player-hand'); ph.innerHTML='';
state.player.hand.forEach((c,i)=>{
const el = makeCard(c,true);
const playable = state.center.some((p,pi)=> matches(c.r, tops[pi].r));
if(playable) el.classList.add('playable');
el.addEventListener('click', ()=>playerPlay(i));
ph.appendChild(el);
});
updateStalemate();
}
/* ---------- jokes ---------- */
function nextJoke(){ const j = DAD_JOKES[jokeIdx % DAD_JOKES.length]; jokeIdx++; return j; }
function toast(msg, who){
const t = $('toast');
t.innerHTML = (who ? `<span class="who">${who}</span>` : '') + msg;
t.classList.add('show');
clearTimeout(toast._t);
toast._t = setTimeout(()=> t.classList.remove('show'), 3600);
}
function randomJoke(){ toast(nextJoke(), "👨 Dad Joke"); }
/* ---------- wiring ---------- */
let chosenMs = 950;
$('seg').addEventListener('click', e=>{
const b = e.target.closest('button'); if(!b) return;
[...$('seg').children].forEach(x=>x.classList.remove('on'));
b.classList.add('on'); chosenMs = +b.dataset.ms;
});
$('startBtn').addEventListener('click', ()=>{
$('overlay').classList.add('hide');
deal(chosenMs);
toast(nextJoke(), "👨 Dad Joke");
});
$('newBtn').addEventListener('click', ()=>{
if(state){ clearTimeout(state.aiTimer); clearTimeout(state.flipScheduled); clearInterval(state.jokeTimer); state.running=false; }
$('ov-title').textContent = "New game?";
$('ov-text').textContent = "Tap a glowing card to slap it onto a center pile. One rank up or down — Aces wrap around Kings!";
$('ov-joke').textContent = nextJoke();
$('startBtn').textContent = "Deal! ▶️";
$('overlay').classList.remove('hide');
});
$('jokeBtn').addEventListener('click', randomJoke);
$('flipBtn').addEventListener('click', ()=>{ if(state){ clearTimeout(state.flipScheduled); state.flipScheduled=null; doFlip(); } });
// initial joke on the start panel
$('ov-joke').textContent = nextJoke();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment