@johnmorrisdotca/houseki 0.4.0 → 0.4.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 CHANGED
@@ -4,6 +4,14 @@ All notable changes to this package are written here, newest first, in the form
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.4.1] - 2026-10-07
8
+
9
+ ### Fixed
10
+
11
+ - Gem Swap rejects excessively nested replay inputs with a bounded validation error.
12
+ - Reshuffling preserves an active Black Hole portal and all remaining gem identities.
13
+ - Black Hole design status now matches the optional shipped tray power. CI explicitly checks public metadata and machine-specific documentation paths.
14
+
7
15
  ## [0.4.0] - 2026-10-06
8
16
 
9
17
  ### Added
@@ -96,7 +104,7 @@ Nothing that was exported has changed. The README is the family's one layout, in
96
104
  - An experimental Magnetic Blocks engine with bonded squares, magnetic floor schedules, a Floor Switch and impact drops.
97
105
  - Typed entry points, a generated API reference, an MIT licence and reproducible package checks.
98
106
 
99
- [Unreleased]: https://github.com/johnmorrisdotca/houseki/compare/v0.4.0...HEAD
107
+ [Unreleased]: https://github.com/johnmorrisdotca/houseki/compare/v0.4.1...HEAD
100
108
  [0.3.0]: https://github.com/johnmorrisdotca/houseki/compare/v0.2.1...v0.3.0
101
109
  [0.2.1]: https://github.com/johnmorrisdotca/houseki/compare/v0.2.0...v0.2.1
102
110
  [0.2.0]: https://github.com/johnmorrisdotca/houseki/compare/v0.1.1...v0.2.0
@@ -104,3 +112,5 @@ Nothing that was exported has changed. The README is the family's one layout, in
104
112
  [0.1.0]: https://github.com/johnmorrisdotca/houseki/releases/tag/v0.1.0
105
113
 
106
114
  [0.4.0]: https://github.com/johnmorrisdotca/houseki/compare/v0.3.0...v0.4.0
115
+
116
+ [0.4.1]: https://github.com/johnmorrisdotca/houseki/compare/v0.4.0...v0.4.1
@@ -8,12 +8,14 @@ export class ReplayError extends TypeError {
8
8
  code;
9
9
  constructor(code, message) { super(message); this.name = 'ReplayError'; this.code = code; }
10
10
  }
11
- function canonical(value) {
11
+ function canonical(value, depth = 0) {
12
+ if (depth > 50)
13
+ throw new ReplayError('save-too-deep', 'Replay is nested too deeply');
12
14
  if (Array.isArray(value))
13
- return `[${value.map(canonical).join(',')}]`;
15
+ return `[${value.map(item => canonical(item, depth + 1)).join(',')}]`;
14
16
  if (value && typeof value === 'object') {
15
17
  const object = value;
16
- return `{${Object.keys(object).sort().map(key => `${JSON.stringify(key)}:${canonical(object[key])}`).join(',')}}`;
18
+ return `{${Object.keys(object).sort().map(key => `${JSON.stringify(key)}:${canonical(object[key], depth + 1)}`).join(',')}}`;
17
19
  }
18
20
  return JSON.stringify(value);
19
21
  }
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/houseki",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Houseki 宝石: five gem and stone puzzle engines, 684 measured challenges, bilingual lessons, deterministic saves, magnetic floors, Shizen and Arashi campaigns. TypeScript. Zero dependencies.",
5
5
  "keywords": [
6
6
  "puzzle",