GPT-6 Astra × slots
8.7Vegas slot machine — reels, pay table, betting, win animations
correctness 9.5quality 9.2documentation 7.5186198ms
$ cat slots.prompt — what the model was asked
Build a fully playable Vegas-style slot machine in a single self-contained HTML file with no external libraries and no external assets.
## Reels
- 3 reels, each with these 7 symbols: 🍒 Cherry, 🍋 Lemon, 🍊 Orange, 🍇 Grape, 🔔 Bell, ⭐ Star, 7️⃣ Seven
- Spin animation: each reel spins independently (reel 1 stops ~800ms, reel 2 ~1200ms, reel 3 ~1600ms), showing symbols scrolling at ~12 symbols/sec before snapping to the result
- SPIN button disabled while spinning
## Pay table (multipliers × bet)
- 7️⃣ 7️⃣ 7️⃣ → 100×
- ⭐ ⭐ ⭐ → 50×
- 🔔 🔔 🔔 → 20×
- 🍇 🍇 🍇 → 15×
- 🍊 🍊 🍊 → 10×
- 🍋 🍋 🍋 → 5×
- 🍒 🍒 🍒 → 3×
- Any two 🍒 in first two positions → 2×
- All other → 0× (loss)
## Betting & credits
- Start with 100 credits
- Bet buttons: 1, 5, 10 credits (highlighted active; default: 1)
- Bet deducted immediately on spin; win adds bet × multiplier
- GAME OVER at 0 credits with 'Play Again' button that resets to 100
## Win feedback
- Win: flash matching symbols, show '+[amount] credits' in gold, display win name ('JACKPOT!', 'THREE BELLS', etc.)
- Loss: shake animation on reels
- Credit balance count-up animation over 400ms
## Pay table display
- Visible panel on page (not a modal) listing all combinations
- Highlight winning row when that combination hits
## Code quality
- Single HTML file; all JS in <script>, all CSS in <style>
- Game state: { credits, bet, spinning, reels, result }
- Named functions: initState(), spin(), checkWin(), animateReels(), updateUI()
- RNG: Math.random() only
- No inline onclick= handlers; use addEventListenerrun 1 of 1
8.7correctness 9.5quality 9.2documentation 7.5186198ms
# judge: The game is polished and functionally complete with correct payouts, state handling, staggered reels, feedback, and game-over behavior, but reel timings and the two-cherry multiplier remain magic numbers rather than fully named constants.
$ cat codex-cli/gpt-6-astra.slots.r1.response
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lucky Seven • Vegas Slots</title>
<style>
:root {
color-scheme: dark;
--gold: #ffd879;
--muted: #bdb0c9;
--cell: 116px;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(ellipse at 25% 10%, #512050 0, transparent 55%),
radial-gradient(ellipse at 90% 80%, #271c49 0, transparent 50%),
#110c1c;
color: #fff7e7;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button { font: inherit; }
button:focus-visible { outline: 3px solid white; outline-offset: 5px; }
button:disabled { cursor: not-allowed; opacity: .42; }
.page { max-width: 1080px; margin: auto; padding: 38px 20px; }
header { text-align: center; margin-bottom: 28px; }
.eyebrow { color: var(--gold); font-size: 11px; letter-spacing: .32em; font-weight: 800; }
h1 {
margin: 7px 0;
font-size: clamp(36px, 7vw, 66px);
line-height: 1.1;
letter-spacing: -.04em;
color: var(--gold);
text-shadow: 0 3px 0 #905027, 0 0 35px #ffc45b33;
}
header p { color: var(--muted); margin: 12px 0 0; font-size: 14px; }
main { display: grid; grid-template-columns: minmax(0, 1fr) 290px; gap: 24px; align-items: start; }
.machine {
position: relative;
padding: 25px;
border: 2px solid #b58b47;
border-radius: 25px;
background: linear-gradient(145deg, #41233d, #221727 65%);
box-shadow: inset 0 0 0 5px #211222, 0 24px 70px #0006, 0 0 35px #edb35c12;
}
.stats { display: flex; gap: 14px; }
.stat { flex: 1; padding: 13px 17px; border: 1px solid #685044; border-radius: 12px; background: #140f1b; }
.label { display: block; color: var(--muted); font-size: 10px; font-weight: 750; letter-spacing: .16em; }
.value { display: block; color: var(--gold); font-size: 30px; font-weight: 800; font-variant-numeric: tabular-nums; }
.value small { color: var(--muted); font-size: 11px; font-weight: 500; }
.announcement { min-height: 86px; padding: 17px 0 12px; text-align: center; }
#headline { margin: 0; font-size: 20px; letter-spacing: .09em; color: var(--gold); }
#feedback { min-height: 22px; margin-top: 5px; color: var(--muted); font-size: 14px; }
#feedback.win { color: var(--gold); font-weight: 800; }
.reel-frame {
padding: 13px;
background: linear-gradient(135deg, #ffe5a0, #97622a 35%, #edc77b 70%, #8c5b2e);
border-radius: 17px;
box-shadow: 0 5px 0 #110c18, 0 0 22px #0005;
}
.reels { display: flex; gap: 9px; }
.reel {
position: relative;
flex: 1;
min-width: 0;
height: var(--cell);
overflow: hidden;
border-radius: 8px;
background: #fff6e2;
box-shadow: inset 0 0 10px #70471755;
}
.reel::after {
content: "";
pointer-events: none;
position: absolute;
inset: 0;
background: linear-gradient(#57341c2e, transparent 28%, transparent 72%, #57341c2e);
border-top: 1px solid #fff9;
border-bottom: 1px solid #fff9;
border-radius: inherit;
}
.strip { will-change: transform; }
.symbol {
height: var(--cell);
display: flex;
justify-content: center;
align-items: center;
font-size: clamp(39px, 6vw, 66px);
line-height: 1;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
user-select: none;
}
.reel.rolling .symbol { filter: blur(.5px); }
.reel.winner { animation: flash .38s ease-in-out 6; }
.reel-frame.shake { animation: shake .35s ease-in-out; }
@keyframes flash {
50% { background-color: #ffdc71; box-shadow: inset 0 0 18px #dc8900, 0 0 20px #fff49c; }
}
@keyframes shake {
20%, 60% { transform: translateX(-5px); }
40%, 80% { transform: translateX(5px); }
}
.payline { text-align: center; margin: 13px 0 23px; color: #d0b487; font-size: 10px; letter-spacing: .2em; }
.bet-controls { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 18px; }
.bet-buttons { display: flex; gap: 8px; }
.bet-button {
min-width: 54px;
padding: 10px 12px;
border: 1px solid #79627c;
border-radius: 9px;
color: #ecdfef;
background: #25182e;
cursor: pointer;
font-weight: 750;
}
.bet-button.active { color: #26130e; background: var(--gold); border-color: var(--gold); box-shadow: 0 0 18px #ffd87922; }
.bet-button:hover:not(:disabled):not(.active) { background: #49304f; }
#spin {
width: 100%;
padding: 17px;
border: 1px solid #ffe8af;
border-radius: 12px;
background: linear-gradient(#ffe49b, #efb746);
color: #3c200f;
box-shadow: 0 5px 0 #875521, 0 8px 25px #0005;
font-size: 22px;
font-weight: 900;
letter-spacing: .2em;
cursor: pointer;
}
#spin:hover:not(:disabled) { filter: brightness(1.1); }
#spin:active:not(:disabled) { transform: translateY(3px); box-shadow: 0 2px 0 #875521; }
.hint { text-align: center; color: var(--muted); font-size: 11px; margin: 18px 0 0; }
.paytable { padding: 23px 18px; border: 1px solid #69503b; border-radius: 18px; background: #1b1324dd; }
.paytable h2 { color: var(--gold); font-size: 15px; letter-spacing: .16em; margin: 0 0 5px; }
.paytable > p { color: var(--muted); font-size: 12px; margin: 0 0 18px; }
.paytable ul { list-style: none; padding: 0; margin: 0; }
.paytable li {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
padding: 11px 9px;
margin: 3px 0;
border: 1px solid transparent;
border-radius: 8px;
}
.paytable .combo { display: block; font-size: 22px; letter-spacing: 3px; }
.paytable .name { display: block; color: var(--muted); font-size: 10px; margin-top: 3px; }
.paytable strong { color: var(--gold); white-space: nowrap; font-variant-numeric: tabular-nums; }
.paytable li.hit { border-color: var(--gold); background: #ffd87920; box-shadow: 0 0 18px #ffc35a12; }
.paytable li.hit .name { color: var(--gold); }
.paytable .note { margin: 15px 8px 0; line-height: 1.6; font-size: 11px; }
.game-over { margin-top: 23px; padding: 18px; text-align: center; border: 1px solid #c69558; border-radius: 12px; background: #160e20; }
.game-over h2 { color: var(--gold); margin: 0 0 7px; letter-spacing: .1em; }
.game-over p { color: var(--muted); font-size: 13px; }
#play-again { padding: 10px 22px; background: var(--gold); color: #301c15; border: 0; border-radius: 8px; font-weight: 800; cursor: pointer; }
[hidden] { display: none !important; }
footer { text-align: center; color: #9c8da8; font-size: 11px; margin-top: 26px; }
@media (max-width: 760px) {
main { grid-template-columns: 1fr; max-width: 530px; margin: auto; }
.paytable { padding: 20px; }
.paytable li { padding: 8px 10px; }
.page { padding: 25px 14px; }
}
@media (max-width: 380px) {
:root { --cell: 96px; }
.machine { padding: 18px; }
.reel-frame { padding: 9px; }
.reels { gap: 5px; }
.bet-button { min-width: 45px; }
.stat { padding: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.reel.winner { animation: none; background: #ffdc71; }
.reel-frame.shake { animation: none; }
}
</style>
</head>
<body>
<div class="page">
<header>
<div class="eyebrow">THE STRIP IS CALLING</div>
<h1>Lucky Seven</h1>
<p>Three reels. One lucky moment.</p>
</header>
<main>
<section class="machine" aria-label="Slot machine">
<div class="stats">
<div class="stat">
<span class="label">YOUR BALANCE</span>
<span class="value"><span id="credits">100</span> <small>credits</small></span>
</div>
<div class="stat">
<span class="label">CURRENT BET</span>
<span class="value"><span id="current-bet">1</span> <small>credits</small></span>
</div>
</div>
<div class="announcement" role="status" aria-live="polite" aria-atomic="true">
<h2 id="headline">FEELING LUCKY?</h2>
<div id="feedback">Choose your bet and give it a spin.</div>
</div>
<div class="reel-frame" id="reel-frame">
<div class="reels" id="reels" role="group" aria-label="Three reels">
<div class="reel" role="img" aria-label="Reel 1: Cherry"><div class="strip" aria-hidden="true"></div></div>
<div class="reel" role="img" aria-label="Reel 2: Star"><div class="strip" aria-hidden="true"></div></div>
<div class="reel" role="img" aria-label="Reel 3: Seven"><div class="strip" aria-hidden="true"></div></div>
</div>
</div>
<div class="payline">◆ SINGLE PAYLINE · THREE OF A KIND ◆</div>
<div class="bet-controls">
<span class="label" id="bet-label">PLACE YOUR BET</span>
<div class="bet-buttons" role="group" aria-labelledby="bet-label">
<button class="bet-button active" data-bet="1" aria-pressed="true">1</button>
<button class="bet-button" data-bet="5" aria-pressed="false">5</button>
<button class="bet-button" data-bet="10" aria-pressed="false">10</button>
</div>
</div>
<button id="spin" type="button">SPIN</button>
<p class="hint" id="hint">Up to 100× your bet. Good luck!</p>
<div class="game-over" id="game-over" hidden>
<h2>GAME OVER</h2>
<p>Fresh luck starts with a fresh stack.</p>
<button id="play-again" type="button">Play Again</button>
</div>
</section>
<aside class="paytable" aria-labelledby="paytable-title">
<h2 id="paytable-title">THE PAY TABLE</h2>
<p>Winning payouts × your current bet</p>
<ul>
<li data-pay="seven"><div><span class="combo" aria-hidden="true">7️⃣ 7️⃣ 7️⃣</span><span class="name">Three sevens · JACKPOT</span></div><strong>100×</strong></li>
<li data-pay="star"><div><span class="combo" aria-hidden="true">⭐ ⭐ ⭐</span><span class="name">Three stars</span></div><strong>50×</strong></li>
<li data-pay="bell"><div><span class="combo" aria-hidden="true">🔔 🔔 🔔</span><span class="name">Three bells</span></div><strong>20×</strong></li>
<li data-pay="grape"><div><span class="combo" aria-hidden="true">🍇 🍇 🍇</span><span class="name">Three grapes</span></div><strong>15×</strong></li>
<li data-pay="orange"><div><span class="combo" aria-hidden="true">🍊 🍊 🍊</span><span class="name">Three oranges</span></div><strong>10×</strong></li>
<li data-pay="lemon"><div><span class="combo" aria-hidden="true">🍋 🍋 🍋</span><span class="name">Three lemons</span></div><strong>5×</strong></li>
<li data-pay="cherry"><div><span class="combo" aria-hidden="true">🍒 🍒 🍒</span><span class="name">Three cherries</span></div><strong>3×</strong></li>
<li data-pay="two-cherries"><div><span class="combo" aria-hidden="true">🍒 🍒 —</span><span class="name">First two cherries*</span></div><strong>2×</strong></li>
</ul>
<p class="note">*First two reels must be cherries. Three cherries pay 3× instead. Highest matching payout only. All other outcomes pay 0×.</p>
</aside>
</main>
<footer>Just for fun · Virtual credits only · No real money</footer>
</div>
<script>
"use strict";
const SYMBOLS = [
{ emoji: "🍒", name: "Cherry", key: "cherry", multiplier: 3, title: "THREE CHERRIES" },
{ emoji: "🍋", name: "Lemon", key: "lemon", multiplier: 5, title: "THREE LEMONS" },
{ emoji: "🍊", name: "Orange", key: "orange", multiplier: 10, title: "THREE ORANGES" },
{ emoji: "🍇", name: "Grape", key: "grape", multiplier: 15, title: "THREE GRAPES" },
{ emoji: "🔔", name: "Bell", key: "bell", multiplier: 20, title: "THREE BELLS" },
{ emoji: "⭐", name: "Star", key: "star", multiplier: 50, title: "THREE STARS" },
{ emoji: "7️⃣", name: "Seven", key: "seven", multiplier: 100, title: "JACKPOT!" }
];
const BETS = [1, 5, 10];
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
const el = {
credits: document.getElementById("credits"),
bet: document.getElementById("current-bet"),
spin: document.getElementById("spin"),
headline: document.getElementById("headline"),
feedback: document.getElementById("feedback"),
hint: document.getElementById("hint"),
frame: document.getElementById("reel-frame"),
reelGroup: document.getElementById("reels"),
reels: Array.from(document.querySelectorAll(".reel")),
bets: Array.from(document.querySelectorAll(".bet-button")),
rows: Array.from(document.querySelectorAll("[data-pay]")),
gameOver: document.getElementById("game-over"),
playAgain: document.getElementById("play-again")
};
let state;
let creditFrame = 0;
let displayedCredits = 100;
function initState() {
state = {
credits: 100,
bet: 1,
spinning: false,
reels: [0, 5, 6],
result: null
};
cancelAnimationFrame(creditFrame);
displayedCredits = state.credits;
el.credits.textContent = state.credits;
clearFeedback();
state.reels.forEach((symbol, index) => showReel(index, symbol));
updateUI();
}
function makeSymbol(index) {
const cell = document.createElement("div");
cell.className = "symbol";
cell.textContent = SYMBOLS[index].emoji;
return cell;
}
function showReel(index, symbol) {
const reel = el.reels[index];
const strip = reel.querySelector(".strip");
strip.replaceChildren(makeSymbol(symbol));
strip.style.transform = "translateY(0)";
reel.classList.remove("rolling");
reel.setAttribute("aria-label", `Reel ${index + 1}: ${SYMBOLS[symbol].name}`);
}
function clearFeedback() {
el.frame.classList.remove("shake");
el.reels.forEach(reel => reel.classList.remove("winner"));
el.rows.forEach(row => row.classList.remove("hit"));
el.feedback.classList.remove("win");
}
function animateCredits(from, to) {
cancelAnimationFrame(creditFrame);
const start = performance.now();
function tick(now) {
const progress = Math.min((now - start) / 400, 1);
displayedCredits = Math.round(from + (to - from) * progress);
el.credits.textContent = displayedCredits;
if (progress < 1) creditFrame = requestAnimationFrame(tick);
}
creditFrame = requestAnimationFrame(tick);
}
function checkWin(reels = state.reels) {
const [a, b, c] = reels;
if (a === b && b === c) {
const symbol = SYMBOLS[a];
return {
key: symbol.key,
name: symbol.title,
multiplier: symbol.multiplier,
matching: [0, 1, 2]
};
}
if (a === 0 && b === 0) {
return {
key: "two-cherries",
name: "TWO CHERRIES",
multiplier: 2,
matching: [0, 1]
};
}
return { key: null, name: "TRY YOUR LUCK AGAIN", multiplier: 0, matching: [] };
}
function animateReels(targets) {
// Non-overlapping time ranges keep each reel's stop order predictable.
const durations = [800, 1200, 1600].map(base => base + Math.random() * 140 - 70);
return Promise.all(el.reels.map((reel, index) => new Promise(resolve => {
const duration = durations[index];
const strip = reel.querySelector(".strip");
const initial = state.reels[index];
const target = targets[index];
reel.classList.add("rolling");
reel.setAttribute("aria-label", `Reel ${index + 1}: spinning`);
function finish() {
state.reels[index] = target;
showReel(index, target);
resolve();
}
if (reducedMotion.matches) {
// Keep the staggered reveals without scrolling for reduced-motion users.
window.setTimeout(finish, duration);
return;
}
const snapDuration = 130;
const scrollingDuration = duration - snapDuration;
const scrollingSteps = scrollingDuration * 12 / 1000;
const finalStep = Math.ceil(scrollingSteps) + 1;
const cells = [];
for (let step = 0; step <= finalStep + 1; step++) {
const symbol = step === finalStep ? target : (initial + step) % SYMBOLS.length;
cells.push(makeSymbol(symbol));
}
strip.replaceChildren(...cells);
const start = performance.now();
function tick(now) {
const elapsed = now - start;
if (elapsed >= duration) {
finish();
return;
}
let position;
if (elapsed < scrollingDuration) {
position = elapsed * 12 / 1000;
} else {
const progress = (elapsed - scrollingDuration) / snapDuration;
const easeOut = 1 - Math.pow(1 - progress, 3);
position = scrollingSteps + (finalStep - scrollingSteps) * easeOut;
}
// Read the live cell height so resizing cannot misalign the payline.
const height = reel.clientHeight;
strip.style.transform = `translateY(${-position * height}px)`;
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
})));
}
async function spin() {
if (state.spinning || state.credits < state.bet) return;
state.spinning = true;
state.result = null;
clearFeedback();
const wager = state.bet;
state.credits -= wager;
// Deduct immediately; count up any payout after all reels have settled.
cancelAnimationFrame(creditFrame);
displayedCredits = state.credits;
el.credits.textContent = state.credits;
updateUI();
const targets = el.reels.map(() => Math.floor(Math.random() * SYMBOLS.length));
await animateReels(targets);
const result = checkWin();
result.amount = wager * result.multiplier;
state.result = result;
const previousBalance = state.credits;
state.credits += result.amount;
state.spinning = false;
if (result.amount > 0) {
result.matching.forEach(index => el.reels[index].classList.add("winner"));
const row = el.rows.find(item => item.dataset.pay === result.key);
if (row) row.classList.add("hit");
animateCredits(previousBalance, state.credits);
} else {
el.frame.classList.add("shake");
}
updateUI();
if (state.credits < Math.min(...BETS)) {
el.playAgain.focus();
}
}
function updateUI() {
const gameOver = !state.spinning && !BETS.some(bet => state.credits >= bet);
const canCoverBet = state.credits >= state.bet;
el.bet.textContent = state.bet;
el.spin.disabled = state.spinning || !canCoverBet || gameOver;
el.spin.textContent = state.spinning ? "SPINNING…" : "SPIN";
el.reelGroup.setAttribute("aria-busy", String(state.spinning));
el.gameOver.hidden = !gameOver;
el.bets.forEach(button => {
const bet = Number(button.dataset.bet);
const selected = bet === state.bet;
button.classList.toggle("active", selected);
button.setAttribute("aria-pressed", String(selected));
button.disabled = state.spinning || state.credits < bet;
});
el.feedback.classList.toggle("win", !!state.result?.amount);
if (state.spinning) {
el.headline.textContent = "LET THE GOOD TIMES ROLL";
el.feedback.textContent = "Your lucky combination is on its way…";
el.hint.textContent = "Reels in motion…";
} else {
if (state.result) {
el.headline.textContent = gameOver ? "GAME OVER" : state.result.name;
el.feedback.textContent = state.result.amount
? `+${state.result.amount} credits`
: gameOver
? "Out of credits. Play again for a fresh 100."
: "No match this time. Take another spin!";
} else {
el.headline.textContent = "FEELING LUCKY?";
el.feedback.textContent = "Choose your bet and give it a spin.";
}
el.hint.textContent = gameOver
? "Play Again resets your balance to 100 credits."
: !canCoverBet
? "Choose a smaller bet to keep playing."
: "Up to 100× your bet. Good luck!";
}
}
el.spin.addEventListener("click", spin);
el.bets.forEach(button => {
button.addEventListener("click", () => {
const bet = Number(button.dataset.bet);
if (state.spinning || state.credits < bet) return;
state.bet = bet;
updateUI();
});
});
el.playAgain.addEventListener("click", () => {
if (state.spinning) return;
initState();
el.spin.focus();
});
initState();
</script>
</body>
</html>