Sneekie

Van GW-BASIC naar JavaScript, naast elkaar

De webversie is geen herschrijving — het is een trouwe port. De spellogica is statement voor statement vertaald uit SNEEKIE.BAS, met de originele variabelenamen en zelfs BASIC-regelnummers als commentaar. Wat echt veranderde is de dunne laag eronder: wat GW-BASIC gratis gaf — schermgeheugen, toetsenbord, geluid, GOTO — moest voor de browser opnieuw worden gebouwd. Deze pagina koppelt representatieve stukken en legt die laag uit; gebruik Uitleg voor elke BASIC-regel.

Een oud groen Sneekie-scherm dat via pixels verbonden is met een moderne browserversie.

De brug: het scherm is de datastructuur

GW-BASIC POKEte tekens rechtstreeks in het 80×25 tekstschermgeheugen van de PC en PEEKte ze terug om te zien wat waar stond. Er was geen apart "slang"- of "muur"-object — het beeld was de toestand. De port houdt dat model exact aan, als een 4000-byte Uint8Array (vram): twee bytes per cel, teken en attribuut, geadresseerd met

offset = (rij − 1) × 160 + (kolom − 1) × 2

Omdat het model identiek is, verandert de gamecode nauwelijks — alleen de woorden POKE en PEEK worden de functies poke() en peek(). De rest van deze pagina volgt uit het opnieuw bouwen van GW-BASIC's runtime rond die ene gedeelde array.

Wat opnieuw gebouwd moest worden

Het mechanisme uit 1988De vervanging uit 2026
POKE / PEEK in videogeheugen op &HB000/&HB800 poke() / peek() op een 4000-byte Uint8Array
LOCATE r,c : PRINT CHR$(n) locate(r,c); pc(n) — dezelfde cursor, dezelfde tekencodes
het scherm ververst zichzelf (het is videogeheugen) een dirty-cell set + requestAnimationFrame-redraw, met glyphs uit een ingebed IBM CP437-font
INKEY$ / INPUT$ blokkeren tot er een toets is async + een Promise-toetsenbuffer (keyOrTimeout/waitKey)
SOUND freq, ticks een Web Audio-blokgolfoscillator op dezelfde 1/18.2 s-klok
ON LEVEL GOSUB … de dispatch-arrays CFG[] en ENEMY[]
GOTO 510 / RETURN 510 (sterven) throw DEATH, gevangen rond de move-loop
variabelen (DEFINT A-Y, Nederlandse namen) de zelfde namen letterlijk behouden (T, BTEL, HART, KLAVER…)

De ene diepe wijziging: blokkeren wordt async

GW-BASIC liep van boven naar beneden en stopte gewoon bij INKEY$/INPUT$ tot je een toets indrukte. Een browsertab mag nooit zo blokkeren — daarom werden de wachtpunten awaits, waardoor playLevels() en program() async moesten worden. Een kleine Promise-toetsenbuffer vervangt de BIOS-buffer, en omdat GOTO niet bestaat, wordt de "spring eruit en sterf"-logica een geworpen DEATH-signaal dat rond de loop wordt opgevangen. Die twee trucs — await voor invoer, throw om te sterven — laten de rest een letterlijke vertaling blijven.

1

Het scherm is het datamodel

De code uit 1988 richt PEEK/POKE op videogeheugen en kiest zelfs mono (&HB000) of kleur (&HB800). De port heeft maar een in-memory raster nodig: vram is een 4000-byte array en poke()/peek() zijn kleine helpers.
1988 · GW-BASIC
80 DEFINT A-Y: SCREEN 0: WIDTH 80: CLS: ZORE=0: RANDOMIZE TIMER: DEF SEG=&H0
90 IF PEEK(&H449)=7 THEN VIDEO=&HB000 ELSE VIDEO=&HB800
100 DEF SEG=VIDEO: CLS: DIM T(15000),S(168),B(10),D(80,3)
2026 · JavaScript
/* ---------- VIDEO: 80x25 text VRAM, identical to B000/B800 layout ----------
offset = (row-1)*160 + (col-1)*2 ; even byte = CP437 char, odd byte = attr.
Only attrs 7 (normal) and 15 (bright) occur, exactly like the original. */
const vram = new Uint8Array(4000);
function poke(off, v){
if(off >= 0 && off < 4000 && vram[off] !== v){
vram[off] = v;
dirty.add(off >> 1);
}
}
function peek(off){ return (off >= 0 && off < 4000) ? vram[off] : 0; }
2

Het vaste kader tekenen

LOCATE r,c : PRINT CHR$(n) wordt locate(r,c); pc(n); STRING$(78,…) wordt pcn(196,78); SPC(78) wordt sp(78). De woorden zijn vertaald, maar de box-drawing-codes blijven gelijk.
1988 · GW-BASIC
110 LOCATE 1, 1: PRINT CHR$(218);STRING$(78,CHR$(196));CHR$(191);
120 LOCATE 2, 1: PRINT CHR$(179);SPC(78);CHR$(179);
130 LOCATE 2,17: PRINT "**** Sneekie **** (c) juli '88 by HerbySoft";
140 LOCATE 22, 1: PRINT CHR$(179);SPC(78);CHR$(179);
150 LOCATE 22, 6: PRINT "10 punten -50 punten Highscore";
160 LOCATE 23, 1: PRINT CHR$(179);SPC(78);CHR$(179);
170 LOCATE 22,55: PRINT "Level Score";
180 LOCATE 23, 6: PRINT "25 punten Steen <ESC> vastgelopen";
190 LOCATE 23,55: PRINT "Lives Bonus";
200 LOCATE 24, 1: PRINT CHR$(192);STRING$(78,CHR$(196));CHR$(217);
2026 · JavaScript
/* 80-210 + 230 + 1090-1130: boot, frame, restart loop */
async function program(){
ZORE = parseInt(lsGet('sneekie.highscore') || '0', 10) || 0; // 80 (persisted)
cls(); // 100
locate(1,1); pc(218); pcn(196,78); pc(191); // 110
locate(2,1); pc(179); sp(78); pc(179); // 120
locate(2,17); ps("**** Sneekie **** (c) July '88 by HerbySoft");
locate(22,1); pc(179); sp(78); pc(179); // 140
locate(22,6); ps('10 points -50 points Highscore'); // 150
locate(23,1); pc(179); sp(78); pc(179); // 160
locate(22,55); ps('Level Score'); // 170
locate(23,6); ps('25 points Stone <ESC> when stuck'); // 180
locate(23,55); ps('Lives Bonus'); // 190
locate(24,1); pc(192); pcn(196,78); pc(217); // 200
poke(3396,1); poke(3556,10); poke(3526,5); poke(3366,3); // 210: legend icons
3

De levelloop & de startslang

FOR LEVEL=1 TO 32 is een for-loop; de slang start op dezelfde offsets (2000 staart, 1840 kop); ON LEVEL GOSUB wordt een indexed call in de CFG-tabel.
1988 · GW-BASIC
240 FOR LEVEL=1 TO 32
250 FOR I=1 TO 17: LOCATE 3+I,1: PRINT CHR$(179);SPC(78);CHR$(179): NEXT
260 LOCATE 3,1: PRINT CHR$(195);STRING$(78,CHR$(196));CHR$(180)
270 LOCATE 21,1: PRINT CHR$(195);STRING$(78,CHR$(196));CHR$(180)
280 T(1)=2000: T(2)=1840: BTEL=2: ETEL=1
290 POKE T(BTEL),219: POKE T(ETEL),186: POKE T(BTEL)+1,15
300 E=72: F=72: HART=0: KLAVER=0: BONUS=10000: OP=0: GOSUB 1190
310 ON LEVEL GOSUB 2320,2330,2340,2350,2360,2370,2380,2390,2410,2420,2430,2440,2450,2460,2470,2480,2320,2330,2340,2350,2360,2370,2380,2390,2410,2420,2430,2440,2450,2460,2470,2480
2026 · JavaScript
/* 240-1080: FOR LEVEL=1 TO 32 */
async function playLevels(){
for(LEVEL = 1; LEVEL <= 32; LEVEL++){
/* 250-270: playfield + inner borders */
for(let I = 1; I <= 17; I++){ locate(3+I,1); pc(179); sp(78); pc(179); }
locate(3,1); pc(195); pcn(196,78); pc(180);
locate(21,1); pc(195); pcn(196,78); pc(180);
/* 280-300: snake start (head row 12, tail row 13, col 41, moving up) */
T[1] = 2000; T[2] = 1840; BTEL = 2; ETEL = 1;
poke(T[BTEL],219); poke(T[ETEL],186); poke(T[BTEL]+1,15);
E = 72; F = 72; HART = 0; KLAVER = 0; BONUS = 10000; score(0);
/* 310: level config + walls */
CFG[(LEVEL-1) % 16]();
4

Een toets lezen: blokkerende INKEY$ wordt async

De grootste structurele wijziging. De INKEY$-wachtlus kan een browser niet blokkeren, dus hij wordt een Promise: keyOrTimeout(ms) resolved bij een toets of bij timeout. De gameloop await daarop.
1988 · GW-BASIC
420 WHILE+ KLAVER+HART>0
430 A$=INKEY$: Z2=0: Z1=TIMER
440 WHILE+ A$="" AND Z2<Z
450 Z2=TIMER-Z1: A$=INKEY$
460 WEND
2026 · JavaScript
function keyOrTimeout(ms){ // 430-460: INKEY$ poll with Z timeout
return new Promise(res => {
if(kbuf.length){ res(kbuf.shift()); return; }
let to = null;
if(ms !== Infinity) to = setTimeout(() => { kwaiter = null; res(''); }, ms);
kwaiter = () => { if(to) clearTimeout(to); res(kbuf.shift()); };
});
}
function waitKey(){ return keyOrTimeout(Infinity); } // INPUT$(1)
5

Toets sorteren, en ESC

LEN(A$) onderscheidt een tweekarakter-pijltje van een enkele toets; JavaScript gebruikt A$.length. ESC wordt een geworpen doodsignaal in plaats van GOTO 510.
1988 · GW-BASIC
490 IF LEN(A$)<>1 THEN GOTO 640
500 IF ASC(A$)<>27 THEN GOTO 910
510 FOR I=1 TO 3
2026 · JavaScript
const A$ = await keyOrTimeout(Z * 1000); // 430-460
if(BONUS > 0) BONUS -= BMIN; // 470
locate(23,73); pu(6,BONUS); // 480
if(A$.length === 1){ // 490
if(A$.charCodeAt(0) === 27) throw DEATH; // 500 -> 510
E = F; score(-BMIN); sound(1000,5); continue; // -> 910 (letter key)
}
if(A$.length === 2) E = A$.charCodeAt(1); // 640
6

De kop omzetten naar een schermstap

Identieke offset-rekenkunde: een richting wordt ±160 voor een rij of ±2 voor een kolom. A is het doelvak; peek(A) vertelt wat daar staat.
1988 · GW-BASIC
640 IF LEN(A$)=2 THEN E=ASC(MID$(A$,2,1))
650 A=T(BTEL)
660 IF E=68 THEN GOTO 1080
670 IF E=67 THEN LIVE=LIVE+1: GOTO 1080
680 IF E=80 THEN A=A+160 ELSE IF E=72 THEN A=A-160
690 IF E=77 THEN A=A+2 ELSE IF E=75 THEN A=A-2
700 D=PEEK(A)
2026 · JavaScript
let A = T[BTEL]; // 650
if(E === 68){ skip = true; break; } // 660: F10
if(E === 67){ LIVE++; skip = true; break; } // 670: F9
if(E === 80) A += 160; else if(E === 72) A -= 160; // 680
if(E === 77) A += 2; else if(E === 75) A -= 2; // 690
const d = peek(A); // 700
7

Wat staat er in het volgende vak?

De IF D<>n THEN GOTO-ladder wordt een if / else if-keten op dezelfde tekencodes: leeg, klaver, hart, steen, smiley, pijl.
1988 · GW-BASIC
710 IF D<>32 THEN GOTO 740
720 POKE T(ETEL),32: POKE T(ETEL)+1,7: SOUND 1500,0.1: ETEL=ETEL+1
730 GOTO 920
740 IF D<>5 THEN GOTO 770
750 L=1: GOSUB 1150: GOSUB 2260
760 OP=25: GOSUB 1190: KLAVER=KLAVER-1: GOTO 920
770 IF D<>3 THEN GOTO 800
780 IF LEVEL>16 THEN L=5: GOSUB 1150: IF K1=1 THEN KLAVER=KLAVER+1
790 L=1: GOSUB 1150: GOSUB 2260: OP=10: GOSUB 1190: HART=HART-1: GOTO 920
800 IF D<>10 THEN GOTO 870
810 TA=A: IF E=80 THEN TA=TA+160 ELSE IF E=72 THEN TA=TA-160
820 IF E=77 THEN TA=TA+2 ELSE IF E=75 THEN TA=TA-2
830 D=PEEK(TA)
840 IF D<>32 THEN GOTO 910
850 POKE TA,10: POKE T(ETEL),32: POKE T(ETEL)+1,7
860 SOUND 1500,0.1: ETEL=ETEL+1: GOTO 920
870 IF D<>1 THEN GOTO 900
880 FOR I=50 TO 1 STEP -1: SOUND 600+75*I,0.35: NEXT
890 OP=-50: GOSUB 1190: L=1: GOSUB 1150: GOTO 920
900 IF D=24 OR D=26 OR D=27 THEN GOTO 510
2026 · JavaScript
const d = peek(A); // 700
let blocked = false;
if(d === 32){ // 710-730: empty
poke(T[ETEL],32); poke(T[ETEL]+1,7); sound(1500,0.1); ETEL++;
} else if(d === 5){ // 740-760: club +25
place(1); sub2260(); score(25); KLAVER--;
} else if(d === 3){ // 770-790: heart +10
if(LEVEL > 16){ place(5); if(K1 === 1) KLAVER++; }
place(1); sub2260(); score(10); HART--;
} else if(d === 10){ // 800-860: push the stone
let TA = A;
if(E === 80) TA += 160; else if(E === 72) TA -= 160;
if(E === 77) TA += 2; else if(E === 75) TA -= 2;
if(peek(TA) !== 32) blocked = true; // 840 -> 910
else {
poke(TA,10); poke(T[ETEL],32); poke(T[ETEL]+1,7);
sound(1500,0.1); ETEL++;
}
} else if(d === 1){ // 870-890: smiley -50
for(let I = 50; I >= 1; I--) sound(600+75*I, 0.35);
score(-50); place(1);
} else if(d === 24 || d === 26 || d === 27){ // 900: arrow = death
throw DEATH;
} else blocked = true; // muur/lijf -> 910
8

Geblokkeerd door muur of staart

Alles anders betekent: je kunt er niet in. BASIC valt door naar regel 910; JavaScript zet blocked = true. Beide houden de oude richting, trekken bonus af en slaan de zet over.
1988 · GW-BASIC
910 E=F: OP=BMIN*-1: GOSUB 1190: SOUND 1000,5: GOTO 1020
2026 · JavaScript
if(blocked){ // 910
E = F; score(-BMIN); sound(1000,5); continue;
}
Een kortlevende AI-komeet draagt een BASIC-slangenspel uit 1988 naar een gloeiende browserport.
9

De lichaamsbocht tekenen

Zes gevallen kiezen het box-drawing-stuk voor de bocht uit nieuwe richting E en oude richting F, en voegen daarna een nieuwe kop toe.
1988 · GW-BASIC
920 IF (E=77 AND F=77) OR (E=75 AND F=75) THEN POKE T(BTEL),205: GOTO 980
930 IF (E=80 AND F=80) OR (E=72 AND F=72) THEN POKE T(BTEL),186: GOTO 980
940 IF (E=80 AND F=77) OR (E=75 AND F=72) THEN POKE T(BTEL),187: GOTO 980
950 IF (E=72 AND F=77) OR (E=75 AND F=80) THEN POKE T(BTEL),188: GOTO 980
960 IF (E=80 AND F=75) OR (E=77 AND F=72) THEN POKE T(BTEL),201: GOTO 980
970 IF (E=72 AND F=75) OR (E=77 AND F=80) THEN POKE T(BTEL),200: GOTO 980
980 BTEL=BTEL+1: T(BTEL)=A: F=E: POKE T(BTEL),219
2026 · JavaScript
/* 920-970: body corner char from old+new direction */
if((E===77&&F===77)||(E===75&&F===75)) poke(T[BTEL],205);
else if((E===80&&F===80)||(E===72&&F===72)) poke(T[BTEL],186);
else if((E===80&&F===77)||(E===75&&F===72)) poke(T[BTEL],187);
else if((E===72&&F===77)||(E===75&&F===80)) poke(T[BTEL],188);
else if((E===80&&F===75)||(E===77&&F===72)) poke(T[BTEL],201);
else if((E===72&&F===75)||(E===77&&F===80)) poke(T[BTEL],200);
BTEL++; T[BTEL] = A; F = E; poke(T[BTEL],219); // 980
10

Glans & levelgevaar

De helderheids-"shimmer" en de ON LEVEL GOSUB die pijlen/poorten animeert worden een for-loop en een indexed call in ENEMY.
1988 · GW-BASIC
1000 FOR I=BTEL TO ETEL STEP -2: POKE T(I)+1,15: POKE T(I-1)+1,7: NEXT
1010 ON LEVEL GOSUB 1170,1170,1170,1170,2130,1830,1970,2130,1170,1170,1170,1170,2130,1830,1970,2130,1170,1170,1170,1170,2130,1830,1970,2130,1170,1170,1170,1170,2130,1830,1970,2130
2026 · JavaScript
for(let I = BTEL; I >= ETEL; I -= 2){ // 1000: shimmer
poke(T[I]+1,15); poke(T[I-1]+1,7);
}
ENEMY[(LEVEL-1) % 16](); // 1010
11

Dood: GOTO/RETURN 510 wordt throw

BASIC sterft met GOTO 510 of RETURN 510 vanuit een GOSUB. JavaScript gebruikt throw DEATH, gevangen rond de loop: dezelfde sprong naar buiten, maar met exceptions.
1988 · GW-BASIC
510 FOR I=1 TO 3
520 SOUND 2000,3: SOUND 3000,3: SOUND 4000,3: SOUND 3000,3
530 NEXT
540 IF PLAY(0)<>0 THEN GOTO 540
550 WHILE+ ETEL<=BTEL
560 Z=TIMER
570 Z1=TIMER-Z: IF Z1<0.075 THEN GOTO 570
580 POKE T(ETEL),32: POKE T(ETEL)+1,7: SOUND 1500,0.1
590 ETEL=ETEL+1: OP=BMIN*-1: GOSUB 1190
600 WEND
610 LIVE=LIVE-1: HART=0: KLAVER=0
620 IF LIVE=0 THEN LEVEL=32 ELSE LEVEL=LEVEL-1
630 GOTO 1080
2026 · JavaScript
async function deathSeq(){
for(let I = 1; I <= 3; I++){ // 510-530
sound(2000,3); sound(3000,3); sound(4000,3); sound(3000,3);
}
await playDrained(); // 540
while(ETEL <= BTEL){ // 550-600
await sleep(75); // 560-570 (0.075s)
poke(T[ETEL],32); poke(T[ETEL]+1,7); sound(1500,0.1);
ETEL++; score(-BMIN);
}
LIVE--; HART = 0; KLAVER = 0; // 610
if(LIVE === 0) LEVEL = 32; else LEVEL--; // 620
}
} catch(sig){
if(sig !== DEATH) throw sig;
await deathSeq();
died = true;
break;
12

Twee dispatch-tabellen (ON LEVEL GOSUB)

ON LEVEL GOSUB is een jumptable. De port maakt dat letterlijk: CFG[] bevat levelrecepten en ENEMY[] bevat de animatie per tick.
1988 · GW-BASIC
310 ON LEVEL GOSUB 2320,2330,2340,2350,2360,2370,2380,2390,2410,2420,2430,2440,2450,2460,2470,2480,2320,2330,2340,2350,2360,2370,2380,2390,2410,2420,2430,2440,2450,2460,2470,2480
2320 Z=999: AANTAL= 75: BMIN=10: RETURN
2330 Z=999: AANTAL= 75: BMIN=10: GOSUB 1230: RETURN
2340 Z=999: AANTAL= 75: BMIN=10: GOSUB 1500: RETURN
2350 Z=999: AANTAL= 50: BMIN=10: GOSUB 1400: RETURN
2360 Z=999: AANTAL= 50: BMIN=10: GOSUB 1670: RETURN
2370 Z=999: AANTAL= 50: BMIN=10: GOSUB 1810: RETURN
2380 Z=999: AANTAL= 50: BMIN=10: GOSUB 1920: RETURN
2390 Z=999: AANTAL= 50: BMIN=10: GOSUB 1750: RETURN
2400 '
2410 Z= 0.4: AANTAL=125: BMIN= 5: RETURN
2420 Z= 0.6: AANTAL=125: BMIN= 5: GOSUB 1230: RETURN
2430 Z= 0.6: AANTAL=125: BMIN= 5: GOSUB 1500: RETURN
2440 Z= 0.9: AANTAL=100: BMIN= 5: GOSUB 1400: RETURN
2450 Z= 0.9: AANTAL=100: BMIN= 5: GOSUB 1670: RETURN
2460 Z=1 : AANTAL=100: BMIN= 5: GOSUB 1810: RETURN
2470 Z=1 : AANTAL=100: BMIN= 5: GOSUB 1920: RETURN
2480 Z=1.2: AANTAL=100: BMIN= 5: GOSUB 1750: RETURN
2026 · JavaScript
const noEnemy = () => {}; // 1170
/* 310 + 1010: the two ON LEVEL GOSUB tables (16 entries, used twice) */
const CFG = [
() => { Z = 999; AANTAL = 75; BMIN = 10; }, // 2320
() => { Z = 999; AANTAL = 75; BMIN = 10; lay1230(); }, // 2330
() => { Z = 999; AANTAL = 75; BMIN = 10; lay1500(); }, // 2340
() => { Z = 999; AANTAL = 50; BMIN = 10; lay1400(); }, // 2350
() => { Z = 999; AANTAL = 50; BMIN = 10; lay1670(); }, // 2360
() => { Z = 999; AANTAL = 50; BMIN = 10; lay1810(); }, // 2370
() => { Z = 999; AANTAL = 50; BMIN = 10; lay1920(); }, // 2380
() => { Z = 999; AANTAL = 50; BMIN = 10; lay1750(); }, // 2390
() => { Z = 0.4; AANTAL = 125; BMIN = 5; }, // 2410
() => { Z = 0.6; AANTAL = 125; BMIN = 5; lay1230(); }, // 2420
() => { Z = 0.6; AANTAL = 125; BMIN = 5; lay1500(); }, // 2430
() => { Z = 0.9; AANTAL = 100; BMIN = 5; lay1400(); }, // 2440
() => { Z = 0.9; AANTAL = 100; BMIN = 5; lay1670(); }, // 2450
() => { Z = 1.0; AANTAL = 100; BMIN = 5; lay1810(); }, // 2460
() => { Z = 1.0; AANTAL = 100; BMIN = 5; lay1920(); }, // 2470
() => { Z = 1.2; AANTAL = 100; BMIN = 5; lay1750(); }, // 2480
];
const ENEMY = [noEnemy, noEnemy, noEnemy, noEnemy, sub2130, sub1830, sub1970, sub2130,
noEnemy, noEnemy, noEnemy, noEnemy, sub2130, sub1830, sub1970, sub2130];
13

Een item willekeurig neerzetten

RND wordt Math.random(). De even-offsettruc en de "is het leeg?"-test blijven hetzelfde.
1988 · GW-BASIC
1150 K1=0: K=INT(RND*2720+480): IF K MOD 2=1 THEN K=K+1
1160 IF PEEK(K)=32 THEN POKE K,L: K1=1
1170 RETURN
2026 · JavaScript
/* 1150: drop item L on a random empty cell (rows 4-20) */
function place(L){
K1 = 0;
let K = Math.trunc(rnd()*2720 + 480);
if(K % 2 === 1) K++;
if(peek(K) === 32){ poke(K, L); K1 = 1; }
}
14

Scoren

Tel OP bij de score op en werk de highscore bij. PRINT USING "######" wordt de helper pu(6, …).
1988 · GW-BASIC
1190 ZCORE=ZCORE+OP: LOCATE 22,73: PRINT USING "######";ZCORE
1200 IF ZCORE>ZORE THEN ZORE=ZCORE: LOCATE 22,46: PRINT USING "######";ZORE
1210 RETURN
2026 · JavaScript
/* 1190: score OP points, track highscore */
function score(OP){
ZCORE += OP;
locate(22,73); pu(6, ZCORE);
if(ZCORE > ZORE){
ZORE = ZCORE;
locate(22,46); pu(6, ZORE);
saveHighscore();
}
}
15

Geluid: SOUND f,d wordt Web Audio

SOUND freq, ticks wordt een korte blokgolf op de Web Audio-klok, met dezelfde frequenties en dezelfde 1/18.2 s-ticks.
1988 · GW-BASIC
2260 PLAY "mb": SOUND 2500,0.1: SOUND 3500,0.1: SOUND 5000,0.1: RETURN
2026 · JavaScript
/* ---------- AUDIO: GW-BASIC SOUND f,d (d in 1/18.2s clock ticks) ---------- */
let actx = null, qEnd = 0;
let muted = lsGet('sneekie.muted') === '1';
function ensureAudio(){
if(!actx){
try{ actx = new (window.AudioContext || window.webkitAudioContext)(); }catch(_){ }
}
if(actx && actx.state === 'suspended') actx.resume()?.catch(() => {});
}
function sound(freq, ticks){
if(!actx) return;
const dur = ticks / 18.2;
const t0 = Math.max(actx.currentTime, qEnd);
if(t0 - actx.currentTime > 3) return; // cap the backlog
if(!muted){
const o = actx.createOscillator(), g = actx.createGain();
o.type = 'square'; o.frequency.value = Math.min(freq, 12000);
const v = 0.05;
g.gain.setValueAtTime(v, t0);
g.gain.setValueAtTime(v, Math.max(t0, t0 + dur - 0.004));
g.gain.linearRampToValueAtTime(0.0001, t0 + dur);
o.connect(g); g.connect(actx.destination);
o.start(t0); o.stop(t0 + dur + 0.02);
}
qEnd = t0 + dur;
}
async function playDrained(){ // 540: IF PLAY(0)<>0 GOTO 540
if(!actx) return;
while(qEnd > actx.currentTime) await sleep(25);
}
16

Invoer: INKEY$-scancodes worden keydown

DOS gaf een pijltje uit INKEY$ terug als twee bytes. De browser-handler maakt precies zulke strings ('\0H' = omhoog), zodat de loop hetzelfde ziet als in 1988.
1988 · GW-BASIC
640 IF LEN(A$)=2 THEN E=ASC(MID$(A$,2,1))
2026 · JavaScript
addEventListener('keydown', e => {
if(bootActive){
ensureAudio();
if(bootWaiting) startBootFromGesture();
else if(bootStarted) bootSkip = true;
if(e.key.startsWith('Arrow') || e.key === 'F9' || e.key === 'F10' || e.key === ' ' || e.key === 'Escape' || e.key === 'Enter') e.preventDefault();
return;
}
ensureAudio();
let s = null;
switch(e.key){
case 'ArrowUp': s = '\0H'; break; // scan 72
case 'ArrowDown': s = '\0P'; break; // scan 80
case 'ArrowLeft': s = '\0K'; break; // scan 75
case 'ArrowRight': s = '\0M'; break; // scan 77
case 'F9': s = '\0C'; break; // scan 67 (extra life — shh!)
case 'F10': s = '\0D'; break; // scan 68 (skip level — shh!)
case 'Escape': s = '\x1b'; break;
case 'Enter': s = '\r'; break;
default: if(e.key.length === 1) s = e.key;
}
if(s !== null){
if(e.key.startsWith('Arrow') || e.key === 'F9' || e.key === 'F10' || e.key === ' ' || e.key === 'Escape' || e.key === 'Enter') e.preventDefault();
pushKey(s);
}
});
Een blokkerige Sneekie-slang springt door een pixelpoort van een oude CRT naar een moderne browser.