@johnmorrisdotca/kazu 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +568 -0
- package/dist/cells.d.ts +28 -0
- package/dist/cells.js +65 -0
- package/dist/check.d.ts +15 -0
- package/dist/check.js +189 -0
- package/dist/clock.d.ts +2 -0
- package/dist/clock.js +9 -0
- package/dist/conflicts.d.ts +14 -0
- package/dist/conflicts.js +79 -0
- package/dist/draw-entry.d.ts +14 -0
- package/dist/draw-entry.js +10 -0
- package/dist/draw.d.ts +47 -0
- package/dist/draw.js +219 -0
- package/dist/element-define.d.ts +1 -0
- package/dist/element-define.js +13 -0
- package/dist/element.d.ts +48 -0
- package/dist/element.js +150 -0
- package/dist/game.d.ts +65 -0
- package/dist/game.js +120 -0
- package/dist/generate.d.ts +11 -0
- package/dist/generate.js +35 -0
- package/dist/geometry.d.ts +33 -0
- package/dist/geometry.js +27 -0
- package/dist/givens.d.ts +30 -0
- package/dist/givens.js +43 -0
- package/dist/groupSolve.d.ts +68 -0
- package/dist/groupSolve.js +284 -0
- package/dist/hint.d.ts +39 -0
- package/dist/hint.js +158 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +31 -0
- package/dist/jigsaw.d.ts +21 -0
- package/dist/jigsaw.js +144 -0
- package/dist/kinds.d.ts +51 -0
- package/dist/kinds.js +40 -0
- package/dist/layout.d.ts +63 -0
- package/dist/layout.js +128 -0
- package/dist/moreOrLess.d.ts +7 -0
- package/dist/moreOrLess.js +90 -0
- package/dist/moreOrLessCode.d.ts +22 -0
- package/dist/moreOrLessCode.js +52 -0
- package/dist/moreOrLessSolve.d.ts +45 -0
- package/dist/moreOrLessSolve.js +180 -0
- package/dist/mount.d.ts +117 -0
- package/dist/mount.js +558 -0
- package/dist/names.d.ts +41 -0
- package/dist/names.js +163 -0
- package/dist/numberPlace.d.ts +14 -0
- package/dist/numberPlace.js +123 -0
- package/dist/play-entry.d.ts +9 -0
- package/dist/play-entry.js +8 -0
- package/dist/playStyle.d.ts +11 -0
- package/dist/playStyle.js +47 -0
- package/dist/progress.d.ts +29 -0
- package/dist/progress.js +96 -0
- package/dist/random.d.ts +25 -0
- package/dist/random.js +42 -0
- package/dist/solve.d.ts +22 -0
- package/dist/solve.js +60 -0
- package/dist/strings.d.ts +17 -0
- package/dist/strings.js +143 -0
- package/dist/style.d.ts +13 -0
- package/dist/style.js +61 -0
- package/dist/sumCages.d.ts +60 -0
- package/dist/sumCages.js +190 -0
- package/dist/towers.d.ts +3 -0
- package/dist/towers.js +48 -0
- package/dist/towersCode.d.ts +31 -0
- package/dist/towersCode.js +79 -0
- package/dist/towersSolve.d.ts +65 -0
- package/dist/towersSolve.js +276 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +104 -0
package/dist/mount.js
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { decodeCells, stepEntry, symbolOf, valueOfSymbol } from "./cells.js";
|
|
2
|
+
import { kazuClockText } from "./clock.js";
|
|
3
|
+
import { drawKazu, kazuCellName, kazuWhere } from "./draw.js";
|
|
4
|
+
import { answerOf, clearCell, enterNumber, gameConflicts, isPrinted, isSolved, kazuProgress, newKazuGame, numberCounts, restartKazu, toggleNote, undoKazu, valuesOf } from "./game.js";
|
|
5
|
+
import { hintKazu } from "./hint.js";
|
|
6
|
+
import { KAZU_PLAY_STYLE } from "./playStyle.js";
|
|
7
|
+
import { decodeNotes, decodeRun, encodeNotes, encodeRun, notesOf } from "./progress.js";
|
|
8
|
+
import { solveKazu } from "./solve.js";
|
|
9
|
+
import { kazuLanguageOf, kazuNameOf, kazuSay } from "./strings.js";
|
|
10
|
+
/** Put the style in the page once: in the document's head, or in the shadow root the host is in. */
|
|
11
|
+
export function ensureKazuPlayStyle(host) {
|
|
12
|
+
const root = host.getRootNode();
|
|
13
|
+
const target = typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot ? root : host.ownerDocument.head;
|
|
14
|
+
if (target.querySelector("style[data-kazu-play]") !== null)
|
|
15
|
+
return;
|
|
16
|
+
const style = host.ownerDocument.createElement("style");
|
|
17
|
+
style.setAttribute("data-kazu-play", "");
|
|
18
|
+
style.textContent = KAZU_PLAY_STYLE;
|
|
19
|
+
target.append(style);
|
|
20
|
+
}
|
|
21
|
+
function create(document, tag, className, text) {
|
|
22
|
+
const element = document.createElement(tag);
|
|
23
|
+
element.className = className;
|
|
24
|
+
if (text !== undefined)
|
|
25
|
+
element.textContent = text;
|
|
26
|
+
return element;
|
|
27
|
+
}
|
|
28
|
+
const ARROWS = { ArrowUp: [-1, 0], ArrowDown: [1, 0], ArrowLeft: [0, -1], ArrowRight: [0, 1] };
|
|
29
|
+
/** Draw a puzzle into `host` and play it. Returns the handle that drives it, or null for givens that are not a puzzle of that kind and side. */
|
|
30
|
+
export function mountKazu(host, options) {
|
|
31
|
+
const document = host.ownerDocument;
|
|
32
|
+
const view = document.defaultView;
|
|
33
|
+
const firstGame = start(options);
|
|
34
|
+
if (firstGame === null)
|
|
35
|
+
return null;
|
|
36
|
+
ensureKazuPlayStyle(host);
|
|
37
|
+
let game = firstGame;
|
|
38
|
+
let level = options.level;
|
|
39
|
+
let seed = options.seed;
|
|
40
|
+
let solution = options.solution ?? null;
|
|
41
|
+
let settings = {
|
|
42
|
+
hints: options.hints ?? "place",
|
|
43
|
+
check: options.check ?? "count",
|
|
44
|
+
conflicts: options.conflicts !== false,
|
|
45
|
+
peers: options.peers !== false,
|
|
46
|
+
tidy: options.tidy !== false,
|
|
47
|
+
tapToStep: options.tapToStep !== false,
|
|
48
|
+
clock: options.clock !== false,
|
|
49
|
+
controls: options.controls !== false,
|
|
50
|
+
};
|
|
51
|
+
const explicitLanguage = { value: options.language };
|
|
52
|
+
const callbacks = options;
|
|
53
|
+
let selected = null;
|
|
54
|
+
let pencilOn = false;
|
|
55
|
+
let hintCell = null;
|
|
56
|
+
let wrongCells = [];
|
|
57
|
+
let says = { key: "tap" };
|
|
58
|
+
let hints = 0;
|
|
59
|
+
let checks = 0;
|
|
60
|
+
let solved = isSolved(game);
|
|
61
|
+
let banked = options.elapsed ?? 0;
|
|
62
|
+
let ranSince = null;
|
|
63
|
+
let tick = null;
|
|
64
|
+
let language = explicitLanguage.value ?? languageOfPage();
|
|
65
|
+
let drawn = "";
|
|
66
|
+
let announced = "";
|
|
67
|
+
function languageOfPage() {
|
|
68
|
+
return kazuLanguageOf(host.closest("[lang]")?.getAttribute("lang") ?? document.documentElement.lang);
|
|
69
|
+
}
|
|
70
|
+
// The parts.
|
|
71
|
+
host.classList.add("kazu-play");
|
|
72
|
+
host.replaceChildren();
|
|
73
|
+
const bar = create(document, "div", "kzp-bar");
|
|
74
|
+
const clockEl = create(document, "span", "kzp-clock");
|
|
75
|
+
clockEl.setAttribute("role", "timer");
|
|
76
|
+
const progressEl = create(document, "span", "kzp-progress");
|
|
77
|
+
bar.append(clockEl, progressEl);
|
|
78
|
+
const box = create(document, "div", "kzp-box");
|
|
79
|
+
box.tabIndex = 0;
|
|
80
|
+
box.setAttribute("role", "application");
|
|
81
|
+
const inner = create(document, "div", "kzp-inner");
|
|
82
|
+
box.append(inner);
|
|
83
|
+
const keysNote = create(document, "p", "kzp-sr");
|
|
84
|
+
box.setAttribute("aria-describedby", `kzp-keys-${Math.random().toString(36).slice(2, 8)}`);
|
|
85
|
+
keysNote.id = box.getAttribute("aria-describedby");
|
|
86
|
+
const announce = create(document, "p", "kzp-sr");
|
|
87
|
+
announce.setAttribute("aria-live", "polite");
|
|
88
|
+
const pad = create(document, "div", "kzp-pad");
|
|
89
|
+
pad.setAttribute("role", "group");
|
|
90
|
+
const controls = create(document, "div", "kzp-controls");
|
|
91
|
+
const saysEl = create(document, "p", "kzp-says");
|
|
92
|
+
saysEl.setAttribute("aria-live", "polite");
|
|
93
|
+
host.append(bar, box, keysNote, announce, pad, controls, saysEl);
|
|
94
|
+
const button = (name, onPress, parent, className = "kzp-button") => {
|
|
95
|
+
const one = create(document, "button", className);
|
|
96
|
+
one.type = "button";
|
|
97
|
+
one.dataset.action = name;
|
|
98
|
+
one.addEventListener("click", onPress);
|
|
99
|
+
parent.append(one);
|
|
100
|
+
return one;
|
|
101
|
+
};
|
|
102
|
+
const undoButton = button("undo", () => api.undo(), controls);
|
|
103
|
+
const pencilButton = button("pencil", () => api.pencil(), controls);
|
|
104
|
+
const hintButton = button("hint", () => api.hint(), controls);
|
|
105
|
+
const checkButton = button("check", () => api.check(), controls);
|
|
106
|
+
let padKeys = [];
|
|
107
|
+
let eraseKey = null;
|
|
108
|
+
const say = (key, values = {}) => kazuSay(language, key, values);
|
|
109
|
+
function elapsed() {
|
|
110
|
+
return banked + (ranSince === null ? 0 : view.performance.now() - ranSince);
|
|
111
|
+
}
|
|
112
|
+
function runClock() {
|
|
113
|
+
if (ranSince !== null || solved || document.hidden)
|
|
114
|
+
return;
|
|
115
|
+
ranSince = view.performance.now();
|
|
116
|
+
if (tick === null)
|
|
117
|
+
tick = setInterval(paintClock, 1000);
|
|
118
|
+
}
|
|
119
|
+
function stopClock() {
|
|
120
|
+
if (ranSince !== null)
|
|
121
|
+
banked += view.performance.now() - ranSince;
|
|
122
|
+
ranSince = null;
|
|
123
|
+
if (tick !== null)
|
|
124
|
+
clearInterval(tick);
|
|
125
|
+
tick = null;
|
|
126
|
+
}
|
|
127
|
+
function paintClock() {
|
|
128
|
+
const text = kazuClockText(elapsed());
|
|
129
|
+
if (clockEl.textContent !== text)
|
|
130
|
+
clockEl.textContent = text;
|
|
131
|
+
}
|
|
132
|
+
// The pad: a key for every number, and one to erase. Its columns are fixed for a puzzle's side.
|
|
133
|
+
function buildPad() {
|
|
134
|
+
pad.replaceChildren();
|
|
135
|
+
padKeys = [];
|
|
136
|
+
for (let value = 1; value <= game.size; value += 1) {
|
|
137
|
+
const key = button(`number-${value}`, () => api.enter(value), pad, "kzp-key");
|
|
138
|
+
key.dataset.value = String(value);
|
|
139
|
+
key.textContent = symbolOf(value);
|
|
140
|
+
padKeys.push(key);
|
|
141
|
+
}
|
|
142
|
+
eraseKey = button("erase", () => api.enter(0), pad, "kzp-key");
|
|
143
|
+
eraseKey.dataset.erase = "true";
|
|
144
|
+
eraseKey.textContent = "×";
|
|
145
|
+
const rows = game.size <= 6 ? 1 : game.size <= 9 ? 2 : 3;
|
|
146
|
+
pad.style.setProperty("--kzp-columns", String(Math.ceil((game.size + 1) / rows)));
|
|
147
|
+
}
|
|
148
|
+
function detail() {
|
|
149
|
+
return {
|
|
150
|
+
kind: game.kind,
|
|
151
|
+
size: game.size,
|
|
152
|
+
level,
|
|
153
|
+
seed,
|
|
154
|
+
run: encodeRun(game.entries),
|
|
155
|
+
notes: encodeNotes(game.notes),
|
|
156
|
+
answer: answerOf(game),
|
|
157
|
+
progress: kazuProgress(game),
|
|
158
|
+
elapsedMs: Math.round(elapsed()),
|
|
159
|
+
hints,
|
|
160
|
+
checks,
|
|
161
|
+
helped: hints > 0,
|
|
162
|
+
solved,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function emit(name, extra = {}) {
|
|
166
|
+
const info = { ...detail(), ...extra };
|
|
167
|
+
host.dispatchEvent(new CustomEvent(name, { detail: info, bubbles: true }));
|
|
168
|
+
const call = { "kazu-change": callbacks.onChange, "kazu-hint": callbacks.onHint, "kazu-check": callbacks.onCheck, "kazu-solve": callbacks.onSolve }[name];
|
|
169
|
+
call?.(info);
|
|
170
|
+
}
|
|
171
|
+
function answerKey() {
|
|
172
|
+
if (solution === null)
|
|
173
|
+
solution = solveKazu(game.kind, game.size, game.givens);
|
|
174
|
+
return solution;
|
|
175
|
+
}
|
|
176
|
+
function sayText() {
|
|
177
|
+
return says.text ?? kazuSay(language, says.key, says.values ?? {});
|
|
178
|
+
}
|
|
179
|
+
function cellDescription(index) {
|
|
180
|
+
const cell = kazuCellName(game.size, index, language);
|
|
181
|
+
const shown = valuesOf(game)[index];
|
|
182
|
+
if (isPrinted(game, index))
|
|
183
|
+
return say("cellGiven", { cell, value: symbolOf(shown) });
|
|
184
|
+
if (shown !== 0)
|
|
185
|
+
return say(gameConflicts(game).includes(index) ? "cellConflict" : "cellValue", { cell, value: symbolOf(shown) });
|
|
186
|
+
const notes = notesOf(game.notes[index]);
|
|
187
|
+
return notes.length > 0 ? say("cellNotes", { cell, notes: notes.map(symbolOf).join(", ") }) : say("cellEmpty", { cell });
|
|
188
|
+
}
|
|
189
|
+
function render() {
|
|
190
|
+
const conflicts = settings.conflicts ? gameConflicts(game) : [];
|
|
191
|
+
const svg = drawKazu(game.kind, game.size, game.givens, {
|
|
192
|
+
entries: game.entries,
|
|
193
|
+
notes: game.notes,
|
|
194
|
+
selected,
|
|
195
|
+
peers: settings.peers,
|
|
196
|
+
conflicts,
|
|
197
|
+
wrong: wrongCells,
|
|
198
|
+
hint: hintCell,
|
|
199
|
+
done: solved,
|
|
200
|
+
interactive: !solved,
|
|
201
|
+
language,
|
|
202
|
+
});
|
|
203
|
+
if (svg !== null && svg !== drawn) {
|
|
204
|
+
inner.innerHTML = svg;
|
|
205
|
+
drawn = svg;
|
|
206
|
+
}
|
|
207
|
+
box.dataset.over = String(solved);
|
|
208
|
+
host.dataset.solved = String(solved);
|
|
209
|
+
host.dataset.pencil = String(pencilOn);
|
|
210
|
+
host.dataset.kind = game.kind;
|
|
211
|
+
host.dataset.size = String(game.size);
|
|
212
|
+
box.setAttribute("aria-label", say("board", { name: kazuNameOf(game.kind, language), size: game.size }));
|
|
213
|
+
keysNote.textContent = say("keys");
|
|
214
|
+
const counts = numberCounts(game);
|
|
215
|
+
padKeys.forEach((key, at) => {
|
|
216
|
+
const value = at + 1;
|
|
217
|
+
key.disabled = solved;
|
|
218
|
+
key.dataset.done = String(counts[value] >= game.size);
|
|
219
|
+
key.setAttribute("aria-label", say(counts[value] >= game.size ? "padDone" : "padNumber", { n: symbolOf(value) }));
|
|
220
|
+
});
|
|
221
|
+
if (eraseKey !== null) {
|
|
222
|
+
eraseKey.disabled = solved;
|
|
223
|
+
eraseKey.setAttribute("aria-label", say("erase"));
|
|
224
|
+
}
|
|
225
|
+
pad.setAttribute("aria-label", say("pad"));
|
|
226
|
+
undoButton.textContent = say("undo");
|
|
227
|
+
undoButton.disabled = game.past.length === 0 || solved;
|
|
228
|
+
pencilButton.textContent = say("pencil");
|
|
229
|
+
pencilButton.title = say("pencilTitle");
|
|
230
|
+
pencilButton.setAttribute("aria-pressed", String(pencilOn));
|
|
231
|
+
pencilButton.disabled = solved;
|
|
232
|
+
hintButton.textContent = say("hint");
|
|
233
|
+
hintButton.title = say("hintTitle");
|
|
234
|
+
hintButton.hidden = settings.hints === "off";
|
|
235
|
+
hintButton.disabled = solved;
|
|
236
|
+
checkButton.textContent = say("check");
|
|
237
|
+
checkButton.title = say("checkTitle");
|
|
238
|
+
checkButton.hidden = settings.check === "off";
|
|
239
|
+
checkButton.disabled = solved;
|
|
240
|
+
controls.hidden = !settings.controls;
|
|
241
|
+
pad.hidden = !settings.controls;
|
|
242
|
+
clockEl.hidden = !settings.clock;
|
|
243
|
+
clockEl.setAttribute("aria-label", say("clock"));
|
|
244
|
+
const progress = kazuProgress(game);
|
|
245
|
+
progressEl.textContent = `${progress.filled} / ${progress.total}`;
|
|
246
|
+
paintClock();
|
|
247
|
+
const text = solved ? say(banked > 0 || ranSince !== null ? "solvedIn" : "solved", { time: kazuClockText(elapsed()) }) : sayText();
|
|
248
|
+
saysEl.textContent = text;
|
|
249
|
+
saysEl.dataset.warn = String(says.warn === true && !solved);
|
|
250
|
+
const note = selected === null ? "" : cellDescription(selected);
|
|
251
|
+
if (note !== announced) {
|
|
252
|
+
announced = note;
|
|
253
|
+
announce.textContent = note;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function setSays(key, values, warn = false, text) {
|
|
257
|
+
says = { key, values, warn, text };
|
|
258
|
+
}
|
|
259
|
+
function tapWords() {
|
|
260
|
+
setSays(pencilOn ? "tapPencil" : "tap");
|
|
261
|
+
}
|
|
262
|
+
function changed() {
|
|
263
|
+
hintCell = null;
|
|
264
|
+
wrongCells = [];
|
|
265
|
+
solved = isSolved(game);
|
|
266
|
+
if (solved) {
|
|
267
|
+
stopClock();
|
|
268
|
+
setSays("solved");
|
|
269
|
+
}
|
|
270
|
+
else if (kazuProgress(game).filled === game.size * game.size)
|
|
271
|
+
setSays("full", undefined, true);
|
|
272
|
+
else
|
|
273
|
+
tapWords();
|
|
274
|
+
render();
|
|
275
|
+
emit("kazu-change");
|
|
276
|
+
if (solved)
|
|
277
|
+
emit("kazu-solve");
|
|
278
|
+
}
|
|
279
|
+
function write(cell, value) {
|
|
280
|
+
if (solved || cell < 0 || isPrinted(game, cell))
|
|
281
|
+
return;
|
|
282
|
+
const next = pencilOn && value !== 0 ? toggleNote(game, cell, value) : value === 0 ? clearCell(game, cell) : enterNumber(game, cell, value, settings.tidy);
|
|
283
|
+
if (next === game)
|
|
284
|
+
return;
|
|
285
|
+
runClock();
|
|
286
|
+
game = next;
|
|
287
|
+
changed();
|
|
288
|
+
}
|
|
289
|
+
function move(by) {
|
|
290
|
+
const { size } = game;
|
|
291
|
+
const from = selected ?? 0;
|
|
292
|
+
const row = Math.min(size - 1, Math.max(0, Math.floor(from / size) + by[0]));
|
|
293
|
+
const col = Math.min(size - 1, Math.max(0, (from % size) + by[1]));
|
|
294
|
+
api.select(row * size + col);
|
|
295
|
+
}
|
|
296
|
+
const onClick = (event) => {
|
|
297
|
+
const target = event.target?.closest("[data-cell]");
|
|
298
|
+
if (target === null || target === undefined || solved)
|
|
299
|
+
return;
|
|
300
|
+
const index = Number(target.dataset.cell);
|
|
301
|
+
if (!Number.isInteger(index))
|
|
302
|
+
return;
|
|
303
|
+
if (index === selected && settings.tapToStep && !pencilOn && !isPrinted(game, index))
|
|
304
|
+
write(index, stepEntry(game.entries[index], game.size));
|
|
305
|
+
else
|
|
306
|
+
api.select(index);
|
|
307
|
+
};
|
|
308
|
+
const onKey = (event) => {
|
|
309
|
+
if (event.metaKey || event.ctrlKey || event.altKey) {
|
|
310
|
+
if ((event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === "z") {
|
|
311
|
+
event.preventDefault();
|
|
312
|
+
api.undo();
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (event.key in ARROWS) {
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
move(ARROWS[event.key]);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (solved)
|
|
322
|
+
return;
|
|
323
|
+
if (event.key === "Escape") {
|
|
324
|
+
event.preventDefault();
|
|
325
|
+
api.select(null);
|
|
326
|
+
}
|
|
327
|
+
else if (event.key === "Backspace" || event.key === "Delete" || event.key === "0" || event.key === " ") {
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
api.enter(0);
|
|
330
|
+
}
|
|
331
|
+
else if (event.key.toLowerCase() === "n") {
|
|
332
|
+
event.preventDefault();
|
|
333
|
+
api.pencil();
|
|
334
|
+
}
|
|
335
|
+
else if (event.shiftKey && selected !== null) {
|
|
336
|
+
// Shift with a number is a pencil mark, whatever the keyboard writes for a shifted digit: read the key's place, not its symbol.
|
|
337
|
+
const code = /^Digit([1-9])$/.exec(event.code)?.[1] ?? /^Key([A-G])$/.exec(event.code)?.[1];
|
|
338
|
+
const value = code === undefined ? 0 : valueOfSymbol(code);
|
|
339
|
+
if (value >= 1 && value <= game.size) {
|
|
340
|
+
event.preventDefault();
|
|
341
|
+
write(selected, 0);
|
|
342
|
+
const next = toggleNote(game, selected, value);
|
|
343
|
+
if (next !== game) {
|
|
344
|
+
runClock();
|
|
345
|
+
game = next;
|
|
346
|
+
changed();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
const value = valueOfSymbol(event.key);
|
|
352
|
+
if (value >= 1 && value <= game.size) {
|
|
353
|
+
event.preventDefault();
|
|
354
|
+
api.enter(value);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const onFocus = () => {
|
|
359
|
+
if (selected === null && !solved)
|
|
360
|
+
api.select(0);
|
|
361
|
+
};
|
|
362
|
+
const onVisible = () => {
|
|
363
|
+
if (document.hidden) {
|
|
364
|
+
if (ranSince !== null) {
|
|
365
|
+
stopClock();
|
|
366
|
+
paintClock();
|
|
367
|
+
resumeOnShow = true;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else if (resumeOnShow) {
|
|
371
|
+
resumeOnShow = false;
|
|
372
|
+
runClock();
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
let resumeOnShow = false;
|
|
376
|
+
box.addEventListener("click", onClick);
|
|
377
|
+
box.addEventListener("keydown", onKey);
|
|
378
|
+
box.addEventListener("focus", onFocus);
|
|
379
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
380
|
+
const watching = typeof MutationObserver === "undefined"
|
|
381
|
+
? null
|
|
382
|
+
: new MutationObserver(() => {
|
|
383
|
+
if (explicitLanguage.value !== undefined)
|
|
384
|
+
return;
|
|
385
|
+
const next = languageOfPage();
|
|
386
|
+
if (next === language)
|
|
387
|
+
return;
|
|
388
|
+
language = next;
|
|
389
|
+
drawn = "";
|
|
390
|
+
render();
|
|
391
|
+
});
|
|
392
|
+
watching?.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
|
|
393
|
+
buildPad();
|
|
394
|
+
render();
|
|
395
|
+
const api = {
|
|
396
|
+
host,
|
|
397
|
+
game: () => game,
|
|
398
|
+
detail,
|
|
399
|
+
load: (puzzle) => {
|
|
400
|
+
const next = start(puzzle);
|
|
401
|
+
if (next === null)
|
|
402
|
+
return false;
|
|
403
|
+
stopClock();
|
|
404
|
+
game = next;
|
|
405
|
+
solution = puzzle.solution ?? null;
|
|
406
|
+
level = puzzle.level;
|
|
407
|
+
seed = puzzle.seed;
|
|
408
|
+
banked = puzzle.elapsed ?? 0;
|
|
409
|
+
selected = null;
|
|
410
|
+
pencilOn = false;
|
|
411
|
+
hints = 0;
|
|
412
|
+
checks = 0;
|
|
413
|
+
hintCell = null;
|
|
414
|
+
wrongCells = [];
|
|
415
|
+
solved = isSolved(game);
|
|
416
|
+
drawn = "";
|
|
417
|
+
setSays("tap");
|
|
418
|
+
buildPad();
|
|
419
|
+
render();
|
|
420
|
+
return true;
|
|
421
|
+
},
|
|
422
|
+
set: (changes) => {
|
|
423
|
+
const { language: nextLanguage, ...rest } = changes;
|
|
424
|
+
settings = { ...settings, ...Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined)) };
|
|
425
|
+
if ("language" in changes) {
|
|
426
|
+
explicitLanguage.value = nextLanguage;
|
|
427
|
+
language = nextLanguage ?? languageOfPage();
|
|
428
|
+
}
|
|
429
|
+
drawn = "";
|
|
430
|
+
render();
|
|
431
|
+
},
|
|
432
|
+
select: (cell) => {
|
|
433
|
+
selected = cell !== null && cell >= 0 && cell < game.size * game.size ? cell : null;
|
|
434
|
+
if (hintCell !== null && hintCell !== selected)
|
|
435
|
+
hintCell = null;
|
|
436
|
+
render();
|
|
437
|
+
},
|
|
438
|
+
enter: (value) => {
|
|
439
|
+
if (selected !== null)
|
|
440
|
+
write(selected, value);
|
|
441
|
+
},
|
|
442
|
+
pencil: (on) => {
|
|
443
|
+
pencilOn = on ?? !pencilOn;
|
|
444
|
+
if (!solved)
|
|
445
|
+
tapWords();
|
|
446
|
+
render();
|
|
447
|
+
},
|
|
448
|
+
undo: () => {
|
|
449
|
+
const next = undoKazu(game);
|
|
450
|
+
if (next === game || solved)
|
|
451
|
+
return;
|
|
452
|
+
game = next;
|
|
453
|
+
changed();
|
|
454
|
+
},
|
|
455
|
+
hint: () => {
|
|
456
|
+
if (solved || settings.hints === "off")
|
|
457
|
+
return;
|
|
458
|
+
const found = hintKazu(game.kind, game.size, game.givens, game.entries, answerKey() ?? undefined);
|
|
459
|
+
if (found === null) {
|
|
460
|
+
setSays("hintDone");
|
|
461
|
+
render();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
hints += 1;
|
|
465
|
+
selected = found.cell;
|
|
466
|
+
const words = hintWords(found);
|
|
467
|
+
if (settings.hints === "place") {
|
|
468
|
+
runClock();
|
|
469
|
+
game = enterNumber(game, found.cell, found.value, settings.tidy);
|
|
470
|
+
changed();
|
|
471
|
+
if (!solved)
|
|
472
|
+
setSays("", undefined, false, words);
|
|
473
|
+
hintCell = found.cell;
|
|
474
|
+
render();
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
hintCell = found.cell;
|
|
478
|
+
wrongCells = [];
|
|
479
|
+
setSays("", undefined, false, words);
|
|
480
|
+
render();
|
|
481
|
+
}
|
|
482
|
+
emit("kazu-hint", { hint: found });
|
|
483
|
+
},
|
|
484
|
+
check: () => {
|
|
485
|
+
if (solved || settings.check === "off")
|
|
486
|
+
return;
|
|
487
|
+
checks += 1;
|
|
488
|
+
const answer = decodeCells(answerKey() ?? "", game.size);
|
|
489
|
+
const wrong = [];
|
|
490
|
+
const empty = game.entries.filter((value, index) => !isPrinted(game, index) && value === 0).length;
|
|
491
|
+
if (answer !== null)
|
|
492
|
+
game.entries.forEach((value, index) => (!isPrinted(game, index) && value !== 0 && value !== answer[index] ? wrong.push(index) : undefined));
|
|
493
|
+
else
|
|
494
|
+
wrong.push(...gameConflicts(game).filter((index) => !isPrinted(game, index)));
|
|
495
|
+
wrongCells = settings.check === "show" ? wrong : [];
|
|
496
|
+
hintCell = null;
|
|
497
|
+
if (wrong.length === 0 && empty === 0)
|
|
498
|
+
setSays("checkRight");
|
|
499
|
+
else if (wrong.length === 0)
|
|
500
|
+
setSays("checkOnlyEmpty", { empty });
|
|
501
|
+
else if (empty === 0)
|
|
502
|
+
setSays("checkWrongNoneEmpty", { wrong: wrong.length }, true);
|
|
503
|
+
else
|
|
504
|
+
setSays("checkWrong", { wrong: wrong.length, empty }, true);
|
|
505
|
+
render();
|
|
506
|
+
emit("kazu-check");
|
|
507
|
+
},
|
|
508
|
+
restart: () => {
|
|
509
|
+
stopClock();
|
|
510
|
+
game = restartKazu(game);
|
|
511
|
+
banked = 0;
|
|
512
|
+
hints = 0;
|
|
513
|
+
checks = 0;
|
|
514
|
+
selected = null;
|
|
515
|
+
hintCell = null;
|
|
516
|
+
wrongCells = [];
|
|
517
|
+
solved = isSolved(game);
|
|
518
|
+
setSays("tap");
|
|
519
|
+
render();
|
|
520
|
+
emit("kazu-change");
|
|
521
|
+
},
|
|
522
|
+
destroy: () => {
|
|
523
|
+
stopClock();
|
|
524
|
+
box.removeEventListener("click", onClick);
|
|
525
|
+
box.removeEventListener("keydown", onKey);
|
|
526
|
+
box.removeEventListener("focus", onFocus);
|
|
527
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
528
|
+
watching?.disconnect();
|
|
529
|
+
host.replaceChildren();
|
|
530
|
+
host.classList.remove("kazu-play");
|
|
531
|
+
for (const name of ["solved", "pencil", "kind", "size"])
|
|
532
|
+
delete host.dataset[name];
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
/** The line a hint says, in the board's language. */
|
|
536
|
+
function hintWords(found) {
|
|
537
|
+
const cell = kazuCellName(game.size, found.cell, language);
|
|
538
|
+
const value = symbolOf(found.value);
|
|
539
|
+
const lead = found.replaces ? say("hintReplaces", { cell }) : "";
|
|
540
|
+
if (found.why === "answer")
|
|
541
|
+
return lead + say("hintAnswer", { cell, value });
|
|
542
|
+
if (found.why === "only-number") {
|
|
543
|
+
const where = kazuWhere(game.kind, language);
|
|
544
|
+
return lead + (found.by === undefined ? say("hintOnly", { cell, value, where }) : say("hintOnlyBy", { cell, value, where, by: say(found.by === "cage" ? "byCage" : found.by === "marks" ? "byMarks" : "byClues") }));
|
|
545
|
+
}
|
|
546
|
+
const group = found.group;
|
|
547
|
+
const diagonal = group.type === "diagonal" ? say(group.index === 0 ? "groupDiagonal" : "groupDiagonalOther") : "";
|
|
548
|
+
const named = group.type === "diagonal" ? diagonal : say(`group${group.type[0].toUpperCase()}${group.type.slice(1)}`, { n: group.index + 1 });
|
|
549
|
+
return lead + say("hintPlace", { group: named, value, cell });
|
|
550
|
+
}
|
|
551
|
+
return api;
|
|
552
|
+
}
|
|
553
|
+
/** The game a set of options starts from, or null for givens that are not a puzzle. */
|
|
554
|
+
function start(options) {
|
|
555
|
+
const entries = options.run === undefined ? undefined : (decodeRun(options.run, options.size) ?? undefined);
|
|
556
|
+
const notes = options.notes === undefined ? undefined : (decodeNotes(options.notes, options.size) ?? undefined);
|
|
557
|
+
return newKazuGame(options.kind, options.size, options.givens, { entries, notes });
|
|
558
|
+
}
|
package/dist/names.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { KazuKind } from "./kinds.ts";
|
|
2
|
+
/**
|
|
3
|
+
* WHAT EACH PUZZLE IS CALLED, and what it is, in English and Japanese: the name a player searches for,
|
|
4
|
+
* its Japanese name, a line saying how it is played, and a few lines of rules. Plain data, for a page
|
|
5
|
+
* that lists the puzzles (a chooser, a rules page); `strings.ts` has the words a playable board says.
|
|
6
|
+
*
|
|
7
|
+
* The English names are the ones players search for: Sudoku, Killer Sudoku, Futoshiki, Skyscrapers.
|
|
8
|
+
* 数独 is Nikoli's mark in Japan, so Sudoku's Japanese name here is ナンプレ (Number Place, the puzzle's
|
|
9
|
+
* own original name, and the word Japanese publishers use); the others keep names of their own. The
|
|
10
|
+
* keys the package uses are `number-place`, `sum-cages`, `more-or-less`, `towers`: what each one is, not
|
|
11
|
+
* somebody's trademark.
|
|
12
|
+
*/
|
|
13
|
+
export type KazuName = {
|
|
14
|
+
/** The name players search for. */
|
|
15
|
+
en: string;
|
|
16
|
+
/** The Japanese name. */
|
|
17
|
+
ja: string;
|
|
18
|
+
/** Other names the same puzzle goes by. */
|
|
19
|
+
alsoKnownAs: readonly string[];
|
|
20
|
+
/** How it is played, in a line. */
|
|
21
|
+
tagline: {
|
|
22
|
+
en: string;
|
|
23
|
+
ja: string;
|
|
24
|
+
};
|
|
25
|
+
/** The rules, a few lines each. */
|
|
26
|
+
rules: {
|
|
27
|
+
en: readonly string[];
|
|
28
|
+
ja: readonly string[];
|
|
29
|
+
};
|
|
30
|
+
/** Where it comes from, in a sentence. */
|
|
31
|
+
origin: {
|
|
32
|
+
en: string;
|
|
33
|
+
ja: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export declare const KAZU_NAMES: Record<KazuKind, KazuName>;
|
|
37
|
+
/** What each side is for, under its size on a chooser: the quick one, the usual one, the long one. */
|
|
38
|
+
export declare const KAZU_SIZE_NAMES: Record<KazuKind, Record<number, {
|
|
39
|
+
en: string;
|
|
40
|
+
ja: string;
|
|
41
|
+
}>>;
|