View a Logic Puzzle in JavaScript | ![]() |
---|
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.
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.
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.
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); } }
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.
<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.
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}`); }
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.