Skip to main content

The creation UI

Creation is version-independent — a new game is always created at the newest version your build ships — so it lives on the Dart GameModule rather than on a GameRules unit. Three members, none of which you write a dialog for: the shell renders the dialog from what you declare.

class RpsModule extends GameModule {
const RpsModule();


Map<int, GameRules> get versions => const {1: RpsRulesV1()};


GameCreationSpec get creationSpec => const GameCreationSpec(
minPlayers: 2,
maxPlayers: 2,
timingConfigs: {
'Per move': PerActionConfig(maxSeconds: 300, presets: [30, 60, 120]),
'Untimed': UntimedConfig(),
},
defaultConfig: {'targetWins': 3},
);


Widget? buildCreationConfig({
required ValueChanged<Map<String, dynamic>> onChanged,
}) => _TargetWinsPicker(onChanged: onChanged);


Widget buildRules(BuildContext context) => const RpsRulesPage();
}

creationSpec

  • timingConfigs keys become segmented-button labels, in insertion order, so the first entry is the default. PerActionConfig renders presets plus a slider; BudgetConfig adds an increment slider. Floors are enforced on both sides (kMinTurnSeconds 30 s, kMinBudgetSeconds 120 s).
  • BudgetConfig is only valid for strictly sequential games. The server rejects a hook envelope with more than one pending seat in a budget-timed game as a game bug. If any phase has multiple pending seats, use a per-action mode for it — or a turnSeconds override on that envelope.
  • defaultConfig seeds the config map before the player touches anything, so a game with no custom UI still creates a valid game.
  • playersForConfig overrides the range when it depends on a creation-time choice — a party game where the host picks 4 or 6, and min == max so joining flips the game to ready at exactly the right threshold.

buildCreationConfig

Returns a widget for game-specific options, or null if timing and player count are the whole story. It calls onChanged on every edit; the dialog stores the latest value in a plain field — not state, since it is never displayed — and sends it with the create request at submit time.

Whatever it produces is the game's config, and the server validates it against your configSchema. An out-of-range value is rejected there, so this widget is a convenience, not a gate.

buildRules

Non-scrolling how-to-play content for the About page — the page supplies the scroll container, padding and chrome. It is free to be interactive (an animated board example) and to read Theme.of.

Two constraints from elsewhere

Bots imply a timed game

If a game can seat a bot, its creation UI must require a turn or budget clock. Bot dispatch is single-attempt, so the turn deadline firing the server's alarm is the only thing that resolves a bot which never moves. The engine enforces this at seating; declaring an untimed-only game that also allows bots just produces a rejection later.

rated is a validated assertion, not a preference

The client computes rated from the Dart ratingPool twin plus its own guest status, and sends a concrete value. The server recomputes it and rejects a mismatch with a 422 rather than coercing.

That is deliberate: coercion would silently paper over a drifted twin or a forged client, and the twin drifting is exactly the failure this design wants to be loud. Keep the two ratingPool implementations in agreement, and let twin fixtures prove it.