Documentation
¶
Index ¶
- Constants
- func CloneBody(pb physics.PhysicsBody2D) physics.PhysicsBody2D
- func DecodeSnapshot(raw []byte) (any, error)
- func EncodeSnapshot(state any) ([]byte, error)
- func InitECS(w *cardinal.World)
- func RegisterPostCapture(w *cardinal.World, into *Capture)
- func RegisterPreCapture(w *cardinal.World, into *Capture)
- func RestoreWorld(w *cardinal.World, state any) error
- func SnapshotWorld(w *cardinal.World) (any, error)
- type Capture
- type CaptureRow
- type Config
- type Ctx
- func (c *Ctx) Alive(id cardinal.EntityID) bool
- func (c *Ctx) AngVel(id cardinal.EntityID) float64
- func (c *Ctx) Between(check string, got, lo, hi float64) bool
- func (c *Ctx) Body(id cardinal.EntityID) physics.PhysicsBody2D
- func (c *Ctx) CircleSweep(fromX, fromY, toX, toY, radius, maxFraction float64, filter *physics.Filter) physics.CircleSweepResult
- func (c *Ctx) CountBetween(kind EventKind, a, b cardinal.EntityID) int
- func (c *Ctx) CountTouching(kind EventKind, id cardinal.EntityID) int
- func (c *Ctx) Destroy(id cardinal.EntityID) bool
- func (c *Ctx) EditBody(id cardinal.EntityID, edit func(pb *physics.PhysicsBody2D))
- func (c *Ctx) EditShape(id cardinal.EntityID, i int, edit func(sh *physics.ColliderShape))
- func (c *Ctx) Events(kind EventKind) []LoggedEvent
- func (c *Ctx) EventsBetween(kind EventKind, a, b cardinal.EntityID) []LoggedEvent
- func (c *Ctx) EventsOnTick(kind EventKind, tick uint64) []LoggedEvent
- func (c *Ctx) ExpectWorldReset()
- func (c *Ctx) False(check string, got bool, format string, args ...any) bool
- func (c *Ctx) Greater(check string, got, threshold float64) bool
- func (c *Ctx) GreaterEq(check string, got, threshold float64) bool
- func (c *Ctx) HasError(check string, err error) bool
- func (c *Ctx) Int(check string, got, want int) bool
- func (c *Ctx) IntAtLeast(check string, got, want int) bool
- func (c *Ctx) Label(id cardinal.EntityID) string
- func (c *Ctx) Lane() float64
- func (c *Ctx) Less(check string, got, threshold float64) bool
- func (c *Ctx) LessEq(check string, got, threshold float64) bool
- func (c *Ctx) Near(check string, got, want, tol float64) bool
- func (c *Ctx) NearVec(check string, got, want physics.Vec2, tol float64) bool
- func (c *Ctx) NoError(check string, err error) bool
- func (c *Ctx) Note(format string, args ...any)
- func (c *Ctx) OverlapAABB(minX, minY, maxX, maxY float64, filter *physics.Filter) physics.AABBOverlapResult
- func (c *Ctx) OverlapHits(res physics.AABBOverlapResult, id cardinal.EntityID) bool
- func (c *Ctx) OverlapHitsShape(res physics.AABBOverlapResult, id cardinal.EntityID, shapeIndex int) bool
- func (c *Ctx) Plugin() *physics.Plugin
- func (c *Ctx) Pos(id cardinal.EntityID) physics.Vec2
- func (c *Ctx) Raycast(fromX, fromY, toX, toY float64, filter *physics.Filter) physics.RaycastResult
- func (c *Ctx) Report() *Report
- func (c *Ctx) Rot(id cardinal.EntityID) float64
- func (c *Ctx) Scenario() string
- func (c *Ctx) SetAngVel(id cardinal.EntityID, angular float64)
- func (c *Ctx) SetBody(id cardinal.EntityID, pb physics.PhysicsBody2D)
- func (c *Ctx) SetPos(id cardinal.EntityID, x, y float64)
- func (c *Ctx) SetRot(id cardinal.EntityID, radians float64)
- func (c *Ctx) SetVel(id cardinal.EntityID, vx, vy float64)
- func (c *Ctx) Skip(check, format string, args ...any)
- func (c *Ctx) Spawn(label string, x, y float64, pb physics.PhysicsBody2D) cardinal.EntityID
- func (c *Ctx) SpawnFull(label string, t physics.Transform2D, v physics.Velocity2D, ...) cardinal.EntityID
- func (c *Ctx) SpawnMoving(label string, x, y, vx, vy float64, pb physics.PhysicsBody2D) cardinal.EntityID
- func (c *Ctx) SpawnSpinning(label string, x, y, angular float64, pb physics.PhysicsBody2D) cardinal.EntityID
- func (c *Ctx) Speed(id cardinal.EntityID) float64
- func (c *Ctx) Str(check, got, want string) bool
- func (c *Ctx) Tick() uint64
- func (c *Ctx) True(check string, got bool, format string, args ...any) bool
- func (c *Ctx) Vel(id cardinal.EntityID) physics.Vec2
- type Diff
- type EventKind
- type LoggedEvent
- type Outcome
- type ProbeRow
- type Report
- func (r *Report) BindTB(tb testing.TB)
- func (r *Report) Bound() bool
- func (r *Report) Fail(scenario, check string, tick uint64, format string, args ...any)
- func (r *Report) Failures() []Result
- func (r *Report) Note(scenario string, tick uint64, format string, args ...any)
- func (r *Report) Pass(scenario, check string, tick uint64)
- func (r *Report) Print()
- func (r *Report) Skip(scenario, check string, tick uint64, format string, args ...any)
- func (r *Report) Totals() (int, int, int)
- type Result
- type Runner
- func (r *Runner) Advance(w *cardinal.World, n int)
- func (r *Runner) BuildWorld(cfg Config) (*cardinal.World, error)
- func (r *Runner) Digest(w *cardinal.World) (int, uint64)
- func (r *Runner) EventsUpTo(scenario string, kind EventKind, tick uint64) int
- func (r *Runner) LastTick() uint64
- func (r *Runner) Plugin() *physics.Plugin
- func (r *Runner) Report() *Report
- func (r *Runner) Run(w *cardinal.World) int
- func (r *Runner) Ticked() int
- type Scenario
- type SingletonRow
- type Step
Constants ¶
const LaneWidth = 300.0
LaneWidth is the world-space X spacing between scenarios. Every scenario is laid out in its own lane so bodies from unrelated scenarios can never touch, collide, or show up in each other's raycasts and AABB overlaps. Scenario code is written in lane-local coordinates; Ctx translates on the way in and out.
const TailTicks = 10
TailTicks is how many extra ticks run after the last scheduled step, so a scenario's final mutation still gets simulated and the NaN watchdog still sees the resulting state.
const TickRate = 60.0
TickRate is the simulation rate for both Cardinal and the physics plugin. The two must match or simulated time drifts from tick time.
Variables ¶
This section is empty.
Functions ¶
func CloneBody ¶
func CloneBody(pb physics.PhysicsBody2D) physics.PhysicsBody2D
CloneBody deep-copies a PhysicsBody2D including its shapes and their chain geometry, so edits to the copy cannot reach the original. immutable.Slice derivations write through the array they share with the component, so a scenario that means to edit a body has to start from a copy like this one; Map and Collect are the two derivations that allocate instead.
func DecodeSnapshot ¶
DecodeSnapshot is the inverse of EncodeSnapshot.
func EncodeSnapshot ¶
EncodeSnapshot marshals a world state to protobuf bytes, the form a snapshot actually takes on its way to JetStream or S3. Going through bytes is what makes a two-process restore a real one.
func InitECS ¶
InitECS runs the world's Init-hook systems and marks the ECS world initialized.
cardinal.World only does this inside StartGame, which also stands up NATS and the ConnectRPC service. This test game drives World.Tick directly so it can run with no infrastructure at all, so it reaches the unexported ecs.World through reflection — the same approach the plugin's own integration tests use.
func RegisterPostCapture ¶
RegisterPostCapture registers a capture that runs after the physics pipeline.
func RegisterPreCapture ¶
RegisterPreCapture registers a capture that runs before the physics plugin. Call it before RegisterPlugin. After a snapshot restore, the first tick's pre-capture is the deserialized ECS state with nothing else having touched it.
func RestoreWorld ¶
RestoreWorld loads a serialized world state, the way World.restore does after a crash. Note the ordering Cardinal uses: Init systems run first and only then is their state thrown away by this call.
Types ¶
type Capture ¶
type Capture struct {
Rows map[string]CaptureRow
// Contacts is the singleton's ActiveContacts, normalised and sorted.
Contacts []physics.ContactPairEntry
// Singletons is how many physics singleton entities exist. Anything but one
// is a bug: the plugin panics on two and loses its dedupe baseline on none.
Singletons int
}
Capture is every body in a world, keyed by its probe label, plus the plugin's singleton state. Labels are used rather than entity IDs so a capture stays comparable across two worlds.
type CaptureRow ¶
type CaptureRow struct {
Transform physics.Transform2D
Velocity physics.Velocity2D
Body physics.PhysicsBody2D
Entity cardinal.EntityID
}
CaptureRow is the complete physics state of one body.
type Config ¶
type Config struct {
// Gravity is the world gravity vector handed to the physics plugin.
Gravity physics.Vec2
// SubStepCount is the plugin's solver sub-step count; zero uses the plugin default.
SubStepCount int
// Verbose prints passing checks as they happen, not just failures.
Verbose bool
// ExtraTicks runs the loop longer than the scenarios strictly require.
ExtraTicks uint64
// PreCapture, when set, receives a copy of every body's components at the
// start of each tick, before the physics plugin runs.
PreCapture *Capture
// PostCapture, when set, receives the same after the physics pipeline has
// reconciled, stepped and written back.
PostCapture *Capture
// Workers is the physics engine's worker count. Results are identical for
// every value, so this only changes throughput.
Workers int
// Digest prints a digest of every body's final state at the end of the run.
// Two runs of the same binary must produce the same digest; if they do not,
// something in the pipeline is reading wall-clock time, map order, or
// uninitialised memory.
Digest bool
// TB, when set, routes every check to a testing.TB as well as the report:
// failures through Errorf at the scenario's call site, notes and skips through
// Logf. The go test entry points set it; the CLI leaves it nil.
TB testing.TB
}
Config controls how the harness builds its world.
type Ctx ¶
type Ctx struct {
// contains filtered or unexported fields
}
Ctx is the API a scenario uses to spawn, inspect, mutate and assert. All positions crossing this boundary are lane-local: Ctx adds the lane offset when writing to ECS and subtracts it when reading back.
func (*Ctx) Body ¶
func (c *Ctx) Body(id cardinal.EntityID) physics.PhysicsBody2D
Body returns a deep copy of the entity's PhysicsBody2D. The Shapes slice is cloned so a scenario can edit it without aliasing ECS storage; write it back with SetBody.
func (*Ctx) CircleSweep ¶
func (c *Ctx) CircleSweep( fromX, fromY, toX, toY, radius, maxFraction float64, filter *physics.Filter, ) physics.CircleSweepResult
CircleSweep sweeps a circle between two lane-local points.
func (*Ctx) CountBetween ¶
CountBetween counts this scenario's events of a kind involving exactly a and b.
func (*Ctx) CountTouching ¶
CountTouching counts this scenario's events of a kind naming the given entity.
func (*Ctx) EditBody ¶
func (c *Ctx) EditBody(id cardinal.EntityID, edit func(pb *physics.PhysicsBody2D))
EditBody reads the body, hands it to edit, and writes the result back.
func (*Ctx) EditShape ¶ added in v0.16.8
EditShape applies edit to one shape of the entity's body and writes the body back. Shapes hands out element copies, so a shape changes by being read out, edited, and put back with With — there is no index to assign through.
func (*Ctx) Events ¶
func (c *Ctx) Events(kind EventKind) []LoggedEvent
Events returns every event of the given kind recorded for this scenario so far.
func (*Ctx) EventsBetween ¶
func (c *Ctx) EventsBetween(kind EventKind, a, b cardinal.EntityID) []LoggedEvent
EventsBetween returns this scenario's events of a kind involving exactly a and b.
func (*Ctx) EventsOnTick ¶
func (c *Ctx) EventsOnTick(kind EventKind, tick uint64) []LoggedEvent
EventsOnTick returns this scenario's events of a kind recorded on one tick.
func (*Ctx) ExpectWorldReset ¶
func (c *Ctx) ExpectWorldReset()
ExpectWorldReset silences the runner's "the world disappeared" watchdog for its next check, so a scenario that calls Plugin.Reset can say so. The watchdog runs on PreUpdate, so a grant from Steps (Update) is consumed on the next tick and one from EachTick (PreUpdate) on the same tick.
func (*Ctx) IntAtLeast ¶
IntAtLeast asserts an integer is at least want.
func (*Ctx) Label ¶
Label returns the human-readable label recorded for an entity at spawn time. It keeps working after the entity is destroyed, which matters for end events.
func (*Ctx) OverlapAABB ¶
func (c *Ctx) OverlapAABB(minX, minY, maxX, maxY float64, filter *physics.Filter) physics.AABBOverlapResult
OverlapAABB queries a lane-local axis-aligned box.
func (*Ctx) OverlapHits ¶
OverlapHits reports whether an AABB overlap returned the given entity.
func (*Ctx) OverlapHitsShape ¶
func (c *Ctx) OverlapHitsShape(res physics.AABBOverlapResult, id cardinal.EntityID, shapeIndex int) bool
OverlapHitsShape reports whether an AABB overlap returned a specific shape slot.
func (*Ctx) Plugin ¶
Plugin returns the physics plugin driving this world. Queries, Reset and Engine are methods on it — the package holds no runtime state.
func (*Ctx) Raycast ¶
Raycast casts from one lane-local point to another. The returned hit point is translated back into lane-local space.
func (*Ctx) SetBody ¶
func (c *Ctx) SetBody(id cardinal.EntityID, pb physics.PhysicsBody2D)
SetBody replaces the entity's PhysicsBody2D.
func (*Ctx) SpawnFull ¶
func (c *Ctx) SpawnFull( label string, t physics.Transform2D, v physics.Velocity2D, pb physics.PhysicsBody2D, ) cardinal.EntityID
SpawnFull creates a probe entity from a complete pose, velocity and body.
func (*Ctx) SpawnMoving ¶
func (c *Ctx) SpawnMoving( label string, x, y, vx, vy float64, pb physics.PhysicsBody2D, ) cardinal.EntityID
SpawnMoving creates a probe entity with an initial linear velocity.
func (*Ctx) SpawnSpinning ¶
func (c *Ctx) SpawnSpinning( label string, x, y, angular float64, pb physics.PhysicsBody2D, ) cardinal.EntityID
SpawnSpinning creates a probe entity with an initial angular velocity.
type Diff ¶
Diff describes one field that differs between two captures.
func CompareCaptures ¶
CompareCaptures reports every field of every body that differs between want and got, within tol on floats. Bodies present in one capture and not the other are reported too, because a body lost in a rebuild is the worst failure of all.
func CompareContacts ¶
CompareContacts reports differences between two worlds' ActiveContacts.
type EventKind ¶
type EventKind uint8
EventKind distinguishes the four physics system events.
Physics system event kinds.
type LoggedEvent ¶
type LoggedEvent struct {
Payload physics.ContactEventPayload
Tick uint64
Kind EventKind
}
LoggedEvent is one physics event captured with the tick it arrived on.
func (LoggedEvent) FilterFor ¶
func (e LoggedEvent) FilterFor(id cardinal.EntityID) (physics.FixtureFilterBits, bool)
FilterFor returns the fixture filter the given entity contributed to the event.
func (LoggedEvent) Involves ¶
func (e LoggedEvent) Involves(a, b cardinal.EntityID) bool
Involves reports whether the event is between exactly a and b, in either order.
func (LoggedEvent) ShapeIndexFor ¶
func (e LoggedEvent) ShapeIndexFor(id cardinal.EntityID) (int, bool)
ShapeIndexFor returns the shape slot the given entity contributed to the event, so a compound body's contacts can be traced back to the exact child.
type ProbeRow ¶
type ProbeRow struct {
Probe probe.Probe
Transform physics.Transform2D
Velocity physics.Velocity2D
Body physics.PhysicsBody2D
}
ProbeRow is the archetype every harness-spawned body uses.
type Report ¶
type Report struct {
// contains filtered or unexported fields
}
Report accumulates assertion results and prints them. It is written for a single-goroutine tick loop, so it holds no lock.
func NewReport ¶
NewReport builds an empty report. verbose prints every passing assertion; otherwise only failures, skips and notes are streamed while the run proceeds.
func (*Report) BindTB ¶
BindTB routes every result to a testing.TB as well as the report: failures through Errorf, attributed to the scenario's own line via Helper; notes, skips and (when verbose) passes through Logf. Colour is switched off because the destination is a test log. The CLI never binds.
func (*Report) Bound ¶
Bound reports whether results are being routed to a testing.TB.
func (*Report) Fail ¶
Fail records a violated assertion and prints it immediately, so a run that later panics still shows everything that had already gone wrong.
func (*Report) Note ¶
Note records a diagnostic observation that is neither pass nor fail. Use it for behaviour worth eyeballing (measured bounce heights, whether a non-bullet body tunnelled) where a hard threshold would only produce flaky failures.
func (*Report) Print ¶
func (r *Report) Print()
Print writes the end-of-run summary: a per-scenario table, then a repeat of every failure so the verdict does not require scrolling back through the log.
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
Runner owns the scenario list, the report, and the tick loop.
func (*Runner) Advance ¶
Advance ticks the world n times, continuing from wherever it left off. The timestamps are derived from the tick index so two worlds ticked the same number of times see the same sequence.
func (*Runner) BuildWorld ¶
BuildWorld creates the Cardinal world and registers everything in the order the physics pipeline requires: scenario Init and PreUpdate systems first, then the plugin, then the Update-hook step system.
func (*Runner) Digest ¶
printDigest reports a hash over every surviving body's final pose and velocity, plus the extremes, so two runs can be compared at a glance. Digest hashes every probe body's final pose and velocity and returns the body count with the hash. Two runs of the same build on the same machine must agree, and so must runs that differ only in Config.Workers.
func (*Runner) EventsUpTo ¶
EventsUpTo counts events of a kind recorded for a scenario on or before a tick. The restore check uses it to tell "the rebuilt world replayed every existing overlap as new" apart from "the rebuilt world kept its dedupe baseline".
func (*Runner) Plugin ¶
Plugin returns the physics plugin bound to this runner's world. It is nil until BuildWorld has run.
type Scenario ¶
type Scenario struct {
Setup func(c *Ctx)
EachTick func(c *Ctx)
Name string
Steps []Step
// contains filtered or unexported fields
}
Scenario is one self-contained group of physics checks.
Setup runs on cardinal.Init, before the plugin's own Init system, so the initial FullRebuildFromECS sees the bodies. EachTick runs on PreUpdate before the physics pipeline (use it to drive gameplay-owned bodies). Steps run on Update, after the pipeline, where post-step positions and this tick's contact events are both visible.
type SingletonRow ¶
type SingletonRow struct {
Tag physics.PhysicsSingletonTag
ActiveContacts physics.ActiveContacts
}
SingletonRow is the plugin's own bookkeeping entity. It carries ActiveContacts, the persisted record of which pairs have had a Begin emitted and not yet an End. The physics step diffs it against Box2D's live contact list after a rebuild, so if it does not survive a restore the rebuilt world replays every existing overlap as a new contact.