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 { 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 { UIX } from "./UIX.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.puzzleNames = [];
        this.puzzleNum = -1;
        this.okAllPuzzles = false;
        this.okSavePuzzle = false;
        this.okSaveSolved = false;
        console.log(`exec viewer 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}"`);
        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}"`);
    }
    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;
        this.okPauseNext = false;
        (_a = this.puzzle) === null || _a === void 0 ? void 0 : _a.reset();
        this.solver.reset();
        this.board.reset();
        this.tabby.reset();
    }
    setPuzzle(puzzle) {
        this.puzzle = puzzle;
        if (this.puzzle !== null) {
            this.puzzle.validate();
            if (!this.puzzle.valid)
                this.puzzle = null;
        }
        this.solver.setPuzzle(this.puzzle);
        this.board.setPuzzle(this.puzzle);
        this.tabby.setPuzzle(this.puzzle);
        this.reset();
        if (this.puzzle !== null && this.okAutorun)
            this.doWork();
    }
    doWork() {
        console.log(`exec viewer.doWork puzzle="${this.puzzle}"`);
        this.reset();
        this.board.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) { this.tabby.updateChartCol1(icol); }
    undoUserMark() { this.solver.undoUserMark(); }
    sayMessage(msg) {
        this.board.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) {
        this.sayMessage(msg);
        this.okPauseNext = false;
        if (okPause)
            this.board.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) {
        const okPause = false;
        console.log(`viewer.sayStopped elapsedTime=${elapsedTime}`);
        this.board.sayStopped();
        this.sayPause(SayEvent.Stopped, msg, okPause);
    }
    sayLevel(msg) {
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseLevel;
        this.board.sayLevel(msg);
        this.sayPause(SayEvent.Level, "Level " + msg, okPause);
    }
    saySolution(msg, elapsedTime) {
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseSolution;
        console.log(`viewer.saySolution elapsedTime=${elapsedTime}`);
        console.log(Helper.getChartAsText(this.puzzle, 0, true));
        this.tabby.saySolution(elapsedTime);
        this.sayPause(SayEvent.Solution, msg, okPause);
    }
    sayAddMark(mark) {
        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);
        this.board.sayMark();
        this.tabby.sayMark(mark, 1);
        this.sayPause(SayEvent.AddMark, mark.reason, okPause);
    }
    sayRemoveMark(msg, mark) {
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseMark || (mark.hasPlacers() && this.okPausePlacers);
        this.board.sayMark();
        this.tabby.sayMark(mark, -1);
        this.sayPause(SayEvent.RemoveMark, msg, okPause);
    }
    sayValidMark(msg, mark) {
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseMark;
        mark.valid = true;
        this.tabby.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) {
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseViolation;
        this.board.sayFactViolation();
        this.tabby.sayFactViolation(fact);
        this.sayPause(SayEvent.FactViolation, msg, okPause);
    }
    sayRuleViolation(msg, mark, rule) {
        const okPause = this.okPauseNext || this.okPauseAll || this.okPauseViolation;
        this.board.sayRuleViolation();
        this.tabby.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) {
        const okPause = this.okPauseNext || this.okPauseAll || this.okPausePlacers;
        const msg = mark.getRulePlacersMsg(rule);
        this.tabby.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 { 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