Sema-tawy: the docs

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

  1. Getting ready
  2. Three rooms
  3. How every file begins
  4. A tile is a number
  5. The names you’ll meet everywhere
  6. Following one tap
  7. Where to read first
  8. How it’s written
  9. 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.

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.

RoomFolderWhat happens there
The rulessrc/*.jswhat a stack is, which tiles are free, what a pair does, how a journey is made. No screen, no sound: only the rules.
The musicsrc/audio/the instruments, the songs and the calm music
The pagesrc/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.

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.

NameWhat it isMade in
GAMEwhat is being played now: the stack, the chosen tile, the mode ('free', 'run', 'tutorial' ...)60-play.js
JOURNEYthe journey under way, if there is one (JOURNEY.run)61-journey.js
HOMEwhat the player keeps between journeys: what they’ve found, what they’ve seen61-journey.js
SETTINGSthe player’s settings10-settings.js
EDITIONthe game’s own edition.jsonc, as the build read itthe build
TEXTevery word the game shows, from the game’s content01-helpers.js
RUN_CONFIGthe journey’s settings, from the game’s content01-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.

  1. The tap arrives. setUpBoardTaps in 70-menu.js listens on the board, works out which tile was under the finger and calls tap(i).
  2. Is it allowed? tap in 60-play.js asks the rules isFree(stack, i). If the tile isn’t free, notFree shakes it and says why.
  3. 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')).
  4. The second of a pair. The rules try to take both: take(stack, first, i) in 01-core.js. Before that, ghost makes a copy of each tile, so the effect has something to move once the real ones are gone.
  5. A pair taken. pairTaken notes it for the play log, draws the board, plays the effect (playFx('pair', ...) in 50-effects.js) and the sound. On a journey, runPair in 65-journey-stack.js adds what the journey gives for it: coins, relics at work, seals opened.
  6. Drawn on the screen. redraw asks drawStack in 40-board.js where every tile goes. Then paintStack in 47-paint-moves.js paints them all onto one canvas, tile by tile (paintTile in 45-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.

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.

  1. Change Plinth in its own folder (~/Code/plinth), then copy it into the game with python3 tools/update-plinth.py --copy --working from the game’s folder. A new file has to be added to Plinth’s git first (git add), or it isn’t copied.
  2. 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.
  3. 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.
  4. 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.