Files
Turing-Complete-PacMan/build.ts

267 lines
7.2 KiB
TypeScript
Executable File

#!/usr/bin/env -S deno run --allow-read --allow-write --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 mapPath = "assets/map.ppm";
const spritesPath = {
"sprite_empty": "assets/sprites/empty.ppm",
"sprite_unimplemented": "assets/sprites/placeholder.ppm",
"sprite_robot_up": "assets/sprites/robot_up.ppm",
"sprite_robot_right": "assets/sprites/robot_right.ppm",
"sprite_robot_down": "assets/sprites/robot_down.ppm",
"sprite_robot_left": "assets/sprites/robot_left.ppm",
"sprite_ghost_up": "assets/sprites/ghost_up.ppm",
"sprite_ghost_right": "assets/sprites/ghost_right.ppm",
"sprite_ghost_down": "assets/sprites/ghost_down.ppm",
"sprite_ghost_left": "assets/sprites/ghost_left.ppm",
"sprite_coin": "assets/sprites/coin.ppm",
"screen": "assets/screen.ppm",
};
/**
* END OF CONFIG
*/
class Minifier {
private buffer = "";
private lines: Array<string> = [];
private insideComment = false;
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 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
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(
pwd: string,
path: string,
output: WritableStreamDefaultWriter<string>,
) {
const includeExpressionRegex = /^\s*include\s+([^\s;]+)/;
const file = await Deno.open(`${pwd}/${path}`);
const readable = file.readable.pipeThrough(new TextDecoderStream())
.pipeThrough(new TextLineStream());
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);
}
}
function printUsage(into: (message: string) => void) {
into(`Usage:
\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
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) {
printUsage(console.error);
Deno.exit(1);
}
let minify = false;
let convertPPM = false;
let path: string | undefined = undefined;
Deno.args.forEach((arg, index) => {
switch (arg) {
case "--help":
printUsage(console.log);
Deno.exit(0);
break;
case "--minify":
minify = true;
break;
case "--ppm":
convertPPM = true;
break;
default:
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();
const stdoutWritable = new WritableStream<string>({
async write(chunk) {
await Deno.stdout.write(encoder.encode(chunk));
},
});
const writer = minify
? (new Minifier(stdoutWritable)).stream.writable.getWriter()
: stdoutWritable.getWriter();
await build(`${Deno.cwd()}/src`, path, writer);
await writer.close();