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.
ATOMIC BANDIT — a chrome-and-bakelite rocket-age dream machine that hums like a warm vacuum tube and promises that every single press of PLAY might put a golden comet across the middle row.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ATOMIC BANDIT — 1954 Dream Machine</title>
<style>
:root{
--blue:#081436; --blue2:#0d1d4e; --orange:#ff6b1a; --amber:#ffc257;
--cream:#f2e6c8; --chrome1:#e9e9ea; --chrome2:#8b8d94; --chrome3:#f8f8f9;
--red:#d8441c; --ink:#1a1208;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden;background:#02030a}
body{font-family:Futura,"Century Gothic",Arial,Helvetica,sans-serif;color:var(--cream)}
#stage{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
background-image:
radial-gradient(1px 1px at 12% 22%,#fff 50%,transparent 51%),
radial-gradient(1px 1px at 78% 12%,#fff 50%,transparent 51%),
radial-gradient(2px 2px at 33% 71%,#ffd 50%,transparent 51%),
radial-gradient(1px 1px at 64% 48%,#fff 50%,transparent 51%),
radial-gradient(1.5px 1.5px at 88% 79%,#fff 50%,transparent 51%),
radial-gradient(1px 1px at 5% 88%,#fff 50%,transparent 51%),
radial-gradient(1.5px 1.5px at 46% 8%,#fff 50%,transparent 51%),
radial-gradient(1px 1px at 20% 55%,#fff 50%,transparent 51%),
radial-gradient(2px 2px at 92% 33%,#ffd 50%,transparent 51%),
radial-gradient(1px 1px at 70% 92%,#fff 50%,transparent 51%);
background-color:#02030a;}
#machine{position:relative;width:620px;height:1000px;flex:none;
border-radius:38px 38px 26px 26px;border:6px solid #6d6f76;
background:
radial-gradient(2px 2px at 6% 4%, #c9ccd2 40%, transparent 45%),
radial-gradient(2px 2px at 94% 4%, #c9ccd2 40%, transparent 45%),
linear-gradient(180deg,#fdfdf 0,#dfe1e6 0,#9ba0a8 0,#4c4f57 100%);
background:linear-gradient(180deg,#e7e9ee 0%,#a9adb5 30%,#565a63 62%,#33363d 100%);
box-shadow:0 0 0 3px #22252b, 0 24px 70px rgba(0,0,0,.75), inset 0 0 60px rgba(0,0,0,.35);
overflow:hidden;}
/* halftone dot texture over the chrome */
#machine::after{content:"";position:absolute;inset:0;pointer-events:none;border-radius:32px;
background-image:radial-gradient(rgba(0,0,0,.14) 1.1px, transparent 1.2px);
background-size:9px 9px;mix-blend-mode:multiply;opacity:.5}
.rivets{position:absolute;inset:10px;pointer-events:none;border-radius:28px}
.rivets i{position:absolute;width:9px;height:9px;border-radius:50%;
background:radial-gradient(circle at 35% 30%,#fff 0,#b8babe 45%,#565861 100%);
box-shadow:0 1px 2px rgba(0,0,0,.6)}
.rivets i:nth-child(odd){background:radial-gradient(circle at 35% 30%,#fff 0,#d8b36a 45%,#6b4a1c 100%)}
#marquee{position:relative;height:118px;margin:14px 18px 0;border-radius:16px;border:4px solid #20232b;
background:
radial-gradient(2px 2px at 12% 30%,#fff 60%,transparent 61%),
radial-gradient(1.5px 1.5px at 30% 70%,#fff 60%,transparent 61%),
radial-gradient(2px 2px at 55% 25%,#ffd977 60%,transparent 61%),
radial-gradient(1.5px 1.5px at 72% 62%,#fff 60%,transparent 61%),
radial-gradient(2px 2px at 88% 35%,#fff 60%,transparent 61%),
linear-gradient(180deg,#17306e 0%,#0a1742 55%,#060f2c 100%);
box-shadow:inset 0 0 30px rgba(0,0,0,.7),0 4px 0 #1b1d22,0 8px 18px rgba(0,0,0,.5);
display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:hidden}
#marquee.hasImg{background-image:linear-gradient(rgba(6,13,38,.25),rgba(6,13,38,.55));}
#marquee h1{font-size:46px;letter-spacing:3px;color:var(--orange);position:relative;
text-shadow:0 0 2px #7c2c00,3px 3px 0 #4d1900,5px 5px 8px rgba(0,0,0,.6);
font-weight:900;font-style:italic}
#marquee h1 span{color:var(--amber)}
#marquee .sub{font-size:11px;letter-spacing:6px;color:#ffe9b0;margin-top:2px;
text-shadow:0 0 8px rgba(255,178,67,.7)}
#bigwin{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
font-size:44px;font-weight:900;letter-spacing:4px;color:#fff;text-shadow:0 0 16px #ff6b1a,0 0 40px #ffc257;
opacity:0;pointer-events:none;transition:opacity .15s;z-index:5}
#machine.tier1 #bigwin,#machine.tier2 #bigwin,#machine.tier3 #bigwin{opacity:1}
#machine.tier1 #marquee{animation:flashA .12s 8}
#machine.tier2 #marquee{animation:flashA .12s 16}
#machine.tier3 #marquee{animation:flashW .12s 22}
@keyframes flashA{50%{background-color:#ff9d45}} @keyframes flashW{50%{background-color:#fff}}
#reelWindow{position:relative;margin:10px 26px 0;border-radius:12px;border:5px solid #2b2e35;
background:#0b0f1a;padding:8px;box-shadow:inset 0 0 40px #000, 0 3px 0 #14161a}
#reels{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
.reel{display:grid;grid-template-rows:repeat(3,1fr);gap:6px}
.cell{position:relative;aspect-ratio:1/1;background:#05070d;border-radius:8px;
border:2px solid #3d4454;display:flex;align-items:center;justify-content:center;overflow:hidden;
box-shadow:inset 0 0 14px rgba(0,0,0,.9)}
.cell .fb{font-size:13px;font-weight:800;letter-spacing:1px;color:#ffd27a;text-align:center;padding:2px;
text-shadow:0 0 6px rgba(255,180,60,.55)}
.cell.lit{border-color:var(--amber);box-shadow:0 0 12px rgba(255,194,87,.75),inset 0 0 14px rgba(0,0,0,.9)}
#lineLegend{display:flex;justify-content:space-between;margin-top:6px;font-size:9px;letter-spacing:2px;color:#9aa3b8}
#meters{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin:12px 26px 0}
.meter{border-radius:10px;border:3px solid #22252b;padding:6px 10px;text-align:center;
background:linear-gradient(180deg,#20232b 0%,#0c0e13 100%);box-shadow:inset 0 2px 6px rgba(0,0,0,.8)}
.meter .lab{font-size:9px;letter-spacing:3px;color:#ffb45e}
.meter .val{font-family:"Courier New",monospace;font-size:24px;font-weight:800;color:#ffe7b0;
text-shadow:0 0 8px rgba(255,200,90,.8)}
#win.val{color:#a8ffca}
#controls{display:flex;align-items:center;gap:14px;margin:12px 26px 0}
#play{flex:1;height:64px;border-radius:16px;border:4px solid #3b1a05;cursor:pointer;
background:linear-gradient(180deg,#ffb257 0%,#ff6b1a 45%,#c93d06 100%);
color:#fff;font-size:30px;font-weight:900;letter-spacing:8px;text-shadow:0 2px 0 #5a1d00;
box-shadow:0 6px 0 #4c1c02,0 10px 20px rgba(0,0,0,.6),inset 0 2px 4px rgba(255,255,255,.55)}
#play:active{transform:translateY(4px);box-shadow:0 2px 0 #4c1c02,0 6px 10px rgba(0,0,0,.6)}
#play:disabled{filter:saturate(.4) brightness(.8);cursor:default}
#mute{width:86px;height:64px;border-radius:16px;border:4px solid #191c22;cursor:pointer;
background:linear-gradient(180deg,#b9bec6 0%,#6f747e 55%,#41454d 100%);
color:#1b1e25;font-size:11px;font-weight:900;letter-spacing:1px;
box-shadow:0 5px 0 #191c22; line-height:1.25}
#mute.off{background:linear-gradient(180deg,#c25b3a 0%,#7e3219 100%);color:#ffe0c8}
#paytable{margin:12px 22px 0;border:4px solid #2b2e35;border-radius:10px;padding:8px;
background:linear-gradient(180deg,#f7ecc9 0%,#e9d7a8 100%);color:var(--ink);
box-shadow:inset 0 0 24px rgba(120,80,20,.25)}
#paytable h2{font-size:11px;letter-spacing:4px;text-align:center;color:#7a3d00;margin-bottom:6px}
#payGrid{display:grid;grid-template-columns:repeat(4,1fr);gap:4px}
.pt{display:flex;align-items:center;gap:6px;border:1.5px solid #a97b33;border-radius:6px;padding:3px;
background:rgba(255,255,255,.35)}
.pt .thumb{width:34px;height:34px;flex:none;border-radius:5px;background:#05070d;border:1px solid #333}
.pt .thumb .fb{font-size:8px;color:#ffd27a;display:flex;align-items:center;justify-content:center;height:100%;font-weight:800}
.pt .info{font-size:8px;line-height:1.25}
.pt .info b{font-size:9px;letter-spacing:.5px;color:#64170a;display:block}
.pt .info span{color:#3c2400;white-space:nowrap}
.pt.wild{background:#ffe2c0;border-color:#c4490a}
.pt.sc{background:#fdeeb8;border-color:#a97816}
#featureNote{margin-top:6px;font-size:9px;letter-spacing:1px;text-align:center;color:#7a3d00;font-weight:700}
#toe{margin-top:10px;height:26px;text-align:center;font-size:9px;letter-spacing:3px;color:#8a8f99}
#vault{position:absolute;inset:0;display:none;align-items:center;justify-content:center;z-index:40;
background:rgba(3,6,18,.88)}
#vault.open{display:flex}
#vault .panel{width:420px;border:5px solid #20232b;border-radius:18px;padding:22px;text-align:center;
background:linear-gradient(180deg,#1a2f6b 0%,#08142e 100%);box-shadow:0 0 60px rgba(255,166,60,.35)}
#vault h2{color:var(--amber);letter-spacing:4px;font-size:24px;margin-bottom:6px;text-shadow:0 0 12px rgba(255,178,67,.8)}
#vault p{color:#ffe9b0;font-size:11px;letter-spacing:2px;margin-bottom:14px}
#tubes{display:flex;gap:18px;justify-content:center}
.tube{width:96px;height:150px;border-radius:14px;border:3px solid #6d6f76;cursor:default;position:relative;
background:linear-gradient(180deg,rgba(255,255,255,.28),rgba(255,255,255,.05)),
radial-gradient(circle at 50% 78%, #3d2b10 0 36%, transparent 37%),#101528;
box-shadow:inset 0 -10px 22px rgba(255,150,40,.25)}
.tube .coil{position:absolute;left:18%;right:18%;bottom:14%;height:44%;border-radius:8px;
background:repeating-linear-gradient(180deg,#ffb45e 0 5px,#7c4a15 5px 8px);opacity:.9}
.tube .result{position:absolute;left:0;right:0;top:38%;text-align:center;font-size:22px;font-weight:900;color:#fff;
text-shadow:0 0 10px #ff9d45;opacity:0}
.tube.reveal .result{opacity:1}
.tube.reveal{border-color:var(--amber);box-shadow:0 0 22px rgba(255,194,87,.8)}
#audit{position:absolute;inset:0;display:none;z-index:50;padding:26px;overflow:hidden;
background:rgba(3,6,18,.96)}
#audit.open{display:block}
#audit h2{letter-spacing:5px;color:var(--amber);font-size:26px;text-align:center;margin-bottom:14px}
#audit table{width:100%;font-family:"Courier New",monospace;font-size:15px;color:#ffe9b0;border-collapse:collapse}
#audit td{padding:6px 8px;border-bottom:1px dashed #6b5a2a}
#audit td:last-child{text-align:right}
#audit .close{margin-top:14px;font-size:10px;letter-spacing:2px;color:#9aa3b8;text-align:center}
</style>
</head>
<body>
<div id="stage">
<div id="machine">
<div class="rivets">
<i style="left:3%;top:1%"></i><i style="left:96%;top:1%"></i>
<i style="left:3%;top:98%"></i><i style="left:96%;top:98%"></i>
<i style="left:3%;top:50%"></i><i style="left:96%;top:50%"></i>
</div>
<div id="marquee">
<h1>ATOMIC <span>BANDIT</span></h1>
<div class="sub">ROCKET-AGE AMUSEMENT · 12 LINES</div>
<div id="bigwin">GALACTIC WIN</div>
</div>
<div id="reelWindow">
<div id="reels"></div>
<div id="lineLegend"><span>12 LINE MACHINE</span><span>WILD SAUCER</span><span>BEACON SCATTER</span></div>
</div>
<div id="meters">
<div class="meter"><div class="lab">CREDITS</div><div class="val" id="credits">500</div></div>
<div class="meter"><div class="lab">TOTAL BET</div><div class="val" id="bet">12</div></div>
<div class="meter"><div class="lab">WIN</div><div class="val" id="win">0</div></div>
</div>
<div id="controls">
<button id="play">PLAY</button>
<button id="mute">SOUND<br>ON</button>
</div>
<div id="paytable">
<h2>PAY TABLE · CREDITS PER LINE</h2>
<div id="payGrid"></div>
<div id="featureNote">3+ BEACON ANYWHERE OPENS THE ROCKET VAULT · BEACON PAYS MULTIPLY THE TOTAL BET</div>
</div>
<div id="toe">MODEL 1954 · ATOMIC BANDIT · HORIZON AMUSEMENT CO.</div>
<div id="vault">
<div class="panel">
<h2>ROCKET VAULT</h2>
<p>PRESS PLAY TO PICK A VACUUM TUBE</p>
<div id="tubes"></div>
</div>
</div>
<div id="audit" class="open0"></div>
</div>
</div>
<script>
/* ============ MACHINE DATA (mirrors the math sheet) ============ */
const ART = 'art/symbols.png';
const SYMS = [
{id:'saucer', name:'SAUCER', short:'UFO', role:'wild', pays:{2:19,3:155,4:790,5:3800}},
{id:'beacon', name:'BEACON', short:'BCN', role:'scatter',pays:{3:1,4:5,5:25}}, // x total bet
{id:'gear', name:'COG', short:'COG', role:'pay', pays:{3:18,4:60,5:225}},
{id:'wrench', name:'SPANNER', short:'SPAN', role:'pay', pays:{3:18,4:60,5:225}},
{id:'tube', name:'VALVE', short:'VALV', role:'pay', pays:{3:18,4:60,5:225}},
{id:'battery',name:'POWER CELL',short:'CELL', role:'pay', pays:{3:19,4:75,5:250}},
{id:'boots', name:'MOON BOOTS',short:'BOOT', role:'pay', pays:{3:24,4:92,5:330}},
{id:'helmet', name:'HELMET', short:'DOME', role:'pay', pays:{3:30,4:122,5:425}},
{id:'raygun', name:'RAY GUN', short:'RAY', role:'pay', pays:{3:30,4:122,5:425}},
{id:'orb', name:'STAR ORB', short:'ORB', role:'pay', pays:{2:6,3:50,4:200,5:790}},
{id:'atom', name:'ATOM', short:'ATOM', role:'pay', pays:{2:6,3:50,4:200,5:790}},
{id:'robot', name:'ROBOT', short:'ROBO', role:'pay', pays:{2:6,3:50,4:200,5:790}},
{id:'dish', name:'RADAR DISH',short:'DISH', role:'pay', pays:{2:6,3:50,4:200,5:790}},
{id:'rocket', name:'MOON ROCKET',short:'RKET',role:'pay', pays:{2:8,3:66,4:255,5:1270}},
{id:'cadet', name:'SPACE CADET',short:'CDT', role:'pay', pays:{2:8,3:66,4:255,5:1270}},
{id:'comet', name:'COMET', short:'COMET', role:'pay', pays:{2:10,3:100,4:500,5:1600}}
];
const CELL = {}; SYMS.forEach((s,i)=>CELL[s.id]=i);
const PAYABLE = SYMS.filter(s=>s.id!=='beacon');
const LINES = [
[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,0,0],
[2,2,1,2,2],[1,0,0,0,1],[1,2,2,2,1],[1,1,0,1,1],[1,1,2,1,1],[0,2,0,2,0]
];
const T = ['gear','atom','wrench','comet','boots','tube','rocket','gear','helmet','wrench',
'saucer','battery','dish','boots','tube','gear','cadet','wrench','raygun','battery',
'orb','robot','tube','boots','wrench','battery','gear','helmet','raygun','beacon','battery','tube'];
const OFFSETS=[0,7,13,19,26];
const REELS = OFFSETS.map(off => T.map((_,i)=>T[(i+off)%T.length]));
const BET = LINES.length, START=500;
let credits=START, spinning=false, artOK=false, muted=false, demoArmed=(location.hash==='#demo');
let audio=null;
/* ============ SPRITE / FALLBACK RENDERING ============ */
function spritePos(idx){
const c=idx%4, r=Math.floor(idx/4);
return (c*100/3)+'% '+(r*100/3)+'%';
}
function setSymbol(el,id){
const fb=el.querySelector('.fb');
if(fb){ fb.textContent=(SYMS.find(s=>s.id===id)||{}).short||id.toUpperCase(); }
if(artOK){
el.style.backgroundImage='url('+ART+')';
el.style.backgroundSize='400% 400%';
el.style.backgroundRepeat='no-repeat';
el.style.backgroundPosition=spritePos(CELL[id]);
if(fb) fb.style.display='none';
}else{
el.style.backgroundImage='none';
}
el.dataset.sym=id;
}
new Image().onload=(()=>{artOK=true; document.querySelectorAll('[data-sym]').forEach(el=>setSymbol(el,el.dataset.sym));});
document.querySelectorAll('[data-sym]').forEach; // noop guard
/* ============ REELS DOM ============ */
const reelsEl=document.getElementById('reels');
const reelCells=[];
for(let r=0;r<5;r++){
const col=document.createElement('div'); col.className='reel';
const cells=[];
for(let row=0;row<3;row++){
const c=document.createElement('div'); c.className='cell';
c.innerHTML='<span class="fb"></span>'; col.appendChild(c); cells.push(c);
}
reelsEl.appendChild(col); reelCells.push(cells);
}
/* ============ PAYTABLE DOM ============ */
const pg=document.getElementById('payGrid');
SYMS.forEach(s=>{
const d=document.createElement('div'); d.className='pt '+(s.role==='wild'?'wild':s.role==='scatter'?'sc':'');
const th=document.createElement('div'); th.className='thumb'; th.innerHTML='<span class="fb"></span>';
const inf=document.createElement('div'); inf.className='info';
let paysTxt;
if(s.role==='scatter') paysTxt='3:1 4:5 5:25 xBET';
else paysTxt=Object.keys(s.pays).sort((a,b)=>b-a).map(k=>k+'→'+s.pays[k]).join(' ');
inf.innerHTML='<b>'+s.name+'</b><span>'+(s.role==='wild'?'WILD · ':'')+(s.role==='scatter'?'SCATTER · ':'')+paysTxt+'</span>';
d.appendChild(th); d.appendChild(inf); pg.appendChild(d);
setSymbol(th,s.id);
});
/* ============ AUDIO (Web Audio only) ============ */
function ac(){
if(!audio){ audio=new (window.AudioContext||window.webkitAudioContext)(); }
if(audio.state==='suspended') audio.resume();
return audio;
}
let spinSrc=null;
function tone(opt){
if(muted) return; const a=ac(); const o=a.createOscillator(), g=a.createGain();
o.type=opt.type||'square'; o.frequency.value=opt.f;
if(opt.slide) o.frequency.exponentialRampToValueAtTime(opt.slide,a.currentTime+(opt.dur||.15));
g.gain.setValueAtTime(0,a.currentTime);
g.gain.linearRampToValueAtTime(opt.v||.14,a.currentTime+.008);
g.gain.exponentialRampToValueAtTime(.0001,a.currentTime+(opt.dur||.15));
o.connect(g).connect(a.destination); o.start(); o.stop(a.currentTime+(opt.dur||.15)+.05);
}
function clunk(){ tone({type:'triangle',f:72,slide:40,dur:.12,v:.3}); }
function ding(i){ tone({type:'sine',f:520+i*160,dur:.25,v:.12}); }
function nearMiss(){ tone({type:'sawtooth',f:340,slide:120,dur:.8,v:.1}); }
function coins(n){
if(muted) return; const a=ac(); const scale=[660,830,990,1108,1320,1480,1660];
for(let i=0;i<n;i++){
const o=a.createOscillator(),g=a.createGain(); o.type='square';
o.frequency.value=scale[i%scale.length];
const t=a.currentTime+i*.075;
g.gain.setValueAtTime(0,t); g.gain.linearRampToValueAtTime(.1,t+.01);
g.gain.exponentialRampToValueAtTime(.0001,t+.09);
o.connect(g).connect(a.destination); o.start(t); o.stop(t+.1);
}
}
function fanfare(){
[262,330,392,523].forEach((f,i)=>setTimeout(()=>tone({type:'triangle',f,dur:.34,v:.16}),i*140));
}
function spinNoise(start){
if(muted||!start) return;
const a=ac(); const buf=a.createBuffer(1,a.sampleRate*.7,a.sampleRate);
const d=buf.getChannelData(0); for(let i=0;i<d.length;i++) d[i]=Math.random()*2-1;
const src=a.createBufferSource(); src.buffer=buf;
const fl=a.createBiquadFilter(); fl.type='bandpass'; fl.frequency.value=900;
const g=a.createGain(); g.gain.value=.05;
src.connect(fl).connect(g).connect(a.destination); src.start(); spinSrc=src;
}
function spinStop(){ if(spinSrc){ try{spinSrc.stop();}catch(e){} spinSrc=null; } }
/* ============ CORE MATH (shared by play, demo, audit) ============ */
function randomStops(){ return REELS.map(s=>Math.floor(Math.random()*s.length)); }
function gridFromStops(stops){
return REELS.map((strip,r)=>{
const p=stops[r], n=strip.length;
return [strip[p%n],strip[(p+1)%n],strip[(p+2)%n]];
});
}
function evaluate(grid){
let lineWin=0, hits=[];
for(const def of LINES){
const seq=def.map((row,r)=>grid[r][row]);
let best=null;
for(const sym of PAYABLE){
let k=0;
for(let r=0;r<5;r++){
if(seq[r]===sym.id || seq[r]==='saucer') k++; else break;
}
const pay=sym.pays[k];
if(pay && (!best || pay>best.pay)) best={sym:sym,k,pay,seq};
}
if(best){ lineWin+=best.pay; hits.push(best); }
}
let sc=0; grid.forEach(col=>col.forEach(s=>{ if(s==='beacon') sc++; }));
let scPay=0;
if(sc>=3){ const key=Math.min(sc,5); scPay=(SYMS[1].pays[key]||0)*BET; }
return {total:lineWin+scPay, sc, scPay, hits};
}
/* ============ SPIN / UI ============ */
const creditsEl=document.getElementById('credits'), winEl=document.getElementById('win');
const machineEl=document.getElementById('machine'), playBtn=document.getElementById('play');
const vaultEl=document.getElementById('vault'), tubesEl=document.getElementById('tubes');
let vaultActive=false, vaultPrizes=[];
function render(stops){
stops.forEach((p,r)=>{
const strip=REELS[r], n=strip.length;
for(let row=0;row<3;row++) setSymbol(reelCells[r][row], strip[(p+row)%n]);
});
}
function clearLights(){ machineEl.classList.remove('tier1','tier2','tier3');
document.querySelectorAll('.cell.lit').forEach(c=>c.classList.remove('lit')); }
function lightHits(hits){
hits.forEach(h=>{
for(let r=0;r<h.k;r++){
const line=LINES.find(def=>def.map((row,rr)=>h.seq[rr]).join()===h.seq.join());
}
h.cells=[]; // computed below
});
hits.forEach(h=>{
LINES.forEach(def=>{
const seq=def.map((row,r)=>gridSnapshot[r][row]);
if(seq.join()===h.seq.join()){
for(let r=0;r<h.k;r++){ const row=def[r]; reelCells[r][row].classList.add('lit'); h.cells.push([r,row]); }
}
});
});
}
let gridSnapshot=null;
function tierFor(mult){ return mult>=50?3: mult>=20?2: mult>=10?1:0; }
function spin(){
if(spinning||vaultActive) return;
if(credits<BET){ credits=START; creditsEl.textContent=credits; tone({type:'sine',f:220,dur:.4,v:.12}); }
credits-=BET; creditsEl.textContent=credits;
clearLights(); spinning=true; playBtn.disabled=true; winEl.textContent='0';
spinNoise(true);
let forced=null;
if(demoArmed){
demoArmed=false;
forced=REELS.map(strip=>{
for(let i=0;i<strip.length;i++) if(strip[(i+1)%strip.length]==='comet') return i;
return 0;
});
}
const stops=forced||randomStops();
const timers=[];
reelCells.forEach((cells,r)=>{
const iv=setInterval(()=>{
cells.forEach(c=>setSymbol(c, REELS[r][Math.floor(Math.random()*REELS[r].length)]));
},70);
timers.push(iv);
});
let done=0;
reelCells.forEach((cells,r)=>{
setTimeout(()=>{
clearInterval(timers[r]);
const strip=REELS[r];
for(let row=0;row<3;row++) setSymbol(cells[row], strip[(stops[r]+row)%strip.length]);
clunk(); done++;
if(done===5){ spinStop(); finish(stops); }
},500+r*260);
});
}
function finish(stops){
const grid=gridFromStops(stops); gridSnapshot=grid;
const res=evaluate(grid);
spinning=false; playBtn.disabled=false;
credits+=res.total; creditsEl.textContent=credits; winEl.textContent=res.total;
if(res.total>0){
lightHits(res.hits);
const mult=res.total/BET, tier=tierFor(mult);
if(tier){ machineEl.classList.add('tier'+tier); document.getElementById('bigwin').textContent =
tier===3?'GALACTIC WIN':tier===2?'METEOR WIN':'ATOMIC WIN'; }
coins(tier?tier*8: Math.min(6,2+res.hits.length));
}else if(res.sc===2){ nearMiss(); }
if(res.sc>=3){
setTimeout(openVault,700);
}
}
/* ============ ROCKET VAULT FEATURE ============ */
function openVault(){
vaultActive=true; fanfare();
vaultPrizes=[1,2,3].sort(()=>Math.random()-.5);
tubesEl.innerHTML='';
for(let i=0;i<3;i++){
const t=document.createElement('div'); t.className='tube';
t.innerHTML='<div class="coil"></div><div class="result">×'+vaultPrizes[i]+'</div>';
tubesEl.appendChild(t);
}
vaultEl.classList.add('open');
}
function resolveVault(){
vaultActive=false;
const pick=Math.floor(Math.random()*3);
const mult=vaultPrizes[pick], prize=mult*BET;
const tubes=tubesEl.children;
for(let i=0;i<3;i++) tubes[i].classList.add('reveal');
for(let i=0;i<3;i++) tubes[i].querySelector('.result').textContent='×'+vaultPrizes[i];
setTimeout(()=>{
credits+=prize; creditsEl.textContent=credits; winEl.textContent=prize;
coins(10); vaultEl.classList.remove('open');
},900);
}
playBtn.addEventListener('click',()=>{ ac(); if(vaultActive){ resolveVault(); } else spin(); });
document.getElementById('mute').addEventListener('click',function(){
muted=!muted; this.classList.toggle('off',muted);
this.innerHTML=muted?'SOUND<br>OFF':'SOUND<br>ON';
if(!muted) ac();
});
/* ============ AUDIT MODE ============ */
const auditMatch=location.hash.match(/audit=(\d+)/);
if(auditMatch){
const n=parseInt(auditMatch[1],10);
const panel=document.getElementById('audit'); panel.classList.add('open');
let wagered=0,won=0,winHits=0,biggest=0,sc3=0;
for(let i=0;i<n;i++){
const stops=randomStops(); const res=evaluate(gridFromStops(stops));
wagered+=BET; won+=res.total; if(res.total>0) winHits++;
if(res.total>biggest) biggest=res.total; if(res.sc>=3) sc3++;
}
panel.innerHTML='<h2>AUDIT FLIGHT REPORT</h2><table>'+
row('SPINS',n)+row('WAGERED',wagered)+row('WON',won)+
row('RTP',(won/wagered*100).toFixed(2)+'%')+
row('HIT FREQUENCY',(winHits/n*100).toFixed(2)+'%')+
row('BIGGEST WIN',biggest)+row('SPINS WITH 3+ SCATTERS',sc3)+
'</table><div class="close">SAME MATH AS LIVE PLAY · RELOAD TO CLEAR</div>';
function row(a,b){return '<tr><td>'+a+'</td><td>'+b+'</td></tr>';}
}
/* ============ SCALING (no scrollbars, any viewport) ============ */
const machine=document.getElementById('machine'), stage=document.getElementById('stage');
function fit(){
const s=Math.min(window.innerWidth/640, window.innerHeight/1020);
machine.style.transform='scale('+s+')';
}
window.addEventListener('resize',fit); fit();
render([0,0,0,0].map(()=>0)); // initial static frame
</script>
</body>
</html>
{
"machine": { "name": "ATOMIC BANDIT", "credits": 500, "betPerLine": 1, "lines": 12 },
"sheet": {
"style": "1954 pulp-magazine cover illustration: hand-airbrushed gouache with visible halftone print dots, atomic orange, vacuum-tube amber and deep-space blue palette, chrome highlights, a single warm spotlight rim against the flat-black field, crisp inking with ruler-straight geometry and compass arcs, optimistic mid-century American advertising finish. Append this paragraph to every cell prompt below.",
"cells": [
{ "cell": 0, "id": "saucer", "name": "Saucer", "role": "wild", "prompt": "A chrome flying saucer tilted three-quarter view, spinning amber dome beacon, riveted hull band, the one wild empress of the reel. Style suffix paragraph applies.", "pays": { "2": 19, "3": 155, "4": 790, "5": 3800 } },
{ "cell": 1, "id": "beacon", "name": "Beacon", "role": "scatter", "prompt": "A tripod radar-beacon tower with concentric signal rings radiating from a glowing orange lamp. Style suffix paragraph applies.", "pays": { "3": 1, "4": 5, "5": 25 } },
{ "cell": 2, "id": "gear", "name": "Cog wheel", "role": "pay", "prompt": "One chunky riveted cog-wheel with a hex hub and machined teeth. Style suffix paragraph applies.", "pays": { "3": 18, "4": 60, "5": 225 } },
{ "cell": 3, "id": "wrench", "name": "Spanner", "role": "pay", "prompt": "One heavy rocket-mechanic's open-end wrench laid diagonally. Style suffix paragraph applies.", "pays": { "3": 18, "4": 60, "5": 225 } },
{ "cell": 4, "id": "tube", "name": "Vacuum tube", "role": "pay", "prompt": "One warm glass vacuum tube with an orange heart-shaped filament glowing inside. Style suffix paragraph applies.", "pays": { "3": 18, "4": 60, "5": 225 } },
{ "cell": 5, "id": "battery", "name": "Power cell", "role": "pay", "prompt": "One squat atomic battery jar filled with crackling orange charge, brass terminals on top. Style suffix paragraph applies.", "pays": { "3": 19, "4": 75, "5": 250 } },
{ "cell": 6, "id": "boots", "name": "Moon boots", "role": "pay", "prompt": "A pair of magnetized moon-walking boots with thick ribbed soles, standing side by side. Style suffix paragraph applies.", "pays": { "3": 24, "4": 92, "5": 330 } },
{ "cell": 7, "id": "helmet", "name": "Bubble helmet", "role": "pay", "prompt": "One round bubble space helmet with frosted star viewport and brass collar ring. Style suffix paragraph applies.", "pays": { "3": 30, "4": 122, "5": 425 } },
{ "cell": 8, "id": "raygun", "name": "Ray gun", "role": "pay", "prompt": "One bulb-nosed ray pistol with cooling fins and a coiled barrel. Style suffix paragraph applies.", "pays": { "3": 30, "4": 122, "5": 425 } },
{ "cell": 9, "id": "orb", "name": "Star orb", "role": "pay", "prompt": "One floating glass orb cradling a tiny sparking galaxy on a brass stand. Style suffix paragraph applies.", "pays": { "2": 6, "3": 50, "4": 200, "5": 790 } },
{ "cell": 10, "id": "atom", "name": "Atom emblem", "role": "pay", "prompt": "One atom emblem: nucleus with three elliptical electron orbits and orbiting sparks. Style suffix paragraph applies.", "pays": { "2": 6, "3": 50, "4": 200, "5": 790 } },
{ "cell": 11, "id": "robot", "name": "Tin robot", "role": "pay", "prompt": "One boxy tin robot with dome head, grill mouth, accordion arms and friendly stance. Style suffix paragraph applies.", "pays": { "2": 6, "3": 50, "4": 200, "5": 790 } },
{ "cell": 12, "id": "dish", "name": "Radar dish", "role": "pay", "prompt": "One parabolic radar dish on a swivel mount with feed-horn antenna. Style suffix paragraph applies.", "pays": { "2": 6, "3": 50, "4": 200, "5": 790 } },
{ "cell": 13, "id": "rocket", "name": "Moon rocket", "role": "pay", "prompt": "One sleek finned moon rocket with a round porthole and a curling vapor trail. Style suffix paragraph applies.", "pays": { "2": 8, "3": 66, "4": 255, "5": 1270 } },
{ "cell": 14, "id": "cadet", "name": "Space cadet", "role": "pay", "prompt": "One cheerful female space cadet in a silver suit and bubble helmet, hand-painted pin-up style, waving. Style suffix paragraph applies.", "pays": { "2": 8, "3": 66, "4": 255, "5": 1270 } },
{ "cell": 15, "id": "comet", "name": "Golden comet", "role": "pay", "prompt": "One golden comet with a sweeping hairpin tail, sparks flying from the nucleus. Style suffix paragraph applies.", "pays": { "2": 10, "3": 100, "4": 500, "5": 1600 } }
]
},
"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, 0, 0], [2, 2, 1, 2, 2], [1, 0, 0, 0, 1],
[1, 2, 2, 2, 1], [1, 1, 0, 1, 1], [1, 1, 2, 1, 1], [0, 2, 0, 2, 0]
],
"reels": [
["gear","atom","wrench","comet","boots","tube","rocket","gear","helmet","wrench","saucer","battery","dish","boots","tube","gear","cadet","wrench","raygun","battery","orb","robot","tube","boots","wrench","battery","gear","helmet","raygun","beacon","battery","tube"],
["gear","helmet","wrench","saucer","battery","dish","boots","tube","gear","cadet","wrench","raygun","battery","gear","robot","tube","boots","wrench","battery","gear","helmet","raygun","beacon","battery","tube","gear","atom","wrench","comet","boots","tube","rocket"],
["boots","tube","gear","cadet","wrench","raygun","battery","gear","robot","tube","boots","wrench","battery","gear","helmet","raygun","beacon","battery","tube","gear","atom","wrench","comet","boots","tube","rocket","gear","helmet","wrench","saucer","battery","dish"],
["battery","gear","robot","tube","boots","wrench","battery","gear","helmet","raygun","beacon","battery","tube","gear","atom","wrench","comet","boots","tube","rocket","gear","helmet","wrench","saucer","battery","dish","boots","tube","gear","cadet","wrench","raygun"],
["gear","helmet","raygun","beacon","battery","tube","gear","atom","wrench","comet","boots","tube","rocket","gear","helmet","wrench","saucer","battery","dish","boots","tube","gear","cadet","wrench","raygun","battery","gear","robot","tube","boots","wrench","battery"]
],
"math": { "claimedRtpBase": 94.2, "claimedHitFrequency": 29.9, "claimedFeatureRtp": 2.0, "volatility": "medium" },
"cabinet": [
{ "id": "backdrop", "aspect": "9:16", "prompt": "Riveted chrome diner-jukebox wall panel in deep-space blue with a big amber radar dial, shooting stars and 1954 airbrush swirls. Style suffix paragraph applies." },
{ "id": "marquee", "aspect": "16:9", "prompt": "Marquee topper artwork: chrome flying saucer orbiting a giant orange atom, with the words ATOMIC BANDIT in ruler-drawn lettering on a deep-blue starfield. Style suffix paragraph applies." },
{ "id": "belly", "aspect": "3:2", "prompt": "Belly-glass artwork: grinning tin robot pulling a lever while a golden comet streaks past a ring of payline tracers around the reels. Style suffix paragraph applies." }
]
}
The mechanism is The Beacon Tease: an engineered near-miss on the scatter. Because two BEACONs are far more common than three, most dead spins land exactly two; the machine detects this and plays a slow descending slide while the two beacon cells stay lit, plainly showing the player that the ROCKET VAULT feature (which averages 2.0× the bet, but whose real value is the drama of the pick) came within one symbol of opening. It works on the classic variable-ratio anticipation loop — the tease is not a consolation, it's proof the lock is pickable — and each near-miss makes the next press of PLAY feel like the overdue one, which is the entire engine of the machine's pull.