feat(newgame): one public, seeded roll the screen can show

roll_attributes(seed) is now the single implementation of the die — construct
calls it, and the creation screen (M4-b) will call it to DISPLAY the roll. So
the screen shows real numbers while still only ever handing over a seed (§2).
validate() goes public so the screen can gate its CTA on the real rules.

Neither takes a default parameter (traps.md #2).
This commit is contained in:
2026-07-13 13:54:02 -05:00
parent 7a74a8de94
commit 17ef142689
2 changed files with 74 additions and 3 deletions

View File

@@ -30,9 +30,9 @@ static func construct(origin: Dictionary, world: ContentDB, creation: Dictionary
# The RNG ORDER is part of the contract: five attributes in a fixed order, then
# Luck. Change the order and every existing seed produces a different character.
var attrs: Dictionary = {}
for stat in Attributes.IDS:
attrs[stat] = _roll_attribute(rng)
# roll_attributes() is the ONE implementation — the creation screen calls the same
# function to DISPLAY the roll, so what the player saw is what he gets (§2/§10).
var attrs: Dictionary = _roll_five(rng)
var bc: Dictionary = origin.get("build_constraints", {})
state.luck_base = Luck.roll_base(rng, int(bc.get("luck_modifier", 0)))
@@ -93,6 +93,30 @@ static func construct(origin: Dictionary, world: ContentDB, creation: Dictionary
return {"ok": true, "errors": [], "log": log, "state": state}
static func roll_attributes(character_seed: int) -> Dictionary:
## PUBLIC and PURE. The creation screen calls this to SHOW the roll; construct
## calls it to BUILD it. One implementation, so the two can never disagree.
## NO DEFAULT PARAMETERS (traps.md #2) — a defaulted arg turns a wrong-arity
## call into a silent miscompile that binds the wrong value into the wrong slot.
var rng := RandomNumberGenerator.new()
rng.seed = character_seed
return _roll_five(rng)
static func _roll_five(rng: RandomNumberGenerator) -> Dictionary:
var attrs: Dictionary = {}
for stat in Attributes.IDS:
attrs[stat] = _roll_attribute(rng)
return attrs
static func validate(origin: Dictionary, world: ContentDB, creation: Dictionary) -> Array:
## PUBLIC. The creation screen gates its CTA on this, so the player cannot press
## his way into a rejection. construct() still calls it and still does not trust
## its caller — the screen is a caller like any other. NO DEFAULT PARAMETERS.
return _validate(origin, world, creation)
static func _roll_attribute(rng: RandomNumberGenerator) -> int:
# 3d6 with a HARD FLOOR of 8. The floor clamps the die, not the player: straight
# 3d6 puts ~9% on a primary the +3 pool cannot rescue, and a character who is bad