package examples_test import ( "encoding/binary" "math" "os" "testing" "wh/config" "wh/fixed" "wh/sim" "wh/vm" ) func asm(t *testing.T, name string) []byte { t.Helper() src, err := os.ReadFile("programs/" + name) if err != nil { t.Fatal(err) } p, err := vm.Assemble(string(src)) if err != nil { t.Fatalf("%s: %v", name, err) } return p } func cfg() config.Config { c := config.Default() c.AsteroidCount = 50 return c } func launch(t *testing.T, c config.Config, prog []byte, uplink []byte) (*sim.State, sim.DayResult) { t.Helper() s := sim.NewState(c) in := sim.DayInput{Launches: []sim.Launch{{ShipID: 1, Owner: 1, Program: prog}}} if uplink != nil { in.Uplinks = []sim.Uplink{{ShipID: 1, Data: uplink}} } res, err := s.RunDay(c, in) if err != nil { t.Fatal(err) } return s, res } func TestEcho(t *testing.T) { msg := make([]byte, 300) for i := range msg { msg[i] = byte(i*7 + 1) } _, res := launch(t, cfg(), asm(t, "echo.s"), msg) got := res.Downlinks[0].Data for i := range msg { if got[i] != msg[i] { t.Fatalf("byte %d: got %d want %d", i, got[i], msg[i]) } } if got[300] != 0 { t.Fatal("echoed too much") } } func TestTelemetry(t *testing.T) { c := cfg() s, res := launch(t, c, asm(t, "telemetry.s"), nil) d := res.Downlinks[0].Data w := func(i int) int32 { return int32(binary.LittleEndian.Uint32(d[i*4:])) } if w(0) != int32(c.TicksPerDay-1) || w(1) != 4000 { t.Fatalf("tick=%d fuel=%d", w(0), w(1)) } // The last report was taken one tick (<= ~180 km) before the final state. end := s.Ships[0].Pos for i, got := range []int32{w(2), w(3), w(4)} { want := []fixed.F{end.X, end.Y, end.Z}[i].Floor() if math.Abs(float64(int64(got)-want)) > 200 { t.Fatalf("axis %d: reported %d, ship at %d", i, got, want) } } } func TestPursueAims(t *testing.T) { c := cfg() c.TicksPerDay = 1 // one long tick: the program aims once, from t=0 state s := sim.NewState(c) stPos, _ := s.Station.Orbit.State(0) // Expected: nearest asteroid, and the bearing to it in whole km. best, bestD := 0, fixed.Max for i, a := range s.Asteroids { p, _ := a.Orbit.State(0) if d := p.Sub(stPos).Len(); d < bestD { best, bestD = i, d } } tp, _ := s.Asteroids[best].Orbit.State(0) rel := tp.Sub(stPos) dx, dy, dz := float64(rel.X.Floor()), float64(rel.Y.Floor()), float64(rel.Z.Floor()) wantAz := math.Atan2(dy, dx) * 1000 wantEl := math.Atan2(dz, math.Hypot(dx, dy)) * 1000 if _, err := s.RunDay(c, sim.DayInput{Launches: []sim.Launch{{ShipID: 1, Owner: 1, Program: asm(t, "pursue.s")}}}); err != nil { t.Fatal(err) } sh := s.Ships[0] if sh.Target != int64(best+1) || sh.Throttle != 1000 { t.Fatalf("target=%d (want %d) throttle=%d", sh.Target, best+1, sh.Throttle) } if math.Abs(float64(sh.Azimuth)-wantAz) > 3 || math.Abs(float64(sh.Pitch)-wantEl) > 3 { t.Fatalf("aim az=%d el=%d, want %.0f %.0f", sh.Azimuth, sh.Pitch, wantAz, wantEl) } } func TestPortsEquatesAssemble(t *testing.T) { // The standard equate file must assemble and be usable alongside code. src, err := os.ReadFile("programs/ports.s") if err != nil { t.Fatal(err) } if _, err := vm.Assemble(string(src) + "\n in r1, P_UPNEW\n out P_THROTTLE, r1\n halt\n"); err != nil { t.Fatal(err) } }