feat: gamemaster

This commit is contained in:
Ilia Denisov
2026-05-03 07:59:03 +02:00
committed by GitHub
parent a7cee15115
commit 3e2622757e
229 changed files with 41521 additions and 1098 deletions
@@ -0,0 +1,59 @@
// Package schedule wraps `pkg/cronutil` with the force-next-turn skip
// rule used by Game Master's scheduler.
//
// The wrapper is pure: callers pass the current `skip_next_tick` flag
// and the wrapper returns both the next firing time and a boolean that
// reports whether the flag was consumed. The runtime-record store is
// responsible for persisting the cleared flag; this package never
// touches it.
//
// `gamemaster/README.md §Force-next-turn` describes the rule:
//
// If `skip_next_tick=true`, advance by one extra cron step and clear
// the flag.
package schedule
import (
"time"
"galaxy/cronutil"
)
// Schedule wraps `cronutil.Schedule` with the GM-specific
// skip-next-tick semantics. The zero value is not usable; callers
// obtain a Schedule from Parse.
type Schedule struct {
inner cronutil.Schedule
}
// Parse parses expr as a five-field cron expression and returns the
// resulting Schedule. Parse returns an error if expr is rejected by the
// underlying cronutil parser.
func Parse(expr string) (Schedule, error) {
inner, err := cronutil.Parse(expr)
if err != nil {
return Schedule{}, err
}
return Schedule{inner: inner}, nil
}
// Next returns the next firing time strictly after `after`, honouring
// the skip flag.
//
// When `skip` is false, Next returns `cronutil.Schedule.Next(after)`
// and reports `skipConsumed=false`.
//
// When `skip` is true, Next computes the cron step immediately after
// `after`, then advances by one further cron step and returns that
// time with `skipConsumed=true`. The caller is responsible for
// persisting the cleared flag after observing `skipConsumed`.
//
// All returned times are in UTC; cronutil.Schedule already enforces
// UTC normalisation on its inputs and outputs.
func (s Schedule) Next(after time.Time, skip bool) (time.Time, bool) {
first := s.inner.Next(after)
if !skip {
return first, false
}
return s.inner.Next(first), true
}