From 3992863664de16e2db121d92c3e36526c4182ac2 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Fri, 24 Apr 2026 09:01:41 +0200 Subject: [PATCH 01/22] remove unused colors.asm const file (cherry picked from commit e6b2d8b08648bc21cea8f6f529474edd3dc10b6b) --- build.ts | 1 - src/consts/colors.asm | 9 --------- 2 files changed, 10 deletions(-) delete mode 100644 src/consts/colors.asm diff --git a/build.ts b/build.ts index e39a66d..204f386 100755 --- a/build.ts +++ b/build.ts @@ -10,7 +10,6 @@ if (Deno.args.length !== 0) { import { TextLineStream } from "jsr:@std/streams@1.1.0"; const asmFilePaths = [ - "src/consts/colors.asm", "src/consts/game.asm", "src/consts/keyboard.asm", "src/consts/map.asm", diff --git a/src/consts/colors.asm b/src/consts/colors.asm deleted file mode 100644 index a40c4af..0000000 --- a/src/consts/colors.asm +++ /dev/null @@ -1,9 +0,0 @@ -pub const colors.black = 0b000_000_00 -pub const colors.blue = 0b000_000_11 -pub const colors.green = 0b000_111_00 -pub const colors.gray = 0b010_011_10 -pub const colors.red = 0b111_000_00 -pub const colors.pink = 0b111_000_11 -pub const colors.orange = 0b111_100_00 -pub const colors.yellow = 0b111_110_00 -pub const colors.white = 0b111_111_11 From b0758c69762e436c508c71ff04976cedb1b49715 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Fri, 24 Apr 2026 09:31:04 +0200 Subject: [PATCH 02/22] change the way static RAM variable are declared and used (cherry picked from commit 6cdb7304c23040726721844df4db855073f53a21) --- build.ts | 10 +++++++- src/consts/offsets.asm | 16 ------------- src/lib/drawing.asm | 2 +- src/main.asm | 52 +++++++++++++++++++----------------------- src/reserved_space.asm | 22 ++++++++++++++++++ 5 files changed, 55 insertions(+), 47 deletions(-) delete mode 100644 src/consts/offsets.asm create mode 100644 src/reserved_space.asm diff --git a/build.ts b/build.ts index 204f386..7892581 100755 --- a/build.ts +++ b/build.ts @@ -13,7 +13,6 @@ const asmFilePaths = [ "src/consts/game.asm", "src/consts/keyboard.asm", "src/consts/map.asm", - "src/consts/offsets.asm", "src/consts/scene.asm", "src/consts/screen.asm", "src/consts/sprites.asm", @@ -24,6 +23,7 @@ const asmFilePaths = [ "src/lib/drawing.asm", "src/lib/sprite_rotation.asm", ]; +const reservedSpacePath = 'src/reserved_space.asm'; const mapWidth = 15; const spriteWidth = 5; @@ -93,6 +93,14 @@ for (const [label, path] of Object.entries(spritesPath)) { console.log(asmRawRepresentations.join("\n\n")); +console.log(` +; +; RESERVED RAM SPACE +; +`); + +console.log(await Deno.readTextFile(reservedSpacePath)) + /** * Return an ASM representation of the given data * diff --git a/src/consts/offsets.asm b/src/consts/offsets.asm deleted file mode 100644 index 53d2e4f..0000000 --- a/src/consts/offsets.asm +++ /dev/null @@ -1,16 +0,0 @@ -;const free_ram_start = 8000 - -pub const offsets.screen = 8000 ; screen size: 80x60 = 4800 - -pub const offsets.tick_duration_high = 12800 -pub const offsets.tick_duration_low = 12804 -pub const offsets.next_tick_high = 12808 -pub const offsets.next_tick_low = 128012 - - -; State: -; U8 x coord -; U8 y coord -; U8 direction -pub const offsets.state_robot = 12816 -pub const offsets.state_ghost = 12819 \ No newline at end of file diff --git a/src/lib/drawing.asm b/src/lib/drawing.asm index 1bfdf0c..1f49b78 100644 --- a/src/lib/drawing.asm +++ b/src/lib/drawing.asm @@ -77,7 +77,7 @@ get_tile_address: cmp r3, sprites.height jne get_tile_address_offset_per_row_loop - mov r13, offsets.screen + add r13, zr, ptr.screen mov r3, 0 get_tile_address_margin_top_loop: diff --git a/src/main.asm b/src/main.asm index 03923c4..312431f 100644 --- a/src/main.asm +++ b/src/main.asm @@ -9,7 +9,8 @@ mov r1, screen.mode_index screen r1, screen.mode_value mov r1, screen.offset_index -screen r1, offsets.screen +add r2, zr, ptr.screen +screen r1, r2 mov r1, screen.resolution_index screen r1, screen.resolution_value @@ -27,7 +28,7 @@ const robot_initial_direction = game.direction_down mov r1, robot_initial_x mov r2, robot_initial_y mov r5, robot_initial_direction -mov r6, offsets.state_robot +add r6, zr, ptr.state_robot store_8 [r6], r1 add r6, r6, 1 store_8 [r6], r2 @@ -46,7 +47,7 @@ const ghost_initial_direction = game.direction_down mov r1, ghost_initial_x mov r2, ghost_initial_y mov r5, ghost_initial_direction -mov r6, offsets.state_ghost +add r6, zr, ptr.state_ghost store_8 [r6], r1 add r6, r6, 1 store_8 [r6], r2 @@ -60,17 +61,10 @@ call drawing.draw_sprite_at_map_coord ; ; Time initialization ; -mov r2, game.tick_duration_1 -movt r2, r2 -movb r2, game.tick_duration_0 -mov r1, offsets.tick_duration_low -store_32 [r1], r2 - -mov r3, game.tick_duration_3 -movt r3, r3 -movb r3, game.tick_duration_2 -mov r1, offsets.tick_duration_high -store_32 [r1], r3 +add r1, zr, ptr.tick_duration_low +load_32 r2, [r1] +add r1, zr, ptr.tick_duration_high +load_32 r3, [r1] time_0 r4 time_1 r5 @@ -81,9 +75,9 @@ jae init_store_next_tick add r5, r5, 1 ; overflow init_store_next_tick: -mov r1, offsets.next_tick_low +add r1, zr, ptr.next_tick_low store_32 [r1], r4 -mov r1, offsets.next_tick_high +add r1, zr, ptr.next_tick_high store_32 [r1], r5 game_loop: @@ -125,7 +119,7 @@ game_loop: ; jmp game_loop_event_handling_end game_loop_event_handling_store_direction: - add r2, zr, offsets.state_robot + add r2, zr, ptr.state_robot add r2, r2, 2 store_8 [r2], r1 @@ -143,9 +137,9 @@ game_loop: time_0 r2 time_1 r3 - mov r1, offsets.next_tick_low + add r1, zr, ptr.next_tick_low load_32 r4, [r1] - mov r1, offsets.next_tick_high + add r1, zr, ptr.next_tick_high load_32 r5, [r1] cmp r3, r5 @@ -155,9 +149,9 @@ game_loop: jb game_loop update_next_tick: - mov r1, offsets.tick_duration_low + add r1, zr, ptr.tick_duration_low load_32 r2, [r1] - mov r1, offsets.tick_duration_high + add r1, zr, ptr.tick_duration_high load_32 r3, [r1] add r4, r4, r2 add r5, r5, r3 @@ -166,9 +160,9 @@ game_loop: add r5, r5, 1 store_next_tick: - mov r1, offsets.next_tick_low + add r1, zr, ptr.next_tick_low store_32 [r1], r4 - mov r1, offsets.next_tick_high + add r1, zr, ptr.next_tick_high store_32 [r1], r5 @@ -184,7 +178,7 @@ game_loop: ; r7 = new position address ; r8 = new position content ; r9 = jump address - mov r1, offsets.state_robot + add r1, zr, ptr.state_robot load_8 r2, [r1] add r1, r1, 1 load_8 r3, [r1] @@ -229,7 +223,7 @@ game_loop: cmp r8, map.tiles.wall jne game_loop_robot_movement_some ; hit wall - mov r1, offsets.state_robot + add r1, zr, ptr.state_robot add r1, r1, 2 mov r4, game.direction_still store_8 [r1], r4 @@ -240,7 +234,7 @@ game_loop: mov r8, map.tiles.empty store_8 [r7], r8 - mov r1, offsets.state_robot + add r1, zr, ptr.state_robot store_8 [r1], r5 add r1, r1, 1 store_8 [r1], r6 @@ -276,7 +270,7 @@ game_loop: ; r9: new position address ; r10: new position content ; r11: jump address - mov r1, offsets.state_ghost + add r1, zr, ptr.state_ghost load_8 r2, [r1] add r1, r1, 1 load_8 r3, [r1] @@ -308,7 +302,7 @@ game_loop: game_loop_ghost_movement_make_move: add r7, r2, r5 add r8, r3, r6 - mov r1, offsets.state_ghost + add r1, zr, ptr.state_ghost store_8 [r1], r7 add r1, r1, 1 store_8 [r1], r8 @@ -355,7 +349,7 @@ game_loop: cmp r10, map.tiles.wall jne game_loop_ghost_movement_end ; increment direction - mov r1, offsets.state_ghost + add r1, zr, ptr.state_ghost add r1, r1, 2 load_8 r4, [r1] add r4, r4, 1 diff --git a/src/reserved_space.asm b/src/reserved_space.asm new file mode 100644 index 0000000..6c8104c --- /dev/null +++ b/src/reserved_space.asm @@ -0,0 +1,22 @@ +; 0.5 second = 500_000_000 nano second = 0x0000_0000_1DCD_6500 +pub ptr.tick_duration_high: U32 0x0000_0000 +pub ptr.tick_duration_low: U32 0x1DCD_6500 + +; 1 second = 1_000_000_000 nano second = 0x0000_0000_3B9A_CA00 +; pub ptr.tick_duration_high: U32 0x0000_0000 +; pub ptr.tick_duration_low: U32 0x3B9A_CA00 + +pub ptr.next_tick_high: U32 0 +pub ptr.next_tick_low: U32 0 + +pub ptr.state_robot: +U8 0 ; x coord +U8 0 ; y coord +U8 0 ; direction + +pub ptr.state_ghost: +U8 0 ; x coord +U8 0 ; y coord +U8 0 ; direction + +pub ptr.screen: \ No newline at end of file From f0656aca402a0702711062acd2b662092f860d8d Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Fri, 24 Apr 2026 10:12:23 +0200 Subject: [PATCH 03/22] replace texture jump table with dispatch table (cherry picked from commit 88f10f9f0657a356eabb2682bb690265b7df78c6) --- build.ts | 2 ++ src/consts/arch.asm | 2 ++ src/init.asm | 70 ++++++++++++++++++++++++++++++++++++++++++ src/lib/drawing.asm | 39 ++++------------------- src/main.asm | 15 +-------- src/reserved_space.asm | 11 +++++++ 6 files changed, 92 insertions(+), 47 deletions(-) create mode 100644 src/consts/arch.asm create mode 100644 src/init.asm diff --git a/build.ts b/build.ts index 7892581..4902471 100755 --- a/build.ts +++ b/build.ts @@ -10,6 +10,7 @@ if (Deno.args.length !== 0) { import { TextLineStream } from "jsr:@std/streams@1.1.0"; const asmFilePaths = [ + "src/consts/arch.asm", "src/consts/game.asm", "src/consts/keyboard.asm", "src/consts/map.asm", @@ -17,6 +18,7 @@ const asmFilePaths = [ "src/consts/screen.asm", "src/consts/sprites.asm", + "src/init.asm", "src/main.asm", "src/lib/game.asm", diff --git a/src/consts/arch.asm b/src/consts/arch.asm new file mode 100644 index 0000000..9d8f400 --- /dev/null +++ b/src/consts/arch.asm @@ -0,0 +1,2 @@ +pub const arch.instruction_size = 4 +pub const arch.instruction_increment_shift = 2 \ No newline at end of file diff --git a/src/init.asm b/src/init.asm new file mode 100644 index 0000000..c5688a2 --- /dev/null +++ b/src/init.asm @@ -0,0 +1,70 @@ +; +; Jump table initialisation +; +; r1: sprite id +; r2: pointer address +; r3: pointer value + +; sprite_empty +add r1, zr, sprites.empty +lsl r2, r1, arch.instruction_increment_shift +add r2, r2, ptr.textures_addresses +add r3, zr, sprite_empty +store_32 [r2], r3 +; sprite_unimplemented +add r1, zr, sprites.unimplemented +lsl r2, r1, arch.instruction_increment_shift +add r2, r2, ptr.textures_addresses +add r3, zr, sprite_unimplemented +store_32 [r2], r3 +; sprite_wall_end +add r1, zr, sprites.wall_end +lsl r2, r1, arch.instruction_increment_shift +add r2, r2, ptr.textures_addresses +add r3, zr, sprite_wall_end +store_32 [r2], r3 +; sprite_wall_straight +add r1, zr, sprites.wall_straight +lsl r2, r1, arch.instruction_increment_shift +add r2, r2, ptr.textures_addresses +add r3, zr, sprite_wall_straight +store_32 [r2], r3 +; sprite_wall_corner +add r1, zr, sprites.wall_corner +lsl r2, r1, arch.instruction_increment_shift +add r2, r2, ptr.textures_addresses +add r3, zr, sprite_wall_corner +store_32 [r2], r3 +; sprite_robot +add r1, zr, sprites.robot +lsl r2, r1, arch.instruction_increment_shift +add r2, r2, ptr.textures_addresses +add r3, zr, sprite_robot +store_32 [r2], r3 +; sprite_ghost +add r1, zr, sprites.ghost +lsl r2, r1, arch.instruction_increment_shift +add r2, r2, ptr.textures_addresses +add r3, zr, sprite_ghost +store_32 [r2], r3 +; sprite_coin +add r1, zr, sprites.coin +lsl r2, r1, arch.instruction_increment_shift +add r2, r2, ptr.textures_addresses +add r3, zr, sprite_coin +store_32 [r2], r3 + +; +; Screen initialization +; + +; r1 = screen parameter value +mov r1, screen.mode_index +screen r1, screen.mode_value + +mov r1, screen.offset_index +add r2, zr, ptr.screen +screen r1, r2 + +mov r1, screen.resolution_index +screen r1, screen.resolution_value diff --git a/src/lib/drawing.asm b/src/lib/drawing.asm index 1f49b78..2424274 100644 --- a/src/lib/drawing.asm +++ b/src/lib/drawing.asm @@ -120,48 +120,21 @@ get_tile_address: ; r13: address ; Locals: -; r2: jump address +; r2: pointer address ; */ get_sprite_address: push r1 push r2 cmp r1, sprites.max - jbe get_sprite_address_jmp_table_init + jbe get_sprite_address_ptr mov r1, sprites.unimplemented - get_sprite_address_jmp_table_init: - lsl r2, r1, 3 ; 2*instruction_size = *8 = <<3 - add r2, r2, get_sprite_address_jmp_table_start - jmp r2 + get_sprite_address_ptr: + lsl r2, r1, arch.instruction_increment_shift + add r2, r2, ptr.textures_addresses + load_32 r13, [r2] - get_sprite_address_jmp_table_start: - ; sprite_empty - add r13, zr, sprite_empty - jmp get_sprite_address_return - ; sprite_unimplemented: - add r13, zr, sprite_unimplemented - jmp get_sprite_address_return - ; sprite_wall_end - add r13, zr, sprite_wall_end - jmp get_sprite_address_return - ; sprite_wall_straight - add r13, zr, sprite_wall_straight - jmp get_sprite_address_return - ; sprite_wall_corner - add r13, zr, sprite_wall_corner - jmp get_sprite_address_return - ; sprite_robot - add r13, zr, sprite_robot - jmp get_sprite_address_return - ; sprite_ghost - add r13, zr, sprite_ghost - jmp get_sprite_address_return - ; sprite_coin: - add r13, zr, sprite_coin - ;jmp get_sprite_address_return - - get_sprite_address_return: pop r2 pop r1 ret diff --git a/src/main.asm b/src/main.asm index 312431f..be88511 100644 --- a/src/main.asm +++ b/src/main.asm @@ -1,19 +1,6 @@ include consts/screen -; -; Screen initialization -; - -; r1 = screen parameter value -mov r1, screen.mode_index -screen r1, screen.mode_value - -mov r1, screen.offset_index -add r2, zr, ptr.screen -screen r1, r2 - -mov r1, screen.resolution_index -screen r1, screen.resolution_value +include init call drawing.draw_whole_map diff --git a/src/reserved_space.asm b/src/reserved_space.asm index 6c8104c..41ea207 100644 --- a/src/reserved_space.asm +++ b/src/reserved_space.asm @@ -1,3 +1,14 @@ +; Sprite addresses jump table +pub ptr.textures_addresses: +U32 0 ; empty +U32 0 ; placeholder +U32 0 ; wall_end +U32 0 ; wall_straight +U32 0 ; wall_corner +U32 0 ; robot +U32 0 ; ghost +U32 0 ; coin + ; 0.5 second = 500_000_000 nano second = 0x0000_0000_1DCD_6500 pub ptr.tick_duration_high: U32 0x0000_0000 pub ptr.tick_duration_low: U32 0x1DCD_6500 From 17c6b07367ba85ce9153a8dc376e56d2ff927e03 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Sat, 25 Apr 2026 05:35:01 +0200 Subject: [PATCH 04/22] add automatic padding in build script (cherry picked from commit aad1fa45f776d9e6ec86902272b3c3b0c54dc075) --- build.ts | 49 ++++++++++++++++++++++++++---------------- src/reserved_space.asm | 2 ++ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/build.ts b/build.ts index 4902471..e60928f 100755 --- a/build.ts +++ b/build.ts @@ -27,25 +27,26 @@ const asmFilePaths = [ ]; const reservedSpacePath = 'src/reserved_space.asm'; +const instructionLength = 4; // in bytes const mapWidth = 15; const spriteWidth = 5; const mapMatchingStrings = { - 0x00_00_00: 'U8 0', // Empty - 0xFF_00_00: 'U8 1', // Wall - 0xFF_FF_00: 'U8 2', // Coin + 0x00_00_00: '0', // Empty + 0xFF_00_00: '1', // Wall + 0xFF_FF_00: '2', // Coin }; const spritesMatchingStrings = { - 0x00_00_00: 'U8 0b000_000_00', - 0x00_00_FF: 'U8 0b000_000_11', - 0x00_FF_00: 'U8 0b000_111_00', - 0x77_77_77: 'U8 0b010_011_10', - 0xFF_00_00: 'U8 0b111_000_00', - 0xFF_00_FF: 'U8 0b111_000_11', - 0xFF_80_00: 'U8 0b111_100_00', - 0xFF_FF_00: 'U8 0b111_110_00', - 0xFF_FF_FF: 'U8 0b111_111_11', + 0x00_00_00: '0b000_000_00', + 0x00_00_FF: '0b000_000_11', + 0x00_FF_00: '0b000_111_00', + 0x77_77_77: '0b010_011_10', + 0xFF_00_00: '0b111_000_00', + 0xFF_00_FF: '0b111_000_11', + 0xFF_80_00: '0b111_100_00', + 0xFF_FF_00: '0b111_110_00', + 0xFF_FF_FF: '0b111_111_11', }; const mapPath = 'assets/map.ppm'; @@ -86,11 +87,11 @@ for (const path of asmFilePaths) { const asmRawRepresentations = []; const mapBytes = await getDataFromPPM(mapPath); -asmRawRepresentations.push(byteToAsmRawData('map', mapBytes, mapMatchingStrings, mapWidth)); +asmRawRepresentations.push(bytesToAsmConstU8('map', mapBytes, mapMatchingStrings, mapWidth)); for (const [label, path] of Object.entries(spritesPath)) { const bytes = await getDataFromPPM(path); - asmRawRepresentations.push(byteToAsmRawData(label, bytes, spritesMatchingStrings, spriteWidth)); + asmRawRepresentations.push(bytesToAsmConstU8(label, bytes, spritesMatchingStrings, spriteWidth)); } console.log(asmRawRepresentations.join("\n\n")); @@ -104,7 +105,9 @@ console.log(` console.log(await Deno.readTextFile(reservedSpacePath)) /** - * Return an ASM representation of the given data + * Return an ASM constant (U8) representation of the given data + * + * If needed, the representation will be padded to ensure the following code stays aligned on the instruction's length * * @param label The label to write before the first raw data * @param data An array of bytes to interprete as 24 bits values @@ -113,14 +116,18 @@ console.log(await Deno.readTextFile(reservedSpacePath)) * * @returns The ASM representation of that raw data, pre- and post-fixed by the given label */ -function byteToAsmRawData(label: string, data: Uint8Array, matchingStrings: Record, lineLength = 0): string { +function bytesToAsmConstU8(label: string, data: Uint8Array, matchingStrings: Record, lineLength = 0): string { lineLength ??= Infinity; const stringValues = []; for (let i = 0; i < data.length - 2; i += 3) { const pixelValue = (data[i] << 16) + (data[i+1] << 8) + (data[i+2]) + if (!Object.hasOwn(matchingStrings, pixelValue)) { + console.error(`No matching value defined for "${pixelValue.toString(2)}" (which is ${label}'s ${i} pixel)`); + Deno.exit(1); + } - stringValues.push(matchingStrings[pixelValue]); + stringValues.push(`U8 ${matchingStrings[pixelValue]}`); } const lines = []; @@ -131,10 +138,16 @@ function byteToAsmRawData(label: string, data: Uint8Array, matchingStrings: Reco lines.push(stringValues.slice(i, i+lineLength).join("\t")); } } + + const paddingBytes = []; + for (let i = 0; i < stringValues.length % instructionLength; i++) { + paddingBytes.push('U8 0'); + } + const paddingString = paddingBytes.length === 0 ? '' : `\n${paddingBytes.join('\t')} ; padding to preserve ${8 * instructionLength} bits alignment` return `${label}: ${lines.join("\n")} -${label}_end:` +${label}_end:${paddingString}` } /** diff --git a/src/reserved_space.asm b/src/reserved_space.asm index 41ea207..f513191 100644 --- a/src/reserved_space.asm +++ b/src/reserved_space.asm @@ -24,10 +24,12 @@ pub ptr.state_robot: U8 0 ; x coord U8 0 ; y coord U8 0 ; direction +U8 0 ; [padding] pub ptr.state_ghost: U8 0 ; x coord U8 0 ; y coord U8 0 ; direction +U8 0 ; [padding] pub ptr.screen: \ No newline at end of file From f96e2420ef5fbb5e4dc1793bd693f231dc0078e0 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Sat, 25 Apr 2026 05:36:20 +0200 Subject: [PATCH 05/22] add way to reduce build output's size (cherry picked from commit 5231f54c2765fd3f3039917496d28b2b9cdf0688) --- readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.md b/readme.md index a68efc9..72c7589 100755 --- a/readme.md +++ b/readme.md @@ -20,6 +20,9 @@ It also convert the PPM assets (map and sprites) into ASM raw values representat Usage example: ```sh ./build.ts > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm + +# script output can easily be piped to strip it off all comments, leading whitespace and empty lines: +./build.ts | sed 's/\s*;.*//' | sed 's/^\s*//' | sed '/^\d*$/d' > result.asm ``` The script is build over Deno's API, so you'll need to have a `deno` binary in your PATH to use it that way. From 2c37af8dd0d5bfe9c68e0bbf7b85c89471205881 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Sat, 25 Apr 2026 06:41:56 +0200 Subject: [PATCH 06/22] change which 8 bit color is used for dark gray (cherry picked from commit b7bc9956f9ac55ac61ade68c4d331641c19cbb81) --- assets.md | 2 +- build.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets.md b/assets.md index 7682244..6ea7497 100644 --- a/assets.md +++ b/assets.md @@ -22,7 +22,7 @@ PPM 8bits channels | In-game `RRRGGGBB` value | Name `0x00_00_00` | `000_000_00` | Black `0x00_00_FF` | `000_000_11` | Blue `0x00_FF_00` | `000_111_00` | Green -`0x77_77_77` | `010_011_10` | Gray +`0x77_77_77` | `011_011_10` | Gray `0xFF_00_00` | `111_000_00` | Red `0xFF_00_FF` | `111_000_11` | Pink `0xFF_80_00` | `111_100_00` | Orange diff --git a/build.ts b/build.ts index e60928f..97074ac 100755 --- a/build.ts +++ b/build.ts @@ -41,7 +41,7 @@ const spritesMatchingStrings = { 0x00_00_00: '0b000_000_00', 0x00_00_FF: '0b000_000_11', 0x00_FF_00: '0b000_111_00', - 0x77_77_77: '0b010_011_10', + 0x77_77_77: '0b011_011_10', 0xFF_00_00: '0b111_000_00', 0xFF_00_FF: '0b111_000_11', 0xFF_80_00: '0b111_100_00', From d413170098074ec9b047ebfcadf2177521695675 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Sat, 25 Apr 2026 06:50:58 +0200 Subject: [PATCH 07/22] add colors: light gray and brown (cherry picked from commit 356cf3fb94a77a49243b7e82255e7e42e76fe895) --- assets.md | 4 +++- build.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/assets.md b/assets.md index 6ea7497..2b4ad5f 100644 --- a/assets.md +++ b/assets.md @@ -22,7 +22,9 @@ PPM 8bits channels | In-game `RRRGGGBB` value | Name `0x00_00_00` | `000_000_00` | Black `0x00_00_FF` | `000_000_11` | Blue `0x00_FF_00` | `000_111_00` | Green -`0x77_77_77` | `011_011_10` | Gray +`0x99_50_00` | `010_001_00` | Brown +`0x77_77_77` | `011_011_10` | Dark gray +`0xB3_B3_B3` | `100_100_10` | Light gray `0xFF_00_00` | `111_000_00` | Red `0xFF_00_FF` | `111_000_11` | Pink `0xFF_80_00` | `111_100_00` | Orange diff --git a/build.ts b/build.ts index 97074ac..dc8cb41 100755 --- a/build.ts +++ b/build.ts @@ -41,7 +41,9 @@ const spritesMatchingStrings = { 0x00_00_00: '0b000_000_00', 0x00_00_FF: '0b000_000_11', 0x00_FF_00: '0b000_111_00', + 0x99_50_00: `0b010_001_00`, 0x77_77_77: '0b011_011_10', + 0xB3_B3_B3: '0b100_100_10', 0xFF_00_00: '0b111_000_00', 0xFF_00_FF: '0b111_000_11', 0xFF_80_00: '0b111_100_00', From 02e1e4c6b31067fb7a5b939a2818ed5377a56eba Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Sat, 25 Apr 2026 07:38:02 +0200 Subject: [PATCH 08/22] update branches description (cherry picked from commit 4e03aada20ccb5bbd7933a3719ab40e9a19546eb) --- readme.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 72c7589..80cab98 100755 --- a/readme.md +++ b/readme.md @@ -9,7 +9,14 @@ It thus should be able to run on the architecture and ISA the players have built The `main` branch contains a minimal implementation of the game. The following branches contains variations, allowing comparisons of performances, visuals, gameplay, ... -- `res-2`: 256x192 screen resolution, 17x17 tile +- `res-2`: + - `256x192` screen resolution + - `17x17` tile resolution +- `res-2-prerendered`: + - `256x192` screen resolution + - `17x17` tile resolution + - initial screen prerendered + - no sprite rotation ## Build script From d5e9dde21d28ef5b0418616390495274a323bba0 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Sat, 25 Apr 2026 07:35:00 +0200 Subject: [PATCH 09/22] use prerendered screen and remove sprite rotation (cherry picked from commit 3783a98f95a11c1122877f57b755a14a8b3ff713) --- assets/screen.ppm | Bin 0 -> 14457 bytes build.ts | 13 +-- src/consts/sprites.asm | 16 +--- src/init.asm | 20 +---- src/lib/drawing.asm | 139 +++++------------------------ src/lib/sprite_rotation.asm | 171 ------------------------------------ src/main.asm | 31 ++----- src/reserved_space.asm | 5 -- 8 files changed, 44 insertions(+), 351 deletions(-) create mode 100644 assets/screen.ppm delete mode 100644 src/lib/sprite_rotation.asm diff --git a/assets/screen.ppm b/assets/screen.ppm new file mode 100644 index 0000000000000000000000000000000000000000..ce01ee700878dbcc0c8075ccbf3b2a8f14671863 GIT binary patch literal 14457 zcmeH}Pm9|y6vT7Rr!e%~W!ZL@-g_uLY{^$>X($V2Teg3ow|?_fb}HNREdS*2BGxtu zCic^4Ml%vf+TCqmRBxa5AAjyYSD*e=@7~|= zK0I#UTvvD3+nd|l?dI~k%)n}A;IP@O?-^UI!}_9zM9m5&BiDIXw!SeD=S`wZY;0FB zX`ay}W#)~Xh|!rxOWNZ^7CrnGkp-4Ek>uGhs(qJHaUw>Skx*T7B1Tnd85JjDG$hR4 zM_?A7h0BR7svA^|^3%V`%!k`|rR!nyDwo}GE8dT|dT#M?Sv%%8CEvIZ<>3lM(eQo98 zS}m2Rfq^59C2(lu2(MXp%(i z#8TzZIR2zJ+n6K~JF!$bG>$*%%{C@U#7-=A8hXkm!*?WuS{pi$>C7cdSv1bl@$ErX zDO2%48M4AGRSs1bnTl;>$O^MmIaFO_Dz=d!E6h^m(BQ%xzTIU*9o9V?j5-X|1{(hx zSn4!Xw-L)7q9$^UM2d!rR;0lOHU|>10i=!tby86|Qtcb0I1!_E z7_B{ARkD>wnYoNwA6k33s$?sVGIJTVKD735RmoN!W#(E`HyO`8hq;WN)0NJpRaTTM z54W5$yJEDQt8&Vmh|zMc$|-XqMuV&FOdNhsI3vW1;#iLqaZt5h!a^<_Z+?j%@ehWbYU3P zzRRdM5u?jUs4h7XqpGxwiW9YH_N-h?hvrVu{3RBs&bz*6{C*XtV104N7B{ipm!``M KTxMYAGw=`912X6U literal 0 HcmV?d00001 diff --git a/build.ts b/build.ts index dc8cb41..9b52f5c 100755 --- a/build.ts +++ b/build.ts @@ -9,6 +9,10 @@ if (Deno.args.length !== 0) { import { TextLineStream } from "jsr:@std/streams@1.1.0"; +const instructionLength = 4; // in bytes +const mapWidth = 15; +const spriteWidth = 5; + const asmFilePaths = [ "src/consts/arch.asm", "src/consts/game.asm", @@ -23,14 +27,10 @@ const asmFilePaths = [ "src/lib/game.asm", "src/lib/drawing.asm", - "src/lib/sprite_rotation.asm", ]; +const initialScreenPath = 'assets/screen.ppm'; const reservedSpacePath = 'src/reserved_space.asm'; -const instructionLength = 4; // in bytes -const mapWidth = 15; -const spriteWidth = 5; - const mapMatchingStrings = { 0x00_00_00: '0', // Empty 0xFF_00_00: '1', // Wall @@ -104,6 +104,9 @@ console.log(` ; `); +const screenBytes = await getDataFromPPM(initialScreenPath); +console.log(bytesToAsmConstU8('screen', screenBytes, spritesMatchingStrings, 1)); + console.log(await Deno.readTextFile(reservedSpacePath)) /** diff --git a/src/consts/sprites.asm b/src/consts/sprites.asm index 8c6f485..9f6f12b 100644 --- a/src/consts/sprites.asm +++ b/src/consts/sprites.asm @@ -2,17 +2,9 @@ pub const sprites.width = 5 pub const sprites.height = 5 ; unused as sprite are all square pub const sprites.size = 25 ; height x width -pub const sprites.rotation_0 = 0 -pub const sprites.rotation_90 = 1 -pub const sprites.rotation_180 = 2 -pub const sprites.rotation_270 = 3 - pub const sprites.empty = 0 pub const sprites.unimplemented = 1 -pub const sprites.wall_end = 2 -pub const sprites.wall_straight = 3 -pub const sprites.wall_corner = 4 -pub const sprites.robot = 5 -pub const sprites.ghost = 6 -pub const sprites.coin = 7 -pub const sprites.max = 7 ; for lookup table +pub const sprites.robot = 2 +pub const sprites.ghost = 3 +pub const sprites.coin = 4 +pub const sprites.max = 4 ; for lookup table diff --git a/src/init.asm b/src/init.asm index c5688a2..06ed09b 100644 --- a/src/init.asm +++ b/src/init.asm @@ -17,24 +17,6 @@ lsl r2, r1, arch.instruction_increment_shift add r2, r2, ptr.textures_addresses add r3, zr, sprite_unimplemented store_32 [r2], r3 -; sprite_wall_end -add r1, zr, sprites.wall_end -lsl r2, r1, arch.instruction_increment_shift -add r2, r2, ptr.textures_addresses -add r3, zr, sprite_wall_end -store_32 [r2], r3 -; sprite_wall_straight -add r1, zr, sprites.wall_straight -lsl r2, r1, arch.instruction_increment_shift -add r2, r2, ptr.textures_addresses -add r3, zr, sprite_wall_straight -store_32 [r2], r3 -; sprite_wall_corner -add r1, zr, sprites.wall_corner -lsl r2, r1, arch.instruction_increment_shift -add r2, r2, ptr.textures_addresses -add r3, zr, sprite_wall_corner -store_32 [r2], r3 ; sprite_robot add r1, zr, sprites.robot lsl r2, r1, arch.instruction_increment_shift @@ -63,7 +45,7 @@ mov r1, screen.mode_index screen r1, screen.mode_value mov r1, screen.offset_index -add r2, zr, ptr.screen +add r2, zr, screen screen r1, r2 mov r1, screen.resolution_index diff --git a/src/lib/drawing.asm b/src/lib/drawing.asm index 2424274..03cbf44 100644 --- a/src/lib/drawing.asm +++ b/src/lib/drawing.asm @@ -5,53 +5,6 @@ include ../consts/scene include ../consts/sprites include sprite_rotation -; /* Draw the whole (fill the screen memory with the values of the sprites based on the map) - -; Inputs: - -; Outputs: - -; Locals: -; r1: x map coord -; r2: y map coord -; r3: sprite rotation -; r4: sprite id -; r12: rotation (returned by `sprite and rotation`) -; r13: sprite (returned by `get_sprite_id_and_rotation`) -; */ -pub drawing.draw_whole_map: - push r1 - push r2 - push r3 - push r4 - push r12 - push r13 - - mov r1, 0 - mov r2, 0 - - draw_whole_map_loop: - call sprite_rotation.get_sprite_id_and_rotation - mov r3, r12 - mov r4, r13 - call drawing.draw_sprite_at_map_coord - add r1, r1, 1 - cmp r1, map.width - jne draw_whole_map_loop - mov r1, 0 - add r2, r2, 1 - cmp r2, map.height - jne draw_whole_map_loop - - pop r13 - pop r12 - pop r4 - pop r3 - pop r2 - pop r1 - - ret - ; /* Return the top-left pixel address of the tile at the given map coordinates ; Inputs: @@ -77,7 +30,7 @@ get_tile_address: cmp r3, sprites.height jne get_tile_address_offset_per_row_loop - add r13, zr, ptr.screen + add r13, zr, screen mov r3, 0 get_tile_address_margin_top_loop: @@ -144,8 +97,7 @@ get_sprite_address: ; Inputs: ; r1: map x coord ; r2: map y coord -; r3: sprite rotation -; r4: sprite id +; r3: sprite id ; Outputs: @@ -158,22 +110,20 @@ pub drawing.draw_sprite_at_map_coord: push r1 push r2 push r3 - push r4 push r13 call get_tile_address - mov r1, r13 - push r1 + push r13 - mov r1, r4 + mov r1, r3 call get_sprite_address - mov r2, r13 - pop r1 + mov r2, r13 + pop r13 + mov r1, r13 call draw_sprite pop r13 - pop r4 pop r3 pop r2 pop r1 @@ -184,18 +134,15 @@ pub drawing.draw_sprite_at_map_coord: ; Inputs: ; r1: screen address ; r2: sprite address -; r3: rotation ; Outputs: ; Locals: ; r1: current screen pixel address -; r4: x -; r5: y -; r6: current sprite pixel address -; r7: color -; r8: x increment -; r9: y increment +; r2: current sprite pixel address +; r3: x +; r4: color +; r5: sprite last pixel address + 1 ; */ draw_sprite: push r1 @@ -203,67 +150,25 @@ draw_sprite: push r3 push r4 push r5 - push r6 - push r7 - push r8 - push r9 - - mov r6, r2 - - ; rotation - draw_sprite_cmp_rotation_0: - cmp r3, sprites.rotation_0 - jne draw_sprite_cmp_rotation_90 - mov r8, 1 - mov r9, 0 - jmp draw_sprite_loop_init - draw_sprite_cmp_rotation_90: - cmp r3, sprites.rotation_90 - jne draw_sprite_cmp_rotation_180 - add r6, r6, sprites.size - sub r6, r6, sprites.width - sub r8, zr, sprites.width - mov r9, sprites.size - add r9, r9, 1 - jmp draw_sprite_loop_init - draw_sprite_cmp_rotation_180: - cmp r3, sprites.rotation_180 - jne draw_sprite_cmp_rotation_270 - add r6, r6, sprites.size - sub r6, r6, 1 - sub r8, zr, 1 - mov r9, 0 - jmp draw_sprite_loop_init - draw_sprite_cmp_rotation_270: - add r6, r6, sprites.width - sub r6, r6, 1 - mov r8, sprites.width - sub r9, zr, sprites.size - sub r9, r9, 1 - draw_sprite_loop_init: - mov r4, 0 - mov r5, 0 + add r5, r2, sprites.size + + mov r3, 0 draw_sprite_loop: - load_8 r7, [r6] - store_8 [r1], r7 - add r4, r4, 1 + load_8 r4, [r2] + store_8 [r1], r4 + add r1, r1, 1 - add r6, r6, r8 - cmp r4, sprites.width + add r2, r2, 1 + add r3, r3, 1 + cmp r3, sprites.width jne draw_sprite_loop - mov r4, 0 - add r6, r6, r9 + mov r3, 0 add r1, r1, screen.width sub r1, r1, sprites.width - add r5, r5, 1 - cmp r5, sprites.width + cmp r5, r2 jne draw_sprite_loop - pop r9 - pop r8 - pop r7 - pop r6 pop r5 pop r4 pop r3 diff --git a/src/lib/sprite_rotation.asm b/src/lib/sprite_rotation.asm deleted file mode 100644 index 2d3a1f0..0000000 --- a/src/lib/sprite_rotation.asm +++ /dev/null @@ -1,171 +0,0 @@ -include ../consts/offsets -include ../consts/map -include ../consts/sprites - -; /* Return the sprite id and rotation for the given map coordinates - -; Inputs: -; r1: x map coord -; r2: y map coord - -; Outputs: -; r12: sprite rotation -; r13: sprite id - -; Locals: -; r3: tile id -; r4: map offset -; r5: counter -; r6: left tile id -; r7: up tile id -; r8: right tile id -; r9: down tile id -; */ -pub sprite_rotation.get_sprite_id_and_rotation: - push r1 - push r2 - push r3 - push r4 - push r5 - push r6 - push r7 - push r8 - push r9 - - add r4, r1, map - mov r5, 0 - get_sprite_id_and_rotation_map_row_loop: - cmp r5, r2 - je get_sprite_id_and_rotation_map_row_loop_end - add r4, r4, map.width - add r5, r5, 1 - jmp get_sprite_id_and_rotation_map_row_loop - get_sprite_id_and_rotation_map_row_loop_end: - - mov r12, sprites.rotation_0 ; default rotation - - load_8 r3, [r4] - lsl r3, r3, 3 ; 2*instruction_size = *8 = <<3 - add r3, r3, get_sprite_id_and_rotation_jump_table - jmp r3 - - get_sprite_id_and_rotation_jump_table: - ; tile_empty - mov r13, sprites.empty - jmp get_sprite_id_and_rotation_return - ; tiles_wall - jmp get_sprite_id_and_rotation_wall - nop - ; tile_coin - mov r13, sprites.coin - jmp get_sprite_id_and_rotation_return - - get_sprite_id_and_rotation_wall: - mov r6, 0 - mov r7, 0 - mov r8, 0 - mov r9, 0 - - get_sprite_id_and_rotation_cmp_left: - cmp r1, 0 - je get_sprite_id_and_rotation_cmp_up - sub r4, r4, 1 - load_8 r6, [r4] - add r4, r4, 1 - get_sprite_id_and_rotation_cmp_up: - cmp r2, 0 - je get_sprite_id_and_rotation_cmp_right - sub r4, r4, map.width - load_8 r7, [r4] - add r4, r4, map.width - get_sprite_id_and_rotation_cmp_right: - cmp r1, map.max_x - je get_sprite_id_and_rotation_cmp_down - add r4, r4, 1 - load_8 r8, [r4] - sub r4, r4, 1 - get_sprite_id_and_rotation_cmp_down: - cmp r2, map.max_y - je get_sprite_id_and_rotation_check_connections - add r4, r4, map.width - load_8 r9, [r4] - sub r4, r4, map.width - - get_sprite_id_and_rotation_check_connections: - get_sprite_id_and_rotation_straight_0: - cmp r7, map.tiles.wall - jne get_sprite_id_and_rotation_straight_90 - cmp r9, map.tiles.wall - jne get_sprite_id_and_rotation_straight_90 - mov r12, sprites.rotation_0 - mov r13, sprites.wall_straight - jmp get_sprite_id_and_rotation_return - get_sprite_id_and_rotation_straight_90: - cmp r6, map.tiles.wall - jne get_sprite_id_and_rotation_corner_0 - cmp r8, map.tiles.wall - jne get_sprite_id_and_rotation_corner_0 - mov r12, sprites.rotation_90 - mov r13, sprites.wall_straight - jmp get_sprite_id_and_rotation_return - - get_sprite_id_and_rotation_corner_0: - cmp r6, map.tiles.wall - jne get_sprite_id_and_rotation_corner_180 - cmp r9, map.tiles.wall - jne get_sprite_id_and_rotation_corner_90 - mov r12,sprites.rotation_0 - mov r13, sprites.wall_corner - jmp get_sprite_id_and_rotation_return - get_sprite_id_and_rotation_corner_90: - cmp r7, map.tiles.wall - jne get_sprite_id_and_rotation_end_90 - mov r12,sprites.rotation_90 - mov r13, sprites.wall_corner - jmp get_sprite_id_and_rotation_return - get_sprite_id_and_rotation_corner_180: - cmp r8, map.tiles.wall - jne get_sprite_id_and_rotation_end_0 - cmp r7, map.tiles.wall - jne get_sprite_id_and_rotation_corner_270 - mov r12,sprites.rotation_180 - mov r13, sprites.wall_corner - jmp get_sprite_id_and_rotation_return - get_sprite_id_and_rotation_corner_270: - cmp r9, map.tiles.wall - jne get_sprite_id_and_rotation_end_270 - mov r12,sprites.rotation_270 - mov r13, sprites.wall_corner - jmp get_sprite_id_and_rotation_return - - get_sprite_id_and_rotation_end_0: - cmp r9, map.tiles.wall - jne get_sprite_id_and_rotation_end_180 - mov r12,sprites.rotation_0 - mov r13, sprites.wall_end - jmp get_sprite_id_and_rotation_return - get_sprite_id_and_rotation_end_90: - mov r12,sprites.rotation_90 - mov r13, sprites.wall_end - jmp get_sprite_id_and_rotation_return - get_sprite_id_and_rotation_end_180: - mov r12,sprites.rotation_180 - mov r13, sprites.wall_end - jmp get_sprite_id_and_rotation_return - get_sprite_id_and_rotation_end_270: - mov r12,sprites.rotation_270 - mov r13, sprites.wall_end - ;jmp get_sprite_id_and_rotation_return - - get_sprite_id_and_rotation_return: - pop r9 - pop r8 - pop r7 - pop r6 - pop r5 - pop r4 - pop r3 - pop r2 - pop r1 - - ret diff --git a/src/main.asm b/src/main.asm index be88511..6118fc4 100644 --- a/src/main.asm +++ b/src/main.asm @@ -2,8 +2,6 @@ include consts/screen include init -call drawing.draw_whole_map - ; ; State initialization ; @@ -22,8 +20,7 @@ store_8 [r6], r2 add r6, r6, 1 store_8 [r6], r5 -mov r3, sprites.rotation_0 -mov r4, sprites.robot +mov r3, sprites.robot call drawing.draw_sprite_at_map_coord ; Ghost @@ -41,8 +38,7 @@ store_8 [r6], r2 add r6, r6, 1 store_8 [r6], r5 -mov r3, sprites.rotation_0 -mov r4, sprites.ghost +mov r3, sprites.ghost call drawing.draw_sprite_at_map_coord ; @@ -229,15 +225,13 @@ game_loop: ; clear old position mov r1, r2 mov r2, r3 - mov r3, sprites.rotation_0 - mov r4, sprites.empty + mov r3, sprites.empty call drawing.draw_sprite_at_map_coord ; draw robot on new position mov r1, r5 mov r2, r6 - mov r3, sprites.rotation_0 - mov r4, sprites.robot + mov r3, sprites.robot call drawing.draw_sprite_at_map_coord @@ -295,34 +289,27 @@ game_loop: store_8 [r1], r8 ; get tile at previous position - push r2 mov r1, r2 mov r2, r3 call game.get_tile_address_from_map_coord - mov r9, r13 - pop r2 + load_8 r13, [r13] ; draw tile at previous position - load_8 r1, [r9] - cmp r1, map.tiles.coin + cmp r13, map.tiles.coin je game_loop_ghost_movement_old_tile_coin ; old tile empty - mov r4, sprites.empty + mov r3, sprites.empty jmp game_loop_ghost_movement_draw_old_tile game_loop_ghost_movement_old_tile_coin: - mov r4, sprites.coin + mov r3, sprites.coin game_loop_ghost_movement_draw_old_tile: - mov r1, r2 - mov r2, r3 - mov r3, sprites.rotation_0 call drawing.draw_sprite_at_map_coord ; draw ghost on new position mov r1, r7 mov r2, r8 - mov r3, sprites.rotation_0 - mov r4, sprites.ghost + mov r3, sprites.ghost call drawing.draw_sprite_at_map_coord ; diff --git a/src/reserved_space.asm b/src/reserved_space.asm index f513191..cda0e83 100644 --- a/src/reserved_space.asm +++ b/src/reserved_space.asm @@ -2,9 +2,6 @@ pub ptr.textures_addresses: U32 0 ; empty U32 0 ; placeholder -U32 0 ; wall_end -U32 0 ; wall_straight -U32 0 ; wall_corner U32 0 ; robot U32 0 ; ghost U32 0 ; coin @@ -31,5 +28,3 @@ U8 0 ; x coord U8 0 ; y coord U8 0 ; direction U8 0 ; [padding] - -pub ptr.screen: \ No newline at end of file From 54ba73c1772df0af055397e490618f31e068633b Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Sat, 25 Apr 2026 09:13:05 +0200 Subject: [PATCH 10/22] use decimal instead of binary representation to save space --- build.ts | 22 +++++++++++----------- readme.md | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build.ts b/build.ts index 9b52f5c..a1089a2 100755 --- a/build.ts +++ b/build.ts @@ -38,17 +38,17 @@ const mapMatchingStrings = { }; const spritesMatchingStrings = { - 0x00_00_00: '0b000_000_00', - 0x00_00_FF: '0b000_000_11', - 0x00_FF_00: '0b000_111_00', - 0x99_50_00: `0b010_001_00`, - 0x77_77_77: '0b011_011_10', - 0xB3_B3_B3: '0b100_100_10', - 0xFF_00_00: '0b111_000_00', - 0xFF_00_FF: '0b111_000_11', - 0xFF_80_00: '0b111_100_00', - 0xFF_FF_00: '0b111_110_00', - 0xFF_FF_FF: '0b111_111_11', + 0x00_00_00: ' 0', // 0b000_000_00 + 0x00_00_FF: ' 3', // 0b000_000_11 + 0x00_FF_00: ' 28', // 0b000_111_00 + 0x99_50_00: ' 68', // 0b010_001_00 + 0x77_77_77: '110', // 0b011_011_10 + 0xB3_B3_B3: '146', // 0b100_100_10 + 0xFF_00_00: '224', // 0b111_000_00 + 0xFF_00_FF: '227', // 0b111_000_11 + 0xFF_80_00: '240', // 0b111_100_00 + 0xFF_FF_00: '248', // 0b111_110_00 + 0xFF_FF_FF: '255', // 0b111_111_11 }; const mapPath = 'assets/map.ppm'; diff --git a/readme.md b/readme.md index 80cab98..1fe95f6 100755 --- a/readme.md +++ b/readme.md @@ -28,8 +28,8 @@ Usage example: ```sh ./build.ts > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm -# script output can easily be piped to strip it off all comments, leading whitespace and empty lines: -./build.ts | sed 's/\s*;.*//' | sed 's/^\s*//' | sed '/^\d*$/d' > result.asm +# script output can easily be piped to strip it off all comments, leading whitespace, double spaces and empty lines: +./build.ts | sed -r 's/\s*;.*//' | sed -r 's/^\s+//' | sed -r 's/ +/ /' | sed -r '/^\s*$/d' > result.asm ``` The script is build over Deno's API, so you'll need to have a `deno` binary in your PATH to use it that way. From 2344a2e874818cfca4c7853d2be14c01f891c4df Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Sat, 25 Apr 2026 09:13:56 +0200 Subject: [PATCH 11/22] update branches description --- readme.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/readme.md b/readme.md index 1fe95f6..402841e 100755 --- a/readme.md +++ b/readme.md @@ -9,6 +9,11 @@ It thus should be able to run on the architecture and ISA the players have built The `main` branch contains a minimal implementation of the game. The following branches contains variations, allowing comparisons of performances, visuals, gameplay, ... +- `res-0-prerendered`: + - `80x60` screen resolution + - `5x5` tile resolution + - initial screen prerendered + - no sprite rotation - `res-2`: - `256x192` screen resolution - `17x17` tile resolution From c378dc95faacfddae11b063ff356be0334d94dc2 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Sat, 25 Apr 2026 09:13:56 +0200 Subject: [PATCH 12/22] update branches description (cherry picked from commit 2344a2e874818cfca4c7853d2be14c01f891c4df) --- readme.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/readme.md b/readme.md index 80cab98..c004ece 100755 --- a/readme.md +++ b/readme.md @@ -9,6 +9,11 @@ It thus should be able to run on the architecture and ISA the players have built The `main` branch contains a minimal implementation of the game. The following branches contains variations, allowing comparisons of performances, visuals, gameplay, ... +- `res-0-prerendered`: + - `80x60` screen resolution + - `5x5` tile resolution + - initial screen prerendered + - no sprite rotation - `res-2`: - `256x192` screen resolution - `17x17` tile resolution From 2b2e2a6d02989943dd005d88f8d76320db9948ad Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Sat, 25 Apr 2026 09:13:05 +0200 Subject: [PATCH 13/22] use decimal instead of binary representation to save space (cherry picked from commit 54ba73c1772df0af055397e490618f31e068633b) --- build.ts | 22 +++++++++++----------- readme.md | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build.ts b/build.ts index dc8cb41..cf8491b 100755 --- a/build.ts +++ b/build.ts @@ -38,17 +38,17 @@ const mapMatchingStrings = { }; const spritesMatchingStrings = { - 0x00_00_00: '0b000_000_00', - 0x00_00_FF: '0b000_000_11', - 0x00_FF_00: '0b000_111_00', - 0x99_50_00: `0b010_001_00`, - 0x77_77_77: '0b011_011_10', - 0xB3_B3_B3: '0b100_100_10', - 0xFF_00_00: '0b111_000_00', - 0xFF_00_FF: '0b111_000_11', - 0xFF_80_00: '0b111_100_00', - 0xFF_FF_00: '0b111_110_00', - 0xFF_FF_FF: '0b111_111_11', + 0x00_00_00: ' 0', // 0b000_000_00 + 0x00_00_FF: ' 3', // 0b000_000_11 + 0x00_FF_00: ' 28', // 0b000_111_00 + 0x99_50_00: ' 68', // 0b010_001_00 + 0x77_77_77: '110', // 0b011_011_10 + 0xB3_B3_B3: '146', // 0b100_100_10 + 0xFF_00_00: '224', // 0b111_000_00 + 0xFF_00_FF: '227', // 0b111_000_11 + 0xFF_80_00: '240', // 0b111_100_00 + 0xFF_FF_00: '248', // 0b111_110_00 + 0xFF_FF_FF: '255', // 0b111_111_11 }; const mapPath = 'assets/map.ppm'; diff --git a/readme.md b/readme.md index c004ece..402841e 100755 --- a/readme.md +++ b/readme.md @@ -33,8 +33,8 @@ Usage example: ```sh ./build.ts > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm -# script output can easily be piped to strip it off all comments, leading whitespace and empty lines: -./build.ts | sed 's/\s*;.*//' | sed 's/^\s*//' | sed '/^\d*$/d' > result.asm +# script output can easily be piped to strip it off all comments, leading whitespace, double spaces and empty lines: +./build.ts | sed -r 's/\s*;.*//' | sed -r 's/^\s+//' | sed -r 's/ +/ /' | sed -r '/^\s*$/d' > result.asm ``` The script is build over Deno's API, so you'll need to have a `deno` binary in your PATH to use it that way. From c88f67559d065eef576f352ae6a02e89abfd498f Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Mon, 27 Apr 2026 12:36:00 +0200 Subject: [PATCH 14/22] add minify flag to build script (cherry picked from commit f0b43f7b7c8e6c7960000e57320656e5b21f3171) --- build.ts | 318 +++++++++++++++++++++++++++++++++++++----------------- readme.md | 5 +- 2 files changed, 222 insertions(+), 101 deletions(-) diff --git a/build.ts b/build.ts index a1089a2..3f1b4b5 100755 --- a/build.ts +++ b/build.ts @@ -1,17 +1,13 @@ #!/usr/bin/env -S deno run --allow-read -if (Deno.args.length !== 0) { - console.error( - "This script takes no argument (The source file list is hardcoded) and output the result on stdout.", - ); - Deno.exit(1); -} - import { TextLineStream } from "jsr:@std/streams@1.1.0"; +/** + * CONFIG + */ + const instructionLength = 4; // in bytes -const mapWidth = 15; -const spriteWidth = 5; +const rawDataInstructionLengthPrefix = "U32"; const asmFilePaths = [ "src/consts/arch.asm", @@ -28,136 +24,216 @@ const asmFilePaths = [ "src/lib/game.asm", "src/lib/drawing.asm", ]; -const initialScreenPath = 'assets/screen.ppm'; -const reservedSpacePath = 'src/reserved_space.asm'; +const initialScreenPath = "assets/screen.ppm"; +const reservedSpacePath = "src/reserved_space.asm"; -const mapMatchingStrings = { - 0x00_00_00: '0', // Empty - 0xFF_00_00: '1', // Wall - 0xFF_FF_00: '2', // Coin +const mapMatchingValues = { + 0x00_00_00: 0, // Empty + 0xFF_00_00: 1, // Wall + 0xFF_FF_00: 2, // Coin }; -const spritesMatchingStrings = { - 0x00_00_00: ' 0', // 0b000_000_00 - 0x00_00_FF: ' 3', // 0b000_000_11 - 0x00_FF_00: ' 28', // 0b000_111_00 - 0x99_50_00: ' 68', // 0b010_001_00 - 0x77_77_77: '110', // 0b011_011_10 - 0xB3_B3_B3: '146', // 0b100_100_10 - 0xFF_00_00: '224', // 0b111_000_00 - 0xFF_00_FF: '227', // 0b111_000_11 - 0xFF_80_00: '240', // 0b111_100_00 - 0xFF_FF_00: '248', // 0b111_110_00 - 0xFF_FF_FF: '255', // 0b111_111_11 +const spritesMatchingValues = { + 0x00_00_00: 0, // 0b000_000_00 + 0x00_00_FF: 3, // 0b000_000_11 + 0x00_FF_00: 28, // 0b000_111_00 + 0x99_50_00: 68, // 0b010_001_00 + 0x77_77_77: 110, // 0b011_011_10 + 0xB3_B3_B3: 146, // 0b100_100_10 + 0xFF_00_00: 224, // 0b111_000_00 + 0xFF_00_FF: 227, // 0b111_000_11 + 0xFF_80_00: 240, // 0b111_100_00 + 0xFF_FF_00: 248, // 0b111_110_00 + 0xFF_FF_FF: 255, // 0b111_111_11 }; -const mapPath = 'assets/map.ppm'; +const mapPath = "assets/map.ppm"; const spritesPath = { - 'sprite_empty': 'assets/sprites/empty.ppm', - 'sprite_unimplemented': 'assets/sprites/placeholder.ppm', - 'sprite_wall_end': 'assets/sprites/wall_end.ppm', - 'sprite_wall_straight': 'assets/sprites/wall_straight.ppm', - 'sprite_wall_corner': 'assets/sprites/wall_corner.ppm', - 'sprite_robot': 'assets/sprites/robot.ppm', - 'sprite_ghost': 'assets/sprites/ghost.ppm', - 'sprite_coin': 'assets/sprites/coin.ppm', + "sprite_empty": "assets/sprites/empty.ppm", + "sprite_unimplemented": "assets/sprites/placeholder.ppm", + "sprite_wall_end": "assets/sprites/wall_end.ppm", + "sprite_wall_straight": "assets/sprites/wall_straight.ppm", + "sprite_wall_corner": "assets/sprites/wall_corner.ppm", + "sprite_robot": "assets/sprites/robot.ppm", + "sprite_ghost": "assets/sprites/ghost.ppm", + "sprite_coin": "assets/sprites/coin.ppm", }; /** - * START OF BUILD + * END OF CONFIG */ -for (const path of asmFilePaths) { - const file = await Deno.open(path); - const readable = file.readable.pipeThrough(new TextDecoderStream()) - .pipeThrough(new TextLineStream()); +class Minifier { + private buffer = ""; + private lines: Array = []; + stream; - console.log(` + constructor(writableStream: WritableStream) { + this.stream = new TransformStream({ + transform: (chunk, controller) => { + this.buffer += chunk; + controller.enqueue(this.processChunk()); + }, + + flush: (controller) => { + this.buffer += "\n"; + controller.enqueue(this.processChunk()); + }, + }); + + this.stream.readable.pipeTo(writableStream); + } + + private processChunk(): string { + const newLines = this.buffer.split("\n"); + this.buffer = newLines.pop() as string; // keep the last (potentially incomplete) line + + const removeComment = (line: string) => line.replace(/\s*;.*/, ""); + const removeLeadingWhitespaces = (line: string) => line.replace(/^\s+/, ""); + const removeTrailingWhitespaces = (line: string) => + line.replace(/\s+$/, ""); + const removeDoubleSpaces = (line: string) => line.replaceAll(/ +/g, " "); + const isNotEmpty = (line: string) => line.length !== 0; + + const newLinesStripped = newLines + .map(removeComment) + .map(removeLeadingWhitespaces) + .map(removeTrailingWhitespaces) + .map(removeDoubleSpaces) + .filter(isNotEmpty); + + this.lines.push(...newLinesStripped); + + return this.processLines(); + } + + private processLines() { + const processedLines: Array = []; + while (this.lines.length >= instructionLength) { + const mergeableLines = this.lines.slice(0, instructionLength); + if (!mergeableLines.every((line) => line.startsWith("U8"))) { + processedLines.push(this.lines.shift() as string); + continue; + } + + const bytesToMerge = mergeableLines.map((line) => + parseInt((line.match(/U8\s(\d+)/) as RegExpMatchArray)[1]) + ); + let mergedBytes = 0; + for (let i = 0; i < bytesToMerge.length; i++) { + mergedBytes += bytesToMerge[i] << (instructionLength - 1 - i) * 8; + } + processedLines.push( + `${rawDataInstructionLengthPrefix} ${mergedBytes >>> 0}`, + ); + this.lines.splice(0, instructionLength); + } + + return processedLines.length === 0 ? "" : `${processedLines.join("\n")}\n`; + } +} + +async function build(output: WritableStreamDefaultWriter) { + for (const path of asmFilePaths) { + const file = await Deno.open(path); + const readable = file.readable.pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()); + + output.write(` ;/******************************* ;* ${path} ;*******************************/ `); - for await (const line of readable) { - if (!line.startsWith("include ")) { - console.log(line); + for await (const line of readable) { + if (!line.startsWith("include ")) { + output.write(`${line}\n`); + } } } -} + const asmRawRepresentations = []; -const asmRawRepresentations = []; + const mapBytes = await getDataFromPPM(mapPath); + asmRawRepresentations.push( + bytesToAsmConstU8("map", mapBytes, mapMatchingValues), + ); -const mapBytes = await getDataFromPPM(mapPath); -asmRawRepresentations.push(bytesToAsmConstU8('map', mapBytes, mapMatchingStrings, mapWidth)); + for (const [label, path] of Object.entries(spritesPath)) { + const bytes = await getDataFromPPM(path); + asmRawRepresentations.push( + bytesToAsmConstU8(label, bytes, spritesMatchingValues), + ); + } -for (const [label, path] of Object.entries(spritesPath)) { - const bytes = await getDataFromPPM(path); - asmRawRepresentations.push(bytesToAsmConstU8(label, bytes, spritesMatchingStrings, spriteWidth)); -} + output.write(asmRawRepresentations.join("\n\n")); -console.log(asmRawRepresentations.join("\n\n")); - -console.log(` + output.write(` ; ; RESERVED RAM SPACE ; `); -const screenBytes = await getDataFromPPM(initialScreenPath); -console.log(bytesToAsmConstU8('screen', screenBytes, spritesMatchingStrings, 1)); + const screenBytes = await getDataFromPPM(initialScreenPath); + output.write(bytesToAsmConstU8("screen", screenBytes, spritesMatchingValues)); -console.log(await Deno.readTextFile(reservedSpacePath)) + output.write(await Deno.readTextFile(reservedSpacePath)); +} /** * Return an ASM constant (U8) representation of the given data - * + * * If needed, the representation will be padded to ensure the following code stays aligned on the instruction's length - * + * * @param label The label to write before the first raw data * @param data An array of bytes to interprete as 24 bits values - * @param matchingStrings An associative array with a string representation for each possible 24 bit data value - * @param lineLength Optional: Provide a way to wrap the result in multiple lines - * + * @param matchingValues An associative array with a string representation for each possible 24 bit data value + * * @returns The ASM representation of that raw data, pre- and post-fixed by the given label */ -function bytesToAsmConstU8(label: string, data: Uint8Array, matchingStrings: Record, lineLength = 0): string { - lineLength ??= Infinity; +function bytesToAsmConstU8( + label: string, + data: Uint8Array, + matchingValues: Record, +): string { + if (data.length % 3 !== 0) { + console.error( + "Length of data passed to bytesToAsmConstU8 are not a multiple of 3 (it needs to, as each resulting byte need a Red, a Green and a Blue value)", + ); + Deno.exit(1); + } - const stringValues = []; - for (let i = 0; i < data.length - 2; i += 3) { - const pixelValue = (data[i] << 16) + (data[i+1] << 8) + (data[i+2]) - if (!Object.hasOwn(matchingStrings, pixelValue)) { - console.error(`No matching value defined for "${pixelValue.toString(2)}" (which is ${label}'s ${i} pixel)`); + const lines = [`${label}:`]; + for (let i = 0; i < data.length; i += 3) { + const pixelValue = (data[i] << 16) + (data[i + 1] << 8) + (data[i + 2]); + if (!Object.hasOwn(matchingValues, pixelValue)) { + console.error( + `No matching value defined for "${ + pixelValue.toString(2) + }" (which is ${label}'s ${i} pixel)`, + ); Deno.exit(1); } - - stringValues.push(`U8 ${matchingStrings[pixelValue]}`); + + lines.push(`U8 ${matchingValues[pixelValue]}`); + } + lines.push(`${label}_end:`); + + const bytesNb = data.length / 3; + const unalignedBytesNb = bytesNb % instructionLength; + + for (let i = 0; i < instructionLength - unalignedBytesNb; i++) { + lines.push( + `U8 0 ; padding to preserve ${8 * instructionLength} bits alignment`, + ); } - const lines = []; - if (lineLength === 0) { - lines.push(stringValues.join("\t")); - } else { - for (let i = 0; i < stringValues.length; i += lineLength) { - lines.push(stringValues.slice(i, i+lineLength).join("\t")); - } - } - - const paddingBytes = []; - for (let i = 0; i < stringValues.length % instructionLength; i++) { - paddingBytes.push('U8 0'); - } - const paddingString = paddingBytes.length === 0 ? '' : `\n${paddingBytes.join('\t')} ; padding to preserve ${8 * instructionLength} bits alignment` - - return `${label}: -${lines.join("\n")} -${label}_end:${paddingString}` + return lines.join("\n"); } /** * Return the image pixels data (excluding all other fields) from a given PPM file - * + * * The implementation differ from the format definition (https://netpbm.sourceforge.net/doc/ppm.html) but match the file created by GIMP. */ async function getDataFromPPM(path: string): Promise { @@ -192,7 +268,7 @@ async function getDataFromPPM(path: string): Promise { let offset = 0; // magic number - while(!whitespaces.includes(content[offset])) { + while (!whitespaces.includes(content[offset])) { offset++; } @@ -200,7 +276,7 @@ async function getDataFromPPM(path: string): Promise { // potential comment if (content[offset] === hashtag) { - while(!endOfComment.includes(content[offset])) { + while (!endOfComment.includes(content[offset])) { offset++; } @@ -208,25 +284,73 @@ async function getDataFromPPM(path: string): Promise { } // image width - while(numbers.includes(content[offset])) { + while (numbers.includes(content[offset])) { offset++; } offset++; // whitespace // image height - while(numbers.includes(content[offset])) { + while (numbers.includes(content[offset])) { offset++; } offset++; // whitespace // maximum color value - while(numbers.includes(content[offset])) { + while (numbers.includes(content[offset])) { offset++; } offset++; // whitespace return content.slice(offset, content.length); +} + +function printUsage(into: (message: string) => void) { + into(`Usage: +\tbuild.ts [--minify] +\t\tMerge the hardcoded files and output the result to stdout. The "--minify" option can be used to strip the result of all comments, leading/trailing double whitespaces, empty lines and use the more compact raw data representation. +\tbuild.ts --help +\t\tPrint this message`); +} + +/** + * SCRIPT + */ + +if (Deno.args.length > 1) { + printUsage(console.error); + Deno.exit(1); +} + +let minify = false; +if (Deno.args.length === 1) { + switch (Deno.args[0]) { + case "--help": + printUsage(console.log); + Deno.exit(0); + break; + case "--minify": + minify = true; + break; + default: + printUsage(console.error); + Deno.exit(1); + break; + } +} + +const encoder = new TextEncoder(); +const stdoutWritable = new WritableStream({ + async write(chunk) { + await Deno.stdout.write(encoder.encode(chunk)); + }, +}); + +if (minify) { + const minifier = new Minifier(stdoutWritable); + await build(minifier.stream.writable.getWriter()); +} else { + await build(stdoutWritable.getWriter()); } \ No newline at end of file diff --git a/readme.md b/readme.md index 402841e..d65be4f 100755 --- a/readme.md +++ b/readme.md @@ -31,10 +31,7 @@ It also convert the PPM assets (map and sprites) into ASM raw values representat Usage example: ```sh -./build.ts > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm - -# script output can easily be piped to strip it off all comments, leading whitespace, double spaces and empty lines: -./build.ts | sed -r 's/\s*;.*//' | sed -r 's/^\s+//' | sed -r 's/ +/ /' | sed -r '/^\s*$/d' > result.asm +./build.ts --minify > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm ``` The script is build over Deno's API, so you'll need to have a `deno` binary in your PATH to use it that way. From 42ea54550724899f49854c9f6637a3184a8998cc Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Fri, 1 May 2026 14:57:11 +0200 Subject: [PATCH 15/22] add minify flag to build script (cherry picked from commit f0b43f7b7c8e6c7960000e57320656e5b21f3171) --- build.ts | 316 +++++++++++++++++++++++++++++++++++++----------------- readme.md | 5 +- 2 files changed, 221 insertions(+), 100 deletions(-) diff --git a/build.ts b/build.ts index cf8491b..01a50ca 100755 --- a/build.ts +++ b/build.ts @@ -1,14 +1,14 @@ #!/usr/bin/env -S deno run --allow-read -if (Deno.args.length !== 0) { - console.error( - "This script takes no argument (The source file list is hardcoded) and output the result on stdout.", - ); - Deno.exit(1); -} - import { TextLineStream } from "jsr:@std/streams@1.1.0"; +/** + * CONFIG + */ + +const instructionLength = 4; // in bytes +const rawDataInstructionLengthPrefix = "U32"; + const asmFilePaths = [ "src/consts/arch.asm", "src/consts/game.asm", @@ -25,136 +25,212 @@ const asmFilePaths = [ "src/lib/drawing.asm", "src/lib/sprite_rotation.asm", ]; -const reservedSpacePath = 'src/reserved_space.asm'; +const reservedSpacePath = "src/reserved_space.asm"; -const instructionLength = 4; // in bytes -const mapWidth = 15; -const spriteWidth = 5; - -const mapMatchingStrings = { - 0x00_00_00: '0', // Empty - 0xFF_00_00: '1', // Wall - 0xFF_FF_00: '2', // Coin +const mapMatchingValues = { + 0x00_00_00: 0, // Empty + 0xFF_00_00: 1, // Wall + 0xFF_FF_00: 2, // Coin }; -const spritesMatchingStrings = { - 0x00_00_00: ' 0', // 0b000_000_00 - 0x00_00_FF: ' 3', // 0b000_000_11 - 0x00_FF_00: ' 28', // 0b000_111_00 - 0x99_50_00: ' 68', // 0b010_001_00 - 0x77_77_77: '110', // 0b011_011_10 - 0xB3_B3_B3: '146', // 0b100_100_10 - 0xFF_00_00: '224', // 0b111_000_00 - 0xFF_00_FF: '227', // 0b111_000_11 - 0xFF_80_00: '240', // 0b111_100_00 - 0xFF_FF_00: '248', // 0b111_110_00 - 0xFF_FF_FF: '255', // 0b111_111_11 +const spritesMatchingValues = { + 0x00_00_00: 0, // 0b000_000_00 + 0x00_00_FF: 3, // 0b000_000_11 + 0x00_FF_00: 28, // 0b000_111_00 + 0x99_50_00: 68, // 0b010_001_00 + 0x77_77_77: 110, // 0b011_011_10 + 0xB3_B3_B3: 146, // 0b100_100_10 + 0xFF_00_00: 224, // 0b111_000_00 + 0xFF_00_FF: 227, // 0b111_000_11 + 0xFF_80_00: 240, // 0b111_100_00 + 0xFF_FF_00: 248, // 0b111_110_00 + 0xFF_FF_FF: 255, // 0b111_111_11 }; -const mapPath = 'assets/map.ppm'; +const mapPath = "assets/map.ppm"; const spritesPath = { - 'sprite_empty': 'assets/sprites/empty.ppm', - 'sprite_unimplemented': 'assets/sprites/placeholder.ppm', - 'sprite_wall_end': 'assets/sprites/wall_end.ppm', - 'sprite_wall_straight': 'assets/sprites/wall_straight.ppm', - 'sprite_wall_corner': 'assets/sprites/wall_corner.ppm', - 'sprite_robot': 'assets/sprites/robot.ppm', - 'sprite_ghost': 'assets/sprites/ghost.ppm', - 'sprite_coin': 'assets/sprites/coin.ppm', + "sprite_empty": "assets/sprites/empty.ppm", + "sprite_unimplemented": "assets/sprites/placeholder.ppm", + "sprite_wall_end": "assets/sprites/wall_end.ppm", + "sprite_wall_straight": "assets/sprites/wall_straight.ppm", + "sprite_wall_corner": "assets/sprites/wall_corner.ppm", + "sprite_robot": "assets/sprites/robot.ppm", + "sprite_ghost": "assets/sprites/ghost.ppm", + "sprite_coin": "assets/sprites/coin.ppm", }; /** - * START OF BUILD + * END OF CONFIG */ -for (const path of asmFilePaths) { - const file = await Deno.open(path); - const readable = file.readable.pipeThrough(new TextDecoderStream()) - .pipeThrough(new TextLineStream()); +class Minifier { + private buffer = ""; + private lines: Array = []; + stream; - console.log(` + constructor(writableStream: WritableStream) { + this.stream = new TransformStream({ + transform: (chunk, controller) => { + this.buffer += chunk; + controller.enqueue(this.processChunk()); + }, + + flush: (controller) => { + this.buffer += "\n"; + controller.enqueue(this.processChunk()); + }, + }); + + this.stream.readable.pipeTo(writableStream); + } + + private processChunk(): string { + const newLines = this.buffer.split("\n"); + this.buffer = newLines.pop() as string; // keep the last (potentially incomplete) line + + const removeComment = (line: string) => line.replace(/\s*;.*/, ""); + const removeLeadingWhitespaces = (line: string) => line.replace(/^\s+/, ""); + const removeTrailingWhitespaces = (line: string) => + line.replace(/\s+$/, ""); + const removeDoubleSpaces = (line: string) => line.replaceAll(/ +/g, " "); + const isNotEmpty = (line: string) => line.length !== 0; + + const newLinesStripped = newLines + .map(removeComment) + .map(removeLeadingWhitespaces) + .map(removeTrailingWhitespaces) + .map(removeDoubleSpaces) + .filter(isNotEmpty); + + this.lines.push(...newLinesStripped); + + return this.processLines(); + } + + private processLines() { + const processedLines: Array = []; + while (this.lines.length >= instructionLength) { + const mergeableLines = this.lines.slice(0, instructionLength); + if (!mergeableLines.every((line) => line.startsWith("U8"))) { + processedLines.push(this.lines.shift() as string); + continue; + } + + const bytesToMerge = mergeableLines.map((line) => + parseInt((line.match(/U8\s(\d+)/) as RegExpMatchArray)[1]) + ); + let mergedBytes = 0; + for (let i = 0; i < bytesToMerge.length; i++) { + mergedBytes += bytesToMerge[i] << (instructionLength - 1 - i) * 8; + } + processedLines.push( + `${rawDataInstructionLengthPrefix} ${mergedBytes >>> 0}`, + ); + this.lines.splice(0, instructionLength); + } + + return processedLines.length === 0 ? "" : `${processedLines.join("\n")}\n`; + } +} + +async function build(output: WritableStreamDefaultWriter) { + for (const path of asmFilePaths) { + const file = await Deno.open(path); + const readable = file.readable.pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()); + + output.write(` ;/******************************* ;* ${path} ;*******************************/ `); - for await (const line of readable) { - if (!line.startsWith("include ")) { - console.log(line); + for await (const line of readable) { + if (!line.startsWith("include ")) { + output.write(`${line}\n`); + } } } -} + const asmRawRepresentations = []; -const asmRawRepresentations = []; + const mapBytes = await getDataFromPPM(mapPath); + asmRawRepresentations.push( + bytesToAsmConstU8("map", mapBytes, mapMatchingValues), + ); -const mapBytes = await getDataFromPPM(mapPath); -asmRawRepresentations.push(bytesToAsmConstU8('map', mapBytes, mapMatchingStrings, mapWidth)); + for (const [label, path] of Object.entries(spritesPath)) { + const bytes = await getDataFromPPM(path); + asmRawRepresentations.push( + bytesToAsmConstU8(label, bytes, spritesMatchingValues), + ); + } -for (const [label, path] of Object.entries(spritesPath)) { - const bytes = await getDataFromPPM(path); - asmRawRepresentations.push(bytesToAsmConstU8(label, bytes, spritesMatchingStrings, spriteWidth)); -} + output.write(asmRawRepresentations.join("\n\n")); -console.log(asmRawRepresentations.join("\n\n")); - -console.log(` + output.write(` ; ; RESERVED RAM SPACE ; `); -console.log(await Deno.readTextFile(reservedSpacePath)) + output.write(await Deno.readTextFile(reservedSpacePath)); +} /** * Return an ASM constant (U8) representation of the given data - * + * * If needed, the representation will be padded to ensure the following code stays aligned on the instruction's length - * + * * @param label The label to write before the first raw data * @param data An array of bytes to interprete as 24 bits values - * @param matchingStrings An associative array with a string representation for each possible 24 bit data value - * @param lineLength Optional: Provide a way to wrap the result in multiple lines - * + * @param matchingValues An associative array with a string representation for each possible 24 bit data value + * * @returns The ASM representation of that raw data, pre- and post-fixed by the given label */ -function bytesToAsmConstU8(label: string, data: Uint8Array, matchingStrings: Record, lineLength = 0): string { - lineLength ??= Infinity; +function bytesToAsmConstU8( + label: string, + data: Uint8Array, + matchingValues: Record, +): string { + if (data.length % 3 !== 0) { + console.error( + "Length of data passed to bytesToAsmConstU8 are not a multiple of 3 (it needs to, as each resulting byte need a Red, a Green and a Blue value)", + ); + Deno.exit(1); + } - const stringValues = []; - for (let i = 0; i < data.length - 2; i += 3) { - const pixelValue = (data[i] << 16) + (data[i+1] << 8) + (data[i+2]) - if (!Object.hasOwn(matchingStrings, pixelValue)) { - console.error(`No matching value defined for "${pixelValue.toString(2)}" (which is ${label}'s ${i} pixel)`); + const lines = [`${label}:`]; + for (let i = 0; i < data.length; i += 3) { + const pixelValue = (data[i] << 16) + (data[i + 1] << 8) + (data[i + 2]); + if (!Object.hasOwn(matchingValues, pixelValue)) { + console.error( + `No matching value defined for "${ + pixelValue.toString(2) + }" (which is ${label}'s ${i} pixel)`, + ); Deno.exit(1); } - - stringValues.push(`U8 ${matchingStrings[pixelValue]}`); + + lines.push(`U8 ${matchingValues[pixelValue]}`); + } + lines.push(`${label}_end:`); + + const bytesNb = data.length / 3; + const unalignedBytesNb = bytesNb % instructionLength; + + for (let i = 0; i < instructionLength - unalignedBytesNb; i++) { + lines.push( + `U8 0 ; padding to preserve ${8 * instructionLength} bits alignment`, + ); } - const lines = []; - if (lineLength === 0) { - lines.push(stringValues.join("\t")); - } else { - for (let i = 0; i < stringValues.length; i += lineLength) { - lines.push(stringValues.slice(i, i+lineLength).join("\t")); - } - } - - const paddingBytes = []; - for (let i = 0; i < stringValues.length % instructionLength; i++) { - paddingBytes.push('U8 0'); - } - const paddingString = paddingBytes.length === 0 ? '' : `\n${paddingBytes.join('\t')} ; padding to preserve ${8 * instructionLength} bits alignment` - - return `${label}: -${lines.join("\n")} -${label}_end:${paddingString}` + return lines.join("\n"); } /** * Return the image pixels data (excluding all other fields) from a given PPM file - * + * * The implementation differ from the format definition (https://netpbm.sourceforge.net/doc/ppm.html) but match the file created by GIMP. */ async function getDataFromPPM(path: string): Promise { @@ -189,7 +265,7 @@ async function getDataFromPPM(path: string): Promise { let offset = 0; // magic number - while(!whitespaces.includes(content[offset])) { + while (!whitespaces.includes(content[offset])) { offset++; } @@ -197,7 +273,7 @@ async function getDataFromPPM(path: string): Promise { // potential comment if (content[offset] === hashtag) { - while(!endOfComment.includes(content[offset])) { + while (!endOfComment.includes(content[offset])) { offset++; } @@ -205,25 +281,73 @@ async function getDataFromPPM(path: string): Promise { } // image width - while(numbers.includes(content[offset])) { + while (numbers.includes(content[offset])) { offset++; } offset++; // whitespace // image height - while(numbers.includes(content[offset])) { + while (numbers.includes(content[offset])) { offset++; } offset++; // whitespace // maximum color value - while(numbers.includes(content[offset])) { + while (numbers.includes(content[offset])) { offset++; } offset++; // whitespace return content.slice(offset, content.length); +} + +function printUsage(into: (message: string) => void) { + into(`Usage: +\tbuild.ts [--minify] +\t\tMerge the hardcoded files and output the result to stdout. The "--minify" option can be used to strip the result of all comments, leading/trailing double whitespaces, empty lines and use the more compact raw data representation. +\tbuild.ts --help +\t\tPrint this message`); +} + +/** + * SCRIPT + */ + +if (Deno.args.length > 1) { + printUsage(console.error); + Deno.exit(1); +} + +let minify = false; +if (Deno.args.length === 1) { + switch (Deno.args[0]) { + case "--help": + printUsage(console.log); + Deno.exit(0); + break; + case "--minify": + minify = true; + break; + default: + printUsage(console.error); + Deno.exit(1); + break; + } +} + +const encoder = new TextEncoder(); +const stdoutWritable = new WritableStream({ + async write(chunk) { + await Deno.stdout.write(encoder.encode(chunk)); + }, +}); + +if (minify) { + const minifier = new Minifier(stdoutWritable); + await build(minifier.stream.writable.getWriter()); +} else { + await build(stdoutWritable.getWriter()); } \ No newline at end of file diff --git a/readme.md b/readme.md index 402841e..d65be4f 100755 --- a/readme.md +++ b/readme.md @@ -31,10 +31,7 @@ It also convert the PPM assets (map and sprites) into ASM raw values representat Usage example: ```sh -./build.ts > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm - -# script output can easily be piped to strip it off all comments, leading whitespace, double spaces and empty lines: -./build.ts | sed -r 's/\s*;.*//' | sed -r 's/^\s+//' | sed -r 's/ +/ /' | sed -r '/^\s*$/d' > result.asm +./build.ts --minify > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm ``` The script is build over Deno's API, so you'll need to have a `deno` binary in your PATH to use it that way. From e90e792ebff220f149f29bb1404c2cad2a87b6b1 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Fri, 1 May 2026 15:08:22 +0200 Subject: [PATCH 16/22] fix build script ignoring last lines --- build.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/build.ts b/build.ts index 01a50ca..d5a7dff 100755 --- a/build.ts +++ b/build.ts @@ -78,6 +78,8 @@ class Minifier { flush: (controller) => { this.buffer += "\n"; controller.enqueue(this.processChunk()); + controller.enqueue(this.lines.join("\n") + "\n"); + this.lines = []; }, }); @@ -139,7 +141,7 @@ async function build(output: WritableStreamDefaultWriter) { const readable = file.readable.pipeThrough(new TextDecoderStream()) .pipeThrough(new TextLineStream()); - output.write(` + await output.write(` ;/******************************* ;* ${path} ;*******************************/ @@ -147,7 +149,7 @@ async function build(output: WritableStreamDefaultWriter) { for await (const line of readable) { if (!line.startsWith("include ")) { - output.write(`${line}\n`); + await output.write(`${line}\n`); } } } @@ -166,15 +168,16 @@ async function build(output: WritableStreamDefaultWriter) { ); } - output.write(asmRawRepresentations.join("\n\n")); + await output.write(asmRawRepresentations.join("\n\n")); - output.write(` + await output.write(` ; ; RESERVED RAM SPACE ; `); - output.write(await Deno.readTextFile(reservedSpacePath)); + await output.write(await Deno.readTextFile(reservedSpacePath)); + await output.close(); } /** From 6920d10089660846378d16b195d91db3d5b10e73 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Fri, 1 May 2026 15:56:05 +0200 Subject: [PATCH 17/22] move ppm to asm representation to its own script --- .gitignore | 1 + build.ts | 191 +++++++++------------------------------------------- ppmToAsm.ts | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 18 +++-- 4 files changed, 229 insertions(+), 165 deletions(-) create mode 100644 .gitignore create mode 100755 ppmToAsm.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7773828 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ \ No newline at end of file diff --git a/build.ts b/build.ts index d5a7dff..dc881db 100755 --- a/build.ts +++ b/build.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env -S deno run --allow-read +#!/usr/bin/env -S deno run --allow-read --allow-run import { TextLineStream } from "jsr:@std/streams@1.1.0"; @@ -9,6 +9,8 @@ import { TextLineStream } from "jsr:@std/streams@1.1.0"; const instructionLength = 4; // in bytes const rawDataInstructionLengthPrefix = "U32"; +const ppmToAsmScriptPath = './ppmToAsm.ts'; + const asmFilePaths = [ "src/consts/arch.asm", "src/consts/game.asm", @@ -27,26 +29,6 @@ const asmFilePaths = [ ]; const reservedSpacePath = "src/reserved_space.asm"; -const mapMatchingValues = { - 0x00_00_00: 0, // Empty - 0xFF_00_00: 1, // Wall - 0xFF_FF_00: 2, // Coin -}; - -const spritesMatchingValues = { - 0x00_00_00: 0, // 0b000_000_00 - 0x00_00_FF: 3, // 0b000_000_11 - 0x00_FF_00: 28, // 0b000_111_00 - 0x99_50_00: 68, // 0b010_001_00 - 0x77_77_77: 110, // 0b011_011_10 - 0xB3_B3_B3: 146, // 0b100_100_10 - 0xFF_00_00: 224, // 0b111_000_00 - 0xFF_00_FF: 227, // 0b111_000_11 - 0xFF_80_00: 240, // 0b111_100_00 - 0xFF_FF_00: 248, // 0b111_110_00 - 0xFF_FF_FF: 255, // 0b111_111_11 -}; - const mapPath = "assets/map.ppm"; const spritesPath = { "sprite_empty": "assets/sprites/empty.ppm", @@ -135,6 +117,29 @@ class Minifier { } } +async function ppmToAsm(mode: "map" | "sprite", label: string, path: string): Promise { + const command = new Deno.Command(ppmToAsmScriptPath, { + args: [ + instructionLength.toString(), + mode, + label, + path, + ], + }); + + const { code, stdout, stderr } = await command.output(); + + const decoder = new TextDecoder(); + + if (code !== 0) { + console.error(`ppmToAsm script returned non-zero value: ${code}`); + console.error(`STDERR: \n${decoder.decode(stderr)}`); + Deno.exit(1); + } + + return decoder.decode(stdout); +} + async function build(output: WritableStreamDefaultWriter) { for (const path of asmFilePaths) { const file = await Deno.open(path); @@ -154,19 +159,10 @@ async function build(output: WritableStreamDefaultWriter) { } } - const asmRawRepresentations = []; - - const mapBytes = await getDataFromPPM(mapPath); - asmRawRepresentations.push( - bytesToAsmConstU8("map", mapBytes, mapMatchingValues), - ); - - for (const [label, path] of Object.entries(spritesPath)) { - const bytes = await getDataFromPPM(path); - asmRawRepresentations.push( - bytesToAsmConstU8(label, bytes, spritesMatchingValues), - ); - } + const asmRawRepresentations = await Promise.all([ + ppmToAsm("map", "map", mapPath), + ...Object.entries(spritesPath).map(([label, path]) => ppmToAsm("sprite", label, path)), + ]); await output.write(asmRawRepresentations.join("\n\n")); @@ -180,133 +176,6 @@ async function build(output: WritableStreamDefaultWriter) { await output.close(); } -/** - * Return an ASM constant (U8) representation of the given data - * - * If needed, the representation will be padded to ensure the following code stays aligned on the instruction's length - * - * @param label The label to write before the first raw data - * @param data An array of bytes to interprete as 24 bits values - * @param matchingValues An associative array with a string representation for each possible 24 bit data value - * - * @returns The ASM representation of that raw data, pre- and post-fixed by the given label - */ -function bytesToAsmConstU8( - label: string, - data: Uint8Array, - matchingValues: Record, -): string { - if (data.length % 3 !== 0) { - console.error( - "Length of data passed to bytesToAsmConstU8 are not a multiple of 3 (it needs to, as each resulting byte need a Red, a Green and a Blue value)", - ); - Deno.exit(1); - } - - const lines = [`${label}:`]; - for (let i = 0; i < data.length; i += 3) { - const pixelValue = (data[i] << 16) + (data[i + 1] << 8) + (data[i + 2]); - if (!Object.hasOwn(matchingValues, pixelValue)) { - console.error( - `No matching value defined for "${ - pixelValue.toString(2) - }" (which is ${label}'s ${i} pixel)`, - ); - Deno.exit(1); - } - - lines.push(`U8 ${matchingValues[pixelValue]}`); - } - lines.push(`${label}_end:`); - - const bytesNb = data.length / 3; - const unalignedBytesNb = bytesNb % instructionLength; - - for (let i = 0; i < instructionLength - unalignedBytesNb; i++) { - lines.push( - `U8 0 ; padding to preserve ${8 * instructionLength} bits alignment`, - ); - } - - return lines.join("\n"); -} - -/** - * Return the image pixels data (excluding all other fields) from a given PPM file - * - * The implementation differ from the format definition (https://netpbm.sourceforge.net/doc/ppm.html) but match the file created by GIMP. - */ -async function getDataFromPPM(path: string): Promise { - const endOfComment = [ - 0x0a, - 0x0d, - ]; - const whitespaces = [ - 0x09, - 0x0a, - 0x0b, - 0x0c, - 0x0d, - 0x20, - ]; - const numbers = [ - 0x30, - 0x31, - 0x32, - 0x33, - 0x34, - 0x35, - 0x36, - 0x37, - 0x38, - 0x39, - ]; - - const hashtag = 0x23; - - const content = await Deno.readFile(path); - let offset = 0; - - // magic number - while (!whitespaces.includes(content[offset])) { - offset++; - } - - offset++; // whitespace - - // potential comment - if (content[offset] === hashtag) { - while (!endOfComment.includes(content[offset])) { - offset++; - } - - offset++; // end of comment - } - - // image width - while (numbers.includes(content[offset])) { - offset++; - } - - offset++; // whitespace - - // image height - while (numbers.includes(content[offset])) { - offset++; - } - - offset++; // whitespace - - // maximum color value - while (numbers.includes(content[offset])) { - offset++; - } - - offset++; // whitespace - - return content.slice(offset, content.length); -} - function printUsage(into: (message: string) => void) { into(`Usage: \tbuild.ts [--minify] diff --git a/ppmToAsm.ts b/ppmToAsm.ts new file mode 100755 index 0000000..376a514 --- /dev/null +++ b/ppmToAsm.ts @@ -0,0 +1,184 @@ +#!/usr/bin/env -S deno run --allow-read + +const ppmCorrespondanceTables = { + map: { + 0x00_00_00: 0, // Empty + 0xFF_00_00: 1, // Wall + 0xFF_FF_00: 2, // Coin + }, + sprite: { + 0x00_00_00: 0, // 0b000_000_00 + 0x00_00_FF: 3, // 0b000_000_11 + 0x00_FF_00: 28, // 0b000_111_00 + 0x99_50_00: 68, // 0b010_001_00 + 0x77_77_77: 110, // 0b011_011_10 + 0xB3_B3_B3: 146, // 0b100_100_10 + 0xFF_00_00: 224, // 0b111_000_00 + 0xFF_00_FF: 227, // 0b111_000_11 + 0xFF_80_00: 240, // 0b111_100_00 + 0xFF_FF_00: 248, // 0b111_110_00 + 0xFF_FF_FF: 255, // 0b111_111_11 + }, +}; + +/** + * Return an ASM constants (U8) representation of the given data + * + * If needed, the representation will be padded to ensure the following code stays aligned on the instruction's length + * + * @param label The label to write before the first raw data + * @param data An array of bytes to interprete as 24 bits values + * @param matchingValues An associative array with a string representation for each possible 24 bit data value + * @param instructionLength The size / length in byte of an instruction. Used to pad result to ensure data stays aligned + * + * @returns The ASM representation of that raw data, pre- and post-fixed by the given label + */ +function ppmBytesToCorrespondingAsmConstU8( + label: string, + data: Uint8Array, + matchingValues: Record, + instructionLength: number, +): string { + if (data.length % 3 !== 0) { + console.error( + "Length of data passed to bytesToAsmConstU8 are not a multiple of 3 (it needs to, as each resulting byte need a Red, a Green and a Blue value)", + ); + Deno.exit(1); + } + + const lines = [`${label}:`]; + for (let i = 0; i < data.length; i += 3) { + const pixelValue = (data[i] << 16) + (data[i + 1] << 8) + (data[i + 2]); + if (!Object.hasOwn(matchingValues, pixelValue)) { + console.error( + `No matching value defined for "${ + pixelValue.toString(2) + }" (which is ${label}'s ${i} pixel)`, + ); + Deno.exit(1); + } + + lines.push(`U8 ${matchingValues[pixelValue]}`); + } + lines.push(`${label}_end:`); + + const bytesNb = data.length / 3; + const unalignedBytesNb = bytesNb % instructionLength; + + for (let i = 0; i < instructionLength - unalignedBytesNb; i++) { + lines.push( + `U8 0 ; padding to preserve ${8 * instructionLength} bits alignment`, + ); + } + + return lines.join("\n"); +} + +/** + * Return the image pixels data (excluding all other fields) from a given PPM file + * + * The implementation differ from the format definition (https://netpbm.sourceforge.net/doc/ppm.html) but match the file created by GIMP. + */ +async function getDataFromPPM(path: string): Promise { + const endOfComment = [ + 0x0a, + 0x0d, + ]; + const whitespaces = [ + 0x09, + 0x0a, + 0x0b, + 0x0c, + 0x0d, + 0x20, + ]; + const numbers = [ + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + 0x36, + 0x37, + 0x38, + 0x39, + ]; + + const hashtag = 0x23; + + const content = await Deno.readFile(path); + let offset = 0; + + // magic number + while (!whitespaces.includes(content[offset])) { + offset++; + } + + offset++; // whitespace + + // potential comment + if (content[offset] === hashtag) { + while (!endOfComment.includes(content[offset])) { + offset++; + } + + offset++; // end of comment + } + + // image width + while (numbers.includes(content[offset])) { + offset++; + } + + offset++; // whitespace + + // image height + while (numbers.includes(content[offset])) { + offset++; + } + + offset++; // whitespace + + // maximum color value + while (numbers.includes(content[offset])) { + offset++; + } + + offset++; // whitespace + + return content.slice(offset, content.length); +} + +function printUsage(into: (message: string) => void) { + into(`Usage: +\tppmToAsm.ts {instruction length} {map|sprite} {label} {file} +\t\tConvert the given PPM file into a Symfony assembly representation using the given correspondance table. Result will be padded with "U8 0"s to ensure alignment to given instruction length. +\tppmToAsm.ts --help +\t\tPrint this message`); +} + +/** + * SCRIPT + */ + +if (Deno.args.length !== 4) { + printUsage(console.error); + Deno.exit(1); +} + +if (!(Deno.args[1] in ppmCorrespondanceTables)) { + console.error(`"${Deno.args[1]}" is not a valid correspondance table.\n`); + printUsage(console.error); + Deno.exit(1); +} + +const mode = Deno.args[1] as keyof typeof ppmCorrespondanceTables; +const correspondanceTable = ppmCorrespondanceTables[mode]; +const label = Deno.args[2]; +const path = Deno.args[3]; +const instructionLength = parseInt(Deno.args[0]); + +const ppmData = await getDataFromPPM(path); +const asm = ppmBytesToCorrespondingAsmConstU8(label, ppmData, correspondanceTable, instructionLength); +console.log(asm); diff --git a/readme.md b/readme.md index d65be4f..d5a85d8 100755 --- a/readme.md +++ b/readme.md @@ -25,16 +25,26 @@ The following branches contains variations, allowing comparisons of performances ## Build script -File inclusion is currently not supported by the game, so a `build.ts` script it provided that will concatenate the content of the hard-coded `.asm` files and output the result on stdout. +Both scripts uses Deno's API, so you'll need to have a `deno` binary in your PATH. -It also convert the PPM assets (map and sprites) into ASM raw values representation and append it after all the code. +### PPM to Symfony assembly + +Output the Symfony assembly representation of the given PPM file. Usage example: ```sh -./build.ts --minify > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm +./ppmToAsm.ts map map ./assets/map.ppm > ./dist/map.asm +./ppmToAsm.ts sprite robot ./assets/sprites/robot.ppm > ./dist/robot.asm ``` -The script is build over Deno's API, so you'll need to have a `deno` binary in your PATH to use it that way. +### Bundle and minify + +Bundle all `.asm` and `.ppm` files and output the (optionaly minified) result. + +Usage example: +```sh +./build.ts --minify > pacman.asm +``` ## Documentation From 92ac475d58e6565ae832863428ac50032b2b0214 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Fri, 1 May 2026 17:22:22 +0200 Subject: [PATCH 18/22] make the build script use 'include' statement instead of a hard-coded '.asm' file list --- build.ts | 140 +++++++++++++++++++----------------- readme.md | 5 +- src/lib/drawing.asm | 9 +-- src/lib/game.asm | 1 - src/lib/sprite_rotation.asm | 4 -- src/main.asm | 21 ++++++ 6 files changed, 101 insertions(+), 79 deletions(-) diff --git a/build.ts b/build.ts index dc881db..cc8a7d9 100755 --- a/build.ts +++ b/build.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env -S deno run --allow-read --allow-run +#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run import { TextLineStream } from "jsr:@std/streams@1.1.0"; @@ -9,25 +9,7 @@ import { TextLineStream } from "jsr:@std/streams@1.1.0"; const instructionLength = 4; // in bytes const rawDataInstructionLengthPrefix = "U32"; -const ppmToAsmScriptPath = './ppmToAsm.ts'; - -const asmFilePaths = [ - "src/consts/arch.asm", - "src/consts/game.asm", - "src/consts/keyboard.asm", - "src/consts/map.asm", - "src/consts/scene.asm", - "src/consts/screen.asm", - "src/consts/sprites.asm", - - "src/init.asm", - "src/main.asm", - - "src/lib/game.asm", - "src/lib/drawing.asm", - "src/lib/sprite_rotation.asm", -]; -const reservedSpacePath = "src/reserved_space.asm"; +const ppmToAsmScriptPath = "./ppmToAsm.ts"; const mapPath = "assets/map.ppm"; const spritesPath = { @@ -117,7 +99,11 @@ class Minifier { } } -async function ppmToAsm(mode: "map" | "sprite", label: string, path: string): Promise { +async function ppmToAsm( + mode: "map" | "sprite", + label: string, + path: string, +): Promise { const command = new Deno.Command(ppmToAsmScriptPath, { args: [ instructionLength.toString(), @@ -140,62 +126,55 @@ async function ppmToAsm(mode: "map" | "sprite", label: string, path: string): Pr return decoder.decode(stdout); } -async function build(output: WritableStreamDefaultWriter) { - for (const path of asmFilePaths) { - const file = await Deno.open(path); - const readable = file.readable.pipeThrough(new TextDecoderStream()) - .pipeThrough(new TextLineStream()); +async function build( + pwd: string, + path: string, + output: WritableStreamDefaultWriter, +) { + const includeExpressionRegex = /^\s*include\s+([^\s;]+)/; - await output.write(` -;/******************************* -;* ${path} -;*******************************/ -`); + const file = await Deno.open(`${pwd}/${path}`); + const readable = file.readable.pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()); - for await (const line of readable) { - if (!line.startsWith("include ")) { - await output.write(`${line}\n`); - } + for await (const line of readable) { + const match = line.match(includeExpressionRegex); + if (match === null) { + await output.write(`${line}\n`); + continue; } + + await build(pwd, `${match[1]}.asm`, output); } - - const asmRawRepresentations = await Promise.all([ - ppmToAsm("map", "map", mapPath), - ...Object.entries(spritesPath).map(([label, path]) => ppmToAsm("sprite", label, path)), - ]); - - await output.write(asmRawRepresentations.join("\n\n")); - - await output.write(` -; -; RESERVED RAM SPACE -; -`); - - await output.write(await Deno.readTextFile(reservedSpacePath)); - await output.close(); } function printUsage(into: (message: string) => void) { into(`Usage: -\tbuild.ts [--minify] -\t\tMerge the hardcoded files and output the result to stdout. The "--minify" option can be used to strip the result of all comments, leading/trailing double whitespaces, empty lines and use the more compact raw data representation. +\tbuild.ts [options...] {file} +\t\tRead the given file and recursively include other '.asm' files to produce a single output. \tbuild.ts --help -\t\tPrint this message`); +\t\tPrint this message + +Options: +\t--minify\tWhen used, the result will be stripped of all comments, leading/trailing double whitespaces, empty lines and use the more compact raw data representation. +\t--ppm \tWhen used, the hardcoded ppm files will be translated to their matching Symfony assembly reprensentation into src/dist.`); } /** * SCRIPT */ -if (Deno.args.length > 1) { +if (Deno.args.length < 1) { printUsage(console.error); Deno.exit(1); } let minify = false; -if (Deno.args.length === 1) { - switch (Deno.args[0]) { +let convertPPM = false; +let path: string | undefined = undefined; + +Deno.args.forEach((arg, index) => { + switch (arg) { case "--help": printUsage(console.log); Deno.exit(0); @@ -203,11 +182,42 @@ if (Deno.args.length === 1) { case "--minify": minify = true; break; + case "--ppm": + convertPPM = true; + break; + default: - printUsage(console.error); - Deno.exit(1); + if (index !== Deno.args.length - 1) { + printUsage(console.error); + Deno.exit(1); + } + path = arg; break; } +}); + +if (path === undefined) { + printUsage(console.error); + Deno.exit(1); +} + +if (convertPPM) { + await Deno.mkdir(`${Deno.cwd()}/src/dist`, { recursive: true }); + const mapPromise = async () => { + const assembly = await ppmToAsm("map", "map", mapPath); + await Deno.writeTextFile(`${Deno.cwd()}/src/dist/map.asm`, assembly); + }; + + const spritePromises = Object.entries(spritesPath).map(async ([label, path]) => { + const assembly = await ppmToAsm("sprite", label, path); + const filename = path.split("/").pop()?.split('.')[0]; + await Deno.writeTextFile(`${Deno.cwd()}/src/dist/${filename}.asm`, assembly); + }); + + await Promise.all([ + mapPromise(), + ...spritePromises, + ]); } const encoder = new TextEncoder(); @@ -217,9 +227,9 @@ const stdoutWritable = new WritableStream({ }, }); -if (minify) { - const minifier = new Minifier(stdoutWritable); - await build(minifier.stream.writable.getWriter()); -} else { - await build(stdoutWritable.getWriter()); -} \ No newline at end of file +const writer = minify + ? (new Minifier(stdoutWritable)).stream.writable.getWriter() + : stdoutWritable.getWriter(); + +await build(`${Deno.cwd()}/src`, path, writer); +await writer.close(); diff --git a/readme.md b/readme.md index d5a85d8..226cf2d 100755 --- a/readme.md +++ b/readme.md @@ -43,7 +43,10 @@ Bundle all `.asm` and `.ppm` files and output the (optionaly minified) result. Usage example: ```sh -./build.ts --minify > pacman.asm +./build.ts --minify main.asm > pacman.asm + +# To beforehand convert all ppm files +./build.ts --ppm --minify main.asm > pacman.asm ``` ## Documentation diff --git a/src/lib/drawing.asm b/src/lib/drawing.asm index 2424274..ad4a7e5 100644 --- a/src/lib/drawing.asm +++ b/src/lib/drawing.asm @@ -1,11 +1,4 @@ -include ../consts/offsets -include ../consts/map -include ../consts/screen -include ../consts/scene -include ../consts/sprites -include sprite_rotation - -; /* Draw the whole (fill the screen memory with the values of the sprites based on the map) +; /* Draw the whole map (fill the screen memory with the values of the sprites based on the map) ; Inputs: diff --git a/src/lib/game.asm b/src/lib/game.asm index 64a86cb..a7f805b 100644 --- a/src/lib/game.asm +++ b/src/lib/game.asm @@ -1,4 +1,3 @@ - ; /* Return the tile address from the given map coordinate ; Inputs: diff --git a/src/lib/sprite_rotation.asm b/src/lib/sprite_rotation.asm index 2d3a1f0..3d3f70c 100644 --- a/src/lib/sprite_rotation.asm +++ b/src/lib/sprite_rotation.asm @@ -1,7 +1,3 @@ -include ../consts/offsets -include ../consts/map -include ../consts/sprites - ; /* Return the sprite id and rotation for the given map coordinates ; Inputs: diff --git a/src/main.asm b/src/main.asm index be88511..81de587 100644 --- a/src/main.asm +++ b/src/main.asm @@ -1,4 +1,10 @@ +include consts/arch +include consts/game +include consts/keyboard +include consts/map +include consts/scene include consts/screen +include consts/sprites include init @@ -347,4 +353,19 @@ game_loop: jmp game_loop +include lib/game include lib/drawing +include lib/sprite_rotation + +include dist/empty +include dist/placeholder +include dist/wall_end +include dist/wall_straight +include dist/wall_corner +include dist/robot +include dist/ghost +include dist/coin + +include dist/map + +include reserved_space From c78a2ff7a00c7cb461d1bf0242971731cc26f502 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Fri, 1 May 2026 18:27:51 +0200 Subject: [PATCH 19/22] handle multi-line comments --- build.ts | 27 +++++++++ src/consts/game.asm | 10 ++-- src/consts/screen.asm | 34 +++++------ src/init.asm | 18 +++--- src/lib/drawing.asm | 112 +++++++++++++++++------------------- src/lib/game.asm | 14 ++--- src/lib/sprite_rotation.asm | 32 +++++------ src/main.asm | 109 +++++++++++++++++------------------ 8 files changed, 188 insertions(+), 168 deletions(-) diff --git a/build.ts b/build.ts index cc8a7d9..fcb59e6 100755 --- a/build.ts +++ b/build.ts @@ -30,6 +30,7 @@ const spritesPath = { class Minifier { private buffer = ""; private lines: Array = []; + private insideComment = false; stream; constructor(writableStream: WritableStream) { @@ -51,6 +52,32 @@ class Minifier { } private processChunk(): string { + const commentFullRegex = /\/\*.*\*\//; + const commentStartRegex = /(.*)\/\*(.*)/; + const commentEndRegex = /(.*)\*\/(.*)/; + + if (this.insideComment) { + const matches = this.buffer.match(commentEndRegex); + if (matches === null) { + this.buffer = ''; + return ''; + } + + this.buffer = matches[2]; + this.insideComment = false; + } + + if (!this.insideComment) { + this.buffer = this.buffer.replace(commentFullRegex, ''); + + const matches = this.buffer.match(commentStartRegex); + if (matches !== null) { + this.buffer = matches[1]; + this.insideComment = true; + return ''; + } + } + const newLines = this.buffer.split("\n"); this.buffer = newLines.pop() as string; // keep the last (potentially incomplete) line diff --git a/src/consts/game.asm b/src/consts/game.asm index bbf69f2..9ef3ae8 100644 --- a/src/consts/game.asm +++ b/src/consts/game.asm @@ -4,11 +4,13 @@ pub const game.direction_down = 2 pub const game.direction_left = 3 pub const game.direction_still = 4 +/* ; 1_000_000_000 nano second = 0x0000_0000_3B9A_CA00 -; pub const game.tick_duration_0 = 0xCA00 -; pub const game.tick_duration_1 = 0x3B9A -; pub const game.tick_duration_2 = 0x0000 -; pub const game.tick_duration_3 = 0x0000 +pub const game.tick_duration_0 = 0xCA00 +pub const game.tick_duration_1 = 0x3B9A +pub const game.tick_duration_2 = 0x0000 +pub const game.tick_duration_3 = 0x0000 +*/ ; 0.5 second = 500_000_000 nano second = 0x0000_0000_1DCD_6500 pub const game.tick_duration_0 = 0x6500 diff --git a/src/consts/screen.asm b/src/consts/screen.asm index a4b8231..ce4ce52 100644 --- a/src/consts/screen.asm +++ b/src/consts/screen.asm @@ -1,21 +1,21 @@ -; Screen pixel mode (settings #0) -; 0: ASCII 8 -; 1: ASCII 24 -; 2: Pixel 8 -; 3: Pixel 24 - -; While in mode #2 (Pixel 8) - ; Screen memory offset (settings #1) - ; Screen resolutions (settings #2) - ; 0. 80x60 - ; 1. 160x120 - ; 2. 256x192 - ; 3. 320x240 - ; 4. 640x480 - ; 5. 800x600 - ; 6. 920x720 - ; 7. 1024x768 +/* Screen pixel mode (settings #0) + 0: ASCII 8 + 1: ASCII 24 + 2: Pixel 8 + 3: Pixel 24 + While in mode #2 (Pixel 8) + Screen memory offset (settings #1) + Screen resolutions (settings #2) + 0. 80x60 + 1. 160x120 + 2. 256x192 + 3. 320x240 + 4. 640x480 + 5. 800x600 + 6. 920x720 + 7. 1024x768 +*/ pub const screen.mode_index = 0 pub const screen.mode_value = 2 pub const screen.offset_index = 1 diff --git a/src/init.asm b/src/init.asm index c5688a2..fdda797 100644 --- a/src/init.asm +++ b/src/init.asm @@ -1,9 +1,9 @@ -; -; Jump table initialisation -; -; r1: sprite id -; r2: pointer address -; r3: pointer value +/* Jump table initialisation + + r1: sprite id + r2: pointer address + r3: pointer value +*/ ; sprite_empty add r1, zr, sprites.empty @@ -54,9 +54,9 @@ add r2, r2, ptr.textures_addresses add r3, zr, sprite_coin store_32 [r2], r3 -; -; Screen initialization -; +/* + Screen initialization +*/ ; r1 = screen parameter value mov r1, screen.mode_index diff --git a/src/lib/drawing.asm b/src/lib/drawing.asm index ad4a7e5..1f7b553 100644 --- a/src/lib/drawing.asm +++ b/src/lib/drawing.asm @@ -1,17 +1,13 @@ -; /* Draw the whole map (fill the screen memory with the values of the sprites based on the map) +/* Draw the whole map (fill the screen memory with the values of the sprites based on the map) -; Inputs: - -; Outputs: - -; Locals: -; r1: x map coord -; r2: y map coord -; r3: sprite rotation -; r4: sprite id -; r12: rotation (returned by `sprite and rotation`) -; r13: sprite (returned by `get_sprite_id_and_rotation`) -; */ + Locals: + r1: x map coord + r2: y map coord + r3: sprite rotation + r4: sprite id + r12: rotation (returned by `sprite and rotation`) + r13: sprite (returned by `get_sprite_id_and_rotation`) +*/ pub drawing.draw_whole_map: push r1 push r2 @@ -45,19 +41,19 @@ pub drawing.draw_whole_map: ret -; /* Return the top-left pixel address of the tile at the given map coordinates +/* Return the top-left pixel address of the tile at the given map coordinates -; Inputs: -; r1: map x coord -; r2: map y coord + Inputs: + r1: map x coord + r2: map y coord -; Locals: -; r3: counter -; r4: offset per tile row (screen width * tile height) + Locals: + r3: counter + r4: offset per tile row (screen width * tile height) -; Ouputs: -; r13: the pixel address -; */ + Ouputs: + r13: the pixel address +*/ get_tile_address: push r3 push r4 @@ -104,17 +100,17 @@ get_tile_address: pop r3 ret -; /* Get address of given sprite id +/* Get address of given sprite id -; Inputs: -; r1: sprite id + Inputs: + r1: sprite id -; Outputs: -; r13: address + Outputs: + r13: address -; Locals: -; r2: pointer address -; */ + Locals: + r2: pointer address +*/ get_sprite_address: push r1 push r2 @@ -132,21 +128,19 @@ get_sprite_address: pop r1 ret -; /* Draw given sprite on the screen at given map coordinates +/* Draw given sprite on the screen at given map coordinates -; Inputs: -; r1: map x coord -; r2: map y coord -; r3: sprite rotation -; r4: sprite id + Inputs: + r1: map x coord + r2: map y coord + r3: sprite rotation + r4: sprite id -; Outputs: - -; Locals: -; r1: screen address -; r2: sprite address -; r13: value returned by call to `get_tile_address` and `get_sprite_address` -; */ + Locals: + r1: screen address + r2: sprite address + r13: value returned by call to `get_tile_address` and `get_sprite_address` +*/ pub drawing.draw_sprite_at_map_coord: push r1 push r2 @@ -172,24 +166,22 @@ pub drawing.draw_sprite_at_map_coord: pop r1 ret -; /* Draw given sprite (by address) at given screen position (by address) with given rotation +/* Draw given sprite (by address) at given screen position (by address) with given rotation -; Inputs: -; r1: screen address -; r2: sprite address -; r3: rotation + Inputs: + r1: screen address + r2: sprite address + r3: rotation -; Outputs: - -; Locals: -; r1: current screen pixel address -; r4: x -; r5: y -; r6: current sprite pixel address -; r7: color -; r8: x increment -; r9: y increment -; */ + Locals: + r1: current screen pixel address + r4: x + r5: y + r6: current sprite pixel address + r7: color + r8: x increment + r9: y increment +*/ draw_sprite: push r1 push r2 diff --git a/src/lib/game.asm b/src/lib/game.asm index a7f805b..e23967e 100644 --- a/src/lib/game.asm +++ b/src/lib/game.asm @@ -1,12 +1,12 @@ -; /* Return the tile address from the given map coordinate +/* Return the tile address from the given map coordinate -; Inputs: -; r1: x map coordinate -; r2: y map coordinate + Inputs: + r1: x map coordinate + r2: y map coordinate -; Outputs: -; r13: tile address -; */ + Outputs: + r13: tile address +*/ pub game.get_tile_address_from_map_coord: push r1 push r2 diff --git a/src/lib/sprite_rotation.asm b/src/lib/sprite_rotation.asm index 3d3f70c..4336162 100644 --- a/src/lib/sprite_rotation.asm +++ b/src/lib/sprite_rotation.asm @@ -1,22 +1,22 @@ -; /* Return the sprite id and rotation for the given map coordinates +/* Return the sprite id and rotation for the given map coordinates -; Inputs: -; r1: x map coord -; r2: y map coord + Inputs: + r1: x map coord + r2: y map coord -; Outputs: -; r12: sprite rotation -; r13: sprite id + Outputs: + r12: sprite rotation + r13: sprite id -; Locals: -; r3: tile id -; r4: map offset -; r5: counter -; r6: left tile id -; r7: up tile id -; r8: right tile id -; r9: down tile id -; */ + Locals: + r3: tile id + r4: map offset + r5: counter + r6: left tile id + r7: up tile id + r8: right tile id + r9: down tile id +*/ pub sprite_rotation.get_sprite_id_and_rotation: push r1 push r2 diff --git a/src/main.asm b/src/main.asm index 81de587..1465223 100644 --- a/src/main.asm +++ b/src/main.asm @@ -10,9 +10,9 @@ include init call drawing.draw_whole_map -; -; State initialization -; +/* + State initialization +*/ ; Robot const robot_initial_x = 1 const robot_initial_y = 1 @@ -51,9 +51,9 @@ mov r3, sprites.rotation_0 mov r4, sprites.ghost call drawing.draw_sprite_at_map_coord -; -; Time initialization -; +/* + Time initialization +*/ add r1, zr, ptr.tick_duration_low load_32 r2, [r1] add r1, zr, ptr.tick_duration_high @@ -74,13 +74,13 @@ add r1, zr, ptr.next_tick_high store_32 [r1], r5 game_loop: - ; - ; Event handling - ; - ; r1 = keyboard event+key / direction - ; r2 = keyboard key / direction address - ; r3 = keyboard event - ; r4 = jump address + /* Event handling + + r1 = keyboard event+key / direction + r2 = keyboard key / direction address + r3 = keyboard event + r4 = jump address + */ keyboard r1 and r2, r1, keyboard.mask_key and r3, r1, keyboard.mask_event @@ -109,7 +109,7 @@ game_loop: jmp game_loop_event_handling_store_direction ; key_left: mov r1, game.direction_left -; jmp game_loop_event_handling_end + ;jmp game_loop_event_handling_end game_loop_event_handling_store_direction: add r2, zr, ptr.state_robot @@ -118,15 +118,14 @@ game_loop: game_loop_event_handling_end: - ; - ; Elapsed time check - ; - ; r1: address - ; r2: now low / delay low - ; r3: now high / delay high - ; r4: next tick low - ; r5: next tick high - ; + /* Elapsed time check + + r1: address + r2: now low / delay low + r3: now high / delay high + r4: next tick low + r5: next tick high + */ time_0 r2 time_1 r3 @@ -159,18 +158,18 @@ game_loop: store_32 [r1], r5 - ; - ; Robot movement - ; - ; r1 = robot's state address - ; r2 = x - ; r3 = y - ; r4 = direction - ; r5 = new x - ; r6 = new y - ; r7 = new position address - ; r8 = new position content - ; r9 = jump address + /* Robot movement + + r1 = robot's state address + r2 = x + r3 = y + r4 = direction + r5 = new x + r6 = new y + r7 = new position address + r8 = new position content + r9 = jump address + */ add r1, zr, ptr.state_robot load_8 r2, [r1] add r1, r1, 1 @@ -209,9 +208,9 @@ game_loop: mov r7, r13 pop r2 - ; - ; Collision handling - ; + /* + Collision handling + */ load_8 r8, [r7] cmp r8, map.tiles.wall jne game_loop_robot_movement_some @@ -249,20 +248,20 @@ game_loop: game_loop_robot_movement_none: - ; - ; Ghost movement - ; - ; r1: ghost's state address - ; r2: x - ; r3: y - ; r4: direction - ; r5: x increment - ; r6: y increment - ; r7: new x - ; r8: new y - ; r9: new position address - ; r10: new position content - ; r11: jump address + /* Ghost movement + + r1: ghost's state address + r2: x + r3: y + r4: direction + r5: x increment + r6: y increment + r7: new x + r8: new y + r9: new position address + r10: new position content + r11: jump address + */ add r1, zr, ptr.state_ghost load_8 r2, [r1] add r1, r1, 1 @@ -331,9 +330,9 @@ game_loop: mov r4, sprites.ghost call drawing.draw_sprite_at_map_coord - ; - ; Check ahead for direction change - ; + /* + Check ahead for direction change + */ add r1, r7, r5 add r2, r8, r6 call game.get_tile_address_from_map_coord From 425b83b1bb84a52f88d23bc90be4da748422bace Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Fri, 1 May 2026 18:48:12 +0200 Subject: [PATCH 20/22] split code in sections --- src/main.asm | 345 +----------------- src/sections/characters_movement.asm | 231 ++++++++++++ src/sections/handle_event.asm | 43 +++ src/sections/init_ghost_state.asm | 17 + src/sections/init_robot_state.asm | 17 + src/sections/init_screen.asm | 10 + .../init_sprite_lookup_table.asm} | 15 - src/sections/init_timers.asm | 18 + 8 files changed, 343 insertions(+), 353 deletions(-) create mode 100644 src/sections/characters_movement.asm create mode 100644 src/sections/handle_event.asm create mode 100644 src/sections/init_ghost_state.asm create mode 100644 src/sections/init_robot_state.asm create mode 100644 src/sections/init_screen.asm rename src/{init.asm => sections/init_sprite_lookup_table.asm} (85%) create mode 100644 src/sections/init_timers.asm diff --git a/src/main.asm b/src/main.asm index 1465223..9282b9c 100644 --- a/src/main.asm +++ b/src/main.asm @@ -6,349 +6,18 @@ include consts/scene include consts/screen include consts/sprites -include init +include sections/init_sprite_lookup_table +include sections/init_screen call drawing.draw_whole_map -/* - State initialization -*/ -; Robot -const robot_initial_x = 1 -const robot_initial_y = 1 -const robot_initial_direction = game.direction_down - -mov r1, robot_initial_x -mov r2, robot_initial_y -mov r5, robot_initial_direction -add r6, zr, ptr.state_robot -store_8 [r6], r1 -add r6, r6, 1 -store_8 [r6], r2 -add r6, r6, 1 -store_8 [r6], r5 - -mov r3, sprites.rotation_0 -mov r4, sprites.robot -call drawing.draw_sprite_at_map_coord - -; Ghost -const ghost_initial_x = 13 -const ghost_initial_y = 1 -const ghost_initial_direction = game.direction_down - -mov r1, ghost_initial_x -mov r2, ghost_initial_y -mov r5, ghost_initial_direction -add r6, zr, ptr.state_ghost -store_8 [r6], r1 -add r6, r6, 1 -store_8 [r6], r2 -add r6, r6, 1 -store_8 [r6], r5 - -mov r3, sprites.rotation_0 -mov r4, sprites.ghost -call drawing.draw_sprite_at_map_coord - -/* - Time initialization -*/ -add r1, zr, ptr.tick_duration_low -load_32 r2, [r1] -add r1, zr, ptr.tick_duration_high -load_32 r3, [r1] - -time_0 r4 -time_1 r5 -add r4, r4, r2 -add r5, r5, r3 -cmp r4, r2 -jae init_store_next_tick -add r5, r5, 1 ; overflow - -init_store_next_tick: -add r1, zr, ptr.next_tick_low -store_32 [r1], r4 -add r1, zr, ptr.next_tick_high -store_32 [r1], r5 +include sections/init_robot_state +include sections/init_ghost_state +include sections/init_timers game_loop: - /* Event handling - - r1 = keyboard event+key / direction - r2 = keyboard key / direction address - r3 = keyboard event - r4 = jump address - */ - keyboard r1 - and r2, r1, keyboard.mask_key - and r3, r1, keyboard.mask_event - - cmp r3, keyboard.event_pressed - jne game_loop_event_handling_end - cmp r2, keyboard.key_min - jb game_loop_event_handling_end - cmp r2, keyboard.key_max - ja game_loop_event_handling_end - - sub r4, r2, keyboard.key_min ; so the first value is 0 - lsl r4, r4, 3 ; 2 * instruction length = 8 - add r4, r4, game_loop_key_jump_table_start - jmp r4 - - game_loop_key_jump_table_start: - ; key_up - mov r1, game.direction_up - jmp game_loop_event_handling_store_direction - ; key_right - mov r1, game.direction_right - jmp game_loop_event_handling_store_direction - ; key_down: - mov r1, game.direction_down - jmp game_loop_event_handling_store_direction - ; key_left: - mov r1, game.direction_left - ;jmp game_loop_event_handling_end - - game_loop_event_handling_store_direction: - add r2, zr, ptr.state_robot - add r2, r2, 2 - store_8 [r2], r1 - - game_loop_event_handling_end: - - /* Elapsed time check - - r1: address - r2: now low / delay low - r3: now high / delay high - r4: next tick low - r5: next tick high - */ - time_0 r2 - time_1 r3 - - add r1, zr, ptr.next_tick_low - load_32 r4, [r1] - add r1, zr, ptr.next_tick_high - load_32 r5, [r1] - - cmp r3, r5 - jb game_loop - ja update_next_tick - cmp r2, r4 - jb game_loop - - update_next_tick: - add r1, zr, ptr.tick_duration_low - load_32 r2, [r1] - add r1, zr, ptr.tick_duration_high - load_32 r3, [r1] - add r4, r4, r2 - add r5, r5, r3 - cmp r4, r2 - jae store_next_tick - add r5, r5, 1 - - store_next_tick: - add r1, zr, ptr.next_tick_low - store_32 [r1], r4 - add r1, zr, ptr.next_tick_high - store_32 [r1], r5 - - - /* Robot movement - - r1 = robot's state address - r2 = x - r3 = y - r4 = direction - r5 = new x - r6 = new y - r7 = new position address - r8 = new position content - r9 = jump address - */ - add r1, zr, ptr.state_robot - load_8 r2, [r1] - add r1, r1, 1 - load_8 r3, [r1] - add r1, r1, 1 - load_8 r4, [r1] - - mov r5, r2 - mov r6, r3 - - lsl r9, r4, 3 ; 2 * instruction size = 8 - add r9, r9, game_loop_robot_movement_jump_table_start - jmp r9 - - game_loop_robot_movement_jump_table_start: - ; direction_up - sub r6, r6, 1 - jmp game_loop_robot_movement_try_move - ; direction_right - add r5, r5, 1 - jmp game_loop_robot_movement_try_move - ; direction_down - add r6, r6, 1 - jmp game_loop_robot_movement_try_move - ; direction_left - sub r5, r5, 1 - jmp game_loop_robot_movement_try_move - ; direction_still - jmp game_loop_robot_movement_none - - game_loop_robot_movement_try_move: - push r2 - mov r1, r5 - mov r2, r6 - call game.get_tile_address_from_map_coord - mov r7, r13 - pop r2 - - /* - Collision handling - */ - load_8 r8, [r7] - cmp r8, map.tiles.wall - jne game_loop_robot_movement_some - ; hit wall - add r1, zr, ptr.state_robot - add r1, r1, 2 - mov r4, game.direction_still - store_8 [r1], r4 - jmp game_loop_robot_movement_none - - game_loop_robot_movement_some: - ; TODO: handle coin and empty differently - mov r8, map.tiles.empty - store_8 [r7], r8 - - add r1, zr, ptr.state_robot - store_8 [r1], r5 - add r1, r1, 1 - store_8 [r1], r6 - - ; clear old position - mov r1, r2 - mov r2, r3 - mov r3, sprites.rotation_0 - mov r4, sprites.empty - call drawing.draw_sprite_at_map_coord - - ; draw robot on new position - mov r1, r5 - mov r2, r6 - mov r3, sprites.rotation_0 - mov r4, sprites.robot - call drawing.draw_sprite_at_map_coord - - - game_loop_robot_movement_none: - - /* Ghost movement - - r1: ghost's state address - r2: x - r3: y - r4: direction - r5: x increment - r6: y increment - r7: new x - r8: new y - r9: new position address - r10: new position content - r11: jump address - */ - add r1, zr, ptr.state_ghost - load_8 r2, [r1] - add r1, r1, 1 - load_8 r3, [r1] - add r1, r1, 1 - load_8 r4, [r1] - - mov r5, 0 - mov r6, 0 - - lsl r11, r4, 3 ; 2 * instruction size = 8 - add r11, r11, game_loop_ghost_movement_jump_table_start - jmp r11 - - game_loop_ghost_movement_jump_table_start: - ; direction_up - sub r6, r6, 1 - jmp game_loop_ghost_movement_make_move - ; direction_right - add r5, r5, 1 - jmp game_loop_ghost_movement_make_move - ; direction_down - add r6, r6, 1 - jmp game_loop_ghost_movement_make_move - ; direction_left - sub r5, r5, 1 - ;jmp game_loop_ghost_movement_make_move - ; direction_still (should never happen) - - game_loop_ghost_movement_make_move: - add r7, r2, r5 - add r8, r3, r6 - add r1, zr, ptr.state_ghost - store_8 [r1], r7 - add r1, r1, 1 - store_8 [r1], r8 - - ; get tile at previous position - push r2 - mov r1, r2 - mov r2, r3 - call game.get_tile_address_from_map_coord - mov r9, r13 - pop r2 - - ; draw tile at previous position - load_8 r1, [r9] - cmp r1, map.tiles.coin - je game_loop_ghost_movement_old_tile_coin - ; old tile empty - mov r4, sprites.empty - jmp game_loop_ghost_movement_draw_old_tile - game_loop_ghost_movement_old_tile_coin: - mov r4, sprites.coin - - game_loop_ghost_movement_draw_old_tile: - mov r1, r2 - mov r2, r3 - mov r3, sprites.rotation_0 - call drawing.draw_sprite_at_map_coord - - ; draw ghost on new position - mov r1, r7 - mov r2, r8 - mov r3, sprites.rotation_0 - mov r4, sprites.ghost - call drawing.draw_sprite_at_map_coord - - /* - Check ahead for direction change - */ - add r1, r7, r5 - add r2, r8, r6 - call game.get_tile_address_from_map_coord - - load_8 r10, [r13] - cmp r10, map.tiles.wall - jne game_loop_ghost_movement_end - ; increment direction - add r1, zr, ptr.state_ghost - add r1, r1, 2 - load_8 r4, [r1] - add r4, r4, 1 - and r4, r4, 0b11 ; modulo 3 - store_8 [r1], r4 - - game_loop_ghost_movement_end: + include sections/handle_event + include sections/characters_movement jmp game_loop diff --git a/src/sections/characters_movement.asm b/src/sections/characters_movement.asm new file mode 100644 index 0000000..b5afced --- /dev/null +++ b/src/sections/characters_movement.asm @@ -0,0 +1,231 @@ +/* Elapsed time check + + r1: address + r2: now low / delay low + r3: now high / delay high + r4: next tick low + r5: next tick high +*/ +time_0 r2 +time_1 r3 + +add r1, zr, ptr.next_tick_low +load_32 r4, [r1] +add r1, zr, ptr.next_tick_high +load_32 r5, [r1] + +cmp r3, r5 +jb game_loop +ja update_next_tick +cmp r2, r4 +jb game_loop + +update_next_tick: +add r1, zr, ptr.tick_duration_low +load_32 r2, [r1] +add r1, zr, ptr.tick_duration_high +load_32 r3, [r1] +add r4, r4, r2 +add r5, r5, r3 +cmp r4, r2 +jae store_next_tick +add r5, r5, 1 + +store_next_tick: +add r1, zr, ptr.next_tick_low +store_32 [r1], r4 +add r1, zr, ptr.next_tick_high +store_32 [r1], r5 + + +/* Robot movement + + r1 = robot's state address + r2 = x + r3 = y + r4 = direction + r5 = new x + r6 = new y + r7 = new position address + r8 = new position content + r9 = jump address +*/ +add r1, zr, ptr.state_robot +load_8 r2, [r1] +add r1, r1, 1 +load_8 r3, [r1] +add r1, r1, 1 +load_8 r4, [r1] + +mov r5, r2 +mov r6, r3 + +lsl r9, r4, 3 ; 2 * instruction size = 8 +add r9, r9, game_loop_robot_movement_jump_table_start +jmp r9 + +game_loop_robot_movement_jump_table_start: +; direction_up +sub r6, r6, 1 +jmp game_loop_robot_movement_try_move +; direction_right +add r5, r5, 1 +jmp game_loop_robot_movement_try_move +; direction_down +add r6, r6, 1 +jmp game_loop_robot_movement_try_move +; direction_left +sub r5, r5, 1 +jmp game_loop_robot_movement_try_move +; direction_still +jmp game_loop_robot_movement_none + +game_loop_robot_movement_try_move: +push r2 +mov r1, r5 +mov r2, r6 +call game.get_tile_address_from_map_coord +mov r7, r13 +pop r2 + +/* + Collision handling +*/ +load_8 r8, [r7] +cmp r8, map.tiles.wall +jne game_loop_robot_movement_some +; hit wall +add r1, zr, ptr.state_robot +add r1, r1, 2 +mov r4, game.direction_still +store_8 [r1], r4 +jmp game_loop_robot_movement_none + +game_loop_robot_movement_some: +; TODO: handle coin and empty differently +mov r8, map.tiles.empty +store_8 [r7], r8 + +add r1, zr, ptr.state_robot +store_8 [r1], r5 +add r1, r1, 1 +store_8 [r1], r6 + +; clear old position +mov r1, r2 +mov r2, r3 +mov r3, sprites.rotation_0 +mov r4, sprites.empty +call drawing.draw_sprite_at_map_coord + +; draw robot on new position +mov r1, r5 +mov r2, r6 +mov r3, sprites.rotation_0 +mov r4, sprites.robot +call drawing.draw_sprite_at_map_coord + + +game_loop_robot_movement_none: + +/* Ghost movement + + r1: ghost's state address + r2: x + r3: y + r4: direction + r5: x increment + r6: y increment + r7: new x + r8: new y + r9: new position address + r10: new position content + r11: jump address +*/ +add r1, zr, ptr.state_ghost +load_8 r2, [r1] +add r1, r1, 1 +load_8 r3, [r1] +add r1, r1, 1 +load_8 r4, [r1] + +mov r5, 0 +mov r6, 0 + +lsl r11, r4, 3 ; 2 * instruction size = 8 +add r11, r11, game_loop_ghost_movement_jump_table_start +jmp r11 + +game_loop_ghost_movement_jump_table_start: +; direction_up +sub r6, r6, 1 +jmp game_loop_ghost_movement_make_move +; direction_right +add r5, r5, 1 +jmp game_loop_ghost_movement_make_move +; direction_down +add r6, r6, 1 +jmp game_loop_ghost_movement_make_move +; direction_left +sub r5, r5, 1 +;jmp game_loop_ghost_movement_make_move +; direction_still (should never happen) + +game_loop_ghost_movement_make_move: +add r7, r2, r5 +add r8, r3, r6 +add r1, zr, ptr.state_ghost +store_8 [r1], r7 +add r1, r1, 1 +store_8 [r1], r8 + +; get tile at previous position +push r2 +mov r1, r2 +mov r2, r3 +call game.get_tile_address_from_map_coord +mov r9, r13 +pop r2 + +; draw tile at previous position +load_8 r1, [r9] +cmp r1, map.tiles.coin +je game_loop_ghost_movement_old_tile_coin +; old tile empty +mov r4, sprites.empty +jmp game_loop_ghost_movement_draw_old_tile +game_loop_ghost_movement_old_tile_coin: +mov r4, sprites.coin + +game_loop_ghost_movement_draw_old_tile: +mov r1, r2 +mov r2, r3 +mov r3, sprites.rotation_0 +call drawing.draw_sprite_at_map_coord + +; draw ghost on new position +mov r1, r7 +mov r2, r8 +mov r3, sprites.rotation_0 +mov r4, sprites.ghost +call drawing.draw_sprite_at_map_coord + +/* + Check ahead for direction change +*/ +add r1, r7, r5 +add r2, r8, r6 +call game.get_tile_address_from_map_coord + +load_8 r10, [r13] +cmp r10, map.tiles.wall +jne game_loop_ghost_movement_end +; increment direction +add r1, zr, ptr.state_ghost +add r1, r1, 2 +load_8 r4, [r1] +add r4, r4, 1 +and r4, r4, 0b11 ; modulo 3 +store_8 [r1], r4 + +game_loop_ghost_movement_end: \ No newline at end of file diff --git a/src/sections/handle_event.asm b/src/sections/handle_event.asm new file mode 100644 index 0000000..f219eab --- /dev/null +++ b/src/sections/handle_event.asm @@ -0,0 +1,43 @@ +/* Event handling + + r1 = keyboard event+key / direction + r2 = keyboard key / direction address + r3 = keyboard event + r4 = jump address +*/ +keyboard r1 +and r2, r1, keyboard.mask_key +and r3, r1, keyboard.mask_event + +cmp r3, keyboard.event_pressed +jne handle_event_end +cmp r2, keyboard.key_min +jb handle_event_end +cmp r2, keyboard.key_max +ja handle_event_end + +sub r4, r2, keyboard.key_min ; so the first value is 0 +lsl r4, r4, 3 ; 2 * instruction length = 8 +add r4, r4, handle_event_jump_table_start +jmp r4 + +handle_event_jump_table_start: +; key_up +mov r1, game.direction_up +jmp handle_event_store_direction +; key_right +mov r1, game.direction_right +jmp handle_event_store_direction +; key_down: +mov r1, game.direction_down +jmp handle_event_store_direction +; key_left: +mov r1, game.direction_left +;jmp handle_event_end + +handle_event_store_direction: +add r2, zr, ptr.state_robot +add r2, r2, 2 +store_8 [r2], r1 + +handle_event_end: \ No newline at end of file diff --git a/src/sections/init_ghost_state.asm b/src/sections/init_ghost_state.asm new file mode 100644 index 0000000..5bb41d0 --- /dev/null +++ b/src/sections/init_ghost_state.asm @@ -0,0 +1,17 @@ +const ghost_initial_x = 13 +const ghost_initial_y = 1 +const ghost_initial_direction = game.direction_down + +mov r1, ghost_initial_x +mov r2, ghost_initial_y +mov r5, ghost_initial_direction +add r6, zr, ptr.state_ghost +store_8 [r6], r1 +add r6, r6, 1 +store_8 [r6], r2 +add r6, r6, 1 +store_8 [r6], r5 + +mov r3, sprites.rotation_0 +mov r4, sprites.ghost +call drawing.draw_sprite_at_map_coord \ No newline at end of file diff --git a/src/sections/init_robot_state.asm b/src/sections/init_robot_state.asm new file mode 100644 index 0000000..383244a --- /dev/null +++ b/src/sections/init_robot_state.asm @@ -0,0 +1,17 @@ +const robot_initial_x = 1 +const robot_initial_y = 1 +const robot_initial_direction = game.direction_down + +mov r1, robot_initial_x +mov r2, robot_initial_y +mov r5, robot_initial_direction +add r6, zr, ptr.state_robot +store_8 [r6], r1 +add r6, r6, 1 +store_8 [r6], r2 +add r6, r6, 1 +store_8 [r6], r5 + +mov r3, sprites.rotation_0 +mov r4, sprites.robot +call drawing.draw_sprite_at_map_coord \ No newline at end of file diff --git a/src/sections/init_screen.asm b/src/sections/init_screen.asm new file mode 100644 index 0000000..aecff20 --- /dev/null +++ b/src/sections/init_screen.asm @@ -0,0 +1,10 @@ +; r1 = screen parameter value +mov r1, screen.mode_index +screen r1, screen.mode_value + +mov r1, screen.offset_index +add r2, zr, ptr.screen +screen r1, r2 + +mov r1, screen.resolution_index +screen r1, screen.resolution_value \ No newline at end of file diff --git a/src/init.asm b/src/sections/init_sprite_lookup_table.asm similarity index 85% rename from src/init.asm rename to src/sections/init_sprite_lookup_table.asm index fdda797..1960054 100644 --- a/src/init.asm +++ b/src/sections/init_sprite_lookup_table.asm @@ -53,18 +53,3 @@ lsl r2, r1, arch.instruction_increment_shift add r2, r2, ptr.textures_addresses add r3, zr, sprite_coin store_32 [r2], r3 - -/* - Screen initialization -*/ - -; r1 = screen parameter value -mov r1, screen.mode_index -screen r1, screen.mode_value - -mov r1, screen.offset_index -add r2, zr, ptr.screen -screen r1, r2 - -mov r1, screen.resolution_index -screen r1, screen.resolution_value diff --git a/src/sections/init_timers.asm b/src/sections/init_timers.asm new file mode 100644 index 0000000..415be9a --- /dev/null +++ b/src/sections/init_timers.asm @@ -0,0 +1,18 @@ +add r1, zr, ptr.tick_duration_low +load_32 r2, [r1] +add r1, zr, ptr.tick_duration_high +load_32 r3, [r1] + +time_0 r4 +time_1 r5 +add r4, r4, r2 +add r5, r5, r3 +cmp r4, r2 +jae init_store_next_tick +add r5, r5, 1 ; overflow + +init_store_next_tick: +add r1, zr, ptr.next_tick_low +store_32 [r1], r4 +add r1, zr, ptr.next_tick_high +store_32 [r1], r5 \ No newline at end of file From 8b3b2aa20d15cbc2dc1c5503b93deaa7a2597579 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Fri, 1 May 2026 21:13:55 +0200 Subject: [PATCH 21/22] add directional characters display --- assets/sprites/{ghost.ppm => ghost_down.ppm} | Bin assets/sprites/ghost_left.ppm | Bin 0 -> 130 bytes assets/sprites/ghost_right.ppm | Bin 0 -> 130 bytes assets/sprites/ghost_up.ppm | Bin 0 -> 130 bytes assets/sprites/robot.ppm | Bin 130 -> 0 bytes assets/sprites/robot_down.ppm | Bin 0 -> 130 bytes assets/sprites/robot_left.ppm | Bin 0 -> 130 bytes assets/sprites/robot_right.ppm | Bin 0 -> 130 bytes assets/sprites/robot_up.ppm | Bin 0 -> 130 bytes build.ts | 10 +- src/consts/game.asm | 15 -- src/consts/sprites.asm | 19 +- src/lib/drawing.asm | 53 +++++ src/lib/u64.asm | 18 ++ src/main.asm | 36 ++- src/reserved_space.asm | 17 +- src/sections/characters_movement.asm | 231 ------------------- src/sections/init_ghost_state.asm | 2 +- src/sections/init_robot_state.asm | 2 +- src/sections/init_sprite_lookup_table.asm | 51 +++- src/sections/init_timers.asm | 48 ++-- src/sections/timer_ghost.asm | 99 ++++++++ src/sections/timer_robot.asm | 99 ++++++++ 23 files changed, 417 insertions(+), 283 deletions(-) rename assets/sprites/{ghost.ppm => ghost_down.ppm} (100%) create mode 100644 assets/sprites/ghost_left.ppm create mode 100644 assets/sprites/ghost_right.ppm create mode 100644 assets/sprites/ghost_up.ppm delete mode 100644 assets/sprites/robot.ppm create mode 100644 assets/sprites/robot_down.ppm create mode 100644 assets/sprites/robot_left.ppm create mode 100644 assets/sprites/robot_right.ppm create mode 100644 assets/sprites/robot_up.ppm create mode 100644 src/lib/u64.asm delete mode 100644 src/sections/characters_movement.asm create mode 100644 src/sections/timer_ghost.asm create mode 100644 src/sections/timer_robot.asm diff --git a/assets/sprites/ghost.ppm b/assets/sprites/ghost_down.ppm similarity index 100% rename from assets/sprites/ghost.ppm rename to assets/sprites/ghost_down.ppm diff --git a/assets/sprites/ghost_left.ppm b/assets/sprites/ghost_left.ppm new file mode 100644 index 0000000000000000000000000000000000000000..5e632614cd9c85d77e087c15d2bb69ea721745f2 GIT binary patch literal 130 zcmWGA<5E^|E=o--Nlj5ms#I|I^bJrbOD!tS%+FIW)-%#GQ3&w!RVc_QP1nuL<1$q+ g(^b literal 0 HcmV?d00001 diff --git a/assets/sprites/ghost_right.ppm b/assets/sprites/ghost_right.ppm new file mode 100644 index 0000000000000000000000000000000000000000..e72da9a532525cc33051f2288550b98766dc984c GIT binary patch literal 130 zcmWGA<5E^|E=o--Nlj5ms#I|I^bJrbOD!tS%+FIW)-%#GQ3&w!RVc_QP1nuL<1$q+ igAh;lDkXi%@0B5%&CIA2c literal 0 HcmV?d00001 diff --git a/assets/sprites/robot_left.ppm b/assets/sprites/robot_left.ppm new file mode 100644 index 0000000000000000000000000000000000000000..68b44fae03b22f734cb569d9c3437f98f14a2016 GIT binary patch literal 130 zcmWGA<5E^|E=o--Nlj5ms#I|I^bJrbOD!tS%+FIW)-%#GQ3&w!RVc_QP1nuL<1$q+ l cancel movement +sub r1, r1, r5 +sub r2, r2, r6 +jmp timer_robot.draw + +pub timer_robot.move: +; TODO: handle coin and empty differently +mov r10, map.tiles.empty +store_8 [r13], r10 + +add r9, zr, ptr.state_robot +store_8 [r9], r1 +add r9, r9, 1 +store_8 [r9], r2 + +pub timer_robot.draw: +; draw robot on new position +mov r3, sprites.rotation_0 +add r4, r4, sprites.robot +call drawing.draw_sprite_at_map_coord + +pub timer_robot.end: \ No newline at end of file From ba12492ebf53410d73808217262394d8fc72f666 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Fri, 1 May 2026 21:14:51 +0200 Subject: [PATCH 22/22] offset ghost first movement time --- src/reserved_space.asm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reserved_space.asm b/src/reserved_space.asm index 2540222..2747dec 100644 --- a/src/reserved_space.asm +++ b/src/reserved_space.asm @@ -27,7 +27,7 @@ pub ptr.timer_robot_next_tick_high: U32 0x0000_0000 pub ptr.timer_robot_next_tick_low: U32 0x1DCD_6500 pub ptr.timer_ghost_next_tick_high: U32 0 -pub ptr.timer_ghost_next_tick_low: U32 0 +pub ptr.timer_ghost_next_tick_low: U32 0x0EE6_B280 pub ptr.state_robot: U8 0 ; x coord