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 * 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); } }
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 * 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}`); }
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.
Pauses
AllLevel Solution Violation Mark Trigger Guess Placer |
General
AutorunRechart Rules Triggers |
Levels
AllLevel 1 Level 2 Level 3 Level 4 |
Laws
AllLaw 1 Law 2 Law 3 Law 4 Law 5 |
Tabby is initially displayed via the tabby.php
file.
<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>
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.
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.
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!"); } } ; }
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.
Pauses
AllLevel Solution Violation Mark Trigger Guess Placer |
General
AutorunRechart Rules Triggers |
Levels
AllLevel 1 Level 2 Level 3 Level 4 |
Laws
AllLaw 1 Law 2 Law 3 Law 4 Law 5 |
The Setup form is initially displayed via the setup.php
file.
<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>
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`); }
The Locker
module is a wrapper for HTML 5 Storage.
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}`);
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.
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> </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}"> </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; }
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.
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; } }
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.
The user may request the program to pause in the situations given below.
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.
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.
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.