diff --git a/CLAUDE.md b/CLAUDE.md index 2ad102e..4327ee7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -314,7 +314,7 @@ This is implemented in `delayed_broadcast()` (`server/broadcast.py`) via an `asy 1. Host clicks Start → `handle_start` calls `deal_round()` (10 fresh dice each), sets `started=True` 2. Players roll; `apply_roll()` re-randomises unlocked dice and auto-locks any that match `target` 3. First player to lock all 10 wins the round → `handle_roll` sets `round_over=True`, sends `round_won` privately and schedules a `delayed_broadcast` -4. After `ROUND_WIN_DELAY` seconds, `delayed_broadcast` advances `target` (cycles 1→2→3→4→5→6→1), increments `round_num`, calls `deal_round()` again to clear per-round state +4. After `ROUND_WIN_DELAY` seconds, `delayed_broadcast` advances `target` (a triangle wave over `round_num`: 1→2→3→4→5→6→5→4→3→2→1→2→…), increments `round_num`, calls `deal_round()` again to clear per-round state When `ENABLE_DRAND_ROLLING` is set (default **off** → local RNG), `handle_roll` derives the unlocked dice from the drand League-of-Entropy beacon (`server/drand.py`) instead of `random`, and records the `drand_round` on the roll — making every roll provably fair and replayable via `/api/game/{code}/verify` and `/api/verify/{code}/{pid}/{roll_count}`. `apply_roll()` stays pure: it takes an optional `dice_values` and otherwise randomises locally. See [`docs/ROLL_TRUST.md`](docs/ROLL_TRUST.md). diff --git a/server/broadcast.py b/server/broadcast.py index cc60c95..4f4d41a 100644 --- a/server/broadcast.py +++ b/server/broadcast.py @@ -6,7 +6,7 @@ from . import db, db_places, fanout, gamestore, places, state from .config import DISCONNECT_GRACE, PAUSE_MAX, ROLL_ACK_TIMEOUT, ROUND_WIN_DELAY, log -from .game import next_target, state_msg +from .game import state_msg, target_for_round from .state import sessions from .telemetry import emit, metrics @@ -52,7 +52,7 @@ async def advance_round(code: str) -> None: old_round = meta["round_num"] old_target = meta["target"] emit("round_ended", game_code=code, round_num=old_round, target=old_target) - new_target = next_target(old_target) + new_target = target_for_round(old_round + 1) await gamestore.advance_round(code, new_target) snap = await gamestore.snapshot(code) if snap is None: diff --git a/server/game.py b/server/game.py index bb8a671..de2f6a1 100644 --- a/server/game.py +++ b/server/game.py @@ -42,9 +42,13 @@ def fresh_dice() -> list[int]: return [random.randint(1, 6) for _ in range(10)] -def next_target(t: int) -> int: - # cycles 1 → 2 → 3 → 4 → 5 → 6 → 1 → … - return t % 6 + 1 +def target_for_round(round_num: int) -> int: + # Triangle wave over the 1-based round number: the target climbs 1→6 then + # descends 6→1 and back up, so it reads 1,2,3,4,5,6,5,4,3,2,1,2,3,4,5,6,… + # The value alone can't encode direction (target 3 could be rising or + # falling), so the round number — a monotonic counter — drives it instead. + m = (round_num - 1) % 10 + return m + 1 if m <= 5 else 11 - m def apply_roll(player: dict, target: int, *, dice_values: list[int] | None = None) -> dict: