package engine import ( "math/rand" "sync" "time" ) type Callback func() bool type subscriber struct { id uint64 interval int callback Callback ticks int } type Engine struct { mu sync.Mutex subscribers map[uint64]*subscriber nextID uint64 ticker *time.Ticker running bool stopCh chan struct{} } func New() *Engine { return &Engine{ subscribers: make(map[uint64]*subscriber), } } func (e *Engine) Subscribe(interval int, cb Callback) uint64 { e.mu.Lock() defer e.mu.Unlock() e.nextID++ e.subscribers[e.nextID] = &subscriber{ id: e.nextID, interval: interval, callback: cb, } return e.nextID } func (e *Engine) Unsubscribe(id uint64) { e.mu.Lock() defer e.mu.Unlock() delete(e.subscribers, id) } func (e *Engine) Start(tickLengthMs int) { e.mu.Lock() defer e.mu.Unlock() if e.running { return } e.running = true if tickLengthMs < 50 { tickLengthMs = 50 } e.ticker = time.NewTicker(time.Duration(tickLengthMs) * time.Millisecond) e.stopCh = make(chan struct{}) go func() { for { select { case <-e.ticker.C: e.processTick() case <-e.stopCh: return } } }() } func (e *Engine) Stop() { e.mu.Lock() defer e.mu.Unlock() if e.ticker != nil { e.ticker.Stop() } e.running = false if e.stopCh != nil { close(e.stopCh) } } func (e *Engine) processTick() { e.mu.Lock() snapshot := make(map[uint64]*subscriber, len(e.subscribers)) for id, sub := range e.subscribers { snapshot[id] = sub } e.mu.Unlock() for id, sub := range snapshot { sub.ticks++ if sub.ticks >= sub.interval { sub.ticks = 0 if !sub.callback() { e.mu.Lock() delete(e.subscribers, id) e.mu.Unlock() } } } } func FractionalTicks(base, speed float64) int { if speed <= 0 { speed = 1 } value := base / speed if value < 1 { value = 1 } floor := int(value) if rand.Float64() < value-float64(floor) { return floor + 1 } return floor }