Від GW-BASIC до JavaScript, поруч
Вебверсія не є переписуванням — це вірний порт. Ігрова логіка перекладена з SNEEKIE.BAS оператор за оператором, з оригінальними назвами змінних і навіть номерами рядків BASIC у коментарях. Насправді змінився тонкий шар під грою: те, що GW-BASIC давав безкоштовно — памʼять екрана, клавіатуру, звук, GOTO — довелося відбудувати для браузера. Ця сторінка ставить поруч показові фрагменти і пояснює цей шар; для кожного рядка BASIC є сторінка Пояснення.
Міст: екран і є структура даних
GW-BASIC записував символи прямо у текстову памʼять екрана PC 80×25 через POKE і читав їх назад через PEEK, щоб дізнатися, що де стоїть. Окремого обʼєкта "змія" чи "стіна" не було — зображення було станом. Порт зберігає цю модель точно: 4000-байтовий Uint8Array (vram), два байти на клітинку, символ і атрибут, адресуються так:
offset = (рядок − 1) × 160 + (стовпець − 1) × 2
Оскільки модель однакова, ігровий код майже не змінюється — слова POKE і PEEK лише стають функціями poke() і peek(). Усе інше на цій сторінці випливає з відбудови решти середовища виконання GW-BASIC навколо одного спільного масиву.
Що довелося відбудувати
| Механізм 1988 року | Заміна 2026 року |
|---|---|
POKE / PEEK у відеопамʼять на &HB000/&HB800 |
poke() / peek() у 4000-байтовому Uint8Array |
LOCATE r,c : PRINT CHR$(n) |
locate(r,c); pc(n) — той самий курсор, ті самі коди символів |
| екран оновлює себе сам (це і є відеопамʼять) | набір брудних клітинок + requestAnimationFrame, гліфи з вбудованого шрифту IBM CP437 |
INKEY$ / INPUT$ блокують до клавіші |
async + Promise-буфер клавіатури (keyOrTimeout/waitKey) |
SOUND freq, ticks |
Web Audio генератор квадратної хвилі на тому самому годиннику 1/18.2 с |
ON LEVEL GOSUB … |
диспетчерські масиви CFG[] і ENEMY[] |
GOTO 510 / RETURN 510 (смерть) |
throw DEATH, перехоплений навколо циклу руху |
змінні (DEFINT A-Y, нідерландські назви) |
ті самі назви збережені буквально (T, BTEL, HART, KLAVER…) |
Одна глибока зміна: блокування стає async
GW-BASIC ішов згори вниз і просто зупинявся на INKEY$/INPUT$, доки ви не натиснете клавішу. Вкладка браузера не може так блокуватися — тому точки очікування стали await, а playLevels() і program() стали async. Малий Promise-буфер клавіатури замінює клавіатурний буфер BIOS, а оскільки GOTO у JavaScript немає, рядки "вистрибнути і померти" стають кинутим сигналом DEATH, перехопленим навколо циклу. Ці два прийоми — await для вводу, throw для смерті — дозволяють решті коду лишатися буквальним перекладом.
Екран — це модель даних
PEEK/POKE у відеопамʼять і навіть вибирає монохромний (&HB000) або кольоровий (&HB800) екран. Порту потрібна лише одна сітка в памʼяті: vram — це 4000-байтовий масив, а poke()/peek() — маленькі допоміжні функції.Малювання сталого кадру
LOCATE r,c : PRINT CHR$(n) стає locate(r,c); pc(n); STRING$(78,…) стає pcn(196,78); SPC(78) стає sp(78). Текст перекладено, але всі коди символів рамки лишилися тими самими.Цикл рівнів і стартова змія
FOR LEVEL=1 TO 32 стає циклом for; змія стартує з тих самих зміщень (2000 хвіст, 1840 голова); ON LEVEL GOSUB стає індексованим викликом у таблиці CFG.Читання клавіші: блокуючий INKEY$ стає асинхронним
INKEY$ не може блокувати браузер, тому стає Promise: keyOrTimeout(ms) завершується при клавіші або таймауті. Ігровий цикл робить await.Сортування клавіші та ESC
LEN(A$) відрізняє двосимвольну стрілку від одиночної клавіші; JavaScript перевіряє A$.length. ESC стає кинутим сигналом смерті замість GOTO 510.Перетворення голови на крок екрана
A — цільова клітинка; peek(A) каже, що там стоїть.Що в наступній клітинці?
IF D<>n THEN GOTO стає ланцюжком if / else if на тих самих кодах символів: порожньо, трефа, серце, камінь, смайлик, стріла.Заблоковано стіною або хвостом
blocked = true. Обидва зберігають старий напрям, знімають бонус і пропускають хід.
Малювання вигину тіла
E і старого F, а потім додають нову голову █.Блиск і небезпека рівня
ON LEVEL GOSUB, що анімує стріли/брами, стають циклом for та індексованим викликом у ENEMY.Смерть: GOTO/RETURN 510 стає кинутим сигналом
GOTO 510 або RETURN 510 з GOSUB. JavaScript використовує throw DEATH, перехоплений навколо циклу: той самий стрибок назовні, але через винятки.Дві диспетчерські таблиці (ON LEVEL GOSUB)
ON LEVEL GOSUB — це таблиця переходів. Порт робить це буквально: CFG[] містить рецепти рівнів, а ENEMY[] містить анімацію кожного такту.Випадково поставити предмет
RND стає Math.random(). Трюк із парним зміщенням і перевірка "чи порожньо?" лишаються незмінними.Рахунок
OP до рахунку і оновити рекорд. PRINT USING "######" стає допоміжною функцією pu(6, …).Звук: SOUND f,d стає Web Audio
SOUND freq, ticks стає короткою нотою квадратної хвилі на годиннику Web Audio, з тими самими частотами і тими самими тактами 1/18.2 с.Ввід: сканкод INKEY$ стає подією клавіатури
INKEY$ як два байти. Обробник браузера створює точно такі рядки ('\0H' = вгору), тому цикл бачить те саме, що у 1988 році.