@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.
- package/CHANGELOG.md +29 -1
- package/README.md +77 -95
- package/dist/colour-chains/challenge.d.ts +12 -1
- package/dist/colour-chains/challenge.js +12 -1
- package/dist/colour-chains/content-types.d.ts +112 -0
- package/dist/colour-chains/content.d.ts +140 -3
- package/dist/colour-chains/content.js +140 -3
- package/dist/colour-chains/engine.d.ts +85 -6
- package/dist/colour-chains/engine.js +85 -6
- package/dist/colour-chains/input.d.ts +97 -7
- package/dist/colour-chains/input.js +97 -7
- package/dist/colour-chains/persistence.d.ts +36 -3
- package/dist/colour-chains/persistence.js +36 -3
- package/dist/colour-chains/types.d.ts +291 -17
- package/dist/colour-chains/validation.d.ts +12 -1
- package/dist/colour-chains/validation.js +12 -1
- package/dist/falling-triplets/content-types.d.ts +69 -0
- package/dist/falling-triplets/content.d.ts +60 -3
- package/dist/falling-triplets/content.js +60 -3
- package/dist/falling-triplets/engine.d.ts +122 -10
- package/dist/falling-triplets/engine.js +122 -10
- package/dist/falling-triplets/input.d.ts +61 -5
- package/dist/falling-triplets/input.js +61 -5
- package/dist/falling-triplets/match.d.ts +26 -2
- package/dist/falling-triplets/match.js +26 -2
- package/dist/falling-triplets/random.d.ts +34 -3
- package/dist/falling-triplets/random.js +34 -3
- package/dist/falling-triplets/types.d.ts +227 -3
- package/dist/gem-swap/content.d.ts +116 -0
- package/dist/gem-swap/content.js +72 -0
- package/dist/gem-swap/engine.d.ts +124 -9
- package/dist/gem-swap/engine.js +124 -9
- package/dist/gem-swap/persistence.d.ts +60 -4
- package/dist/gem-swap/persistence.js +65 -7
- package/dist/gem-swap/practice.js +2 -2
- package/dist/gem-swap/types.d.ts +336 -3
- package/dist/magnetic-blocks/content-types.d.ts +56 -0
- package/dist/magnetic-blocks/content.d.ts +91 -5
- package/dist/magnetic-blocks/content.js +91 -5
- package/dist/magnetic-blocks/engine.d.ts +63 -5
- package/dist/magnetic-blocks/engine.js +63 -5
- package/dist/magnetic-blocks/persistence.d.ts +36 -3
- package/dist/magnetic-blocks/persistence.js +36 -3
- package/dist/magnetic-blocks/physics.d.ts +176 -6
- package/dist/magnetic-blocks/physics.js +176 -6
- package/dist/magnetic-blocks/types.d.ts +185 -1
- package/dist/magnetic-blocks/validation.d.ts +12 -0
- package/dist/magnetic-blocks/validation.js +12 -0
- package/dist/nature/attraction.d.ts +24 -2
- package/dist/nature/attraction.js +24 -2
- package/dist/nature/environment.d.ts +200 -7
- package/dist/nature/environment.js +88 -7
- package/dist/nature/marking.d.ts +13 -1
- package/dist/nature/marking.js +13 -1
- package/dist/nature/rebound.d.ts +30 -2
- package/dist/nature/rebound.js +30 -2
- package/dist/nature/state.d.ts +11 -0
- package/dist/nature/state.js +11 -0
- package/dist/nature/types.d.ts +194 -1
- package/dist/nature/validation.d.ts +12 -0
- package/dist/nature/validation.js +12 -0
- package/dist/stone-collapse/content-types.d.ts +69 -0
- package/dist/stone-collapse/content.d.ts +60 -3
- package/dist/stone-collapse/content.js +60 -3
- package/dist/stone-collapse/engine.d.ts +99 -8
- package/dist/stone-collapse/engine.js +99 -8
- package/dist/stone-collapse/persistence.d.ts +36 -3
- package/dist/stone-collapse/persistence.js +36 -3
- package/dist/stone-collapse/types.d.ts +220 -3
- package/dist/stone-collapse/validation.d.ts +12 -1
- package/dist/stone-collapse/validation.js +12 -1
- 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;
|
package/dist/gem-swap/engine.js
CHANGED
|
@@ -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 };
|