Для демонстрации используется модификация программы
Пещера сокровищ
, из журнала
Техника Молодёжи №7 за 1987.
Для тех, кто не желает идти по ссылке, или высматривать шрифт скана,
опишем игру. Игра представляет собой трёхмерный лабиринт в 3 этажа размером 7 на 4,
где каждая ячейка на этаже или свободна для прохода, или
занята – стена
. Цель игры – пройти лабиринт, и собрать как можно больше
сокровищ. Начальная точка входа задаётся игроком, а выход
в левой нижней точке на третьем этаже влево.
Для понимания адресации покажем план 1-го этажа из публикации
в журнале:
1 | 2 | 3 | 4 | 5 | 6 | 7 | |
---|---|---|---|---|---|---|---|
1 | |||||||
2 | |||||||
4 | |||||||
8 | X |
Сверху показана нумерация колонок, слева – строк с учётом битовой арифметики. Стенки отображены ячейками с тёмным цветом. Крестиком, как пример, отображено положение игрока в точке 1.0000008. Координаты расшифровываются так:
С учётом этого выход из лабиринта будет влево от точки 3.8.
У игрока в наличии ограниченные ресурсы:
Для пополнения ресурса выполняется раскопки или поиск. Учтите, что поиск может быть безуспешным. Далее такие обнаруженные ресурсы будем называть кладами. При этом результат будет зависеть от этажа. На 1-м этаже будет еда. За раз +9 единиц, в оригинале +10. На 2-м – динамит, за раз +4. И только на 3-м только за раз будет одно сокровище. Особенность поиска – как только нашли клад, то на всех этажах в той же ячейке клад пропадает.
Если нашли клад, то появляется разбойник, с которым можно подраться. Ставка – новое общее значение ресурса, в зависимости от этажа. В результате сумма может как увеличиться, так и уменьшиться. Если не хотите рисковать, то от драки можно просто отказаться.
Управление игрока – задание команды на движение, или поиск кладов, или подрыв стены в последней ячейке, где не удалось пройти. Каждая команда расходует еду. Подрыв, понятно, ещё и динамит. Код направления движения определяется положением цифр на клавиатуре относительно центра: 2, 4, 6, 8, ±5 – вниз, влево, вправо, вверх и выше, ниже этажом. Подрыв стены нулём. В оригинале число Fπ, но обычно при неудаче прохода выдаёт 0, так что сразу удобнее его использовать для команды подрыва. Поиск – число 10. В оригинале ноль, но 1/0 как бы намекает на найдём или нет.
Если движение прошло успешно, то по окончании отображается новая координата игрока, в кодировке, как описано выше. Если нет – ноль. Выход из лабиринта определяется числом 11, в оригинале трамвай 11-го маршрута.
При поиске клада: если не найдено – ноль. Если найдено, то показывает новое количество ресурса с учётом найденного, а в регистре Y букву типа клада и позицию, где нашли, например: E.00002 . При этом, как в оригинале, с разбойником, который защищал клад, можно бороться, нажав В↑, или пропустить, нажав 0. Затем С/П. Если боремся, то итог может как увеличиться, так и уменьшиться.
При успешном подрыве – новое положение игрока, т. е. подрыв подразумевает сразу ход в новую ячейку. В оригинале для этого приходилось делать отдельный ход. При неудаче, когда была попытка подрыва границ лабиринта, или последний ход это не движение – ноль.
Несколько отличий демонстрационной программы от оригинала уже было сказано, но приведём весь список:
щедротразбойника у вас могло остаться нечётное число гранат. Отсутствие ЕГГ0Г ясно показывает, что игру можно продолжить.
xor. Хотя можно сделать в коде и так, поменяв К∨ на К⊕, но ставить новые стенки для затруднения прохода, на мой взгляд, нецелесообразно.
телепортиз N.0000008 в N.0000001 при движении вправо, возникающий в силу особенности округления. В оригинале автор в примере в нескольких местах поставил стенки, чтобы это не проявлялось.
капитальныхстен. Это которые по краям лабиринта: слева, справа, сверху, снизу, в том числе подвал и крыша. В оригинале это можно делать, что приводило к непредсказуемым результатам.
началосделано по-другому.
не в стенке. В оригинале нужно было самому разработать, закодировать и вбить лабиринт при каждой новой игре. Получается, что в отличии от оригинала у игрока нет плана лабиринта и кладов. С учётом этого начальный объем еды задаётся вручную, т. к. план неизвестен и придётся многократно упираться в стенки при изучении. Также подрыв стены совмещен с ходом на новое место.
Полный код программы приведём в конце, а по ходу изложения будем приводить фрагменты, изучая трюки и использование недокументированных особенностей ПМК, без которых не удалось обойтись, с учётом всех улучшений.
Сначала общий алгоритм программы. Как в оригинале
сказано, это демонстрация возможностей ПМК работать с отдельными битами,
используя шестнадцатеричные числа. Планы этажей и кладов представляются
битовыми масками.
Хватает одного числа на весь этаж. А местоположение
игрока фактически определяет один бит. Именно он двигается
,
накладываясь на битовые маски, определяя возможность пройти.
При взятии клада биты в масках удаляются, а при удалении стенок
устанавливаются.
Большая часть кода программы – это преобразование хода игрока в
правильные битовые операции, проверка границ, ресурсов и т. п.
Распределение регистров частично совпадает с оригиналом.
не ходахтам может быть совсем другое значение, но в любом случае подрыв стен контролирует и это.
Начнём с основной подпрограммы, которая делает битовую проверку – конъюнкцию. Сначала приведём формализацию её действия. На входе в регистре X план этажа для проверки в виде шестнадцатеричной битовой маски. В регистре Y тоже шестнадцатеричное число – новое положение игрока. После исполнения значение регистра Y (новое положение игрока) безусловно запишется в регистр Rb, чтобы запомнить предполагаемое место, даже если нет прохода. При успешной битовой проверке, т. е. при совпадении битов, проход возможен, и значение из регистра Y (новое положение игрока) будет скопировано и в регистры X и Ra, а в регистре Y останется только дробная часть, лучше отображающая положение на этаже, потому что порядок числа и будет равен номеру колонки. Если битовая проверка не успешна, то вернет ноль в регистрах X и Y. Посмотрим, как это делается.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
40 | | К∧ | К{x} | ←→ | x→Пb | Кmax | Кx≠07 | x→Пa |
Если с битовым умножением и копированием Y в Rb всё ясно, то вот операция
Кmax явно использован
нестандартно
. При неудаче ноль из регистра Y будет
недокументированно
скопирован как самое большое число
в X.
Т. е. при неудаче будет ноль, что от подпрограммы и требуется.
А если битовая операция успешная, т. е. есть дробная часть, то ничего
не произойдёт: исходный Y останется в X, а дробная часть,
которая конечно меньше всего числа, в Y.
И результат будет скопирован в Ra.
Напомню, что в регистре R7 у нас 0.5, а косвенный переход по нему при неудаче аналогичен переходу на нулевой адрес, где собственно и располагается начало основного цикла программы.
На самом деле эта подпрограмма полезна ещё в другом случае. Если на входе в одном из регистров X или Y число состоит из одной цифры, то эта процедура безусловно из-за логического умножения даст на выходе ноль. Эта возможность тоже используется в программе, поэтому назовём её также процедурой очистки.
Но внимательный читатель спросит, а где же В/О? А он… не нужен! Так вот, тут используется побочная ветвь адресного пространства, но только не после 105 адреса, а та, которая начинается с адреса B2, и заканчивается на адресе F9. А так как адрес F9 совпадает с адресом 47, то следующим для выполнения будет адрес 00. И сама подпрограмма начинается не с адреса 41, а с аналогичного адреса F3.
Здесь уже можно раскрыть, что константа в регистре R9, который
используется для перехода на эту подпрограмму, заканчивается на F3, хотя
можно чтобы оканчивалась и на ED. Точнее в R9 хранится
4. 3 -08. Зачем вначале идёт 4 и такой порядок будет
пояснено позднее. Хотя, если читали весь документ, то об этом уже
упоминалось. Главное, что такой довесок
не меняет косвенной
адресации. Упомяну только, что после использования
в
косвенной адресации значение в R9 станет ненормализованным
0.00004 3-03,
зато сразу покажет адрес перехода.
Давайте, наконец, рассмотрим основной цикл программы, который начинается с адреса 00.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
00 | | С/П | П→xe | 1 | − | Fx<0 | B9 | В/О | x→Пe | <-> |
Как и ожидалось, цикл начинается с остановки, чтобы игрок мог ввести свой ход. Затем идёт уменьшение ресурса еды и… необычный переход.
Поясню зачем это. Выше уже упоминалось, что основная процедура выполняется
по адресам второй побочной ветки, чтобы по окончании автоматически
завернуть
на начало. В этой проверке и делается переход на
побочную ветвь через адрес B9, который аналогичен адресу 07.
В этом случае уменьшенное значение еды запоминается, а в регистр X,
через ←→,
возвращается ход игрока. Обращаю внимание, что если
исключить наш случай, когда регистров просто не хватает, тут всё равно
делается экономия на команде. Всё-таки
x→ПR
П→xR – это
две команды, а ←→ – только одна.
Но это конечно при условии, что запомненное значение не понадобится ещё раз.
А в случае, если еда кончилась, то идёт переход на команду В/О, которая, с адреса 01, снова сделает ту же проверку с тем же результатом – вот и зациклилась программа при нехватке еды.
Продолжим. Сначала содержимое Ra (текущее положение игрока) заталкивается заранее в регистр Y. Зачем это нужно, поясню позднее.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
00 | | П→xa | |||||||||
10 | | <-> |
Для того чтобы ход игрока 2, 4, 6, 8 преобразовать в номер регистра R4, R5, R6, R7, которые содержат коэффициенты для умножения, нужно провести несложную операцию: X / 2 + 3. Но для начала неплохо бы проверить на ноль, что является ходом для взлома стены.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
10 | | 2 | ÷ | Fx≠0 | 63 |
Вот и проверка команды на взлом стены на ноль. Но позвольте, зачем сначала
деление, а только потом проверка на ноль?
спросите вы. Ответ в том,
что нам не хватает регистров. Кстати, обратите внимание, что рабочий
регистр Rb пока не задействован, и взлом будет на его основе. Поэтому ход игрока,
точнее уже половину 😊 хода, мы запоминаем в регистре… X2. Да, да,
вспомните,
ведь проверка условий при успехе запоминает X в X2. А дальше
посмотрим, как мы это используем:
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
10 | | Fπ | + | x→Пb | ВП | КЗН |
Ага, вот и выбор пользователя преобразован в номер регистра для коэффициента умножения. Для прояснения, приведу таблицу с возможным выбором игрока, при условии, что ноль уже исключили, и то что попало в регистр Rb:
Ход игрока | После ÷ 2 | После + 3 |
---|---|---|
2 | 1 | 4 |
4 | 2 | 5 |
±5 | ±2.5 | Что-то больше нуля |
6 | 3 | 6 |
8 | 4 | 7 |
10 | 5 | 8 |
Вариант ±5 не будет использовать регистр Rb, как мы увидим позднее.
Обратите внимание, что мы на самом деле сложили не с тройкой, а с числом π. Почему? Дело в том, что команда Fπ не X2-влияющая, к тому же при косвенной адресации дробная часть значения не имеет. Вспоминаем – всё это из главы по косвенной адресации. Т. е. для нас всё равно, будет в Rb число 7 или 7.1415926, при КП→xb он всё равно извлечёт 0.5, как значение в R7.
А самое главное, это то, что останется после
команды ВП.
Вспомним из
главы про X2, что сочетание этой команды с предшествующей
командой сохранения в регистр съест
первую цифру числа из X2,
и восстановит его в X.
Важно ещё то, что в результате сложения с π значение в X
не отрицательное. А что у нас в X2?
Это значение во второй колонке таблицы выше. Т. е. он для всех
вариантов из таблицы оставит ноль, и
только от ±2.5 оставит ±0.5.
Вот сколько может сделать одна недокументированная команда!
Кстати, в данном случае действие ВП
очень похоже на команду К{x},
которая нам не походит. Потому что, даже если бы мы вместо π
использовали просто тройку,
К{x} выполняет
действия над регистром X, который в случае движения между
этажами равен 0.5 или 5.5. Оба положительных.
А ВП использует X2, который помнит
отрицательное число,
хотя документально
нигде и не хранится.
Завершающий КЗН из ±0.5 делает ±1 – такой вот трюк вместо умножения на два. Ноль при этом остаётся нулём. А далее мы оставляем только варианты движения по этажу, с нулём, уводя ±1 в другое место.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
20 | | Fx=0 | EA |
Напомню, что у нас программа выполняется во второй побочной ветви,
и странный
адрес EA равен 38. Точнее, адресу 38 соответствует
адрес F0, но такое не ввести, поэтому мы заменяем аналогом – трюк.
Ладно, займемся этим адресом, когда туда дойдём, а сейчас продолжим
разбор движения по этажу, но сначала таинственное
пропускание вперёд:
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
20 | | П→x6 |
Так, пришла пора раскрыть, что же находится в регистре R6, которое
почти 0.1. Там хранится число D.|−02:
Г. -02.
Дело в том, что в операции умножения, если D находится слева в регистре X, то
оно ведёт себя как число 10. Кто забыл, просмотрите главу про шестнадцатеричную
арифметику. А для степени −02 получается как 0.1:
10 × 10.|−2 = 10.|−1.
А когда справа в регистре Y, то гораздо хитрее.
Это хитрее
нам понадобится позднее, поэтому пока
загоняем число в стек, чтобы оно было справа
. Я мысленно стек
представляю как X1 – X – Y – Z – T, если кто-то по другому,
то справа
поменяйте на другое.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
20 | | П→xa | К{x} | КП→xb | × | Fx≥0 | 77 |
Ну вот, наконец-то умножение дробной части числа Ra, как положение на этаже, на коэффициент движения. Ещё не забыли, что у нас в Rb? Тут же проводится отсечка варианта поиска клада, когда команда = 10. Дело в том, что в R8, на который указывает Rb в варианте 10 / 2 + 3, специально записано отрицательное число, а значит поиск уходит на адрес 77. Но это не всё. Тут мы опять используем регистр X2, через X2-влияющую операцию сравнения. Мы сохраняем новое положение игрока на этаже в X2, но не потому что нет регистра, а для экономии команд: x→Пb П→xb это две команды, а ., всего одна, а сравнение всё равно нужно делать. Именно поэтому сравнение делается не сразу после КП→xb, а после умножения, чтобы запомнить в X2 итог.
Так, теперь, после получения нового значения на этаже нам нужно проверить, что в результате мы не вышли за границы этажа. Изучим такую проверку подробнее.
Рассмотрим для начала левую границу. Как мы можем за нее попасть? Только
если перед этим было число M.N (M = 1, 2, 3; N = 1, 2, 4, 8), а затем мы N
умножили на 10. Понятно, что потом мы дробную
часть приплюсуем к
этажу M. В данном случае она очень даже не дробная, но в результате
сложения итоговых вариантов не так много, и самое главное, они будут в
диапазоне 2…11 и состоять только из одной цифры, кроме 11. А одна
цифра бинарную конъюнкцию
К∧
не переживёт
в процедуре очистки. Вариант с 11, который влево от 3.8 –
это исключение, поэтому он и выбран как выход из лабиринта.
Причём будет выполнена операция
1
1
К∧
КП→xb,
при условии, что Rb = 11. Это обеспечит гарантированный выход.
Получается даже регистр Rb, а b = 11, кто забыл, выбран неслучайно:
трюк с подтасовкой регистров!
В общем, левая граница защитилась
автоматически.
Правая граница. Там значение дробной части становится порядка 1.|−08 и при сложении с номером этажа просто потеряется, потому что точности не хватит на 9 цифр, опять же оставляя только одну целую часть, которая будет 1…3 и конъюнкцию не переживёт. Исключение составляет 8.|−08, которое округляется при сложении до 1.|−07, но и его мы победим позднее.
Верх и низ. При переходе верхней границы, 1 × 0.5, получается число, оканчивающиеся на 5, а при переходе нижней, 8 × 2, число из двух цифр 16. Вот тут нам и пригодится таинственное D.|−02 из R6. Используя знания шестнадцатеричной арифметики можно узнать, что при умножении, а D.|−02 как раз осталось справа, на число 1, 2, 4 или 8 результат будет содержать только одну цифру, а на 5 и тем более 16 уже больше. Это поясняет последующий код:
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
20 | | × | |||||||||
30 | | К∨ | К{x} | Кx=09 |
Здесь кроме того, что указано, применяется ещё один трюк. После умножения
будет число с одной или более цифрой.
Как же вычислить одна или больше? Приходит на помощь операция
К∨
при том, что в регистре Y остался ноль ещё после
команды ВП по адресу 18. Какой автор
предусмотрительный, даже давний ноль у него при деле 😊.
В результате будет ровно 8. – всё хорошо и границы не нарушены.
Или 8.{с чем-то} – плохо, выход за границы. А после
К{x} либо ноль,
либо 0.{что-то}. Вот это плохое что-то
последняя команда и отправит
на процедуру очистки. Для очистки важно, чтобы в одном из регистров X
или Y было число с одной цифрой – ноль в Y
вполне подходит. Важно что в логических бинарных операциях
второй операнд, ноль в данном случае, не исчезает.
Продолжим далее.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
30 | | . | П→x9 | − |
Первое, что мы делаем, это восстанавливаем X2, содержащий новое положение на этаже
ещё с команды сравнения по адресу 27,
т. к. все команды в промежутке были не X2-влияющие. А потом
корректируем значение на маленькую величину порядка 4.|−08. Это не
изменит итог ни для кого, кроме 8.|−08. В этом случае оно тоже станет
очень маленьким. Вот где заблокирован телепорт
и вот зачем в R9 к
адресу перехода были добавлены четвёрка вначале и порядок −08.
Далее проще:
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
30 | | П→xa | К[x] | + | x→Пb | ||||||
40 | | КП→xb |
Мы к нашему новому значению на этаже прибавляем номер этажа, это целая часть Ra. Заталкиваем новые координаты игрока в Y, а в X извлекаем план соответствующего ходу этажа. Далее следом идёт переход на основную процедуру, которая с адреса 41 = F3, и мы её уже смотрели. Она отбросит все нестандартные этажи, где одна цифра в числе, а также случай, когда битовая карта этажа просто не совпадёт с положением игрока. Обращаю внимание, что косвенной адресацией мы из Rb удалили дробную часть, и в исходном виде число осталось только в регистре Y. Но основная процедура запомнит в Rb полное значение нового положения. Т. е. если даже по этажу не пройти, мы запомним точку, куда хотели попасть. В случае удачи эта процедура автоматически обновит содержимое Ra.
Вспомним про адрес перехода 38 = EA, куда мы попадаем при движении между
этажами. Оказывается – банальный плюс, но с учётом ±1 и
заранее, на шаге 09, извлеченного Ra, это то, что доктор прописал
: вверх/вниз.
Так вариант с движением вверх/вниз плавно влился в текущий кусок
кода: очередной трюк. Тут тоже стоит заострить внимание, как экономятся команды.
По адресу 09 потрачено
две команды, но зато потом идёт просто переход
на нужный адрес без дополнительных команды. В случае, если бы мы
выделили это в отдельную ветку исполнения, то после извлечения из Ra
потребовался бы безусловный переход на тот же плюс. Но в итоге это
было бы уже три команды, потому что команда БП двойная.
Сделаем паузу и подведём итоги, что мы изучили до этого. Первое – основную процедуру проверки, так же процедуру очистки. Второе – начало основного цикла программы с анализом ходов. Третье – все ходы движения по этажу, в том числе выход. Четвертое – все ходы движения между этажами. Осталось три возможности: поиск клада, взлом стены и начальная инициализация программы. Вот с неё и продолжим.
Как начинается игра, предполагая что глобальные константы вбиты, как и сама программа? А вот так: задаётся начальное количество еды, затем B↑, чтобы это значение попало в регистр Y, а затем начальное положение игрока в лабиринте. После чего выполняется команды БП 44 С/П. Я рекомендую задавать следующие значения: для еды 44, а для положения 1.0000001. Поясню почему. Положение такое является максимально удалённым от выхода и набирать его не сложно, как я сейчас покажу. А 44 просто удобно для ввода после перехода на адрес 44, на случай дребезга числовых клавиш. Проще всего так: БП 44 4 4 П→x9 F10x С/П. Последние две команды перед запуском как раз и выдают значение 1.0000001 . Кто-то может подумать, что автор даже это предусмотрел, но нет, это, но только это, случайно так совпало. Тут же фрагмент начальной инициализации:
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
40 | | x→Пb | Кmax | Кx≠07 | x→Пa | x→Пe | 0 | ||||
50 | | x→Пc |
Положение игрока запоминается в Rb, как бы куда хотим пойти. А потом начальное значение еды как-то очень сложно с дополнительными ненужными проверками запоминается кроме Re зачем-то ещё и в Ra, и обнуляется счётчик сокровищ. В данном случае Кmax выступает аналогом <->, потому что начальное значение еды обычно больше, чем номер этажа. Впрочем, если вы укажете меньше, то программа разумно даст хоть что-то.
Зачем такие сложности?
может кто-то спросить, а наиболее
догадливые уже увидели, что это же код основной процедуры, который
мы рассмотрели, и использовали совсем для других целей.
Но здесь мы идём с нормального адреса 44, а значит нестандартного
перехода на адрес 00 после адреса 47 не будет.
Да, такой вот необычный трюк, когда
один и тот же набор команд используется для разных целей в
разных ветках исполнения. В результате мы бесплатно
получили две команды для сохранения начального ввода:
x→Пb
<->.
Ладно, поудивлялись и продолжим далее код инициализации.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
50 | | x→П0 | П→x3 | 6 | x→Пd | КСЧ | F10x | FВx | Fxy | К∨ | |
60 | | Кx→П0 | П→x0 | Кx≥08 |
Тут идёт код генерации плана лабиринта и расположения кладов. Поясню, как это делается. С помощью датчика случайных чисел генерируется число в диапазоне (0..1). Из него с помощью стандартных степенных функций получается два случайных числа, которые уже содержат все семь знаков мантиссы. Затем результат логического сложения этих двух случайных чисел запоминается в нужном регистре.
А теперь детали. Сначала зануляем R0, который нестандартно в цикле используем для заполнения регистров R1…R3 и R0 (!). Благодаря особенностям косвенной адресации при первом обращении Кx→П0 ноль превратится в −99999999, что соответствует регистру R3, затем в −99999998 – R2, и так пока не дойдёт до R0, где −99999996 перепишется новым сгенерированным значением, но уже положительным. Это и отслеживаем в цикле на шаге 62. Тут уже можно сказать, что в R8 храниться адрес начала цикла = 52, только со знаком минус. Почему минус – читай ранее по вычленению команды поиска клада.
Зачем П→x3? Дело в датчике случайных чисел. Его генерируемое значение очень зависит от содержимого регистра Y, и это документировано, и частично от X. Чтобы лабиринты отличались от одной игры к другой, именно значение регистра R3, как первое сгенерируемое случаное значение, и вносится в регистр Y. А в регистре X остаётся шестёрка, как начальное значение гранат. Почему 6, а не 4? Так две мы потратим, чтобы убрать возможную стенку в точке высадки. Всё-таки лабиринт генерируется случайно, и начальное положение игрока может случайно оказаться в стенке. Кажется, что инициализацию Rd можно вынести за цикл, но испытания показали, что именно такой порядок команд лучше всего сказывается на датчике случайных чисел – он не циклиться.
F10x
более равномерно размазывает
цифры случайного числа по мантиссе.
При этом первая цифра, конечно, получается из очень ограниченного
диапазона, но для логических операций она не важна.
Fxy
делает другое подобное размазывание
из случайного числа,
но эта операция, в отличии от обычных операций, оставляет регистр Y на месте.
Любители усложнения вместо К∨ могут записать команду К⊕. В этом случае будет больше стенок и меньше кладов.
Далее код последовательно переходит в процедуру взлома стены. Сначала проверяется наличный запас гранат, с уменьшением. Если не хватает, то переход на адрес 00. Причём сохранение остатка делается после проверки, чтобы не было −2, −4, −6 и т. д.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
60 | | П→xd | 2 | − | Кx≥07 | x→Пd |
Потом код проверки границ и удаление стенки.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
60 | | П→xb | 4 | ||||||||
70 | | − | Кx<09 | П→xb | КП→xb | К∨ | Кx→Пb | КБП9 |
Напомню, что координаты взламываемой стены находятся в Rb. Вначале
проверяем, что это значение в пределах регистров R1…R3,
т. е. < 4, причём R0 тоже пройдёт проверку, но это не страшно
и вот почему.
На самом деле в Rb число меньше единицы может быть или в виде одной цифры,
например, хотели с первого этажа 1.0002 пойти вниз и получили 0.0002.
Или просто ноль – получается при попытке выхода за границы этажа.
Главное, что число будет из одной цифры, т. е. со второго разряда цифр нет,
а значит в этом случае план этажа извлечётся из плана кладов R0,
потому что косвенная адресация через Rb будет равна нулю. Да,
последние две цифры мантиссы будут нулевыми, даже для числа 1.|−07,
что можно посмотреть
в главе по косвенной адресации.
Затем операция
К∨ ничего не
изменит, потому что нет цифр у второго операнда, и вернет обратно
в R0 значение неизменным. Последующая процедура очистки, с учётом
того, что второй операнд из одной цифры, поставит 0
на попытке
пробить пол в подвал.
А если число больше или равно 4, то мы сразу перейдём на процедуру очистки
через регистр R9. С учётом работы остальной части программы, это
можно потом прикинуть, для Rb ⩾ 4 число в Rb либо состоит из
одной цифры, в случае взлома левой стенки, и процедура очистки безусловно
сработает, либо это крыша
, при Rb = 4.{…}. Но в R4, как это
я так удачно всё расположил, сам удивляюсь,
у нас тоже константа из одной цифры,
а значит процедура очистки тоже сработает.
Для R1…R3 это операция установит бит из Rb в плане этажа. В этом случае в X у нас план этажа, в Y точка взлома, которую мы тут устанавливаем. В этом случае основная процедура успешно пройдёт битовую проверку, потому что мы же только что этот бит поставили, и завершится, записав новое расположение в регистре Ra. Т. е. мы сразу сделаем ход в новое место.
Итак. Инициализация закончена. Игрок в точке высадки, стенки там нет. Код начальной инициализации и код взлома стены изучен. Остался поиск клада, и разбойник. Там тоже не всё так просто.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
70 | | П→x0 | П→xa | КИНВ | |||||||
80 | | К∧ | x→П0 |
Путём инверсии бита положения игрока и наложения на маску плана мы безусловно очистим этот бит в плане кладов. Кажется странным, что мы сразу сохраняем новое значение в R0, даже не проверив, что он там может уже отсутствовал. Но последующее
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
80 | | − | Кx≠07 |
проясняет ситуацию. Мы опять пользуемся тем, что логическая операция оставляет регистр Y на месте, а именно в нём хранилось исходное значение плана кладов. Вот тут и делаем проверку. Заодно убираем безвозвратно план кладов из стека, чтобы он случайно не засветился.
А теперь приведём код, который из номера этажа получает
шестнадцатеричную цифру, из соответствия 1 = E, 2 = D, 3 = C.
В оригинале автор использовал для этого заранее сохранённую
константу E
и цикл с уменьшением её через регистр R0.
Мы сделаем то же самое без
использования дополнительных регистров и короче, пользуясь
недокументированными возможностями.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
80 | | П→x5 | П→xa | + | КИНВ | К{x} | КСЧ | ||||
90 | | ВП |
Тут снова придётся вспомнить
раздел про регистр X2
и команду
ВП, которая восстанавливает его. Сначала с помощью
сложения с 10, это константа из R5, мы номер этажа
текущего положения игрока загоняем во второй разряд: N.xxx + 10 = 1N.xxx.
Тут порядок операндов важен, т. к. последний
П→x и будет содержаться в X2.
Затем с помощью инверсии и отсеканию целой части, первой цифры,
мы получим, что нужная буква находится в первом разряде. Но в X вроде
ещё что-то есть? Так вот, восстановление X2 через
ВП, сделает так, что
при восстановлении X2 только первая цифра останется из X, т. е. 2.00004
восстановится в D.00004. То, что нам нужно. Ещё нужно помнить, что перед
ВП должна быть любая не X2-влияющая команда,
которая будет выполнена, но результат проигнорирован. Обычно для этих
целей используется КНОП,
но мы используем КСЧ. Дело
в том, что основная процедура может использовать
Кmax с нулём в Y, а это
сбрасывает
датчик случайных чисел. Для последующих игр,
вдруг захочется пройти другой лабиринт, желательно оживить
датчик
случайных чисел. Как говориться с паршивой овцы хоть шерсти клок
– даже
из игнорируемой команды пытаемся получить эффект. Идём далее.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
90 | | x→Пb | КП→xb | П→xb | 1 | − | Fx2 | + | С/П |
Сохраняем полученную букву с дробной частью в Rb и оставляем его в стеке
для информации игроку. Потом не только извлекаем старое значение ресурса,
но и заодно отсекаем дробную часть от буквы, при косвенной адресации,
чтобы потом преобразовать её в нужную добавку.
Тут нужно пояснить, что именно происходит для вычисления добавки
, в
зависимости от значения регистра X: C, D или E.
При вычитании срабатывают правила шестнадцатеричной арифметики.
X | После − 1 |
После x2 |
---|---|---|
C | 1 | 1 (сокровище) |
D | 2 | 4 (динамит) |
E | 3 | 9 (еды) |
Код борьбы с разбойником полностью взят из оригинала. Главное, чтобы переключатель Р-ГРД-Г был в положении Р. Окончательное значение ресурса сохраняется.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
90 | | Fsin | |||||||||
A0 | | 1 | + | × | К[x] | Кx→Пb |
Вот собственно и всё. Конечно далее программа пойдёт по малой побочной ветке адресации, как бы с A5 = 00. Но она заканичивается на команде с адресом B1 = 06, а в этом диапазоне существующие команды безусловно переведут на другую ветвь.
# | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
---|---|---|---|---|---|---|---|---|---|---|
00 | | С/П | П→xe | 1 | − | Fx<0 | B9 | В/О | x→Пe | <-> | П→xa |
10 | | <-> | 2 | ÷ | Fx≠0 | 63 | Fπ | + | x→Пb | ВП | КЗН |
20 | | Fx=0 | EA | П→x6 | П→xa | К{x} | КП→xb | × | Fx≥0 | 77 | × |
30 | | К∨ | К{x} | Кx=09 | . | П→x9 | − | П→xa | К[x] | + | x→Пb |
40 | | КП→xb | К∧ | К{x} | <-> | x→Пb | Кmax | Кx≠07 | x→Пa | x→Пe | 0 |
50 | | x→Пc | x→П0 | П→x3 | 6 | x→Пd | КСЧ | F10x | FВx | Fxy | К∨ |
60 | | Кx→П0 | П→x0 | Кx≥08 | П→xd | 2 | − | Кx≥07 | x→Пd | П→xb | 4 |
70 | | − | Кx<09 | П→xb | КП→xb | К∨ | Кx→Пb | КБП9 | П→x0 | П→xa | КИНВ |
80 | | К∧ | x→П0 | − | Кx≠07 | П→x5 | П→xa | + | КИНВ | К{x} | КСЧ |
90 | | ВП | x→Пb | КП→xb | П→xb | 1 | − | Fx2 | + | С/П | Fsin |
A0 | | 1 | + | × | К[x] | Кx→Пb |
Начальные значения констант, которые не меняются между играми:
Вот последовательность для ввода констант:
5
2
/-/
x→П8
4
4
7
3
В↑
8
0
8
К∨
К{x}
ВП
7
/-/
x→П9
1
0
x→П5
2
x→П4
F1/x
x→П7
2
2
КИНВ
К{x}
ВП
1
К[x]
ВП
2
/-/
x→П6
Положение переключателя Р-ГРД-Г должно быть в Р.
И напомню порядок начала игры: в регистре Y – начальное количество еды, в регистре X – начальное положение. Выполнение должно начинаться с адреса 44. Проще всего так: БП 44 4 4 П→x9 F10x С/П.
сдвигаво второй разряд по адресу 84. R7 – тоже как коэффициент умножения, но и как адрес перехода. R8 – кроме косвенной адресации используется отрицательным для проверки хода поиска клада по адресу 27. R9 – кроме косвенной адресации используется для корректировки округления по адресу 34.
Слияниеокончания одной процедуры с другой для исключения команды перехода. В данном случае окончание инициализации и начало взлома стены по адресу 63. А также слияние двух веток вычисления, этажной и межэтажной, по адресу 38.
игнорированиедробной части после сложения по адресам 16 или 38, и умышленное
отрубаниееё по адресу 92. А также необычное использование регистра R0 в цикле по адресу 60 с отрицательным счётчиком.
справав Rb, а для процедуры подрыва и проверки выхода за границу Rb такое количество цифр в числе недопустимо, потому что процедура очистки вместо очистки внесёт в Ra что-то случайное. Вот так, иногда приходится использовать недокументированные возможности для корректности поведения алгоритма без увеличения длины программы.
просвечиваниянуля из Y по адресу 45. А также как замена команде <->.