;============================================================================== ; 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