759 lines
25 KiB
Go
759 lines
25 KiB
Go
package sim
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/gob"
|
|
"math"
|
|
"testing"
|
|
|
|
"halcyon/config"
|
|
"halcyon/fixed"
|
|
"halcyon/market"
|
|
"halcyon/vm"
|
|
"halcyon/world"
|
|
)
|
|
|
|
const mineProgram = `
|
|
ldi r0, 1
|
|
out 0x30, r0 ; select asteroid 1
|
|
out 0x50, r0 ; mine
|
|
yield
|
|
jmp -2
|
|
`
|
|
|
|
// ctxAt is a turn context at absolute time now, for calling the container rules directly.
|
|
func ctxAt(s *State, cfg config.Config, now int64) *tickCtx {
|
|
pos, vel := s.Station.Orbit.State(now)
|
|
return newTickCtx(s, cfg, 0, now, pos, vel, &DayResult{}, new([world.NumOre]int64))
|
|
}
|
|
|
|
// dockAt puts the ship at the station as it is at absolute time now.
|
|
func dockAt(s *State, sh *Ship, now int64) {
|
|
sh.Pos, sh.Vel = s.Station.Orbit.State(now)
|
|
}
|
|
|
|
func expiredEvents(tc *tickCtx) (n int) {
|
|
for _, e := range tc.res.Events {
|
|
if e.Kind == "expired" {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func towed(s *State, sh *Ship, n int) []*Container {
|
|
var out []*Container
|
|
for i := 0; i < n; i++ {
|
|
c := s.newContainer(sh.Owner, sh.Pos, sh.Vel)
|
|
c.Towed = sh.ID
|
|
out = append(out, c)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestMiningFillsTowedContainers(t *testing.T) {
|
|
cfg := testCfg()
|
|
|
|
// With no container in tow there is nowhere to put ore: nothing is mined.
|
|
s := NewState(cfg)
|
|
pos, vel := s.Asteroids[0].Orbit.State(0)
|
|
before := s.Asteroids[0].TotalOre()
|
|
addShip(t, s, cfg, 1, mustAsm(t, mineProgram), pos, vel)
|
|
if _, err := s.RunDay(cfg, DayInput{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Asteroids[0].TotalOre() != before {
|
|
t.Fatal("ore was taken from the asteroid with no container to hold it")
|
|
}
|
|
|
|
// With a container in tow, ore goes into it. (Gravity soon carries the ship off
|
|
// the rails-mounted asteroid, so only the first stretch mines.)
|
|
s = NewState(cfg)
|
|
pos, vel = s.Asteroids[0].Orbit.State(0)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, mineProgram), pos, vel)
|
|
cs := towed(s, sh, 2)
|
|
cs[0].Ore[world.Iron] = ContainerCapKg - 15 // nearly full: takes 15 kg, the rest spills into the next
|
|
if _, err := s.RunDay(cfg, DayInput{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mined := before - s.Asteroids[0].TotalOre()
|
|
if mined <= 15 {
|
|
t.Fatalf("only %d kg mined", mined)
|
|
}
|
|
if cs[0].OreTotal() != ContainerCapKg {
|
|
t.Fatalf("the first container holds %d kg, want it full (%d)", cs[0].OreTotal(), ContainerCapKg)
|
|
}
|
|
if got := cs[0].OreTotal() + cs[1].OreTotal() - (ContainerCapKg - 15); got != mined {
|
|
t.Fatalf("containers gained %d kg but the asteroid lost %d", got, mined)
|
|
}
|
|
}
|
|
|
|
func TestMiningStopsWhenContainersAreFull(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
pos, vel := s.Asteroids[0].Orbit.State(0)
|
|
before := s.Asteroids[0].TotalOre()
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, mineProgram), pos, vel)
|
|
c := towed(s, sh, 1)[0]
|
|
c.Ore[world.Iron] = ContainerCapKg
|
|
if _, err := s.RunDay(cfg, DayInput{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Asteroids[0].TotalOre() != before || c.OreTotal() != ContainerCapKg {
|
|
t.Fatal("a full container should stop the laser")
|
|
}
|
|
}
|
|
|
|
func TestOrderContainersAtTheStation(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
stPos, stVel := s.Station.Orbit.State(0)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt"), stPos, stVel)
|
|
|
|
if n := s.order(sh, 3, ctxAt(s, cfg, 0)); n != 3 || len(s.Containers) != 3 {
|
|
t.Fatalf("ordered 3, got %d (%d exist)", n, len(s.Containers))
|
|
}
|
|
for i, c := range s.Containers {
|
|
if c.ID != int64(i+1) || c.OrderedBy != 1 || c.Towed != 0 || c.OreTotal() != 0 || c.Pos != sh.Pos || c.Vel != sh.Vel {
|
|
t.Fatalf("delivered container %d: %+v (want empty, adrift, ordered by 1, at the ship)", i, c)
|
|
}
|
|
}
|
|
if n := s.order(sh, 25, ctxAt(s, cfg, 0)); n != MaxOrder {
|
|
t.Fatalf("a request for 25 delivered %d, want the maximum %d", n, MaxOrder)
|
|
}
|
|
if n := s.order(sh, 0, ctxAt(s, cfg, 0)); n != 0 {
|
|
t.Fatal("ordering none delivered some")
|
|
}
|
|
if n := s.order(sh, -4, ctxAt(s, cfg, 0)); n != 0 {
|
|
t.Fatal("a negative order delivered some")
|
|
}
|
|
|
|
// Never past the per-player ordering allowance.
|
|
for i := 0; i < 10; i++ {
|
|
s.order(sh, MaxOrder, ctxAt(s, cfg, 0))
|
|
}
|
|
if len(s.Containers) != MaxContainersPerPlayer {
|
|
t.Fatalf("%d containers, want the limit %d", len(s.Containers), MaxContainersPerPlayer)
|
|
}
|
|
// Another player has their own allowance.
|
|
other := &Ship{ID: 2, Owner: 2, Pos: stPos, Vel: stVel, Alive: true}
|
|
if n := s.order(other, 4, ctxAt(s, cfg, 0)); n != 4 {
|
|
t.Fatalf("second player got %d", n)
|
|
}
|
|
|
|
// Not near the station: refused.
|
|
far := NewState(cfg)
|
|
apos, avel := far.Asteroids[0].Orbit.State(0)
|
|
fsh := addShip(t, far, cfg, 1, mustAsm(t, "halt"), apos, avel)
|
|
if n := far.order(fsh, 5, ctxAt(far, cfg, 0)); n != 0 || len(far.Containers) != 0 {
|
|
t.Fatalf("delivered %d far from the station", n)
|
|
}
|
|
// Just inside and just outside the station's reach.
|
|
edge := NewState(cfg)
|
|
inside := &Ship{ID: 1, Owner: 1, Pos: stPos.Add(fixed.Vec{X: fixed.FromInt(DockRangeKm - 1)}), Alive: true}
|
|
outside := &Ship{ID: 2, Owner: 1, Pos: stPos.Add(fixed.Vec{X: fixed.FromInt(DockRangeKm + 1)}), Alive: true}
|
|
if edge.order(inside, 1, ctxAt(edge, cfg, 0)) != 1 || edge.order(outside, 1, ctxAt(edge, cfg, 0)) != 0 {
|
|
t.Fatal("the station's reach is not DockRangeKm")
|
|
}
|
|
}
|
|
|
|
func TestAttachAndDetach(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
pos, vel := s.Station.Orbit.State(0)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt"), pos, vel)
|
|
|
|
km := func(x int64) fixed.Vec { return fixed.Vec{X: fixed.FromInt(x)} }
|
|
near1 := s.newContainer(1, pos, vel)
|
|
near2 := s.newContainer(1, pos.Add(km(ContainerRangeKm-1)), vel)
|
|
fromElsewhere := s.newContainer(2, pos, vel) // ordered by another player: containers are not owned
|
|
tooFar := s.newContainer(1, pos.Add(km(ContainerRangeKm+1)), vel)
|
|
tooFast := s.newContainer(1, pos, vel.Add(fixed.Vec{Y: fixed.FromRatio(200, 1000)})) // 200 m/s away
|
|
already := s.newContainer(1, pos, vel)
|
|
already.Towed = 99 // being towed by another ship
|
|
|
|
if n := s.nearbyFree(sh); n != 3 {
|
|
t.Fatalf("%d containers reported near, want 3", n)
|
|
}
|
|
s.attach(sh, ctxAt(s, cfg, 0))
|
|
for name, c := range map[string]*Container{"near1": near1, "near2": near2, "fromElsewhere": fromElsewhere} {
|
|
if c.Towed != sh.ID {
|
|
t.Errorf("%s was not attached", name)
|
|
}
|
|
}
|
|
if near2.Pos != sh.Pos { // attaching brings it alongside
|
|
t.Error("an attached container should move to the ship")
|
|
}
|
|
for name, c := range map[string]*Container{"tooFar": tooFar, "tooFast": tooFast} {
|
|
if c.Towed != 0 {
|
|
t.Errorf("%s should not have been attached", name)
|
|
}
|
|
}
|
|
if already.Towed != 99 {
|
|
t.Error("took a container that another ship was towing")
|
|
}
|
|
if tow := s.towOf(sh.ID); tow.Count != 3 || tow.Mass != 3*ContainerMassKg || tow.Room != 3*ContainerCapKg {
|
|
t.Fatalf("tow = %+v", tow)
|
|
}
|
|
if s.nearbyFree(sh) != 0 {
|
|
t.Error("nothing should be left to attach")
|
|
}
|
|
|
|
s.detach(sh, ctxAt(s, cfg, 0))
|
|
if near1.Towed != 0 || near2.Towed != 0 || fromElsewhere.Towed != 0 || near1.Dropped != sh.ID {
|
|
t.Fatalf("detach: %+v %+v", near1, near2)
|
|
}
|
|
if already.Towed != 99 {
|
|
t.Error("detach touched another ship's tow")
|
|
}
|
|
if s.nearbyFree(sh) != 3 {
|
|
t.Error("detached containers next to the ship can be attached again")
|
|
}
|
|
}
|
|
|
|
func TestTowedMassReducesAcceleration(t *testing.T) {
|
|
cfg := testCfg()
|
|
dvOf := func(containers int, ore int64) (dv float64, mass float64) {
|
|
s := NewState(cfg)
|
|
pos, vel := s.Station.Orbit.State(0)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt"), pos, vel)
|
|
for _, c := range towed(s, sh, containers) {
|
|
c.Ore[world.Iron] = ore
|
|
}
|
|
sh.Throttle = 1000
|
|
sh.Azimuth = 0
|
|
mass = s.shipMass(sh).Float64()
|
|
before := sh.Vel
|
|
// One burning turn; gravity's share of dv is negligible next to the engine's,
|
|
// and identical in both runs.
|
|
var res DayResult
|
|
s.physics(cfg, sh, 60, 0, &res)
|
|
return sh.Vel.Sub(before).Len().Float64(), mass
|
|
}
|
|
dv0, m0 := dvOf(0, 0)
|
|
dv4, m4 := dvOf(4, ContainerCapKg)
|
|
if want := float64(CommandShip.DryMass + CommandShip.FuelCap); math.Abs(m0-want) > 1e-6 {
|
|
t.Fatalf("bare mass = %v, want %v", m0, want)
|
|
}
|
|
if want := m0 + 4*(ContainerMassKg+ContainerCapKg); math.Abs(m4-want) > 1e-6 {
|
|
t.Fatalf("towing mass = %v, want %v", m4, want)
|
|
}
|
|
if !(dv4 < dv0) {
|
|
t.Fatalf("towing four full containers did not slow the acceleration: %v vs %v", dv4, dv0)
|
|
}
|
|
if got, want := dv0/dv4, m4/m0; math.Abs(got-want)/want > 0.01 {
|
|
t.Fatalf("acceleration ratio %.3f, mass ratio %.3f: acceleration should be inversely proportional to mass", got, want)
|
|
}
|
|
}
|
|
|
|
func TestTowedContainersTravelWithTheShip(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
pos, vel := s.Station.Orbit.State(0)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, burner), pos, vel)
|
|
cs := towed(s, sh, 3)
|
|
loose := s.newContainer(1, pos.Add(fixed.Vec{X: fixed.FromInt(3)}), vel) // near, but not attached
|
|
if _, err := s.RunDay(cfg, DayInput{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sh.Vel.Sub(vel).Len() < fixed.FromRatio(1, 10) {
|
|
t.Fatal("the ship did not burn")
|
|
}
|
|
for i, c := range cs {
|
|
if c.Pos != sh.Pos || c.Vel != sh.Vel {
|
|
t.Fatalf("towed container %d fell behind: %+v vs ship %+v %+v", i, c.Pos, sh.Pos, sh.Vel)
|
|
}
|
|
}
|
|
if loose.Pos.Sub(sh.Pos).Len() < fixed.FromInt(1000) {
|
|
t.Fatal("an unattached container should have been left behind by the burn")
|
|
}
|
|
}
|
|
|
|
func TestSoldContainersDisappearAndTheDropperIsPaid(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
stPos, stVel := s.Station.Orbit.State(0)
|
|
away := stPos.Add(fixed.Vec{Z: fixed.FromInt(50_000)}) // well outside the station's reach
|
|
a := addShip(t, s, cfg, 1, mustAsm(t, "halt"), away, stVel)
|
|
b := addShip(t, s, cfg, 2, mustAsm(t, "halt"), away, stVel)
|
|
b.Owner = 2 // addShip makes every ship player 1's
|
|
|
|
// Player 1's ship let this one go. Player 2 ordered it: that gives them no claim.
|
|
sold := s.newContainer(2, stPos, stVel)
|
|
sold.Ore[world.Iron] = 1000 // 2 credits/kg
|
|
sold.Ore[world.Ice] = 100 // 3 credits/kg
|
|
sold.Dropped = a.ID
|
|
inReach := s.newContainer(1, stPos.Add(fixed.Vec{X: fixed.FromInt(DockRangeKm - 1)}), stVel)
|
|
inReach.Ore[world.Iron] = 10
|
|
inReach.Dropped = b.ID
|
|
tooFar := s.newContainer(1, stPos.Add(fixed.Vec{X: fixed.FromInt(DockRangeKm + 10)}), stVel)
|
|
tooFar.Ore[world.Iron] = 500
|
|
tooFar.Dropped = a.ID
|
|
inTow := s.newContainer(1, stPos, stVel)
|
|
inTow.Ore[world.Iron] = 700
|
|
inTow.Towed = a.ID // a ship towing ore past the station does not sell it
|
|
empty := s.newContainer(1, stPos, stVel)
|
|
empty.Dropped = a.ID // nothing in it: the station does not buy it
|
|
nobody := s.newContainer(1, stPos, stVel)
|
|
nobody.Ore[world.Iron] = 40 // no ship ever let it go, so there is no one to pay
|
|
nobody.Dropped = 0
|
|
|
|
if _, err := s.RunDay(cfg, DayInput{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := int64(1000*2 + 100*3); s.Credits[1] != want {
|
|
t.Errorf("player 1 (the ship that let go) is paid %d, want %d", s.Credits[1], want)
|
|
}
|
|
if want := int64(10 * 2); s.Credits[2] != want {
|
|
t.Errorf("player 2 is paid %d, want %d (only for the container their ship let go)", s.Credits[2], want)
|
|
}
|
|
if a.Earned != int64(1000*2+100*3) || b.Earned != 20 {
|
|
t.Errorf("ships earned %d and %d", a.Earned, b.Earned)
|
|
}
|
|
|
|
// Sold containers are gone, with their ore. Everything else is still there.
|
|
var left []*Container
|
|
for _, c := range s.Containers {
|
|
left = append(left, c)
|
|
}
|
|
want := map[*Container]bool{tooFar: true, inTow: true, empty: true, nobody: true}
|
|
if len(left) != len(want) {
|
|
t.Fatalf("%d containers left, want %d", len(left), len(want))
|
|
}
|
|
for _, c := range left {
|
|
if !want[c] {
|
|
t.Errorf("container %d survived its sale", c.ID)
|
|
}
|
|
}
|
|
if tooFar.OreTotal() != 500 || inTow.OreTotal() != 700 || nobody.OreTotal() != 40 {
|
|
t.Error("ore that was not sold must stay in its container")
|
|
}
|
|
if s.Market.Supply[world.Iron] != 1010 || s.Market.Supply[world.Ice] != 100 {
|
|
t.Errorf("market supply %+v: sold ore should count", s.Market.Supply)
|
|
}
|
|
}
|
|
|
|
// The ordering allowance counts the containers a player ordered that still exist.
|
|
// A sale removes them, so selling a load frees room to order the next.
|
|
func TestSaleFreesTheOrderingAllowance(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
stPos, stVel := s.Station.Orbit.State(0)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt"), stPos, stVel)
|
|
var res DayResult
|
|
for i := 0; i < 5; i++ {
|
|
s.order(sh, MaxOrder, ctxAt(s, cfg, 0))
|
|
}
|
|
if len(s.Containers) != MaxContainersPerPlayer || s.order(sh, 1, ctxAt(s, cfg, 0)) != 0 {
|
|
t.Fatalf("expected to be at the allowance (%d containers)", len(s.Containers))
|
|
}
|
|
for _, c := range s.Containers[:10] {
|
|
c.Ore[world.Iron] = 100
|
|
c.Dropped = sh.ID
|
|
c.Pos = stPos
|
|
}
|
|
tc := newTickCtx(s, cfg, 0, 0, stPos, stVel, &res, new([world.NumOre]int64))
|
|
s.sellNearStation(tc)
|
|
if len(s.Containers) != MaxContainersPerPlayer-10 {
|
|
t.Fatalf("%d containers after selling 10", len(s.Containers))
|
|
}
|
|
if n := s.order(sh, MaxOrder, ctxAt(s, cfg, 0)); n != 10 {
|
|
t.Fatalf("after the sale the player could order %d, want 10", n)
|
|
}
|
|
}
|
|
|
|
func TestFreeContainersOrbitAndFallIntoTheStar(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
pos, vel := s.Station.Orbit.State(0)
|
|
// A container let go at the station's velocity stays in the station's orbit.
|
|
drifter := s.newContainer(1, pos, vel)
|
|
r0 := pos.Len().Float64()
|
|
// One with no speed, closer in, falls straight into the star within a few days
|
|
// (well inside the ten a container adrift is allowed).
|
|
faller := s.newContainer(1, fixed.Vec{X: fixed.FromInt(700_000)}, fixed.Vec{})
|
|
faller.Ore[world.Platinum] = 1000
|
|
addShip(t, s, cfg, 1, mustAsm(t, "halt"), fixed.Vec{X: fixed.FromInt(10_000_000)}, fixed.Vec{})
|
|
|
|
var res DayResult
|
|
for d := 0; d < 6; d++ {
|
|
r, err := s.RunDay(cfg, DayInput{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res.Events = append(res.Events, r.Events...)
|
|
}
|
|
if r := drifter.Pos.Len().Float64(); r < r0*0.99 || r > r0*1.01 {
|
|
t.Fatalf("drifting container left its orbit: %.0f -> %.0f km", r0, r)
|
|
}
|
|
lost := false
|
|
for _, e := range res.Events {
|
|
lost = lost || e.Kind == "lost"
|
|
}
|
|
if !lost || len(s.Containers) != 1 || s.Containers[0] != drifter {
|
|
t.Fatalf("the container with no speed should have fallen in and been lost (%d left, lost=%v)", len(s.Containers), lost)
|
|
}
|
|
}
|
|
|
|
func TestLosingAShipSetsItsContainersAdrift(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
// A ship inside the star's exclusion zone is destroyed on the first tick.
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt"), fixed.Vec{X: fixed.FromInt(StarRadiusKm - 1)}, fixed.Vec{})
|
|
c := towed(s, sh, 1)[0]
|
|
if _, err := s.RunDay(cfg, DayInput{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sh.Alive {
|
|
t.Fatal("the ship should have been destroyed")
|
|
}
|
|
if c.Towed != 0 && len(s.Containers) != 0 {
|
|
t.Fatal("a destroyed ship's tow must be released, not left attached to a dead ship")
|
|
}
|
|
}
|
|
|
|
func TestLegacyCargoBecomesContainers(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
pos, vel := s.Station.Orbit.State(0)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt"), pos, vel)
|
|
sh.Cargo = [world.NumOre]int64{world.Iron: 5000, world.Nickel: 1200, world.Platinum: 7}
|
|
if _, err := s.RunDay(cfg, DayInput{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tow := s.towOf(sh.ID)
|
|
if tow.Count != 4 || tow.OreTotal != 6207 || tow.Ore[world.Iron] != 5000 || tow.Ore[world.Nickel] != 1200 || tow.Ore[world.Platinum] != 7 {
|
|
t.Fatalf("tow = %+v, want 4 containers holding all 6207 kg", tow)
|
|
}
|
|
for _, c := range s.Containers {
|
|
if c.OreTotal() > ContainerCapKg {
|
|
t.Fatalf("container %d holds %d kg", c.ID, c.OreTotal())
|
|
}
|
|
}
|
|
if sh.Cargo != ([world.NumOre]int64{}) {
|
|
t.Fatal("the old hold should be empty after conversion")
|
|
}
|
|
}
|
|
|
|
// Snapshots written before containers existed must still load: their ships
|
|
// have a Cargo array and a CargoCap in the hull, and no container list.
|
|
func TestSnapshotFromBeforeContainersStillLoads(t *testing.T) {
|
|
type legacyHull struct{ DryMass, FuelCap, CargoCap, Thrust, ExhaustVel, MineRate int64 }
|
|
type legacyShip struct {
|
|
ID, Owner int64
|
|
Hull legacyHull
|
|
Pos, Vel fixed.Vec
|
|
Fuel fixed.F
|
|
Cargo [world.NumOre]int64
|
|
CPU *vm.CPU
|
|
Alive bool
|
|
Earned int64
|
|
Throttle, Azimuth int32
|
|
Pitch int32
|
|
Target int64
|
|
Mining, UplinkNew bool
|
|
UplinkLen int32
|
|
MathX, MathY, MathZ int32
|
|
}
|
|
type legacyState struct {
|
|
Day int64
|
|
Asteroids []world.Asteroid
|
|
Station world.Station
|
|
Ships []*legacyShip
|
|
Credits map[int64]int64
|
|
Market market.Market
|
|
}
|
|
cfg := testCfg()
|
|
fresh := NewState(cfg)
|
|
pos, vel := fresh.Station.Orbit.State(0)
|
|
cpu, _ := vm.New(mustAsm(t, "halt"), cfg.RAMBytes)
|
|
old := legacyState{
|
|
Day: 5, Asteroids: fresh.Asteroids, Station: fresh.Station, Credits: map[int64]int64{1: 123}, Market: fresh.Market,
|
|
Ships: []*legacyShip{{
|
|
ID: 7, Owner: 1, Hull: legacyHull{DryMass: 8000, FuelCap: 4000, CargoCap: 6000, Thrust: 6000, ExhaustVel: 30000, MineRate: 10},
|
|
Pos: pos, Vel: vel, Fuel: fixed.FromInt(4000), CPU: cpu, Alive: true,
|
|
Cargo: [world.NumOre]int64{world.Iron: 3000},
|
|
}},
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := gob.NewEncoder(&buf).Encode(&old); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var s State
|
|
if err := gob.NewDecoder(&buf).Decode(&s); err != nil {
|
|
t.Fatalf("an old snapshot no longer decodes: %v", err)
|
|
}
|
|
if s.Day != 5 || len(s.Ships) != 1 || s.Ships[0].Cargo[world.Iron] != 3000 || s.Credits[1] != 123 || len(s.Containers) != 0 {
|
|
t.Fatalf("decoded = day %d, %d ships, cargo %v, credits %v", s.Day, len(s.Ships), s.Ships[0].Cargo, s.Credits)
|
|
}
|
|
if _, err := s.RunDay(cfg, DayInput{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tow := s.towOf(7); tow.Count != 2 || tow.Ore[world.Iron] != 3000 {
|
|
t.Fatalf("the old ship's 3000 kg of iron should now ride in 2 containers: %+v", tow)
|
|
}
|
|
}
|
|
|
|
// A program that orders, attaches, reads and detaches, through the real ports.
|
|
const haulProgram = `
|
|
.equ P_CONTS 0x51
|
|
.equ P_CONTROOM 0x53
|
|
.equ P_CONTMASS 0x54
|
|
.equ P_ATTACH 0x55
|
|
.equ P_DETACH 0x56
|
|
.equ P_NEARBY 0x57
|
|
.equ P_ORDER 0x66
|
|
.equ P_MASS 0x24
|
|
.equ TX 1024
|
|
|
|
ldi r0, 0
|
|
ldi r1, 3
|
|
out P_ORDER, r1 ; three empty containers, delivered to the ship
|
|
in r2, P_NEARBY
|
|
stw r2, [r0+1024] ; 3 nearby before attaching
|
|
ldi r1, 1
|
|
out P_ATTACH, r1
|
|
in r2, P_CONTS
|
|
stw r2, [r0+1028] ; 3 in tow
|
|
in r2, P_CONTROOM
|
|
stw r2, [r0+1032] ; 6000 kg of room
|
|
in r2, P_CONTMASS
|
|
stw r2, [r0+1036] ; 1500 kg of container
|
|
in r2, P_NEARBY
|
|
stw r2, [r0+1040] ; 0 nearby: all attached
|
|
in r2, P_MASS
|
|
stw r2, [r0+1044] ; total ship mass
|
|
out P_DETACH, r1
|
|
in r2, P_CONTS
|
|
stw r2, [r0+1048] ; 0 in tow
|
|
in r2, P_NEARBY
|
|
stw r2, [r0+1052] ; 3 nearby again
|
|
in r2, P_MASS
|
|
stw r2, [r0+1056] ; mass without them
|
|
halt
|
|
`
|
|
|
|
func TestContainerPorts(t *testing.T) {
|
|
cfg := testCfg()
|
|
run := func() (DayResult, *State) {
|
|
s := NewState(cfg)
|
|
pos, vel := s.Station.Orbit.State(0)
|
|
addShip(t, s, cfg, 1, mustAsm(t, haulProgram), pos, vel)
|
|
res, err := s.RunDay(cfg, DayInput{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return res, s
|
|
}
|
|
res, s := run()
|
|
d := res.Downlinks[0].Data
|
|
w := func(i int) int32 { return int32(binary.LittleEndian.Uint32(d[i*4:])) }
|
|
base := int32(CommandShip.DryMass + CommandShip.FuelCap)
|
|
want := []int32{3, 3, 3 * ContainerCapKg, 3 * ContainerMassKg, 0, base + 3*ContainerMassKg, 0, 3, base}
|
|
for i, v := range want {
|
|
if w(i) != v {
|
|
t.Errorf("word %d = %d, want %d", i, w(i), v)
|
|
}
|
|
}
|
|
if len(s.Containers) != 3 {
|
|
t.Errorf("%d containers exist", len(s.Containers))
|
|
}
|
|
// The same program on the same world is the same day, containers and all.
|
|
if res2, _ := run(); res2.Hash != res.Hash {
|
|
t.Error("ordering, attaching and detaching are not deterministic")
|
|
}
|
|
}
|
|
|
|
const life = ContainerLifeDays * config.SecondsPerDay
|
|
|
|
func TestContainersAdriftForTenDaysVanish(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
stPos, stVel := s.Station.Orbit.State(0)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt"), stPos, stVel)
|
|
|
|
t0 := int64(12345)
|
|
dockAt(s, sh, t0)
|
|
if n := s.order(sh, 3, ctxAt(s, cfg, t0)); n != 3 {
|
|
t.Fatalf("ordered %d", n)
|
|
}
|
|
if s.Containers[0].ExpiresAt != t0+life {
|
|
t.Fatalf("a delivered container expires at %d, want %d", s.Containers[0].ExpiresAt, t0+life)
|
|
}
|
|
just := ctxAt(s, cfg, t0+life-1)
|
|
s.expireContainers(just)
|
|
if len(s.Containers) != 3 || expiredEvents(just) != 0 {
|
|
t.Fatal("a container vanished before its ten days were up")
|
|
}
|
|
end := ctxAt(s, cfg, t0+life)
|
|
s.expireContainers(end)
|
|
if len(s.Containers) != 0 || expiredEvents(end) != 3 {
|
|
t.Fatalf("%d containers left, %d expiry events: after ten days they should be gone", len(s.Containers), expiredEvents(end))
|
|
}
|
|
}
|
|
|
|
func TestTowingStopsTheClockAndLettingGoRestartsIt(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
stPos, stVel := s.Station.Orbit.State(0)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt"), stPos, stVel)
|
|
t0 := int64(1000)
|
|
dockAt(s, sh, t0)
|
|
s.order(sh, 1, ctxAt(s, cfg, t0))
|
|
c := s.Containers[0]
|
|
|
|
s.attach(sh, ctxAt(s, cfg, t0+5*config.SecondsPerDay))
|
|
if c.Towed != sh.ID || c.ExpiresAt != 0 {
|
|
t.Fatalf("attached container: towed=%d expires=%d, want towed with no timer", c.Towed, c.ExpiresAt)
|
|
}
|
|
// A month in tow, and it is still here.
|
|
s.expireContainers(ctxAt(s, cfg, t0+30*config.SecondsPerDay))
|
|
if len(s.Containers) != 1 {
|
|
t.Fatal("a container in tow expired")
|
|
}
|
|
// Let go, and the ten days start again from that moment, not from the delivery.
|
|
letGoAt := t0 + 30*config.SecondsPerDay
|
|
s.detach(sh, ctxAt(s, cfg, letGoAt))
|
|
if c.ExpiresAt != letGoAt+life {
|
|
t.Fatalf("expires at %d, want %d", c.ExpiresAt, letGoAt+life)
|
|
}
|
|
s.expireContainers(ctxAt(s, cfg, letGoAt+life-1))
|
|
if len(s.Containers) != 1 {
|
|
t.Fatal("expired early")
|
|
}
|
|
s.expireContainers(ctxAt(s, cfg, letGoAt+life))
|
|
if len(s.Containers) != 0 {
|
|
t.Fatal("did not expire ten days after being let go")
|
|
}
|
|
}
|
|
|
|
func TestLosingContainersDoesNotBlockOrdering(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
stPos, stVel := s.Station.Orbit.State(0)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt"), stPos, stVel)
|
|
for i := 0; i < 5; i++ {
|
|
s.order(sh, MaxOrder, ctxAt(s, cfg, 0))
|
|
}
|
|
if len(s.Containers) != MaxContainersPerPlayer || s.order(sh, 1, ctxAt(s, cfg, 0)) != 0 {
|
|
t.Fatalf("not at the allowance: %d containers", len(s.Containers))
|
|
}
|
|
// Lost and forgotten: ten days later they are gone and the allowance is back.
|
|
s.expireContainers(ctxAt(s, cfg, life))
|
|
if len(s.Containers) != 0 {
|
|
t.Fatalf("%d containers survived", len(s.Containers))
|
|
}
|
|
dockAt(s, sh, life) // the station has moved on in ten days
|
|
if n := s.order(sh, MaxOrder, ctxAt(s, cfg, life)); n != MaxOrder {
|
|
t.Fatalf("after they expired the player could order %d, want %d", n, MaxOrder)
|
|
}
|
|
}
|
|
|
|
func TestContainerWithNoTimerGetsAFreshTenDays(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
pos, vel := s.Station.Orbit.State(0)
|
|
old := s.newContainer(1, pos, vel) // as decoded from a snapshot made before containers expired
|
|
now := int64(50 * config.SecondsPerDay)
|
|
s.expireContainers(ctxAt(s, cfg, now))
|
|
if len(s.Containers) != 1 || old.ExpiresAt != now+life {
|
|
t.Fatalf("expires at %d (want %d), %d containers", old.ExpiresAt, now+life, len(s.Containers))
|
|
}
|
|
}
|
|
|
|
func TestLostShipsContainersStartTheClock(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
pos, vel := s.Station.Orbit.State(0)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt"), pos, vel)
|
|
c := towed(s, sh, 1)[0]
|
|
s.releaseTow(sh, 777)
|
|
if c.Towed != 0 || c.ExpiresAt != 777+life {
|
|
t.Fatalf("released container: towed=%d expires=%d", c.Towed, c.ExpiresAt)
|
|
}
|
|
}
|
|
|
|
// Through the real daily loop: an adrift container that nobody collects is
|
|
// removed on schedule, while one in tow and the day's other rules carry on.
|
|
func TestExpiryThroughTheDailyRun(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
pos, vel := s.Station.Orbit.State(0)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt"), pos.Add(fixed.Vec{Z: fixed.FromInt(50_000)}), vel)
|
|
drifting := s.newContainer(1, pos, vel)
|
|
drifting.ExpiresAt = 3*config.SecondsPerDay + 7200 // two hours into the fourth day
|
|
inTow := towed(s, sh, 1)[0]
|
|
|
|
var events []Event
|
|
var alive []bool
|
|
for d := 0; d < 5; d++ {
|
|
r, err := s.RunDay(cfg, DayInput{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
events = append(events, r.Events...)
|
|
gone := true
|
|
for _, c := range s.Containers {
|
|
if c == drifting {
|
|
gone = false
|
|
}
|
|
}
|
|
alive = append(alive, !gone)
|
|
}
|
|
if want := []bool{true, true, true, false, false}; !equalBools(alive, want) {
|
|
t.Fatalf("drifting container present after each day: %v, want %v", alive, want)
|
|
}
|
|
expired := 0
|
|
for _, e := range events {
|
|
if e.Kind == "expired" {
|
|
expired++
|
|
}
|
|
}
|
|
if expired != 1 {
|
|
t.Fatalf("%d expiry events, want 1", expired)
|
|
}
|
|
if inTow.Towed != sh.ID {
|
|
t.Fatal("the container in tow was affected")
|
|
}
|
|
}
|
|
|
|
func equalBools(a, b []bool) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// On the very turn its ten days end, a container that is at the station with ore
|
|
// is bought, not thrown away: the sale comes first.
|
|
func TestSaleBeatsExpiry(t *testing.T) {
|
|
cfg := testCfg()
|
|
s := NewState(cfg)
|
|
s.Day = 20
|
|
now := s.Day * config.SecondsPerDay
|
|
stPos, stVel := s.Station.Orbit.State(now)
|
|
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt"), stPos.Add(fixed.Vec{Z: fixed.FromInt(50_000)}), stVel)
|
|
c := s.newContainer(0, stPos, stVel)
|
|
c.Ore[world.Iron] = 100
|
|
c.Dropped = sh.ID
|
|
c.ExpiresAt = now // due on the first turn of the day
|
|
res, err := s.RunDay(cfg, DayInput{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Credits[1] != 200 {
|
|
t.Fatalf("credits = %d: the ore should have been sold before the container expired", s.Credits[1])
|
|
}
|
|
for _, e := range res.Events {
|
|
if e.Kind == "expired" {
|
|
t.Fatal("the container was reported expired as well as sold")
|
|
}
|
|
}
|
|
}
|