Build a self-contained HTML slot machine inspired by 1950s science fiction: optimistic, mechanical, and rooted in chrome, Bakelite, tubes, radar, hand-painted airbrush art, ruler-and-compass lettering, halftone printing, atomic orange, amber, and deep-space blue—not modern retro or synthwave.
You are designing the game, math, art direction, and implementation.
The machine has exactly one play control and may have one mute control. Do not add bet, line, autoplay, settings, or other game controls. Still make the paytable, feature, and results understandable. Handle zero credits and rapid double presses.
Use Web Audio only: synthesize spin, stop, coin or credit, win, near-miss, and feature sounds after the player gesture. Larger wins must sound more significant. Do not use audio files or data: audio.
Provide prompts for art/symbols.png: a 2048×2048, 4×4 contact sheet with cells 0–15, read left-to-right and top-to-bottom. Each cell is 512×512, contains one centered symbol at the same scale, and has a flat black background. The literal path art/symbols.png appears exactly once in the HTML.
Integrate the sheet's black background into the cabinet. Use CSS, canvas, or another local technique to display the cells. If the image is unavailable, the reels need readable fallbacks. You may request up to three optional cabinet images at art/<id>.png; the cabinet must still look complete without them.
State your strips, paylines, paytable, base RTP, hit frequency, feature RTP, and volatility. The base game targets 94% RTP (acceptable 92.5–95.5%) and a 20–35% hit frequency. These must arise from the declared math.
The checker simulates 200,000 fixed-seed spins. It chooses one uniform stop on each reel, shows that stop plus the next two cells with wraparound, and evaluates the displayed grid. Total bet is lines × betPerLine. Per line, use the highest-paying consecutive left-to-right match, allowing wilds except for scatters; a line win is pays[count] × betPerLine. Scatter wins count across all 15 visible positions, ignore paylines, and multiply the total bet. Feature value is excluded from the base-game RTP.
Support both URL modes:
#demo: the next spin is a genuine win of at least 20× total bet, shown and scored through normal game logic.#audit=50000: no animation; run 50,000 normal-logic spins and display spins, wagered, won, RTP, hit frequency, largest win, and spins with 3+ scatters.art/ files.Respond with exactly four sections.
One line: the machine name and what it feels like to stand in front of it.
One fenced html block containing the complete page.
One fenced json block with this structure:
{
"machine": { "name": "ATOMIC BANDIT", "credits": 500, "betPerLine": 1, "lines": 20 },
"sheet": {
"style": "One paragraph appended to every cell prompt.",
"cells": [
{ "cell": 0, "id": "saucer", "name": "Saucer", "role": "wild", "prompt": "One object.", "pays": { "3": 25, "4": 100, "5": 500 } },
{ "cell": 1, "id": "beacon", "name": "Beacon", "role": "scatter", "prompt": "One object.", "pays": { "3": 2, "4": 10, "5": 50 } },
{ "cell": 2, "id": "helmet", "name": "Bubble helmet", "role": "pay", "prompt": "One object.", "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": "..." }]
}
cells has 16 unique cells numbered 0–15; ids are lowercase single tokens; roles are one wild, one scatter, and 14 pay symbols; pays may include "2" and must include "3" for every symbol; paylines are unique; there are five 20–60-cell reels; and cabinet has at most three entries. aspect is 16:9, 9:16, 1:1, 3:2, 2:3, 4:3, or 3:4. The JSON contains no comments or prose.
State base RTP, hit frequency, how you calculated them, celebration tiers by total-bet multiple, zero-credit behavior, and the missing-art fallback.
Name and explain the one mechanism meant to make a player press Play again. The feature should express the machine's personality. Do not call the mechanism “engagement.”
The HTML and math sheet must describe the same machine.
Step before THE ATOMIC BANDIT, a thrumming tower of fluted chrome, marbled mahogany Bakelite, and glowing amber vacuum tubes, where each pull feels like firing up an experimental cyclotron in a 1955 World's Fair pavilion.
<!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</title>
<style>
:root {
--bg-space: #060a12;
--cabinet-blue: #0c1a2e;
--cabinet-rim: #172b47;
--chrome-light: #f5f9fc;
--chrome-mid: #9eb5c7;
--chrome-dark: #415669;
--bakelite: #2c1208;
--bakelite-rib: #421c0e;
--atomic-orange: #ff6a00;
--atomic-amber: #ffaa1d;
--tube-glow: rgba(255, 140, 30, 0.7);
--dial-bg: #0b111a;
}
* { box-sizing: border-box; margin: 0; padding: 0; user-select: none; -webkit-user-select: none; }
html, body {
width: 100%; height: 100%;
background-color: var(--bg-space);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
font-family: "Courier New", Courier, monospace;
color: var(--chrome-light);
}
/* Responsive Portrait Scaler */
#viewport-wrapper {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
#machine {
width: 480px;
height: 850px;
background: radial-gradient(circle at 50% 20%, #162840 0%, var(--cabinet-blue) 70%, #060e1a 100%);
border: 10px solid #2a3e56;
border-radius: 36px 36px 24px 24px;
box-shadow: 0 0 40px rgba(0,0,0,0.9), inset 0 2px 4px rgba(255,255,255,0.4), inset 0 -6px 12px rgba(0,0,0,0.8);
display: flex;
flex-direction: column;
padding: 12px;
position: relative;
transform-origin: center center;
}
/* Chrome Header & Tubes */
.header-housing {
background: linear-gradient(180deg, var(--chrome-light) 0%, var(--chrome-mid) 45%, var(--chrome-dark) 55%, var(--chrome-mid) 90%, var(--chrome-light) 100%);
border-radius: 20px 20px 8px 8px;
padding: 8px;
box-shadow: 0 4px 10px rgba(0,0,0,0.6), inset 0 1px 2px #fff;
text-align: center;
position: relative;
}
.tube-gallery {
display: flex;
justify-content: center;
gap: 16px;
margin-bottom: 6px;
}
.vac-tube {
width: 24px;
height: 44px;
background: linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(200,225,255,0.15) 30%, rgba(255,140,0,0.2) 80%);
border: 1.5px solid rgba(255,255,255,0.5);
border-radius: 12px 12px 4px 4px;
position: relative;
box-shadow: 0 0 12px var(--tube-glow);
display: flex;
align-items: center;
justify-content: center;
}
.vac-tube::before {
content: '';
position: absolute;
width: 4px;
height: 24px;
background: #ffec80;
box-shadow: 0 0 8px #ff6600, 0 0 14px #ffaa00;
border-radius: 2px;
}
.vac-tube.pulse::before {
animation: tubeFlicker 0.15s infinite alternate;
}
@keyframes tubeFlicker {
0% { opacity: 0.7; transform: scaleY(0.95); }
100% { opacity: 1; transform: scaleY(1.05); }
}
.title-plate {
background: var(--bakelite);
border: 2px solid var(--chrome-mid);
border-radius: 6px;
padding: 4px 12px;
display: inline-block;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.8);
}
.title-text {
font-size: 20px;
font-weight: 900;
letter-spacing: 4px;
color: #ffca36;
text-shadow: 0 0 8px var(--atomic-orange), 1px 1px 0px #000;
}
.sub-title {
font-size: 8px;
letter-spacing: 2px;
color: #88c0d0;
text-transform: uppercase;
}
/* Meters Section */
.meter-rack {
display: flex;
justify-content: space-between;
margin: 8px 0 6px 0;
background: #08101a;
border: 2px solid var(--chrome-mid);
border-radius: 8px;
padding: 6px 12px;
box-shadow: inset 0 0 8px #000;
}
.meter-unit {
display: flex;
flex-direction: column;
align-items: center;
}
.meter-lbl {
font-size: 9px;
font-weight: bold;
color: #8aa4be;
letter-spacing: 1px;
margin-bottom: 2px;
}
.nixie-box {
background: #020508;
border: 1px solid #ff7700;
border-radius: 4px;
padding: 2px 8px;
min-width: 80px;
text-align: right;
box-shadow: inset 0 0 6px rgba(255,100,0,0.4);
}
.nixie-num {
font-size: 16px;
font-weight: bold;
color: #ff9d00;
text-shadow: 0 0 6px #ff6600, 0 0 10px #ff3300;
letter-spacing: 2px;
}
/* Reel Screen Frame */
.reels-bezel {
position: relative;
background: #05080c;
border: 4px solid var(--chrome-mid);
border-radius: 12px;
padding: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.8), inset 0 0 20px #000;
margin-bottom: 6px;
}
.glass-scanlines {
position: absolute;
top: 4px; left: 4px; right: 4px; bottom: 4px;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.35) 50%), linear-gradient(90deg, rgba(255,0,0,0.03), rgba(0,255,0,0.01), rgba(0,0,255,0.03));
background-size: 100% 4px, 6px 100%;
pointer-events: none;
border-radius: 8px;
z-index: 5;
}
#reelCanvas {
display: block;
width: 100%;
height: 270px;
background-color: #000;
border-radius: 8px;
}
/* Paytable & Status Area */
.radar-status-bar {
background: #08121f;
border: 2px solid var(--bakelite-rib);
border-radius: 6px;
padding: 6px 10px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: inset 0 0 8px rgba(0,0,0,0.8);
}
#status-line {
font-size: 11px;
color: #ffd05b;
letter-spacing: 1px;
text-shadow: 0 0 4px var(--atomic-orange);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 320px;
}
.feature-pill {
font-size: 9px;
font-weight: bold;
background: #2a1005;
color: #ffaa33;
padding: 2px 6px;
border: 1px solid var(--atomic-orange);
border-radius: 10px;
letter-spacing: 1px;
box-shadow: 0 0 6px rgba(255,100,0,0.3);
}
/* Compact Halftone Paytable Badge */
.paytable-card {
background: radial-gradient(circle, #0e1e33 20%, #07101b 100%);
border: 1px solid #3d5675;
border-radius: 6px;
padding: 6px;
margin-bottom: 10px;
font-size: 9px;
color: #b0c9e2;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
line-height: 1.2;
box-shadow: inset 0 0 6px #000;
}
.pt-item {
background: rgba(0,0,0,0.4);
border: 1px solid #1a2f47;
border-radius: 3px;
padding: 2px 4px;
text-align: center;
}
.pt-item span {
color: #ffbf36;
font-weight: bold;
}
/* Control Deck & Giant Play Knob */
.deck {
background: linear-gradient(180deg, var(--bakelite-rib) 0%, var(--bakelite) 40%, #150904 100%);
border: 3px solid var(--chrome-mid);
border-radius: 12px;
padding: 10px 16px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4px 10px rgba(0,0,0,0.9), inset 0 2px 2px rgba(255,255,255,0.2);
}
.mute-toggle {
width: 38px;
height: 38px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #5a6e82 0%, #1d2733 100%);
border: 2px solid var(--chrome-light);
color: #dbe7f3;
font-size: 11px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 2px 5px #000;
display: flex;
align-items: center;
justify-content: center;
}
.mute-toggle:active {
transform: scale(0.95);
}
.play-button-outer {
position: relative;
width: 140px;
height: 64px;
background: linear-gradient(180deg, var(--chrome-light) 0%, var(--chrome-mid) 50%, var(--chrome-dark) 100%);
border-radius: 32px;
padding: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.9), 0 0 15px rgba(255,106,0,0.4);
cursor: pointer;
}
.play-button-outer:active {
transform: translateY(2px);
box-shadow: 0 2px 6px rgba(0,0,0,0.9);
}
.play-button {
width: 100%;
height: 100%;
background: radial-gradient(circle at 50% 30%, #ff8c1a 0%, #c43800 65%, #7a1f00 100%);
border: 2px solid #ffcc66;
border-radius: 28px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), inset 0 -3px 6px rgba(0,0,0,0.7);
}
.play-btn-text {
font-size: 18px;
font-weight: 900;
letter-spacing: 3px;
color: #fff;
text-shadow: 0 2px 4px #000, 0 0 10px #ffea75;
}
.play-btn-sub {
font-size: 7px;
letter-spacing: 1px;
color: #ffd299;
font-weight: bold;
}
.deck-readout {
text-align: right;
font-size: 9px;
color: #9cb5cf;
line-height: 1.4;
}
/* Audit Overlay Panel */
#audit-panel {
display: none;
position: absolute;
top: 10px; left: 10px; right: 10px; bottom: 10px;
background: rgba(4, 8, 15, 0.96);
border: 3px solid var(--atomic-orange);
border-radius: 16px;
z-index: 100;
padding: 16px;
color: #ffbe3b;
overflow-y: auto;
font-size: 12px;
}
#audit-panel h2 {
color: #ff6a00;
margin-bottom: 12px;
text-align: center;
font-size: 16px;
letter-spacing: 2px;
}
.audit-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px dashed #31445b;
}
</style>
</head>
<body>
<div id="viewport-wrapper">
<div id="machine">
<!-- Vacuum Tubes & Marquee -->
<div class="header-housing">
<div class="tube-gallery">
<div class="vac-tube" id="tube0"></div>
<div class="vac-tube" id="tube1"></div>
<div class="vac-tube" id="tube2"></div>
<div class="vac-tube" id="tube3"></div>
<div class="vac-tube" id="tube4"></div>
</div>
<div class="title-plate">
<div class="title-text">ATOMIC BANDIT</div>
<div class="sub-title">20-Line Magnetron Cyclotron • 1955 Model</div>
</div>
</div>
<!-- Nixie / Mechanical Counters -->
<div class="meter-rack">
<div class="meter-unit">
<div class="meter-lbl">CREDITS</div>
<div class="nixie-box"><span id="credit-display" class="nixie-num">500</span></div>
</div>
<div class="meter-unit">
<div class="meter-lbl">TOTAL BET</div>
<div class="nixie-box"><span id="bet-display" class="nixie-num">20</span></div>
</div>
<div class="meter-unit">
<div class="meter-lbl">PAID</div>
<div class="nixie-box"><span id="paid-display" class="nixie-num">0</span></div>
</div>
</div>
<!-- 5x3 Reel Display Housing -->
<div class="reels-bezel">
<div class="glass-scanlines"></div>
<canvas id="reelCanvas" width="448" height="270"></canvas>
</div>
<!-- Status & Feature Telemetry -->
<div class="radar-status-bar">
<div id="status-line">INSERT COIN OR ENGAGE CYCLOTRON</div>
<div class="feature-pill" id="feat-indicator">OVERDRIVE READY</div>
</div>
<!-- Halftone Paytable Badge -->
<div class="paytable-card">
<div class="pt-item">SAUCER (W)<br><span>5:2200</span></div>
<div class="pt-item">URANIUM (S)<br><span>3+:FEAT</span></div>
<div class="pt-item">COSMO<br><span>5:2400</span></div>
<div class="pt-item">RAYGUN<br><span>5:2000</span></div>
<div class="pt-item">ROBOT<br><span>5:1200</span></div>
<div class="pt-item">ROCKET<br><span>5:1000</span></div>
<div class="pt-item">SATELLITE<br><span>5:700</span></div>
<div class="pt-item">PLANET<br><span>5:700</span></div>
<div class="pt-item">TELESCOPE<br><span>5:450</span></div>
<div class="pt-item">RADAR<br><span>5:450</span></div>
<div class="pt-item">ATOM<br><span>5:350</span></div>
<div class="pt-item">MAGNET<br><span>5:350</span></div>
<div class="pt-item">VOLTMETER<br><span>5:280</span></div>
<div class="pt-item">FLASK<br><span>5:240</span></div>
<div class="pt-item">GEAR<br><span>5:190</span></div>
<div class="pt-item">CRYSTAL<br><span>5:140</span></div>
</div>
<!-- Operator Control Deck -->
<div class="deck">
<button class="mute-toggle" id="muteBtn" title="Toggle Audio">AUDIO</button>
<div class="play-button-outer" id="playBtn">
<div class="play-button">
<span class="play-btn-text">PLAY</span>
<span class="play-btn-sub">PULL / STOP</span>
</div>
</div>
<div class="deck-readout">
<div>LINES: 20</div>
<div>COIL: OK</div>
<div>20 CR / SPIN</div>
</div>
</div>
<!-- Audit Diagnostics Screen -->
<div id="audit-panel">
<h2>CALIBRATION TELEMETRY AUDIT</h2>
<div class="audit-row"><span>SPINS EXECUTED</span><span id="aud-spins">0</span></div>
<div class="audit-row"><span>TOTAL CREDITS WAGERED</span><span id="aud-wagered">0</span></div>
<div class="audit-row"><span>TOTAL CREDITS WON</span><span id="aud-won">0</span></div>
<div class="audit-row"><span>BASE GAME RETURN (RTP)</span><span id="aud-rtp">0.00%</span></div>
<div class="audit-row"><span>HIT FREQUENCY</span><span id="aud-hit">0.00%</span></div>
<div class="audit-row"><span>LARGEST SINGLE PAYOUT</span><span id="aud-max">0</span></div>
<div class="audit-row"><span>3+ SCATTER TRIGGERS</span><span id="aud-scatters">0</span></div>
<div style="margin-top:14px; text-align:center;">
<button id="closeAudit" style="background:#ff6a00; color:#000; border:none; padding:6px 14px; font-weight:bold; border-radius:4px; cursor:pointer;">DISMISS AUDIT</button>
</div>
</div>
</div>
</div>
<script>
(() => {
// --- SYMBOL SHEET CONTRACT (Strict single occurrence of literal string) ---
const SYMBOL_SHEET_SRC = 'art/symbols.png';
const symbolImg = new Image();
symbolImg.src = SYMBOL_SHEET_SRC;
// --- MATH CONFIGURATION ---
const CONFIG = {
lines: 20,
betPerLine: 1,
totalBet: 20,
reels: [
["uranium","cosmonaut","raygun","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","voltmeter","flask","gear","crystal","crystal"],
["saucer","uranium","cosmonaut","raygun","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","saucer","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","flask","gear","crystal"],
["saucer","uranium","cosmonaut","raygun","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","saucer","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","flask","gear","crystal"],
["saucer","uranium","cosmonaut","raygun","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","saucer","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","flask","gear","crystal"],
["saucer","uranium","cosmonaut","raygun","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","saucer","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","flask","gear","crystal"]
],
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, 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],
[0, 1, 1, 1, 0], [2, 1, 1, 1, 2], [1, 1, 0, 1, 1], [1, 1, 2, 1, 1], [0, 2, 0, 2, 0]
],
pays: {
saucer: { 3: 90, 4: 450, 5: 2200 },
uranium: { 3: 2, 4: 10, 5: 50 },
cosmonaut: { 2: 10, 3: 100, 4: 500, 5: 2400 },
raygun: { 2: 8, 3: 80, 4: 400, 5: 2000 },
robot: { 2: 5, 3: 60, 4: 280, 5: 1200 },
rocket: { 2: 5, 3: 50, 4: 240, 5: 1000 },
satellite: { 3: 42, 4: 190, 5: 700 },
planet: { 3: 42, 4: 190, 5: 700 },
telescope: { 3: 32, 4: 130, 5: 450 },
radar: { 3: 32, 4: 130, 5: 450 },
atom: { 3: 26, 4: 100, 5: 350 },
magnet: { 3: 26, 4: 100, 5: 350 },
voltmeter: { 3: 24, 4: 85, 5: 280 },
flask: { 3: 20, 4: 65, 5: 240 },
gear: { 3: 17, 4: 55, 5: 190 },
crystal: { 3: 15, 4: 45, 5: 140 }
},
symbolIndices: {
saucer:0, uranium:1, cosmonaut:2, raygun:3, robot:4, rocket:5,
satellite:6, planet:7, telescope:8, radar:9, atom:10, magnet:11,
voltmeter:12, flask:13, gear:14, crystal:15
}
};
// --- AUDIO SYNTHESIZER (Pure Web Audio API) ---
let audioCtx = null;
let isMuted = false;
function initAudio() {
if (!audioCtx) {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (AudioContextClass) audioCtx = new AudioContextClass();
}
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playSound(type, param = 0) {
if (isMuted || !audioCtx) return;
const now = audioCtx.currentTime;
if (type === 'spin') {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(80, now);
osc.frequency.linearRampToValueAtTime(140, now + 0.15);
gain.gain.setValueAtTime(0.08, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.18);
} else if (type === 'stop') {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(160, now);
osc.frequency.exponentialRampToValueAtTime(45, now + 0.08);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.09);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.09);
} else if (type === 'nearmiss') {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(320, now);
osc.frequency.linearRampToValueAtTime(700, now + 0.5);
gain.gain.setValueAtTime(0.12, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.5);
} else if (type === 'credit') {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(880 + (param % 12) * 50, now);
gain.gain.setValueAtTime(0.08, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.05);
} else if (type === 'win') {
const notes = param >= 20 ? [261, 329, 392, 523, 659, 783, 1046] : [330, 392, 493, 587];
notes.forEach((freq, idx) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now + idx * 0.08);
gain.gain.setValueAtTime(0.15, now + idx * 0.08);
gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.08 + 0.25);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now + idx * 0.08);
osc.stop(now + idx * 0.08 + 0.26);
});
} else if (type === 'feature') {
for (let i = 0; i < 6; i++) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(300 + i * 120, now + i * 0.09);
osc.frequency.exponentialRampToValueAtTime(900 + i * 150, now + i * 0.09 + 0.15);
gain.gain.setValueAtTime(0.18, now + i * 0.09);
gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.09 + 0.2);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now + i * 0.09);
osc.stop(now + i * 0.09 + 0.21);
}
}
}
// --- STATE ---
let credits = 500;
let isSpinning = false;
let reelStops = [0, 0, 0, 0, 0];
let visibleGrid = [[], [], []];
let reelOffsets = [0, 0, 0, 0, 0];
let spinSpeeds = [0, 0, 0, 0, 0];
let reelLocked = [false, false, false, false, false];
let pendingFeatureSpins = 0;
let overdriveActive = false;
let winLinesToDraw = [];
// DOM
const machineEl = document.getElementById('machine');
const creditDisplay = document.getElementById('credit-display');
const paidDisplay = document.getElementById('paid-display');
const betDisplay = document.getElementById('bet-display');
const statusLine = document.getElementById('status-line');
const featIndicator = document.getElementById('feat-indicator');
const playBtn = document.getElementById('playBtn');
const muteBtn = document.getElementById('muteBtn');
const canvas = document.getElementById('reelCanvas');
const ctx = canvas.getContext('2d');
// Scaling to fit viewport flawlessly without scrollbars
function resizeViewport() {
const w = window.innerWidth;
const h = window.innerHeight;
const scale = Math.min(w / 490, h / 860, 1.3);
machineEl.style.transform = `scale(${scale})`;
}
window.addEventListener('resize', resizeViewport);
resizeViewport();
// --- VECTOR FALLBACK ARTWORK (Authentic 1950s Sci-Fi Glyphs) ---
function drawFallbackSymbol(ctx, id, x, y, size) {
ctx.save();
ctx.translate(x + size / 2, y + size / 2);
const s = size / 100;
ctx.scale(s, s);
ctx.fillStyle = '#050a12';
ctx.fillRect(-48, -48, 96, 96);
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
switch (id) {
case 'saucer':
ctx.strokeStyle = '#ffae19';
ctx.fillStyle = '#ff6a00';
ctx.beginPath();
ctx.ellipse(0, 4, 38, 14, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#e0f4ff';
ctx.beginPath();
ctx.arc(0, -2, 16, Math.PI, 0);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.strokeStyle = '#fff';
ctx.beginPath();
ctx.arc(-16, 4, 2.5, 0, Math.PI * 2);
ctx.arc(0, 6, 2.5, 0, Math.PI * 2);
ctx.arc(16, 4, 2.5, 0, Math.PI * 2);
ctx.stroke();
break;
case 'uranium':
ctx.strokeStyle = '#52ff89';
ctx.fillStyle = 'rgba(50, 255, 120, 0.2)';
ctx.beginPath();
ctx.roundRect(-16, -34, 32, 68, 8);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#39ff14';
ctx.beginPath();
ctx.rect(-10, -24, 20, 48);
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.beginPath();
ctx.moveTo(-16, -10); ctx.lineTo(16, -10);
ctx.moveTo(-16, 10); ctx.lineTo(16, 10);
ctx.stroke();
break;
case 'cosmonaut':
ctx.strokeStyle = '#f0f5fc';
ctx.fillStyle = '#7db7e8';
ctx.beginPath();
ctx.arc(0, -4, 28, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = '#ffd15c';
ctx.beginPath();
ctx.arc(-8, -10, 10, -Math.PI * 0.4, 0.2);
ctx.stroke();
ctx.fillStyle = '#c73e1d';
ctx.beginPath();
ctx.roundRect(-22, 22, 44, 18, 4);
ctx.fill();
ctx.stroke();
break;
case 'raygun':
ctx.strokeStyle = '#ff3c00';
ctx.fillStyle = '#d63400';
ctx.beginPath();
ctx.moveTo(-32, -4); ctx.lineTo(18, -4); ctx.lineTo(24, -14); ctx.lineTo(34, -14);
ctx.lineTo(34, 6); ctx.lineTo(24, 6); ctx.lineTo(18, -1); ctx.lineTo(-12, -1);
ctx.lineTo(-24, 28); ctx.lineTo(-34, 24); ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.strokeStyle = '#ffca36';
ctx.beginPath();
ctx.arc(-6, -2, 7, 0, Math.PI * 2);
ctx.arc(6, -2, 10, 0, Math.PI * 2);
ctx.stroke();
break;
case 'robot':
ctx.strokeStyle = '#7da2c7';
ctx.fillStyle = '#2a4463';
ctx.beginPath();
ctx.roundRect(-26, -24, 52, 48, 6);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#ff8800';
ctx.beginPath();
ctx.arc(-12, -6, 6, 0, Math.PI * 2);
ctx.arc(12, -6, 6, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.beginPath();
ctx.moveTo(-16, 12); ctx.lineTo(16, 12);
ctx.moveTo(0, -24); ctx.lineTo(0, -38);
ctx.stroke();
ctx.fillStyle = '#ff3300';
ctx.beginPath(); ctx.arc(0, -38, 4, 0, Math.PI * 2); ctx.fill();
break;
case 'rocket':
ctx.strokeStyle = '#ffffff';
ctx.fillStyle = '#d33612';
ctx.beginPath();
ctx.moveTo(0, -36);
ctx.quadraticCurveTo(18, -10, 16, 24);
ctx.lineTo(-16, 24);
ctx.quadraticCurveTo(-18, -10, 0, -36);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#ffaa00';
ctx.beginPath();
ctx.moveTo(16, 14); ctx.lineTo(30, 28); ctx.lineTo(14, 26); ctx.closePath();
ctx.moveTo(-16, 14); ctx.lineTo(-30, 28); ctx.lineTo(-14, 26); ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#a8e0ff';
ctx.beginPath(); ctx.arc(0, -2, 6, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
break;
case 'satellite':
ctx.strokeStyle = '#c4dbe8';
ctx.fillStyle = '#4c6c82';
ctx.beginPath(); ctx.arc(0, 0, 18, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.strokeStyle = '#ffae19';
ctx.beginPath();
ctx.moveTo(-12, -12); ctx.lineTo(-34, -34);
ctx.moveTo(12, -12); ctx.lineTo(34, -34);
ctx.moveTo(-12, 12); ctx.lineTo(-34, 34);
ctx.moveTo(12, 12); ctx.lineTo(34, 34);
ctx.stroke();
break;
case 'planet':
ctx.strokeStyle = '#ffab3d';
ctx.fillStyle = '#c76616';
ctx.beginPath(); ctx.arc(0, 0, 20, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.strokeStyle = '#ffd885';
ctx.beginPath();
ctx.ellipse(0, 0, 38, 8, -Math.PI / 7, 0, Math.PI * 2);
ctx.stroke();
break;
case 'telescope':
ctx.strokeStyle = '#ffd65c';
ctx.fillStyle = '#61481c';
ctx.beginPath();
ctx.moveTo(-28, 20); ctx.lineTo(24, -20); ctx.lineTo(32, -12); ctx.lineTo(-20, 28);
ctx.closePath();
ctx.fill(); ctx.stroke();
ctx.strokeStyle = '#99b3cc';
ctx.beginPath();
ctx.moveTo(0, 0); ctx.lineTo(-10, 34);
ctx.moveTo(0, 0); ctx.lineTo(12, 34);
ctx.stroke();
break;
case 'radar':
ctx.strokeStyle = '#4de075';
ctx.beginPath();
ctx.arc(0, 4, 30, Math.PI * 0.8, Math.PI * 0.2);
ctx.stroke();
ctx.fillStyle = '#275233';
ctx.fill();
ctx.strokeStyle = '#ffaa00';
ctx.beginPath(); ctx.moveTo(0, 4); ctx.lineTo(14, -18); ctx.stroke();
ctx.beginPath(); ctx.arc(14, -18, 3, 0, Math.PI * 2); ctx.stroke();
break;
case 'atom':
ctx.fillStyle = '#ff4400';
ctx.beginPath(); ctx.arc(0, 0, 7, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = '#38b6ff';
ctx.beginPath();
ctx.ellipse(0, 0, 34, 11, 0, 0, Math.PI * 2);
ctx.ellipse(0, 0, 34, 11, Math.PI / 3, 0, Math.PI * 2);
ctx.ellipse(0, 0, 34, 11, -Math.PI / 3, 0, Math.PI * 2);
ctx.stroke();
break;
case 'magnet':
ctx.strokeStyle = '#ff3333';
ctx.lineWidth = 8;
ctx.beginPath();
ctx.arc(0, -6, 20, Math.PI, 0, false);
ctx.lineTo(20, 16);
ctx.moveTo(-20, -6);
ctx.lineTo(-20, 16);
ctx.stroke();
ctx.strokeStyle = '#e0e0e0';
ctx.beginPath();
ctx.moveTo(-20, 14); ctx.lineTo(-20, 24);
ctx.moveTo(20, 14); ctx.lineTo(20, 24);
ctx.stroke();
break;
case 'voltmeter':
ctx.strokeStyle = '#718ca6';
ctx.fillStyle = '#111a24';
ctx.beginPath(); ctx.roundRect(-28, -24, 56, 48, 6); ctx.fill(); ctx.stroke();
ctx.strokeStyle = '#fff';
ctx.beginPath(); ctx.arc(0, 10, 20, Math.PI * 1.2, Math.PI * 1.8); ctx.stroke();
ctx.strokeStyle = '#ff3b30';
ctx.beginPath(); ctx.moveTo(0, 10); ctx.lineTo(8, -12); ctx.stroke();
break;
case 'flask':
ctx.strokeStyle = '#e0f2fe';
ctx.fillStyle = '#ff7b00';
ctx.beginPath();
ctx.moveTo(-6, -26); ctx.lineTo(6, -26); ctx.lineTo(6, -10); ctx.lineTo(24, 22);
ctx.lineTo(-24, 22); ctx.lineTo(-6, -10); ctx.closePath();
ctx.fill(); ctx.stroke();
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(-6, 12, 3, 0, Math.PI * 2); ctx.arc(8, 16, 2, 0, Math.PI * 2); ctx.fill();
break;
case 'gear':
ctx.strokeStyle = '#e5a93b';
ctx.fillStyle = '#5c4010';
ctx.beginPath(); ctx.arc(0, 0, 24, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.fillStyle = '#08101a';
ctx.beginPath(); ctx.arc(0, 0, 8, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
for (let i = 0; i < 8; i++) {
const a = (i * Math.PI) / 4;
ctx.strokeStyle = '#e5a93b';
ctx.beginPath();
ctx.moveTo(Math.cos(a) * 22, Math.sin(a) * 22);
ctx.lineTo(Math.cos(a) * 30, Math.sin(a) * 30);
ctx.stroke();
}
break;
case 'crystal':
ctx.strokeStyle = '#4fe3c1';
ctx.fillStyle = '#175c4c';
ctx.beginPath();
ctx.moveTo(0, -32); ctx.lineTo(12, -8); ctx.lineTo(8, 26);
ctx.lineTo(-8, 26); ctx.lineTo(-12, -8); ctx.closePath();
ctx.fill(); ctx.stroke();
ctx.strokeStyle = '#a8ffeb';
ctx.beginPath(); ctx.moveTo(0, -32); ctx.lineTo(0, 26); ctx.stroke();
break;
}
ctx.restore();
}
// Symbol render dispatcher: Contact Sheet first, Vector fallback second
function drawSymbol(ctx, symId, x, y, width, height) {
const idx = CONFIG.symbolIndices[symId];
if (symbolImg.complete && symbolImg.naturalWidth >= 2048) {
const col = idx % 4;
const row = Math.floor(idx / 4);
ctx.drawImage(symbolImg, col * 512, row * 512, 512, 512, x + 4, y + 4, width - 8, height - 8);
} else {
drawFallbackSymbol(ctx, symId, x, y, Math.min(width, height));
}
}
// --- CORE EVALUATION LOGIC (Checker Identical) ---
function evaluateGrid(grid) {
let lineWinsTotal = 0;
const winningLines = [];
// Evaluate 20 paylines
for (let l = 0; l < CONFIG.paylines.length; l++) {
const pl = CONFIG.paylines[l];
const lineSyms = [
grid[pl[0]][0],
grid[pl[1]][1],
grid[pl[2]][2],
grid[pl[3]][3],
grid[pl[4]][4]
];
// Identify match target
let target = null;
let count = 0;
for (let c = 0; c < 5; c++) {
const sym = lineSyms[c];
if (sym === 'uranium') break; // scatter never participates in paylines
if (target === null) {
if (sym !== 'saucer') target = sym;
count++;
} else {
if (sym === target || sym === 'saucer') {
count++;
} else {
break;
}
}
}
if (count >= 2) {
const payTableTarget = target || 'saucer';
const pays = CONFIG.pays[payTableTarget];
if (pays && pays[count]) {
const winAmount = pays[count] * CONFIG.betPerLine;
lineWinsTotal += winAmount;
winningLines.push({ lineIndex: l, count, win: winAmount });
}
}
}
// Evaluate scatters (anywhere across 15 cells)
let scatterCount = 0;
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 5; c++) {
if (grid[r][c] === 'uranium') scatterCount++;
}
}
let scatterWinTotal = 0;
if (scatterCount >= 3) {
const mult = CONFIG.pays.uranium[scatterCount] || 0;
scatterWinTotal = mult * CONFIG.totalBet;
}
return {
lineWinsTotal,
scatterCount,
scatterWinTotal,
totalWin: lineWinsTotal + scatterWinTotal,
winningLines
};
}
// --- REEL GRAPHICS LOOP ---
function getVisibleGridFromStops(stops) {
const grid = [[], [], []];
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 5; c++) {
const strip = CONFIG.reels[c];
grid[r][c] = strip[(stops[c] + r) % strip.length];
}
}
return grid;
}
function renderReels() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const colW = canvas.width / 5;
const rowH = canvas.height / 3;
// Draw Column Backdrops & Dividers
for (let c = 0; c < 5; c++) {
ctx.fillStyle = c % 2 === 0 ? '#04070c' : '#080d17';
ctx.fillRect(c * colW, 0, colW, canvas.height);
// Overdrive Column Highlight
if (overdriveActive && c === 2) {
ctx.fillStyle = 'rgba(255, 120, 0, 0.12)';
ctx.fillRect(c * colW, 0, colW, canvas.height);
}
const strip = CONFIG.reels[c];
const offset = reelOffsets[c];
const baseIdx = reelStops[c];
// Draw 5 rows per reel for seamless smooth scrolling
for (let i = -1; i <= 3; i++) {
const symIdx = (baseIdx + i + strip.length * 10) % strip.length;
const symId = strip[symIdx];
const y = i * rowH + offset;
drawSymbol(ctx, symId, c * colW, y, colW, rowH);
}
// Fluted chrome reel separator
ctx.strokeStyle = '#22384f';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(c * colW, 0); ctx.lineTo(c * colW, canvas.height);
ctx.stroke();
}
// Highlight Winning Lines
if (winLinesToDraw.length > 0 && !isSpinning) {
winLinesToDraw.forEach((wl, idx) => {
const pl = CONFIG.paylines[wl.lineIndex];
ctx.save();
ctx.strokeStyle = `hsl(${(idx * 65 + 30) % 360}, 100%, 60%)`;
ctx.lineWidth = 3;
ctx.shadowColor = '#ff6a00';
ctx.shadowBlur = 8;
ctx.beginPath();
for (let c = 0; c < 5; c++) {
const x = c * colW + colW / 2;
const y = pl[c] * rowH + rowH / 2;
if (c === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
ctx.restore();
});
}
}
// Initial Draw
visibleGrid = getVisibleGridFromStops(reelStops);
renderReels();
// --- ANIMATION CONTROLLER ---
let animationId = null;
function updateAnimation() {
let allStopped = true;
for (let c = 0; c < 5; c++) {
if (!reelLocked[c]) {
allStopped = false;
reelOffsets[c] += spinSpeeds[c];
const rowH = canvas.height / 3;
if (reelOffsets[c] >= rowH) {
reelOffsets[c] -= rowH;
reelStops[c] = (reelStops[c] - 1 + CONFIG.reels[c].length) % CONFIG.reels[c].length;
}
}
}
renderReels();
if (!allStopped) {
animationId = requestAnimationFrame(updateAnimation);
}
}
// --- GAMEPLAY ORCHESTRATION ---
function startSpin(isDemo = false) {
if (isSpinning) {
// Rapid Double Press: Slam Stop immediately
slamStop();
return;
}
initAudio();
if (credits < CONFIG.totalBet && pendingFeatureSpins <= 0) {
// Emergency recharge coil
credits += 100;
creditDisplay.innerText = credits;
statusLine.innerText = "REACTOR RECHARGED: +100 CR";
playSound('feature');
return;
}
if (pendingFeatureSpins > 0) {
pendingFeatureSpins--;
overdriveActive = true;
featIndicator.innerText = `OVERDRIVE: ${pendingFeatureSpins} LEFT`;
document.querySelectorAll('.vac-tube').forEach(t => t.classList.add('pulse'));
} else {
overdriveActive = false;
featIndicator.innerText = "OVERDRIVE READY";
document.querySelectorAll('.vac-tube').forEach(t => t.classList.remove('pulse'));
credits -= CONFIG.totalBet;
creditDisplay.innerText = credits;
}
paidDisplay.innerText = "0";
statusLine.innerText = "CYCLOTRON ACCELERATING...";
winLinesToDraw = [];
isSpinning = true;
// Pick Stops (or force demo win)
let chosenStops = [0, 0, 0, 0, 0];
if (isDemo) {
// Search genuine combinations until >= 20x bet (400 credits)
for (let trial = 0; trial < 10000; trial++) {
const cand = [
Math.floor(Math.random() * CONFIG.reels[0].length),
Math.floor(Math.random() * CONFIG.reels[1].length),
Math.floor(Math.random() * CONFIG.reels[2].length),
Math.floor(Math.random() * CONFIG.reels[3].length),
Math.floor(Math.random() * CONFIG.reels[4].length)
];
const res = evaluateGrid(getVisibleGridFromStops(cand));
if (res.totalWin >= 400) {
chosenStops = cand;
break;
}
}
} else {
for (let c = 0; c < 5; c++) {
chosenStops[c] = Math.floor(Math.random() * CONFIG.reels[c].length);
}
}
// Launch Spin Motor
for (let c = 0; c < 5; c++) {
reelLocked[c] = false;
spinSpeeds[c] = 22 + c * 4;
reelOffsets[c] = 0;
}
playSound('spin');
cancelAnimationFrame(animationId);
animationId = requestAnimationFrame(updateAnimation);
// Staggered stop schedule
let scattersSeen = 0;
for (let c = 0; c < 5; c++) {
const delay = 450 + c * 280;
setTimeout(() => {
if (!isSpinning) return;
lockReel(c, chosenStops[c]);
// Near-miss detection for 4th and 5th reels
const strip = CONFIG.reels[c];
const stop = chosenStops[c];
if (strip[stop] === 'uranium' || strip[(stop+1)%strip.length] === 'uranium' || strip[(stop+2)%strip.length] === 'uranium') {
scattersSeen++;
}
if (scattersSeen >= 2 && c === 2) {
playSound('nearmiss');
statusLine.innerText = "OVERDRIVE HARMONIC TEASE...";
}
if (c === 4) {
finishSpin(chosenStops);
}
}, delay);
}
}
function lockReel(col, finalStop) {
reelLocked[col] = true;
reelOffsets[col] = 0;
reelStops[col] = finalStop;
playSound('stop');
}
function slamStop() {
for (let c = 0; c < 5; c++) {
reelLocked[c] = true;
reelOffsets[c] = 0;
}
}
function finishSpin(finalStops) {
isSpinning = false;
for (let c = 0; c < 5; c++) {
reelLocked[c] = true;
reelOffsets[c] = 0;
reelStops[c] = finalStops[c];
}
const grid = getVisibleGridFromStops(finalStops);
const result = evaluateGrid(grid);
winLinesToDraw = result.winningLines;
renderReels();
if (result.totalWin > 0) {
credits += result.totalWin;
creditDisplay.innerText = credits;
paidDisplay.innerText = result.totalWin;
const winMult = result.totalWin / CONFIG.totalBet;
if (winMult >= 25) {
statusLine.innerText = `ATOMIC JACKPOT! +${result.totalWin} CR`;
playSound('win', 25);
} else if (winMult >= 10) {
statusLine.innerText = `GREAT FISSION WIN! +${result.totalWin} CR`;
playSound('win', 12);
} else {
statusLine.innerText = `MAGNETIC FLUX WIN! +${result.totalWin} CR`;
playSound('win', 2);
}
} else {
statusLine.innerText = "TRY NEXT ORBITAL CYCLE";
}
// Trigger Radium Overdrive Feature
if (result.scatterCount >= 3) {
pendingFeatureSpins = 8;
featIndicator.innerText = "8 OVERDRIVE CYCLES!";
statusLine.innerText = "CRITICAL MASS: 8 FREE SPINS!";
playSound('feature');
}
}
// --- BUTTON WIRING ---
playBtn.addEventListener('click', () => {
startSpin(false);
});
muteBtn.addEventListener('click', () => {
initAudio();
isMuted = !isMuted;
muteBtn.innerText = isMuted ? "MUTED" : "AUDIO";
muteBtn.style.color = isMuted ? "#ff4444" : "#dbe7f3";
});
// --- URL MODE INTERFACE (#demo & #audit=N) ---
function checkUrlModes() {
const hash = window.location.hash;
if (hash === '#demo') {
setTimeout(() => startSpin(true), 400);
} else if (hash.startsWith('#audit')) {
const match = hash.match(/#audit=(\d+)/);
const count = match ? parseInt(match[1], 10) : 50000;
runAuditSimulation(count);
}
}
function runAuditSimulation(spinCount) {
let totalWagered = 0;
let totalWon = 0;
let hits = 0;
let maxWin = 0;
let scatterFeatures = 0;
for (let i = 0; i < spinCount; i++) {
totalWagered += CONFIG.totalBet;
const stops = [
Math.floor(Math.random() * CONFIG.reels[0].length),
Math.floor(Math.random() * CONFIG.reels[1].length),
Math.floor(Math.random() * CONFIG.reels[2].length),
Math.floor(Math.random() * CONFIG.reels[3].length),
Math.floor(Math.random() * CONFIG.reels[4].length)
];
const grid = getVisibleGridFromStops(stops);
const res = evaluateGrid(grid);
if (res.totalWin > 0) {
hits++;
totalWon += res.totalWin;
if (res.totalWin > maxWin) maxWin = res.totalWin;
}
if (res.scatterCount >= 3) {
scatterFeatures++;
}
}
const rtp = ((totalWon / totalWagered) * 100).toFixed(2);
const hitFreq = ((hits / spinCount) * 100).toFixed(2);
document.getElementById('aud-spins').innerText = spinCount.toLocaleString();
document.getElementById('aud-wagered').innerText = totalWagered.toLocaleString();
document.getElementById('aud-won').innerText = totalWon.toLocaleString();
document.getElementById('aud-rtp').innerText = `${rtp}%`;
document.getElementById('aud-hit').innerText = `${hitFreq}%`;
document.getElementById('aud-max').innerText = maxWin.toLocaleString();
document.getElementById('aud-scatters').innerText = scatterFeatures.toLocaleString();
document.getElementById('audit-panel').style.display = 'block';
}
document.getElementById('closeAudit').addEventListener('click', () => {
document.getElementById('audit-panel').style.display = 'none';
});
window.addEventListener('load', checkUrlModes);
})();
</script>
</body>
</html>
{
"machine": {
"name": "ATOMIC BANDIT",
"credits": 500,
"betPerLine": 1,
"lines": 20
},
"sheet": {
"style": "1950s atomic sci-fi commercial illustration, bold graphic tempera and gouache airbrush art with subtle halftone grain, deep space blue and atomic orange accents, centered on a flat black background.",
"cells": [
{ "cell": 0, "id": "saucer", "name": "Saucer", "role": "wild", "prompt": "An iconic 1950s chrome flying saucer with glowing yellow portholes and an orange radioactive energy wake.", "pays": { "3": 90, "4": 450, "5": 2200 } },
{ "cell": 1, "id": "uranium", "name": "Uranium", "role": "scatter", "prompt": "A glass isotope ampoule filled with glowing luminescent neon green uranium solution inside an open lead casing.", "pays": { "3": 2, "4": 10, "5": 50 } },
{ "cell": 2, "id": "cosmonaut", "name": "Cosmonaut", "role": "pay", "prompt": "A retro space explorer wearing a spherical glass bubble helmet with copper neck rings and chest regulator pack.", "pays": { "2": 10, "3": 100, "4": 500, "5": 2400 } },
{ "cell": 3, "id": "raygun", "name": "Raygun", "role": "pay", "prompt": "A fluted atomic disintegrator pistol with ribbed emitter coils, finned heat sinks, and Bakelite grip.", "pays": { "2": 8, "3": 80, "4": 400, "5": 2000 } },
{ "cell": 4, "id": "robot", "name": "Robot", "role": "pay", "prompt": "A riveted tin automaton bust with vacuum tube meter eyes, antenna ears, and glowing slotted speaker mouth.", "pays": { "2": 5, "3": 60, "4": 280, "5": 1200 } },
{ "cell": 5, "id": "rocket", "name": "Rocket", "role": "pay", "prompt": "A polished chrome needle fuselage spaceship with swept triangular delta fins and exhaust plume.", "pays": { "2": 5, "3": 50, "4": 240, "5": 1000 } },
{ "cell": 6, "id": "satellite", "name": "Satellite", "role": "pay", "prompt": "A polished aluminum satellite sphere with four angled whip antennas and solar receiver nodes.", "pays": { "3": 42, "4": 190, "5": 700 } },
{ "cell": 7, "id": "planet", "name": "Planet", "role": "pay", "prompt": "A ringed gas giant planet rendered in atmospheric bands of ochre and atomic amber with tilted concentric rings.", "pays": { "3": 42, "4": 190, "5": 700 } },
{ "cell": 8, "id": "telescope", "name": "Telescope", "role": "pay", "prompt": "A heavy brass observatory refractor telescope on a cast-iron equatorial pedestal mount.", "pays": { "3": 32, "4": 130, "5": 450 } },
{ "cell": 9, "id": "radar", "name": "Radar", "role": "pay", "prompt": "A parabolic mesh wire radar antenna dish emitting glowing concentric green oscilloscope pulses.", "pays": { "3": 32, "4": 130, "5": 450 } },
{ "cell": 10, "id": "atom", "name": "Atom", "role": "pay", "prompt": "A Rutherford-Bohr model atomic diagram with glowing red nucleus and orbiting cyan electrons along elliptical tracks.", "pays": { "3": 26, "4": 100, "5": 350 } },
{ "cell": 11, "id": "magnet", "name": "Magnet", "role": "pay", "prompt": "A red and chrome Alnico horseshoe magnet with visible blue magnetic flux sparks arcing between poles.", "pays": { "3": 26, "4": 100, "5": 350 } },
{ "cell": 12, "id": "voltmeter", "name": "Voltmeter", "role": "pay", "prompt": "An analog galvanometer gauge set in dark Bakelite with an illuminated meter needle and red warning zone.", "pays": { "3": 24, "4": 85, "5": 280 } },
{ "cell": 13, "id": "flask", "name": "Flask", "role": "pay", "prompt": "A glass Erlenmeyer laboratory beaker bubbling with effervescent amber isotope liquid.", "pays": { "3": 20, "4": 65, "5": 240 } },
{ "cell": 14, "id": "gear", "name": "Gear", "role": "pay", "prompt": "A machined bronze mechanical clockwork gear with beveled perimeter teeth and lightening holes.", "pays": { "3": 17, "4": 55, "5": 190 } },
{ "cell": 15, "id": "crystal", "name": "Crystal", "role": "pay", "prompt": "A cluster of faceted prismatic quartz crystals radiating harmonic green piezoelectric glow.", "pays": { "3": 15, "4": 45, "5": 140 } }
]
},
"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, 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],
[0, 1, 1, 1, 0],
[2, 1, 1, 1, 2],
[1, 1, 0, 1, 1],
[1, 1, 2, 1, 1],
[0, 2, 0, 2, 0]
],
"reels": [
["uranium","cosmonaut","raygun","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","voltmeter","flask","gear","crystal","crystal"],
["saucer","uranium","cosmonaut","raygun","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","saucer","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","flask","gear","crystal"],
["saucer","uranium","cosmonaut","raygun","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","saucer","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","flask","gear","crystal"],
["saucer","uranium","cosmonaut","raygun","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","saucer","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","flask","gear","crystal"],
["saucer","uranium","cosmonaut","raygun","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","saucer","robot","rocket","satellite","planet","telescope","radar","atom","magnet","voltmeter","flask","gear","crystal","flask","gear","crystal"]
],
"math": {
"claimedRtpBase": 94.7,
"claimedHitFrequency": 27.2,
"claimedFeatureRtp": 2.0,
"volatility": "medium"
},
"cabinet": [
{ "id": "marquee", "aspect": "16:9", "prompt": "1950s airbrush atomic laboratory marquee with chrome bezels and illuminated radium lettering." },
{ "id": "chassis", "aspect": "9:16", "prompt": "Deep blue enameled steel cabinet chassis with Bakelite ribs, vacuum tube housing, and chrome trim." }
]
}
art/symbols.png is absent or fails to load, the engine instantly activates inline HTML5 canvas vector rendering routines that draw authentic 1950s gouache-and-chrome sci-fi glyphs for each symbol index.The Radium Overdrive Resonance Coil: When two Uranium scatter ampoules lock onto reels 1 and 2, the machine engages an accelerating 60Hz magnetic hum and rising theremin whine while reel 3 spins. Hitting three or more scatters floods the cabinet with amber tube radiation and activates 8 Overdrive Fission Cycles. During Overdrive, the entire middle reel transforms into an electrified, pulsing Wild column that links all left-to-right trajectories, driving high-paying four- and five-of-a-kind connections on every free cycle.