UNPKG

weave-maze-generator

Version:
2,510 lines 83.3 kB
#!/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.1\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();