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

Add boot-man, bootbricks, bootfbird, bootinvaders, bootmine, bootpillman, bootris, bootsokoban
author Pascal Bellard <pascal.bellard@slitaz.org>
date Wed Sep 20 13:08:44 2023 +0000 (8 months ago)
parents
children bbb34fe4904d
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 'q'
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 cbw
154 int 0x1a
155 push dx
156 %endif
158 ;; Populate text buffer with mines and digits
159 ;;
160 ;; This is done with a single triple-nested loop. The nested loops iterate over
161 ;; y coordinates, then x coordinates, then over the 8 adjacent cells at (y, x).
162 ;;
163 ;; Inside the 2nd loop level is bomb generation logic. Digit incrementing logic
164 ;; is in the 3rd loop level.
165 ;;
166 ;; Note that the coordinates on the outside border are skipped to avoid bounds
167 ;; checking logic.
168 PopulateTextBuf:
169 ; Iterate over y coordinates
170 mov bx, TextBuf.Height - 2
172 .LoopY:
173 ; Iterate over x coordinates. ch = 0 form ZeroTextBuf
174 %ifndef USE_RDTSC
175 mov cx, TextBuf.Width - 2
176 %else
177 mov cl, TextBuf.Width - 2
178 %endif
180 .LoopX:
181 ; di = &TextBuf[y][x]
182 call GetTextBufIndex
183 .si_value:
185 ; The register dl holds a boolean that is 1 if the current cell is a bomb, 0
186 ; otherwise.
187 %ifndef USE_RDTSC
188 ; It is calculated by bitwise and-ing the result of the simple pseudo-random number
189 ; generator seed = ((seed + LARGE_PRIME1) * LARGE_PRIME2) % LARGE_PRIME3
190 ;
191 ; dl = ! (prng() & BombFreq)
192 %assign Prime1 32749
193 %assign Prime2 65519
194 %assign Prime3 65521
195 pop ax
196 add ax, Prime1
197 mov bp, Prime2
198 mul bp
199 inc bp
200 inc bp
201 div bp
202 xchg ax, dx
203 push ax
204 %else
205 ; It is calculated by bitwise and-ing the result of rdtsc. (rdtsc returns the
206 ; amount of CPU cycles since boot, which works okay as a cheap random number
207 ; generator, and it's apparently supported on all x86 CPUs since the Pentium line)
208 ;
209 ; dl = ! (rdtsc() & BombFreq)
210 cpu 686
211 rdtsc
212 cpu 8086
213 %endif
214 and al, BombFreq
215 mov dx, '*' * 256 + 0
217 ; Initialize loop counter for .LoopDir
218 mov bp, Dirs.Len
220 ; If this cell isn't a bomb, then skip marking it as a bomb
221 jnz .LoopDir
223 ; Mark the current cell as a bomb
224 mov byte [di], dh
225 inc dx
227 ; Iterate over adjacent cells (directions)
228 .LoopDir:
229 ; Load adjacent cell offset from Dirs array into ax.
230 mov al, byte [cs:bp + si + Dirs - .si_value - 1]
231 cbw
232 ; Set di = pointer to adjacent cell
233 add di, ax
235 ; If adjacent cell is a bomb, skip digit incrementing
236 cmp byte [di], dh
237 je .LoopDirIsMine
238 ; The adjacent cell is a 0-7 digit and not a bomb. Add dl to the cell, which
239 ; is 1 if the original cell is a bomb. This gradually accumulates to the
240 ; amount of neighboring bombs and represents the number cells in the
241 ; minesweeper game.
242 add [di], dl
243 .LoopDirIsMine:
244 ; Restore di to original cell pointer
245 sub di, ax
247 ; Decrement adjacent direction loop counter and continue if nonzero
248 dec bp
249 jnz .LoopDir
251 ; Decrement x coordinate loop counter and continue if nonzero
252 loop .LoopX
254 ; Decrement y coordinate loop counter and continue if nonzero
255 dec bx
256 jnz .LoopY
257 %ifndef USE_RDTSC
258 pop ax
259 %endif
261 ;; Done populating the text buffer
263 ; Set the initial cursor color for game loop. The dl register is now used to
264 ; store the saved cell color that the cursor is on, since the cursor
265 ; overwrites the cell color with the cursor color.
266 mov dl, Color.Veiled
268 ;; Main loop to process key presses and update state
269 GameLoop:
270 ; Get keystroke
271 ; ah = BIOS scan code
272 ; al = ASCII character
273 ; http://www.delorie.com/djgpp/doc/rbinter/id/63/17.html
274 xor ax, ax
275 int 0x16
277 ; bx and cx are zeroed from the PopulateTextBuf loops above
278 ; bx = y coord
279 ; cx = x coord
281 ; di = cell pointer
282 call GetTextBufIndex
283 ; Apply saved cell color
284 mov [di + 1], dl
286 ;; Detect win (a win occurs when every veiled cell is a mine)
287 DetectWin:
288 ; Use si register as cell pointer for win detection
289 xor si, si
290 ; Use bp as loop counter
291 mov bp, TextBuf.Size
292 .Loop:
293 ; if (char != '*' && (color == Color.Veiled || color == Color.Flag)) {
294 ; break; // Didn't win yet :(
295 ; }
296 ; Load VGA char into al
297 lodsb
298 cmp al, '*'
299 ; Load VGA color into al
300 lodsb
301 je .Continue
302 cmp al, Color.Veiled
303 je Break
304 cmp al, Color.Flag
305 je Break
306 .Continue:
307 dec bp
308 jnz .Loop
309 ; If loop completes without breaking, then we win! :)
311 ;; Show game win screen
312 ;;GameWin:
313 mov ah, Color.GameWinText
314 call GameEndHelper
315 db 'GAME WIN'
317 ;; Wait for restart key to be pressed, then restart game
318 WaitRestartLoop:
319 je RunGame
320 WaitRestart:
321 xor ax, ax
322 int 0x16
323 %ifdef DOS_QUIT
324 cmp al, Key.Ascii.QuitGame
325 jnz .notQuit
326 mov ax,0x0003 ; Restore text mode
327 int 0x10
328 int 0x20
329 .notQuit:
330 %endif
331 cmp al, Key.Ascii.RestartGame
332 jmp WaitRestartLoop
334 ;; Array of adjacent cell offsets. A byte in this array can be added to a text
335 ;; buffer cell pointer to get the pointer to an adjacent cell. This is used for
336 ;; spawning digit cells.
337 Dirs:
338 db TextBuf.Index(-1, -1)
339 db TextBuf.Index(-1, 0)
340 db TextBuf.Index(-1, +1)
341 db TextBuf.Index( 0, +1)
342 db TextBuf.Index(+1, +1)
343 db TextBuf.Index(+1, 0)
344 db TextBuf.Index(+1, -1)
345 db TextBuf.Index( 0, -1)
347 Break:
348 ; Didn't win yet
349 mov al, ah
351 ;; Process key press. This is an if-else chain that runs code depending on the
352 ;; key pressed.
353 CmpUp:
354 ; Move cursor up
355 dec bx
356 cmp al, Key.ScanCode.Up
357 je WrapCursor
358 inc bx
359 CmpDown:
360 ; Move cursor down
361 inc bx
362 cmp al, Key.ScanCode.Down
363 je WrapCursor
364 dec bx
365 CmpLeft:
366 ; Move cursor left
367 dec cx
368 cmp al, Key.ScanCode.Left
369 je WrapCursor
370 inc cx
371 CmpRight:
372 ; Move cursor right
373 inc cx
374 cmp al, Key.ScanCode.Right
375 je WrapCursor
376 dec cx
377 CmpEnter:
378 cmp al, Key.ScanCode.Enter
379 jne CmpSpace
380 ; Place flag by coloring current cell
381 mov dl, Color.Flag
382 mov [di + 1], dl
383 ; jmp GameLoop
384 CmpSpace:
385 cmp al, Key.ScanCode.Space
386 jne GameLoop
388 ;; If the player pressed space, clear the current cell
389 ClearCell:
390 ; Set ax = cell value
391 mov ax, [di]
392 call UnveilCell
393 ;; If-else chain checking the cell value
394 .CmpEmpty:
395 cmp al, '0'
396 jne .CmpMine
397 ; If cell is empty, run flood fill algorithm
398 call Flood
399 .jmpGameLoop:
400 jmp GameLoop
401 .CmpMine:
402 cmp al, '*'
403 ; No handling needed if cell is digit
404 jne .jmpGameLoop
405 ; If cell is bomb, game over :(
407 ;; Show game over screen
409 %ifdef UNVEIL_ON_GAME_OVER
410 mov cx, TextBuf.Size
411 xor si, si
412 .Loop:
413 ; Load VGA character into ax
414 lodsw
415 cmp al, '*'
416 jne .Next
417 and byte [si-1], Color.Unveiled
418 .Next:
419 loop .Loop
420 %endif
421 ;;GameOver:
422 mov ah, Color.GameOverText
423 call GameEndHelper
424 GameOverStr:
425 db 'GAME OVER'
426 %assign GameOverStr.Len $ - GameOverStr
428 ;; Helper code for GameWin and GameOver; print a string in the center of the
429 ;; text buffer, then wait for game to be restarted.
430 GameEndHelper:
431 pop si
432 mov di, TextBuf.Index(TextBuf.Height / 2, TextBuf.Width / 2 - GameOverStr.Len / 2)
433 .Loop:
434 cs lodsb
435 cmp al, 'Z'
436 ja WaitRestart
437 stosw
438 jmp .Loop
440 ;; Set y and x coordinates of cursor to zero if they are out of bounds
441 WrapCursor:
442 .Y:
443 ; Wrap y cursor
444 cmp bx, TextBuf.Height
445 jb .X
446 xor bx, bx
448 .X:
449 ; Wrap x cursor
450 cmp cx, TextBuf.Width
451 jb SetCursorPos
452 xor cx, cx
454 ;; Redraw cursor in new position
455 SetCursorPos:
456 ; Get text buffer index (it changed)
457 call GetTextBufIndex
458 ; Draw cursor by changing cell to the cursor color, but save current color for
459 ; restoring in the next iteration of the game loop.
460 mov dl, Color.Cursor
461 xchg dl, [di + 1]
463 jmp ClearCell.jmpGameLoop
465 ;; Compute the text buffer index from y and x coordinates
466 ;;
467 ;; di = &TextBuf[bx = y][cx = x]
468 ;;
469 ;; This computes the equivalent of the TextBuf.Index(y, x) macro, but at runtime
470 ;;
471 ;; Parameters:
472 ;; * bx - y coordinate
473 ;; * cx - x coordinate
474 ;; Returns:
475 ;; * di - text buffer index
476 ;; * si - caller return address
477 GetTextBufIndex:
478 xchg ax, di
479 mov al, TextBuf.Width * 2
480 imul bl
481 xchg ax, di
482 add di, cx
483 add di, cx
484 ; load caller return address in si
485 pop si
486 push si
487 ret
489 ;; Unveil a cell so it is visible on the screen
490 ;;
491 ;; Parameters:
492 ;; * di - cell pointer in text buffer
493 ;; * al - cell ASCII value
494 ;; Returns:
495 ;; * dl - written VGA color code
496 UnveilCell:
497 ; TLDR: Use xor magic to make the cells colored.
498 ;
499 ; We have three cases to consider:
500 ;
501 ; Case 1: the cell is a digit from 1-8
502 ;
503 ; The ASCII values '1', '2', '3', ..., '8' are 0x31, 0x32, 0x33, ..., 0x38. In
504 ; other words, the upper nibble is always 0x3 and the lower nibble is the
505 ; digit. We want the VGA color code to be `Color.Unveiled | digit`. For
506 ; example, the color of a '3' cell would be 0xf3.
507 ;
508 ; We can accomplish this with the formula `cell_value ^ '0' ^ Color.Unveiled`.
509 ; Xor-ing by '0' (0x30), clears the upper nibble of the cell value, leaving
510 ; just the digit value. Xor-ing again by Color.Unveiled sets the upper nibble
511 ; to 0xf, leading to the value `Color.Unveiled | digit`.
512 ;
513 ; Since xor is associative, this can be done in one operation, by xor-ing the
514 ; cell value by ('0' ^ Color.Unveiled).
515 ;
516 ; Case 2: the cell is a bomb
517 ;
518 ; We don't really care about this case as long as the bomb is visible against
519 ; the background. The bomb turns out to be green, oh well.
520 ;
521 ; Case 3: the cell is an empty space
522 ;
523 ; This ends up coloring the cell bright yellow, which isn't a big problem.
524 mov dl, al
525 xor dl, '0' ^ Color.Unveiled
526 mov [di + 1], dl
527 ret
529 ;; Flood fill empty cells
530 ;;
531 ;; Parameters:
532 ;; * bx - cell y coordinate
533 ;; * cx - cell x coordinate
534 ;; Clobbered registers:
535 ;; * ax - cell value
536 ;; * di - cell pointer in text buffer
537 Flood:
538 ; Init: get cell pointer and value
539 call GetTextBufIndex
540 mov ax, [di]
542 ; Base case: bounds check y
543 cmp bx, TextBuf.Height
544 jae .Ret
546 ; Base case: bounds check x
547 cmp cx, TextBuf.Width
548 jae .Ret
550 cmp al, '0'
552 ; Base case: we visited this cell already or bomb
553 jb .Ret
555 ; Base case: nonempty cell unveiled and stop recursion
556 jne UnveilCell
558 ; Body: unveil empty cell
559 call UnveilCell
561 ; Body: mark cell as visited and empty
562 mov byte [di], ' '
564 ; Recursive case: flood adjacent cells
566 ; Flood down
567 inc bx
568 call Flood
569 dec bx
571 ; Flood left
572 dec cx
573 call Flood
574 inc cx
576 ; Flood right
577 inc cx
578 call Flood
579 dec cx
581 ; Flood up-left
582 dec cx
583 call .Flood_up
584 inc cx
586 ; Flood up-right
587 inc cx
588 call .Flood_up
589 dec cx
591 ; Flood down-left
592 inc bx
593 dec cx
594 call Flood
595 inc cx
596 dec bx
598 ; Flood down-right
599 inc bx
600 inc cx
601 call Flood
602 dec cx
603 dec bx
605 .Flood_up:
606 ; Flood up
607 dec bx
608 call Flood
609 inc bx
611 .Ret:
612 ret
615 ;; Print program size at build time
616 %assign CodeSize $ - $$
617 %warning Code is CodeSize bytes
619 %ifdef MBR_BOOT
620 %assign PartitionTable 0x1BE
621 %if CodeSize > PartitionTable
622 %assign OverFlow CodeSize - PartitionTable
623 %error Code is OverFlow bytes too large
624 %endif
625 %endif
627 CodeEnd:
628 ; Pad to size of boot sector, minus the size of a word for the boot sector
629 ; magic value. If the code is too big to fit in a boot sector, the `times`
630 ; directive uses a negative value, causing a build error.
631 times (BootSector.Size - WordSize) - CodeSize db 0
633 ; Boot sector magic
634 dw 0xaa55