rev |
line source |
pascal@180
|
1 bits 16
|
pascal@180
|
2
|
pascal@180
|
3 %define UNVEIL_ON_GAME_OVER
|
pascal@180
|
4 %define CGA_DISPLAY
|
pascal@180
|
5 %define DOS_QUIT
|
pascal@180
|
6 %define FAT_BOOT
|
pascal@180
|
7
|
pascal@180
|
8 cpu 8086
|
pascal@180
|
9
|
pascal@180
|
10 ;; Constants
|
pascal@180
|
11
|
pascal@180
|
12 ;; Boot sector size in bytes
|
pascal@180
|
13 %assign BootSector.Size 512
|
pascal@180
|
14
|
pascal@180
|
15 ;; Words in 16 bit x86 are 2 bytes
|
pascal@180
|
16 %assign WordSize 2
|
pascal@180
|
17
|
pascal@180
|
18 ;; This is the value to store in segment register to access the VGA text buffer.
|
pascal@180
|
19 ;; In 16 bit x86, segmented memory accesses are of the form:
|
pascal@180
|
20 ;;
|
pascal@180
|
21 ;; (segment register) * 0x10 + (offset register)
|
pascal@180
|
22 ;;
|
pascal@180
|
23 ;; The VGA text buffer is at 0xb80000, so if 0xb800 is stored in a segment
|
pascal@180
|
24 ;; register, then memory access instructions will be relative to the VGA text
|
pascal@180
|
25 ;; buffer, allowing easier access. For example, trying to access the nth byte of
|
pascal@180
|
26 ;; memory will *actually* access the nth byte of the text buffer.
|
pascal@180
|
27 %assign TextBuf.Seg 0xb800
|
pascal@180
|
28
|
pascal@180
|
29 ;; Dimensions of text buffer
|
pascal@180
|
30 %assign TextBuf.Width 40
|
pascal@180
|
31 %assign TextBuf.Height 25
|
pascal@180
|
32 %assign TextBuf.Size (TextBuf.Width * TextBuf.Height)
|
pascal@180
|
33
|
pascal@180
|
34 ;; Macro to get the index of a text buffer cell from coordinates
|
pascal@180
|
35 %define TextBuf.Index(y, x) ((y) * TextBuf.Width * 2 + (x) * 2)
|
pascal@180
|
36
|
pascal@180
|
37 ;; Length of Dirs array defined below
|
pascal@180
|
38 %assign Dirs.Len 8
|
pascal@180
|
39
|
pascal@180
|
40 ;; Keyboard scan codes
|
pascal@180
|
41 ;; http://www.delorie.com/djgpp/doc/rbinter/it/06/0.html
|
pascal@180
|
42 %assign Key.ScanCode.Space 0x39
|
pascal@180
|
43 %assign Key.ScanCode.Up 0x48
|
pascal@180
|
44 %assign Key.ScanCode.Down 0x50
|
pascal@180
|
45 %assign Key.ScanCode.Left 0x4b
|
pascal@180
|
46 %assign Key.ScanCode.Right 0x4d
|
pascal@180
|
47 %assign Key.ScanCode.Enter 0x1c
|
pascal@180
|
48
|
pascal@180
|
49 ;; Keyboard ASCII codes
|
pascal@180
|
50 %assign Key.Ascii.RestartGame 'r'
|
pascal@182
|
51 %assign Key.Ascii.QuitGame 27
|
pascal@180
|
52
|
pascal@180
|
53 ;; This is a convenience macro for creating VGA characters. VGA characters are
|
pascal@180
|
54 ;; 16 bit words, with the lower byte as the ASCII value and the upper byte
|
pascal@180
|
55 ;; holding the foreground and background colors.
|
pascal@180
|
56 %define VgaChar(color, ascii) (((color) << 8) | (ascii))
|
pascal@180
|
57
|
pascal@180
|
58 ;; VGA colors to use for game items
|
pascal@180
|
59 ;; https://wiki.osdev.org/Text_UI#Colours
|
pascal@180
|
60 %assign Color.Veiled 0x77
|
pascal@180
|
61 %assign Color.Unveiled 0xf0
|
pascal@180
|
62 %assign Color.Cursor 0x00
|
pascal@180
|
63 %assign Color.Flag 0xcc
|
pascal@180
|
64 %assign Color.GameWinText 0x20
|
pascal@180
|
65 %assign Color.GameOverText 0xc0
|
pascal@180
|
66
|
pascal@180
|
67 ;; This value is used to calculate bomb frequency. The probability that any
|
pascal@180
|
68 ;; given cell is a bomb is (1/2)^n, where n = "number of ones in the binary
|
pascal@180
|
69 ;; representation of BombFreq".
|
pascal@180
|
70 ;;
|
pascal@180
|
71 ;; In other words, when BombFreq=0, every cell is a bomb, and appending a one
|
pascal@180
|
72 ;; halves the amount of bombs.
|
pascal@180
|
73 %assign BombFreq 0b111
|
pascal@180
|
74
|
pascal@180
|
75 %ifdef FAT_BOOT
|
pascal@180
|
76 jmp BootMine
|
pascal@180
|
77 nop
|
pascal@180
|
78 times 0x3B db 0
|
pascal@180
|
79 %endif
|
pascal@180
|
80
|
pascal@180
|
81 ;; BootMine is supported as both a DOS game and a boot sector game :)
|
pascal@180
|
82
|
pascal@180
|
83 ;; Entry point: set up graphics and run game
|
pascal@180
|
84 BootMine:
|
pascal@180
|
85 cld
|
pascal@180
|
86
|
pascal@180
|
87 ; VGA text mode 0x00
|
pascal@180
|
88 ; 320x200 pixel resolution
|
pascal@180
|
89 ; 40x25 text resolution
|
pascal@180
|
90 ; 16 colors
|
pascal@180
|
91 ; http://www.delorie.com/djgpp/doc/rbinter/id/74/0.html
|
pascal@180
|
92 xor ax, ax
|
pascal@180
|
93 int 0x10
|
pascal@180
|
94
|
pascal@180
|
95 %ifdef CGA_DISPLAY
|
pascal@180
|
96 ; Toggle intensity/blinking bit
|
pascal@180
|
97 ; http://www.techhelpmanual.com/140-int_10h_1003h__select_foreground_blink_or_bold_background.html
|
pascal@180
|
98 xor bx, bx ;blinking off
|
pascal@180
|
99 mov ds, bx
|
pascal@180
|
100 mov ax, 0x1003 ;toggle intensity/blinking bit (Jr, PS, TANDY 1000, EGA, VGA)
|
pascal@180
|
101 int 0x10
|
pascal@180
|
102 mov si, 0x463
|
pascal@180
|
103 lodsw ;get port address for 6845 video controller chip
|
pascal@180
|
104 add al, 4
|
pascal@180
|
105 xchg ax, dx
|
pascal@180
|
106 and byte [si], 0xdf ;mask value by 1101 1111 (to clear bit 5)
|
pascal@180
|
107 lodsb ;get new value of Mode Select Register
|
pascal@180
|
108 out dx, al ;disable blink (set for bold background)
|
pascal@180
|
109 %else
|
pascal@180
|
110 ; Disable blinking text
|
pascal@180
|
111 ; https://www.reddit.com/r/osdev/comments/70fcig/blinking_text/dn2t6u8?utm_source=share&utm_medium=web2x
|
pascal@180
|
112 ; Read I/O Address 0x03DA to reset index/data flip-flop
|
pascal@180
|
113 mov dx, 0x03DA
|
pascal@180
|
114 in al, dx
|
pascal@180
|
115 ; Write index 0x30 to 0x03C0 to set register index to 0x30
|
pascal@180
|
116 mov dl, 0xC0
|
pascal@180
|
117 mov al, 0x30
|
pascal@180
|
118 out dx, al
|
pascal@180
|
119 ; Read from 0x03C1 to get register contents
|
pascal@180
|
120 inc dx
|
pascal@180
|
121 in al, dx
|
pascal@180
|
122 ; Unset Bit 3 to disable Blink
|
pascal@180
|
123 and al, 0xF7
|
pascal@180
|
124 ; Write to 0x03C0 to update register with changed value
|
pascal@180
|
125 dec dx
|
pascal@180
|
126 out dx, al
|
pascal@180
|
127 %endif
|
pascal@180
|
128
|
pascal@180
|
129 ; Load VGA text buffer segment into segment registers
|
pascal@180
|
130 mov dx, TextBuf.Seg
|
pascal@180
|
131 mov es, dx
|
pascal@180
|
132 mov ds, dx
|
pascal@180
|
133
|
pascal@180
|
134 ; Disable VGA text mode cursor
|
pascal@180
|
135 ; https://wiki.osdev.org/Text_Mode_Cursor#Disabling_the_Cursor
|
pascal@180
|
136 mov ah, 0x01
|
pascal@180
|
137 mov ch, 0x3f
|
pascal@180
|
138 int 0x10
|
pascal@180
|
139
|
pascal@180
|
140 ;; Run game (the game is restarted by jumping here)
|
pascal@180
|
141 RunGame:
|
pascal@180
|
142
|
pascal@180
|
143 ;; Set all cells of game map to veiled '0' cells
|
pascal@180
|
144 ZeroTextBuf:
|
pascal@180
|
145 xor di, di
|
pascal@180
|
146 mov cx, TextBuf.Size
|
pascal@180
|
147 mov ax, VgaChar(Color.Veiled, '0')
|
pascal@180
|
148 rep stosw
|
pascal@180
|
149
|
pascal@180
|
150 %ifndef USE_RDTSC
|
pascal@180
|
151 ; Initialyze the simple pseudo-random number generator
|
pascal@180
|
152 ; seed = set_system_time()
|
pascal@182
|
153 %if 1
|
pascal@180
|
154 cbw
|
pascal@180
|
155 int 0x1a
|
pascal@180
|
156 push dx
|
pascal@182
|
157 %else
|
pascal@182
|
158 in al,(0x40) ; Read timer
|
pascal@182
|
159 push ax
|
pascal@182
|
160 %endif
|
pascal@180
|
161 %endif
|
pascal@180
|
162
|
pascal@180
|
163 ;; Populate text buffer with mines and digits
|
pascal@180
|
164 ;;
|
pascal@180
|
165 ;; This is done with a single triple-nested loop. The nested loops iterate over
|
pascal@180
|
166 ;; y coordinates, then x coordinates, then over the 8 adjacent cells at (y, x).
|
pascal@180
|
167 ;;
|
pascal@180
|
168 ;; Inside the 2nd loop level is bomb generation logic. Digit incrementing logic
|
pascal@180
|
169 ;; is in the 3rd loop level.
|
pascal@180
|
170 ;;
|
pascal@180
|
171 ;; Note that the coordinates on the outside border are skipped to avoid bounds
|
pascal@180
|
172 ;; checking logic.
|
pascal@180
|
173 PopulateTextBuf:
|
pascal@180
|
174 ; Iterate over y coordinates
|
pascal@180
|
175 mov bx, TextBuf.Height - 2
|
pascal@180
|
176
|
pascal@180
|
177 .LoopY:
|
pascal@180
|
178 ; Iterate over x coordinates. ch = 0 form ZeroTextBuf
|
pascal@180
|
179 %ifndef USE_RDTSC
|
pascal@180
|
180 mov cx, TextBuf.Width - 2
|
pascal@180
|
181 %else
|
pascal@180
|
182 mov cl, TextBuf.Width - 2
|
pascal@180
|
183 %endif
|
pascal@180
|
184
|
pascal@180
|
185 .LoopX:
|
pascal@180
|
186 ; di = &TextBuf[y][x]
|
pascal@180
|
187 call GetTextBufIndex
|
pascal@180
|
188 .si_value:
|
pascal@180
|
189
|
pascal@180
|
190 ; The register dl holds a boolean that is 1 if the current cell is a bomb, 0
|
pascal@180
|
191 ; otherwise.
|
pascal@180
|
192 %ifndef USE_RDTSC
|
pascal@180
|
193 ; It is calculated by bitwise and-ing the result of the simple pseudo-random number
|
pascal@180
|
194 ; generator seed = ((seed + LARGE_PRIME1) * LARGE_PRIME2) % LARGE_PRIME3
|
pascal@180
|
195 ;
|
pascal@180
|
196 ; dl = ! (prng() & BombFreq)
|
pascal@180
|
197 %assign Prime1 32749
|
pascal@180
|
198 %assign Prime2 65519
|
pascal@180
|
199 %assign Prime3 65521
|
pascal@180
|
200 pop ax
|
pascal@180
|
201 add ax, Prime1
|
pascal@180
|
202 mov bp, Prime2
|
pascal@180
|
203 mul bp
|
pascal@180
|
204 inc bp
|
pascal@180
|
205 inc bp
|
pascal@180
|
206 div bp
|
pascal@180
|
207 xchg ax, dx
|
pascal@180
|
208 push ax
|
pascal@180
|
209 %else
|
pascal@180
|
210 ; It is calculated by bitwise and-ing the result of rdtsc. (rdtsc returns the
|
pascal@180
|
211 ; amount of CPU cycles since boot, which works okay as a cheap random number
|
pascal@180
|
212 ; generator, and it's apparently supported on all x86 CPUs since the Pentium line)
|
pascal@180
|
213 ;
|
pascal@180
|
214 ; dl = ! (rdtsc() & BombFreq)
|
pascal@180
|
215 cpu 686
|
pascal@180
|
216 rdtsc
|
pascal@180
|
217 cpu 8086
|
pascal@180
|
218 %endif
|
pascal@180
|
219 and al, BombFreq
|
pascal@180
|
220 mov dx, '*' * 256 + 0
|
pascal@180
|
221
|
pascal@180
|
222 ; Initialize loop counter for .LoopDir
|
pascal@180
|
223 mov bp, Dirs.Len
|
pascal@180
|
224
|
pascal@180
|
225 ; If this cell isn't a bomb, then skip marking it as a bomb
|
pascal@180
|
226 jnz .LoopDir
|
pascal@180
|
227
|
pascal@180
|
228 ; Mark the current cell as a bomb
|
pascal@180
|
229 mov byte [di], dh
|
pascal@180
|
230 inc dx
|
pascal@180
|
231
|
pascal@180
|
232 ; Iterate over adjacent cells (directions)
|
pascal@180
|
233 .LoopDir:
|
pascal@180
|
234 ; Load adjacent cell offset from Dirs array into ax.
|
pascal@180
|
235 mov al, byte [cs:bp + si + Dirs - .si_value - 1]
|
pascal@180
|
236 cbw
|
pascal@180
|
237 ; Set di = pointer to adjacent cell
|
pascal@180
|
238 add di, ax
|
pascal@180
|
239
|
pascal@180
|
240 ; If adjacent cell is a bomb, skip digit incrementing
|
pascal@180
|
241 cmp byte [di], dh
|
pascal@180
|
242 je .LoopDirIsMine
|
pascal@180
|
243 ; The adjacent cell is a 0-7 digit and not a bomb. Add dl to the cell, which
|
pascal@180
|
244 ; is 1 if the original cell is a bomb. This gradually accumulates to the
|
pascal@180
|
245 ; amount of neighboring bombs and represents the number cells in the
|
pascal@180
|
246 ; minesweeper game.
|
pascal@180
|
247 add [di], dl
|
pascal@180
|
248 .LoopDirIsMine:
|
pascal@180
|
249 ; Restore di to original cell pointer
|
pascal@180
|
250 sub di, ax
|
pascal@180
|
251
|
pascal@180
|
252 ; Decrement adjacent direction loop counter and continue if nonzero
|
pascal@180
|
253 dec bp
|
pascal@180
|
254 jnz .LoopDir
|
pascal@180
|
255
|
pascal@180
|
256 ; Decrement x coordinate loop counter and continue if nonzero
|
pascal@180
|
257 loop .LoopX
|
pascal@180
|
258
|
pascal@180
|
259 ; Decrement y coordinate loop counter and continue if nonzero
|
pascal@180
|
260 dec bx
|
pascal@180
|
261 jnz .LoopY
|
pascal@180
|
262 %ifndef USE_RDTSC
|
pascal@180
|
263 pop ax
|
pascal@180
|
264 %endif
|
pascal@180
|
265
|
pascal@180
|
266 ;; Done populating the text buffer
|
pascal@180
|
267
|
pascal@180
|
268 ; Set the initial cursor color for game loop. The dl register is now used to
|
pascal@180
|
269 ; store the saved cell color that the cursor is on, since the cursor
|
pascal@180
|
270 ; overwrites the cell color with the cursor color.
|
pascal@180
|
271 mov dl, Color.Veiled
|
pascal@180
|
272
|
pascal@180
|
273 ;; Main loop to process key presses and update state
|
pascal@180
|
274 GameLoop:
|
pascal@180
|
275 ; Get keystroke
|
pascal@180
|
276 ; ah = BIOS scan code
|
pascal@180
|
277 ; al = ASCII character
|
pascal@180
|
278 ; http://www.delorie.com/djgpp/doc/rbinter/id/63/17.html
|
pascal@180
|
279 xor ax, ax
|
pascal@180
|
280 int 0x16
|
pascal@182
|
281 %ifdef DOS_QUIT
|
pascal@182
|
282 cmp al, Key.Ascii.QuitGame
|
pascal@182
|
283 je Quit
|
pascal@182
|
284 %endif
|
pascal@180
|
285
|
pascal@180
|
286 ; bx and cx are zeroed from the PopulateTextBuf loops above
|
pascal@180
|
287 ; bx = y coord
|
pascal@180
|
288 ; cx = x coord
|
pascal@180
|
289
|
pascal@180
|
290 ; di = cell pointer
|
pascal@180
|
291 call GetTextBufIndex
|
pascal@180
|
292 ; Apply saved cell color
|
pascal@180
|
293 mov [di + 1], dl
|
pascal@180
|
294
|
pascal@180
|
295 ;; Detect win (a win occurs when every veiled cell is a mine)
|
pascal@180
|
296 DetectWin:
|
pascal@180
|
297 ; Use si register as cell pointer for win detection
|
pascal@180
|
298 xor si, si
|
pascal@180
|
299 ; Use bp as loop counter
|
pascal@180
|
300 mov bp, TextBuf.Size
|
pascal@180
|
301 .Loop:
|
pascal@180
|
302 ; if (char != '*' && (color == Color.Veiled || color == Color.Flag)) {
|
pascal@180
|
303 ; break; // Didn't win yet :(
|
pascal@180
|
304 ; }
|
pascal@180
|
305 ; Load VGA char into al
|
pascal@180
|
306 lodsb
|
pascal@180
|
307 cmp al, '*'
|
pascal@180
|
308 ; Load VGA color into al
|
pascal@180
|
309 lodsb
|
pascal@180
|
310 je .Continue
|
pascal@180
|
311 cmp al, Color.Veiled
|
pascal@180
|
312 je Break
|
pascal@180
|
313 cmp al, Color.Flag
|
pascal@180
|
314 je Break
|
pascal@180
|
315 .Continue:
|
pascal@180
|
316 dec bp
|
pascal@180
|
317 jnz .Loop
|
pascal@180
|
318 ; If loop completes without breaking, then we win! :)
|
pascal@180
|
319
|
pascal@180
|
320 ;; Show game win screen
|
pascal@180
|
321 ;;GameWin:
|
pascal@180
|
322 mov ah, Color.GameWinText
|
pascal@180
|
323 call GameEndHelper
|
pascal@180
|
324 db 'GAME WIN'
|
pascal@180
|
325
|
pascal@180
|
326 ;; Wait for restart key to be pressed, then restart game
|
pascal@180
|
327 WaitRestartLoop:
|
pascal@180
|
328 je RunGame
|
pascal@180
|
329 WaitRestart:
|
pascal@180
|
330 xor ax, ax
|
pascal@180
|
331 int 0x16
|
pascal@180
|
332 %ifdef DOS_QUIT
|
pascal@180
|
333 cmp al, Key.Ascii.QuitGame
|
pascal@182
|
334 jnz Quit.notQuit
|
pascal@182
|
335 Quit:
|
pascal@180
|
336 mov ax,0x0003 ; Restore text mode
|
pascal@180
|
337 int 0x10
|
pascal@180
|
338 int 0x20
|
pascal@182
|
339 int 0x19
|
pascal@180
|
340 .notQuit:
|
pascal@180
|
341 %endif
|
pascal@180
|
342 cmp al, Key.Ascii.RestartGame
|
pascal@180
|
343 jmp WaitRestartLoop
|
pascal@180
|
344
|
pascal@180
|
345 ;; Array of adjacent cell offsets. A byte in this array can be added to a text
|
pascal@180
|
346 ;; buffer cell pointer to get the pointer to an adjacent cell. This is used for
|
pascal@180
|
347 ;; spawning digit cells.
|
pascal@180
|
348 Dirs:
|
pascal@180
|
349 db TextBuf.Index(-1, -1)
|
pascal@180
|
350 db TextBuf.Index(-1, 0)
|
pascal@180
|
351 db TextBuf.Index(-1, +1)
|
pascal@180
|
352 db TextBuf.Index( 0, +1)
|
pascal@180
|
353 db TextBuf.Index(+1, +1)
|
pascal@180
|
354 db TextBuf.Index(+1, 0)
|
pascal@180
|
355 db TextBuf.Index(+1, -1)
|
pascal@180
|
356 db TextBuf.Index( 0, -1)
|
pascal@180
|
357
|
pascal@180
|
358 Break:
|
pascal@180
|
359 ; Didn't win yet
|
pascal@180
|
360 mov al, ah
|
pascal@180
|
361
|
pascal@180
|
362 ;; Process key press. This is an if-else chain that runs code depending on the
|
pascal@180
|
363 ;; key pressed.
|
pascal@180
|
364 CmpUp:
|
pascal@180
|
365 ; Move cursor up
|
pascal@180
|
366 dec bx
|
pascal@180
|
367 cmp al, Key.ScanCode.Up
|
pascal@180
|
368 je WrapCursor
|
pascal@180
|
369 inc bx
|
pascal@180
|
370 CmpDown:
|
pascal@180
|
371 ; Move cursor down
|
pascal@180
|
372 inc bx
|
pascal@180
|
373 cmp al, Key.ScanCode.Down
|
pascal@180
|
374 je WrapCursor
|
pascal@180
|
375 dec bx
|
pascal@180
|
376 CmpLeft:
|
pascal@180
|
377 ; Move cursor left
|
pascal@180
|
378 dec cx
|
pascal@180
|
379 cmp al, Key.ScanCode.Left
|
pascal@180
|
380 je WrapCursor
|
pascal@180
|
381 inc cx
|
pascal@180
|
382 CmpRight:
|
pascal@180
|
383 ; Move cursor right
|
pascal@180
|
384 inc cx
|
pascal@180
|
385 cmp al, Key.ScanCode.Right
|
pascal@180
|
386 je WrapCursor
|
pascal@180
|
387 dec cx
|
pascal@180
|
388 CmpEnter:
|
pascal@180
|
389 cmp al, Key.ScanCode.Enter
|
pascal@180
|
390 jne CmpSpace
|
pascal@180
|
391 ; Place flag by coloring current cell
|
pascal@180
|
392 mov dl, Color.Flag
|
pascal@180
|
393 mov [di + 1], dl
|
pascal@180
|
394 ; jmp GameLoop
|
pascal@180
|
395 CmpSpace:
|
pascal@180
|
396 cmp al, Key.ScanCode.Space
|
pascal@180
|
397 jne GameLoop
|
pascal@180
|
398
|
pascal@180
|
399 ;; If the player pressed space, clear the current cell
|
pascal@180
|
400 ClearCell:
|
pascal@180
|
401 ; Set ax = cell value
|
pascal@180
|
402 mov ax, [di]
|
pascal@180
|
403 call UnveilCell
|
pascal@180
|
404 ;; If-else chain checking the cell value
|
pascal@180
|
405 .CmpEmpty:
|
pascal@180
|
406 cmp al, '0'
|
pascal@180
|
407 jne .CmpMine
|
pascal@180
|
408 ; If cell is empty, run flood fill algorithm
|
pascal@180
|
409 call Flood
|
pascal@180
|
410 .jmpGameLoop:
|
pascal@180
|
411 jmp GameLoop
|
pascal@180
|
412 .CmpMine:
|
pascal@180
|
413 cmp al, '*'
|
pascal@180
|
414 ; No handling needed if cell is digit
|
pascal@180
|
415 jne .jmpGameLoop
|
pascal@180
|
416 ; If cell is bomb, game over :(
|
pascal@180
|
417
|
pascal@180
|
418 ;; Show game over screen
|
pascal@180
|
419
|
pascal@180
|
420 %ifdef UNVEIL_ON_GAME_OVER
|
pascal@180
|
421 mov cx, TextBuf.Size
|
pascal@180
|
422 xor si, si
|
pascal@180
|
423 .Loop:
|
pascal@180
|
424 ; Load VGA character into ax
|
pascal@180
|
425 lodsw
|
pascal@180
|
426 cmp al, '*'
|
pascal@180
|
427 jne .Next
|
pascal@180
|
428 and byte [si-1], Color.Unveiled
|
pascal@180
|
429 .Next:
|
pascal@180
|
430 loop .Loop
|
pascal@180
|
431 %endif
|
pascal@180
|
432 ;;GameOver:
|
pascal@180
|
433 mov ah, Color.GameOverText
|
pascal@180
|
434 call GameEndHelper
|
pascal@180
|
435 GameOverStr:
|
pascal@180
|
436 db 'GAME OVER'
|
pascal@180
|
437 %assign GameOverStr.Len $ - GameOverStr
|
pascal@180
|
438
|
pascal@180
|
439 ;; Helper code for GameWin and GameOver; print a string in the center of the
|
pascal@180
|
440 ;; text buffer, then wait for game to be restarted.
|
pascal@180
|
441 GameEndHelper:
|
pascal@180
|
442 pop si
|
pascal@180
|
443 mov di, TextBuf.Index(TextBuf.Height / 2, TextBuf.Width / 2 - GameOverStr.Len / 2)
|
pascal@180
|
444 .Loop:
|
pascal@180
|
445 cs lodsb
|
pascal@180
|
446 cmp al, 'Z'
|
pascal@180
|
447 ja WaitRestart
|
pascal@180
|
448 stosw
|
pascal@180
|
449 jmp .Loop
|
pascal@180
|
450
|
pascal@180
|
451 ;; Set y and x coordinates of cursor to zero if they are out of bounds
|
pascal@180
|
452 WrapCursor:
|
pascal@180
|
453 .Y:
|
pascal@180
|
454 ; Wrap y cursor
|
pascal@180
|
455 cmp bx, TextBuf.Height
|
pascal@180
|
456 jb .X
|
pascal@180
|
457 xor bx, bx
|
pascal@180
|
458
|
pascal@180
|
459 .X:
|
pascal@180
|
460 ; Wrap x cursor
|
pascal@180
|
461 cmp cx, TextBuf.Width
|
pascal@180
|
462 jb SetCursorPos
|
pascal@180
|
463 xor cx, cx
|
pascal@180
|
464
|
pascal@180
|
465 ;; Redraw cursor in new position
|
pascal@180
|
466 SetCursorPos:
|
pascal@180
|
467 ; Get text buffer index (it changed)
|
pascal@180
|
468 call GetTextBufIndex
|
pascal@180
|
469 ; Draw cursor by changing cell to the cursor color, but save current color for
|
pascal@180
|
470 ; restoring in the next iteration of the game loop.
|
pascal@180
|
471 mov dl, Color.Cursor
|
pascal@180
|
472 xchg dl, [di + 1]
|
pascal@180
|
473
|
pascal@180
|
474 jmp ClearCell.jmpGameLoop
|
pascal@180
|
475
|
pascal@180
|
476 ;; Compute the text buffer index from y and x coordinates
|
pascal@180
|
477 ;;
|
pascal@180
|
478 ;; di = &TextBuf[bx = y][cx = x]
|
pascal@180
|
479 ;;
|
pascal@180
|
480 ;; This computes the equivalent of the TextBuf.Index(y, x) macro, but at runtime
|
pascal@180
|
481 ;;
|
pascal@180
|
482 ;; Parameters:
|
pascal@180
|
483 ;; * bx - y coordinate
|
pascal@180
|
484 ;; * cx - x coordinate
|
pascal@180
|
485 ;; Returns:
|
pascal@180
|
486 ;; * di - text buffer index
|
pascal@180
|
487 ;; * si - caller return address
|
pascal@180
|
488 GetTextBufIndex:
|
pascal@180
|
489 xchg ax, di
|
pascal@180
|
490 mov al, TextBuf.Width * 2
|
pascal@180
|
491 imul bl
|
pascal@180
|
492 xchg ax, di
|
pascal@180
|
493 add di, cx
|
pascal@180
|
494 add di, cx
|
pascal@180
|
495 ; load caller return address in si
|
pascal@180
|
496 pop si
|
pascal@180
|
497 push si
|
pascal@180
|
498 ret
|
pascal@180
|
499
|
pascal@180
|
500 ;; Unveil a cell so it is visible on the screen
|
pascal@180
|
501 ;;
|
pascal@180
|
502 ;; Parameters:
|
pascal@180
|
503 ;; * di - cell pointer in text buffer
|
pascal@180
|
504 ;; * al - cell ASCII value
|
pascal@180
|
505 ;; Returns:
|
pascal@180
|
506 ;; * dl - written VGA color code
|
pascal@180
|
507 UnveilCell:
|
pascal@180
|
508 ; TLDR: Use xor magic to make the cells colored.
|
pascal@180
|
509 ;
|
pascal@180
|
510 ; We have three cases to consider:
|
pascal@180
|
511 ;
|
pascal@180
|
512 ; Case 1: the cell is a digit from 1-8
|
pascal@180
|
513 ;
|
pascal@180
|
514 ; The ASCII values '1', '2', '3', ..., '8' are 0x31, 0x32, 0x33, ..., 0x38. In
|
pascal@180
|
515 ; other words, the upper nibble is always 0x3 and the lower nibble is the
|
pascal@180
|
516 ; digit. We want the VGA color code to be `Color.Unveiled | digit`. For
|
pascal@180
|
517 ; example, the color of a '3' cell would be 0xf3.
|
pascal@180
|
518 ;
|
pascal@180
|
519 ; We can accomplish this with the formula `cell_value ^ '0' ^ Color.Unveiled`.
|
pascal@180
|
520 ; Xor-ing by '0' (0x30), clears the upper nibble of the cell value, leaving
|
pascal@180
|
521 ; just the digit value. Xor-ing again by Color.Unveiled sets the upper nibble
|
pascal@180
|
522 ; to 0xf, leading to the value `Color.Unveiled | digit`.
|
pascal@180
|
523 ;
|
pascal@180
|
524 ; Since xor is associative, this can be done in one operation, by xor-ing the
|
pascal@180
|
525 ; cell value by ('0' ^ Color.Unveiled).
|
pascal@180
|
526 ;
|
pascal@180
|
527 ; Case 2: the cell is a bomb
|
pascal@180
|
528 ;
|
pascal@180
|
529 ; We don't really care about this case as long as the bomb is visible against
|
pascal@180
|
530 ; the background. The bomb turns out to be green, oh well.
|
pascal@180
|
531 ;
|
pascal@180
|
532 ; Case 3: the cell is an empty space
|
pascal@180
|
533 ;
|
pascal@180
|
534 ; This ends up coloring the cell bright yellow, which isn't a big problem.
|
pascal@180
|
535 mov dl, al
|
pascal@180
|
536 xor dl, '0' ^ Color.Unveiled
|
pascal@180
|
537 mov [di + 1], dl
|
pascal@180
|
538 ret
|
pascal@180
|
539
|
pascal@180
|
540 ;; Flood fill empty cells
|
pascal@180
|
541 ;;
|
pascal@180
|
542 ;; Parameters:
|
pascal@180
|
543 ;; * bx - cell y coordinate
|
pascal@180
|
544 ;; * cx - cell x coordinate
|
pascal@180
|
545 ;; Clobbered registers:
|
pascal@180
|
546 ;; * ax - cell value
|
pascal@180
|
547 ;; * di - cell pointer in text buffer
|
pascal@180
|
548 Flood:
|
pascal@180
|
549 ; Init: get cell pointer and value
|
pascal@180
|
550 call GetTextBufIndex
|
pascal@180
|
551 mov ax, [di]
|
pascal@180
|
552
|
pascal@180
|
553 ; Base case: bounds check y
|
pascal@180
|
554 cmp bx, TextBuf.Height
|
pascal@180
|
555 jae .Ret
|
pascal@180
|
556
|
pascal@180
|
557 ; Base case: bounds check x
|
pascal@180
|
558 cmp cx, TextBuf.Width
|
pascal@180
|
559 jae .Ret
|
pascal@180
|
560
|
pascal@180
|
561 cmp al, '0'
|
pascal@180
|
562
|
pascal@180
|
563 ; Base case: we visited this cell already or bomb
|
pascal@180
|
564 jb .Ret
|
pascal@180
|
565
|
pascal@180
|
566 ; Base case: nonempty cell unveiled and stop recursion
|
pascal@180
|
567 jne UnveilCell
|
pascal@180
|
568
|
pascal@180
|
569 ; Body: unveil empty cell
|
pascal@180
|
570 call UnveilCell
|
pascal@180
|
571
|
pascal@180
|
572 ; Body: mark cell as visited and empty
|
pascal@180
|
573 mov byte [di], ' '
|
pascal@180
|
574
|
pascal@180
|
575 ; Recursive case: flood adjacent cells
|
pascal@180
|
576
|
pascal@180
|
577 ; Flood down
|
pascal@180
|
578 inc bx
|
pascal@180
|
579 call Flood
|
pascal@180
|
580 dec bx
|
pascal@180
|
581
|
pascal@180
|
582 ; Flood left
|
pascal@180
|
583 dec cx
|
pascal@180
|
584 call Flood
|
pascal@180
|
585 inc cx
|
pascal@180
|
586
|
pascal@180
|
587 ; Flood right
|
pascal@180
|
588 inc cx
|
pascal@180
|
589 call Flood
|
pascal@180
|
590 dec cx
|
pascal@180
|
591
|
pascal@180
|
592 ; Flood up-left
|
pascal@180
|
593 dec cx
|
pascal@180
|
594 call .Flood_up
|
pascal@180
|
595 inc cx
|
pascal@180
|
596
|
pascal@180
|
597 ; Flood up-right
|
pascal@180
|
598 inc cx
|
pascal@180
|
599 call .Flood_up
|
pascal@180
|
600 dec cx
|
pascal@180
|
601
|
pascal@180
|
602 ; Flood down-left
|
pascal@180
|
603 inc bx
|
pascal@180
|
604 dec cx
|
pascal@180
|
605 call Flood
|
pascal@180
|
606 inc cx
|
pascal@180
|
607 dec bx
|
pascal@180
|
608
|
pascal@180
|
609 ; Flood down-right
|
pascal@180
|
610 inc bx
|
pascal@180
|
611 inc cx
|
pascal@180
|
612 call Flood
|
pascal@180
|
613 dec cx
|
pascal@180
|
614 dec bx
|
pascal@180
|
615
|
pascal@180
|
616 .Flood_up:
|
pascal@180
|
617 ; Flood up
|
pascal@180
|
618 dec bx
|
pascal@180
|
619 call Flood
|
pascal@180
|
620 inc bx
|
pascal@180
|
621
|
pascal@180
|
622 .Ret:
|
pascal@180
|
623 ret
|
pascal@180
|
624
|
pascal@180
|
625
|
pascal@180
|
626 ;; Print program size at build time
|
pascal@180
|
627 %assign CodeSize $ - $$
|
pascal@180
|
628 %warning Code is CodeSize bytes
|
pascal@180
|
629
|
pascal@180
|
630 %ifdef MBR_BOOT
|
pascal@180
|
631 %assign PartitionTable 0x1BE
|
pascal@180
|
632 %if CodeSize > PartitionTable
|
pascal@180
|
633 %assign OverFlow CodeSize - PartitionTable
|
pascal@180
|
634 %error Code is OverFlow bytes too large
|
pascal@180
|
635 %endif
|
pascal@180
|
636 %endif
|
pascal@180
|
637
|
pascal@180
|
638 CodeEnd:
|
pascal@180
|
639 ; Pad to size of boot sector, minus the size of a word for the boot sector
|
pascal@180
|
640 ; magic value. If the code is too big to fit in a boot sector, the `times`
|
pascal@180
|
641 ; directive uses a negative value, causing a build error.
|
pascal@180
|
642 times (BootSector.Size - WordSize) - CodeSize db 0
|
pascal@180
|
643
|
pascal@180
|
644 ; Boot sector magic
|
pascal@180
|
645 dw 0xaa55
|