VimMonsters Academy is a browser game that teaches Vim motions through a retro creature-catching RPG. It is also structured as a learning codebase: people should be able to read it, fork it, mod it, and extend it without first untangling one giant file.
The project is released under the MIT License in LICENSE.
VimMonsters Academy is an independent learning project and is not affiliated with or endorsed by Vim.
Overworld and progression screenshot:
Drill system screenshot:
Battle and mini-drill screenshot:
- Teach Vim motions through play, drills, and battle interactions
- Stay readable enough to use as a programming learning project
- Be easy to fork for new lessons, new creatures, new art, and new maps
- Node.js 22 or newer
- npm
- Docker, if you want to run the container build
The repo includes .nvmrc and the GitHub Actions workflows use Node 22.
cd vimmonsters-academy
npm install
npm run serveThen open http://localhost:8002.
If 8002 is busy:
PORT=8004 npm run serveLeaderboard data is written to leaderboard.json in the project root unless you override it:
LEADERBOARD_PATH=./data/leaderboard.json npm run serveIf you want the public competition API to persist its used-run state too:
LEADERBOARD_PATH=./data/leaderboard.json \
COMPETITION_STATE_PATH=./data/competition-state.json \
npm run serveBuild the image:
cd vimmonsters-academy
docker build -t vimmonsters-academy .Run it:
docker run --rm -p 8002:8002 -v "$(pwd)/data:/data" vimmonsters-academyThen open http://localhost:8002.
Notes:
- leaderboard data is written to
/data/leaderboard.jsoninside the container - the bind mount keeps leaderboard data between container runs
- if you do not care about persistence, you can drop the
-v "$(pwd)/data:/data"mount - if
8002is busy on your machine, change the published port:docker run --rm -p 8004:8002 -v "$(pwd)/data:/data" vimmonsters-academy
For public hosting, pass a real competition secret and persist both JSON files:
docker run --rm \
-p 8002:8002 \
-e COMPETITIVE_SECRET="replace-this-with-a-long-random-secret" \
-e LEADERBOARD_PATH=/data/leaderboard.json \
-e COMPETITION_STATE_PATH=/data/competition-state.json \
-v "$(pwd)/data:/data" \
vimmonsters-academyThe server is now safer for public hosting than the old trust-the-browser flow:
- the browser can no longer overwrite the full leaderboard
- runs are append-only submissions, not arbitrary leaderboard writes
- each run starts with a server-issued signed run ticket
- submissions are rate-limited and body-size limited
- the server only serves public game files, not repo internals like
package.json,server.js, or dotfiles - static responses ship with stricter security headers
Recommended environment variables for public hosting:
COMPETITIVE_SECRET: required in practice; use a long random secretLEADERBOARD_PATH: persistent JSON file for leaderboard entriesCOMPETITION_STATE_PATH: persistent JSON file for used run ticketsHOST: bind host, defaults to0.0.0.0PORT: listen port, defaults to8002MIN_RUN_MS,MAX_RUN_MS,MAX_SCORE,MAX_SCORE_PER_SECOND: optional competition guardrails
Start from .env.example when setting up a real hosted instance.
Important limitation:
This is still a client-side game. The public leaderboard flow is much safer now, but it is not fully cheat-proof against a determined attacker who modifies the browser client. To get truly strong anti-cheat, you would need server-authoritative gameplay or a server-side replay verifier with a much richer proof-of-play model.
cd vimmonsters-academy
npm install
npm run build:assets
npm run serveUse this when you are editing game logic, content, sprites, or UI. Re-run npm run build:assets after changing sprite frame data or palette-driven art in src/content.js.
cd vimmonsters-academy
npm install
npm run build:assets
npm test
npm run build:readme-media
npm run lint
npm run check
npm run smokeUse TESTING.md for the manual runtime checklist after gameplay, UI, or refactor changes.
- Home Row House: learn
hjkl, then choose a starter VimMonster - Word Meadow: learn
w,b,e, andge, then catch your first wild VimMonster - Line Ridge: learn
0,$,^,x, and party switching with[] - Count Grove: learn count prefixes like
3wand2j, plusddandcw - Finder Fen: learn
f,t,F,T, and repeat-find with;and, - Operator Studio: learn
dwandciw - Macro Tower: learn
gg,G,/, and:replace, then beat or catch the final boss
hjkl: movei: inspect or talk to what is in front of youo: toggle the in-game VimTreeEnter: focus the selected VimTree sectionwb: move one tile forward or backward once unlockedege: word-end motions in drillsftFT: character find motions in drills and battle mini-drills;,: repeat the last find forward or in reverse1-9+ motion: counted motions like3wor2j0$^: line motions once unlockedggG: file motions once unlocked[]: cycle the active VimMonster once unlocked.: repeat the last learned motion once unlockedR: rename the current runm: mute or unmute music and sound:: command mode for:help,:party,:map,:lesson,:name,:q,:w,:load,:heal
a: attackx: Quick Jab after the ridge lessondd: Heavy Slam after the grove lessoncw: Focus Ball, which powers the next VimOrb after the grove lessondw: Break Word after the studio lessonciw: Inner Word after the studio lessonf: throw a VimOrbr: run[]: switch to a different party member
- src/content.js: editable game content and modding surface
- src/game.js: main orchestration, shared wiring, render loop, and browser event setup
- src/game-run-runtime.js: run/session lifecycle, leaderboard sync, and completed-run submission flow
- src/game-world-helpers.js: house-route progress helpers, map/tile queries, and shared world-coordinate utilities
- src/game-motion-runtime.js: overworld motion routing, repeat/count behavior, and locked-control messaging
- src/state.js: run setup, state helpers, map randomization, and monster creation
- src/drills.js: lesson drill definitions and drill hydration
- src/drill-runtime.js: drill cursor, prompt, and insert-mode state machine
- src/battle.js: top-level battle composition and battle input routing
- src/battle-flow.js: encounter setup, party switching, defeat reset, and finish/reward flow
- src/battle-techniques.js: player techniques, VimOrb throws, and catch resolution
- src/battle-enemy.js: enemy status pressure, cooldowns, and enemy-turn move resolution
- src/battle-challenge-runtime.js: battle mini-drill cursor flow, motion handling, and challenge resolution
- src/battle-challenges.js: authored battle mini-drill templates
- src/input.js: command mode, rename mode, VimTree navigation, and key normalization
- src/overworld.js: overworld movement, gate transitions, and NPC/sign interactions
- src/progression.js: lesson completion, objective text, gate text, and control unlock rules
- src/render.js: reusable canvas primitives and text helpers
- src/bitmap-assets.js: bitmap asset loader and sprite-sheet metadata
- src/persistence.js: leaderboard, save payload, and storage helpers
- src/scenes.js: scene composition and shared HUD layout
- src/scene-tree.js: VimTree overlay renderer
- src/scene-drill.js: lesson deck and drill editor overlay renderer
- src/scene-battle.js: battle scene, command window, and battle mini-drill overlay rendering
- server.js: static file server plus persistent leaderboard API
- scripts/build-bitmaps.mjs: rasterizes trainer, creature, and UI PNGs
- scripts/build-readme-media.mjs: optional helper for regenerating README preview media in
docs/media - scripts/runtime-smoke.mjs: automated runtime smoke for the client-side game logic
- scripts/api-smoke.mjs: API smoke for the public competition server flow
- TESTING.md: manual runtime checklist
- ARCHITECTURE.md: module ownership and common change paths
- CONTENT_GUIDE.md: shortest path for adding lessons, creatures, trainers, and maps
- MODDING.md: more detailed modding/customization guide
- CONTRIBUTING.md: contribution workflow and expectations
- SECURITY.md: how to report vulnerabilities or hosting abuse issues
- .github/PULL_REQUEST_TEMPLATE.md: PR checklist and reviewer context
Start here if you want to change content without rewriting engine code:
- edit
PLAYER_STYLE,NPC_STYLES,CREATURES,LESSONS,MAPS,RANDOMIZATION_RULES, orCONTROL_INFOin src/content.js - add or update drills in src/drills.js
- read CONTENT_GUIDE.md for the shortest path
- read MODDING.md for more detailed examples
If you change sprite frame data or palettes:
npm run build:assetsUse CONTRIBUTING.md.
The short version:
- keep the code readable for learners
- keep the game friendly for new Vim users
- run
npm run build:assets,npm test,npm run build:readme-media,npm run lint,npm run check, andnpm run smoke - use TESTING.md after gameplay or UI changes
The repo now includes:
- issue templates in .github/ISSUE_TEMPLATE
- CI in .github/workflows/ci.yml for asset build, unit tests, lint, syntax check, and runtime smoke
- Docker build verification in .github/workflows/docker.yml
For a clean public repo, make sure each change set does these before merge:
- update docs if controls, lessons, or run steps changed
- rebuild generated art if sprite definitions changed
- run
npm test,npm run lint,npm run check, andnpm run smoke - use TESTING.md for gameplay or UI changes
- include screenshots or GIFs in the PR if the visible UI changed
- the leaderboard is JSON-backed through server.js
- public competition runs now use server-issued run tickets plus append-only leaderboard submission
- the game uses browser ES modules and a shared runtime
appobject centered in src/game.js - run and leaderboard orchestration now live in src/game-run-runtime.js, which keeps that slice out of the main bootstrap file
- house-route and map/tile helpers now live in src/game-world-helpers.js, which keeps those cross-cutting helpers out of the bootstrap file too
- overworld motion routing and locked-control messaging now live in src/game-motion-runtime.js, which keeps input-to-world behavior out of the bootstrap file too
- encounter setup, party switching, defeat reset, and finish/reward handling now live in src/battle-flow.js, which keeps that lifecycle logic out of the main battle runtime
- player techniques, VimOrb throws, and catch resolution now live in src/battle-techniques.js, which keeps that player-action logic out of the coordinator
- enemy status pressure, cooldowns, and enemy-turn move resolution now live in src/battle-enemy.js, which keeps enemy behavior out of the coordinator too
- battle mini-drill cursor flow and motion resolution now live in src/battle-challenge-runtime.js, which keeps that challenge state machine out of the broader battle runtime
- the runtime is split so learners can study one system at a time instead of reading a single giant file first
docs/medianow contains the repository screenshots used in the README preview section


