npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sokoban-tui

v0.1.0

Published

A Sokoban puzzle game for your terminal, with 245 classic levels

Readme

sokoban-tui

A Sokoban puzzle game for your terminal, with 245 classic levels.

Built with Ink — colored block tiles, full-screen alternate buffer, undo/redo, saved progress, a built-in solver for hints, and deadlock warnings.

                    Microban  ·  level 37 of 155  ·  #37 overall

                        ╭──────────────────────────────╮
                        │  ████████████                │
                        │  ██··    ████                │
                        │  ██  ▨▨     ██               │
                        │  ████    ▨▨ ██               │
                        │    ██ ●●  ··██               │
                        │    ██████████                │
                        ╰──────────────────────────────╯

                     moves 42   pushes 11   boxes 1/2   best 38/9

              ←↑↓→ hjkl move  u undo  r restart  i hint  esc menu  ? help

Install

Requires Node 22 or newer (Ink 7 is ESM-only and needs it).

npm install -g sokoban-tui
sokoban

Or run it without installing:

npx sokoban-tui

The published package is two bundled files with zero runtime dependencies — 828 KB installed, no dependency tree to resolve.

From source

npm install
npm run dev          # play straight from source

npm run build        # produce bundle/
npm link             # then `sokoban` runs the bundle

Controls

| Key | | | --- | --- | | ← ↑ ↓ → / h j k l | Walk. Walking into a box pushes it. | | u / U | Undo / redo | | r / R | Restart level / undo a restart | | i | Hint — press again for more, again to play it | | z | Compact mode (half-width tiles) | | esc | Pause menu | | ? | Help | | q | Quit |

Options

sokoban --level 42            Start at level 42 (1-245 overall)
sokoban --pack original       microban | original
sokoban --ascii               Plain ASCII tiles (# $ * @) instead of blocks
sokoban --no-color            Disable colour (also honours NO_COLOR)
sokoban --no-save             Play without writing progress
sokoban --config-dir <path>   Override where settings and progress live
sokoban --credits             Print level attribution

Progress and settings are stored in $XDG_CONFIG_HOME/sokoban-cli (which is ~/.config/sokoban-cli on macOS).

The levels

| Pack | Levels | Author | | --- | --- | --- | | Microban | 155 | David W. Skinner | | Original & Extra | 90 | Thinking Rabbit (1982) |

Microban comes first because Skinner designed it as a gentle on-ramp; the Thinking Rabbit classics follow. See NOTICE.md for attribution and licensing — the level data is not covered by this project's MIT licence.

Levels are regenerated from the cached upstream files with:

npm run levels:build         # parse, validate, emit src/levels/generated/
npm run solutions:bake       # precompute solutions used by the hint system

How it works

A few decisions worth knowing if you're reading the source.

The engine is pure. src/core/ and src/render/ import no React, no Ink, and no node:fs. Rules, board painting, and search are functions over plain data, unit-tested with no terminal involved.

Terrain is split from state. A compiled level holds immutable typed arrays for walls and goals; only the player position and ~30 box indices change per move. The grid is padded with a wall border and everything outside the reachable region is sealed, so the move hot path needs no bounds checks.

Undo is two bits per move. A move is (direction, was-it-a-push), and the inverse is fully determined by that, so history never stores boards. It also serializes to standard LURD notation for free, which is what powers replay, stored best runs, and the baked solutions.

The board is painted to strings, not components. One Ink <Text> per row carrying pre-built ANSI, rather than a component per tile — a <Box> per tile would create a Yoga layout node per cell. Ink measures text through string-width, which strips ANSI first, so embedded colour is zero-width and this is a supported path rather than a trick.

Effects are data. An effect is a pure function of (elapsed, cell) → emphasis that writes into a per-cell buffer the painter already consults, so adding one never touches the React tree. The shared animation timer only runs while something is actually animating.

One component owns the keyboard. Ink's useInput fires in every mounted component, which silently breaks overlays. Screens and overlays register with a focus-layer stack (src/ui/input/) and only the topmost one is active. Raw useInput is confined to useKeyLayer.ts; there is a regression test that opens the pause menu, presses an arrow, and asserts the board did not move.

The solver searches push space. Nodes are box configurations with the player normalized to the lowest cell it can reach, which collapses all non-pushing wandering into one node. Two things earn their keep: a greedy box→goal matching heuristic (summing each box's distance to its nearest goal badly underestimates when boxes compete for one goal room — adding matching took Original #1 from "unsolved at 300k nodes" to "solved in 23k"), and pruning any push that freezes a box off-goal.

What the hint system can and can't do

Solutions are precomputed offline (npm run solutions:bake) and shipped in src/levels/generated/solutions.ts. Current coverage:

| Pack | Solved offline | | --- | --- | | Microban | 152 / 155 | | Original & Extra | 7 / 90 |

Microban is where hints genuinely work — those boards have ≤6 boxes and solve in milliseconds, live as well as baked. The Original set mostly does not: those levels have 10–30 boxes packed into a single goal room, and cracking them needs corral pruning and tunnel macros, which is research-grade work this solver deliberately doesn't attempt. All 245 levels are published, human-verified puzzles and are solvable — the solver just can't prove it for most of the classics.

So the hint UX degrades in a defined order: use the baked line if the player is still on it (instant), otherwise search live for 5s with a cancellable spinner, otherwise say plainly that it found nothing rather than pretending. Pruning soundness is tested — every baked solution is replayed and asserted never to pass through a position the search would have discarded.

Development

npm test             # 113 tests
npm run typecheck
npm run build        # esbuild -> bundle/cli.js + bundle/worker.js
npm run smoke        # exercise the shipped bundle, isolated from node_modules

npm run build produces the artifact that ships. Three details in scripts/bundle.mjs were each found by the bundle breaking, and are commented there: react-devtools-core must be stubbed (it is an uninstalled optional peer of ink), a createRequire banner is needed because a CJS dependency calls require() at runtime, and the solver worker must stay a separate file so new URL('./worker.js', import.meta.url) still resolves. Do not add a shebang to the banner — esbuild already carries over the one from src/cli.tsx.

The test suite covers the rules (including the object-identity invariant that an illegal move returns the same state, which is what lets React.memo skip re-renders), the full 245-level corpus, row widths across every zoom and glyph set, persistence corruption handling, the solver, and the UI input routing.

Note on imports: moduleResolution is node16, so every relative import needs a .js extension even in .ts source — import { step } from './core/rules.js'. This trips everyone up once.

Licence

MIT for the code — see LICENSE. The bundled levels are covered separately; see NOTICE.md.