rev |
line source |
pascal@180
|
1 ; Boot-Man
|
pascal@180
|
2 ;
|
pascal@180
|
3 ; (c) 2019 Guido van den Heuvel
|
pascal@180
|
4 ;
|
pascal@180
|
5 ; Boot-Man is a Pac-Man clone that fits (snugly) inside the Master Boot Record of a USB stick.
|
pascal@180
|
6 ; A USB stick with Boot-Man on it boots into the game (hence the name). Unfortunately, however,
|
pascal@180
|
7 ; Boot-Man leaves no room in the MBR for a partition table, which means that a USB stick with Boot-Man
|
pascal@180
|
8 ; in its MBR cannot be used to store data. In fact, Windows does not recognize a USB stick with
|
pascal@180
|
9 ; Boot-Man in its MBR as a valid storage medium.
|
pascal@180
|
10 ;
|
pascal@180
|
11 ; Controls of the game: you control Boot-Man using the arrow keys. No other user input is necessary. Some other
|
pascal@180
|
12 ; keys can also be used to control Boot-Man, this is a side effect of coding my own keyboard handler in
|
pascal@180
|
13 ; just a few bytes. There simply wasn't room for checking the validity of every key press.
|
pascal@180
|
14 ;
|
pascal@180
|
15 ; The game starts automatically, and when Boot-Man dies, a new game starts automatically after any key hit.
|
pascal@180
|
16 ;
|
pascal@180
|
17 ;
|
pascal@180
|
18 ; I've had to take a couple of liberties with the original Pac-Man game to fit Boot-Man inside the 510
|
pascal@180
|
19 ; bytes available in the MBR:
|
pascal@180
|
20 ;
|
pascal@180
|
21 ; * The ghosts start in the four corners of the maze, they do not emerge from a central cage like in the original
|
pascal@180
|
22 ;
|
pascal@180
|
23 ; * There's just a single level. If you finish the level, the game keeps running with an empty maze. While
|
pascal@180
|
24 ; it is rather difficult to finish the game (which is intentional, because of the single level), it is possible.
|
pascal@180
|
25 ;
|
pascal@180
|
26 ; * Boot-Man only has 1 life. If Boot-Man dies, another game is started automatically.
|
pascal@180
|
27 ;
|
pascal@180
|
28 ; * Power pills function differently from the original. When Boot-Man eats a power pill, all ghosts become
|
pascal@180
|
29 ; ethereal (represented in game by just their eyes being visible) and cease to chase Boot-Man. While ethereal,
|
pascal@180
|
30 ; Boot-Man can walk through ghosts with no ill effects. While I would really like to include the "ghost hunting"
|
pascal@180
|
31 ; from the original, which I consider to be an iconic part of the game, this simply isn't possible in the little
|
pascal@180
|
32 ; space available.
|
pascal@180
|
33 ;
|
pascal@180
|
34 ; * There's no score, and no fruit to get bonus points from.
|
pascal@180
|
35 ;
|
pascal@180
|
36 ; * All ghosts, as well as Boot-Man itself, have the same, constant movement speed. In the original, the ghosts
|
pascal@180
|
37 ; run at higher speeds than Pac-Man, while Pac-Man gets delayed slightly when eating and ghosts get delayed when moving
|
pascal@180
|
38 ; through the tunnel connecting both sides of the maze. This leads to very interesting dynamics and strategies
|
pascal@180
|
39 ; in the original that Boot-Man, by necessity, lacks.
|
pascal@180
|
40 ;
|
pascal@180
|
41 ;
|
pascal@180
|
42 ; Boot-Man runs in text mode. It uses some of the graphical characters found in IBM codepage 437 for its objects:
|
pascal@180
|
43 ; - Boot-Man itself is represented by the smiley face (☻), which is character 0x02 in the IBM charset
|
pascal@180
|
44 ; - The Ghosts are represented by the infinity symbol (∞), which is character 0xec. These represent
|
pascal@180
|
45 ; a ghost's eyes, with the ghost's body being represented simply by putting the character on a
|
pascal@180
|
46 ; coloured background
|
pascal@180
|
47 ; - The dots that represent Boot-Man's food are represented by the bullet character (•),
|
pascal@180
|
48 ; which is character 0xf9
|
pascal@180
|
49 ; - The power pills with which Boot-Man gains extra powers are represented by the diamond (♦),
|
pascal@180
|
50 ; which is character 0x04
|
pascal@180
|
51 ; - The walls of the maze are represented by the full block character (█), which is character 0xdb
|
pascal@180
|
52 ;
|
pascal@180
|
53 ; Boot-Man runs off BIOS clock. It should therefore run at the same speed on all PCs. The code is quite heavily
|
pascal@180
|
54 ; optimized for size, so code quality is questionable at best, and downright atrocious at worst.
|
pascal@180
|
55
|
pascal@186
|
56 ;%define WaitForAnyKey ; +9 bytes
|
pascal@186
|
57 %define MDAsupport ; +7 bytes
|
pascal@186
|
58 %define MDAinverse ; +5 bytes
|
pascal@186
|
59 %define MDA_CGA40 ; +17 bytes
|
pascal@180
|
60
|
pascal@180
|
61 cpu 8086 ; Boot-Man runs on the original IBM PC with a CGA card.
|
pascal@180
|
62 bits 16 ; Boot-Man runs in Real Mode. I am assuming that the BIOS leaves the CPU is Real Mode.
|
pascal@180
|
63 ; This is true for the vast majority of PC systems. If your system's BIOS
|
pascal@180
|
64 ; switches to Protected Mode or Long Mode during the boot process, Boot-Man
|
pascal@180
|
65 ; won't run on your machine.
|
pascal@186
|
66 org 0x600
|
pascal@180
|
67
|
pascal@186
|
68 %define COL40 40
|
pascal@186
|
69 %define COL80 80
|
pascal@186
|
70
|
pascal@180
|
71 start:
|
pascal@180
|
72 call copy ; Can run as bootsector or DOS COM file
|
pascal@180
|
73
|
pascal@180
|
74 moved:
|
pascal@180
|
75 push cs
|
pascal@180
|
76 pop ss
|
pascal@186
|
77 xor sp, sp ; Set up the stack.
|
pascal@180
|
78
|
pascal@186
|
79 mov ax, 1 ; int 0x10 / ah = 0: Switch video mode. Switch mode to 40x25 characters (al = 1).
|
pascal@180
|
80 int 0x10 ; In this mode, characters are approximately square, which means that horizontal
|
pascal@180
|
81 ; and vertical movement speeds are almost the same.
|
pascal@186
|
82 mov ax, 0xb800
|
pascal@186
|
83 %ifdef MDAsupport
|
pascal@186
|
84 %define LeftCorner (COL80*2*24+COL80-32)
|
pascal@186
|
85 %define PreviousLine (COL80*2+32)
|
pascal@186
|
86 %define Columns COL80
|
pascal@186
|
87 %define Margin ((COL80-32)/2)
|
pascal@186
|
88 %ifdef MDA_CGA40
|
pascal@186
|
89 mov dx, PreviousLine
|
pascal@186
|
90 %undef PreviousLine
|
pascal@186
|
91 %define PreviousLine dx
|
pascal@186
|
92 %endif
|
pascal@186
|
93 %else
|
pascal@186
|
94 %define LeftCorner (COL40*2*24+COL40-32)
|
pascal@186
|
95 %define PreviousLine (COL40*2+32)
|
pascal@186
|
96 %define Columns COL40
|
pascal@186
|
97 %define Margin ((COL40-32)/2)
|
pascal@186
|
98 %endif
|
pascal@186
|
99 initES:
|
pascal@186
|
100 mov es, ax ; Set up the es segment to point to video RAM
|
pascal@186
|
101 mov si, maze ; The first byte of the bit array containing the maze
|
pascal@186
|
102 %define ghost_n_incr 0x2f10
|
pascal@186
|
103 %ifdef MDAsupport
|
pascal@186
|
104 mov ah, 0xb0
|
pascal@186
|
105 %ifdef MDAinverse
|
pascal@186
|
106 %define ghost_attr 0x70
|
pascal@186
|
107 %define incr_attr 0x80
|
pascal@186
|
108 %undef ghost_n_incr
|
pascal@186
|
109 %define ghost_n_incr (ghost_attr*256 + incr_attr)
|
pascal@186
|
110 xor word [si + ghost_attr_patch - maze], ghost_n_incr^0x2f10
|
pascal@186
|
111 %endif
|
pascal@186
|
112 %ifdef MDA_CGA40
|
pascal@186
|
113 ; xor dl, (COL80*2+32)^(COL40*2+32)
|
pascal@186
|
114 xor dl, ah
|
pascal@186
|
115 xor word [si + LeftCorner_patch - maze], LeftCorner^(COL40*2*24+COL40-32)
|
pascal@186
|
116 xor byte [si + Columns_patch - maze], Columns^COL40
|
pascal@186
|
117 xor byte [si + Margin_patch - maze], Margin^((COL40-32)/2)
|
pascal@186
|
118 %endif
|
pascal@186
|
119 scasw
|
pascal@186
|
120 jb initES
|
pascal@186
|
121 %endif
|
pascal@180
|
122
|
pascal@180
|
123 mov ax, 0x0101 ; int 0x10 / ah = 1: Determine shape of hardware cursor. al = 1 avoid AMI BIOS bug.
|
pascal@180
|
124 mov ch, 0x20 ; With cx = 0x2000, this removes the hardware cursor from the screen altogether
|
pascal@180
|
125 int 0x10
|
pascal@180
|
126
|
pascal@186
|
127 %define ghost_color 0x2fec
|
pascal@186
|
128 %define color_increment 0x10
|
pascal@180
|
129
|
pascal@180
|
130
|
pascal@180
|
131 ;-----------------------------------------------------------------------------------------------------
|
pascal@180
|
132 ; buildmaze: builds the maze. The maze is stored in memory as a bit array, with 1 representing a wall
|
pascal@180
|
133 ; and 0 representing a food dot. Since the maze is left-right symmetrical, only half of the
|
pascal@180
|
134 ; maze is stored in memory. The positions of the power pills is hard-coded in the code.
|
pascal@180
|
135 ; Adding the power pills to the bit array would have necessitated 2 bits for every
|
pascal@180
|
136 ; character, increasing its size drastically.
|
pascal@180
|
137 ;
|
pascal@180
|
138 ; Both sides of the maze are drawn simultaneously. The left part is drawn left to right,
|
pascal@180
|
139 ; while the right part is drawn right to left. For efficiency reasons, the entire maze
|
pascal@180
|
140 ; is built from the bottom up. Therefore, the maze is stored upside down in memory
|
pascal@180
|
141 ;-----------------------------------------------------------------------------------------------------
|
pascal@186
|
142 ;buildmaze:
|
pascal@186
|
143 mov di, LeftCorner ; Lower left corner of maze in video ram
|
pascal@186
|
144 LeftCorner_patch equ $ - 2
|
pascal@186
|
145 mov cx, 34 ; Lines count to the lower left powerpill
|
pascal@180
|
146 .maze_outerloop:
|
pascal@186
|
147 mov bx, 0x003c ; The distance between a character in the maze and its
|
pascal@180
|
148 ; symmetric counterpart. Also functions as loop counter
|
pascal@180
|
149 lodsw ; Read 16 bits from the bit array, which represents one
|
pascal@186
|
150 xchg ax, bp ; 32 character-wide row of the maze
|
pascal@180
|
151 .maze_innerloop:
|
pascal@186
|
152 add bp, bp ; shift out a single bit to determine whether a wall or dot must be shown
|
pascal@180
|
153 mov ax, 0x01db ; Assume it is a wall character (0x01: blue; 0xdb: full solid block)
|
pascal@180
|
154 jc .draw ; Draw the character if a 1 was shifted out
|
pascal@180
|
155 mov ax, 0x0ff9 ; otherwise, assume a food character (0x0f: white; x0f9: bullet)
|
pascal@186
|
156 loop .draw ; See if instead of food we need to draw a power pill
|
pascal@186
|
157 mov cl, 125 ; Update powerpill address to draw remaining powerpills
|
pascal@180
|
158 mov al, 0x04 ; powerpill character (0x04: diamond - no need to set up colour again)
|
pascal@180
|
159 .draw:
|
pascal@180
|
160 stosw ; Store character + colour in video ram
|
pascal@186
|
161 mov [es:bx+di], ax ; Go to its symmetric counterpart and store it as well
|
pascal@186
|
162 sub bx, 4 ; Update the distance between the two sides of the maze
|
pascal@180
|
163 jns .maze_innerloop ; As long as the distance between the two halves is positive, we continue
|
pascal@180
|
164
|
pascal@186
|
165 sub di, PreviousLine ; Go to the previous line on the screen in video RAM.
|
pascal@180
|
166 jns .maze_outerloop ; Keep going as long as this line is on screen.
|
pascal@180
|
167
|
pascal@186
|
168 ;mov si, bootman_data ; Use si as a pointer to the game data. This reduces byte count of the code:
|
pascal@186
|
169 ; mov reg, [address] is a 4 byte instruction, while mov reg, [si] only has 2.
|
pascal@186
|
170
|
pascal@180
|
171 ;-----------------------------------------------------------------------------------------------------
|
pascal@180
|
172 ; game_loop: The main loop of the game. Tied to BIOS clock (which fires 18x per
|
pascal@180
|
173 ; second by default), to ensure that the game runs at the same speed on all machines
|
pascal@180
|
174 ;
|
pascal@180
|
175 ; The code first updates Boot-Man's position according to its movement direction
|
pascal@180
|
176 ; and keyboard input. Then the ghost AI is run, to determine the ghosts' movement
|
pascal@180
|
177 ; direction and update their position. Finally, Boot-Man and the ghosts are drawn
|
pascal@180
|
178 ; in their new positions. Collisions between Boot-Man and the ghosts are checked
|
pascal@180
|
179 ; before and after ghost movement. We need to detect for collisions twice, because
|
pascal@180
|
180 ; if you only check once, Boot-Man can change position with a ghost without colliding
|
pascal@180
|
181 ; (in the original, collisions are checked only once, and as a consequence, it is
|
pascal@180
|
182 ; possible in some circumstances to actually move Pac-Man through a ghost).
|
pascal@180
|
183 ;-----------------------------------------------------------------------------------------------------
|
pascal@186
|
184
|
pascal@180
|
185 game_loop:
|
pascal@180
|
186 hlt ; Wait for clock interrupt
|
pascal@186
|
187 mov ah, 0x00
|
pascal@180
|
188 int 0x1a ; BIOS clock read
|
pascal@186
|
189 xchg ax, dx
|
pascal@180
|
190 old_time equ $ + 1
|
pascal@186
|
191 cmp al, 0x12 ; Wait for time change
|
pascal@180
|
192 je game_loop
|
pascal@186
|
193 mov [old_time], al ; Save new time
|
pascal@180
|
194
|
pascal@186
|
195 mov bx, 3
|
pascal@186
|
196 mov ah, 0x01 ; BIOS Key available
|
pascal@180
|
197 int 0x16
|
pascal@180
|
198 cbw ; BIOS Read Key
|
pascal@180
|
199 je .no_key
|
pascal@180
|
200 int 0x16
|
pascal@186
|
201 mov al, ah
|
pascal@184
|
202 dec ah ; Escape ?
|
pascal@184
|
203 jne .convert_scancode
|
pascal@186
|
204 ;.exit:
|
pascal@186
|
205 xchg ax, bx ; int 0x10 / ax = 3: Switch video mode. Switch mode to 80x25 characters
|
pascal@184
|
206 int 0x10 ; Reset screen
|
pascal@184
|
207 int 0x20 ; Exit to DOS...
|
pascal@184
|
208 int 0x19 ; ...or reboot
|
pascal@180
|
209
|
pascal@180
|
210 ; This code converts al from scancode to movement direction.
|
pascal@180
|
211 ; Input: 0x48 (up), 0x4b (right), 0x50 (down), 0x4d (left)
|
pascal@180
|
212 ; Output: 0xce (up), 0xca (right), 0xc6 (down), 0xc2 (left)
|
pascal@186
|
213 ; fe xx: dec dh dec dl inc dh inc dl
|
pascal@180
|
214
|
pascal@184
|
215 .convert_scancode:
|
pascal@180
|
216 sub al, 0x47 ; 0x01 0x04 0x09 0x06
|
pascal@186
|
217 and al, 0xfe ; 0x00 0x04 0x08 0x06
|
pascal@180
|
218 cmp al, 9
|
pascal@180
|
219 jnc .no_key ; if al >= 0x50, ignore scancode;
|
pascal@180
|
220 ; this includes key release events
|
pascal@180
|
221 neg al ; 0x00 0xfc 0xf8 0xfa
|
pascal@186
|
222 add al, 0xce ; 0xce 0xca 0xc6 0xc8
|
pascal@186
|
223 test al, 2
|
pascal@180
|
224 jne .translated
|
pascal@186
|
225 mov al, 0xc2
|
pascal@180
|
226 .translated:
|
pascal@180
|
227 cmp al, [si + 2] ; If the new direction is the same as the current direction, ignore it
|
pascal@180
|
228 jz .no_key
|
pascal@186
|
229 mov [bx+si], al ; Set new direction to the direction derived from the keyboard input
|
pascal@180
|
230 .no_key:
|
pascal@180
|
231 dec byte [si + pace_offset] ; Decrease the pace counter. The pace counter determines the overall
|
pascal@180
|
232 ; speed of the game. We found that moving at a speed of 6 per second
|
pascal@180
|
233 ; gives good speed and control, so we use a counter to only move
|
pascal@180
|
234 ; once for every three times that the interrupt fires.
|
pascal@180
|
235 ; We also use the pace counter to include longer delays at game start
|
pascal@180
|
236 ; and after Boot-Man dies, by intitalizing the counter with higher values.
|
pascal@180
|
237 jnz game_loop ; If the pace counter is not 0, we immediately finish.
|
pascal@180
|
238
|
pascal@186
|
239 mov byte [si + pace_offset], bl ; Reset the pace counter to 3.
|
pascal@180
|
240 ;-----------------------------------------------------------------------------------------------------
|
pascal@180
|
241 ; Move Boot-Man
|
pascal@180
|
242 ;-----------------------------------------------------------------------------------------------------
|
pascal@180
|
243 call newpos_bootman ; Update dx to move 1 square in the direction indicated by al
|
pascal@180
|
244 ; newpos also checks for collisions with walls (in which case ZF is set)
|
pascal@180
|
245 jz .nodirchange ; ZF indicates that new position collides with wall. We therefore try to keep
|
pascal@180
|
246 ; moving in the current direction instead.
|
pascal@186
|
247 mov [bx+si], al ; If there's no collision, update the current movement direction
|
pascal@180
|
248 .nodirchange:
|
pascal@180
|
249 call newpos_bootman ; Update dx to move 1 square in direction al
|
pascal@180
|
250 jz .endbootman ; If there's a wall there, do nothing
|
pascal@186
|
251 ;.move:
|
pascal@180
|
252 mov ax, 0x0f20 ; Prepare to remove Boot-Man from screen, before drawing it in the new position
|
pascal@180
|
253 ; 0x0f = black background, white foreground; 0x20 = space character
|
pascal@186
|
254 cmp byte [es:di], ah ; Detect power pill (0x04)
|
pascal@186
|
255 ja .nopowerpill
|
pascal@180
|
256 mov byte [si + timer_offset], al; If Boot-Man just ate a power pill, set up the ghost timer to 0x20. We use al here
|
pascal@180
|
257 ; as it accidentally contains 0x20, and this is one byte shorter than having an
|
pascal@180
|
258 ; explicit constant.
|
pascal@180
|
259 .nopowerpill:
|
pascal@186
|
260 xchg dx, [si + bm_offset_pos] ; Retrieve current position and store new position
|
pascal@180
|
261 call paint ; Actually remove Boot-Man from the screen
|
pascal@180
|
262 .endbootman:
|
pascal@180
|
263 ;-----------------------------------------------------------------------------------------------------
|
pascal@180
|
264 ; ghost AI and movement
|
pascal@180
|
265 ;
|
pascal@180
|
266 ; Determine the new movement direction for each ghost. Ghost movement direction is determined by
|
pascal@180
|
267 ; the following rule:
|
pascal@180
|
268 ; (1) Every ghost must keep moving
|
pascal@180
|
269 ; (2) It is forbidden for ghosts to suddenly start moving backwards. Unless Boot-Man just consumed
|
pascal@180
|
270 ; a powerpill, in which case ghosts are forbidden from continuing in the direction they were going
|
pascal@180
|
271 ; (3) Whenever a ghost has multiple movement options (i.e., it is at a crossroads), try moving 1 space
|
pascal@180
|
272 ; in each direction that is allowed, and calculate the distance to the target location after
|
pascal@180
|
273 ; that move. Choose the direction for which this distance is lowest as the new movement direction
|
pascal@180
|
274 ;
|
pascal@180
|
275 ; During normal movement, ghosts target a position that is related to the position of Boot-Man, as follows:
|
pascal@180
|
276 ;
|
pascal@180
|
277 ; number | ghost colour | target
|
pascal@180
|
278 ; -------+--------------+-------------------
|
pascal@180
|
279 ; 1 | purple | bootman's position
|
pascal@180
|
280 ; 2 | red | 4 squares below Boot-Man
|
pascal@180
|
281 ; 3 | cyan | 4 squares to the left of Boot-Man
|
pascal@180
|
282 ; 4 | green | 4 squares to the right of Boot-Man
|
pascal@180
|
283 ;
|
pascal@180
|
284 ; There's two different reasons for having slightly different AI for each ghost:
|
pascal@180
|
285 ; (1) If all ghosts have the same AI they tend to bunch together and stay there. With the current AI,
|
pascal@180
|
286 ; ghosts will sometimes bunch together, but they will split apart eventually
|
pascal@180
|
287 ; (2) With this setup, the ghosts tend to surround Boot-Man, making it harder for the player
|
pascal@180
|
288 ;
|
pascal@180
|
289 ; When Boot-Man picks up a power pill, a timer starts running, and ghosts become ethereal.
|
pascal@180
|
290 ; As long as the ghosts are ethereal, the
|
pascal@180
|
291 ; ghosts will not chase Boot-Man. Instead they will use the center of the big rectangular block
|
pascal@180
|
292 ; in the middle of the maze as their target. They cannot reach it, obviously, so the result is
|
pascal@180
|
293 ; that they will keep circling this block for as long as the timer runs.
|
pascal@180
|
294 ;
|
pascal@180
|
295 ; This AI is related to, but not the same as, the AI actually used in Pac-Man. The red Pac-Man ghost
|
pascal@180
|
296 ; uses Pac-Man itself as target, same as my purple ghost, while the pink Pac-Man ghost will
|
pascal@180
|
297 ; target 4 squares ahead of Pac-Man, in the direction Pac-Man is currently moving. The other ghosts'
|
pascal@180
|
298 ; movement is a bit more complex than that. I had to simplify the AI because of the limited code size.
|
pascal@180
|
299 ;-----------------------------------------------------------------------------------------------------
|
pascal@186
|
300 mov bl, 3 * gh_length + bm_length ; Set up offset to ghost data. With this, si + bx is a
|
pascal@180
|
301 ; pointer to the data from the last ghost. Also used as
|
pascal@180
|
302 ; loop counter to loop through all the ghosts
|
pascal@180
|
303 .ghost_ai_outer:
|
pascal@186
|
304 mov bp, si ; bp = minimum distance; start out at a big int
|
pascal@186
|
305 mov ah, [bx + si + gh_offset_dir] ; ah will become the forbidden movement direction. We start
|
pascal@180
|
306 ; with the current direction, which is forbidden if Boot-Man
|
pascal@180
|
307 ; just ate a power pill
|
pascal@180
|
308 cmp byte [si + timer_offset], 0x20 ; If timer_offset == 0x20, Boot-Man just picked up a power pill
|
pascal@180
|
309 jz .reverse ; so in that case we do not flip the direction.
|
pascal@180
|
310 xor ah, 8 ; Flip the current direction to obtain the forbidden direction in ah
|
pascal@180
|
311 .reverse:
|
pascal@180
|
312 mov al, 0xce ; al = current directions being tried. Doubles as loop counter
|
pascal@180
|
313 ; over all directions.
|
pascal@180
|
314 ; Values are the same as those used by the newpos routine
|
pascal@186
|
315 call test_collision ; dx = current ghost position
|
pascal@180
|
316 .ghost_ai_loop:
|
pascal@180
|
317 push dx
|
pascal@180
|
318 cmp al, ah ; If the current direction is the forbidden direction
|
pascal@180
|
319 jz .next ; we continue with the next direction
|
pascal@180
|
320 call newpos ; Update ghost position and check if it collides with a wall
|
pascal@180
|
321 jz .next ; if so, we continue with the next direction
|
pascal@186
|
322 push ax
|
pascal@186
|
323 mov ax, 0x0c10 ; Target position if ghosts are ethereal. Position 0x0c10
|
pascal@180
|
324 ; (x = 0x10, y = 0x0c) is in the center of the maze.
|
pascal@180
|
325 cmp byte [si + timer_offset], bh ; See if ghost timer runs. We compare with bh, which is known to be 0.
|
pascal@180
|
326 jnz .skip_target ; If ghost timer runs, we use the aforementioned target position
|
pascal@186
|
327 mov ax, [si + bm_offset_pos] ; Otherwise we use Boot-Man's current position,
|
pascal@186
|
328 add ax, [bx + si + gh_offset_focus] ; Updated with an offset that is different for each ghost
|
pascal@180
|
329 .skip_target:
|
pascal@180
|
330 ;-----------------------------------------------------------------------------------------------------
|
pascal@180
|
331 ; get_distance: Calculate distance between positions in cx (target position) and dx (ghost position)
|
pascal@180
|
332 ; This used to be a function, but I inlined it to save some space.
|
pascal@180
|
333 ; The square of the distance between the positions in cx and dx is calculated,
|
pascal@180
|
334 ; according to Pythagoras' theorem.
|
pascal@180
|
335 ;-----------------------------------------------------------------------------------------------------
|
pascal@186
|
336 sub al, dl ; after this, al contains the horizontal difference
|
pascal@186
|
337 sub ah, dh ; and ah the vertical difference
|
pascal@180
|
338
|
pascal@186
|
339 mov cl, ah
|
pascal@186
|
340 imul al
|
pascal@186
|
341 xchg ax, cx ; cx = square of horizontal difference
|
pascal@186
|
342 imul al ; ax = square of vertical difference
|
pascal@180
|
343 add cx, ax ; cx = distance squared between positions in cx and dx
|
pascal@180
|
344 pop ax
|
pascal@180
|
345
|
pascal@180
|
346 cmp cx, bp ; Compare this distance to the current minimum
|
pascal@180
|
347 jnc .next ; and if it is,
|
pascal@180
|
348 mov bp, cx ; update the minimum distance
|
pascal@186
|
349 mov [bx + si + gh_offset_dir], al ; set the movement direction to the current direction
|
pascal@180
|
350 mov [bx + si + gh_offset_pos], dx ; Store the new ghost position
|
pascal@180
|
351 .next:
|
pascal@180
|
352 pop dx ; Restore the current ghost position
|
pascal@180
|
353 sub al, 4 ; Update the current direction / loop counter
|
pascal@180
|
354 cmp al, 0xc2
|
pascal@180
|
355 jnc .ghost_ai_loop
|
pascal@180
|
356
|
pascal@180
|
357 mov ax, [bx + si + gh_offset_terrain] ; Remove the ghost in the old position from the screen
|
pascal@180
|
358 call paint ; by painting the terrain underneath that ghost that was
|
pascal@180
|
359 ; determined in the previous movement phase.
|
pascal@180
|
360 sub bx, gh_length ; Go to the next ghost,
|
pascal@180
|
361 jns .ghost_ai_outer ; and stop after the final ghost
|
pascal@180
|
362
|
pascal@180
|
363
|
pascal@180
|
364 .ghostterrain_loop: ; Second loop through all the ghosts, to determine terrain
|
pascal@180
|
365 ; underneath each one. This is used in the next movement phase
|
pascal@180
|
366 ; to restore the terrain underneath the ghosts.
|
pascal@180
|
367 ; Note that this "terrain storing" approach can trigger a bug
|
pascal@180
|
368 ; if Boot-Man and a ghost share a position. In that case
|
pascal@180
|
369 ; an extra Boot-Man character may be drawn on screen.
|
pascal@186
|
370 add bx, gh_length ; go to next ghost
|
pascal@186
|
371 call test_collision ; dx = current ghost position
|
pascal@180
|
372 call get_screenpos ; find the address in video ram of the updated ghost position,
|
pascal@180
|
373 mov ax, [es:di] ; store its content in ax
|
pascal@186
|
374 mov [bx + si + gh_offset_terrain], ax ; and copy it to ghostterrain
|
pascal@180
|
375 cmp bx, 3 * gh_length + bm_length ; and determine if it is the final ghost
|
pascal@180
|
376 jnz .ghostterrain_loop
|
pascal@180
|
377
|
pascal@186
|
378 mov ax, 0x0f00 ; Update ghost colour to black background, white eyes
|
pascal@186
|
379 ; Update difference between colours of successive ghosts. Value of 0x0
|
pascal@186
|
380 ; means all ghosts are the same colour when they are ethereal.
|
pascal@186
|
381 dec byte [si + timer_offset] ; Update ghost_timer to limit the period of the ghosts being ethereal
|
pascal@186
|
382 jns .ghostcolor
|
pascal@186
|
383 mov byte [si + timer_offset], bh ; Ghost timer was not running
|
pascal@186
|
384 mov ax, ghost_n_incr
|
pascal@186
|
385 ghost_attr_patch equ $ - 2
|
pascal@186
|
386
|
pascal@186
|
387 .ghostcolor:
|
pascal@186
|
388 mov cl, al ; cl = difference in colour between successive ghosts
|
pascal@186
|
389 mov al, ghost_color&255 ; 0xec = infinity symbol = ghost eyes
|
pascal@186
|
390 .ghostdraw: ; Draw the ghosts on the screen
|
pascal@186
|
391 mov dx, [bx + si + gh_offset_pos] ; dx = new ghost position
|
pascal@186
|
392 call paint ; show ghost in video ram
|
pascal@186
|
393 add ah, cl ; Update ghost colour.
|
pascal@186
|
394 sub bl, gh_length ; Loop over all ghosts
|
pascal@186
|
395 jns .ghostdraw ; until the final one.
|
pascal@180
|
396
|
pascal@186
|
397 mov ax, game_loop ; ret instruction will jump to game_loop
|
pascal@186
|
398 push ax
|
pascal@186
|
399 mov ax, word 0x0e02 ; Draw boot-man on the screen. 0x0e = black background, yellow foreground
|
pascal@186
|
400 ; 0x02 = smiley face
|
pascal@186
|
401 ;-----------------------------------------------------------------------------------------------------
|
pascal@186
|
402 ; paint: paints a character on screen at given x, y coordinates in dx
|
pascal@186
|
403 ; simple convenience function that gets called enough to be actually worth it, in terms
|
pascal@186
|
404 ; of code length.
|
pascal@186
|
405 ;-----------------------------------------------------------------------------------------------------
|
pascal@186
|
406 paint_bootman:
|
pascal@186
|
407 mov dx, [si + bm_offset_pos] ; dx = Boot-Man position
|
pascal@186
|
408 paint:
|
pascal@186
|
409 call get_screenpos ; Convert x, y coordinates in dx to video memory address
|
pascal@186
|
410 stosw ; stosw = shorter code for mov [es:di], ax
|
pascal@186
|
411 ; stosw also adds 2 to di, but that effect is ignored here
|
pascal@186
|
412 ret
|
pascal@186
|
413
|
pascal@186
|
414
|
pascal@186
|
415 ;-----------------------------------------------------------------------------------------------------
|
pascal@186
|
416 ; test_collision: if end of game, restart from the beginning
|
pascal@186
|
417 ;-----------------------------------------------------------------------------------------------------
|
pascal@186
|
418
|
pascal@186
|
419 test_collision:
|
pascal@186
|
420 mov dx, [bx + si + gh_offset_pos] ; dx = current ghost position
|
pascal@186
|
421 cmp dx, [si + bm_offset_pos] ; compare dx with Boot-Man position
|
pascal@186
|
422 jnz no_collision
|
pascal@186
|
423 cmp byte [si + timer_offset], bh ; Ghost timer was not running
|
pascal@186
|
424 jnz no_collision
|
pascal@186
|
425 pop ax ; Adjust stack
|
pascal@180
|
426 %ifdef WaitForAnyKey
|
pascal@180
|
427 ; Ghosts are visible and collide with boot-man, therefore boot-man is dead
|
pascal@180
|
428 mov ax, 0x0e0f ; Dead boot-man: 0x0e = black background, yellow foreground
|
pascal@180
|
429 ; 0x0f = 8 pointed star
|
pascal@180
|
430 call paint_bootman
|
pascal@180
|
431 cbw
|
pascal@180
|
432 int 0x16 ; Wait for any key
|
pascal@180
|
433 %endif
|
pascal@186
|
434 mov si, 0xfb5e ; restart boot sector
|
pascal@186
|
435 source equ $ - 2
|
pascal@180
|
436
|
pascal@180
|
437
|
pascal@180
|
438 ;-----------------------------------------------------------------------------------------------------
|
pascal@180
|
439 ; copy: self copy to a fixed location
|
pascal@180
|
440 ;-----------------------------------------------------------------------------------------------------
|
pascal@186
|
441
|
pascal@186
|
442 copy equ $ - 2 ; seek to 'pop si, sti' instruction (0x5e 0xfb)
|
pascal@186
|
443 ; pop si ; Get ip value (0x103 or 0x7C03, sometimes 0x0003)
|
pascal@186
|
444 ; sti ; Allow interrupts
|
pascal@180
|
445 push cs
|
pascal@180
|
446 pop ds
|
pascal@180
|
447 push cs ; Setup ds and es
|
pascal@180
|
448 pop es
|
pascal@180
|
449 mov di, moved ; Move self to a well known address
|
pascal@180
|
450 push di
|
pascal@186
|
451 mov ch, (end-moved)/256+1
|
pascal@180
|
452 cld ; Clear the direction flag. We use string instructions a lot as they have one-byte codes
|
pascal@186
|
453 mov [si+source-moved], si ; Save value to the restart with unpatched code
|
pascal@180
|
454 rep movsb
|
pascal@186
|
455 no_collision:
|
pascal@180
|
456 ret
|
pascal@180
|
457
|
pascal@180
|
458
|
pascal@180
|
459 ;-----------------------------------------------------------------------------------------------------
|
pascal@180
|
460 ; newpos: calculates a new position, starting from a position in dx and movement direction in al.
|
pascal@180
|
461 ; dl contains the x coordinate, while dh contains the y coordinate. The movement directions
|
pascal@180
|
462 ; in al are as follows:
|
pascal@180
|
463 ; 0xc2: move right
|
pascal@180
|
464 ; 0xc6: move down
|
pascal@180
|
465 ; 0xca: move left
|
pascal@180
|
466 ; 0xce: move up
|
pascal@180
|
467 ;
|
pascal@180
|
468 ; The reason for these fairly strange values is that they form the 2nd byte (the ModR/M byte)
|
pascal@180
|
469 ; of the instruction updating the position:
|
pascal@180
|
470 ; inc dl (0xfe, 0xc2), inc dh (0xfe), dec dl (0xfe, 0xca), dec dh (0xfe, 0xce)
|
pascal@180
|
471 ; The code first modifies itself to the correct instruction, then executes this instruction. The
|
pascal@180
|
472 ; reason for doing it in this way is that this is a lot shorter than the traditional way of
|
pascal@180
|
473 ; doing an if / elif / elif / else chain.
|
pascal@180
|
474 ;
|
pascal@180
|
475 ; Immediately after calculating the new position we also determine the address in video RAM
|
pascal@180
|
476 ; corresponding to this position. All lines of the screen are stored one after the other in RAM,
|
pascal@180
|
477 ; starting at 0xb800:0x0000. Since each line has 40 characters, and every character takes up
|
pascal@180
|
478 ; two bytes (one for colour, one for the character code), the equation to calculate video RAM
|
pascal@180
|
479 ; offset from x, y coordinates is as follows:
|
pascal@180
|
480 ;
|
pascal@180
|
481 ; offset = 2 * (40 * y + x + 4),
|
pascal@180
|
482 ;
|
pascal@180
|
483 ; with the +4 due to the fact that the maze is in the center of the screen, with a 4 character wide
|
pascal@180
|
484 ; border to the left.
|
pascal@180
|
485 ;
|
pascal@180
|
486 ; newpos and get_screenpos used to be two separate functions but since they were almost
|
pascal@180
|
487 ; always called one after the other, combining them saved some bytes of code.
|
pascal@180
|
488 ;-----------------------------------------------------------------------------------------------------
|
pascal@180
|
489
|
pascal@180
|
490 newpos_bootman:
|
pascal@186
|
491 mov al, [bx+si] ; al = new or current movement direction
|
pascal@186
|
492 dec bx
|
pascal@186
|
493 mov dx, [si + bm_offset_pos] ; dx = current position of Boot-Man
|
pascal@180
|
494 newpos:
|
pascal@180
|
495 mov [.modified_instruction + 1], al ; Here the instruction to be executed is modified
|
pascal@180
|
496 .modified_instruction:
|
pascal@180
|
497 db 0xfe, 0xc2 ; inc dl in machine code
|
pascal@180
|
498 and dl, 0x1f ; Deal with tunnels
|
pascal@180
|
499 get_screenpos:
|
pascal@180
|
500 xchg ax,di ; save ax
|
pascal@186
|
501 mov al, Columns
|
pascal@186
|
502 Columns_patch equ $ - 1
|
pascal@180
|
503 mul dh ; multiply ax by 0x28 = 40 decimal, the screen width
|
pascal@180
|
504 add al, dl
|
pascal@186
|
505 adc ah, bh
|
pascal@180
|
506 xchg ax, di ; di = y * 40 + x
|
pascal@186
|
507 %ifdef MDAsupport
|
pascal@186
|
508 add di, Margin
|
pascal@186
|
509 Margin_patch equ $ - 1
|
pascal@186
|
510 %else
|
pascal@180
|
511 scasw ; Skip the left border by adding 4 to di
|
pascal@180
|
512 scasw
|
pascal@186
|
513 %endif
|
pascal@186
|
514 add di, di ; Multiply di by 2
|
pascal@180
|
515 cmp byte [es:di], 0xdb ; Check to see if the new position collides with a wall
|
pascal@180
|
516 ; 0xdb = full block character that makes up the wall
|
pascal@180
|
517 ret
|
pascal@180
|
518
|
pascal@180
|
519 ; The maze, as a bit array. Ones denote walls, zeroes denote food dots / corridors
|
pascal@180
|
520 ; The maze is stored upside down to save one cmp instruction in buildmaze
|
pascal@180
|
521 maze: dw 0xffff, 0x8000, 0xbffd, 0x8081, 0xfabf, 0x8200, 0xbefd, 0x8001
|
pascal@180
|
522 dw 0xfebf, 0x0080, 0xfebf, 0x803f, 0xaebf, 0xaebf, 0x80bf, 0xfebf
|
pascal@180
|
523 dw 0x0080, 0xfefd, 0x8081, 0xbebf, 0x8000, 0xbefd, 0xbefd, 0x8001
|
pascal@180
|
524 dw 0xffff
|
pascal@180
|
525
|
pascal@186
|
526 bootman_data:
|
pascal@186
|
527 bootmanpos:
|
pascal@186
|
528 db 0x0f, 0x0f ; Boot-Man's x and y position
|
pascal@186
|
529 bootmandir:
|
pascal@186
|
530 db 0xca ; Boot-Man's direction
|
pascal@186
|
531 db 0xca ; Boot-Man's future direction
|
pascal@180
|
532
|
pascal@186
|
533 ghost_timer equ maze + 2 ; if > 0 ghosts are invisible, and is counted backwards to 0
|
pascal@186
|
534 ;pace_counter equ maze + 22 ; 0x3f
|
pascal@186
|
535 pace_counter: db 0x10
|
pascal@186
|
536
|
pascal@186
|
537 ghostdata:
|
pascal@186
|
538 ghostpos:
|
pascal@186
|
539 db 0x01, 0x01 ; 1st ghost, x and y position
|
pascal@186
|
540 ghostdir:
|
pascal@186
|
541 db 0xc2 ; direction
|
pascal@186
|
542 ghostterrain:
|
pascal@186
|
543 dw 0x0ff9 ; terrain underneath
|
pascal@186
|
544 ghostfocus:
|
pascal@186
|
545 db 0x0, 0x0 ; focus point for movement
|
pascal@186
|
546 secondghost:
|
pascal@186
|
547 db 0x01, 0x17 ; 2nd ghost, x and y position
|
pascal@186
|
548 db 0xce ; direction
|
pascal@186
|
549 dw 0x0ff9 ; terrain underneath
|
pascal@186
|
550 db 0x0, 0x4 ; focus point for movement
|
pascal@186
|
551 db 0x1e, 0x01 ; 3rd ghost, x and y position
|
pascal@186
|
552 db 0xca ; direction
|
pascal@186
|
553 dw 0x0ff9 ; terrain underneath
|
pascal@186
|
554 db 0xfc, 0x0 ; focus point for movement
|
pascal@186
|
555 db 0x1e, 0x17 ; 4th ghost, x and y position
|
pascal@186
|
556 db 0xce ; direction
|
pascal@186
|
557 dw 0x0ff9 ; terrain underneath
|
pascal@186
|
558 db 0x4, 0x0 ; focus point for movement
|
pascal@186
|
559 lastghost:
|
pascal@186
|
560
|
pascal@186
|
561 bm_length equ ghostdata - bootman_data
|
pascal@186
|
562 gh_length equ secondghost - ghostdata
|
pascal@186
|
563 gh_offset_dir equ ghostdir - ghostdata
|
pascal@186
|
564 gh_offset_pos equ ghostpos - ghostdata
|
pascal@186
|
565 gh_offset_terrain equ ghostterrain - ghostdata
|
pascal@186
|
566 gh_offset_focus equ ghostfocus - ghostdata
|
pascal@186
|
567 pace_offset equ pace_counter - bootman_data
|
pascal@186
|
568 timer_offset equ ghost_timer - bootman_data
|
pascal@186
|
569 bm_offset_pos equ bootmanpos - bootman_data
|
pascal@180
|
570
|
pascal@180
|
571 times 510 - ($ - $$) db 0
|
pascal@180
|
572 db 0x55
|
pascal@186
|
573 db 0xaa
|
pascal@186
|
574
|
pascal@186
|
575 end:
|