Documentation
¶
Overview ¶
DST (Deterministic Simulation Testing) provides a game-logic-agnostic fuzzer and structural state checker for Cardinal. It generates random commands by introspecting registered command types (via reflection), injects engine operations (tick, restart, snapshot/restore) with randomized weights, and validates structural ECS invariants after every tick. Game logic correctness is irrelevant — only engine correctness matters.
Usage from a game shard's test directory:
func TestDST(t *testing.T) {
cardinal.RunDST(t, func(w *cardinal.World) {
cardinal.RegisterSystem(w, system.MySystem)
// ... register all systems
})
}
E2E test helper with real (in-memory) NATS. Similar to the DST harness in dst.go, but runs the full World.run loop (ticking, snapshotting, restoring) while a separate goroutine sends randomized commands through real NATS subjects.
Index ¶
- Constants
- Variables
- func RegisterPlugin(world *World, plugin Plugin)
- func RegisterSystem[T any](world *World, system func(*T), opts ...SystemOption)
- func RunDST(t *testing.T, setup DSTSetupFunc)
- func RunE2E(t *testing.T, setup E2ESetupFunc)
- type BaseSystemState
- type Command
- type CommandContext
- type Contains
- type DSTSetupFunc
- type E2ESetupFunc
- type EntityID
- type Event
- type Exact
- type OtherWorld
- type Plugin
- type Ref
- type SearchResult
- type SystemHook
- type SystemOption
- type Tick
- type WithCommand
- type WithEvent
- type WithSystemEventEmitter
- type WithSystemEventReceiver
- type World
- type WorldOptions
Constants ¶
const ( // PreUpdate runs before the main update. PreUpdate = ecs.PreUpdate // Update runs during the main update phase. Update = ecs.Update // PostUpdate runs after the main update. PostUpdate = ecs.PostUpdate // Init runs once during world initialization. Init = ecs.Init )
const MinEpochFrequency = 10
Variables ¶
var ( ErrSingleNoResult = eris.New("expected exactly 1 result, got 0") ErrSingleMultipleResult = eris.New("expected exactly 1 result, got more than 1") )
Functions ¶
func RegisterPlugin ¶ added in v0.11.0
RegisterPlugin registers a plugin with the world. Must be called before StartGame(). Panics if the plugin fails to register, consistent with other registration functions.
func RegisterSystem ¶
func RegisterSystem[T any](world *World, system func(*T), opts ...SystemOption)
func RunDST ¶ added in v0.10.2
func RunDST(t *testing.T, setup DSTSetupFunc)
RunDST executes a deterministic simulation test. The setup function registers game-specific systems; the harness handles everything else: randomized engine config, command generation, ticking, restart/restore operations, and structural invariant checking.
The number of ticks defaults to 1000 and can be overridden with -dst.ticks:
go test ./pkg/cardinal/... -dst.ticks=5000
func RunE2E ¶ added in v0.10.2
func RunE2E(t *testing.T, setup E2ESetupFunc)
RunE2E executes an end-to-end test. The setup function creates the world the same way production does; the harness transparently injects the in-memory test NATS URL. The harness starts the world's run loop in a background goroutine and sends randomized commands over NATS from the test goroutine.
E2E tests are skipped by default. Use the -e2e.run flag to enable them:
go test ./pkg/cardinal/... -e2e.run
The run duration defaults to 30s and can be overridden with -e2e.duration:
go test ./pkg/cardinal/... -e2e.run -e2e.duration=2m
Types ¶
type BaseSystemState ¶
type BaseSystemState struct {
// contains filtered or unexported fields
}
func (*BaseSystemState) Logger ¶
func (b *BaseSystemState) Logger() *zerolog.Logger
TODO: pass init args (similar to boot info) to get system name in logger. Logger returns the logger for the world.
func (*BaseSystemState) Tick ¶
func (b *BaseSystemState) Tick() uint64
Tick returns the current tick of the world.
func (*BaseSystemState) Timestamp ¶
func (b *BaseSystemState) Timestamp() time.Time
Timestamp returns the current timestamp of the world.
type CommandContext ¶ added in v0.10.0
type Contains ¶
type Contains[T any] struct { // contains filtered or unexported fields }
Contains provides a search that matches archetypes containing all specified component types, potentially along with additional components.
Example:
type MovementSystemState struct {
Movers ecs.Contains[struct {
Position ecs.Ref[Position]
Velocity ecs.Ref[Velocity]
}]
// Other fields...
}
// Your system function receives a pointer to your system state.
func MovementSystem(state *MovementSystemState) error {
for entity, mover := range state.Movers.Iter() {
// Process entity and compnents.
}
return nil
}
func (*Contains) Create ¶ added in v0.11.0
func (s *Contains) Create() (EntityID, T)
Create creates a new entity with the given components. Returns an error if any of the components are not defined in the search field.
Example:
entity, err := state.Mob.Create(Health{Value: 100}, Position{X: 0, Y: 0})
if err != nil {
state.Logger().Error().Err(err).Msg("Failed to create entity")
}
// Use entity...
func (*Contains) Destroy ¶ added in v0.11.0
Destroy deletes an entity and all its components from the world.
Example:
ok := state.Mob.Destroy(entityID)
if !ok {
state.Logger().Warn().Msg("Entity doesn't exist or is already destroyed")
}
func (*Contains[T]) GetByID ¶ added in v0.11.0
GetByID retrieves an entity's components by its ID. Returns ErrEntityNotFound if the entity doesn't exist, or ErrArchetypeMismatch if the entity doesn't contain all the required components.
Example:
mob, err := state.Mob.GetByID(entityID)
if err != nil {
state.Logger().Warn().Err(err).Msg("Entity not found or doesn't match")
return err
}
health := mob.Health.Get()
func (*Contains[T]) Iter ¶ added in v0.11.0
func (c *Contains[T]) Iter() SearchResult[EntityID, T]
Iter returns an iterator over entities and their components that match the Contains search.
Example:
for _, mover := range state.Movers.Iter() {
pos := mover.Position.Get()
vel := mover.Velocity.Get()
mover.Position.Set(Position{X: pos.X + vel.X, Y: pos.Y + vel.Y})
}
type DSTSetupFunc ¶ added in v0.10.2
type DSTSetupFunc func(world *World)
DSTSetupFunc registers systems, components, and commands on a World. It is called once during fixture creation, before the first tick.
type E2ESetupFunc ¶ added in v0.10.2
type E2ESetupFunc func() *World
E2ESetupFunc builds and returns the world used by the E2E harness. The harness injects a test NATS URL via environment variables before invoking setup.
type Exact ¶
type Exact[T any] struct { // contains filtered or unexported fields }
Exact provides a search that matches archetypes containing exactly the specified component types, without any additional components.
Example:
type PlayerSystemState struct {
Players ecs.Exact[struct {
Tag ecs.Ref[PlayerTag]
Health ecs.Ref[Health]
}]
// Other fields...
}
// Your system function receives a pointer to your system state.
func PlayerSystem(state *PlayerSystemState) error {
for entity, player := range state.Players.Iter() {
// Process entity and compnents.
}
return nil
}
func (*Exact) Create ¶ added in v0.11.0
func (s *Exact) Create() (EntityID, T)
Create creates a new entity with the given components. Returns an error if any of the components are not defined in the search field.
Example:
entity, err := state.Mob.Create(Health{Value: 100}, Position{X: 0, Y: 0})
if err != nil {
state.Logger().Error().Err(err).Msg("Failed to create entity")
}
// Use entity...
func (*Exact) Destroy ¶ added in v0.11.0
Destroy deletes an entity and all its components from the world.
Example:
ok := state.Mob.Destroy(entityID)
if !ok {
state.Logger().Warn().Msg("Entity doesn't exist or is already destroyed")
}
func (*Exact[T]) GetByID ¶ added in v0.11.0
GetByID retrieves an entity's components by its ID. Returns ErrEntityNotFound if the entity doesn't exist, or ErrArchetypeMismatch if the entity doesn't have exactly the required components.
Example:
player, err := state.Players.GetByID(entityID)
if err != nil {
state.Logger().Warn().Err(err).Msg("Entity not found or doesn't match")
return err
}
health := player.Health.Get()
func (*Exact[T]) Iter ¶ added in v0.11.0
func (c *Exact[T]) Iter() SearchResult[EntityID, T]
Iter returns an iterator over entities and their components that match the Exact query.
Example:
for _, player := range state.Players.Iter() {
health := player.Health.Get()
player.Health.Set(Health{HP: health.HP + 100})
}
type OtherWorld ¶
OtherWorld is a type that represents the address of an external service.
func (OtherWorld) SendCommand ¶ added in v0.10.0
func (o OtherWorld) SendCommand(state *BaseSystemState, cmd command.Payload)
SendCommand sends a command to an external service.
Example:
import external "another-game-shard/system"
// Define a shard address of one of your game shards. const MatchmakingService ecs.ServiceAddress = "world.argus.rampage.matchmaking"
func GameSystem(state *GameSystem) error {
MatchmakingService.Send(state, &external.EndGameCommand{
Winner: "Team 1",
})
}
type Plugin ¶ added in v0.11.0
type Plugin interface {
Register(world *World)
}
Plugin defines a self-contained extension that can register systems, components, commands, and events with a World. Plugins allow reusable game logic to be packaged and shared across projects.
Components, commands, and events are automatically registered when referenced by system state fields (via Ref[T], WithCommand[T], WithEvent[T]), so a plugin's Register method typically only needs to call RegisterSystem.
Example:
type MyPlugin struct{ config MyConfig }
func (p *MyPlugin) Register(world *cardinal.World) error {
cardinal.RegisterSystem(world, MyInitSystem, cardinal.WithHook(cardinal.Init))
cardinal.RegisterSystem(world, MyTickSystem)
return nil
}
type Ref ¶
Ref provides a type-safe handle to a component on an entity.
func (*Ref[T]) Get ¶ added in v0.11.0
func (r *Ref[T]) Get() T
Get retrieves the component value for this Ref's entity.
This is the recommended system-friendly alternative to ecs.Get() for accessing components within systems.
Example:
for _, player := range state.Players.Iter() {
health := player.Health.Get()
}
func (*Ref[T]) Remove ¶ added in v0.11.0
func (r *Ref[T]) Remove()
Remove removes the component from this Ref's entity.
This is the recommended system-friendly alternative to ecs.Remove() for removing components within systems.
Example:
for _, player := range state.Players.Iter() {
player.Shield.Remove()
}
func (*Ref[T]) Set ¶ added in v0.11.0
func (r *Ref[T]) Set(component T)
Set updates the component value for this Ref's entity.
This is the recommended system-friendly alternative to ecs.Set() for modifying components within systems.
Example:
for _, player := range state.Players.Iter() {
player.Health.Set(Health{HP: 100})
}
type SearchResult ¶ added in v0.11.0
SearchResult is a chainable iterator over key-value pairs.
func (SearchResult[E, C]) Filter ¶ added in v0.11.0
func (s SearchResult[E, C]) Filter(predicate func(E, C) bool) SearchResult[E, C]
Filter returns a new iterator that only yields values that satisfy predicate. A nil predicate returns the original iterator unchanged.
func (SearchResult[E, C]) Limit ¶ added in v0.11.0
func (s SearchResult[E, C]) Limit(limit uint32) SearchResult[E, C]
Limit returns a new iterator that yields at most limit values. A limit <= 0 yields no values.
func (SearchResult[E, C]) Single ¶ added in v0.11.0
func (s SearchResult[E, C]) Single() (E, C, error)
Single returns the single value in the iterator. It returns an error if the iterator yields zero or more than one result.
type SystemHook ¶ added in v0.10.0
type SystemHook = ecs.SystemHook
SystemHook defines when a system should be executed in the update cycle.
type SystemOption ¶ added in v0.10.0
type SystemOption func(*systemConfig)
SystemOption is a function that configures a SystemConfig.
func WithHook ¶
func WithHook(hook SystemHook) SystemOption
WithHook returns an option to set the system hook.
type WithCommand ¶
type WithCommand[T Command] struct { // contains filtered or unexported fields }
func (*WithCommand[T]) Iter ¶ added in v0.10.0
func (c *WithCommand[T]) Iter() iter.Seq[CommandContext[T]]
type WithSystemEventEmitter ¶
type WithSystemEventEmitter[T ecs.SystemEvent] struct { // contains filtered or unexported fields }
WithSystemEventEmitter is a generic system state field that allows systems to emit system events of type T. System events are automatically registered when the system is registered.
Example:
// Define a system event for player deaths.
type PlayerDeath struct{ Nickname string }
func (PlayerDeath) Name() string { return "player-death" }
type CombatSystemState struct {
PlayerDeathSystemEvents ecs.WithSystemEventEmitter[PlayerDeath]
// Other fields...
}
// Your system function receives a pointer to your system state.
func CombatSystem(state *CombatSystemState) error {
// Emit a player death event to be handled in another system.
state.PlayerDeathEvents.Emit(PlayerDeath{Nickname: "Player1"})
return nil
}
func (*WithSystemEventEmitter[T]) Emit ¶ added in v0.11.0
func (s *WithSystemEventEmitter[T]) Emit(systemEvent T)
Emit emits a system event of type T.
Example:
state.PlayerDeathEvents.Emit(PlayerDeath{Nickname: "Player1"})
type WithSystemEventReceiver ¶
type WithSystemEventReceiver[T ecs.SystemEvent] struct { // contains filtered or unexported fields }
WithSystemEventReceiver is a generic system state field that allows systems to receive system events of type T. System events are automatically registered when the system is registered.
Example:
// Define a system event for player deaths.
type PlayerDeath struct{ Nickname string }
func (PlayerDeath) Name() string { return "player-death" }
type GraveyardSystemState struct {
PlayerDeathSystemEvents ecs.WithSystemEventReceiver[PlayerDeath]
// Other fields...
}
// Your system function receives a pointer to your system state.
func GraveyardSystem(state *GraveyardSystemState) error {
// Receive system events emitted from another system.
for systemEvent := range state.PlayerDeathSystemEvents.Iter() {
// Process the system event.
}
return nil
}
func (*WithSystemEventReceiver[T]) Iter ¶ added in v0.11.0
func (s *WithSystemEventReceiver[T]) Iter() iter.Seq[T]
Iter returns an iterator over all system events of type T.
Example usage:
for systemEvent := range state.PlayerDeathEvents.Iter() {
// Process each system event
}
type World ¶
type World struct {
// contains filtered or unexported fields
}
World represents your game world and serves as the main entry point for Cardinal.
func NewWorld ¶
func NewWorld(opts WorldOptions) (*World, error)
NewWorld creates a new game world with the specified configuration.
type WorldOptions ¶
type WorldOptions struct {
Region string // Region the shard is deployed to
Organization string // The organization that owns this world
Project string // Name of the project within the organization
ShardID string // Unique ID for of world's instance
TickRate float64 // Number of ticks per second
SnapshotStorageType snapshot.StorageType // Snapshot storage type
SnapshotRate uint32 // Number of ticks per snapshot
Debug *bool // Enable debug server
NATSConfig *micro.NATSConfig // Optional NATS config override (nil = use env/defaults)
}