Stage 03 — Draw the boundary: private means private
You will produce: a refactor with zero behavior change — real public APIs on Conductor
and Note, and judgment flowing through an enum instead of strings.
Warm-up — from memory:
- From stage 02: which defect couldn't have shipped, and why?
- From the farming FSM (if built): what does an enum make unrepresentable that strings allow?
Why this exists#
Every method on the Conductor starts with _ — and every one of them is called from other
nodes. The underscore convention says "internal, don't touch"; this codebase says it and then
touches everywhere. The official demo draws the line correctly: public play(),
get_current_beat(); private _update_position(). Naming is architecture at script scale
— it tells the next reader (you, in six months) what they may depend on.
The second boundary is the judgment channel: judged.emit("perfect") and
_counts[kind] — a typo'd string compiles fine and fails at runtime, silently or with a
Dictionary crash. Your farming stage 05 already taught the cure.
Do it — requirements, not recipes#
- Sort every underscore member of
ConductorandNoteinto actually public (called cross-node) vs actually private. The Signals/warnings won't help you here — read the call sites. Rename the public ones; keep genuinely internal ones prefixed. The editor's rename-symbol helps but does not catch scene-file references — recall stage 02's typo lesson and verify accordingly. - Introduce
enum Judgment { PERFECT, GOOD, MISS }where it belongs (whose concept is it — the manager's or the controller's? Take a position, log it). Convert the signal, the emissions, the counter dictionary, and the display formatting.matchwith all three arms — and consider what the demo'sassertin itsmatchdefault buys, then decide whether you want it. - The counts display currently dot-accesses a Dictionary (
_counts.perfect). Does your enum version still want a Dictionary at all? There's a smaller structure hiding here — find it or argue for keeping the Dictionary; either is fine with a reason. - One commit per step; play the game after each.
Checkpoint — definition of done#
-
grep -n "\._" *.gd scenes/*.gd music/*.gd scripts/globals/*.gd(or the editor's search) shows no cross-node underscore calls - No judgment string literal survives anywhere; the score line still reads correctly
- The game plays identically to the stage-02 baseline
- You can explain: what class of bug did the enum kill, and what did renaming kill?
Close the module by reading your boundary decisions side by side with the official demo's — where you differ, decide deliberately whose call is better and why. That comparison marks this project, at last, as understood code.
Stretch (no instructions)#
The demo's conductor smooths playback jitter with a 1€ filter and a second clock. Write — prose only, no implementation — the paragraph explaining why the prototype's single-clock approach visibly stutters at 144 Hz and what the demo's design trades to fix it. If that paragraph comes easily, this module did its job.