SPEAK AND-MATH - INTERACTIVE ARCADE GAME SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Speak and Math</title>
<style>
:root{
--case-blue:#1d5fb0;
--case-blue-dark:#123f78;
--case-blue-deep:#0c2c56;
--panel-navy:#0f345f;
--orange:#f5821f;
--orange-dark:#c25f0a;
--key-cream:#f3ecdc;
--key-cream-edge:#cbc2ab;
--key-red:#e23b2e;
--key-teal:#1fa79a;
--vfd-bg:#04120c;
--vfd-glow:#77ffd2;
--vfd-dim:#123d2f;
--text-light:#eaf2ff;
}
*{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
html,body{margin:0;padding:0;}
body{
min-height:100vh;
background:transparent;
font-family:"Arial Narrow", "Helvetica Neue", Arial, sans-serif;
display:flex;
align-items:flex-start;
justify-content:center;
padding:185px 24px 48px;
color:#12161f;
}
@media (prefers-color-scheme: dark) {
body { color:var(--text-light); }
}
/* Fullscreen Mode Styling */
body.in-fullscreen { padding: 48px 24px !important; align-items: center !important; }
.machine-wrap{width:min(640px,100%);}
.handle{
width:62%;
height:58px;
margin:0 auto -14px;
border:14px solid var(--orange);
border-bottom:none;
border-radius:34px 34px 0 0;
box-shadow:
inset 0 4px 0 rgba(255,255,255,.35),
inset 0 -6px 0 var(--orange-dark),
0 6px 10px rgba(0,0,0,.35);
position:relative;
z-index:0;
}
.case{
position:relative;
z-index:1;
background:linear-gradient(180deg,#2a72c8 0%, var(--case-blue) 22%, var(--case-blue-dark) 100%);
border-radius:34px;
padding:22px 24px 30px;
box-shadow:
inset 0 3px 0 rgba(255,255,255,.28),
inset 0 -10px 18px rgba(0,0,0,.35),
inset 8px 0 14px rgba(255,255,255,.06),
inset -8px 0 14px rgba(0,0,0,.25),
0 22px 40px rgba(0,0,0,.55);
}
.case::after{
content:"";
}
.screw{
position:absolute;width:14px;height:14px;border-radius:50%;
background:radial-gradient(circle at 35% 30%, #9db6d6, #45608a 60%, #223a5c);
box-shadow:inset 0 1px 2px rgba(0,0,0,.6);
}
.screw::after{
content:"";position:absolute;left:2px;right:2px;top:50%;height:2px;margin-top:-1px;
background:#132844;border-radius:2px;transform:rotate(35deg);
}
.screw.tl{top:14px;left:16px;} .screw.tr{top:14px;right:16px;}
.screw.bl{bottom:14px;left:16px;} .screw.br{bottom:14px;right:16px;}
.topbar{
display:flex;align-items:center;justify-content:space-between;
gap:14px;margin:2px 6px 14px;
}
.logo{
background:var(--orange);
color:#fff;
font-weight:900;
font-family:Arial, Helvetica, sans-serif;
font-style:italic;
letter-spacing:.5px;
padding:7px 16px 8px;
border-radius:12px;
font-size:clamp(18px,4vw,26px);
text-shadow:0 2px 0 rgba(0,0,0,.25);
box-shadow:inset 0 2px 0 rgba(255,255,255,.4), inset 0 -3px 0 var(--orange-dark), 0 3px 6px rgba(0,0,0,.3);
white-space:nowrap;
}
.logo small{font-size:.5em;font-weight:700;display:block;font-style:normal;letter-spacing:2px;margin-top:-2px;}
.speaker{
flex:1;max-width:210px;height:56px;border-radius:14px;
background:
repeating-linear-gradient(90deg, #0c2c56 0 6px, #1a4a86 6px 12px);
box-shadow:inset 0 3px 8px rgba(0,0,0,.6), inset 0 -1px 0 rgba(255,255,255,.15);
}
.display-bezel{
background:linear-gradient(180deg,#0a1f3c,#122c52);
border-radius:16px;
padding:12px 14px;
box-shadow:inset 0 4px 10px rgba(0,0,0,.7), inset 0 -1px 0 rgba(255,255,255,.12);
margin:0 4px 16px;
}
.vfd{
position:relative;
background:
radial-gradient(120% 160% at 50% 0%, #0a2418 0%, var(--vfd-bg) 60%);
border-radius:8px;
min-height:86px;
padding:14px 16px 10px;
overflow:hidden;
box-shadow:inset 0 0 24px rgba(0,0,0,.9);
}
.vfd::before{
content:"";position:absolute;inset:0;
background:linear-gradient(115deg, rgba(255,255,255,.10) 0%, rgba(255,255,255,0) 28%);
pointer-events:none;
}
.vfd::after{
content:"";position:absolute;inset:0;
background:repeating-linear-gradient(0deg, rgba(0,0,0,.22) 0 2px, transparent 2px 4px);
pointer-events:none;mix-blend-mode:multiply;
}
.vfd-main{
font-family:"Courier New", ui-monospace, Menlo, monospace;
font-weight:700;
font-size:clamp(24px,6vw,40px);
letter-spacing:.14em;
color:var(--vfd-glow);
text-shadow:0 0 6px rgba(119,255,210,.9), 0 0 18px rgba(119,255,210,.45);
min-height:1.2em;
white-space:nowrap;
overflow:hidden;
}
.vfd-sub{
font-family:"Courier New", ui-monospace, monospace;
font-size:clamp(11px,2.6vw,14px);
letter-spacing:.22em;
color:var(--vfd-glow);
opacity:.75;
text-shadow:0 0 5px rgba(119,255,210,.7);
margin-top:6px;
text-transform:uppercase;
min-height:1.1em;
white-space:nowrap;overflow:hidden;
}
.vfd.off .vfd-main,.vfd.off .vfd-sub{color:var(--vfd-dim);text-shadow:none;opacity:.5;}
.vfd-status{
position:absolute;top:8px;right:12px;display:flex;gap:10px;
font-family:Arial, sans-serif;font-size:9px;letter-spacing:1px;color:var(--vfd-glow);
}
.lamp{display:flex;align-items:center;gap:4px;opacity:.25;transition:opacity .2s;}
.lamp.on{opacity:1;text-shadow:0 0 6px rgba(119,255,210,.8);}
.lamp::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor;box-shadow:0 0 6px currentColor;}
.panel{
background:linear-gradient(180deg,#144073,var(--panel-navy));
border-radius:20px;
padding:16px 16px 18px;
box-shadow:inset 0 3px 8px rgba(0,0,0,.55), inset 0 -1px 0 rgba(255,255,255,.1);
}
.section-label{
font-size:10px;letter-spacing:2.5px;text-transform:uppercase;
color:#9fc0e8;margin:2px 4px 8px;font-weight:700;
}
.activity-row{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:10px;
margin-bottom:16px;
}
button.key{
appearance:none;border:none;cursor:pointer;
font-family:"Arial Narrow", Arial, sans-serif;
border-radius:12px;
padding:10px 4px 12px;
font-weight:800;
letter-spacing:.4px;
color:#20242c;
background:linear-gradient(180deg,#fff 0%, var(--key-cream) 45%, #e2d8c2 100%);
box-shadow:
inset 0 2px 0 rgba(255,255,255,.9),
inset 0 -4px 0 var(--key-cream-edge),
0 4px 0 #8d8570,
0 7px 10px rgba(0,0,0,.45);
transition:transform .05s, box-shadow .05s, filter .15s;
user-select:none;
line-height:1.05;
}
button.key:active, button.key.pressed{
transform:translateY(3px);
box-shadow:
inset 0 2px 0 rgba(255,255,255,.7),
inset 0 -2px 0 var(--key-cream-edge),
0 1px 0 #8d8570,
0 2px 4px rgba(0,0,0,.4);
}
button.key:focus-visible{outline:3px solid #ffd27a;outline-offset:2px;}
.key .big{display:block;font-size:clamp(15px,3.6vw,20px);}
.key .tiny{display:block;font-size:9px;letter-spacing:1px;margin-top:2px;font-weight:700;color:#5a5343;text-transform:uppercase;}
.key.orange{
color:#fff;
background:linear-gradient(180deg,#ffb066 0%, var(--orange) 45%, #d96f10 100%);
box-shadow:
inset 0 2px 0 rgba(255,255,255,.55),
inset 0 -4px 0 var(--orange-dark),
0 4px 0 #7c3f05,
0 7px 10px rgba(0,0,0,.45);
text-shadow:0 1px 0 rgba(0,0,0,.25);
}
.key.orange .tiny{color:#ffe6cc;}
.key.red{
color:#fff;
background:linear-gradient(180deg,#ff8a7e 0%, var(--key-red) 45%, #b32418 100%);
box-shadow:inset 0 2px 0 rgba(255,255,255,.5), inset 0 -4px 0 #8f1d13, 0 4px 0 #5c0f08, 0 7px 10px rgba(0,0,0,.45);
text-shadow:0 1px 0 rgba(0,0,0,.3);
}
.key.red .tiny{color:#ffd9d4;}
.key.teal{
color:#fff;
background:linear-gradient(180deg,#63d9cc 0%, var(--key-teal) 45%, #147d73 100%);
box-shadow:inset 0 2px 0 rgba(255,255,255,.5), inset 0 -4px 0 #0e6058, 0 4px 0 #093f3a, 0 7px 10px rgba(0,0,0,.45);
text-shadow:0 1px 0 rgba(0,0,0,.3);
}
.key.teal .tiny{color:#d5fbf6;}
.key.navy{
color:#fff;
background:linear-gradient(180deg,#4d7fc0 0%, #2a5a9c 45%, #1c4278 100%);
box-shadow:inset 0 2px 0 rgba(255,255,255,.4), inset 0 -4px 0 #14315a, 0 4px 0 #0b2140, 0 7px 10px rgba(0,0,0,.45);
text-shadow:0 1px 0 rgba(0,0,0,.3);
}
.key.navy .tiny{color:#cfe1f8;}
.main-grid{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:10px;
}
.key.wide{grid-column:span 2;}
.machine.off button.key{filter:saturate(.7) brightness(.92);}
.foot{
text-align:center;
margin-top:18px;
font-size:11.5px;
letter-spacing:.4px;
color:#8ea3c4;
}
.foot b{color:#c8d8f2;}
@media (max-width:480px){
.case{padding:16px 12px 22px;border-radius:26px;}
.activity-row{gap:7px;}
.main-grid{gap:7px;}
button.key{border-radius:10px;padding:9px 2px 11px;}
.speaker{height:44px;}
.handle{height:44px;border-width:11px;}
}
@media (prefers-reduced-motion:reduce){
button.key{transition:none;}
}
</style>
</head>
<body>
<div class="machine-wrap machine off" id="machine">
<div class="handle" aria-hidden="true"></div>
<div class="case">
<div class="screw tl"></div><div class="screw tr"></div>
<div class="screw bl"></div><div class="screw br"></div>
<div class="topbar">
<div class="logo">Speak & Math<small>TALKING LEARNING AID</small></div>
<div class="speaker" aria-hidden="true"></div>
</div>
<div class="display-bezel">
<div class="vfd off" id="vfd">
<div class="vfd-status">
<span class="lamp" id="lampLevel">LEVEL 1</span>
<span class="lamp" id="lampScore">PROB 0/10</span>
</div>
<div class="vfd-main" id="displayMain"> </div>
<div class="vfd-sub" id="displaySub"> </div>
</div>
</div>
<div class="panel">
<div class="section-label">Activities</div>
<div class="activity-row">
<button class="key orange" data-act="solve"><span class="big">SOLVE</span><span class="tiny">It</span></button>
<button class="key orange" data-act="gl"><span class="big">> <</span><span class="tiny">Greater / Less</span></button>
<button class="key orange" data-act="word"><span class="big">WORD</span><span class="tiny">Problems</span></button>
<button class="key orange" data-act="write"><span class="big">WRITE</span><span class="tiny">It</span></button>
<button class="key orange" data-act="stumper"><span class="big">NUMBER</span><span class="tiny">Stumper</span></button>
</div>
<div class="section-label">Keyboard</div>
<div class="main-grid">
<button class="key teal" data-k="on"><span class="big">ON</span><span class="tiny">Go</span></button>
<button class="key" data-k="7"><span class="big">7</span></button>
<button class="key" data-k="8"><span class="big">8</span></button>
<button class="key" data-k="9"><span class="big">9</span></button>
<button class="key navy" data-k="div"><span class="big">÷</span></button>
<button class="key navy" data-k="level"><span class="big">LEVEL</span><span class="tiny">1·2·3</span></button>
<button class="key" data-k="4"><span class="big">4</span></button>
<button class="key" data-k="5"><span class="big">5</span></button>
<button class="key" data-k="6"><span class="big">6</span></button>
<button class="key navy" data-k="mul"><span class="big">×</span></button>
<button class="key navy" data-k="repeat"><span class="big">REPEAT</span><span class="tiny">Say again</span></button>
<button class="key" data-k="1"><span class="big">1</span></button>
<button class="key" data-k="2"><span class="big">2</span></button>
<button class="key" data-k="3"><span class="big">3</span></button>
<button class="key navy" data-k="sub"><span class="big">−</span></button>
<button class="key red" data-k="off"><span class="big">OFF</span></button>
<button class="key" data-k="gt"><span class="big">></span><span class="tiny">Greater</span></button>
<button class="key" data-k="0"><span class="big">0</span></button>
<button class="key" data-k="lt"><span class="big"><</span><span class="tiny">Less</span></button>
<button class="key navy" data-k="add"><span class="big">+</span></button>
<button class="key red" data-k="erase"><span class="big">ERASE</span></button>
<button class="key teal wide" data-k="enter"><span class="big">ENTER</span></button>
<button class="key navy wide" data-k="eq"><span class="big">=</span></button>
</div>
</div>
</div>
<p class="foot">A fan-made homage to the classic 1980 talking math tutor. <b>Turn your sound on</b> — it speaks!
Keyboard works too: digits, <b>+ − * /</b>, <b>< ></b>, <b>Enter</b>, <b>Backspace</b> = erase, <b>R</b> = repeat, <b>L</b> = level, <b>F</b> = fullscreen.</p>
</div>
<script>
(function(){
"use strict";
let ac = null;
function audio(){
if(!ac){ try{ ac = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} }
if(ac && ac.state === "suspended") ac.resume();
return ac;
}
function tone(freq, dur, type, vol, when){
const ctx = audio(); if(!ctx) return;
const t0 = ctx.currentTime + (when||0);
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = type||"square";
o.frequency.setValueAtTime(freq, t0);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(vol||0.12, t0+0.01);
g.gain.exponentialRampToValueAtTime(0.0001, t0+dur);
o.connect(g).connect(ctx.destination);
o.start(t0); o.stop(t0+dur+0.02);
}
const sfx = {
key(){ tone(1150, .06, "square", .07); },
enter(){ tone(880,.07,"square",.09); tone(1320,.09,"square",.08,.07); },
right(){ [660,880,1100,1470].forEach((f,i)=>tone(f,.11,"triangle",.12,i*.09)); },
wrong(){ tone(160,.28,"sawtooth",.14); tone(120,.3,"sawtooth",.12,.05); },
fanfare(){ [523,659,784,1046,784,1046].forEach((f,i)=>tone(f,.14,"triangle",.13,i*.12)); },
power(){ [400,600,800,1200].forEach((f,i)=>tone(f,.09,"square",.1,i*.07)); },
off(){ [900,600,350].forEach((f,i)=>tone(f,.09,"square",.1,i*.06)); }
};
const speech = {
voice:null,
pick(){
const vs = window.speechSynthesis ? speechSynthesis.getVoices() : [];
this.voice = vs.find(v=>/en[-_]/i.test(v.lang) && /male|daniel|david|alex|fred/i.test(v.name))
|| vs.find(v=>/^en/i.test(v.lang)) || vs[0] || null;
},
say(text, cb){
if(!window.speechSynthesis){ if(cb) setTimeout(cb, 300); return; }
speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text);
if(this.voice) u.voice = this.voice;
u.pitch = 0.55;
u.rate = 0.85;
u.volume = 1;
if(cb){ u.onend = cb; u.onerror = cb; }
speechSynthesis.speak(u);
},
stop(){ if(window.speechSynthesis) speechSynthesis.cancel(); }
};
if(window.speechSynthesis){
speech.pick();
speechSynthesis.onvoiceschanged = ()=>speech.pick();
}
const $ = s=>document.querySelector(s);
const machineEl = $("#machine");
const vfd = $("#vfd");
const dMain = $("#displayMain");
const dSub = $("#displaySub");
const lampLevel = $("#lampLevel");
const lampScore = $("#lampScore");
function show(main, sub){
dMain.textContent = (main===undefined||main===null||main==="") ? "\u00a0" : main;
if(sub !== undefined) dSub.textContent = sub==="" ? "\u00a0" : sub;
}
function updateLamps(){
lampLevel.textContent = "LEVEL " + S.level;
lampScore.textContent = "PROB " + Math.min(S.probNum, ROUND) + "/" + ROUND;
lampLevel.classList.toggle("on", S.power);
lampScore.classList.toggle("on", S.power && !!S.activity);
}
const rnd = (a,b)=>Math.floor(Math.random()*(b-a+1))+a;
const pick = arr=>arr[rnd(0,arr.length-1)];
const OPSYM = {add:"+", sub:"−", mul:"×", div:"÷"};
const OPWORD = {add:"plus", sub:"minus", mul:"times", div:"divided by"};
const ROUND = 10;
const S = {
power:false,
activity:null,
level:1,
probNum:0,
correctCount:0,
tries:0,
entry:"",
answer:null,
promptSpeech:"",
displayProblem:"",
locked:false,
stump:null
};
function genSolve(){
const L = S.level;
let op, a, b;
if(L===1){
op = pick(["add","sub"]);
a = rnd(1,9); b = rnd(1,9);
if(op==="sub" && b>a) [a,b]=[b,a];
}else if(L===2){
op = pick(["add","sub","mul"]);
if(op==="mul"){ a=rnd(2,9); b=rnd(2,9); }
else { a=rnd(10,99); b=rnd(1,99); if(op==="sub" && b>a) [a,b]=[b,a]; }
}else{
op = pick(["add","sub","mul","div"]);
if(op==="mul"){ a=rnd(3,12); b=rnd(3,19); }
else if(op==="div"){ b=rnd(2,12); const q=rnd(2,12); a=b*q; }
else { a=rnd(25,999); b=rnd(10,999); if(op==="sub" && b>a) [a,b]=[b,a]; }
}
const ans = op==="add"?a+b: op==="sub"?a-b: op==="mul"?a*b: a/b;
S.answer = ans;
S.displayProblem = a+" "+OPSYM[op]+" "+b+" = ";
S.promptSpeech = a+" "+OPWORD[op]+" "+b+" equals?";
show(S.displayProblem, "SOLVE IT · TYPE ANSWER, ENTER");
speech.say(S.promptSpeech);
}
function genGL(){
const L = S.level;
const hi = L===1?20 : L===2?200 : 5000;
let a = rnd(0,hi), b = rnd(0,hi);
while(a===b) b = rnd(0,hi);
S.answer = a>b ? ">" : "<";
S.displayProblem = a+" ? "+b;
S.promptSpeech = "Is "+a+" greater than, or less than "+b+"?";
show(S.displayProblem, "PRESS > OR <");
speech.say(S.promptSpeech);
}
function genWrite(){
const L = S.level;
const n = L===1 ? rnd(3,99) : L===2 ? rnd(100,999) : rnd(1000,99999);
S.answer = n;
S.displayProblem = "";
S.promptSpeech = "Write the number "+n+".";
show("_", "WRITE IT · TYPE WHAT I SAY");
speech.say(S.promptSpeech);
}
function genWord(){
const L = S.level;
let a,b,ans,text,op;
const T1 = [
(a,b)=>["You have "+a+" apples. A friend gives you "+b+" more. How many apples do you have?", a+b],
(a,b)=>["There are "+a+" birds in a tree. "+b+" fly away. How many birds are left?", a-b],
(a,b)=>["You read "+a+" pages, then "+b+" more pages. How many pages did you read?", a+b],
(a,b)=>["A box holds "+a+" crayons. You take out "+b+". How many crayons stay in the box?", a-b]
];
const T2 = [
(a,b)=>["Stickers come in packs of "+a+". You buy "+b+" packs. How many stickers is that?", a*b],
(a,b)=>["A bus has "+a+" seats. "+b+" seats are empty. How many seats are taken?", a-b],
(a,b)=>["You save "+a+" pennies each day for "+b+" days. How many pennies do you save?", a*b],
(a,b)=>["A book has "+a+" pages. You have read "+b+". How many pages are left?", a-b]
];
const T3 = [
(a,b)=>["There are "+(a*b)+" cookies shared equally among "+b+" friends. How many cookies does each friend get?", a],
(a,b)=>["A train travels "+a+" miles every hour for "+b+" hours. How far does it travel?", a*b],
(a,b)=>["You have "+(a*b)+" marbles in bags of "+b+". How many bags do you have?", a],
(a,b)=>["Tickets cost "+a+" dollars each. What do "+b+" tickets cost?", a*b]
];
if(L===1){ a=rnd(2,9); b=rnd(1,9); if(b>a)[a,b]=[b,a]; [text,ans]=pick(T1)(a,b); }
else if(L===2){ a=rnd(3,12); b=rnd(2,9); if(b>a)[a,b]=[b,a]; [text,ans]=pick(T2)(a,b); }
else { a=rnd(3,12); b=rnd(2,9); [text,ans]=pick(T3)(a,b); }
S.answer = ans;
S.displayProblem = "";
S.promptSpeech = text;
show("?", "WORD PROBLEM · LISTEN, THEN TYPE");
speech.say(text);
}
function genStumper(){
const digits = S.level===1?3 : S.level===2?4 : 5;
const lo = Math.pow(10,digits-1), hi = Math.pow(10,digits)-1;
S.stump = { secret:rnd(lo,hi), digits:digits, known:Array(digits).fill("_"), guesses:0 };
S.answer = S.stump.secret;
S.displayProblem = "";
S.promptSpeech = "I am thinking of a "+digits+" digit number. Guess my number.";
show(S.stump.known.join(" "), "NUMBER STUMPER · GUESS + ENTER");
speech.say(S.promptSpeech);
}
const GENS = {solve:genSolve, gl:genGL, write:genWrite, word:genWord, stumper:genStumper};
const ACTNAME = {solve:"Solve it", gl:"Greater than, less than", word:"Word problems", write:"Write it", stumper:"Number stumper"};
function startActivity(act){
S.activity = act;
S.probNum = 0;
S.correctCount = 0;
S.stump = null;
speech.say("Here is "+ACTNAME[act]+", level "+S.level+".", ()=>nextProblem());
show("", ACTNAME[act].toUpperCase()+" · LEVEL "+S.level);
updateLamps();
}
function nextProblem(){
if(!S.power || !S.activity) return;
S.probNum++;
S.tries = 0;
S.entry = "";
updateLamps();
if(S.probNum > ROUND){ finishRound(); return; }
GENS[S.activity]();
}
function finishRound(){
const pct = Math.round(S.correctCount/ROUND*100);
S.locked = true;
sfx.fanfare();
show("SCORE "+pct+"%", S.correctCount+" OF "+ROUND+" CORRECT");
let line = "Here is your score. "+S.correctCount+" out of "+ROUND+" correct. That is "+pct+" percent. ";
line += pct===100 ? "Perfect score! You are a math whiz!"
: pct>=80 ? "Great job!"
: pct>=50 ? "Good work. Keep practicing."
: "Nice try. Let's practice some more.";
speech.say(line, ()=>{
S.locked = false;
S.activity = null;
updateLamps();
show("PICK AN ACTIVITY","OR PRESS LEVEL");
});
}
function redrawEntry(){
if(S.activity==="stumper" && S.stump){
const g = S.entry.padEnd(S.stump.digits,"_").slice(0,S.stump.digits);
show(g.split("").join(" "), undefined);
}else if(S.activity==="solve"){
show(S.displayProblem + (S.entry||"_"));
}else{
show(S.entry||"_");
}
}
function handleCorrect(){
S.correctCount++;
S.locked = true;
sfx.right();
const praise = pick(["That is correct.","Correct!","You are right.","That is right. Super!","Correct. Well done!"]);
if(S.activity==="solve") show(S.displayProblem + S.answer, "CORRECT!");
else if(S.activity==="gl") show(S.displayProblem.replace("?", S.answer), "CORRECT!");
else show(String(S.answer), "CORRECT!");
speech.say(praise, ()=>{ S.locked=false; nextProblem(); });
}
function handleWrong(){
S.tries++;
sfx.wrong();
if(S.tries===1){
S.entry = "";
speech.say("Wrong. Try again.", ()=>{
if(S.activity==="solve") show(S.displayProblem+"_","TRY AGAIN");
else if(S.activity==="gl") show(S.displayProblem,"TRY AGAIN · > OR <");
else show("_","TRY AGAIN");
});
show(S.activity==="solve" ? S.displayProblem+"_" : (S.activity==="gl" ? S.displayProblem : "_"), "TRY AGAIN");
}else{
S.locked = true;
const ansText = S.answer===">" ? "greater than" : S.answer==="<" ? "less than" : String(S.answer);
if(S.activity==="solve") show(S.displayProblem + S.answer, "THE ANSWER");
else if(S.activity==="gl") show(S.displayProblem.replace("?", S.answer), "THE ANSWER");
else show(String(S.answer), "THE ANSWER");
speech.say("Wrong. The answer is "+ansText+".", ()=>{ S.locked=false; nextProblem(); });
}
}
function checkStumperGuess(){
const st = S.stump;
if(!st) return;
if(S.entry.length !== st.digits){
sfx.wrong();
speech.say("Guess a "+st.digits+" digit number.");
return;
}
st.guesses++;
const guess = parseInt(S.entry,10);
const secretStr = String(st.secret);
for(let i=0;i<st.digits;i++){
if(S.entry[i]===secretStr[i]) st.known[i]=secretStr[i];
}
if(guess===st.secret){
S.correctCount++;
S.locked = true;
sfx.right();
show(secretStr.split("").join(" "), "GOT IT IN "+st.guesses+" GUESSES");
speech.say("You got it! My number was "+st.secret+". You took "+st.guesses+" guesses.", ()=>{
S.locked=false; nextProblem();
});
}else{
sfx.wrong();
const hint = guess>st.secret ? "too big" : "too small";
S.entry = "";
show(st.known.join(" "), "YOUR GUESS WAS "+hint.toUpperCase());
speech.say(String(guess)+" is "+hint+". Try again.");
}
}
function onEnter(){
if(!S.power || S.locked || !S.activity) return;
if(S.activity==="gl"){ sfx.wrong(); speech.say("Press the greater than, or less than key."); return; }
if(S.activity==="stumper"){ sfx.enter(); checkStumperGuess(); return; }
if(S.entry===""){ sfx.wrong(); return; }
sfx.enter();
const val = parseInt(S.entry,10);
S.entry = "";
if(val === S.answer) handleCorrect(); else handleWrong();
}
function onCompare(sym){
if(!S.power || S.locked) return;
if(S.activity!=="gl"){ sfx.key(); return; }
sfx.enter();
if(sym === S.answer) handleCorrect(); else handleWrong();
}
function press(k){
if(k==="on"){
audio();
if(!S.power){
S.power = true;
machineEl.classList.remove("off");
vfd.classList.remove("off");
sfx.power();
show("HELLO","PICK AN ACTIVITY");
speech.say("Hello. This is math time! Pick an activity, or press level.");
updateLamps();
}else if(S.activity && !S.locked){
speech.stop(); nextProblem();
}
return;
}
if(k==="off"){
if(!S.power) return;
speech.stop();
sfx.off();
S.power=false; S.activity=null; S.entry=""; S.stump=null;
machineEl.classList.add("off");
vfd.classList.add("off");
show("",""); updateLamps();
return;
}
if(!S.power){ return; }
if(k==="level"){
if(S.locked) return;
sfx.key();
S.level = S.level%3 + 1;
updateLamps();
speech.say("Level "+S.level+".");
if(S.activity){ startActivity(S.activity); }
else show("LEVEL "+S.level, "PICK AN ACTIVITY");
return;
}
if(k==="repeat"){
if(S.promptSpeech && S.activity && !S.locked){ sfx.key(); speech.say(S.promptSpeech); }
return;
}
if(k==="erase"){
if(S.locked) return;
sfx.key();
S.entry="";
if(S.activity) redrawEntry();
return;
}
if(k==="enter" || k==="eq"){ onEnter(); return; }
if(k==="gt"){ onCompare(">"); return; }
if(k==="lt"){ onCompare("<"); return; }
if(k==="add"||k==="sub"||k==="mul"||k==="div"){ sfx.key(); return; }
if(/^[0-9]$/.test(k)){
if(S.locked || !S.activity){ sfx.key(); return; }
if(S.activity==="gl"){ sfx.key(); return; }
sfx.key();
const maxLen = S.activity==="stumper" && S.stump ? S.stump.digits : 7;
if(S.entry.length < maxLen) S.entry += k;
redrawEntry();
return;
}
}
document.querySelectorAll("[data-act]").forEach(btn=>{
btn.addEventListener("click", ()=>{
if(!S.power){ sfx.wrong(); return; }
if(S.locked) return;
audio(); sfx.enter(); speech.stop();
startActivity(btn.dataset.act);
});
});
document.querySelectorAll("[data-k]").forEach(btn=>{
btn.addEventListener("click", ()=>press(btn.dataset.k));
});
const KEYMAP = {
"Enter":"enter","=":"eq","Backspace":"erase","Delete":"erase",
">":"gt","<":"lt","+":"add","-":"sub","*":"mul","/":"div",
"r":"repeat","R":"repeat","l":"level","L":"level",
"o":"on","O":"on"
};
document.addEventListener("keydown", e=>{
if(e.metaKey||e.ctrlKey||e.altKey) return;
if(e.key === "f" || e.key === "F"){
toggleFullscreen();
e.preventDefault();
return;
}
let k = null;
if(/^[0-9]$/.test(e.key)) k = e.key;
else if(KEYMAP[e.key]) k = KEYMAP[e.key];
if(k){
e.preventDefault();
press(k);
const btn = document.querySelector('[data-k="'+k+'"]');
if(btn){ btn.classList.add("pressed"); setTimeout(()=>btn.classList.remove("pressed"),120); }
}
});
function toggleFullscreen(){
const isFull = document.fullscreenElement || document.webkitFullscreenElement || window.innerHeight >= window.screen.height - 10;
if(!isFull){
const elem = document.documentElement;
if(elem.requestFullscreen) elem.requestFullscreen().catch(err=>console.log(err));
else if(elem.webkitRequestFullscreen) elem.webkitRequestFullscreen();
} else {
if(document.exitFullscreen) document.exitFullscreen();
else if(document.webkitExitFullscreen) document.webkitExitFullscreen();
}
}
function updateFullscreenState(){
const isFull = !!(document.fullscreenElement || document.webkitFullscreenElement || window.innerHeight >= window.screen.height - 10);
document.body.classList.toggle("in-fullscreen", isFull);
}
document.addEventListener("fullscreenchange", updateFullscreenState);
document.addEventListener("webkitfullscreenchange", updateFullscreenState);
window.addEventListener("resize", updateFullscreenState);
show("PRESS ON","");
updateLamps();
})();
</script>
</body>
</html>