├── .gitignore ├── qr.png ├── CP437.F16 ├── v86 ├── v86.wasm ├── index.html └── main.js ├── .github └── workflows │ └── deploy.yml ├── README.md └── bios.asm /.gitignore: -------------------------------------------------------------------------------- 1 | snake.raw 2 | -------------------------------------------------------------------------------- /qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donno2048/snake-bios/HEAD/qr.png -------------------------------------------------------------------------------- /CP437.F16: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donno2048/snake-bios/HEAD/CP437.F16 -------------------------------------------------------------------------------- /v86/v86.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donno2048/snake-bios/HEAD/v86/v86.wasm -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub pages 2 | on: 3 | workflow_dispatch: 4 | permissions: 5 | pages: write 6 | id-token: write 7 | concurrency: 8 | group: $GITHUB_WORKFLOW 9 | cancel-in-progress: true 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - run: | 16 | sudo apt install nasm -y 17 | nasm bios.asm -D V86 -D NONUMPAD -o v86/bios.raw 18 | - uses: actions/upload-pages-artifact@v3 19 | with: 20 | path: 'v86/' 21 | - uses: actions/deploy-pages@v4 22 | -------------------------------------------------------------------------------- /v86/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Snake 7 | 8 | 9 | 10 | 11 | 14 |
15 |
16 | 17 |
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 | ![](./qr.png) 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 | --------------------------------------------------------------------------------