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 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
}