@bendyline/gilde 0.1.21 → 0.1.23

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.
Files changed (69) hide show
  1. package/authoring/chat-models/nemotron3.5-lightning-30b-q4.json +3 -2
  2. package/authoring/chat-models/nemotron3.5-lightning-30b-q8.json +3 -2
  3. package/authoring/page-demo-stub.js +85 -0
  4. package/data/chat-models/index.json +1 -1
  5. package/data/chat-models/ne/nemotron3.5-lightning-30b-q4/manifest.json +3 -2
  6. package/data/chat-models/ne/nemotron3.5-lightning-30b-q8/manifest.json +3 -2
  7. package/data/connector-types/bl/bluesky-posts/manifest.json +16 -0
  8. package/data/connector-types/bl/bluesky-posts/versions/1.0.0/manifest.json +76 -0
  9. package/data/connector-types/ca/calendar-google/versions/1.0.0/manifest.json +14 -0
  10. package/data/connector-types/gi/github-issues/versions/1.0.1/manifest.json +67 -0
  11. package/data/connector-types/in/instagram-media/manifest.json +17 -0
  12. package/data/connector-types/in/instagram-media/versions/1.0.0/manifest.json +96 -0
  13. package/data/connector-types/index.json +1 -1
  14. package/data/connector-types/li/linkedin-posts/manifest.json +16 -0
  15. package/data/connector-types/li/linkedin-posts/versions/1.0.0/manifest.json +54 -0
  16. package/data/connector-types/ma/mail-gmail/versions/1.0.0/manifest.json +14 -0
  17. package/data/connector-types/ma/mail-microsoft365/versions/1.0.0/manifest.json +14 -1
  18. package/data/connector-types/ma/mail-outlook/versions/1.0.0/manifest.json +14 -1
  19. package/data/connector-types/x-/x-posts/manifest.json +17 -0
  20. package/data/connector-types/x-/x-posts/versions/1.0.0/manifest.json +92 -0
  21. package/data/craftbook-templates/dr/draft-social-post/manifest.json +24 -0
  22. package/data/craftbook-templates/dr/draft-social-post/versions/1.0.0/craftbook.json +178 -0
  23. package/data/craftbook-templates/dr/draft-social-post/versions/1.0.0/test.json +161 -0
  24. package/data/craftbook-templates/index.json +1 -1
  25. package/data/craftbook-templates/re/reception-report/manifest.json +21 -0
  26. package/data/craftbook-templates/re/reception-report/versions/1.0.0/craftbook.json +170 -0
  27. package/data/craftbook-templates/re/reception-report/versions/1.0.0/test.json +168 -0
  28. package/data/craftbook-templates/so/social-digest/manifest.json +21 -0
  29. package/data/craftbook-templates/so/social-digest/versions/1.0.0/craftbook.json +133 -0
  30. package/data/craftbook-templates/so/social-digest/versions/1.0.0/test.json +168 -0
  31. package/data/gezel-templates/index.json +1 -1
  32. package/data/gezel-templates/om/omroeper/manifest.json +20 -0
  33. package/data/gezel-templates/om/omroeper/versions/1.0.0/about.md +33 -0
  34. package/data/gezel-templates/om/omroeper/versions/1.0.0/manifest.json +23 -0
  35. package/data/project-types/ca/caregiving-binder/versions/1.1.0/about.md +8 -0
  36. package/data/project-types/ca/caregiving-binder/versions/1.1.0/care.json +4 -0
  37. package/data/project-types/ca/caregiving-binder/versions/1.1.0/manifest.json +178 -0
  38. package/data/project-types/ca/caregiving-binder/versions/1.1.0/meds.json +8 -0
  39. package/data/project-types/ca/caregiving-binder/versions/1.1.0/mission.md +8 -0
  40. package/data/project-types/ca/caregiving-binder/versions/1.1.0/pages/dashboard/index.html +265 -0
  41. package/data/project-types/ch/checkers/versions/1.2.0/about.md +9 -0
  42. package/data/project-types/ch/checkers/versions/1.2.0/game.json +77 -0
  43. package/data/project-types/ch/checkers/versions/1.2.0/manifest.json +167 -0
  44. package/data/project-types/ch/checkers/versions/1.2.0/mission.md +9 -0
  45. package/data/project-types/ch/checkers/versions/1.2.0/pages/board/index.html +1157 -0
  46. package/data/project-types/fl/flashcards/versions/1.1.0/about.md +8 -0
  47. package/data/project-types/fl/flashcards/versions/1.1.0/deck.json +7 -0
  48. package/data/project-types/fl/flashcards/versions/1.1.0/manifest.json +168 -0
  49. package/data/project-types/fl/flashcards/versions/1.1.0/mission.md +6 -0
  50. package/data/project-types/fl/flashcards/versions/1.1.0/pages/review/index.html +476 -0
  51. package/data/project-types/im/image-feed/manifest.json +20 -0
  52. package/data/project-types/im/image-feed/versions/1.0.0/about.md +21 -0
  53. package/data/project-types/im/image-feed/versions/1.0.0/manifest.json +330 -0
  54. package/data/project-types/im/image-feed/versions/1.0.0/mission.md +10 -0
  55. package/data/project-types/im/image-feed/versions/1.0.0/pages/gallery/index.html +832 -0
  56. package/data/project-types/im/image-feed/versions/1.0.0/posts/index.json +4 -0
  57. package/data/project-types/im/image-feed/versions/1.0.0/style-guide.md +24 -0
  58. package/data/project-types/index.json +1 -1
  59. package/data/project-types/so/social-feed/manifest.json +21 -0
  60. package/data/project-types/so/social-feed/versions/1.0.0/about.md +19 -0
  61. package/data/project-types/so/social-feed/versions/1.0.0/manifest.json +336 -0
  62. package/data/project-types/so/social-feed/versions/1.0.0/mission.md +10 -0
  63. package/data/project-types/so/social-feed/versions/1.0.0/pages/dashboard/index.html +756 -0
  64. package/data/project-types/so/social-feed/versions/1.0.0/posts/index.json +4 -0
  65. package/data/project-types/so/social-feed/versions/1.0.0/voice-guide.md +22 -0
  66. package/package.json +1 -1
  67. package/schemas/connector-type-version.schema.json +6 -0
  68. package/schemas/craftbook-test.schema.json +1 -0
  69. package/schemas/project-type-version.schema.json +4 -0
@@ -0,0 +1,1157 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="gezel-demo" content="standalone" />
7
+ <title>Checkers</title>
8
+ <style>
9
+ :root {
10
+ color-scheme: light dark;
11
+ --bg: #faf7f2;
12
+ --card: #fff;
13
+ --ink: #2b2620;
14
+ --muted: #7a7166;
15
+ --accent: #b5651d;
16
+ --track: #ece5da;
17
+ --sq-light: #f3ead9;
18
+ --sq-dark: #b08968;
19
+ --sq-hint: #7a9e7e;
20
+ --red-a: #c9502c;
21
+ --red-b: #8f3018;
22
+ --black-a: #4a423a;
23
+ --black-b: #211c17;
24
+ --coord-on-light: rgba(43, 38, 32, 0.38);
25
+ --coord-on-dark: rgba(255, 250, 242, 0.5);
26
+ --frame-a: #9a7557;
27
+ --frame-b: #513725;
28
+ --frame-edge: #352217;
29
+ --last-move: rgba(239, 180, 92, 0.36);
30
+ }
31
+ :root[data-theme='dark'] {
32
+ --bg: #1c1a17;
33
+ --card: #262320;
34
+ --ink: #efe9e0;
35
+ --muted: #a59a8c;
36
+ --accent: #e0894a;
37
+ --track: #3a352f;
38
+ --sq-light: #3c362e;
39
+ --sq-dark: #6b4f37;
40
+ --sq-hint: #5d7f62;
41
+ --red-a: #d96a42;
42
+ --red-b: #9c3b1e;
43
+ --black-a: #8c8378;
44
+ --black-b: #3a342d;
45
+ --coord-on-light: rgba(239, 233, 224, 0.38);
46
+ --coord-on-dark: rgba(255, 248, 238, 0.48);
47
+ --frame-a: #74543d;
48
+ --frame-b: #34241a;
49
+ --frame-edge: #1b120d;
50
+ --last-move: rgba(236, 159, 85, 0.32);
51
+ }
52
+ @media (prefers-color-scheme: dark) {
53
+ :root:not([data-theme='light']) {
54
+ --bg: #1c1a17;
55
+ --card: #262320;
56
+ --ink: #efe9e0;
57
+ --muted: #a59a8c;
58
+ --accent: #e0894a;
59
+ --track: #3a352f;
60
+ --sq-light: #3c362e;
61
+ --sq-dark: #6b4f37;
62
+ --sq-hint: #5d7f62;
63
+ --red-a: #d96a42;
64
+ --red-b: #9c3b1e;
65
+ --black-a: #8c8378;
66
+ --black-b: #3a342d;
67
+ --coord-on-light: rgba(239, 233, 224, 0.38);
68
+ --coord-on-dark: rgba(255, 248, 238, 0.48);
69
+ --frame-a: #74543d;
70
+ --frame-b: #34241a;
71
+ --frame-edge: #1b120d;
72
+ --last-move: rgba(236, 159, 85, 0.32);
73
+ }
74
+ }
75
+ * { box-sizing: border-box; }
76
+ body {
77
+ margin: 0;
78
+ font: 15px/1.5 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
79
+ background: var(--bg);
80
+ color: var(--ink);
81
+ padding: 1.25rem;
82
+ }
83
+ main { max-width: 720px; margin: 0 auto; }
84
+ header { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
85
+ h1 {
86
+ font-family: Georgia, 'PT Serif', 'Times New Roman', serif;
87
+ font-size: 1.35rem;
88
+ font-weight: 600;
89
+ margin: 0;
90
+ }
91
+ .status { color: var(--muted); font-size: 0.9rem; margin: 0.15rem 0 0.9rem; }
92
+ .status .thinking { color: var(--accent); }
93
+ button.newgame {
94
+ border: 1px solid var(--track);
95
+ background: var(--card);
96
+ color: var(--ink);
97
+ border-radius: 6px;
98
+ padding: 0.3rem 0.7rem;
99
+ font: inherit;
100
+ font-size: 0.85rem;
101
+ cursor: pointer;
102
+ }
103
+ button.newgame:hover { border-color: var(--accent); }
104
+ dialog {
105
+ width: min(430px, calc(100vw - 2rem));
106
+ border: 1px solid var(--track);
107
+ border-radius: 10px;
108
+ padding: 0;
109
+ background: var(--card);
110
+ color: var(--ink);
111
+ box-shadow: 0 18px 55px rgba(0, 0, 0, 0.35);
112
+ }
113
+ dialog::backdrop { background: rgba(20, 17, 14, 0.62); }
114
+ .new-game-form { padding: 1rem; }
115
+ .new-game-form h2 {
116
+ font-family: Georgia, 'PT Serif', 'Times New Roman', serif;
117
+ font-size: 1.1rem;
118
+ margin: 0;
119
+ }
120
+ .dialog-intro { color: var(--muted); margin: 0.2rem 0 0.9rem; font-size: 0.85rem; }
121
+ .rule-fieldset { border: 0; margin: 0; padding: 0; }
122
+ .rule-fieldset legend {
123
+ color: var(--muted);
124
+ font-size: 0.72rem;
125
+ font-weight: 700;
126
+ letter-spacing: 0.05em;
127
+ margin-bottom: 0.35rem;
128
+ text-transform: uppercase;
129
+ }
130
+ .rule-tray {
131
+ display: grid;
132
+ grid-template-columns: 1fr 1fr;
133
+ gap: 0.35rem;
134
+ padding: 0.35rem;
135
+ border: 1px solid var(--track);
136
+ border-radius: 10px;
137
+ background: var(--track);
138
+ }
139
+ .rule-key {
140
+ min-width: 0;
141
+ border: 1px solid transparent;
142
+ border-radius: 6px;
143
+ padding: 0.55rem 0.6rem;
144
+ background: var(--card);
145
+ color: var(--ink);
146
+ cursor: pointer;
147
+ font: inherit;
148
+ text-align: left;
149
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
150
+ }
151
+ .rule-key:hover { border-color: var(--accent); }
152
+ .rule-key[aria-checked='true'] {
153
+ border-color: var(--accent);
154
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
155
+ transform: translateY(1px);
156
+ }
157
+ .rule-key strong { display: block; font-size: 0.88rem; }
158
+ .rule-key span { display: block; color: var(--muted); font-size: 0.75rem; line-height: 1.3; margin-top: 0.1rem; }
159
+ .dialog-actions { display: flex; justify-content: flex-end; gap: 0.45rem; margin-top: 1rem; }
160
+ .dialog-button {
161
+ border: 1px solid var(--accent);
162
+ border-radius: 6px;
163
+ padding: 0.4rem 0.75rem;
164
+ background: var(--accent);
165
+ color: var(--bg);
166
+ cursor: pointer;
167
+ font: inherit;
168
+ font-size: 0.85rem;
169
+ }
170
+ .dialog-button.secondary { border-color: var(--track); background: var(--card); color: var(--ink); }
171
+ .table { display: grid; grid-template-columns: minmax(0, 1fr) 220px; gap: 0.9rem; }
172
+ .boardwrap {
173
+ background: linear-gradient(145deg, var(--frame-a), var(--frame-b));
174
+ border: 1px solid var(--frame-edge);
175
+ border-radius: 14px;
176
+ padding: 0.85rem;
177
+ box-shadow:
178
+ inset 0 1px 0 rgba(255, 255, 255, 0.24),
179
+ inset 0 -4px 8px rgba(0, 0, 0, 0.24),
180
+ 0 10px 24px rgba(31, 21, 15, 0.22);
181
+ }
182
+ .board {
183
+ position: relative;
184
+ display: grid;
185
+ grid-template-columns: repeat(8, 1fr);
186
+ grid-template-rows: repeat(8, minmax(0, 1fr));
187
+ aspect-ratio: 1;
188
+ border: 1px solid var(--frame-edge);
189
+ border-radius: 8px;
190
+ overflow: hidden;
191
+ user-select: none;
192
+ box-shadow:
193
+ inset 0 0 0 1px rgba(255, 255, 255, 0.08),
194
+ 0 5px 12px rgba(0, 0, 0, 0.3);
195
+ }
196
+ .cell { position: relative; display: grid; min-width: 0; min-height: 0; place-items: center; }
197
+ .cell.light { background: var(--sq-light); }
198
+ .cell.dark { background: var(--sq-dark); cursor: default; }
199
+ .cell::before {
200
+ content: '';
201
+ position: absolute;
202
+ inset: 0;
203
+ background: linear-gradient(145deg, rgba(255, 255, 255, 0.13), transparent 46%, rgba(0, 0, 0, 0.09));
204
+ pointer-events: none;
205
+ }
206
+ .cell.dark.selectable { cursor: pointer; }
207
+ .cell.last-move { box-shadow: inset 0 0 0 999px var(--last-move); }
208
+ .cell.last-move-from { box-shadow: inset 0 0 0 2px var(--last-move); }
209
+ .cell.dark.selected {
210
+ outline: 3px solid var(--accent);
211
+ outline-offset: -3px;
212
+ box-shadow: inset 0 0 14px rgba(255, 255, 255, 0.16);
213
+ }
214
+ .cell.dark.hint { box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.08); cursor: pointer; }
215
+ .coord {
216
+ position: absolute;
217
+ top: 6%;
218
+ left: 7%;
219
+ z-index: 2;
220
+ font-size: clamp(0.45rem, 1.25vw, 0.65rem);
221
+ font-weight: 600;
222
+ letter-spacing: 0.01em;
223
+ line-height: 1;
224
+ pointer-events: none;
225
+ user-select: none;
226
+ }
227
+ .cell.light .coord { color: var(--coord-on-light); }
228
+ .cell.dark .coord { color: var(--coord-on-dark); }
229
+ .cell.dark.hint::after {
230
+ content: '';
231
+ width: 26%;
232
+ height: 26%;
233
+ z-index: 3;
234
+ border-radius: 50%;
235
+ background: var(--sq-hint);
236
+ box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.16), 0 2px 6px rgba(0, 0, 0, 0.24);
237
+ opacity: 0.92;
238
+ animation: hint-breathe 1.6s ease-in-out infinite;
239
+ }
240
+ .piece {
241
+ position: relative;
242
+ z-index: 1;
243
+ width: 76%;
244
+ height: 76%;
245
+ border: 1px solid rgba(30, 20, 14, 0.45);
246
+ border-radius: 50%;
247
+ display: grid;
248
+ place-items: center;
249
+ font-weight: 700;
250
+ font-size: clamp(0.62rem, 1.7vw, 0.9rem);
251
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.35);
252
+ box-shadow:
253
+ inset 0 4px 5px rgba(255, 255, 255, 0.23),
254
+ inset 0 -6px 8px rgba(0, 0, 0, 0.32),
255
+ 0 5px 7px rgba(0, 0, 0, 0.34);
256
+ transition: transform 150ms ease-out, box-shadow 150ms ease-out, opacity 120ms ease-out;
257
+ }
258
+ .piece::before {
259
+ content: '';
260
+ position: absolute;
261
+ inset: 12%;
262
+ border: 1px solid rgba(255, 255, 255, 0.2);
263
+ border-radius: 50%;
264
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.17);
265
+ pointer-events: none;
266
+ }
267
+ .piece.red { background: radial-gradient(circle at 32% 25%, var(--red-a), var(--red-b) 72%); color: #f8e8de; }
268
+ .piece.black { background: radial-gradient(circle at 32% 25%, var(--black-a), var(--black-b) 72%); color: #d9d2c7; }
269
+ .piece.king::before { inset: 16%; border: 2px double currentColor; opacity: 0.72; }
270
+ .cell.selectable .piece:hover,
271
+ .cell.selected .piece { transform: translateY(-2px) scale(1.025); }
272
+ .piece.draggable { cursor: grab; touch-action: none; }
273
+ .piece.dragging {
274
+ z-index: 9;
275
+ cursor: grabbing;
276
+ opacity: 0.94;
277
+ transition: none;
278
+ filter: drop-shadow(0 10px 7px rgba(0, 0, 0, 0.34));
279
+ }
280
+ .piece.moving-piece {
281
+ position: absolute;
282
+ z-index: 10;
283
+ margin: 0;
284
+ pointer-events: none;
285
+ transition: none;
286
+ }
287
+ .piece.move-source { opacity: 0; }
288
+ @keyframes hint-breathe {
289
+ 0%, 100% { transform: scale(0.9); opacity: 0.7; }
290
+ 50% { transform: scale(1.06); opacity: 1; }
291
+ }
292
+ .aside { display: flex; flex-direction: column; gap: 0.6rem; min-width: 0; }
293
+ .panel {
294
+ background: var(--card);
295
+ border: 1px solid var(--track);
296
+ border-radius: 10px;
297
+ padding: 0.7rem 0.85rem;
298
+ font-size: 0.85rem;
299
+ }
300
+ .panel h2 { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.35rem; }
301
+ .moveThought { font-style: italic; }
302
+ .moveThought .who { color: var(--muted); font-style: normal; font-size: 0.78rem; display: block; margin-bottom: 0.15rem; }
303
+ .log { margin: 0; padding-left: 1.05rem; max-height: 200px; overflow-y: auto; }
304
+ .log li { margin: 0.15rem 0; color: var(--muted); }
305
+ .log li.latest { color: var(--ink); }
306
+ .banner {
307
+ border: 1px solid var(--accent);
308
+ border-radius: 6px;
309
+ padding: 0.45rem 0.7rem;
310
+ font-size: 0.85rem;
311
+ margin-bottom: 0.75rem;
312
+ display: none;
313
+ }
314
+ .banner.show { display: block; }
315
+ .note {
316
+ margin: 0 0 0.85rem;
317
+ padding: 0.5rem 0.65rem;
318
+ border: 1px solid var(--track);
319
+ border-radius: 6px;
320
+ background: var(--card);
321
+ color: var(--muted);
322
+ font-size: 0.8rem;
323
+ }
324
+ @media (prefers-reduced-motion: reduce) {
325
+ .cell.dark.hint::after { animation: none; }
326
+ .piece { transition: none; }
327
+ }
328
+ @media (max-width: 640px) { .table { grid-template-columns: 1fr; } }
329
+ .demo-banner {
330
+ display: flex;
331
+ align-items: center;
332
+ justify-content: space-between;
333
+ gap: 0.75rem;
334
+ margin: 0 0 0.85rem;
335
+ padding: 0.5rem 0.65rem;
336
+ border: 1px solid var(--accent);
337
+ border-radius: 6px;
338
+ background: color-mix(in srgb, var(--card) 88%, var(--accent));
339
+ color: var(--ink);
340
+ font-size: 0.8rem;
341
+ }
342
+ .demo-banner[hidden] { display: none; }
343
+ .demo-banner strong { font-weight: 700; }
344
+ .demo-reset {
345
+ flex: none;
346
+ padding: 0.25rem 0.5rem;
347
+ border: 1px solid var(--accent);
348
+ border-radius: 6px;
349
+ background: var(--card);
350
+ color: var(--ink);
351
+ cursor: pointer;
352
+ font: inherit;
353
+ }
354
+ .demo-reset:hover { border-color: var(--ink); }
355
+ </style>
356
+ </head>
357
+ <body>
358
+ <main>
359
+ <div class="demo-banner" data-gezel-demo-banner hidden>
360
+ <span><strong>Demo mode</strong> — hot-seat play, sample data stays in this page.</span>
361
+ <button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
362
+ </div>
363
+ <p class="note" id="browser-note" hidden>
364
+ Viewing from a browser tab — the board stays live, but moves happen in the app's Output pane.
365
+ </p>
366
+ <div class="banner" id="banner"></div>
367
+ <header>
368
+ <h1>Checkers</h1>
369
+ <button type="button" class="newgame" id="newgame">New game</button>
370
+ </header>
371
+ <p class="status" id="status">Setting up the board…</p>
372
+ <div class="table">
373
+ <div class="boardwrap"><div class="board" id="board"></div></div>
374
+ <div class="aside">
375
+ <div class="panel moveThought" id="moveThought-panel" hidden>
376
+ <span class="who">Damspeler says</span>
377
+ <span id="moveThought"></span>
378
+ </div>
379
+ <div class="panel">
380
+ <h2>Moves</h2>
381
+ <ol class="log" id="log"><li>No moves yet — red opens.</li></ol>
382
+ </div>
383
+ </div>
384
+ </div>
385
+ <dialog id="new-game-dialog" aria-labelledby="new-game-title">
386
+ <form class="new-game-form" id="new-game-form">
387
+ <h2 id="new-game-title">New game</h2>
388
+ <p class="dialog-intro">Choose the capture rule for this game.</p>
389
+ <fieldset class="rule-fieldset">
390
+ <legend>Captures</legend>
391
+ <div class="rule-tray" role="radiogroup" aria-label="Capture rule">
392
+ <button type="button" class="rule-key" role="radio" aria-checked="true" data-force-captures="true">
393
+ <strong>Required</strong>
394
+ <span>Standard rules. Take a capture when one is available.</span>
395
+ </button>
396
+ <button type="button" class="rule-key" role="radio" aria-checked="false" data-force-captures="false">
397
+ <strong>Optional</strong>
398
+ <span>Ordinary moves stay available when you can capture.</span>
399
+ </button>
400
+ </div>
401
+ </fieldset>
402
+ <div class="dialog-actions">
403
+ <button type="button" class="dialog-button secondary" id="cancel-new-game">Cancel</button>
404
+ <button type="button" class="dialog-button" id="start-new-game">Start game</button>
405
+ </div>
406
+ </form>
407
+ </dialog>
408
+ </main>
409
+ <script>
410
+ /* gezel-page-demo-stub v1 */
411
+ /*
412
+ * The canonical demo stub for Gezel type pages. MIT License.
413
+ *
414
+ * Paste this file's bytes verbatim into a <script> block ahead of the
415
+ * page's own code, then call makeDemoGezel({ page, tools, data }) with the
416
+ * page's demo handlers. Served by Gezel, the real window.gezel is already
417
+ * injected and the stub steps aside; opened as a raw file (the live demos
418
+ * on gezelgilde.com), it installs a window.gezel whose tools and data are
419
+ * the page-supplied handler maps — one code path in every mode. `tools`
420
+ * and `data` map a tool name / declared read path to a function; data
421
+ * handlers back read() and list(), and url() returns the handler's value
422
+ * or passes a data: URI through. See docs/page-authoring.md.
423
+ */
424
+ (function () {
425
+ 'use strict';
426
+ window.makeDemoGezel = function (opts) {
427
+ if (window.gezel) return window.gezel;
428
+ var page = (opts && opts.page) || {};
429
+ var tools = (opts && opts.tools) || {};
430
+ var data = (opts && opts.data) || {};
431
+ var themeListeners = [];
432
+ var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
433
+ var ui = {
434
+ theme: { mode: media && media.matches ? 'dark' : 'light' },
435
+ onTheme: function (cb) {
436
+ themeListeners.push(cb);
437
+ return function () {
438
+ var at = themeListeners.indexOf(cb);
439
+ if (at >= 0) themeListeners.splice(at, 1);
440
+ };
441
+ },
442
+ };
443
+ if (media && media.addEventListener) {
444
+ media.addEventListener('change', function (ev) {
445
+ ui.theme = { mode: ev.matches ? 'dark' : 'light' };
446
+ themeListeners.slice().forEach(function (cb) { cb(ui.theme); });
447
+ });
448
+ }
449
+ function fail(code, message) {
450
+ var err = new Error(message);
451
+ err.code = code;
452
+ return Promise.reject(err);
453
+ }
454
+ function run(map, key, arg) {
455
+ if (!Object.prototype.hasOwnProperty.call(map, key)) return null;
456
+ return Promise.resolve().then(function () { return map[key](arg); });
457
+ }
458
+ window.gezel = {
459
+ page: {
460
+ api: 1,
461
+ projectId: 'demo',
462
+ source: 'type',
463
+ entry: page.entry || 'index.html',
464
+ typeName: page.typeName || 'demo',
465
+ params: page.params || {},
466
+ mode: 'demo',
467
+ },
468
+ tools: {
469
+ list: function () { return Promise.resolve(Object.keys(tools)); },
470
+ invoke: function (name, input) {
471
+ var call = run(tools, name, input || {});
472
+ if (!call) return fail('not-allowed', "The demo does not handle the tool '" + name + "'.");
473
+ return call.then(function (output) { return { output: output }; });
474
+ },
475
+ },
476
+ data: {
477
+ read: function (path, o) {
478
+ return run(data, path, o || {}) || fail('not-allowed', "The demo has no data for '" + path + "'.");
479
+ },
480
+ list: function (path, o) {
481
+ return run(data, path, o || {}) || Promise.resolve([]);
482
+ },
483
+ watch: function () { return function () {}; },
484
+ url: function (path) {
485
+ if (Object.prototype.hasOwnProperty.call(data, path)) return String(data[path]({}));
486
+ return typeof path === 'string' && path.indexOf('data:') === 0 ? path : '';
487
+ },
488
+ },
489
+ ui: ui,
490
+ refresh: function () {},
491
+ };
492
+ return window.gezel;
493
+ };
494
+ })();
495
+ </script>
496
+ <script>
497
+ (function () {
498
+ 'use strict';
499
+ var FILES = 'abcdefgh';
500
+
501
+ function sqName(x, y) { return FILES[x] + String(8 - y); }
502
+ function parseSquare(name) {
503
+ return { x: FILES.indexOf(name[0].toLowerCase()), y: 8 - Number(name[1]) };
504
+ }
505
+ function withPiece(squares, x, y, ch) {
506
+ var index = y * 8 + x;
507
+ return squares.slice(0, index) + ch + squares.slice(index + 1);
508
+ }
509
+ function applyPreviewHop(squares, from, to) {
510
+ var start = parseSquare(from);
511
+ var end = parseSquare(to);
512
+ var ch = squares[start.y * 8 + start.x];
513
+ var next = withPiece(squares, start.x, start.y, '.');
514
+ if (Math.abs(start.x - end.x) === 2) {
515
+ next = withPiece(next, (start.x + end.x) / 2, (start.y + end.y) / 2, '.');
516
+ }
517
+ if (ch === 'r' && end.y === 0) ch = 'R';
518
+ if (ch === 'b' && end.y === 7) ch = 'B';
519
+ return withPiece(next, end.x, end.y, ch);
520
+ }
521
+
522
+ // ── demo rules engine: hot-seat play when the page opens as a raw file ──
523
+ function demoSide(ch) {
524
+ if (ch === 'r' || ch === 'R') return 'user';
525
+ if (ch === 'b' || ch === 'B') return 'ai';
526
+ return null;
527
+ }
528
+ function demoDirections(ch) {
529
+ if (ch === 'R' || ch === 'B') return [[-1,-1],[1,-1],[-1,1],[1,1]];
530
+ return ch === 'r' ? [[-1,-1],[1,-1]] : [[-1,1],[1,1]];
531
+ }
532
+ function demoCaptures(squares, x, y, ch, origin, path, captures, out) {
533
+ var found = false;
534
+ demoDirections(ch).forEach(function (d) {
535
+ var mx = x + d[0]; var my = y + d[1];
536
+ var tx = x + d[0] * 2; var ty = y + d[1] * 2;
537
+ if (tx < 0 || tx > 7 || ty < 0 || ty > 7 || mx < 0 || mx > 7 || my < 0 || my > 7) return;
538
+ var middle = squares[my * 8 + mx];
539
+ if (demoSide(middle) === null || demoSide(middle) === demoSide(ch) || squares[ty * 8 + tx] !== '.') return;
540
+ found = true;
541
+ var next = withPiece(withPiece(withPiece(squares, x, y, '.'), mx, my, '.'), tx, ty, ch);
542
+ demoCaptures(next, tx, ty, ch, origin, path.concat(sqName(tx, ty)), captures.concat(sqName(mx, my)), out);
543
+ });
544
+ if (!found && captures.length) out.push({ from: origin, to: path[path.length - 1], path: path, captures: captures });
545
+ }
546
+ function demoMoves(game) {
547
+ var captures = []; var simple = [];
548
+ for (var y = 0; y < 8; y += 1) {
549
+ for (var x = 0; x < 8; x += 1) {
550
+ var ch = game.squares[y * 8 + x];
551
+ if (demoSide(ch) !== game.turn) continue;
552
+ var from = sqName(x, y);
553
+ demoCaptures(game.squares, x, y, ch, from, [from], [], captures);
554
+ demoDirections(ch).forEach(function (d) {
555
+ var tx = x + d[0]; var ty = y + d[1];
556
+ if (tx >= 0 && tx < 8 && ty >= 0 && ty < 8 && game.squares[ty * 8 + tx] === '.') {
557
+ simple.push({ from: from, to: sqName(tx, ty), path: [from, sqName(tx, ty)], captures: [] });
558
+ }
559
+ });
560
+ }
561
+ }
562
+ return captures.length && game.forceCaptures !== false ? captures : captures.concat(simple);
563
+ }
564
+ function demoNewGame(forceCaptures) {
565
+ var game = {
566
+ version: 1,
567
+ squares: '.b.b.b.bb.b.b.b..b.b.b.b................r.r.r.r..r.r.r.rr.r.r.r.',
568
+ turn: 'user', status: 'playing', userColor: 'red', forceCaptures: forceCaptures !== false,
569
+ legalMoves: [], lastMove: null, lastMoveThought: '', moveLog: [],
570
+ };
571
+ game.legalMoves = demoMoves(game);
572
+ return game;
573
+ }
574
+ var demoGame = demoNewGame(true);
575
+
576
+ makeDemoGezel({
577
+ page: { entry: 'board/index.html', typeName: 'Checkers' },
578
+ tools: {
579
+ user_move: function (input) {
580
+ var move = demoGame.legalMoves.find(function (m) { return m.from === input.from && m.to === input.to; });
581
+ if (!move) throw new Error('That move is not available in the hot-seat demo.');
582
+ var next = demoGame.squares;
583
+ for (var i = 1; i < move.path.length; i += 1) next = applyPreviewHop(next, move.path[i - 1], move.path[i]);
584
+ demoGame.squares = next;
585
+ demoGame.lastMove = { by: demoGame.turn, from: move.from, to: move.to };
586
+ demoGame.moveLog.push({ by: demoGame.turn, from: move.from, to: move.to, captures: move.captures.slice() });
587
+ demoGame.turn = demoGame.turn === 'user' ? 'ai' : 'user';
588
+ demoGame.legalMoves = demoMoves(demoGame);
589
+ if (demoGame.legalMoves.length === 0) demoGame.status = demoGame.turn === 'user' ? 'ai_won' : 'user_won';
590
+ return { status: demoGame.status, turn: demoGame.turn, summary: 'Move played.' };
591
+ },
592
+ new_game: function (input) {
593
+ demoGame = demoNewGame(input && input.forceCaptures);
594
+ return { status: demoGame.status, turn: demoGame.turn, summary: 'New game.' };
595
+ },
596
+ },
597
+ data: {
598
+ 'game.json': function () { return demoGame; },
599
+ },
600
+ });
601
+
602
+ // ── one code path from here: window.gezel exists in every mode ──
603
+ var demoMode = gezel.page.mode === 'demo';
604
+ var browserMode = gezel.page.mode === 'browser';
605
+
606
+ function applyTheme(mode) {
607
+ document.documentElement.setAttribute('data-theme', mode === 'dark' ? 'dark' : 'light');
608
+ }
609
+ applyTheme(gezel.ui.theme.mode);
610
+ gezel.ui.onTheme(function (theme) { applyTheme(theme.mode); });
611
+
612
+ if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
613
+ if (browserMode) document.getElementById('browser-note').hidden = false;
614
+
615
+ var state = null;
616
+ var selected = null;
617
+ var moveOrigin = null;
618
+ var movePath = [];
619
+ var previewSquares = null;
620
+ var animationSquares = null;
621
+ var busy = false;
622
+ var animating = false;
623
+ var suppressClickUntil = 0;
624
+ var newGameForceCaptures = true;
625
+
626
+ function squaresForRender() {
627
+ return previewSquares || animationSquares || (state && state.squares) || '.'.repeat(64);
628
+ }
629
+ function resetMovePreview() {
630
+ selected = null;
631
+ moveOrigin = null;
632
+ movePath = [];
633
+ previewSquares = null;
634
+ }
635
+ function beginMove(name) {
636
+ selected = name;
637
+ moveOrigin = name;
638
+ movePath = [name];
639
+ previewSquares = null;
640
+ }
641
+ function matchingMoves() {
642
+ if (!state || !moveOrigin || movePath.length === 0) return [];
643
+ return state.legalMoves.filter(function (move) {
644
+ if (move.from !== moveOrigin || move.path.length < movePath.length) return false;
645
+ for (var i = 0; i < movePath.length; i += 1) {
646
+ if (move.path[i] !== movePath[i]) return false;
647
+ }
648
+ return true;
649
+ });
650
+ }
651
+ function currentHints() {
652
+ var hints = {};
653
+ matchingMoves().forEach(function (move) {
654
+ var landing = move.path[movePath.length];
655
+ if (!landing) return;
656
+ if (!hints[landing]) hints[landing] = [];
657
+ hints[landing].push(move);
658
+ });
659
+ return hints;
660
+ }
661
+ function originMap() {
662
+ var origins = {};
663
+ if (!state) return origins;
664
+ if (movePath.length > 1 && selected) {
665
+ origins[selected] = true;
666
+ return origins;
667
+ }
668
+ state.legalMoves.forEach(function (move) { origins[move.from] = true; });
669
+ return origins;
670
+ }
671
+ function esc(s) {
672
+ return String(s).replace(/[&<>"]/g, function (c) {
673
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];
674
+ });
675
+ }
676
+
677
+ function setBanner(text) {
678
+ var el = document.getElementById('banner');
679
+ el.textContent = text || '';
680
+ el.className = text ? 'banner show' : 'banner';
681
+ }
682
+
683
+ function invokeErrorText(err) {
684
+ if (err && err.code === 'unavailable') {
685
+ return "Moves happen in the app's Output pane — this browser tab stays view-only.";
686
+ }
687
+ return String(err && err.message ? err.message : err);
688
+ }
689
+
690
+ function statusLine() {
691
+ if (!state) return 'Setting up the board…';
692
+ if (demoMode && state.status === 'playing') {
693
+ var demoCaps = state.legalMoves.some(function (m) { return m.captures.length > 0; });
694
+ return 'Hot seat — ' + (state.turn === 'user' ? 'red' : 'black') + ' to move' + (demoCaps ? ' (capture available).' : '.');
695
+ }
696
+ if (demoMode && state.status === 'user_won') return 'Red wins the hot-seat demo.';
697
+ if (demoMode && state.status === 'ai_won') return 'Black wins the hot-seat demo.';
698
+ if (state.status === 'user_won') return 'You won! A gracious rematch awaits.';
699
+ if (state.status === 'ai_won') return 'The Damspeler takes this one — rematch?';
700
+ if (animating) return 'Piece in motion…';
701
+ if (busy) return 'Sending your move…';
702
+ if (movePath.length > 1) return 'Keep jumping — choose the next landing square.';
703
+ if (state.turn === 'ai') return 'Damspeler is thinking…';
704
+ var caps = state.legalMoves.some(function (m) { return m.captures.length > 0; });
705
+ if (caps && state.forceCaptures !== false) {
706
+ return 'Your move — a capture is available (captures are forced).';
707
+ }
708
+ if (caps) return 'Your move — a capture is available, but optional.';
709
+ return state.forceCaptures === false ? 'Your move — captures are optional.' : 'Your move.';
710
+ }
711
+
712
+ function render() {
713
+ document.getElementById('status').innerHTML =
714
+ state && !demoMode && state.turn === 'ai' && state.status === 'playing'
715
+ ? '<span class="thinking">' + esc(statusLine()) + '</span>'
716
+ : esc(statusLine());
717
+
718
+ var moveThoughtPanel = document.getElementById('moveThought-panel');
719
+ if (state && state.lastMoveThought) {
720
+ moveThoughtPanel.hidden = false;
721
+ document.getElementById('moveThought').textContent = state.lastMoveThought;
722
+ } else {
723
+ moveThoughtPanel.hidden = true;
724
+ }
725
+
726
+ var log = document.getElementById('log');
727
+ if (state && state.moveLog.length) {
728
+ log.innerHTML = state.moveLog
729
+ .slice(-8)
730
+ .map(function (m, i, arr) {
731
+ var text = (m.by === 'user' ? 'Red ' : 'Black ') + m.from +
732
+ (m.captures.length ? 'x' : '-') + m.to;
733
+ return '<li' + (i === arr.length - 1 ? ' class="latest"' : '') + '>' + esc(text) + '</li>';
734
+ })
735
+ .join('');
736
+ }
737
+
738
+ var board = document.getElementById('board');
739
+ board.innerHTML = '';
740
+ if (!state) return;
741
+ var myTurn = (demoMode || state.turn === 'user') && state.status === 'playing' && !busy && !animating;
742
+ var origins = myTurn ? originMap() : {};
743
+ var hints = myTurn && selected ? currentHints() : {};
744
+ var shownSquares = squaresForRender();
745
+ for (var y = 0; y < 8; y += 1) {
746
+ for (var x = 0; x < 8; x += 1) {
747
+ (function (x, y) {
748
+ var name = sqName(x, y);
749
+ var ch = shownSquares[y * 8 + x];
750
+ var dark = (x + y) % 2 === 1;
751
+ var cell = document.createElement('div');
752
+ cell.dataset.square = name;
753
+ var cls = 'cell ' + (dark ? 'dark' : 'light');
754
+ if (name === selected) cls += ' selected';
755
+ if (hints[name]) cls += ' hint';
756
+ if (myTurn && origins[name]) cls += ' selectable';
757
+ if (!previewSquares && !animationSquares && state.lastMove) {
758
+ if (name === state.lastMove.to) cls += ' last-move';
759
+ if (name === state.lastMove.from) cls += ' last-move-from';
760
+ }
761
+ cell.className = cls;
762
+ var coord = document.createElement('span');
763
+ coord.className = 'coord';
764
+ coord.textContent = name.toUpperCase();
765
+ coord.setAttribute('aria-hidden', 'true');
766
+ cell.appendChild(coord);
767
+ if (ch !== '.') {
768
+ var piece = document.createElement('div');
769
+ var red = ch === 'r' || ch === 'R';
770
+ piece.className = 'piece ' + (red ? 'red' : 'black');
771
+ if (ch === 'R' || ch === 'B') {
772
+ piece.className += ' king';
773
+ piece.textContent = 'K';
774
+ }
775
+ if (myTurn && origins[name]) {
776
+ piece.className += ' draggable';
777
+ enablePieceDrag(piece, name);
778
+ }
779
+ cell.appendChild(piece);
780
+ }
781
+ cell.addEventListener('click', function () {
782
+ if (!myTurn || Date.now() < suppressClickUntil) return;
783
+ if (hints[name]) {
784
+ playLanding(name);
785
+ } else if (origins[name]) {
786
+ if (selected === name && movePath.length <= 1) resetMovePreview();
787
+ else beginMove(name);
788
+ render();
789
+ } else if (selected && movePath.length <= 1) {
790
+ resetMovePreview();
791
+ render();
792
+ }
793
+ });
794
+ board.appendChild(cell);
795
+ })(x, y);
796
+ }
797
+ }
798
+ }
799
+
800
+ function prefersReducedMotion() {
801
+ return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
802
+ }
803
+
804
+ function capturedSquare(from, to) {
805
+ var start = parseSquare(from);
806
+ var end = parseSquare(to);
807
+ if (Math.abs(start.x - end.x) !== 2) return null;
808
+ return sqName((start.x + end.x) / 2, (start.y + end.y) / 2);
809
+ }
810
+
811
+ function animateHop(from, to) {
812
+ if (prefersReducedMotion()) return Promise.resolve();
813
+ var board = document.getElementById('board');
814
+ var sourceCell = board.querySelector('[data-square="' + from + '"]');
815
+ var targetCell = board.querySelector('[data-square="' + to + '"]');
816
+ var sourcePiece = sourceCell && sourceCell.querySelector('.piece');
817
+ if (!sourcePiece || !targetCell) return Promise.resolve();
818
+
819
+ var boardRect = board.getBoundingClientRect();
820
+ var sourceRect = sourcePiece.getBoundingClientRect();
821
+ var targetRect = targetCell.getBoundingClientRect();
822
+ var clone = sourcePiece.cloneNode(true);
823
+ clone.classList.remove('draggable', 'dragging', 'move-source');
824
+ clone.classList.add('moving-piece');
825
+ clone.style.left = sourceRect.left - boardRect.left + 'px';
826
+ clone.style.top = sourceRect.top - boardRect.top + 'px';
827
+ clone.style.width = sourceRect.width + 'px';
828
+ clone.style.height = sourceRect.height + 'px';
829
+ board.appendChild(clone);
830
+ sourcePiece.classList.add('move-source');
831
+
832
+ var dx = targetRect.left + targetRect.width / 2 - (sourceRect.left + sourceRect.width / 2);
833
+ var dy = targetRect.top + targetRect.height / 2 - (sourceRect.top + sourceRect.height / 2);
834
+ var turn = dx >= 0 ? 5 : -5;
835
+ var motion = clone.animate(
836
+ [
837
+ { transform: 'translate(0, 0) rotate(0deg) scale(1)' },
838
+ { transform: 'translate(' + dx * 0.55 + 'px, ' + dy * 0.55 + 'px) rotate(' + turn + 'deg) scale(1.1)', offset: 0.55 },
839
+ { transform: 'translate(' + dx + 'px, ' + dy + 'px) rotate(0deg) scale(1)' },
840
+ ],
841
+ { duration: 320, easing: 'cubic-bezier(.2,.76,.25,1)' },
842
+ );
843
+
844
+ var capture = capturedSquare(from, to);
845
+ var capturedPiece = capture && board.querySelector('[data-square="' + capture + '"] .piece');
846
+ var captureFinished = Promise.resolve();
847
+ if (capturedPiece) {
848
+ var captureMotion = capturedPiece.animate(
849
+ [
850
+ { transform: 'scale(1)', opacity: 1 },
851
+ { transform: 'scale(0.72) rotate(7deg)', opacity: 0 },
852
+ ],
853
+ { duration: 190, delay: 100, easing: 'ease-in' },
854
+ );
855
+ captureFinished = captureMotion.finished.catch(function () {});
856
+ }
857
+
858
+ return Promise.all([motion.finished.catch(function () {}), captureFinished]).then(function () {
859
+ clone.remove();
860
+ sourcePiece.classList.remove('move-source');
861
+ });
862
+ }
863
+
864
+ function paintCurrentMove() {
865
+ var board = document.getElementById('board');
866
+ Array.prototype.forEach.call(board.querySelectorAll('.cell.selected, .cell.hint'), function (cell) {
867
+ cell.classList.remove('selected', 'hint');
868
+ });
869
+ if (selected) {
870
+ var selectedCell = board.querySelector('[data-square="' + selected + '"]');
871
+ if (selectedCell) selectedCell.classList.add('selected');
872
+ }
873
+ var hints = currentHints();
874
+ Object.keys(hints).forEach(function (name) {
875
+ var cell = board.querySelector('[data-square="' + name + '"]');
876
+ if (cell) cell.classList.add('hint');
877
+ });
878
+ }
879
+
880
+ function landingAtPoint(piece, x, y) {
881
+ var oldPointerEvents = piece.style.pointerEvents;
882
+ piece.style.pointerEvents = 'none';
883
+ var hit = document.elementFromPoint(x, y);
884
+ piece.style.pointerEvents = oldPointerEvents;
885
+ var cell = hit && hit.closest ? hit.closest('.cell') : null;
886
+ var name = cell && cell.dataset.square;
887
+ return name && currentHints()[name] ? name : null;
888
+ }
889
+
890
+ function nearestThrownLanding(x, y, vx, vy) {
891
+ var speed = Math.sqrt(vx * vx + vy * vy);
892
+ if (speed < 0.18) return null;
893
+ var travel = Math.min(100, speed * 105);
894
+ var px = x + (vx / speed) * travel;
895
+ var py = y + (vy / speed) * travel;
896
+ var best = null;
897
+ var bestDistance = Infinity;
898
+ Object.keys(currentHints()).forEach(function (name) {
899
+ var cell = document.querySelector('[data-square="' + name + '"]');
900
+ if (!cell) return;
901
+ var rect = cell.getBoundingClientRect();
902
+ var cx = rect.left + rect.width / 2;
903
+ var cy = rect.top + rect.height / 2;
904
+ var distance = Math.sqrt((cx - px) * (cx - px) + (cy - py) * (cy - py));
905
+ if (distance < bestDistance && distance < rect.width * 0.9) {
906
+ best = name;
907
+ bestDistance = distance;
908
+ }
909
+ });
910
+ return best;
911
+ }
912
+
913
+ function enablePieceDrag(piece, name) {
914
+ piece.addEventListener('pointerdown', function (event) {
915
+ if (event.button !== 0 || busy || animating) return;
916
+ var start = { x: event.clientX, y: event.clientY, at: event.timeStamp };
917
+ var previous = start;
918
+ var last = start;
919
+ var moved = false;
920
+ piece.setPointerCapture(event.pointerId);
921
+
922
+ function onMove(moveEvent) {
923
+ var dx = moveEvent.clientX - start.x;
924
+ var dy = moveEvent.clientY - start.y;
925
+ if (!moved && Math.sqrt(dx * dx + dy * dy) < 4) return;
926
+ if (!moved) {
927
+ moved = true;
928
+ if (movePath.length <= 1 && moveOrigin !== name) beginMove(name);
929
+ paintCurrentMove();
930
+ piece.classList.add('dragging');
931
+ }
932
+ previous = last;
933
+ last = { x: moveEvent.clientX, y: moveEvent.clientY, at: moveEvent.timeStamp };
934
+ piece.style.transform =
935
+ 'translate(' + dx + 'px, ' + dy + 'px) rotate(' + dx * 0.035 + 'deg) scale(1.06)';
936
+ }
937
+
938
+ function onEnd(endEvent) {
939
+ piece.removeEventListener('pointermove', onMove);
940
+ piece.removeEventListener('pointerup', onEnd);
941
+ piece.removeEventListener('pointercancel', onEnd);
942
+ try { piece.releasePointerCapture(event.pointerId); } catch (_) {}
943
+ if (!moved) return;
944
+
945
+ // Swallow only the synthetic click emitted with this drag;
946
+ // the next deliberate hop should be available immediately.
947
+ suppressClickUntil = Date.now() + 80;
948
+ var elapsed = Math.max(1, last.at - previous.at);
949
+ var vx = (last.x - previous.x) / elapsed;
950
+ var vy = (last.y - previous.y) / elapsed;
951
+ var landing = landingAtPoint(piece, endEvent.clientX, endEvent.clientY) ||
952
+ nearestThrownLanding(endEvent.clientX, endEvent.clientY, vx, vy);
953
+ var currentTransform = piece.style.transform;
954
+ piece.classList.remove('dragging');
955
+ piece.style.transform = '';
956
+ if (landing) {
957
+ playLanding(landing);
958
+ return;
959
+ }
960
+
961
+ var rebound = piece.animate(
962
+ [
963
+ { transform: currentTransform },
964
+ { transform: 'translate(0, 0) rotate(0deg) scale(1)' },
965
+ ],
966
+ { duration: 180, easing: 'cubic-bezier(.2,.8,.3,1)' },
967
+ );
968
+ rebound.finished.catch(function () {}).then(render);
969
+ }
970
+
971
+ piece.addEventListener('pointermove', onMove);
972
+ piece.addEventListener('pointerup', onEnd);
973
+ piece.addEventListener('pointercancel', onEnd);
974
+ });
975
+ }
976
+
977
+ function playLanding(landing) {
978
+ var candidates = currentHints()[landing];
979
+ if (!candidates || !selected || busy || animating) return;
980
+ var from = selected;
981
+ var before = squaresForRender();
982
+ animating = true;
983
+ document.getElementById('status').textContent = statusLine();
984
+ animateHop(from, landing).then(function () {
985
+ previewSquares = applyPreviewHop(before, from, landing);
986
+ movePath.push(landing);
987
+ selected = landing;
988
+ animating = false;
989
+ var remaining = matchingMoves();
990
+ var hasNext = remaining.some(function (move) { return move.path.length > movePath.length; });
991
+ if (hasNext) {
992
+ render();
993
+ return;
994
+ }
995
+ var complete = remaining.find(function (move) { return move.path.length === movePath.length; });
996
+ if (!complete) {
997
+ resetMovePreview();
998
+ setBanner('That jump sequence is no longer available. Please choose again.');
999
+ render();
1000
+ return;
1001
+ }
1002
+ submitMove(complete);
1003
+ });
1004
+ }
1005
+
1006
+ function submitMove(move) {
1007
+ busy = true;
1008
+ render();
1009
+ gezel.tools
1010
+ .invoke('user_move', { from: move.from, to: move.to })
1011
+ .then(function (res) {
1012
+ busy = false;
1013
+ if (res.reaction && res.reaction.delivered === false && res.reaction.reason === 'engagement-off') {
1014
+ setBanner('Move played — but the AI is off in Settings, so the Damspeler cannot reply.');
1015
+ }
1016
+ return loadGame();
1017
+ })
1018
+ .catch(function (err) {
1019
+ busy = false;
1020
+ resetMovePreview();
1021
+ setBanner(invokeErrorText(err));
1022
+ render();
1023
+ });
1024
+ }
1025
+
1026
+ function animateServerMove(sequence, next) {
1027
+ animating = true;
1028
+ resetMovePreview();
1029
+ animationSquares = state.squares;
1030
+ render();
1031
+ var index = 1;
1032
+
1033
+ function nextHop() {
1034
+ if (index >= sequence.path.length) return Promise.resolve();
1035
+ var from = sequence.path[index - 1];
1036
+ var to = sequence.path[index];
1037
+ return animateHop(from, to).then(function () {
1038
+ animationSquares = applyPreviewHop(animationSquares, from, to);
1039
+ index += 1;
1040
+ render();
1041
+ return nextHop();
1042
+ });
1043
+ }
1044
+
1045
+ return nextHop().then(function () {
1046
+ animationSquares = null;
1047
+ state = next;
1048
+ animating = false;
1049
+ render();
1050
+ });
1051
+ }
1052
+
1053
+ function syncCaptureChoice() {
1054
+ Array.prototype.forEach.call(document.querySelectorAll('.rule-key'), function (button) {
1055
+ button.setAttribute(
1056
+ 'aria-checked',
1057
+ String((button.dataset.forceCaptures === 'true') === newGameForceCaptures),
1058
+ );
1059
+ });
1060
+ }
1061
+
1062
+ function openNewGameDialog() {
1063
+ if (busy || animating) return;
1064
+ newGameForceCaptures = !state || state.forceCaptures !== false;
1065
+ syncCaptureChoice();
1066
+ document.getElementById('new-game-dialog').showModal();
1067
+ }
1068
+
1069
+ function newGame(forceCaptures) {
1070
+ resetMovePreview();
1071
+ animationSquares = null;
1072
+ busy = true;
1073
+ render();
1074
+ gezel.tools
1075
+ .invoke('new_game', { forceCaptures: forceCaptures })
1076
+ .then(function () {
1077
+ busy = false;
1078
+ setBanner('');
1079
+ return loadGame();
1080
+ })
1081
+ .catch(function (err) {
1082
+ busy = false;
1083
+ setBanner(invokeErrorText(err));
1084
+ render();
1085
+ });
1086
+ }
1087
+
1088
+ function transitionState(next) {
1089
+ if (!state) {
1090
+ state = next;
1091
+ resetMovePreview();
1092
+ render();
1093
+ return Promise.resolve();
1094
+ }
1095
+
1096
+ var addedMove = next.moveLog.length > state.moveLog.length;
1097
+ if (!addedMove) {
1098
+ state = next;
1099
+ render();
1100
+ return Promise.resolve();
1101
+ }
1102
+
1103
+ if (previewSquares) {
1104
+ state = next;
1105
+ resetMovePreview();
1106
+ render();
1107
+ return Promise.resolve();
1108
+ }
1109
+
1110
+ var last = next.lastMove;
1111
+ if (last && last.by === 'ai') {
1112
+ var sequence = state.legalMoves.find(function (move) {
1113
+ return move.from === last.from && move.to === last.to;
1114
+ });
1115
+ if (sequence) return animateServerMove(sequence, next);
1116
+ }
1117
+
1118
+ state = next;
1119
+ resetMovePreview();
1120
+ render();
1121
+ return Promise.resolve();
1122
+ }
1123
+
1124
+ // Reads are serialized so a watch change landing mid-animation
1125
+ // cannot start a second transition from stale state.
1126
+ var loading = Promise.resolve();
1127
+ function loadGame() {
1128
+ loading = loading.then(function () {
1129
+ return gezel.data.read('game.json').then(
1130
+ function (next) { return transitionState(next); },
1131
+ function () {},
1132
+ );
1133
+ });
1134
+ return loading;
1135
+ }
1136
+
1137
+ Array.prototype.forEach.call(document.querySelectorAll('.rule-key'), function (button) {
1138
+ button.addEventListener('click', function () {
1139
+ newGameForceCaptures = button.dataset.forceCaptures === 'true';
1140
+ syncCaptureChoice();
1141
+ });
1142
+ });
1143
+ document.getElementById('newgame').addEventListener('click', openNewGameDialog);
1144
+ document.getElementById('cancel-new-game').addEventListener('click', function () {
1145
+ document.getElementById('new-game-dialog').close();
1146
+ });
1147
+ document.getElementById('start-new-game').addEventListener('click', function () {
1148
+ document.getElementById('new-game-dialog').close();
1149
+ newGame(newGameForceCaptures);
1150
+ });
1151
+
1152
+ loadGame();
1153
+ gezel.data.watch('game.json', function () { loadGame(); });
1154
+ })();
1155
+ </script>
1156
+ </body>
1157
+ </html>