domma-cms 1.8.32 → 1.8.33

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.
@@ -12,6 +12,11 @@
12
12
  * Writes go one at a time (`withLock`), so two answers sent at once cannot
13
13
  * both be marked.
14
14
  *
15
+ * A TEST play (`test: true`) is an admin trying the game from its Tool. It
16
+ * plays exactly like any other, but it is nobody's go: it never counts for a
17
+ * board, a place, the goes-per-name limit, the badge or the admin's list of
18
+ * plays, and the next test of the same game clears it away.
19
+ *
15
20
  * @module _lib/games/plays
16
21
  */
17
22
 
@@ -53,7 +58,13 @@ export class PlayStore {
53
58
 
54
59
  forget() { this.cache = null; }
55
60
 
61
+ /** The real plays of a game - test plays left out. */
56
62
  async forGame(gameId) {
63
+ return (await this.all()).filter(p => p.gameId === gameId && p.test !== true);
64
+ }
65
+
66
+ /** Every play of a game, tests included. */
67
+ async everyForGame(gameId) {
57
68
  return (await this.all()).filter(p => p.gameId === gameId);
58
69
  }
59
70
 
@@ -81,17 +92,18 @@ export class PlayStore {
81
92
  }
82
93
 
83
94
  /**
84
- * @param {{gameId: string, name: string, maxScore?: number, state?: object, userId?: string}} play
95
+ * @param {{gameId: string, name: string, maxScore?: number, state?: object, userId?: string, test?: boolean}} play
96
+ * `test`: an admin trying the game - see the module comment
85
97
  * @returns {Promise<object>} the play, token included - for the player, once
86
98
  */
87
- async start({gameId, name, maxScore = 0, state = {}, userId = ''}) {
99
+ async start({gameId, name, maxScore = 0, state = {}, userId = '', test = false}) {
88
100
  return this.withLock(async () => {
89
101
  const now = new Date().toISOString();
90
102
  const saved = await createEntry(this.slug, {
91
103
  gameId, name, nameKey: nameKey(name), userId,
92
104
  status: 'playing', startedAt: now, finishedAt: '', timeMs: 0, score: 0, maxScore,
93
- hidden: false, token: randomBytes(24).toString('base64url'), state
94
- }, {source: 'public'});
105
+ hidden: false, ...(test ? {test: true} : {}), token: randomBytes(24).toString('base64url'), state
106
+ }, {source: test ? 'admin' : 'public'});
95
107
  this.forget();
96
108
  return flat(saved);
97
109
  });
@@ -129,12 +141,21 @@ export class PlayStore {
129
141
  this.forget();
130
142
  }
131
143
 
132
- /** Every play of a game gone: a fresh board. */
144
+ /** Every play of a game gone: a fresh board. Answers how many real plays went. */
133
145
  async reset(gameId) {
134
- const mine = await this.forGame(gameId);
146
+ const mine = await this.everyForGame(gameId);
135
147
  await this.withLock(async () => { for (const p of mine) await deleteEntry(this.slug, p.id).catch(() => {}); });
136
148
  this.forget();
137
- return mine.length;
149
+ return mine.filter(p => p.test !== true).length;
150
+ }
151
+
152
+ /** A game's test plays gone (before the next test starts). */
153
+ async clearTests(gameId) {
154
+ const tests = (await this.everyForGame(gameId)).filter(p => p.test === true);
155
+ if (!tests.length) return 0;
156
+ await this.withLock(async () => { for (const p of tests) await deleteEntry(this.slug, p.id).catch(() => {}); });
157
+ this.forget();
158
+ return tests.length;
138
159
  }
139
160
 
140
161
  async board(gameId, {period = 'all', limit = 10} = {}) {
@@ -0,0 +1,37 @@
1
+ # crossword - AI Assistant Guide
2
+
3
+ ## Purpose
4
+
5
+ Crosswords on the public site, built from a word + clue list, timed by the server, with a fastest-solver
6
+ leaderboard. 1.1.0 (2026-10-21), `tier: free`, disabled on arrival. Sibling of `quiz`; shares `plugins/_lib/games`.
7
+
8
+ ## Files
9
+
10
+ - `public/lib/layout.js` - PURE, isomorphic, SERVED: `generate(words, {seed})` (greedy, deterministic from the
11
+ seed, clues never affect the layout), `cleanAnswer` (letters + enumeration), `solutionGrid`, `shapeOf`. The admin
12
+ builder imports it over HTTP for the live preview; the server runs the same function on save, so a layout is
13
+ never taken from the browser. It holds no puzzle, which is why it may live under `public/`.
14
+ - `lib/crossword.js` - PURE: `cleanPuzzle`, `isOpen`, `publicPuzzle` (shape + clues, no letters), `readCells`,
15
+ `compare`, `catchUp`, `finish`, `view`.
16
+ - `lib/store.js` - `crossword-puzzles` + `plays = new PlayStore('crossword-plays', {order: 'time'})`.
17
+ - `plugin.js` - routes (header comment), shortcodes, the `fastest` notification source.
18
+ - `public/crossword.js` - the player: the screens are domma-reactive; the grid is a `Board` class over real
19
+ `<input>`s (keyboard rules in its header - all focus and keystrokes, which bindings do not reach), letters
20
+ remembered per play in storage. Exports `mount(el, {test})`, which the admin's Test screen uses.
21
+ - `admin/views/crossword.js` - list, editor with live grid, test and plays screens, all domma-reactive through the
22
+ admin kit (`_lib/admin/games/kit.js`). The preview grid is a data-each over cells.
23
+
24
+ ## Contracts
25
+
26
+ - **No letters before solving.** `/public/crossword/:slug` gives only the size; the clues come with the play (so
27
+ nobody studies them off the clock); `solution` is in `/result` only when `status === 'finished'`. The tests
28
+ assert no answer string appears in the play payload - keep that test.
29
+ - **Time = server elapsed + `state.penaltyMs`.** Check adds `checkPenalty` seconds (403 when `checks` is off); a
30
+ FULL but wrong submit adds `wrongPenalty`; a grid with empties only reports the count.
31
+ - Cells travel as `height` strings of `width` chars, `A-Z` or space; anything else is 400.
32
+ - The grid uses fixed ink/paper colours (`--cw-ink`, `--cw-paper`) whatever the theme - a themed grid inverted
33
+ under dark themes.
34
+ - Same shortcode prefix trap as quiz: `crossword-leaderboard` is registered with `{priority: 5}`.
35
+ - **Test plays** (1.1.0): `GET/POST /puzzles/:id/test` (manage), as in quiz's CLAUDE.md - the saved crossword
36
+ whatever its status, never on a board, not in the plays list, no notification, cleared by the next test.
37
+ `/result` answers `test: true` and `wouldPlace`.
@@ -1 +1,18 @@
1
- .cwa-words{display:flex;flex-direction:column;gap:.4rem}.cwa-word{display:grid;grid-template-columns:1.6rem minmax(7rem,12rem) 3.5rem minmax(0,1fr) auto;gap:.4rem;align-items:center}.cwa-n{color:var(--dm-text-muted);font-size:var(--dm-text-sm);text-align:right}.cwa-answer{text-transform:uppercase;font-family:var(--dm-font-mono, monospace);letter-spacing:.04em}.cwa-enum{color:var(--dm-text-muted);font-size:var(--dm-text-xs);white-space:nowrap}@media(max-width:700px){.cwa-word{grid-template-columns:1.4rem 1fr auto}.cwa-enum{display:none}.cwa-clue{grid-column:2 / 4}}.cwa-side{position:sticky;top:.5rem}.cwa-preview{overflow:auto;max-height:70vh}.cwa-grid{display:grid;gap:0;width:max-content;border:2px solid var(--dm-text)}.cwa-cell{position:relative;width:1.7rem;height:1.7rem;display:grid;place-items:center;font-weight:700;font-size:.85rem;background:var(--dm-background, #fff);color:var(--dm-text);border:1px solid color-mix(in srgb,var(--dm-text) 35%,transparent);box-sizing:border-box}.cwa-cell.is-block{background:var(--dm-text);border-color:var(--dm-text)}.cwa-num{position:absolute;top:1px;left:2px;font-size:.5rem;font-weight:600;line-height:1;color:var(--dm-text-muted)}.cwa-unplaced{margin:0;padding:.5rem .75rem;border-radius:6px;font-size:var(--dm-text-sm);background:color-mix(in srgb,var(--dm-warning) 15%,transparent);color:color-mix(in srgb,var(--dm-warning) 55%,var(--dm-text))}
1
+ /* Crossword admin - the word list and the live grid. Shared pieces are in /plugins/_lib/admin/games/games.css. */
2
+ .cwa-words { display: flex; flex-direction: column; gap: .4rem; }
3
+ .cwa-word { display: grid; grid-template-columns: 1.6rem minmax(7rem, 12rem) 3.5rem minmax(0, 1fr) auto; gap: .4rem; align-items: center; }
4
+ .cwa-n { color: var(--dm-text-muted); font-size: var(--dm-text-sm); text-align: right; }
5
+ .cwa-answer { text-transform: uppercase; font-family: var(--dm-font-mono, monospace); letter-spacing: .04em; }
6
+ .cwa-enum { color: var(--dm-text-muted); font-size: var(--dm-text-xs); white-space: nowrap; }
7
+ @media (max-width: 700px) { .cwa-word { grid-template-columns: 1.4rem 1fr auto; } .cwa-enum { display: none; } .cwa-clue { grid-column: 2 / 4; } }
8
+
9
+ .cwa-side { position: sticky; top: .5rem; }
10
+ .cwa-preview { overflow: auto; max-height: 70vh; }
11
+ /* Fixed ink and paper, as on the public grid: a themed grid came out inverted under dark themes. */
12
+ .cwa-grid { --cwa-ink: #111827; --cwa-paper: #ffffff; display: grid; gap: 0; width: max-content; border: 2px solid var(--cwa-ink); background: var(--cwa-ink); }
13
+ .cwa-cell { position: relative; width: 1.7rem; height: 1.7rem; display: grid; place-items: center; font-weight: 700; font-size: .85rem;
14
+ background: var(--cwa-paper); color: var(--cwa-ink); border: 1px solid color-mix(in srgb, var(--cwa-ink) 35%, transparent); box-sizing: border-box; }
15
+ .cwa-cell.is-block { background: var(--cwa-ink); border-color: var(--cwa-ink); }
16
+ .cwa-num { position: absolute; top: 1px; left: 2px; font-size: .5rem; font-weight: 600; line-height: 1; color: #4b5563; }
17
+ .cwa-unplaced { margin: 0; padding: .5rem .75rem; border-radius: 6px; font-size: var(--dm-text-sm);
18
+ background: color-mix(in srgb, var(--dm-warning) 15%, transparent); color: color-mix(in srgb, var(--dm-warning) 55%, var(--dm-text)); }