@johnmorrisdotca/houseki 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,9 +4,22 @@ All notable changes to this package are written here, newest first, in the form
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.0] - 2026-10-06
8
+
9
+ ### Added
10
+
11
+ - Three dedicated fifty-level campaigns: Magnetic Blocks, Shizen and Arashi, bringing the package to 450 original graded challenges. Exact boards, finite queues, stable IDs, measured difficulty, winning witnesses and deterministic generation are published with the engines.
12
+ - Campaign selectors, fixed rule displays, goal progress, replay and next-level recovery in the players.
13
+
14
+ ### Fixed
15
+
16
+ - Clear game-over results and disabled play controls; fixed challenge settings; exact save recovery; responsive goal text; shared cloth and piece appearance; generated API imports and markup.
17
+ - Stable board geometry when switching English and Japanese at phone, tablet and desktop widths.
18
+
19
+
7
20
  ### Changed
8
21
 
9
- - Repository only: the package and everything it exports are unchanged. `CONTRIBUTING.md` is the family's one text with a section of its own for Houseki, held to the master in johnmorrisdotca/.github by `src/family.test.js`; `ci.yml` and `pages.yml` are the family's one text (`pnpm check`, the demo, and the package on Linux, macOS and Windows), and any jobs of the package's own after them.
22
+ - `CONTRIBUTING.md` is the family's one text with a section of its own for Houseki, held to the master in johnmorrisdotca/.github by `src/family.test.js`; `ci.yml` and `pages.yml` are the family's one text (`pnpm check`, the demo, and the package on Linux, macOS and Windows), and any jobs of the package's own after them.
10
23
  - The demo's shared stylesheet is `demo/houseki.css` (it was `game.css`), named for the package like the family's.
11
24
 
12
25
  ## [0.1.1] - 2026-10-05
@@ -36,6 +49,7 @@ All notable changes to this package are written here, newest first, in the form
36
49
  - An experimental Magnetic Blocks engine with bonded squares, magnetic floor schedules, a Floor Switch and impact drops.
37
50
  - Typed entry points, a generated API reference, an MIT licence and reproducible package checks.
38
51
 
39
- [Unreleased]: https://github.com/johnmorrisdotca/houseki/compare/v0.1.1...HEAD
52
+ [Unreleased]: https://github.com/johnmorrisdotca/houseki/compare/v0.2.0...HEAD
53
+ [0.2.0]: https://github.com/johnmorrisdotca/houseki/compare/v0.1.1...v0.2.0
40
54
  [0.1.1]: https://github.com/johnmorrisdotca/houseki/compare/v0.1.0...v0.1.1
41
55
  [0.1.0]: https://github.com/johnmorrisdotca/houseki/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <h1 align="center">Houseki <sub>宝石</sub></h1>
2
2
 
3
- <p align="center"><strong>Four original gem and stone puzzle games for JavaScript and TypeScript.</strong><br>
4
- Falling Triplets, Colour Chains, Stone Collapse and Gem Swap: each an immutable, seeded rules engine that runs anywhere, with graded challenges and lessons, saves that replay to the same board, and a keyboard, keypad and touch player in the demo. Magnetic Blocks, with bonded squares and a magnetic floor, is an experiment beside them. In English and Japanese. No dependencies.</p>
3
+ <p align="center"><strong>Five original gem and stone puzzle games for JavaScript and TypeScript.</strong><br>
4
+ Falling Triplets, Colour Chains, Stone Collapse and Gem Swap: each an immutable, seeded rules engine that runs anywhere, with graded challenges and lessons, saves that replay to the same board, and a keyboard, keypad and touch player in the demo. Magnetic Blocks adds bonded squares, a magnetic floor and its own campaign. Shizen and Arashi each add a separate Colour Chains campaign. In English and Japanese. No dependencies.</p>
5
5
 
6
6
  <p align="center">
7
7
  <a href="https://github.com/johnmorrisdotca/houseki/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/johnmorrisdotca/houseki/actions/workflows/ci.yml/badge.svg"></a>
@@ -61,14 +61,14 @@ Or straight from a page, with nothing to build:
61
61
 
62
62
  ## Features
63
63
 
64
- - **Four games**, each its own engine and its own entry point: [Falling Triplets](docs/design/01-FALLING-TRIPLETS.md), [Stone Collapse](docs/design/02-STONE-COLLAPSE.md), [Colour Chains](docs/design/03-COLOUR-CHAINS.md) and [Gem Swap](docs/design/04-GEM-SWAP.md).
65
- - **Graded campaigns and lessons**: 100 Falling Triplets, 50 Colour Chains, 100 Stone Collapse and 50 Gem Swap challenges, ordered by measured difficulty, each with a recorded winning play that the tests replay, and three guided lessons for each game. The boards are original, regenerated from a script, and a challenge keeps its identity if the order changes.
64
+ - **Five games**, each its own engine and its own entry point: [Falling Triplets](docs/design/01-FALLING-TRIPLETS.md), [Stone Collapse](docs/design/02-STONE-COLLAPSE.md), [Colour Chains](docs/design/03-COLOUR-CHAINS.md) [Gem Swap](docs/design/04-GEM-SWAP.md) and [Magnetic Blocks](docs/design/CAMPAIGN-EXTENSIONS.md).
65
+ - **Graded campaigns and lessons**: 100 Falling Triplets, 50 Colour Chains, 100 Stone Collapse, 50 Gem Swap, 50 Magnetic Blocks, 50 Shizen and 50 Arashi challenges (450 total), ordered by measured difficulty, each with a recorded winning play that the tests replay, and three guided lessons for each game. The boards are original, regenerated from a script, and a challenge keeps its identity if the order changes.
66
66
  - **Three modes**: Relaxed (no clock, plan each move), Arcade (a timer drops the pieces) and Daily (the same board for everyone on a date).
67
67
  - **Seeded and replayable**: a state is made from a seed and a list of actions, so a save is that list, and it decodes to exactly the board it came from.
68
68
  - **Configurable boards**: presets from compact to extra wide and deep, custom sizes within stated limits, up to six colours, and shaped boards (heart, star, hexagon) for the full-board games.
69
69
  - **Stored tools** for Gem Swap (Bomb, Row clear, Colour clear) and Stone Collapse (Bomb, Pick): select a tool and a stone, look at the highlighted effect, then confirm or cancel. A run that uses a tool is marked assisted. An optional earned Black Hole shrinks as it consumes nearby gems.
70
70
  - **Nature, optional**: Shizen magnetic stones with one bounded rebound, and Arashi weather, a rare or frequent earthquake that jumbles stones and lightning that removes exposed ones, on a seeded schedule.
71
- - **Magnetic Blocks, experimental**: bonded 2×2 squares, a floor that turns magnetic on a schedule, a one-use Floor Switch and impact drops. It has no graded campaign yet.
71
+ - **Magnetic Blocks**: bonded 2×2 squares, a floor that turns magnetic on a schedule, a one-use Floor Switch and impact drops. Its fifty challenges introduce Calm/Pull choices, planned floor schedules, the Floor Switch and impact drops.
72
72
  - **Accessible**: permanent colour symbols, keyboard and keypad control, an Animation setting that is on by default and a reduced-motion choice that is respected.
73
73
  - **Light and dark** that follow the device, in the demo's family look.
74
74
  - **English and Japanese** words for every lesson, challenge, button and line of the demo.
@@ -82,7 +82,9 @@ Or straight from a page, with nothing to build:
82
82
  | Stone Collapse | `stone-collapse` | select a connected group and confirm | two or more touching stones of one colour | 100 |
83
83
  | Colour Chains | `colour-chains` | rotate a falling pair | a group of four or more | 50 |
84
84
  | Gem Swap | `gem-swap` | swap two neighbours | a run of three or more, which can make special gems | 50 |
85
- | Magnetic Blocks (experimental) | `magnetic-blocks` | rotate a falling 2×2 block | bonded groups, on a floor that pulls | none yet |
85
+ | Magnetic Blocks | `magnetic-blocks` | rotate a falling 2×2 block | four connected gems, with Calm/Pull settling | 50 |
86
+ | Shizen | `colour-chains` | anticipate marked stones and rebound | four connected gems after attraction | 50 |
87
+ | Arashi | `colour-chains` | plan around earthquakes and lightning | four connected gems after weather | 50 |
86
88
 
87
89
  ## Use it in your project
88
90
 
@@ -105,6 +107,21 @@ Every function returns a new state and leaves the one it was given untouched. `l
105
107
 
106
108
  To run the whole demo, `pnpm install && pnpm site`, then serve `site/` with any static server. A page of the demo takes `?seed=anything` in its address to start the same board again, and `?lang=ja` for Japanese.
107
109
 
110
+ ### Additional campaigns
111
+
112
+ ![Magnetic Blocks campaign: fixed board settings, a marked-gem objective, progress bars and the shared cloth and piece appearance](docs/images/magnetic-blocks-campaign-desktop.png)
113
+
114
+ ```ts
115
+ import { createShizenLevel, createArashiLevel } from "@johnmorrisdotca/houseki/colour-chains";
116
+ import { createLevel } from "@johnmorrisdotca/houseki/magnetic-blocks";
117
+
118
+ const magneticStones = createShizenLevel(1);
119
+ const weather = createArashiLevel(1);
120
+ const bondedSquares = createLevel(1);
121
+ ```
122
+
123
+ Each campaign has fifty fixed puzzles ordered from easy to hard. Use its manifest for stable IDs, bilingual titles, objectives, difficulty marks and verified winning plans. Select Shizen or Arashi in the Colour Chains demo, or a challenge in Magnetic Blocks. A campaign uses a finite queue with no falling clock; the goal and progress appear beside the board. Fixed rules stay visible, and the result offers replay or the next challenge. See the [campaign contract](docs/design/CAMPAIGN-EXTENSIONS.md).
124
+
108
125
  ## API
109
126
 
110
127
  Every export of every entry point is in the [API reference](https://johnmorrisdotca.github.io/houseki/api.html), made from the source when the demo is built, with each signature and doc comment. A test fails when a public export has no doc comment.
@@ -135,7 +152,7 @@ The demo is in English and Japanese: every lesson and challenge title, button, s
135
152
 
136
153
  ## Roadmap
137
154
 
138
- Physical-device feel testing, a fluent Japanese review and human difficulty review of the campaigns are next, and a graded campaign for Magnetic Blocks. [docs/COVERAGE.md](docs/COVERAGE.md) says what is verified by tests and what is not. Nothing here is promised for a date.
155
+ Physical-device feel testing, a fluent Japanese review and human difficulty review of the campaigns are next, and new techniques for later campaigns. [docs/COVERAGE.md](docs/COVERAGE.md) says what is verified by tests and what is not. Nothing here is promised for a date.
139
156
 
140
157
  ## Architecture
141
158
 
@@ -178,6 +195,9 @@ src/
178
195
  ├── gem-swap.ts
179
196
  ├── index.ts
180
197
  ├── magnetic-blocks/
198
+ │ ├── content-data.ts
199
+ │ ├── content-types.ts
200
+ │ ├── content.ts
181
201
  │ ├── engine.ts
182
202
  │ ├── persistence.ts
183
203
  │ ├── physics.ts
@@ -214,7 +234,7 @@ Houseki (宝石) is Japanese for a gem or precious stone: the pieces of these ga
214
234
 
215
235
  ## Where it comes from
216
236
 
217
- Match games are an old family and their rules are common property. These four are original: the rules are written out in the design pack in our own words, every board is generated and checked by the repository's own scripts, and the pictures are drawn in code. No art, sound or level comes from another game.
237
+ Match games are an old family and their rules are common property. These five are original: the rules are written out in the design pack in our own words, every board is generated and checked by the repository's own scripts, and the pictures are drawn in code. No art, sound or level comes from another game.
218
238
 
219
239
  ### The family
220
240
 
@@ -61,7 +61,7 @@ function solveWitness(start, witness) {
61
61
  export function createChallenge(options) {
62
62
  if (!options || typeof options !== 'object' || Array.isArray(options))
63
63
  throw new StoneChainsOptionsError('invalid-challenge', 'Challenge options must be an object');
64
- const allowed = ['id', 'width', 'height', 'colourCount', 'seed', 'board', 'queue', 'goal', 'witness'];
64
+ const allowed = ['id', 'width', 'height', 'colourCount', 'seed', 'board', 'queue', 'magneticQueue', 'nature', 'weather', 'goal', 'witness'];
65
65
  if (Object.keys(options).some(key => !allowed.includes(key)))
66
66
  throw new StoneChainsOptionsError('unknown-challenge-option', 'Challenge options contain unsupported fields');
67
67
  if (typeof options.id !== 'string' || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(options.id))
@@ -75,6 +75,14 @@ export function createChallenge(options) {
75
75
  const seed = options.seed ?? `challenge:${options.id}`;
76
76
  if (typeof seed !== 'string' || seed.length < 1 || seed.length > 200)
77
77
  throw new StoneChainsOptionsError('invalid-seed', 'Seed must contain 1–200 characters');
78
+ if (options.nature !== undefined && typeof options.nature !== 'boolean')
79
+ throw new StoneChainsOptionsError('invalid-nature-option', 'nature must be a boolean');
80
+ if (options.weather !== undefined && options.weather !== 'frequent' && options.weather !== 'rare')
81
+ throw new StoneChainsOptionsError('invalid-weather-option', 'weather must be frequent or rare');
82
+ if (options.weather !== undefined && options.nature !== true)
83
+ throw new StoneChainsOptionsError('weather-requires-nature', 'Arashi weather requires nature:true');
84
+ if (options.magneticQueue !== undefined && options.nature !== true)
85
+ throw new StoneChainsOptionsError('magnetic-queue-requires-nature', 'Magnetic queue flags require nature:true');
78
86
  if (!Array.isArray(options.board) || options.board.length !== width * height)
79
87
  throw new StoneChainsOptionsError('invalid-challenge-board', 'Challenge board must contain width × height visible cells');
80
88
  if (!Array.isArray(options.queue) || options.queue.length < 2 || options.queue.length > 12)
@@ -84,10 +92,10 @@ export function createChallenge(options) {
84
92
  const board = options.board.map((gem, index) => {
85
93
  if (gem === null)
86
94
  return null;
87
- if (!gem || typeof gem !== 'object' || Array.isArray(gem) || Object.keys(gem).some(key => key !== 'id' && key !== 'colour') || !Number.isSafeInteger(gem.id) || gem.id < 1 || gem.id > 0x7fff_ffff || ids.has(gem.id) || !allowedColours.includes(gem.colour))
95
+ if (!gem || typeof gem !== 'object' || Array.isArray(gem) || Object.keys(gem).some(key => key !== 'id' && key !== 'colour' && key !== 'magnetic') || !Number.isSafeInteger(gem.id) || gem.id < 1 || gem.id > 0x7fff_ffff || ids.has(gem.id) || !allowedColours.includes(gem.colour) || gem.magnetic !== undefined && (gem.magnetic !== true || options.nature !== true))
88
96
  throw new StoneChainsOptionsError('invalid-challenge-gem', `Invalid challenge gem at cell ${index}`);
89
97
  ids.add(gem.id);
90
- return Object.freeze({ id: gem.id, colour: gem.colour });
98
+ return Object.freeze({ id: gem.id, colour: gem.colour, ...(gem.magnetic ? { magnetic: true } : {}) });
91
99
  });
92
100
  for (let x = 0; x < width; x++) {
93
101
  let gap = false;
@@ -103,6 +111,12 @@ export function createChallenge(options) {
103
111
  throw new StoneChainsOptionsError('invalid-challenge-pair', `Invalid queue pair ${index + 1}`);
104
112
  return Object.freeze([pair[0], pair[1]]);
105
113
  });
114
+ let magneticQueue;
115
+ if (options.magneticQueue !== undefined) {
116
+ if (!Array.isArray(options.magneticQueue) || options.magneticQueue.length !== queue.length || options.magneticQueue.some(pair => !Array.isArray(pair) || pair.length !== 2 || pair.some(flag => typeof flag !== 'boolean')))
117
+ throw new StoneChainsOptionsError('invalid-magnetic-queue', 'Magnetic queue must contain one pair of boolean flags per finite queue pair');
118
+ magneticQueue = Object.freeze(options.magneticQueue.map(pair => Object.freeze([pair[0], pair[1]])));
119
+ }
106
120
  const goal = validateChallengeGoal(options.goal, ids);
107
121
  if (!Array.isArray(options.witness) || options.witness.length < 1 || options.witness.length > queue.length)
108
122
  throw new StoneChainsOptionsError('invalid-witness', 'A challenge requires a complete witness of 1–12 pair placements');
@@ -111,7 +125,7 @@ export function createChallenge(options) {
111
125
  throw new StoneChainsOptionsError('invalid-witness-step', `Invalid witness placement ${index + 1}`);
112
126
  return Object.freeze({ pivotX: step.pivotX, orientation: step.orientation });
113
127
  });
114
- const settings = Object.freeze({ mode: 'challenge', width, height, colourCount, seed, challengeId: options.id, goal, initialBoard: Object.freeze(board), queue: Object.freeze(queue), witness: Object.freeze(witness) });
128
+ const settings = Object.freeze({ mode: 'challenge', width, height, colourCount, seed, challengeId: options.id, goal, initialBoard: Object.freeze(board), queue: Object.freeze(queue), ...(magneticQueue ? { magneticQueue } : {}), ...(options.nature ? { nature: true } : {}), ...(options.weather ? { weather: options.weather } : {}), witness: Object.freeze(witness) });
115
129
  const state = initialChallengeState(settings, settings.initialBoard, settings.queue);
116
130
  const solved = solveWitness(state, witness);
117
131
  if (solved.phase !== 'won' || stable(solved.settings) !== stable(settings))