Metadata-Version: 2.4
Name: vimny
Version: 1.0.0
Summary: A TUI dungeon crawler that teaches Vim through play.
Author: Chas Kissick
License: GPL-3.0-or-later
Project-URL: Homepage, https://github.com/chkiss/Vimny
Project-URL: Repository, https://github.com/chkiss/Vimny
Project-URL: Issues, https://github.com/chkiss/Vimny/issues
Project-URL: Level shelf, https://github.com/chkiss/vimny-levels
Keywords: vim,game,dungeon-crawler,tui,teaching
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console :: Curses
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Games/Entertainment :: Puzzle Games
Classifier: Topic :: Education
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS :: MacOS X
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: blessed
Dynamic: license-file

# Vimny

**A dungeon crawler where the dungeons are text buffers and every puzzle is solved with real Vim commands.**

The floor is made of characters. Walls are the ends of lines. A door opens for the word you cut out of the floor and carried there.

Every room has a **keystroke budget** to enforce efficiency, and a **par** — the cheapest route that exists. Finishing wins the room; finishing *at par* means you found the way a fluent Vim user would have and fully learned the lesson taught at each dungeon.

![A terminal playing The Character Cataracts. The dungeon floor is written text — "Most files you encounter will be scribed in letters so you can jump quite easily to anything you can type!" — split by rivers of water. The cursor hops from character to character with f, F, t and T, aims at the wrong letter, presses u to snap back (and watches the keystroke count go down again), then reaches the exit at 21 keystrokes against a par of 19.](docs/media/character-cataracts.gif)

*The Character Cataracts, the fourth level. `f`/`F`/`t`/`T` jump to a character you can see; the water is impassable, so you aim rather than walk. This run misfires once, takes it back with `u`, and clears the room at 21 against a par of 19 — a win, but only one of the two stars.*

## How it works

The map is a buffer.

Editing reflows the line exactly as Vim does: insert, delete or paste and the rest of the row shifts along, and whatever gets shoved past a wall falls into the void. Cut a word out of the floor and the floor closes up behind it.

| Dungeon concept          | Vim concept |
|--------------------------|-------------|
| Floor cells              | Characters  |
| Character runs           | Words       |
| Empty floor between runs | Whitespace  |
| Room row                 | Line        |
| Room wall                | End of line |
| Dungeon                  | File        |

**Keystroke budget**: Every puzzle room displays a budget. Reaching the exit within it completes the room. The par is the minimum possible keystrokes using the level's taught commands — hitting par earns a second star. `u` (undo) returns budget; you can backtrack freely.

> **Note — `u` undoes motions, which Vim's does not.** In Vim, undo is for *changes*: it will not walk your cursor back from a `4j`. In Vimny it will, refunding the keystrokes with it. This is a deliberate break with faithfulness, and the only one in the movement keys — a game that charges you for every keypress has to let you take a wrong turn back, or exploring a room becomes something you pay for and learners stop trying things. Everything `u` does to *edits* is Vim's behaviour, undo stack and all.

> **Note — par is not the absolute minimum on search levels.** On levels that use search (`/`, `?`), par is computed assuming you type the **full search term** the level highlights (e.g. `/cipher<CR>`). Because a search pattern only needs enough characters to land uniquely on the target, an expert can type a shorter prefix (e.g. `/cip<CR>`) and finish *under* par. This is intentional; par matches playing all puzzles all the way through, but if you can search a partial term or guess the answer to a puzzle without playing it out, you may be able to beat par on some levels.

**Terrain**: Levels use terrain to make a particular Vim command the *only* good answer, rather than merely the intended one.

- **Void runes** — holes in the floor. Landing on one costs 1 HP, but count motions pass *through* them silently: only the final landing cell bites. So a void rune bars stepping, never jumping.
- **Water** — impassable on foot; you have to jump over it.
- **Fogged water** — impassable on foot *and* obscures your vision, so a blind jump like `$` won't clear it. You have to aim at a character you can see.

## Play it

Requirements are just Linux or macOS, Python 3.9+, and a terminal at least 80 columns wide. Windows is untested.

```bash
uvx vimny
```

That needs nothing installed but [uv](https://docs.astral.sh/uv/), which will fetch Python itself if you haven't got one. Or, if you'd rather it stayed on your machine:

```bash
pipx install vimny          # or: pip install vimny
vimny
```

On macOS and Linux, Homebrew works too, and doesn't need Python at all:

```bash
brew install chkiss/tap/vimny
```

Progress saves to `~/.Vimny/saves/<player>.json`, one file per player.

<details>
<summary>Playing from a clone</summary>

If you want to read or change the code, skip the packaged install:

```bash
git clone --depth 1 https://github.com/chkiss/Vimny.git
cd Vimny
pip install blessed
python3 main.py
```

`--depth 1` is worth it — the full history is 13 MB against a 2 MB working tree.
(You'll want the whole history if you intend to send a pull request.)

</details>

<details>
<summary>Terminal size and Windows</summary>

The playfield grows with the window up to **189 columns** — the overworld and
The Archivist's Library use the extra width — and stops widening beyond that.
80 columns is the supported minimum.

Windows is untested rather than unsupported: Vimny reaches the console through
blessed and jinxed, which ought to work in Windows Terminal, but nobody has
confirmed it. There's a [Scoop](https://scoop.sh) manifest in `packaging/` if
you'd like to be the one who finds out — `scoop install chkiss/vimny`. Please
[open an issue](https://github.com/chkiss/Vimny/issues) either way.

</details>

## Levels

The main sequence of sixty levels is complete. The curriculum runs from `hjkl` through motions and counts, operators and text objects, visual mode, search and macros, registers, marks and jumps, to Ex commands (`:s`, `:g`) — with boss levels that make you use the lot at once, and a bonus wing for the corners of Vim that don't fit a straight line.

Each level teaches one thing and is built so that thing is the *cheapest* way through, not merely the intended one.

<details>
<summary>The full curriculum — all 60 levels</summary>

<!-- BEGIN GENERATED LEVELS TABLE -->
| # | Name | Commands |
|---|---|---|
| 0 | The First Cave | `h j k l u :w :q :q!` |
| 1 | The Line Halls | `^ $ 0` |
| 1.1 | The Reliquary | `x` |
| 2 | The Counting Crypts | `[count] prefix` |
| 3 | The Rune Halls | `w b e` |
| 4 | The Character Cataracts | `f F t T` |
| 5 | The Goblin Gauntlet | `; , p` |
| 5.1 | The Warden's Keep | (boss) |
| 6 | The WORD Forge | `W B E` |
| 7 | The Backward Vaults | `ge gE` |
| 8 | The Lineheads | `G gg` |
| 9 | The Screen Vault | `H M L` |
| 10 | The Bracket Vaults | `%` |
| 12 | The Runic Archives | `} {` |
| 13 | The Sentence Corridor | `) (` |
| 13.1 | The Warden Surveyor | (boss) |
| 14 | The Seekers' Labyrinth | `/ ? n N * #` |
| 14.1 | The Binder's Reliquary | `:h za :q` |
| 15 | The Waypoint Sanctum | `` m ' ` `` |
| 16 | The Archivist's Library | `:set wrap  :e!  :w {file}` |
| 16.1 | The Warden Pathfinder | (boss) |
| 17 | The Operator's Vault | `d{m}  dd` |
| 18 | The Cipher Cell | `r  D  X` |
| 19 | The Beacon Tiers | `y yy P` |
| 20 | The Echo Vault | `.` |
| 20.1 | The Warden Manifold | (boss) |
| 21 | The Inscription Halls | `i a` |
| 22 | The Change Annex | `c{m}  cE  cc  s` |
| 23 | The Change Extension | `S  C  Y` |
| 24 | The Sculpting Chambers | `I A o O` |
| 25 | The Overwrite Halls | `R` |
| 26 | The Case Chambers | `~ g~ gU gu` |
| 27 | The Joiner's Gate | `J gJ` |
| 28 | The Alignment Halls | `>> <<` |
| 29 | The Indentation Sanctum | `>{m} <{m} =` |
| 29.1 | The Warden Scrivener | (boss) |
| 30 | The Sight Sanctum | `v {m} d/c/~` |
| 31 | The Selection Halls | `V  <C-v>` |
| 32 | The Word Enclosure | `iw aw iW aW` |
| 33 | The Bracket Enclosure | `i( a(` |
| 34 | The Brace & Square Enclosure | `i[ a[ i{ a{` |
| 35 | The Quote Enclosure | `i" a" i' a'` |
| 36 | The Tag Enclosure | `it at` |
| 37 | The Sentence Enclosure | `is as` |
| 38 | The Paragraph Enclosure | `ip ap` |
| 38.1 | The Grandmaster's Sanctum | (boss) |
| 39 | The Spellwright's Forge | `:s///  :g  &` |
| 40 | The Culling Ledger | `:d _ :a,bd :v//d` |
| 41 | The Shelving Room | `:m :t :> :<` |
| 42 | The Refrain Vault | `& :&& :j :y` |
| 43 | The Stair Rail | `+ - _` |
| 44 | The Last Reach | `g_ g* gi gp` |
| 45 | The Buried Word | `g* n` |
| 46 | The Wet Ink | `gi` |
| 47 | The Hall of Echoes | `q @ "` |
| 48 | The Gauntlet | — |
| 48.1 | The Warden Eternal | (boss) |
| R1 | The Unnamed Hold | `""  y  p` |
| R2 | The Named Vault | `"ay  "by  "aP  "bP` |
<!-- END GENERATED LEVELS TABLE -->

</details>

## Commands

The full command reference (also the hint-bar source) is `vimny/render/vim_commands.md`; this table mirrors it.

<details><summary>Show all commands</summary>

<!-- BEGIN GENERATED COMMANDS TABLE -->
| Command | Effect |
|---|---|
| `u` | undo |
| `:w` | write (save) |
| `:q` | quit |
| `:q!` | quit without saving |
| `h` | left |
| `j` | down |
| `k` | up |
| `l` | right |
| `0` | line start |
| `^` | first non-blank |
| `$` | end of line |
| `x` | delete char |
| `[N]hjkl` | count move |
| `w` | word start |
| `b` | word back |
| `e` | word end |
| `f{c}` | jump to char |
| `F{c}` | jump back to char |
| `t{c}` | before next char |
| `T{c}` | after prev char |
| `;` | repeat |
| `,` | reverse |
| `p` | paste |
| `W` | WORD start |
| `B` | WORD back |
| `E` | WORD end |
| `ge` | word-end back |
| `gE` | WORD-end back |
| `G` | last line |
| `gg` | first line |
| `[N]G` | go to line N |
| `H` | top of screen |
| `M` | middle of screen |
| `L` | bottom of screen |
| `%` | match bracket |
| `}` | next block |
| `{` | prev block |
| `)` | next sentence |
| `(` | prev sentence |
| `v` | visual mode |
| `v{m} d/c/~/p/r/J` | act on the selection |
| `V` | select whole lines |
| `<C-v>` | select a block |
| `:h {name}` | open the Codex to a page |
| `za` | unfold / fold a section |
| `:q` | close the book |
| `/{pat}` | search |
| `?{pat}` | search back |
| `n` | next match |
| `N` | prev match |
| `*` | search word |
| `#` | search word back |
| `m{a}` | set mark |
| `` `{a} `` | to mark |
| `'{a}` | to mark ↑ |
| `:set wrap` | wrap lines |
| `:e!` | reload file |
| `:w {file}` | save as |
| `d{m}  dd` | delete |
| `c{m}` | change |
| `cc` | change line |
| `r{c}` | replace char |
| `D` | delete to line end |
| `X` | delete before cursor |
| `c{m}  cc` | change |
| `s` | substitute |
| `S` | substitute line |
| `C` | change to end |
| `Y` | yank line |
| `y{m}  yy` | yank |
| `P` | paste before |
| `.` | repeat change |
| `i` | insert |
| `a` | append |
| `Esc` | exit insert |
| `I` | insert at start |
| `A` | append at end |
| `o` | new line below |
| `O` | new line above |
| `R` | replace mode |
| `~` | toggle case |
| `gU{m}` | uppercase |
| `gu{m}` | lowercase |
| `g~{m}` | toggle case |
| `J` | join lines |
| `gJ` | join, no space |
| `>{m}` | indent |
| `<{m}` | dedent |
| `={m}` | apply the law |
| `iw` | inner word |
| `aw` | a word |
| `iW` | inner WORD |
| `aW` | a WORD |
| `i(` | inner ( |
| `a(` | a () |
| `i[` | inner [ |
| `a[` | a [] |
| `i{` | inner { |
| `a{` | a {} |
| `i"` | inner " |
| `a"` | a "" |
| `i'` | inner ' |
| `a'` | a '' |
| `it` | inner tag |
| `at` | a tag |
| `is` | inner sentence |
| `as` | a sentence |
| `ip` | inner paragraph |
| `ap` | a paragraph |
| `:s/old/new/` | substitute |
| `:%s//g` | substitute all |
| `:g/pat/d` | global delete |
| `&` | repeat last :s |
| `:{n}d` | delete line n |
| `:{a},{b}d` | delete range |
| `:{r}v//d` | keep matching |
| `:d _` | cut, keep reg |
| `q{a}` | record macro |
| `@{a}` | play macro |
| `@@` | repeat macro |
| `"{a}` | named reg |
| `+` | down, first word |
| `{n}_` | to line n below |
| `g_` | last non-blank |
| `g*` | search substring |
| `g#` | substring back |
| `gi` | resume inserting |
| `gp` | paste, cursor after |
<!-- END GENERATED COMMANDS TABLE -->

</details>

### What Vim commands does Vimny not teach?

Vimny aims for Vim-faithfulness in everything it *does* implement, but some commands are deliberately out of scope:

- **Scrolling & viewport** — `zz` `zt` `zb` `<C-d>` `<C-u>` `<C-f>` `<C-b>` `<C-e>` `<C-y>`: dungeons fit the screen; there is no viewport-scroll model (`H`/`M`/`L` are the only screen-relative commands).
- **`U` (vi's line-undo)** — `u` and the redo scroll (`<C-r>`) cover the undo story; a third undo channel would complicate it for a key modern Vim users rarely reach for.
- **Window/tab/buffer management** — Vimny is a single buffer by design; each dungeon *is* the file. On the roadmap, not in the curriculum.
- **Insert-mode editing keys** — `<C-w>`, `<C-u>`, `<C-o>`, `<C-r>{reg}` are implemented and can be found as scrolls, but no level *teaches* them. They are priced to be free (`<C-w>` and `<C-u>` cost nothing; `<C-r>` charges per pasted character, exactly what typing the text would cost), so no puzzle can force them at par — which is what a Vimny level does. Pricing them by keystroke instead would make a register paste cheaper than typing and hand every text-entry level a shortcut, so they stay free flourishes rather than curriculum.
- **Completion, plugins, ex-mode scripting** — out of scope.
- **NORMAL-mode `Enter`** — a duplicate of `+`, which the Stair Rail
  already teaches.

## Working on Vimny

<details>
<summary>Project layout</summary>

```
main.py                    Launcher — `python3 main.py` and nothing else
vimny/                     Everything importable lives under one package
  game.py                  Game loop, run_dungeon / run_overworld, the forge
  engine/
    world.py               Room, Dungeon, Entity, CharRun, CellType, Seal
    player.py              Player dataclass
    vim_parser.py          Keystroke → action dict
    command_guard.py       action_allowed — what the curriculum has taught yet
    motion.py              apply_motion, move_player, the fog laws
    operator.py            d y c p and friends — operator + text object
    text_object.py         iw aw i( a" ip … — the spans an operator acts on
    insert.py              i a I A o O s S, INSERT-mode editing
    reflow.py              Reflow editing primitives (insert/delete/join/ledge-build)
    visual.py              v V <C-v> — the selections
    search.py              / ? n N * # — Vim-regex search, matched per line
    substitute.py          :s :g :v & — ex substitute & global
    registers.py           named/unnamed registers, clip ↔ text
    macro.py               q @ — record and replay
    jumplist.py            <C-o> <C-i> — where you have been
    tape.py                The keystroke-tape notation (<Space> <CR> <Esc> <C-v>)
    budget.py              Budget tracking
  generation/
    dungeon_gen.py         build_dungeon_<slug> per level, par solvers
  content/
    levels.py              Level definitions (slug identity), known_commands(slug)
    scrolls.py             Scroll text + the scroll catalogue
    passwords.py           The password pools a fancy_door opens for
  render/
    renderer.py            Read-only dungeon view (no mutation)
    overworld.py           Read-only netrw overworld buffer
    title.py               Title screen and name prompt
    scroll_library.py      The scrolls you have collected
    remote_shelf.py        Browse the remote level shelf
    symbols.py             Every glyph the game draws, with width fallbacks
    hint_bar.py            Hint-bar text (reads vim_commands.md)
    vim_commands.md        Hint-bar text source (token → keys/desc)
  sharing/                 Levels as DATA — the authoring/sharing pipeline
    format.py              The level file format: parse, build, export
    validate.py            Every rule a level file must satisfy
    draft.py               The forge's in-progress level
    replay.py              Replay a keystroke tape through the real game loop
    jumpgolf.py            Does a line jump beat a tape's travel? (par audit)
    remote.py              The one place Vimny makes a network request
    submit.py              :submit — a prefilled pull request link for your browser
    cli.py                 python3 -m vimny.sharing — validate / audit / export / …
  save/
    save_manager.py        Progress I/O, layout save
  art/                     Wizard art, poems, the word pools
  tools/                   Offline audits (cheese, par)
tests/                     pytest test suite
packaging/                 Release runbook, Homebrew formula, Scoop manifest
docs/ARCHITECTURE.md       The canonical reference: architecture, laws, conventions
docs/AUTHORING.md          Writing a level, in the forge or in an editor
docs/SPEC.md               Design vision, UI spec, forward-looking notes
docs/LEVELS_PLAN.md        Design rubric + the levels not yet built
docs/blueprints/           Design docs for wings not yet built
```

</details>

Run the tests with `python3 -m pytest`, and `python3 -m vimny.sharing audit` to check every level's par against a replay of its own solution.

## Writing your own levels

There are two ways in, and they produce the same thing — a level is a plain JSON file either way.

**In the game — the forge.** An authoring bench in the overworld under `forge/`, where a level is built by playing it: paint the room, place the text and the doors, then `:record` walks your own solution and captures it as the level's answer. The par comes from replaying that recording, so a level cannot ship claiming a route nobody has walked. The forge is **admin-only** — sign in with the player name `admin` to reach it. Be warned that the same name also unlocks every level and shows you each puzzle's solution as you play, so use a separate save for authoring rather than the one you are playing on.

**In a text editor.** The format is documented, so you never have to use the forge:

```
python3 -m vimny.sharing export rune_halls mylevel.json   # start from a working level
python3 -m vimny.sharing validate mylevel.json            # check it
python3 -m vimny.sharing install  mylevel.json            # put it on your shelf
```

Either way, drop the file in `~/.Vimny/levels/` and it shows up in the overworld under `community/`.

**The shelf.** Community levels live at **[github.com/chkiss/vimny-levels](https://github.com/chkiss/vimny-levels)** — type `:e remote` in the overworld to browse what's there and install any of them without leaving the game. To add yours, open a pull request against that repo with your level file; it is checked by the same validator you can run yourself (`python3 -m vimny.sharing validate mylevel.json`), so if it passes locally it will pass there.

Two things worth knowing before you install a level someone else wrote:

- **A level is data, not code.** Vimny reads the file and builds a room from it. Nothing in it is ever executed — that is what makes it safe to play, rather than anyone having vetted it.
- **Vimny goes online only when you ask it to, in one place.** Nothing is fetched at startup, in the background, or on a timer: there is no phone-home, no telemetry, and no update check. The single exception is the **remote shelf** — type `:e remote` in the overworld and Vimny fetches a public index of community levels over HTTPS so you can browse and install them.

A community level's par comes from replaying the author's own solution, so it is labelled *author's par* — the cost of a route that definitely works, not a promise that no shorter one exists. Nobody gets to type in their own par or budget.

Full guide: [docs/AUTHORING.md](docs/AUTHORING.md).

## Upcoming features

- **The Registry** — a bonus wing on the register family. The yank register (`"0`) and the black hole (`"_`) already work; still to come are the delete ring (`"1`–`"9`), the small-delete register (`"-`), the read-only registers (`":` `".` `"%` `"#`), the expression register (`"=`), the system clipboard (`"*` `"+`), the search register (`"/`), and a boss to close it out. The first two levels are in.
- **Folds** — `za` opens and shuts a section of the codex (`:h`), but nothing folds the dungeon itself. `zf` and a level built around it are the work.
- **Windows, tabs and buffers** — Vimny is one buffer per dungeon today; multi-buffer play is on the roadmap.
- **Alternate levels** — a community level may already declare itself a replacement for a shipped one, and the validator holds it to that slug's exact lesson. Nothing offers the swap to a player yet.

## Design principles

- **Vim fidelity above all else.** Commands behave exactly as they do in Vim.
- **Efficiency is enforced by par and budget.** The keystroke budget makes Vim's core value proposition central, and the par encourages the player to strive for perfect execution efficiency.
- **Everything is a buffer.** Dungeons are files; the overworld is a directory; `:w`, `:q`, `:e` are real mechanics.

See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) if you want to work on the engine — it is the canonical reference for the architecture, the laws the levels are held to, and the conventions. `docs/LEVELS_PLAN.md` is what's planned next, and `docs/SPEC.md` the design vision & UI.

## License

Vimny is free software, licensed under the **GNU General Public License v3.0** — see [`LICENSE`](LICENSE) for the full text.
