10 Commits

Author SHA1 Message Date
2b2e2a6d02 use decimal instead of binary representation to save space
(cherry picked from commit 54ba73c177)
2026-04-25 09:22:21 +02:00
c378dc95fa update branches description
(cherry picked from commit 2344a2e874)
2026-04-25 09:21:00 +02:00
02e1e4c6b3 update branches description
(cherry picked from commit 4e03aada20)
2026-04-25 08:37:57 +02:00
d413170098 add colors: light gray and brown
(cherry picked from commit 356cf3fb94)
2026-04-25 08:34:26 +02:00
2c37af8dd0 change which 8 bit color is used for dark gray
(cherry picked from commit b7bc9956f9)
2026-04-25 08:34:26 +02:00
f96e2420ef add way to reduce build output's size
(cherry picked from commit 5231f54c27)
2026-04-25 08:34:26 +02:00
17c6b07367 add automatic padding in build script
(cherry picked from commit aad1fa45f7)
2026-04-25 08:34:26 +02:00
f0656aca40 replace texture jump table with dispatch table
(cherry picked from commit 88f10f9f06)
2026-04-25 08:34:26 +02:00
b0758c6976 change the way static RAM variable are declared and used
(cherry picked from commit 6cdb7304c2)
2026-04-25 08:34:26 +02:00
3992863664 remove unused colors.asm const file
(cherry picked from commit e6b2d8b086)
2026-04-25 08:34:26 +02:00
10 changed files with 199 additions and 122 deletions

View File

@@ -22,7 +22,9 @@ PPM 8bits channels | In-game `RRRGGGBB` value | Name
`0x00_00_00` | `000_000_00` | Black `0x00_00_00` | `000_000_00` | Black
`0x00_00_FF` | `000_000_11` | Blue `0x00_00_FF` | `000_000_11` | Blue
`0x00_FF_00` | `000_111_00` | Green `0x00_FF_00` | `000_111_00` | Green
`0x77_77_77` | `010_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_00` | `111_000_00` | Red
`0xFF_00_FF` | `111_000_11` | Pink `0xFF_00_FF` | `111_000_11` | Pink
`0xFF_80_00` | `111_100_00` | Orange `0xFF_80_00` | `111_100_00` | Orange

View File

@@ -10,41 +10,45 @@ if (Deno.args.length !== 0) {
import { TextLineStream } from "jsr:@std/streams@1.1.0"; import { TextLineStream } from "jsr:@std/streams@1.1.0";
const asmFilePaths = [ const asmFilePaths = [
"src/consts/colors.asm", "src/consts/arch.asm",
"src/consts/game.asm", "src/consts/game.asm",
"src/consts/keyboard.asm", "src/consts/keyboard.asm",
"src/consts/map.asm", "src/consts/map.asm",
"src/consts/offsets.asm",
"src/consts/scene.asm", "src/consts/scene.asm",
"src/consts/screen.asm", "src/consts/screen.asm",
"src/consts/sprites.asm", "src/consts/sprites.asm",
"src/init.asm",
"src/main.asm", "src/main.asm",
"src/lib/game.asm", "src/lib/game.asm",
"src/lib/drawing.asm", "src/lib/drawing.asm",
"src/lib/sprite_rotation.asm", "src/lib/sprite_rotation.asm",
]; ];
const reservedSpacePath = 'src/reserved_space.asm';
const instructionLength = 4; // in bytes
const mapWidth = 15; const mapWidth = 15;
const spriteWidth = 5; const spriteWidth = 5;
const mapMatchingStrings = { const mapMatchingStrings = {
0x00_00_00: 'U8 0', // Empty 0x00_00_00: '0', // Empty
0xFF_00_00: 'U8 1', // Wall 0xFF_00_00: '1', // Wall
0xFF_FF_00: 'U8 2', // Coin 0xFF_FF_00: '2', // Coin
}; };
const spritesMatchingStrings = { const spritesMatchingStrings = {
0x00_00_00: 'U8 0b000_000_00', 0x00_00_00: ' 0', // 0b000_000_00
0x00_00_FF: 'U8 0b000_000_11', 0x00_00_FF: ' 3', // 0b000_000_11
0x00_FF_00: 'U8 0b000_111_00', 0x00_FF_00: ' 28', // 0b000_111_00
0x77_77_77: 'U8 0b010_011_10', 0x99_50_00: ' 68', // 0b010_001_00
0xFF_00_00: 'U8 0b111_000_00', 0x77_77_77: '110', // 0b011_011_10
0xFF_00_FF: 'U8 0b111_000_11', 0xB3_B3_B3: '146', // 0b100_100_10
0xFF_80_00: 'U8 0b111_100_00', 0xFF_00_00: '224', // 0b111_000_00
0xFF_FF_00: 'U8 0b111_110_00', 0xFF_00_FF: '227', // 0b111_000_11
0xFF_FF_FF: 'U8 0b111_111_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';
@@ -85,17 +89,27 @@ for (const path of asmFilePaths) {
const asmRawRepresentations = []; const asmRawRepresentations = [];
const mapBytes = await getDataFromPPM(mapPath); 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)) { for (const [label, path] of Object.entries(spritesPath)) {
const bytes = await getDataFromPPM(path); 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")); 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 * 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 label The label to write before the first raw data
* @param data An array of bytes to interprete as 24 bits values * @param data An array of bytes to interprete as 24 bits values
@@ -104,14 +118,18 @@ console.log(asmRawRepresentations.join("\n\n"));
* *
* @returns The ASM representation of that raw data, pre- and post-fixed by the given label * @returns The ASM representation of that raw data, pre- and post-fixed by the given label
*/ */
function byteToAsmRawData(label: string, data: Uint8Array, matchingStrings: Record<number, string>, lineLength = 0): string { function bytesToAsmConstU8(label: string, data: Uint8Array, matchingStrings: Record<number, string>, lineLength = 0): string {
lineLength ??= Infinity; lineLength ??= Infinity;
const stringValues = []; const stringValues = [];
for (let i = 0; i < data.length - 2; i += 3) { for (let i = 0; i < data.length - 2; i += 3) {
const pixelValue = (data[i] << 16) + (data[i+1] << 8) + (data[i+2]) 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 = []; const lines = [];
@@ -122,10 +140,16 @@ function byteToAsmRawData(label: string, data: Uint8Array, matchingStrings: Reco
lines.push(stringValues.slice(i, i+lineLength).join("\t")); 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}: return `${label}:
${lines.join("\n")} ${lines.join("\n")}
${label}_end:` ${label}_end:${paddingString}`
} }
/** /**

View File

@@ -9,7 +9,19 @@ 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 `main` branch contains a minimal implementation of the game.
The following branches contains variations, allowing comparisons of performances, visuals, gameplay, ... The following branches contains variations, allowing comparisons of performances, visuals, gameplay, ...
- `res-2`: 256x192 screen resolution, 17x17 tile - `res-0-prerendered`:
- `80x60` screen resolution
- `5x5` tile resolution
- initial screen prerendered
- no sprite rotation
- `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 ## Build script
@@ -20,6 +32,9 @@ It also convert the PPM assets (map and sprites) into ASM raw values representat
Usage example: Usage example:
```sh ```sh
./build.ts > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm ./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
``` ```
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. 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.

2
src/consts/arch.asm Normal file
View File

@@ -0,0 +1,2 @@
pub const arch.instruction_size = 4
pub const arch.instruction_increment_shift = 2

View File

@@ -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

View File

@@ -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

70
src/init.asm Normal file
View File

@@ -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

View File

@@ -77,7 +77,7 @@ get_tile_address:
cmp r3, sprites.height cmp r3, sprites.height
jne get_tile_address_offset_per_row_loop jne get_tile_address_offset_per_row_loop
mov r13, offsets.screen add r13, zr, ptr.screen
mov r3, 0 mov r3, 0
get_tile_address_margin_top_loop: get_tile_address_margin_top_loop:
@@ -120,48 +120,21 @@ get_tile_address:
; r13: address ; r13: address
; Locals: ; Locals:
; r2: jump address ; r2: pointer address
; */ ; */
get_sprite_address: get_sprite_address:
push r1 push r1
push r2 push r2
cmp r1, sprites.max cmp r1, sprites.max
jbe get_sprite_address_jmp_table_init jbe get_sprite_address_ptr
mov r1, sprites.unimplemented mov r1, sprites.unimplemented
get_sprite_address_jmp_table_init: get_sprite_address_ptr:
lsl r2, r1, 3 ; 2*instruction_size = *8 = <<3 lsl r2, r1, arch.instruction_increment_shift
add r2, r2, get_sprite_address_jmp_table_start add r2, r2, ptr.textures_addresses
jmp r2 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 r2
pop r1 pop r1
ret ret

View File

@@ -1,18 +1,6 @@
include consts/screen include consts/screen
; include init
; Screen initialization
;
; r1 = screen parameter value
mov r1, screen.mode_index
screen r1, screen.mode_value
mov r1, screen.offset_index
screen r1, offsets.screen
mov r1, screen.resolution_index
screen r1, screen.resolution_value
call drawing.draw_whole_map call drawing.draw_whole_map
@@ -27,7 +15,7 @@ const robot_initial_direction = game.direction_down
mov r1, robot_initial_x mov r1, robot_initial_x
mov r2, robot_initial_y mov r2, robot_initial_y
mov r5, robot_initial_direction mov r5, robot_initial_direction
mov r6, offsets.state_robot add r6, zr, ptr.state_robot
store_8 [r6], r1 store_8 [r6], r1
add r6, r6, 1 add r6, r6, 1
store_8 [r6], r2 store_8 [r6], r2
@@ -46,7 +34,7 @@ const ghost_initial_direction = game.direction_down
mov r1, ghost_initial_x mov r1, ghost_initial_x
mov r2, ghost_initial_y mov r2, ghost_initial_y
mov r5, ghost_initial_direction mov r5, ghost_initial_direction
mov r6, offsets.state_ghost add r6, zr, ptr.state_ghost
store_8 [r6], r1 store_8 [r6], r1
add r6, r6, 1 add r6, r6, 1
store_8 [r6], r2 store_8 [r6], r2
@@ -60,17 +48,10 @@ call drawing.draw_sprite_at_map_coord
; ;
; Time initialization ; Time initialization
; ;
mov r2, game.tick_duration_1 add r1, zr, ptr.tick_duration_low
movt r2, r2 load_32 r2, [r1]
movb r2, game.tick_duration_0 add r1, zr, ptr.tick_duration_high
mov r1, offsets.tick_duration_low load_32 r3, [r1]
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
time_0 r4 time_0 r4
time_1 r5 time_1 r5
@@ -81,9 +62,9 @@ jae init_store_next_tick
add r5, r5, 1 ; overflow add r5, r5, 1 ; overflow
init_store_next_tick: init_store_next_tick:
mov r1, offsets.next_tick_low add r1, zr, ptr.next_tick_low
store_32 [r1], r4 store_32 [r1], r4
mov r1, offsets.next_tick_high add r1, zr, ptr.next_tick_high
store_32 [r1], r5 store_32 [r1], r5
game_loop: game_loop:
@@ -125,7 +106,7 @@ game_loop:
; jmp game_loop_event_handling_end ; jmp game_loop_event_handling_end
game_loop_event_handling_store_direction: game_loop_event_handling_store_direction:
add r2, zr, offsets.state_robot add r2, zr, ptr.state_robot
add r2, r2, 2 add r2, r2, 2
store_8 [r2], r1 store_8 [r2], r1
@@ -143,9 +124,9 @@ game_loop:
time_0 r2 time_0 r2
time_1 r3 time_1 r3
mov r1, offsets.next_tick_low add r1, zr, ptr.next_tick_low
load_32 r4, [r1] load_32 r4, [r1]
mov r1, offsets.next_tick_high add r1, zr, ptr.next_tick_high
load_32 r5, [r1] load_32 r5, [r1]
cmp r3, r5 cmp r3, r5
@@ -155,9 +136,9 @@ game_loop:
jb game_loop jb game_loop
update_next_tick: update_next_tick:
mov r1, offsets.tick_duration_low add r1, zr, ptr.tick_duration_low
load_32 r2, [r1] load_32 r2, [r1]
mov r1, offsets.tick_duration_high add r1, zr, ptr.tick_duration_high
load_32 r3, [r1] load_32 r3, [r1]
add r4, r4, r2 add r4, r4, r2
add r5, r5, r3 add r5, r5, r3
@@ -166,9 +147,9 @@ game_loop:
add r5, r5, 1 add r5, r5, 1
store_next_tick: store_next_tick:
mov r1, offsets.next_tick_low add r1, zr, ptr.next_tick_low
store_32 [r1], r4 store_32 [r1], r4
mov r1, offsets.next_tick_high add r1, zr, ptr.next_tick_high
store_32 [r1], r5 store_32 [r1], r5
@@ -184,7 +165,7 @@ game_loop:
; r7 = new position address ; r7 = new position address
; r8 = new position content ; r8 = new position content
; r9 = jump address ; r9 = jump address
mov r1, offsets.state_robot add r1, zr, ptr.state_robot
load_8 r2, [r1] load_8 r2, [r1]
add r1, r1, 1 add r1, r1, 1
load_8 r3, [r1] load_8 r3, [r1]
@@ -229,7 +210,7 @@ game_loop:
cmp r8, map.tiles.wall cmp r8, map.tiles.wall
jne game_loop_robot_movement_some jne game_loop_robot_movement_some
; hit wall ; hit wall
mov r1, offsets.state_robot add r1, zr, ptr.state_robot
add r1, r1, 2 add r1, r1, 2
mov r4, game.direction_still mov r4, game.direction_still
store_8 [r1], r4 store_8 [r1], r4
@@ -240,7 +221,7 @@ game_loop:
mov r8, map.tiles.empty mov r8, map.tiles.empty
store_8 [r7], r8 store_8 [r7], r8
mov r1, offsets.state_robot add r1, zr, ptr.state_robot
store_8 [r1], r5 store_8 [r1], r5
add r1, r1, 1 add r1, r1, 1
store_8 [r1], r6 store_8 [r1], r6
@@ -276,7 +257,7 @@ game_loop:
; r9: new position address ; r9: new position address
; r10: new position content ; r10: new position content
; r11: jump address ; r11: jump address
mov r1, offsets.state_ghost add r1, zr, ptr.state_ghost
load_8 r2, [r1] load_8 r2, [r1]
add r1, r1, 1 add r1, r1, 1
load_8 r3, [r1] load_8 r3, [r1]
@@ -308,7 +289,7 @@ game_loop:
game_loop_ghost_movement_make_move: game_loop_ghost_movement_make_move:
add r7, r2, r5 add r7, r2, r5
add r8, r3, r6 add r8, r3, r6
mov r1, offsets.state_ghost add r1, zr, ptr.state_ghost
store_8 [r1], r7 store_8 [r1], r7
add r1, r1, 1 add r1, r1, 1
store_8 [r1], r8 store_8 [r1], r8
@@ -355,7 +336,7 @@ game_loop:
cmp r10, map.tiles.wall cmp r10, map.tiles.wall
jne game_loop_ghost_movement_end jne game_loop_ghost_movement_end
; increment direction ; increment direction
mov r1, offsets.state_ghost add r1, zr, ptr.state_ghost
add r1, r1, 2 add r1, r1, 2
load_8 r4, [r1] load_8 r4, [r1]
add r4, r4, 1 add r4, r4, 1

35
src/reserved_space.asm Normal file
View File

@@ -0,0 +1,35 @@
; 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
; 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
U8 0 ; [padding]
pub ptr.state_ghost:
U8 0 ; x coord
U8 0 ; y coord
U8 0 ; direction
U8 0 ; [padding]
pub ptr.screen: