Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.)