Игры наших бабушек
Дейкстра как-то сказал, что студентов, ранее изучавших Бейсик, практически невозможно обучить хорошему программированию. Как потенциальные программисты они умственно изувечены без надежды на восстановление. После такой рекламы нельзя не захотеть поиграть в игры на Бейсике, созданные в 70х годах ещё для больших машин. А запускать мы их будем на Ардуино с подключенным к нему телетайпом, чтобы получилось более-менее аутентично.
Язык Бейсик бып придуман в 1964 и предназначался для неподготовленных программистов, например студентов. И внезапно оказалось, что все хотят писать игры.
К примеру, в 1971 году школьные учителя придумали игру «Орегонская тропа». В этой игре переселенцы пытаются попасть на Запад, постоянно сталкиваясь с нехваткой ресурсов, дикими животными, бандитами, индейцами. Сначала это должна была быть настольная игра, но и в итоге получилась программа на Бейсике, которая стала одной из самых долгоживующих компьютерных игр. Позднее, в 80х, она была портирована уже на микрокомпьютеры с графическими дисплеями, а в 2000х и под мобильные телефоны.
Орегонская тропа в 1971Орегонская тропа в 1985Орегонская тропа в 2022Можно и сейчас поиграть в оригинальную версию в режиме «удалённого доступа к мейнфрейму»
Подключиться через telnet к mickey.publicvm.com
Поочерёдно нажимать Ctrl-J и Ctrl-M, пока не появится сообщение «PLEASE LOG IN»
Ввести «HEL-T001, HP2000,1» без кавычек
Ввести «EXE-OREGON» для запуска версии 1975 года и «EXE-ORE2» для запуска версии 1978 года
101 игра на бейсике
В 1973 году Дэвид Эл (David Ahl), этот Гамовер и Бонус в одном лице, составил сборник »101 компьютерная игра на языке Бейсик». В книге были листинги программ, которые можно ввести самостоятельно и поиграть. Чтобы оценить потенциал игры заранее, не набирая её код, для каждой приводились скриншоты логи с терминала.
Игра «Бомбардировщик»
Так как книга — это сборник программ от разных авторов, на последней странице есть предложение предлагать свои программы для будущих изданий. И судя по тому, что их просили присылать на чистой белой бумаге, эти программы наверняка не набирали, а просто фотокопировали.
Все игры Дэвид разделил на несколько классов. Самая популярная группа — это логические игры. Ещё были всякие простые типа «угадай число» и даже спортивные симуляции: баскетбол, боулинг, гольф, хоккей…
Конечно текстовый терминал, который только выплёвывает последовательность символов — это серьёзное ограничение. Несмотря на то, что буквы разбиты на строки, это не то же самое, что двумерный экран. В арканоид на таком терминале играть будет очень скучно. Хотя та же «Орегонская тропа» заставляла игрока как можно быстрее ввести «BANG», чтобы охота на диких зверей была успешной. Чем не аркадный элемент?
В конце 70х эта книга была адаптирована под популярные микро-ЭВМ того времени (для меня знакомыми оказались названия Radio Shack, Apple II, Commodore PET, а в книге их прям много). Бейсик был действительно «языком по умолчанию». Но мы сегодня не будем смотреть в сторону микрокомпьютеров, а позапускаем версии для больших машин с телетайпом.
Телетайп и бейсик
Как вводить и выводить данные через телетайп я уже рассказывал. После этого я даже попробовал использовать его как терминал для входа в Linux:
Правда пришлось увеличить размер буфера для последовательного порта в Arduino, очень уж эта машина медленно печатает.
Но держать два компьютера (Ардуино и линукс-машину), чтобы играть в простенькие игры — это перебор. Так что представим, что Ардуино — это мейнфрейм 70х, и будем запускать игры там.
Как запустить игру? Можно переписать её на Си (или на ассемблере, если угодно), но это будет не так аутентично. Поэтому будем исполнять программы на бейсике. С помощью интерпретатора, а не компилятора. У нашего мейнфрейма же огромная вычислительная мощность.
Интерпретатор бейсика
Доступных интерпретаторов не на всяких питонах, js и php не так уж и много. Я выбрал вот этот проект. Запустить его удалось почти сразу, но в процессе тестирования игр из книги пришлось исправлять в интерпретаторе баги и бороться с отличиями разных диалектов бейсика. Для некоторых особенностей я исправлял исходную программу, чтобы немного упростить себе жизнь, и не чинить интерпретатор. Вот неполный список того, что пришлось обходить или чинить:
Оператор NEXT, завершающий итерацию цикла, может иметь несколько параметров для работы во вложенных циклах
Одновременное присваивание нескольким переменным (A=B=C=0)
Оператор «switch» может иметь форму GOTO X OF 10,20,30 или ON X GOTO 10,20,30
В «Орегонской тропе» встречается оператор измерения времени (в разных версиях разный)
Из цикла FOR можно уйти с помощью GOTO, а потом снова начать такой же цикл
При возврате из подпрограмм циклы FOR, которые в них начались, надо завершить
В интерпретаторе не было правильной поддержки логических AND/OR в выражениях (и починил я это не до конца)
Оператор возведения в степень (его поддержки не было совсем)
Оператор INPUT с несколькими переменными
Не работало считывание отрицательных чисел, записанных под директивой DATA
Конечно же, не каждую игру имело смысл загружать в Ардуино. Во-первых, телетайп очень медленно печатает. Вывод инструкции по игре «Орегонская тропа» занял бы минут 10. Во-вторых, бейсболы и хоккеи не очень увлекательны, как мне кажется. Так что я отобрал всего 11 программ.
Запихиваем всё в Arduino
Интерпретаторы бейсика в микрокомпьютерах чаще всего находились в ПЗУ. А программы, которые они выполняют, вводились пользователем вручную или загружались с магнитных носителей. Поэтому программы всегда хранились в ОЗУ. Вот и тот интерпретатор, который я взял, рассчитан на изменяемость кода программы: можно добавлять и удалять строки прямо из терминала.
Редактировать программу, используя телетайп, это отдельный вид мазохизма, поэтому на это я даже не рассчитывал. Такие функции интерпретатора я выкинул с чистой совестью. Но даже потом он занимал около 24Кб ПЗУ. И как наши предки влезали в 8Кб?
Раз программы у нас будут неизменные, почему бы их не хранить в ПЗУ? Только вот AVR имеет гарвардскую архитектуру, поэтому с ПЗУ работать несколько сложнее, чем с обычными переменными. По крайней мере, в языке Си, который на такое не очень-то рассчитан.
На грабли с массивами в кодовой памяти я уже понаступал раньше, и рассчитывал, что нужно только аккуратно использовать расширения gcc-avr и его библиотечные функции, предназначенные для работы с ПЗУ. Но оказалось, что авторы стандартной библиотеки Arduino тоже используют такие функции, но делают это недостаточно хорошо — при увеличении объёма прошивки сверх 64 кб перестаёт работать функция digitalRead (которая используется наверное во всех примерах скетчей для этой среды). В итоге я создал pull request для исправления бага.
Отладка
Очень удобно, что телетайпом у меня управляет именно Ардуино, а не какой-нибудь PIC18. Потому что в эмуляторе QEMU есть поддержка платформы AVR (и в частности микроконтроллера Mega2560). Конечно, работу с телетайпом QEMU «из коробки» не сможет эмулировать, но зато там есть поддержка последовательного порта.
Как запустить программу для Arduino в QEMU
Компилируем проект в Arduino IDE, находим получившийся ELF-файл, а потом запускаем QEMU следующей командой:
qemu-system-avr -M mega2560 -serial stdio -bios basic.ino.elf
Для отладки интерпретатора бейсика на AVR, я переделал ввод-вывод на работу с последовательным портом. Так получилось быстро устранить основные баги, возникшие из-за переноса интерпретатора с x86 на AVR.
Наиболее неожиданным для меня оказалось отсутствие поддержки спецификатора %f для printf и scanf по умолчанию. В бейсике же нет типизации для переменных-хранящих-числа, поэтому там все они вещественные. Пришлось вместо printf использовать atof (она почему-то была), а вместо scanf писать свою функцию.
Конечно сначала я запускал эмулятор с прошивкой, где была только одна игра. Потом ошибки исправил, все игры подключил, внедрил меню, скомпилировал. Включаешь — не работает. Эмулятор завершался с ошибкой.
По логам QEMU удалось установить, что для выполнения табличного перехода читается адрес из ПЗУ, а потом выполнение улетает в какую-то мусорную область памяти.
IN:
0x000180ac: ELPMX r0, Z+
PC: 0180ac
SP: 21ff
rampD: 00
rampX: 00
rampY: 00
rampZ: 01
EIND: 00
X: 0200
Y: 21ff
Z: de00
SREG: [ - - H S - - - I ]
SKIP: 00
R[00]: 00 R[01]: 00 R[02]: 00 R[03]: 00 R[04]: 00 R[05]: 00 R[06]: 00 R[07]: 00
R[08]: 00 R[09]: 00 R[10]: 00 R[11]: 00 R[12]: 00 R[13]: 00 R[14]: 00 R[15]: 00
R[16]: 01 R[17]: 06 R[18]: 00 R[19]: 00 R[20]: 00 R[21]: 00 R[22]: 00 R[23]: 00
R[24]: 00 R[25]: 00 R[26]: 00 R[27]: 02 R[28]: ff R[29]: 21 R[30]: 00 R[31]: de
Инструкция ELPMX, читающая байт из ПЗУ, формирует адрес из трёх регистров: RAMPZ: R31: R30. Значения в них вроде правильные, а ничего не выходит. Для эмуляции инструкций QEMU транслирует их сначала в псевдокод, а потом в машинный код хоста. И вот псевдокод для эмуляции инструкции ELPMX строил полный адрес для чтения данных как-то не так. Оказалось, там произошла небольшая путаница с регистрами и их смещениями, поэтому всё работало только при RAMPZ=0.
Исправить это было не очень сложно. Но полная прошивка всё равно не заработала в эмуляторе. На экран не выводилось даже приветствие в начале работы программы. А функция strlen, вызов которой есть в выводе на консоль, делала всего одну итерацию, то есть из памяти сразу читался 0.
.text:0000C3C4 movw r30, r28
.text:0000C3C5 ld r0, Z+
.text:0000C3C6 tst r0
.text:0000C3C7 brne loc_3C5
И тут мы видим использование того же самого регистра Z. Который в «маленьких» AVR состоит из R31: R30, а в общем случае ещё включает RAMPZ. Этот регистр устанавливается в функциях чтения ПЗУ, но не сбрасывается потом. И в коде strlen он тоже не сбрасывается. Так что стандартная библиотека avr-libc тоже нормально работает в эмуляторе только при размере прошивки до 64 кб. А всё потому, что эмулятор поддерживает большое ОЗУ для контроллеров типа xmega.
К счастью, в «железном» AVR ОЗУ всего 8 кб, поддержка внешнего ОЗУ ограничена 64 кб, поэтому там RAMPZ при обращении к ОЗУ не используется, и всё запускается нормально.
Наконец-то можно поиграть
Я загрузил в свою «игровую консоль» следующие программы. Вот их краткие описания из оригинальной книги:
23MTCH — надо не взять последнюю спичку из 23, поочерёдно забирая по 1, 2 или 3
ANIMAL — компьютер угадывает животное и узнаёт новые варианты от вас
BAGELS — отгадываение трёхзначного числа с помощью логики
BUZZWD — составляет речь из модных слов
GUESS — угадывание числа по подсказкам от компьютера
HMRABI — управление древним шумерским городом
HANG — угадывайте слово в игре «виселица»
HELLO — компьютер становится дружелюбным психианалитиком
NICOMA — компьютер угадывает задуманное число
RUSROU — русская рулетка
TICTAC — крестики-нолики
Видео ниже показывает игру в »23 спички». В ней компьютер использует выигрышную стратегию, если ему выпадает ходить первым.
Угадываем трёхзначное число в игре BAGELS, типа как «быки и коровы».Пытаемся привести город к процеветанию или, что более вероятно, к смерти в игре HAMURABI.Можно получить немного издевательских комментариев от программы-«психоаналитика» HELLO.
Ещё одно видео ниже показывает партию в крестики-нолики. Правда программа недостаточно умна, чтобы ставить «вилку», играя за «крестиков».
Итоги
Итак, чтобы поиграть в старинные бейсиковские игры на телетайпе, нужно всего лишь:
Немного поправить игры
Доработать интерпретатор Бейсика
Пофиксить баг в стандартной библиотеке Arduino
Исправить трансляцию одной из инструкций AVR в эмуляторе QEMU
Добавить в QEMU проверку, что оперативной памяти не очень много
Получать удовольствие от медленного вывода текста на телетайпе
Не пытайтесь повторить это дома.