From 78a03df50c14563d6c21bf2e39199b1d42e384c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 01:10:38 +0000 Subject: [PATCH 1/2] Make target dice sequence a ping-pong triangle wave MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The round target cycled 1→2→3→4→5→6→1, jumping from 6 back down to 1. Change it to climb and descend — 1,2,3,4,5,6,5,4,3,2,1,2,… — so the progression reads smoothly in both directions. The value alone can't encode direction (target 3 could be rising or falling), so derive it from round_num (a monotonic counter) as a triangle wave instead of from the previous target. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Ug1kqfq8hC7G8PstttbzY8 --- CLAUDE.md | 2 +- server/broadcast.py | 4 ++-- server/game.py | 10 +++++++--- 3 files changed, 10 insertions(+), 6 deletions(-) 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..21e58c2 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 target_for_round, state_msg 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: From bd40fd0b6fc022789fcd61a091a23174cc839e29 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 01:18:39 +0000 Subject: [PATCH 2/2] Sort .game import to satisfy ruff (I001) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Ug1kqfq8hC7G8PstttbzY8 --- server/broadcast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/broadcast.py b/server/broadcast.py index 21e58c2..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 target_for_round, state_msg +from .game import state_msg, target_for_round from .state import sessions from .telemetry import emit, metrics