ГЛАВА ДЕВЯТАЯ,

из которой вы узнаете, как подсчитать число заработанных очков и вообще оценить состояние игры



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

Сразу скажем, что оценка игровой ситуации не сводится к одним лишь только математическим расчетам. На самом деле не вся оценочная информация может выводиться на экран в виде чисел, а кое-что остается, так сказать, для «внутреннего пользования» самой программе, которая следит за развитием событий и соответствующим образом себя ведет. Например, в игре Tetris при завершении очередного ряда он должен автоматически удаляться, а все ряды выше «списанного» опускаться вниз. Это можно назвать уже не количественной, а качественной оценкой, используемой самой программой.


ПОДСЧЕТ КОЛИЧЕСТВЕННЫХ ВЕЛИЧИН


Покажем в начале, каким образом реализовать в игровой программе наиболее простые виды оценок, например, подсчет количества очков, контроль числа оставшихся жизней, количество попаданий в противника и так далее, в основе которых лежит, в общем-то, простая операция сложения чисел. До начала подсчета числа очков, следует обнулить переменную SUM, которая будет выполнять функции счетчика:
       XOR   A
       LD    (SUM),A
       ..............
Если же контролируется количество «жизней», то следует наоборот занести в SUM какое-то начальное значение:
       LD    A,10
       LD    (SUM),A
       ..............
Затем эта переменная будет изменяться в блоке оценки игровой ситуации, увеличиваясь или уменьшаясь в зависимости от ее типа:
       ............
       LD    A,(SUM)
       INC   A           ;или DEC A
       LD    (SUM),A
       CALL  PRINT       ;вывод числовой оценки на экран
       ............
SUM    DEFB  0           ;переменная для накопления суммы
Однако все так просто лишь до тех пор, пока подсчеты не требуют разного рода дополнительных проверок. Более сложным и интересным является случай, когда одновременно с подсчетами, выполняется еще и анализ игровой ситуации, а результаты этого анализа оказывают влияние на сами оценки.

Для иллюстрации рассмотрим программу МИШЕНЬ, которая уже использовалась нами в пятой главе для демонстрации случайных чисел. Дополним теперь эту программу некоторыми оценками, например, после каждого выстрела будем суммировать набранные очки, выводить общее количество произведенных выстрелов и, наконец, подсчитаем средний балл, полученный за один выстрел.

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

Мы уже упоминали подпрограмму ПЗУ, расположенную по адресу 12457, выполняющую умножение двух целых чисел, находящихся в регистровых парах HL и DE. Ниже показана аналогичная процедура, отличающаяся только тем, что при умножении учитываются знаки сомножителей, заданных также в HL и DE. То есть числа, участвующие в операции могут находиться в пределах от -32768 до +32767. Произведение возвращается в регистровой паре HL.

MULT   LD    B,8
       LD    A,D
       AND   A
       JR    Z,MULT1
       RLC   B
MULT1  LD    C,D
       LD    A,E
       EX    DE,HL
       LD    HL,0
MULT2  SRL   C
       RRA
       JR    NC,MULT3
       ADD   HL,DE
MULT3  EX    DE,HL
       ADD   HL,HL
       EX    DE,HL
       DJNZ  MULT2
       RET
Следующая подпрограмма предназначена для деления знаковых величин. Делимое перед обращением к ней должно находиться в паре HL, а делитель - в DE. Напоминаем, что деление на 0 невозможно, поэтому в программе имеет смысл выполнять подобную проверку. Если такая ошибка все же произойдет, будет выдано сообщение Бейсика Number too big. После выполнения процедуры частное окажется в паре HL, а остаток от деления будет отброшен.
DIVIS  LD    A,D
       OR    E
       JR    NZ,DIVIS1
       RST   8
       DEFB  5           ;Number too big
DIVIS1 CALL  DIVIS5
       PUSH  BC
       LD    C,E
       LD    B,D
       LD    DE,0
       PUSH  DE
       EX    DE,HL
       INC   HL
DIVIS2 ADD   HL,HL
       EX    DE,HL
       ADD   HL,HL
       LD    A,C
       SUB   L
       LD    A,B
       SBC   A,H
       EX    DE,HL
       JR    NC,DIVIS2
       EX    DE,HL
DIVIS3 EX    DE,HL
       XOR   A
       LD    A,H
       RRA
       LD    H,A
       LD    A,L
       RRA
       LD    L,A
       OR    H
       JR    Z,DIVIS4
       EX    DE,HL
       XOR   A
       RR    H
       RR    L
       LD    A,C
       SUB   L
       LD    A,B
       SBC   A,H
       JP    M,DIVIS3
       LD    A,C
       SUB   L
       LD    C,A
       LD    A,B
       SBC   A,H
       LD    B,A
       EX    (SP),HL
       ADD   HL,DE
       EX    (SP),HL
       JR    DIVIS3
DIVIS4 POP   HL
       POP   BC
       BIT   7,B
       JR    NZ,MINUS
       RET
DIVIS5 LD    B,H
       LD    A,H
       RLA
       CALL  C,MINUS
       EX    DE,HL
       LD    A,H
       XOR   B
       LD    B,A
       BIT   7,H
       RET   Z
MINUS  LD    A,H
       CPL
       LD    H,A
       LD    A,L
       CPL
       LD    L,A
       INC   HL
       RET
Как вы помните, извлечение квадратного корня из отрицательного числа невозможно, результат также не может быть меньше нуля, поэтому и соответствующая процедура работает с величинами из диапазона 0...65535. Перед обращением к ней число, из которого нужно извлечь корень, поместите в пару HL. Результат, как и в предыдущих подпрограммах, будет возвращен в HL. Дробная часть при этом, к сожалению, также теряется.
SQR    LD    A,L
       LD    L,H
       LD    H,0
       LD    DE,64
       LD    B,8
SQR1   SBC   HL,DE
       JR    NC,SQR2
       ADD   HL,DE
SQR2   CCF
       RL    D
       ADD   A,A
       ADC   HL,HL
       ADD   A,A
       ADC   HL,HL
       DJNZ  SQR1
       LD    L,D
       LD    H,A
       RET
А теперь приводим модифицированный текст программы МИШЕНЬ, в которой присутствуют некоторые типы оценок, а также демонстрируется применение только что предложенных математических процедур:
       ORG   60000
       ENT   $
       LD    A,7
       LD    (23693),A
       XOR   A
       CALL  8859
       CALL  3435
       LD    A,2
       CALL  5633
;Основная часть программы МИШЕНЬ
       LD    HL,UDG
       LD    (23675),HL
       LD    HL,0
       LD    (SCORE),HL  ;обнуление счетчика количества очков,
                         ; заработанных при стрельбе по «мишени»
       XOR   A
       LD    (KOL_W),A   ;обнуление счетчика числа выстрелов,
                         ; произведенных по «мишени»
       CALL  MISH        ;рисование «мишени»
MAIN   CALL  WAIT        ;ожидание нажатия любой клавиши
       LD    A,22
       RST   16
       LD    E,19        ;диапазон изменения координаты Y
                         ; для пулевого отверстия
       CALL  RND         ;задаем координату Y
       LD    (ROW),A     ;заносим ее в переменную ROW
       RST   16
       LD    E,30        ;диапазон изменения координаты X
                         ; для пулевого отверстия
       CALL  RND         ;задаем координату X
       LD    (COL),A     ;заносим ее в переменную COL
       RST   16
       LD    A,16
       RST   16
       LD    A,6         ;задаем цвет пулевых отверстий
       RST   16
       LD    E,3         ;количество видов пулевых отверстий
       CALL  RND
       ADD   A,144
       RST   16
       CALL  SND         ;звуковой сигнал, имитирующий полет пули
       CALL  OCENKA      ;вычисление оценок результата стрельбы
                         ; и вывод их на экран
       LD    A,(23560)   ;выход из программы
       CP    " "
       JR    NZ,MAIN
       RET
; Подпрограмма оценки результата стрельбы
OCENKA LD    BC,(COL)    ;заносим в BC координаты выстрела
       LD    A,10        ;вертикальная координата центра «мишени»
       CP    B
       JR    C,BOT_Y
       SUB   B           ;пулевое отверстие находится в верхней
                         ; половине «мишени»
       JR    CONT1
BOT_Y  LD    A,B
       SUB   10          ;пулевое отверстие находится в нижней
                         ; половине «мишени»
CONT1  LD    B,A         ;в регистре B длина катета по Y
       LD    A,15        ;горизонтальная координата центра «мишени»
       CP    C
       JR    C,BIG_X
       SUB   C
       JR    CONT2
BIG_X  LD    A,C
       SUB   15
CONT2  LD    C,A         ;в регистре C длина катета по X
; Определяем длину гипотенузы прямоугольного треугольника
       LD    H,0
       LD    L,B
       LD    D,H
       LD    E,L
       PUSH  BC
       CALL  MULT        ;вычисляем квадрат величины Y
       LD    B,H
       LD    C,L
       POP   HL
       LD    H,0
       LD    D,H
       LD    E,L
       PUSH  BC
       CALL  MULT        ;вычисляем квадрат величины X
       POP   BC
       ADD   HL,BC
       CALL  SQR         ;определяем длину гипотенузы,
                         ; величину которой помещаем в пару HL
; По длине гипотенузы находим количество заработанных очков
;  в результате одного выстрела
       LD    C,0
       LD    A,L
       CP    11
       JR    NC,SUM
       LD    DE,D_SUM
       ADD   HL,DE
       LD    C,(HL)
; Вычисление трех оценочных характеристик стрельбы и их вывод на экран
SUM    PUSH  BC
       LD    DE,TXT1
       LD    BC,11
       CALL  8252
       POP   BC
       LD    HL,(SCORE)
       LD    B,0
       ADD   HL,BC
       LD    (SCORE),HL
       LD    B,H
       LD    C,L
       CALL  11563
       CALL  11747       ;печать общего количества заработанных
                         ; в результате стрельбы очков
       LD    DE,TXT2
       LD    BC,11
       CALL  8252
       LD    HL,KOL_W
       INC   (HL)
       LD    C,(HL)
       LD    B,0
       CALL  11563
       CALL  11747       ;печать количества произведенных
                         ; по «мишени» выстрелов
       LD    DE,TXT3
       LD    BC,10
       CALL  8252
       LD    HL,(SCORE)
       LD    D,H
       LD    E,L
       LD    A,(KOL_W)
       LD    L,A
       LD    H,0
       CALL  DIVIS
       LD    B,H
       LD    C,L
       CALL  11563
       JP    11747       ;печать среднего числа очков за один выстрел
MULT   .........
DIVIS  .........
SQR    .........
; Подпрограммы MISH, CIRC и другие, а также блоки TEXT и UDG,
;  на которые имеются ссылки в основной программе, описывались нами
;  в первом варианте программы МИШЕНЬ
MISH   .........
CIRC   .........
RND    .........
SND    .........
WAIT   .........
; Данные для мишени
TEXT   .........
LENTXT EQU   $-TEXT
; Данные для пулевых отверстий
UDG    .........
; Данные оценок
D_SUM  DEFB  10,10,8,8,8,6,6,6,4,4,4
TXT1   DEFB  22,21,0,16,4
       DEFM  "SCORE:"
TXT2   DEFB  22,21,12,16,1
       DEFM  "SHOTS:"
TXT3   DEFB  22,21,22,16,2
       DEFM  "MEAN:"
; Переменные для оценок
SCORE  DEFW  0
KOL_W  DEFB  0
COL    DEFB  0
ROW    DEFB  0


РАБОТА С КАЛЬКУЛЯТОРОМ


Как вы уже могли заметить, в игровых программах в большинстве случаев вполне можно обойтись только целыми числами. Но иногда все же приходится привлекать к расчетам и вещественные величины, особенно в блоке оценки игровой ситуации (это вы могли заметить в программе МИШЕНЬ: при расчете среднего арифметического явно требуются дробные числа). В свое время мы говорили, что для подобных вычислений можно обращаться к программе ПЗУ, выполняющей различные математические операции именно с такими числами и именуемой калькулятором. Эта программа расположена по адресу 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Замена элементовXY 
3ВычитаниеY - X  
4УмножениеY ґ X  
5ДелениеY / X  
6Возведение в степеньYX  
15СложениеY + X  
27Изменение знакаY-X 
39Целая часть числаYINT X 
40Квадратный кореньYSQR X 
41Знак числаYSGN X 
42Абсолютная величинаYABS X 
49Копирование стекаYXX
56Конец расчетовYX 
88Округление числаYINT(X+.5) 
160Дописать 0YX0
161Дописать 1YX1
162Дописать 0.5YX.5
163Дописать PI/2YXPI / 2
164Дописать 10YX10

В качестве иллюстрации приведем программку, вычисляющую выражение 823ґ5503/(32-17) и выводящую результат на экран. При выполнении расчетов необходимо внимательно следить за очередностью выполнения операций, поэтому прежде нужно продумать порядок занесения чисел в стек (помните, что калькулятор имеет доступ только к величинам, находящимся на вершине стека). Поскольку в данном случае первым должно выполняться действие в скобках, а умножение и деление имеют одинаковый приоритет, то укладывать числа в стек будем в той же последовательности, в которой они встречаются в выражении, чтобы калькулятор мог выбирать их в обратном порядке:

       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


КОНТРОЛЬ ВРЕМЕНИ (РАБОТА С ПРЕРЫВАНИЯМИ)


Прерывания с полным правом можно отнести к наиболее мощным и интересным ресурсам компьютера. К сожалению, работа с ними из Бейсика абсолютно невозможна и именно поэтому данный вопрос для многих из вас может оказаться совершенно новым и неизведанным. К настоящему моменту вы уже, должно быть, достаточно освоились с ассемблером и, надеемся, неплохо представляете, как работает микропроцессор, что дает возможность приступить наконец к изучению этой серьезной темы.

Для начала выясним, что же собой представляют прерывания. Попробуем, не вдаваясь в конструкторские тонкости, объяснить принцип этого явления просто «на пальцах». Когда вы находитесь в редакторе Бейсика или GENS и размышляете над очередной строкой программы, компьютер не торопит вас и терпеливо ожидает нажатия той или иной клавиши. Может даже показаться, что микропроцессор в это время и вовсе не работает. Но, как вы уже знаете, это не так. Просто выполняется некоторая часть программы, аналогичная процедуре WAIT, описанной ранее: в цикле опрашивается системная переменная LAST_K и когда вы нажимаете какую-то клавишу, код ее появляется в ячейке 23560. Но, спрашивается, откуда он там берется? Программа ведь только читает ее значение, никак не модифицируя ее содержимое. А разрешается эта загадка довольно просто. Дело в том, что 50 раз в секунду микропроцессор отвлекается от основной программы и переключается на выполнение специальной процедуры обработки прерываний, расположенной по адресу 56, словно бы встретив команду RST 56 или CALL 56, только переход этот происходит не программным, а аппаратным путем. У процедуры 56 есть две основных задачи: опрос клавиатуры и изменение текущего значения таймера (системная переменная FRAMES - 23672/73/74). Результаты опроса клавиш также заносятся в область системных переменных, в частности, код нажатой клавиши помещается в LAST_K. После выхода из прерывания микропроцессор как ни в чем не бывало продолжает выполнять основную программу. В результате получается довольно интересный эффект: создается впечатление, будто бы параллельно работают два микропроцессора, каждый из которых выполняет свою независимую задачу.

Все это прекрасно, но какую пользу для себя мы можем из этого извлечь? Ведь в ПЗУ ничего не изменишь. Действительно, от прерываний программистам было бы не много проку, если бы невозможно было переопределять адрес процедуры для их обработки. Мы уже говорили о существовании регистра I, называемого регистром вектора прерываний, а сейчас расскажем, какую роль он выполняет в программах, использующих собственные прерывания.

Прежде всего вам нужно знать, что существует три различных режима прерываний. Они обозначаются цифрами от 0 до 2. Стандартный режим имеет номер 1, и о нем мы уже кое-что сказали. Нулевой режим нам не интересен, поскольку на практике он ничем не отличается от первого (именно, на практике, потому что на самом деле имеются существенные различия, но в ZX Spectrum они не реализованы). А вот о втором режиме нужно поговорить более основательно.

Сначала скажем несколько слов о том, как он работает и что при этом происходит в компьютере. С приходом сигнала прерываний микропроцессор определяет адрес указателя на процедуру обработки прерываний. Он составляется из байта, считанного с шины данных (младший), который, собственно, и называется вектором прерывания и содержимого регистра I (старший байт адреса). Затем на адресную шину переписывается значение полученного указателя, но предварительно прежнее состояние шины адреса заносится в стек. Таким образом, совершается действие, аналогичное выполнению команды микропроцессора CALL. Поскольку в ZX Spectrum вектор прерывания, как правило, равен 255, то на практике адрес указателя может быть определен только регистром I. Для этого его значение нужно умножить на 256 и прибавить 255.

Для установки нового обработчика прерываний нужно выполнить ряд действий. Перечислим их в том порядке, в котором они должны производиться:

  1. Запретить прерывания, так как есть вероятность того, что сигнал прерываний придет во время установки, а это может привести к нежелательным последствиям. Достигается это выполнением команды микропроцессора DI.
  2. Записать в память по рассчитанному заранее адресу указатель на процедуру обработки прерываний (то есть адрес этой процедуры).
  3. Задать в регистре вектора прерываний I старший байт адреса указателя на обработчик.
  4. Установить командой IM 2 второй режим прерываний.
  5. Вновь разрешить прерывания командой EI.
Естественно, что к этому моменту сама процедура обработки прерываний должна иметься в памяти. Для возврата к стандартному режиму обработки прерываний нужно выполнить похожие действия:
  1. Запретить прерывания.
  2. Не помешает восстановить значение регистра I, записав в него число 63.
  3. Назначить командой IM 1 первый режим прерываний.
  4. Разрешить прерывания.
Несколько подробнее нужно остановиться на втором и третьем пунктах установки прерываний. Предположим, что процедура-обработчик находится по адресу 60000 (#EA60) и память, начиная с адреса 65000, никак в программе не используется. Значит указатель можно поместить именно в эту область. Для регистра I в этом случае можно выбрать одно из двух значений: 253 или 254. Тогда для размещения указателя можно использовать либо адреса 65023/65024 (253ґ256+255/256) либо 65279/65280 (254ґ256+255/256). Например, при I равном 254 запишем по адресу 65279 младший байт адреса обработчика - #60, а в 65280 поместим старший байт - #EA.

Однако нужно учитывать, что некоторые внешние устройства могут изменять значение вектора прерывания. Кроме того, если ваш Speccy сработан не слишком добросовестным производителем, то вектор прерывания иногда может скакать совершенно произвольным и непредсказуемым образом. Принимая это во внимание, даже во многих фирменных играх используется несколько иной подход. Вместо записи двух байтов по определенному адресу выстраивается целая таблица размером как минимум 257 байт с таким расчетом, чтобы при любом значении вектора прерываний считывался один и тот же адрес. Понятно, что для этого все байты таблицы должны быть одинаковыми. Это несколько осложняет установку прерывания и требует больше памяти, но зато значительно увеличивает надежность работы программы.

Наиболее удачным для такой таблицы представляется байт 255 (#FF). В этом случае обработчик прерываний должен находиться по адресу 65535 (#FFFF). На первый взгляд может показаться странным выбор такого адреса, ведь остается всего один байт! Но и этого единственного байта оказывается достаточным, если в него поместить код команды JR. Следующий байт, находящийся уже по адресу 0, укажет смещение относительного перехода. По нулевому адресу в ПЗУ записан код команды DI (#F3), поэтому полностью команда будет выглядеть как JR 65524. Далее в ячейке 65524 можно разместить уже более «длинную» команду JP address и заданный в ней адрес может быть совершенно произвольным.

Приведем пример такой подпрограммы установки прерываний:

IMON   LD    A,24        ;код команды JR
       LD    (65535),A
       LD    A,195       ;код команды JP
       LD    (65524),A
       LD    (65525),HL  ;в HL - адрес обработчика прерываний
       LD    HL,#FE00    ;построение таблицы для векторов прерываний
       LD    DE,#FE01
       LD    BC,256      ;размер таблицы минус 1
       LD    (HL),#FF    ;адрес перехода #FFFF (65535)
       LD    A,H         ;запоминаем старший байт адреса таблицы
       LDIR              ;заполняем таблицу
       DI                ;запрещаем прерывания на время
                         ; установки второго режима
       LD    I,A         ;задаем в регистре I старший байт адреса
                         ; таблицы для векторов прерываний
       IM    2           ;назначаем второй режим прерываний
       EI                ;разрешаем прерывания
       RET
Перед обращением к ней в регистровой паре HL необходимо указать адрес соответствующей процедуры обработки прерываний. Учтите, что в области памяти, начиная с адреса 65024, менять что-либо не желательно. Если все же возникнет такая необходимость, убедитесь прежде, что своими действиями вы не затроните установленные процедурой байты.

Подпрограмма восстановления первого режима выглядит заметно проще и в комментариях уже не нуждается:

IMOFF  DI
       LD    A,63
       LD    I,A
       IM    1
       EI
       RET
При составлении процедуры обработки прерываний нужно придерживаться определенных правил. Во-первых, написанная вами подпрограмма должна выполняться за достаточно короткий промежуток времени. Желательно, чтобы ее быстродействие было сопоставимо с «пульсом» прерываний, то есть чтобы ее продолжительность не превышала 1/50 секунды. Это правило не является обязательным, но в противном случае трудно будет получить эффект «параллельности» процессов. Во-вторых, и это уже совершенно необходимо, все регистры, которые могут изменить свое значение в вашей процедуре, должны быть сохранены на входе и восстановлены перед выходом. Это же относится и к любым переменным, используемым не только в прерывании, но и в основной программе. В связи с этим не рекомендуется обращаться к подпрограммам ПЗУ, по крайней мере, до тех пор, пока вы не знаете совершенно точно, какие в них используются регистры и какие системные переменные при этом могут быть изменены. Вызов подпрограмм ПЗУ не желателен еще и потому, что некоторые из них разрешают прерывания, что совершенно недопустимо во избежание рекурсии (т. е. самовызова) обработчика, который должен работать при запрещенных прерываниях. Однако использовать команду DI в самом начале процедуры не обязательно, так как это действие выполняется автоматически и вам нужно только позаботиться о разрешении прерываний перед выходом.

Если вы не хотите лишаться возможностей, предоставляемых стандартной процедурой обработки прерываний, можете завершать свою подпрограмму командой JP 56. А при использовании прерываний в бейсик-программах без этого просто не обойтись, иначе клавиатура окажется заблокирована. В общем случае обработчик прерываний может иметь такой вид:

INTERR PUSH  AF
       PUSH  BC
       PUSH  DE
       PUSH  HL
       .......
       POP   HL
       POP   DE
       POP   BC
       POP   AF
       JP    56
В заключение этого раздела приведем процедуру, отсчитывающую секунды, остающиеся до окончания игры. Эта процедура может вызываться как из машинных кодов, так и из программы на Бейсике. В верхнем левом углу экрана постоянно будет находиться число, уменьшающееся на единицу по истечении каждой секунды. Для применения этой подпрограммы в реальной игре вам достаточно изменить адрес экранной области, куда будут выводиться числа и, возможно, начальное значение времени, отводимое на игру. Момент истечения времени определяется содержимым ячейки по смещению ORG+4. Если ее значение окажется не равным 0, значит игра закончилась.
       ORG   60000
       JR    INITI
       JR    IMOFF
OUTTIM DEFB  0
INITI  LD    HL,D_TIM0
       LD    DE,D_TIME
       LD    BC,3
       LDIR
       XOR   A
       LD    (OUTTIM),A
       LD    HL,TIM0
       LD    (HL),50
       INC   HL
       LD    (HL),A
       INC   HL
       LD    (HL),A
       LD    HL,TIMER    ;установка прерывания
IMON   .........
IMOFF  .........
TIMER  PUSH  AF
       PUSH  BC
       PUSH  DE
       PUSH  HL
       CALL  CLOCK
       POP   HL
       POP   DE
       POP   BC
       POP   AF
       JP    56
TIM0   DEFB  50          ;количество прерываний в секунду
TIM1   DEFB  0           ;время «проворота» третьего символа
TIM2   DEFB  0           ;время «проворота» второго символа
TIM3   DEFB  0           ;время «проворота» первого символа
D_TIM0 DEFM  "150"       ;символы, выводимые на экран
D_TIME DEFM  "150"       ;начальное значение времени
; Проверка необходимости изменения текущего времени
CLOCK  LD    HL,TIM0
       DEC   (HL)
       JR    NZ,CLOCK1
       LD    (HL),50
; Уменьшение секунд
       LD    A,8         ;символ «проворачивается» за 8
       LD    (TIM1),A    ; тактов прерывания
       LD    HL,D_TIME+2
       LD    A,(HL)
       DEC   (HL)
       CP    "0"
       JR    NZ,CLOCK1
       LD    (HL),"9"
; Уменьшение десятков секунд
       LD    A,8
       LD    (TIM2),A
       DEC   HL
       LD    A,(HL)
       DEC   (HL)
       CP    "0"
       JR    NZ,CLOCK1
       LD    (HL),"9"
; Уменьшение сотен секунд
       LD    A,8
       LD    (TIM3),A
       DEC   HL
       LD    A,(HL)
       DEC   (HL)
       CP    "0"
       JR    Z,ENDTIM    ;если время истекло
CLOCK1 LD    DE,#401D    ;адрес экранной области
       LD    A,(D_TIME)  ;первый символ - сотни секунд
       LD    HL,TIM3
       CALL  PRNT
       LD    A,(D_TIME+1) ;второй символ - десятки секунд
       CALL  PRNT
       LD    A,(D_TIME+2) ;третий символ - секунды
; Печать символов с учетом их «проворота»
PRNT   PUSH  HL
       LD    L,A         ;расчет адреса символа
       LD    H,0         ; в стандартном наборе
       ADD   HL,HL
       ADD   HL,HL
       ADD   HL,HL
       LD    A,60
       ADD   A,H
       LD    H,A
       EX    (SP),HL
       LD    A,(HL)
       LD    C,A
       AND   A
       JR    Z,PRNT1     ;если символ «проворачивать» не нужно
       DEC   (HL)
       EX    (SP),HL
       NEG               ;пересчет адреса символьного набора для
                         ; создания иллюзии «проворота» цифры
       LD    B,A
       LD    A,L
       SUB   B
       LD    L,A
       JR    PRNT2
PRNT1  EX    (SP),HL
PRNT2  LD    B,8
       PUSH  DE
PRNT3  LD    A,(HL)
       LD    (DE),A
       INC   HL
       INC   D
       LD    A,C
       AND   A
       JR    Z,PRNT4
; После цифры 9 при «провороте» должен появляться 0, а не двоеточие
       LD    A,L
       CP    208         ;адрес символа :
       JR    C,PRNT4
       SUB   80          ;возвращаемся к адресу символа 0
       LD    L,A
PRNT4  DJNZ  PRNT3
       POP   DE
       POP   HL
       INC   DE
       DEC   HL
       RET
; Истечение времени - выключение 2-го режима прерываний
ENDTIM POP   HL          ;восстановление значения указателя стека
                         ; после команды CALL CLOCK
       CALL  IMOFF
       LD    A,1         ;установка флага истечения времени
       LD    (OUTTIM),A
       POP   HL          ;восстановление регистров
       POP   DE
       POP   BC
       POP   AF
       RET

Глава 10
Глава 8
Оглавление