@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are written here, newest first, in the form of [Keep a Changelog](https://keepachangelog.com), and this package follows [Semantic Versioning](https://semver.org).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
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.
|
|
10
|
+
- The demo's shared stylesheet is `demo/houseki.css` (it was `game.css`), named for the package like the family's.
|
|
11
|
+
|
|
12
|
+
## [0.1.1] - 2026-10-05
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- The package is brought to the family's standard: the same checks (`pnpm check` is lint, types and tests), tests run by Vitest, a CI workflow on Node 22 and 24, a release workflow that publishes by trusted publishing, and a Pages workflow, all on current action versions; `pnpm test:package` packs, installs and imports every entry point.
|
|
17
|
+
- `package.json` is made like the family's: a description short enough for npm, homepage, author, `module` and `sideEffects`.
|
|
18
|
+
- The README follows the family's order and lists all twenty-four packages of the family, made from the shared template.
|
|
19
|
+
- The demo's four pages are made under the family's header, footer, language chooser, Help switch and table cloths, with a description, an icon and a title in the family's form, and an API reference in the same header. The page's own Theme button is gone: light and dark follow the device, as in the other demos.
|
|
20
|
+
- The demo's tests are Playwright tests that run at a phone's width and a desk's, in Chromium, and the pages themselves in WebKit too.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- A new game on a page served over plain http, from an address other than localhost, no longer fails: the seed falls back to random bytes where `crypto.randomUUID` does not exist.
|
|
25
|
+
- The API reference no longer widens the page on a phone for a doc comment that names a long entry path.
|
|
26
|
+
- Unused imports and variables are removed from the source; no public export, signature or behaviour changes.
|
|
27
|
+
|
|
28
|
+
## [0.1.0] - 2026-10-05
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Original Falling Triplets, Colour Chains, Stone Collapse and Gem Swap engines with immutable state, seeded play, validated settings and canonical saves.
|
|
33
|
+
- 300 graded challenges and three bilingual lessons for each game.
|
|
34
|
+
- Family-style keyboard, keypad and touch demos, configurable boards, stored tools, goal progress and shared animation preferences.
|
|
35
|
+
- Optional Black Hole powers, Shizen magnetic stones with bounded rebound, and Arashi jumble and lightning schedules.
|
|
36
|
+
- An experimental Magnetic Blocks engine with bonded squares, magnetic floor schedules, a Floor Switch and impact drops.
|
|
37
|
+
- Typed entry points, a generated API reference, an MIT licence and reproducible package checks.
|
|
38
|
+
|
|
39
|
+
[Unreleased]: https://github.com/johnmorrisdotca/houseki/compare/v0.1.1...HEAD
|
|
40
|
+
[0.1.1]: https://github.com/johnmorrisdotca/houseki/compare/v0.1.0...v0.1.1
|
|
41
|
+
[0.1.0]: https://github.com/johnmorrisdotca/houseki/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Morris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,273 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">Houseki <sub>宝石</sub></h1>
|
|
2
2
|
|
|
3
|
-
|
|
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>
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
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>
|
|
8
|
+
<a href="https://www.npmjs.com/package/@johnmorrisdotca/houseki"><img alt="npm" src="https://img.shields.io/npm/v/@johnmorrisdotca/houseki?color=2f5d4a"></a>
|
|
9
|
+
<a href="./LICENSE"><img alt="MIT licence" src="https://img.shields.io/badge/licence-MIT-2f5d4a"></a>
|
|
10
|
+
<img alt="No dependencies" src="https://img.shields.io/badge/dependencies-0-2f5d4a">
|
|
11
|
+
<img alt="TypeScript" src="https://img.shields.io/badge/types-TypeScript-2f5d4a">
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center"><a href="https://johnmorrisdotca.github.io/houseki/"><strong>Play a game →</strong></a> · <a href="https://johnmorrisdotca.github.io/houseki/api.html">API reference</a></p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img src="docs/images/gem-swap-desktop.png" alt="Gem Swap on a full board in the demo: a grid of red, blue, green, gold, purple and teal gems with permanent symbols on them, the tool tray beside it with Bomb, Row clear and Colour clear, and the family's header above" width="620">
|
|
18
|
+
<img src="docs/images/tool-tray-phone.png" alt="The same tool tray on a phone, under the board" width="200">
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
Houseki is a set of match-style puzzle games made to be dropped into a page or a server. Every game is a set of plain functions on plain data: a state in, a new state out, nothing read from the page, the clock or the network. The demo's players draw those states, and a person can play them with the keyboard, a keypad or a finger.
|
|
22
|
+
|
|
23
|
+
## In 30 seconds
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm install @johnmorrisdotca/houseki
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createGame, applyAction, advanceTicks } from "@johnmorrisdotca/houseki/falling-triplets";
|
|
31
|
+
|
|
32
|
+
let game = createGame({ mode: "relaxed", seed: "first-game" });
|
|
33
|
+
game = applyAction(game, { kind: "cycle-forward" }).state;
|
|
34
|
+
game = applyAction(game, { kind: "place" }).state;
|
|
35
|
+
game = advanceTicks(game, 22).state;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Every engine is a path of its own, and the main entry groups them by game:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { colourChains } from "@johnmorrisdotca/houseki";
|
|
42
|
+
|
|
43
|
+
const firstChallenge = colourChains.createLevel(1);
|
|
44
|
+
const goal = firstChallenge.settings.goal;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or straight from a page, with nothing to build:
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<script type="module">
|
|
51
|
+
import { createGame } from "https://cdn.jsdelivr.net/npm/@johnmorrisdotca/houseki@0/dist/gem-swap.js";
|
|
52
|
+
const game = createGame({ seed: "hello" });
|
|
53
|
+
</script>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Who it is for
|
|
57
|
+
|
|
58
|
+
- **Game and puzzle sites** that want match games with the rules already right: challenges everybody plays alike, a win that is witnessed, and saves that cannot drift.
|
|
59
|
+
- **Anyone who needs the rules without a screen**: a server that checks a finished game, a test that plays a campaign, a bot, a solver. Every engine is deterministic from its seed.
|
|
60
|
+
- **Teachers and parents**: nothing is timed against you in Relaxed play, every colour has its own permanent symbol, and every word is plain.
|
|
61
|
+
|
|
62
|
+
## Features
|
|
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.
|
|
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
|
+
- **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
|
+
- **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
|
+
- **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
|
+
- **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.
|
|
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
|
+
- **Light and dark** that follow the device, in the demo's family look.
|
|
74
|
+
- **English and Japanese** words for every lesson, challenge, button and line of the demo.
|
|
75
|
+
- **Zero dependencies**, for the package and for the demo.
|
|
76
|
+
|
|
77
|
+
### The games
|
|
78
|
+
|
|
79
|
+
| Game | Entry | You | A match is | Challenges |
|
|
80
|
+
| --- | --- | --- | --- | --- |
|
|
81
|
+
| Falling Triplets | `falling-triplets` | cycle a falling vertical triplet and place it | a line of three, across, down or diagonal | 100 |
|
|
82
|
+
| Stone Collapse | `stone-collapse` | select a connected group and confirm | two or more touching stones of one colour | 100 |
|
|
83
|
+
| Colour Chains | `colour-chains` | rotate a falling pair | a group of four or more | 50 |
|
|
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 |
|
|
86
|
+
|
|
87
|
+
## Use it in your project
|
|
88
|
+
|
|
89
|
+
Import a game directly from `@johnmorrisdotca/houseki/falling-triplets`, `@johnmorrisdotca/houseki/stone-collapse`, `@johnmorrisdotca/houseki/colour-chains`, `@johnmorrisdotca/houseki/gem-swap` or `@johnmorrisdotca/houseki/magnetic-blocks`, and the deterministic attraction, rebound and environment utilities from `@johnmorrisdotca/houseki/nature`. The main entry, `@johnmorrisdotca/houseki`, groups the same names as the namespaces `fallingTriplets`, `stoneCollapse`, `colourChains`, `gemSwap`, `nature` and `magneticBlocks`. The engines do not touch the DOM, storage or the network.
|
|
90
|
+
|
|
91
|
+
Every engine works the same way:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { createGame, createLevel, applyAction, advanceTicks, legalActions, encodeGame, decodeGame } from "@johnmorrisdotca/houseki/colour-chains";
|
|
95
|
+
|
|
96
|
+
const level = createLevel(1); // a graded challenge; createGame({ seed }) is a free game
|
|
97
|
+
const move = applyAction(level, { kind: "rotate-clockwise" });
|
|
98
|
+
move.accepted; // false when the action is not allowed
|
|
99
|
+
move.events; // what happened, in order, for a screen to animate
|
|
100
|
+
const later = advanceTicks(move.state, 30).state; // time is a count of ticks, never the clock
|
|
101
|
+
const saved = encodeGame(later); // a string; decodeGame(saved) is the same game again
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Every function returns a new state and leaves the one it was given untouched. `legalActions(state)` lists what may be done now, so a bot or a test needs no knowledge of the rules. The two full-board games, Gem Swap and Stone Collapse, also have an undo and a hint (`undoGame` and `hintGame` in Gem Swap, `undo` and `requestHint` in Stone Collapse) and a restart; their stored tools are actions of their own.
|
|
105
|
+
|
|
106
|
+
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
|
+
|
|
108
|
+
## API
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
|
|
112
|
+
## Theming
|
|
113
|
+
|
|
114
|
+
The engines draw nothing. The demo's players take their colours from the family's stylesheet, `demo/family.css`: custom properties for the page, ink, accent and the felt the board sits on, which follow `prefers-color-scheme`, and five table cloths (green, blue, red, black and wood) chosen in the header. The boards add their own `--board`, `--frame` and `--gem-edge` in `demo/houseki.css`, and a Board material choice (ivory, slate or wood). Gems are told apart by their symbol as well as their colour, so no choice of cloth hides a colour.
|
|
115
|
+
|
|
116
|
+
## Limits
|
|
117
|
+
|
|
118
|
+
| What | Limit |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| Colours | 4 to 6 |
|
|
121
|
+
| Colour Chains | 5 to 16 columns and 12 to 32 visible rows |
|
|
122
|
+
| Magnetic Blocks | 4 to 16 columns and 4 to 32 rows; at most 200 pieces in the demo |
|
|
123
|
+
| Stone Collapse | presets from 6×8 to 16×10 and 8×32 deep; shaped boards by mask |
|
|
124
|
+
| Daily mode | the standard board and colour count only, from a date |
|
|
125
|
+
| Saves | a replay of the actions taken, checked when it is decoded; a save that does not match its own record is refused |
|
|
126
|
+
| Tools | capped inventory, earned by ordinary clears, and not offered in Daily or in authored challenges |
|
|
127
|
+
|
|
128
|
+
## Browser support
|
|
129
|
+
|
|
130
|
+
The engines run wherever JavaScript does: Node 22 or later, and current Chrome, Edge, Firefox and Safari. The demo's tests run in Chromium and WebKit, at a phone's width and a desk's.
|
|
131
|
+
|
|
132
|
+
## Languages
|
|
133
|
+
|
|
134
|
+
The demo is in English and Japanese: every lesson and challenge title, button, status line and the family's header and footer. The Japanese has not yet been read by a native reader: corrections are welcome as a *Fix a translation* issue.
|
|
135
|
+
|
|
136
|
+
## Roadmap
|
|
137
|
+
|
|
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.
|
|
139
|
+
|
|
140
|
+
## Architecture
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
src/
|
|
144
|
+
├── colour-chains/
|
|
145
|
+
│ ├── challenge.ts
|
|
146
|
+
│ ├── content-data.ts
|
|
147
|
+
│ ├── content-types.ts
|
|
148
|
+
│ ├── content.ts
|
|
149
|
+
│ ├── engine.ts
|
|
150
|
+
│ ├── input.ts
|
|
151
|
+
│ ├── match.ts
|
|
152
|
+
│ ├── persistence.ts
|
|
153
|
+
│ ├── random.ts
|
|
154
|
+
│ ├── types.ts
|
|
155
|
+
│ └── validation.ts
|
|
156
|
+
├── colour-chains.ts
|
|
157
|
+
├── falling-triplets/
|
|
158
|
+
│ ├── content-data.ts
|
|
159
|
+
│ ├── content-types.ts
|
|
160
|
+
│ ├── content.ts
|
|
161
|
+
│ ├── engine.ts
|
|
162
|
+
│ ├── input.ts
|
|
163
|
+
│ ├── match.ts
|
|
164
|
+
│ ├── random.ts
|
|
165
|
+
│ └── types.ts
|
|
166
|
+
├── falling-triplets.ts
|
|
167
|
+
├── gem-swap/
|
|
168
|
+
│ ├── black-hole.ts
|
|
169
|
+
│ ├── board.ts
|
|
170
|
+
│ ├── content.ts
|
|
171
|
+
│ ├── engine.ts
|
|
172
|
+
│ ├── persistence.ts
|
|
173
|
+
│ ├── practice.ts
|
|
174
|
+
│ ├── random.ts
|
|
175
|
+
│ ├── specials.ts
|
|
176
|
+
│ ├── tools.ts
|
|
177
|
+
│ └── types.ts
|
|
178
|
+
├── gem-swap.ts
|
|
179
|
+
├── index.ts
|
|
180
|
+
├── magnetic-blocks/
|
|
181
|
+
│ ├── engine.ts
|
|
182
|
+
│ ├── persistence.ts
|
|
183
|
+
│ ├── physics.ts
|
|
184
|
+
│ ├── random.ts
|
|
185
|
+
│ ├── types.ts
|
|
186
|
+
│ └── validation.ts
|
|
187
|
+
├── magnetic-blocks.ts
|
|
188
|
+
├── nature/
|
|
189
|
+
│ ├── attraction.ts
|
|
190
|
+
│ ├── environment.ts
|
|
191
|
+
│ ├── marking.ts
|
|
192
|
+
│ ├── random.ts
|
|
193
|
+
│ ├── rebound.ts
|
|
194
|
+
│ ├── state.ts
|
|
195
|
+
│ ├── types.ts
|
|
196
|
+
│ └── validation.ts
|
|
197
|
+
├── nature.ts
|
|
198
|
+
├── stone-collapse/
|
|
199
|
+
│ ├── content-data.ts
|
|
200
|
+
│ ├── content-types.ts
|
|
201
|
+
│ ├── content.ts
|
|
202
|
+
│ ├── engine.ts
|
|
203
|
+
│ ├── persistence.ts
|
|
204
|
+
│ ├── types.ts
|
|
205
|
+
│ └── validation.ts
|
|
206
|
+
└── stone-collapse.ts
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The [design pack](docs/design/README.md) specifies the rules, controls, materials, accessibility, level generation, grading, persistence and acceptance checks of every game; [level generation](docs/design/LEVEL-GENERATION.md) follows a deliberate complete count: generate and prove original boards, remove duplicates, grade the decisions a player faces, then sort and number from easy to hard.
|
|
210
|
+
|
|
211
|
+
## The name
|
|
212
|
+
|
|
213
|
+
Houseki (宝石) is Japanese for a gem or precious stone: the pieces of these games are cut and polished, and the games are small.
|
|
214
|
+
|
|
215
|
+
## Where it comes from
|
|
216
|
+
|
|
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.
|
|
218
|
+
|
|
219
|
+
### The family
|
|
220
|
+
|
|
221
|
+
<!-- family:start (made by scripts/family-readme.mjs from scripts/family-template.mjs; change those, not this) -->
|
|
222
|
+
Houseki is one of twenty-four packages, each made for the same site, each at
|
|
223
|
+
[github.com/johnmorrisdotca](https://github.com/johnmorrisdotca). The code of every one is MIT.
|
|
224
|
+
|
|
225
|
+
- [Korokoro](https://github.com/johnmorrisdotca/korokoro) (コロコロ): dice, with notation, exact odds, real sounds and the dice of many games. [Demo](https://johnmorrisdotca.github.io/korokoro/).
|
|
226
|
+
- [Kyuubu](https://github.com/johnmorrisdotca/kyuubu) (キューブ): a turning cube for the browser, 2×2 to 7×7, with record solves to replay. [Demo](https://johnmorrisdotca.github.io/kyuubu/).
|
|
227
|
+
- [Hitotsu](https://github.com/johnmorrisdotca/hitotsu) (一つ): a colour-card shedding game for two to eight, with the house rules people play. [Demo](https://johnmorrisdotca.github.io/hitotsu/).
|
|
228
|
+
- [Toranpu](https://github.com/johnmorrisdotca/toranpu) (トランプ): a deck of playing cards, card games with computer players, and solitaires. [Demo](https://johnmorrisdotca.github.io/toranpu/).
|
|
229
|
+
- [Tane](https://github.com/johnmorrisdotca/tane) (種): seeded random numbers and daily seeds, the same in every browser and on every server. [Demo](https://johnmorrisdotca.github.io/tane/).
|
|
230
|
+
- [Narabe](https://github.com/johnmorrisdotca/narabe) (並べ): one rules engine for abstract board games, from gomoku and Reversi to Go and checkers. [Demo](https://johnmorrisdotca.github.io/narabe/).
|
|
231
|
+
- [Tenka](https://github.com/johnmorrisdotca/tenka) (天下): world conquest for two to six, on a map of the real world. [Demo](https://johnmorrisdotca.github.io/tenka/).
|
|
232
|
+
- [Kumimoji](https://github.com/johnmorrisdotca/kumimoji) (組み文字): a crossword tile race, in English and Japanese kana. [Demo](https://johnmorrisdotca.github.io/kumimoji/).
|
|
233
|
+
- [Tsunagi](https://github.com/johnmorrisdotca/tsunagi) (繋ぎ): a line-joining logic puzzle whose every level has exactly one answer. [Demo](https://johnmorrisdotca.github.io/tsunagi/).
|
|
234
|
+
- [Jarajara](https://github.com/johnmorrisdotca/jarajara) (ジャラジャラ): mahjong tiles drawn as SVG, stacked layouts, and the matching solitaire Awase. [Demo](https://johnmorrisdotca.github.io/jarajara/).
|
|
235
|
+
- [Suido](https://github.com/johnmorrisdotca/suido) (水道): a pipe puzzle: turn the pieces until the water reaches every drain. [Demo](https://johnmorrisdotca.github.io/suido/).
|
|
236
|
+
- [Domino](https://github.com/johnmorrisdotca/domino) (ドミノ): dominoes and Mexican Train. [Demo](https://johnmorrisdotca.github.io/domino/).
|
|
237
|
+
- [Kotoba](https://github.com/johnmorrisdotca/kotoba) (言葉): word lists and word-game rules in English, French, German and Japanese. [Demo](https://johnmorrisdotca.github.io/kotoba/).
|
|
238
|
+
- [Sugoroku](https://github.com/johnmorrisdotca/sugoroku) (双六): backgammon and its variants, with the doubling cube and match play. [Demo](https://johnmorrisdotca.github.io/sugoroku/).
|
|
239
|
+
- [Kazu](https://github.com/johnmorrisdotca/kazu) (数): grid number puzzles: Sudoku and its variants, Futoshiki and Skyscrapers. [Demo](https://johnmorrisdotca.github.io/kazu/).
|
|
240
|
+
- [Meikyuu](https://github.com/johnmorrisdotca/meikyuu) (迷宮): mazes on squares, hexagons, triangles and circles, made from a seed and drawn through with a finger or the mouse. [Demo](https://johnmorrisdotca.github.io/meikyuu/).
|
|
241
|
+
- [Hikidashi](https://github.com/johnmorrisdotca/hikidashi) (引き出し): a drawer of small Japanese text tools: era dates, kanji numerals, readings and sentence difficulty. [Demo](https://johnmorrisdotca.github.io/hikidashi/).
|
|
242
|
+
- [Chizu](https://github.com/johnmorrisdotca/chizu) (地図): maps of the world and of countries' regions, in English and Japanese, with a quiz and callouts. [Demo](https://johnmorrisdotca.github.io/chizu/).
|
|
243
|
+
- [Bushu](https://github.com/johnmorrisdotca/bushu) (部首): find a kanji by the parts it is made of. [Demo](https://johnmorrisdotca.github.io/bushu/).
|
|
244
|
+
- [Tobiishi](https://github.com/johnmorrisdotca/tobiishi) (飛び石): peg solitaire with nine boards and seeded solvable challenges. [Demo](https://johnmorrisdotca.github.io/tobiishi/).
|
|
245
|
+
- [Jirai](https://github.com/johnmorrisdotca/jirai) (地雷): minesweeper on shaped grids with verified no-guess boards. [Demo](https://johnmorrisdotca.github.io/jirai/).
|
|
246
|
+
- [Gunjin](https://github.com/johnmorrisdotca/gunjin) (軍人): five hidden-rank strategy games with pass-the-device play. [Demo](https://johnmorrisdotca.github.io/gunjin/).
|
|
247
|
+
- [Karakuri](https://github.com/johnmorrisdotca/karakuri) (からくり): eight hyper-casual puzzle games, some of them physics: draw a shield, pull pins, cut ropes, slide blocks, pour tubes. [Demo](https://johnmorrisdotca.github.io/karakuri/).
|
|
248
|
+
- [Houseki](https://github.com/johnmorrisdotca/houseki) (宝石): gem and stone matching puzzles: falling triplets, stone collapse, colour chains and gem swap. [Demo](https://johnmorrisdotca.github.io/houseki/).
|
|
249
|
+
|
|
250
|
+
**This package is Houseki.** The demos of all twenty-four share one header and footer, so each links the rest.
|
|
251
|
+
<!-- family:end -->
|
|
252
|
+
|
|
253
|
+
## Development
|
|
254
|
+
|
|
255
|
+
```sh
|
|
256
|
+
pnpm install
|
|
257
|
+
pnpm check # lint, types and tests: every game's rules, every campaign's recorded wins
|
|
258
|
+
pnpm test:package # pack it as npm does, install it in an empty project, import every entry
|
|
259
|
+
pnpm test:demo # build the demo and play it in a real browser, at a phone's width and a desk's
|
|
260
|
+
pnpm site # build the demo and the API reference into site/
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Contributing
|
|
264
|
+
|
|
265
|
+
Ideas, bug reports and pull requests are welcome in the [issues](https://github.com/johnmorrisdotca/houseki/issues). See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
266
|
+
|
|
267
|
+
## Changes
|
|
268
|
+
|
|
269
|
+
Every release is written up in [CHANGELOG.md](./CHANGELOG.md).
|
|
270
|
+
|
|
271
|
+
## Licence
|
|
272
|
+
|
|
273
|
+
[MIT](./LICENSE) © John Morris. The code, the boards and the words are all the package's own. Report vulnerabilities through [SECURITY.md](./SECURITY.md).
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { applyAction, advanceTicks, initialChallengeState } from './engine.js';
|
|
2
|
+
import { StoneChainsOptionsError, validateChallengeGoal, validateSize } from './validation.js';
|
|
3
|
+
const COLOUR_NAMES = ['red', 'blue', 'green', 'gold', 'purple', 'teal'];
|
|
4
|
+
const ORIENTATIONS = ['up', 'right', 'down', 'left'];
|
|
5
|
+
function stable(value) {
|
|
6
|
+
if (Array.isArray(value))
|
|
7
|
+
return `[${value.map(stable).join(',')}]`;
|
|
8
|
+
if (value && typeof value === 'object')
|
|
9
|
+
return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`).join(',')}}`;
|
|
10
|
+
return JSON.stringify(value) ?? 'null';
|
|
11
|
+
}
|
|
12
|
+
function solveWitness(start, witness) {
|
|
13
|
+
let state = start;
|
|
14
|
+
let work = 0;
|
|
15
|
+
const act = (kind) => {
|
|
16
|
+
const result = applyAction(state, { kind });
|
|
17
|
+
if (!result.accepted)
|
|
18
|
+
throw new StoneChainsOptionsError('invalid-witness', `Witness action ${kind} was rejected: ${result.reason}`);
|
|
19
|
+
state = result.state;
|
|
20
|
+
if (++work > 100_000)
|
|
21
|
+
throw new StoneChainsOptionsError('witness-work-limit', 'Challenge witness exceeds bounded verification work');
|
|
22
|
+
};
|
|
23
|
+
for (let stepIndex = 0; stepIndex < witness.length; stepIndex++) {
|
|
24
|
+
const step = witness[stepIndex];
|
|
25
|
+
if (state.phase !== 'falling' || !state.active)
|
|
26
|
+
break;
|
|
27
|
+
const rotateToward = (target) => {
|
|
28
|
+
for (let count = 0; state.active.orientation !== target && count < 4; count++)
|
|
29
|
+
act('rotate-clockwise');
|
|
30
|
+
if (state.active.orientation !== target)
|
|
31
|
+
throw new StoneChainsOptionsError('invalid-witness', `Witness step ${stepIndex + 1} cannot reach its orientation`);
|
|
32
|
+
};
|
|
33
|
+
for (let count = 0; state.active.pivot.x !== step.pivotX && count < 12; count++)
|
|
34
|
+
act(state.active.pivot.x < step.pivotX ? 'right' : 'left');
|
|
35
|
+
if (state.active.pivot.x !== step.pivotX)
|
|
36
|
+
throw new StoneChainsOptionsError('invalid-witness', `Witness step ${stepIndex + 1} cannot reach its pivot column`);
|
|
37
|
+
rotateToward(step.orientation);
|
|
38
|
+
for (let count = 0; state.active.pivot.x !== step.pivotX && count < 12; count++)
|
|
39
|
+
act(state.active.pivot.x < step.pivotX ? 'right' : 'left');
|
|
40
|
+
if (state.active.pivot.x !== step.pivotX)
|
|
41
|
+
throw new StoneChainsOptionsError('invalid-witness', `Witness step ${stepIndex + 1} cannot finish at its pivot column`);
|
|
42
|
+
act('hard-drop');
|
|
43
|
+
while (['clear-mark', 'clear-remove', 'gravity'].includes(state.phase) && work < 100_000) {
|
|
44
|
+
const result = advanceTicks(state, 3600);
|
|
45
|
+
state = result.state;
|
|
46
|
+
work += 3600;
|
|
47
|
+
}
|
|
48
|
+
if (state.phase === 'lost' || state.phase === 'finished')
|
|
49
|
+
throw new StoneChainsOptionsError('invalid-witness', `Witness ends before its goal after pair ${stepIndex + 1}`);
|
|
50
|
+
if (state.phase === 'won') {
|
|
51
|
+
if (stepIndex !== witness.length - 1)
|
|
52
|
+
throw new StoneChainsOptionsError('witness-after-goal', 'Witness must stop at the first completed goal');
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (state.phase !== 'won')
|
|
57
|
+
throw new StoneChainsOptionsError('witness-does-not-win', 'Witness does not complete the challenge goal within its finite queue');
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
/** Validates and creates a finite witnessed challenge. Hints use only the supplied verified pair placements. */
|
|
61
|
+
export function createChallenge(options) {
|
|
62
|
+
if (!options || typeof options !== 'object' || Array.isArray(options))
|
|
63
|
+
throw new StoneChainsOptionsError('invalid-challenge', 'Challenge options must be an object');
|
|
64
|
+
const allowed = ['id', 'width', 'height', 'colourCount', 'seed', 'board', 'queue', 'goal', 'witness'];
|
|
65
|
+
if (Object.keys(options).some(key => !allowed.includes(key)))
|
|
66
|
+
throw new StoneChainsOptionsError('unknown-challenge-option', 'Challenge options contain unsupported fields');
|
|
67
|
+
if (typeof options.id !== 'string' || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(options.id))
|
|
68
|
+
throw new StoneChainsOptionsError('invalid-challenge-id', 'Challenge ID must be stable lowercase text');
|
|
69
|
+
const width = options.width ?? 6;
|
|
70
|
+
const height = options.height ?? 12;
|
|
71
|
+
validateSize(width, height);
|
|
72
|
+
const colourCount = options.colourCount ?? 4;
|
|
73
|
+
if (colourCount !== 4 && colourCount !== 5 && colourCount !== 6)
|
|
74
|
+
throw new StoneChainsOptionsError('invalid-colour-count', 'Challenge colour count must be 4, 5 or 6');
|
|
75
|
+
const seed = options.seed ?? `challenge:${options.id}`;
|
|
76
|
+
if (typeof seed !== 'string' || seed.length < 1 || seed.length > 200)
|
|
77
|
+
throw new StoneChainsOptionsError('invalid-seed', 'Seed must contain 1–200 characters');
|
|
78
|
+
if (!Array.isArray(options.board) || options.board.length !== width * height)
|
|
79
|
+
throw new StoneChainsOptionsError('invalid-challenge-board', 'Challenge board must contain width × height visible cells');
|
|
80
|
+
if (!Array.isArray(options.queue) || options.queue.length < 2 || options.queue.length > 12)
|
|
81
|
+
throw new StoneChainsOptionsError('invalid-challenge-queue', 'Challenge queue must contain 2–12 pairs');
|
|
82
|
+
const allowedColours = COLOUR_NAMES.slice(0, colourCount);
|
|
83
|
+
const ids = new Set();
|
|
84
|
+
const board = options.board.map((gem, index) => {
|
|
85
|
+
if (gem === null)
|
|
86
|
+
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))
|
|
88
|
+
throw new StoneChainsOptionsError('invalid-challenge-gem', `Invalid challenge gem at cell ${index}`);
|
|
89
|
+
ids.add(gem.id);
|
|
90
|
+
return Object.freeze({ id: gem.id, colour: gem.colour });
|
|
91
|
+
});
|
|
92
|
+
for (let x = 0; x < width; x++) {
|
|
93
|
+
let gap = false;
|
|
94
|
+
for (let y = height - 1; y >= 0; y--) {
|
|
95
|
+
if (board[y * width + x] === null)
|
|
96
|
+
gap = true;
|
|
97
|
+
else if (gap)
|
|
98
|
+
throw new StoneChainsOptionsError('unstable-challenge-board', `Starting column ${x} is not gravity-stable`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const queue = options.queue.map((pair, index) => {
|
|
102
|
+
if (!Array.isArray(pair) || pair.length !== 2 || pair.some(colour => !allowedColours.includes(colour)))
|
|
103
|
+
throw new StoneChainsOptionsError('invalid-challenge-pair', `Invalid queue pair ${index + 1}`);
|
|
104
|
+
return Object.freeze([pair[0], pair[1]]);
|
|
105
|
+
});
|
|
106
|
+
const goal = validateChallengeGoal(options.goal, ids);
|
|
107
|
+
if (!Array.isArray(options.witness) || options.witness.length < 1 || options.witness.length > queue.length)
|
|
108
|
+
throw new StoneChainsOptionsError('invalid-witness', 'A challenge requires a complete witness of 1–12 pair placements');
|
|
109
|
+
const witness = options.witness.map((step, index) => {
|
|
110
|
+
if (!step || typeof step !== 'object' || Array.isArray(step) || Object.keys(step).some(key => key !== 'pivotX' && key !== 'orientation') || !Number.isInteger(step.pivotX) || step.pivotX < 0 || step.pivotX >= width || !ORIENTATIONS.includes(step.orientation))
|
|
111
|
+
throw new StoneChainsOptionsError('invalid-witness-step', `Invalid witness placement ${index + 1}`);
|
|
112
|
+
return Object.freeze({ pivotX: step.pivotX, orientation: step.orientation });
|
|
113
|
+
});
|
|
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) });
|
|
115
|
+
const state = initialChallengeState(settings, settings.initialBoard, settings.queue);
|
|
116
|
+
const solved = solveWitness(state, witness);
|
|
117
|
+
if (solved.phase !== 'won' || stable(solved.settings) !== stable(settings))
|
|
118
|
+
throw new StoneChainsOptionsError('invalid-witness', 'Challenge witness validation failed');
|
|
119
|
+
return state;
|
|
120
|
+
}
|