РАБОТА СКАЛЬКУЛЯТОРОМ
РАБОТА С КАЛЬКУЛЯТОРОМ
Как вы уже могли заметить, в игровых программах в большинстве случаев вполне можно обойтись только целыми числами. Но иногда все же приходится привлекать к расчетам и вещественные величины, особенно в блоке оценки игровой ситуации (это вы могли заметить в программе МИШЕНЬ: при расчете среднего арифметического явно требуются дробные числа). В свое время мы говорили, что для подобных вычислений можно обращаться к программе ПЗУ, выполняющей различные математические операции именно с такими числами и именуемой калькулятором. Эта программа расположена по адресу 40, что позволяет вызывать ее командой RST 40.
Работать с этой программой непросто, что объясняется, во-первых, большим количеством допустимых операций, а во-вторых, необходимостью следить за порядком обмена данными со стеком калькулятора. Поэтому мы расскажем лишь о самых необходимых в игровых программах функциях.
Необходимо знать, что параметры калькулятору передаются через его собственный стек, о котором вы уже знаете достаточно, а выполняемое действие определяется последовательностью байтов-литералов, записываемых непосредственно за командой RST 40. Поскольку все математические операции калькулятор выполняет на своем стеке, то прежде всего необходимо научиться записывать туда числа и затем снимать со стека результат. По крайней мере с двумя процедурами записи в стек значений из аккумулятора и пары BC мы вас уже познакомили, но существуют и другие подпрограммы, о которых также не мешает знать.
По адресу 10934 в ПЗУ имеется процедура, записывающая в стек калькулятора вещественное число в пятибайтовом представлении. Эти пять байт числа перед обращением к процедуре нужно последовательно разместить на регистрах A, E, D, C и B. Основная сложность здесь заключена в разбивке числа с плавающей запятой на 5 компонентов, так как при этом применяются довольно хитрые расчеты. Однако если вам требуется записать заранее предопределенную константу, то можно воспользоваться очень простым способом, заставив операционную систему саму выполнить все необходимые действия. Идея сводится к тому, что при вводе строки в редакторе Бейсика все числа, прежде чем попадут в программу, переводятся интерпретатором из символьного в пятибайтовое представление. Делается это для того, чтобы во время выполнения программы уже не заниматься такими расчетами и тем самым сэкономить время. Следом за символами каждого числа записывается байт 14 и затем рассчитанные 5 байт. Например, число 12803.52 в памяти будет выглядеть таким образом:
1 2 8 0 3 . 5 2 Префикс Число 49 50 56 48 51 46 53 50 14 142 72 14 20 123
Код 14 и пять байт числа при выводе листинга бейсик-программы на экран пропускаются, но в памяти они всегда присутствуют. Просмотрев дамп программы, нетрудно найти нужные байты. Можно воспользоваться и небольшой программкой, которая будет печатать нужные числа на экране, так что останется только записать их, а затем использовать в своей программе на ассемблере. Вот примерный текст такой программки:
10 PRINT 12803.52 20 LET addr=PEEK 23635+256*PEEK 23636+5 30 LET addr=addr+1: IF PEEK (addr-1)<>14 THEN GO TO 30 40 FOR n=addr TO addr+4: PRINT PEEK n: NEXT n
Дадим некоторые пояснения относительно этой программки. В строке 10 после оператора PRINT записывается любое вещественное число, пятибайтовое представление которого вы хотите узнать. Эта строка может иметь другой номер, но обязательно должна располагаться в самом начале программы. Учтите, что перед ней не должно быть даже комментариев.
Далее, в 20-й строке вычисляется адрес начала бейсик-программы (берется из системной переменной PROG) и пропускается 5 байт, включающих номер, длину строки и код оператора PRINT.
Операторы строки 30 отыскивают байт с кодом 14, за которым в памяти располагаются нужные нам байты числа. А в следующей строке эти 5 байт последовательно считываются в цикле и выводятся на экран.
Узнав таким образом значения составляющих числа в пятибайтовом представлении, можно загрузить регистры и вызвать процедуру 10934 для записи его на вершину стека калькулятора:
LD A,142 ;размещаем 5 байт числа на регистрах A, LD E,72 ; E LD D,14 ; D LD C,20 ; C LD B,123 ; и B CALL 10934 ;заносим число в стек калькулятора
Можно предложить еще один способ укладки десятичного числа в стек калькулятора с применением процедуры 11448. Именно этой процедурой пользуется интерпретатор, работая с числовыми величинами в символьном представлении. Выполняя программу, Бейсик сохраняет адрес текущего интерпретируемого кода в системной переменной CH_ADD (23645/23646) и в данном случае нам достаточно записать в нее адрес символьной строки, содержащей требуемое число, чтобы заставить интерпретатор разбить его на 5 байт и уложить в стек калькулятора. Не помешает предварительно сохранить, а затем восстановить прежнее значение переменной CH_ADD, иначе нормальный выход в операционную систему, а тем более, продолжение выполнения бейсик-программы окажется невозможным. Не забывайте, пользуясь этим методом, в конце строки, представляющей десятичное число, ставить код 13 (в принципе, это может быть практически любой символ, кроме цифр, точки, плюса и минуса, а также букв E и e).
LD HL,(23645) ;запоминаем в машинном стеке PUSH HL ; значение переменной CH_ADD LD HL,NUMBER ;адрес строки с десятичным числом LD (23645),HL ; записываем в переменную CH_ADD LD A,(HL) ;берем в аккумулятор первый символ ; (обязательно!) CALL 11448 ;помещаем число из текстовой строки ; NUMBER в стек калькулятора POP HL ;восстанавливаем прежнее значение LD (23645),HL ; системной переменной CH_ADD ......... ;продолжаем программу ; Символьное представление десятичного числа NUMBER DEFM "12803.52" DEFB 13 ;байт-ограничитель символьной строки
Надо добавить, что хотя этот способ и кажется наиболее удобным, но у него есть один серьезный недостаток - работает он несравненно дольше всех предыдущих. Самое смешное, что он требует даже больше времени, чем в Бейсике, так как эта операция выполняется при вводе строки и во время исполнения программы интерпретатор уже располагает пятибайтовым представлением каждого числа.
После занесения в стек калькулятора тем или иным способом числовых значений, с ними нужно что-то сделать, для чего и предназначена команда RST 40. Как вы помните, раньше мы использовали стек калькулятора для вывода чисел на экран, а также для рисования линий и окружностей. Теперь посмотрим, как над числами в стеке производить различные математические операции.
Как мы уже сказали, для этого нужно записать специальные управляющие последовательности байтов непосредственно за командой RST 40. В табл. 9.1 перечислены наиболее употребительные команды калькулятора, выполняемые ими функции и состояние стека после выполнения операции, считая, что изначально в стеке были записаны два числа: X - на вершине (был записан последним) и Y - под ним. Например, для сложения этих двух вещественных чисел применяется литерал 15, а для деления - 5. В одной команде можно перечислить произвольное количество действий, а для завершения расчетов в конце последовательности литералов всегда обязательно указывать байт 56, который возвращает управление на следующую за ним ячейку памяти. Понятно, что последовательность литералов в программу на ассемблере может быть вставлена с помощью директивы DEFB.
Таблица 9.1. Значение некоторых кодов калькулятора
Литерал | Операция | Состояние стека после операции | ||
1 | Замена элементов | X | Y | |
3 | Вычитание | Y - X | ||
4 | Умножение | Y ґ X | ||
5 | Деление | Y / X | ||
6 | Возведение в степень | YX | ||
15 | Сложение | Y + X | ||
27 | Изменение знака | Y | -X | |
39 | Целая часть числа | Y | INT X | |
40 | Квадратный корень | Y | SQR X | |
41 | Знак числа | Y | SGN X | |
42 | Абсолютная величина | Y | ABS X | |
49 | Копирование стека | Y | X | X |
56 | Конец расчетов | Y | X | |
88 | Округление числа | Y | INT(X+.5) | |
160 | Дописать 0 | Y | X | 0 |
161 | Дописать 1 | Y | X | 1 |
162 | Дописать 0.5 | Y | X | .5 |
163 | Дописать PI/2 | Y | X | PI / 2 |
164 | Дописать 10 | Y | X | 10 |
ORG 60000 ENT $ CALL 3435 ;очищаем экран LD A,2 ; и подготавливаем его для печати CALL 5633 LD BC,823 ;заносим в стек все части выражения CALL 11563 LD BC,5503 CALL 11563 LD A,32 CALL 11560 LD A,17 CALL 11560 RST 40 ;вызываем калькулятор DEFB 3 ; X = 32 - 17 DEFB 5 ; X = 5503 / X DEFB 4 ; X = 823 ґ X DEFB 56 ; конец расчетов CALL 11747 ;выводим результат на экран RET
После запуска этой подпрограммы вы увидите на экране число 301931.27. Тот же результат получается и при выполнении оператора PRINT 823*5503/(32-17).
Обязательным условием при работе с калькулятором является не только соблюдение порядка выполнения расчетов. При ошибке вы в худшем случае получите неверный результат. Гораздо важнее следить за состоянием стека калькулятора, так как если после завершения программы он окажется не в том же виде, как и в начале, последствия могут даже оказаться фатальными. Для безопасности перед выходом в Бейсик можно вызвать процедуру по адресу 5829, которая очистит стек калькулятора, хотя нужно сказать, что и это лекарство в тяжелых случаях может не помочь. Поэтому при особо сложных вычислениях (а по началу и в самых простых случаях) желательно проследить за стеком на каждом шаге расчетов. Для приведенной выше программки можно сделать примерно такую схемку:
Последовательно заносим числа в стек:
823 823 5503 823 5503 32 823 5503 32 17
Вызываем калькулятор (RST 40):
823 5503 15 ; 32 - 17 823 366.867 ; 5503 / 15 301931.27 ; 823 ґ 366.867
Из этой схемы сразу видно, что в конце расчетов на вершине стека калькулятора осталось единственное число - результат. Перед выходом в Бейсик необходимо удалить также и его. Для этого мы вызвали процедуру 11747, которая сняла полученное значение с вершины стека и напечатала его на экране. Таким образом, состояние стека осталось тем же, что до начала работы нашей программки.
Если вы не собираетесь сразу после вычислений печатать результат на экране или использовать его для вывода графики, нужно каким-то образом снять полученное значение со стека и сохранить его для будущего применения. Для этого нужно обратиться к одной из перечисленных ниже процедур, выбрав из них наиболее подходящую для каждого конкретного случая.
Подпрограмма, расположенная по адресу 11682, снимает число с вершины стека, округляет его до ближайшего целого и помещает в регистровую пару BC. Если число было положительным или нулем, то устанавливается флаг Z, в противном случае он будет сброшен. Может оказаться, что значение в стеке по абсолютной величине превышает максимально допустимое для регистровых пар (как это произошло в предыдущем примере). В этом случае на ошибку укажет флаг CY, который будет установлен в 1. Поэтому если вы не уверены в том, что результат не превысит 65535, лучше всегда проверять условие C и при его выполнении производить в программе те или иные коррекции, либо выводить на экран соответствующее сообщение.
Процедура 8980 похожа на предыдущую, но округленное значение из стека калькулятора помещается в аккумулятор. Здесь знак числа возвращается в регистр C: 1 для положительных чисел и нуля и -1 для отрицательных. Если число в стеке превысит величину байта и выйдет из диапазона -255...+255, то будет выдано сообщение Бейсика Integer out of range. Естественно, что ни о каком продолжении программы в этом случае речи быть не может, поэтому не применяйте ее, если не уверены, что результат не окажется слишком велик.
Округление чисел не всегда может оказаться удовлетворительным решением. Иногда требуется сохранить число в первозданном виде и для этого можно применить вызов процедуры ПЗУ, находящейся по адресу 11249. Она выполняет действие, обратное подпрограмме 10934 и извлекает из стека калькулятора все 5 байт числа, а затем последовательно размещает их на регистрах A, E, D, C и B. Выделив в программе на ассемблере область в 5 байт с помощью директивы DEFS 5, можно сохранить там полученный результат, чтобы впоследствии вновь им воспользоваться при расчетах.
Однако приведенные процедуры мало пригодны при работе с большим количеством пятибайтовых переменных. В этом случае лучше не обращаться за помощью к ПЗУ, а написать собственные процедуры для обмена данными между переменными и стеком калькулятора.
В процедуре укладки в стек пятибайтовой переменной не повредит предварительная проверка на предмет наличия свободной памяти. Для этого вызовем подпрограмму 13225, которая проверит, можно ли разместить на стеке 5 байт, и в случае нехватки памяти выдаст сообщение об ошибке Out of memory. Затем перенесем 5 байт переменной на вершину стека калькулятора и увеличим системную переменную STKEND, выполняющую ту же роль, что и регистр SP для машинного стека. Перед обращением к процедуре в паре HL нужно указать адрес пятибайтовой переменной.
PUTNUM CALL 13225 ;проверка наличия свободной памяти LD BC,5 ;переносим 5 байт LD DE,(23653) ;адрес вершины стека калькулятора LDIR ;переносим LD (23653),DE ;новый адрес вершины стека RET
Процедура GETNUM будет выполнять противоположное действие: перемещение пяти байт числа с вершины стека калькулятора и уменьшение указателя STKEND. Адрес переменной также будем указывать в HL. Заодно можно выполнить проверку перебора стека, так как именно эта ошибка наиболее опасна.
GETNUM PUSH HL LD DE,(23653) ;проверка достижения «дна» стека LD HL,(23651) ;системная переменная STKBOT, адресующая ; основание стека калькулятора AND A SBC HL,DE ;сравниваем значения STKEND и STKBOT JR NC,OUTDAT ;переход на сообщение, если стек ; полностью выбран POP HL LD BC,5 ADD HL,BC ;указываем на последний байт переменной DEC HL DEC DE EX DE,HL LDDR ;переносим 5 байт из стека в переменную INC HL LD (23653),HL ;обновляем указатель на вершину ; стека калькулятора RET OUTDAT RST 8 ;сообщение об ошибке DEFB 13 ; Out of DATA