Phase machine
The engine’s lifecycle is three classes: Phase (string-keyed identifier with display name), PhaseFlow (the per-game driver interface with next, onEnter, onExit, onTick), and PhaseMachine (the per-instance runner that calls those hooks and applies transitions). Engine-shipped phase constants are LOBBY, PRE_GAME, IN_GAME, ROUND_BREAK, CELEBRATION, CLEANUP. Two built-in flows ship: defaultMulti() and defaultSingleton().
When to use it
Section titled “When to use it”- You’re picking a built-in phase flow or writing your own.
- You need to force an explicit transition from an event handler (the host clicked Start).
- You want per-phase enter/exit side effects (announce, open doors, play effects).
package me.zlex.conduit.game;
public record Phase(String name, String displayName) { public static final Phase LOBBY, PRE_GAME, IN_GAME, ROUND_BREAK, CELEBRATION, CLEANUP; public static Phase of(String name);}
public interface PhaseFlow { Phase next(Phase current, GameInstance ctx); default void onTick(Phase current, GameInstance ctx) {} default void onEnter(Phase entered, @Nullable Phase previous, GameInstance ctx) {} default void onExit(Phase exited, Phase next, GameInstance ctx) {}
static PhaseFlow defaultMulti(); // LOBBY → IN_GAME → CELEBRATION → CLEANUP → LOBBY static PhaseFlow defaultSingleton(); // LOBBY → IN_GAME → CLEANUP}
public final class PhaseMachine { public PhaseMachine(PhaseFlow flow, GameInstance ctx, Phase initial);
public Phase current(); public void tick(); // call from your server-tick handler public void transitionTo(Phase target); // force a transition}Example
Section titled “Example”import me.zlex.conduit.game.Phase;import me.zlex.conduit.game.PhaseFlow;import me.zlex.conduit.instance.GameInstance;import me.zlex.conduit.fx.Effects;
public final class SprintRaceFlow implements PhaseFlow {
public static final Phase RACING = Phase.of("RACING"); public static final Phase CELEBRATE = Phase.CELEBRATION;
@Override public Phase next(Phase current, GameInstance ctx) { var state = SprintState.of(ctx); if (current.equals(Phase.LOBBY) && state.allReady()) return RACING; if (current.equals(RACING) && state.finishersAtLeast(3)) return CELEBRATE; if (current.equals(CELEBRATE) && state.celebrationElapsed()) return Phase.CLEANUP; if (current.equals(Phase.CLEANUP)) return Phase.LOBBY; return current; }
@Override public void onEnter(Phase entered, Phase previous, GameInstance ctx) { if (entered.equals(RACING)) { Effects.roundStartChime(SprintState.of(ctx).players()); } }}Phase.equalscompares only byname—displayNameis for logs and inspector UI only.PhaseMachine.tickcallsonTickfirst, then asksnext— ifnextreturns a different phase, the transition firesonExit(old)thenonEnter(new).- For event-driven moves (button click, command), call
transitionTo(target)directly.