@johnmorrisdotca/houseki 0.0.0-stage → 0.1.1
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 +41 -0
- package/LICENSE +21 -0
- package/README.md +272 -2
- package/dist/colour-chains/challenge.d.ts +3 -0
- package/dist/colour-chains/challenge.js +120 -0
- package/dist/colour-chains/content-data.d.ts +2 -0
- package/dist/colour-chains/content-data.js +9534 -0
- package/dist/colour-chains/content-types.d.ts +80 -0
- package/dist/colour-chains/content-types.js +1 -0
- package/dist/colour-chains/content.d.ts +11 -0
- package/dist/colour-chains/content.js +28 -0
- package/dist/colour-chains/engine.d.ts +19 -0
- package/dist/colour-chains/engine.js +493 -0
- package/dist/colour-chains/input.d.ts +24 -0
- package/dist/colour-chains/input.js +78 -0
- package/dist/colour-chains/match.d.ts +5 -0
- package/dist/colour-chains/match.js +48 -0
- package/dist/colour-chains/persistence.d.ts +7 -0
- package/dist/colour-chains/persistence.js +96 -0
- package/dist/colour-chains/random.d.ts +7 -0
- package/dist/colour-chains/random.js +49 -0
- package/dist/colour-chains/types.d.ts +191 -0
- package/dist/colour-chains/types.js +1 -0
- package/dist/colour-chains/validation.d.ts +16 -0
- package/dist/colour-chains/validation.js +80 -0
- package/dist/colour-chains.d.ts +8 -0
- package/dist/colour-chains.js +7 -0
- package/dist/falling-triplets/content-data.d.ts +7258 -0
- package/dist/falling-triplets/content-data.js +18073 -0
- package/dist/falling-triplets/content-types.d.ts +72 -0
- package/dist/falling-triplets/content-types.js +1 -0
- package/dist/falling-triplets/content.d.ts +11 -0
- package/dist/falling-triplets/content.js +39 -0
- package/dist/falling-triplets/engine.d.ts +26 -0
- package/dist/falling-triplets/engine.js +541 -0
- package/dist/falling-triplets/input.d.ts +18 -0
- package/dist/falling-triplets/input.js +42 -0
- package/dist/falling-triplets/match.d.ts +9 -0
- package/dist/falling-triplets/match.js +45 -0
- package/dist/falling-triplets/random.d.ts +9 -0
- package/dist/falling-triplets/random.js +44 -0
- package/dist/falling-triplets/types.d.ts +134 -0
- package/dist/falling-triplets/types.js +1 -0
- package/dist/falling-triplets.d.ts +7 -0
- package/dist/falling-triplets.js +6 -0
- package/dist/gem-swap/black-hole.d.ts +7 -0
- package/dist/gem-swap/black-hole.js +54 -0
- package/dist/gem-swap/board.d.ts +25 -0
- package/dist/gem-swap/board.js +89 -0
- package/dist/gem-swap/content.d.ts +75 -0
- package/dist/gem-swap/content.js +68 -0
- package/dist/gem-swap/engine.d.ts +25 -0
- package/dist/gem-swap/engine.js +689 -0
- package/dist/gem-swap/persistence.d.ts +17 -0
- package/dist/gem-swap/persistence.js +107 -0
- package/dist/gem-swap/practice.d.ts +4 -0
- package/dist/gem-swap/practice.js +57 -0
- package/dist/gem-swap/random.d.ts +9 -0
- package/dist/gem-swap/random.js +32 -0
- package/dist/gem-swap/specials.d.ts +13 -0
- package/dist/gem-swap/specials.js +300 -0
- package/dist/gem-swap/tools.d.ts +3 -0
- package/dist/gem-swap/tools.js +20 -0
- package/dist/gem-swap/types.d.ts +200 -0
- package/dist/gem-swap/types.js +1 -0
- package/dist/gem-swap.d.ts +5 -0
- package/dist/gem-swap.js +3 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/magnetic-blocks/engine.d.ts +11 -0
- package/dist/magnetic-blocks/engine.js +263 -0
- package/dist/magnetic-blocks/persistence.d.ts +7 -0
- package/dist/magnetic-blocks/persistence.js +104 -0
- package/dist/magnetic-blocks/physics.d.ts +30 -0
- package/dist/magnetic-blocks/physics.js +155 -0
- package/dist/magnetic-blocks/random.d.ts +6 -0
- package/dist/magnetic-blocks/random.js +19 -0
- package/dist/magnetic-blocks/types.d.ts +158 -0
- package/dist/magnetic-blocks/types.js +1 -0
- package/dist/magnetic-blocks/validation.d.ts +23 -0
- package/dist/magnetic-blocks/validation.js +121 -0
- package/dist/magnetic-blocks.d.ts +5 -0
- package/dist/magnetic-blocks.js +4 -0
- package/dist/nature/attraction.d.ts +5 -0
- package/dist/nature/attraction.js +136 -0
- package/dist/nature/environment.d.ts +91 -0
- package/dist/nature/environment.js +271 -0
- package/dist/nature/marking.d.ts +3 -0
- package/dist/nature/marking.js +39 -0
- package/dist/nature/random.d.ts +3 -0
- package/dist/nature/random.js +19 -0
- package/dist/nature/rebound.d.ts +5 -0
- package/dist/nature/rebound.js +98 -0
- package/dist/nature/state.d.ts +3 -0
- package/dist/nature/state.js +22 -0
- package/dist/nature/types.d.ts +124 -0
- package/dist/nature/types.js +1 -0
- package/dist/nature/validation.d.ts +10 -0
- package/dist/nature/validation.js +117 -0
- package/dist/nature.d.ts +8 -0
- package/dist/nature.js +7 -0
- package/dist/stone-collapse/content-data.d.ts +14218 -0
- package/dist/stone-collapse/content-data.js +25217 -0
- package/dist/stone-collapse/content-types.d.ts +77 -0
- package/dist/stone-collapse/content-types.js +1 -0
- package/dist/stone-collapse/content.d.ts +11 -0
- package/dist/stone-collapse/content.js +28 -0
- package/dist/stone-collapse/engine.d.ts +19 -0
- package/dist/stone-collapse/engine.js +570 -0
- package/dist/stone-collapse/persistence.d.ts +7 -0
- package/dist/stone-collapse/persistence.js +230 -0
- package/dist/stone-collapse/types.d.ts +216 -0
- package/dist/stone-collapse/types.js +1 -0
- package/dist/stone-collapse/validation.d.ts +21 -0
- package/dist/stone-collapse/validation.js +134 -0
- package/dist/stone-collapse.d.ts +6 -0
- package/dist/stone-collapse.js +4 -0
- package/package.json +118 -4
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { advanceTicks, applyAction, createChallenge, createGame, requestHint, undo } from './engine.js';
|
|
2
|
+
const RULES = 'collapse-2';
|
|
3
|
+
const MAX_RECORDED_ACTIONS = 100_000;
|
|
4
|
+
const MAX_RECORDED_TICKS = 10_000_000;
|
|
5
|
+
function groupScore(size) { return 5 * size * (size - 1); }
|
|
6
|
+
function indexOfId(board, id) { return board.findIndex(stone => stone?.id === id); }
|
|
7
|
+
function groupAt(board, settings, start) {
|
|
8
|
+
const stone = board[start];
|
|
9
|
+
if (!stone)
|
|
10
|
+
return [];
|
|
11
|
+
const width = settings.width;
|
|
12
|
+
const height = settings.height;
|
|
13
|
+
const mask = settings.mask;
|
|
14
|
+
const neighbors = (index) => {
|
|
15
|
+
const x = index % width;
|
|
16
|
+
const y = Math.floor(index / width);
|
|
17
|
+
const out = [];
|
|
18
|
+
if (x > 0 && mask[index - 1])
|
|
19
|
+
out.push(index - 1);
|
|
20
|
+
if (x + 1 < width && mask[index + 1])
|
|
21
|
+
out.push(index + 1);
|
|
22
|
+
if (y > 0 && mask[index - width])
|
|
23
|
+
out.push(index - width);
|
|
24
|
+
if (y + 1 < height && mask[index + width])
|
|
25
|
+
out.push(index + width);
|
|
26
|
+
return out;
|
|
27
|
+
};
|
|
28
|
+
const found = new Set([start]);
|
|
29
|
+
const pending = [start];
|
|
30
|
+
while (pending.length)
|
|
31
|
+
for (const next of neighbors(pending.pop()))
|
|
32
|
+
if (!found.has(next) && board[next]?.colour === stone.colour) {
|
|
33
|
+
found.add(next);
|
|
34
|
+
pending.push(next);
|
|
35
|
+
}
|
|
36
|
+
return [...found].sort((a, b) => a - b);
|
|
37
|
+
}
|
|
38
|
+
/** Starts a fresh attempt from the identical seeded board or preserved challenge definition. */
|
|
39
|
+
export function restartGame(state) {
|
|
40
|
+
if (state.settings.mode === 'challenge')
|
|
41
|
+
return createChallenge(state.challenge);
|
|
42
|
+
if (state.settings.mode === 'daily')
|
|
43
|
+
return createGame({ mode: 'daily', dailyDate: state.settings.dailyDate });
|
|
44
|
+
const { settings } = state;
|
|
45
|
+
const mode = settings.mode === 'arcade' ? 'arcade' : 'relaxed';
|
|
46
|
+
return createGame({ mode, colourCount: settings.colourCount, seed: settings.seed, tools: settings.tools, ...(settings.shape ? { shape: settings.shape } : { width: settings.width, height: settings.height, ...(settings.mask.every(Boolean) ? {} : { mask: settings.mask }) }) });
|
|
47
|
+
}
|
|
48
|
+
/** Encodes the canonical initial configuration, accepted replay actions, and a derived checkpoint. */
|
|
49
|
+
export function encodeGame(state) {
|
|
50
|
+
// Undo frames are derived by replaying committed actions; storing their full board snapshots scales poorly on deep boards.
|
|
51
|
+
const { history: _history, ...checkpoint } = state;
|
|
52
|
+
const encoded = JSON.stringify({ format: 1, game: state.game, rules: state.rules, settings: state.settings, challenge: state.challenge, actions: state.recording, checkpoint });
|
|
53
|
+
if (new TextEncoder().encode(encoded).length > 2 * 1024 * 1024)
|
|
54
|
+
throw new RangeError('Save exceeds 2 MiB');
|
|
55
|
+
return encoded;
|
|
56
|
+
}
|
|
57
|
+
function stable(value, depth = 0) {
|
|
58
|
+
if (depth > 50)
|
|
59
|
+
throw new TypeError('Save checkpoint is nested too deeply');
|
|
60
|
+
if (Array.isArray(value))
|
|
61
|
+
return `[${value.map(item => stable(item, depth + 1)).join(',')}]`;
|
|
62
|
+
if (value && typeof value === 'object')
|
|
63
|
+
return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stable(item, depth + 1)}`).join(',')}}`;
|
|
64
|
+
return JSON.stringify(value) ?? 'null';
|
|
65
|
+
}
|
|
66
|
+
function applyUiSelection(state, stoneId) {
|
|
67
|
+
if (stoneId === null)
|
|
68
|
+
return { ...state, selectedId: null, selectedIds: Object.freeze([]), previewScore: 0 };
|
|
69
|
+
const at = indexOfId(state.board, stoneId);
|
|
70
|
+
if (at < 0)
|
|
71
|
+
throw new TypeError('Checkpoint selection names an unknown stone');
|
|
72
|
+
const cells = groupAt(state.board, state.settings, at);
|
|
73
|
+
if (cells.length < 2)
|
|
74
|
+
throw new TypeError('Checkpoint selection is not a legal group');
|
|
75
|
+
const ids = cells.map(cell => state.board[cell].id);
|
|
76
|
+
return { ...state, selectedId: stoneId, selectedIds: Object.freeze(ids), previewScore: groupScore(ids.length), selectedTool: null, toolTarget: null, toolPreview: Object.freeze([]) };
|
|
77
|
+
}
|
|
78
|
+
function stateFromInitial(settings, challenge) {
|
|
79
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings))
|
|
80
|
+
throw new TypeError('Save has invalid initial settings');
|
|
81
|
+
const settingsKeys = ['mode', 'width', 'height', 'colourCount', 'seed', 'shape', 'mask', 'dailyDate', 'challengeId', 'goal', 'moveLimit', 'tools'];
|
|
82
|
+
if (Object.keys(settings).some(key => !settingsKeys.includes(key)))
|
|
83
|
+
throw new TypeError('Initial settings contain unsupported fields');
|
|
84
|
+
if (settings.mode === 'challenge') {
|
|
85
|
+
if (!challenge)
|
|
86
|
+
throw new TypeError('Challenge save has no initial challenge definition');
|
|
87
|
+
const state = createChallenge(challenge);
|
|
88
|
+
if (stable(state.settings) !== stable(settings))
|
|
89
|
+
throw new TypeError('Challenge settings do not match the initial definition');
|
|
90
|
+
return state;
|
|
91
|
+
}
|
|
92
|
+
if (challenge !== null)
|
|
93
|
+
throw new TypeError('Score-game save cannot contain a challenge definition');
|
|
94
|
+
const options = settings.mode === 'daily'
|
|
95
|
+
? { mode: 'daily', dailyDate: settings.dailyDate }
|
|
96
|
+
: { mode: settings.mode, colourCount: settings.colourCount, seed: settings.seed, tools: settings.tools, ...(settings.shape ? { shape: settings.shape } : { width: settings.width, height: settings.height, ...(settings.mask.every(Boolean) ? {} : { mask: settings.mask }) }) };
|
|
97
|
+
const state = createGame(options);
|
|
98
|
+
if (stable(state.settings) !== stable(settings))
|
|
99
|
+
throw new TypeError('Initial settings are not canonical');
|
|
100
|
+
return state;
|
|
101
|
+
}
|
|
102
|
+
/** Reconstructs and validates a bounded replay before accepting its checkpoint. */
|
|
103
|
+
export function decodeGame(text) {
|
|
104
|
+
if (typeof text !== 'string' || new TextEncoder().encode(text).length > 2 * 1024 * 1024)
|
|
105
|
+
throw new RangeError('Save must be text no larger than 2 MiB');
|
|
106
|
+
let value;
|
|
107
|
+
try {
|
|
108
|
+
value = JSON.parse(text);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
throw new TypeError('Save is not valid JSON');
|
|
112
|
+
}
|
|
113
|
+
if (!value || value.format !== 1 || value.game !== 'stone-collapse' || value.rules !== RULES || !value.settings || !Array.isArray(value.actions) || !value.checkpoint)
|
|
114
|
+
throw new TypeError('Unsupported or invalid Stone Collapse save');
|
|
115
|
+
if (Object.keys(value).some(key => !['format', 'game', 'rules', 'settings', 'challenge', 'actions', 'checkpoint'].includes(key)))
|
|
116
|
+
throw new TypeError('Save contains unsupported fields');
|
|
117
|
+
if (value.actions.length > MAX_RECORDED_ACTIONS)
|
|
118
|
+
throw new RangeError('Save exceeds 100,000 replay entries');
|
|
119
|
+
let state = stateFromInitial(value.settings, value.challenge ?? null);
|
|
120
|
+
let totalTicks = 0;
|
|
121
|
+
for (let index = 0; index < value.actions.length; index++) {
|
|
122
|
+
const raw = value.actions[index];
|
|
123
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
124
|
+
throw new TypeError(`Invalid replay entry ${index + 1}`);
|
|
125
|
+
const entry = raw;
|
|
126
|
+
if (entry.kind === 'remove') {
|
|
127
|
+
if (Object.keys(entry).some(key => !['kind', 'move', 'stoneId'].includes(key)))
|
|
128
|
+
throw new TypeError(`Removal record ${index + 1} contains unsupported fields`);
|
|
129
|
+
if (!Number.isSafeInteger(entry.move) || entry.move !== state.moves + 1 || !Number.isSafeInteger(entry.stoneId))
|
|
130
|
+
throw new TypeError(`Invalid removal record ${index + 1}`);
|
|
131
|
+
const at = indexOfId(state.board, entry.stoneId);
|
|
132
|
+
if (at < 0)
|
|
133
|
+
throw new TypeError(`Illegal removal record ${index + 1}: unknown stone`);
|
|
134
|
+
const groupIds = groupAt(state.board, state.settings, at).map(cell => state.board[cell].id);
|
|
135
|
+
const alreadySelected = state.selectedIds.length === groupIds.length && groupIds.every(id => state.selectedIds.includes(id));
|
|
136
|
+
const selected = alreadySelected ? { state, accepted: true } : applyAction(state, { kind: 'select', stoneId: entry.stoneId });
|
|
137
|
+
if (!selected.accepted)
|
|
138
|
+
throw new TypeError(`Illegal removal record ${index + 1}: ${selected.reason}`);
|
|
139
|
+
const committed = applyAction(selected.state, { kind: 'confirm' });
|
|
140
|
+
if (!committed.accepted || committed.state.moves !== entry.move)
|
|
141
|
+
throw new TypeError(`Illegal removal record ${index + 1}: ${committed.reason ?? 'wrong move number'}`);
|
|
142
|
+
state = committed.state;
|
|
143
|
+
}
|
|
144
|
+
else if (entry.kind === 'select-tool') {
|
|
145
|
+
if (Object.keys(entry).some(key => !['kind', 'tool'].includes(key)) || !['bomb', 'pick'].includes(entry.tool))
|
|
146
|
+
throw new TypeError(`Invalid tool selection record ${index + 1}`);
|
|
147
|
+
const result = applyAction(state, { kind: 'select-tool', tool: entry.tool });
|
|
148
|
+
if (!result.accepted)
|
|
149
|
+
throw new TypeError(`Illegal tool selection record ${index + 1}: ${result.reason}`);
|
|
150
|
+
state = result.state;
|
|
151
|
+
}
|
|
152
|
+
else if (entry.kind === 'target-tool') {
|
|
153
|
+
if (Object.keys(entry).some(key => !['kind', 'cell'].includes(key)) || !Number.isSafeInteger(entry.cell))
|
|
154
|
+
throw new TypeError(`Invalid tool target record ${index + 1}`);
|
|
155
|
+
const result = applyAction(state, { kind: 'target-tool', cell: entry.cell });
|
|
156
|
+
if (!result.accepted)
|
|
157
|
+
throw new TypeError(`Illegal tool target record ${index + 1}: ${result.reason}`);
|
|
158
|
+
state = result.state;
|
|
159
|
+
}
|
|
160
|
+
else if (entry.kind === 'confirm-tool') {
|
|
161
|
+
if (Object.keys(entry).some(key => !['kind', 'move'].includes(key)) || !Number.isSafeInteger(entry.move) || entry.move !== state.moves + 1)
|
|
162
|
+
throw new TypeError(`Invalid tool-use record ${index + 1}`);
|
|
163
|
+
const result = applyAction(state, { kind: 'confirm-tool' });
|
|
164
|
+
if (!result.accepted || result.state.moves !== entry.move)
|
|
165
|
+
throw new TypeError(`Illegal tool-use record ${index + 1}: ${result.reason ?? 'wrong move number'}`);
|
|
166
|
+
state = result.state;
|
|
167
|
+
}
|
|
168
|
+
else if (entry.kind === 'cancel-tool') {
|
|
169
|
+
if (Object.keys(entry).some(key => key !== 'kind'))
|
|
170
|
+
throw new TypeError(`Tool cancellation record ${index + 1} contains unsupported fields`);
|
|
171
|
+
const result = applyAction(state, { kind: 'cancel-tool' });
|
|
172
|
+
if (!result.accepted)
|
|
173
|
+
throw new TypeError(`Illegal tool cancellation record ${index + 1}: ${result.reason}`);
|
|
174
|
+
state = result.state;
|
|
175
|
+
}
|
|
176
|
+
else if (entry.kind === 'undo') {
|
|
177
|
+
if (Object.keys(entry).some(key => !['kind', 'move'].includes(key)))
|
|
178
|
+
throw new TypeError(`Undo record ${index + 1} contains unsupported fields`);
|
|
179
|
+
if (!Number.isSafeInteger(entry.move) || entry.move !== state.moves - 1)
|
|
180
|
+
throw new TypeError(`Invalid undo record ${index + 1}`);
|
|
181
|
+
const result = undo(state);
|
|
182
|
+
if (!result.accepted || result.state.moves !== entry.move)
|
|
183
|
+
throw new TypeError(`Illegal undo record ${index + 1}`);
|
|
184
|
+
state = result.state;
|
|
185
|
+
}
|
|
186
|
+
else if (entry.kind === 'hint') {
|
|
187
|
+
if (Object.keys(entry).some(key => !['kind', 'witnessIndex'].includes(key)))
|
|
188
|
+
throw new TypeError(`Hint record ${index + 1} contains unsupported fields`);
|
|
189
|
+
if (!Number.isSafeInteger(entry.witnessIndex) || entry.witnessIndex !== state.witnessIndex)
|
|
190
|
+
throw new TypeError(`Invalid hint record ${index + 1}`);
|
|
191
|
+
const result = requestHint(state);
|
|
192
|
+
if (!result.accepted)
|
|
193
|
+
throw new TypeError(`Illegal hint record ${index + 1}: ${result.reason}`);
|
|
194
|
+
state = result.state;
|
|
195
|
+
}
|
|
196
|
+
else if (entry.kind === 'ticks') {
|
|
197
|
+
if (Object.keys(entry).some(key => !['kind', 'count'].includes(key)))
|
|
198
|
+
throw new TypeError(`Tick record ${index + 1} contains unsupported fields`);
|
|
199
|
+
if (!Number.isSafeInteger(entry.count) || entry.count < 1 || (totalTicks += entry.count) > MAX_RECORDED_TICKS)
|
|
200
|
+
throw new RangeError(`Invalid tick record ${index + 1}`);
|
|
201
|
+
let remaining = entry.count;
|
|
202
|
+
while (remaining > 0) {
|
|
203
|
+
const amount = Math.min(3600, remaining);
|
|
204
|
+
const prior = state.elapsedTicks;
|
|
205
|
+
state = advanceTicks(state, amount).state;
|
|
206
|
+
const advanced = state.elapsedTicks - prior;
|
|
207
|
+
if (advanced !== amount)
|
|
208
|
+
throw new TypeError(`Ticks continue after the run stopped at replay entry ${index + 1}`);
|
|
209
|
+
remaining -= amount;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else
|
|
213
|
+
throw new TypeError(`Unknown replay entry ${index + 1}`);
|
|
214
|
+
}
|
|
215
|
+
const checkpoint = value.checkpoint;
|
|
216
|
+
try {
|
|
217
|
+
if (checkpoint.selectedId !== state.selectedId)
|
|
218
|
+
state = applyUiSelection(state, checkpoint.selectedId ?? null);
|
|
219
|
+
const checkpointHasHistory = Object.hasOwn(checkpoint, 'history');
|
|
220
|
+
const { history: _history, ...derivedCheckpoint } = state;
|
|
221
|
+
if (stable(checkpointHasHistory ? state : derivedCheckpoint) !== stable(checkpoint))
|
|
222
|
+
throw new TypeError('Save checkpoint does not match its action recording');
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (error instanceof TypeError && error.message.startsWith('Save checkpoint'))
|
|
226
|
+
throw error;
|
|
227
|
+
throw new TypeError('Save checkpoint is invalid or does not match its action recording');
|
|
228
|
+
}
|
|
229
|
+
return state;
|
|
230
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/** A colour used by a stone. */
|
|
2
|
+
export type StoneColour = 'red' | 'blue' | 'green' | 'gold' | 'purple' | 'teal';
|
|
3
|
+
/** A stone keeps its run-local identity as it falls or changes column. */
|
|
4
|
+
export interface Stone {
|
|
5
|
+
readonly id: number;
|
|
6
|
+
readonly colour: StoneColour;
|
|
7
|
+
}
|
|
8
|
+
export type BoardPreset = 'compact' | 'standard' | 'wide' | 'tall' | 'extraWide' | 'deep' | 'large';
|
|
9
|
+
export type BoardShape = 'heart' | 'star' | 'hexagon';
|
|
10
|
+
export type GameMode = 'relaxed' | 'arcade' | 'daily' | 'challenge';
|
|
11
|
+
export type StoredTool = 'bomb' | 'pick';
|
|
12
|
+
export interface ToolInventory {
|
|
13
|
+
readonly bomb: number;
|
|
14
|
+
readonly pick: number;
|
|
15
|
+
}
|
|
16
|
+
export type GamePhase = 'ready' | 'clear-mark' | 'clear-remove' | 'gravity' | 'won' | 'lost' | 'finished';
|
|
17
|
+
export type ChallengeGoal = {
|
|
18
|
+
readonly kind: 'clear-all';
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: 'clear-targets';
|
|
21
|
+
readonly targetIds: readonly number[];
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: 'score-target';
|
|
24
|
+
readonly minimumScore: number;
|
|
25
|
+
};
|
|
26
|
+
export interface Settings {
|
|
27
|
+
readonly mode: GameMode;
|
|
28
|
+
readonly width: number;
|
|
29
|
+
readonly height: number;
|
|
30
|
+
readonly colourCount: 4 | 5 | 6;
|
|
31
|
+
readonly seed: string | number;
|
|
32
|
+
readonly shape?: BoardShape;
|
|
33
|
+
readonly mask: readonly boolean[];
|
|
34
|
+
readonly dailyDate?: string;
|
|
35
|
+
readonly challengeId?: string;
|
|
36
|
+
readonly goal?: ChallengeGoal;
|
|
37
|
+
readonly moveLimit?: number;
|
|
38
|
+
readonly tools: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface ChallengeDefinition {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly width: number;
|
|
43
|
+
readonly height: number;
|
|
44
|
+
readonly colourCount: 4 | 5 | 6;
|
|
45
|
+
readonly seed: string | number;
|
|
46
|
+
readonly mask: readonly boolean[];
|
|
47
|
+
readonly initialBoard: readonly (Stone | null)[];
|
|
48
|
+
readonly goal: ChallengeGoal;
|
|
49
|
+
readonly moveLimit?: number;
|
|
50
|
+
/** Each entry is the complete stable-ID set of the next witnessed group. */
|
|
51
|
+
readonly witness: readonly (readonly number[])[];
|
|
52
|
+
}
|
|
53
|
+
/** Data sufficient to restore the complete game immediately before a committed move. */
|
|
54
|
+
export interface UndoFrame {
|
|
55
|
+
readonly board: readonly (Stone | null)[];
|
|
56
|
+
readonly score: number;
|
|
57
|
+
readonly moves: number;
|
|
58
|
+
readonly removed: number;
|
|
59
|
+
readonly randomState: number;
|
|
60
|
+
readonly nextId: number;
|
|
61
|
+
readonly usedFallback: boolean;
|
|
62
|
+
readonly finishAdjustmentApplied: boolean;
|
|
63
|
+
readonly selectedId: number | null;
|
|
64
|
+
readonly selectedIds: readonly number[];
|
|
65
|
+
readonly previewScore: number;
|
|
66
|
+
readonly witnessIndex: number;
|
|
67
|
+
readonly inventory: ToolInventory;
|
|
68
|
+
readonly toolProgress: number;
|
|
69
|
+
readonly toolAwardCursor: 0 | 1;
|
|
70
|
+
readonly selectedTool: StoredTool | null;
|
|
71
|
+
readonly toolTarget: number | null;
|
|
72
|
+
readonly toolPreview: readonly number[];
|
|
73
|
+
}
|
|
74
|
+
export type RecordedAction = {
|
|
75
|
+
readonly kind: 'remove';
|
|
76
|
+
readonly move: number;
|
|
77
|
+
readonly stoneId: number;
|
|
78
|
+
} | {
|
|
79
|
+
readonly kind: 'select-tool';
|
|
80
|
+
readonly tool: StoredTool;
|
|
81
|
+
} | {
|
|
82
|
+
readonly kind: 'target-tool';
|
|
83
|
+
readonly cell: number;
|
|
84
|
+
} | {
|
|
85
|
+
readonly kind: 'confirm-tool';
|
|
86
|
+
readonly move: number;
|
|
87
|
+
} | {
|
|
88
|
+
readonly kind: 'cancel-tool';
|
|
89
|
+
} | {
|
|
90
|
+
readonly kind: 'undo';
|
|
91
|
+
readonly move: number;
|
|
92
|
+
} | {
|
|
93
|
+
readonly kind: 'hint';
|
|
94
|
+
readonly witnessIndex: number;
|
|
95
|
+
} | {
|
|
96
|
+
readonly kind: 'ticks';
|
|
97
|
+
readonly count: number;
|
|
98
|
+
};
|
|
99
|
+
export interface GameState {
|
|
100
|
+
readonly game: 'stone-collapse';
|
|
101
|
+
readonly rules: 'collapse-2';
|
|
102
|
+
readonly settings: Settings;
|
|
103
|
+
readonly challenge: ChallengeDefinition | null;
|
|
104
|
+
readonly initialBoard: readonly (Stone | null)[];
|
|
105
|
+
/** Row-major; masked and currently empty cells contain null. */
|
|
106
|
+
readonly board: readonly (Stone | null)[];
|
|
107
|
+
readonly phase: GamePhase;
|
|
108
|
+
readonly selectedId: number | null;
|
|
109
|
+
readonly selectedIds: readonly number[];
|
|
110
|
+
readonly previewScore: number;
|
|
111
|
+
readonly score: number;
|
|
112
|
+
readonly moves: number;
|
|
113
|
+
readonly removed: number;
|
|
114
|
+
readonly randomState: number;
|
|
115
|
+
readonly nextId: number;
|
|
116
|
+
readonly usedFallback: boolean;
|
|
117
|
+
readonly resolutionTick: number;
|
|
118
|
+
readonly pendingIds: readonly number[];
|
|
119
|
+
/** Settled destination board held during the gravity animation. */
|
|
120
|
+
readonly gravityBoard: readonly (Stone | null)[] | null;
|
|
121
|
+
readonly finishAdjustmentApplied: boolean;
|
|
122
|
+
readonly assisted: boolean;
|
|
123
|
+
readonly history: readonly UndoFrame[];
|
|
124
|
+
/** -1 means the player's current board has left the supplied witness path. */
|
|
125
|
+
readonly witnessIndex: number;
|
|
126
|
+
readonly elapsedTicks: number;
|
|
127
|
+
readonly reason?: string;
|
|
128
|
+
readonly recording: readonly RecordedAction[];
|
|
129
|
+
readonly inventory: ToolInventory;
|
|
130
|
+
/** Ordinary removed stones carried toward the next tool award; always 0–11. */
|
|
131
|
+
readonly toolProgress: number;
|
|
132
|
+
/** 0 awards Bomb next; 1 awards Pick next. */
|
|
133
|
+
readonly toolAwardCursor: 0 | 1;
|
|
134
|
+
readonly selectedTool: StoredTool | null;
|
|
135
|
+
readonly toolTarget: number | null;
|
|
136
|
+
/** Exact row-major cells affected by the current target. */
|
|
137
|
+
readonly toolPreview: readonly number[];
|
|
138
|
+
}
|
|
139
|
+
export type Action = {
|
|
140
|
+
readonly kind: 'select';
|
|
141
|
+
readonly stoneId: number;
|
|
142
|
+
} | {
|
|
143
|
+
readonly kind: 'confirm';
|
|
144
|
+
} | {
|
|
145
|
+
readonly kind: 'cancel';
|
|
146
|
+
} | {
|
|
147
|
+
readonly kind: 'undo';
|
|
148
|
+
} | {
|
|
149
|
+
readonly kind: 'hint';
|
|
150
|
+
} | {
|
|
151
|
+
readonly kind: 'select-tool';
|
|
152
|
+
readonly tool: StoredTool;
|
|
153
|
+
} | {
|
|
154
|
+
readonly kind: 'target-tool';
|
|
155
|
+
readonly cell: number;
|
|
156
|
+
} | {
|
|
157
|
+
readonly kind: 'confirm-tool';
|
|
158
|
+
} | {
|
|
159
|
+
readonly kind: 'cancel-tool';
|
|
160
|
+
};
|
|
161
|
+
export interface GameEvent {
|
|
162
|
+
readonly type: string;
|
|
163
|
+
readonly [key: string]: unknown;
|
|
164
|
+
}
|
|
165
|
+
export interface Transition {
|
|
166
|
+
readonly state: GameState;
|
|
167
|
+
readonly events: readonly GameEvent[];
|
|
168
|
+
readonly accepted: boolean;
|
|
169
|
+
readonly reason?: string;
|
|
170
|
+
}
|
|
171
|
+
export interface CreateOptions {
|
|
172
|
+
readonly mode?: Exclude<GameMode, 'challenge'>;
|
|
173
|
+
readonly preset?: BoardPreset;
|
|
174
|
+
readonly width?: number;
|
|
175
|
+
readonly height?: number;
|
|
176
|
+
readonly colourCount?: 4 | 5 | 6;
|
|
177
|
+
readonly seed?: string | number;
|
|
178
|
+
readonly shape?: BoardShape;
|
|
179
|
+
/** Row-major custom active-cell mask. Supply width and height with a custom mask. */
|
|
180
|
+
readonly mask?: readonly boolean[];
|
|
181
|
+
/** Required for Daily; supplied by the host in UTC. */
|
|
182
|
+
readonly dailyDate?: string;
|
|
183
|
+
/** Optional Bomb and Pick tray; available only in Relaxed and Arcade. */
|
|
184
|
+
readonly tools?: boolean;
|
|
185
|
+
}
|
|
186
|
+
export interface ChallengeOptions {
|
|
187
|
+
readonly id: string;
|
|
188
|
+
readonly width: number;
|
|
189
|
+
readonly height: number;
|
|
190
|
+
readonly colourCount: 4 | 5 | 6;
|
|
191
|
+
readonly seed?: string | number;
|
|
192
|
+
readonly mask?: readonly boolean[];
|
|
193
|
+
readonly initialBoard: readonly (Stone | null)[];
|
|
194
|
+
readonly goal: ChallengeGoal;
|
|
195
|
+
readonly moveLimit?: number;
|
|
196
|
+
readonly witness: readonly (readonly number[])[];
|
|
197
|
+
}
|
|
198
|
+
export interface GameStatus {
|
|
199
|
+
readonly mode: GameMode;
|
|
200
|
+
readonly phase: GamePhase;
|
|
201
|
+
readonly score: number;
|
|
202
|
+
readonly moves: number;
|
|
203
|
+
readonly removed: number;
|
|
204
|
+
readonly remaining: number;
|
|
205
|
+
readonly previewScore: number;
|
|
206
|
+
readonly usedFallback: boolean;
|
|
207
|
+
readonly assisted: boolean;
|
|
208
|
+
readonly goal?: ChallengeGoal;
|
|
209
|
+
readonly reason?: string;
|
|
210
|
+
readonly inventory: ToolInventory;
|
|
211
|
+
readonly toolProgress: number;
|
|
212
|
+
readonly toolAwardCursor: 0 | 1;
|
|
213
|
+
readonly selectedTool: StoredTool | null;
|
|
214
|
+
readonly toolTarget: number | null;
|
|
215
|
+
readonly toolPreview: readonly number[];
|
|
216
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BoardShape, CreateOptions, GameMode } from './types.js';
|
|
2
|
+
/** Typed public-settings validation error. */
|
|
3
|
+
export declare class StoneCollapseOptionsError extends RangeError {
|
|
4
|
+
readonly code: string;
|
|
5
|
+
constructor(code: string, message: string);
|
|
6
|
+
}
|
|
7
|
+
export interface NormalizedOptions {
|
|
8
|
+
readonly mode: Exclude<GameMode, 'challenge'>;
|
|
9
|
+
readonly dailyDate?: string;
|
|
10
|
+
readonly width: number;
|
|
11
|
+
readonly height: number;
|
|
12
|
+
readonly colourCount: 4 | 5 | 6;
|
|
13
|
+
readonly seed: string | number;
|
|
14
|
+
readonly tools: boolean;
|
|
15
|
+
readonly shape?: BoardShape;
|
|
16
|
+
readonly mask: readonly boolean[];
|
|
17
|
+
}
|
|
18
|
+
/** Returns orthogonal active neighbors in deterministic row-major order. */
|
|
19
|
+
export declare function neighbors(index: number, width: number, height: number, mask: readonly boolean[]): number[];
|
|
20
|
+
/** Validates all public score-game options before any random draw occurs. */
|
|
21
|
+
export declare function optionsFor(options: CreateOptions): NormalizedOptions;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const PRESETS = {
|
|
2
|
+
compact: [6, 8], standard: [8, 10], wide: [10, 8], tall: [6, 12], extraWide: [16, 10], deep: [8, 32], large: [12, 32],
|
|
3
|
+
};
|
|
4
|
+
/** Typed public-settings validation error. */
|
|
5
|
+
export class StoneCollapseOptionsError extends RangeError {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message) { super(message); this.name = 'StoneCollapseOptionsError'; this.code = code; }
|
|
8
|
+
}
|
|
9
|
+
function shapeMask(shape) {
|
|
10
|
+
if (shape === 'heart') {
|
|
11
|
+
const width = 10;
|
|
12
|
+
const rows = [
|
|
13
|
+
[1, 3, 6, 8], [0, 4, 5, 9], [0, 9], [0, 9], [1, 8], [2, 7], [3, 6], [4, 5], [4, 5], [4, 5],
|
|
14
|
+
];
|
|
15
|
+
return [width, rows.length, rows.flatMap(([a, b, c, d]) => Array.from({ length: width }, (_, x) => x >= a && x <= b || c !== undefined && x >= c && x <= d))];
|
|
16
|
+
}
|
|
17
|
+
if (shape === 'star') {
|
|
18
|
+
const width = 12;
|
|
19
|
+
const rowRanges = [
|
|
20
|
+
[5, 6], [5, 6], [4, 7], [4, 7], [0, 11], [0, 11], [2, 9], [2, 9], [3, 8], [3, 8], [4, 7], [4, 7],
|
|
21
|
+
];
|
|
22
|
+
return [width, rowRanges.length, rowRanges.flatMap(([a, b]) => Array.from({ length: width }, (_, x) => x >= a && x <= b))];
|
|
23
|
+
}
|
|
24
|
+
const width = 10;
|
|
25
|
+
const rowRanges = [
|
|
26
|
+
[2, 7], [1, 8], [0, 9], [0, 9], [0, 9], [0, 9], [0, 9], [0, 9], [1, 8], [2, 7],
|
|
27
|
+
];
|
|
28
|
+
return [width, rowRanges.length, rowRanges.flatMap(([a, b]) => Array.from({ length: width }, (_, x) => x >= a && x <= b))];
|
|
29
|
+
}
|
|
30
|
+
/** Returns orthogonal active neighbors in deterministic row-major order. */
|
|
31
|
+
export function neighbors(index, width, height, mask) {
|
|
32
|
+
const x = index % width;
|
|
33
|
+
const y = Math.floor(index / width);
|
|
34
|
+
const out = [];
|
|
35
|
+
if (x > 0 && mask[index - 1])
|
|
36
|
+
out.push(index - 1);
|
|
37
|
+
if (x + 1 < width && mask[index + 1])
|
|
38
|
+
out.push(index + 1);
|
|
39
|
+
if (y > 0 && mask[index - width])
|
|
40
|
+
out.push(index - width);
|
|
41
|
+
if (y + 1 < height && mask[index + width])
|
|
42
|
+
out.push(index + width);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function validateMask(mask, width, height) {
|
|
46
|
+
if (!Array.isArray(mask))
|
|
47
|
+
throw new StoneCollapseOptionsError('invalid-mask', 'Mask must be a boolean array');
|
|
48
|
+
if (mask.length !== width * height || Array.from(mask).some(value => typeof value !== 'boolean'))
|
|
49
|
+
throw new StoneCollapseOptionsError('invalid-mask-size', 'Mask must contain one boolean per board cell');
|
|
50
|
+
const active = mask.flatMap((on, i) => on ? [i] : []);
|
|
51
|
+
if (active.length < 16 || active.length > 512)
|
|
52
|
+
throw new StoneCollapseOptionsError('invalid-active-count', 'A board must have 16–512 active cells');
|
|
53
|
+
if (active.some(index => neighbors(index, width, height, mask).length === 0))
|
|
54
|
+
throw new StoneCollapseOptionsError('isolated-mask-cell', 'Mask cannot contain an isolated active cell');
|
|
55
|
+
const visited = new Set([active[0]]);
|
|
56
|
+
const pending = [active[0]];
|
|
57
|
+
while (pending.length)
|
|
58
|
+
for (const next of neighbors(pending.pop(), width, height, mask))
|
|
59
|
+
if (!visited.has(next)) {
|
|
60
|
+
visited.add(next);
|
|
61
|
+
pending.push(next);
|
|
62
|
+
}
|
|
63
|
+
if (visited.size !== active.length)
|
|
64
|
+
throw new StoneCollapseOptionsError('disconnected-mask', 'Active mask cells must be orthogonally connected');
|
|
65
|
+
}
|
|
66
|
+
function isUtcDate(value) {
|
|
67
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
68
|
+
return false;
|
|
69
|
+
const time = Date.parse(`${value}T00:00:00.000Z`);
|
|
70
|
+
return Number.isFinite(time) && new Date(time).toISOString().slice(0, 10) === value;
|
|
71
|
+
}
|
|
72
|
+
/** Validates all public score-game options before any random draw occurs. */
|
|
73
|
+
export function optionsFor(options) {
|
|
74
|
+
if (!options || typeof options !== 'object' || Array.isArray(options))
|
|
75
|
+
throw new StoneCollapseOptionsError('invalid-options', 'Options must be an object');
|
|
76
|
+
const allowedKeys = ['mode', 'preset', 'width', 'height', 'colourCount', 'seed', 'shape', 'mask', 'dailyDate', 'tools'];
|
|
77
|
+
if (Object.keys(options).some(key => !allowedKeys.includes(key)))
|
|
78
|
+
throw new StoneCollapseOptionsError('unknown-option', 'Create options contain an unsupported field');
|
|
79
|
+
const mode = options.mode === undefined ? 'relaxed' : options.mode;
|
|
80
|
+
if (!['relaxed', 'arcade', 'daily'].includes(mode))
|
|
81
|
+
throw new StoneCollapseOptionsError('invalid-mode', 'Create options support Relaxed, Arcade, or Daily score play');
|
|
82
|
+
if (options.tools !== undefined && typeof options.tools !== 'boolean')
|
|
83
|
+
throw new StoneCollapseOptionsError('invalid-tools-option', 'tools must be a boolean');
|
|
84
|
+
if (mode === 'daily') {
|
|
85
|
+
if (options.tools === true)
|
|
86
|
+
throw new StoneCollapseOptionsError('daily-tools-disabled', 'Daily does not allow stored tools');
|
|
87
|
+
if (typeof options.dailyDate !== 'string' || !isUtcDate(options.dailyDate))
|
|
88
|
+
throw new StoneCollapseOptionsError('invalid-daily-date', 'Daily requires a valid UTC YYYY-MM-DD date from the host');
|
|
89
|
+
if (options.preset !== undefined || options.width !== undefined || options.height !== undefined || options.colourCount !== undefined || options.seed !== undefined || options.shape !== undefined || options.mask !== undefined)
|
|
90
|
+
throw new StoneCollapseOptionsError('noncanonical-daily-settings', 'Daily uses the fixed standard 8×10 rectangular board and four colours');
|
|
91
|
+
return { mode, dailyDate: options.dailyDate, width: 8, height: 10, colourCount: 4, seed: `stone-collapse:daily:${options.dailyDate}`, tools: false, mask: Array(80).fill(true) };
|
|
92
|
+
}
|
|
93
|
+
if (options.dailyDate !== undefined)
|
|
94
|
+
throw new StoneCollapseOptionsError('daily-date-outside-daily', 'dailyDate is supported only in Daily mode');
|
|
95
|
+
const hasDimensions = options.width !== undefined || options.height !== undefined;
|
|
96
|
+
let width;
|
|
97
|
+
let height;
|
|
98
|
+
const shape = options.shape;
|
|
99
|
+
let mask = options.mask;
|
|
100
|
+
if (shape !== undefined) {
|
|
101
|
+
if (options.preset !== undefined || hasDimensions || mask !== undefined || !['heart', 'star', 'hexagon'].includes(shape))
|
|
102
|
+
throw new StoneCollapseOptionsError('conflicting-board-options', 'Choose one shape or one rectangular preset/custom size');
|
|
103
|
+
[width, height, mask] = shapeMask(shape);
|
|
104
|
+
}
|
|
105
|
+
else if (mask !== undefined) {
|
|
106
|
+
if (options.preset !== undefined || options.width === undefined || options.height === undefined)
|
|
107
|
+
throw new StoneCollapseOptionsError('custom-mask-dimensions-required', 'A custom mask requires explicit width and height and cannot use a preset');
|
|
108
|
+
width = options.width;
|
|
109
|
+
height = options.height;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const presetName = options.preset === undefined ? 'standard' : options.preset;
|
|
113
|
+
if (!Object.hasOwn(PRESETS, presetName))
|
|
114
|
+
throw new StoneCollapseOptionsError('invalid-preset', 'Unknown board preset');
|
|
115
|
+
const preset = PRESETS[presetName];
|
|
116
|
+
if (hasDimensions && options.preset !== undefined)
|
|
117
|
+
throw new StoneCollapseOptionsError('conflicting-board-options', 'Choose a preset or custom dimensions');
|
|
118
|
+
width = options.width ?? preset[0];
|
|
119
|
+
height = options.height ?? preset[1];
|
|
120
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 4 || width > 16 || height < 4 || height > 32 || width * height > 512)
|
|
121
|
+
throw new StoneCollapseOptionsError('invalid-board-size', 'Board dimensions must be 4–16 columns by 4–32 rows, with at most 512 cells');
|
|
122
|
+
mask = Array(width * height).fill(true);
|
|
123
|
+
}
|
|
124
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 4 || width > 16 || height < 4 || height > 32 || width * height > 512)
|
|
125
|
+
throw new StoneCollapseOptionsError('invalid-board-size', 'Board dimensions must be 4–16 columns by 4–32 rows, with at most 512 cells');
|
|
126
|
+
validateMask(mask, width, height);
|
|
127
|
+
const colourCount = options.colourCount ?? 4;
|
|
128
|
+
if (colourCount !== 4 && colourCount !== 5 && colourCount !== 6)
|
|
129
|
+
throw new StoneCollapseOptionsError('invalid-colour-count', 'Colour count must be 4, 5, or 6');
|
|
130
|
+
const seed = options.seed === undefined ? 'houseki-stone-collapse' : options.seed;
|
|
131
|
+
if (typeof seed !== 'string' && (!Number.isInteger(seed) || seed < 0 || seed > 0xffff_ffff))
|
|
132
|
+
throw new StoneCollapseOptionsError('invalid-seed', 'Numeric seed must be an unsigned 32-bit integer');
|
|
133
|
+
return { mode, width: width, height: height, colourCount, seed, tools: options.tools ?? false, ...(shape ? { shape } : {}), mask: [...mask] };
|
|
134
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { StoneCollapseOptionsError } from './stone-collapse/validation.js';
|
|
2
|
+
export { createGame, createChallenge, applyAction, advanceTicks, legalActions, requestHint, undo, statusOf } from './stone-collapse/engine.js';
|
|
3
|
+
export { restartGame, encodeGame, decodeGame } from './stone-collapse/persistence.js';
|
|
4
|
+
export { campaignManifest, levelManifest, tutorialManifest, getLevel, createLevel, getTutorial } from './stone-collapse/content.js';
|
|
5
|
+
export type { Action, BoardPreset, BoardShape, ChallengeDefinition, ChallengeGoal, ChallengeOptions, CreateOptions, GameEvent, GameMode, GamePhase, GameState, GameStatus, RecordedAction, Settings, Stone, StoneColour, StoredTool, ToolInventory, Transition, UndoFrame } from './stone-collapse/types.js';
|
|
6
|
+
export type { CollapseCampaignLevel, CollapseCampaignManifest, CollapseDifficultyMetrics, CollapseTutorialDefinition, CollapseTutorialStep, LocalizedText } from './stone-collapse/content-types.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { StoneCollapseOptionsError } from './stone-collapse/validation.js';
|
|
2
|
+
export { createGame, createChallenge, applyAction, advanceTicks, legalActions, requestHint, undo, statusOf } from './stone-collapse/engine.js';
|
|
3
|
+
export { restartGame, encodeGame, decodeGame } from './stone-collapse/persistence.js';
|
|
4
|
+
export { campaignManifest, levelManifest, tutorialManifest, getLevel, createLevel, getTutorial } from './stone-collapse/content.js';
|