/**
* Filer module for the Mystery Master Logic Puzzle Solver.
* TypeScript files are in project's ts folder, while JavaScript is output to either
* Server (Node.js): project's metawork/js folder.
* Client (Browser): project's js folder.
* @copyright mysterymaster.com. All rights reserved.
* @author Michael Benson
* @version 2021-08-31
* @module
*/
import { Puzzle } from "../puzzle/Puzzle.js";
import { Solver } from "../solver/Solver.js";
import { Viewer } from "../viewer/Viewer.js";
import * as Former from "../viewer/Former.js";
/** Absolute path to metawork folder. */
const pathToMetawork: string = "C:/projects/mysterymaster.com/metawork";
/** Absolute path to he,p folder. */
const pathToHelp: string = "C:/projects/mysterymaster.com/help";
/**
* Relative path from viewer (or server) folder to read puzzles folder.
* Used by getPuzzleNames, getPuzzles, loadPuzzles, loadPuzzlesAsync, loadPuzzle.
*/
const puzzlesFolder: string = "../puzzles";
// #region General File I/O
// File System (fs) module and Path module.
// Note: To define require, install type definitions for node: npm i --save-dev @types/node`.
// Instead of const fs = require('rs');
import * as fs from "fs";
import * as path from "path";
/** Document root for PHP files. Used by savePuzzlePHPFile, savePuzzleParts. */
const rootPHP: string = "\$_SERVER['DOCUMENT_ROOT']";
/**
* Returns array of base names for files in puzzles folder (aka puzzle modules).
* Must use readdirSync (blocking) version to create array!
* Called by viewer.constructor, buildPuzzleIndexes.
*/
export function getPuzzleNames() {
const ext: string = ".js";
const filenames: string[] = [];
fs.readdirSync(puzzlesFolder).forEach((file: string) => {
if (path.extname(file) === ext) {
console.log(file);
filenames.push(path.basename(file, ext));
}
});
return filenames;
}
/**
* Loads puzzle module given its name.
* Called by viewer.doNextPuzzle, buildPuzzleIndexes, buildPuzzlePHPFile.
* @param solver Solver.
* @param name Basename of puzzle module.
*/
export function getPuzzle(solver: Solver, name: string): Puzzle {
const ext: string = ".js";
const filename = `${puzzlesFolder}/${name}${ext}`;
const f = require(filename).loader;
const puzzle: Puzzle = f(solver);
return puzzle;
}
/**
* Loads each puzzle module.
* @param viewer Viewer.
*/
function loadPuzzles(viewer: Viewer) {
const ext: string = ".js";
let cnt = 0;
fs.readdirSync(puzzlesFolder).forEach((file: string) => {
if (file.slice(-3) === ext) {
const name:string = file.slice(0, -3);
const puzzle = loadPuzzle(viewer, name);
console.log(`${++cnt} name="${name}" title="${puzzle.title}" valid=${puzzle.valid}`);
}
});
}
/**
* Loads each puzzle module asynchronously.
* @param viewer Viewer.
*/
function loadPuzzlesAsync(viewer: Viewer) {
const ext: string = ".js";
let cnt = 0;
fs.readdir(puzzlesFolder, (err: Error, files: string[]) => {
files.forEach(file => {
if (file.slice(-3) === ext) {
const name = file.slice(0, -3);
const puzzle = loadPuzzle(viewer, name);
console.log(`${++cnt} name="${name}" title="${puzzle.title}" valid=${puzzle.valid}`);
}
});
});
}
// #endregion
// #region Metawork - Build PHP files for puzzles.
/**
* Builds PHP files for one puzzle or all puzzles.
* @param puzzleName Puzzle name. Assume all puzzles if puzzle name is null.
*/
export function buildPHPFiles(puzzleName: string = null) {
/**
* Builds PHP file for puzzle given its name.
* @param puzzleName Puzzle name.
*/
function buildPHPFile(puzzleName: string) {
// Path to output php files. Create path if does not exist.
const pathname: string = `${pathToMetawork}/php`;
if (!fs.existsSync(pathname)) fs.mkdirSync(pathname, { recursive: true });
// Load puzzle and HTML encode puzzle's string properties.
// Example: Must replace every embedded double-quote character with """.
const puzzle = getPuzzle(null, puzzleName);
const s1 = /\"/g, s2 = """;
const name = puzzle.name.replace(s1, s2);
const title = puzzle.title.replace(s1, s2);
const blurb = puzzle.blurb.replace(s1, s2);
const author = puzzle.author.replace(s1, s2);
const source = puzzle.source.replace(s1, s2);
console.log(`puzzle "${name}" "${title}" ${puzzle.rank} "${blurb}" "${author}" "${source}"`);
const text =
'\n' +
'\n' +
'\n' +
'\t
\n' +
'\t\t\n' +
'\t\n' +
'\t\n' +
'\t\t\n' +
'\t\t\n' +
'\t\t\n' +
'\t\n' +
'\n'
// Create PHP file.
const ext: string = ".php";
const filename = `${pathname}/${puzzleName}${ext}`;
console.log(`savePuzzlePHPFile filename="${filename}"`);
// Write content to file. Note: writeFileSync returns void.
fs.writeFileSync(filename, text, {encoding:'utf8',flag:'w'});
}
if (puzzleName !== null)
buildPHPFile(puzzleName);
else {
const puzzleNames = getPuzzleNames();
for (let puzzleName of puzzleNames) buildPHPFile(puzzleName);
}
}
// #endregion
// #region Metawork - Build Puzzle Indexes
/**
* Builds the index files that list puzzles by name as list, name as tile, name grouped by rank.
* Called by build-puzzle-indexes.
*/
export function buildPuzzleIndexes() {
// Path to output index files. Create path if does not exist.
const pathname: string = `${pathToMetawork}/indexes`;
if (!fs.existsSync(pathname)) fs.mkdirSync(pathname, { recursive: true });
// Builds PHP index files for all puzzles.
const puzzleNames = getPuzzleNames();
const puzzles: Puzzle[] = [];
for (let puzzleName of puzzleNames) {
const puzzle = getPuzzle(null, puzzleName);
puzzles.push(puzzle);
}
const ext: string = ".html";
/**
* Builds index file sorted by puzzle name, displayed as list.
* @param puzzles Puzzles sorted by name.
*/
function buildIndexByName(puzzles: Puzzle[]) {
const basename = "index-by-name"
const filename = `${pathname}/${basename}${ext}`;
console.log(`buildIndexByName filename="${filename}"`);
let text = `\n\nStars | Name |
\n\n\n`;
for (let puzzle of puzzles) {
const s = `${puzzle.rank} | ${puzzle.title} - ${puzzle.blurb} |
\n`;
text += s;
}
text += `\n
\n
`;
fs.writeFileSync(filename, text, {encoding:'utf8',flag:'w'});
}
/**
* Builds index file sorted by puzzle, displayed as tile.
* @param puzzles Puzzles sorted by name.
*/
function buildIndexByTile(puzzles: Puzzle[]) {
const basename = "index-by-tile"
const filename = `${pathname}/${basename}${ext}`;
console.log(`buildIndexByTile filename="${filename}"`);
let text = "";
for (let puzzle of puzzles) {
const s = `\n`;
text += s;
}
fs.writeFileSync(filename, text, {encoding:'utf8',flag:'w'});
}
/**
* Builds index file sorted by rank and name, displayed as list grouped by rank.
* @param puzzles Puzzles sorted by rank and name.
*/
function buildIndexByRank(puzzles: Puzzle[]) {
const basename = "index-by-rank"
const filename = `${pathname}/${basename}${ext}`;
console.log(`buildIndexByName filename="${filename}"`);
const rankNames = ["Easy Puzzles", "Moderate Puzzles", "Challenging Puzzles", "Difficult Puzzles", "Genius Puzzles"];
function getStars(rank: number): string {
let s = `\n\n`;
for (let i = 1; i <= rank; i++) {
s += ``;
}
s += ` ${rankNames[rank - 1]} |
\n\n\n`;
return s;
}
let rank0 = 0, rank1: number;
let text = "";
for (let puzzle of puzzles) {
rank1 = puzzle.rank;
// Create new HTML table when the rank changes.
if (rank1 > rank0) {
if (rank0 > 0) text += `\n
\n`;
rank0 = rank1;
text += getStars(rank1);
}
const s = `${puzzle.title} - ${puzzle.blurb} |
\n`;
text += s;
}
// Write content to file.
fs.writeFileSync(filename, text, {encoding:'utf8',flag:'w'});
}
// Sort puzzles by name. Assume name is unique.
puzzles.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
//for (let puzzle of puzzles) console.log(`puzzle.name="${puzzle.name}`);
buildIndexByName(puzzles);
buildIndexByTile(puzzles);
// Sort puzzles by rank, then by name. Assume name is unique.
puzzles.sort((a, b) => a.rank < b.rank ? -1 : a.rank > b.rank ? 1 : a.name < b.name ? -1 : 1);
//for (let puzzle of puzzles) console.log(`puzzle.name="${puzzle.name}`);
buildIndexByRank(puzzles);
}
// #endregion
// #region Metawork - Build Puzzle Parts
// See /help/build-puzzle-parts.php for more info.
/**
* Load puzzle module given its basename.
* Called by loadPuzzles, loadPuzzlesAsync,
* @param viewer Viewer.
* @param name Basename of puzzle module.
*/
export function loadPuzzle(viewer: Viewer, name: string): Puzzle {
const filename = `${puzzlesFolder}/${name}`;
const f = require(filename).loader;
const puzzle: Puzzle = f(viewer.solver);
viewer.setPuzzle(puzzle);
viewer.doWork();
return puzzle;
}
/**
* Saves each part of puzzle to appropriate file. Check if puzzle or solver is null!
* @param puzzle Puzzle.
* @param solver Solver.
* @param partType Part type is either "puzzle" or "solved".
*/
export function savePuzzleParts(puzzle: Puzzle, solver: Solver, partType: string) {
if (puzzle === null || solver === null) return;
/**
* Creates header for PHP puzzle part file.
* @param partName Part name.
* @returns Header for PHP file.
*/
const getHeader = (partName: string): string =>
`\n` +
`\n` +
`\n` +
`\n` +
`\n` +
`\n` +
`\n` +
`\n` +
`
\n`;
/**
* Creates footer for PHP puzzle part file.
* @returns Footer for PHP file.
*/
const getFooter = (): string =>
`
\n` +
`\n`+
`\n` +
`\n`
/**
* Saves puzzle part info to PHP file determined by part type and name.
* @param partName Part name. See code in savePuzzleParts for all names.
* @param body Part info as HTML.
*/
function savePuzzlePart(partName: string, body: string) {
const ext: string = ".php";
// Create content with given part name.
const text = getHeader(partName) + body + getFooter();
// Path to output parts with part name in lowercase.
const pathname = `${pathToHelp}/${partType}/${partName.toLowerCase()}`;
// Create nested folder structure (full path) if does not exist.
if (!fs.existsSync(pathname)) fs.mkdirSync(pathname, { recursive: true });
const filename = `${pathname}/${puzzle.name}${ext}`;
console.log(`savePuzzlePart filename="${filename}"`);
// Write content to file.
fs.writeFileSync(filename, text, {encoding:'utf8',flag:'w'});
}
if (partType !== "") {
const b = partType === "solved";
savePuzzlePart("Nouns", Former.getNounsAsHtml(puzzle));
savePuzzlePart("Facts", Former.getFactsAsHtml(puzzle));
savePuzzlePart("Rules", Former.getRulesAsHtml(puzzle));
savePuzzlePart("Chart", Former.getChartAsHtml(puzzle, 0, b));
savePuzzlePart("Grids", Former.getGridsAsHtml(solver, puzzle));
}
if (partType === "puzzle") {
savePuzzlePart("Verbs", Former.getVerbsAsHtml(puzzle));
savePuzzlePart("Links", Former.getLinksAsHtml(puzzle));
}
if (partType === "solved") {
savePuzzlePart("Marks", Former.getMarksAsHtml(solver));
savePuzzlePart("Stats", Former.getStatsAsHtml(solver.stats));
}
}
// #endregion