@terminalgames/ink-sokoban 0.1.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/README.md +105 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/game/engine.d.ts +31 -0
- package/dist/game/engine.js +157 -0
- package/dist/game/engine.js.map +1 -0
- package/dist/game/levels.d.ts +18 -0
- package/dist/game/levels.js +565 -0
- package/dist/game/levels.js.map +1 -0
- package/dist/ui/App.d.ts +6 -0
- package/dist/ui/App.js +33 -0
- package/dist/ui/App.js.map +1 -0
- package/dist/ui/Cell.d.ts +6 -0
- package/dist/ui/Cell.js +8 -0
- package/dist/ui/Cell.js.map +1 -0
- package/dist/ui/MapView.d.ts +8 -0
- package/dist/ui/MapView.js +35 -0
- package/dist/ui/MapView.js.map +1 -0
- package/dist/ui/Sidebar.d.ts +7 -0
- package/dist/ui/Sidebar.js +49 -0
- package/dist/ui/Sidebar.js.map +1 -0
- package/dist/ui/input.d.ts +8 -0
- package/dist/ui/input.js +34 -0
- package/dist/ui/input.js.map +1 -0
- package/dist/ui/palette.d.ts +22 -0
- package/dist/ui/palette.js +43 -0
- package/dist/ui/palette.js.map +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Ink Sokoban
|
|
2
|
+
|
|
3
|
+
Terminal Sokoban built with [Ink](https://github.com/vadimdemedes/ink). Push
|
|
4
|
+
every crate onto its mark to clear each of the bundled puzzles — a 40-level
|
|
5
|
+
campaign that starts with tiny one-screen warm-ups and grows into wide, walled
|
|
6
|
+
vaults with a dozen crates to shuffle.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Requires **Node 20+**.
|
|
11
|
+
|
|
12
|
+
The unscoped name `ink-sokoban` may be taken on npm, so this package publishes
|
|
13
|
+
as **`@terminalgames/ink-sokoban`**. The command is still **`ink-sokoban`**.
|
|
14
|
+
|
|
15
|
+
### Run without installing (easiest)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @terminalgames/ink-sokoban
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Global install
|
|
22
|
+
|
|
23
|
+
If `npm install -g` fails with `EACCES`, your npm global prefix is probably
|
|
24
|
+
system-owned (`/usr/local`). Either use `npx` above, or point npm at a
|
|
25
|
+
user-owned directory:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
mkdir -p ~/.npm-global
|
|
29
|
+
npm config set prefix ~/.npm-global
|
|
30
|
+
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
|
|
31
|
+
source ~/.bashrc
|
|
32
|
+
npm install -g @terminalgames/ink-sokoban
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ink-sokoban
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## How to play
|
|
42
|
+
|
|
43
|
+
Move the player (`☻`) around the grid and push crates (`◇`) onto the target
|
|
44
|
+
marks (`·`). A crate sitting on a mark turns into `◆`. Clear a level by filling
|
|
45
|
+
every mark; then press `n` for the next puzzle.
|
|
46
|
+
|
|
47
|
+
Moves are turn-based and grid-aligned. You can only **push** crates — never
|
|
48
|
+
pull — and a push is blocked if a wall or another crate is behind the crate.
|
|
49
|
+
Stuck? Press `u` to undo (unlimited, back to the level start) or `r` to restart.
|
|
50
|
+
|
|
51
|
+
## Controls
|
|
52
|
+
|
|
53
|
+
| Key | Action |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `h` / `←` | Move left |
|
|
56
|
+
| `j` / `↓` | Move down |
|
|
57
|
+
| `k` / `↑` | Move up |
|
|
58
|
+
| `l` / `→` | Move right |
|
|
59
|
+
| `u` | Undo last move |
|
|
60
|
+
| `r` | Restart level |
|
|
61
|
+
| `n` | Next level (after a solve) |
|
|
62
|
+
| `p` | Pause / resume |
|
|
63
|
+
| `?` | Toggle help (shows legal pushes) |
|
|
64
|
+
| `q` | Quit |
|
|
65
|
+
|
|
66
|
+
The sidebar tracks the current level, move count, push count, undo count, and
|
|
67
|
+
resets.
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install
|
|
73
|
+
npm run build
|
|
74
|
+
node dist/cli.js
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm test
|
|
79
|
+
npm run typecheck
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Game logic lives in pure TypeScript modules under `src/game/` (`levels.ts` for
|
|
83
|
+
the puzzle data + XSB parser, `engine.ts` for the move/push/undo rules); the Ink
|
|
84
|
+
UI is under `src/ui/`. Every shipped level is verified solvable by a search in
|
|
85
|
+
`tests/game/levels.test.ts`.
|
|
86
|
+
|
|
87
|
+
Two dev-only helpers live under `scripts/` (build first, then run with `node`):
|
|
88
|
+
`verify-levels.mjs` runs that same BFS but prints per-level diagnostics
|
|
89
|
+
(dimensions, crate count, solution depth, nodes explored) for tuning new
|
|
90
|
+
layouts, and `preview.mjs <index>` renders a single level the way the player
|
|
91
|
+
sees it.
|
|
92
|
+
|
|
93
|
+
## Credits
|
|
94
|
+
|
|
95
|
+
The bundled levels are original layouts written in the compact, one-screen
|
|
96
|
+
style popularised by the **Microban** collection by **David W. Skinner**.
|
|
97
|
+
|
|
98
|
+
## Publish
|
|
99
|
+
|
|
100
|
+
Maintainers only. Requires access to the `@terminalgames` npm organization:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm login
|
|
104
|
+
npm publish --access public
|
|
105
|
+
```
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { App } from './ui/App.js';
|
|
6
|
+
export function runCli(options = {}) {
|
|
7
|
+
render(React.createElement(App, { initialLevel: options.level ?? 0 }));
|
|
8
|
+
}
|
|
9
|
+
function isDirectRun(entrypoint = process.argv[1]) {
|
|
10
|
+
return entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href;
|
|
11
|
+
}
|
|
12
|
+
if (isDirectRun()) {
|
|
13
|
+
runCli();
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAC,MAAM,EAAC,MAAM,KAAK,CAAC;AAC3B,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAC,aAAa,EAAC,MAAM,UAAU,CAAC;AACvC,OAAO,EAAC,GAAG,EAAC,MAAM,aAAa,CAAC;AAMhC,MAAM,UAAU,MAAM,CAAC,UAAyB,EAAE;IAChD,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,EAAE,EAAC,YAAY,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,EAAC,CAAC,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,WAAW,CAAC,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/C,OAAO,UAAU,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;AACxF,CAAC;AAED,IAAI,WAAW,EAAE,EAAE,CAAC;IAClB,MAAM,EAAE,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Point } from './levels.js';
|
|
2
|
+
export type { Point } from './levels.js';
|
|
3
|
+
export type GameStatus = 'playing' | 'paused' | 'solved' | 'complete';
|
|
4
|
+
export type GameAction = 'move-up' | 'move-down' | 'move-left' | 'move-right' | 'undo' | 'restart' | 'next-level' | 'pause';
|
|
5
|
+
export type Move = {
|
|
6
|
+
dx: number;
|
|
7
|
+
dy: number;
|
|
8
|
+
pushedBox: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type SokobanState = {
|
|
11
|
+
levelIndex: number;
|
|
12
|
+
levelCount: number;
|
|
13
|
+
levelName: string;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
walls: boolean[][];
|
|
17
|
+
targets: boolean[][];
|
|
18
|
+
boxes: Point[];
|
|
19
|
+
player: Point;
|
|
20
|
+
moves: number;
|
|
21
|
+
pushes: number;
|
|
22
|
+
undoCount: number;
|
|
23
|
+
resets: number;
|
|
24
|
+
history: Move[];
|
|
25
|
+
status: GameStatus;
|
|
26
|
+
};
|
|
27
|
+
export declare function createInitialState(levelIndex?: number): SokobanState;
|
|
28
|
+
export declare function isSolved(state: SokobanState): boolean;
|
|
29
|
+
export declare function applyAction(state: SokobanState, action: GameAction): SokobanState;
|
|
30
|
+
export declare function isWall(state: SokobanState, x: number, y: number): boolean;
|
|
31
|
+
export declare function boxIndexAt(boxes: Point[], x: number, y: number): number;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { LEVELS, parseLevel } from './levels.js';
|
|
2
|
+
const DELTAS = {
|
|
3
|
+
'move-up': { dx: 0, dy: -1, pushedBox: false },
|
|
4
|
+
'move-down': { dx: 0, dy: 1, pushedBox: false },
|
|
5
|
+
'move-left': { dx: -1, dy: 0, pushedBox: false },
|
|
6
|
+
'move-right': { dx: 1, dy: 0, pushedBox: false }
|
|
7
|
+
};
|
|
8
|
+
export function createInitialState(levelIndex = 0) {
|
|
9
|
+
const level = LEVELS[levelIndex];
|
|
10
|
+
if (level === undefined) {
|
|
11
|
+
throw new Error(`No level at index ${levelIndex}`);
|
|
12
|
+
}
|
|
13
|
+
const parsed = parseLevel(level.text);
|
|
14
|
+
return {
|
|
15
|
+
levelIndex,
|
|
16
|
+
levelCount: LEVELS.length,
|
|
17
|
+
levelName: level.name,
|
|
18
|
+
width: parsed.width,
|
|
19
|
+
height: parsed.height,
|
|
20
|
+
walls: parsed.walls,
|
|
21
|
+
targets: parsed.targets,
|
|
22
|
+
boxes: parsed.boxes.map(box => ({ ...box })),
|
|
23
|
+
player: { ...parsed.player },
|
|
24
|
+
moves: 0,
|
|
25
|
+
pushes: 0,
|
|
26
|
+
undoCount: 0,
|
|
27
|
+
resets: 0,
|
|
28
|
+
history: [],
|
|
29
|
+
status: 'playing'
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function isSolved(state) {
|
|
33
|
+
for (let y = 0; y < state.height; y += 1) {
|
|
34
|
+
for (let x = 0; x < state.width; x += 1) {
|
|
35
|
+
if (state.targets[y][x] && boxIndexAt(state.boxes, x, y) === -1) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
export function applyAction(state, action) {
|
|
43
|
+
switch (action) {
|
|
44
|
+
case 'restart':
|
|
45
|
+
return restart(state);
|
|
46
|
+
case 'next-level':
|
|
47
|
+
return nextLevel(state);
|
|
48
|
+
case 'pause':
|
|
49
|
+
return togglePause(state);
|
|
50
|
+
case 'undo':
|
|
51
|
+
return undo(state);
|
|
52
|
+
case 'move-up':
|
|
53
|
+
case 'move-down':
|
|
54
|
+
case 'move-left':
|
|
55
|
+
case 'move-right':
|
|
56
|
+
return move(state, DELTAS[action]);
|
|
57
|
+
default:
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function move(state, delta) {
|
|
62
|
+
if (state.status !== 'playing') {
|
|
63
|
+
return state;
|
|
64
|
+
}
|
|
65
|
+
const { dx, dy } = delta;
|
|
66
|
+
const targetX = state.player.x + dx;
|
|
67
|
+
const targetY = state.player.y + dy;
|
|
68
|
+
if (isWall(state, targetX, targetY)) {
|
|
69
|
+
return state;
|
|
70
|
+
}
|
|
71
|
+
const boxIndex = boxIndexAt(state.boxes, targetX, targetY);
|
|
72
|
+
// Empty floor: just step.
|
|
73
|
+
if (boxIndex === -1) {
|
|
74
|
+
return finishMove(state, { x: targetX, y: targetY }, state.boxes, { dx, dy, pushedBox: false });
|
|
75
|
+
}
|
|
76
|
+
// A box is in the way — can only move it by pushing into a free cell.
|
|
77
|
+
const beyondX = targetX + dx;
|
|
78
|
+
const beyondY = targetY + dy;
|
|
79
|
+
if (isWall(state, beyondX, beyondY) || boxIndexAt(state.boxes, beyondX, beyondY) !== -1) {
|
|
80
|
+
return state; // blocked: wall or another box behind it
|
|
81
|
+
}
|
|
82
|
+
const boxes = state.boxes.map((box, index) => index === boxIndex ? { x: beyondX, y: beyondY } : box);
|
|
83
|
+
return finishMove(state, { x: targetX, y: targetY }, boxes, { dx, dy, pushedBox: true });
|
|
84
|
+
}
|
|
85
|
+
function finishMove(state, player, boxes, record) {
|
|
86
|
+
const next = {
|
|
87
|
+
...state,
|
|
88
|
+
player,
|
|
89
|
+
boxes,
|
|
90
|
+
moves: state.moves + 1,
|
|
91
|
+
pushes: state.pushes + (record.pushedBox ? 1 : 0),
|
|
92
|
+
history: [...state.history, record]
|
|
93
|
+
};
|
|
94
|
+
if (isSolved(next)) {
|
|
95
|
+
next.status = 'solved';
|
|
96
|
+
}
|
|
97
|
+
return next;
|
|
98
|
+
}
|
|
99
|
+
function undo(state) {
|
|
100
|
+
const last = state.history[state.history.length - 1];
|
|
101
|
+
if (last === undefined) {
|
|
102
|
+
return state;
|
|
103
|
+
}
|
|
104
|
+
const { dx, dy, pushedBox } = last;
|
|
105
|
+
let boxes = state.boxes;
|
|
106
|
+
if (pushedBox) {
|
|
107
|
+
// The box now sits one cell ahead of the player; drag it back onto the
|
|
108
|
+
// player's current cell, then the player retreats to where it came from.
|
|
109
|
+
const boxX = state.player.x + dx;
|
|
110
|
+
const boxY = state.player.y + dy;
|
|
111
|
+
const boxIndex = boxIndexAt(state.boxes, boxX, boxY);
|
|
112
|
+
boxes = state.boxes.map((box, index) => index === boxIndex ? { x: state.player.x, y: state.player.y } : box);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
...state,
|
|
116
|
+
player: { x: state.player.x - dx, y: state.player.y - dy },
|
|
117
|
+
boxes,
|
|
118
|
+
moves: state.moves - 1,
|
|
119
|
+
pushes: state.pushes - (pushedBox ? 1 : 0),
|
|
120
|
+
undoCount: state.undoCount + 1,
|
|
121
|
+
history: state.history.slice(0, -1),
|
|
122
|
+
status: 'playing'
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function restart(state) {
|
|
126
|
+
const fresh = createInitialState(state.levelIndex);
|
|
127
|
+
return { ...fresh, resets: state.resets + 1 };
|
|
128
|
+
}
|
|
129
|
+
function nextLevel(state) {
|
|
130
|
+
if (state.status !== 'solved') {
|
|
131
|
+
return state;
|
|
132
|
+
}
|
|
133
|
+
const nextIndex = state.levelIndex + 1;
|
|
134
|
+
if (nextIndex >= LEVELS.length) {
|
|
135
|
+
return { ...state, status: 'complete' };
|
|
136
|
+
}
|
|
137
|
+
return createInitialState(nextIndex);
|
|
138
|
+
}
|
|
139
|
+
function togglePause(state) {
|
|
140
|
+
if (state.status === 'playing') {
|
|
141
|
+
return { ...state, status: 'paused' };
|
|
142
|
+
}
|
|
143
|
+
if (state.status === 'paused') {
|
|
144
|
+
return { ...state, status: 'playing' };
|
|
145
|
+
}
|
|
146
|
+
return state;
|
|
147
|
+
}
|
|
148
|
+
export function isWall(state, x, y) {
|
|
149
|
+
if (y < 0 || y >= state.height || x < 0 || x >= state.width) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return state.walls[y][x];
|
|
153
|
+
}
|
|
154
|
+
export function boxIndexAt(boxes, x, y) {
|
|
155
|
+
return boxes.findIndex(box => box.x === x && box.y === y);
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/game/engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Point = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
};
|
|
5
|
+
export type Level = {
|
|
6
|
+
name: string;
|
|
7
|
+
text: string;
|
|
8
|
+
};
|
|
9
|
+
export type ParsedLevel = {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
walls: boolean[][];
|
|
13
|
+
targets: boolean[][];
|
|
14
|
+
boxes: Point[];
|
|
15
|
+
player: Point;
|
|
16
|
+
};
|
|
17
|
+
export declare const LEVELS: Level[];
|
|
18
|
+
export declare function parseLevel(text: string): ParsedLevel;
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
// Levels use the classic XSB notation:
|
|
2
|
+
// # wall @ player + player on target
|
|
3
|
+
// $ box * box on target . target
|
|
4
|
+
// (space / - / _) floor
|
|
5
|
+
//
|
|
6
|
+
// A 40-level campaign of original, hand-authored layouts in the compact
|
|
7
|
+
// "one screen" tradition popularised by David W. Skinner's Microban set (see
|
|
8
|
+
// README credits). Boards start tiny and grow gradually — the early levels are
|
|
9
|
+
// a few cells across, the late ones span roughly a dozen — while box counts and
|
|
10
|
+
// the amount of maneuvering ramp up alongside. Every shipped level is verified
|
|
11
|
+
// solvable (and not already solved) by the BFS search in
|
|
12
|
+
// tests/game/levels.test.ts; scripts/verify-levels.mjs prints per-level
|
|
13
|
+
// diagnostics for tuning.
|
|
14
|
+
export const LEVELS = [
|
|
15
|
+
// ── Tier 1: first light (tiny, 1–4 boxes) ───────────────────────────────
|
|
16
|
+
{
|
|
17
|
+
name: 'First Light',
|
|
18
|
+
text: `
|
|
19
|
+
#######
|
|
20
|
+
#@ $ .#
|
|
21
|
+
#######
|
|
22
|
+
`
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'Twin Slots',
|
|
26
|
+
text: `
|
|
27
|
+
######
|
|
28
|
+
#@ $.#
|
|
29
|
+
# $.#
|
|
30
|
+
######
|
|
31
|
+
`
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'The Turn',
|
|
35
|
+
text: `
|
|
36
|
+
######
|
|
37
|
+
#@ #
|
|
38
|
+
# $ #
|
|
39
|
+
# . #
|
|
40
|
+
######
|
|
41
|
+
`
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'Sidestep',
|
|
45
|
+
text: `
|
|
46
|
+
######
|
|
47
|
+
# .#
|
|
48
|
+
# @$ #
|
|
49
|
+
# ##
|
|
50
|
+
######
|
|
51
|
+
`
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'Bookends',
|
|
55
|
+
text: `
|
|
56
|
+
#######
|
|
57
|
+
#. .#
|
|
58
|
+
# $@$ #
|
|
59
|
+
# #
|
|
60
|
+
#######
|
|
61
|
+
`
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'Two-Step',
|
|
65
|
+
text: `
|
|
66
|
+
#######
|
|
67
|
+
# @ #
|
|
68
|
+
# $ $ #
|
|
69
|
+
#. .#
|
|
70
|
+
#######
|
|
71
|
+
`
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'Compass',
|
|
75
|
+
text: `
|
|
76
|
+
#######
|
|
77
|
+
# . #
|
|
78
|
+
# $ #
|
|
79
|
+
#.$@$.#
|
|
80
|
+
# $ #
|
|
81
|
+
# . #
|
|
82
|
+
#######
|
|
83
|
+
`
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'Observatory',
|
|
87
|
+
text: `
|
|
88
|
+
########
|
|
89
|
+
# . #
|
|
90
|
+
# $ #
|
|
91
|
+
#.$@$. #
|
|
92
|
+
# #
|
|
93
|
+
########
|
|
94
|
+
`
|
|
95
|
+
},
|
|
96
|
+
// ── Tier 2: open sky (8–9 wide, lanes and lifts) ─────────────────────────
|
|
97
|
+
{
|
|
98
|
+
name: 'Three Lanes',
|
|
99
|
+
text: `
|
|
100
|
+
#########
|
|
101
|
+
#. $ @#
|
|
102
|
+
#. $ #
|
|
103
|
+
#. $ #
|
|
104
|
+
# #
|
|
105
|
+
#########
|
|
106
|
+
`
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'Lift Off',
|
|
110
|
+
text: `
|
|
111
|
+
########
|
|
112
|
+
# . .. #
|
|
113
|
+
# #
|
|
114
|
+
# #
|
|
115
|
+
# $ $$ #
|
|
116
|
+
#@ #
|
|
117
|
+
########
|
|
118
|
+
`
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'Long Haul',
|
|
122
|
+
text: `
|
|
123
|
+
########
|
|
124
|
+
#@$ .#
|
|
125
|
+
# #
|
|
126
|
+
# $ .#
|
|
127
|
+
# #
|
|
128
|
+
# $ .#
|
|
129
|
+
########
|
|
130
|
+
`
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'Four Winds',
|
|
134
|
+
text: `
|
|
135
|
+
#######
|
|
136
|
+
#. $ #
|
|
137
|
+
#. $ #
|
|
138
|
+
#. $ #
|
|
139
|
+
#. $ #
|
|
140
|
+
# @#
|
|
141
|
+
#######
|
|
142
|
+
`
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'Switchback',
|
|
146
|
+
text: `
|
|
147
|
+
#########
|
|
148
|
+
# ...##
|
|
149
|
+
# #
|
|
150
|
+
#@$$$ #
|
|
151
|
+
# #
|
|
152
|
+
#########
|
|
153
|
+
`
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'Four Corners',
|
|
157
|
+
text: `
|
|
158
|
+
#########
|
|
159
|
+
# . . #
|
|
160
|
+
# #
|
|
161
|
+
# $ $ #
|
|
162
|
+
#@ #
|
|
163
|
+
# $ $ #
|
|
164
|
+
# . . #
|
|
165
|
+
#########
|
|
166
|
+
`
|
|
167
|
+
},
|
|
168
|
+
// ── Tier 3: the vaults (walled combs, 4–6 boxes) ─────────────────────────
|
|
169
|
+
// From here on the boards grow in footprint but stay confined: lanes are
|
|
170
|
+
// separated by walls so each box has a single forced path to its mark, which
|
|
171
|
+
// keeps every layout provably solvable and quick for the BFS to confirm.
|
|
172
|
+
{
|
|
173
|
+
name: 'Portcullis',
|
|
174
|
+
text: `
|
|
175
|
+
#########
|
|
176
|
+
#@ #
|
|
177
|
+
#$#$#$#$#
|
|
178
|
+
#.#.#.#.#
|
|
179
|
+
#########
|
|
180
|
+
`
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'Lantern Row',
|
|
184
|
+
text: `
|
|
185
|
+
#########
|
|
186
|
+
#@ #
|
|
187
|
+
#$#$#$#$#
|
|
188
|
+
# # # # #
|
|
189
|
+
#.#.#.#.#
|
|
190
|
+
#########
|
|
191
|
+
`
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'Stalactites',
|
|
195
|
+
text: `
|
|
196
|
+
#########
|
|
197
|
+
#.#.#.#.#
|
|
198
|
+
# # # # #
|
|
199
|
+
#$#$#$#$#
|
|
200
|
+
#@ #
|
|
201
|
+
#########
|
|
202
|
+
`
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'The Cellars',
|
|
206
|
+
text: `
|
|
207
|
+
###########
|
|
208
|
+
#@ #
|
|
209
|
+
#$#$#$#$#$#
|
|
210
|
+
# # # # # #
|
|
211
|
+
#.#.#.#.#.#
|
|
212
|
+
###########
|
|
213
|
+
`
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'The Mirror',
|
|
217
|
+
text: `
|
|
218
|
+
#######
|
|
219
|
+
#.#.#.#
|
|
220
|
+
#$#$#$#
|
|
221
|
+
#@ #
|
|
222
|
+
#$#$#$#
|
|
223
|
+
#.#.#.#
|
|
224
|
+
#######
|
|
225
|
+
`
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'Vault Rows',
|
|
229
|
+
text: `
|
|
230
|
+
###########
|
|
231
|
+
#@ #
|
|
232
|
+
#$#$#$#$#$#
|
|
233
|
+
# # # # # #
|
|
234
|
+
# # # # # #
|
|
235
|
+
#.#.#.#.#.#
|
|
236
|
+
###########
|
|
237
|
+
`
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'Stalagmites',
|
|
241
|
+
text: `
|
|
242
|
+
###########
|
|
243
|
+
#.#.#.#.#.#
|
|
244
|
+
# # # # # #
|
|
245
|
+
#$#$#$#$#$#
|
|
246
|
+
#@ #
|
|
247
|
+
###########
|
|
248
|
+
`
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'Deep Wells',
|
|
252
|
+
text: `
|
|
253
|
+
#########
|
|
254
|
+
#@ #
|
|
255
|
+
#$#$#$#$#
|
|
256
|
+
# # # # #
|
|
257
|
+
# # # # #
|
|
258
|
+
#.#.#.#.#
|
|
259
|
+
#########
|
|
260
|
+
`
|
|
261
|
+
},
|
|
262
|
+
// ── Tier 4: the works (wider combs and side corridors, 4–6 boxes) ────────
|
|
263
|
+
{
|
|
264
|
+
name: 'Dovetail',
|
|
265
|
+
text: `
|
|
266
|
+
#########
|
|
267
|
+
#.#.#.#.#
|
|
268
|
+
#$#$#$#$#
|
|
269
|
+
#@ #
|
|
270
|
+
#$#$#$#$#
|
|
271
|
+
#.#.#.#.#
|
|
272
|
+
#########
|
|
273
|
+
`
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'The Granary',
|
|
277
|
+
text: `
|
|
278
|
+
#############
|
|
279
|
+
#@ #
|
|
280
|
+
#$#$#$#$#$#$#
|
|
281
|
+
# # # # # # #
|
|
282
|
+
#.#.#.#.#.#.#
|
|
283
|
+
#############
|
|
284
|
+
`
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'Catacomb',
|
|
288
|
+
text: `
|
|
289
|
+
###########
|
|
290
|
+
#.#.#.#.#.#
|
|
291
|
+
# # # # # #
|
|
292
|
+
# # # # # #
|
|
293
|
+
#$#$#$#$#$#
|
|
294
|
+
#@ #
|
|
295
|
+
###########
|
|
296
|
+
`
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'Twin Combs',
|
|
300
|
+
text: `
|
|
301
|
+
#######
|
|
302
|
+
#.#.#.#
|
|
303
|
+
# # # #
|
|
304
|
+
#$#$#$#
|
|
305
|
+
#@ #
|
|
306
|
+
#$#$#$#
|
|
307
|
+
# # # #
|
|
308
|
+
#.#.#.#
|
|
309
|
+
#######
|
|
310
|
+
`
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: 'The Spillway',
|
|
314
|
+
text: `
|
|
315
|
+
#############
|
|
316
|
+
#@ #
|
|
317
|
+
#$#$#$#$#$#$#
|
|
318
|
+
# # # # # # #
|
|
319
|
+
# # # # # # #
|
|
320
|
+
#.#.#.#.#.#.#
|
|
321
|
+
#############
|
|
322
|
+
`
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: 'Honeycomb',
|
|
326
|
+
text: `
|
|
327
|
+
#############
|
|
328
|
+
#.#.#.#.#.#.#
|
|
329
|
+
# # # # # # #
|
|
330
|
+
#$#$#$#$#$#$#
|
|
331
|
+
#@ #
|
|
332
|
+
#############
|
|
333
|
+
`
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: 'The Sluice',
|
|
337
|
+
text: `
|
|
338
|
+
##########
|
|
339
|
+
#. $ @#
|
|
340
|
+
######## #
|
|
341
|
+
#. $ #
|
|
342
|
+
######## #
|
|
343
|
+
#. $ #
|
|
344
|
+
######## #
|
|
345
|
+
#. $ #
|
|
346
|
+
##########
|
|
347
|
+
`
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: 'Deep Vaults',
|
|
351
|
+
text: `
|
|
352
|
+
###########
|
|
353
|
+
#@ #
|
|
354
|
+
#$#$#$#$#$#
|
|
355
|
+
# # # # # #
|
|
356
|
+
# # # # # #
|
|
357
|
+
# # # # # #
|
|
358
|
+
#.#.#.#.#.#
|
|
359
|
+
###########
|
|
360
|
+
`
|
|
361
|
+
},
|
|
362
|
+
// ── Tier 5: the deep (largest footprints, long forced hauls) ─────────────
|
|
363
|
+
{
|
|
364
|
+
name: 'Drift',
|
|
365
|
+
text: `
|
|
366
|
+
##########
|
|
367
|
+
#@ $ .#
|
|
368
|
+
# ########
|
|
369
|
+
# $ .#
|
|
370
|
+
# ########
|
|
371
|
+
# $ .#
|
|
372
|
+
# ########
|
|
373
|
+
# $ .#
|
|
374
|
+
##########
|
|
375
|
+
`
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: 'The Reckoning',
|
|
379
|
+
text: `
|
|
380
|
+
###########
|
|
381
|
+
#.#.#.#.#.#
|
|
382
|
+
#$#$#$#$#$#
|
|
383
|
+
#@ #
|
|
384
|
+
#$#$#$#$#$#
|
|
385
|
+
#.#.#.#.#.#
|
|
386
|
+
###########
|
|
387
|
+
`
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: 'Cold Storage',
|
|
391
|
+
text: `
|
|
392
|
+
#######
|
|
393
|
+
#.#.#.#
|
|
394
|
+
# # # #
|
|
395
|
+
# # # #
|
|
396
|
+
#$#$#$#
|
|
397
|
+
#@ #
|
|
398
|
+
#$#$#$#
|
|
399
|
+
# # # #
|
|
400
|
+
# # # #
|
|
401
|
+
#.#.#.#
|
|
402
|
+
#######
|
|
403
|
+
`
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'Honeycomb Deep',
|
|
407
|
+
text: `
|
|
408
|
+
#############
|
|
409
|
+
#.#.#.#.#.#.#
|
|
410
|
+
# # # # # # #
|
|
411
|
+
# # # # # # #
|
|
412
|
+
#$#$#$#$#$#$#
|
|
413
|
+
#@ #
|
|
414
|
+
#############
|
|
415
|
+
`
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: 'The Sluice II',
|
|
419
|
+
text: `
|
|
420
|
+
############
|
|
421
|
+
#. $ @#
|
|
422
|
+
########## #
|
|
423
|
+
#. $ #
|
|
424
|
+
########## #
|
|
425
|
+
#. $ #
|
|
426
|
+
########## #
|
|
427
|
+
#. $ #
|
|
428
|
+
############
|
|
429
|
+
`
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: 'The Reactor',
|
|
433
|
+
text: `
|
|
434
|
+
###########
|
|
435
|
+
#.#.#.#.#.#
|
|
436
|
+
# # # # # #
|
|
437
|
+
# # # # # #
|
|
438
|
+
#$#$#$#$#$#
|
|
439
|
+
#@ #
|
|
440
|
+
###########
|
|
441
|
+
`
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: 'Deep Storage',
|
|
445
|
+
text: `
|
|
446
|
+
##########
|
|
447
|
+
#. $ @#
|
|
448
|
+
######## #
|
|
449
|
+
#. $ #
|
|
450
|
+
######## #
|
|
451
|
+
#. $ #
|
|
452
|
+
######## #
|
|
453
|
+
#. $ #
|
|
454
|
+
######## #
|
|
455
|
+
#. $ #
|
|
456
|
+
##########
|
|
457
|
+
`
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: 'The Bulwark',
|
|
461
|
+
text: `
|
|
462
|
+
###########
|
|
463
|
+
#@ $ .#
|
|
464
|
+
# #########
|
|
465
|
+
# $ .#
|
|
466
|
+
# #########
|
|
467
|
+
# $ .#
|
|
468
|
+
# #########
|
|
469
|
+
# $ .#
|
|
470
|
+
###########
|
|
471
|
+
`
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: 'Leviathan',
|
|
475
|
+
text: `
|
|
476
|
+
#########
|
|
477
|
+
#.#.#.#.#
|
|
478
|
+
# # # # #
|
|
479
|
+
#$#$#$#$#
|
|
480
|
+
#@ #
|
|
481
|
+
#$#$#$#$#
|
|
482
|
+
# # # # #
|
|
483
|
+
#.#.#.#.#
|
|
484
|
+
#########
|
|
485
|
+
`
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: 'Event Horizon',
|
|
489
|
+
text: `
|
|
490
|
+
#############
|
|
491
|
+
#.#.#.#.#.#.#
|
|
492
|
+
#$#$#$#$#$#$#
|
|
493
|
+
#@ #
|
|
494
|
+
#$#$#$#$#$#$#
|
|
495
|
+
#.#.#.#.#.#.#
|
|
496
|
+
#############
|
|
497
|
+
`
|
|
498
|
+
}
|
|
499
|
+
];
|
|
500
|
+
const FLOOR_CHARS = new Set([' ', '-', '_']);
|
|
501
|
+
export function parseLevel(text) {
|
|
502
|
+
const rows = text.replace(/\r/g, '').split('\n');
|
|
503
|
+
// Drop blank leading/trailing lines so levels can be written with surrounding
|
|
504
|
+
// newlines in template literals.
|
|
505
|
+
while (rows.length > 0 && rows[0].trim() === '') {
|
|
506
|
+
rows.shift();
|
|
507
|
+
}
|
|
508
|
+
while (rows.length > 0 && rows[rows.length - 1].trim() === '') {
|
|
509
|
+
rows.pop();
|
|
510
|
+
}
|
|
511
|
+
if (rows.length === 0) {
|
|
512
|
+
throw new Error('Level is empty');
|
|
513
|
+
}
|
|
514
|
+
const height = rows.length;
|
|
515
|
+
const width = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
|
516
|
+
const walls = [];
|
|
517
|
+
const targets = [];
|
|
518
|
+
const boxes = [];
|
|
519
|
+
let player;
|
|
520
|
+
for (let y = 0; y < height; y += 1) {
|
|
521
|
+
const wallRow = [];
|
|
522
|
+
const targetRow = [];
|
|
523
|
+
const row = rows[y];
|
|
524
|
+
for (let x = 0; x < width; x += 1) {
|
|
525
|
+
const char = x < row.length ? row[x] : ' ';
|
|
526
|
+
let isWall = false;
|
|
527
|
+
let isTarget = false;
|
|
528
|
+
switch (char) {
|
|
529
|
+
case '#':
|
|
530
|
+
isWall = true;
|
|
531
|
+
break;
|
|
532
|
+
case '@':
|
|
533
|
+
player = { x, y };
|
|
534
|
+
break;
|
|
535
|
+
case '+':
|
|
536
|
+
player = { x, y };
|
|
537
|
+
isTarget = true;
|
|
538
|
+
break;
|
|
539
|
+
case '$':
|
|
540
|
+
boxes.push({ x, y });
|
|
541
|
+
break;
|
|
542
|
+
case '*':
|
|
543
|
+
boxes.push({ x, y });
|
|
544
|
+
isTarget = true;
|
|
545
|
+
break;
|
|
546
|
+
case '.':
|
|
547
|
+
isTarget = true;
|
|
548
|
+
break;
|
|
549
|
+
default:
|
|
550
|
+
if (!FLOOR_CHARS.has(char)) {
|
|
551
|
+
throw new Error(`Unknown level character: ${JSON.stringify(char)}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
wallRow.push(isWall);
|
|
555
|
+
targetRow.push(isTarget);
|
|
556
|
+
}
|
|
557
|
+
walls.push(wallRow);
|
|
558
|
+
targets.push(targetRow);
|
|
559
|
+
}
|
|
560
|
+
if (player === undefined) {
|
|
561
|
+
throw new Error('Level has no player');
|
|
562
|
+
}
|
|
563
|
+
return { width, height, walls, targets, boxes, player };
|
|
564
|
+
}
|
|
565
|
+
//# sourceMappingURL=levels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"levels.js","sourceRoot":"","sources":["../../src/game/levels.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,8DAA8D;AAC9D,oDAAoD;AACpD,6BAA6B;AAC7B,EAAE;AACF,wEAAwE;AACxE,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,yDAAyD;AACzD,wEAAwE;AACxE,0BAA0B;AAkB1B,MAAM,CAAC,MAAM,MAAM,GAAY;IAC7B,2EAA2E;IAC3E;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;;;;CAIT;KACE;IACD;QACE,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;;;;;CAKT;KACE;IACD;QACE,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE;;;;;;CAMT;KACE;IACD;QACE,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE;;;;;;CAMT;KACE;IACD;QACE,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE;;;;;;CAMT;KACE;IACD;QACE,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE;;;;;;CAMT;KACE;IACD;QACE,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;;;;;;;CAOT;KACE;IACD,4EAA4E;IAC5E;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;;;;;;;CAOT;KACE;IACD;QACE,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD;QACE,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD;QACE,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD;QACE,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;;;;;;;CAOT;KACE;IACD;QACE,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE;;;;;;;;;CAST;KACE;IACD,4EAA4E;IAC5E,yEAAyE;IACzE,6EAA6E;IAC7E,yEAAyE;IACzE;QACE,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;;;;;;CAMT;KACE;IACD;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;;;;;;;CAOT;KACE;IACD;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;;;;;;;CAOT;KACE;IACD;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;;;;;;;CAOT;KACE;IACD;QACE,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD;QACE,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;;;;;;;CAOT;KACE;IACD;QACE,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD,4EAA4E;IAC5E;QACE,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;;;;;;;CAOT;KACE;IACD;QACE,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD;QACE,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;;;;;;;;;;CAUT;KACE;IACD;QACE,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD;QACE,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE;;;;;;;CAOT;KACE;IACD;QACE,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;;;;;;;;;;CAUT;KACE;IACD;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;;;;;;;;;CAST;KACE;IACD,4EAA4E;IAC5E;QACE,IAAI,EAAE,OAAO;QACb,IAAI,EAAE;;;;;;;;;;CAUT;KACE;IACD;QACE,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD;QACE,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE;;;;;;;;;;;;CAYT;KACE;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD;QACE,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE;;;;;;;;;;CAUT;KACE;IACD;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;;;;;;;;CAQT;KACE;IACD;QACE,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE;;;;;;;;;;;;CAYT;KACE;IACD;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;;;;;;;;;;CAUT;KACE;IACD;QACE,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE;;;;;;;;;;CAUT;KACE;IACD;QACE,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE;;;;;;;;CAQT;KACE;CACF,CAAC;AAEF,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAE7C,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEjD,8EAA8E;IAC9E,iCAAiC;IACjC,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACjD,IAAI,CAAC,KAAK,EAAE,CAAC;IACf,CAAC;IAED,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC/D,IAAI,CAAC,GAAG,EAAE,CAAC;IACb,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACpC,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IAEtE,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,MAAM,KAAK,GAAY,EAAE,CAAC;IAC1B,IAAI,MAAyB,CAAC;IAE9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,OAAO,GAAc,EAAE,CAAC;QAC9B,MAAM,SAAS,GAAc,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;QAErB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,GAAG,CAAC;YAC5C,IAAI,MAAM,GAAG,KAAK,CAAC;YACnB,IAAI,QAAQ,GAAG,KAAK,CAAC;YAErB,QAAQ,IAAI,EAAE,CAAC;gBACb,KAAK,GAAG;oBACN,MAAM,GAAG,IAAI,CAAC;oBACd,MAAM;gBACR,KAAK,GAAG;oBACN,MAAM,GAAG,EAAC,CAAC,EAAE,CAAC,EAAC,CAAC;oBAChB,MAAM;gBACR,KAAK,GAAG;oBACN,MAAM,GAAG,EAAC,CAAC,EAAE,CAAC,EAAC,CAAC;oBAChB,QAAQ,GAAG,IAAI,CAAC;oBAChB,MAAM;gBACR,KAAK,GAAG;oBACN,KAAK,CAAC,IAAI,CAAC,EAAC,CAAC,EAAE,CAAC,EAAC,CAAC,CAAC;oBACnB,MAAM;gBACR,KAAK,GAAG;oBACN,KAAK,CAAC,IAAI,CAAC,EAAC,CAAC,EAAE,CAAC,EAAC,CAAC,CAAC;oBACnB,QAAQ,GAAG,IAAI,CAAC;oBAChB,MAAM;gBACR,KAAK,GAAG;oBACN,QAAQ,GAAG,IAAI,CAAC;oBAChB,MAAM;gBACR;oBACE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;wBAC3B,MAAM,IAAI,KAAK,CAAC,4BAA4B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBACtE,CAAC;YACL,CAAC;YAED,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACrB,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3B,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACzC,CAAC;IAED,OAAO,EAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAC,CAAC;AACxD,CAAC"}
|
package/dist/ui/App.d.ts
ADDED
package/dist/ui/App.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, useApp, useInput } from 'ink';
|
|
4
|
+
import { applyAction, createInitialState } from '../game/engine.js';
|
|
5
|
+
import { MapView } from './MapView.js';
|
|
6
|
+
import { mapInputToAction } from './input.js';
|
|
7
|
+
import { Sidebar } from './Sidebar.js';
|
|
8
|
+
export function App({ initialLevel = 0, onExit }) {
|
|
9
|
+
const { exit } = useApp();
|
|
10
|
+
const [state, setState] = React.useState(() => createInitialState(initialLevel));
|
|
11
|
+
const [showHelp, setShowHelp] = React.useState(false);
|
|
12
|
+
// Sokoban is turn-based: state only changes in response to a keypress, so
|
|
13
|
+
// there is no game-loop timer (unlike tetris's gravity tick).
|
|
14
|
+
useInput((input, key) => {
|
|
15
|
+
const action = mapInputToAction(input, key);
|
|
16
|
+
if (action === undefined) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (action === 'toggle-help') {
|
|
20
|
+
setShowHelp(current => !current);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (action === 'quit') {
|
|
24
|
+
onExit?.();
|
|
25
|
+
exit();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const gameAction = action;
|
|
29
|
+
setState(current => applyAction(current, gameAction));
|
|
30
|
+
});
|
|
31
|
+
return (_jsxs(Box, { children: [_jsx(MapView, { state: state }), _jsx(Sidebar, { state: state, showHelp: showHelp })] }));
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=App.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"App.js","sourceRoot":"","sources":["../../src/ui/App.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAC,MAAM,KAAK,CAAC;AAC1C,OAAO,EAAC,WAAW,EAAE,kBAAkB,EAAqC,MAAM,mBAAmB,CAAC;AACtG,OAAO,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AACrC,OAAO,EAAC,gBAAgB,EAAC,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAOrC,MAAM,UAAU,GAAG,CAAC,EAAC,YAAY,GAAG,CAAC,EAAE,MAAM,EAAW;IACtD,MAAM,EAAC,IAAI,EAAC,GAAG,MAAM,EAAE,CAAC;IACxB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAe,GAAG,EAAE,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/F,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEtD,0EAA0E;IAC1E,8DAA8D;IAC9D,QAAQ,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACtB,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAE5C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO;QACT,CAAC;QAED,IAAI,MAAM,KAAK,aAAa,EAAE,CAAC;YAC7B,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;YACjC,OAAO;QACT,CAAC;QAED,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACtB,MAAM,EAAE,EAAE,CAAC;YACX,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,MAAM,UAAU,GAAG,MAAoB,CAAC;QACxC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,OAAO,CACL,MAAC,GAAG,eACF,KAAC,OAAO,IAAC,KAAK,EAAE,KAAK,GAAI,EACzB,KAAC,OAAO,IAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,GAAI,IACzC,CACP,CAAC;AACJ,CAAC"}
|
package/dist/ui/Cell.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import { tileGlyph, tileStyle } from './palette.js';
|
|
4
|
+
export function Cell({ tile }) {
|
|
5
|
+
const style = tileStyle(tile);
|
|
6
|
+
return _jsx(Text, { ...style, children: tileGlyph(tile) });
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=Cell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Cell.js","sourceRoot":"","sources":["../../src/ui/Cell.tsx"],"names":[],"mappings":";AACA,OAAO,EAAC,IAAI,EAAC,MAAM,KAAK,CAAC;AACzB,OAAO,EAAC,SAAS,EAAE,SAAS,EAAY,MAAM,cAAc,CAAC;AAM7D,MAAM,UAAU,IAAI,CAAC,EAAC,IAAI,EAAY;IACpC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAE9B,OAAO,KAAC,IAAI,OAAK,KAAK,YAAG,SAAS,CAAC,IAAI,CAAC,GAAQ,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type SokobanState } from '../game/engine.js';
|
|
3
|
+
import { type Tile } from './palette.js';
|
|
4
|
+
export type MapViewProps = {
|
|
5
|
+
state: SokobanState;
|
|
6
|
+
};
|
|
7
|
+
export declare function MapView({ state }: MapViewProps): React.ReactElement;
|
|
8
|
+
export declare function tileAt(state: SokobanState, x: number, y: number): Tile;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { boxIndexAt } from '../game/engine.js';
|
|
4
|
+
import { Cell } from './Cell.js';
|
|
5
|
+
import { ui } from './palette.js';
|
|
6
|
+
export function MapView({ state }) {
|
|
7
|
+
const banner = bannerFor(state);
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Text, { bold: true, color: ui.title, children: ["\u2726 ", state.levelName] }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: ui.frame, paddingX: 1, children: Array.from({ length: state.height }, (_, y) => (_jsx(Box, { children: Array.from({ length: state.width }, (_, x) => (_jsx(Cell, { tile: tileAt(state, x, y) }, `cell-${y}-${x}`))) }, `row-${y}`))) }), banner === undefined ? (_jsx(Text, { children: " " })) : (_jsx(Text, { bold: true, color: banner.color, children: banner.label }))] }));
|
|
9
|
+
}
|
|
10
|
+
export function tileAt(state, x, y) {
|
|
11
|
+
if (state.walls[y][x]) {
|
|
12
|
+
return 'wall';
|
|
13
|
+
}
|
|
14
|
+
const isTarget = state.targets[y][x];
|
|
15
|
+
if (state.player.x === x && state.player.y === y) {
|
|
16
|
+
return isTarget ? 'player-on-target' : 'player';
|
|
17
|
+
}
|
|
18
|
+
if (boxIndexAt(state.boxes, x, y) !== -1) {
|
|
19
|
+
return isTarget ? 'box-on-target' : 'box';
|
|
20
|
+
}
|
|
21
|
+
return isTarget ? 'target' : 'floor';
|
|
22
|
+
}
|
|
23
|
+
function bannerFor(state) {
|
|
24
|
+
switch (state.status) {
|
|
25
|
+
case 'solved':
|
|
26
|
+
return { label: '✦ Solved! — press n for next level', color: ui.ok };
|
|
27
|
+
case 'paused':
|
|
28
|
+
return { label: '❚❚ Paused — press p to resume', color: ui.warn };
|
|
29
|
+
case 'complete':
|
|
30
|
+
return { label: '★ All levels complete! — press q to quit', color: ui.accent };
|
|
31
|
+
default:
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=MapView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MapView.js","sourceRoot":"","sources":["../../src/ui/MapView.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { applyAction } from '../game/engine.js';
|
|
4
|
+
import { ui } from './palette.js';
|
|
5
|
+
const labelWidth = 8;
|
|
6
|
+
export function Sidebar({ state, showHelp }) {
|
|
7
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 3, width: 30, children: [_jsx(Text, { bold: true, color: ui.title, children: "\u25C6 Ink Sokoban" }), _jsx(Text, { dimColor: true, children: '─'.repeat(26) }), _jsx(Stat, { label: "Level", value: `${state.levelIndex + 1}/${state.levelCount}` }), _jsx(Stat, { label: "Moves", value: String(state.moves) }), _jsx(Stat, { label: "Pushes", value: String(state.pushes) }), _jsx(Stat, { label: "Undo", value: String(state.undoCount) }), _jsx(Stat, { label: "Resets", value: String(state.resets) }), _jsx(Text, { children: " " }), _jsx(StatusBanner, { status: state.status }), _jsx(Text, { children: " " }), showHelp ? _jsx(HelpPanel, { state: state }) : _jsx(HintPanel, {})] }));
|
|
8
|
+
}
|
|
9
|
+
function Stat({ label, value }) {
|
|
10
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: label.padEnd(labelWidth) }), _jsx(Text, { bold: true, children: value })] }));
|
|
11
|
+
}
|
|
12
|
+
function StatusBanner({ status }) {
|
|
13
|
+
if (status === 'paused') {
|
|
14
|
+
return (_jsx(Text, { color: ui.warn, bold: true, children: "\u275A\u275A Paused" }));
|
|
15
|
+
}
|
|
16
|
+
if (status === 'solved') {
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: ui.ok, bold: true, children: "\u2726 Solved" }), _jsx(Text, { dimColor: true, children: "Press n for next level" })] }));
|
|
18
|
+
}
|
|
19
|
+
if (status === 'complete') {
|
|
20
|
+
return (_jsx(Text, { color: ui.accent, bold: true, children: "\u2605 All levels complete" }));
|
|
21
|
+
}
|
|
22
|
+
return (_jsx(Text, { color: ui.ok, bold: true, children: "\u25CF Playing" }));
|
|
23
|
+
}
|
|
24
|
+
function HintPanel() {
|
|
25
|
+
return _jsx(Text, { dimColor: true, children: "Hint: ? for controls" });
|
|
26
|
+
}
|
|
27
|
+
function HelpPanel({ state }) {
|
|
28
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: '─'.repeat(26) }), _jsx(Text, { bold: true, children: "Controls" }), _jsx(HelpRow, { keys: "h j k l", action: "Move" }), _jsx(HelpRow, { keys: "\u2190 \u2193 \u2191 \u2192", action: "Move" }), _jsx(HelpRow, { keys: "u", action: "Undo" }), _jsx(HelpRow, { keys: "r", action: "Restart" }), _jsx(HelpRow, { keys: "n", action: "Next level" }), _jsx(HelpRow, { keys: "p", action: "Pause" }), _jsx(HelpRow, { keys: "?", action: "Help" }), _jsx(HelpRow, { keys: "q", action: "Quit" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Legal pushes" }), _jsx(PushHints, { state: state })] }));
|
|
29
|
+
}
|
|
30
|
+
// Shows which directions would currently shove a box (the optional "reachable
|
|
31
|
+
// push directions" hint). A direction lights up only if moving that way is a
|
|
32
|
+
// legal push from the player's current square.
|
|
33
|
+
function PushHints({ state }) {
|
|
34
|
+
const directions = [
|
|
35
|
+
{ label: '↑', action: 'move-up' },
|
|
36
|
+
{ label: '↓', action: 'move-down' },
|
|
37
|
+
{ label: '←', action: 'move-left' },
|
|
38
|
+
{ label: '→', action: 'move-right' }
|
|
39
|
+
];
|
|
40
|
+
const live = directions.filter(({ action }) => applyAction(state, action).pushes > state.pushes);
|
|
41
|
+
if (live.length === 0) {
|
|
42
|
+
return _jsx(Text, { dimColor: true, children: "none from here" });
|
|
43
|
+
}
|
|
44
|
+
return (_jsx(Text, { color: ui.crate, bold: true, children: live.map(({ label }) => label).join(' ') }));
|
|
45
|
+
}
|
|
46
|
+
function HelpRow({ keys, action }) {
|
|
47
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: keys.padEnd(10) }), action] }));
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=Sidebar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Sidebar.js","sourceRoot":"","sources":["../../src/ui/Sidebar.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type InputKey = {
|
|
2
|
+
leftArrow?: boolean;
|
|
3
|
+
rightArrow?: boolean;
|
|
4
|
+
upArrow?: boolean;
|
|
5
|
+
downArrow?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type UiAction = 'move-up' | 'move-down' | 'move-left' | 'move-right' | 'undo' | 'restart' | 'next-level' | 'pause' | 'toggle-help' | 'quit';
|
|
8
|
+
export declare function mapInputToAction(input: string, key: InputKey): UiAction | undefined;
|
package/dist/ui/input.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function mapInputToAction(input, key) {
|
|
2
|
+
if (input === 'k' || key.upArrow) {
|
|
3
|
+
return 'move-up';
|
|
4
|
+
}
|
|
5
|
+
if (input === 'j' || key.downArrow) {
|
|
6
|
+
return 'move-down';
|
|
7
|
+
}
|
|
8
|
+
if (input === 'h' || key.leftArrow) {
|
|
9
|
+
return 'move-left';
|
|
10
|
+
}
|
|
11
|
+
if (input === 'l' || key.rightArrow) {
|
|
12
|
+
return 'move-right';
|
|
13
|
+
}
|
|
14
|
+
if (input === 'u') {
|
|
15
|
+
return 'undo';
|
|
16
|
+
}
|
|
17
|
+
if (input === 'r') {
|
|
18
|
+
return 'restart';
|
|
19
|
+
}
|
|
20
|
+
if (input === 'n') {
|
|
21
|
+
return 'next-level';
|
|
22
|
+
}
|
|
23
|
+
if (input === 'p') {
|
|
24
|
+
return 'pause';
|
|
25
|
+
}
|
|
26
|
+
if (input === '?') {
|
|
27
|
+
return 'toggle-help';
|
|
28
|
+
}
|
|
29
|
+
if (input === 'q') {
|
|
30
|
+
return 'quit';
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.js","sourceRoot":"","sources":["../../src/ui/input.ts"],"names":[],"mappings":"AAmBA,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,GAAa;IAC3D,IAAI,KAAK,KAAK,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;QACjC,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,KAAK,KAAK,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;QACnC,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,IAAI,KAAK,KAAK,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;QACnC,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,IAAI,KAAK,KAAK,GAAG,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QACpC,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;QAClB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;QAClB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;QAClB,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;QAClB,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;QAClB,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;QAClB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type Tile = 'wall' | 'floor' | 'target' | 'box' | 'box-on-target' | 'player' | 'player-on-target';
|
|
2
|
+
export type TileStyle = {
|
|
3
|
+
color?: string;
|
|
4
|
+
backgroundColor?: string;
|
|
5
|
+
bold?: boolean;
|
|
6
|
+
dimColor?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare const ui: {
|
|
9
|
+
readonly accent: "cyan";
|
|
10
|
+
readonly muted: "gray";
|
|
11
|
+
readonly title: "cyan";
|
|
12
|
+
readonly danger: "red";
|
|
13
|
+
readonly warn: "yellow";
|
|
14
|
+
readonly ok: "green";
|
|
15
|
+
readonly frame: "#475569";
|
|
16
|
+
readonly star: "#64748b";
|
|
17
|
+
readonly crate: "#d8a657";
|
|
18
|
+
readonly docked: "#7dcfb6";
|
|
19
|
+
readonly probe: "#e8e8e8";
|
|
20
|
+
};
|
|
21
|
+
export declare function tileGlyph(tile: Tile): string;
|
|
22
|
+
export declare function tileStyle(tile: Tile): TileStyle;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Muted "observatory" palette: deep slate frame, dim starfield targets, a warm
|
|
2
|
+
// amber crate that turns cool green once it settles onto its mark, and a bright
|
|
3
|
+
// probe for the player.
|
|
4
|
+
export const ui = {
|
|
5
|
+
accent: 'cyan',
|
|
6
|
+
muted: 'gray',
|
|
7
|
+
title: 'cyan',
|
|
8
|
+
danger: 'red',
|
|
9
|
+
warn: 'yellow',
|
|
10
|
+
ok: 'green',
|
|
11
|
+
frame: '#475569',
|
|
12
|
+
star: '#64748b',
|
|
13
|
+
crate: '#d8a657',
|
|
14
|
+
docked: '#7dcfb6',
|
|
15
|
+
probe: '#e8e8e8'
|
|
16
|
+
};
|
|
17
|
+
// Every tile renders as two terminal columns so the map keeps a roughly square
|
|
18
|
+
// aspect ratio (same trick the tetris well uses).
|
|
19
|
+
const GLYPHS = {
|
|
20
|
+
wall: '██',
|
|
21
|
+
floor: ' ',
|
|
22
|
+
target: '· ',
|
|
23
|
+
box: '◇ ',
|
|
24
|
+
'box-on-target': '◆ ',
|
|
25
|
+
player: '☻ ',
|
|
26
|
+
'player-on-target': '☻ '
|
|
27
|
+
};
|
|
28
|
+
const STYLES = {
|
|
29
|
+
wall: { color: ui.frame },
|
|
30
|
+
floor: {},
|
|
31
|
+
target: { color: ui.star, dimColor: true },
|
|
32
|
+
box: { color: ui.crate, bold: true },
|
|
33
|
+
'box-on-target': { color: ui.docked, bold: true },
|
|
34
|
+
player: { color: ui.probe, bold: true },
|
|
35
|
+
'player-on-target': { color: ui.docked, bold: true }
|
|
36
|
+
};
|
|
37
|
+
export function tileGlyph(tile) {
|
|
38
|
+
return GLYPHS[tile];
|
|
39
|
+
}
|
|
40
|
+
export function tileStyle(tile) {
|
|
41
|
+
return STYLES[tile];
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=palette.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"palette.js","sourceRoot":"","sources":["../../src/ui/palette.ts"],"names":[],"mappings":"AAgBA,+EAA+E;AAC/E,gFAAgF;AAChF,wBAAwB;AACxB,MAAM,CAAC,MAAM,EAAE,GAAG;IAChB,MAAM,EAAE,MAAM;IACd,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,MAAM;IACb,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,QAAQ;IACd,EAAE,EAAE,OAAO;IACX,KAAK,EAAE,SAAS;IAChB,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,MAAM,EAAE,SAAS;IACjB,KAAK,EAAE,SAAS;CACR,CAAC;AAEX,+EAA+E;AAC/E,kDAAkD;AAClD,MAAM,MAAM,GAAyB;IACnC,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;IACZ,GAAG,EAAE,IAAI;IACT,eAAe,EAAE,IAAI;IACrB,MAAM,EAAE,IAAI;IACZ,kBAAkB,EAAE,IAAI;CACzB,CAAC;AAEF,MAAM,MAAM,GAA4B;IACtC,IAAI,EAAE,EAAC,KAAK,EAAE,EAAE,CAAC,KAAK,EAAC;IACvB,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAC,KAAK,EAAE,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAC;IACxC,GAAG,EAAE,EAAC,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAC;IAClC,eAAe,EAAE,EAAC,KAAK,EAAE,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAC;IAC/C,MAAM,EAAE,EAAC,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAC;IACrC,kBAAkB,EAAE,EAAC,KAAK,EAAE,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAC;CACnD,CAAC;AAEF,MAAM,UAAU,SAAS,CAAC,IAAU;IAClC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAU;IAClC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@terminalgames/ink-sokoban",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Terminal Sokoban built with Ink.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/cli.js",
|
|
7
|
+
"types": "./dist/cli.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"ink-sokoban": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"prepublishOnly": "npm run build && npm test",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"cli",
|
|
29
|
+
"ink",
|
|
30
|
+
"sokoban",
|
|
31
|
+
"puzzle",
|
|
32
|
+
"game",
|
|
33
|
+
"terminal"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"ink": "^7.0.4",
|
|
38
|
+
"react": "^19.2.6"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.9.1",
|
|
42
|
+
"@types/react": "^19.2.15",
|
|
43
|
+
"ink-testing-library": "^4.0.0",
|
|
44
|
+
"typescript": "^6.0.3",
|
|
45
|
+
"vitest": "^4.1.7"
|
|
46
|
+
}
|
|
47
|
+
}
|