Reading the code
A guide for anyone opening Plinth’s code for the first time. Every file is listed in the code map and part 2 of the manual explains how the parts fit together. This page is the gentle way in.
You don’t need to be a programmer to read this code. If you know what a variable, an if and a loop are, you know enough to follow almost all of it. I’ve written it so that it reads as plainly as I could manage, with small functions that each do one thing and say what they do in their name. Where something is harder, a comment says why.
Don’t worry about breaking anything while you look around! Reading changes nothing. If you do try a change, the build checks your work before it writes a single file.
Contents
- Getting ready
- Three rooms
- How every file begins
- A tile is a number
- The names you’ll meet everywhere
- Following one tap
- Where to read first
- How it’s written
- Trying a change
1. Getting ready
What you’ll do: open the code in an editor and build the game once. Time: about 10 minutes.
- An editor. I would recommend VS Code. It colours the code and lets you jump to any function: hold Ctrl (or Cmd on a Mac) and click its name.
- Search the whole folder. Most of reading code is finding where a name is made and where it’s used. In VS Code that’s Edit → Find in Files.
- Build once, so you have a game to look at while you read:
python3 build.pyin the game’s folder. Open the file it writes indist/in your browser.
A good trick: open the game, press F12 and choose Console. Every function in the code can be called from there. Type GAME.stack and press Enter to see the stack being played, as the code sees it.
2. Three rooms
The code is in three folders, which I think of as three rooms of a house.
| Room | Folder | What happens there |
|---|---|---|
| The rules | src/*.js | what a stack is, which tiles are free, what a pair does, how a journey is made. No screen, no sound: only the rules. |
| The music | src/audio/ | the instruments, the songs and the calm music |
| The page | src/game/ | everything you see and hear: the board, the screens, the menus, the effects |
The build joins every file into one script, in the order of the numbers in their names: first the rules, then the music, then the page. So 01-core.js comes before 02-run.js and 60-play.js before 61-journey.js. Two files with the same number (65-places.js, 65-satchel.js) go in the order of their names.
Because it’s one script, a function in one file can call a function in any other. There are no imports to follow. If you see a name and wonder where it comes from, search the folder for function and the name.
The rules never touch the page. That’s on purpose: the tools that test the rules and play thousands of games (tools/) can then run them without a browser at all.
3. How every file begins
Every file opens with a comment that says what the file is for. Read it first! It has two parts that are always there.
- What’s here: the main names in the file and a line on each. If you only want to know what a file offers, this is enough.
- Changes in the save: what the file writes to the player’s save, if anything. Most files write nothing. The ones that do are the ones to be careful with, as a player’s save must keep working after every update.
Inside, a file reads from the top down: the small helpers first, then the functions that use them. A line of dashes (// ---- the satchel ----) marks where a new part of the file begins.
4. A tile is a number
This is the one idea to have in your head before you read the rules.
A stack isn’t a list of tile objects. Every tile is a number: 0, 1, 2 and so on. The stack keeps a list for each thing a tile can have and tile i‘s is at place i in each list.
stack.kinds[i] // which amulet tile i shows
stack.gone[i] // true once it has been taken
stack.places[i] // where it lies: { x, y, z } (z is its layer)
stack.seal[i] // the seal on it, or null
So “is tile 7 taken?” is stack.gone[7]. You’ll see this everywhere. Once it clicks, the rules read quite easily. makeStack in 01-core.js makes these lists. You can look at them all in the console with GAME.stack.
5. The names you’ll meet everywhere
Names in capitals are made once and used all over. These are the ones you’ll meet most often.
| Name | What it is | Made in |
|---|---|---|
GAME | what is being played now: the stack, the chosen tile, the mode ('free', 'run', 'tutorial' ...) | 60-play.js |
JOURNEY | the journey under way, if there is one (JOURNEY.run) | 61-journey.js |
HOME | what the player keeps between journeys: what they’ve found, what they’ve seen | 61-journey.js |
SETTINGS | the player’s settings | 10-settings.js |
EDITION | the game’s own edition.jsonc, as the build read it | the build |
TEXT | every word the game shows, from the game’s content | 01-helpers.js |
RUN_CONFIG | the journey’s settings, from the game’s content | 01-helpers.js |
Plinth knows no game. It never writes a word, a colour or a picture of one in its code: those always come from EDITION, TEXT and the game’s content. The build has a guard that refuses a game’s words in the engine, so if you add one by mistake, it will tell you.
6. Following one tap
The best way to see how the rooms work together is to follow one thing from start to finish. Here’s what happens when a player taps a tile.
- The tap arrives.
setUpBoardTapsin70-menu.jslistens on the board, works out which tile was under the finger and callstap(i). - Is it allowed?
tapin60-play.jsasks the rulesisFree(stack, i). If the tile isn’t free,notFreeshakes it and says why. - The first of a pair. If nothing was chosen yet, the tile becomes
GAME.chosen, the board is drawn again (redraw) and a sound plays (sfx('choose')). - The second of a pair. The rules try to take both:
take(stack, first, i)in01-core.js. Before that,ghostmakes a copy of each tile, so the effect has something to move once the real ones are gone. - A pair taken.
pairTakennotes it for the play log, draws the board, plays the effect (playFx('pair', ...)in50-effects.js) and the sound. On a journey,runPairin65-journey-stack.jsadds what the journey gives for it: coins, relics at work, seals opened. - Drawn on the screen.
redrawasksdrawStackin40-board.jswhere every tile goes. ThenpaintStackin47-paint-moves.jspaints them all onto one canvas, tile by tile (paintTilein45-paint.js).
If you read those functions in that order, you’ll have seen a good part of the engine.
7. Where to read first
A few ways in, depending on what you’re curious about.
How the game plays. 01-core.js: makeStack, isFree, take. Then tap and pairTaken in 60-play.js.
How a journey is made. 02-run.js (newRun, goTo), then 03-run-map.js for the map, 04-run-stacks.js for how hard each stack is and 05-run-play.js for what happens as it’s played.
What you see. 62-screens.js has the small parts every screen is made from (card, act, scene). 63-home.js builds the home screen from them and is a friendly first file to read.
How it looks. 45-paint.js paints a tile and 46-paint-specials.js paints what a journey adds to one (a veil, rubble, a seal, a gem).
How it moves. 50-effects.js. Each effect is one function (fxMeet, fxDust, fxGather) and the list FX at the end names them.
8. How it’s written
I kept to a few habits everywhere, so that one file reads like the next.
- Words for names.
tilesTakenSince,goOnPicture,shopCard. A function says what it gives or does. Short names (i,k,n) are for counting only. - Small helpers. When a function grew long, a piece of it became a helper of its own with a comment above it. So a long function mostly reads as a list of steps.
- Plain loops and plain ifs. A
forloop rather than.reduceor a chain of.filter().map(); anifand anelserather thana ? b : c ? d : e. - One thing per line. One statement, one variable.
functionfor functions. A function is writtenfunction name() { ... }. Only a very short one passed to another (htmlOf(list, function (item) { ... })) is written in place.- Comments say why. What the code does, the code says. The comment says why it’s done that way, often with what was measured or what a player found.
Here’s a small example of the difference, from the satchel:
// before
if (hint) (hint.querySelector('.cost').innerHTML = ''), (hint.title = TEXT.hint), (hint.disabled = false);
// after
// outside a journey a hint costs nothing: no price left from one
function hintWithoutPrice() {
const hint = document.getElementById('hint');
if (!hint) return;
hint.querySelector('.cost').innerHTML = '';
hint.title = TEXT.hint;
hint.disabled = false;
}
It’s longer, but you can read it once and know what it does.
The one room that still reads the old way is the music (src/audio/). It came from another of our projects and works well, so it was left as it is for now.
9. Trying a change
What you’ll do: change something, build and check it. Time: as long as you like.
- Change Plinth in its own folder (
~/Code/plinth), then copy it into the game withpython3 tools/update-plinth.py --copy --workingfrom the game’s folder. A new file has to be added to Plinth’s git first (git add), or it isn’t copied. - Build:
python3 build.py. If anything is wrong, it tells you the file and what to do. Nothing is written. The game you had stays as it was. - Check the rules:
node engine/tools/rules-test.js. It plays the rules through about a hundred small cases and says which fails, if any. - Check the page:
node engine/tools/smoke.js. It opens the built game at four screen sizes, plays it a little and looks for errors.
If all three say they passed, your change hasn’t broken anything they know about. Then play it yourself! They can’t tell you whether it feels right.