weave-maze-generator
Version:
A generator of weave mazes.
2,510 lines • 83.3 kB
JavaScript
#!/usr/bin/env node
import * as path from 'path';
import { dirname } from 'path';
import { promises, constants } from 'fs';
import { loadImage, createCanvas } from 'canvas';
import * as console from 'console';
const MIN_MAZE_SIZE = 1;
const MAX_MAZE_SIZE = 200;
const DEFAULT_MAZE_SIZE = 30;
const MIN_LOOP_FRAC = 0;
const MAX_LOOP_FRAC = 1;
const DEFAULT_LOOP_FRAC = .05;
const MIN_CROSS_FRAC = 0;
const MAX_CROSS_FRAC = 1;
const DEFAULT_CROSS_FRAC = .25;
const DEFAULT_LONG_PASSAGES = false;
class MazeOptions {
width;
height;
loopFrac;
crossFrac;
longPassages;
mask;
constructor(width = DEFAULT_MAZE_SIZE, height = DEFAULT_MAZE_SIZE, loopFrac = DEFAULT_LOOP_FRAC, crossFrac = DEFAULT_CROSS_FRAC, longPassages = DEFAULT_LONG_PASSAGES, mask) {
this.width = width;
this.height = height;
this.loopFrac = loopFrac;
this.crossFrac = crossFrac;
this.longPassages = longPassages;
this.mask = mask;
}
}
var ParamType;
(function (ParamType) {
ParamType[ParamType["NONE"] = 0] = "NONE";
ParamType[ParamType["STRING"] = 1] = "STRING";
ParamType[ParamType["BOOLEAN"] = 2] = "BOOLEAN";
ParamType[ParamType["INTEGER"] = 3] = "INTEGER";
ParamType[ParamType["FLOAT"] = 4] = "FLOAT";
})(ParamType || (ParamType = {}));
function extractArgs(params) {
const result = new Map();
const flagMap = new Map();
params.forEach(param => {
if (param.type === ParamType.NONE) {
result.set(param.key, false);
}
param.flags.forEach(flag => flagMap.set(flag, param));
});
const args = process.argv;
for (let i = 2; i < args.length;) {
const flag = args[i++];
const param = flagMap.get(flag);
if (!param) {
throw new Error(`Invalid flag: ${flag}`);
}
if (param.type === ParamType.NONE) {
result.set(param.key, true);
continue;
}
else if (i >= args.length) {
throw new Error(`Flag ${flag} missing value.`);
}
const values = [];
do {
const v = args[i++];
if (flagMap.has(v)) {
--i;
break;
}
values.push(v);
} while (i < args.length);
switch (param.type) {
case ParamType.STRING:
result.set(param.key, values[0]);
break;
case ParamType.BOOLEAN:
result.set(param.key, values[0].toLowerCase().charAt(0) === 't');
break;
case ParamType.INTEGER: {
const v = parseInt(values[0]);
if (isNaN(v)) {
throw new Error(`Value for flag ${flag} is not a number.`);
}
result.set(param.key, v);
break;
}
case ParamType.FLOAT: {
const v = parseFloat(values[0]);
if (isNaN(v)) {
throw new Error(`Value for flag ${flag} is not a number.`);
}
result.set(param.key, v);
break;
}
}
}
return result;
}
const MARGIN_INCHES = .25;
const DPI = 72;
const MILLIMETERS_PER_INCH = 25.4;
const MARGIN_DOTS = DPI * MARGIN_INCHES;
var DimensionUnits;
(function (DimensionUnits) {
DimensionUnits[DimensionUnits["INCHES"] = 0] = "INCHES";
DimensionUnits[DimensionUnits["MILLIMETERS"] = 1] = "MILLIMETERS";
})(DimensionUnits || (DimensionUnits = {}));
function toPaperSize(paperSize) {
if (!paperSize) {
return DEFAULT_PAPER_SIZE;
}
switch (paperSize.trim().toLowerCase()) {
case 'letter':
return PaperSize.LETTER;
case 'tabloid':
return PaperSize.TABLOID;
case 'legal':
return PaperSize.LEGAL;
case 'statement':
return PaperSize.STATEMENT;
case 'executive':
return PaperSize.EXECUTIVE;
case 'folio':
return PaperSize.FOLIO;
case 'quarto':
return PaperSize.QUARTO;
case 'a3':
return PaperSize.A3;
case 'a4':
return PaperSize.A4;
case 'a5':
return PaperSize.A5;
case 'b4':
return PaperSize.B4_JIS;
case 'b5':
return PaperSize.B5_JIS;
case 'fit':
return PaperSize.FIT;
}
throw new Error('\nUnknown paper size.\n');
}
class PaperSize {
name;
width;
height;
units;
widthDots;
heightDots;
printableWidthDots;
printableHeightDots;
constructor(name, width, height, units = DimensionUnits.INCHES) {
this.name = name;
this.width = width;
this.height = height;
this.units = units;
let widthInches = width;
let heightInches = height;
if (units === DimensionUnits.MILLIMETERS) {
widthInches /= MILLIMETERS_PER_INCH;
heightInches /= MILLIMETERS_PER_INCH;
}
this.widthDots = DPI * widthInches;
this.heightDots = DPI * heightInches;
this.printableWidthDots = this.widthDots - 2 * MARGIN_DOTS;
this.printableHeightDots = this.heightDots - 2 * MARGIN_DOTS;
}
static LETTER = new PaperSize('Letter', 8.5, 11);
static TABLOID = new PaperSize('Tabloid', 11, 17);
static LEGAL = new PaperSize('Legal', 8.5, 14);
static STATEMENT = new PaperSize('Statement', 5.5, 8.5);
static EXECUTIVE = new PaperSize('Executive', 7.25, 10.5);
static FOLIO = new PaperSize('Folio', 8.5, 13.5);
static QUARTO = new PaperSize('Quarto', 8.5, 10 + 5 / 6);
static A3 = new PaperSize('A3', 297, 420, DimensionUnits.MILLIMETERS);
static A4 = new PaperSize('A4', 210, 297, DimensionUnits.MILLIMETERS);
static A5 = new PaperSize('A5', 148, 210, DimensionUnits.MILLIMETERS);
static B4_JIS = new PaperSize('B4 (JIS)', 257, 364, DimensionUnits.MILLIMETERS);
static B5_JIS = new PaperSize('B5 (JIS)', 182, 257, DimensionUnits.MILLIMETERS);
static FIT = new PaperSize('Fit', 0, 0);
}
const DEFAULT_PAPER_SIZE = PaperSize.LETTER;
class Color {
red;
green;
blue;
alpha;
constructor(red, // 0--255
green, // 0--255
blue, // 0--255
alpha) {
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
}
toStyle() {
return `rgba(${this.red}, ${this.green}, ${this.blue}, ${this.alpha})`;
}
}
const hexPattern = /^[0-9a-fA-F]+$/;
function toColor(hexCode) {
hexCode = hexCode.trim();
if (hexCode.length !== 6 && hexCode.length !== 8) {
throw new Error('Bad length.');
}
if (!hexPattern.test(hexCode)) {
throw new Error('Not hex.');
}
const red = Number.parseInt(hexCode.substring(0, 2), 16);
const green = Number.parseInt(hexCode.substring(2, 4), 16);
const blue = Number.parseInt(hexCode.substring(4, 6), 16);
const alpha = ((hexCode.length === 8) ? Number.parseInt(hexCode.substring(6, 8), 16) : 255) / 255;
return new Color(red, green, blue, alpha);
}
var FileFormat;
(function (FileFormat) {
FileFormat[FileFormat["PNG"] = 0] = "PNG";
FileFormat[FileFormat["SVG"] = 1] = "SVG";
FileFormat[FileFormat["PDF"] = 2] = "PDF";
FileFormat[FileFormat["ALL_FORMATS"] = 3] = "ALL_FORMATS";
})(FileFormat || (FileFormat = {}));
function toFileFormat(format) {
if (!format) {
return DEFAULT_FILE_FORMAT;
}
switch (format.trim().toLowerCase()) {
case 'png':
return FileFormat.PNG;
case 'svg':
return FileFormat.SVG;
case 'pdf':
return FileFormat.PDF;
default:
throw new Error('\nFile format must be either png, svg, or pdf.\n');
}
}
function toFileExtensions(format) {
switch (format) {
case FileFormat.PNG:
return ['png'];
case FileFormat.SVG:
return ['svg'];
case FileFormat.PDF:
return ['pdf'];
default:
return ['png', 'svg', 'pdf'];
}
}
const DEFAULT_FILE_FORMAT = FileFormat.ALL_FORMATS;
const DEFAULT_FILENAME_PREFIX = 'maze';
const DEFAULT_FILENAME_SOLUTION_SUFFIX = 'solution';
const DEFAULT_TIMESTAMP = true;
const DEFAULT_SOLUTION = true;
const DEFAULT_ROUNDED_CORNERS = true;
const MIN_CELL_SIZE = 1;
const DEFAULT_CELL_SIZE = 25;
const MIN_IMAGE_SIZE = 1;
const MAX_IMAGE_SIZE = 10_000;
const MIN_LINE_WIDTH_FRAC = 0;
const MAX_LINE_WIDTH_FRAC = 1;
const DEFAULT_LINE_WIDTH_FRAC = 0.15;
const MIN_PASSAGE_WIDTH_FRAC = 0;
const MAX_PASSAGE_WIDTH_FRAC = 1;
const DEFAULT_PASSAGE_WIDTH_FRAC = 0.7;
const DEFAULT_PNG_BACKGROUND_COLOR = new Color(255, 255, 255, 1);
const DEFAULT_SVG_AND_PDF_BACKGROUND_COLOR = new Color(0, 0, 0, 0);
const DEFAULT_WALL_COLOR = new Color(0, 0, 0, 1);
const DEFAULT_SOLUTION_COLOR = new Color(255, 0, 0, 1);
class RenderOptions {
outputDirectory;
fileFormat;
filenamePrefix;
filenameSuffix;
timestamp;
solution;
paperSize;
cellSize;
imageWidth;
imageHeight;
roundedCorners;
lineWidthFrac;
passageWidthFrac;
wallColor;
solutionColor;
backgroundColor;
constructor(outputDirectory, fileFormat = DEFAULT_FILE_FORMAT, filenamePrefix = DEFAULT_FILENAME_PREFIX, filenameSuffix = DEFAULT_FILENAME_SOLUTION_SUFFIX, timestamp = DEFAULT_TIMESTAMP, solution = DEFAULT_SOLUTION, paperSize = DEFAULT_PAPER_SIZE, // only applicable to PDF
cellSize = DEFAULT_CELL_SIZE, imageWidth = MIN_IMAGE_SIZE, imageHeight = MIN_IMAGE_SIZE, roundedCorners = DEFAULT_ROUNDED_CORNERS, lineWidthFrac = DEFAULT_LINE_WIDTH_FRAC, passageWidthFrac = DEFAULT_PASSAGE_WIDTH_FRAC, wallColor = DEFAULT_WALL_COLOR, solutionColor = DEFAULT_SOLUTION_COLOR, backgroundColor) {
this.outputDirectory = outputDirectory;
this.fileFormat = fileFormat;
this.filenamePrefix = filenamePrefix;
this.filenameSuffix = filenameSuffix;
this.timestamp = timestamp;
this.solution = solution;
this.paperSize = paperSize;
this.cellSize = cellSize;
this.imageWidth = imageWidth;
this.imageHeight = imageHeight;
this.roundedCorners = roundedCorners;
this.lineWidthFrac = lineWidthFrac;
this.passageWidthFrac = passageWidthFrac;
this.wallColor = wallColor;
this.solutionColor = solutionColor;
this.backgroundColor = backgroundColor;
}
}
async function checkFileExists(filePath) {
try {
await promises.access(filePath, constants.F_OK);
return true;
}
catch {
return false;
}
}
async function ensureDirectoryExists(filePath) {
try {
const directoryPath = dirname(filePath);
await promises.mkdir(directoryPath, { recursive: true });
return true;
}
catch {
}
return false;
}
const INVALID_FILENAME_CHARS = /[\\/:*?"<>|]/;
const MAX_FILENAME_LENGTH = 128;
function validateFilename(filename) {
filename = filename.trim();
return filename.length > 0 && filename.length <= MAX_FILENAME_LENGTH && !INVALID_FILENAME_CHARS.test(filename);
}
let Cell$1 = class Cell {
x;
y;
white = false;
region = -1;
visitedBy = null;
constructor(x, y) {
this.x = x;
this.y = y;
}
};
function mergeRegions$1(cells, width, height, cell, c) {
let sourceRegion;
let targetRegion;
if (cell.region > c.region) {
sourceRegion = cell.region;
targetRegion = c.region;
}
else {
sourceRegion = c.region;
targetRegion = cell.region;
}
for (let y = height - 1; y >= 0; --y) {
for (let x = width - 1; x >= 0; --x) {
const cell = cells[y][x];
if (cell.region === sourceRegion) {
cell.region = targetRegion;
}
}
}
while (cell.visitedBy && !cell.white) {
cell.white = true;
cell = cell.visitedBy;
}
while (c.visitedBy && !c.white) {
c.white = true;
c = c.visitedBy;
}
}
function enqueue(cells, queue, cell, x, y) {
const c = cells[y][x];
if (!(c.white || c.visitedBy)) {
c.visitedBy = cell;
c.region = cell.region;
queue.push(c);
}
}
function merge(cells, width, height, queue, cell, x, y) {
const c = cells[y][x];
if (c.visitedBy || c.white) {
if (c.region !== cell.region) {
mergeRegions$1(cells, width, height, cell, c);
}
}
else {
c.visitedBy = cell;
c.region = cell.region;
queue.push(c);
}
}
function joinRegions(cells, width, height) {
const queue = [];
for (let y = height - 1; y >= 0; --y) {
for (let x = width - 1; x >= 0; --x) {
const cell = cells[y][x];
if (cell.white) {
if (cell.y > 0) {
enqueue(cells, queue, cell, cell.x, cell.y - 1);
}
if (cell.x < width - 1) {
enqueue(cells, queue, cell, cell.x + 1, cell.y);
}
if (cell.y < height - 1) {
enqueue(cells, queue, cell, cell.x, cell.y + 1);
}
if (cell.x > 0) {
enqueue(cells, queue, cell, cell.x - 1, cell.y);
}
}
}
}
while (true) {
const cell = queue.shift();
if (!cell) {
break;
}
if (cell.y > 0) {
merge(cells, width, height, queue, cell, cell.x, cell.y - 1);
}
if (cell.x < width - 1) {
merge(cells, width, height, queue, cell, cell.x + 1, cell.y);
}
if (cell.y < height - 1) {
merge(cells, width, height, queue, cell, cell.x, cell.y + 1);
}
if (cell.x > 0) {
merge(cells, width, height, queue, cell, cell.x - 1, cell.y);
}
}
}
function pushRegion(cells, stack, region, x, y) {
const c = cells[y][x];
if (c.white && c.region < 0) {
c.region = region;
stack.push(c);
}
}
function fillRegion(cells, width, height, seed, region) {
seed.region = region;
const stack = [seed];
while (true) {
const cell = stack.pop();
if (!cell) {
break;
}
if (cell.y > 0) {
pushRegion(cells, stack, region, cell.x, cell.y - 1);
}
if (cell.x < width - 1) {
pushRegion(cells, stack, region, cell.x + 1, cell.y);
}
if (cell.y < height - 1) {
pushRegion(cells, stack, region, cell.x, cell.y + 1);
}
if (cell.x > 0) {
pushRegion(cells, stack, region, cell.x - 1, cell.y);
}
}
}
function findRegions(cells, width, height) {
let region = 0;
for (let y = height - 1; y >= 0; --y) {
for (let x = width - 1; x >= 0; --x) {
const cell = cells[y][x];
if (cell.white && cell.region < 0) {
fillRegion(cells, width, height, cell, region++);
}
}
}
}
function createCells(data, width, height) {
const stride = 4 * width;
const cells = new Array(height);
for (let y = height - 1; y >= 0; --y) {
cells[y] = new Array(width);
const yOffset = stride * y;
for (let x = width - 1; x >= 0; --x) {
cells[y][x] = new Cell$1(x, y);
const i = yOffset + 4 * x;
cells[y][x].white = data[i + 3] >= 128 && .299 * data[i] + .587 * data[i + 1] + .114 * data[i + 2] >= 128;
}
}
return cells;
}
function createMask(cells, width, height) {
let minX = Number.POSITIVE_INFINITY;
let maxX = Number.NEGATIVE_INFINITY;
let minY = Number.POSITIVE_INFINITY;
let maxY = Number.NEGATIVE_INFINITY;
for (let y = height - 1; y >= 0; --y) {
for (let x = width - 1; x >= 0; --x) {
if (cells[y][x].white) {
minX = Math.min(minX, x);
maxX = Math.max(maxX, x);
minY = Math.min(minY, y);
maxY = Math.max(maxY, y);
}
}
}
const w = maxX - minX + 1;
const h = maxY - minY + 1;
const mask = new Array(h);
for (let y = h - 1; y >= 0; --y) {
mask[y] = new Array(w);
for (let x = w - 1; x >= 0; --x) {
mask[y][x] = cells[minY + y][minX + x].white;
}
}
return mask;
}
async function loadMask(filename) {
const image = await loadImage(filename);
const { width, height } = image;
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
const cells = createCells(ctx.getImageData(0, 0, canvas.width, canvas.height).data, width, height);
findRegions(cells, width, height);
joinRegions(cells, width, height);
return createMask(cells, width, height);
}
class Node {
cell;
north = null;
east = null;
south = null;
west = null;
north2 = null;
east2 = null;
south2 = null;
west2 = null;
visitedBy = null;
region = -1;
constructor(cell) {
this.cell = cell;
}
backup() {
this.north2 = this.north;
this.east2 = this.east;
this.south2 = this.south;
this.west2 = this.west;
}
restore() {
this.north = this.north2;
this.east = this.east2;
this.south = this.south2;
this.west = this.west2;
}
}
class Cell {
x;
y;
white;
lower = new Node(this);
upper = new Node(this);
constructor(x, y, white) {
this.x = x;
this.y = y;
this.white = white;
}
backup() {
this.lower.backup();
this.upper.backup();
}
restore() {
this.lower.restore();
this.upper.restore();
}
isFlat() {
return !this.isNotFlat();
}
isNotFlat() {
return this.upper.north || this.upper.east || this.upper.south || this.upper.west;
}
}
class Maze {
width;
height;
cells;
constructor(options) {
if (options.mask) {
this.width = options.mask[0].length;
this.height = options.mask.length;
}
else {
this.width = options.width;
this.height = options.height;
}
this.cells = new Array(this.height);
for (let i = this.height - 1; i >= 0; --i) {
this.cells[i] = new Array(this.width);
for (let j = this.width - 1; j >= 0; --j) {
this.cells[i][j] = new Cell(j, i, options.mask ? options.mask[i][j] : true);
}
}
}
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
function generatePermutations(arr) {
const result = [];
function permute(n) {
if (n === 1) {
result.push(arr.slice());
}
else {
for (let i = 0; i < n; i++) {
permute(n - 1);
if ((n & 1) === 0) {
[arr[i], arr[n - 1]] = [arr[n - 1], arr[i]];
}
else {
[arr[0], arr[n - 1]] = [arr[n - 1], arr[0]];
}
}
}
}
permute(arr.length);
return result;
}
const permutations = generatePermutations([0, 1, 2, 3]);
function findBorderNodes(maze) {
const cells = maze.cells;
const set = new Set();
for (let x = maze.width - 1; x >= 0; --x) {
for (let y = 0; y < maze.height; ++y) {
if (cells[y][x].white) {
set.add(cells[y][x]);
break;
}
}
for (let y = maze.height - 1; y >= 0; --y) {
if (cells[y][x].white) {
set.add(cells[y][x]);
break;
}
}
}
for (let y = maze.height - 1; y >= 0; --y) {
for (let x = 0; x < maze.width; ++x) {
if (cells[y][x].white) {
set.add(cells[y][x]);
break;
}
}
for (let x = maze.width - 1; x >= 0; --x) {
if (cells[y][x].white) {
set.add(cells[y][x]);
break;
}
}
}
const borderCells = Array.from(set);
shuffleArray(borderCells);
const borderNodes = new Set();
borderCells.forEach(cell => borderNodes.add(cell.lower));
return borderNodes;
}
function flood(seed, maze, borderNodes, bestSolution, stack) {
const cells = maze.cells;
for (let y = maze.height - 1; y >= 0; --y) {
for (let x = maze.width - 1; x >= 0; --x) {
const cell = cells[y][x];
const { lower, upper } = cell;
lower.visitedBy = null;
upper.visitedBy = null;
}
}
seed.visitedBy = seed;
seed.region = 0;
stack.push(seed);
while (true) {
const node = stack.pop();
if (!node) {
break;
}
if (borderNodes.has(node) && node.region > bestSolution.length) {
bestSolution.length = 0;
let n = node;
while (true) {
bestSolution.push(n);
if (!n.visitedBy || n.visitedBy === n) {
break;
}
n = n.visitedBy;
}
}
const nextLength = node.region + 1;
if (node.north && !node.north.visitedBy) {
node.north.visitedBy = node;
node.north.region = nextLength;
stack.push(node.north);
}
if (node.east && !node.east.visitedBy) {
node.east.visitedBy = node;
node.east.region = nextLength;
stack.push(node.east);
}
if (node.south && !node.south.visitedBy) {
node.south.visitedBy = node;
node.south.region = nextLength;
stack.push(node.south);
}
if (node.west && !node.west.visitedBy) {
node.west.visitedBy = node;
node.west.region = nextLength;
stack.push(node.west);
}
}
}
function wireTerminal(maze, node) {
const cells = maze.cells;
const cell = node.cell;
const permutation = permutations[Math.floor(permutations.length * Math.random())];
for (let i = permutation.length - 1; i >= 0; --i) {
switch (permutation[i]) {
case 0: {
const y = cell.y - 1;
if (y < 0 || !cells[y][cell.x].white) {
node.north = node.north2 = node;
return;
}
break;
}
case 1: {
const x = cell.x + 1;
if (x >= maze.width || !cells[cell.y][x].white) {
node.east = node.east2 = node;
return;
}
break;
}
case 2: {
const y = cell.y + 1;
if (y >= maze.height || !cells[y][cell.x].white) {
node.south = node.south2 = node;
return;
}
break;
}
default: {
const x = cell.x - 1;
if (x < 0 || !cells[cell.y][x].white) {
node.west = node.west2 = node;
return;
}
break;
}
}
}
}
function wireSolution(solution, maze) {
const cells = maze.cells;
for (let y = maze.height - 1; y >= 0; --y) {
for (let x = maze.width - 1; x >= 0; --x) {
const cell = cells[y][x];
const { lower, upper } = cell;
lower.north2 = lower.east2 = lower.south2 = lower.west2 = null;
upper.north2 = upper.east2 = upper.south2 = upper.west2 = null;
}
}
wireTerminal(maze, solution[0]);
wireTerminal(maze, solution[solution.length - 1]);
for (let i = solution.length - 2; i >= 0; --i) {
const n0 = solution[i];
const n1 = solution[i + 1];
if (n0.north === n1) {
n0.north2 = n1;
n1.south2 = n0;
}
else if (n0.east === n1) {
n0.east2 = n1;
n1.west2 = n0;
}
else if (n0.south === n1) {
n0.south2 = n1;
n1.north2 = n0;
}
else if (n0.west === n1) {
n0.west2 = n1;
n1.east2 = n0;
}
}
}
function solveMaze(maze) {
const borderNodes = findBorderNodes(maze);
const bestSolution = [];
const stack = [];
borderNodes.forEach(node => flood(node, maze, borderNodes, bestSolution, stack));
wireSolution(bestSolution, maze);
}
function assignRegion(region, seed, stack) {
const nodes = [];
seed.region = region;
stack.push(seed);
nodes.push(seed);
try {
while (true) {
const node = stack.pop();
if (!node) {
break;
}
if (node.north && node.north.region < 0) {
node.north.region = region;
stack.push(node.north);
nodes.push(node.north);
}
if (node.east && node.east.region < 0) {
node.east.region = region;
stack.push(node.east);
nodes.push(node.east);
}
if (node.south && node.south.region < 0) {
node.south.region = region;
stack.push(node.south);
nodes.push(node.south);
}
if (node.west && node.west.region < 0) {
node.west.region = region;
stack.push(node.west);
nodes.push(node.west);
}
}
}
finally {
stack.length = 0;
}
return nodes;
}
function assignRegions(maze, stack) {
const nodes = [[]];
let id = 0;
for (let i = maze.height - 1; i >= 0; --i) {
for (let j = maze.width - 1; j >= 0; --j) {
const cell = maze.cells[i][j];
if (cell.lower.region < 0) {
nodes[id] = assignRegion(id, cell.lower, stack);
++id;
}
if (cell.upper.region < 0) {
nodes[id] = assignRegion(id++, cell.upper, stack);
++id;
}
}
}
return nodes;
}
function findLoop(maze, seed, stack) {
for (let i = maze.height - 1; i >= 0; --i) {
for (let j = maze.width - 1; j >= 0; --j) {
const cell = maze.cells[i][j];
cell.lower.visitedBy = cell.upper.visitedBy = null;
}
}
seed.visitedBy = seed;
stack.push(seed);
try {
while (true) {
const node = stack.pop();
if (!node) {
break;
}
if (node.north) {
if (node.north.visitedBy) {
if (node.north !== node.visitedBy) {
return true;
}
}
else {
node.north.visitedBy = node;
stack.push(node.north);
}
}
if (node.east) {
if (node.east.visitedBy) {
if (node.east !== node.visitedBy) {
return true;
}
}
else {
node.east.visitedBy = node;
stack.push(node.east);
}
}
if (node.south) {
if (node.south.visitedBy) {
if (node.south !== node.visitedBy) {
return true;
}
}
else {
node.south.visitedBy = node;
stack.push(node.south);
}
}
if (node.west) {
if (node.west.visitedBy) {
if (node.west !== node.visitedBy) {
return true;
}
}
else {
node.west.visitedBy = node;
stack.push(node.west);
}
}
}
}
finally {
stack.length = 0;
}
return false;
}
function wireCross(cell, northCell, eastCell, southCell, westCell, northSouthHopsEastWest) {
if (northSouthHopsEastWest) {
// north-south hops east-west
if (cell.lower.north) {
cell.lower.north.south = cell.upper;
cell.upper.north = cell.lower.north;
cell.lower.north = null;
}
else {
northCell.lower.south = cell.upper;
cell.upper.north = northCell.lower;
}
if (cell.lower.south) {
cell.lower.south.north = cell.upper;
cell.upper.south = cell.lower.south;
cell.lower.south = null;
}
else {
southCell.lower.north = cell.upper;
cell.upper.south = southCell.lower;
}
if (!cell.lower.east) {
cell.lower.east = eastCell.lower;
eastCell.lower.west = cell.lower;
}
if (!cell.lower.west) {
cell.lower.west = westCell.lower;
westCell.lower.east = cell.lower;
}
}
else {
// east-west hops north-south
if (cell.lower.east) {
cell.lower.east.west = cell.upper;
cell.upper.east = cell.lower.east;
cell.lower.east = null;
}
else {
eastCell.lower.west = cell.upper;
cell.upper.east = eastCell.lower;
}
if (cell.lower.west) {
cell.lower.west.east = cell.upper;
cell.upper.west = cell.lower.west;
cell.lower.west = null;
}
else {
westCell.lower.east = cell.upper;
cell.upper.west = westCell.lower;
}
if (!cell.lower.north) {
cell.lower.north = northCell.lower;
northCell.lower.south = cell.lower;
}
if (!cell.lower.south) {
cell.lower.south = southCell.lower;
southCell.lower.north = cell.lower;
}
}
}
function addNorthEastLoop(maze, cell, stack, northSouthHopsEastWest) {
const northCell = maze.cells[cell.y - 1][cell.x];
if (!northCell.white || northCell.isNotFlat()) {
return false;
}
const northEastCell = maze.cells[cell.y - 1][cell.x + 1];
if (!northEastCell.white || northEastCell.isNotFlat()) {
return false;
}
const eastCell = maze.cells[cell.y][cell.x + 1];
if (!eastCell.white || eastCell.isNotFlat()) {
return false;
}
const southCell = maze.cells[cell.y + 1][cell.x];
if (!southCell.white) {
return false;
}
const westCell = maze.cells[cell.y][cell.x - 1];
if (!westCell.white) {
return false;
}
cell.backup();
northCell.backup();
northEastCell.backup();
eastCell.backup();
southCell.backup();
westCell.backup();
wireCross(cell, northCell, eastCell, southCell, westCell, northSouthHopsEastWest);
if (!northCell.lower.east) {
northCell.lower.east = northEastCell.lower;
northEastCell.lower.west = northCell.lower;
}
if (!eastCell.lower.north) {
eastCell.lower.north = northEastCell.lower;
northEastCell.lower.south = eastCell.lower;
}
if (findLoop(maze, cell.lower, stack) || findLoop(maze, cell.upper, stack)) {
cell.restore();
northCell.restore();
northEastCell.restore();
eastCell.restore();
southCell.restore();
westCell.restore();
return false;
}
return true;
}
function addSouthEastLoop(maze, cell, stack, northSouthHopsEastWest) {
const southCell = maze.cells[cell.y + 1][cell.x];
if (!southCell.white || southCell.isNotFlat()) {
return false;
}
const southEastCell = maze.cells[cell.y + 1][cell.x + 1];
if (!southEastCell.white || southEastCell.isNotFlat()) {
return false;
}
const eastCell = maze.cells[cell.y][cell.x + 1];
if (!eastCell.white || eastCell.isNotFlat()) {
return false;
}
const northCell = maze.cells[cell.y - 1][cell.x];
if (!northCell.white) {
return false;
}
const westCell = maze.cells[cell.y][cell.x - 1];
if (!westCell.white) {
return false;
}
cell.backup();
northCell.backup();
southEastCell.backup();
eastCell.backup();
southCell.backup();
westCell.backup();
wireCross(cell, northCell, eastCell, southCell, westCell, northSouthHopsEastWest);
if (!southCell.lower.east) {
southCell.lower.east = southEastCell.lower;
southEastCell.lower.west = southCell.lower;
}
if (!eastCell.lower.south) {
eastCell.lower.south = southEastCell.lower;
southEastCell.lower.north = eastCell.lower;
}
if (findLoop(maze, cell.lower, stack) || findLoop(maze, cell.upper, stack)) {
cell.restore();
northCell.restore();
southEastCell.restore();
eastCell.restore();
southCell.restore();
westCell.restore();
return false;
}
return true;
}
function addSouthWestLoop(maze, cell, stack, northSouthHopsEastWest) {
const southCell = maze.cells[cell.y + 1][cell.x];
if (!southCell.white || southCell.isNotFlat()) {
return false;
}
const southWestCell = maze.cells[cell.y + 1][cell.x - 1];
if (!southWestCell.white || southWestCell.isNotFlat()) {
return false;
}
const westCell = maze.cells[cell.y][cell.x - 1];
if (!westCell.white || westCell.isNotFlat()) {
return false;
}
const northCell = maze.cells[cell.y - 1][cell.x];
if (!northCell.white) {
return false;
}
const eastCell = maze.cells[cell.y][cell.x + 1];
if (!eastCell.white) {
return false;
}
cell.backup();
northCell.backup();
southWestCell.backup();
eastCell.backup();
southCell.backup();
westCell.backup();
wireCross(cell, northCell, eastCell, southCell, westCell, northSouthHopsEastWest);
if (!southCell.lower.west) {
southCell.lower.west = southWestCell.lower;
southWestCell.lower.east = southCell.lower;
}
if (!westCell.lower.south) {
westCell.lower.south = southWestCell.lower;
southWestCell.lower.north = westCell.lower;
}
if (findLoop(maze, cell.lower, stack) || findLoop(maze, cell.upper, stack)) {
cell.restore();
northCell.restore();
southWestCell.restore();
eastCell.restore();
southCell.restore();
westCell.restore();
return false;
}
return true;
}
function addNorthWestLoop(maze, cell, stack, northSouthHopsEastWest) {
const northCell = maze.cells[cell.y - 1][cell.x];
if (!northCell.white || northCell.isNotFlat()) {
return false;
}
const northWestCell = maze.cells[cell.y - 1][cell.x - 1];
if (!northWestCell.white || northWestCell.isNotFlat()) {
return false;
}
const westCell = maze.cells[cell.y][cell.x - 1];
if (!westCell.white || westCell.isNotFlat()) {
return false;
}
const southCell = maze.cells[cell.y + 1][cell.x];
if (!southCell.white) {
return false;
}
const eastCell = maze.cells[cell.y][cell.x + 1];
if (!eastCell.white) {
return false;
}
cell.backup();
northCell.backup();
northWestCell.backup();
eastCell.backup();
southCell.backup();
westCell.backup();
wireCross(cell, northCell, eastCell, southCell, westCell, northSouthHopsEastWest);
if (!northCell.lower.west) {
northCell.lower.west = northWestCell.lower;
northWestCell.lower.east = northCell.lower;
}
if (!westCell.lower.north) {
westCell.lower.north = northWestCell.lower;
northWestCell.lower.south = westCell.lower;
}
if (findLoop(maze, cell.lower, stack) || findLoop(maze, cell.upper, stack)) {
cell.restore();
northCell.restore();
northWestCell.restore();
eastCell.restore();
southCell.restore();
westCell.restore();
return false;
}
return true;
}
function addCross(maze, cell, stack, northSouthHopsEastWest) {
const northCell = maze.cells[cell.y - 1][cell.x];
if (!northCell.white) {
return false;
}
const eastCell = maze.cells[cell.y][cell.x + 1];
if (!eastCell.white) {
return false;
}
const southCell = maze.cells[cell.y + 1][cell.x];
if (!southCell.white) {
return false;
}
const westCell = maze.cells[cell.y][cell.x - 1];
if (!westCell.white) {
return false;
}
cell.backup();
northCell.backup();
eastCell.backup();
southCell.backup();
westCell.backup();
wireCross(cell, northCell, eastCell, southCell, westCell, northSouthHopsEastWest);
if (findLoop(maze, cell.lower, stack) || findLoop(maze, cell.upper, stack)) {
cell.restore();
northCell.restore();
eastCell.restore();
southCell.restore();
westCell.restore();
return false;
}
return true;
}
function addLoopsAndCrosses(maze, loopFraction, crossFraction, stack) {
const cells = [];
for (let i = maze.height - 2; i >= 1; --i) {
for (let j = maze.width - 2; j >= 1; --j) {
if (maze.cells[i][j].white) {
cells.push(maze.cells[i][j]);
}
}
}
let loops = 0;
const maxLoops = Math.round(cells.length * loopFraction);
while (loops < maxLoops && cells.length > 0) {
const index = Math.floor(cells.length * Math.random());
const cell = cells[index];
cells.splice(index, 1);
const permutation = permutations[Math.floor(permutations.length * Math.random())];
for (let i = permutation.length - 1; i >= 0; --i) {
let addLoop;
switch (permutation[i]) {
case 0:
addLoop = addNorthEastLoop;
break;
case 1:
addLoop = addSouthEastLoop;
break;
case 2:
addLoop = addSouthWestLoop;
break;
default:
addLoop = addNorthWestLoop;
break;
}
const northSouthHopsEastWest = Math.random() < 0.5;
if (addLoop(maze, cell, stack, northSouthHopsEastWest)) {
++loops;
break;
}
else if (addLoop(maze, cell, stack, !northSouthHopsEastWest)) {
++loops;
break;
}
}
}
cells.length = 0;
for (let i = maze.height - 2; i >= 1; --i) {
for (let j = maze.width - 2; j >= 1; --j) {
if (maze.cells[i][j].white && maze.cells[i][j].isFlat()) {
cells.push(maze.cells[i][j]);
}
}
}
let crosses = 0;
const maxCrosses = Math.round(cells.length * crossFraction);
while (crosses < maxCrosses && cells.length > 0) {
const index = Math.floor(cells.length * Math.random());
const cell = cells[index];
cells.splice(index, 1);
const northSouthHopsEastWest = Math.random() < 0.5;
if (addCross(maze, cell, stack, northSouthHopsEastWest)) {
++crosses;
}
else if (addCross(maze, cell, stack, !northSouthHopsEastWest)) {
++crosses;
}
}
}
function mergeRegions(region1, region2, regions) {
const region1Nodes = regions[region1];
const region2Nodes = regions[region2];
for (let i = region1Nodes.length - 1; i >= 0; --i) {
region1Nodes[i].region = region2;
region2Nodes.push(region1Nodes[i]);
}
regions[region1] = [];
}
function moveToEnd(nodes, node) {
const index = nodes.indexOf(node);
if (index < 0 || index === nodes.length - 1) {
return;
}
nodes.splice(index, 1);
nodes.push(node);
}
function createSpanningTree(maze, nodes, regions, longCorridors) {
const maxX = maze.width - 1;
const maxY = maze.height - 1;
for (let i = maxY; i >= 0; --i) {
for (let j = maxX; j >= 0; --j) {
const cell = maze.cells[i][j];
if (cell.white && !(cell.upper.north || cell.upper.east)) {
nodes.push(cell.lower);
}
}
}
if (longCorridors) {
shuffleArray(nodes);
}
outer: while (nodes.length > 0) {
const index = longCorridors ? nodes.length - 1 : Math.floor(nodes.length * Math.random());
const node = nodes[index];
if (longCorridors) {
moveToEnd(nodes, node);
}
const cell = node.cell;
const permutation = permutations[Math.floor(permutations.length * Math.random())];
for (let i = permutation.length - 1; i >= 0; --i) {
switch (permutation[i]) {
case 0: {
// north
if (cell.y === 0 || node.north) {
continue;
}
const northCell = maze.cells[cell.y - 1][cell.x];
if (!northCell.white || northCell.lower.region === node.region) {
continue;
}
northCell.lower.south = node;
node.north = northCell.lower;
if (longCorridors) {
moveToEnd(nodes, node.north);
}
mergeRegions(northCell.lower.region, node.region, regions);
continue outer;
}
case 1: {
// east
if (cell.x === maxX || node.east) {
continue;
}
const eastCell = maze.cells[cell.y][cell.x + 1];
if (!eastCell.white || eastCell.lower.region === node.region) {
continue;
}
eastCell.lower.west = node;
node.east = eastCell.lower;
if (longCorridors) {
moveToEnd(nodes, node.east);
}
mergeRegions(eastCell.lower.region, node.region, regions);
continue outer;
}
case 2: {
// south
if (cell.y === maxY || node.south) {
continue;
}
const southCell = maze.cells[cell.y + 1][cell.x];
if (!southCell.white || southCell.lower.region === node.region) {
continue;
}
southCell.lower.north = node;
node.south = southCell.lower;
if (longCorridors) {
moveToEnd(nodes, node.south);
}
mergeRegions(southCell.lower.region, node.region, regions);
continue outer;
}
default: {
// west
if (cell.x === 0 || node.west) {
continue;
}
const westCell = maze.cells[cell.y][cell.x - 1];
if (!westCell.white || westCell.lower.region === node.region) {
continue;
}
westCell.lower.east = node;
node.west = westCell.lower;
if (longCorridors) {
moveToEnd(nodes, node.west);
}
mergeRegions(westCell.lower.region, node.region, regions);
continue outer;
}
}
}
if (longCorridors) {
nodes.pop();
}
else {
nodes.splice(index, 1);
}
}
}
function generateMaze(options) {
const maze = new Maze(options);
const stack = [];
addLoopsAndCrosses(maze, options.loopFrac, options.crossFrac, stack);
const regions = assignRegions(maze, stack);
createSpanningTree(maze, stack, regions, options.longPassages);
solveMaze(maze);
return maze;
}
const TOLERANCE = 1 / 256;
class Point {
x;
y;
hash;
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
this.hash = Math.round(65537 * this.x) - Math.round(257 * this.y);
}
compare(a, b) {
return Math.abs(a - b) <= TOLERANCE;
}
compareX(other) {
return this.compare(this.x, other.x);
}
compareY(other) {
return this.compare(this.y, other.y);
}
hashCode() {
return this.hash;
}
equals(other) {
return this === other || (this.compareX(other) && this.compareY(other));
}
toString() {
return `(${this.x}, ${this.y})`;
}
}
class Line {
p0;
p1;
horizontal;
constructor(p0, p1) {
this.p0 = p0;
this.p1 = p1;
this.horizontal = p0.compareY(p1);
}
isLine() {
return true;
}
getStart() {
return this.p0;
}
getEnd() {
return this.p1;
}
getLeft() {
return null;
}
getRight() {
return null;
}
reverse() {
const t = this.p0;
this.p0 = this.p1;
this.p1 = t;
}
merge(line) {
if (this.horizontal === line.horizontal) {
if (this.p1.equals(line.p0)) {
this.p1 = line.p1;
}
else {
this.p0 = line.p0;
}
return true;
}
return false;
}
toString() {
return `${this.p0}-${this.p1}`;
}
}
class Arc {
p0;
p1;
p2;
radius;
constructor(p0, p1, p2, radius) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.radius = radius;
}
isLine() {
return false;
}
getStart() {
return this.p0;
}
getEnd() {
return this.p2;
}
getLeft() {
return null;
}
getRight() {
return null;
}
reverse() {
const t = this.p0;
this.p0 = this.p2;
this.p2 = t;
}
toString() {
return `${this.p0}~${this.p1}~${this.p2}:${this.radius}`;
}
}
class HashMap {
map = new Map();
set(key, value) {
const hash = key.hashCode();
let bucket = this.map.get(hash);
if (!bucket) {
bucket = [];
this.map.set(hash, bucket);
}
const existing = bucket.find(entry => entry.key.equals(key));
if (existing) {
existing.value = value;
}
else {
bucket.push({ key, value });
}
}
get(key) {
const bucket = this.map.get(key.hashCode());
if (!bucket) {
return undefined;
}
const entry = bucket.find(entry => entry.key.equals(key));
return entry ? entry.value : undefined;
}
has(key) {
return this.get(key) !== undefined;
}
delete(key) {
const hash = key.hashCode();
const bucket = this.map.get(hash);
if (!bucket) {
return false;
}
const index = bucket.findIndex(entry => entry.key.equals(key));
if (index !== -1) {
bucket.splice(index, 1);
if (bucket.length === 0) {
this.map.delete(hash);
}
return true;
}
return false;
}
entries() {
const allEntries = [];
for (const bucket of this.map.values()) {
allEntries.push(...bucket);
}
return allEntries;
}
values() {
const allValues = [];
for (const bucket of this.map.values()) {
for (const entry of bucket) {
allValues.push(entry.value);
}
}
return allValues;
}
forEach(callback) {
for (const bucket of this.map.values()) {
for (const entry of bucket) {
callback(entry.value, entry.key);
}
}
}
}
class PathNode {
s0;
s1;
constructor(s0, s1) {
this.s0 = s0;
this.s1 = s1;
}
getStart() {
return this.s0.getStart();
}
getEnd() {
return this.s1.getEnd();
}
getLeft() {
return this.s0;
}
getRight() {
return this.s1;
}
reverse() {
this.s0.reverse();
this.s1.reverse();
const t = this.s0;
this.s0 = this.s1;
this.s1 = t;
}
isLine() {
return false;
}
toString() {
return `${this.s0}, ${this.s1}`;
}
}
class PathOptimizer {
cursor = new Point();
segments = [];
moveTo(x, y) {
this.cursor = new Point(x, y);
}
lineTo(x, y) {
const p1 = new Point(x, y);
this.segments.push(new Line(this.cursor, p1));
this.cursor = p1;
}
arcTo(x1, y1, x2, y2, radius) {
const p1 = new Point(x1, y1);
const p2 = new Point(x2, y2);
this.segments.push(new Arc(this.cursor, p1, p2, radius));
this.cursor = p2;
}
flatten(s, path) {
if (!s) {
return;
}
if (!(s.getLeft() || s.getRight())) {
path.push(s);
return;
}
if (s.getLeft()) {
this.flatten(s.getLeft(), path);
}
if (s.getRight()) {
this.flatten(s.getRight(), path);
}
}
optimize(path) {
for (let i = path.length - 1; i > 0; --i) {
if (path[i - 1].isLine() && path[i].isLine()) {
const l0 = path[i - 1];
const l1 = path[i];
if (l0.merge(l1)) {
path.splice(i, 1);
}
}
}
}
getPaths() {
const paths = [];
const map = new HashMap();
this.segments.forEach(segment => {
const startSegment = map.get(segment.getStart());
if (startSegment) {
map.delete(startSegment.getStart());
map.delete(startSegment.getEnd());
if (startSegment.getStart().equals(segment.getStart())) {
startSegment.reverse();
}
segment = new PathNode(startSegment, segment);
}
const endSegment = map.get(segment.getEnd());
if (endSegment) {
map.delete(endSegment.getStart());
map.delete(endSegment.getEnd());
if (endSegment.getEnd().equals(segment.getEnd())) {
endSegment.reverse();
}
segment = new PathNode(segment, endSegment);
}
map.set(segment.getStart(), segment);
map.set(segment.getEnd(), segment);
});
map.values().forEach(segment => {
if (!segment.getStart().equals(segment.getEnd())) {
map.delete(segment.getEnd());
}
});
map.values().forEach(segment => {
const path = [];
this.flatten(segment, path);
this.optimize(path);
paths.push(path);
});
this.cursor = new Point();
this.segments = [];
return paths;
}
}
function getTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are 0-based
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
}
function renderPaths(ctx, paths, roundedCorners) {
ctx.beginPath();
paths.forEach(path => {
let cursor = new Point();
path.forEach(segment => {
const p0 = segment.getStart();
if (!p0.equals(cursor)) {
ctx.moveTo(p0.x, p0.y);
}
if (segment.isLine()) {
const line = segment;
ctx.lineTo(line.p1.x, line.p1.y);
cursor = line.p1;
}
else {
const arc = segment;
if (roundedCorners) {
ctx.arcTo(arc.p1.x, arc.p1.y, arc.p2.x, arc.p2.y, arc.radius);
}
else {
ctx.lineTo(arc.p1.x, arc.p1.y);
ctx.lineTo(arc.p2.x, arc.p2.y);
}
cursor = arc.p2;
}
});
});
ctx.stroke();
}
function generateSolutionPaths(maze, cellSize, cellMarginFrac) {
const c = new PathOptimizer();
const d0 = cellMarginFrac * cellSize;
const d1 = (1 - cellMarginFrac) * cellSize;
const dm = cellSize / 2;
for (let i = maze.height - 1; i >= 0; --i) {
const oy = i * cellSize;
for (let j = maze.width - 1; j >= 0; --j) {
const ox = j * cellSize;
const cell = maze.cells[i][j];
if (cell.upper.north2) {
c.moveTo(ox + dm, oy);
c.lineTo(ox + dm, oy + cellSize);
}
else if (cell.upper.east2) {
c.moveTo(ox, oy + dm);
c.lineTo(ox + cellSize, oy + dm);
}
if (cell.upper.north && cell.lower.east2) {
c.moveTo(ox, oy + dm);
c.lineTo(ox + d0, oy + dm);
c.moveTo(ox + d1, oy + dm);
c.lineTo(ox + cellSize, oy + dm);
}
else if (cell.upper.east && cell.lower.north2) {
c.moveTo(ox + dm, oy);
c.lineTo(ox + dm, oy + d0);
c.moveTo(ox + dm, oy + d1);
c.lineTo(ox + dm, oy + cellSize);
}
else {
const lower = cell.lower;
const value = (lower.north2 ? 0b1000 : 0) | (lower.east2 ? 0b0100 : 0) | (lower.south2 ? 0b0010 : 0)
| (lower.west2 ? 0b0001 : 0);
switch (value) {
case 0b1100:
c.moveTo(ox + dm, oy);
c.arcTo(ox + dm, oy + dm, ox + cellSize, oy + dm, dm);
break;
case 0b0110:
c.moveTo(ox + cellSize, oy + dm);
c.arcTo(ox + dm, oy + dm, ox + dm, oy + cellSize, dm);
break;
case 0b0011:
c.moveTo(ox + dm, oy + cellSize);
c.arcTo(ox + dm, oy + dm, ox, oy + dm, dm);
break;
case 0b1001:
c.moveTo(ox, oy + dm);
c.arcTo(ox + dm, oy + dm, ox + dm, oy, dm);
break;
case 0b1010:
c.moveTo(ox + dm, oy);
c.lineTo(ox + dm, oy + cellSize);
break;
case 0b0101:
c.moveTo(ox, oy + dm);
c.lineTo(ox + cellSize, oy + dm);
break;
}
}
}
}
return c.getPaths();
}
function generateWallPaths(maze, cellSize, cellMarginFrac) {
const c = new PathOptimizer();
const d0 = cellMarginFrac * cellSize;
const d1 = (1 - cellMarginFrac) * cellSize;
const dm = cellSize / 2;
const r0 = (d1 - d0) / 2;
for (let i = maze.height - 1; i >= 0; --i) {
const oy = i * cellSize;
for (let j = maze.width - 1; j >= 0; --j) {
const ox = j * cellSize;
const cell = maze.cells[i][j];
if (cell.upper.north) {
c.moveTo(ox + d0, oy);
c.lineTo(ox + d0, oy + cellSize);
c.moveTo(ox + d1, oy);
c.lineTo(ox + d1, oy + cellSize);
c.moveTo(ox, oy + d0);
c.lineTo(ox + d0, oy + d0);
c.moveTo(ox, oy + d1);
c.lineTo(ox + d0, oy + d1);
c.moveTo(ox + d1, oy + d0);
c.lineTo(ox + cellSize, oy + d0);
c.moveTo(ox + d1, oy + d1);
c.lineTo(ox + cellSize, oy + d1);
}
else if (cell.upper.east) {
c.moveTo(ox, oy + d0);
c.lineTo(ox + cellSize, oy + d0);
c.moveTo(ox, oy + d1);
c.lineTo(ox + cellSize, oy + d1);
c.moveTo(ox + d0, oy);
c.lineTo(ox + d0, oy + d0);
c.moveTo(ox + d1, oy);
c.lineTo(ox + d1, oy + d0);
c.moveTo(ox + d0, oy + d1);
c.lineTo(ox + d0, oy + cellSize);
c.moveTo(ox + d1, oy + d1);
c.lineTo(ox + d1, oy + cellSize);
}
else {
const lower = cell.lower;
const value = (lower.north ? 0b1000 : 0) | (lower.east ? 0b0100 : 0) | (lower.south ? 0b0010 : 0)
| (lower.west ? 0b0001 : 0);
switch (value) {
case 0b1000:
c.moveTo(ox + d0, oy);
c.lineTo(ox + d0, oy + dm);
c.arcTo(ox + d0, oy + d1, ox + dm, oy + d1, r0);
c.arcTo(ox + d1, oy + d1, ox + d1, oy + dm, r0);
c.lineTo(ox + d1, oy);
break;
case 0b0100:
c.moveTo(ox + cellSize, oy + d0);
c.lineTo(ox + dm, oy + d0);
c.arcTo(ox + d0, oy + d0, ox + d0, oy + dm, r0);
c.arcTo(ox + d0, oy + d1, ox + dm, oy + d1, r0);
c.lineTo(ox + cellSize, oy + d1);
break;
case 0b0010:
c.moveTo(ox + d0, oy + cellSize);
c.lineTo(ox + d0, oy + dm);
c.arcTo(ox + d0, oy + d0, ox + dm, oy + d0, r0);
c.arcTo(ox + d1, oy + d0, ox + d1, oy + dm, r0);
c.lineTo(ox + d1, oy + cellSize);
break;
case 0b0001:
c.moveTo(ox, oy + d0);
c.lineTo(ox + dm, oy + d0);
c.arcTo(ox + d1, oy + d0, ox + d1, oy + dm, r0);
c.arcTo(ox + d1, oy + d1, ox + dm, oy + d1, r0);
c.lineTo(ox, oy + d1);
break;
case 0b1100:
c.moveTo(ox + d0, oy);
c.arcTo(ox + d0, oy + d1, ox + cellSize, oy + d1, d1);
c.moveTo(ox + d1, oy);
c.arcTo(ox + d1, oy + d0, ox + cellSize, oy + d0, d0);
break;
case 0b0110:
c.moveTo(ox + d0, oy + cellSize);
c.arcTo(ox + d0, oy + d0, ox + cellSize, oy + d0, d1);
c.moveTo(ox + d1, oy + cellSize);
c.arcTo(ox + d1, oy + d1, ox + cellSize, oy + d1, d0);
break;
case 0b0011:
c.moveTo(ox + d1, oy + cellSize);
c.arcTo(ox + d1, oy + d0, ox, oy + d0, d1);
c.moveTo(ox + d0, oy + cellSize);
c.arcTo(ox + d0, oy + d1, ox, oy + d1, d0);
break;
case 0b1001:
c.moveTo(ox + d1, oy);
c.arcTo(ox + d1, oy + d1, ox, oy + d1, d1);
c.moveTo(ox + d0, oy);
c.arcTo(ox + d0, oy + d0, ox, oy + d0, d0);
break;
case 0b1101:
c.moveTo(ox, oy + d1);
c.lineTo(ox + cellSize, oy + d1);
c.moveTo(ox + d1, oy);
c.arcTo(ox + d1, oy + d0, ox + cellSize, oy + d0, d0);
c.moveTo(ox + d0, oy);
c.arcTo(ox + d0, oy + d0, ox, oy + d0, d0);
break;
case 0b1110:
c.moveTo(ox + d0, oy);
c.lineTo(ox + d0, oy + cellSize);
c.moveTo(ox + d1, oy);
c.arcTo(ox + d1, oy + d0, ox + cellSize, oy + d0, d0);
c.moveTo(ox + d1, oy + cellSize);
c.arcTo(ox + d1, oy + d1, ox + cellSize, oy + d1, d0);
break;
case 0b0111:
c.moveTo(ox, oy + d0);
c.lineTo(ox + cellSize, oy + d0);
c.moveTo(ox + d1, oy + cellSize);
c.arcTo(ox + d1, oy + d1, ox + cellSize, oy + d1, d0);
c.moveTo(ox + d0, oy + cellSize);
c.arcTo(ox + d0, oy + d1, ox, oy + d1, d0);
break;
case 0b1011:
c.moveTo(ox + d1, oy);
c.lineTo(ox + d1, oy + cellSize);
c.moveTo(ox + d0, oy + cellSize);
c.arcTo(ox + d0, oy + d1, ox, oy + d1, d0);
c.moveTo(ox + d0, oy);
c.arcTo(ox + d0, oy + d0, ox, oy + d0, d0);
break;
case 0b1111:
c.moveTo(ox + d1, oy);
c.arcTo(ox + d1, oy + d0, ox + cellSize, oy + d0, d0);
c.moveTo(ox + d1, oy + cellSize);
c.arcTo(ox + d1, oy + d1, ox + cellSize, oy + d1, d0);
c.moveTo(ox + d0, oy + cellSize);
c.arcTo(ox + d0, oy + d1, ox, oy + d1, d0);
c.moveTo(ox + d0, oy);
c.arcTo(ox + d0, oy + d0, ox, oy + d0, d0);
break;
case 0b1010:
c.moveTo(ox + d0, oy);
c.lineTo(ox + d0, oy + cellSize);
c.moveTo(ox + d1, oy);
c.lineTo(ox + d1, oy + cellSize);
break;
case 0b0101:
c.moveTo(ox, oy + d0);
c.lineTo(ox + cellSize, oy + d0);
c.moveTo(ox, oy + d1);
c.lineTo(ox + cellSize, oy + d1);
break;
}
}
}
}
return c.getPaths();
}
async function renderAndSave(solutionPaths, wallPaths, canvasType, filename, renderOptions) {
let canvas;
let ctx;
if (canvasType === 'pdf' && renderOptions.paperSize !== PaperSize.FIT) {
const width = renderOptions.imageWidth;
const height = renderOptions.imageHeight;
const { paperSize } = renderOptions;
let w1 = paperSize.printableWidthDots;
let s1 = w1 / width;
let h1 = s1 * height;
if (h1 > paperSize.printableHeightDots) {
h1 = paperSize.printableHeightDots;
s1 = h1 / height;
w1 = s1 * width;
}
let w2 = paperSize.printableHeightDots;
let s2 = w2 / width;
let h2 = s2 * height;
if (h2 > paperSize.printableWidthDots) {
h2 = paperSize.printableWidthDots;
s2 = h2 / height;
w2 = s2 * width;
}
if (w1 >= w2) {
canvas = createCanvas(paperSize.widthDots, paperSize.heightDots, 'pdf');
ctx = canvas.getContext('2d');
ctx.translate((paperSize.widthDots - w1) / 2, (paperSize.heightDots - h1) / 2);
ctx.beginPath();
ctx.rect(0, 0, w1, h1);
ctx.clip();
ctx.scale(s1, s1);
}
else {
canvas = createCanvas(paperSize.heightDots, paperSize.widthDots, 'pdf');
ctx = canvas.getContext('2d');
ctx.translate((paperSize.heightDots - w2) / 2, (paperSize.widthDots - h2) / 2);
ctx.beginPath();
ctx.rect(0, 0, w2, h2);
ctx.clip();
ctx.scale(s2, s2);
}
}
else {
canvas = createCanvas(renderOptions.imageWidth, renderOptions.imageHeight, canvasType);
ctx = canvas.getContext('2d');
}
ctx.lineWidth = renderOptions.lineWidthFrac * renderOptions.cellSize;
ctx.lineCap = renderOptions.roundedCorners ? 'round' : 'square';
let backgroundColor = renderOptions.backgroundColor;
if (!backgroundColor) {
backgroundColor = canvasType ? DEFAULT_SVG_AND_PDF_BACKGROUND_COLOR : DEFAULT_PNG_BACKGROUND_COLOR;
}
if (backgroundColor.alpha > 0) {
ctx.fillStyle = backgroundColor.toStyle();
ctx.fillRect(0, 0, renderOptions.imageWidth, renderOptions.imageHeight);
}
if (solutionPaths && renderOptions.solutionColor.alpha > 0) {
ctx.strokeStyle = renderOptions.solutionColor.toStyle();
renderPaths(ctx, solutionPaths, renderOptions.roundedCorners);
}
if (renderOptions.wallColor.alpha > 0) {
ctx.strokeStyle = renderOptions.wallColor.toStyle();
renderPaths(ctx, wallPaths, renderOptions.roundedCorners);
}
await promises.writeFile(filename, canvas.toBuffer());
}
async function saveMaze(maze, renderOptions) {
const cellMarginFrac = (1 - renderOptions.passageWidthFrac) / 2;
const solutionPaths = renderOptions.solution
? generateSolutionPaths(maze, renderOptions.cellSize, cellMarginFrac) : undefined;
const wallPaths = generateWallPaths(maze, renderOptions.cellSize, cellMarginFrac);
const timestamp = getTimestamp();
for (const extension of toFileExtensions(renderOptions.fileFormat)) {
const canvasType = (extension === 'png') ? undefined : extension;
for (const solution of renderOptions.solution ? [false, true] : [false]) {
let filename = renderOptions.outputDirectory + path.sep + renderOptions.filenamePrefix;
if (solution) {
filename += '-' + renderOptions.filenameSuffix;
}
if (renderOptions.timestamp) {
filename += '-' + timestamp;
}
filename += '.' + extension;
await renderAndSave(solution ? solutionPaths : undefined, wallPaths, canvasType, filename, renderOptions);
}
}
}
const DEFAULT_CROSS_PER = Math.round(100 * DEFAULT_CROSS_FRAC);
const DEFAULT_LOOP_PER = Math.round(100 * DEFAULT_LOOP_FRAC);
function printUsage() {
console.log(`
Usage: weave-maze-generator [options]
Output:
-d, --destination "..." Output directory (required)
-f, --format Output file format: png | svg | pdf (default: all three formats)
-p, --prefix Output filename prefix (default: ${DEFAULT_FILENAME_PREFIX})
-x, --solution-suffix Output filename solution suffix (default: ${DEFAULT_FILENAME_SOLUTION_SUFFIX})
-n, --no-timestamp Disables output filename timestamp
-N, --no-solution Disables solution file generation
-P, --paper-size ... Paper size for pdf files:
letter 8.5 x 11 in (default)
tabloid 11 x 17 in
legal 8.5 x 14 in
statement 5.5 x 8.5 in
executive 7.25 x 10.5 in
folio 8.5 x 13.5 in
quarto 8.5 x 10 5/6 in
a3 297 x 420 mm
a4 210 x 297 mm
a5 148 x 210 mm
b4 257 x 364 mm (JIS)
b5 182 x 257 mm (JIS)
fit drawing dimensions establish the paper size
Rectangle Mazes:
-w, --maze-width ... Number of cells spanning the width (default: ${DEFAULT_MAZE_SIZE}, max: ${MAX_MAZE_SIZE})
-h, --maze-height ... Number of cells spanning the height (default: ${DEFAULT_MAZE_SIZE}, max: ${MAX_MAZE_SIZE})
Custom-shaped Mazes:
-m, --mask "..." Filename of png image containing white pixels for maze cells and black or transparent
pixels for empty cells, with a maximum width and height of 200 pixels
Passages:
-X, --crosses ... Percentage of maze cells where two passages cross (default: ${DEFAULT_CROSS_PER})
-l, --loops ... Percentage of maze cells where a passage loops over itself (default: ${DEFAULT_LOOP_PER})
-L, --long Enables long passage generation.
Dimensions (specify one only):
-S, --cell-size ... Square maze cell size in pixels
Default: 25 or (image width / maze width) or (image height / maze height)
-W, --image-width ... Output image width in pixels
Default: (maze width x cell size) or (image height x maze width / maze height)
-H, --image-height ... Output image height in pixels
Default: (maze height x cell size) or (image width x maze height / maze width)
Corners:
-s, --square Enables square corners instead of the default rounded corners.
Widths (percentage of cell size):
-i, --line-width ... Wall and solution path line width (default: 15)
-g, --passage-width ... Maze passage width (default: 70)
Colors (hexadecimal color codes: RRGGBB or RRGGBBAA):
-a, --wall-color ... Wall color; default black (000000FF)
-b, --background-color ... Background color; default white for png (FFFFFFFF), transparent for svg and pdf (00000000)
-c, --solution-color ... Solution path color; default red (FF0000FF)
Other:
-v, --version Shows version number
-e, --help Shows this help message
`);
}
async function main() {
let args;
try {
args = extractArgs([
{
key: 'destination',
flags: ['-d', '--destination'],
type: ParamType.STRING,
},
{
key: 'format',
flags: ['-f', '--format'],
type: ParamType.STRING,
},
{
key: 'prefix',
flags: ['-p', '--prefix'],
type: ParamType.STRING,
},
{
key: 'solution-suffix',
flags: ['-x', '--solution-suffix'],
type: ParamType.STRING,
},
{
key: 'no-timestamp',
flags: ['-n', '--no-timestamp'],
type: ParamType.NONE,
},
{
key: 'no-solution',
flags: ['-N', '--no-solution'],
type: ParamType.NONE,
},
{
key: 'paper-size',
flags: ['-P', '--paper-size'],
type: ParamType.STRING,
},
{
key: 'maze-width',
flags: ['-w', '--maze-width'],
type: ParamType.INTEGER,
},
{
key: 'maze-height',
flags: ['-h', '--maze-height'],
type: ParamType.INTEGER,
},
{
key: 'mask',
flags: ['-m', '--mask'],
type: ParamType.STRING,
},
{
key: 'crosses',
flags: ['-X', '--crosses'],
type: ParamType.FLOAT,
},
{
key: 'loops',
flags: ['-l', '--loops'],
type: ParamType.FLOAT,
},
{
key: 'long',
flags: ['-L', '--long'],
type: ParamType.NONE,
},
{
key: 'cell-size',
flags: ['-S', '--cell-size'],
type: ParamType.FLOAT,
},
{
key: 'image-width',
flags: ['-W', '--image-width'],
type: ParamType.FLOAT,
},
{
key: 'image-height',
flags: ['-H', '--image-height'],
type: ParamType.FLOAT,
},
{
key: 'square',
flags: ['-s', '--square'],
type: ParamType.NONE,
},
{
key: 'line-width',
flags: ['-i', '--line-width'],
type: ParamType.FLOAT,
},
{
key: 'passage-width',
flags: ['-g', '--passage-width'],
type: ParamType.FLOAT,
},
{
key: 'wall-color',
flags: ['-a', '--wall-color'],
type: ParamType.STRING,
},
{
key: 'background-color',
flags: ['-b', '--background-color'],
type: ParamType.STRING,
},
{
key: 'solution-color',
flags: ['-c', '--solution-color'],
type: ParamType.STRING,
},
{
key: 'version',
flags: ['-v', '--version'],
type: ParamType.NONE,
},
{
key: 'help',
flags: ['-e', '--help'],
type: ParamType.NONE,
},
]);
}
catch (e) {
console.log();
console.log(e.message);
printUsage();
return;
}
if (args.get('version')) {
console.log('\n1.0.0\n');
return;
}
if (args.get('help')) {
printUsage();
return;
}
let outputDirectory = args.get('destination');
if (!outputDirectory) {
printUsage();
return;
}
outputDirectory = outputDirectory.trim();
let fileFormat;
try {
fileFormat = toFileFormat(args.get('format'));
}
catch (e) {
console.log(e.message);
return;
}
let filenamePrefix = args.get('prefix');
if (!filenamePrefix) {
filenamePrefix = DEFAULT_FILENAME_PREFIX;
}
else if (!validateFilename(filenamePrefix)) {
console.log('\nInvalid filename prefix.\n');
return;
}
else {
filenamePrefix = filenamePrefix.trim();
}
let filenameSolutionSuffix = args.get('solution-suffix');
if (!filenameSolutionSuffix) {
filenameSolutionSuffix = DEFAULT_FILENAME_SOLUTION_SUFFIX;
}
else if (!validateFilename(filenameSolutionSuffix)) {
console.log('\nInvalid filename solution suffix.\n');
return;
}
else {
filenameSolutionSuffix = filenameSolutionSuffix.trim();
}
let timestamp = args.get('no-timestamp');
if (timestamp === undefined) {
timestamp = DEFAULT_TIMESTAMP;
}
else {
timestamp = !timestamp;
}
let solution = args.get('no-solution');
if (solution === undefined) {
solution = DEFAULT_SOLUTION;
}
else {
solution = !solution;
}
let paperSize;
try {
paperSize = toPaperSize(args.get('paper-size'));
}
catch (e) {
console.log(e.message);
return;
}
let mazeWidth = args.get('maze-width');
let mazeHeight = args.get('maze-height');
const maskFilename = args.get('mask');
let mask;
if (maskFilename) {
if (mazeWidth !== undefined || mazeHeight !== undefined) {
console.log('\nSpecify either maze dimensions or a mask image, but not both.\n');
return;
}
if (!(await checkFileExists(maskFilename))) {
console.log('\nMask file not found.\n');
return;
}
try {
mask = await loadMask(maskFilename);
}
catch {
console.log('\nFailed to load mask file.\n');
return;
}
mazeHeight = mask.length;
if (mazeHeight < MIN_MAZE_SIZE || mazeHeight > MAX_MAZE_SIZE) {
console.log(`\nMask height must be between ${MIN_MAZE_SIZE} and ${MAX_MAZE_SIZE}.\n`);
return;
}
mazeWidth = mask[0].length;
if (mazeWidth < MIN_MAZE_SIZE || mazeWidth > MAX_MAZE_SIZE) {
console.log(`\nMask width must be between ${MIN_MAZE_SIZE} and ${MAX_MAZE_SIZE}.\n`);
return;
}
}
else {
if (mazeWidth === undefined) {
mazeWidth = DEFAULT_MAZE_SIZE;
}
if (!Number.isInteger(mazeWidth) || mazeWidth < MIN_MAZE_SIZE || mazeWidth > MAX_MAZE_SIZE) {
console.log(`\nMaze width must be an integer between ${MIN_MAZE_SIZE} and ${MAX_MAZE_SIZE}.\n`);
return;
}
if (mazeHeight === undefined) {
mazeHeight = DEFAULT_MAZE_SIZE;
}
if (!Number.isInteger(mazeHeight) || mazeHeight < MIN_MAZE_SIZE || mazeHeight > MAX_MAZE_SIZE) {
console.log(`\nMaze height must be an integer between ${MIN_MAZE_SIZE} and ${MAX_MAZE_SIZE}.\n`);
return;
}
}
let crossFrac = args.get('crosses');
if (crossFrac === undefined) {
crossFrac = DEFAULT_CROSS_FRAC;
}
else {
crossFrac /= 100;
}
if (crossFrac < MIN_CROSS_FRAC || crossFrac > MAX_CROSS_FRAC) {
console.log(`\nCrosses must be between ${100 * MIN_CROSS_FRAC} and ${100 * MAX_CROSS_FRAC}.\n`);
return;
}
let loopsFrac = args.get('loops');
if (loopsFrac === undefined) {
loopsFrac = DEFAULT_LOOP_FRAC;
}
else {
loopsFrac /= 100;
}
if (loopsFrac < MIN_LOOP_FRAC || loopsFrac > MAX_LOOP_FRAC) {
console.log(`\nLoops must be between ${100 * MIN_LOOP_FRAC} and ${100 * MAX_LOOP_FRAC}.\n`);
return;
}
let longPassages = args.get('long');
if (longPassages === undefined) {
longPassages = DEFAULT_LONG_PASSAGES;
}
let cellSize = args.get('cell-size');
let imageWidth = args.get('image-width');
let imageHeight = args.get('image-height');
if (cellSize === undefined && imageWidth === undefined && imageHeight === undefined) {
cellSize = DEFAULT_CELL_SIZE;
}
if (cellSize !== undefined) {
if (imageWidth !== undefined || imageHeight !== undefined) {
console.log('\nExclusively specify either cell size, image width, or image height.\n');
return;
}
if (cellSize < MIN_CELL_SIZE) {
console.log(`\nCell size must be at least ${MIN_CELL_SIZE}.\n`);
return;
}
imageWidth = cellSize * mazeWidth;
imageHeight = cellSize * mazeHeight;
if (imageWidth > MAX_IMAGE_SIZE || imageHeight > MAX_IMAGE_SIZE) {
console.log('\nCell size too big.\n');
return;
}
}
else if (imageWidth !== undefined) {
if (imageHeight !== undefined) {
console.log('\nExclusively specify either cell size, image width, or image height.\n');
return;
}
if (imageWidth < MIN_IMAGE_SIZE || imageWidth > MAX_IMAGE_SIZE) {
console.log(`\nImage width must be between ${MIN_IMAGE_SIZE} and ${MAX_IMAGE_SIZE}.\n`);
return;
}
cellSize = imageWidth / mazeWidth;
imageHeight = cellSize * mazeHeight;
}
else if (imageHeight !== undefined) {
if (imageHeight < MIN_IMAGE_SIZE || imageHeight > MAX_IMAGE_SIZE) {
console.log(`\nImage height must be between ${MIN_IMAGE_SIZE} and ${MAX_IMAGE_SIZE}.\n`);
return;
}
cellSize = imageHeight / mazeHeight;
imageWidth = cellSize * mazeWidth;
}
const roundedCorners = !args.get('square');
let lineWidthFrac = args.get('line-width');
if (lineWidthFrac === undefined) {
lineWidthFrac = DEFAULT_LINE_WIDTH_FRAC;
}
else {
lineWidthFrac /= 100;
}
if (lineWidthFrac < MIN_LINE_WIDTH_FRAC || lineWidthFrac > MAX_LINE_WIDTH_FRAC) {
console.log(`\nLine width must be between ${100 * MIN_LINE_WIDTH_FRAC} and ${100 * MAX_LINE_WIDTH_FRAC}.\n`);
return;
}
let passageWidthFrac = args.get('passage-width');
if (passageWidthFrac === undefined) {
passageWidthFrac = DEFAULT_PASSAGE_WIDTH_FRAC;
}
else {
passageWidthFrac /= 100;
}
if (passageWidthFrac < MIN_PASSAGE_WIDTH_FRAC || passageWidthFrac > MAX_PASSAGE_WIDTH_FRAC) {
console.log(`\nPassage width must be between ${100 * MIN_PASSAGE_WIDTH_FRAC} and ` +
`${100 * MAX_PASSAGE_WIDTH_FRAC}.\n`);
return;
}
const wallColorStr = args.get('wall-color');
let wallColor;
if (wallColorStr) {
try {
wallColor = toColor(wallColorStr);
}
catch {
console.log('\nInvalid wall color.\n');
return;
}
}
else {
wallColor = DEFAULT_WALL_COLOR;
}
const backgroundColorStr = args.get('background-color');
let backgroundColor;
if (backgroundColorStr) {
try {
backgroundColor = toColor(backgroundColorStr);
}
catch {
console.log('\nInvalid background color.\n');
return;
}
}
const solutionColorStr = args.get('solution-color');
let solutionColor;
if (solutionColorStr) {
try {
solutionColor = toColor(solutionColorStr);
}
catch {
console.log('\nInvalid solution color.\n');
return;
}
}
else {
solutionColor = DEFAULT_SOLUTION_COLOR;
}
if (!(await ensureDirectoryExists(outputDirectory))) {
console.log('\nFailed to created output directory.\n');
return;
}
void await saveMaze(generateMaze(new MazeOptions(mazeWidth, mazeHeight, loopsFrac, crossFrac, longPassages, mask)), new RenderOptions(outputDirectory, fileFormat, filenamePrefix, filenameSolutionSuffix, timestamp, solution, paperSize, cellSize, imageWidth, imageHeight, roundedCorners, lineWidthFrac, passageWidthFrac, wallColor, solutionColor, backgroundColor));
}
void await main();