Appendix B: The Trap List
Every entry here was hit by a real game first. Format: the symptom you will see, the cause, the escape.
1. The game compiles, then crashes at startup with nonsense data. Top-level values evaluate eagerly, in file order; one referred to a name defined below it and captured a unit value. Functions are immune; data is not. Keep constants and tables flowing strictly downward: palette, then things built from the palette. (Chapter 13.)
2. Combat is mysteriously weak; effects apply only sometimes. A value computed inside a fold or map branch was not carried out through the accumulator, so it evaporated. A shipped RTS had shots that only hit on their first tick this way. Resolve many-against-many in one fold whose accumulator record carries all outputs. (Chapter 9.)
3. List.foldl behaves backward.
Mar’s fold function takes the accumulator first, then the item: List.foldl (\acc x -> ...) init xs. Same-typed accumulators make a swap invisible to the compiler. (Chapter 9.)
4. “Random” events repeat identically forever.
A seed was reused or the new seed was not stored. Every rnd call must consume the previous seed and its result seed must leave in the returned model; never use a seed variable twice. (Chapter 6.)
5. A looped song slowly turns to soup.
In a looped chord, one voice’s durations sum differently from the others, so channels drift a few milliseconds per pass. Pad every voice with Sound.rest to the exact bar total and keep per-voice sum comments. (Chapter 11.)
6. f -2 will not compile, or computes a subtraction.
Negative literal arguments parse as subtraction. Write f (-2), or 0 - x in expressions. (Every chapter’s code does this.)
7. Wrapping and tile math break for negative positions.
There is no modBy, and // truncates toward zero, so -7 // 2 == -3 and plain remainders go negative. Use the modI helper for anything cyclic: animation frames, wrapping worlds, camera-to-tile. (Chapter 5.)
8. / produces something that will not do arithmetic.
/ is exact Decimal division and returns an inert quotient awaiting an explicit rounding decision; simulations never want it. Games use // everywhere. (Chapter 5.)
9. min, max, abs, clamp do not exist.
By design; write the four two-liners once (Appendix A’s math kit) and move on.
10. mar check Main.mar cannot find your other modules.
Point the tools at the directory, not the file: mar check my-game.
11. On a phone, the game leaks outside its letterbox. A scaled or translated group is not clipped; everything drawn off-stage shows in the margins. Draw four opaque mattes in the page background color after the world group. (Chapter 15.)
12. A mechanic works on desktop and silently does not exist on phones. It depended on hover (or right-click, or a long-press the OS owns). Touch needs a visible-at-rest equivalent for every hover affordance. (Chapter 7.)
13. Random.generate inside the tick feels one frame late.
Because it is: it is a command, and its result arrives as the next message. Inside step, use the in-model LCG; save Random for event-shaped chance. (Chapter 6.)
14. Multiplayer clients see a polite “too many requests” error. Rapid bursts of service calls met the framework’s rate limiter. Batch a turn into one payload; pace background polling in seconds, not milliseconds. (Chapter 16.)
15. The game runs at half speed on someone’s monitor. Or does it? It does not; check before “fixing”. The runtime’s catch-up clock keeps game speed constant on 30 Hz displays (fewer frames, same speed, up to 4 ticks per frame). If you made it speed-dependent by scaling movement with measured elapsed time, remove that: a tick is a constant unit of game time. (Chapter 4.)
16. The game (and its music) stops in a hidden tab. By design: the display clock pauses, so ticks pause. Do not compensate with wall-clock reads; embrace the free pause, and drive music from the world so it pauses too. (Chapters 4 and 11.)
17. String.slice (or another favorite) is missing.
The String module is compact: toList, split, padLeft, repeat, indexes, and friends. For tile rows, String.toList + List.drop/List.head gives Char tiles, which compare and case nicely. (Chapter 9.)
18. A builtin constructor is “not in scope”, or is, confusingly.
Builtin unions use qualified constructors: Canvas.Center, Canvas.Translate, Keyboard.ArrowLeft, Gamepad.A, Sound.Square. A few types deliberately use bare names: the canvas modes (Pixelated, Crisp) and Device’s Pointer values (Coarse, Fine). When in doubt, write it the way the compiler’s error suggests; it knows.
19. After a balance patch, old replays and daily scores disagree. Not a bug: the rules are the protocol, and you changed them. Determinism means any physics tweak invalidates recorded runs. Version your seeds/seasons deliberately instead of editing silently. (Chapters 6 and 17.)
20. The restart is haunted.
Second run starts with last run’s ghost: a timer, a powerup, a boss flag your start function forgot to reset. Restart bugs are always a missing field in one place; keep start next to init and diff them by eye whenever the Model grows. (The ship checklist makes you exercise this twenty times.)