12 Commits
Author SHA1 Message Date
Chappatte c88f67559d add minify flag to build script
(cherry picked from commit f0b43f7b7c)
2026-04-27 12:43:56 +02:00
Chappatte 2344a2e874 update branches description 2026-04-25 09:13:56 +02:00
Chappatte 54ba73c177 use decimal instead of binary representation to save space 2026-04-25 09:13:05 +02:00
Chappatte d5e9dde21d use prerendered screen and remove sprite rotation
(cherry picked from commit 3783a98f95)
2026-04-25 08:59:04 +02:00
Chappatte 02e1e4c6b3 update branches description
(cherry picked from commit 4e03aada20)
2026-04-25 08:37:57 +02:00
Chappatte d413170098 add colors: light gray and brown
(cherry picked from commit 356cf3fb94)
2026-04-25 08:34:26 +02:00
Chappatte 2c37af8dd0 change which 8 bit color is used for dark gray
(cherry picked from commit b7bc9956f9)
2026-04-25 08:34:26 +02:00
Chappatte f96e2420ef add way to reduce build output's size
(cherry picked from commit 5231f54c27)
2026-04-25 08:34:26 +02:00
Chappatte 17c6b07367 add automatic padding in build script
(cherry picked from commit aad1fa45f7)
2026-04-25 08:34:26 +02:00
Chappatte f0656aca40 replace texture jump table with dispatch table
(cherry picked from commit 88f10f9f06)
2026-04-25 08:34:26 +02:00
Chappatte b0758c6976 change the way static RAM variable are declared and used
(cherry picked from commit 6cdb7304c2)
2026-04-25 08:34:26 +02:00
Chappatte 3992863664 remove unused colors.asm const file
(cherry picked from commit e6b2d8b086)
2026-04-25 08:34:26 +02:00
16 changed files with 239 additions and 113 deletions
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+216 -92
View File
@@ -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 = 17;
const rawDataInstructionLengthPrefix = "U32";
const asmFilePaths = [
"src/consts/arch.asm",
@@ -28,86 +24,161 @@ 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: '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',
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<string> = [];
stream;
console.log(`
constructor(writableStream: WritableStream) {
this.stream = new TransformStream<string>({
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<string> = [];
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<string>) {
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
@@ -116,43 +187,48 @@ console.log(await Deno.readTextFile(reservedSpacePath))
*
* @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<number, string>, lineLength = 0): string {
lineLength ??= Infinity;
function bytesToAsmConstU8(
label: string,
data: Uint8Array,
matchingValues: Record<number, 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 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");
}
/**
@@ -192,7 +268,7 @@ async function getDataFromPPM(path: string): Promise<Uint8Array> {
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<Uint8Array> {
// potential comment
if (content[offset] === hashtag) {
while(!endOfComment.includes(content[offset])) {
while (!endOfComment.includes(content[offset])) {
offset++;
}
@@ -208,21 +284,21 @@ async function getDataFromPPM(path: string): Promise<Uint8Array> {
}
// 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++;
}
@@ -230,3 +306,51 @@ async function getDataFromPPM(path: string): Promise<Uint8Array> {
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<string>({
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());
}
+6 -4
View File
@@ -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
@@ -26,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 and empty lines:
./build.ts | sed 's/\s*;.*//' | sed 's/^\s*//' | sed '/^\d*$/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.
+1
View File
@@ -1,3 +1,4 @@
; map size 16x12 (or 15x11 with margins)
pub const map.width = 15
pub const map.height = 11
pub const map.size = 165 ; 11*15
+4 -5
View File
@@ -1,9 +1,8 @@
; given:
; -> 256x192 pixels screen
; -> 15x11 tiles map
; => 17x17 pixels tile
; - 16x12 tiles map
; - 5x5 pixels tile
pub const scene.margin_top = 2
pub const scene.margin_bottom = 3
pub const scene.margin_left = 0
pub const scene.margin_right = 1
pub const scene.margin_left = 2
pub const scene.margin_right = 3
+3 -3
View File
@@ -20,7 +20,7 @@ pub const screen.mode_index = 0
pub const screen.mode_value = 2
pub const screen.offset_index = 1
pub const screen.resolution_index = 2
pub const screen.resolution_value = 2
pub const screen.resolution_value = 0
pub const screen.width = 256
pub const screen.height = 192
pub const screen.width = 80
pub const screen.height = 60
+3 -3
View File
@@ -1,6 +1,6 @@
pub const sprites.width = 17
pub const sprites.height = 17 ; unused as sprite are all square
pub const sprites.size = 289 ; height x width
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.empty = 0
pub const sprites.unimplemented = 1
+1 -1
View File
@@ -23,4 +23,4 @@ Resolution ID | Tile size (in pixels) | Comment
-- | -- | --
`0` | `5x5` | with a reminder of 5 pixels en width and 5 pixels in height.
`1` | `10x10` | with a reminder of 10w and 10h
`2` | `17x17` | with a reminder of 1w and 5h
`2` | `17x17` | no reminder