Files

196 lines
5.6 KiB
Go

package world
import (
"math"
"testing"
)
func TestStackSettles(t *testing.T) {
w := newTestWorld(t, nil)
groundBox(t, w, nil)
var boxes []BodyHandle
for i := 0; i < 10; i++ {
b, _ := addDynamic(t, w, Vec2{Y: -0.5 - float32(i)}, 0, Box{Half: Vec2{X: 0.5, Y: 0.5}}, func(d *BodyDef, _ *ShapeDef) { d.AllowSleep = false })
boxes = append(boxes, b)
}
run(w, 300)
mid := make([]Vec2, len(boxes))
for i, b := range boxes {
mid[i] = posOf(w, b)
}
run(w, 300)
for i, b := range boxes {
p := posOf(w, b)
if !near2(p, mid[i], 1e-3) {
t.Fatalf("box %d drifted from %v to %v", i, mid[i], p)
}
}
if top := posOf(w, boxes[9]); !near(top.X, 0, 0.05) || !near(top.Y, -9.5, 0.1) {
t.Fatalf("stack fell: top at %v", top)
}
}
func TestPyramidStands(t *testing.T) {
w := newTestWorld(t, nil)
groundBox(t, w, nil)
type item struct {
b BodyHandle
p Vec2
}
var all []item
const rows = 15
for r := 0; r < rows; r++ {
n := rows - r
for k := 0; k < n; k++ {
p := Vec2{X: (float32(k) - float32(n-1)/2) * 0.5, Y: -0.25 - float32(r)*0.5}
b, _ := addDynamic(t, w, p, 0, Box{Half: Vec2{X: 0.25, Y: 0.25}}, nil)
all = append(all, item{b, p})
}
}
run(w, 600)
for i, it := range all {
if p := posOf(w, it.b); p.Sub(it.p).Len() > 0.2 {
t.Fatalf("box %d moved from %v to %v", i, it.p, p)
}
}
}
func slopeBox(t *testing.T, mu float32) float32 {
w := newTestWorld(t, nil)
a := float32(math.Pi / 6)
slope := addStatic(t, w, Vec2{}, a, Box{Half: Vec2{X: 20, Y: 0.5}}, func(s *ShapeDef) { s.Friction = mu })
sp, _, _ := w.BodyTransform(slope)
start := xform{p: sp, q: makeRot(a)}.apply(Vec2{Y: -0.75})
b, _ := addDynamic(t, w, start, a, Box{Half: Vec2{X: 0.25, Y: 0.25}}, func(_ *BodyDef, s *ShapeDef) { s.Friction = mu })
run(w, 120)
return posOf(w, b).Sub(start).Len()
}
func TestSlopeFriction(t *testing.T) {
if d := slopeBox(t, 0.6); d > 0.05 {
t.Fatalf("mu=0.6 slid %v on 30 degrees", d)
}
if d := slopeBox(t, 0.4); d < 0.5 {
t.Fatalf("mu=0.4 slid only %v on 30 degrees", d)
}
}
func bounce(t *testing.T, e float32) float32 {
w := newTestWorld(t, func(c *Config) { c.Gravity = Vec2{} })
groundBox(t, w, func(s *ShapeDef) { s.Restitution = e })
b, _ := addDynamic(t, w, Vec2{Y: -3}, 0, Circle{R: 0.5}, func(d *BodyDef, s *ShapeDef) {
d.LinearVelocity = Vec2{Y: 5}
s.Restitution = e
})
run(w, 60)
return velOf(w, b).Y
}
func TestRestitution(t *testing.T) {
if v := bounce(t, 1); !near(v, -5, 0.05) {
t.Fatalf("e=1 bounced with %v, want -5", v)
}
if v := bounce(t, 0); !near(v, 0, 0.05) {
t.Fatalf("e=0 bounced with %v, want 0", v)
}
}
func TestRestingBallDoesNotJitter(t *testing.T) {
w := newTestWorld(t, nil)
groundBox(t, w, func(s *ShapeDef) { s.Restitution = 0.9 })
b, _ := addDynamic(t, w, Vec2{Y: -0.5}, 0, Circle{R: 0.5}, func(d *BodyDef, s *ShapeDef) {
d.AllowSleep = false
s.Restitution = 0.9
})
run(w, 180)
if v := velOf(w, b); v.Len() > 0.05 {
t.Fatalf("resting ball moves with %v", v)
}
if p := posOf(w, b); !near(p.Y, -0.5, 0.02) {
t.Fatalf("resting ball at %v", p)
}
}
func TestBulletDoesNotTunnel(t *testing.T) {
w := newTestWorld(t, func(c *Config) { c.Gravity = Vec2{} })
addStatic(t, w, Vec2{X: 10}, 0, Box{Half: Vec2{X: 0.05, Y: 5}}, nil)
b, _ := addDynamic(t, w, Vec2{}, 0, Circle{R: 0.1}, func(d *BodyDef, _ *ShapeDef) {
d.Bullet = true
d.LinearVelocity = Vec2{X: 200}
})
for i := 0; i < 60; i++ {
w.Step(dt)
if p := posOf(w, b); p.X > 9.95 {
t.Fatalf("bullet passed the wall at step %d: %v", i, p)
}
}
}
func TestMaxTranslationClamps(t *testing.T) {
w := newTestWorld(t, func(c *Config) { c.Gravity = Vec2{} })
b, _ := addDynamic(t, w, Vec2{}, 0, Circle{R: 0.5}, func(d *BodyDef, _ *ShapeDef) { d.LinearVelocity = Vec2{X: 1000} })
w.Step(dt)
if p := posOf(w, b); p.X > w.cfg.MaxTranslationPerStep+1e-4 {
t.Fatalf("body moved %v in one step", p.X)
}
}
func TestNaNImpulseIsRejected(t *testing.T) {
w := newTestWorld(t, nil)
groundBox(t, w, nil)
b, _ := addDynamic(t, w, Vec2{Y: -2}, 0, Box{Half: Vec2{X: 0.5, Y: 0.5}}, nil)
run(w, 10)
before := w.Snapshot()
nan := float32(math.NaN())
if err := w.ApplyImpulse(b, Vec2{X: nan}); err == nil {
t.Fatal("NaN impulse accepted")
}
if err := w.ApplyForceAtPoint(b, Vec2{X: 1}, Vec2{Y: nan}); err == nil {
t.Fatal("NaN point accepted")
}
if err := w.SetBodyTransform(b, Vec2{X: float32(math.Inf(1))}, 0); err == nil {
t.Fatal("infinite transform accepted")
}
if !before.Equal(w.Snapshot()) {
t.Fatal("rejected input changed the world")
}
}
func columnTop(t *testing.T, n int, mod func(*Config)) Vec2 {
w := newTestWorld(t, mod)
groundBox(t, w, nil)
var top BodyHandle
for i := 0; i < n; i++ {
top, _ = addDynamic(t, w, Vec2{Y: -0.5 - float32(i)}, 0, Box{Half: Vec2{X: 0.5, Y: 0.5}}, func(d *BodyDef, _ *ShapeDef) { d.AllowSleep = false })
}
run(w, 600)
return posOf(w, top)
}
func TestTallColumnStands(t *testing.T) {
if p := columnTop(t, 12, nil); !near(p.X, 0, 0.05) || !near(p.Y, -11.5, 0.1) {
t.Fatalf("column leans: top at %v", p)
}
}
func TestTallColumnStandsStiff(t *testing.T) {
stiff := func(c *Config) {
c.ContactHertz, c.Substeps, c.VelocityIterations, c.RelaxIterations = 60, 8, 1, 1
}
if p := columnTop(t, 20, stiff); !near(p.X, 0, 0.05) || !near(p.Y, -19.5, 0.1) {
t.Fatalf("column leans: top at %v", p)
}
}
func TestConfigIterationsZeroMeansDefault(t *testing.T) {
w := must(New(Config{}))
if c := w.Config(); c.VelocityIterations != 1 || c.RelaxIterations != 1 {
t.Fatalf("zero iterations did not take the defaults: %+v", c)
}
for _, c := range []Config{{RelaxIterations: -1}, {VelocityIterations: -1}, {RelaxIterations: 65}} {
if _, err := New(c); err == nil {
t.Errorf("config %+v must be rejected", c)
}
}
}