Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
4 changes: 2 additions & 2 deletions server/broadcast.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
10 changes: 7 additions & 3 deletions server/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down