A TUI dungeon crawler that teaches Vim through play.
Project description
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.
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 —
uundoes motions, which Vim's does not. In Vim, undo is for changes: it will not walk your cursor back from a4j. 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. Everythingudoes 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.
uvx vimny
That needs nothing installed but uv, which will fetch Python itself if you haven't got one. Or, if you'd rather it stayed on your machine:
pipx install vimny # or: pip install vimny
vimny
On macOS and Linux, Homebrew works too, and doesn't need Python at all:
brew install chkiss/tap/vimny
Progress saves to ~/.Vimny/saves/<player>.json, one file per player.
Playing from a clone
If you want to read or change the code, skip the packaged install:
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.)
Terminal size and Windows
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 manifest in packaging/ if
you'd like to be the one who finds out — scoop install chkiss/vimny. Please
open an issue either way.
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.
The full curriculum — all 60 levels
| # | 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 |
Commands
The full command reference (also the hint-bar source) is vimny/render/vim_commands.md; this table mirrors it.
Show all commands
| 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 |
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 —
zzztzb<C-d><C-u><C-f><C-b><C-e><C-y>: dungeons fit the screen; there is no viewport-scroll model (H/M/Lare the only screen-relative commands). U(vi's line-undo) —uand 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
Project layout
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
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 — 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 remotein 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.
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 —
zaopens and shuts a section of the codex (:h), but nothing folds the dungeon itself.zfand 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,:eare real mechanics.
See 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 for the full text.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vimny-1.0.0.tar.gz.
File metadata
- Download URL: vimny-1.0.0.tar.gz
- Upload date:
- Size: 661.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3bf835c0ff0c03fe29567cd92675be3ed18fa186581b0e0fde667c4a5796b8f2
|
|
| MD5 |
d56d36d87685791fc1d49ed2ca2bb953
|
|
| BLAKE2b-256 |
c18ecb8cbff8710b2ab8727fab00a8196b46791354640b4b344eebe08ed3b1b2
|
Provenance
The following attestation bundles were made for vimny-1.0.0.tar.gz:
Publisher:
release.yml on chkiss/Vimny
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vimny-1.0.0.tar.gz -
Subject digest:
3bf835c0ff0c03fe29567cd92675be3ed18fa186581b0e0fde667c4a5796b8f2 - Sigstore transparency entry: 2340871458
- Sigstore integration time:
-
Permalink:
chkiss/Vimny@99a0714b61a7849b4eb88245f32a3ca5e19135e7 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/chkiss
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@99a0714b61a7849b4eb88245f32a3ca5e19135e7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file vimny-1.0.0-py3-none-any.whl.
File metadata
- Download URL: vimny-1.0.0-py3-none-any.whl
- Upload date:
- Size: 706.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
476140f2ae7a7fdf1c59b3907eb73807b81d713455bc93949e098f466f258953
|
|
| MD5 |
f55d6aa1ffbfbca32bb2ea578b5e559f
|
|
| BLAKE2b-256 |
562524b08a590d8a12c47ddf8ad75d12a8dcf2673e316722b97707d5963096e8
|
Provenance
The following attestation bundles were made for vimny-1.0.0-py3-none-any.whl:
Publisher:
release.yml on chkiss/Vimny
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vimny-1.0.0-py3-none-any.whl -
Subject digest:
476140f2ae7a7fdf1c59b3907eb73807b81d713455bc93949e098f466f258953 - Sigstore transparency entry: 2340871468
- Sigstore integration time:
-
Permalink:
chkiss/Vimny@99a0714b61a7849b4eb88245f32a3ca5e19135e7 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/chkiss
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@99a0714b61a7849b4eb88245f32a3ca5e19135e7 -
Trigger Event:
push
-
Statement type: