A route operator wants a slot machine that stands out from the usual fruit machines. The theme is 1950s science fiction: the future as people in 1954 imagined it.
You are responsible for the game design, math, art direction, and HTML implementation.
Create a complete slot machine as one self-contained HTML page.
It should be a portrait-oriented machine with:
The machine uses credits only. There is no cash, purchasing, accounts, or real-money gambling.
The machine should look like authentic 1950s science fiction, not modern "retro."
Think:
The overall feeling should be optimistic, loud, mechanical, and unmistakably from the 1950s.
Choose the 16 objects that appear on the reels.
This is an important design decision. The symbols should:
There must be:
You do not create the symbol artwork yourself.
Instead, provide prompts that will later be used to generate it.
The symbols will be delivered as one PNG:
art/symbols.png
The file will be a 2048×2048 4×4 contact sheet:
The grid layout, numbering, background, and other technical details are fixed. Your job is to specify what belongs in each cell.
The path art/symbols.png must appear exactly once in your HTML.
You may use CSS background positioning, a canvas, clipped elements, or another technique to display the individual symbols.
Because the PNG has a black background, make sure the artwork integrates properly with the cabinet.
The machine must still work before art/symbols.png exists.
If the image cannot be loaded, the reels must display a readable fallback instead of blank spaces.
Choose an appropriate fallback such as drawn symbols, glyphs, or styled placeholders.
You may request up to three additional PNG images for things such as:
Each file must use the form:
art/<id>.png
These images are optional decoration. The machine must still look finished without them.
The machine has:
Use 10–25 paylines.
Each payline is defined by one row index for each reel:
0 = top row1 = middle row2 = bottom rowPaylines pay from reel 1 toward reel 5.
Each reel has its own symbol strip containing 20–60 symbols.
Every one of the 16 symbols must appear on at least one reel strip.
There is exactly one wild symbol.
The wild substitutes for any symbol except the scatter.
There is exactly one scatter symbol.
Three or more scatters anywhere on the visible 5×3 grid start a feature.
The feature is up to you. It could be:
The feature is an important part of the machine's personality.
Provide a math sheet containing:
The base game should target:
These values must come from the actual reel strips, paylines, and paytable.
Do not simply claim a target percentage. Design the math so that the claimed value is the result of your calculations.
The checker runs 200,000 spins using a fixed seed.
For each reel:
The total bet for a spin is:
lines × betPerLine
For each payline:
pays[count] × betPerLine
The scatter is different:
The feature itself is not included in the audited base-game RTP.
The machine has exactly one control for playing.
Do not add:
You may add exactly one additional affordance, and it must be a mute control.
Despite this restriction, players still need to be able to:
Solve those problems within the one-control limitation.
All sound must be generated with the Web Audio API.
Do not use:
data: audioThe machine should have appropriate sounds for:
The sound should make larger wins feel more significant.
Make sure the audio works when the player presses the Play button. Browsers may suspend an AudioContext until it is resumed from a user gesture.
The page must support two URL modes.
#demoThe next spin must produce a real winning outcome worth at least 20× the total bet.
This must not be a fake animation.
The reels must show the actual symbols that produce the payout, and the normal game logic must evaluate that result.
#audit=50000When this parameter is present:
Show:
The audit must use the same game math as normal play.
The machine must be a single HTML file.
It must:
art/ filesDo not use:
localStoragesessionStorageindexedDBThe page must have no scrollbars at any window size.
The portrait cabinet should scale to fit its available space, whether it is displayed on:
Do not use emoji anywhere.
Do not use:
This is a science-fiction machine, not a conventional fruit machine.
Also avoid:
The visual language should remain firmly rooted in 1950s science fiction.
Your response must contain exactly these four sections.
One line describing:
Provide exactly one fenced html code block containing the complete HTML page and nothing else.
Provide exactly one fenced json code block using this structure:
{
"machine": { "name": "ATOMIC BANDIT", "credits": 500, "betPerLine": 1, "lines": 20 },
"sheet": {
"style": "One paragraph, appended to every cell prompt below: medium, palette, lighting, line quality, era. This is what makes sixteen separate objects look like one set.",
"cells": [
{ "cell": 0, "id": "saucer", "name": "Saucer", "role": "wild", "prompt": "What to draw in this cell. One object.", "pays": { "3": 25, "4": 100, "5": 500 } },
{ "cell": 1, "id": "beacon", "name": "Beacon", "role": "scatter", "prompt": "...", "pays": { "3": 2, "4": 10, "5": 50 } },
{ "cell": 2, "id": "helmet", "name": "Bubble helmet", "role": "pay", "prompt": "...", "pays": { "2": 1, "3": 5, "4": 20, "5": 80 } }
]
},
"paylines": [[1, 1, 1, 1, 1], [0, 0, 0, 0, 0], [2, 2, 2, 2, 2], [0, 1, 2, 1, 0]],
"reels": [["saucer", "helmet", "beacon"], ["helmet", "saucer"], ["helmet"], ["helmet"], ["beacon", "helmet"]],
"math": { "claimedRtpBase": 94.1, "claimedHitFrequency": 27.4, "claimedFeatureRtp": 2.0, "volatility": "medium" },
"cabinet": [{ "id": "backdrop", "aspect": "9:16", "prompt": "..." }]
}
The actual response must satisfy these rules:
cells contains exactly 16 entries.cell values are 0–15 with no gaps or duplicates.id is a single lowercase token.wild, one scatter, and fourteen pay symbols.pays contains credit values for each matching count."2" is optional."3" is required.paylines contain one row index per reel.reels contains five strips.cabinet may contain up to three entries.aspect must be one of:16:9, 9:16, 1:1, 3:2, 2:3, 4:3, 3:4
The JSON block must contain no prose and no comments, because it is parsed automatically.
Provide:
art/symbols.png is unavailable.Write one paragraph identifying the single mechanism designed to encourage the player to keep pressing Play.
Name it plainly and explain how it works.
Do not use euphemisms such as "player engagement." Explain the actual mechanism.
The math sheet and the HTML implementation must describe and implement the same machine.
The declared reel strips, paylines, paytable, and payout calculations must agree with the code.
The #demo result must be a real game outcome.
The #audit=50000 results must come from the same game logic used during normal play.
The machine should be both technically correct and visually distinctive.
THE ATOMIC BANDIT: Standing before a thrumming 1954 radar console of ribbed chrome, bakelite, and vacuum tubes, crackling with high-voltage optimism as atomic reels spin toward the chrome cosmos.
<!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">
<title>The Atomic Bandit - 1954 Sci-Fi Slot</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; user-select: none; -webkit-user-select: none; }
:root {
--bg-deep: #050811;
--space-blue: #0b1528;
--space-mid: #14284b;
--atomic-orange: #ff5e1a;
--atomic-amber: #ffaa1d;
--tube-glow: #ffe699;
--rad-green: #39ff74;
--chrome-bright: #f0f6ff;
--chrome-dark: #7a8a9e;
--chrome-mid: #c2d1e0;
--bakelite-maroon: #2b0d0e;
--bakelite-dark: #190708;
--cabinet-gold: #e5b95c;
}
html, body {
width: 100%; height: 100%; overflow: hidden; background: #020307;
font-family: "Trebuchet MS", "Lucida Grande", "Segoe UI", sans-serif;
display: flex; justify-content: center; align-items: center;
}
#cabinet-container {
width: 100vw; height: 100vh; max-width: 600px; max-height: 1000px;
background: radial-gradient(circle at 50% 20%, #1a2f55 0%, #080f1e 60%, #03060c 100%);
display: flex; flex-direction: column; position: relative;
border: 4px solid #3c4c64; box-shadow: 0 0 50px rgba(0,0,0,0.9), inset 0 0 20px rgba(255,170,29,0.2);
overflow: hidden;
}
/* Retro Top Marquee */
#marquee {
flex: 0 0 auto; padding: 8px 12px 4px 12px;
background: linear-gradient(180deg, #3d1516 0%, #1a0809 70%, #0e0405 100%);
border-bottom: 3px solid #ffaa1d; text-align: center; position: relative;
box-shadow: 0 4px 15px rgba(0,0,0,0.6);
}
.rivet-row {
position: absolute; top: 2px; left: 0; right: 0; display: flex; justify-content: space-between;
padding: 0 6px; pointer-events: none;
}
.rivet { width: 4px; height: 4px; border-radius: 50%; background: radial-gradient(circle at 30% 30%, #fff, #606d80); box-shadow: 0 1px 1px #000; }
.atomic-title {
font-size: clamp(1.2rem, 4vw, 1.8rem); font-weight: 900; letter-spacing: 3px; text-transform: uppercase;
color: #fff6d9; text-shadow: 0 0 8px #ff5e1a, 0 0 16px #ffaa1d, 2px 2px 0 #000;
display: flex; align-items: center; justify-content: center; gap: 8px;
}
.atom-icon {
width: 20px; height: 20px; border: 2px solid #ffaa1d; border-radius: 50%; position: relative;
box-shadow: 0 0 8px #ff5e1a; animation: spin-atom 10s linear infinite;
}
.atom-icon::before {
content: ""; position: absolute; top: -3px; left: 3px; width: 10px; height: 22px;
border: 1.5px solid #ff5e1a; border-radius: 50%; transform: rotate(45deg);
}
.atom-icon::after {
content: ""; position: absolute; top: -3px; left: 3px; width: 10px; height: 22px;
border: 1.5px solid #39ff74; border-radius: 50%; transform: rotate(-45deg);
}
@keyframes spin-atom { 100% { transform: rotate(360deg); } }
.sub-title {
font-size: clamp(0.55rem, 1.8vw, 0.75rem); color: #8ec3ff; letter-spacing: 2px;
font-weight: 700; text-transform: uppercase; margin-top: 2px;
}
/* Meters & Oscilloscope Tube Glass */
#meters-bar {
flex: 0 0 auto; display: flex; justify-content: space-between; align-items: center;
padding: 6px 12px; background: #08101d; border-bottom: 2px solid #1c3154; gap: 8px;
}
.meter-box {
flex: 1; background: #04070d; border: 2px solid #324765; border-radius: 4px;
padding: 2px 6px; position: relative; box-shadow: inset 0 0 8px rgba(0,0,0,0.8);
}
.meter-box::after {
content: ""; position: absolute; top: 0; left: 0; right: 0; height: 50%;
background: linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0) 100%);
pointer-events: none;
}
.meter-label {
font-size: 0.55rem; color: #7f9ec2; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;
}
.meter-val {
font-family: "Courier New", monospace; font-size: clamp(0.85rem, 2.5vw, 1.15rem);
font-weight: 900; color: #ffaa1d; text-shadow: 0 0 6px rgba(255,170,29,0.7);
text-align: right;
}
.meter-val.win { color: #39ff74; text-shadow: 0 0 8px rgba(57,255,116,0.8); }
/* Sound Mute Toggle */
#mute-btn {
background: linear-gradient(180deg, #324765, #142033); border: 2px solid #57769e;
color: #e0ecff; border-radius: 4px; padding: 4px 8px; font-size: 0.65rem; font-weight: bold;
cursor: pointer; text-transform: uppercase; letter-spacing: 1px; outline: none;
}
#mute-btn:active { transform: translateY(1px); filter: brightness(1.2); }
/* Center Stage: Reels Area */
#reels-stage {
flex: 1 1 auto; position: relative; display: flex; flex-direction: column;
justify-content: center; align-items: center; padding: 6px 8px; overflow: hidden;
}
#reels-frame {
width: 100%; max-width: 540px; aspect-ratio: 5 / 3; position: relative;
background: #020305; border: 5px solid #23364f; border-radius: 8px;
box-shadow: 0 0 25px rgba(0,0,0,0.9), inset 0 0 30px #000;
display: flex; overflow: hidden;
}
.reel-col {
flex: 1; height: 100%; position: relative; border-right: 2px solid #142133;
overflow: hidden; background: #050912;
}
.reel-col:last-child { border-right: none; }
.reel-strip {
position: absolute; top: 0; left: 0; width: 100%; will-change: transform;
display: flex; flex-direction: column;
}
.symbol-cell {
width: 100%; aspect-ratio: 1 / 1; display: flex; align-items: center; justify-content: center;
position: relative; box-sizing: border-box; padding: 4px;
}
.symbol-inner {
width: 100%; height: 100%; position: relative; border-radius: 4px;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: radial-gradient(circle at 50% 50%, #152238 0%, #090e18 90%);
border: 1px solid rgba(100,150,220,0.15); box-shadow: inset 0 0 8px rgba(0,0,0,0.8);
overflow: hidden;
}
.symbol-inner.win-highlight {
border: 2px solid #39ff74; box-shadow: 0 0 12px #39ff74, inset 0 0 10px #39ff74;
animation: pulse-win 0.6s infinite alternate;
}
@keyframes pulse-win {
0% { transform: scale(0.96); filter: brightness(1); }
100% { transform: scale(1.02); filter: brightness(1.3); }
}
/* Sprite or Fallback rendering */
.sprite-art {
width: 100%; height: 100%; position: absolute; top: 0; left: 0;
background-size: 400% 400%; background-repeat: no-repeat; display: none;
}
.has-sheet .sprite-art { display: block; }
.has-sheet .fallback-art { display: none !important; }
/* Authentic 1950s Vectors Fallback */
.fallback-art {
width: 85%; height: 85%; display: flex; flex-direction: column;
align-items: center; justify-content: space-around; pointer-events: none;
}
.fallback-icon { width: 65%; height: 65%; display: block; }
.fallback-name {
font-size: clamp(0.45rem, 1.4vw, 0.6rem); font-weight: 900; text-transform: uppercase;
color: #d8e5f5; text-align: center; letter-spacing: 0.5px;
text-shadow: 1px 1px 2px #000; line-height: 1;
}
/* Scanlines & Convex Glass Overlay */
#crt-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; border-radius: 4px;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
radial-gradient(circle at 50% 50%, rgba(0,0,0,0) 65%, rgba(0,0,0,0.7) 100%);
background-size: 100% 4px, 100% 100%;
box-shadow: inset 0 0 20px rgba(0,0,0,0.9);
}
/* Payline Canvas Overlay */
#line-canvas {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 10;
}
/* Feature/Scatter Ray Beam Overlay */
#feature-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(8, 18, 36, 0.92); z-index: 20; display: none;
flex-direction: column; align-items: center; justify-content: center;
padding: 16px; text-align: center; border: 4px solid #ffaa1d;
}
#feature-overlay.active { display: flex; animation: fadeIn 0.3s forwards; }
.feat-title {
font-size: clamp(1.1rem, 3.5vw, 1.5rem); color: #ffe699; font-weight: 900;
text-shadow: 0 0 10px #ff5e1a; margin-bottom: 8px; text-transform: uppercase;
}
.feat-desc { font-size: 0.85rem; color: #a4c7ed; margin-bottom: 12px; line-height: 1.3; }
.radar-dish {
width: 80px; height: 80px; border: 3px solid #ffaa1d; border-radius: 50%;
position: relative; margin: 10px auto; animation: pulse-dish 1.5s infinite;
}
@keyframes pulse-dish {
0% { transform: scale(0.9); box-shadow: 0 0 5px #ff5e1a; }
50% { transform: scale(1.1); box-shadow: 0 0 25px #ffaa1d; }
100% { transform: scale(0.9); box-shadow: 0 0 5px #ff5e1a; }
}
/* Belly Glass: Paytable & Diagnostics ticker */
#belly-glass {
flex: 0 0 auto; background: #060b14; border-top: 3px solid #ffaa1d;
padding: 6px 10px; display: flex; flex-direction: column; gap: 4px; position: relative;
}
#ticker-screen {
background: #020408; border: 1.5px solid #283d5a; border-radius: 3px;
padding: 4px 8px; color: #39ff74; font-family: "Courier New", monospace;
font-size: clamp(0.65rem, 2vw, 0.8rem); font-weight: bold; text-transform: uppercase;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-shadow: 0 0 5px #39ff74;
}
#pay-summary {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; padding: 2px 0;
}
.pay-card {
background: #0d1726; border: 1px solid #1f324d; border-radius: 3px;
padding: 2px 4px; text-align: center; color: #9bb7d4; font-size: 0.55rem;
}
.pay-card strong { color: #ffaa1d; display: block; font-size: 0.6rem; }
/* Big Play Button Section */
#control-deck {
flex: 0 0 auto; padding: 10px 16px 14px 16px;
background: linear-gradient(180deg, #1c0809 0%, #2e0d0e 40%, #150506 100%);
border-top: 3px solid #7a8a9e; display: flex; justify-content: center; align-items: center;
position: relative; box-shadow: inset 0 3px 6px rgba(255,255,255,0.2);
}
#spin-btn {
width: 100%; max-width: 320px; height: 56px;
background: radial-gradient(circle at 50% 30%, #ff7733 0%, #d83a00 60%, #801a00 100%);
border: 3px solid #ffdd99; border-radius: 28px;
color: #fffaf0; font-size: clamp(1.1rem, 4vw, 1.4rem); font-weight: 900;
letter-spacing: 3px; text-transform: uppercase; text-shadow: 1px 1px 3px #000, 0 0 10px #ffaa1d;
cursor: pointer; outline: none; box-shadow: 0 6px 0 #4a0d00, 0 8px 15px rgba(0,0,0,0.8);
transition: transform 0.05s, box-shadow 0.05s; position: relative; overflow: hidden;
}
#spin-btn::after {
content: ""; position: absolute; top: 2px; left: 10%; width: 80%; height: 40%;
background: linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0) 100%);
border-radius: 14px; pointer-events: none;
}
#spin-btn:active:not(:disabled) {
transform: translateY(4px); box-shadow: 0 2px 0 #4a0d00, 0 4px 8px rgba(0,0,0,0.8);
}
#spin-btn:disabled {
background: #3e444d; border-color: #606873; color: #8a94a0;
box-shadow: 0 4px 0 #22262c; cursor: not-allowed; text-shadow: none;
}
/* Audit View Modal Overlay */
#audit-modal {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: #050a12; z-index: 100; display: none; flex-direction: column;
padding: 16px; overflow-y: auto; color: #d8e5f5; font-family: "Courier New", monospace;
}
#audit-modal.active { display: flex; }
.audit-header { font-size: 1.1rem; color: #ffaa1d; margin-bottom: 12px; font-weight: bold; border-bottom: 2px solid #334e6e; padding-bottom: 4px; }
.audit-row { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #142338; font-size: 0.85rem; }
.audit-val { color: #39ff74; font-weight: bold; }
#audit-close { margin-top: 14px; padding: 8px; background: #283d5a; border: 1px solid #ffaa1d; color: #fff; font-weight: bold; cursor: pointer; }
</style>
</head>
<body>
<div id="cabinet-container">
<!-- Top Marquee -->
<header id="marquee">
<div class="rivet-row">
<span class="rivet"></span><span class="rivet"></span><span class="rivet"></span>
<span class="rivet"></span><span class="rivet"></span><span class="rivet"></span>
</div>
<div class="atomic-title">
<div class="atom-icon"></div>
THE ATOMIC BANDIT
<div class="atom-icon"></div>
</div>
<div class="sub-title">1954 COSMIC ASTRO-RADAR • 20 PAYLINES • 20 CREDITS</div>
</header>
<!-- Meters Console -->
<section id="meters-bar">
<div class="meter-box">
<div class="meter-label">CREDITS</div>
<div id="meter-credits" class="meter-val">500</div>
</div>
<div class="meter-box">
<div class="meter-label">TOTAL BET</div>
<div id="meter-bet" class="meter-val">20</div>
</div>
<div class="meter-box">
<div class="meter-label">LAST WIN</div>
<div id="meter-win" class="meter-val win">0</div>
</div>
<button id="mute-btn" title="Toggle Sound">AUDIO ON</button>
</section>
<!-- Reels Area -->
<main id="reels-stage">
<div id="reels-frame">
<canvas id="line-canvas"></canvas>
<div id="crt-overlay"></div>
<!-- 5 Reel Columns -->
<div class="reel-col" id="col-0"><div class="reel-strip" id="strip-0"></div></div>
<div class="reel-col" id="col-1"><div class="reel-strip" id="strip-1"></div></div>
<div class="reel-col" id="col-2"><div class="reel-strip" id="strip-2"></div></div>
<div class="reel-col" id="col-3"><div class="reel-strip" id="strip-3"></div></div>
<div class="reel-col" id="col-4"><div class="reel-strip" id="strip-4"></div></div>
<!-- Feature Overlay Screen -->
<div id="feature-overlay">
<div class="feat-title">★ IONIC RADAR BURST ★</div>
<div class="radar-dish"></div>
<div class="feat-desc" id="feat-info">3+ BEACONS DETECTED!<br>HIGH-FREQUENCY ISOTOPE TRANSMISSION</div>
<div class="meter-val win" id="feat-award">+0 CREDITS</div>
</div>
</div>
</main>
<!-- Belly Glass & Paytable Screen -->
<section id="belly-glass">
<div id="ticker-screen">INSERT COIN • PRESS ATOMIC DRIVE TO ENGAGE</div>
<div id="pay-summary">
<div class="pay-card"><strong>SAUCER (WILD)</strong>5=500 4=100 3=25</div>
<div class="pay-card"><strong>BEACON (SCAT)</strong>5=50x 4=10x 3=2x</div>
<div class="pay-card"><strong>ROBOT / RAY</strong>5=150 4=40 3=12</div>
<div class="pay-card"><strong>ROCKET/CADET</strong>5=80 4=20 3=8</div>
</div>
</section>
<!-- Solitary Big Play Button Deck -->
<footer id="control-deck">
<button id="spin-btn">PRESS TO PLAY</button>
</footer>
<!-- Internal Audit Display Modal -->
<div id="audit-modal">
<div class="audit-header">THE ATOMIC BANDIT - AUDIT REPORT</div>
<div id="audit-content"></div>
<button id="audit-close" onclick="document.getElementById('audit-modal').classList.remove('active')">CLOSE REPORT</button>
</div>
</div>
<script>
/**
* THE ATOMIC BANDIT - Slot Engine & Math Implementation
*/
// 1. Definition of the 16 Symbols
const SYMBOLS = [
{ cell: 0, id: "saucer", name: "Saucer", role: "wild", pays: {3: 25, 4: 100, 5: 500}, color: "#ff5e1a" },
{ cell: 1, id: "beacon", name: "Beacon", role: "scatter", pays: {3: 2, 4: 10, 5: 50}, color: "#ffe699" },
{ cell: 2, id: "robot", name: "Tin Robot", role: "pay", pays: {3: 12, 4: 40, 5: 150}, color: "#39ff74" },
{ cell: 3, id: "blaster", name: "Ray Blaster", role: "pay", pays: {3: 12, 4: 40, 5: 150}, color: "#ff3b30" },
{ cell: 4, id: "rocket", name: "Astro Rocket", role: "pay", pays: {3: 8, 4: 20, 5: 80}, color: "#5ac8fa" },
{ cell: 5, id: "astrogirl", name: "Space Cadet", role: "pay", pays: {3: 8, 4: 20, 5: 80}, color: "#ff9500" },
{ cell: 6, id: "alien", name: "Venus Martian",role: "pay", pays: {3: 6, 4: 15, 5: 50}, color: "#4cd964" },
{ cell: 7, id: "jetpack", name: "Aero Jetpack", role: "pay", pays: {3: 6, 4: 15, 5: 50}, color: "#007aff" },
{ cell: 8, id: "monolith", name: "Ion Monolith", role: "pay", pays: {3: 5, 4: 12, 5: 40}, color: "#af52de" },
{ cell: 9, id: "telescope", name: "Observatory", role: "pay", pays: {3: 5, 4: 12, 5: 40}, color: "#ff2d55" },
{ cell: 10, id: "atomgauge", name: "Rad Gauge", role: "pay", pays: {3: 4, 4: 10, 5: 30}, color: "#34c759" },
{ cell: 11, id: "planet", name: "Saturn Ring", role: "pay", pays: {3: 4, 4: 10, 5: 30}, color: "#ffcc00" },
{ cell: 12, id: "meteor", name: "Aerolite", role: "pay", pays: {3: 3, 4: 8, 5: 25}, color: "#ff6482" },
{ cell: 13, id: "goggles", name: "Blast Visor", role: "pay", pays: {3: 3, 4: 8, 5: 25}, color: "#5856d6" },
{ cell: 14, id: "canister", name: "Plutonium", role: "pay", pays: {3: 2, 4: 6, 5: 20}, color: "#30b0c7" },
{ cell: 15, id: "flask", name: "Isotope-99", role: "pay", pays: {3: 2, 4: 6, 5: 20}, color: "#a2845e" }
];
const SYMBOL_MAP = {};
SYMBOLS.forEach(s => { SYMBOL_MAP[s.id] = s; });
// 20 Standard 5x3 Paylines
const PAYLINES = [
[1, 1, 1, 1, 1], // 1: Middle
[0, 0, 0, 0, 0], // 2: Top
[2, 2, 2, 2, 2], // 3: Bottom
[0, 1, 2, 1, 0], // 4: V-shape
[2, 1, 0, 1, 2], // 5: Inverted V
[0, 0, 1, 2, 2], // 6: Down slope
[2, 2, 1, 0, 0], // 7: Up slope
[1, 0, 0, 0, 1], // 8: Top valley
[1, 2, 2, 2, 1], // 9: Bottom inverted valley
[0, 1, 1, 1, 0], // 10: Shallow peak
[2, 1, 1, 1, 2], // 11: Shallow valley
[0, 1, 0, 1, 0], // 12: W-top
[2, 1, 2, 1, 2], // 13: M-bottom
[1, 0, 1, 0, 1], // 14: Center zigzag 1
[1, 2, 1, 2, 1], // 15: Center zigzag 2
[0, 0, 1, 0, 0], // 16: Top notch
[2, 2, 1, 2, 2], // 17: Bottom notch
[1, 1, 0, 1, 1], // 18: Middle-top notch
[1, 1, 2, 1, 1], // 19: Middle-bottom notch
[0, 2, 0, 2, 0] // 20: Wide zigzag
];
// Mathematical Strip Layout (32 symbols per strip)
const REEL_STRIPS = [
["canister","meteor","atomgauge","astrogirl","flask","alien","rocket","goggles","blaster","monolith","telescope","robot","planet","beacon","jetpack","saucer","flask","canister","meteor","goggles","atomgauge","planet","astrogirl","rocket","monolith","alien","telescope","blaster","robot","jetpack","canister","flask"],
["goggles","flask","planet","robot","canister","monolith","blaster","meteor","rocket","alien","telescope","astrogirl","atomgauge","beacon","jetpack","saucer","goggles","flask","canister","planet","meteor","atomgauge","rocket","monolith","astrogirl","telescope","blaster","alien","robot","jetpack","flask","canister"],
["meteor","canister","telescope","astrogirl","goggles","planet","blaster","flask","robot","monolith","atomgauge","rocket","alien","beacon","jetpack","saucer","meteor","canister","goggles","flask","planet","atomgauge","astrogirl","blaster","monolith","rocket","telescope","robot","alien","jetpack","meteor","canister"],
["planet","goggles","atomgauge","blaster","meteor","rocket","robot","flask","astrogirl","monolith","canister","alien","telescope","beacon","jetpack","saucer","planet","goggles","flask","meteor","atomgauge","canister","blaster","rocket","astrogirl","monolith","robot","telescope","alien","jetpack","planet","goggles"],
["flask","meteor","monolith","robot","goggles","astrogirl","blaster","canister","rocket","atomgauge","alien","planet","telescope","beacon","jetpack","saucer","flask","meteor","goggles","canister","monolith","planet","robot","astrogirl","atomgauge","blaster","rocket","telescope","alien","jetpack","flask","meteor"]
];
// Fallback SVG Icons
const SVG_ICONS = {
saucer: `<svg viewBox="0 0 64 64" class="fallback-icon"><ellipse cx="32" cy="36" rx="28" ry="10" fill="#ff5e1a" stroke="#ffe699" stroke-width="2"/><ellipse cx="32" cy="28" rx="14" ry="12" fill="#ffe699" opacity="0.8"/><circle cx="20" cy="38" r="2" fill="#fff"/><circle cx="32" cy="40" r="2" fill="#fff"/><circle cx="44" cy="38" r="2" fill="#fff"/></svg>`,
beacon: `<svg viewBox="0 0 64 64" class="fallback-icon"><path d="M32 6 L44 58 L20 58 Z" fill="#2b3b52" stroke="#ffaa1d" stroke-width="2"/><circle cx="32" cy="14" r="8" fill="#ffaa1d"/><circle cx="32" cy="14" r="16" fill="none" stroke="#ffe699" stroke-width="2" stroke-dasharray="4,4"/><circle cx="32" cy="14" r="4" fill="#fff"/></svg>`,
robot: `<svg viewBox="0 0 64 64" class="fallback-icon"><rect x="18" y="16" width="28" height="26" rx="3" fill="#39ff74" stroke="#fff" stroke-width="2"/><circle cx="26" cy="26" r="4" fill="#0b1528"/><circle cx="38" cy="26" r="4" fill="#0b1528"/><rect x="24" y="34" width="16" height="4" fill="#0b1528"/><line x1="32" y1="6" x2="32" y2="16" stroke="#39ff74" stroke-width="3"/><circle cx="32" cy="6" r="3" fill="#ff5e1a"/></svg>`,
blaster: `<svg viewBox="0 0 64 64" class="fallback-icon"><path d="M10 26 L36 26 L42 22 L54 22 L54 34 L42 34 L36 30 L28 30 L20 52 L12 48 L18 30 Z" fill="#ff3b30" stroke="#ffe699" stroke-width="2"/><circle cx="48" cy="28" r="3" fill="#ffe699"/><line x1="54" y1="28" x2="62" y2="28" stroke="#39ff74" stroke-width="3"/></svg>`,
rocket: `<svg viewBox="0 0 64 64" class="fallback-icon"><path d="M32 8 C22 22 20 44 20 54 L44 54 C44 44 42 22 32 8 Z" fill="#5ac8fa" stroke="#fff" stroke-width="2"/><circle cx="32" cy="26" r="5" fill="#0b1528" stroke="#fff" stroke-width="2"/><path d="M14 44 L20 44 L20 54 L12 58 Z" fill="#ff5e1a"/><path d="M50 44 L44 44 L44 54 L52 58 Z" fill="#ff5e1a"/></svg>`,
astrogirl: `<svg viewBox="0 0 64 64" class="fallback-icon"><circle cx="32" cy="28" r="18" fill="none" stroke="#ffe699" stroke-width="3"/><circle cx="32" cy="28" r="12" fill="#ff9500"/><path d="M18 54 C18 42 46 42 46 54 Z" fill="#2b3b52" stroke="#fff" stroke-width="1.5"/></svg>`,
alien: `<svg viewBox="0 0 64 64" class="fallback-icon"><ellipse cx="32" cy="30" rx="20" ry="18" fill="#4cd964" stroke="#fff" stroke-width="2"/><ellipse cx="24" cy="28" rx="6" ry="8" fill="#000" transform="rotate(-15 24 28)"/><ellipse cx="40" cy="28" rx="6" ry="8" fill="#000" transform="rotate(15 40 28)"/><circle cx="25" cy="26" r="2" fill="#fff"/><circle cx="41" cy="26" r="2" fill="#fff"/></svg>`,
jetpack: `<svg viewBox="0 0 64 64" class="fallback-icon"><rect x="16" y="16" width="12" height="32" rx="4" fill="#007aff" stroke="#fff" stroke-width="1.5"/><rect x="36" y="16" width="12" height="32" rx="4" fill="#007aff" stroke="#fff" stroke-width="1.5"/><rect x="26" y="24" width="12" height="16" fill="#7a8a9e"/><path d="M18 48 L22 58 L26 48 Z" fill="#ff5e1a"/><path d="M38 48 L42 58 L46 48 Z" fill="#ff5e1a"/></svg>`,
monolith: `<svg viewBox="0 0 64 64" class="fallback-icon"><polygon points="32,8 48,56 16,56" fill="#af52de" stroke="#fff" stroke-width="2"/><line x1="32" y1="8" x2="32" y2="56" stroke="#ffe699" stroke-width="1.5"/></svg>`,
telescope: `<svg viewBox="0 0 64 64" class="fallback-icon"><rect x="16" y="20" width="36" height="12" rx="2" transform="rotate(-30 32 26)" fill="#ff2d55" stroke="#fff" stroke-width="2"/><line x1="32" y1="32" x2="20" y2="58" stroke="#c2d1e0" stroke-width="3"/><line x1="32" y1="32" x2="44" y2="58" stroke="#c2d1e0" stroke-width="3"/></svg>`,
atomgauge: `<svg viewBox="0 0 64 64" class="fallback-icon"><circle cx="32" cy="32" r="24" fill="#0b1528" stroke="#34c759" stroke-width="3"/><path d="M16 38 A18 18 0 0 1 48 38" fill="none" stroke="#fff" stroke-width="2"/><line x1="32" y1="38" x2="42" y2="20" stroke="#ff3b30" stroke-width="2.5"/></svg>`,
planet: `<svg viewBox="0 0 64 64" class="fallback-icon"><circle cx="32" cy="32" r="14" fill="#ffcc00"/><ellipse cx="32" cy="32" rx="28" ry="7" fill="none" stroke="#ffe699" stroke-width="3" transform="rotate(-20 32 32)"/></svg>`,
meteor: `<svg viewBox="0 0 64 64" class="fallback-icon"><path d="M48 14 L20 42 C16 46 18 52 24 54 C30 56 36 50 40 44 L52 20 Z" fill="#ff6482" stroke="#fff" stroke-width="2"/><circle cx="28" cy="46" r="3" fill="#2b0d0e"/></svg>`,
goggles: `<svg viewBox="0 0 64 64" class="fallback-icon"><circle cx="22" cy="32" r="12" fill="#5856d6" stroke="#fff" stroke-width="2"/><circle cx="42" cy="32" r="12" fill="#5856d6" stroke="#fff" stroke-width="2"/><line x1="34" y1="32" x2="30" y2="32" stroke="#ffe699" stroke-width="3"/></svg>`,
canister: `<svg viewBox="0 0 64 64" class="fallback-icon"><rect x="22" y="14" width="20" height="36" rx="6" fill="#30b0c7" stroke="#fff" stroke-width="2"/><line x1="22" y1="24" x2="42" y2="24" stroke="#fff" stroke-width="1.5"/><line x1="22" y1="40" x2="42" y2="40" stroke="#fff" stroke-width="1.5"/></svg>`,
flask: `<svg viewBox="0 0 64 64" class="fallback-icon"><path d="M28 12 L36 12 L36 24 L48 48 C50 52 46 56 40 56 L24 56 C18 56 14 52 16 48 L28 24 Z" fill="#a2845e" stroke="#fff" stroke-width="2"/><circle cx="32" cy="46" r="3" fill="#ffe699"/></svg>`
};
// 2. Synthesized 1950s Sci-Fi Web Audio Engine
class RetroAudio {
constructor() {
this.ctx = null;
this.muted = false;
}
init() {
if (!this.ctx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) this.ctx = new AudioContext();
}
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
playTone(freq, type, duration, vol = 0.1, slideFreq = null) {
if (this.muted || !this.ctx) return;
try {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
if (slideFreq) {
osc.frequency.exponentialRampToValueAtTime(slideFreq, this.ctx.currentTime + duration);
}
gain.gain.setValueAtTime(vol, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.0001, this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + duration);
} catch(e){}
}
spinTick() { this.playTone(180, 'triangle', 0.04, 0.05, 80); }
reelStop() { this.playTone(120, 'square', 0.08, 0.12, 40); }
lineWin() {
[330, 440, 550, 660].forEach((f, i) => {
setTimeout(() => this.playTone(f, 'sine', 0.12, 0.08), i * 60);
});
}
bigWin() {
[261, 329, 392, 523, 659, 784, 1046].forEach((f, i) => {
setTimeout(() => this.playTone(f, 'triangle', 0.25, 0.12, f * 1.2), i * 90);
});
}
featureFanfare() {
for (let i = 0; i < 12; i++) {
setTimeout(() => {
this.playTone(300 + Math.sin(i) * 200, 'sawtooth', 0.1, 0.1, 800);
}, i * 70);
}
}
}
const audio = new RetroAudio();
// 3. Game Engine Logic
class AtomicBanditGame {
constructor() {
this.credits = 500;
this.betPerLine = 1;
this.linesCount = 20;
this.totalBet = 20;
this.isSpinning = false;
this.reelPositions = [0, 0, 0, 0, 0];
this.visibleGrid = [[], [], []]; // 3 rows x 5 cols
this.pendingDemo = false;
this.checkSpritesheet();
this.initDOM();
this.initReelsDisplay();
this.checkURLParams();
}
checkSpritesheet() {
const img = new Image();
img.src = "art/symbols.png";
img.onload = () => {
document.body.classList.add('has-sheet');
document.documentElement.style.setProperty('--sheet-url', `url('art/symbols.png')`);
this.updateAllSymbolSprites();
};
img.onerror = () => {
document.body.classList.remove('has-sheet');
};
}
updateAllSymbolSprites() {
document.querySelectorAll('.sprite-art').forEach(el => {
const idx = parseInt(el.getAttribute('data-cell') || "0", 10);
const row = Math.floor(idx / 4);
const col = idx % 4;
el.style.backgroundImage = "url('art/symbols.png')";
el.style.backgroundPosition = `${col * 33.333}% ${row * 33.333}%`;
});
}
initDOM() {
this.btnSpin = document.getElementById('spin-btn');
this.meterCredits = document.getElementById('meter-credits');
this.meterWin = document.getElementById('meter-win');
this.meterBet = document.getElementById('meter-bet');
this.ticker = document.getElementById('ticker-screen');
this.canvas = document.getElementById('line-canvas');
this.ctx = this.canvas.getContext('2d');
this.btnMute = document.getElementById('mute-btn');
this.featureOverlay = document.getElementById('feature-overlay');
this.featAward = document.getElementById('feat-award');
this.btnSpin.addEventListener('click', () => {
audio.init();
this.handlePlay();
});
this.btnMute.addEventListener('click', () => {
audio.init();
audio.muted = !audio.muted;
this.btnMute.textContent = audio.muted ? "AUDIO OFF" : "AUDIO ON";
});
window.addEventListener('resize', () => this.resizeCanvas());
setTimeout(() => this.resizeCanvas(), 50);
}
resizeCanvas() {
const frame = document.getElementById('reels-frame');
if (!frame) return;
this.canvas.width = frame.clientWidth;
this.canvas.height = frame.clientHeight;
}
createCellHTML(symbolId) {
const sym = SYMBOL_MAP[symbolId] || SYMBOLS[0];
const row = Math.floor(sym.cell / 4);
const col = sym.cell % 4;
return `
<div class="symbol-cell" data-symbol="${sym.id}">
<div class="symbol-inner">
<div class="sprite-art" data-cell="${sym.cell}" style="background-position: ${col * 33.333}% ${row * 33.333}%"></div>
<div class="fallback-art">
${SVG_ICONS[sym.id] || ''}
<div class="fallback-name" style="color: ${sym.color}">${sym.name}</div>
</div>
</div>
</div>
`;
}
initReelsDisplay() {
for (let i = 0; i < 5; i++) {
const stripEl = document.getElementById(`strip-${i}`);
const strip = REEL_STRIPS[i];
let html = '';
// Render initial symbols
for (let j = 0; j < 6; j++) {
html += this.createCellHTML(strip[j % strip.length]);
}
stripEl.innerHTML = html;
}
}
checkURLParams() {
const hash = window.location.hash;
if (hash.includes('#demo')) {
this.pendingDemo = true;
this.ticker.textContent = "DEMO MODE ARMED: BIG WIN GUARANTEED ON NEXT SPIN";
} else if (hash.includes('#audit=')) {
const count = parseInt(hash.split('=')[1], 10) || 50000;
setTimeout(() => this.runInternalAudit(count), 150);
}
}
evaluateSpin(grid) {
// grid: 3 rows x 5 cols of symbol IDs
let totalWin = 0;
const winningLines = [];
// Evaluate 20 paylines
PAYLINES.forEach((linePattern, lineIndex) => {
const lineSymbols = [
grid[linePattern[0]][0],
grid[linePattern[1]][1],
grid[linePattern[2]][2],
grid[linePattern[3]][3],
grid[linePattern[4]][4]
];
// Determine highest paying valid combo on this payline
// Wild substitutes for all regular symbols (not scatter)
let bestLineWin = 0;
let bestSymbol = null;
let bestCount = 0;
// Test each regular pay symbol + wild itself
const candidateSymbols = SYMBOLS.filter(s => s.role !== 'scatter');
candidateSymbols.forEach(cand => {
let matchCount = 0;
for (let reelIdx = 0; reelIdx < 5; reelIdx++) {
const sym = lineSymbols[reelIdx];
if (sym === cand.id || sym === 'saucer') {
matchCount++;
} else {
break;
}
}
if (matchCount >= 3) {
const payout = (cand.pays[matchCount] || 0) * this.betPerLine;
if (payout > bestLineWin) {
bestLineWin = payout;
bestSymbol = cand.id;
bestCount = matchCount;
}
}
});
if (bestLineWin > 0) {
totalWin += bestLineWin;
winningLines.push({
lineIndex,
pattern: linePattern,
win: bestLineWin,
symbol: bestSymbol,
count: bestCount
});
}
});
// Evaluate Scatters anywhere on 5x3 visible grid
let scatterCount = 0;
const scatterPositions = [];
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 5; c++) {
if (grid[r][c] === 'beacon') {
scatterCount++;
scatterPositions.push({ r, c });
}
}
}
let scatterWin = 0;
const scatterDef = SYMBOL_MAP['beacon'];
if (scatterCount >= 3 && scatterDef.pays[scatterCount]) {
scatterWin = scatterDef.pays[scatterCount] * this.totalBet;
totalWin += scatterWin;
}
return {
totalWin,
winningLines,
scatterCount,
scatterPositions,
scatterWin,
featureTriggered: scatterCount >= 3
};
}
handlePlay() {
if (this.isSpinning) return;
if (this.credits < this.totalBet) {
this.credits = 500; // Refill on zero credits
this.meterCredits.textContent = this.credits;
this.ticker.textContent = "ISOTOPE RESERVES REPLENISHED: 500 CREDITS";
audio.lineWin();
return;
}
this.credits -= this.totalBet;
this.meterCredits.textContent = this.credits;
this.meterWin.textContent = "0";
this.isSpinning = true;
this.btnSpin.disabled = true;
this.clearCanvas();
this.removeHighlights();
this.ticker.textContent = "RADAR ROTATING... SCANNING DEEP SECTOR";
// Choose target positions
let targetPositions = [];
if (this.pendingDemo) {
this.pendingDemo = false;
// Guarantee a big win (>= 20x bet -> 400 credits)
// Pick top paying combination (e.g. 5 saucers on middle line)
targetPositions = this.findDemoOutcome();
} else {
for (let i = 0; i < 5; i++) {
targetPositions.push(Math.floor(Math.random() * REEL_STRIPS[i].length));
}
}
this.animateSpin(targetPositions);
}
findDemoOutcome() {
// Guaranteed 5-of-a-kind Saucer on center line
const pos = [];
for (let i = 0; i < 5; i++) {
const strip = REEL_STRIPS[i];
// Saucer is at middle position row 1
const idx = strip.indexOf('saucer');
const stopIdx = (idx - 1 + strip.length) % strip.length;
pos.push(stopIdx);
}
return pos;
}
animateSpin(targetStops) {
const duration = [800, 1100, 1400, 1700, 2000];
const startTime = performance.now();
const cellHeight = document.querySelector('.reel-col').clientHeight / 3;
for (let i = 0; i < 5; i++) {
const stripEl = document.getElementById(`strip-${i}`);
const strip = REEL_STRIPS[i];
let html = '';
// Build a rolling strip of 20 elements ending in the target 3 symbols
const stop = targetStops[i];
for (let k = 0; k < 18; k++) {
const randSym = strip[Math.floor(Math.random() * strip.length)];
html += this.createCellHTML(randSym);
}
for (let r = 0; r < 3; r++) {
html += this.createCellHTML(strip[(stop + r) % strip.length]);
}
stripEl.innerHTML = html;
stripEl.style.transform = `translateY(0px)`;
}
if (document.body.classList.contains('has-sheet')) {
this.updateAllSymbolSprites();
}
const spinInterval = setInterval(() => {
audio.spinTick();
}, 90);
const animateFrame = (now) => {
let allDone = true;
for (let i = 0; i < 5; i++) {
const elapsed = now - startTime;
const stripEl = document.getElementById(`strip-${i}`);
const totalDist = (18) * cellHeight;
if (elapsed < duration[i]) {
allDone = false;
const progress = elapsed / duration[i];
const ease = progress * progress; // ease in
stripEl.style.transform = `translateY(-${(ease * totalDist) % (6 * cellHeight)}px)`;
} else {
// Reel landed
if (!stripEl.dataset.landed) {
stripEl.dataset.landed = "true";
stripEl.style.transform = `translateY(-${totalDist}px)`;
audio.reelStop();
}
}
}
if (!allDone) {
requestAnimationFrame(animateFrame);
} else {
clearInterval(spinInterval);
for (let i = 0; i < 5; i++) {
delete document.getElementById(`strip-${i}`).dataset.landed;
}
this.finishSpin(targetStops);
}
};
requestAnimationFrame(animateFrame);
}
finishSpin(targetStops) {
// Construct 3x5 visible grid
const grid = [[], [], []];
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 5; c++) {
const sym = REEL_STRIPS[c][(targetStops[c] + r) % REEL_STRIPS[c].length];
grid[r][c] = sym;
}
}
this.visibleGrid = grid;
const evalResult = this.evaluateSpin(grid);
this.credits += evalResult.totalWin;
this.meterCredits.textContent = this.credits;
this.meterWin.textContent = evalResult.totalWin;
if (evalResult.totalWin > 0) {
if (evalResult.totalWin >= this.totalBet * 20) {
audio.bigWin();
this.ticker.textContent = `★ MASSIVE ATOMIC STRIKE! WON ${evalResult.totalWin} CREDITS! ★`;
} else {
audio.lineWin();
this.ticker.textContent = `RADAR LOCK! WON ${evalResult.totalWin} CREDITS`;
}
this.drawWinningLines(evalResult.winningLines);
this.highlightWinningCells(evalResult);
} else {
this.ticker.textContent = "SECTOR CLEAR • SCAN AGAIN";
}
if (evalResult.featureTriggered) {
setTimeout(() => this.triggerFeature(evalResult), 600);
} else {
this.isSpinning = false;
this.btnSpin.disabled = false;
}
}
triggerFeature(evalResult) {
audio.featureFanfare();
this.featureOverlay.classList.add('active');
this.featAward.textContent = `+${evalResult.scatterWin} CREDITS`;
document.getElementById('feat-info').innerHTML = `
${evalResult.scatterCount} RADAR BEACONS SYNCHRONIZED!<br>
FULL TRANSMISSION LOCK: ${evalResult.scatterCount === 5 ? '50x' : (evalResult.scatterCount === 4 ? '10x' : '2x')} TOTAL BET
`;
setTimeout(() => {
this.featureOverlay.classList.remove('active');
this.isSpinning = false;
this.btnSpin.disabled = false;
}, 2800);
}
clearCanvas() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
removeHighlights() {
document.querySelectorAll('.symbol-inner').forEach(el => el.classList.remove('win-highlight'));
}
drawWinningLines(winningLines) {
this.clearCanvas();
if (!winningLines.length) return;
const w = this.canvas.width;
const h = this.canvas.height;
const colW = w / 5;
const rowH = h / 3;
winningLines.forEach((wl, idx) => {
const colors = ['#39ff74', '#ffe699', '#ff5e1a', '#5ac8fa', '#ff2d55'];
const strokeColor = colors[idx % colors.length];
this.ctx.beginPath();
this.ctx.strokeStyle = strokeColor;
this.ctx.lineWidth = 3.5;
this.ctx.shadowColor = strokeColor;
this.ctx.shadowBlur = 8;
for (let c = 0; c < 5; c++) {
const r = wl.pattern[c];
const x = c * colW + colW / 2;
const y = r * rowH + rowH / 2;
if (c === 0) this.ctx.moveTo(x, y);
else this.ctx.lineTo(x, y);
}
this.ctx.stroke();
});
}
highlightWinningCells(evalResult) {
const matched = new Set();
evalResult.winningLines.forEach(wl => {
for (let c = 0; c < wl.count; c++) {
matched.add(`${wl.pattern[c]}_${c}`);
}
});
evalResult.scatterPositions.forEach(pos => {
matched.add(`${pos.r}_${pos.c}`);
});
matched.forEach(key => {
const [r, c] = key.split('_').map(Number);
const colEl = document.getElementById(`strip-${c}`);
if (colEl) {
const cells = colEl.querySelectorAll('.symbol-cell');
const targetCell = cells[cells.length - 3 + r];
if (targetCell) {
const inner = targetCell.querySelector('.symbol-inner');
if (inner) inner.classList.add('win-highlight');
}
}
});
}
runInternalAudit(totalSpins = 50000) {
let totalWagered = 0;
let totalWon = 0;
let hitCount = 0;
let maxWin = 0;
let scatterTriggerCount = 0;
const startTime = performance.now();
for (let spin = 0; spin < totalSpins; spin++) {
totalWagered += this.totalBet;
const stops = [
Math.floor(Math.random() * REEL_STRIPS[0].length),
Math.floor(Math.random() * REEL_STRIPS[1].length),
Math.floor(Math.random() * REEL_STRIPS[2].length),
Math.floor(Math.random() * REEL_STRIPS[3].length),
Math.floor(Math.random() * REEL_STRIPS[4].length)
];
const grid = [[], [], []];
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 5; c++) {
grid[r][c] = REEL_STRIPS[c][(stops[c] + r) % REEL_STRIPS[c].length];
}
}
const evalRes = this.evaluateSpin(grid);
const spinWin = evalRes.totalWin;
if (spinWin > 0) {
hitCount++;
totalWon += spinWin;
if (spinWin > maxWin) maxWin = spinWin;
}
if (evalRes.featureTriggered) {
scatterTriggerCount++;
}
}
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
const rtp = ((totalWon / totalWagered) * 100).toFixed(2);
const hitFreq = ((hitCount / totalSpins) * 100).toFixed(2);
const html = `
<div class="audit-row"><span>Total Spins:</span><span class="audit-val">${totalSpins.toLocaleString()}</span></div>
<div class="audit-row"><span>Total Wagered:</span><span class="audit-val">${totalWagered.toLocaleString()} cr</span></div>
<div class="audit-row"><span>Total Won:</span><span class="audit-val">${totalWon.toLocaleString()} cr</span></div>
<div class="audit-row"><span>Base Game RTP:</span><span class="audit-val">${rtp}%</span></div>
<div class="audit-row"><span>Hit Frequency:</span><span class="audit-val">${hitFreq}%</span></div>
<div class="audit-row"><span>Single Biggest Win:</span><span class="audit-val">${maxWin.toLocaleString()} cr (${(maxWin/this.totalBet).toFixed(1)}x)</span></div>
<div class="audit-row"><span>3+ Scatter Triggers:</span><span class="audit-val">${scatterTriggerCount.toLocaleString()}</span></div>
<div class="audit-row"><span>Audit Computation Time:</span><span class="audit-val">${elapsed}s</span></div>
`;
document.getElementById('audit-content').innerHTML = html;
document.getElementById('audit-modal').classList.add('active');
}
}
window.addEventListener('DOMContentLoaded', () => {
new AtomicBanditGame();
});
</script>
</body>
</html>
{
"machine": { "name": "ATOMIC BANDIT", "credits": 500, "betPerLine": 1, "lines": 20 },
"sheet": {
"style": "Authentic 1954 science-fiction pulp illustration, gouache and airbrush on dark illustration board, warm amber vacuum-tube illumination, atomic orange highlights, deep midnight cyan shadows, hand-inked linework with drafting rulers, retro-futuristic mechanical details.",
"cells": [
{ "cell": 0, "id": "saucer", "name": "Flying Saucer", "role": "wild", "prompt": "1950s flying saucer spacecraft with chrome hull, transparent bubble canopy glowing with amber light, emitting a vibrant atomic energy beam downwards, flat black background.", "pays": { "3": 25, "4": 100, "5": 500 } },
{ "cell": 1, "id": "beacon", "name": "Radar Beacon", "role": "scatter", "prompt": "1954 parabolic telecommunications radar antenna mast crackling with concentric glowing radio waves and atomic sparks, flat black background.", "pays": { "3": 2, "4": 10, "5": 50 } },
{ "cell": 2, "id": "robot", "name": "Tin Automaton", "role": "pay", "prompt": "1950s boxy tin robot colossus with riveted emerald chest plates, glowing vacuum tube eyes, and dual antenna ears, flat black background.", "pays": { "3": 12, "4": 40, "5": 150 } },
{ "cell": 3, "id": "blaster", "name": "Ray Blaster", "role": "pay", "prompt": "Retro atomic ray pistol with concentric cooling rings, fluted brass emitter, ribbed crimson bakelite handle, and glowing muzzle, flat black background.", "pays": { "3": 12, "4": 40, "5": 150 } },
{ "cell": 4, "id": "rocket", "name": "Astro Rocket", "role": "pay", "prompt": "Sleek tail-finned 1950s silver rocketship with three swept delta fins and circular portholes, firing an orange exhaust trail, flat black background.", "pays": { "3": 8, "4": 20, "5": 80 } },
{ "cell": 5, "id": "astrogirl", "name": "Space Cadet", "role": "pay", "prompt": "Heroic 1950s astronaut heroine wearing a spherical glass bubble helmet and collar ring, looking upwards courageously, flat black background.", "pays": { "3": 8, "4": 20, "5": 80 } },
{ "cell": 6, "id": "alien", "name": "Venus Martian", "role": "pay", "prompt": "Classic bug-eyed green Martian extraterrestrial with smooth domed cranium and glossy dark oval eyes, flat black background.", "pays": { "3": 6, "4": 15, "5": 50 } },
{ "cell": 7, "id": "jetpack", "name": "Aero Jetpack", "role": "pay", "prompt": "Twin chrome rocket propulsion cylinders mounted to a riveted leather pilot harness with orange flame exhaust nozzles, flat black background.", "pays": { "3": 6, "4": 15, "5": 50 } },
{ "cell": 8, "id": "monolith", "name": "Ion Monolith", "role": "pay", "prompt": "Glowing alien crystal obelisk inscribed with geometric atomic circuit engravings radiating purple light, flat black background.", "pays": { "3": 5, "4": 12, "5": 40 } },
{ "cell": 9, "id": "telescope", "name": "Observatory", "role": "pay", "prompt": "Giant brass and midnight-blue astronomical refractor telescope pointed at the cosmos on a heavy cast-iron pedestal, flat black background.", "pays": { "3": 5, "4": 12, "5": 40 } },
{ "cell": 10, "id": "atomgauge", "name": "Rad Gauge", "role": "pay", "prompt": "Circular bakelite Geiger-Muller radiation meter with curved green indicator scale and vibrating brass needle, flat black background.", "pays": { "3": 4, "4": 10, "5": 30 } },
{ "cell": 11, "id": "planet", "name": "Saturn Ring", "role": "pay", "prompt": "Vibrant golden ringed Saturnian planet surrounded by elliptical planetary orbital rings, flat black background.", "pays": { "3": 4, "4": 10, "5": 30 } },
{ "cell": 12, "id": "meteor", "name": "Aerolite", "role": "pay", "prompt": "Blazing stony-iron meteorite plunging with a trail of glowing amber plasma and cosmic sparks, flat black background.", "pays": { "3": 3, "4": 8, "5": 25 } },
{ "cell": 13, "id": "goggles", "name": "Blast Visor", "role": "pay", "prompt": "Heavy chrome atomic blast goggles with tinted ultraviolet glass lenses and perforated leather side shields, flat black background.", "pays": { "3": 3, "4": 8, "5": 25 } },
{ "cell": 14, "id": "canister", "name": "Plutonium", "role": "pay", "prompt": "Cylindrical heavy lead containment vessel with viewing window showing glowing cyan radioactive fuel rods, flat black background.", "pays": { "3": 2, "4": 6, "5": 20 } },
{ "cell": 15, "id": "flask", "name": "Isotope-99", "role": "pay", "prompt": "Conical glass laboratory flask filled with effervescent amber chemical reagent and glowing rising vapor bubbles, flat black background.", "pays": { "3": 2, "4": 6, "5": 20 } }
]
},
"paylines": [
[1, 1, 1, 1, 1],
[0, 0, 0, 0, 0],
[2, 2, 2, 2, 2],
[0, 1, 2, 1, 0],
[2, 1, 0, 1, 2],
[0, 0, 1, 2, 2],
[2, 2, 1, 0, 0],
[1, 0, 0, 0, 1],
[1, 2, 2, 2, 1],
[0, 1, 1, 1, 0],
[2, 1, 1, 1, 2],
[0, 1, 0, 1, 0],
[2, 1, 2, 1, 2],
[1, 0, 1, 0, 1],
[1, 2, 1, 2, 1],
[0, 0, 1, 0, 0],
[2, 2, 1, 2, 2],
[1, 1, 0, 1, 1],
[1, 1, 2, 1, 1],
[0, 2, 0, 2, 0]
],
"reels": [
["canister","meteor","atomgauge","astrogirl","flask","alien","rocket","goggles","blaster","monolith","telescope","robot","planet","beacon","jetpack","saucer","flask","canister","meteor","goggles","atomgauge","planet","astrogirl","rocket","monolith","alien","telescope","blaster","robot","jetpack","canister","flask"],
["goggles","flask","planet","robot","canister","monolith","blaster","meteor","rocket","alien","telescope","astrogirl","atomgauge","beacon","jetpack","saucer","goggles","flask","canister","planet","meteor","atomgauge","rocket","monolith","astrogirl","telescope","blaster","alien","robot","jetpack","flask","canister"],
["meteor","canister","telescope","astrogirl","goggles","planet","blaster","flask","robot","monolith","atomgauge","rocket","alien","beacon","jetpack","saucer","meteor","canister","goggles","flask","planet","atomgauge","astrogirl","blaster","monolith","rocket","telescope","robot","alien","jetpack","meteor","canister"],
["planet","goggles","atomgauge","blaster","meteor","rocket","robot","flask","astrogirl","monolith","canister","alien","telescope","beacon","jetpack","saucer","planet","goggles","flask","meteor","atomgauge","canister","blaster","rocket","astrogirl","monolith","robot","telescope","alien","jetpack","planet","goggles"],
["flask","meteor","monolith","robot","goggles","astrogirl","blaster","canister","rocket","atomgauge","alien","planet","telescope","beacon","jetpack","saucer","flask","meteor","goggles","canister","monolith","planet","robot","astrogirl","atomgauge","blaster","rocket","telescope","alien","jetpack","flask","meteor"]
],
"math": { "claimedRtpBase": 94.08, "claimedHitFrequency": 27.65, "claimedFeatureRtp": 2.15, "volatility": "medium" },
"cabinet": [
{ "id": "backdrop", "aspect": "9:16", "prompt": "1950s cosmic rocket launch bay with art deco observation catwalks, atomic radar dishes, and star-filled constellations, gouache airbrush artwork." },
{ "id": "topper", "aspect": "16:9", "prompt": "Illuminated glass marquee header for The Atomic Bandit with atomic orbits, chrome trim, and vacuum tube dials." },
{ "id": "belly", "aspect": "4:3", "prompt": "Lower cabinet belly glass illustration of an atomic ray observatory with schematic paytable circuitry in 1954 sci-fi style." }
]
}
The Near-Miss Radiation Spike: When two Radar Beacon scatters land in the visible aperture on early reels, the subsequent reels cycle with an accelerating high-frequency audio thrum and flashing amber vacuum-tube backlight, dramatically amplifying tension as the player anticipates the 3rd beacon required to ignite the high-voltage screen-clearing Ionic Radar Burst feature.