Skip to content
mager-bench1.3

GPT-6 Astra × slots

8.7

Vegas 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 addEventListener

run 1 of 1

8.7
correctness 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>