@joystyk/testing 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.
Files changed (69) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +36 -0
  4. package/README.md +191 -2
  5. package/dist/builders.d.ts +59 -0
  6. package/dist/builders.js +129 -0
  7. package/dist/builders.js.map +1 -0
  8. package/dist/clock.d.ts +30 -0
  9. package/dist/clock.js +58 -0
  10. package/dist/clock.js.map +1 -0
  11. package/dist/deep.d.ts +25 -0
  12. package/dist/deep.js +105 -0
  13. package/dist/deep.js.map +1 -0
  14. package/dist/engine-conformance.d.ts +166 -0
  15. package/dist/engine-conformance.js +995 -0
  16. package/dist/engine-conformance.js.map +1 -0
  17. package/dist/fixtures/dice-race.d.ts +41 -0
  18. package/dist/fixtures/dice-race.js +153 -0
  19. package/dist/fixtures/dice-race.js.map +1 -0
  20. package/dist/fixtures/draw-play.d.ts +54 -0
  21. package/dist/fixtures/draw-play.js +188 -0
  22. package/dist/fixtures/draw-play.js.map +1 -0
  23. package/dist/fixtures/drift.d.ts +34 -0
  24. package/dist/fixtures/drift.js +109 -0
  25. package/dist/fixtures/drift.js.map +1 -0
  26. package/dist/fixtures/guards.d.ts +11 -0
  27. package/dist/fixtures/guards.js +16 -0
  28. package/dist/fixtures/guards.js.map +1 -0
  29. package/dist/fixtures/hint-puzzle.d.ts +47 -0
  30. package/dist/fixtures/hint-puzzle.js +129 -0
  31. package/dist/fixtures/hint-puzzle.js.map +1 -0
  32. package/dist/fixtures/index.d.ts +6 -0
  33. package/dist/fixtures/index.js +7 -0
  34. package/dist/fixtures/nim.d.ts +34 -0
  35. package/dist/fixtures/nim.js +141 -0
  36. package/dist/fixtures/nim.js.map +1 -0
  37. package/dist/fixtures/sealed-bids.d.ts +46 -0
  38. package/dist/fixtures/sealed-bids.js +141 -0
  39. package/dist/fixtures/sealed-bids.js.map +1 -0
  40. package/dist/index.d.ts +13 -0
  41. package/dist/index.js +14 -0
  42. package/dist/replay.d.ts +58 -0
  43. package/dist/replay.js +110 -0
  44. package/dist/replay.js.map +1 -0
  45. package/dist/report.d.ts +68 -0
  46. package/dist/report.js +104 -0
  47. package/dist/report.js.map +1 -0
  48. package/dist/rng.d.ts +18 -0
  49. package/dist/rng.js +25 -0
  50. package/dist/rng.js.map +1 -0
  51. package/dist/schema.d.ts +13 -0
  52. package/dist/schema.js +21 -0
  53. package/dist/schema.js.map +1 -0
  54. package/dist/session-ports.d.ts +16 -0
  55. package/dist/session-ports.js +32 -0
  56. package/dist/session-ports.js.map +1 -0
  57. package/dist/simulation-conformance.d.ts +71 -0
  58. package/dist/simulation-conformance.js +312 -0
  59. package/dist/simulation-conformance.js.map +1 -0
  60. package/dist/storage.d.ts +29 -0
  61. package/dist/storage.js +63 -0
  62. package/dist/storage.js.map +1 -0
  63. package/dist/vitest.d.ts +32 -0
  64. package/dist/vitest.js +45 -0
  65. package/dist/vitest.js.map +1 -0
  66. package/dist/waiting.d.ts +58 -0
  67. package/dist/waiting.js +97 -0
  68. package/dist/waiting.js.map +1 -0
  69. package/package.json +55 -3
@@ -0,0 +1,188 @@
1
+ import { guardSchema } from "../builders.js";
2
+ import { isArrayOf, isInt, isRecord, isSeat, isSeatOrNull, rankPlaces } from "./guards.js";
3
+ import { defineEngine, err, gameError, ok } from "@joystyk/contracts";
4
+ import { assignIds, isOpaqueId, isRank, isSuit, nextActiveSeat, perturbHidden, standardDeck } from "@joystyk/kit";
5
+ //#region src/fixtures/draw-play.ts
6
+ var DRAW_PLAY_CARDS = 26;
7
+ var MAX_HAND = 5;
8
+ var isCard = (v) => isRecord(v) && isRank(v["rank"]) && isSuit(v["suit"]) && isOpaqueId(v["id"]);
9
+ var isTable = (v) => isArrayOf(v, (t) => isRecord(t) && isSeat(t["seat"]) && isCard(t["card"]));
10
+ var isScores = (v) => isArrayOf(v, (n) => isInt(n, 0));
11
+ var isState = (v) => isRecord(v) && isArrayOf(v["hands"], (h) => isArrayOf(h, isCard)) && isArrayOf(v["deck"], isCard) && isTable(v["table"]) && isSeat(v["turn"]) && isScores(v["scores"]) && isSeatOrNull(v["resigned"]) && typeof v["over"] === "boolean";
12
+ var isView = (v) => isRecord(v) && (v["hand"] === null || isArrayOf(v["hand"], isCard)) && isArrayOf(v["handSizes"], (n) => isInt(n, 0)) && isInt(v["deckSize"], 0) && isTable(v["table"]) && isSeat(v["turn"]) && isScores(v["scores"]) && isSeatOrNull(v["resigned"]) && typeof v["over"] === "boolean";
13
+ var isCommand = (v) => isRecord(v) && (v["type"] === "play" && typeof v["cardId"] === "string" || v["type"] === "draw");
14
+ var isEvent = (v) => isRecord(v) && isSeat(v["seat"]) && (v["type"] === "drew" && (v["card"] === null || isCard(v["card"])) || v["type"] === "played" && isCard(v["card"]) || v["type"] === "resigned");
15
+ var isConfig = (v) => isRecord(v) && isInt(v["handSize"], 1, 4);
16
+ var canMove = (state, seat) => (state.hands[seat]?.length ?? 0) > 0 || state.deck.length > 0;
17
+ var finish = (state, seat) => {
18
+ const turn = nextActiveSeat(seat, state.hands.length, (s) => canMove(state, s));
19
+ return turn === null ? {
20
+ ...state,
21
+ over: true
22
+ } : {
23
+ ...state,
24
+ turn
25
+ };
26
+ };
27
+ var drawPlay = defineEngine({
28
+ rulesVersion: 1,
29
+ schemas: {
30
+ config: guardSchema(isConfig, "Expected { handSize } from 1 to 4."),
31
+ command: guardSchema(isCommand, "Not a Draw and Play command."),
32
+ state: guardSchema(isState, "Not a Draw and Play state."),
33
+ view: guardSchema(isView, "Not a Draw and Play view."),
34
+ event: guardSchema(isEvent, "Not a Draw and Play event.")
35
+ },
36
+ init: (config, seats, rng) => {
37
+ if (seats.length < 2 || seats.length > 4) return err(gameError("Seats", "Draw and Play needs two to four players."));
38
+ const faces = rng.shuffle(standardDeck({ suits: ["S", "H"] }));
39
+ const cards = assignIds(faces, rng);
40
+ const state = {
41
+ hands: seats.map((_, s) => cards.slice(s * config.handSize, (s + 1) * config.handSize)),
42
+ deck: cards.slice(seats.length * config.handSize),
43
+ table: [],
44
+ turn: rng.int(0, seats.length),
45
+ scores: seats.map(() => 0),
46
+ resigned: null,
47
+ over: false
48
+ };
49
+ return ok(state);
50
+ },
51
+ apply: (state, input) => {
52
+ if (state.over) return err(gameError("GameOver", "The game is over."));
53
+ if (input.kind === "resign") {
54
+ if (state.hands[input.seat] === void 0) return err(gameError("NoSuchSeat", "There is no such seat."));
55
+ return ok({
56
+ state: {
57
+ ...state,
58
+ resigned: input.seat,
59
+ over: true
60
+ },
61
+ beats: [{ event: {
62
+ type: "resigned",
63
+ seat: input.seat
64
+ } }]
65
+ });
66
+ }
67
+ if (input.kind !== "command") return err(gameError("Unsupported", "Draw and Play has no clocks, seat changes or jobs."));
68
+ if (input.seat !== state.turn) return err(gameError("NotYourTurn", "It's not your turn."));
69
+ const hand = state.hands[input.seat] ?? [];
70
+ const command = input.command;
71
+ if (command.type === "draw") {
72
+ const [card, ...deck] = state.deck;
73
+ if (card === void 0) return err(gameError("DeckEmpty", "The deck is empty."));
74
+ if (hand.length >= MAX_HAND) return err(gameError("HandFull", `You can hold at most ${MAX_HAND} cards.`));
75
+ const hands = state.hands.map((h, s) => s === input.seat ? [...h, card] : h);
76
+ const next = finish({
77
+ ...state,
78
+ hands,
79
+ deck
80
+ }, input.seat);
81
+ return ok({
82
+ state: next,
83
+ beats: [{ event: {
84
+ type: "drew",
85
+ seat: input.seat,
86
+ card
87
+ } }]
88
+ });
89
+ }
90
+ const card = hand.find((c) => c.id === command.cardId);
91
+ if (card === void 0) return err(gameError("NotInHand", "That card isn't in your hand."));
92
+ const hands = state.hands.map((h, s) => s === input.seat ? h.filter((c) => c.id !== card.id) : h);
93
+ const scores = state.scores.map((score, s) => s === input.seat ? score + card.rank : score);
94
+ const next = finish({
95
+ ...state,
96
+ hands,
97
+ scores,
98
+ table: [...state.table, {
99
+ seat: input.seat,
100
+ card
101
+ }]
102
+ }, input.seat);
103
+ return ok({
104
+ state: next,
105
+ beats: [{ event: {
106
+ type: "played",
107
+ seat: input.seat,
108
+ card
109
+ } }]
110
+ });
111
+ },
112
+ pending: (state) => state.over ? [] : [state.turn],
113
+ viewFor: (state, viewer) => ({
114
+ hand: viewer === null ? null : state.hands[viewer] ?? null,
115
+ handSizes: state.hands.map((h) => h.length),
116
+ deckSize: state.deck.length,
117
+ table: state.table,
118
+ turn: state.turn,
119
+ scores: state.scores,
120
+ resigned: state.resigned,
121
+ over: state.over
122
+ }),
123
+ eventFor: (event, viewer) => event.type === "drew" && event.seat !== viewer ? {
124
+ ...event,
125
+ card: null
126
+ } : event,
127
+ outcome: (state) => state.over ? {
128
+ result: "won",
129
+ placements: rankPlaces(state.scores, state.resigned),
130
+ summary: state.resigned === null ? "Every card has been played." : `Seat ${state.resigned + 1} resigned.`
131
+ } : null,
132
+ ai: {
133
+ levels: ["easy"],
134
+ choose: (view) => {
135
+ const best = [...view.hand ?? []].sort((a, b) => b.rank - a.rank)[0];
136
+ return best === void 0 ? { type: "draw" } : {
137
+ type: "play",
138
+ cardId: best.id
139
+ };
140
+ }
141
+ }
142
+ });
143
+ /** Shuffles everything `viewer` can't see: the deck and every other hand. */
144
+ var perturbDrawPlay = (state, viewer, rng) => {
145
+ const zones = { deck: state.deck };
146
+ state.hands.forEach((hand, s) => {
147
+ zones[`hand${s}`] = hand;
148
+ });
149
+ const out = perturbHidden(zones, (zone) => zone === `hand${viewer}`, rng);
150
+ return {
151
+ ...state,
152
+ deck: out["deck"] ?? [],
153
+ hands: state.hands.map((_, s) => out[`hand${s}`] ?? [])
154
+ };
155
+ };
156
+ var cardsOf = (s) => [
157
+ ...s.hands.flat(),
158
+ ...s.deck,
159
+ ...s.table.map((t) => t.card)
160
+ ];
161
+ /** Conformance options for the Draw and Play fixture. */
162
+ var drawPlayConformance = {
163
+ name: "draw-play",
164
+ configs: [{ handSize: 3 }],
165
+ seatCounts: [
166
+ 2,
167
+ 3,
168
+ 4
169
+ ],
170
+ games: 8,
171
+ commands: (state, seat) => [{ type: "draw" }, ...(state.hands[seat] ?? []).map((c) => ({
172
+ type: "play",
173
+ cardId: c.id
174
+ }))],
175
+ perturbHidden: perturbDrawPlay,
176
+ invariants: (s) => {
177
+ const cards = cardsOf(s);
178
+ if (cards.length !== 26) return `${cards.length} cards in play, not 26`;
179
+ if (new Set(cards.map((c) => c.id)).size !== cards.length) return "two cards share an id";
180
+ const total = s.table.reduce((sum, t) => sum + t.card.rank, 0);
181
+ if (s.scores.reduce((a, b) => a + b, 0) !== total) return "the scores disagree with the table";
182
+ return true;
183
+ }
184
+ };
185
+ //#endregion
186
+ export { DRAW_PLAY_CARDS, drawPlay, drawPlayConformance, perturbDrawPlay };
187
+
188
+ //# sourceMappingURL=draw-play.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draw-play.js","names":[],"sources":["../../src/fixtures/draw-play.ts"],"sourcesContent":["// Draw and Play: a hidden-hand card game for two to four players. Each player is dealt three\n// cards from a shuffled 26-card deck (spades and hearts). On your turn, play a card face up\n// (you score its rank) or draw one (up to five in hand). The game ends when every card has\n// been played; the highest score wins. Other hands and the deck are hidden, so this fixture\n// exercises the no-leak check, opaque card ids and redacted events.\n\nimport { defineEngine, err, gameError, ok, type SeatId } from '@joystyk/contracts';\nimport {\n assignIds,\n isOpaqueId,\n isRank,\n isSuit,\n perturbHidden,\n nextActiveSeat,\n standardDeck,\n type SuitedCard,\n type SuitedFace,\n} from '@joystyk/kit';\nimport { guardSchema } from '../builders.ts';\nimport type { EngineConformanceOptions } from '../engine-conformance.ts';\nimport { isArrayOf, isInt, isRecord, isSeat, isSeatOrNull, rankPlaces } from './guards.ts';\n\nexport type DrawPlayCard = SuitedCard;\nexport type DrawPlayState = {\n readonly hands: readonly (readonly DrawPlayCard[])[];\n readonly deck: readonly DrawPlayCard[];\n readonly table: readonly { readonly seat: SeatId; readonly card: DrawPlayCard }[];\n readonly turn: SeatId;\n readonly scores: readonly number[];\n readonly resigned: SeatId | null;\n readonly over: boolean;\n};\nexport type DrawPlayCommand = { readonly type: 'play'; readonly cardId: string } | { readonly type: 'draw' };\nexport type DrawPlayEvent =\n | { readonly type: 'drew'; readonly seat: SeatId; readonly card: DrawPlayCard | null }\n | { readonly type: 'played'; readonly seat: SeatId; readonly card: DrawPlayCard }\n | { readonly type: 'resigned'; readonly seat: SeatId };\nexport type DrawPlayView = {\n /** Your own cards; null for every other hand and for a spectator. */\n readonly hand: readonly DrawPlayCard[] | null;\n readonly handSizes: readonly number[];\n readonly deckSize: number;\n readonly table: DrawPlayState['table'];\n readonly turn: SeatId;\n readonly scores: readonly number[];\n readonly resigned: SeatId | null;\n readonly over: boolean;\n};\nexport type DrawPlayConfig = { readonly handSize: number };\n\nexport const DRAW_PLAY_CARDS = 26;\nconst MAX_HAND = 5;\n\nconst isCard = (v: unknown): v is DrawPlayCard =>\n isRecord(v) && isRank(v['rank']) && isSuit(v['suit']) && isOpaqueId(v['id']);\nconst isTable = (v: unknown): v is DrawPlayState['table'] =>\n isArrayOf(v, (t): t is { seat: number; card: DrawPlayCard } => isRecord(t) && isSeat(t['seat']) && isCard(t['card']));\nconst isScores = (v: unknown): v is number[] => isArrayOf(v, (n): n is number => isInt(n, 0));\nconst isState = (v: unknown): v is DrawPlayState =>\n isRecord(v) &&\n isArrayOf(v['hands'], (h): h is DrawPlayCard[] => isArrayOf(h, isCard)) &&\n isArrayOf(v['deck'], isCard) &&\n isTable(v['table']) &&\n isSeat(v['turn']) &&\n isScores(v['scores']) &&\n isSeatOrNull(v['resigned']) &&\n typeof v['over'] === 'boolean';\nconst isView = (v: unknown): v is DrawPlayView =>\n isRecord(v) &&\n (v['hand'] === null || isArrayOf(v['hand'], isCard)) &&\n isArrayOf(v['handSizes'], (n): n is number => isInt(n, 0)) &&\n isInt(v['deckSize'], 0) &&\n isTable(v['table']) &&\n isSeat(v['turn']) &&\n isScores(v['scores']) &&\n isSeatOrNull(v['resigned']) &&\n typeof v['over'] === 'boolean';\nconst isCommand = (v: unknown): v is DrawPlayCommand =>\n isRecord(v) && ((v['type'] === 'play' && typeof v['cardId'] === 'string') || v['type'] === 'draw');\nconst isEvent = (v: unknown): v is DrawPlayEvent =>\n isRecord(v) &&\n isSeat(v['seat']) &&\n ((v['type'] === 'drew' && (v['card'] === null || isCard(v['card']))) ||\n (v['type'] === 'played' && isCard(v['card'])) ||\n v['type'] === 'resigned');\nconst isConfig = (v: unknown): v is DrawPlayConfig => isRecord(v) && isInt(v['handSize'], 1, 4);\n\nconst canMove = (state: DrawPlayState, seat: SeatId): boolean =>\n (state.hands[seat]?.length ?? 0) > 0 || state.deck.length > 0;\n\nconst finish = (state: DrawPlayState, seat: SeatId): DrawPlayState => {\n const turn = nextActiveSeat(seat, state.hands.length, (s) => canMove(state, s));\n return turn === null ? { ...state, over: true } : { ...state, turn };\n};\n\nexport const drawPlay = defineEngine<DrawPlayState, DrawPlayCommand, DrawPlayView, DrawPlayEvent, DrawPlayConfig>({\n rulesVersion: 1,\n schemas: {\n config: guardSchema(isConfig, 'Expected { handSize } from 1 to 4.'),\n command: guardSchema(isCommand, 'Not a Draw and Play command.'),\n state: guardSchema(isState, 'Not a Draw and Play state.'),\n view: guardSchema(isView, 'Not a Draw and Play view.'),\n event: guardSchema(isEvent, 'Not a Draw and Play event.'),\n },\n init: (config: DrawPlayConfig, seats, rng) => {\n if (seats.length < 2 || seats.length > 4)\n return err(gameError('Seats', 'Draw and Play needs two to four players.'));\n const faces = rng.shuffle(standardDeck({ suits: ['S', 'H'] })) as SuitedFace[];\n const cards = assignIds(faces, rng);\n const hands = seats.map((_, s) => cards.slice(s * config.handSize, (s + 1) * config.handSize));\n const state: DrawPlayState = {\n hands,\n deck: cards.slice(seats.length * config.handSize),\n table: [],\n turn: rng.int(0, seats.length),\n scores: seats.map(() => 0),\n resigned: null,\n over: false,\n };\n return ok(state);\n },\n apply: (state: DrawPlayState, input) => {\n if (state.over) return err(gameError('GameOver', 'The game is over.'));\n if (input.kind === 'resign') {\n if (state.hands[input.seat] === undefined) return err(gameError('NoSuchSeat', 'There is no such seat.'));\n return ok({\n state: { ...state, resigned: input.seat, over: true },\n beats: [{ event: { type: 'resigned', seat: input.seat } }],\n });\n }\n if (input.kind !== 'command')\n return err(gameError('Unsupported', 'Draw and Play has no clocks, seat changes or jobs.'));\n if (input.seat !== state.turn) return err(gameError('NotYourTurn', \"It's not your turn.\"));\n const hand = state.hands[input.seat] ?? [];\n const command = input.command;\n if (command.type === 'draw') {\n const [card, ...deck] = state.deck;\n if (card === undefined) return err(gameError('DeckEmpty', 'The deck is empty.'));\n if (hand.length >= MAX_HAND) return err(gameError('HandFull', `You can hold at most ${MAX_HAND} cards.`));\n const hands = state.hands.map((h, s) => (s === input.seat ? [...h, card] : h));\n const next = finish({ ...state, hands, deck }, input.seat);\n return ok({ state: next, beats: [{ event: { type: 'drew', seat: input.seat, card } }] });\n }\n const card = hand.find((c) => c.id === command.cardId);\n if (card === undefined) return err(gameError('NotInHand', \"That card isn't in your hand.\"));\n const hands = state.hands.map((h, s) => (s === input.seat ? h.filter((c) => c.id !== card.id) : h));\n const scores = state.scores.map((score, s) => (s === input.seat ? score + card.rank : score));\n const next = finish({ ...state, hands, scores, table: [...state.table, { seat: input.seat, card }] }, input.seat);\n return ok({ state: next, beats: [{ event: { type: 'played', seat: input.seat, card } }] });\n },\n pending: (state) => (state.over ? [] : [state.turn]),\n viewFor: (state, viewer): DrawPlayView => ({\n hand: viewer === null ? null : (state.hands[viewer] ?? null),\n handSizes: state.hands.map((h) => h.length),\n deckSize: state.deck.length,\n table: state.table,\n turn: state.turn,\n scores: state.scores,\n resigned: state.resigned,\n over: state.over,\n }),\n eventFor: (event, viewer) => (event.type === 'drew' && event.seat !== viewer ? { ...event, card: null } : event),\n outcome: (state) =>\n state.over\n ? {\n result: 'won',\n placements: rankPlaces(state.scores, state.resigned),\n summary: state.resigned === null ? 'Every card has been played.' : `Seat ${state.resigned + 1} resigned.`,\n }\n : null,\n ai: {\n levels: ['easy'],\n choose: (view) => {\n const best = [...(view.hand ?? [])].sort((a, b) => b.rank - a.rank)[0];\n return best === undefined ? { type: 'draw' } : { type: 'play', cardId: best.id };\n },\n },\n});\n\n/** Shuffles everything `viewer` can't see: the deck and every other hand. */\nexport const perturbDrawPlay = (\n state: DrawPlayState,\n viewer: SeatId | null,\n rng: Parameters<typeof perturbHidden>[2],\n): DrawPlayState => {\n const zones: Record<string, readonly DrawPlayCard[]> = { deck: state.deck };\n state.hands.forEach((hand, s) => {\n zones[`hand${s}`] = hand;\n });\n const out = perturbHidden(zones, (zone) => zone === `hand${viewer}`, rng);\n return { ...state, deck: out['deck'] ?? [], hands: state.hands.map((_, s) => out[`hand${s}`] ?? []) };\n};\n\nconst cardsOf = (s: DrawPlayState): DrawPlayCard[] => [...s.hands.flat(), ...s.deck, ...s.table.map((t) => t.card)];\n\n/** Conformance options for the Draw and Play fixture. */\nexport const drawPlayConformance: EngineConformanceOptions<DrawPlayState, DrawPlayCommand, DrawPlayConfig> = {\n name: 'draw-play',\n configs: [{ handSize: 3 }],\n seatCounts: [2, 3, 4],\n games: 8,\n commands: (state, seat) => [\n { type: 'draw' },\n ...(state.hands[seat] ?? []).map((c): DrawPlayCommand => ({ type: 'play', cardId: c.id })),\n ],\n perturbHidden: perturbDrawPlay,\n invariants: (s) => {\n const cards = cardsOf(s);\n if (cards.length !== DRAW_PLAY_CARDS) return `${cards.length} cards in play, not ${DRAW_PLAY_CARDS}`;\n if (new Set(cards.map((c) => c.id)).size !== cards.length) return 'two cards share an id';\n const total = s.table.reduce((sum, t) => sum + t.card.rank, 0);\n if (s.scores.reduce((a, b) => a + b, 0) !== total) return 'the scores disagree with the table';\n return true;\n },\n};\n"],"mappings":";;;;;AAkDA,IAAa,kBAAkB;AAC/B,IAAM,WAAW;AAEjB,IAAM,UAAU,MACd,SAAS,CAAC,KAAK,OAAO,EAAE,OAAO,KAAK,OAAO,EAAE,OAAO,KAAK,WAAW,EAAE,KAAK;AAC7E,IAAM,WAAW,MACf,UAAU,IAAI,MAAiD,SAAS,CAAC,KAAK,OAAO,EAAE,OAAO,KAAK,OAAO,EAAE,OAAO,CAAC;AACtH,IAAM,YAAY,MAA8B,UAAU,IAAI,MAAmB,MAAM,GAAG,CAAC,CAAC;AAC5F,IAAM,WAAW,MACf,SAAS,CAAC,KACV,UAAU,EAAE,WAAW,MAA2B,UAAU,GAAG,MAAM,CAAC,KACtE,UAAU,EAAE,SAAS,MAAM,KAC3B,QAAQ,EAAE,QAAQ,KAClB,OAAO,EAAE,OAAO,KAChB,SAAS,EAAE,SAAS,KACpB,aAAa,EAAE,WAAW,KAC1B,OAAO,EAAE,YAAY;AACvB,IAAM,UAAU,MACd,SAAS,CAAC,MACT,EAAE,YAAY,QAAQ,UAAU,EAAE,SAAS,MAAM,MAClD,UAAU,EAAE,eAAe,MAAmB,MAAM,GAAG,CAAC,CAAC,KACzD,MAAM,EAAE,aAAa,CAAC,KACtB,QAAQ,EAAE,QAAQ,KAClB,OAAO,EAAE,OAAO,KAChB,SAAS,EAAE,SAAS,KACpB,aAAa,EAAE,WAAW,KAC1B,OAAO,EAAE,YAAY;AACvB,IAAM,aAAa,MACjB,SAAS,CAAC,MAAO,EAAE,YAAY,UAAU,OAAO,EAAE,cAAc,YAAa,EAAE,YAAY;AAC7F,IAAM,WAAW,MACf,SAAS,CAAC,KACV,OAAO,EAAE,OAAO,MACd,EAAE,YAAY,WAAW,EAAE,YAAY,QAAQ,OAAO,EAAE,OAAO,MAC9D,EAAE,YAAY,YAAY,OAAO,EAAE,OAAO,KAC3C,EAAE,YAAY;AAClB,IAAM,YAAY,MAAoC,SAAS,CAAC,KAAK,MAAM,EAAE,aAAa,GAAG,CAAC;AAE9F,IAAM,WAAW,OAAsB,UACpC,MAAM,MAAM,KAAK,EAAE,UAAU,KAAK,KAAK,MAAM,KAAK,SAAS;AAE9D,IAAM,UAAU,OAAsB,SAAgC;CACpE,MAAM,OAAO,eAAe,MAAM,MAAM,MAAM,SAAS,MAAM,QAAQ,OAAO,CAAC,CAAC;CAC9E,OAAO,SAAS,OAAO;EAAE,GAAG;EAAO,MAAM;CAAK,IAAI;EAAE,GAAG;EAAO;CAAK;AACrE;AAEA,IAAa,WAAW,aAA0F;CAChH,cAAc;CACd,SAAS;EACP,QAAQ,YAAY,UAAU,oCAAoC;EAClE,SAAS,YAAY,WAAW,8BAA8B;EAC9D,OAAO,YAAY,SAAS,4BAA4B;EACxD,MAAM,YAAY,QAAQ,2BAA2B;EACrD,OAAO,YAAY,SAAS,4BAA4B;CAC1D;CACA,OAAO,QAAwB,OAAO,QAAQ;EAC5C,IAAI,MAAM,SAAS,KAAK,MAAM,SAAS,GACrC,OAAO,IAAI,UAAU,SAAS,0CAA0C,CAAC;EAC3E,MAAM,QAAQ,IAAI,QAAQ,aAAa,EAAE,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;EAC7D,MAAM,QAAQ,UAAU,OAAO,GAAG;EAElC,MAAM,QAAuB;GAC3B,OAFY,MAAM,KAAK,GAAG,MAAM,MAAM,MAAM,IAAI,OAAO,WAAW,IAAI,KAAK,OAAO,QAAQ,CAE1F;GACA,MAAM,MAAM,MAAM,MAAM,SAAS,OAAO,QAAQ;GAChD,OAAO,CAAC;GACR,MAAM,IAAI,IAAI,GAAG,MAAM,MAAM;GAC7B,QAAQ,MAAM,UAAU,CAAC;GACzB,UAAU;GACV,MAAM;EACR;EACA,OAAO,GAAG,KAAK;CACjB;CACA,QAAQ,OAAsB,UAAU;EACtC,IAAI,MAAM,MAAM,OAAO,IAAI,UAAU,YAAY,mBAAmB,CAAC;EACrE,IAAI,MAAM,SAAS,UAAU;GAC3B,IAAI,MAAM,MAAM,MAAM,UAAU,KAAA,GAAW,OAAO,IAAI,UAAU,cAAc,wBAAwB,CAAC;GACvG,OAAO,GAAG;IACR,OAAO;KAAE,GAAG;KAAO,UAAU,MAAM;KAAM,MAAM;IAAK;IACpD,OAAO,CAAC,EAAE,OAAO;KAAE,MAAM;KAAY,MAAM,MAAM;IAAK,EAAE,CAAC;GAC3D,CAAC;EACH;EACA,IAAI,MAAM,SAAS,WACjB,OAAO,IAAI,UAAU,eAAe,oDAAoD,CAAC;EAC3F,IAAI,MAAM,SAAS,MAAM,MAAM,OAAO,IAAI,UAAU,eAAe,qBAAqB,CAAC;EACzF,MAAM,OAAO,MAAM,MAAM,MAAM,SAAS,CAAC;EACzC,MAAM,UAAU,MAAM;EACtB,IAAI,QAAQ,SAAS,QAAQ;GAC3B,MAAM,CAAC,MAAM,GAAG,QAAQ,MAAM;GAC9B,IAAI,SAAS,KAAA,GAAW,OAAO,IAAI,UAAU,aAAa,oBAAoB,CAAC;GAC/E,IAAI,KAAK,UAAU,UAAU,OAAO,IAAI,UAAU,YAAY,wBAAwB,SAAS,QAAQ,CAAC;GACxG,MAAM,QAAQ,MAAM,MAAM,KAAK,GAAG,MAAO,MAAM,MAAM,OAAO,CAAC,GAAG,GAAG,IAAI,IAAI,CAAE;GAC7E,MAAM,OAAO,OAAO;IAAE,GAAG;IAAO;IAAO;GAAK,GAAG,MAAM,IAAI;GACzD,OAAO,GAAG;IAAE,OAAO;IAAM,OAAO,CAAC,EAAE,OAAO;KAAE,MAAM;KAAQ,MAAM,MAAM;KAAM;IAAK,EAAE,CAAC;GAAE,CAAC;EACzF;EACA,MAAM,OAAO,KAAK,MAAM,MAAM,EAAE,OAAO,QAAQ,MAAM;EACrD,IAAI,SAAS,KAAA,GAAW,OAAO,IAAI,UAAU,aAAa,+BAA+B,CAAC;EAC1F,MAAM,QAAQ,MAAM,MAAM,KAAK,GAAG,MAAO,MAAM,MAAM,OAAO,EAAE,QAAQ,MAAM,EAAE,OAAO,KAAK,EAAE,IAAI,CAAE;EAClG,MAAM,SAAS,MAAM,OAAO,KAAK,OAAO,MAAO,MAAM,MAAM,OAAO,QAAQ,KAAK,OAAO,KAAM;EAC5F,MAAM,OAAO,OAAO;GAAE,GAAG;GAAO;GAAO;GAAQ,OAAO,CAAC,GAAG,MAAM,OAAO;IAAE,MAAM,MAAM;IAAM;GAAK,CAAC;EAAE,GAAG,MAAM,IAAI;EAChH,OAAO,GAAG;GAAE,OAAO;GAAM,OAAO,CAAC,EAAE,OAAO;IAAE,MAAM;IAAU,MAAM,MAAM;IAAM;GAAK,EAAE,CAAC;EAAE,CAAC;CAC3F;CACA,UAAU,UAAW,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,IAAI;CAClD,UAAU,OAAO,YAA0B;EACzC,MAAM,WAAW,OAAO,OAAQ,MAAM,MAAM,WAAW;EACvD,WAAW,MAAM,MAAM,KAAK,MAAM,EAAE,MAAM;EAC1C,UAAU,MAAM,KAAK;EACrB,OAAO,MAAM;EACb,MAAM,MAAM;EACZ,QAAQ,MAAM;EACd,UAAU,MAAM;EAChB,MAAM,MAAM;CACd;CACA,WAAW,OAAO,WAAY,MAAM,SAAS,UAAU,MAAM,SAAS,SAAS;EAAE,GAAG;EAAO,MAAM;CAAK,IAAI;CAC1G,UAAU,UACR,MAAM,OACF;EACE,QAAQ;EACR,YAAY,WAAW,MAAM,QAAQ,MAAM,QAAQ;EACnD,SAAS,MAAM,aAAa,OAAO,gCAAgC,QAAQ,MAAM,WAAW,EAAE;CAChG,IACA;CACN,IAAI;EACF,QAAQ,CAAC,MAAM;EACf,SAAS,SAAS;GAChB,MAAM,OAAO,CAAC,GAAI,KAAK,QAAQ,CAAC,CAAE,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;GACpE,OAAO,SAAS,KAAA,IAAY,EAAE,MAAM,OAAO,IAAI;IAAE,MAAM;IAAQ,QAAQ,KAAK;GAAG;EACjF;CACF;AACF,CAAC;;AAGD,IAAa,mBACX,OACA,QACA,QACkB;CAClB,MAAM,QAAiD,EAAE,MAAM,MAAM,KAAK;CAC1E,MAAM,MAAM,SAAS,MAAM,MAAM;EAC/B,MAAM,OAAO,OAAO;CACtB,CAAC;CACD,MAAM,MAAM,cAAc,QAAQ,SAAS,SAAS,OAAO,UAAU,GAAG;CACxE,OAAO;EAAE,GAAG;EAAO,MAAM,IAAI,WAAW,CAAC;EAAG,OAAO,MAAM,MAAM,KAAK,GAAG,MAAM,IAAI,OAAO,QAAQ,CAAC,CAAC;CAAE;AACtG;AAEA,IAAM,WAAW,MAAqC;CAAC,GAAG,EAAE,MAAM,KAAK;CAAG,GAAG,EAAE;CAAM,GAAG,EAAE,MAAM,KAAK,MAAM,EAAE,IAAI;AAAC;;AAGlH,IAAa,sBAAgG;CAC3G,MAAM;CACN,SAAS,CAAC,EAAE,UAAU,EAAE,CAAC;CACzB,YAAY;EAAC;EAAG;EAAG;CAAC;CACpB,OAAO;CACP,WAAW,OAAO,SAAS,CACzB,EAAE,MAAM,OAAO,GACf,IAAI,MAAM,MAAM,SAAS,CAAC,EAAA,CAAG,KAAK,OAAwB;EAAE,MAAM;EAAQ,QAAQ,EAAE;CAAG,EAAE,CAC3F;CACA,eAAe;CACf,aAAa,MAAM;EACjB,MAAM,QAAQ,QAAQ,CAAC;EACvB,IAAI,MAAM,WAAA,IAA4B,OAAO,GAAG,MAAM,OAAO;EAC7D,IAAI,IAAI,IAAI,MAAM,KAAK,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,MAAM,QAAQ,OAAO;EAClE,MAAM,QAAQ,EAAE,MAAM,QAAQ,KAAK,MAAM,MAAM,EAAE,KAAK,MAAM,CAAC;EAC7D,IAAI,EAAE,OAAO,QAAQ,GAAG,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,OAAO;EAC1D,OAAO;CACT;AACF"}
@@ -0,0 +1,34 @@
1
+ import type { SimulationConformanceOptions } from '../simulation-conformance.js';
2
+ export type DriftState = {
3
+ readonly tick: number;
4
+ readonly length: number;
5
+ readonly limit: number;
6
+ readonly pos: number;
7
+ readonly vel: number;
8
+ readonly wind: number;
9
+ readonly target: number;
10
+ readonly score: number;
11
+ };
12
+ export type DriftInput = {
13
+ readonly type: 'nudge';
14
+ readonly dir: -1 | 1;
15
+ };
16
+ export type DriftEvent = {
17
+ readonly type: 'scored';
18
+ readonly score: number;
19
+ } | {
20
+ readonly type: 'bounced';
21
+ };
22
+ export type DriftConfig = {
23
+ readonly length: number;
24
+ readonly limit: number;
25
+ };
26
+ export type DriftView = {
27
+ readonly x: number;
28
+ readonly target: number;
29
+ readonly score: number;
30
+ };
31
+ export declare const MAX_SPEED = 3;
32
+ export declare const drift: import("@joystyk/contracts").FixedStepSimulation<DriftState, DriftInput, DriftView, DriftEvent, DriftConfig>;
33
+ /** Conformance options for the Drift fixture. */
34
+ export declare const driftConformance: SimulationConformanceOptions<DriftState, DriftInput, DriftConfig>;
@@ -0,0 +1,109 @@
1
+ import { guardSchema } from "../builders.js";
2
+ import { isInt, isRecord } from "./guards.js";
3
+ import { defineSimulation, ok } from "@joystyk/contracts";
4
+ //#region src/fixtures/drift.ts
5
+ var MAX_SPEED = 3;
6
+ var isState = (v) => isRecord(v) && [
7
+ "tick",
8
+ "length",
9
+ "limit",
10
+ "pos",
11
+ "target",
12
+ "score"
13
+ ].every((k) => isInt(v[k], 0)) && isInt(v["vel"], -3, 3) && isInt(v["wind"], -1, 1);
14
+ var isInput = (v) => isRecord(v) && v["type"] === "nudge" && (v["dir"] === -1 || v["dir"] === 1);
15
+ var isEvent = (v) => isRecord(v) && (v["type"] === "scored" && isInt(v["score"], 0) || v["type"] === "bounced");
16
+ var isConfig = (v) => isRecord(v) && isInt(v["length"], 4, 1e3) && isInt(v["limit"], 1);
17
+ var clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
18
+ var drift = defineSimulation({
19
+ rulesVersion: 1,
20
+ stepHz: 60,
21
+ schemas: {
22
+ config: guardSchema(isConfig, "Expected { length, limit }."),
23
+ input: guardSchema(isInput, "Expected { type: \"nudge\", dir: -1 | 1 }."),
24
+ state: guardSchema(isState, "Not a Drift state."),
25
+ event: guardSchema(isEvent, "Not a Drift event.")
26
+ },
27
+ init: (config, rng) => ok({
28
+ tick: 0,
29
+ length: config.length,
30
+ limit: config.limit,
31
+ pos: rng.int(0, config.length + 1),
32
+ vel: 0,
33
+ wind: 0,
34
+ target: rng.int(0, config.length + 1),
35
+ score: 0
36
+ }),
37
+ step: (state, inputs, rng) => {
38
+ let vel = state.vel;
39
+ for (const input of inputs) vel = clamp(vel + input.dir, -3, 3);
40
+ const wind = state.tick % 30 === 0 ? rng.int(-1, 2) : state.wind;
41
+ let pos = state.pos + vel + wind;
42
+ const events = [];
43
+ if (pos < 0) {
44
+ pos = 0 - pos;
45
+ vel = Math.abs(vel);
46
+ events.push({ type: "bounced" });
47
+ } else if (pos > state.length) {
48
+ pos = 2 * state.length - pos;
49
+ vel = 0 - Math.abs(vel);
50
+ events.push({ type: "bounced" });
51
+ }
52
+ let { target, score } = state;
53
+ if (Math.abs(pos - target) <= 1) {
54
+ score += 1;
55
+ target = rng.int(0, state.length + 1);
56
+ events.push({
57
+ type: "scored",
58
+ score
59
+ });
60
+ }
61
+ return {
62
+ state: {
63
+ ...state,
64
+ tick: state.tick + 1,
65
+ pos,
66
+ vel,
67
+ wind,
68
+ target,
69
+ score
70
+ },
71
+ events
72
+ };
73
+ },
74
+ view: (state) => ({
75
+ x: state.pos,
76
+ target: state.target,
77
+ score: state.score
78
+ }),
79
+ outcome: (state) => state.tick >= state.limit ? {
80
+ result: "completed",
81
+ placements: [{
82
+ seat: 0,
83
+ place: 1,
84
+ score: state.score
85
+ }],
86
+ summary: `${state.score} points.`,
87
+ stats: { score: state.score }
88
+ } : null
89
+ });
90
+ /** Conformance options for the Drift fixture. */
91
+ var driftConformance = {
92
+ name: "drift",
93
+ configs: [{
94
+ length: 40,
95
+ limit: 600
96
+ }],
97
+ runs: 3,
98
+ ticks: 1e3,
99
+ requireOutcome: true,
100
+ inputs: (_state, _tick, rng) => rng.int(0, 5) === 0 ? [{
101
+ type: "nudge",
102
+ dir: rng.int(0, 2) === 0 ? -1 : 1
103
+ }] : [],
104
+ invariants: (s) => s.pos >= 0 && s.pos <= s.length ? true : `the puck left the track at ${s.pos}`
105
+ };
106
+ //#endregion
107
+ export { MAX_SPEED, drift, driftConformance };
108
+
109
+ //# sourceMappingURL=drift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drift.js","names":[],"sources":["../../src/fixtures/drift.ts"],"sourcesContent":["// Drift: a tiny fixed-step simulation. A puck slides along a track, bouncing off both ends;\n// the player nudges it left or right, and a wind gust from the seeded generator changes every\n// 30 ticks. Touching the target scores a point and moves the target. The run ends after the\n// configured number of ticks. Integer arithmetic only, so replays match in every engine.\n\nimport { defineSimulation, ok, type Rng } from '@joystyk/contracts';\nimport { guardSchema } from '../builders.ts';\nimport type { SimulationConformanceOptions } from '../simulation-conformance.ts';\nimport { isInt, isRecord } from './guards.ts';\n\nexport type DriftState = {\n readonly tick: number;\n readonly length: number;\n readonly limit: number;\n readonly pos: number;\n readonly vel: number;\n readonly wind: number;\n readonly target: number;\n readonly score: number;\n};\nexport type DriftInput = { readonly type: 'nudge'; readonly dir: -1 | 1 };\nexport type DriftEvent = { readonly type: 'scored'; readonly score: number } | { readonly type: 'bounced' };\nexport type DriftConfig = { readonly length: number; readonly limit: number };\nexport type DriftView = { readonly x: number; readonly target: number; readonly score: number };\n\nexport const MAX_SPEED = 3;\n\nconst isState = (v: unknown): v is DriftState =>\n isRecord(v) &&\n ['tick', 'length', 'limit', 'pos', 'target', 'score'].every((k) => isInt(v[k], 0)) &&\n isInt(v['vel'], -MAX_SPEED, MAX_SPEED) &&\n isInt(v['wind'], -1, 1);\nconst isInput = (v: unknown): v is DriftInput =>\n isRecord(v) && v['type'] === 'nudge' && (v['dir'] === -1 || v['dir'] === 1);\nconst isEvent = (v: unknown): v is DriftEvent =>\n isRecord(v) && ((v['type'] === 'scored' && isInt(v['score'], 0)) || v['type'] === 'bounced');\nconst isConfig = (v: unknown): v is DriftConfig => isRecord(v) && isInt(v['length'], 4, 1000) && isInt(v['limit'], 1);\n\nconst clamp = (n: number, lo: number, hi: number): number => Math.max(lo, Math.min(hi, n));\n\nexport const drift = defineSimulation<DriftState, DriftInput, DriftView, DriftEvent, DriftConfig>({\n rulesVersion: 1,\n stepHz: 60,\n schemas: {\n config: guardSchema(isConfig, 'Expected { length, limit }.'),\n input: guardSchema(isInput, 'Expected { type: \"nudge\", dir: -1 | 1 }.'),\n state: guardSchema(isState, 'Not a Drift state.'),\n event: guardSchema(isEvent, 'Not a Drift event.'),\n },\n init: (config: DriftConfig, rng) =>\n ok({\n tick: 0,\n length: config.length,\n limit: config.limit,\n pos: rng.int(0, config.length + 1),\n vel: 0,\n wind: 0,\n target: rng.int(0, config.length + 1),\n score: 0,\n }),\n step: (state: DriftState, inputs: readonly DriftInput[], rng: Rng) => {\n let vel = state.vel;\n for (const input of inputs) vel = clamp(vel + input.dir, -MAX_SPEED, MAX_SPEED);\n const wind = state.tick % 30 === 0 ? rng.int(-1, 2) : state.wind;\n let pos = state.pos + vel + wind;\n const events: DriftEvent[] = [];\n if (pos < 0) {\n pos = 0 - pos;\n vel = Math.abs(vel);\n events.push({ type: 'bounced' });\n } else if (pos > state.length) {\n pos = 2 * state.length - pos;\n vel = 0 - Math.abs(vel);\n events.push({ type: 'bounced' });\n }\n let { target, score } = state;\n if (Math.abs(pos - target) <= 1) {\n score += 1;\n target = rng.int(0, state.length + 1);\n events.push({ type: 'scored', score });\n }\n return { state: { ...state, tick: state.tick + 1, pos, vel, wind, target, score }, events };\n },\n view: (state): DriftView => ({ x: state.pos, target: state.target, score: state.score }),\n outcome: (state) =>\n state.tick >= state.limit\n ? {\n result: 'completed',\n placements: [{ seat: 0, place: 1, score: state.score }],\n summary: `${state.score} points.`,\n stats: { score: state.score },\n }\n : null,\n});\n\n/** Conformance options for the Drift fixture. */\nexport const driftConformance: SimulationConformanceOptions<DriftState, DriftInput, DriftConfig> = {\n name: 'drift',\n configs: [{ length: 40, limit: 600 }],\n runs: 3,\n ticks: 1000,\n requireOutcome: true,\n inputs: (_state, _tick, rng) => (rng.int(0, 5) === 0 ? [{ type: 'nudge', dir: rng.int(0, 2) === 0 ? -1 : 1 }] : []),\n invariants: (s) => (s.pos >= 0 && s.pos <= s.length ? true : `the puck left the track at ${s.pos}`),\n};\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
2
+ export declare const isInt: (value: unknown, min?: number, max?: number) => value is number;
3
+ export declare const isSeat: (value: unknown) => value is number;
4
+ export declare const isSeatOrNull: (value: unknown) => value is number | null;
5
+ export declare const isArrayOf: <T>(value: unknown, item: (v: unknown) => v is T) => value is T[];
6
+ /** Placements and results shared by the fixtures' outcomes. */
7
+ export declare const rankPlaces: (scores: readonly number[], last?: number | null) => {
8
+ seat: number;
9
+ place: number;
10
+ score: number;
11
+ }[];
@@ -0,0 +1,16 @@
1
+ //#region src/fixtures/guards.ts
2
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3
+ var isInt = (value, min = -Infinity, max = Infinity) => typeof value === "number" && Number.isInteger(value) && value >= min && value <= max && !Object.is(value, -0);
4
+ var isSeat = (value) => isInt(value, 0, 7);
5
+ var isSeatOrNull = (value) => value === null || isSeat(value);
6
+ var isArrayOf = (value, item) => Array.isArray(value) && value.every((v) => item(v));
7
+ /** Placements and results shared by the fixtures' outcomes. */
8
+ var rankPlaces = (scores, last = null) => scores.map((score, seat) => ({
9
+ seat,
10
+ place: seat === last ? scores.length : 1 + scores.filter((other, s) => s !== last && other > score).length,
11
+ score
12
+ }));
13
+ //#endregion
14
+ export { isArrayOf, isInt, isRecord, isSeat, isSeatOrNull, rankPlaces };
15
+
16
+ //# sourceMappingURL=guards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.js","names":[],"sources":["../../src/fixtures/guards.ts"],"sourcesContent":["// Tiny type guards for the fixtures' schemas.\n\nexport const isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nexport const isInt = (value: unknown, min = -Infinity, max = Infinity): value is number =>\n typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max && !Object.is(value, -0);\n\nexport const isSeat = (value: unknown): value is number => isInt(value, 0, 7);\n\nexport const isSeatOrNull = (value: unknown): value is number | null => value === null || isSeat(value);\n\nexport const isArrayOf = <T>(value: unknown, item: (v: unknown) => v is T): value is T[] =>\n Array.isArray(value) && value.every((v) => item(v));\n\n/** Placements and results shared by the fixtures' outcomes. */\nexport const rankPlaces = (\n scores: readonly number[],\n last: number | null = null,\n): { seat: number; place: number; score: number }[] =>\n scores.map((score, seat) => ({\n seat,\n place: seat === last ? scores.length : 1 + scores.filter((other, s) => s !== last && other > score).length,\n score,\n }));\n"],"mappings":";AAEA,IAAa,YAAY,UACvB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAErE,IAAa,SAAS,OAAgB,MAAM,WAAW,MAAM,aAC3D,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,KAAK,SAAS,OAAO,SAAS,OAAO,CAAC,OAAO,GAAG,OAAO,EAAE;AAE9G,IAAa,UAAU,UAAoC,MAAM,OAAO,GAAG,CAAC;AAE5E,IAAa,gBAAgB,UAA2C,UAAU,QAAQ,OAAO,KAAK;AAEtG,IAAa,aAAgB,OAAgB,SAC3C,MAAM,QAAQ,KAAK,KAAK,MAAM,OAAO,MAAM,KAAK,CAAC,CAAC;;AAGpD,IAAa,cACX,QACA,OAAsB,SAEtB,OAAO,KAAK,OAAO,UAAU;CAC3B;CACA,OAAO,SAAS,OAAO,OAAO,SAAS,IAAI,OAAO,QAAQ,OAAO,MAAM,MAAM,QAAQ,QAAQ,KAAK,CAAC,CAAC;CACpG;AACF,EAAE"}
@@ -0,0 +1,47 @@
1
+ import { type EngineInput, type SeatId } from '@joystyk/contracts';
2
+ import type { EngineConformanceOptions } from '../engine-conformance.js';
3
+ export type Clue = 'higher' | 'lower' | 'correct';
4
+ export type HintPuzzleState = {
5
+ readonly max: number;
6
+ readonly secret: number;
7
+ readonly guesses: readonly {
8
+ readonly n: number;
9
+ readonly clue: Clue;
10
+ }[];
11
+ readonly hints: readonly ('even' | 'odd')[];
12
+ readonly solved: boolean;
13
+ readonly gaveUp: boolean;
14
+ };
15
+ export type HintPuzzleCommand = {
16
+ readonly type: 'guess';
17
+ readonly n: number;
18
+ } | {
19
+ readonly type: 'hint';
20
+ };
21
+ export type HintPuzzleEvent = {
22
+ readonly type: 'guessed';
23
+ readonly n: number;
24
+ readonly clue: Clue;
25
+ } | {
26
+ readonly type: 'hinted';
27
+ readonly parity: 'even' | 'odd';
28
+ } | {
29
+ readonly type: 'gave-up';
30
+ };
31
+ export type HintPuzzleView = Omit<HintPuzzleState, 'secret'> & {
32
+ readonly secret: number | null;
33
+ };
34
+ export type HintPuzzleConfig = {
35
+ readonly max: number;
36
+ };
37
+ /** At most this many hints per game. */
38
+ export declare const MAX_HINTS = 2;
39
+ export declare const hintPuzzle: import("@joystyk/contracts").DiscreteEngine<HintPuzzleState, HintPuzzleCommand, HintPuzzleView, HintPuzzleEvent, HintPuzzleConfig>;
40
+ /** The spec's barrier rule: every hint. */
41
+ export declare const isHint: (input: EngineInput<HintPuzzleCommand>) => boolean;
42
+ /** Moves the secret, which the player can't see until solving the puzzle. */
43
+ export declare const perturbHintPuzzle: (state: HintPuzzleState, _viewer: SeatId | null, rng: {
44
+ int(min: number, max: number): number;
45
+ }) => HintPuzzleState;
46
+ /** Conformance options for the Hint Puzzle fixture. */
47
+ export declare const hintPuzzleConformance: EngineConformanceOptions<HintPuzzleState, HintPuzzleCommand, HintPuzzleConfig>;
@@ -0,0 +1,129 @@
1
+ import { guardSchema } from "../builders.js";
2
+ import { isArrayOf, isInt, isRecord } from "./guards.js";
3
+ import { defineEngine, err, gameError, ok } from "@joystyk/contracts";
4
+ //#region src/fixtures/hint-puzzle.ts
5
+ /** At most this many hints per game. */
6
+ var MAX_HINTS = 2;
7
+ var isClue = (v) => v === "higher" || v === "lower" || v === "correct";
8
+ var isGuesses = (v) => isArrayOf(v, (g) => isRecord(g) && isInt(g["n"]) && isClue(g["clue"]));
9
+ var isHints = (v) => isArrayOf(v, (h) => h === "even" || h === "odd");
10
+ var isState = (v) => isRecord(v) && isInt(v["max"], 2) && isInt(v["secret"], 1) && isGuesses(v["guesses"]) && isHints(v["hints"]) && typeof v["solved"] === "boolean" && typeof v["gaveUp"] === "boolean";
11
+ var isView = (v) => isRecord(v) && isInt(v["max"], 2) && (v["secret"] === null || isInt(v["secret"], 1)) && isGuesses(v["guesses"]) && isHints(v["hints"]) && typeof v["solved"] === "boolean" && typeof v["gaveUp"] === "boolean";
12
+ var isCommand = (v) => isRecord(v) && (v["type"] === "guess" && isInt(v["n"]) || v["type"] === "hint");
13
+ var isEvent = (v) => isRecord(v) && (v["type"] === "guessed" && isInt(v["n"]) && isClue(v["clue"]) || v["type"] === "hinted" && (v["parity"] === "even" || v["parity"] === "odd") || v["type"] === "gave-up");
14
+ var over = (s) => s.solved || s.gaveUp;
15
+ var hintPuzzle = defineEngine({
16
+ rulesVersion: 1,
17
+ schemas: {
18
+ config: guardSchema((v) => isRecord(v) && isInt(v["max"], 2, 1e3), "Expected { max }."),
19
+ command: guardSchema(isCommand, "Not a Hint Puzzle command."),
20
+ state: guardSchema(isState, "Not a Hint Puzzle state."),
21
+ view: guardSchema(isView, "Not a Hint Puzzle view."),
22
+ event: guardSchema(isEvent, "Not a Hint Puzzle event.")
23
+ },
24
+ init: (config, seats, rng) => seats.length === 1 ? ok({
25
+ max: config.max,
26
+ secret: rng.int(1, config.max + 1),
27
+ guesses: [],
28
+ hints: [],
29
+ solved: false,
30
+ gaveUp: false
31
+ }) : err(gameError("Seats", "Hint Puzzle is a solo game.")),
32
+ apply: (state, input) => {
33
+ if (over(state)) return err(gameError("GameOver", "The puzzle is finished."));
34
+ if (input.kind !== "job" && input.seat !== 0) return err(gameError("NotYourTurn", "Only seat 0 plays."));
35
+ if (input.kind === "resign") return ok({
36
+ state: {
37
+ ...state,
38
+ gaveUp: true
39
+ },
40
+ beats: [{ event: { type: "gave-up" } }]
41
+ });
42
+ if (input.kind !== "command") return err(gameError("Unsupported", "Hint Puzzle has no clocks, seat changes or jobs."));
43
+ const command = input.command;
44
+ if (command.type === "hint") {
45
+ if (state.hints.length >= 2) return err(gameError("NoHintsLeft", "No hints left."));
46
+ const parity = state.secret % 2 === 0 ? "even" : "odd";
47
+ return ok({
48
+ state: {
49
+ ...state,
50
+ hints: [...state.hints, parity]
51
+ },
52
+ beats: [{ event: {
53
+ type: "hinted",
54
+ parity
55
+ } }],
56
+ undoBarrier: true,
57
+ assist: "hint"
58
+ });
59
+ }
60
+ if (command.n < 1 || command.n > state.max) return err(gameError("OutOfRange", `Guess from 1 to ${state.max}.`));
61
+ const clue = command.n === state.secret ? "correct" : command.n < state.secret ? "higher" : "lower";
62
+ const next = {
63
+ ...state,
64
+ guesses: [...state.guesses, {
65
+ n: command.n,
66
+ clue
67
+ }],
68
+ solved: clue === "correct"
69
+ };
70
+ const beats = [{ event: {
71
+ type: "guessed",
72
+ n: command.n,
73
+ clue
74
+ } }];
75
+ return ok(state.guesses.length === 0 ? {
76
+ state: next,
77
+ beats,
78
+ startsClock: true
79
+ } : {
80
+ state: next,
81
+ beats
82
+ });
83
+ },
84
+ clockStart: "transition",
85
+ worthSaving: (state) => state.guesses.length > 0 || state.hints.length > 0,
86
+ pending: (state) => over(state) ? [] : [0],
87
+ viewFor: (state) => ({
88
+ ...state,
89
+ secret: state.solved ? state.secret : null
90
+ }),
91
+ eventFor: (event) => event,
92
+ outcome: (state) => over(state) ? {
93
+ result: state.solved ? "won" : "abandoned",
94
+ placements: [{
95
+ seat: 0,
96
+ place: 1
97
+ }],
98
+ summary: state.solved ? `Solved in ${state.guesses.length} guesses.` : "Gave up.",
99
+ stats: {
100
+ guesses: state.guesses.length,
101
+ hints: state.hints.length
102
+ }
103
+ } : null
104
+ });
105
+ /** The spec's barrier rule: every hint. */
106
+ var isHint = (input) => input.kind === "command" && input.command.type === "hint";
107
+ /** Moves the secret, which the player can't see until solving the puzzle. */
108
+ var perturbHintPuzzle = (state, _viewer, rng) => state.solved ? state : {
109
+ ...state,
110
+ secret: rng.int(1, state.max + 1)
111
+ };
112
+ /** Conformance options for the Hint Puzzle fixture. */
113
+ var hintPuzzleConformance = {
114
+ name: "hint-puzzle",
115
+ configs: [{ max: 12 }],
116
+ seatCounts: [1],
117
+ commands: (state) => [{ type: "hint" }, ...Array.from({ length: state.max + 1 }, (_, n) => ({
118
+ type: "guess",
119
+ n
120
+ }))],
121
+ perturbHidden: perturbHintPuzzle,
122
+ undoBarrier: isHint,
123
+ assist: (input) => isHint(input) ? "hint" : null,
124
+ invariants: (s) => s.solved === s.guesses.some((g) => g.clue === "correct") ? true : "solved disagrees with the guesses"
125
+ };
126
+ //#endregion
127
+ export { MAX_HINTS, hintPuzzle, hintPuzzleConformance, isHint, perturbHintPuzzle };
128
+
129
+ //# sourceMappingURL=hint-puzzle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hint-puzzle.js","names":[],"sources":["../../src/fixtures/hint-puzzle.ts"],"sourcesContent":["// Hint Puzzle: a solo guessing puzzle. The engine picks a secret number; each guess is told\n// \"higher\", \"lower\" or \"correct\". A hint reveals whether the secret is even or odd; its\n// transition flags `assist: 'hint'` and is an undo barrier, as Sudoku's hints are. The\n// secret is hidden until solved, so the no-leak check applies to a solo game too. It uses\n// both session hooks: the first guess starts the clock (a hint doesn't), and a puzzle with\n// no guess or hint isn't worth saving.\n\nimport { defineEngine, err, gameError, ok, type EngineInput, type SeatId } from '@joystyk/contracts';\nimport { guardSchema } from '../builders.ts';\nimport type { EngineConformanceOptions } from '../engine-conformance.ts';\nimport { isArrayOf, isInt, isRecord } from './guards.ts';\n\nexport type Clue = 'higher' | 'lower' | 'correct';\nexport type HintPuzzleState = {\n readonly max: number;\n readonly secret: number;\n readonly guesses: readonly { readonly n: number; readonly clue: Clue }[];\n readonly hints: readonly ('even' | 'odd')[];\n readonly solved: boolean;\n readonly gaveUp: boolean;\n};\nexport type HintPuzzleCommand = { readonly type: 'guess'; readonly n: number } | { readonly type: 'hint' };\nexport type HintPuzzleEvent =\n | { readonly type: 'guessed'; readonly n: number; readonly clue: Clue }\n | { readonly type: 'hinted'; readonly parity: 'even' | 'odd' }\n | { readonly type: 'gave-up' };\nexport type HintPuzzleView = Omit<HintPuzzleState, 'secret'> & { readonly secret: number | null };\nexport type HintPuzzleConfig = { readonly max: number };\n\n/** At most this many hints per game. */\nexport const MAX_HINTS = 2;\n\nconst isClue = (v: unknown): v is Clue => v === 'higher' || v === 'lower' || v === 'correct';\nconst isGuesses = (v: unknown): v is HintPuzzleState['guesses'] =>\n isArrayOf(v, (g): g is { n: number; clue: Clue } => isRecord(g) && isInt(g['n']) && isClue(g['clue']));\nconst isHints = (v: unknown): v is ('even' | 'odd')[] =>\n isArrayOf(v, (h): h is 'even' | 'odd' => h === 'even' || h === 'odd');\nconst isState = (v: unknown): v is HintPuzzleState =>\n isRecord(v) &&\n isInt(v['max'], 2) &&\n isInt(v['secret'], 1) &&\n isGuesses(v['guesses']) &&\n isHints(v['hints']) &&\n typeof v['solved'] === 'boolean' &&\n typeof v['gaveUp'] === 'boolean';\nconst isView = (v: unknown): v is HintPuzzleView =>\n isRecord(v) &&\n isInt(v['max'], 2) &&\n (v['secret'] === null || isInt(v['secret'], 1)) &&\n isGuesses(v['guesses']) &&\n isHints(v['hints']) &&\n typeof v['solved'] === 'boolean' &&\n typeof v['gaveUp'] === 'boolean';\nconst isCommand = (v: unknown): v is HintPuzzleCommand =>\n isRecord(v) && ((v['type'] === 'guess' && isInt(v['n'])) || v['type'] === 'hint');\nconst isEvent = (v: unknown): v is HintPuzzleEvent =>\n isRecord(v) &&\n ((v['type'] === 'guessed' && isInt(v['n']) && isClue(v['clue'])) ||\n (v['type'] === 'hinted' && (v['parity'] === 'even' || v['parity'] === 'odd')) ||\n v['type'] === 'gave-up');\n\nconst over = (s: HintPuzzleState): boolean => s.solved || s.gaveUp;\n\nexport const hintPuzzle = defineEngine<\n HintPuzzleState,\n HintPuzzleCommand,\n HintPuzzleView,\n HintPuzzleEvent,\n HintPuzzleConfig\n>({\n rulesVersion: 1,\n schemas: {\n config: guardSchema((v): v is HintPuzzleConfig => isRecord(v) && isInt(v['max'], 2, 1000), 'Expected { max }.'),\n command: guardSchema(isCommand, 'Not a Hint Puzzle command.'),\n state: guardSchema(isState, 'Not a Hint Puzzle state.'),\n view: guardSchema(isView, 'Not a Hint Puzzle view.'),\n event: guardSchema(isEvent, 'Not a Hint Puzzle event.'),\n },\n init: (config: HintPuzzleConfig, seats, rng) =>\n seats.length === 1\n ? ok({\n max: config.max,\n secret: rng.int(1, config.max + 1),\n guesses: [],\n hints: [],\n solved: false,\n gaveUp: false,\n })\n : err(gameError('Seats', 'Hint Puzzle is a solo game.')),\n apply: (state: HintPuzzleState, input) => {\n if (over(state)) return err(gameError('GameOver', 'The puzzle is finished.'));\n if (input.kind !== 'job' && input.seat !== 0) return err(gameError('NotYourTurn', 'Only seat 0 plays.'));\n if (input.kind === 'resign')\n return ok({ state: { ...state, gaveUp: true }, beats: [{ event: { type: 'gave-up' } }] });\n if (input.kind !== 'command')\n return err(gameError('Unsupported', 'Hint Puzzle has no clocks, seat changes or jobs.'));\n const command = input.command;\n if (command.type === 'hint') {\n if (state.hints.length >= MAX_HINTS) return err(gameError('NoHintsLeft', 'No hints left.'));\n const parity = state.secret % 2 === 0 ? 'even' : 'odd';\n return ok({\n state: { ...state, hints: [...state.hints, parity] },\n beats: [{ event: { type: 'hinted', parity } }],\n undoBarrier: true,\n assist: 'hint',\n });\n }\n if (command.n < 1 || command.n > state.max) return err(gameError('OutOfRange', `Guess from 1 to ${state.max}.`));\n const clue: Clue = command.n === state.secret ? 'correct' : command.n < state.secret ? 'higher' : 'lower';\n const next = { ...state, guesses: [...state.guesses, { n: command.n, clue }], solved: clue === 'correct' };\n const beats = [{ event: { type: 'guessed', n: command.n, clue } as const }];\n return ok(state.guesses.length === 0 ? { state: next, beats, startsClock: true } : { state: next, beats });\n },\n clockStart: 'transition',\n worthSaving: (state) => state.guesses.length > 0 || state.hints.length > 0,\n pending: (state) => (over(state) ? [] : [0]),\n viewFor: (state): HintPuzzleView => ({ ...state, secret: state.solved ? state.secret : null }),\n eventFor: (event) => event,\n outcome: (state) =>\n over(state)\n ? {\n result: state.solved ? 'won' : 'abandoned',\n placements: [{ seat: 0, place: 1 }],\n summary: state.solved ? `Solved in ${state.guesses.length} guesses.` : 'Gave up.',\n stats: { guesses: state.guesses.length, hints: state.hints.length },\n }\n : null,\n});\n\n/** The spec's barrier rule: every hint. */\nexport const isHint = (input: EngineInput<HintPuzzleCommand>): boolean =>\n input.kind === 'command' && input.command.type === 'hint';\n\n/** Moves the secret, which the player can't see until solving the puzzle. */\nexport const perturbHintPuzzle = (\n state: HintPuzzleState,\n _viewer: SeatId | null,\n rng: { int(min: number, max: number): number },\n): HintPuzzleState => (state.solved ? state : { ...state, secret: rng.int(1, state.max + 1) });\n\n/** Conformance options for the Hint Puzzle fixture. */\nexport const hintPuzzleConformance: EngineConformanceOptions<HintPuzzleState, HintPuzzleCommand, HintPuzzleConfig> = {\n name: 'hint-puzzle',\n configs: [{ max: 12 }],\n seatCounts: [1],\n commands: (state) => [\n { type: 'hint' },\n ...Array.from({ length: state.max + 1 }, (_, n): HintPuzzleCommand => ({ type: 'guess', n })),\n ],\n perturbHidden: perturbHintPuzzle,\n undoBarrier: isHint,\n assist: (input) => (isHint(input) ? 'hint' : null),\n invariants: (s) =>\n s.solved === s.guesses.some((g) => g.clue === 'correct') ? true : 'solved disagrees with the guesses',\n};\n"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export { isNimState, nim, nimConformance, nimMigrations, nimSaveFixtures, NIM_SAVE_VERSION, type NimCommand, type NimConfig, type NimEvent, type NimState, type NimView, } from './nim.js';
2
+ export { DRAW_PLAY_CARDS, drawPlay, drawPlayConformance, perturbDrawPlay, type DrawPlayCard, type DrawPlayCommand, type DrawPlayConfig, type DrawPlayEvent, type DrawPlayState, type DrawPlayView, } from './draw-play.js';
3
+ export { perturbSealedBids, sealedBids, sealedBidsConformance, type SealedBidsCommand, type SealedBidsConfig, type SealedBidsEvent, type SealedBidsState, type SealedBidsView, } from './sealed-bids.js';
4
+ export { diceRace, diceRaceConformance, throwDice, type DiceRaceCommand, type DiceRaceConfig, type DiceRaceEvent, type DiceRaceState, type ThrowPayload, } from './dice-race.js';
5
+ export { hintPuzzle, hintPuzzleConformance, isHint, MAX_HINTS, perturbHintPuzzle, type Clue, type HintPuzzleCommand, type HintPuzzleConfig, type HintPuzzleEvent, type HintPuzzleState, type HintPuzzleView, } from './hint-puzzle.js';
6
+ export { drift, driftConformance, MAX_SPEED, type DriftConfig, type DriftEvent, type DriftInput, type DriftState, type DriftView, } from './drift.js';
@@ -0,0 +1,7 @@
1
+ import { diceRace, diceRaceConformance, throwDice } from "./dice-race.js";
2
+ import { DRAW_PLAY_CARDS, drawPlay, drawPlayConformance, perturbDrawPlay } from "./draw-play.js";
3
+ import { MAX_SPEED, drift, driftConformance } from "./drift.js";
4
+ import { MAX_HINTS, hintPuzzle, hintPuzzleConformance, isHint, perturbHintPuzzle } from "./hint-puzzle.js";
5
+ import { NIM_SAVE_VERSION, isNimState, nim, nimConformance, nimMigrations, nimSaveFixtures } from "./nim.js";
6
+ import { perturbSealedBids, sealedBids, sealedBidsConformance } from "./sealed-bids.js";
7
+ export { DRAW_PLAY_CARDS, MAX_HINTS, MAX_SPEED, NIM_SAVE_VERSION, diceRace, diceRaceConformance, drawPlay, drawPlayConformance, drift, driftConformance, hintPuzzle, hintPuzzleConformance, isHint, isNimState, nim, nimConformance, nimMigrations, nimSaveFixtures, perturbDrawPlay, perturbHintPuzzle, perturbSealedBids, sealedBids, sealedBidsConformance, throwDice };