diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a8a0dcec4472004b260c13b9ce9cf029f4d66041 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.bin diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..88b3f1f86abd49522a1ff1aac9451edc36064a86 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +AS = fasm +QEMU = qemu-system-i386 +SRC = invaders.asm +BIN = invaders.bin + +all: $(BIN) + +$(BIN): $(SRC) + $(AS) $(SRC) $(BIN) + +run: $(BIN) + $(QEMU) -drive format=raw,file=$(BIN) + +clean: + rm -f $(BIN) + +.PHONY: all run clean diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..bbbc98f0d334e08a1839ec72705fbbefef9f9964 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# BootInvaders + +A functional Space Invaders clone implemented entirely within a 512-byte x86 +boot sector. Written in assembly (FASM) for the BIOS real mode. + +> [!IMPORTANT] +> This is for educational purposes, demonstrating how to make a bootloader do +> interesting stuff. + +## Technical Specifications + +- **Architecture:** x86 (16-bit Real Mode) +- **Memory Address:** Loaded at `0x0000:0x7C00` by the BIOS. +- **Display:** VGA Mode 13h (320x200, 256-color linear framebuffer). +- **Video Memory:** Direct writes to segment `0xA000`. +- **I/O & Interrupts:** + - `int 0x10`: Video services for mode setting and character output. + - `int 0x16`: Keyboard services for polling and reading scan codes. + - `int 0x1A`: Clock services used for frame timing and pseudo-random seed. + - `0x3DA`: VGA Status Register polling for vertical retrace synchronization. + +## Features + +- **Double Buffering Simulation:** Clears and redraws every frame synchronized + with the vertical retrace to prevent flickering. +- **Physics & Collision:** AABB (Axis-Aligned Bounding Box) collision detection + for bullets, enemies, and player. +- **Procedural Respawn:** Uses the system timer's lower bits to calculate the + next enemy X-coordinate. +- **Score Tracking:** Real-time score display using a custom decimal-to-string + conversion routine. + +## Building and Running + +### Prerequisites +- [FASM](https://flatassembler.net/) (Flat Assembler) +- [QEMU](https://www.qemu.org/) (for emulation) + +### Commands +```bash +# Compile the source to a 512-byte binary +make + +# Run the binary in QEMU as a raw disk image +make run +``` + +## Controls +- **Left Arrow:** Move Left +- **Right Arrow:** Move Right +- **Space Bar:** Fire Bullet +- **Any Key:** Restart (on Game Over) + +## Resources + +- [x86 Instruction Set Reference](https://c9x.me/x86/) +- [OS Dev Wiki](https://wiki.osdev.org/Expanded_Main_Page) +- [Interrupt Jump Table (Ralf Brown's List)](https://www.ctyme.com/intr/int.htm) diff --git a/invaders.asm b/invaders.asm new file mode 100644 index 0000000000000000000000000000000000000000..fb77d245e46ef1d13917ebf24516efd81bc3a115 --- /dev/null +++ b/invaders.asm @@ -0,0 +1,259 @@ +org 0x7C00 + +; Entry point: BIOS loads this at 0x7C00 and jumps here +start: + ; Set video mode to 13h: 320x200 pixels, 256 colors + mov ax, 0x0013 + int 0x10 + + ; ES = 0xA000 is the segment for VGA memory + push 0xA000 + pop es + + ; Clear AX and set Data Segment to 0 + xor ax, ax + mov ds, ax + + ; Initialize game variables + mov word [player_x], 155 + mov word [enemy_x], 100 + mov word [enemy_y], 0 + mov byte [bullet_active], 0 + mov word [score], 0 + +game_loop: + ; Use BIOS timer (ticks at 18.2Hz) for consistent game speed + mov ah, 0x00 + int 0x1A + mov bx, dx + +timer_wait: + int 0x1A + cmp dx, bx + je timer_wait + + ; Wait for vertical retrace to prevent screen flickering + mov dx, 0x3DA + +retrace_wait: + in al, dx + test al, 8 + jz retrace_wait + + ; Clear the entire screen (64000 pixels) with color 0 (black) + xor di, di + xor al, al + mov cx, 64000 + rep stosb + + ; Move cursor to (0,0) and print the current score + xor dx, dx + mov ah, 0x02 + xor bh, bh + int 0x10 + mov ax, [score] + call print_num + + ; Check if a key is waiting in the keyboard buffer + mov ah, 0x01 + int 0x16 + jz update_physics + + ; Get the actual key pressed + mov ah, 0x00 + int 0x16 + + ; Handle Arrow keys (scan codes) and Space bar + cmp ah, 0x4B ; Left Arrow + je move_left + cmp ah, 0x4D ; Right Arrow + je move_right + cmp ah, 0x39 ; Space Bar + je fire_bullet + jmp update_physics + +move_left: + sub word [player_x], 10 + jnc update_physics + mov word [player_x], 0 + jmp update_physics + +move_right: + add word [player_x], 10 + cmp word [player_x], 310 + jbe update_physics + mov word [player_x], 310 + jmp update_physics + +fire_bullet: + ; Allow only one bullet on screen at a time + cmp byte [bullet_active], 0 + jnz update_physics + mov byte [bullet_active], 1 + mov ax, [player_x] + add ax, 4 ; Center bullet relative to player ship + mov [bullet_x], ax + mov word [bullet_y], 175 + +update_physics: + ; Process bullet movement if active + cmp byte [bullet_active], 0 + jz update_enemy + sub word [bullet_y], 10 + jnc bullet_in_bounds + mov byte [bullet_active], 0 + jmp update_enemy + +bullet_in_bounds: + ; Collision check: Bullet vs Enemy (Bounding box) + mov ax, [bullet_y] + sub ax, [enemy_y] + add ax, 4 + cmp ax, 19 + ja update_enemy + mov ax, [bullet_x] + sub ax, [enemy_x] + add ax, 2 + cmp ax, 17 + ja update_enemy + + ; If hit: Kill bullet, increase score, and respawn enemy + mov byte [bullet_active], 0 + inc word [score] + jmp respawn_enemy + +update_enemy: + ; Move enemy down by 2 pixels + add word [enemy_y], 2 + cmp word [enemy_y], 200 + jae respawn_enemy + + ; Collision check: Player vs Enemy + mov ax, [enemy_y] + add ax, 15 ; Enemy height + cmp ax, 185 ; Player Y start + jb draw_game + + ; Bounding box check for X axis + mov ax, [player_x] + sub ax, [enemy_x] + add ax, 10 + cmp ax, 25 + jbe game_over + +respawn_enemy: + ; Reset enemy to top at a "random" X position based on system clock + mov word [enemy_y], 0 + xor ax, ax + int 0x1A + mov ax, dx + xor dx, dx + mov bx, 305 + div bx ; Divide clock ticks by 305 to get X in screen range + mov [enemy_x], dx + +draw_game: + ; Render Player (10x5 Blue rectangle) + mov ax, 185 + mov bx, [player_x] + mov si, 10 + mov dx, 5 + mov cl, 1 ; Color Blue + call rect + + ; Render Bullet (2x4 Yellow rectangle) if active + cmp byte [bullet_active], 0 + jz skip_bullet_draw + mov ax, [bullet_y] + mov bx, [bullet_x] + mov si, 2 + mov dx, 4 + mov cl, 14 ; Color Yellow + call rect + +skip_bullet_draw: + ; Render Enemy (15x15 Red rectangle) + mov ax, [enemy_y] + mov bx, [enemy_x] + mov si, 15 + mov dx, 15 + mov cl, 4 ; Color Red + call rect + jmp game_loop + +game_over: + ; Display Game Over message in middle of screen + mov dx, 0x0C0E ; Position: Row 12, Column 14 + mov ah, 0x02 + xor bh, bh + int 0x10 + mov si, msg_gameover + +print_msg: + lodsb + test al, al + jz wait_key + mov ah, 0x0E + int 0x10 + jmp print_msg + +wait_key: + ; Wait for any key press to restart the game + mov ah, 0x00 + int 0x16 + jmp start + +; Subroutine: Convert number in AX to decimal and print to screen +print_num: + mov bx, 10 + xor cx, cx + +digit_loop: + xor dx, dx + div bx + push dx + inc cx + test ax, ax + jnz digit_loop + +char_loop: + pop ax + add al, '0' + mov ah, 0x0E + int 0x10 + loop char_loop + ret + +; Subroutine: Draw a rectangle +; Inputs: AX=Y, BX=X, SI=Width, DX=Height, CL=Color +rect: + mov bp, dx + mov dx, 320 + mul dx + add ax, bx + mov di, ax ; Destination offset in VGA memory + mov al, cl + +draw_rect_loop: + push di + mov cx, si + rep stosb ; Fill row with color + pop di + add di, 320 ; Move to next pixel row + dec bp + jnz draw_rect_loop + ret + +; Game variables and strings +player_x dw 0 +enemy_x dw 0 +enemy_y dw 0 +bullet_active db 0 +bullet_x dw 0 +bullet_y dw 0 +score dw 0 +msg_gameover db 'GAME OVER', 0 + +; Pad file to 510 bytes and add the 0xAA55 boot signature +times 510-($-$$) db 0 +dw 0xAA55