felt/internal/clock/ticker.go
Mikkel Georgsen 9ce05f6c67 feat(01-04): implement clock engine state machine, ticker, and registry
- ClockEngine with full state machine (stopped/running/paused transitions)
- Level management: load, advance, rewind, jump, hand-for-hand mode
- Drift-free ticker at 100ms with 1/sec broadcast (10/sec in final 10s)
- ClockRegistry for multi-tournament support (thread-safe)
- ClockSnapshot for reconnecting clients (CLOCK-09)
- Configurable overtime mode (repeat/stop)
- Crash recovery via RestoreState (resumes as paused for safety)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 03:51:07 +01:00

67 lines
1.6 KiB
Go

package clock
import (
"context"
"encoding/json"
"time"
)
// StartTicker runs the clock ticker for the given engine.
// It ticks every 100ms and broadcasts clock snapshots:
// - 1/sec during normal play
// - 10/sec (every 100ms) during the final 10 seconds
//
// The ticker stops when the context is cancelled.
func StartTicker(ctx context.Context, engine *ClockEngine) {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
var lastBroadcastSecond int64 = -1
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
shouldBroadcast, snapshot := engine.Tick()
if !shouldBroadcast {
continue
}
// Determine broadcast rate:
// - Final 10 seconds: broadcast every tick (10/sec)
// - Normal: broadcast once per second (1/sec)
currentSecond := snapshot.RemainingMs / 1000
isFinalSeconds := snapshot.RemainingMs <= 10000
if isFinalSeconds {
// Broadcast every 100ms tick in final 10 seconds
broadcastSnapshot(engine, snapshot)
lastBroadcastSecond = currentSecond
} else if currentSecond != lastBroadcastSecond {
// Broadcast once per second change
broadcastSnapshot(engine, snapshot)
lastBroadcastSecond = currentSecond
}
}
}
}
// broadcastSnapshot sends a clock snapshot to all WebSocket clients.
func broadcastSnapshot(engine *ClockEngine, snapshot ClockSnapshot) {
engine.mu.RLock()
hub := engine.hub
tournamentID := engine.tournamentID
engine.mu.RUnlock()
if hub == nil {
return
}
data, err := json.Marshal(snapshot)
if err != nil {
return
}
hub.Broadcast(tournamentID, "clock.tick", data)
}