wok-tiny annotate boot-man/stuff/boot-man.asm @ rev 186

Add bootlife
author Pascal Bellard <pascal.bellard@slitaz.org>
date Sun Feb 04 18:02:38 2024 +0000 (4 months ago)
parents 5d44015ce878
children
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: