Files
Turing-Complete-PacMan/build.ts

225 lines
5.9 KiB
TypeScript
Executable File

#!/usr/bin/env -S deno run --allow-read --allow-run
import { TextLineStream } from "jsr:@std/streams@1.1.0";
/**
* CONFIG
*/
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 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",
};
/**
* END OF CONFIG
*/
class Minifier {
private buffer = "";
private lines: Array<string> = [];
stream;
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());
controller.enqueue(this.lines.join("\n") + "\n");
this.lines = [];
},
});
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 ppmToAsm(mode: "map" | "sprite", label: string, path: string): Promise<string> {
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<string>) {
for (const path of asmFilePaths) {
const file = await Deno.open(path);
const readable = file.readable.pipeThrough(new TextDecoderStream())
.pipeThrough(new TextLineStream());
await output.write(`
;/*******************************
;* ${path}
;*******************************/
`);
for await (const line of readable) {
if (!line.startsWith("include ")) {
await output.write(`${line}\n`);
}
}
}
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 --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());
}