@joystyk/cards-ui 0.0.0 → 0.2.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 ADDED
@@ -0,0 +1,22 @@
1
+ # @joystyk/cards-ui
2
+
3
+ ## 0.2.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [bb095c7]
8
+ - Updated dependencies [bb095c7]
9
+ - @joystyk/contracts@0.2.0
10
+ - @joystyk/kit@0.2.0
11
+
12
+ ## 0.1.0
13
+
14
+ ### Minor Changes
15
+
16
+ - Prepare the first compiled library release: ESM and API declarations for every public entry point, runtime assets, and checks against installed npm tarballs. Games and applications remain private.
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies
21
+ - @joystyk/contracts@0.1.0
22
+ - @joystyk/kit@0.1.0
package/README.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @joystyk/cards-ui
2
2
 
3
- The first Joystyk library release is not yet available. This placeholder reserves the package name.
3
+ Shared card tables, hands and solitaire controls for Gyral games. A game supplies a public table layout and intentions; these components handle pointer, keyboard and screen-reader interaction. Keep game authority in the engine.
4
4
 
5
- Source: https://github.com/joystyk-games/game-libraries
5
+ ## npm package
6
+
7
+ The first `0.1.0` release is being prepared; it has not been published. Once available:
8
+
9
+ ```sh
10
+ npm install @joystyk/cards-ui
11
+ ```
12
+
13
+ Node consumers require Node 24.15 or later. Browser entry points require a modern
14
+ browser and should be loaded in the browser. Tested with Gyral 0.3.1.
15
+
16
+ | Import | Format |
17
+ | --- | --- |
18
+ | `@joystyk/cards-ui` | ES module and TypeScript declarations |
19
+ | `@joystyk/cards-ui/controller` | ES module and TypeScript declarations |
20
+ | `@joystyk/cards-ui/layout` | ES module and TypeScript declarations |
21
+ | `@joystyk/cards-ui/solitaire` | ES module and TypeScript declarations |
22
+
23
+ Only the entries listed above are public API. Installed packages contain compiled ESM,
24
+ type declarations and runtime assets. Repository-relative links describe the Joystyk
25
+ workspace and are not needed to use the npm package.
26
+
27
+ ```ts
28
+ import { CardTable, CardHand } from '@joystyk/cards-ui';
29
+ import { EMPTY_LAYOUT, DEFAULT_OPTIONS } from '@joystyk/cards-ui/layout';
30
+
31
+ // CardTable and CardHand register <jy-card-table> and <jy-card-hand>.
32
+ // Bind the layout/options properties from your Gyral parent.
33
+ ```
@@ -0,0 +1,110 @@
1
+ import { type FaceCard, type PileSpec, type TableLayout, type TableOptions } from './layout.js';
2
+ /** Pointer travel, in CSS pixels, that turns a press into a drag (spec: Pointer). */
3
+ export declare const DRAG_THRESHOLD = 6;
4
+ export type Via = 'drag' | 'click' | 'keyboard';
5
+ /** Cards the player has picked up: the top `count` of pile `from`. */
6
+ export type Held = {
7
+ readonly from: string;
8
+ readonly count: number;
9
+ readonly via: Via;
10
+ };
11
+ /** A pointer pressed on a pile; `depth` counts cards from the top (0: the pile, not a card). */
12
+ export type Press = {
13
+ readonly pile: string | null;
14
+ readonly depth: number;
15
+ readonly x: number;
16
+ readonly y: number;
17
+ readonly dragging: boolean;
18
+ readonly dx: number;
19
+ readonly dy: number;
20
+ readonly over: string | null;
21
+ };
22
+ export type Ctl = {
23
+ /** The focused region. */
24
+ readonly region: number;
25
+ /** The remembered pile of each region: the one Tab reaches (roving tabindex). */
26
+ readonly piles: readonly number[];
27
+ /** Cards counted from the top of the focused pile: what Enter picks up. */
28
+ readonly depth: number;
29
+ readonly held: Held | null;
30
+ readonly press: Press | null;
31
+ /** The layout version last seen. */
32
+ readonly version: number;
33
+ /** A drag just dropped: the next layout lands without card flights. */
34
+ readonly settle: boolean;
35
+ /** The layout version that renders without card flights. */
36
+ readonly quiet: number | null;
37
+ };
38
+ /** What the table tells the game. */
39
+ export type TableOut = {
40
+ readonly _tag: 'Move';
41
+ readonly from: string;
42
+ readonly to: string;
43
+ readonly count: number;
44
+ readonly via: Via;
45
+ } | {
46
+ readonly _tag: 'Smart';
47
+ readonly from: string;
48
+ readonly count: number;
49
+ readonly via: Via;
50
+ } | {
51
+ readonly _tag: 'Activate';
52
+ readonly pile: string;
53
+ } | {
54
+ readonly _tag: 'Key';
55
+ readonly key: string;
56
+ readonly pile: string;
57
+ readonly count: number;
58
+ } | {
59
+ readonly _tag: 'Announce';
60
+ readonly message: string;
61
+ };
62
+ export type CtlMsg = {
63
+ readonly _tag: 'Key';
64
+ readonly key: string;
65
+ readonly ctrl: boolean;
66
+ readonly alt: boolean;
67
+ readonly meta: boolean;
68
+ readonly shift: boolean;
69
+ } | {
70
+ readonly _tag: 'Focus';
71
+ readonly pile: string;
72
+ } | {
73
+ readonly _tag: 'Down';
74
+ readonly pile: string | null;
75
+ readonly depth: number;
76
+ readonly x: number;
77
+ readonly y: number;
78
+ } | {
79
+ readonly _tag: 'Move';
80
+ readonly x: number;
81
+ readonly y: number;
82
+ readonly over: string | null;
83
+ } | {
84
+ readonly _tag: 'Up';
85
+ readonly over: string | null;
86
+ } | {
87
+ readonly _tag: 'Cancel';
88
+ };
89
+ /** One step: the new controller state, outputs, and a pile to focus after rendering. */
90
+ export type Step = {
91
+ readonly ctl: Ctl;
92
+ readonly outs: readonly TableOut[];
93
+ readonly focus: string | null;
94
+ };
95
+ export declare const initialCtl: (layout: TableLayout) => Ctl;
96
+ /**
97
+ * Brings the controller in line with a new layout: indexes stay in range, and a new version
98
+ * (the cards changed) drops any pick-up or press.
99
+ */
100
+ export declare const sync: (ctl: Ctl, layout: TableLayout) => Ctl;
101
+ /** The focused pile, if the table has any. */
102
+ export declare const focusedPile: (ctl: Ctl, layout: TableLayout) => PileSpec | null;
103
+ /** A card's name for the middle of a sentence: "queen of hearts", "7 of clubs". */
104
+ export declare const spoken: (card: FaceCard) => string;
105
+ /** "9 of spades", or "9 of spades and 2 more cards": the cards a pick-up of `count` takes. */
106
+ export declare const describe: (pile: PileSpec, count: number) => string;
107
+ /** The controller's reducer. */
108
+ export declare const step: (before: Ctl, layout: TableLayout, options: TableOptions, m: CtlMsg) => Step;
109
+ /** True when the table should render this layout without card flights. */
110
+ export declare const isQuiet: (ctl: Ctl, layout: TableLayout, options: TableOptions) => boolean;
@@ -0,0 +1,449 @@
1
+ import { findPile, isFaceCard, topCards } from "./layout.js";
2
+ import { cardName } from "@joystyk/kit/cards";
3
+ //#region src/controller.ts
4
+ /** Pointer travel, in CSS pixels, that turns a press into a drag (spec: Pointer). */
5
+ var DRAG_THRESHOLD = 6;
6
+ var initialCtl = (layout) => {
7
+ const first = layout.regions.findIndex((r) => r.piles.length > 0);
8
+ return {
9
+ region: Math.max(0, first),
10
+ piles: layout.regions.map(() => 0),
11
+ depth: 1,
12
+ held: null,
13
+ press: null,
14
+ version: layout.version,
15
+ settle: false,
16
+ quiet: null
17
+ };
18
+ };
19
+ var clamp = (n, min, max) => Math.min(max, Math.max(min, n));
20
+ /**
21
+ * Brings the controller in line with a new layout: indexes stay in range, and a new version
22
+ * (the cards changed) drops any pick-up or press.
23
+ */
24
+ var sync = (ctl, layout) => {
25
+ const piles = layout.regions.map((r, i) => clamp(ctl.piles[i] ?? 0, 0, Math.max(0, r.piles.length - 1)));
26
+ let region = clamp(ctl.region, 0, Math.max(0, layout.regions.length - 1));
27
+ if ((layout.regions[region]?.piles.length ?? 0) === 0) region = Math.max(0, layout.regions.findIndex((r) => r.piles.length > 0));
28
+ const moved = layout.version !== ctl.version;
29
+ const same = piles.length === ctl.piles.length && piles.every((p, i) => p === ctl.piles[i]) && region === ctl.region;
30
+ if (!moved && same) return ctl;
31
+ if (!moved) return {
32
+ ...ctl,
33
+ piles,
34
+ region
35
+ };
36
+ return {
37
+ ...ctl,
38
+ piles,
39
+ region,
40
+ depth: 1,
41
+ held: null,
42
+ press: null,
43
+ version: layout.version,
44
+ settle: false,
45
+ quiet: ctl.settle ? layout.version : null
46
+ };
47
+ };
48
+ /** The focused pile, if the table has any. */
49
+ var focusedPile = (ctl, layout) => layout.regions[ctl.region]?.piles[ctl.piles[ctl.region] ?? 0] ?? null;
50
+ /** A card's name for the middle of a sentence: "queen of hearts", "7 of clubs". */
51
+ var spoken = (card) => cardName(card).toLowerCase();
52
+ var capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
53
+ /** "9 of spades", or "9 of spades and 2 more cards": the cards a pick-up of `count` takes. */
54
+ var describe = (pile, count) => {
55
+ const [first] = topCards(pile, count);
56
+ if (first === void 0) return "nothing";
57
+ const more = count - 1;
58
+ return more === 0 ? spoken(first) : `${spoken(first)} and ${more} more ${more === 1 ? "card" : "cards"}`;
59
+ };
60
+ var none = (ctl) => ({
61
+ ctl,
62
+ outs: [],
63
+ focus: null
64
+ });
65
+ var say = (message) => ({
66
+ _tag: "Announce",
67
+ message
68
+ });
69
+ /** Picks up `count` cards from `pile` and says so. */
70
+ var pickUp = (ctl, pile, count, via) => {
71
+ const note = pile.note === void 0 ? "" : ` ${pile.note}`;
72
+ const help = via === "keyboard" ? " Choose a pile, then press Enter." : "";
73
+ return {
74
+ ctl: {
75
+ ...ctl,
76
+ held: {
77
+ from: pile.id,
78
+ count,
79
+ via
80
+ }
81
+ },
82
+ outs: [say(`Picked up ${describe(pile, count)}.${note}${help}`)],
83
+ focus: null
84
+ };
85
+ };
86
+ /** Drops the held cards on `to`: a move, or a smart move on their own pile. */
87
+ var drop = (ctl, held, to, via) => {
88
+ return {
89
+ ctl: {
90
+ ...ctl,
91
+ held: null
92
+ },
93
+ outs: [to === held.from ? {
94
+ _tag: "Smart",
95
+ from: held.from,
96
+ count: held.count,
97
+ via
98
+ } : {
99
+ _tag: "Move",
100
+ from: held.from,
101
+ to,
102
+ count: held.count,
103
+ via
104
+ }],
105
+ focus: null
106
+ };
107
+ };
108
+ var putBack = (ctl, layout) => {
109
+ const held = ctl.held;
110
+ if (held === null) return none(ctl);
111
+ const pile = findPile(layout, held.from)?.pile;
112
+ return {
113
+ ctl: {
114
+ ...ctl,
115
+ held: null
116
+ },
117
+ outs: [say(pile === void 0 ? "Put back." : `Put back ${describe(pile, held.count)}.`)],
118
+ focus: null
119
+ };
120
+ };
121
+ /** Moves the focus to pile `index` of the focused region. */
122
+ var focusAt = (ctl, layout, region, index) => {
123
+ const pile = layout.regions[region]?.piles[index];
124
+ if (pile === void 0) return none(ctl);
125
+ const piles = ctl.piles.slice();
126
+ piles[region] = index;
127
+ const changed = region !== ctl.region || index !== ctl.piles[region];
128
+ return {
129
+ ctl: {
130
+ ...ctl,
131
+ region,
132
+ piles,
133
+ depth: changed ? 1 : ctl.depth
134
+ },
135
+ outs: [],
136
+ focus: pile.id
137
+ };
138
+ };
139
+ /** Enter or Space on the focused pile. */
140
+ var activate = (ctl, pile) => {
141
+ if (ctl.held !== null) return drop(ctl, ctl.held, pile.id, "keyboard");
142
+ if (pile.action === true) return {
143
+ ctl,
144
+ outs: [{
145
+ _tag: "Activate",
146
+ pile: pile.id
147
+ }],
148
+ focus: null
149
+ };
150
+ if (pile.pickable >= 1) return pickUp(ctl, pile, clamp(ctl.depth, 1, pile.pickable), "keyboard");
151
+ return {
152
+ ctl,
153
+ outs: [say(pile.cards.length === 0 ? "Nothing to pick up: the pile is empty." : "Nothing here can be picked up.")],
154
+ focus: null
155
+ };
156
+ };
157
+ /** Up and Down: how many cards Enter picks up. */
158
+ var deepen = (ctl, pile, by) => {
159
+ if (pile.inspect === true && ctl.held === null) {
160
+ const depth = clamp(ctl.depth + by, 1, Math.max(1, pile.cards.length));
161
+ const card = pile.cards[pile.cards.length - depth];
162
+ return {
163
+ ctl: {
164
+ ...ctl,
165
+ depth
166
+ },
167
+ outs: isFaceCard(card) ? [say(capital(spoken(card)))] : [],
168
+ focus: null
169
+ };
170
+ }
171
+ if (ctl.held !== null || pile.pickable < 1) return none(ctl);
172
+ const depth = clamp(ctl.depth, 1, pile.pickable);
173
+ const next = depth + by;
174
+ if (next < 1) return {
175
+ ctl: {
176
+ ...ctl,
177
+ depth
178
+ },
179
+ outs: [],
180
+ focus: null
181
+ };
182
+ if (next > pile.pickable) return {
183
+ ctl: {
184
+ ...ctl,
185
+ depth
186
+ },
187
+ outs: pile.limit === void 0 ? [] : [say(pile.limit)],
188
+ focus: null
189
+ };
190
+ return {
191
+ ctl: {
192
+ ...ctl,
193
+ depth: next
194
+ },
195
+ outs: [say(capital(describe(pile, next)))],
196
+ focus: null
197
+ };
198
+ };
199
+ var isDigit = (key) => key.length === 1 && key >= "0" && key <= "9";
200
+ /** Navigate a geometric layout without consulting the DOM or skipping covered cards. */
201
+ var geometricFocus = (ctl, layout, key) => {
202
+ const region = layout.regions[ctl.region];
203
+ const here = focusedPile(ctl, layout)?.position;
204
+ if (region?.placement !== "free" || here === void 0) return null;
205
+ const candidates = region.piles.map((pile, index) => ({
206
+ index,
207
+ position: pile.position
208
+ })).filter((p) => p.position !== void 0);
209
+ const row = key === "ArrowUp" ? Math.max(-1, ...candidates.filter((p) => p.position.row < here.row).map((p) => p.position.row)) : key === "ArrowDown" ? Math.min(Infinity, ...candidates.filter((p) => p.position.row > here.row).map((p) => p.position.row)) : here.row;
210
+ let choices = candidates.filter((p) => p.position.row === row);
211
+ if (key === "ArrowLeft") choices = choices.filter((p) => p.position.column < here.column);
212
+ if (key === "ArrowRight") choices = choices.filter((p) => p.position.column > here.column);
213
+ choices.sort((a, b) => key === "Home" ? a.position.column - b.position.column : key === "End" ? b.position.column - a.position.column : Math.abs(a.position.column - here.column) - Math.abs(b.position.column - here.column) || a.position.column - b.position.column);
214
+ return choices[0] === void 0 ? none(ctl) : focusAt(ctl, layout, ctl.region, choices[0].index);
215
+ };
216
+ var onKey = (ctl, layout, options, m) => {
217
+ const pile = focusedPile(ctl, layout);
218
+ if (pile === null || m.alt || m.meta || m.ctrl) return none(ctl);
219
+ const count = layout.regions[ctl.region]?.piles.length ?? 0;
220
+ const index = ctl.piles[ctl.region] ?? 0;
221
+ if ([
222
+ "ArrowLeft",
223
+ "ArrowRight",
224
+ "ArrowUp",
225
+ "ArrowDown",
226
+ "Home",
227
+ "End"
228
+ ].includes(m.key)) {
229
+ const spatial = geometricFocus(ctl, layout, m.key);
230
+ if (spatial !== null) return spatial;
231
+ }
232
+ switch (m.key) {
233
+ case "ArrowLeft": return focusAt(ctl, layout, ctl.region, Math.max(0, index - 1));
234
+ case "ArrowRight": return focusAt(ctl, layout, ctl.region, Math.min(count - 1, index + 1));
235
+ case "Home": return focusAt(ctl, layout, ctl.region, 0);
236
+ case "End": return focusAt(ctl, layout, ctl.region, count - 1);
237
+ case "ArrowUp": return deepen(ctl, pile, 1);
238
+ case "ArrowDown": return deepen(ctl, pile, -1);
239
+ case "Enter":
240
+ case " ": return activate(ctl, pile);
241
+ case "Escape": return putBack(ctl, layout);
242
+ }
243
+ if (!options.singleKeys || m.shift) return none(ctl);
244
+ const key = m.key.toLowerCase();
245
+ if (isDigit(key)) {
246
+ const target = layout.regions.flatMap((r, ri) => r.piles.map((p, pi) => ({
247
+ p,
248
+ ri,
249
+ pi
250
+ }))).find(({ p }) => p.key === key);
251
+ if (target === void 0) return none(ctl);
252
+ const moved = focusAt(ctl, layout, target.ri, target.pi);
253
+ if (ctl.held === null) return layout.activateNumbers === true ? {
254
+ ...activate(moved.ctl, target.p),
255
+ focus: moved.focus
256
+ } : moved;
257
+ return {
258
+ ...drop(moved.ctl, ctl.held, target.p.id, "keyboard"),
259
+ focus: moved.focus
260
+ };
261
+ }
262
+ if (layout.keys.includes(key)) {
263
+ if (key === "n" || key === "t" || key === "w") {
264
+ const piles = layout.regions.flatMap((r, ri) => r.piles.map((p, pi) => ({
265
+ p,
266
+ ri,
267
+ pi
268
+ })));
269
+ const available = piles.filter(({ p }) => p.pickable > 0).sort((a, b) => (b.p.position?.row ?? -1) - (a.p.position?.row ?? -1) || (a.p.position?.column ?? a.pi) - (b.p.position?.column ?? b.pi));
270
+ const current = available.findIndex(({ p }) => p.id === pile.id);
271
+ const next = key === "n" ? available[(current + 1) % Math.max(1, available.length)] : piles.find(({ p }) => p.id === (key === "t" ? "turned" : "waste"));
272
+ if (next !== void 0) return focusAt(ctl, layout, next.ri, next.pi);
273
+ return none(ctl);
274
+ }
275
+ const held = ctl.held;
276
+ const out = held === null ? {
277
+ _tag: "Key",
278
+ key,
279
+ pile: pile.id,
280
+ count: clamp(ctl.depth, 1, Math.max(1, pile.pickable))
281
+ } : {
282
+ _tag: "Key",
283
+ key,
284
+ pile: held.from,
285
+ count: held.count
286
+ };
287
+ return {
288
+ ctl: {
289
+ ...ctl,
290
+ held: null
291
+ },
292
+ outs: [out],
293
+ focus: null
294
+ };
295
+ }
296
+ return none(ctl);
297
+ };
298
+ /** A click (a press released without dragging) on `pileId` at `depth`. */
299
+ var click = (ctl, layout, pileId, depth) => {
300
+ const found = pileId === null ? null : findPile(layout, pileId);
301
+ if (found === null) return putBack(ctl, layout);
302
+ const { pile } = found;
303
+ const focused = {
304
+ ...ctl,
305
+ region: found.region,
306
+ piles: ctl.piles.map((p, i) => i === found.region ? found.index : p)
307
+ };
308
+ const held = ctl.held;
309
+ if (held !== null) {
310
+ if (pile.id !== held.from) return drop(focused, held, pile.id, "click");
311
+ if (depth === held.count) return drop(focused, held, pile.id, "click");
312
+ if (depth >= 1 && depth <= pile.pickable) return pickUp({
313
+ ...focused,
314
+ depth
315
+ }, pile, depth, "click");
316
+ return putBack(focused, layout);
317
+ }
318
+ if (pile.action === true) return {
319
+ ctl: focused,
320
+ outs: [{
321
+ _tag: "Activate",
322
+ pile: pile.id
323
+ }],
324
+ focus: null
325
+ };
326
+ if (depth >= 1 && depth <= pile.pickable) return pickUp({
327
+ ...focused,
328
+ depth
329
+ }, pile, depth, "click");
330
+ return none(focused);
331
+ };
332
+ /** The controller's reducer. */
333
+ var step = (before, layout, options, m) => {
334
+ const ctl = sync(before, layout);
335
+ switch (m._tag) {
336
+ case "Key": return onKey(ctl, layout, options, m);
337
+ case "Focus": {
338
+ const found = findPile(layout, m.pile);
339
+ if (found === null) return none(ctl);
340
+ if (found.region === ctl.region && found.index === ctl.piles[found.region]) return none(ctl);
341
+ const piles = ctl.piles.slice();
342
+ piles[found.region] = found.index;
343
+ return none({
344
+ ...ctl,
345
+ region: found.region,
346
+ piles,
347
+ depth: 1
348
+ });
349
+ }
350
+ case "Down": {
351
+ const stale = ctl.press?.dragging === true;
352
+ return none({
353
+ ...ctl,
354
+ held: stale ? null : ctl.held,
355
+ press: {
356
+ pile: m.pile,
357
+ depth: m.depth,
358
+ x: m.x,
359
+ y: m.y,
360
+ dragging: false,
361
+ dx: 0,
362
+ dy: 0,
363
+ over: null
364
+ }
365
+ });
366
+ }
367
+ case "Move": {
368
+ const press = ctl.press;
369
+ if (press === null || press.pile === null) return none(ctl);
370
+ const dx = m.x - press.x;
371
+ const dy = m.y - press.y;
372
+ if (press.dragging) return none({
373
+ ...ctl,
374
+ press: {
375
+ ...press,
376
+ dx,
377
+ dy,
378
+ over: m.over
379
+ }
380
+ });
381
+ if (Math.sqrt(dx * dx + dy * dy) < 6) return none(ctl);
382
+ const pile = findPile(layout, press.pile)?.pile;
383
+ const top = pile?.cards[pile.cards.length - press.depth];
384
+ if (pile === void 0 || press.depth < 1 || press.depth > pile.pickable || !isFaceCard(top)) return none({
385
+ ...ctl,
386
+ press: null
387
+ });
388
+ return none({
389
+ ...ctl,
390
+ held: {
391
+ from: pile.id,
392
+ count: press.depth,
393
+ via: "drag"
394
+ },
395
+ press: {
396
+ ...press,
397
+ dragging: true,
398
+ dx,
399
+ dy,
400
+ over: m.over
401
+ }
402
+ });
403
+ }
404
+ case "Up": {
405
+ const press = ctl.press;
406
+ if (press === null) return none(ctl);
407
+ const released = {
408
+ ...ctl,
409
+ press: null
410
+ };
411
+ if (!press.dragging) return click(released, layout, press.pile, press.depth);
412
+ const held = ctl.held;
413
+ const to = m.over;
414
+ if (held === null || to === null || to === held.from) return none({
415
+ ...released,
416
+ held: null
417
+ });
418
+ return {
419
+ ctl: {
420
+ ...released,
421
+ held: null,
422
+ settle: true
423
+ },
424
+ outs: [{
425
+ _tag: "Move",
426
+ from: held.from,
427
+ to,
428
+ count: held.count,
429
+ via: "drag"
430
+ }],
431
+ focus: null
432
+ };
433
+ }
434
+ case "Cancel": {
435
+ const dragging = ctl.press?.dragging === true;
436
+ return none({
437
+ ...ctl,
438
+ press: null,
439
+ held: dragging ? null : ctl.held
440
+ });
441
+ }
442
+ }
443
+ };
444
+ /** True when the table should render this layout without card flights. */
445
+ var isQuiet = (ctl, layout, options) => options.calm || ctl.quiet === layout.version;
446
+ //#endregion
447
+ export { DRAG_THRESHOLD, describe, focusedPile, initialCtl, isQuiet, spoken, step, sync };
448
+
449
+ //# sourceMappingURL=controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.js","names":[],"sources":["../src/controller.ts"],"sourcesContent":["// The pile interaction controller: a pure state machine that turns pointer and keyboard\n// input on a card table into moves (klondike.md, \"Interaction and accessibility\"). It knows\n// no rules; the layout says how many cards each pile lets go of and where they may land. The\n// engine still decides every move, so a move the controller sends may be refused.\n//\n// - Pointer: press a face-up card and move 6 CSS pixels to drag it with the cards on top of\n// it; drop anywhere over a pile. A click without dragging picks the cards up; a click on\n// another pile moves them there; a second click on the same card is a smart move (so a\n// double-click or double-tap is one); a click elsewhere puts them back.\n// - Keyboard: one Tab stop per region (a roving tabindex), Left and Right between piles, Up\n// and Down for how many cards, Enter or Space to pick up and to drop (on the cards' own\n// pile: a smart move), Escape to put back, number keys for numbered piles, and the layout's\n// single-key shortcuts, all only while `singleKeys` is on (WCAG 2.1.4).\n//\n// Everything it says goes out as `Announce` outputs; the game passes them to the frame's\n// live region.\nimport { cardName } from '@joystyk/kit/cards';\nimport {\n findPile,\n isFaceCard,\n topCards,\n type FaceCard,\n type PileSpec,\n type TableLayout,\n type TableOptions,\n} from './layout.ts';\n\n/** Pointer travel, in CSS pixels, that turns a press into a drag (spec: Pointer). */\nexport const DRAG_THRESHOLD = 6;\n\nexport type Via = 'drag' | 'click' | 'keyboard';\n\n/** Cards the player has picked up: the top `count` of pile `from`. */\nexport type Held = { readonly from: string; readonly count: number; readonly via: Via };\n\n/** A pointer pressed on a pile; `depth` counts cards from the top (0: the pile, not a card). */\nexport type Press = {\n readonly pile: string | null;\n readonly depth: number;\n readonly x: number;\n readonly y: number;\n readonly dragging: boolean;\n readonly dx: number;\n readonly dy: number;\n readonly over: string | null;\n};\n\nexport type Ctl = {\n /** The focused region. */\n readonly region: number;\n /** The remembered pile of each region: the one Tab reaches (roving tabindex). */\n readonly piles: readonly number[];\n /** Cards counted from the top of the focused pile: what Enter picks up. */\n readonly depth: number;\n readonly held: Held | null;\n readonly press: Press | null;\n /** The layout version last seen. */\n readonly version: number;\n /** A drag just dropped: the next layout lands without card flights. */\n readonly settle: boolean;\n /** The layout version that renders without card flights. */\n readonly quiet: number | null;\n};\n\n/** What the table tells the game. */\nexport type TableOut =\n | { readonly _tag: 'Move'; readonly from: string; readonly to: string; readonly count: number; readonly via: Via }\n | { readonly _tag: 'Smart'; readonly from: string; readonly count: number; readonly via: Via }\n | { readonly _tag: 'Activate'; readonly pile: string }\n | { readonly _tag: 'Key'; readonly key: string; readonly pile: string; readonly count: number }\n | { readonly _tag: 'Announce'; readonly message: string };\n\nexport type CtlMsg =\n | {\n readonly _tag: 'Key';\n readonly key: string;\n readonly ctrl: boolean;\n readonly alt: boolean;\n readonly meta: boolean;\n readonly shift: boolean;\n }\n | { readonly _tag: 'Focus'; readonly pile: string }\n | {\n readonly _tag: 'Down';\n readonly pile: string | null;\n readonly depth: number;\n readonly x: number;\n readonly y: number;\n }\n | { readonly _tag: 'Move'; readonly x: number; readonly y: number; readonly over: string | null }\n | { readonly _tag: 'Up'; readonly over: string | null }\n | { readonly _tag: 'Cancel' };\n\n/** One step: the new controller state, outputs, and a pile to focus after rendering. */\nexport type Step = { readonly ctl: Ctl; readonly outs: readonly TableOut[]; readonly focus: string | null };\n\nexport const initialCtl = (layout: TableLayout): Ctl => {\n const first = layout.regions.findIndex((r) => r.piles.length > 0);\n return {\n region: Math.max(0, first),\n piles: layout.regions.map(() => 0),\n depth: 1,\n held: null,\n press: null,\n version: layout.version,\n settle: false,\n quiet: null,\n };\n};\n\nconst clamp = (n: number, min: number, max: number): number => Math.min(max, Math.max(min, n));\n\n/**\n * Brings the controller in line with a new layout: indexes stay in range, and a new version\n * (the cards changed) drops any pick-up or press.\n */\nexport const sync = (ctl: Ctl, layout: TableLayout): Ctl => {\n const piles = layout.regions.map((r, i) => clamp(ctl.piles[i] ?? 0, 0, Math.max(0, r.piles.length - 1)));\n let region = clamp(ctl.region, 0, Math.max(0, layout.regions.length - 1));\n if ((layout.regions[region]?.piles.length ?? 0) === 0)\n region = Math.max(\n 0,\n layout.regions.findIndex((r) => r.piles.length > 0),\n );\n const moved = layout.version !== ctl.version;\n const same = piles.length === ctl.piles.length && piles.every((p, i) => p === ctl.piles[i]) && region === ctl.region;\n if (!moved && same) return ctl;\n if (!moved) return { ...ctl, piles, region };\n return {\n ...ctl,\n piles,\n region,\n depth: 1,\n held: null,\n press: null,\n version: layout.version,\n settle: false,\n quiet: ctl.settle ? layout.version : null,\n };\n};\n\n/** The focused pile, if the table has any. */\nexport const focusedPile = (ctl: Ctl, layout: TableLayout): PileSpec | null =>\n layout.regions[ctl.region]?.piles[ctl.piles[ctl.region] ?? 0] ?? null;\n\n/** A card's name for the middle of a sentence: \"queen of hearts\", \"7 of clubs\". */\nexport const spoken = (card: FaceCard): string => cardName(card).toLowerCase();\n\nconst capital = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);\n\n/** \"9 of spades\", or \"9 of spades and 2 more cards\": the cards a pick-up of `count` takes. */\nexport const describe = (pile: PileSpec, count: number): string => {\n const [first] = topCards(pile, count);\n if (first === undefined) return 'nothing';\n const more = count - 1;\n return more === 0 ? spoken(first) : `${spoken(first)} and ${more} more ${more === 1 ? 'card' : 'cards'}`;\n};\n\nconst none = (ctl: Ctl): Step => ({ ctl, outs: [], focus: null });\n\nconst say = (message: string): TableOut => ({ _tag: 'Announce', message });\n\n/** Picks up `count` cards from `pile` and says so. */\nconst pickUp = (ctl: Ctl, pile: PileSpec, count: number, via: Via): Step => {\n const note = pile.note === undefined ? '' : ` ${pile.note}`;\n const help = via === 'keyboard' ? ' Choose a pile, then press Enter.' : '';\n return {\n ctl: { ...ctl, held: { from: pile.id, count, via } },\n outs: [say(`Picked up ${describe(pile, count)}.${note}${help}`)],\n focus: null,\n };\n};\n\n/** Drops the held cards on `to`: a move, or a smart move on their own pile. */\nconst drop = (ctl: Ctl, held: Held, to: string, via: Via): Step => {\n const cleared = { ...ctl, held: null };\n const out: TableOut =\n to === held.from\n ? { _tag: 'Smart', from: held.from, count: held.count, via }\n : { _tag: 'Move', from: held.from, to, count: held.count, via };\n return { ctl: cleared, outs: [out], focus: null };\n};\n\nconst putBack = (ctl: Ctl, layout: TableLayout): Step => {\n const held = ctl.held;\n if (held === null) return none(ctl);\n const pile = findPile(layout, held.from)?.pile;\n return {\n ctl: { ...ctl, held: null },\n outs: [say(pile === undefined ? 'Put back.' : `Put back ${describe(pile, held.count)}.`)],\n focus: null,\n };\n};\n\n/** Moves the focus to pile `index` of the focused region. */\nconst focusAt = (ctl: Ctl, layout: TableLayout, region: number, index: number): Step => {\n const pile = layout.regions[region]?.piles[index];\n if (pile === undefined) return none(ctl);\n const piles = ctl.piles.slice();\n piles[region] = index;\n const changed = region !== ctl.region || index !== ctl.piles[region];\n return { ctl: { ...ctl, region, piles, depth: changed ? 1 : ctl.depth }, outs: [], focus: pile.id };\n};\n\n/** Enter or Space on the focused pile. */\nconst activate = (ctl: Ctl, pile: PileSpec): Step => {\n if (ctl.held !== null) return drop(ctl, ctl.held, pile.id, 'keyboard');\n if (pile.action === true) return { ctl, outs: [{ _tag: 'Activate', pile: pile.id }], focus: null };\n if (pile.pickable >= 1) return pickUp(ctl, pile, clamp(ctl.depth, 1, pile.pickable), 'keyboard');\n return {\n ctl,\n outs: [say(pile.cards.length === 0 ? 'Nothing to pick up: the pile is empty.' : 'Nothing here can be picked up.')],\n focus: null,\n };\n};\n\n/** Up and Down: how many cards Enter picks up. */\nconst deepen = (ctl: Ctl, pile: PileSpec, by: 1 | -1): Step => {\n if (pile.inspect === true && ctl.held === null) {\n const depth = clamp(ctl.depth + by, 1, Math.max(1, pile.cards.length));\n const card = pile.cards[pile.cards.length - depth];\n return { ctl: { ...ctl, depth }, outs: isFaceCard(card) ? [say(capital(spoken(card)))] : [], focus: null };\n }\n if (ctl.held !== null || pile.pickable < 1) return none(ctl);\n const depth = clamp(ctl.depth, 1, pile.pickable);\n const next = depth + by;\n if (next < 1) return { ctl: { ...ctl, depth }, outs: [], focus: null };\n if (next > pile.pickable)\n return { ctl: { ...ctl, depth }, outs: pile.limit === undefined ? [] : [say(pile.limit)], focus: null };\n return { ctl: { ...ctl, depth: next }, outs: [say(capital(describe(pile, next)))], focus: null };\n};\n\nconst isDigit = (key: string): boolean => key.length === 1 && key >= '0' && key <= '9';\n\n/** Navigate a geometric layout without consulting the DOM or skipping covered cards. */\nconst geometricFocus = (ctl: Ctl, layout: TableLayout, key: string): Step | null => {\n const region = layout.regions[ctl.region];\n const here = focusedPile(ctl, layout)?.position;\n if (region?.placement !== 'free' || here === undefined) return null;\n const candidates = region.piles\n .map((pile, index) => ({ index, position: pile.position }))\n .filter((p) => p.position !== undefined);\n const row =\n key === 'ArrowUp'\n ? Math.max(-1, ...candidates.filter((p) => p.position!.row < here.row).map((p) => p.position!.row))\n : key === 'ArrowDown'\n ? Math.min(Infinity, ...candidates.filter((p) => p.position!.row > here.row).map((p) => p.position!.row))\n : here.row;\n let choices = candidates.filter((p) => p.position!.row === row);\n if (key === 'ArrowLeft') choices = choices.filter((p) => p.position!.column < here.column);\n if (key === 'ArrowRight') choices = choices.filter((p) => p.position!.column > here.column);\n choices.sort((a, b) =>\n key === 'Home'\n ? a.position!.column - b.position!.column\n : key === 'End'\n ? b.position!.column - a.position!.column\n : Math.abs(a.position!.column - here.column) - Math.abs(b.position!.column - here.column) ||\n a.position!.column - b.position!.column,\n );\n return choices[0] === undefined ? none(ctl) : focusAt(ctl, layout, ctl.region, choices[0].index);\n};\n\nconst onKey = (ctl: Ctl, layout: TableLayout, options: TableOptions, m: Extract<CtlMsg, { _tag: 'Key' }>): Step => {\n const pile = focusedPile(ctl, layout);\n if (pile === null || m.alt || m.meta || m.ctrl) return none(ctl);\n const region = layout.regions[ctl.region];\n const count = region?.piles.length ?? 0;\n const index = ctl.piles[ctl.region] ?? 0;\n if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(m.key)) {\n const spatial = geometricFocus(ctl, layout, m.key);\n if (spatial !== null) return spatial;\n }\n switch (m.key) {\n case 'ArrowLeft':\n return focusAt(ctl, layout, ctl.region, Math.max(0, index - 1));\n case 'ArrowRight':\n return focusAt(ctl, layout, ctl.region, Math.min(count - 1, index + 1));\n case 'Home':\n return focusAt(ctl, layout, ctl.region, 0);\n case 'End':\n return focusAt(ctl, layout, ctl.region, count - 1);\n case 'ArrowUp':\n return deepen(ctl, pile, 1);\n case 'ArrowDown':\n return deepen(ctl, pile, -1);\n case 'Enter':\n case ' ':\n return activate(ctl, pile);\n case 'Escape':\n return putBack(ctl, layout);\n }\n if (!options.singleKeys || m.shift) return none(ctl);\n const key = m.key.toLowerCase();\n if (isDigit(key)) {\n const target = layout.regions\n .flatMap((r, ri) => r.piles.map((p, pi) => ({ p, ri, pi })))\n .find(({ p }) => p.key === key);\n if (target === undefined) return none(ctl);\n const moved = focusAt(ctl, layout, target.ri, target.pi);\n if (ctl.held === null)\n return layout.activateNumbers === true ? { ...activate(moved.ctl, target.p), focus: moved.focus } : moved;\n const dropped = drop(moved.ctl, ctl.held, target.p.id, 'keyboard');\n return { ...dropped, focus: moved.focus };\n }\n if (layout.keys.includes(key)) {\n if (key === 'n' || key === 't' || key === 'w') {\n const piles = layout.regions.flatMap((r, ri) => r.piles.map((p, pi) => ({ p, ri, pi })));\n const available = piles\n .filter(({ p }) => p.pickable > 0)\n .sort(\n (a, b) =>\n (b.p.position?.row ?? -1) - (a.p.position?.row ?? -1) ||\n (a.p.position?.column ?? a.pi) - (b.p.position?.column ?? b.pi),\n );\n const current = available.findIndex(({ p }) => p.id === pile.id);\n const next =\n key === 'n'\n ? available[(current + 1) % Math.max(1, available.length)]\n : piles.find(({ p }) => p.id === (key === 't' ? 'turned' : 'waste'));\n if (next !== undefined) return focusAt(ctl, layout, next.ri, next.pi);\n return none(ctl);\n }\n const held = ctl.held;\n const out: TableOut =\n held === null\n ? { _tag: 'Key', key, pile: pile.id, count: clamp(ctl.depth, 1, Math.max(1, pile.pickable)) }\n : { _tag: 'Key', key, pile: held.from, count: held.count };\n return { ctl: { ...ctl, held: null }, outs: [out], focus: null };\n }\n return none(ctl);\n};\n\n/** A click (a press released without dragging) on `pileId` at `depth`. */\nconst click = (ctl: Ctl, layout: TableLayout, pileId: string | null, depth: number): Step => {\n const found = pileId === null ? null : findPile(layout, pileId);\n if (found === null) return putBack(ctl, layout);\n const { pile } = found;\n const focused = {\n ...ctl,\n region: found.region,\n piles: ctl.piles.map((p, i) => (i === found.region ? found.index : p)),\n };\n const held = ctl.held;\n if (held !== null) {\n if (pile.id !== held.from) return drop(focused, held, pile.id, 'click');\n if (depth === held.count) return drop(focused, held, pile.id, 'click');\n if (depth >= 1 && depth <= pile.pickable) return pickUp({ ...focused, depth }, pile, depth, 'click');\n return putBack(focused, layout);\n }\n if (pile.action === true) return { ctl: focused, outs: [{ _tag: 'Activate', pile: pile.id }], focus: null };\n if (depth >= 1 && depth <= pile.pickable) return pickUp({ ...focused, depth }, pile, depth, 'click');\n return none(focused);\n};\n\n/** The controller's reducer. */\nexport const step = (before: Ctl, layout: TableLayout, options: TableOptions, m: CtlMsg): Step => {\n const ctl = sync(before, layout);\n switch (m._tag) {\n case 'Key':\n return onKey(ctl, layout, options, m);\n case 'Focus': {\n const found = findPile(layout, m.pile);\n if (found === null) return none(ctl);\n const same = found.region === ctl.region && found.index === ctl.piles[found.region];\n if (same) return none(ctl);\n const piles = ctl.piles.slice();\n piles[found.region] = found.index;\n return none({ ...ctl, region: found.region, piles, depth: 1 });\n }\n case 'Down': {\n // A drag whose release never arrived (the pointer left the window) is dropped back first.\n const stale = ctl.press?.dragging === true;\n return none({\n ...ctl,\n held: stale ? null : ctl.held,\n press: { pile: m.pile, depth: m.depth, x: m.x, y: m.y, dragging: false, dx: 0, dy: 0, over: null },\n });\n }\n case 'Move': {\n const press = ctl.press;\n if (press === null || press.pile === null) return none(ctl);\n const dx = m.x - press.x;\n const dy = m.y - press.y;\n if (press.dragging) return none({ ...ctl, press: { ...press, dx, dy, over: m.over } });\n if (Math.sqrt(dx * dx + dy * dy) < DRAG_THRESHOLD) return none(ctl);\n const pile = findPile(layout, press.pile)?.pile;\n const top = pile?.cards[pile.cards.length - press.depth];\n if (pile === undefined || press.depth < 1 || press.depth > pile.pickable || !isFaceCard(top))\n return none({ ...ctl, press: null });\n return none({\n ...ctl,\n held: { from: pile.id, count: press.depth, via: 'drag' },\n press: { ...press, dragging: true, dx, dy, over: m.over },\n });\n }\n case 'Up': {\n const press = ctl.press;\n if (press === null) return none(ctl);\n const released = { ...ctl, press: null };\n if (!press.dragging) return click(released, layout, press.pile, press.depth);\n const held = ctl.held;\n const to = m.over;\n if (held === null || to === null || to === held.from) return none({ ...released, held: null });\n return {\n ctl: { ...released, held: null, settle: true },\n outs: [{ _tag: 'Move', from: held.from, to, count: held.count, via: 'drag' }],\n focus: null,\n };\n }\n case 'Cancel': {\n const dragging = ctl.press?.dragging === true;\n return none({ ...ctl, press: null, held: dragging ? null : ctl.held });\n }\n }\n};\n\n/** True when the table should render this layout without card flights. */\nexport const isQuiet = (ctl: Ctl, layout: TableLayout, options: TableOptions): boolean =>\n options.calm || ctl.quiet === layout.version;\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const faceStyles: import("@gyral/core").StyleSource;