vibes
This commit is contained in:
+30
-22
@@ -94,12 +94,13 @@ func (b *shipBus) In(port uint16) int32 {
|
||||
case port == PortFuel:
|
||||
return sat32(sh.Fuel.Floor())
|
||||
case port == PortMass:
|
||||
return sat32(sh.Mass().Floor())
|
||||
return sat32(tc.s.shipMass(sh).Floor())
|
||||
case port == PortScanNearest:
|
||||
scanRange := fixed.FromInt(ScanRangeKm)
|
||||
best, bestD := int64(0), fixed.Max
|
||||
for i := range tc.s.Asteroids {
|
||||
p, _ := tc.asteroid(i)
|
||||
if d := p.Sub(sh.Pos).Len(); d < bestD {
|
||||
if d := p.Sub(sh.Pos).Len(); d <= scanRange && d < bestD {
|
||||
best, bestD = int64(i+1), d
|
||||
}
|
||||
}
|
||||
@@ -110,6 +111,9 @@ func (b *shipBus) In(port uint16) int32 {
|
||||
return 0
|
||||
}
|
||||
rel, relV := p.Sub(sh.Pos), v.Sub(sh.Vel)
|
||||
if rel.Len() > fixed.FromInt(ScanRangeKm) {
|
||||
return 0
|
||||
}
|
||||
switch {
|
||||
case port <= PortScanRelZ:
|
||||
return axis(rel, port, PortScanRelX, kmInt)
|
||||
@@ -119,16 +123,25 @@ func (b *shipBus) In(port uint16) int32 {
|
||||
return kmInt(rel.Len())
|
||||
case port >= PortScanOre && port < PortScanOre+8:
|
||||
if a := tc.s.asteroid(sh.Target); a != nil && port-PortScanOre < uint16(world.NumOre) {
|
||||
if p, _, _ := b.target(); p.Sub(sh.Pos).Len() > fixed.FromInt(ScanRangeKm) {
|
||||
return 0
|
||||
}
|
||||
return sat32(a.Ore[port-PortScanOre])
|
||||
}
|
||||
return 0
|
||||
case port == PortCargo:
|
||||
return sat32(sh.CargoTotal())
|
||||
case port == PortCargoCap:
|
||||
return sat32(sh.Hull.CargoCap)
|
||||
case port >= PortCargoOre && port < PortCargoOre+8:
|
||||
if port-PortCargoOre < uint16(world.NumOre) {
|
||||
return sat32(sh.Cargo[port-PortCargoOre])
|
||||
case port == PortConts:
|
||||
return int32(tc.s.towOf(sh.ID).Count)
|
||||
case port == PortContKg:
|
||||
return sat32(tc.s.towOf(sh.ID).OreTotal)
|
||||
case port == PortContRoom:
|
||||
return sat32(tc.s.towOf(sh.ID).Room)
|
||||
case port == PortContMass:
|
||||
return sat32(tc.s.towOf(sh.ID).Mass)
|
||||
case port == PortNearby:
|
||||
return int32(tc.s.nearbyFree(sh))
|
||||
case port >= PortContOre && port < PortContOre+8:
|
||||
if port-PortContOre < uint16(world.NumOre) {
|
||||
return sat32(tc.s.towOf(sh.ID).Ore[port-PortContOre])
|
||||
}
|
||||
return 0
|
||||
case port >= PortStnRelX && port <= PortStnRelVZ:
|
||||
@@ -170,21 +183,16 @@ func (b *shipBus) Out(port uint16, v int32) {
|
||||
sh.Target = int64(v)
|
||||
case PortMine:
|
||||
sh.Mining = v != 0
|
||||
case PortSell:
|
||||
if v == 0 || !tc.s.canReach(sh, tc.stPos, tc.stVel, DockRangeKm) {
|
||||
return
|
||||
case PortAttach:
|
||||
if v != 0 {
|
||||
tc.s.attach(sh, tc)
|
||||
}
|
||||
value := tc.s.Market.Value(sh.Cargo)
|
||||
if value == 0 {
|
||||
return
|
||||
case PortDetach:
|
||||
if v != 0 {
|
||||
tc.s.detach(sh, tc)
|
||||
}
|
||||
tc.s.Credits[sh.Owner] += value
|
||||
sh.Earned += value
|
||||
for o, kg := range sh.Cargo {
|
||||
tc.sold[o] += kg
|
||||
sh.Cargo[o] = 0
|
||||
}
|
||||
tc.res.Events = append(tc.res.Events, Event{tc.tick, sh.ID, "sold", itoa(value) + " credits"})
|
||||
case PortOrder:
|
||||
tc.s.order(sh, int(v), tc)
|
||||
case PortUplinkNew:
|
||||
sh.UplinkNew = false
|
||||
case PortMathX:
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
package sim
|
||||
|
||||
import (
|
||||
"halcyon/config"
|
||||
"halcyon/fixed"
|
||||
"halcyon/world"
|
||||
)
|
||||
|
||||
// Ore travels in containers. A ship has no hold of its own: it tows containers
|
||||
// behind it, and every kilogram of them (the container and the ore in it) adds
|
||||
// to the mass its engine has to push. Containers belong to no one: any ship can
|
||||
// tow any container that is adrift near it.
|
||||
const (
|
||||
ContainerMassKg = 500 // an empty container
|
||||
ContainerCapKg = 2000 // ore one container holds
|
||||
ContainerRangeKm = 10 // "near the ship": how close a container must be to be attached
|
||||
MaxOrder = 10 // empty containers the station delivers per request
|
||||
MaxContainersPerPlayer = 50 // the station will not deliver past this many of a player's ordered containers
|
||||
ContainerLifeDays = 10 // a container adrift this long vanishes, so lost ones cannot pile up
|
||||
)
|
||||
|
||||
// Container is a free-floating body. It has no owner.
|
||||
type Container struct {
|
||||
ID int64
|
||||
// OrderedBy is the player who had the station deliver it. It is bookkeeping for
|
||||
// the ordering allowance only (MaxContainersPerPlayer): it gives no right to the
|
||||
// container, which anyone may tow, and a sale ends it.
|
||||
OrderedBy int64
|
||||
Pos fixed.Vec
|
||||
Vel fixed.Vec
|
||||
Ore [world.NumOre]int64 // kg of each ore inside
|
||||
Towed int64 // the ship towing it, or 0 while it drifts on its own
|
||||
// Dropped is the ship that last let it go: when the container is sold, that ship
|
||||
// and its player are paid.
|
||||
Dropped int64
|
||||
// ExpiresAt is when a container that stays adrift vanishes (seconds since the
|
||||
// world began); 0 while it is in tow.
|
||||
ExpiresAt int64
|
||||
}
|
||||
|
||||
func (c *Container) OreTotal() int64 {
|
||||
var t int64
|
||||
for _, v := range c.Ore {
|
||||
t += v
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Mass is the container plus the ore in it, in kg.
|
||||
func (c *Container) Mass() int64 { return ContainerMassKg + c.OreTotal() }
|
||||
|
||||
// Room is how much more ore fits, in kg.
|
||||
func (c *Container) Room() int64 { return ContainerCapKg - c.OreTotal() }
|
||||
|
||||
// Tow summarises the containers a ship is towing.
|
||||
type Tow struct {
|
||||
Count int
|
||||
Ore [world.NumOre]int64
|
||||
OreTotal int64
|
||||
Room int64
|
||||
Mass int64
|
||||
}
|
||||
|
||||
func (s *State) towOf(shipID int64) Tow {
|
||||
var t Tow
|
||||
for _, c := range s.Containers {
|
||||
if c.Towed != shipID {
|
||||
continue
|
||||
}
|
||||
t.Count++
|
||||
for o, kg := range c.Ore {
|
||||
t.Ore[o] += kg
|
||||
}
|
||||
t.OreTotal += c.OreTotal()
|
||||
t.Room += c.Room()
|
||||
t.Mass += c.Mass()
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// shipMass is everything the engine pushes: hull, fuel and towed containers.
|
||||
func (s *State) shipMass(sh *Ship) fixed.F {
|
||||
return sh.baseMass() + fixed.FromInt(s.towOf(sh.ID).Mass)
|
||||
}
|
||||
|
||||
func (s *State) newContainer(orderedBy int64, pos, vel fixed.Vec) *Container {
|
||||
s.NextContainer++
|
||||
c := &Container{ID: s.NextContainer, OrderedBy: orderedBy, Pos: pos, Vel: vel}
|
||||
s.Containers = append(s.Containers, c) // IDs only grow, so the slice stays sorted
|
||||
return c
|
||||
}
|
||||
|
||||
// canAttach reports whether container c is adrift and near enough to take in tow.
|
||||
func (s *State) canAttach(sh *Ship, c *Container) bool {
|
||||
return c.Towed == 0 && s.canReach(sh, c.Pos, c.Vel, ContainerRangeKm)
|
||||
}
|
||||
|
||||
// nearbyFree counts the drifting containers the ship could attach now.
|
||||
func (s *State) nearbyFree(sh *Ship) int {
|
||||
n := 0
|
||||
for _, c := range s.Containers {
|
||||
if s.canAttach(sh, c) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// attach takes every drifting container near the ship in tow.
|
||||
func (s *State) attach(sh *Ship, tc *tickCtx) {
|
||||
n := 0
|
||||
for _, c := range s.Containers {
|
||||
if s.canAttach(sh, c) {
|
||||
c.Towed, c.Pos, c.Vel, c.ExpiresAt = sh.ID, sh.Pos, sh.Vel, 0
|
||||
n++
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
tc.res.Events = append(tc.res.Events, Event{tc.tick, sh.ID, "attach", itoa(int64(n)) + " containers"})
|
||||
}
|
||||
}
|
||||
|
||||
// detach lets go of everything the ship is towing. The containers keep drifting
|
||||
// with the ship's velocity.
|
||||
func (s *State) detach(sh *Ship, tc *tickCtx) {
|
||||
n := 0
|
||||
for _, c := range s.Containers {
|
||||
if c.Towed == sh.ID {
|
||||
s.letGo(c, sh.ID, tc.now)
|
||||
n++
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
tc.res.Events = append(tc.res.Events, Event{tc.tick, sh.ID, "detach", itoa(int64(n)) + " containers"})
|
||||
}
|
||||
}
|
||||
|
||||
// order has the station deliver up to n empty containers to the ship's position,
|
||||
// provided the ship is close to the station. It returns how many were delivered.
|
||||
func (s *State) order(sh *Ship, n int, tc *tickCtx) int {
|
||||
if n > MaxOrder {
|
||||
n = MaxOrder
|
||||
}
|
||||
if n <= 0 || sh.Pos.Sub(tc.stPos).Len() > fixed.FromInt(DockRangeKm) {
|
||||
return 0
|
||||
}
|
||||
owned := 0
|
||||
for _, c := range s.Containers {
|
||||
if c.OrderedBy == sh.Owner {
|
||||
owned++
|
||||
}
|
||||
}
|
||||
n = min(n, MaxContainersPerPlayer-owned)
|
||||
for i := 0; i < n; i++ {
|
||||
c := s.newContainer(sh.Owner, sh.Pos, sh.Vel)
|
||||
c.ExpiresAt = tc.now + ContainerLifeDays*config.SecondsPerDay // delivered adrift
|
||||
}
|
||||
if n > 0 {
|
||||
tc.res.Events = append(tc.res.Events, Event{tc.tick, sh.ID, "delivery", itoa(int64(n)) + " containers"})
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// fillTow pours mined ore into the ship's containers, first come first filled.
|
||||
// The caller has checked that it all fits.
|
||||
func (s *State) fillTow(shipID int64, taken [world.NumOre]int64) {
|
||||
for o, kg := range taken {
|
||||
for _, c := range s.Containers {
|
||||
if kg == 0 {
|
||||
break
|
||||
}
|
||||
if c.Towed != shipID {
|
||||
continue
|
||||
}
|
||||
put := min(kg, c.Room())
|
||||
c.Ore[o] += put
|
||||
kg -= put
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// syncTow keeps a ship's containers where the ship is.
|
||||
func (s *State) syncTow(sh *Ship) {
|
||||
for _, c := range s.Containers {
|
||||
if c.Towed == sh.ID {
|
||||
c.Pos, c.Vel = sh.Pos, sh.Vel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// releaseTow sets a lost ship's containers adrift.
|
||||
func (s *State) releaseTow(sh *Ship, now int64) {
|
||||
for _, c := range s.Containers {
|
||||
if c.Towed == sh.ID {
|
||||
s.letGo(c, sh.ID, now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// letGo sets a towed container adrift and starts its ten days.
|
||||
func (s *State) letGo(c *Container, ship, now int64) {
|
||||
c.Towed, c.Dropped = 0, ship
|
||||
c.ExpiresAt = now + ContainerLifeDays*config.SecondsPerDay
|
||||
}
|
||||
|
||||
// expireContainers removes containers that have been adrift for their ten days,
|
||||
// with whatever ore they hold. A container adrift with no timer at all (from a
|
||||
// snapshot made before there was one) is given a full ten days from now.
|
||||
func (s *State) expireContainers(tc *tickCtx) {
|
||||
kept := s.Containers[:0]
|
||||
for _, c := range s.Containers {
|
||||
if c.Towed == 0 {
|
||||
if c.ExpiresAt == 0 {
|
||||
c.ExpiresAt = tc.now + ContainerLifeDays*config.SecondsPerDay
|
||||
} else if tc.now >= c.ExpiresAt {
|
||||
tc.res.Events = append(tc.res.Events, Event{tc.tick, c.Dropped, "expired", "container " + itoa(c.ID) + " adrift for " + itoa(ContainerLifeDays) + " days"})
|
||||
continue
|
||||
}
|
||||
}
|
||||
kept = append(kept, c)
|
||||
}
|
||||
s.Containers = kept
|
||||
}
|
||||
|
||||
// sellNearStation buys every drifting container that has ore in it and is close
|
||||
// to the station, at the tick's positions. The station takes the container along
|
||||
// with the ore, so a sold container is gone. The ship that last let it go, and its
|
||||
// player, are paid. Empty containers are not bought.
|
||||
func (s *State) sellNearStation(tc *tickCtx) {
|
||||
kept := s.Containers[:0]
|
||||
for _, c := range s.Containers {
|
||||
payee := s.ship(c.Dropped)
|
||||
if c.Towed != 0 || c.OreTotal() == 0 || payee == nil || c.Pos.Sub(tc.stPos).Len() > fixed.FromInt(DockRangeKm) {
|
||||
kept = append(kept, c)
|
||||
continue
|
||||
}
|
||||
value := s.Market.Value(c.Ore)
|
||||
s.Credits[payee.Owner] += value
|
||||
payee.Earned += value
|
||||
for o, kg := range c.Ore {
|
||||
tc.sold[o] += kg
|
||||
}
|
||||
tc.res.Events = append(tc.res.Events, Event{tc.tick, payee.ID, "sold", itoa(value) + " credits (container " + itoa(c.ID) + " taken)"})
|
||||
}
|
||||
s.Containers = kept
|
||||
}
|
||||
|
||||
// driftContainers moves the containers nobody is towing. The star pulls them
|
||||
// exactly as it pulls ships; one that falls in is lost with its ore.
|
||||
func (s *State) driftContainers(orbitSpeed fixed.F, dt int64, tick int, res *DayResult) {
|
||||
dtF := fixed.FromInt(dt)
|
||||
kept := s.Containers[:0]
|
||||
for _, c := range s.Containers {
|
||||
if c.Towed == 0 {
|
||||
r := c.Pos.Len()
|
||||
if r < fixed.FromInt(StarRadiusKm) {
|
||||
res.Events = append(res.Events, Event{tick, c.Dropped, "lost", "container " + itoa(c.ID) + " fell into the star"})
|
||||
continue
|
||||
}
|
||||
g := orbitSpeed.Mul(orbitSpeed).Mul(dtF).Div(r)
|
||||
c.Vel = c.Vel.Sub(c.Pos.Unit().Scale(g))
|
||||
c.Pos = c.Pos.Add(c.Vel.Scale(dtF))
|
||||
}
|
||||
kept = append(kept, c)
|
||||
}
|
||||
s.Containers = kept
|
||||
}
|
||||
|
||||
// migrateLegacyCargo turns ore that a ship still carries in the cargo hold of
|
||||
// the earlier design into towed containers, so nothing is lost across the change.
|
||||
func (s *State) migrateLegacyCargo() {
|
||||
for _, sh := range s.Ships {
|
||||
var left int64
|
||||
for _, kg := range sh.Cargo {
|
||||
left += kg
|
||||
}
|
||||
for left > 0 {
|
||||
c := s.newContainer(0, sh.Pos, sh.Vel) // not counted against anyone's ordering allowance
|
||||
c.Towed = sh.ID
|
||||
for o := range sh.Cargo {
|
||||
put := min(sh.Cargo[o], c.Room())
|
||||
c.Ore[o] += put
|
||||
sh.Cargo[o] -= put
|
||||
left -= put
|
||||
}
|
||||
}
|
||||
sh.Cargo = [world.NumOre]int64{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,758 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -34,11 +34,16 @@ func (s *State) Hash() [32]byte {
|
||||
w(sh.ID, sh.Owner, alive, int64(sh.Pos.X), int64(sh.Pos.Y), int64(sh.Pos.Z),
|
||||
int64(sh.Vel.X), int64(sh.Vel.Y), int64(sh.Vel.Z),
|
||||
int64(sh.Fuel), sh.Earned, int64(sh.Throttle), int64(sh.Azimuth), int64(sh.Pitch), sh.Target)
|
||||
for _, c := range sh.Cargo {
|
||||
w(c)
|
||||
}
|
||||
h.Write(sh.CPU.MarshalState())
|
||||
}
|
||||
w(int64(len(s.Containers)), s.NextContainer)
|
||||
for _, c := range s.Containers {
|
||||
w(c.ID, c.OrderedBy, int64(c.Pos.X), int64(c.Pos.Y), int64(c.Pos.Z),
|
||||
int64(c.Vel.X), int64(c.Vel.Y), int64(c.Vel.Z), c.Towed, c.Dropped, c.ExpiresAt)
|
||||
for _, o := range c.Ore {
|
||||
w(o)
|
||||
}
|
||||
}
|
||||
owners := make([]int64, 0, len(s.Credits))
|
||||
for o := range s.Credits {
|
||||
owners = append(owners, o)
|
||||
|
||||
+10
-5
@@ -37,11 +37,16 @@ const (
|
||||
PortScanDist = 0x38 // in: distance to target, km
|
||||
PortScanOre = 0x40 // in: PortScanOre+ore = kg of ore remaining (8 ports)
|
||||
|
||||
// Mining laser and cargo hold.
|
||||
// Mining laser and the containers in tow.
|
||||
PortMine = 0x50 // out: 1 to mine the selected asteroid, 0 to stop
|
||||
PortCargo = 0x51 // in: total cargo kg
|
||||
PortCargoCap = 0x52 // in: cargo capacity kg
|
||||
PortCargoOre = 0x58 // in: PortCargoOre+ore = kg of ore carried (8 ports)
|
||||
PortConts = 0x51 // in: number of containers in tow
|
||||
PortContKg = 0x52 // in: kg of ore in the containers in tow
|
||||
PortContRoom = 0x53 // in: kg of ore the containers in tow can still take
|
||||
PortContMass = 0x54 // in: mass of the containers in tow, ore included, kg
|
||||
PortAttach = 0x55 // out: non-zero: take every one of your containers near the ship in tow
|
||||
PortDetach = 0x56 // out: non-zero: let go of every container in tow
|
||||
PortNearby = 0x57 // in: how many of your drifting containers are near enough to attach
|
||||
PortContOre = 0x58 // in: PortContOre+ore = kg of that ore in the containers in tow (8 ports)
|
||||
|
||||
// Dropoff station.
|
||||
PortStnRelX = 0x60 // in: station position relative to ship, km
|
||||
@@ -50,7 +55,7 @@ const (
|
||||
PortStnRelVX = 0x63 // in: station velocity relative to ship, m/s
|
||||
PortStnRelVY = 0x64
|
||||
PortStnRelVZ = 0x65
|
||||
PortSell = 0x66 // out: 1 to sell all cargo (needs to be docked)
|
||||
PortOrder = 0x66 // out: 1..10: the station delivers that many empty containers to the ship (near the station)
|
||||
PortCredits = 0x67 // in: credits earned by this ship (saturating)
|
||||
|
||||
// Comms buffers live in RAM; these ports coordinate them.
|
||||
|
||||
+66
-37
@@ -7,6 +7,7 @@ import (
|
||||
"halcyon/config"
|
||||
"halcyon/fixed"
|
||||
"halcyon/vm"
|
||||
"halcyon/world"
|
||||
)
|
||||
|
||||
func testCfg() config.Config {
|
||||
@@ -147,43 +148,6 @@ func TestInclinedOrbitLeavesPlane(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMineAndSell(t *testing.T) {
|
||||
cfg := testCfg()
|
||||
s := NewState(cfg)
|
||||
// Sit on asteroid 1, matching its motion for the whole first tick, and mine.
|
||||
pos, vel := s.Asteroids[0].Orbit.State(0)
|
||||
sh := addShip(t, s, cfg, 1, mustAsm(t, `
|
||||
ldi r0, 1
|
||||
out 0x30, r0 ; select asteroid 1
|
||||
out 0x50, r0 ; mine
|
||||
yield
|
||||
jmp -2
|
||||
`), pos, vel)
|
||||
if _, err := s.RunDay(cfg, DayInput{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Gravity acts on the ship differently than the rails, so it soon drifts
|
||||
// out of range; it should still have mined something at the start.
|
||||
if sh.CargoTotal() == 0 {
|
||||
t.Fatal("nothing mined")
|
||||
}
|
||||
|
||||
// Now dock at the station and sell.
|
||||
s2 := NewState(cfg)
|
||||
sp, sv := s2.Station.Orbit.State(0)
|
||||
sh2 := addShip(t, s2, cfg, 1, mustAsm(t, "ldi r0, 1\n out 0x66, r0\n halt"), sp, sv)
|
||||
sh2.Cargo[0] = 1000 // iron
|
||||
if _, err := s2.RunDay(cfg, DayInput{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s2.Credits[1] != 2000 || sh2.CargoTotal() != 0 {
|
||||
t.Fatalf("credits=%d cargo=%d", s2.Credits[1], sh2.CargoTotal())
|
||||
}
|
||||
if s2.Market.Prices[0] >= 2 && s2.Market.Supply[0] != 1000 {
|
||||
t.Fatalf("market not updated: %+v", s2.Market)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommsBuffers(t *testing.T) {
|
||||
cfg := testCfg()
|
||||
s := NewState(cfg)
|
||||
@@ -209,3 +173,68 @@ func TestCommsBuffers(t *testing.T) {
|
||||
t.Fatalf("downlink = %q", res.Downlinks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScannerRange(t *testing.T) {
|
||||
cfg := testCfg()
|
||||
s := NewState(cfg)
|
||||
stPos, stVel := s.Station.Orbit.State(0)
|
||||
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt\n"), stPos, stVel)
|
||||
tc := newTickCtx(s, cfg, 0, 0, stPos, stVel, &DayResult{}, new([world.NumOre]int64))
|
||||
b := &shipBus{tc: tc, sh: sh}
|
||||
|
||||
// Find the true nearest asteroid and its distance from the ship.
|
||||
var near int
|
||||
var nd fixed.F = fixed.Max
|
||||
for i := range s.Asteroids {
|
||||
p, _ := tc.asteroid(i)
|
||||
if d := p.Sub(sh.Pos).Len(); d < nd {
|
||||
near, nd = i+1, d
|
||||
}
|
||||
}
|
||||
limit := fixed.FromInt(ScanRangeKm)
|
||||
if nd <= limit {
|
||||
if got := b.In(PortScanNearest); got != int32(near) {
|
||||
t.Fatalf("nearest = %d, want %d", got, near)
|
||||
}
|
||||
} else if got := b.In(PortScanNearest); got != 0 {
|
||||
t.Fatalf("nearest = %d beyond range, want 0", got)
|
||||
}
|
||||
|
||||
// Move the ship out of range of everything: the scanner goes blind.
|
||||
sh.Pos = fixed.Vec{X: fixed.FromInt(50_000_000)}
|
||||
if got := b.In(PortScanNearest); got != 0 {
|
||||
t.Fatalf("nearest out of range = %d, want 0", got)
|
||||
}
|
||||
b.Out(PortScanSelect, int32(near))
|
||||
for _, p := range []uint16{PortScanRelX, PortScanRelVX, PortScanDist, PortScanOre} {
|
||||
if got := b.In(p); got != 0 {
|
||||
t.Fatalf("port %#x = %d out of range, want 0", p, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrapRemovesShipAndReleasesTow(t *testing.T) {
|
||||
cfg := testCfg()
|
||||
s := NewState(cfg)
|
||||
stPos, stVel := s.Station.Orbit.State(0)
|
||||
sh := addShip(t, s, cfg, 1, mustAsm(t, "halt\n"), stPos, stVel)
|
||||
s.Containers = append(s.Containers, &Container{ID: 1, Towed: sh.ID, Pos: stPos, Vel: stVel})
|
||||
res, err := s.RunDay(cfg, DayInput{Scraps: []int64{1, 42}}) // an unknown id is ignored
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(s.Ships) != 0 {
|
||||
t.Fatalf("%d ships left after scrapping", len(s.Ships))
|
||||
}
|
||||
c := s.Containers[0]
|
||||
if c.Towed != 0 || c.Dropped != 1 {
|
||||
t.Fatalf("container after scrapping: towed %d dropped %d, want adrift", c.Towed, c.Dropped)
|
||||
}
|
||||
found := false
|
||||
for _, e := range res.Events {
|
||||
found = found || (e.Kind == "scrapped" && e.ShipID == 1)
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("no scrapped event")
|
||||
}
|
||||
}
|
||||
|
||||
+19
-21
@@ -16,7 +16,6 @@ import (
|
||||
type Hull struct {
|
||||
DryMass int64 // kg
|
||||
FuelCap int64 // kg
|
||||
CargoCap int64 // kg
|
||||
Thrust int64 // newtons at full throttle
|
||||
ExhaustVel int64 // m/s
|
||||
MineRate int64 // kg mined per tick
|
||||
@@ -26,7 +25,6 @@ type Hull struct {
|
||||
var CommandShip = Hull{
|
||||
DryMass: 8000,
|
||||
FuelCap: 4000,
|
||||
CargoCap: 6000,
|
||||
Thrust: 6000,
|
||||
ExhaustVel: 30000,
|
||||
MineRate: 10,
|
||||
@@ -35,18 +33,21 @@ var CommandShip = Hull{
|
||||
// Docking / mining limits.
|
||||
const (
|
||||
MineRangeKm = 5 // max distance to an asteroid for mining
|
||||
DockRangeKm = 20 // max distance to the station for selling
|
||||
DockRangeKm = 20 // how close counts as "near the station"
|
||||
MaxRelSpeedM = 100 // max relative speed (m/s) for mining or docking
|
||||
StarRadiusKm = 200_000
|
||||
ScanRangeKm = 500_000 // the scanner sees nothing farther away than this
|
||||
)
|
||||
|
||||
type Ship struct {
|
||||
ID int64
|
||||
Owner int64
|
||||
Hull Hull
|
||||
Pos fixed.Vec
|
||||
Vel fixed.Vec
|
||||
Fuel fixed.F
|
||||
ID int64
|
||||
Owner int64
|
||||
Hull Hull
|
||||
Pos fixed.Vec
|
||||
Vel fixed.Vec
|
||||
Fuel fixed.F
|
||||
// Cargo is the hold of the earlier design, before ore travelled in containers.
|
||||
// It is only read to convert what old ships still carry (migrateLegacyCargo).
|
||||
Cargo [world.NumOre]int64
|
||||
CPU *vm.CPU
|
||||
Alive bool
|
||||
@@ -65,16 +66,9 @@ type Ship struct {
|
||||
MathZ int32
|
||||
}
|
||||
|
||||
func (s *Ship) CargoTotal() int64 {
|
||||
var t int64
|
||||
for _, v := range s.Cargo {
|
||||
t += v
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (s *Ship) Mass() fixed.F {
|
||||
return fixed.FromInt(s.Hull.DryMass+s.CargoTotal()) + s.Fuel
|
||||
// baseMass is the hull and its fuel; towed containers add to it (State.shipMass).
|
||||
func (s *Ship) baseMass() fixed.F {
|
||||
return fixed.FromInt(s.Hull.DryMass) + s.Fuel
|
||||
}
|
||||
|
||||
// State is everything that persists between daily runs.
|
||||
@@ -83,8 +77,11 @@ type State struct {
|
||||
Asteroids []world.Asteroid // sorted by ID
|
||||
Station world.Station
|
||||
Ships []*Ship // sorted by ID
|
||||
Credits map[int64]int64
|
||||
Market market.Market
|
||||
// Containers are sorted by ID; NextContainer is the last ID handed out.
|
||||
Containers []*Container
|
||||
NextContainer int64
|
||||
Credits map[int64]int64
|
||||
Market market.Market
|
||||
}
|
||||
|
||||
// NewState builds the initial world from the config seed.
|
||||
@@ -123,6 +120,7 @@ type Uplink struct {
|
||||
type DayInput struct {
|
||||
Launches []Launch // processed in slice order (must be deterministic)
|
||||
Uplinks []Uplink
|
||||
Scraps []int64 // ships to remove from the world before the day starts, in slice order
|
||||
}
|
||||
|
||||
// Downlink is the content of a ship's transmit buffer at the end of the day.
|
||||
|
||||
+32
-7
@@ -37,6 +37,22 @@ func (s *State) RunDay(cfg config.Config, in DayInput) (DayResult, error) {
|
||||
res.Events = append(res.Events, Event{0, l.ShipID, "launch", ""})
|
||||
}
|
||||
sort.Slice(s.Ships, func(i, j int) bool { return s.Ships[i].ID < s.Ships[j].ID })
|
||||
s.migrateLegacyCargo()
|
||||
|
||||
// Scrapped ships vanish; whatever they were towing is left adrift where it is.
|
||||
for _, id := range in.Scraps {
|
||||
for i, sh := range s.Ships {
|
||||
if sh.ID != id {
|
||||
continue
|
||||
}
|
||||
if sh.Alive {
|
||||
s.releaseTow(sh, dayStart)
|
||||
}
|
||||
s.Ships = append(s.Ships[:i], s.Ships[i+1:]...)
|
||||
res.Events = append(res.Events, Event{0, id, "scrapped", ""})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Uplinks land in the RX buffer at the start of RAM.
|
||||
for _, u := range in.Uplinks {
|
||||
@@ -57,16 +73,24 @@ func (s *State) RunDay(cfg config.Config, in DayInput) (DayResult, error) {
|
||||
now := dayStart + int64(tick)*dt
|
||||
stPos, stVel = s.Station.Orbit.State(now)
|
||||
tc := newTickCtx(s, cfg, tick, now, stPos, stVel, &res, &sold)
|
||||
s.sellNearStation(tc) // ore in containers left near the station is sold...
|
||||
s.expireContainers(tc) // ...and containers adrift for ten days vanish
|
||||
for _, sh := range s.Ships {
|
||||
if !sh.Alive {
|
||||
continue
|
||||
}
|
||||
sh.CPU.Run(&shipBus{tc: tc, sh: sh}, cfg.CyclesPerTick)
|
||||
s.physics(cfg, sh, dt, tick, &res)
|
||||
if sh.Alive && sh.Mining {
|
||||
s.mine(sh, tc.now+dt)
|
||||
if sh.Alive {
|
||||
s.syncTow(sh)
|
||||
if sh.Mining {
|
||||
s.mine(sh, tc.now+dt)
|
||||
}
|
||||
} else {
|
||||
s.releaseTow(sh, tc.now)
|
||||
}
|
||||
}
|
||||
s.driftContainers(cfg.OrbitSpeed, dt, tick, &res)
|
||||
}
|
||||
|
||||
for _, sh := range s.Ships {
|
||||
@@ -109,7 +133,7 @@ func (s *State) physics(cfg config.Config, sh *Ship, dt int64, tick int, res *Da
|
||||
burn = sh.Fuel
|
||||
}
|
||||
// dv (km/s) = thrust*dt / mass / 1000.
|
||||
dv := fixed.FromInt(thrust * dt).Div(sh.Mass()).DivInt(1000)
|
||||
dv := fixed.FromInt(thrust * dt).Div(s.shipMass(sh)).DivInt(1000)
|
||||
sh.Vel = sh.Vel.Add(thrustDir(sh).Scale(dv))
|
||||
sh.Fuel -= burn
|
||||
}
|
||||
@@ -132,8 +156,9 @@ func (s *State) canReach(sh *Ship, pos, vel fixed.Vec, rangeKm int64) bool {
|
||||
relV.Len() <= fixed.FromRatio(MaxRelSpeedM, 1000)
|
||||
}
|
||||
|
||||
// mine moves ore from the target asteroid into the ship's hold, split
|
||||
// proportionally to the asteroid's composition. at is the absolute time (s)
|
||||
// mine moves ore from the target asteroid into the containers the ship is towing,
|
||||
// split proportionally to the asteroid's composition. With no container in tow, or
|
||||
// none with room, nothing is mined. at is the absolute time (s)
|
||||
// at which range is evaluated.
|
||||
func (s *State) mine(sh *Ship, at int64) {
|
||||
a := s.asteroid(sh.Target)
|
||||
@@ -141,7 +166,7 @@ func (s *State) mine(sh *Ship, at int64) {
|
||||
return
|
||||
}
|
||||
total := a.TotalOre()
|
||||
room := sh.Hull.CargoCap - sh.CargoTotal()
|
||||
room := s.towOf(sh.ID).Room
|
||||
amt := min(sh.Hull.MineRate, room, total)
|
||||
if amt <= 0 {
|
||||
return
|
||||
@@ -163,8 +188,8 @@ func (s *State) mine(sh *Ship, at int64) {
|
||||
}
|
||||
for o, kg := range taken {
|
||||
a.Ore[o] -= kg
|
||||
sh.Cargo[o] += kg
|
||||
}
|
||||
s.fillTow(sh.ID, taken)
|
||||
}
|
||||
|
||||
// thrustDir converts the ship's azimuth/pitch (milliradians) to a unit vector.
|
||||
|
||||
Reference in New Issue
Block a user