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