// Package scheduler exposes the next-tick computation Game Master uses // to advance `runtime_records.next_generation_at` after a successful // turn generation. It is a thin, stateless wrapper over // `domain/schedule.Schedule.Next` with the force-next-turn skip rule // baked in via the `skipNextTick` parameter. // // Two callers consume the wrapper today: // // - `service/turngeneration` recomputes the next tick after a // successful (non-finished) generation; // - `service/adminforce` (Stage 17) reuses the same instance so the // skip rule lives in exactly one place. // // The package depends only on `domain/schedule` and stdlib `time`. It // holds no clock and no logger; callers pass `after` explicitly. package scheduler import ( "errors" "strings" "time" "galaxy/gamemaster/internal/domain/schedule" ) // Service computes the next scheduler-driven turn-generation tick. type Service struct{} // New constructs a stateless Service value. Returning a pointer keeps // the construction shape consistent with the other GM services even // though Service has no dependencies. func New() *Service { return &Service{} } // ComputeNext parses turnSchedule and returns the next firing time // strictly after `after`, applying the force-next-turn skip rule when // skipNextTick is true. // // When skipNextTick is true the wrapper computes the immediate next // cron step and then advances by one further step, so the inter-turn // spacing is never shorter than one schedule interval. The returned // `skipConsumed` flag reports whether the wrapper consumed the skip // (true when skipNextTick was true). // // On parse error ComputeNext returns the zero time, false, and the // error wrapped from `schedule.Parse`. The caller is responsible for // mapping it to the orchestrator-level `invalid_request` code. func (service *Service) ComputeNext(turnSchedule string, after time.Time, skipNextTick bool) (time.Time, bool, error) { if service == nil { return time.Time{}, false, errors.New("scheduler compute next: nil service") } parsed, err := schedule.Parse(strings.TrimSpace(turnSchedule)) if err != nil { return time.Time{}, false, err } next, skipConsumed := parsed.Next(after, skipNextTick) return next, skipConsumed, nil }