@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 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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ export type RunCliOptions = {
3
+ level?: number;
4
+ };
5
+ export declare function runCli(options?: RunCliOptions): void;
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
@@ -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":"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"}
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ export type AppProps = {
3
+ initialLevel?: number;
4
+ onExit?: () => void;
5
+ };
6
+ export declare function App({ initialLevel, onExit }: AppProps): React.ReactElement;
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"}
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { type Tile } from './palette.js';
3
+ export type CellProps = {
4
+ tile: Tile;
5
+ };
6
+ export declare function Cell({ tile }: CellProps): React.ReactElement;
@@ -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":";AACA,OAAO,EAAC,GAAG,EAAE,IAAI,EAAC,MAAM,KAAK,CAAC;AAC9B,OAAO,EAAC,UAAU,EAAoB,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAC;AAC/B,OAAO,EAAC,EAAE,EAAY,MAAM,cAAc,CAAC;AAM3C,MAAM,UAAU,OAAO,CAAC,EAAC,KAAK,EAAe;IAC3C,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAEhC,OAAO,CACL,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,EAAC,UAAU,EAAE,CAAC,aACvC,MAAC,IAAI,IAAC,IAAI,QAAC,KAAK,EAAE,EAAE,CAAC,KAAK,wBACrB,KAAK,CAAC,SAAS,IACb,EACP,KAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,EAAC,WAAW,EAAC,OAAO,EAAC,WAAW,EAAE,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,YAC/E,KAAK,CAAC,IAAI,CAAC,EAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAC5C,KAAC,GAAG,cACD,KAAK,CAAC,IAAI,CAAC,EAAC,MAAM,EAAE,KAAK,CAAC,KAAK,EAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAC3C,KAAC,IAAI,IAAwB,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAA3C,QAAQ,CAAC,IAAI,CAAC,EAAE,CAA+B,CAC3D,CAAC,IAHM,OAAO,CAAC,EAAE,CAId,CACP,CAAC,GACE,EACL,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CACtB,KAAC,IAAI,oBAAS,CACf,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,IAAI,QAAC,KAAK,EAAE,MAAM,CAAC,KAAK,YAC3B,MAAM,CAAC,KAAK,GACR,CACR,IACG,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAmB,EAAE,CAAS,EAAE,CAAS;IAC9D,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,CAAC;IAEvC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,OAAO,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,QAAQ,CAAC;IAClD,CAAC;IAED,IAAI,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QACzC,OAAO,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC;IAC5C,CAAC;IAED,OAAO,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;AACvC,CAAC;AAED,SAAS,SAAS,CAAC,KAAmB;IACpC,QAAQ,KAAK,CAAC,MAAM,EAAE,CAAC;QACrB,KAAK,QAAQ;YACX,OAAO,EAAC,KAAK,EAAE,oCAAoC,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,EAAC,CAAC;QACrE,KAAK,QAAQ;YACX,OAAO,EAAC,KAAK,EAAE,+BAA+B,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,EAAC,CAAC;QAClE,KAAK,UAAU;YACb,OAAO,EAAC,KAAK,EAAE,0CAA0C,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,EAAC,CAAC;QAC/E;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC"}
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import { type SokobanState } from '../game/engine.js';
3
+ export type SidebarProps = {
4
+ state: SokobanState;
5
+ showHelp: boolean;
6
+ };
7
+ export declare function Sidebar({ state, showHelp }: SidebarProps): React.ReactElement;
@@ -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;
@@ -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
+ }