cardinal

package
v0.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 1, 2026 License: LGPL-3.0 Imports: 63 Imported by: 0

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
    }, []cardinal.Command{system.BootstrapCommand{Seed: 42}})
}

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 the real ConnectRPC service.

Index

Constants

View Source
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
)
View Source
const MinEpochFrequency = 10

Variables

View Source
var (
	ErrSingleNoResult       = eris.New("expected exactly 1 result, got 0")
	ErrSingleMultipleResult = eris.New("expected exactly 1 result, got more than 1")
)

Functions

func RegisterCommandCodec added in v0.14.0

func RegisterCommandCodec(name string, c CommandCodec)

RegisterCommandCodec registers the wire codec for a command name. Generated code calls this from an init() in the command's package, so the codec is available once that package is imported.

func RegisterPlugin added in v0.11.0

func RegisterPlugin(world *World, plugin Plugin)

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, preTestCommands []Command)

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.

preTestCommands are enqueued before randomized fuzz operations begin. This supports worlds that require deterministic bootstrap commands before entering an active state.

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 ConnectRPC 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 AuthMode added in v0.12.0

type AuthMode uint8

AuthMode selects the authentication mode for the client-facing ConnectRPC service.

const (
	AuthModeUndefined AuthMode = iota
	AuthModeArgus
	AuthModeDev
	AuthModePassthrough
)

func ParseAuthMode added in v0.12.0

func ParseAuthMode(s string) (AuthMode, error)

func (AuthMode) IsValid added in v0.12.0

func (a AuthMode) IsValid() bool

func (AuthMode) String added in v0.12.0

func (a AuthMode) String() string

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 Command added in v0.10.0

type Command = command.Payload

type CommandCodec added in v0.14.0

type CommandCodec = command.Codec

CommandCodec encodes and decodes a command payload to and from its wire bytes. Generated code implements one per command type and registers it via RegisterCommandCodec.

type CommandContext added in v0.10.0

type CommandContext[T Command] struct {
	Payload T
	Persona string
}

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

func (s *Contains) Destroy(eid EntityID) bool

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

func (c *Contains[T]) GetByID(eid EntityID) (T, error)

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 EntityID added in v0.10.0

type EntityID = ecs.EntityID

type Event added in v0.10.0

type Event = event.Payload

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

func (s *Exact) Destroy(eid EntityID) bool

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

func (c *Exact[T]) GetByID(eid EntityID) (T, error)

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

type OtherWorld struct {
	Region       string
	Organization string
	Project      string
	ShardID      string
}

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

type Ref[T ecs.Component] struct {
	// contains filtered or unexported fields
}

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

type SearchResult[E EntityID, C any] func(yield func(E, C) bool)

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 Tick added in v0.10.0

type Tick struct {
	// contains filtered or unexported fields
}

type User added in v0.12.0

type User struct {
	jwt.RegisteredClaims

	ID    string `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

func UserFromContext added in v0.12.0

func UserFromContext(ctx context.Context) *User

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 WithEvent

type WithEvent[T Event] struct {
	// contains filtered or unexported fields
}

func (*WithEvent[T]) Emit added in v0.10.0

func (e *WithEvent[T]) Emit(evt 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.

func (*World) StartGame

func (w *World) StartGame()

StartGame launches your game and runs it until stopped.

func (*World) Tick added in v0.10.0

func (w *World) Tick(ctx context.Context, timestamp time.Time)

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
	Pprof               *bool                // Enable pprof server
	NATSConfig          *micro.NATSConfig    // Optional NATS config override (nil = use env/defaults)
	AuthMode            AuthMode             // Auth mode for the client-facing ConnectRPC service
	ArgusAuthURL        string               // URL of the Argus Auth service when AuthMode is ARGUS
}

Directories

Path Synopsis
internal
commandtest
Package commandtest registers wire codecs for the shared testutils command fixtures so that command tests across pkg/cardinal can enqueue them.
Package commandtest registers wire codecs for the shared testutils command fixtures so that command tests across pkg/cardinal can enqueue them.
ecs

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL