Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# nvim-quest

**Learn Neovim through an epic three-act terminal quest — no Neovim required.**
**Learn Neovim through an epic four-act terminal quest — no Neovim required.**
Comment thread
coderabbitai[bot] marked this conversation as resolved.

nvim-quest teaches real Vim motions and edits by making you *use* them. You press
the actual keys (`h j k l`, `d w`, `c w`, `/` …) and a live buffer reacts instantly,
Expand Down Expand Up @@ -115,9 +115,11 @@ of time and you retry from the first step.
| **I** | The Cursor Dojo | The Two Stances · First Steps · Way of the Word · The Great Leaps | modes & `i`/`Esc`, `h j k l`, `w`/`b`, `0` `$` `gg` `G` |
| **II** | The Motion Crypts | Hall of Insertion · The Line's Ends · The Deletion Pits · The Word's Edge · Echo Chamber · Opening Lines · The Inner Cut · Rewind · Mend & Weld · The Severing · The Shapeshifter | `i`/`a`, `A`/`I`/`^`, `x` `dw` `dd`, `e`, `yy`/`p`/`P`, `o`/`O`, `ciw`/`diw`, `u`/`Ctrl-r`, `r`/`~`/`J`, `D`/`C`/`ce`/`yw`, `cw` `cc` |
| **III** | The Neon Grid | Trace Evasion · Backtrace · The Marksman · The Gatekeeper · The Command Sigils · The Great Substitution · Power Surge | `/` search & `n`, `*`/`N`, `f`/`;`/`,`, `%`, `:w`/`:wq`/`:q!`, `:%s/old/new/g`, count prefixes (`4w`, `3dd`, `4x`) |
| **IV** | The Archives | The Selection · Inner Sanctum *(more coming)* | visual mode `v` + `d`/`y`/`c`, text objects `ci"` `di(` `dip` |

Each act has a distinct color palette (dojo greens, crypt embers, neon magenta/cyan)
and ends with a timed boss. Lessons unlock sequentially; clearing an act's boss unlocks
Each act has a distinct color palette (dojo greens, crypt embers, neon magenta/cyan,
archive brass) and ends with a timed boss — Act IV's boss arrives with its final
lesson, The Macro Forge. Lessons unlock sequentially; clearing an act's boss unlocks
the next act.

The curriculum is inspired by
Expand Down
38 changes: 38 additions & 0 deletions assets/lessons/act4-23-the-selection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"id": "act4-23-the-selection",
"act": 4, "order": 23,
"title": "The Selection",
"story": "The Archives open. Before automation comes sight: press v and watch your selection glow before you commit the cut.",
"challenges": [
{
"id": "a4-selection-c1",
"intro": "Purge the middle of the incantation — see it selected first.",
"buffer": ["purge every last echo now"], "cursor": [0, 6],
"goal": { "type": "bufferEquals", "lines": ["purge now"] },
"par": 6, "xp": 80,
"hint": "v starts a selection; motions grow it; d cuts everything selected.",
"newKeys": ["v"],
"allowedKeys": ["h", "j", "k", "l", "w", "b", "e", "0", "$", "^", "g", "G", "i", "a", "A", "I", "o", "O", "x", "d", "c", "y", "p", "P", "u", "ctrl+r", "r", "~", "J", "D", "C", "/", "n", "*", "N", "f", ";", ",", "%", ":", "v"]
},
{
"id": "a4-selection-c2",
"intro": "Echo the spell-word — select it, yank it, paste it.",
"buffer": ["the spell of binding"], "cursor": [0, 4],
"goal": { "type": "bufferEquals", "lines": ["the spell spell of binding"] },
"par": 7, "xp": 80,
"hint": "y yanks the selection; p pastes it after the cursor.",
"newKeys": ["vy"],
"allowedKeys": ["h", "j", "k", "l", "w", "b", "e", "0", "$", "^", "g", "G", "i", "a", "A", "I", "o", "O", "x", "d", "c", "y", "p", "P", "u", "ctrl+r", "r", "~", "J", "D", "C", "/", "n", "*", "N", "f", ";", ",", "%", ":", "v"]
},
{
"id": "a4-selection-c3",
"intro": "The gate must swing — select 'of iron' and rewrite it.",
"buffer": ["the gate of iron holds"], "cursor": [0, 9],
"goal": { "type": "bufferEquals", "lines": ["the gate ajar holds"] },
"par": 8, "xp": 80,
"hint": "c cuts the selection and drops you into Insert mode.",
"newKeys": ["vc"],
"allowedKeys": ["h", "j", "k", "l", "w", "b", "e", "0", "$", "^", "g", "G", "i", "a", "A", "I", "o", "O", "x", "d", "c", "y", "p", "P", "u", "ctrl+r", "r", "~", "J", "D", "C", "/", "n", "*", "N", "f", ";", ",", "%", ":", "v"]
}
]
}
38 changes: 38 additions & 0 deletions assets/lessons/act4-24-inner-sanctum.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"id": "act4-24-inner-sanctum",
"act": 4, "order": 24,
"title": "Inner Sanctum",
"story": "Deep in the Archives, meaning nests inside delimiters. Inner objects strike whole spans: quoted words, bracketed spells, entire paragraphs.",
"challenges": [
{
"id": "a4-sanctum-c1",
"intro": "The password is wrong. Rewrite what's inside the quotes — from anywhere.",
"buffer": ["speak \"the old password\" aloud"], "cursor": [0, 10],
"goal": { "type": "bufferEquals", "lines": ["speak \"hooray\" aloud"] },
"par": 9, "xp": 80,
"hint": "ci\" changes everything inside the quotes, wherever the cursor sits in them.",
"newKeys": ["ci\""],
"allowedKeys": ["h", "j", "k", "l", "w", "b", "e", "0", "$", "^", "g", "G", "i", "a", "A", "I", "o", "O", "x", "d", "c", "y", "p", "P", "u", "ctrl+r", "r", "~", "J", "D", "C", "/", "n", "*", "N", "f", ";", ",", "%", ":", "v"]
},
{
"id": "a4-sanctum-c2",
"intro": "The trap is armed inside the brackets. Empty them — one strike.",
"buffer": ["disarm(the trap inside) safely"], "cursor": [0, 12],
"goal": { "type": "bufferEquals", "lines": ["disarm() safely"] },
"par": 3, "xp": 80,
"hint": "di( deletes everything inside the parentheses.",
"newKeys": ["di("],
"allowedKeys": ["h", "j", "k", "l", "w", "b", "e", "0", "$", "^", "g", "G", "i", "a", "A", "I", "o", "O", "x", "d", "c", "y", "p", "P", "u", "ctrl+r", "r", "~", "J", "D", "C", "/", "n", "*", "N", "f", ";", ",", "%", ":", "v"]
},
{
"id": "a4-sanctum-c3",
"intro": "Purge the corrupted stanza — every line of it, one command.",
"buffer": ["keep this verse", "", "corrupt line one", "corrupt line two", "", "keep this verse too"], "cursor": [3, 2],
"goal": { "type": "bufferEquals", "lines": ["keep this verse", "", "", "keep this verse too"] },
"par": 3, "xp": 80,
"hint": "dip deletes the whole paragraph under the cursor.",
"newKeys": ["dip"],
"allowedKeys": ["h", "j", "k", "l", "w", "b", "e", "0", "$", "^", "g", "G", "i", "a", "A", "I", "o", "O", "x", "d", "c", "y", "p", "P", "u", "ctrl+r", "r", "~", "J", "D", "C", "/", "n", "*", "N", "f", ";", ",", "%", ":", "v"]
}
]
}
15 changes: 9 additions & 6 deletions docs/LESSON-GAP-ANALYSIS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Lesson gap analysis: nvim-quest vs. the field

Compares our 22 shipped lessons (see [`lessons.csv`](lessons.csv)) against the major vim
Compares our 24 shipped lessons (see [`lessons.csv`](lessons.csv)) against the major vim
curricula and games, so new lessons and mechanics can be picked by consensus rather than
guesswork. Sources:

Expand Down Expand Up @@ -42,8 +42,8 @@ guesswork. Sources:
| Match pair | `%` | ✅ The Gatekeeper | vimtutor L4 | code-flavored buffers |
| Substitute | `:s :%s` + `/g` | ✅ The Great Substitution | vimtutor L4, sequel | plain-text patterns; regex later |
| **Ex basics** | `:w :wq :q!` | ✅ The Command Sigils | **vimtutor lesson 1** | the meme gap is CLOSED — this game teaches you how to exit vim |
| Text objects | `iw` only | ✅ partial | Learn-Vim, VimHero | `i" i( ip` are the daily drivers; generalize `pendingInner` |
| Visual mode | `v V Ctrl-v` | 🔜 Act IV | vimtutor L5, all advanced | |
| Text objects | `iw i" i( ip` | ✅ Inner Sanctum | Learn-Vim, VimHero | the daily drivers all shipped |
| Visual mode | `v V Ctrl-v` | ✅ partial (The Selection) | vimtutor L5, all advanced | charwise `v` shipped; `V`/`Ctrl-v` later |
| Registers | `"a`–`"z` | 🔜 Act IV | Learn-Vim ch8, sequel | |
| Macros | `q @` | 🔜 Act IV | Learn-Vim ch9, sequel | |
| Marks | `m` `` ` `` | 🔜 Act IV | Learn-Vim | |
Expand All @@ -59,11 +59,14 @@ Edge), `r/~/J` (Mend & Weld), `*/N` (Backtrace), `f/;/,` (The Marksman).
**Shipped as the command pack:** `:w/:wq/:q!` (The Command Sigils), `:s/:%s//g`
(The Great Substitution) — command-line mode landed, and with it the meme lesson.

Still open — every remaining item needs a medium-or-larger engine feature:
**Shipped as Act IV part 1:** charwise visual `v`+`d/y/c` (The Selection), `i" i( ip`
text objects (Inner Sanctum).

Still open — the back half of Act IV:

1. **`.` repeat** — medium; pairs beautifully with combo/golf mechanics (fewer keys via `.`).
2. **More text objects `i" i( ip`** — generalize the existing `pendingInner` path.
3. Act IV as planned (visual, registers, macros, marks).
2. **Marks, registers** — medium each.
3. **`Ctrl-v` visual block, macros `q`/`@`** — the two large features, ending in The Archivist boss.

## Mechanics comparison (for "more playful games")

Expand Down
33 changes: 18 additions & 15 deletions docs/LESSONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ content-integrity test picks it up.
act<ACT>-<ORDER>-<slug>.json e.g. act2-06-the-deletion-pits.json
```

`ACT` is 1–3 and `ORDER` is the global lesson order (lessons are sorted by `(act, order)`).
`ACT` is 1–4 and `ORDER` is the global lesson order (lessons are sorted by `(act, order)`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the schema example to match the new act range.

The file-naming rule now says ACT is 1–4, but the inline schema example still says act is 1..3, so the guide contradicts itself.

Suggested fix
-  "act": 2,                            // 1..3
+  "act": 2,                            // 1..4
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`ACT` is 1–4 and `ORDER` is the global lesson order (lessons are sorted by `(act, order)`).
`ACT` is 1–4 and `ORDER` is the global lesson order (lessons are sorted by `(act, order)`).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/LESSONS.md` at line 13, The inline schema example is inconsistent with
the updated lesson naming rule, because it still describes act as 1..3 while the
new rule in the same section says ACT is 1–4. Update the schema example in the
lessons guide so the act range matches the current rule, keeping the reference
aligned with the lesson ordering description that mentions ORDER and sorting by
(act, order).

Keep `order` unique and increasing across the whole game.

## Schema
Expand Down Expand Up @@ -81,15 +81,16 @@ gated. Omit `allowedKeys` (boss steps do) to allow everything.

`internal/content/loader_test.go` and `solvable_test.go` will fail the build unless:

1. Every lesson has a unique `id`, an act in 1–3, and `order` increasing across the game.
1. Every lesson has a unique `id`, an act in 1–4, and `order` increasing across the game.
2. Every challenge & boss-step `id` is globally unique.
3. Every `cursor` is in bounds (`col` may equal the line length for append-at-end).
4. Every `goal.type` is valid and its payload field is non-empty.
5. **Every challenge and boss step has a non-empty `hint`** (the `[?]` key renders it; an
empty hint shows the player nothing).
6. Regular challenges have `par >= 1` and `xp > 0`; bosses have `timeLimitSec >= 30`,
`xp > 0`, and at least one step.
7. The act's final lesson (and only it) carries a `boss`.
7. A `boss` may sit only on its act's final lesson; every act except the highest
(still under construction) must end with one.
8. **Every challenge is solvable at par.** `solvable_test.go` plays an authored optimal
key sequence through the real engine and asserts the goal is met within par. When you
add a challenge, add its solution there too.
Expand All @@ -110,7 +111,7 @@ To add a lesson teaching `t` (till char) — once the engine supports `t`:

# Roadmap: the full curriculum

The target curriculum: 29 lessons across four acts — 22 shipped, 7 planned. Lesson
The target curriculum: 29 lessons across four acts — 24 shipped, 5 planned. Lesson
selection follows the consensus analysis in
[`LESSON-GAP-ANALYSIS.md`](LESSON-GAP-ANALYSIS.md) (vimtutor, Learn-Vim, Vim Adventures,
VimGolf et al.); phases mirror the original design spec
Expand Down Expand Up @@ -167,19 +168,21 @@ lesson must carry the act's boss — starting Act IV boss-less broke that invari
They live in Act III instead (where `:%s` sat in the original roadmap), and Act IV
stays the pure automation world.*

### Act IV · The Archives — power tools (all planned)
### Act IV · The Archives — power tools

Library/clockwork theme for automation.
Library/clockwork theme for automation. Under construction: until The Macro Forge
lands, this act has no boss and the game finale is unreachable (the content test
allows a boss-less final act while it grows).

| # | Lesson | Teaches | Engine work |
| --- | --- | --- | --- |
| 23 | The Selection | Visual `v` + `d/y/c` | medium: anchor + selection range |
| 24 | Inner Sanctum | `i" i( ip` text objects | small-medium: generalize `pendingInner` beyond `w` |
| 25 | The Echo Rite | `.` repeat | medium: record + replay the last change |
| 26 | Waypoints | marks `m{a-z}` `` `{a-z} `` | medium: per-buffer mark table |
| 27 | The Registers | `"a`–`"z` yank/paste | medium: register map through yank/delete/paste |
| 28 | Block Party | `Ctrl-v` + column `I/A` | larger: rectangular selection |
| 29 | The Macro Forge · **boss: The Archivist** | `q{reg}…q` `@{reg}` `@@` | larger: keystroke record & replay |
| # | Lesson | Teaches | Status | Engine work |
| --- | --- | --- | --- | --- |
| 23 | The Selection | Visual `v` + `d/y/c` | ✅ | |
| 24 | Inner Sanctum | `i" i( ip` text objects | ✅ | |
| 25 | The Echo Rite | `.` repeat | 📋 | medium: record + replay the last change |
| 26 | Waypoints | marks `m{a-z}` `` `{a-z} `` | 📋 | medium: per-buffer mark table |
| 27 | The Registers | `"a`–`"z` yank/paste | 📋 | medium: register map through yank/delete/paste |
| 28 | Block Party | `Ctrl-v` + column `I/A` | 📋 | larger: rectangular selection |
| 29 | The Macro Forge · **boss: The Archivist** | `q{reg}…q` `@{reg}` `@@` | 📋 | larger: keystroke record & replay |

Once command-line mode exists, `:g/pattern/d` (Learn-Vim ch13) is a natural bonus room
inside The Great Substitution — no extra lesson needed.
Expand Down
4 changes: 2 additions & 2 deletions docs/lessons.csv
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ shipped,3,19,The Gatekeeper,%,2,,
shipped,3,20,The Command Sigils,:w :wq :q!,3,,
shipped,3,21,The Great Substitution,:s :%s + /g flag,3,,
shipped,3,22,Power Surge,counts (4w 3dd),3,The Grid Core,
planned,4,23,The Selection,visual mode v + d/y/c,,,medium: visual mode with anchor + selection range
planned,4,24,Inner Sanctum,"i"" i( ip text objects",,,small-medium: generalize pendingInner beyond w
shipped,4,23,The Selection,visual mode v + d/y/c,3,,
shipped,4,24,Inner Sanctum,"i"" i( ip text objects",3,,
planned,4,25,The Echo Rite,. repeat,,,medium: record + replay last change
planned,4,26,Waypoints,marks m{a-z} + jumps `a 'a,,,medium: per-buffer mark table
planned,4,27,The Registers,"named registers ""a-""z",,,medium: register map through yank/delete/paste
Expand Down
24 changes: 15 additions & 9 deletions internal/content/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestAllLoadsAndValidates(t *testing.T) {
t.Errorf("duplicate lesson id %s", l.ID)
}
seenLessons[l.ID] = true
if l.Act < 1 || l.Act > 3 {
if l.Act < 1 || l.Act > 4 {
t.Errorf("%s: act %d out of range", l.ID, l.Act)
}
if i > 0 {
Expand Down Expand Up @@ -56,21 +56,27 @@ func TestAllLoadsAndValidates(t *testing.T) {
}
}
}
if len(lessons) != 22 {
t.Errorf("expected 22 lessons, got %d", len(lessons))
if len(lessons) != 24 {
t.Errorf("expected 24 lessons, got %d", len(lessons))
}
bosses := 0
lastInAct := map[int]string{}
maxAct := 0
for _, l := range lessons {
lastInAct[l.Act] = l.ID
if l.Boss != nil {
bosses++
}
maxAct = max(maxAct, l.Act)
}
if bosses != 3 {
t.Errorf("expected 3 bosses, got %d", bosses)
// A boss may sit only on its act's final lesson.
for _, l := range lessons {
if l.Boss != nil && lastInAct[l.Act] != l.ID {
t.Errorf("%s carries a boss but is not act %d's final lesson", l.ID, l.Act)
}
}
// Every finished act ends in a boss. The highest act may still be under
// construction — its boss ships with its final planned lesson.
for act, id := range lastInAct {
if act == maxAct {
continue
}
for _, l := range lessons {
if l.ID == id && l.Boss == nil {
t.Errorf("act %d final lesson %s must carry the boss", act, id)
Expand Down
6 changes: 6 additions & 0 deletions internal/content/solvable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ func TestEveryChallengeIsSolvable(t *testing.T) {
"a3l10c1": {"4", "w"},
"a3l10c2": {"3", "d", "d"},
"a3l10c3": {"4", "x"},
"a4-selection-c1": {"v", "e", "e", "e", "l", "d"},
"a4-selection-c2": {"v", "e", "l", "y", "e", "l", "p"},
"a4-selection-c3": {"v", "e", "e", "c", "a", "j", "a", "r"},
"a4-sanctum-c1": {"c", "i", "\"", "h", "o", "o", "r", "a", "y"},
"a4-sanctum-c2": {"d", "i", "("},
"a4-sanctum-c3": {"d", "i", "p"},
}

lessons, err := All()
Expand Down
Loading
Loading