View a Logic Puzzle in JavaScript Scroll To Bottom


logic-puzzle-view-js

Table of Contents

Introduction

This article explains how to view a logic grid puzzle in the JavaScript programming language. If you have not already done so, please read my article "Model a Logic Puzzle in JavaScript". This article will focus on the logic puzzle "Five Houses". Please look at this logic puzzle before continuing. You should have a basic knowledge of JavaScript, know some of the newer constructs, and understand how classes work in JavaScript.

User Interface

One of the main concerns of any application is to have a responsive User Interface (UI). If the UI is unresponsive, the user will think the application has stopped working. A C# application can get around this problem by using the async/await keywords. Until these keywords are common in JavaScript, I use the following two technologies: (1) the setTimeout keyword, and (2) Web Workers. The setTimeout keyword is discussed towards the end of this article. Web Workers will be discussed in a future article. The component that manages the UI is called the Viewer.

Viewer

The Viewer component is the UI manager for Mystery Master. Because this is is a web application, it must manage HTML, CSS, and JavaScript. Some of this is done using PHP. For instance, I use PHP to display common headers and footers for most of my pages. But the two most important visual components for solving a logic puzzle are the dashboard (aka Board), and the tabbed-interface (aka Tabby). These two components are displayed on the logic puzzle page after the text.

Note: Given everything I've said so far, I should probably take a look at React.

The Viewer Class

import { Verb } from "../puzzle/Verb.js";
import { Mark } from "../puzzle/Mark.js";
import * as Helper from "../puzzle/Helper.js";
import { Solver } from "../solver/Solver.js";
import { Board } from "./Board.js";
import { Tabby } from "./Tabby.js";
import { Setup } from "./Setup.js";
import * as UIX from "./UIX.js";
import * as Filer from "./Filer.js";
var SayEvent;
(function (SayEvent) {
    SayEvent[SayEvent["Tuple"] = 0] = "Tuple";
    SayEvent[SayEvent["Started"] = 1] = "Started";
    SayEvent[SayEvent["Stopped"] = 2] = "Stopped";
    SayEvent[SayEvent["Level"] = 3] = "Level";
    SayEvent[SayEvent["Solution"] = 4] = "Solution";
    SayEvent[SayEvent["AddMark"] = 5] = "AddMark";
    SayEvent[SayEvent["RemoveMark"] = 6] = "RemoveMark";
    SayEvent[SayEvent["ValidMark"] = 7] = "ValidMark";
    SayEvent[SayEvent["Contradiction"] = 8] = "Contradiction";
    SayEvent[SayEvent["FactViolation"] = 9] = "FactViolation";
    SayEvent[SayEvent["RuleViolation"] = 10] = "RuleViolation";
    SayEvent[SayEvent["LawViolation"] = 11] = "LawViolation";
    SayEvent[SayEvent["Placers"] = 12] = "Placers";
})(SayEvent || (SayEvent = {}));
export class Viewer {
    constructor(okAllPuzzles = false, okSavePuzzle = false, okSaveSolved = false) {
        this.puzzle = null;
        this.solver = null;
        this.board = null;
        this.tabby = null;
        this.setup = null;
        this.okPauseAll = false;
        this.okPauseLevel = false;
        this.okPauseSolution = true;
        this.okPauseViolation = false;
        this.okPauseMark = false;
        this.okPauseTrigger = false;
        this.okPauseGuess = false;
        this.okPausePlacers = false;
        this.okPauseNext = false;
        this.okAutorun = false;
        this.okRechart = false;
        this.toString = () => "Viewer";
        this.asString = () => toString();
        this.nodeFlag = typeof document === "undefined";
        this.puzzleNames = [];
        this.puzzleNum = -1;
        this.okAllPuzzles = false;
        this.okSavePuzzle = false;
        this.okSaveSolved = false;
        console.log(`exec viewer nodeFlag=${this.nodeFlag} okAllPuzzles=${okAllPuzzles} okSavePuzzle=${okSavePuzzle} okSaveSolved=${okSaveSolved}`);
        this.okAllPuzzles = okAllPuzzles;
        this.okSavePuzzle = okSavePuzzle;
        this.okSaveSolved = okSaveSolved;
        this.solver = new Solver(this);
        console.log(`viewer solver="${this.solver}"`);
        if (this.nodeFlag) {
            if (this.okAllPuzzles) {
                this.okAutorun = true;
                this.puzzleNames = Filer.getPuzzleNames();
                console.log("Puzzle Modules\n" + this.puzzleNames.join("\n"));
                this.puzzleNum = -1;
                this.doNextPuzzle();
            }
        }
        else {
            this.board = Object.seal(new Board(this));
            this.tabby = Object.seal(new Tabby(this));
            this.setup = Object.seal(new Setup(this));
        }
        console.log(`done viewer board="${this.board}" tabby="${this.tabby}" setup="${this.setup}"`);
    }
    doNextPuzzle() {
        let puzzle = null;
        while (true) {
            if (++this.puzzleNum >= this.puzzleNames.length)
                return;
            const name = this.puzzleNames[this.puzzleNum];
            puzzle = Filer.getPuzzle(this.solver, name);
            console.log(`viewer.doNextPuzzle name="${name}" puzzle="${puzzle}" enabled=${puzzle === null || puzzle === void 0 ? void 0 : puzzle.enabled} okAutorun=${this.okAutorun}`);
            if (puzzle.enabled)
                break;
        }
        this.setPuzzle(puzzle);
    }
    updateOption(key, val) {
        switch (key) {
            case "chkAutorun":
                this.okAutorun = val;
                break;
            case "chkRechart":
                this.okRechart = val;
                break;
            case "chkPauseAll":
                this.okPauseAll = val;
                break;
            case "chkPauseLevel":
                this.okPauseLevel = val;
                break;
            case "chkPauseSolution":
                this.okPauseSolution = val;
                break;
            case "chkPauseViolation":
                this.okPauseViolation = val;
                break;
            case "chkPauseMark":
                this.okPauseMark = val;
                break;
            case "chkPauseTrigger":
                this.okPauseTrigger = val;
                break;
            case "chkPauseGuess":
                this.okPauseGuess = val;
                break;
            case "chkPausePlacers":
                this.okPausePlacers = val;
                break;
            default:
                this.solver.updateOption(key, val);
                break;
        }
    }
    reset() {
        var _a, _b, _c;
        this.okPauseNext = false;
        (_a = this.puzzle) === null || _a === void 0 ? void 0 : _a.reset();
        this.solver.reset();
        (_b = this.board) === null || _b === void 0 ? void 0 : _b.reset();
        (_c = this.tabby) === null || _c === void 0 ? void 0 : _c.reset();
    }
    setPuzzle(puzzle) {
        var _a, _b;
        this.puzzle = puzzle;
        if (this.puzzle !== null) {
            this.puzzle.validate();
            if (!this.puzzle.valid)
                this.puzzle = null;
        }
        this.solver.setPuzzle(this.puzzle);
        (_a = this.board) === null || _a === void 0 ? void 0 : _a.setPuzzle(this.puzzle);
        (_b = this.tabby) === null || _b === void 0 ? void 0 : _b.setPuzzle(this.puzzle);
        this.reset();
        if (this.nodeFlag) {
            if (this.okSavePuzzle) {
                Filer.savePuzzleParts(this.puzzle, this.solver, "puzzle");
            }
        }
        if (this.puzzle !== null && this.okAutorun)
            this.doWork();
    }
    doWork() {
        var _a;
        console.log(`exec viewer.doWork puzzle="${this.puzzle}"`);
        this.reset();
        (_a = this.board) === null || _a === void 0 ? void 0 : _a.doWork();
        this.solver.doWork();
        console.log("done viewer.doWork");
    }
    doPause() { this.okPauseNext = true; }
    doResume() {
        const callback = this.solver.callback;
        if (callback !== null)
            setTimeout(callback, 0);
    }
    doReset() { this.reset(); }
    doQuit() { this.solver.doQuit(); }
    toggleFact(num) {
        const fact = this.puzzle.facts[num - 1];
        fact.enabled = !fact.enabled;
        if (this.solver.numMarks === 0)
            fact.initEnabled = fact.enabled;
    }
    toggleRule(num) {
        const rule = this.puzzle.rules[num - 1];
        rule.enabled = !rule.enabled;
        if (this.solver.numMarks === 0)
            rule.initEnabled = rule.enabled;
    }
    updateChartCol1(icol) { var _a; (_a = this.tabby) === null || _a === void 0 ? void 0 : _a.updateChartCol1(icol); }
    undoUserMark() { this.solver.undoUserMark(); }
    sayMessage(msg) {
        var _a;
        (_a = this.board) === null || _a === void 0 ? void 0 : _a.sayMessage(msg);
    }
    clickGridCell(t1, n1, t2, n2, v) {
        if (this.puzzle === null)
            return;
        if (v !== Verb.Maybe.num)
            return;
        const verb = UIX.getGridVerbFromLocker();
        if (verb === Verb.Maybe)
            return;
        const noun1 = this.puzzle.nounTypes[t1].nouns[n1];
        const noun2 = this.puzzle.nounTypes[t2].nouns[n2];
        const msg = `You entered "${verb.code}" for ${noun1} and ${noun2}`;
        this.sayMessage(msg);
        this.solver.addMark(0, "", Mark.Type.User, 0, "", noun1, verb, noun2, "", [], null);
    }
    sayPause(evn, msg, okPause) {
        var _a;
        this.sayMessage(msg);
        this.okPauseNext = false;
        if (this.nodeFlag && evn == SayEvent.Stopped) {
            if (this.okAllPuzzles)
                this.doNextPuzzle();
            return;
        }
        if (okPause)
            (_a = this.board) === null || _a === void 0 ? void 0 : _a.sayPause();
        else
            this.doResume();
    }
    sayTuple(key, val) {
        const okPause = false;
        const msg = `${key}=${val}`;
        this.sayPause(SayEvent.Tuple, msg, okPause);
    }
    sayStarted(msg) {
        const okPause = msg !== null && (this.okPauseNext || this.okPauseAll);
        console.log(`viewer.sayStarted`);
        this.sayPause(SayEvent.Started, msg, okPause);
    }
    sayStopped(msg, elapsedTime) {
        var _a;
        const okPause = false;
        console.log(`viewer.sayStopped elapsedTime=${elapsedTime}`);
        (_a = this.board) === null || _a === void 0 ? void 0 : _a.sayStopped();
        this.sayPause(SayEvent.Stopped, msg, okPause);
    }
    sayLevel(msg) {
        var _a;
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseLevel;
        (_a = this.board) === null || _a === void 0 ? void 0 : _a.sayLevel(msg);
        this.sayPause(SayEvent.Level, "Level " + msg, okPause);
    }
    saySolution(msg, elapsedTime) {
        var _a;
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseSolution;
        console.log(`viewer.saySolution elapsedTime=${elapsedTime}`);
        console.log(Helper.getChartAsText(this.puzzle, 0, true));
        (_a = this.tabby) === null || _a === void 0 ? void 0 : _a.saySolution(elapsedTime);
        if (this.nodeFlag) {
            if (this.okSaveSolved) {
                Filer.savePuzzleParts(this.puzzle, this.solver, "solved");
            }
            if (this.okAllPuzzles) {
                this.doQuit();
            }
        }
        this.sayPause(SayEvent.Solution, msg, okPause);
    }
    sayAddMark(mark) {
        var _a, _b;
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseMark || (this.okPauseTrigger && mark.type === Mark.Type.Rule) || (this.okPauseGuess && mark.type === Mark.Type.Level && mark.levelNum === Solver.MAX_LEVELS);
        (_a = this.board) === null || _a === void 0 ? void 0 : _a.sayMark();
        (_b = this.tabby) === null || _b === void 0 ? void 0 : _b.sayMark(mark, 1);
        this.sayPause(SayEvent.AddMark, mark.reason, okPause);
    }
    sayRemoveMark(msg, mark) {
        var _a, _b;
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseMark || (mark.hasPlacers() && this.okPausePlacers);
        (_a = this.board) === null || _a === void 0 ? void 0 : _a.sayMark();
        (_b = this.tabby) === null || _b === void 0 ? void 0 : _b.sayMark(mark, -1);
        this.sayPause(SayEvent.RemoveMark, msg, okPause);
    }
    sayValidMark(msg, mark) {
        var _a;
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseMark;
        mark.valid = true;
        (_a = this.tabby) === null || _a === void 0 ? void 0 : _a.sayMark(mark, 0);
        this.sayPause(SayEvent.ValidMark, msg, okPause);
    }
    sayContradiction(msg) {
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseViolation;
        this.sayPause(SayEvent.Contradiction, msg, okPause);
    }
    sayFactViolation(msg, mark, fact) {
        var _a, _b;
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseViolation;
        (_a = this.board) === null || _a === void 0 ? void 0 : _a.sayFactViolation();
        (_b = this.tabby) === null || _b === void 0 ? void 0 : _b.sayFactViolation(fact);
        this.sayPause(SayEvent.FactViolation, msg, okPause);
    }
    sayRuleViolation(msg, mark, rule) {
        var _a, _b;
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseViolation;
        (_a = this.board) === null || _a === void 0 ? void 0 : _a.sayRuleViolation();
        (_b = this.tabby) === null || _b === void 0 ? void 0 : _b.sayRuleViolation(rule);
        this.sayPause(SayEvent.RuleViolation, msg, okPause);
    }
    sayLawViolation(msg, mark) {
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseViolation;
        this.sayPause(SayEvent.LawViolation, msg, okPause);
    }
    sayPlacers(mark, rule) {
        var _a;
        const okPause = this.okPauseNext || this.okPauseAll || this.okPausePlacers;
        const msg = mark.getRulePlacersMsg(rule);
        (_a = this.tabby) === null || _a === void 0 ? void 0 : _a.sayPlacers(mark, rule);
        this.sayPause(SayEvent.Placers, msg, okPause);
    }
}

Board

The Board form displays information about the puzzle. Below is what the board looks like when there is no puzzle. If you want to know more about this form or any other form, please take a look at the Mystery Master Guide.

Level Pairs of Facts of Hits
Guess Marks of Rules of Hits

The board is initially displayed via the board.php file.

board.php

<table id="tblBoard" class="clsBoard">
	<tr>
		<td><button disabled id="btnSolve">Solve</button></td>
		<td>Level</td>
		<td><input type="text" readonly id="txtLevel" /></td>
		<td>Pairs</td>
		<td><input type="text" readonly id="txtNumPairs" /></td>
		<th>of</th>
		<td><input type="text" readonly id="txtMaxPairs" /></td>
		<td>Facts</td>
		<td><input type="text" readonly id="txtNumFacts" /></td>
		<th>of</th>
		<td><input type="text" readonly id="txtMaxFacts" /></td>
		<td>Hits</td>
		<td><input type="text" readonly id="txtNumFactHits" /></td>
	</tr>
	<tr>
		<td><button disabled id="btnReset">Reset</button></td>
		<td>Guess</td>
		<td><input type="text" readonly id="txtNumGuesses" /></td>
		<td>Marks</td>
		<td><input type="text" readonly id="txtNumMarks" /></td>
		<th>of</th>
		<td><input type="text" readonly id="txtMaxMarks" /></td>
		<td>Rules</td>
		<td><input type="text" readonly id="txtNumRules" /></td>
		<th>of</th>
		<td><input type="text" readonly id="txtMaxRules" /></td>
		<td>Hits</td>
		<td><input type="text" readonly id="txtNumRuleHits" /></td>
	</tr>
	<tr>
		<td colspan="13">
			<textarea rows="4" readonly id="txtMsg">
			</textarea>
		</td>
	</tr>
</table>

The board is updated by the Board class.

The Board Class

import * as UIX from "./UIX.js";
const SOLVE_CAPTIONS = ["Solve", "Pause", "Resume"];
const RESET_CAPTIONS = ["Reset", "Quit"];
export function Board(viewer) {
    let puzzle = null;
    const solver = viewer.solver;
    console.log(`exec board viewer="${viewer}" solver="${solver}"`);
    this.toString = () => "Board";
    this.asString = () => toString();
    const txtNumFacts = UIX.getInputById("txtNumFacts");
    const txtNumRules = UIX.getInputById("txtNumRules");
    const txtNumPairs = UIX.getInputById("txtNumPairs");
    const txtNumMarks = UIX.getInputById("txtNumMarks");
    const txtMaxFacts = UIX.getInputById("txtMaxFacts");
    const txtMaxRules = UIX.getInputById("txtMaxRules");
    const txtMaxPairs = UIX.getInputById("txtMaxPairs");
    const txtMaxMarks = UIX.getInputById("txtMaxMarks");
    const txtNumFactHits = UIX.getInputById("txtNumFactHits");
    const txtNumRuleHits = UIX.getInputById("txtNumRuleHits");
    const txtLevel = UIX.getInputById("txtLevel");
    const txtNumGuesses = UIX.getInputById("txtNumGuesses");
    const txtMsg = UIX.getInputById("txtMsg");
    const btnSolve = UIX.getButtonById("btnSolve");
    const btnReset = UIX.getButtonById("btnReset");
    function btnSolve_OnClick() {
        const caption = btnSolve.innerText;
        console.log(`exec board.btnSolve_OnClick caption="${caption}"`);
        switch (caption) {
            case SOLVE_CAPTIONS[0]:
                viewer.doWork();
                break;
            case SOLVE_CAPTIONS[1]:
                btnSolve.innerText = SOLVE_CAPTIONS[2];
                viewer.doPause();
                break;
            case SOLVE_CAPTIONS[2]:
                btnSolve.innerText = SOLVE_CAPTIONS[1];
                viewer.doResume();
                break;
        }
        console.log("done board.btnSolve_OnClick");
    }
    function btnReset_OnClick() {
        const caption = btnReset.innerText;
        console.log(`exec board.btnReset_OnClick caption="${caption}"`);
        switch (caption) {
            case RESET_CAPTIONS[0]:
                btnReset.disabled = true;
                viewer.doReset();
                break;
            case RESET_CAPTIONS[1]:
                btnReset.innerText = RESET_CAPTIONS[0];
                viewer.doQuit();
                break;
        }
        console.log("done board.btnReset_OnClick");
    }
    btnSolve.innerText = SOLVE_CAPTIONS[0];
    btnSolve.disabled = true;
    btnSolve.addEventListener('click', btnSolve_OnClick, false);
    btnReset.innerText = RESET_CAPTIONS[0];
    btnReset.disabled = true;
    btnReset.addEventListener('click', btnReset_OnClick, false);
    this.reset = function () {
        console.log(`exec board.reset puzzle="${puzzle}"`);
        const b = puzzle === null;
        txtLevel.value = "";
        txtNumGuesses.value = b ? "" : solver.numGuesses.toString();
        txtNumPairs.value = b ? "" : solver.numPairs.toString();
        txtNumMarks.value = b ? "" : solver.numMarks.toString();
        txtNumFacts.value = b ? "" : solver.numFacts.toString();
        txtNumRules.value = b ? "" : solver.numRules.toString();
        txtMaxPairs.value = b ? "" : solver.maxPairs.toString();
        txtMaxMarks.value = b ? "" : solver.maxMarks.toString();
        txtMaxFacts.value = b ? "" : puzzle.facts.length.toString();
        txtMaxRules.value = b ? "" : puzzle.rules.length.toString();
        txtNumFactHits.value = b ? "" : solver.numFactHits.toString();
        txtNumRuleHits.value = b ? "" : solver.numRuleHits.toString();
        txtMsg.value = "";
        console.log("done board.reset");
    };
    this.setPuzzle = function (myPuzzle) {
        puzzle = myPuzzle;
        btnSolve.disabled = (puzzle === null);
        console.log(`board.setPuzzle puzzle="${puzzle}" btnSolve.disabled=${btnSolve.disabled}`);
    };
    this.doWork = function () {
        console.log(`exec board.doWork`);
        btnSolve.innerText = SOLVE_CAPTIONS[1];
        btnSolve.disabled = false;
        btnReset.innerText = RESET_CAPTIONS[1];
        btnReset.disabled = false;
        console.log(`done board.doWork btnSolve.disabled=${btnSolve.disabled} btnReset.disabled=${btnReset.disabled}`);
    };
    this.sayMessage = function (msg) { txtMsg.value = msg; };
    this.sayPause = function () {
        console.log(`exec board.sayPause NOTHING SHOULD HAPPEN UNTIL RESUME BUTTON IS PRESSED!`);
        btnSolve.innerText = SOLVE_CAPTIONS[2];
        console.log("done board.sayPause");
    };
    this.sayStopped = function () {
        btnSolve.innerText = SOLVE_CAPTIONS[0];
        btnReset.innerText = RESET_CAPTIONS[0];
    };
    this.sayLevel = function (msg) { txtLevel.value = msg; };
    this.sayMark = function () {
        txtNumGuesses.value = solver.numGuesses.toString();
        txtNumPairs.value = solver.numPairs.toString();
        txtNumMarks.value = solver.numMarks.toString();
        txtNumFacts.value = solver.numFacts.toString();
        txtNumFactHits.value = solver.numFactHits.toString();
        txtNumRules.value = solver.numRules.toString();
        txtNumRuleHits.value = solver.numRuleHits.toString();
    };
    this.sayFactViolation = function () {
        txtNumFacts.value = solver.numFacts.toString();
        txtNumFactHits.value = solver.numFactHits.toString();
    };
    this.sayRuleViolation = function () {
        txtNumRules.value = solver.numRules.toString();
        txtNumRuleHits.value = solver.numRuleHits.toString();
    };
    console.log(`done board btnSolve.disabled=${btnSolve.disabled} btnReset.disabled=${btnReset.disabled}`);
}

Tabby

The tabbed-interface controller, affectionately called tabby, displays one of many forms at a time. Here is what tabby looks like when the Setup form is displayed.

Nouns
Verbs
Facts
Rules
Marks
Chart
Grids
Stats
Setup
Pauses
All
Level
Solution
Violation
Mark
Trigger
Guess
Placer
General
Autorun
Rechart
Rules
Triggers


Levels
All
Level 1
Level 2
Level 3
Level 4
Laws
All
Law 1
Law 2
Law 3
Law 4
Law 5

Tabby is initially displayed via the tabby.php file.

tabby.php

<div id="divTabby" style="margin-top:12px;">
	<div id="divTabButtons">
		<button type="button" id="btnNouns">Nouns</button>
		<button type="button" id="btnVerbs">Verbs</button>
		<button type="button" id="btnLinks">Links</button>
		<button type="button" id="btnFacts">Facts</button>
		<button type="button" id="btnRules">Rules</button>
		<button type="button" id="btnMarks">Marks</button>
		<button type="button" id="btnChart">Chart</button>
		<button type="button" id="btnGrids">Grids</button>
		<button type="button" id="btnStats">Stats</button>
		<button type="button" id="btnSetup">Setup</button>
	</div>
	<div id="divTabContents">
		<div id="divNouns">Nouns</div>
		<div id="divVerbs">Verbs</div>
		<div id="divLinks">Links</div>
		<div id="divFacts">Facts</div>
		<div id="divRules">Rules</div>
		<div id="divMarks">Marks</div>
		<div id="divChart">Chart</div>
		<div id="divGrids">Grids</div>
		<div id="divStats">Stats</div>
		<div id="divSetup" style="display:inherit;">
			<?php include("setup.php") ?>
		</div>
	</div>
</div>

The Tabby Class

import { Verb } from "../puzzle/Verb.js";
import { Mark } from "../puzzle/Mark.js";
import * as Helper from "../puzzle/Helper.js";
import * as Locker from "./Locker.js";
import * as Former from "./Former.js";
import * as UIX from "./UIX.js";
const TAB_NAMES = ["Nouns", "Verbs", "Links", "Facts", "Rules", "Marks", "Chart", "Grids", "Stats", "Setup"];
const MAX_TABS = TAB_NAMES.length;
var TabNum;
(function (TabNum) {
    TabNum[TabNum["Nouns"] = 0] = "Nouns";
    TabNum[TabNum["Verbs"] = 1] = "Verbs";
    TabNum[TabNum["Links"] = 2] = "Links";
    TabNum[TabNum["Facts"] = 3] = "Facts";
    TabNum[TabNum["Rules"] = 4] = "Rules";
    TabNum[TabNum["Marks"] = 5] = "Marks";
    TabNum[TabNum["Chart"] = 6] = "Chart";
    TabNum[TabNum["Grids"] = 7] = "Grids";
    TabNum[TabNum["Stats"] = 8] = "Stats";
    TabNum[TabNum["Setup"] = 9] = "Setup";
})(TabNum || (TabNum = {}));
export function Tabby(viewer) {
    let puzzle = null;
    const solver = viewer.solver;
    console.log(`exec tabby viewer="${viewer}" solver="${solver}"`);
    let chartCol1 = 0;
    this.toString = () => "Tabby";
    this.asString = () => toString();
    const divs = [];
    const divNouns = divs[TabNum.Nouns] = UIX.getDivById("divNouns");
    const divVerbs = divs[TabNum.Verbs] = UIX.getDivById("divVerbs");
    const divLinks = divs[TabNum.Links] = UIX.getDivById("divLinks");
    const divFacts = divs[TabNum.Facts] = UIX.getDivById("divFacts");
    const divRules = divs[TabNum.Rules] = UIX.getDivById("divRules");
    const divMarks = divs[TabNum.Marks] = UIX.getDivById("divMarks");
    const divChart = divs[TabNum.Chart] = UIX.getDivById("divChart");
    const divGrids = divs[TabNum.Grids] = UIX.getDivById("divGrids");
    const divStats = divs[TabNum.Stats] = UIX.getDivById("divStats");
    const divSetup = divs[TabNum.Setup] = UIX.getDivById("divSetup");
    const btns = [];
    let tblFacts = null;
    let tblRules = null;
    let tblMarks = null;
    let tblGrids = null;
    function updateNouns() { divNouns.innerHTML = Former.getNounsAsHtml(puzzle); }
    function updateVerbs() { divVerbs.innerHTML = Former.getVerbsAsHtml(puzzle); }
    function updateLinks() { divLinks.innerHTML = Former.getLinksAsHtml(puzzle); }
    function updateFacts() {
        divFacts.innerHTML = Former.getFactsAsHtml(puzzle);
        tblFacts = divFacts.getElementsByTagName("table")[0];
    }
    function updateFact(fact) {
        const irow = fact.num;
        let cell;
        cell = tblFacts.rows[irow].cells[1];
        const fld = cell.children[0];
        fld.checked = fact.enabled;
        cell = tblFacts.rows[irow].cells[2];
        cell.innerHTML = fact.hits.toString();
        tblFacts.rows[irow].scrollIntoView();
    }
    function updateRules() {
        divRules.innerHTML = Former.getRulesAsHtml(puzzle);
        tblRules = divRules.getElementsByTagName("table")[0];
    }
    function updateRule(rule) {
        const irow = rule.num;
        const cell = tblRules.rows[irow].cells[2];
        cell.innerHTML = rule.hits.toString();
        tblRules.rows[irow].scrollIntoView();
    }
    function updateMarks() {
        const irow = solver.numMarks;
        divMarks.innerHTML = Former.getMarksAsHtml(solver);
        tblMarks = divMarks.getElementsByTagName("table")[0];
        tblMarks.rows[irow].scrollIntoView();
    }
    function updateMark(mark, d) {
        const irow = mark.num;
        const row = tblMarks.rows[irow];
        let cell;
        let fld;
        row.style.display = d < 0 ? "none" : "table-row";
        if (d < 0)
            return;
        cell = row.cells[1];
        fld = cell.children[0];
        fld.checked = mark.valid;
        if (d < 1)
            return;
        cell = row.cells[2];
        cell.innerHTML = mark.levelNum + mark.levelSub;
        cell = row.cells[3];
        cell.innerHTML = mark.type.name;
        cell = row.cells[4];
        fld = cell.children[0];
        fld.value = Helper.getMsgAsOneLine(mark.reason);
        tblMarks.rows[irow].scrollIntoView();
    }
    function updateChart() { divChart.innerHTML = Former.getChartAsHtml(puzzle, chartCol1); }
    ;
    this.updateChartCol1 = function (icol) {
        if (icol === 0)
            return;
        chartCol1 = (icol > chartCol1 ? icol : icol - 1);
        updateChart();
    };
    function updateGrids() {
        divGrids.innerHTML = Former.getGridsAsHtml(solver, puzzle);
        tblGrids = divGrids.getElementsByTagName("table")[0];
    }
    function updateGridCell(mark, d) {
        const t1 = mark.noun1.type.num, n1 = mark.noun1.num;
        let irow = 0;
        const t2 = mark.noun2.type.num, n2 = mark.noun2.num;
        let icol = 0;
        const n = puzzle.numNouns;
        if (t1 === 1) {
            irow = 1 + n1 + (n + 1) * (t1 - 1);
            icol = n2 + n * (t2 - 2);
        }
        else {
            const m = puzzle.nounTypes.length;
            irow = 1 + n2 + (n + 1) * (m - (t2 - 1));
            icol = n1 + n * (t1 - 2);
        }
        const cell = tblGrids.rows[irow].cells[icol];
        const verb = d !== -1 ? mark.verb : Verb.Maybe;
        cell.innerHTML = verb.getCodeAsHtml();
    }
    function updateStats() { divStats.innerHTML = Former.getStatsAsHtml(solver.stats); }
    function update(flag) {
        console.log(`exec tabby.update flag=${flag} tabNum1=${tabNum1} MAX_TABS=${MAX_TABS}`);
        for (let i = 0; i < MAX_TABS; i++) {
            btns[i].style.color = "black";
            divs[i].style.display = "none";
        }
        if (flag) {
            switch (tabNum1) {
                case TabNum.Nouns:
                    updateNouns();
                    break;
                case TabNum.Verbs:
                    updateVerbs();
                    break;
                case TabNum.Links:
                    updateLinks();
                    break;
                case TabNum.Facts:
                    updateFacts();
                    break;
                case TabNum.Rules:
                    updateRules();
                    break;
                case TabNum.Marks:
                    updateMarks();
                    break;
                case TabNum.Chart:
                    updateChart();
                    break;
                case TabNum.Grids:
                    updateGrids();
                    break;
                case TabNum.Stats:
                    updateStats();
                    break;
            }
        }
        btns[tabNum1].style.color = "blue";
        divs[tabNum1].style.display = "block";
    }
    function selectTab(evt) {
        const btn = evt.target;
        const id = btn.id;
        console.log(`tabby.selectTab id="${id}" tabIndex=${btn.tabIndex}`);
        tabNum1 = btn.tabIndex;
        Locker.setValue("tabNum1", tabNum1);
        console.log(`tabby.selectTab saved to locker tabNum1=${tabNum1}`);
        update(true);
    }
    function getButtonWithEvent(id) {
        const btn = UIX.getButtonById(id);
        btn.addEventListener('click', selectTab, false);
        return btn;
    }
    for (let i = 0; i < MAX_TABS; i++) {
        const id = "btn" + TAB_NAMES[i];
        btns[i] = getButtonWithEvent(id);
        btns[i].tabIndex = i;
    }
    this.reset = function () {
        console.log(`exec tabby.reset puzzle="${puzzle}"`);
        updateNouns();
        updateVerbs();
        updateLinks();
        updateFacts();
        updateRules();
        updateMarks();
        updateChart();
        updateGrids();
        updateStats();
        console.log("done tabby.reset");
    };
    this.setPuzzle = function (myPuzzle) {
        puzzle = myPuzzle;
        console.log(`tabby.setPuzzle puzzle="${puzzle}"`);
    };
    this.saySolution = function (elapsedTime) {
        chartCol1 = 0;
        updateChart();
    };
    this.sayMark = function (mark, d) {
        switch (tabNum1) {
            case TabNum.Facts:
                let facts = mark.facts.concat(mark.offFacts);
                facts.sort((a, b) => (a.num > b.num) ? 1 : -1);
                let oldFact = null;
                for (let fact of facts) {
                    if (fact !== oldFact)
                        updateFact(fact);
                    oldFact = fact;
                }
                break;
            case TabNum.Rules:
                if (mark.type === Mark.Type.Rule) {
                    const rule = puzzle.rules[mark.refNum - 1];
                    updateRule(rule);
                }
                break;
            case TabNum.Marks:
                updateMark(mark, d);
                break;
            case TabNum.Chart:
                if (mark.verb === Verb.Is) {
                    const i = mark.noun1.type.num - 1;
                    if (viewer.okRechart)
                        chartCol1 = i;
                    if (chartCol1 === i || chartCol1 === mark.noun2.type.num - 1) {
                        updateChart();
                    }
                }
                break;
            case TabNum.Grids:
                if (d !== 0)
                    updateGridCell(mark, d);
                break;
            case TabNum.Stats:
                updateStats();
                break;
        }
    };
    this.sayFactViolation = function (fact) { if (tabNum1 === TabNum.Facts)
        updateFact(fact); };
    this.sayRuleViolation = function (rule) { if (tabNum1 === TabNum.Rules)
        updateRule(rule); };
    this.sayPlacers = function (mark, rule) {
        if (tabNum1 === TabNum.Rules)
            updateRule(rule);
        if (tabNum1 === TabNum.Chart)
            updateChart();
        if (tabNum1 === TabNum.Grids)
            updateGrids();
    };
    let tabNum1 = Locker.getNumber("tabNum1", TabNum.Chart);
    update(true);
    console.log(`tabby from locker tabNum1=${tabNum1}`);
    console.log(`done tabby viewer="${viewer}" solver="${solver}" tabNum1=${tabNum1}`);
}

The forms for the nouns, verbs, links, facts, and rules are shown in my article "Model a Logic Puzzle in JavaScript". The forms for the marks, chart, and grids will be discussed in a future article. So that means the Stats form and the Setup form are discussed in this article.

Stats

The Statistics form displays information about how the puzzle was solved. It tells you how many marks and pairs were entered by level, law, and rule. Here are the statistics after our example puzzle has been solved.

The Stats Class

import { Mark } from "../puzzle/Mark.js";
import { Solver } from "./Solver.js";
export class LevelCounter {
    constructor() {
        this.levelHits = 0;
        this.ruleHits = 0;
        this.lawsHits = [];
        this.sum = 0;
        this.reset();
    }
    reset() {
        this.levelHits = 0;
        this.ruleHits = 0;
        for (let i = 0; i < Solver.MAX_LAWS; i++)
            this.lawsHits[i] = 0;
        this.sum = 0;
    }
}
export class Stats {
    constructor() {
        this.levelPairs = [];
        this.levelMarks = [];
        this.nrows = 0;
        this.totalPair = null;
        this.totalMark = null;
        this.toString = () => "Stats";
        this.asString = () => toString();
        this.levelPairs = this.getLevelCounters();
        this.levelMarks = this.getLevelCounters();
        this.nrows = this.levelMarks.length;
        this.totalPair = this.levelPairs[this.nrows - 1];
        this.totalMark = this.levelMarks[this.nrows - 1];
    }
    resetLevelCounters(levelCounters) {
        for (let levelCounter of levelCounters)
            levelCounter.reset();
    }
    reset() {
        this.resetLevelCounters(this.levelPairs);
        this.resetLevelCounters(this.levelMarks);
    }
    getLevelCounters() {
        const levelCounters = [];
        for (let i = 0; i < Solver.MAX_LEVELS + 1; i++) {
            levelCounters[i] = new LevelCounter();
        }
        return levelCounters;
    }
    update(mark, d) {
        const levelNum = mark.levelNum;
        if (levelNum < 1)
            return;
        const levelMark = this.levelMarks[levelNum - 1];
        const levelPair = this.levelPairs[levelNum - 1];
        const isPositive = mark.verb.num > 0;
        levelMark.sum += d;
        this.totalMark.sum += d;
        if (isPositive) {
            levelPair.sum += d;
            this.totalPair.sum += d;
        }
        switch (mark.type) {
            case Mark.Type.Level:
            case Mark.Type.User:
                levelMark.levelHits += d;
                this.totalMark.levelHits += d;
                if (isPositive) {
                    levelPair.levelHits += d;
                    this.totalPair.levelHits += d;
                }
                break;
            case Mark.Type.Rule:
                levelMark.ruleHits += d;
                this.totalMark.ruleHits += d;
                if (isPositive) {
                    levelPair.ruleHits += d;
                    this.totalPair.ruleHits += d;
                }
                break;
            case Mark.Type.Law:
                const j = mark.refNum - 1;
                levelMark.lawsHits[j] += d;
                this.totalMark.lawsHits[j] += d;
                if (isPositive) {
                    levelPair.lawsHits[j] += d;
                    this.totalPair.lawsHits[j] += d;
                }
                break;
            default:
                console.log("stats.updateOnMark bad mark.type!");
        }
    }
    ;
}

Setup

The Setup form displays options for the user that control how the application solves a logic puzzle. Here is what setup looks like when there is no puzzle.

Setup
Pauses
All
Level
Solution
Violation
Mark
Trigger
Guess
Placer
General
Autorun
Rechart
Rules
Triggers


Levels
All
Level 1
Level 2
Level 3
Level 4
Laws
All
Law 1
Law 2
Law 3
Law 4
Law 5

The Setup form is initially displayed via the setup.php file.

setup.php

<table class="clsAppTable clsSetup">
	<caption>Setup</caption>
	<tr>
		<td>
			<div class="clsSetupHeader">Pauses</div>
			<input type="checkbox" id="chkPauseAll">All<br />
			<input type="checkbox" id="chkPauseLevel">Level<br />
			<input type="checkbox" id="chkPauseSolution">Solution<br />
			<input type="checkbox" id="chkPauseViolation">Violation<br />
			<input type="checkbox" id="chkPauseMark">Mark<br />
			<input type="checkbox" id="chkPauseTrigger">Trigger<br />
			<input type="checkbox" id="chkPauseGuess">Guess<br />
			<input type="checkbox" id="chkPausePlacer">Placer<br />
		</td>
		<td>
			<div class="clsSetupHeader">General</div>
			<input type="checkbox" id="chkAutorun">Autorun<br />
			<input type="checkbox" id="chkRechart">Rechart<br />
			<input type="checkbox" id="chkRules">Rules<br />
			<input type="checkbox" id="chkTriggers">Triggers<br />
			<br />
			<button id="btnResetOptions">Reset</button><br />
		</td>
		<td>
			<div class="clsSetupHeader">Levels</div>
			<input type="checkbox" id="chkLevel0">All<br />
			<input type="checkbox" id="chkLevel1">Level 1<br />
			<input type="checkbox" id="chkLevel2">Level 2<br />
			<input type="checkbox" id="chkLevel3">Level 3<br />
			<input type="checkbox" id="chkLevel4">Level 4<br />
		</td>
		<td>
			<div class="clsSetupHeader">Laws</div>
			<input type="checkbox" id="chkLaw0">All<br />
			<input type="checkbox" id="chkLaw1">Law 1<br />
			<input type="checkbox" id="chkLaw2">Law 2<br />
			<input type="checkbox" id="chkLaw3">Law 3<br />
			<input type="checkbox" id="chkLaw4">Law 4<br />
			<input type="checkbox" id="chkLaw5">Law 5<br />
		</td>
	</tr>
</table>

The Setup Class

import { Solver } from "../solver/Solver.js";
import * as UIX from "./UIX.js";
export function Setup(myViewer) {
    const viewer = myViewer;
    console.log(`exec setup viewer="${viewer}"`);
    this.toString = () => "Setup";
    this.asString = () => toString();
    function updateField(fld, val, dir) {
        if (dir !== 0)
            UIX.setFieldFromLocker(fld, val);
        else
            UIX.setField(fld, val);
        viewer.updateOption(fld.id, fld.checked);
        if (dir === 0)
            UIX.putFieldInLocker(fld, val);
    }
    function updateOption(evt) {
        const fld = evt.target;
        updateField(fld, fld.checked, 0);
    }
    function getField(id) {
        const fld = UIX.getInputById(id);
        const eventType = 'click';
        fld.addEventListener(eventType, updateOption, false);
        return fld;
    }
    const chkPauseAll = getField("chkPauseAll");
    const chkPauseLevel = getField("chkPauseLevel");
    const chkPauseSolution = getField("chkPauseSolution");
    const chkPauseViolation = getField("chkPauseViolation");
    const chkPauseMark = getField("chkPauseMark");
    const chkPauseTrigger = getField("chkPauseTrigger");
    const chkPauseGuess = getField("chkPauseGuess");
    const chkPausePlacer = getField("chkPausePlacer");
    const chkAutorun = getField("chkAutorun");
    const chkRechart = getField("chkRechart");
    const chkRules = getField("chkRules");
    const chkTriggers = getField("chkTriggers");
    const btnResetOptions = UIX.getButtonById("btnResetOptions");
    const chkLevels = [];
    const chkLaws = [];
    function reset(dir) {
        updateField(chkPauseAll, false, dir);
        updateField(chkPauseLevel, false, dir);
        updateField(chkPauseSolution, true, dir);
        updateField(chkPauseViolation, false, dir);
        updateField(chkPauseMark, false, dir);
        updateField(chkPauseTrigger, false, dir);
        updateField(chkPauseGuess, false, dir);
        updateField(chkPausePlacer, false, dir);
        updateField(chkAutorun, false, dir);
        updateField(chkRechart, false, dir);
        updateField(chkRules, true, dir);
        updateField(chkTriggers, true, dir);
        updateField(chkLevels[0], true, dir);
        for (let i = 1; i < chkLevels.length; i++)
            updateField(chkLevels[i], true, dir);
        updateField(chkLaws[0], true, dir);
        for (let i = 1; i < chkLaws.length; i++)
            updateField(chkLaws[i], true, dir);
    }
    function btnResetOptions_OnClick(evt) { reset(0); }
    btnResetOptions.addEventListener('click', btnResetOptions_OnClick, false);
    for (let i = 0; i <= Solver.MAX_LEVELS; i++)
        chkLevels[i] = getField(`chkLevel${i}`);
    for (let i = 0; i <= Solver.MAX_LAWS; i++)
        chkLaws[i] = getField(`chkLaw${i}`);
    reset(1);
    console.log(`done setup`);
}

Locker

The Locker module is a wrapper for HTML 5 Storage.

The Locker Class

import * as Helper from "../puzzle/Helper.js";
export function supportHtml5Storage() {
    try {
        return 'localStorage' in window && window.localStorage !== null;
    }
    catch (e) {
        return false;
    }
}
export const ok = supportHtml5Storage();
export function setValue(key, val) {
    if (ok)
        localStorage.setItem(key, val);
}
export function getString(key, def) {
    let val = def;
    if (ok) {
        const tmp = localStorage.getItem(key);
        if (tmp !== null)
            val = tmp;
    }
    return val;
}
export function getNumber(key, def) {
    let val = def;
    if (ok) {
        const tmp = localStorage.getItem(key);
        const num = parseInt(tmp, 10);
        if (Helper.isNumber(num))
            val = num;
    }
    return val;
}
export function getBoolean(key, def) {
    let val = def;
    if (ok) {
        const tmp = localStorage.getItem(key);
        if (tmp !== null)
            val = Helper.getBoolean(tmp);
    }
    return val;
}
console.log(`Locker.ok=${ok}`);

Former

The Former module contains helpful methods that return HTML as a string. For example, these methods are called to populate most of the forms in the tabbed-interface component.

The Former Class

import * as Helper from "../puzzle/Helper.js";
import { Solver } from "../solver/Solver.js";
import * as UIX from "./UIX.js";
export function getNounsAsHtml(puzzle) {
    let txt = "";
    if (puzzle === null)
        return txt;
    txt = `<table id="tblNouns" class="clsAppTable clsNouns">\n<caption>Nouns</caption>\n<thead>\n<tr><th>#</th>`;
    let n = 0;
    for (let nounType of puzzle.nounTypes) {
        txt += `<th>${nounType.name}</th>`;
        if (nounType.num === 1)
            n = nounType.nouns.length;
    }
    txt += "</tr>\n</thead>\n<tbody>\n";
    for (let j = 0; j < n; j++) {
        txt += `<tr><td>${j + 1}</td>`;
        for (let nounType of puzzle.nounTypes) {
            const noun = nounType.nouns[j];
            txt += `<td>${noun.title}</td>`;
        }
        txt += "</tr>\n";
    }
    txt += "</tbody>\n</table>\n";
    return txt;
}
export function getVerbsAsHtml(puzzle) {
    let txt = "";
    if (puzzle === null)
        return txt;
    txt = `<table id="tblVerbs" class="clsAppTable clsVerbs">\n<caption>Verbs</caption>\n<thead>\n<tr><th>#</th><th>Type</th><th>Name</th><th>Code</th></tr>\n</thead>\n<tbody>\n`;
    for (let verb of puzzle.verbs) {
        txt += `<tr><td>${verb.num}</td><td>${verb.type}</td><td>${verb.name}</td><td>${verb.getCodeAsHtml()}</td></tr>\n`;
    }
    txt += "</tbody>\n</table>\n";
    return txt;
}
export function getLinksAsHtml(puzzle) {
    let txt = "";
    if (puzzle === null)
        return txt;
    txt = `<table id="tblLinks" class="clsAppTable clsLinks">\n<caption>Links</caption>\n<thead>\n<tr><th>#</th><th>Noun Type</th><th>Name</th><th>1:1</th></tr>\n</thead>\n<tbody>\n`;
    for (let link of puzzle.links) {
        txt += `<tr><td>${link.num}</td><td>${link.nounType.name}</td><td><input type="text" value="${link.name}" readonly></td><td><input type="checkbox"${link.oneToOne ? " checked" : ""} disabled="disabled" /></td></tr>\n`;
    }
    txt += "</tbody>\n</table>\n";
    for (let link of puzzle.links) {
        const nounType = link.nounType;
        txt += `<br />\n<table id="tblLinkGrid${link.num}" class="clsAppTable clsLinkGrid">\n<caption>${link.name}</caption>\n<thead>\n<tr><th style="font-weight:bold; text-align:center;">${nounType.name}</th>`;
        for (let noun of nounType.nouns) {
            txt += `<th>${noun.title}</th>`;
        }
        txt += "</tr>\n</thead>\n<tbody>\n";
        for (let noun1 of nounType.nouns) {
            txt += `<tr><th>${noun1.title}</th>`;
            for (let noun2 of nounType.nouns) {
                const verb = link.getVerb(noun1, noun2);
                txt += `<td>${verb.getCodeAsHtml()}</td>`;
            }
            txt += "</tr>\n";
        }
        txt += "</tbody>\n</table>\n";
    }
    return txt;
}
export function getFactsAsHtml(puzzle) {
    let txt = "";
    if (puzzle === null)
        return txt;
    txt = `<table id="tblFacts" class="clsAppTable clsFacts">\n<caption>Facts</caption>\n<thead>\n<tr><th>#</th><th>X</th><th>Hits</th><th>Name</th></tr>\n</thead>\n<tbody>\n`;
    for (let fact of puzzle.facts) {
        txt += `<tr><td>${fact.num}</td><td><input type="checkbox" onclick="UIX.toggleFact(${fact.num})"${fact.enabled ? " checked" : ""} /></td><td>${fact.hits}</td><td><input type="text" value="${fact.name}" readonly></td></tr>\n`;
    }
    txt += "</tbody>\n</table>\n";
    return txt;
}
export function getRulesAsHtml(puzzle) {
    let txt = "";
    if (puzzle === null)
        return txt;
    txt = `<table id="tblRules" class="clsAppTable clsRules">\n<caption>Rules</caption>\n<thead>\n<tr><th>#</th><th>X</th><th>Hits</th><th>Name</th></tr>\n</thead>\n<tbody>\n`;
    for (let rule of puzzle.rules) {
        txt += `<tr><td>${rule.num}</td><td><input type="checkbox" onclick="UIX.toggleRule(${rule.num})"${rule.enabled ? " checked" : ""} /></td><td>${rule.hits}</td><td><input type="text" value="${rule.name}" readonly></td></tr>\n`;
    }
    txt += "</tbody>\n</table>\n";
    return txt;
}
export function getMarksAsHtml(solver) {
    let txt = "";
    if (solver === null)
        return txt;
    txt = `<table id="tblMarks" class="clsAppTable clsMarks">\n<caption>Marks</caption>\n<thead>\n<tr><th>#</th><th>X</th><th>L</th><th>Type</th><th>Reason</th></tr>\n</thead>\n<tbody>\n`;
    for (let mark of solver.marks) {
        const rowStyle = `style="display:${mark.num > solver.numMarks ? "none" : "table-row"};"`;
        const tmp1 = `<input type="checkbox"${mark.valid ? " checked" : ""} />`;
        const tmp2 = `<input type="text" value="${Helper.getMsgAsOneLine(mark.reason)}" readonly>`;
        txt += `<tr ${rowStyle}><td>${mark.num}</td><td>${tmp1}</td><td>${mark.levelNum}${mark.levelSub}</td><td>${mark.type.name}</td><td>${tmp2}</td></tr>\n`;
    }
    txt += "</tbody>\n</table>\n";
    return txt;
}
export function getChartAsHtml(puzzle, chartCol1, isSolution = false) {
    let txt = "";
    if (puzzle === null)
        return txt;
    const caption = isSolution ? "Solution" : "Chart";
    txt = `<table id="tblChart" class="clsAppTable clsChart">\n<caption>${caption}</caption>\n<thead>\n<tr>\n`;
    const t = chartCol1;
    const nounTypes = puzzle.nounTypes;
    const nounType1 = nounTypes[t];
    const m = nounTypes.length;
    const n = puzzle.numNouns;
    let i = 0, j = 0, k = 0;
    for (j = 0; j < m; j++) {
        if (k === t)
            ++k;
        const nounType = (j === 0 ? nounType1 : nounTypes[k++]);
        txt += `<th onclick="UIX.updateChartCol1(${j})">${nounType.name}</th>\n`;
    }
    txt += "</tr>\n</thead>\n<tbody>\n";
    for (i = 0; i < n; i++) {
        txt += "<tr>\n";
        k = 0;
        for (j = 0; j < m; j++) {
            if (k === t)
                ++k;
            const noun1 = nounType1.nouns[i];
            if (j === 0)
                txt += `<td>${noun1.title}</td>`;
            else {
                const noun2 = noun1.getPairNoun(nounTypes[k]);
                if (noun2 === null)
                    txt += "<td>&nbsp;</td>";
                else
                    txt += `<td>${noun2.title}</td>`;
                ++k;
            }
        }
        txt += "\n</tr>\n";
    }
    txt += "</tbody>\n</table>\n";
    return txt;
}
export function getGridsAsHtml(solver, puzzle) {
    const gridVerb = UIX.getGridVerbFromLocker();
    let txt = "";
    if (puzzle === null)
        return txt;
    const getVerticalName = (name) => {
        let txt = "";
        for (let i = 0; i < name.length; i++)
            txt += `${name[i]}<br />`;
        return txt;
    };
    const gridControls = `<div class="clsGridControls">` +
        `<button id="btnGridVerb" alt="Verb" onclick="UIX.updateGridVerb()" title="Click to change verb">${gridVerb.getCodeAsHtml()}</button><br />` +
        `<button id="btnGridUndo" alt="Undo" onclick="UIX.undoUserMark()"   title="Undo your last mark">Undo</button>` +
        `</div>`;
    txt = `<table id="tblGrids" class="clsAppTable clsBigGrid">\n<caption>Grids</caption\n`;
    const n = puzzle.numNouns;
    const nounTypes = puzzle.nounTypes;
    let m = nounTypes.length;
    let irow = -1;
    let t1 = 0;
    do {
        const nounType1 = nounTypes[t1];
        if (t1 < 1) {
            ++irow;
            txt += `<thead>\n<tr><th colspan="${n}">&nbsp;</th>`;
            for (let t2 = 1; t2 < m; t2++) {
                const type2 = nounTypes[t2];
                txt += `<th colspan="${n}">${type2.name}</th>`;
                if (t2 < m - 1) {
                    txt += `<th class="clsGridSep" rowspan="${(m - t2 - 1) * (n + 1) + 1}"></th>`;
                }
            }
            txt += "</thead>\n</tr>\n";
            ++irow;
            txt += `<tr style="vertical-align:bottom;">`;
            txt += `<th colspan="${n}">${gridControls}<br />${nounType1.name}</th>`;
            for (let t2 = 1; t2 < m; t2++) {
                const type2 = nounTypes[t2];
                for (let n2 = 0; n2 < n; n2++) {
                    const noun2 = type2.nouns[n2];
                    txt += `<td>${getVerticalName(noun2.title)}</td>`;
                }
            }
            txt += "</tr>\n";
        }
        else {
            ++irow;
            txt += `<tr><th colspan="${n}">${nounType1.name}</th></tr>`;
        }
        for (let n1 = 0; n1 < n; n1++) {
            const noun1 = nounType1.nouns[n1];
            ++irow;
            let icol = 1;
            txt += "<tr>";
            txt += `<td colspan="${n}">${noun1.title}</td>`;
            for (let t2 = 1; t2 < m; t2++) {
                const type2 = nounTypes[t2];
                for (let n2 = 0; n2 < n; n2++) {
                    const noun2 = type2.nouns[n2];
                    const verb = solver.getGridVerb(noun1, noun2);
                    const evt3 = `onclick="UIX.clickGridCell(${t1},${n1},${t2},${n2},${verb.num});"`;
                    ++icol;
                    txt += `<td ${evt3}>${verb.getCodeAsHtml()}</td>`;
                }
            }
            txt += "</tr>\n";
        }
        if (t1 < 1)
            t1 = m;
    } while (--t1 > 1 && --m > 0);
    txt += "</table>\n";
    return txt;
}
export function getStatsAsHtml(stats) {
    function getStatsTable(levelCounters, label) {
        let txt = "";
        txt = `<table id="tblStats" class="clsAppTable clsStats">\n<caption>${label} By Level</caption>\n<thead>\n<tr><th>#</th><th>Facts</th><th>Rules</th>`;
        for (let j = 0; j < Solver.MAX_LAWS; j++) {
            txt += `<th>Law ${j + 1}</th>`;
        }
        txt += "<th>Total</th></tr>\n</thead>\n<tbody>\n";
        for (let i = 0; i < Solver.MAX_LEVELS; i++) {
            const lc = levelCounters[i];
            txt += `<tr><th>${i + 1}</th><td>${lc.levelHits}</td><td>${lc.ruleHits}</td>`;
            for (let j = 0; j < Solver.MAX_LAWS; j++) {
                txt += `<td>${lc.lawsHits[j]}</td>`;
            }
            txt += `<td>${lc.sum}</td></tr>\n`;
        }
        let totals = levelCounters[Solver.MAX_LEVELS];
        txt += `<tr><th>Total</th><td>${totals.levelHits}</td><td>${totals.ruleHits}</td>`;
        for (let j = 0; j < Solver.MAX_LAWS; j++) {
            txt += `<td>${totals.lawsHits[j]}</td>`;
        }
        txt += `<td>${totals.sum}</td></tr>\n</tbody>\n</table>\n`;
        return txt;
    }
    const txt = getStatsTable(stats.levelPairs, "Pairs") + "<br />\n" + getStatsTable(stats.levelMarks, "Marks");
    return txt;
}

UIX

The User Interface and eXperience (UIX) module for all pages. It has methods to show/hide the "fun" images that float on the right side of my pages.

The UIX Class

import { Verb } from "../puzzle/Verb.js";
import { Viewer } from "./Viewer.js";
import * as Locker from "./Locker.js";
export const getInputById = (id) => document.getElementById(id);
export const getButtonById = (id) => document.getElementById(id);
export const getDivById = (id) => document.getElementById(id);
export const getTableById = (id) => document.getElementById(id);
export function setField(fld, val) {
    switch (fld.type) {
        case "checkbox":
            if (val !== undefined)
                fld.checked = val;
            break;
        case "range":
            if (val !== undefined)
                fld.value = val;
            break;
    }
}
export function addStyle() {
    const style = document.createElement("style");
    document.head.appendChild(style);
    return style;
}
export function deleteStyleRules(style) {
    if (style === null)
        return;
    for (let i = style.sheet.cssRules.length - 1; i > -1; i--) {
        style.sheet.deleteRule(i);
    }
}
export let viewer = null;
export function setViewer() {
    viewer = Object.seal(new Viewer());
    console.log(`UIX.setViewer viewer="${viewer}"`);
}
export function setPuzzle(puzzle) {
    console.log(`UIX.setPuzzle puzzle="${puzzle}"`);
    viewer.setPuzzle(puzzle);
    console.log(`UIX.setPuzzle puzzle.valid=${puzzle.valid}`);
}
export function toggleFact(num) {
    console.log("UIX.toggleFact num=" + num);
    viewer === null || viewer === void 0 ? void 0 : viewer.toggleFact(num);
}
export function toggleRule(num) {
    console.log("UIX.toggleRule num=" + num);
    viewer === null || viewer === void 0 ? void 0 : viewer.toggleRule(num);
}
export function updateChartCol1(icol) {
    console.log("UIX.updateChartCol1 icol=" + icol);
    viewer === null || viewer === void 0 ? void 0 : viewer.updateChartCol1(icol);
}
export function getGridVerbFromLocker() {
    const num = Locker.getNumber("gridVerb", Verb.IsNot.num);
    return (typeof num !== "number" || num !== Verb.Is.num) ? Verb.IsNot : Verb.Is;
}
export function updateGridVerb() {
    let verb = getGridVerbFromLocker();
    console.log(`UIX.updateGridVerb verb=${verb}`);
    verb = verb === Verb.Is ? Verb.IsNot : Verb.Is;
    Locker.setValue("gridVerb", verb.num);
    const btn = getButtonById("btnGridVerb");
    if (btn)
        btn.innerHTML = verb.getCodeAsHtml();
}
export function undoUserMark() {
    console.log("UIX.undoUserMark");
    viewer === null || viewer === void 0 ? void 0 : viewer.undoUserMark();
}
export function clickGridCell(t1, n1, t2, n2, v) {
    console.log(`UNIX.clickGridCell t1=${t1} n1=${n1} t2=${t2} n2=${n2} v=${v}`);
    viewer === null || viewer === void 0 ? void 0 : viewer.clickGridCell(t1, n1, t2, n2, v);
}
const imgFunKey = "imgFun";
let imgFunStyle = null;
export function addImgFunRule(style, val) {
    if (style === null)
        return;
    style.sheet.insertRule(".imgFun { display: " + val + "; }", 0);
}
export function initImgFun() {
    const key = imgFunKey;
    const style = addStyle();
    let val = Locker.getString(key, "inline");
    if (val !== "none")
        val = "inline";
    console.log(`initImgFun val=${val}`);
    Locker.setValue(key, val);
    addImgFunRule(style, val);
    imgFunStyle = style;
}
export function toggleImgFun() {
    const key = imgFunKey;
    let style = imgFunStyle;
    let val = Locker.getString(key, "inline");
    if (val !== "inline")
        val = "none";
    val = (val === "inline" ? "none" : "inline");
    Locker.setValue(key, val);
    console.log(`toggleImgFun val=${val}`);
    if (style === null)
        style = addStyle();
    else
        deleteStyleRules(style);
    addImgFunRule(style, val);
    imgFunStyle = style;
}
export function setFieldFromLocker(fld, def) {
    if (!Locker.ok)
        return;
    switch (fld.type) {
        case "checkbox":
            def = Locker.getBoolean(fld.id, def);
            if (def !== undefined)
                fld.checked = def;
            break;
        case "range":
            def = Locker.getNumber(fld.id, def);
            if (def !== undefined)
                fld.value = def;
            break;
    }
}
export function putFieldInLocker(fld, val) {
    if (!Locker.ok)
        return;
    switch (fld.type) {
        case "checkbox":
            if (val !== undefined)
                fld.checked = val;
            Locker.setValue(fld.id, fld.checked);
            break;
        case "range":
            if (val !== undefined)
                fld.value = val;
            Locker.setValue(fld.id, fld.value);
            break;
    }
}

setTimeout

Here is an interesting article concerning setTimeout. To wit, JavaScript has a concurrency model based on an "event loop", and calling setTimeout adds a message to the queue. This technique avoids the dreaded "a script is taking too long to run" dialog.

Using setTimeout has its consequences - it turns a procedure-based program into an event-driven one. If the UI could be updated every time a mark was entered, this would not be a problem. But there is one thing we must take into account - the user may want the program to pause for certain events, like when a mark is entered. If we say that each event places one or more tasks into a queue, we need a method that can process each task in order. Below is a simplified version of the doTasks method found in the Viewer class.

		function doTasks() {
			if (numTasks === maxTasks) return;
			let data = tasks[numTasks++];

			// Perform the task based on the key.
			switch (data.key) {
			}

			// Exit if the pause flag is true. Call resumeTasks when the Resume button is pressed.
			if (pauseFlag) {
				board.pauseWork();
				return;
			}

			// Recursively call doTasks via setTimeout.
			setTimeout(function () { doTasks(); }, ms);
		}
		

Sharp-minded people will notice this is similar to the Producer-Consumer problem. After the consumer performs a task, it must see if it needs to wait. If the consumer does not need to wait, it simply moves on to the next task in the queue. But if the consumer needs to wait, instead of spinning CPU cycles in a loop until the user presses the Resume button, the consumer simply stops running. This is the Not Runnable state. Now the only way for the consumer to resume is to have the click event of the Resume button tell the consumer to "wake up" and put the consumer into the Runnable state. I'm probably mixing metaphors here, but you get the idea.

Pausing

The user may request the program to pause in the situations given below.

Board Buttons

Here are all of the possible states for the Work and Quit buttons. Note that a button's caption may change for a specific state. For simplicity, the following refers to a button by its current caption.

  1. When there is no solver or no puzzle, both buttons are disabled.
  2. When the puzzle is loaded, the Work button is enabled and says "Solve".
  3. When the Solve button is pressed, the Solve button says "Pause" and the Quit button is enabled.
  4. When the Pause button is pressed, the Pause button says "Resume".
  5. When the Resume button is pressed, the Resume button says "Pause".
  6. When the Quit button is pressed, the Pause button says "Solve" and the Quit button says "Reset".
  7. When the Reset button is pressed, the Reset button says "Quit" and is disabled.

Warning: For DOM objects with an id, most browsers will auto-create JavaScript variables with the id as its name! I don't like this because I do not want the viewer to access fields that are managed by subcomponents.

Conclusion

I hope you enjoyed reading this article. My motivation for writing this article is that you will try to model a logic puzzle on your own. Then together we can find better ways to model and/or solve logic puzzles. Thank you.