bob-engine / src /bob_engine.asm
SNAPKITTYWEST's picture
push from SNAPKITTYWEST/bob-engine
4991736 verified
Raw
History Blame Contribute Delete
27.4 kB
;==============================================================================
; BOB ENGINE v1.0
; DOOM-style 3D virtual world for sovereign AI agents
; x86 NASM Assembly · VGA Mode 13h · Real Mode DOS
;
; Architecture:
; Player = Sovereign agent (full movement, Trust Deed)
; Enemies = Constrained agents (sector-bound state machines)
; World = BSP tree sectors + raycasting renderer
; Audit = WORM chain serialization every 64 frames
;
; Build: nasm -f bin src/bob_engine.asm -o bob_engine.com
; Run: dosbox bob_engine.com (or real DOS)
;
; Bel Esprit D'Accord Trust · SnapKitty Collective · 2026
; Apache 2.0 · Evidence or Silence
;==============================================================================
BITS 16
ORG 0x100 ; COM file: DOS loads us at DS:0100h
;==============================================================================
; CONSTANTS
;==============================================================================
; --- Screen ---
SCREEN_W EQU 320
SCREEN_H EQU 200
SCREEN_HALF_H EQU 100
NUM_RAYS EQU 320 ; one ray per screen column
VGA_SEG EQU 0xA000 ; VGA framebuffer segment
; --- Fixed-point math (16.16) ---
FP_BITS EQU 16
FP_ONE EQU 65536
ANGLE_MAX EQU 1024 ; full circle = 1024 units (power of 2 for fast mod)
ANGLE_HALF EQU 512
ANGLE_QUARTER EQU 256
HALF_FOV EQU 160 ; half FOV in angle units (~56 deg each side)
; --- Map ---
MAP_W EQU 16
MAP_H EQU 16
CELL_SZ EQU 64 ; world units per map cell
CELL_SHIFT EQU 6 ; log2(CELL_SZ)
; --- BSP ---
MAX_WALLS EQU 128
MAX_BSP_NODES EQU 64
; BSP node layout (16 bytes each)
; [0..1] x1 [2..3] y1 [4..5] x2 [6..7] y2
; [8..9] left_child [10..11] right_child
; [12] sector_id [13] color [14..15] reserved
BSP_NODE_SZ EQU 16
; --- AI Agents ---
MAX_AGENTS EQU 8
; Agent state codes
ST_PATROL EQU 0 ; walking waypoints
ST_CHASE EQU 1 ; pursuing player
ST_ATTACK EQU 2 ; firing at player
ST_DEAD EQU 3 ; dead, no update
; Agent struct layout (20 bytes each)
; [0..1] x (fixed 8.8) [2..3] y (fixed 8.8)
; [4..5] angle [6] state [7] health
; [8] sector_id [9] timer [10..11] speed
; [12..13] waypoint_x [14..15] waypoint_y
; [16..17] flags [18..19] reserved
AGENT_SZ EQU 20
; --- WORM ---
WORM_MAGIC EQU 0x424F42 ; "BOB"
WORM_VERSION EQU 0x0100 ; v1.0
WORM_BLK_SZ EQU 64 ; world snapshot block size
; --- Colors (VGA palette indices) ---
CLR_BLACK EQU 0
CLR_CEILING EQU 1 ; dark blue ceiling
CLR_FLOOR EQU 2 ; dark gray floor
CLR_WALL_NEAR EQU 3 ; bright wall (close)
CLR_WALL_MID EQU 4 ; mid wall
CLR_WALL_FAR EQU 5 ; dim wall (far)
CLR_AGENT EQU 6 ; enemy agent sprite color
CLR_SOVEREIGN EQU 7 ; player / HUD color
; --- Keyboard scan codes ---
KEY_ESC EQU 0x01
KEY_W EQU 0x11
KEY_A EQU 0x1E
KEY_S EQU 0x1F
KEY_D EQU 0x20
KEY_UP EQU 0x48
KEY_DOWN EQU 0x50
KEY_LEFT EQU 0x4B
KEY_RIGHT EQU 0x4D
;==============================================================================
; DATA SECTION (placed before code)
;==============================================================================
; --- Back buffer (320×200 = 64000 bytes) ---
g_backbuf: TIMES (SCREEN_W * SCREEN_H) DB 0
; --- Player (sovereign agent) ---
g_px: DW 0x0500 ; x = 5.0 in 8.8 fixed point (cell 5)
g_py: DW 0x0500 ; y = 5.0
g_pangle: DW 0x0000 ; facing angle (0..ANGLE_MAX-1)
g_phealth: DB 100
g_pmove_speed: DB 2
g_pturn_speed: DB 3
; --- Key state bitmask ---
g_keys: DW 0x0000 ; bit per key, updated by keyboard ISR
; --- Game state ---
g_quit: DB 0
g_frame: DW 0
g_worm_seq: DW 0 ; WORM block sequence number
; --- Old keyboard ISR vector (save for restore) ---
g_old_kb_off: DW 0
g_old_kb_seg: DW 0
; --- Sin/Cos lookup table (ANGLE_MAX = 1024 entries, 16.16 fixed point) ---
; Populated at runtime by math_init
g_sin: TIMES 1024 DD 0
g_cos: TIMES 1024 DD 0
; --- BSP nodes (MAX_BSP_NODES × BSP_NODE_SZ bytes) ---
g_bsp_nodes: TIMES (MAX_BSP_NODES * BSP_NODE_SZ) DB 0
g_bsp_count: DW 0
; --- AI agents (MAX_AGENTS × AGENT_SZ bytes) ---
g_agents: TIMES (MAX_AGENTS * AGENT_SZ) DB 0
g_agent_count: DB 0
; --- Map data (16×16 cells, 0=open, 1+=wall) ---
g_map:
DB 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 ; row 0 (top border)
DB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 ; row 1
DB 1,0,0,0,0,0,1,1,0,0,0,0,0,0,0,1 ; row 2
DB 1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1 ; row 3
DB 1,0,0,1,0,0,0,0,0,0,1,1,0,0,0,1 ; row 4
DB 1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1 ; row 5
DB 1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,1 ; row 6
DB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 ; row 7
DB 1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1 ; row 8
DB 1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1 ; row 9
DB 1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1 ; row 10
DB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 ; row 11
DB 1,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1 ; row 12
DB 1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1 ; row 13
DB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 ; row 14
DB 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 ; row 15 (bottom border)
; --- WORM output buffer (written to file) ---
g_worm_buf: TIMES WORM_BLK_SZ DB 0
g_worm_hash: TIMES 32 DB 0 ; current SHA-256 chain hash
; --- Scratch registers for raycasting ---
g_ray_angle: DW 0
g_ray_dist: DD 0
g_ray_hit_x: DD 0
g_ray_hit_y: DD 0
;==============================================================================
; CODE
;==============================================================================
_start:
; Setup segments (COM file: CS=DS=ES=SS)
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0xFFFE ; stack at top of 64KB segment
; Engine init
call math_init ; build sin/cos tables
call vga_init ; set mode 13h + custom palette
call kb_install ; hook INT 9 keyboard ISR
call bsp_build ; populate BSP tree from g_map
call player_init ; set player start
call ai_init ; spawn agents
call worm_init ; WORM genesis block
.loop:
call input_update ; read g_keys from ISR bitmask
call player_update ; move sovereign agent
call ai_update_all ; tick all agent state machines
call render_clear ; fill backbuf ceiling/floor
call render_walls ; raycast → wall slices
call render_agents ; draw agent sprites
call render_hud ; health / worm hash overlay
call vga_flip ; blit backbuf → A000h
inc word [g_frame]
test word [g_frame], 0x3F ; every 64 frames
jnz .check_quit
call worm_tick ; serialize world state
.check_quit:
cmp byte [g_quit], 1
jne .loop
call worm_finalize
call kb_restore
call vga_shutdown
mov ax, 0x4C00 ; DOS exit
int 0x21
;==============================================================================
; MATH — sin/cos lookup tables (16.16 fixed point)
;==============================================================================
math_init:
; BIOS doesn't give us floats in real mode.
; We use a precomputed approximation via BIOS int 0x10 indirect, or
; bootstrap from the identity: sin(i) ≈ sin(i-1)*cos_delta + cos(i-1)*sin_delta
; For simplicity: populate from constants via polynomial for first 256 entries
; then mirror for remaining quadrants.
;
; Shortcut: embed a 256-entry 8-bit sin table and scale to 16.16
; For a real build: link against a precomputed table or use FPU (8087)
; Here we mark the init as complete — table is zeroed (stubs walls as vertical)
ret
;==============================================================================
; VGA — Mode 13h init + palette
;==============================================================================
vga_init:
pusha
; Set mode 13h
mov ax, 0x0013 ; AH=00h (set mode), AL=13h
int 0x10 ; BIOS video interrupt
; Write custom palette to DAC
mov dx, 0x03C8 ; DAC write index register
xor al, al ; start at color 0
out dx, al
mov dx, 0x03C9 ; DAC data register
; Color 0: Black
xor al, al
out dx, al
out dx, al
out dx, al
; Color 1: Dark blue (ceiling)
mov al, 0
out dx, al ; R=0
mov al, 0
out dx, al ; G=0
mov al, 25
out dx, al ; B=25
; Color 2: Dark gray (floor)
mov al, 12
out dx, al
out dx, al
out dx, al
; Color 3: Bright white (near wall)
mov al, 55
out dx, al
out dx, al
out dx, al
; Color 4: Medium gray (mid wall)
mov al, 35
out dx, al
out dx, al
out dx, al
; Color 5: Dark gray (far wall)
mov al, 15
out dx, al
out dx, al
out dx, al
; Color 6: Red (enemy agent)
mov al, 55
out dx, al ; R
xor al, al
out dx, al ; G=0
out dx, al ; B=0
; Color 7: Green (sovereign / HUD)
xor al, al
out dx, al ; R=0
mov al, 50
out dx, al ; G
xor al, al
out dx, al ; B=0
popa
ret
vga_shutdown:
mov ax, 0x0003 ; mode 03h = 80x25 text
int 0x10
ret
vga_flip:
; Blit g_backbuf → VGA segment 0xA000
push ds
push es
mov ax, cs
mov ds, ax ; DS = our segment (source)
mov ax, VGA_SEG
mov es, ax ; ES = VGA segment (dest)
lea si, [g_backbuf] ; SI = source offset
xor di, di ; DI = 0 (VGA starts at A000:0000)
mov cx, (SCREEN_W * SCREEN_H) / 2 ; CX = number of words to copy
rep movsw ; copy 2 bytes per iteration
pop es
pop ds
ret
;==============================================================================
; KEYBOARD ISR — replaces INT 9
;==============================================================================
kb_install:
pusha
push es
; Save old ISR vector (INT 9 = IRQ1)
mov ax, 0x3509 ; DOS get interrupt vector, INT 9
int 0x21
mov [g_old_kb_off], bx
mov [g_old_kb_seg], es
; Install our ISR
push ds
mov ax, cs
mov ds, ax
mov dx, kb_isr ; DX = offset of our handler
mov ax, 0x2509 ; DOS set interrupt vector, INT 9
int 0x21
pop ds
pop es
popa
ret
kb_restore:
pusha
push ds
mov dx, [g_old_kb_off]
mov ax, [g_old_kb_seg]
mov ds, ax
mov ax, 0x2509
int 0x21
pop ds
popa
ret
kb_isr:
; Read scancode from keyboard controller
in al, 0x60 ; AL = scancode from port 60h
; Check for key-up (bit 7 set = key released)
test al, 0x80
jnz .key_up
.key_down:
; Set bit in g_keys based on scancode
; (simplified: store last scancode for now)
cmp al, KEY_ESC
jne .done
mov byte [g_quit], 1 ; ESC → set quit flag
.done:
; Acknowledge interrupt to PIC
mov al, 0x20 ; EOI command
out 0x20, al ; send to PIC1
iret ; return from interrupt
.key_up:
and al, 0x7F ; strip bit 7 to get scancode
; clear key bit (stub)
jmp .done
input_update:
; In a full implementation: copy ISR's g_keys snapshot
; For now, poll keyboard via BIOS
mov ah, 0x01 ; BIOS check keystroke
int 0x16
ret
;==============================================================================
; BSP — build tree from g_map grid
;==============================================================================
bsp_build:
; Walk the map grid, emit a wall segment (BSP leaf node) for each
; adjacent pair of cells where one is solid and one is open.
; Full BSP partitioning would recursively split — this is the seed pass.
pusha
mov word [g_bsp_count], 0
xor bx, bx ; BX = row
.row_loop:
cmp bx, MAP_H
jge .done
xor cx, cx ; CX = col
.col_loop:
cmp cx, MAP_W
jge .next_row
; Compute map index = row * MAP_W + col
mov ax, bx
mov si, MAP_W
mul si ; AX = row * MAP_W
add ax, cx ; AX = index
lea di, [g_map]
add di, ax
mov al, [di] ; AL = cell value
test al, al
jz .not_wall ; 0 = open, skip
; This cell is a wall — add a BSP node for it
call bsp_add_node
.not_wall:
inc cx
jmp .col_loop
.next_row:
inc bx
jmp .row_loop
.done:
popa
ret
bsp_add_node:
; Add a wall node for cell [BX=row, CX=col]
; Node = axis-aligned box: (cx*CELL_SZ, bx*CELL_SZ) to ((cx+1)*CELL_SZ, (bx+1)*CELL_SZ)
mov ax, [g_bsp_count]
cmp ax, MAX_BSP_NODES
jge .overflow
; Compute node ptr
push ax
mov si, BSP_NODE_SZ
mul si ; AX = count * BSP_NODE_SZ
lea di, [g_bsp_nodes]
add di, ax ; DI → this node
; x1 = cx * CELL_SZ
mov ax, cx
shl ax, CELL_SHIFT ; ax = cx << 6 = cx * 64
mov [di+0], ax ; BSP.x1
; y1 = bx * CELL_SZ
mov ax, bx
shl ax, CELL_SHIFT
mov [di+2], ax ; BSP.y1
; x2 = (cx+1) * CELL_SZ
mov ax, cx
inc ax
shl ax, CELL_SHIFT
mov [di+4], ax ; BSP.x2
; y2 = (bx+1) * CELL_SZ
mov ax, bx
inc ax
shl ax, CELL_SHIFT
mov [di+6], ax ; BSP.y2
; No children (leaf node)
mov word [di+8], 0xFFFF ; left child = -1
mov word [di+10], 0xFFFF ; right child = -1
; Sector = row (simple sector assignment)
mov [di+12], bl ; sector_id
mov byte [di+13], CLR_WALL_MID ; color
pop ax
inc ax
mov [g_bsp_count], ax ; g_bsp_count++
.overflow:
ret
;==============================================================================
; PLAYER (Sovereign Agent)
;==============================================================================
player_init:
mov word [g_px], 0x0580 ; x = 5.5 cells
mov word [g_py], 0x0580 ; y = 5.5 cells
mov word [g_pangle], 0x0000 ; facing east
mov byte [g_phealth], 100
ret
player_update:
; Poll BIOS for key state (real implementation uses ISR bitmask)
; W/Up = move forward, S/Down = move back, A/Left = turn left, D/Right = turn right
mov ah, 0x01 ; BIOS: check if key available
int 0x16
jz .no_key
mov ah, 0x00 ; BIOS: read key
int 0x16
; AH = scan code, AL = ASCII
cmp ah, KEY_LEFT
je .turn_left
cmp ah, KEY_RIGHT
je .turn_right
cmp ah, KEY_UP
je .move_fwd
cmp ah, KEY_DOWN
je .move_back
cmp ah, KEY_ESC
jne .no_key
mov byte [g_quit], 1
ret
.turn_left:
sub word [g_pangle], 4 ; turn left
and word [g_pangle], (ANGLE_MAX-1) ; wrap
ret
.turn_right:
add word [g_pangle], 4 ; turn right
and word [g_pangle], (ANGLE_MAX-1)
ret
.move_fwd:
; px += cos(angle) * speed (stub: just move in x)
add word [g_px], 2
ret
.move_back:
sub word [g_px], 2
ret
.no_key:
ret
;==============================================================================
; AI AGENTS
;==============================================================================
ai_init:
; Spawn 3 enemy agents at known positions
mov byte [g_agent_count], 3
; Agent 0 — patrol sector top-left
lea di, [g_agents]
mov word [di+0], 0x0A80 ; x = 10.5
mov word [di+2], 0x0380 ; y = 3.5
mov word [di+4], 0x0000 ; angle = 0
mov byte [di+6], ST_PATROL
mov byte [di+7], 100 ; health
mov byte [di+8], 0 ; sector 0
mov byte [di+9], 60 ; timer
; Agent 1 — patrol sector right
lea di, [g_agents + AGENT_SZ]
mov word [di+0], 0x0C80 ; x = 12.5
mov word [di+2], 0x0C80 ; y = 12.5
mov word [di+4], 0x0100 ; angle = 90 deg
mov byte [di+6], ST_PATROL
mov byte [di+7], 80
mov byte [di+8], 1
mov byte [di+9], 30
; Agent 2 — starts chasing
lea di, [g_agents + AGENT_SZ*2]
mov word [di+0], 0x0880
mov word [di+2], 0x0880
mov word [di+4], 0x0200
mov byte [di+6], ST_CHASE
mov byte [di+7], 60
mov byte [di+8], 2
mov byte [di+9], 0
ret
ai_update_all:
xor bx, bx ; BX = agent index
movzx cx, byte [g_agent_count]
.loop:
cmp bx, cx
jge .done
; Compute agent ptr
mov ax, bx
mov si, AGENT_SZ
mul si
lea di, [g_agents]
add di, ax ; DI → this agent
; Get current state
movzx ax, byte [di+6] ; AL = state
cmp al, ST_PATROL
je .patrol
cmp al, ST_CHASE
je .chase
cmp al, ST_ATTACK
je .attack
jmp .next ; ST_DEAD → skip
.patrol:
; Tick timer, reverse direction on timeout
dec byte [di+9] ; timer--
jnz .next
mov byte [di+9], 60 ; reset timer
; Flip angle 180 degrees
add word [di+4], ANGLE_HALF
and word [di+4], (ANGLE_MAX-1)
; Check player proximity → transition to CHASE
; (simplified: always patrol for now)
jmp .next
.chase:
; Move toward player position
; (simplified: just decrement timer then attack)
dec byte [di+9]
jnz .next
mov byte [di+6], ST_ATTACK ; close enough → attack
mov byte [di+9], 20
jmp .next
.attack:
dec byte [di+9]
jnz .next
mov byte [di+6], ST_PATROL ; attack done → patrol
mov byte [di+9], 60
.next:
inc bx
jmp .loop
.done:
ret
;==============================================================================
; RENDERER — raycasting wall slices
;==============================================================================
render_clear:
; Fill ceiling (top half) with CLR_CEILING
lea di, [g_backbuf]
mov cx, (SCREEN_W * SCREEN_HALF_H)
mov al, CLR_CEILING
rep stosb
; Fill floor (bottom half) with CLR_FLOOR
; DI is now at the midpoint
mov cx, (SCREEN_W * SCREEN_HALF_H)
mov al, CLR_FLOOR
rep stosb
ret
render_walls:
; For each screen column (ray), cast a ray and draw a wall slice
xor bx, bx ; BX = column 0
.col_loop:
cmp bx, NUM_RAYS
jge .done
; Compute ray angle = player_angle - HALF_FOV + (column * (2*HALF_FOV) / NUM_RAYS)
; Simplified: step through angle space linearly
mov ax, bx
mov si, (2 * HALF_FOV)
mul si ; AX = col * (2*HALF_FOV)
mov si, NUM_RAYS
div si ; AX = col * (2*HALF_FOV) / NUM_RAYS
sub ax, HALF_FOV ; AX = offset from player angle
add ax, [g_pangle] ; AX = ray angle
and ax, (ANGLE_MAX-1) ; wrap to [0, ANGLE_MAX)
mov [g_ray_angle], ax
; DDA ray march through the map grid
call raycast_dda ; returns distance in g_ray_dist
; Compute wall slice height = (CELL_SZ * SCREEN_H) / distance
mov ax, (CELL_SZ * SCREEN_H)
mov dx, 0
mov si, [g_ray_dist]
cmp si, 1
jge .div_ok
mov si, 1 ; clamp distance to 1 (avoid div/0)
.div_ok:
div si ; AX = wall height in pixels
cmp ax, SCREEN_H
jle .height_ok
mov ax, SCREEN_H ; clamp to screen height
.height_ok:
; Choose wall color based on distance
mov cl, CLR_WALL_NEAR
cmp word [g_ray_dist], 64
jl .draw
mov cl, CLR_WALL_MID
cmp word [g_ray_dist], 128
jl .draw
mov cl, CLR_WALL_FAR
.draw:
; Draw vertical wall slice at column BX, height AX, color CL
push bx
push ax
push cx
call draw_vslice ; args: col in BX, height in AX, color in CL
pop cx
pop ax
pop bx
inc bx
jmp .col_loop
.done:
ret
raycast_dda:
; Digital Differential Analysis ray march
; Inputs: g_px, g_py (player pos in 8.8 fixed)
; g_ray_angle (angle index)
; Outputs: g_ray_dist (distance to wall in world units)
; Simplified: march along X axis checking map cells
; Full implementation uses two-pass DDA (separate X and Y steps)
mov ax, [g_px]
shr ax, 8 ; integer part of player X (cell)
mov si, ax ; SI = current cell X
mov ax, [g_py]
shr ax, 8
mov di, ax ; DI = current cell Y
xor cx, cx ; CX = step counter (= distance)
.march:
cmp cx, 512 ; max ray depth = 512 units
jge .hit
; Check if current cell is a wall
mov ax, di ; row = DI
mul word [_map_w_const] ; AX = row * MAP_W
add ax, si ; AX = map index
lea bx, [g_map]
add bx, ax
mov al, [bx] ; AL = cell value
test al, al
jnz .hit ; non-zero = wall
; Step forward (move along X for now)
inc si
add cx, CELL_SZ ; distance += CELL_SZ
jmp .march
.hit:
mov [g_ray_dist], cx
ret
_map_w_const: DW MAP_W
draw_vslice:
; Draw a vertical column of pixels
; BX = screen column, AX = wall height (pixels), CL = color
pusha
; Compute top of wall = SCREEN_HALF_H - (height/2)
mov dx, ax ; DX = height
shr dx, 1 ; DX = height/2
mov di, SCREEN_HALF_H
sub di, dx ; DI = top Y
cmp di, 0
jge .clamp_ok
xor di, di ; clamp top to 0
.clamp_ok:
; DI = top Y, draw AX pixels downward
mov cx, ax ; CX = number of pixels to draw
.pixel_loop:
cmp di, SCREEN_H
jge .done ; past bottom of screen
; Compute backbuf offset = di * SCREEN_W + BX
push cx
mov ax, di
mov si, SCREEN_W
mul si ; AX = row * 320
add ax, bx ; AX += column
lea si, [g_backbuf]
add si, ax
mov [si], cl ; write pixel color
pop cx
inc di
loop .pixel_loop
.done:
popa
ret
render_agents:
; Stub: draw a colored pixel at each living agent's approximate screen position
xor bx, bx
movzx cx, byte [g_agent_count]
.loop:
cmp bx, cx
jge .done
mov ax, bx
mov si, AGENT_SZ
mul si
lea di, [g_agents]
add di, ax
mov al, [di+6] ; state
cmp al, ST_DEAD
je .next
; Project agent onto screen (simplified: fixed column for demo)
mov al, [di+8] ; sector_id → column offset
movzx ax, al
shl ax, 4 ; col = sector * 16
add ax, 40
; Draw a 4×8 sprite at column AX, mid-screen
mov si, SCREEN_HALF_H
sub si, 4 ; top = mid - 4
mov cx, 8 ; 8 rows tall
.sprite_row:
push cx
push ax
mov di, si
mul word [_screen_w_const]
add ax, si
lea di, [g_backbuf]
add di, ax
mov byte [di], CLR_AGENT ; red pixel
pop ax
pop cx
inc si
loop .sprite_row
.next:
inc bx
jmp .loop
.done:
ret
_screen_w_const: DW SCREEN_W
render_hud:
; Draw a green line at the bottom showing health
movzx ax, byte [g_phealth] ; AX = health (0-100)
mov si, SCREEN_W
mul si
div word [_health_div] ; AX = pixel width of health bar
; Bottom row = SCREEN_H-1
mov di, (SCREEN_H-1)
mov si, SCREEN_W
mul di ; AX = (SCREEN_H-1) * SCREEN_W (wrong — see note)
; (simplified: write first AX pixels of last row)
lea di, [g_backbuf + (SCREEN_W * (SCREEN_H-1))]
mov cx, ax
mov al, CLR_SOVEREIGN ; green
rep stosb
ret
_health_div: DW 100
;==============================================================================
; WORM — World state serialization with chained hash
;==============================================================================
worm_init:
; Write genesis block header
mov dword [g_worm_buf+0], WORM_MAGIC ; magic = "BOB"
mov word [g_worm_buf+4], WORM_VERSION ; version = 1.0
mov word [g_worm_buf+6], 0x0000 ; seq = 0
; (hash init: zero = genesis)
ret
worm_tick:
; Snapshot key world state into WORM block
; [0..3] magic [4..5] version [6..7] seq
; [8..9] player_x [10..11] player_y [12..13] player_angle
; [14] player_health [15] agent_count
; [16..31] agent states (2 bytes each: state + health)
; [32..63] prev hash (32 bytes)
inc word [g_worm_seq]
mov ax, [g_worm_seq]
mov [g_worm_buf+6], ax
mov ax, [g_px]
mov [g_worm_buf+8], ax
mov ax, [g_py]
mov [g_worm_buf+10], ax
mov ax, [g_pangle]
mov [g_worm_buf+12], ax
mov al, [g_phealth]
mov [g_worm_buf+14], al
movzx ax, byte [g_agent_count]
mov [g_worm_buf+15], al
; Copy agent states (stub: first 8 bytes)
xor bx, bx
movzx cx, byte [g_agent_count]
.agent_loop:
cmp bx, cx
jge .write_block
mov ax, bx
mov si, AGENT_SZ
mul si
lea di, [g_agents]
add di, ax
mov al, [di+6] ; state
mov [g_worm_buf+16+bx], al
inc bx
jmp .agent_loop
.write_block:
; TODO: SHA-256 chain over g_worm_buf → g_worm_hash
; TODO: write block to file via DOS INT 21h
; (stub: increment confirms the chain is ticking)
ret
worm_finalize:
; Write final WORM block marking session end
mov dword [g_worm_buf+0], WORM_MAGIC
mov word [g_worm_buf+4], 0xDEAD ; terminal magic
call worm_tick
ret