18 |
19 |
20 |
--------------------------------------------------------------------------------
/v86/main.js:
--------------------------------------------------------------------------------
1 | window.onload = function() {
2 | var startX, startY;
3 |
4 | const emulator = new V86({
5 | wasm_path: "./v86.wasm",
6 | screen_container: document.getElementById("screen_container"),
7 | bios: {
8 | url: "bios.raw",
9 | },
10 | disable_mouse: true,
11 | });
12 |
13 | emulator.add_listener("emulator-ready", () => setInterval(emulator.v86.cpu.cycle_internal, 100));
14 |
15 | document.addEventListener("touchstart", function(e) {
16 | startX = e.changedTouches[0].pageX;
17 | startY = e.changedTouches[0].pageY;
18 | }, false);
19 |
20 | document.addEventListener("touchend", function(e) {
21 | let key;
22 | const distX = e.changedTouches[0].pageX - startX;
23 | const distY = e.changedTouches[0].pageY - startY;
24 |
25 | if (distX >= 100) key = 0x4D;
26 | else if (distX <= -100) key = 0x4B;
27 | else if (distY >= 100) key = 0x50;
28 | else if (distY <= -100) key = 0x48;
29 |
30 | if (key) emulator.keyboard_send_scancodes([key]);
31 | }, false);
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # snake-bios
2 |
3 | A snake game made entirely in the BIOS.
4 |
5 | Based on my [other snake game](https://github.com/donno2048/snake).
6 |
7 | It's `114` bytes including all the code used to initialize the hardware (the rest of the BIOS is filled with zeros).
8 |
9 | ## Running
10 |
11 | I can't show a demo using QEMU because I couldn't find any online tool to imitate QEMU.
12 |
13 | That's why I'm using the [V86](https://github.com/copy/v86) x86 emulator.
14 |
15 | However, when making this game I found a couple of differences between what QEMU and V86 require.
16 |
17 | I didn't want to take the easy route so that's why the code is filled with those `%ifdef QEMU` and `%ifdef V86`.
18 |
19 | This makes it possible to compile for QEMU compatibillity, V86 compatibillity or both.
20 |
21 | ### Online demo
22 |
23 | The V86 version of the BIOS is `116` bytes (ignoring the last 4 bytes because we need them just because of a V86 bug as detailed in the comments).
24 |
25 | You can try the game in the [online demo](https://donno2048.github.io/snake-bios/).
26 |
27 | Use the arrow keys on PC or swipe on mobile.
28 |
29 | The [`libv86`](./v86/libv86.js) and [`v86`](./v86/v86.wasm) are genrated like so:
30 |
31 | ```sh
32 | cd v86
33 | git clone --depth 1 https://github.com/donno2048/v86
34 | cd v86
35 | make all
36 | cp build/libv86.js build/v86.wasm ..
37 | cd ..
38 | rm -rf v86
39 | ```
40 |
41 | ### Self-hosting
42 |
43 | ### Compiling
44 |
45 | We're compiling for QEMU only so use `-D QEMU`.
46 |
47 | ```sh
48 | nasm bios.asm -o snake.raw -D QEMU
49 | ```
50 |
51 | ### Run
52 |
53 | ```sh
54 | qemu-system-i386 -display curses -bios snake.raw -plugin contrib/plugins/libips.so,ips=2000
55 | ```
56 |
57 | For some reason the ips plugin doesn't come with QEMU so we have to build QEMU from source to use it (or go [here](#Slow)):
58 |
59 | ```sh
60 | git clone --branch stable-9.2 --depth 1 https://github.com/qemu/qemu
61 | cd qemu
62 | ./configure --target-list=i386-softmmu --enable-curses --enable-plugins --disable-docs
63 | make all
64 | ls build/
65 | ```
66 |
67 | You might need to install QEMU build dependencies:
68 |
69 | ```sh
70 | apt install gcc libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev ninja-build qemu libncurses5-dev libncursesw5-dev python3-venv python3-pip -y
71 | pip3 install tomli
72 | ```
73 |
74 | The game will take some time to initialize the hardware, then you just need to use the numpad arrows to control the snake movement.
75 |
76 | #### Numpad
77 |
78 | If you don't have a numpad you can compile with
79 |
80 | ```sh
81 | nasm bios.asm -o snake.raw -D NONUMPAD
82 | ```
83 |
84 | And then use the keypad.
85 |
86 | #### Slow
87 |
88 | If you don't want to use the QEMU ips plugin, or just want to slow the game down you can use:
89 |
90 | ```sh
91 | nasm bios.asm -o snake.raw -D SLOW=0x5000
92 | ```
93 |
94 | You can adjust the `SLOW` factor to fit your machine/emulator.
95 |
96 | #### Font
97 |
98 | To run the game in QEMU with standard graphics we have to include a font, as QEMU doesn't have it built-in.
99 |
100 | I'm using [CP437.F16](./CP437.F16), which was taken from https://github.com/viler-int10h/vga-text-mode-fonts/blob/master/FONTS/SYSTEM/DOSMIXED
101 |
102 | To compile using the font use:
103 |
104 | ```sh
105 | nasm bios.asm -o snake.raw -D FONT
106 | ```
107 |
108 | ## QR Code
109 |
110 | Here is the game as a QR Code (made with `qrencode -r <(sed 's/\x00*$//' snake.raw) -8 -o qr.png`)
111 |
112 | 
113 |
--------------------------------------------------------------------------------
/bios.asm:
--------------------------------------------------------------------------------
1 | %ifndef QEMU
2 | %ifndef V86
3 | %error must select either QEMU or V86, or both
4 | %endif
5 | %endif
6 |
7 | %ifdef FONT
8 | %define V86
9 | %endif
10 |
11 | push 0xA000 ; push start of screen buffer for game, also start of font buffer if we need a font
12 | pop es ; set ES right away for font loading, also will later be used to access screen buffer
13 | push es ; push ES with screen buffer
14 | pop ds ; set DS to screen buffer too
15 | mov dx, 0x3C0 ; port 0x3C0 writes to the attribute address register
16 | %ifdef V86
17 | mov al, 0xF4 ; enable keyboard command
18 | out 0x60, al ; sends the command to the i8042 controller
19 | mov al, 0x7 ; use 0x7 both to choose text color and the DAC corresponding to it
20 | out dx, al ; choose 0x7 ("white on black") text color
21 | out dx, al ; set it to DAC index 7, in this case, black (this inverts colors, but it's OK)
22 | %endif
23 | %ifdef QEMU
24 | mov al, 0x60 ; send 0x60 to the 8042 controller, and use later to set pallete address source bit
25 | out 0x64, al ; command 0x60 write byte to controller configuration at byte 0
26 | out 0x60, al ; write byte 0x60, disables internal clock
27 | out dx, al ; "lock" color palette by setting the palette address source bit to 1 (the 0x40 is being ignored), necessary to initiate video
28 | mov dl, 0xC4 ; port 0x3C4 writes to the sequencer registers
29 | mov ax, 0x702 ; set the value of sequencer register 2 (the map mask register) to 7
30 | out dx, ax ; don't mask any region of VGA memory
31 | %ifdef FONT
32 | mov ax, 0x404 ; set the value of sequencer register 4 (the character map select register) to 4
33 | out dx, ax ; disable spliting input into VGA regions so that we could write the font, later restore
34 | %endif
35 | mov dl, 0xCE ; port 0x3CE writes to the graphics registers
36 | mov ax, 0x1005 ; set the value of graphics register 5 (graphics mode register) to 0x10
37 | out dx, ax ; store characters as color-value pairs, not with two matrices
38 | mov ax, 0xFF08 ; set the value of graphics register 8 (byte mask) to 0xFF
39 | out dx, ax ; don't mask the bytes when writing
40 | %endif
41 | %ifdef V86
42 | mov dl, 0xC9 ; port 0x3C9 writes to the DAC data register
43 | mov al, 0x1F ; store a 0x1F byte in the first DAC entry - not needed because we can use old AL but this looks better
44 | times 3 out dx, al ; set rgb value of background to grey (rgb #1f1f1f)
45 | %endif
46 | mov dl, 0xB4 ; port 0x3B4 writes to the CRTC registers
47 | mov ax, 0x2701 ; set the value of CRTC register 1 (horizontal display end) to 0x27
48 | out dx, ax ; set the char count in each row to 0x27+1 i.e. 40
49 | xchg si, ax ; arbitrary pointer to memory location where the initial position of the snake head is stored
50 | mov ax, 0xA07 ; set the value of CTRC register 7 (the overflow register) to 0xA
51 | out dx, ax ; setting bit 1 (0x2) sets the 8th bit of vertical display end, setting bit 3 (0x8) sets bit 8 of register index 0x15 (which we set for V86)
52 | mov ax, 0x9012 ; set the value of CTRC register 0x12 (the vertical display end register) to 0x190, the set 8 bit comes from the overflow register (index 0x07)
53 | out dx, ax ; set screen height to 0x10 (character height) times 25 lines
54 | %ifdef V86
55 | mov al, 0x2 ; write 0x90 into register index 0x02 (start horizontal blanking register)
56 | out dx, ax ; disable blanking as 0x90 must be above the character clocks of a scan line as it's above the character clocks for the display
57 | mov al, 0x15 ; write 0x190 into register index 0x15 (start vertical blanking register), the set 8 bit comes from the overflow register (index 0x07)
58 | out dx, ax ; set vertical blanking register to vertical display end
59 | %endif
60 | mov ax, 0xF09 ; set the value of CTRC register 9 (the minimum scan line register) to 0xF
61 | out dx, ax ; set character height to 0xF+1 i.e. 16px
62 | %ifdef FONT
63 | push si ; save arbitrary SI
64 | mov ax, 0x1413 ; set the value of CTRC register 0x13 (the offset register) to 0x14
65 | out dx, ax ; for some reason this is not necessary without a font, set address offset between lines (chars in line = width/2 = 20) to 0x14
66 | mov si, font ; make CS:SI point to the font to enable copying
67 | xor di, di ; make ES:DI point to start of font segment
68 | mov cx, 0x100 ; copy all 0x100 characters
69 | copy_font:
70 | push cx ; save CX
71 | mov cx, 0x10 ; we write only 0x10 byte values each time to move to the next 0x20 byte character section
72 | cs rep movsb ; move from font location to VGA section
73 | add di, 0x10 ; move to next character section
74 | pop cx ; pop CX
75 | loop copy_font ; copy all characters
76 | mov dl, 0xC4 ; port 0x3C4 writes to the sequencer registers
77 | mov ax, 0x302 ; set the value of sequencer register 2 (the map mask register) to 3
78 | out dx, ax ; make font region masked
79 | mov al, 0x4 ; set the value of sequencer register 4 (the character map select register) to 4
80 | out dx, ax ; restore it to make the color-character writing method possible again
81 | pop si ; restore SI
82 | %endif
83 |
84 | mov ch, 0x3B ; override initial CX so that in initial screen clearing the entire buffer will be cleared
85 | start: ; reset game
86 | mov ax, 0x720 ; fill the screen with word 0x720 (white on black space)
87 | add ch, 0x5 ; add 0x500 to initial CX (0xFFFF) to write 0x4FF words (a little more then the screen)
88 | xor di, di ; start writing at the start of the screen
89 | rep stosw ; clear the screen
90 | dec cx ; set CX to 0xFFFF again
91 | mov di, [bx] ; reset head position, BX always points to a valid screen position containing 0x720 after setting video mode
92 | lea sp, [bp+si] ; set stack pointer (tail) to current head pointer
93 | .food: ; create new food item
94 | %ifdef V86
95 | push di ; save old DI before overwriting for randomization
96 | .rand: ; lots of code to randomize food positions is better than initializing the PIT chip
97 | xchg di, bx ; alternate BX between head position (not to iterate over the same food locations) and the end of the screen
98 | dec bh ; decreasing BH for randomization ensures BX is still divisble by 2 and if the snake isn't filling all the possible options, below 0x7D0
99 | xor [bx], cl ; place food item and check if position was empty by applying XOR with CL (assumed to be 0xFF)
100 | jp .rand ; if position was occupied by snake or wall in food generation => try again, if we came from main loop PF=0
101 | pop di ; restore actual head position
102 | %else
103 | in ax, 0x40 ; read 16 bit timer counter into AX for randomization
104 | and bx, ax ; mask with BX to make divisible by 4 and less than or equal to screen size
105 | xor [bx], cl ; place food item and check if position was empty by applying XOR with CL (assumed to be 0xFF)
106 | %endif
107 | .input: ; handle keyboard input
108 | mov bx, 0x7D0 ; initialize BX to screen size (40x25x2 bytes)
109 | jp .food ; if position was occupied by snake or wall in food generation => try again, if we came from main loop PF=0
110 | .move: ; dummy label for jumping back to input evaluation
111 | in al, 0x60 ; read scancode from keyboard controller - bit 7 is set in case key was released
112 | %ifdef NONUMPAD
113 | cmp al, 0xE0 ; if AL is the byte appended when using the keypad
114 | je .move ; ignore it
115 | %endif
116 | imul ax, BYTE 0xA ; we want to map scancodes for arrow up (0x48/0xC8), left (0x4B/0xCB), right (0x4D/0xCD), down (0x50/0xD0) to movement offsets
117 | aam 0x14 ; IMUL (AH is irrelevant here), AAM and AAD with some magic constants maps up => -80, left => -2, right => 2, down => 80
118 | aad 0x44 ; using arithmetic instructions is more compact than checks and conditional jumps
119 | cbw ; but causes weird snake movements though with other keys
120 | add di, ax ; add offset to head position
121 | cmp di, bx ; check if head crossed vertical edge by comparing against screen size in BX
122 | lodsw ; load 0x2007 into AX from off-screen screen buffer and advance head pointer
123 | adc [di], ah ; ADC head position with 0x20 to set snake character
124 | jnp start ; if it already had snake or wall in it or if it crossed a vertical edge, PF=0 from ADC => game over
125 | mov [bp+si], di ; store head position, use BP+SI to default to SS
126 | jz .food ; if food was consumed, ZF=1 from ADC => generate new food
127 | %ifdef SLOW
128 | mov cx, SLOW ; set outer slow-down loop counter
129 | .slow:
130 | push cx ; push CX to do 2 loops
131 | loop $ ; the inner empty loop
132 | pop cx ; pop CX to use it in outer loop for more slow down
133 | loop .slow ; do outer loop
134 | dec cx ; set CL=0xFF back
135 | %endif
136 | .wall: ; draw an invisible wall on the left side
137 | mov [bx], cl ; store wall character
138 | sub bx, BYTE 0x50 ; go one line backwards
139 | jns .wall ; jump to draw the next wall
140 | pop bx ; no food was consumed so pop tail position into BX
141 | mov [bx], ah ; clear old tail position on screen
142 | jnp .input ; loop to keyboard input, PF=0 from SUB
143 |
144 | %ifdef FONT
145 | font: incbin "CP437.F16" ; include the font
146 | %endif
147 |
148 | %ifdef V86
149 | times ($$-$+0xFFFC) db 0x00 ; fill with zeros
150 | nop ; this is only required because of a V86 bug (https://github.com/copy/v86/issues/1253)
151 | jmp $$ ; so I'll ignore this section for now but will remove it when the bug is fixed
152 | %else
153 | times (0x10000+$$-$) db 0x0 ; fill the rest with zeros as the BIOS needs to be 0x10000 bytes
154 | %endif
155 |
--------------------------------------------------------------------------------