wok-tiny view bootmine/stuff/mine.asm @ rev 182

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