Некромант

Пролог

Вы некромант, работаете по профессии. Договорились с местными, что разберётесь с неспокойным кладбищем за вознаграждение. Подготовив всё для мощного заклинания успокоения, вы ночью пришли на погост, а потом, выполнив всё приготовления и запустив заклинание, начали ждать. К сожалению, заклинание медленное и завязано на вас и на место. Поэтому уйти вы не можете, пока оно не сработает. Это около 25 минут.

Но зомби тоже почувствовали, что скоро их нежизнь закончится, поэтому начали вылазить с намерением вас уничтожить. Вылазят не спеша, где-то раз в минуту, и двигаются медленно, но с разных сторон.

Из-за основного заклинания на зрении вы решили не концентрироваться, поэтому видите только на 3 метра. Запаса сил на разрывные заклинания достаточно, но каждое нужно также готовить не спеша. При этом есть возможность применить заклинание целиком в одну сторону, или более рассеяно и с меньшим эффектом во все стороны. На тот случай, если кто-то всё-таки доберётся до вас, есть амулеты, которые сработают автоматически на уничтожение. Но их только два. Так что третий контакт будет фатальным.

Задача

Продержаться 25 минут, уничтожая мертвяков, пока не сработает основное заклинание.

Общее описание

Визуально место битвы представлено на экране в виде 3.CГ 8 E , где 8 - отображает вас в центре. Слева и справа идут зомби, где буква означает уровень нежизни монстра. Первая цифра на экране (целая часть) означает, сколько жизней у вас в запасе. За один ход (минуту) появляется только один зомби. Либо справа, либо слева, либо вообще нет.

В регистре Y отображается количество минут, сколько вам осталось продержаться.

Применять можно заклинание удара либо в обе стороны, рассеянное, набрав ноль, либо в одну, которая определяется знаком числа (минус – влево). Зомби идут друг за другом, так что заклинание подействуют только на ближайших на любом расстоянии, остальные продолжат движение. Некоторые зомби достаточно сильные, и их не получится уничтожить, даже трижды применив рассеянное заклинание. И с другой стороны, самые слабые обладают иммунитетом к мощным ударам, используя избыток для собственного восстановления. Вот таблица, поясняющая эффект ударов:

Сила монстра После рассеянного удара После концентрированного удара
L Становится C Становится Г
С Становится Г Становится E
Г Становится E Уничтожается
E Уничтожается Иммунитет! Остаётся E

Игровой процесс

Начало

Если не используете эмулятор, где сохранённое состояние уже включает регистры, но необходимо задать константы, которые не меняются между играми:

Регистр Значение
R4 -. . Получается так: 5 5 KИНВ K{x} ВП 1 K[x].
R5 Случайное число от 0 до 1. В процессе игры меняется.
R6 25 – начальное количество минут.
R7 74
R8 83
R9 92
Rb 53
Rc 96
Re 1000

Игра

После начального В/О С/П ПМК отобразит поле битвы. Силы зомби, и с какой стороны появляются (если появляются), определяется генератором случайных чисел.

Оценив положение и выбрав направление удара нажмите С/П. Направление и сила удара определяется знаком числа в регистре X, а само значение не важно:

Число Тип удара
< 0 Концентрированный влево
= 0 Рассеянный в обе стороны
> 0 Концентрированный вправо

После остановки ПМК отобразит поле после вашего удара. Если появится --. , это означает, что как минимум один зомби добрался до вас. После нажатия С/П сработает амулет и уничтожит всех, кто вошёл с вами в контакт. Если у вас уже не осталось амулетов к этому моменту, то снова появится тот же знак – вы проиграли.

Окончание

Если вы продержались нужное время, то основное заклинание сработало, погост успокоен и экране отобразится 1000. – ваш гонорар за работу. Если неудача, то всё время будет появляться признак --. – зомби навалились и расправляются с некромантом.

Для начала новой игры нажмите В/0 С/П.

Дополнительная информация

Модификация

Количество жизней зашито в программе. Достаточно в программе по адресу 01 вместо 3 указать другую цифру (но не ноль!). Большее значение – облегчение игры.

Количество минут на игру определяется регистром R6. Обязательно больше нуля!

История

Игра написана по мотивам ранее опубликованной игры Оборотни. К сожалению в ней не всё хорошо.

Неудобства:

Ошибки:


Далее только для тех, кто не только играет…

Распределение регистров

R0 Монстры слева в виде дробного числа.
R1 Количество оставшихся минут.
R2 Количество оставшихся жизней.
R3 Рабочий регистр. В процессе генерации экрана используется как счётчик цикла, а затем как уровень удара (1 или 2).
R4 -. . Оно же является множителем 10.
R5 Очередное случайное число генератора.
R6 25. Для инициализации R1.
R7 74. Адрес подпрограммы движения зомби к центру.
R8 83. Адрес подпрограммы проведения удара.
R9 92. Адрес подпрограммы хитрого сложения.
Ra Монстры справа в виде дробного числа.
Rb 53. Адрес перехода на концентрированный удар.
Rc 96. Адрес подпрограммы генерации случайного числа на основе множителя в RX.
Rd Номер регистра для обработки группы монстров. 0 или 10.
Re 1000. Множитель для добавления нового зомби в конец. Признак успешного окончания. Адрес косвенного перехода в начало (00).

Текст программы

 # |  00 01 02 03 04 05 06 07 08 09
 00 |  В/О 3 x→П2 0 x→П0 x→Пa П→x6 x→П1 2 КППc
 10 |  П→x4 × x→Пd 5 КППc П→xe ÷ КП→xd + Кx→Пd
 20 |  4 x→П3 П→x3 2 F10x П→x0 × К{x} +
 30 |  П→x4 × К[x] FL3 22 7 + П→xa + КИНВ
 40 |  Fπ x→П3 П→x1 КП→x3 FВx КПП9 С/П КЗН Кx=0b x→Пd
 50 |  КП→x3 КПП8 1 F10x x→Пd КПП8 П→x4 x→Пd КПП7 КПП7
 60 |  + К[x] Fx=0 70 FL1 08 П→xe С/П FL2 64
 70 |  П→x4 /-/ БП 67 КП→xd П→x4 × В↑ К{x} Кx→Пd
 80 |  x→Пd F В/О КП→xd Кx≠0e 1 КПП9 П→x3 КП→xd
 90 |  КПП9 Кx→Пd <-> F ВП В/О П→x5 7 × Fπ
 A0 |  + К{x} x→П5 × К[x]

Объяснение работы программы

Сначала поясню несколько неочевидных моментов. Первое – зачем команда В/О перенесена в начало. Это чтобы можно было косвенно через 0 в регистре Re быстро перейти в конец подпрограммы.

Второе. Шестнадцатеричная цифра A в регистре R4 успешно заменяет десятку (если как множитель находится слева), но смотрится лучше как признак контакта.

Последнее, это хитрое сложение. Чтобы его понять, рекомендую почитать раздел про регистр X2 из статьи по недокументированным возможностям. Так вот, эта подпрограмма, состоящая всего из трёх команд по адресу 92 (R9), в регистре X2 заменяет первую значащую цифру, без изменения порядка числа, на первую цифру числа из регистра Y, знак, порядок и остальные цифры которого не имеют значение. Результат записывается в регистр X, а содержимое Y, как при обычном сложении, исчезает, с подтягиванием стека. Как так получается, можно узнать здесь. Остальные фишки по мере изложения.

По поводу зомби. Разделены на группы слева (R0) и справа (Ra). Каждая группа состоит из трёх цифр от 0 до 4. Хранится как дробное число вида 0.123. Цифра 1 ближе к центру (к игроку). После хода сдвигается влево, а правый (3-й) разряд пополняется новым монстром. Каждая цифра определяет силу монстра, где ноль – это его отсутствие. Если при сдвиге (умножении на 10) получается не нулевая целая часть, значит есть контакт игрока с монстром. При подготовке поля битвы цифры на экране инвертируются (в шестнадцатеричном представлении) для наглядности, а разряды из R0 ещё и разворачиваются в обратном порядке. В дальнейшем будем использовать термин число-группа.

Разберём вспомогательные процедуры. Про хитрое сложение уже сказали. Перейдём к генератору случайных чисел (псевдослучайных, конечно). Подпрограмма с адреса 96 (Rc) сначала генерит очередное случайное число по формуле Ni+1 = {7 * Ni + π}, а затем умножает его на множитель, которых до вызова был в RX. В программе используется только два варианта. Множитель 2 – результат 0, 1 для выбора группы зомби. Множитель 5 – результат 0…4 для уровня очередного монстра.

Подпрограмма сдвига монстров к центру по адресу 74 (R7). Индекс группы находится в Rd. Поэтому число просто умножается на 10, копируется временно в RY, дробная часть сохраняется обратно, а результат умножения, для анализа контакта, возвращается в RX. Тут не очевидна команда x→Пd по адресу 80. Так вот, сама подпрограмма точно вызывается дважды. Первый раз с RD = 10 (точнее = R4), и вот, чтобы записать ноль в RD для повторного вызова это и делается. Конечно, дробная часть может быть и не нулевая, но из той же статьи из раздела по косвенной адресации видно, что числа в степени −01 (или −02, −03) не меняются при косвенном использовании. А последние цифры мантиссы всегда остаются нулевыми: число-группа использует только три первых разряда.

Подпрограмма проведения удара с адреса 83 (R8). Как обычно, индекс группы в Rd. Сначала отсекается вариант, что группа целиком пустая (вот где нужен Re с возвратом через адрес 00). Потом используется хитрое сложение. Для этого просто единица заменяется на первую значащую цифру в группе (ближайший монстр). Потом от результата отнимается сила удара (1 или 2). А затем снова используем хитрое сложение, но уже чтобы результирующую цифру подставить на место первой значащей цифры в числе-группе. Может и ноль, кстати. Вот здесь и видно, как у монстра E работает иммунитет. Ему соответствует цифра 1, и при вычитании 1 − 2 = −1. А хитрое сложение знаки игнорирует, вот и получается, как по легенде, лишняя энергия используется для восстановления.

Возможно обратили внимание, что у предыдущей подпрограммы нет команды В/О, и код продолжает выполнять что-то другое с адреса 92. Это просто экономия одной команды. От процедуры проведения удара результат не ожидается, поэтому если что-то ещё сделает в стеке, то ничего страшного. А потом всё равно встретится В/О.

Ну вот, все процедуры рассмотрели, можно переходить к основному коду.

Адреса 01..07. Инициализация переменных игры начальными значениями.

Адреса 08..12. Запись индекса группы в Rd, в которую будет добавлен новый монстр. Как уже упоминалось про процедуру генерации случайного числа, на выходе будет 0 или 1. Умноженное на 10 и даст 0 или 10.

Адреса 03..19. Генерация нового монстра и добавление его в конец группы. К этому моменту в числе-группе точно нет последнего разряда, так что плюс безопасен.

Адреса 20..34. Этот фрагмент разворачивает разряды из регистра R0, добавляя ещё ноль справа, и одну цифру слева (какую не важно, т. к. инверсия всё равно эту цифру в 8 превратит, но в частности это всегда 4). Т. е. число 0.321 будет преобразовано в 41230. Как именно, оставляю как упражнение читателю. Тут нет ничего сложного или недокументированного.

Адреса 35..39. Добавление признака игрока (7 это инверсное 8) и второй группы. Не важно, что дробное число. Для инверсии важны только цифры. Вот, уже получилось почти то, что нужно. Только в RY нет счётчика дней и первый разряд 8, а не число жизней. Всё это добавляется далее.

Адреса 40..46. Вывод поля битвы. Тут делается ещё одно действие. Внесение цифры 2 в регистр R3, как значение силы удара по умолчанию. Делается это нестандартно. Команда Fπ, как побочный эффект, сбросит X в X1. Т. е. наша картинка пока отдохнёт там. В это время в стек догружаются другие нужные константы. И опять же вместо П→x2 используется КП→x3, чтобы в R3 тройку (да π при косвенном использования ведёт себя как тройка) заменить на два. И потом, используя хитрое сложение и тот факт, что при FВx картинка из X1 попадёт в X2, получим нужный результат.

Адреса 47..55. Проведение удара. Поясню, что для удара в обе стороны, нужно дважды вызвать процедуру в R8, но при значении R3 = 1. Если посмотреть код прямо, пока исключив вариант не ноль, то это именно и делается. Ноль заносится в Rd. R3 уменьшается, делается вызов. Затем в Rd заносится 10 (длинновато, конечно) и повторяется вызов. А теперь посмотрим на случай, если игрок указал не ноль. Тогда знак числа сразу (через Rb) попадёт на F10x (так вот он зачем тут!), превращаясь в 0.1 (что фактически ноль) и 10. А R3 останется двойкой. И процедура будет вызвана только один раз. Как раз и есть концентрированный удар в одном направлении.

Адреса 56..59. Движение монстров. Саму процедуру мы уже рассмотрели, как и пояснение, почему второй раз индекс группы в Rd уже будет нулевым.

Адреса 60..67. Проверка на контакт, и если сумма целых чисел групп нулевая (нет контакта), то переход на начало с уменьшением счётчика минут. При исчерпании – вывод признака успешного окончания.

Адреса 68..73. Обработка контакта. Тут проще начать с адреса 70, куда идёт переход при контакте. C него в Rx готовиться признак контакта, а затем идёт обратный переход на финальный С/П. И только после него проверятся счётчик жизней. Вот такая хитрая карусель для сокращения одного С/П.

Вот и всё. Дальше в программе идут процедуры, которые мы уже рассмотрели.