@johnmorrisdotca/houseki 0.4.0 → 0.4.2

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 (72) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +77 -95
  3. package/dist/colour-chains/challenge.d.ts +12 -1
  4. package/dist/colour-chains/challenge.js +12 -1
  5. package/dist/colour-chains/content-types.d.ts +112 -0
  6. package/dist/colour-chains/content.d.ts +140 -3
  7. package/dist/colour-chains/content.js +140 -3
  8. package/dist/colour-chains/engine.d.ts +85 -6
  9. package/dist/colour-chains/engine.js +85 -6
  10. package/dist/colour-chains/input.d.ts +97 -7
  11. package/dist/colour-chains/input.js +97 -7
  12. package/dist/colour-chains/persistence.d.ts +36 -3
  13. package/dist/colour-chains/persistence.js +36 -3
  14. package/dist/colour-chains/types.d.ts +291 -17
  15. package/dist/colour-chains/validation.d.ts +12 -1
  16. package/dist/colour-chains/validation.js +12 -1
  17. package/dist/falling-triplets/content-types.d.ts +69 -0
  18. package/dist/falling-triplets/content.d.ts +60 -3
  19. package/dist/falling-triplets/content.js +60 -3
  20. package/dist/falling-triplets/engine.d.ts +122 -10
  21. package/dist/falling-triplets/engine.js +122 -10
  22. package/dist/falling-triplets/input.d.ts +61 -5
  23. package/dist/falling-triplets/input.js +61 -5
  24. package/dist/falling-triplets/match.d.ts +26 -2
  25. package/dist/falling-triplets/match.js +26 -2
  26. package/dist/falling-triplets/random.d.ts +34 -3
  27. package/dist/falling-triplets/random.js +34 -3
  28. package/dist/falling-triplets/types.d.ts +227 -3
  29. package/dist/gem-swap/content.d.ts +116 -0
  30. package/dist/gem-swap/content.js +72 -0
  31. package/dist/gem-swap/engine.d.ts +124 -9
  32. package/dist/gem-swap/engine.js +124 -9
  33. package/dist/gem-swap/persistence.d.ts +60 -4
  34. package/dist/gem-swap/persistence.js +65 -7
  35. package/dist/gem-swap/practice.js +2 -2
  36. package/dist/gem-swap/types.d.ts +336 -3
  37. package/dist/magnetic-blocks/content-types.d.ts +56 -0
  38. package/dist/magnetic-blocks/content.d.ts +91 -5
  39. package/dist/magnetic-blocks/content.js +91 -5
  40. package/dist/magnetic-blocks/engine.d.ts +63 -5
  41. package/dist/magnetic-blocks/engine.js +63 -5
  42. package/dist/magnetic-blocks/persistence.d.ts +36 -3
  43. package/dist/magnetic-blocks/persistence.js +36 -3
  44. package/dist/magnetic-blocks/physics.d.ts +176 -6
  45. package/dist/magnetic-blocks/physics.js +176 -6
  46. package/dist/magnetic-blocks/types.d.ts +185 -1
  47. package/dist/magnetic-blocks/validation.d.ts +12 -0
  48. package/dist/magnetic-blocks/validation.js +12 -0
  49. package/dist/nature/attraction.d.ts +24 -2
  50. package/dist/nature/attraction.js +24 -2
  51. package/dist/nature/environment.d.ts +200 -7
  52. package/dist/nature/environment.js +88 -7
  53. package/dist/nature/marking.d.ts +13 -1
  54. package/dist/nature/marking.js +13 -1
  55. package/dist/nature/rebound.d.ts +30 -2
  56. package/dist/nature/rebound.js +30 -2
  57. package/dist/nature/state.d.ts +11 -0
  58. package/dist/nature/state.js +11 -0
  59. package/dist/nature/types.d.ts +194 -1
  60. package/dist/nature/validation.d.ts +12 -0
  61. package/dist/nature/validation.js +12 -0
  62. package/dist/stone-collapse/content-types.d.ts +69 -0
  63. package/dist/stone-collapse/content.d.ts +60 -3
  64. package/dist/stone-collapse/content.js +60 -3
  65. package/dist/stone-collapse/engine.d.ts +99 -8
  66. package/dist/stone-collapse/engine.js +99 -8
  67. package/dist/stone-collapse/persistence.d.ts +36 -3
  68. package/dist/stone-collapse/persistence.js +36 -3
  69. package/dist/stone-collapse/types.d.ts +220 -3
  70. package/dist/stone-collapse/validation.d.ts +12 -1
  71. package/dist/stone-collapse/validation.js +12 -1
  72. package/package.json +34 -26
@@ -1,25 +1,140 @@
1
1
  import type { Action, CreateOptions, GameState, GameStatus, Gem, Settings, Transition } from './types.js';
2
- /** Typed error for invalid settings or a board that cannot be generated safely. */
2
+ /** Typed error for invalid settings or a board that cannot be generated safely.
3
+ *
4
+ * @param code - The native stable error code used by callers to distinguish setup or replay failures.
5
+ * @param message - The human-readable diagnostic accompanying the native code.
6
+ * @example
7
+ * ```ts
8
+ * import { GemSwapOptionsError } from "@johnmorrisdotca/houseki/gem-swap";
9
+ *
10
+ * const error=new GemSwapOptionsError("docs-example","Invalid example setup");
11
+ * console.log(error.name,error.code,error.message);
12
+ * ```
13
+ */
3
14
  export declare class GemSwapOptionsError extends RangeError {
4
15
  readonly code: string;
5
16
  constructor(code: string, message: string);
6
17
  }
7
18
  /** @internal Test seam for checking the built-in standard-board fallback witness. */
8
19
  export declare function fallbackBoardForTest(settings: Settings): readonly (Gem | null)[];
9
- /** Creates a deterministic stable board with at least one legal normal swap. */
20
+ /** Creates a deterministic stable board with at least one legal normal swap.
21
+ *
22
+ * @param options - Validated native creation, authored challenge, or environment options described by this signature.
23
+ * @returns Creates a deterministic stable board with at least one legal normal swap.
24
+ * @example
25
+ * ```ts
26
+ * import { createGame } from "@johnmorrisdotca/houseki/gem-swap";
27
+ *
28
+ * console.log(createGame({seed:"docs"}).phase);
29
+ * ```
30
+ */
10
31
  export declare function createGame(options?: CreateOptions): GameState;
11
- /** Applies one recorded player action. Rejected actions preserve state identity and are not recorded. */
32
+ /** Applies one recorded player action. Rejected actions preserve state identity and are not recorded.
33
+ *
34
+ * @param state - The immutable native state to read or transition; the input is retained.
35
+ * @param action - One native action for this entry point; inspect accepted and reason on the result.
36
+ * @returns Applies one recorded player action. Rejected actions preserve state identity and are not recorded.
37
+ * @example
38
+ * ```ts
39
+ * import { applyAction, createGame } from "@johnmorrisdotca/houseki/gem-swap";
40
+ * import type { Action } from "@johnmorrisdotca/houseki/gem-swap";
41
+ *
42
+ * const state=createGame({seed:"docs"});
43
+ * const action: Action={kind:"hint"};
44
+ * console.log(applyAction(state,action).accepted);
45
+ * ```
46
+ */
12
47
  export declare function applyAction(state: GameState, action: Action): Transition;
13
- /** Advances deterministic resolution ticks; wall-clock time is supplied separately by the host. */
48
+ /** Advances deterministic resolution ticks; wall-clock time is supplied separately by the host.
49
+ *
50
+ * @param state - The immutable native state to read or transition; the input is retained.
51
+ * @param ticks - The nonnegative number of fixed 60 Hz logical ticks to advance.
52
+ * @returns Advances deterministic resolution ticks; wall-clock time is supplied separately by the host.
53
+ * @example
54
+ * ```ts
55
+ * import { advanceTicks, createGame } from "@johnmorrisdotca/houseki/gem-swap";
56
+ *
57
+ * const state=createGame({seed:"docs"});
58
+ * console.log(advanceTicks(state,1).state.phase);
59
+ * ```
60
+ */
14
61
  export declare function advanceTicks(state: GameState, ticks: number): Transition;
15
- /** Adds explicit host-measured elapsed time. Arcade runs end at 180 seconds; no clock is read here. */
62
+ /** Adds explicit host-measured elapsed time. Arcade runs end at 180 seconds; no clock is read here.
63
+ *
64
+ * @param state - The immutable native state to read or transition; the input is retained.
65
+ * @param milliseconds - The nonnegative elapsed milliseconds for the native timer.
66
+ * @returns Adds explicit host-measured elapsed time. Arcade runs end at 180 seconds; no clock is read here.
67
+ * @example
68
+ * ```ts
69
+ * import { advanceTime, createGame } from "@johnmorrisdotca/houseki/gem-swap";
70
+ *
71
+ * const state=createGame({seed:"docs"});
72
+ * console.log(advanceTime(state,16).state.phase);
73
+ * ```
74
+ */
16
75
  export declare function advanceTime(state: GameState, milliseconds: number): Transition;
17
- /** Returns a deterministic legal swap hint and permanently marks the run assisted. */
76
+ /** Returns a deterministic legal swap hint and permanently marks the run assisted.
77
+ *
78
+ * @param state - The immutable native state to read or transition; the input is retained.
79
+ * @returns Returns a deterministic legal swap hint and permanently marks the run assisted.
80
+ * @example
81
+ * ```ts
82
+ * import { createGame, hintGame } from "@johnmorrisdotca/houseki/gem-swap";
83
+ *
84
+ * const state=createGame({seed:"docs"});
85
+ * console.log(hintGame(state).accepted);
86
+ * ```
87
+ */
18
88
  export declare function hintGame(state: GameState): Transition;
19
- /** Shuffles existing gem records in Relaxed play, preserving their colour and special counts. */
89
+ /** Shuffles existing gem records in Relaxed play, preserving their colour and special counts.
90
+ *
91
+ * @param state - The immutable native state to read or transition; the input is retained.
92
+ * @returns Shuffles existing gem records in Relaxed play, preserving their colour and special counts.
93
+ * @example
94
+ * ```ts
95
+ * import { createGame, reshuffleGame } from "@johnmorrisdotca/houseki/gem-swap";
96
+ *
97
+ * const state=createGame({seed:"docs"});
98
+ * console.log(reshuffleGame(state).accepted);
99
+ * ```
100
+ */
20
101
  export declare function reshuffleGame(state: GameState): Transition;
21
- /** Restores the last committed swap in Relaxed or Challenge play and marks assistance. */
102
+ /** Restores the last committed swap in Relaxed or Challenge play and marks assistance.
103
+ *
104
+ * @param state - The immutable native state to read or transition; the input is retained.
105
+ * @returns Restores the last committed swap in Relaxed or Challenge play and marks assistance.
106
+ * @example
107
+ * ```ts
108
+ * import { createGame, undoGame } from "@johnmorrisdotca/houseki/gem-swap";
109
+ *
110
+ * const state=createGame({seed:"docs"});
111
+ * console.log(undoGame(state).accepted);
112
+ * ```
113
+ */
22
114
  export declare function undoGame(state: GameState): Transition;
23
- /** Lists all legal swaps that would be accepted from the current stable state. */
115
+ /** Lists all legal swaps that would be accepted from the current stable state.
116
+ *
117
+ * @param state - The immutable native state to read or transition; the input is retained.
118
+ * @returns Lists all legal swaps that would be accepted from the current stable state.
119
+ * @example
120
+ * ```ts
121
+ * import { createGame, legalActions } from "@johnmorrisdotca/houseki/gem-swap";
122
+ *
123
+ * const state=createGame({seed:"docs"});
124
+ * console.log(legalActions(state).length);
125
+ * ```
126
+ */
24
127
  export declare function legalActions(state: GameState): readonly Action[];
128
+ /** Derive the native phase, score, legal-move count, tools and goal/timer progress for host labels.
129
+ *
130
+ * @param state - The immutable native state to read or transition; the input is retained.
131
+ * @returns Derive the native phase, score, legal-move count, tools and goal/timer progress for host labels.
132
+ * @example
133
+ * ```ts
134
+ * import { createGame, statusOf } from "@johnmorrisdotca/houseki/gem-swap";
135
+ *
136
+ * const state=createGame({seed:"docs"});
137
+ * console.log(statusOf(state).phase);
138
+ * ```
139
+ */
25
140
  export declare function statusOf(state: GameState): GameStatus;
@@ -4,7 +4,18 @@ import { isValidSpecialSwap, materializeSpecials, planWave } from './specials.js
4
4
  import { previewTool } from './tools.js';
5
5
  import { consumePortal, contactCells, placementCells } from './black-hole.js';
6
6
  import { hintTransition, reshuffleTransition, undoTransition } from './practice.js';
7
- /** Typed error for invalid settings or a board that cannot be generated safely. */
7
+ /** Typed error for invalid settings or a board that cannot be generated safely.
8
+ *
9
+ * @param code - The native stable error code used by callers to distinguish setup or replay failures.
10
+ * @param message - The human-readable diagnostic accompanying the native code.
11
+ * @example
12
+ * ```ts
13
+ * import { GemSwapOptionsError } from "@johnmorrisdotca/houseki/gem-swap";
14
+ *
15
+ * const error=new GemSwapOptionsError("docs-example","Invalid example setup");
16
+ * console.log(error.name,error.code,error.message);
17
+ * ```
18
+ */
8
19
  export class GemSwapOptionsError extends RangeError {
9
20
  code;
10
21
  constructor(code, message) { super(message); this.name = 'GemSwapOptionsError'; this.code = code; }
@@ -185,7 +196,17 @@ export function fallbackBoardForTest(settings) {
185
196
  // Swapping (3,4) blue with (3,3) red completes the red row at y=4, x=2..4.
186
197
  return Object.freeze(board);
187
198
  }
188
- /** Creates a deterministic stable board with at least one legal normal swap. */
199
+ /** Creates a deterministic stable board with at least one legal normal swap.
200
+ *
201
+ * @param options - Validated native creation, authored challenge, or environment options described by this signature.
202
+ * @returns Creates a deterministic stable board with at least one legal normal swap.
203
+ * @example
204
+ * ```ts
205
+ * import { createGame } from "@johnmorrisdotca/houseki/gem-swap";
206
+ *
207
+ * console.log(createGame({seed:"docs"}).phase);
208
+ * ```
209
+ */
189
210
  export function createGame(options = {}) {
190
211
  const config = optionsFor(options);
191
212
  const { settings } = config;
@@ -284,7 +305,21 @@ function appendHistory(state, operation) {
284
305
  return Object.freeze({ ...state, history: Object.freeze([...state.history.slice(0, -1), Object.freeze({ kind: 'time', milliseconds: previous.milliseconds + operation.milliseconds })]) });
285
306
  return Object.freeze({ ...state, history: Object.freeze([...state.history, Object.freeze(operation)]) });
286
307
  }
287
- /** Applies one recorded player action. Rejected actions preserve state identity and are not recorded. */
308
+ /** Applies one recorded player action. Rejected actions preserve state identity and are not recorded.
309
+ *
310
+ * @param state - The immutable native state to read or transition; the input is retained.
311
+ * @param action - One native action for this entry point; inspect accepted and reason on the result.
312
+ * @returns Applies one recorded player action. Rejected actions preserve state identity and are not recorded.
313
+ * @example
314
+ * ```ts
315
+ * import { applyAction, createGame } from "@johnmorrisdotca/houseki/gem-swap";
316
+ * import type { Action } from "@johnmorrisdotca/houseki/gem-swap";
317
+ *
318
+ * const state=createGame({seed:"docs"});
319
+ * const action: Action={kind:"hint"};
320
+ * console.log(applyAction(state,action).accepted);
321
+ * ```
322
+ */
288
323
  export function applyAction(state, action) {
289
324
  const explicitPracticeReshuffle = action?.kind === 'reshuffle' && state.mode === 'relaxed' && state.outcome === 'finished';
290
325
  if ((state.outcome && !explicitPracticeReshuffle) || (state.mode === 'arcade' && state.elapsedMs >= 180_000))
@@ -580,7 +615,19 @@ function advanceTicksBase(state, ticks) {
580
615
  }
581
616
  return { state: current, events: Object.freeze(events), accepted: true };
582
617
  }
583
- /** Advances deterministic resolution ticks; wall-clock time is supplied separately by the host. */
618
+ /** Advances deterministic resolution ticks; wall-clock time is supplied separately by the host.
619
+ *
620
+ * @param state - The immutable native state to read or transition; the input is retained.
621
+ * @param ticks - The nonnegative number of fixed 60 Hz logical ticks to advance.
622
+ * @returns Advances deterministic resolution ticks; wall-clock time is supplied separately by the host.
623
+ * @example
624
+ * ```ts
625
+ * import { advanceTicks, createGame } from "@johnmorrisdotca/houseki/gem-swap";
626
+ *
627
+ * const state=createGame({seed:"docs"});
628
+ * console.log(advanceTicks(state,1).state.phase);
629
+ * ```
630
+ */
584
631
  export function advanceTicks(state, ticks) {
585
632
  if (!Number.isInteger(ticks) || ticks < 0 || ticks > 3600)
586
633
  throw new RangeError('ticks must be an integer from 0 to 3600');
@@ -597,7 +644,19 @@ export function advanceTicks(state, ticks) {
597
644
  current = appendHistory(current, { kind: 'ticks', count: consumed });
598
645
  return { state: current, events: Object.freeze(events), accepted: true };
599
646
  }
600
- /** Adds explicit host-measured elapsed time. Arcade runs end at 180 seconds; no clock is read here. */
647
+ /** Adds explicit host-measured elapsed time. Arcade runs end at 180 seconds; no clock is read here.
648
+ *
649
+ * @param state - The immutable native state to read or transition; the input is retained.
650
+ * @param milliseconds - The nonnegative elapsed milliseconds for the native timer.
651
+ * @returns Adds explicit host-measured elapsed time. Arcade runs end at 180 seconds; no clock is read here.
652
+ * @example
653
+ * ```ts
654
+ * import { advanceTime, createGame } from "@johnmorrisdotca/houseki/gem-swap";
655
+ *
656
+ * const state=createGame({seed:"docs"});
657
+ * console.log(advanceTime(state,16).state.phase);
658
+ * ```
659
+ */
601
660
  export function advanceTime(state, milliseconds) {
602
661
  if (!Number.isInteger(milliseconds) || milliseconds < 0 || milliseconds > 180_000)
603
662
  throw new RangeError('milliseconds must be an integer from 0 to 180000');
@@ -629,13 +688,57 @@ function replayOperations(initial, operations) {
629
688
  }
630
689
  return state;
631
690
  }
632
- /** Returns a deterministic legal swap hint and permanently marks the run assisted. */
691
+ /** Returns a deterministic legal swap hint and permanently marks the run assisted.
692
+ *
693
+ * @param state - The immutable native state to read or transition; the input is retained.
694
+ * @returns Returns a deterministic legal swap hint and permanently marks the run assisted.
695
+ * @example
696
+ * ```ts
697
+ * import { createGame, hintGame } from "@johnmorrisdotca/houseki/gem-swap";
698
+ *
699
+ * const state=createGame({seed:"docs"});
700
+ * console.log(hintGame(state).accepted);
701
+ * ```
702
+ */
633
703
  export function hintGame(state) { return applyAction(state, { kind: 'hint' }); }
634
- /** Shuffles existing gem records in Relaxed play, preserving their colour and special counts. */
704
+ /** Shuffles existing gem records in Relaxed play, preserving their colour and special counts.
705
+ *
706
+ * @param state - The immutable native state to read or transition; the input is retained.
707
+ * @returns Shuffles existing gem records in Relaxed play, preserving their colour and special counts.
708
+ * @example
709
+ * ```ts
710
+ * import { createGame, reshuffleGame } from "@johnmorrisdotca/houseki/gem-swap";
711
+ *
712
+ * const state=createGame({seed:"docs"});
713
+ * console.log(reshuffleGame(state).accepted);
714
+ * ```
715
+ */
635
716
  export function reshuffleGame(state) { return applyAction(state, { kind: 'reshuffle' }); }
636
- /** Restores the last committed swap in Relaxed or Challenge play and marks assistance. */
717
+ /** Restores the last committed swap in Relaxed or Challenge play and marks assistance.
718
+ *
719
+ * @param state - The immutable native state to read or transition; the input is retained.
720
+ * @returns Restores the last committed swap in Relaxed or Challenge play and marks assistance.
721
+ * @example
722
+ * ```ts
723
+ * import { createGame, undoGame } from "@johnmorrisdotca/houseki/gem-swap";
724
+ *
725
+ * const state=createGame({seed:"docs"});
726
+ * console.log(undoGame(state).accepted);
727
+ * ```
728
+ */
637
729
  export function undoGame(state) { return applyAction(state, { kind: 'undo' }); }
638
- /** Lists all legal swaps that would be accepted from the current stable state. */
730
+ /** Lists all legal swaps that would be accepted from the current stable state.
731
+ *
732
+ * @param state - The immutable native state to read or transition; the input is retained.
733
+ * @returns Lists all legal swaps that would be accepted from the current stable state.
734
+ * @example
735
+ * ```ts
736
+ * import { createGame, legalActions } from "@johnmorrisdotca/houseki/gem-swap";
737
+ *
738
+ * const state=createGame({seed:"docs"});
739
+ * console.log(legalActions(state).length);
740
+ * ```
741
+ */
639
742
  export function legalActions(state) {
640
743
  if (state.phase === 'finished')
641
744
  return state.mode === 'relaxed' && state.outcome === 'finished' ? Object.freeze([{ kind: 'reshuffle' }]) : Object.freeze([]);
@@ -681,6 +784,18 @@ export function legalActions(state) {
681
784
  /** Returns the current score, move count and accurately enumerated legal move count. */
682
785
  function availableToolCount(state) { return TOOL_ORDER.reduce((count, tool) => count + state.inventory[tool], state.blackHoleCharges); }
683
786
  function hasUsableTool(state) { return TOOL_ORDER.some(tool => state.inventory[tool] > 0) || (state.settings.advancedTools && state.blackHoleCharges > 0 && state.blackHole === null); }
787
+ /** Derive the native phase, score, legal-move count, tools and goal/timer progress for host labels.
788
+ *
789
+ * @param state - The immutable native state to read or transition; the input is retained.
790
+ * @returns Derive the native phase, score, legal-move count, tools and goal/timer progress for host labels.
791
+ * @example
792
+ * ```ts
793
+ * import { createGame, statusOf } from "@johnmorrisdotca/houseki/gem-swap";
794
+ *
795
+ * const state=createGame({seed:"docs"});
796
+ * console.log(statusOf(state).phase);
797
+ * ```
798
+ */
684
799
  export function statusOf(state) {
685
800
  const goals = state.challenge?.goals ?? Object.freeze([]);
686
801
  const goalProgress = goals.map(goal => ({ kind: goal.kind, ...(goal.kind === 'collect' ? { colour: goal.colour } : {}), current: goalValue(state, goal), target: goalTarget(state, goal), complete: goalValue(state, goal) >= goalTarget(state, goal) }));
@@ -1,15 +1,71 @@
1
1
  import type { CreateOptions, GameState, ReplayOperation } from './types.js';
2
+ /** A typed rejection of a malformed, unsupported or inconsistent native replay.
3
+ *
4
+ * @param code - The native stable error code used by callers to distinguish setup or replay failures.
5
+ * @param message - The human-readable diagnostic accompanying the native code.
6
+ * @example
7
+ * ```ts
8
+ * import { ReplayError } from "@johnmorrisdotca/houseki/gem-swap";
9
+ *
10
+ * const error=new ReplayError("docs-example","Invalid example setup");
11
+ * console.log(error.name,error.code,error.message);
12
+ * ```
13
+ */
2
14
  export declare class ReplayError extends TypeError {
3
15
  readonly code: string;
4
16
  constructor(code: string, message: string);
5
17
  }
6
- /** Encodes the initial rules plus accepted actions and logical inputs in canonical JSON. */
18
+ /** Encodes the initial rules plus accepted actions and logical inputs in canonical JSON.
19
+ *
20
+ * @param state - The immutable native state to read or transition; the input is retained.
21
+ * @returns Encodes the initial rules plus accepted actions and logical inputs in canonical JSON.
22
+ * @example
23
+ * ```ts
24
+ * import { createGame, encodeGame } from "@johnmorrisdotca/houseki/gem-swap";
25
+ *
26
+ * const state=createGame({seed:"docs"});
27
+ * console.log(encodeGame(state).length);
28
+ * ```
29
+ */
7
30
  export declare function encodeGame(state: GameState): string;
8
- /** Validates and reconstructs a state by replay; serialized board snapshots are never trusted. */
31
+ /** Validates and reconstructs a state by replay; serialized board snapshots are never trusted.
32
+ *
33
+ * @param serialized - A saved JSON recording from this entry’s encodeGame; malformed or inconsistent saves throw.
34
+ * @returns Validates and reconstructs a state by replay; serialized board snapshots are never trusted.
35
+ * @example
36
+ * ```ts
37
+ * import { createGame, decodeGame, encodeGame } from "@johnmorrisdotca/houseki/gem-swap";
38
+ *
39
+ * const state=createGame({seed:"docs"});
40
+ * console.log(decodeGame(encodeGame(state)).phase);
41
+ * ```
42
+ */
9
43
  export declare function decodeGame(serialized: string): GameState;
10
- /** Starts the exact initial ruleset and seed again, discarding all run progress. */
44
+ /** Starts the exact initial ruleset and seed again, discarding all run progress.
45
+ *
46
+ * @param state - The immutable native state to read or transition; the input is retained.
47
+ * @returns Starts the exact initial ruleset and seed again, discarding all run progress.
48
+ * @example
49
+ * ```ts
50
+ * import { createGame, restartGame } from "@johnmorrisdotca/houseki/gem-swap";
51
+ *
52
+ * const state=createGame({seed:"docs"});
53
+ * console.log(restartGame(state).phase);
54
+ * ```
55
+ */
11
56
  export declare function restartGame(state: GameState): GameState;
12
- /** Checks a complete authored witness through the public deterministic rules API. */
57
+ /** Checks a complete authored witness through the public deterministic rules API.
58
+ *
59
+ * @param options - Validated native creation, authored challenge, or environment options described by this signature.
60
+ * @param witness - The ordered native replay operations proposed as a winning challenge proof.
61
+ * @returns Checks a complete authored witness through the public deterministic rules API.
62
+ * @example
63
+ * ```ts
64
+ * import { GEM_SWAP_CAMPAIGN, validateChallengeWitness } from "@johnmorrisdotca/houseki/gem-swap";
65
+ *
66
+ * console.log(validateChallengeWitness(GEM_SWAP_CAMPAIGN[0].options,GEM_SWAP_CAMPAIGN[0].witness).valid);
67
+ * ```
68
+ */
13
69
  export declare function validateChallengeWitness(options: CreateOptions, witness: readonly ReplayOperation[]): {
14
70
  readonly valid: boolean;
15
71
  readonly state?: GameState;
@@ -4,16 +4,30 @@ const MAX_BYTES = 262_144;
4
4
  const MAX_OPERATIONS = 5_000;
5
5
  const MAX_TICKS = 100_000;
6
6
  const byteLength = (value) => new TextEncoder().encode(value).byteLength;
7
+ /** A typed rejection of a malformed, unsupported or inconsistent native replay.
8
+ *
9
+ * @param code - The native stable error code used by callers to distinguish setup or replay failures.
10
+ * @param message - The human-readable diagnostic accompanying the native code.
11
+ * @example
12
+ * ```ts
13
+ * import { ReplayError } from "@johnmorrisdotca/houseki/gem-swap";
14
+ *
15
+ * const error=new ReplayError("docs-example","Invalid example setup");
16
+ * console.log(error.name,error.code,error.message);
17
+ * ```
18
+ */
7
19
  export class ReplayError extends TypeError {
8
20
  code;
9
21
  constructor(code, message) { super(message); this.name = 'ReplayError'; this.code = code; }
10
22
  }
11
- function canonical(value) {
23
+ function canonical(value, depth = 0) {
24
+ if (depth > 50)
25
+ throw new ReplayError('save-too-deep', 'Replay is nested too deeply');
12
26
  if (Array.isArray(value))
13
- return `[${value.map(canonical).join(',')}]`;
27
+ return `[${value.map(item => canonical(item, depth + 1)).join(',')}]`;
14
28
  if (value && typeof value === 'object') {
15
29
  const object = value;
16
- return `{${Object.keys(object).sort().map(key => `${JSON.stringify(key)}:${canonical(object[key])}`).join(',')}}`;
30
+ return `{${Object.keys(object).sort().map(key => `${JSON.stringify(key)}:${canonical(object[key], depth + 1)}`).join(',')}}`;
17
31
  }
18
32
  return JSON.stringify(value);
19
33
  }
@@ -55,7 +69,18 @@ function execute(initial, operations) {
55
69
  }
56
70
  return state;
57
71
  }
58
- /** Encodes the initial rules plus accepted actions and logical inputs in canonical JSON. */
72
+ /** Encodes the initial rules plus accepted actions and logical inputs in canonical JSON.
73
+ *
74
+ * @param state - The immutable native state to read or transition; the input is retained.
75
+ * @returns Encodes the initial rules plus accepted actions and logical inputs in canonical JSON.
76
+ * @example
77
+ * ```ts
78
+ * import { createGame, encodeGame } from "@johnmorrisdotca/houseki/gem-swap";
79
+ *
80
+ * const state=createGame({seed:"docs"});
81
+ * console.log(encodeGame(state).length);
82
+ * ```
83
+ */
59
84
  export function encodeGame(state) {
60
85
  if (!state || state.game !== 'gem-swap' || state.history.length > MAX_OPERATIONS)
61
86
  throw new ReplayError('invalid-state', 'State is invalid or exceeds the operation limit');
@@ -68,7 +93,18 @@ export function encodeGame(state) {
68
93
  throw new ReplayError('save-too-large', 'Replay exceeds the 256 KiB size limit');
69
94
  return result;
70
95
  }
71
- /** Validates and reconstructs a state by replay; serialized board snapshots are never trusted. */
96
+ /** Validates and reconstructs a state by replay; serialized board snapshots are never trusted.
97
+ *
98
+ * @param serialized - A saved JSON recording from this entry’s encodeGame; malformed or inconsistent saves throw.
99
+ * @returns Validates and reconstructs a state by replay; serialized board snapshots are never trusted.
100
+ * @example
101
+ * ```ts
102
+ * import { createGame, decodeGame, encodeGame } from "@johnmorrisdotca/houseki/gem-swap";
103
+ *
104
+ * const state=createGame({seed:"docs"});
105
+ * console.log(decodeGame(encodeGame(state)).phase);
106
+ * ```
107
+ */
72
108
  export function decodeGame(serialized) {
73
109
  if (typeof serialized !== 'string' || byteLength(serialized) > MAX_BYTES)
74
110
  throw new ReplayError('save-too-large', 'Replay must be a string no larger than 256 KiB');
@@ -89,9 +125,31 @@ export function decodeGame(serialized) {
89
125
  const operations = value.operations.map(validateOperation);
90
126
  return execute(value.initial, operations);
91
127
  }
92
- /** Starts the exact initial ruleset and seed again, discarding all run progress. */
128
+ /** Starts the exact initial ruleset and seed again, discarding all run progress.
129
+ *
130
+ * @param state - The immutable native state to read or transition; the input is retained.
131
+ * @returns Starts the exact initial ruleset and seed again, discarding all run progress.
132
+ * @example
133
+ * ```ts
134
+ * import { createGame, restartGame } from "@johnmorrisdotca/houseki/gem-swap";
135
+ *
136
+ * const state=createGame({seed:"docs"});
137
+ * console.log(restartGame(state).phase);
138
+ * ```
139
+ */
93
140
  export function restartGame(state) { return createGame(state.initialOptions); }
94
- /** Checks a complete authored witness through the public deterministic rules API. */
141
+ /** Checks a complete authored witness through the public deterministic rules API.
142
+ *
143
+ * @param options - Validated native creation, authored challenge, or environment options described by this signature.
144
+ * @param witness - The ordered native replay operations proposed as a winning challenge proof.
145
+ * @returns Checks a complete authored witness through the public deterministic rules API.
146
+ * @example
147
+ * ```ts
148
+ * import { GEM_SWAP_CAMPAIGN, validateChallengeWitness } from "@johnmorrisdotca/houseki/gem-swap";
149
+ *
150
+ * console.log(validateChallengeWitness(GEM_SWAP_CAMPAIGN[0].options,GEM_SWAP_CAMPAIGN[0].witness).valid);
151
+ * ```
152
+ */
95
153
  export function validateChallengeWitness(options, witness) {
96
154
  try {
97
155
  if (options.mode !== 'challenge' || !Array.isArray(witness) || witness.length > MAX_OPERATIONS)
@@ -35,7 +35,7 @@ export function reshuffleTransition(state) {
35
35
  const finishedRelaxed = state.mode === 'relaxed' && state.phase === 'finished' && state.outcome === 'finished' && !findMatches(state.board, state.settings).length;
36
36
  if (state.mode !== 'relaxed' || (!onReady(state) && !finishedRelaxed) || state.selectedTool)
37
37
  return rejected(state, 'reshuffle-unavailable');
38
- const gems = state.board.filter((item) => item !== null);
38
+ const gems = state.board.filter((item, index) => item !== null && index !== state.blackHole?.cell);
39
39
  let randomState = state.randomState;
40
40
  for (let attempt = 0; attempt < 128; attempt++) {
41
41
  const shuffled = [...gems];
@@ -47,7 +47,7 @@ export function reshuffleTransition(state) {
47
47
  const board = [...state.board];
48
48
  let at = 0;
49
49
  for (let index = 0; index < board.length; index++)
50
- if (state.settings.mask[index])
50
+ if (state.settings.mask[index] && index !== state.blackHole?.cell)
51
51
  board[index] = shuffled[at++];
52
52
  if (!findMatches(board, state.settings).length && listLegalSwaps({ ...state, board }).length) {
53
53
  return { state: Object.freeze({ ...state, board: Object.freeze(board), randomState, assisted: true, phase: 'ready', outcome: null, selectedTool: null, toolTarget: null, toolPreview: Object.freeze([]) }), events: Object.freeze([{ type: 'board-reshuffled', attempts: attempt + 1, assisted: true }]), accepted: true };