@joystyk/cards-ui 0.0.0-stage → 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 +22 -0
- package/LICENSE +21 -0
- package/NOTICE +36 -0
- package/README.md +32 -2
- package/dist/controller.d.ts +110 -0
- package/dist/controller.js +449 -0
- package/dist/controller.js.map +1 -0
- package/dist/face-styles.d.ts +1 -0
- package/dist/face-styles.js +106 -0
- package/dist/face-styles.js.map +1 -0
- package/dist/faces.d.ts +11 -0
- package/dist/faces.js +403 -0
- package/dist/faces.js.map +1 -0
- package/dist/hand-styles.d.ts +1 -0
- package/dist/hand-styles.js +121 -0
- package/dist/hand-styles.js.map +1 -0
- package/dist/hand.d.ts +100 -0
- package/dist/hand.js +480 -0
- package/dist/hand.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/layout.d.ts +122 -0
- package/dist/layout.js +35 -0
- package/dist/layout.js.map +1 -0
- package/dist/solitaire.d.ts +155 -0
- package/dist/solitaire.js +1508 -0
- package/dist/solitaire.js.map +1 -0
- package/dist/styles.d.ts +1 -0
- package/dist/styles.js +452 -0
- package/dist/styles.js.map +1 -0
- package/dist/table.d.ts +28 -0
- package/dist/table.js +629 -0
- package/dist/table.js.map +1 -0
- package/dist/tiles.d.ts +3 -0
- package/dist/tiles.js +157 -0
- package/dist/tiles.js.map +1 -0
- package/dist/worker-setup.d.ts +7 -0
- package/dist/worker-setup.js +45 -0
- package/dist/worker-setup.js.map +1 -0
- package/package.json +67 -3
package/dist/hand.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hand.js","names":["_gyral$compiled"],"sources":["../src/hand.ts"],"sourcesContent":["import type { ChildValue } from '@gyral/core';\n// <jy-card-hand>: the player's own hand in a multiplayer card game (hearts.md, \"Shared needs\"\n// and \"Keyboard and pointer controls\"). The hand is one Tab stop, a toolbar of card buttons\n// with a roving focus: Left and Right move along it, Home and End jump to its ends. Each card\n// is a real <button> named in full (\"Queen of spades\"), never by glyph.\n//\n// Two modes, chosen by the game for the current decision:\n// - `play`: Enter, Space or a tap plays the card. A card that can't be played now is\n// `aria-disabled`; activating it reports `Refused`, so the game can say why.\n// - `choose`: a multi-card choice such as Hearts' pass. Space or a tap toggles a card\n// (`aria-pressed`), and Enter reports `Commit`.\n//\n// The hand knows no rules: the game passes the cards with what each may do, and the engine\n// still decides. Other keys bubble to the game, which adds its own (H, T, S…). Cards are\n// fanned and overlap; each keeps at least a 44 px strip, and the fan wraps onto a second row\n// on narrow screens rather than shrink below it. Focus follows the hand when a played card\n// leaves it.\nimport { define, each, focus, html, intentsOf, outputs, prop, type Command } from '@gyral/core';\nimport { cardName, type Rank, type Suit } from '@joystyk/kit/cards';\nimport { cardFace, suitDefs } from './faces.ts';\nimport { tileName, type TileFace } from '@joystyk/kit/tiles';\nimport { tileFace } from './tiles.ts';\nimport { handStyles } from './hand-styles.ts';\nimport { faceStyles } from './face-styles.ts';\n\n/** One card in the hand, with what the player may do with it now. */\nexport type HandCard = (\n | {\n readonly tile?: never;\n readonly id: string;\n readonly rank: Rank;\n readonly suit: Suit;\n }\n | { readonly tile: TileFace; readonly rank?: never; readonly suit?: never }\n) & {\n readonly id: string;\n /** It may be played (or chosen) now. A card that may not is aria-disabled. */\n readonly playable: boolean;\n /** Chosen for a multi-card choice (aria-pressed), drawn lifted. */\n readonly chosen?: boolean;\n /** Added to the card's name and marked on its face: \"just received\". */\n readonly note?: string;\n};\n\nexport type HandMode = 'play' | 'choose';\n\nexport type HandLayout = {\n /** The hand's name: \"Your hand, 13 cards\". */\n readonly label: string;\n /** In display order. */\n readonly cards: readonly HandCard[];\n readonly mode: HandMode;\n};\n\nexport type HandOptions = {\n /** Reduced motion: chosen cards lift without a transition. */\n readonly calm: boolean;\n /** Blue diamonds and green clubs. */\n readonly fourColor: boolean;\n readonly numbers?: boolean;\n readonly draggable?: boolean;\n};\n\n/** What the hand tells the game. */\nexport type HandOut =\n | { readonly _tag: 'Play'; readonly id: string }\n | { readonly _tag: 'Refused'; readonly id: string }\n | { readonly _tag: 'Toggle'; readonly id: string }\n | { readonly _tag: 'Commit' };\n\nexport type HandState = {\n /** The index of the card the Tab stop is on. */\n readonly focus: number;\n /** The ids last rendered, to keep the focus on the same card when the hand changes. */\n readonly ids: readonly string[];\n /** Focus is inside the hand, so a card that leaves it hands the focus on. */\n readonly inside: boolean;\n};\n\nexport type HandMsg =\n | { readonly _tag: 'Key'; readonly key: string }\n | { readonly _tag: 'Press'; readonly id: string }\n | { readonly _tag: 'Enter'; readonly id: string }\n | { readonly _tag: 'Leave' };\n\nexport const EMPTY_HAND: HandLayout = { label: 'Your hand', cards: [], mode: 'play' };\nexport const DEFAULT_HAND_OPTIONS: HandOptions = { calm: false, fourColor: false };\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;\n\nexport const isHandLayout = (value: unknown): value is HandLayout =>\n isRecord(value) &&\n typeof value['label'] === 'string' &&\n Array.isArray(value['cards']) &&\n (value['mode'] === 'play' || value['mode'] === 'choose');\n\nexport const isHandOptions = (value: unknown): value is HandOptions =>\n isRecord(value) && typeof value['calm'] === 'boolean' && typeof value['fourColor'] === 'boolean';\n\n/** A card's accessible name: \"Queen of spades\", \"9 of hearts, just received\". */\nexport const handCardLabel = (card: HandCard): string =>\n card.note === undefined\n ? card.tile === undefined\n ? cardName(card)\n : tileName(card.tile)\n : `${card.tile === undefined ? cardName(card) : tileName(card.tile)}, ${card.note}`;\n\nconst clamp = (n: number, max: number): number => Math.max(0, Math.min(max, n));\n\nexport const initialHand = (layout: HandLayout): HandState => ({\n focus: 0,\n ids: layout.cards.map((c) => c.id),\n inside: false,\n});\n\n/** One step of the hand's controller: the new state, outputs, and a card to focus after rendering. */\nexport type HandStep = { readonly state: HandState; readonly outs: readonly HandOut[]; readonly focus: string | null };\n\n/**\n * Brings the state in line with a new hand. The focus stays on the same card if it is still\n * there, otherwise on the card now at its place; if focus was inside the hand and its card\n * left, the new card takes the focus.\n */\nexport const syncHand = (s: HandState, layout: HandLayout): HandStep => {\n const ids = layout.cards.map((c) => c.id);\n const same = ids.length === s.ids.length && ids.every((id, k) => id === s.ids[k]);\n if (same) return { state: s, outs: [], focus: null };\n const was = s.ids[s.focus];\n const kept = was === undefined ? -1 : ids.indexOf(was);\n const index = kept >= 0 ? kept : clamp(s.focus, ids.length - 1);\n const target = ids[index] ?? null;\n const refocus = s.inside && kept < 0 && target !== null;\n return {\n state: { focus: index, ids, inside: s.inside && target !== null },\n outs: [],\n focus: refocus ? target : null,\n };\n};\n\nconst activate = (layout: HandLayout, card: HandCard, via: 'enter' | 'space' | 'pointer'): HandOut[] => {\n if (layout.mode === 'choose') {\n if (via === 'enter') return [{ _tag: 'Commit' }];\n return card.playable ? [{ _tag: 'Toggle', id: card.id }] : [{ _tag: 'Refused', id: card.id }];\n }\n return card.playable ? [{ _tag: 'Play', id: card.id }] : [{ _tag: 'Refused', id: card.id }];\n};\n\n/** The hand's reducer, pure, so its behavior is tested without a DOM. */\nexport const handStep = (before: HandState, layout: HandLayout, m: HandMsg): HandStep => {\n const s = syncHand(before, layout).state;\n const cards = layout.cards;\n const last = cards.length - 1;\n switch (m._tag) {\n case 'Key': {\n const card = cards[s.focus];\n if (card === undefined) return { state: s, outs: [], focus: null };\n const moveTo = (index: number): HandStep => {\n const next = clamp(index, last);\n return { state: { ...s, focus: next, inside: true }, outs: [], focus: cards[next]!.id };\n };\n switch (m.key) {\n case 'ArrowLeft':\n return moveTo(s.focus - 1);\n case 'ArrowRight':\n return moveTo(s.focus + 1);\n case 'Home':\n return moveTo(0);\n case 'End':\n return moveTo(last);\n case 'Enter':\n return { state: s, outs: activate(layout, card, 'enter'), focus: null };\n case ' ':\n return { state: s, outs: activate(layout, card, 'space'), focus: null };\n default:\n return { state: s, outs: [], focus: null };\n }\n }\n case 'Press': {\n const index = cards.findIndex((c) => c.id === m.id);\n const card = cards[index];\n if (card === undefined) return { state: s, outs: [], focus: null };\n return { state: { ...s, focus: index, inside: true }, outs: activate(layout, card, 'pointer'), focus: null };\n }\n case 'Enter': {\n const index = cards.findIndex((c) => c.id === m.id);\n return { state: { ...s, focus: index >= 0 ? index : s.focus, inside: true }, outs: [], focus: null };\n }\n case 'Leave':\n return { state: { ...s, inside: false }, outs: [], focus: null };\n }\n};\n\n/** Keys the hand handles; their default actions (scrolling, a button's click) are suppressed. */\nconst HAND_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' ']);\n\nconst emit = outputs<HandOut>();\nconst i = intentsOf<typeof CardHand>();\n\ntype CardView = {\n readonly focused: boolean;\n readonly mode: HandMode;\n readonly calm: boolean;\n readonly numbers: boolean;\n readonly draggable: boolean;\n};\n\nconst CardButton = (card: HandCard, v: CardView) =>\n html`<button\n type=\"button\"\n class=\"slot ${card.chosen === true ? 'chosen' : ''} ${card.playable ? '' : 'off'} ${card.note === undefined ? '' : 'noted'}\"\n data-card=${card.id}\n draggable=${v.draggable ? 'true' : 'false'}\n tabindex=${v.focused ? 0 : -1}\n aria-label=${handCardLabel(card)}\n aria-disabled=${card.playable ? null : 'true'}\n aria-pressed=${v.mode === 'choose' ? String(card.chosen === true) : null}\n data-intent=${i.Press}\n >\n <span class=\"cface s-${card.suit ?? 'tile'}\">${card.tile === undefined ? cardFace(card.rank, card.suit) : tileFace(card.tile, v.numbers)}</span>\n </button>`;\n\nconst run = (s: HandState, layout: HandLayout, m: HandMsg): HandState | readonly [HandState, Command<HandMsg>[]] => {\n const step = handStep(s, layout, m);\n const commands: Command<HandMsg>[] = step.outs.map((out) => emit(out));\n if (step.focus !== null) commands.push(focus(`[data-card=\"${step.focus}\"]`));\n return commands.length === 0 ? step.state : [step.state, commands];\n};\n\nexport const CardHand = define<\n HandState,\n HandMsg,\n { readonly layout: HandLayout; readonly options: HandOptions },\n HandOut\n>()('jy-card-hand', {\n props: {\n layout: prop.value(isHandLayout, { default: EMPTY_HAND }),\n options: prop.value(isHandOptions, { default: DEFAULT_HAND_OPTIONS }),\n },\n init: (p) => initialHand(p.layout),\n intent: {\n Key: ({ key, event }) => {\n if (\n key === undefined ||\n !(event instanceof KeyboardEvent) ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.shiftKey\n )\n return undefined;\n if (!HAND_KEYS.has(key)) return undefined;\n event.preventDefault();\n return { _tag: 'Key', key };\n },\n Press: ({ event }) => {\n const id =\n event.target instanceof Element ? event.target.closest('[data-card]')?.getAttribute('data-card') : null;\n return typeof id === 'string' ? { _tag: 'Press', id } : undefined;\n },\n Enter: ({ event }) => {\n const id =\n event.target instanceof Element ? event.target.closest('[data-card]')?.getAttribute('data-card') : null;\n return typeof id === 'string' ? { _tag: 'Enter', id } : undefined;\n },\n Leave: ({ event, target }) => {\n const next = event instanceof FocusEvent ? event.relatedTarget : null;\n return next instanceof Node && target instanceof Node && target.contains(next) ? undefined : { _tag: 'Leave' };\n },\n },\n update: {\n Key: (s, m, { props: p }) => run(s, p.layout, m),\n Press: (s, m, { props: p }) => run(s, p.layout, m),\n Enter: (s, m, { props: p }) => run(s, p.layout, m),\n Leave: (s, m, { props: p }) => run(s, p.layout, m),\n PropsChanged: (s, { props: p }) => {\n const step = syncHand(s, p.layout);\n return step.focus === null ? step.state : [step.state, [focus(`[data-card=\"${step.focus}\"]`)]];\n },\n },\n view: (s, _i, { props: p }): ChildValue => {\n const { layout, options } = p;\n const focusIndex = clamp(s.focus, layout.cards.length - 1);\n return html`${suitDefs()}\n <div class=\"h-enter\" data-intent=${i.Enter} data-intent-on=\"focusin\">\n <div class=\"h-leave\" data-intent=${i.Leave} data-intent-on=\"focusout\">\n <div\n class=\"hand ${layout.cards.some((c) => c.tile !== undefined) ? 'tiles' : ''} n-${Math.min(layout.cards.length, 32)} ${options.fourColor ? 'four' : ''} ${options.calm ? 'calm' : ''} mode-${layout.mode}\"\n role=\"toolbar\"\n aria-label=${layout.label}\n data-intent=${i.Key}\n data-intent-on=\"keydown\"\n >\n ${each(\n layout.cards,\n (card) => card.id,\n CardButton,\n (card): CardView => ({\n focused: layout.cards[focusIndex]?.id === card.id,\n mode: layout.mode,\n calm: options.calm,\n numbers: options.numbers === true,\n draggable: options.draggable === true,\n }),\n )}\n </div>\n </div>\n </div>`;\n },\n styles: [faceStyles, handStyles],\n});\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'jy-card-hand': InstanceType<typeof CardHand>;\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { CardTable, FLIGHT_MS, density, shownCards, type TableMsg, type TableProps, type TableState } from './table.js';
|
|
2
|
+
export { DRAG_THRESHOLD, describe, focusedPile, initialCtl, spoken, step, sync, type Ctl, type CtlMsg, type Held, type Press, type Step, type TableOut, type Via, } from './controller.js';
|
|
3
|
+
export { DEFAULT_OPTIONS, EMPTY_LAYOUT, allPiles, findPile, isFaceCard, isTableLayout, isTableOptions, isTarget, topCards, type Back, type DownCard, type FaceCard, type Fan, type HintSpec, type PileCard, type PileSpec, type Place, type RegionSpec, type TableLayout, type TableOptions, } from './layout.js';
|
|
4
|
+
export { PIPS, SUIT_PATHS, cardFace, rankLabel, suitDefs } from './faces.js';
|
|
5
|
+
export { tableStyles } from './styles.js';
|
|
6
|
+
export { faceStyles } from './face-styles.js';
|
|
7
|
+
export { CardHand, DEFAULT_HAND_OPTIONS, EMPTY_HAND, handCardLabel, handStep, initialHand, isHandLayout, isHandOptions, syncHand, type HandCard, type HandLayout, type HandMode, type HandMsg, type HandOptions, type HandOut, type HandState, type HandStep, } from './hand.js';
|
|
8
|
+
export { tileFace } from './tiles.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DEFAULT_OPTIONS, EMPTY_LAYOUT, allPiles, findPile, isFaceCard, isTableLayout, isTableOptions, isTarget, topCards } from "./layout.js";
|
|
2
|
+
import { DRAG_THRESHOLD, describe, focusedPile, initialCtl, spoken, step, sync } from "./controller.js";
|
|
3
|
+
import { faceStyles } from "./face-styles.js";
|
|
4
|
+
import { PIPS, SUIT_PATHS, cardFace, rankLabel, suitDefs } from "./faces.js";
|
|
5
|
+
import { tileFace } from "./tiles.js";
|
|
6
|
+
import { CardHand, DEFAULT_HAND_OPTIONS, EMPTY_HAND, handCardLabel, handStep, initialHand, isHandLayout, isHandOptions, syncHand } from "./hand.js";
|
|
7
|
+
import { tableStyles } from "./styles.js";
|
|
8
|
+
import { CardTable, FLIGHT_MS, density, shownCards } from "./table.js";
|
|
9
|
+
export { CardHand, CardTable, DEFAULT_HAND_OPTIONS, DEFAULT_OPTIONS, DRAG_THRESHOLD, EMPTY_HAND, EMPTY_LAYOUT, FLIGHT_MS, PIPS, SUIT_PATHS, allPiles, cardFace, density, describe, faceStyles, findPile, focusedPile, handCardLabel, handStep, initialCtl, initialHand, isFaceCard, isHandLayout, isHandOptions, isTableLayout, isTableOptions, isTarget, rankLabel, shownCards, spoken, step, suitDefs, sync, syncHand, tableStyles, tileFace, topCards };
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { Rank, Suit } from '@joystyk/kit/cards';
|
|
2
|
+
/** A face-up card: its stable id (an animation key) and its face. */
|
|
3
|
+
export type FaceCard = {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly rank: Rank;
|
|
6
|
+
readonly suit: Suit;
|
|
7
|
+
};
|
|
8
|
+
/** A face-down card. It carries no id, so nothing about it reaches the page. */
|
|
9
|
+
export type DownCard = {
|
|
10
|
+
readonly hidden: true;
|
|
11
|
+
};
|
|
12
|
+
export type PileCard = FaceCard | DownCard;
|
|
13
|
+
/**
|
|
14
|
+
* How a pile shows its cards:
|
|
15
|
+
* - `down`: a column, each card lower than the last, so every face-up card shows its strip;
|
|
16
|
+
* - `right`: fanned to the right (a draw-3 waste, completed runs), showing the last `show`;
|
|
17
|
+
* - `stack`: squared up, only the top card visible (stock, foundations, free cells).
|
|
18
|
+
*/
|
|
19
|
+
export type Fan = 'down' | 'right' | 'stack';
|
|
20
|
+
export type PileSpec = {
|
|
21
|
+
/** Unique within the table: `t3`, `waste`, `f0`. Moves name piles by it. */
|
|
22
|
+
readonly id: string;
|
|
23
|
+
/** The accessible name, already in words: "Column 3: 2 face-down cards, then 9 of spades." */
|
|
24
|
+
readonly label: string;
|
|
25
|
+
/** Bottom to top. */
|
|
26
|
+
readonly cards: readonly PileCard[];
|
|
27
|
+
readonly fan: Fan;
|
|
28
|
+
/** For `right` and `stack`: how many cards from the top are drawn. Default 1 for a stack, 3 fanned. */
|
|
29
|
+
readonly show?: number;
|
|
30
|
+
/** How many cards from the top may be picked up together; 0 for none. */
|
|
31
|
+
readonly pickable: number;
|
|
32
|
+
/** Said when the player asks for more cards than `pickable` (Spider: "different suits"). */
|
|
33
|
+
readonly limit?: string;
|
|
34
|
+
/** Legal destinations by pick-up size: `targets[k - 1]` lists the piles `k` cards may go to. */
|
|
35
|
+
readonly targets?: readonly (readonly string[])[];
|
|
36
|
+
/** Clicking the pile, or Enter on it, runs its action instead of picking up (the stock). */
|
|
37
|
+
readonly action?: boolean;
|
|
38
|
+
/** What an empty pile shows: "A" on a foundation, "K" on a column, "↺" on a stock that can redeal. */
|
|
39
|
+
readonly mark?: string;
|
|
40
|
+
/** A styling hook for the game's theme: `stock`, `waste`, `foundation`, `column`, `cell`, `runs`. */
|
|
41
|
+
readonly kind: string;
|
|
42
|
+
/** The number key that jumps to this pile: "1" … "9", "0". */
|
|
43
|
+
readonly key?: string;
|
|
44
|
+
/** How many grid columns the pile spans. Default 1. */
|
|
45
|
+
readonly span?: number;
|
|
46
|
+
/** Said after "Picked up …" from this pile: "Up to 5 can move." */
|
|
47
|
+
readonly note?: string;
|
|
48
|
+
/** Half-card horizontal steps and half-card rows in a freely placed region. */
|
|
49
|
+
readonly position?: {
|
|
50
|
+
readonly row: number;
|
|
51
|
+
readonly column: number;
|
|
52
|
+
};
|
|
53
|
+
/** A visible, non-color cue, such as a Pyramid partner's value. */
|
|
54
|
+
readonly badge?: string;
|
|
55
|
+
/** Up/Down reads individual cards without changing the one-card action. */
|
|
56
|
+
readonly inspect?: boolean;
|
|
57
|
+
};
|
|
58
|
+
/** Where a region sits on the table grid. Tab order follows the region list, not the place. */
|
|
59
|
+
export type Place = 'top-start' | 'top-end' | 'main' | 'bottom-start' | 'bottom-end';
|
|
60
|
+
export type RegionSpec = {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
/** "Stock and waste", "Foundations", "Tableau". */
|
|
63
|
+
readonly label: string;
|
|
64
|
+
readonly place: Place;
|
|
65
|
+
readonly piles: readonly PileSpec[];
|
|
66
|
+
readonly placement?: 'free';
|
|
67
|
+
};
|
|
68
|
+
/** A suggested move, outlined on the table. */
|
|
69
|
+
export type HintSpec = {
|
|
70
|
+
readonly from: string;
|
|
71
|
+
readonly to: string;
|
|
72
|
+
readonly count: number;
|
|
73
|
+
};
|
|
74
|
+
export type TableLayout = {
|
|
75
|
+
/** The table's name: "Klondike table". */
|
|
76
|
+
readonly label: string;
|
|
77
|
+
/** Grid columns the cards are sized for: 7 for Klondike, 8 for FreeCell, 10 for Spider. */
|
|
78
|
+
readonly columns: number;
|
|
79
|
+
readonly regions: readonly RegionSpec[];
|
|
80
|
+
readonly hint: HintSpec | null;
|
|
81
|
+
/**
|
|
82
|
+
* Single-key shortcuts that act on the focused pile (Klondike's `F`, FreeCell's `C`). The
|
|
83
|
+
* table reports them as `Key` outputs with the pile and count; other keys bubble to the game.
|
|
84
|
+
*/
|
|
85
|
+
readonly keys: readonly string[];
|
|
86
|
+
/** Number shortcuts activate the named pile (rather than just focusing it). */
|
|
87
|
+
readonly activateNumbers?: boolean;
|
|
88
|
+
/** Changes whenever the cards do (the session revision). A change drops any pick-up. */
|
|
89
|
+
readonly version: number;
|
|
90
|
+
};
|
|
91
|
+
/** Card backs: Klondike's lattice, Spider's fine-line web, FreeCell's cells. All original. */
|
|
92
|
+
export type Back = 'lattice' | 'web' | 'cells' | 'terraces' | 'contours' | 'dunes';
|
|
93
|
+
export type TableOptions = {
|
|
94
|
+
/** WCAG 2.1.4: false turns off number keys and the layout's single-key shortcuts. */
|
|
95
|
+
readonly singleKeys: boolean;
|
|
96
|
+
/** Reduced motion: cards move instantly. */
|
|
97
|
+
readonly calm: boolean;
|
|
98
|
+
/** Blue diamonds and green clubs, for players who struggle with red and black. */
|
|
99
|
+
readonly fourColor: boolean;
|
|
100
|
+
readonly back: Back;
|
|
101
|
+
};
|
|
102
|
+
export declare const DEFAULT_OPTIONS: TableOptions;
|
|
103
|
+
export declare const EMPTY_LAYOUT: TableLayout;
|
|
104
|
+
export declare const isFaceCard: (card: PileCard | undefined) => card is FaceCard;
|
|
105
|
+
/** A light shape check for the `layout` property (development builds validate properties). */
|
|
106
|
+
export declare const isTableLayout: (value: unknown) => value is TableLayout;
|
|
107
|
+
export declare const isTableOptions: (value: unknown) => value is TableOptions;
|
|
108
|
+
/** Every pile with its region index and position, in Tab order. */
|
|
109
|
+
export declare const allPiles: (layout: TableLayout) => readonly {
|
|
110
|
+
readonly region: number;
|
|
111
|
+
readonly index: number;
|
|
112
|
+
readonly pile: PileSpec;
|
|
113
|
+
}[];
|
|
114
|
+
export declare const findPile: (layout: TableLayout, id: string) => {
|
|
115
|
+
readonly region: number;
|
|
116
|
+
readonly index: number;
|
|
117
|
+
readonly pile: PileSpec;
|
|
118
|
+
} | null;
|
|
119
|
+
/** The face-up cards at the top of a pile, `count` of them, bottom first. */
|
|
120
|
+
export declare const topCards: (pile: PileSpec, count: number) => FaceCard[];
|
|
121
|
+
/** True when `count` cards from `from` may go to `to`. */
|
|
122
|
+
export declare const isTarget: (pile: PileSpec, count: number, to: string) => boolean;
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/layout.ts
|
|
2
|
+
var DEFAULT_OPTIONS = {
|
|
3
|
+
singleKeys: true,
|
|
4
|
+
calm: false,
|
|
5
|
+
fourColor: false,
|
|
6
|
+
back: "lattice"
|
|
7
|
+
};
|
|
8
|
+
var EMPTY_LAYOUT = {
|
|
9
|
+
label: "Card table",
|
|
10
|
+
columns: 7,
|
|
11
|
+
regions: [],
|
|
12
|
+
hint: null,
|
|
13
|
+
keys: [],
|
|
14
|
+
version: 0
|
|
15
|
+
};
|
|
16
|
+
var isFaceCard = (card) => card !== void 0 && !("hidden" in card);
|
|
17
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
18
|
+
/** A light shape check for the `layout` property (development builds validate properties). */
|
|
19
|
+
var isTableLayout = (value) => isRecord(value) && typeof value["label"] === "string" && typeof value["columns"] === "number" && Array.isArray(value["regions"]) && typeof value["version"] === "number";
|
|
20
|
+
var isTableOptions = (value) => isRecord(value) && typeof value["singleKeys"] === "boolean" && typeof value["calm"] === "boolean" && typeof value["fourColor"] === "boolean" && typeof value["back"] === "string";
|
|
21
|
+
/** Every pile with its region index and position, in Tab order. */
|
|
22
|
+
var allPiles = (layout) => layout.regions.flatMap((region, r) => region.piles.map((pile, index) => ({
|
|
23
|
+
region: r,
|
|
24
|
+
index,
|
|
25
|
+
pile
|
|
26
|
+
})));
|
|
27
|
+
var findPile = (layout, id) => allPiles(layout).find((entry) => entry.pile.id === id) ?? null;
|
|
28
|
+
/** The face-up cards at the top of a pile, `count` of them, bottom first. */
|
|
29
|
+
var topCards = (pile, count) => pile.cards.slice(Math.max(0, pile.cards.length - count)).filter(isFaceCard);
|
|
30
|
+
/** True when `count` cards from `from` may go to `to`. */
|
|
31
|
+
var isTarget = (pile, count, to) => pile.targets?.[count - 1]?.includes(to) === true;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { DEFAULT_OPTIONS, EMPTY_LAYOUT, allPiles, findPile, isFaceCard, isTableLayout, isTableOptions, isTarget, topCards };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","names":[],"sources":["../src/layout.ts"],"sourcesContent":["// The table a card game hands to <jy-card-table>: regions of piles, each pile's cards bottom\n// to top, and what the player may do with them. It is plain JSON built from the game's view\n// on every snapshot, so the table never needs the rules: the game says how many cards a pile\n// lets go of and where each pick-up may land, and the table turns pointer and keyboard input\n// into moves (klondike.md, \"Interaction and accessibility\").\nimport type { Rank, Suit } from '@joystyk/kit/cards';\n\n/** A face-up card: its stable id (an animation key) and its face. */\nexport type FaceCard = { readonly id: string; readonly rank: Rank; readonly suit: Suit };\n\n/** A face-down card. It carries no id, so nothing about it reaches the page. */\nexport type DownCard = { readonly hidden: true };\n\nexport type PileCard = FaceCard | DownCard;\n\n/**\n * How a pile shows its cards:\n * - `down`: a column, each card lower than the last, so every face-up card shows its strip;\n * - `right`: fanned to the right (a draw-3 waste, completed runs), showing the last `show`;\n * - `stack`: squared up, only the top card visible (stock, foundations, free cells).\n */\nexport type Fan = 'down' | 'right' | 'stack';\n\nexport type PileSpec = {\n /** Unique within the table: `t3`, `waste`, `f0`. Moves name piles by it. */\n readonly id: string;\n /** The accessible name, already in words: \"Column 3: 2 face-down cards, then 9 of spades.\" */\n readonly label: string;\n /** Bottom to top. */\n readonly cards: readonly PileCard[];\n readonly fan: Fan;\n /** For `right` and `stack`: how many cards from the top are drawn. Default 1 for a stack, 3 fanned. */\n readonly show?: number;\n /** How many cards from the top may be picked up together; 0 for none. */\n readonly pickable: number;\n /** Said when the player asks for more cards than `pickable` (Spider: \"different suits\"). */\n readonly limit?: string;\n /** Legal destinations by pick-up size: `targets[k - 1]` lists the piles `k` cards may go to. */\n readonly targets?: readonly (readonly string[])[];\n /** Clicking the pile, or Enter on it, runs its action instead of picking up (the stock). */\n readonly action?: boolean;\n /** What an empty pile shows: \"A\" on a foundation, \"K\" on a column, \"↺\" on a stock that can redeal. */\n readonly mark?: string;\n /** A styling hook for the game's theme: `stock`, `waste`, `foundation`, `column`, `cell`, `runs`. */\n readonly kind: string;\n /** The number key that jumps to this pile: \"1\" … \"9\", \"0\". */\n readonly key?: string;\n /** How many grid columns the pile spans. Default 1. */\n readonly span?: number;\n /** Said after \"Picked up …\" from this pile: \"Up to 5 can move.\" */\n readonly note?: string;\n /** Half-card horizontal steps and half-card rows in a freely placed region. */\n readonly position?: { readonly row: number; readonly column: number };\n /** A visible, non-color cue, such as a Pyramid partner's value. */\n readonly badge?: string;\n /** Up/Down reads individual cards without changing the one-card action. */\n readonly inspect?: boolean;\n};\n\n/** Where a region sits on the table grid. Tab order follows the region list, not the place. */\nexport type Place = 'top-start' | 'top-end' | 'main' | 'bottom-start' | 'bottom-end';\n\nexport type RegionSpec = {\n readonly id: string;\n /** \"Stock and waste\", \"Foundations\", \"Tableau\". */\n readonly label: string;\n readonly place: Place;\n readonly piles: readonly PileSpec[];\n readonly placement?: 'free';\n};\n\n/** A suggested move, outlined on the table. */\nexport type HintSpec = { readonly from: string; readonly to: string; readonly count: number };\n\nexport type TableLayout = {\n /** The table's name: \"Klondike table\". */\n readonly label: string;\n /** Grid columns the cards are sized for: 7 for Klondike, 8 for FreeCell, 10 for Spider. */\n readonly columns: number;\n readonly regions: readonly RegionSpec[];\n readonly hint: HintSpec | null;\n /**\n * Single-key shortcuts that act on the focused pile (Klondike's `F`, FreeCell's `C`). The\n * table reports them as `Key` outputs with the pile and count; other keys bubble to the game.\n */\n readonly keys: readonly string[];\n /** Number shortcuts activate the named pile (rather than just focusing it). */\n readonly activateNumbers?: boolean;\n /** Changes whenever the cards do (the session revision). A change drops any pick-up. */\n readonly version: number;\n};\n\n/** Card backs: Klondike's lattice, Spider's fine-line web, FreeCell's cells. All original. */\nexport type Back = 'lattice' | 'web' | 'cells' | 'terraces' | 'contours' | 'dunes';\n\nexport type TableOptions = {\n /** WCAG 2.1.4: false turns off number keys and the layout's single-key shortcuts. */\n readonly singleKeys: boolean;\n /** Reduced motion: cards move instantly. */\n readonly calm: boolean;\n /** Blue diamonds and green clubs, for players who struggle with red and black. */\n readonly fourColor: boolean;\n readonly back: Back;\n};\n\nexport const DEFAULT_OPTIONS: TableOptions = { singleKeys: true, calm: false, fourColor: false, back: 'lattice' };\n\nexport const EMPTY_LAYOUT: TableLayout = {\n label: 'Card table',\n columns: 7,\n regions: [],\n hint: null,\n keys: [],\n version: 0,\n};\n\nexport const isFaceCard = (card: PileCard | undefined): card is FaceCard => card !== undefined && !('hidden' in card);\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;\n\n/** A light shape check for the `layout` property (development builds validate properties). */\nexport const isTableLayout = (value: unknown): value is TableLayout =>\n isRecord(value) &&\n typeof value['label'] === 'string' &&\n typeof value['columns'] === 'number' &&\n Array.isArray(value['regions']) &&\n typeof value['version'] === 'number';\n\nexport const isTableOptions = (value: unknown): value is TableOptions =>\n isRecord(value) &&\n typeof value['singleKeys'] === 'boolean' &&\n typeof value['calm'] === 'boolean' &&\n typeof value['fourColor'] === 'boolean' &&\n typeof value['back'] === 'string';\n\n/** Every pile with its region index and position, in Tab order. */\nexport const allPiles = (\n layout: TableLayout,\n): readonly { readonly region: number; readonly index: number; readonly pile: PileSpec }[] =>\n layout.regions.flatMap((region, r) => region.piles.map((pile, index) => ({ region: r, index, pile })));\n\nexport const findPile = (\n layout: TableLayout,\n id: string,\n): { readonly region: number; readonly index: number; readonly pile: PileSpec } | null =>\n allPiles(layout).find((entry) => entry.pile.id === id) ?? null;\n\n/** The face-up cards at the top of a pile, `count` of them, bottom first. */\nexport const topCards = (pile: PileSpec, count: number): FaceCard[] =>\n pile.cards.slice(Math.max(0, pile.cards.length - count)).filter(isFaceCard);\n\n/** True when `count` cards from `from` may go to `to`. */\nexport const isTarget = (pile: PileSpec, count: number, to: string): boolean =>\n pile.targets?.[count - 1]?.includes(to) === true;\n"],"mappings":";AAyGA,IAAa,kBAAgC;CAAE,YAAY;CAAM,MAAM;CAAO,WAAW;CAAO,MAAM;AAAU;AAEhH,IAAa,eAA4B;CACvC,OAAO;CACP,SAAS;CACT,SAAS,CAAC;CACV,MAAM;CACN,MAAM,CAAC;CACP,SAAS;AACX;AAEA,IAAa,cAAc,SAAiD,SAAS,KAAA,KAAa,EAAE,YAAY;AAEhH,IAAM,YAAY,UAAqD,OAAO,UAAU,YAAY,UAAU;;AAG9G,IAAa,iBAAiB,UAC5B,SAAS,KAAK,KACd,OAAO,MAAM,aAAa,YAC1B,OAAO,MAAM,eAAe,YAC5B,MAAM,QAAQ,MAAM,UAAU,KAC9B,OAAO,MAAM,eAAe;AAE9B,IAAa,kBAAkB,UAC7B,SAAS,KAAK,KACd,OAAO,MAAM,kBAAkB,aAC/B,OAAO,MAAM,YAAY,aACzB,OAAO,MAAM,iBAAiB,aAC9B,OAAO,MAAM,YAAY;;AAG3B,IAAa,YACX,WAEA,OAAO,QAAQ,SAAS,QAAQ,MAAM,OAAO,MAAM,KAAK,MAAM,WAAW;CAAE,QAAQ;CAAG;CAAO;AAAK,EAAE,CAAC;AAEvG,IAAa,YACX,QACA,OAEA,SAAS,MAAM,CAAC,CAAC,MAAM,UAAU,MAAM,KAAK,OAAO,EAAE,KAAK;;AAG5D,IAAa,YAAY,MAAgB,UACvC,KAAK,MAAM,MAAM,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,KAAK,CAAC,CAAC,CAAC,OAAO,UAAU;;AAG5E,IAAa,YAAY,MAAgB,OAAe,OACtD,KAAK,UAAU,QAAQ,EAAE,EAAE,SAAS,EAAE,MAAM"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { type AnyGameSession, type Json, type Mounted, type PlatformApi, type Preferences, type Validator } from '@joystyk/contracts';
|
|
2
|
+
import { type Back, type TableLayout, type TableOptions } from './layout.js';
|
|
3
|
+
import type { TableOut } from './controller.js';
|
|
4
|
+
export type SetupField = {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly value: string;
|
|
8
|
+
readonly choices?: readonly {
|
|
9
|
+
readonly value: string;
|
|
10
|
+
readonly label: string;
|
|
11
|
+
}[];
|
|
12
|
+
};
|
|
13
|
+
export type SolitaireData = {
|
|
14
|
+
readonly title: string;
|
|
15
|
+
readonly theme: 'noon' | 'dawn' | 'dusk';
|
|
16
|
+
readonly back: Back;
|
|
17
|
+
readonly layout: TableLayout;
|
|
18
|
+
readonly status: string;
|
|
19
|
+
readonly line: string;
|
|
20
|
+
readonly meters: readonly {
|
|
21
|
+
readonly label: string;
|
|
22
|
+
readonly value: string;
|
|
23
|
+
}[];
|
|
24
|
+
readonly fields: readonly SetupField[];
|
|
25
|
+
readonly setupAllowed: boolean;
|
|
26
|
+
readonly actions: readonly {
|
|
27
|
+
readonly label: string;
|
|
28
|
+
readonly command: Json;
|
|
29
|
+
readonly disabled: boolean;
|
|
30
|
+
}[];
|
|
31
|
+
readonly scorecard?: readonly {
|
|
32
|
+
readonly hole: number;
|
|
33
|
+
readonly strokes: number | null;
|
|
34
|
+
readonly total: number | null;
|
|
35
|
+
readonly par: number;
|
|
36
|
+
readonly cleared: boolean;
|
|
37
|
+
}[];
|
|
38
|
+
readonly recordGroup: string;
|
|
39
|
+
readonly moves: number;
|
|
40
|
+
readonly score: number;
|
|
41
|
+
readonly bestRun: number;
|
|
42
|
+
readonly cleared: number;
|
|
43
|
+
readonly keys: string;
|
|
44
|
+
readonly history?: readonly string[];
|
|
45
|
+
readonly round?: {
|
|
46
|
+
readonly strokes: number;
|
|
47
|
+
readonly bestHole: number;
|
|
48
|
+
readonly par: number;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export type SolitaireAdapter<V, E> = {
|
|
52
|
+
readonly view: Validator<V>;
|
|
53
|
+
readonly event: Validator<E>;
|
|
54
|
+
readonly describe: (view: V, events: readonly E[], revision: number) => SolitaireData;
|
|
55
|
+
readonly table: (view: V, out: TableOut) => Json | null;
|
|
56
|
+
readonly shortcut: (view: V, key: string) => Json | null;
|
|
57
|
+
readonly setup: (view: V, values: Readonly<Record<string, string>>) => Json | null;
|
|
58
|
+
/** Optional worker validation/certification before a setup command is sent. */
|
|
59
|
+
readonly prepareSetup?: (command: Json) => Promise<Json>;
|
|
60
|
+
readonly dispose?: () => void;
|
|
61
|
+
};
|
|
62
|
+
type Snapshot = {
|
|
63
|
+
readonly data: SolitaireData;
|
|
64
|
+
readonly revision: number;
|
|
65
|
+
readonly canUndo: boolean;
|
|
66
|
+
readonly result: string | null;
|
|
67
|
+
readonly summary: string;
|
|
68
|
+
readonly notice: string;
|
|
69
|
+
readonly elapsedMs: number;
|
|
70
|
+
readonly undosUsed: number;
|
|
71
|
+
readonly assisted: boolean;
|
|
72
|
+
readonly paused: boolean;
|
|
73
|
+
};
|
|
74
|
+
export type RecordRow = {
|
|
75
|
+
readonly played: number;
|
|
76
|
+
readonly won: number;
|
|
77
|
+
readonly bestScore: number | null;
|
|
78
|
+
readonly fewestMoves: number | null;
|
|
79
|
+
readonly bestMs: number | null;
|
|
80
|
+
readonly bestRun: number;
|
|
81
|
+
readonly cleared: number;
|
|
82
|
+
readonly cleanWins: number;
|
|
83
|
+
readonly streak: number;
|
|
84
|
+
readonly longestStreak: number;
|
|
85
|
+
readonly completed: number;
|
|
86
|
+
readonly totalStrokes: number;
|
|
87
|
+
readonly bestRound: number | null;
|
|
88
|
+
readonly bestHole: number | null;
|
|
89
|
+
readonly underPar: number;
|
|
90
|
+
};
|
|
91
|
+
type Records = Readonly<Record<string, RecordRow>>;
|
|
92
|
+
export declare const decode: <T>(schema: Validator<T>, raw: unknown) => T | null;
|
|
93
|
+
type Action = {
|
|
94
|
+
readonly kind: 'command';
|
|
95
|
+
readonly command: Json;
|
|
96
|
+
} | {
|
|
97
|
+
readonly kind: 'undo' | 'resign';
|
|
98
|
+
} | {
|
|
99
|
+
readonly kind: 'table';
|
|
100
|
+
readonly out: TableOut;
|
|
101
|
+
} | {
|
|
102
|
+
readonly kind: 'key';
|
|
103
|
+
readonly key: string;
|
|
104
|
+
} | {
|
|
105
|
+
readonly kind: 'setup';
|
|
106
|
+
readonly values: Readonly<Record<string, string>>;
|
|
107
|
+
};
|
|
108
|
+
type State = {
|
|
109
|
+
readonly snapshot: Snapshot | null;
|
|
110
|
+
readonly options: TableOptions;
|
|
111
|
+
readonly dark: boolean;
|
|
112
|
+
readonly message: string;
|
|
113
|
+
readonly dialog: 'none' | 'keys' | 'confirm';
|
|
114
|
+
readonly elapsedMs: number;
|
|
115
|
+
readonly records: Records;
|
|
116
|
+
readonly sound: boolean;
|
|
117
|
+
};
|
|
118
|
+
type Msg = {
|
|
119
|
+
readonly _tag: 'Snapshot';
|
|
120
|
+
readonly snapshot: Snapshot;
|
|
121
|
+
} | {
|
|
122
|
+
readonly _tag: 'Preferences';
|
|
123
|
+
readonly p: Preferences;
|
|
124
|
+
} | {
|
|
125
|
+
readonly _tag: 'Records';
|
|
126
|
+
readonly records: Records;
|
|
127
|
+
} | {
|
|
128
|
+
readonly _tag: 'Tick';
|
|
129
|
+
readonly ms: number;
|
|
130
|
+
} | {
|
|
131
|
+
readonly _tag: 'Action';
|
|
132
|
+
readonly action: Action;
|
|
133
|
+
} | {
|
|
134
|
+
readonly _tag: 'Table';
|
|
135
|
+
readonly out: TableOut;
|
|
136
|
+
} | {
|
|
137
|
+
readonly _tag: 'Key';
|
|
138
|
+
readonly key: string;
|
|
139
|
+
readonly ctrl: boolean;
|
|
140
|
+
} | {
|
|
141
|
+
readonly _tag: 'Dialog';
|
|
142
|
+
readonly dialog: State['dialog'];
|
|
143
|
+
} | {
|
|
144
|
+
readonly _tag: 'Color';
|
|
145
|
+
readonly on: boolean;
|
|
146
|
+
} | {
|
|
147
|
+
readonly _tag: 'Sound';
|
|
148
|
+
readonly on: boolean;
|
|
149
|
+
} | {
|
|
150
|
+
readonly _tag: 'Message';
|
|
151
|
+
readonly message: string;
|
|
152
|
+
};
|
|
153
|
+
export declare const SolitaireBoard: import("@gyral/core").GyralElementClass<State, Msg, object, never, "Key" | "Cancel" | "Action" | "Table" | "Color" | "Sound" | "Undo" | "Hint" | "New" | "Keys" | "Confirm" | "Setup">;
|
|
154
|
+
export declare const mountSolitaire: <V, E>(slot: HTMLElement, session: AnyGameSession, platform: PlatformApi, adapter: SolitaireAdapter<V, E>) => Mounted;
|
|
155
|
+
export { workerSetup } from './worker-setup.js';
|