init
This commit is contained in:
+175
@@ -0,0 +1,175 @@
|
||||
package sim
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"wh/config"
|
||||
"wh/fixed"
|
||||
"wh/vm"
|
||||
"wh/world"
|
||||
)
|
||||
|
||||
// RunDay simulates one full day, mutating the state, and returns the result.
|
||||
func (s *State) RunDay(cfg config.Config, in DayInput) (DayResult, error) {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return DayResult{}, err
|
||||
}
|
||||
res := DayResult{Day: s.Day}
|
||||
dayStart := s.Day * config.SecondsPerDay
|
||||
dt := int64(cfg.TickSeconds())
|
||||
|
||||
// Launches: ships appear at the station, matching its velocity.
|
||||
stPos, stVel := s.Station.Orbit.State(dayStart)
|
||||
for _, l := range in.Launches {
|
||||
if len(l.Program) > cfg.ProgramBytes {
|
||||
return res, fmt.Errorf("ship %d: program of %d bytes exceeds limit %d", l.ShipID, len(l.Program), cfg.ProgramBytes)
|
||||
}
|
||||
cpu, err := vm.New(l.Program, cfg.RAMBytes)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("ship %d: %w", l.ShipID, err)
|
||||
}
|
||||
s.Ships = append(s.Ships, &Ship{
|
||||
ID: l.ShipID, Owner: l.Owner, Hull: CommandShip,
|
||||
Pos: stPos, Vel: stVel, Fuel: fixed.FromInt(CommandShip.FuelCap),
|
||||
CPU: cpu, Alive: true,
|
||||
})
|
||||
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 })
|
||||
|
||||
// Uplinks land in the RX buffer at the start of RAM.
|
||||
for _, u := range in.Uplinks {
|
||||
sh := s.ship(u.ShipID)
|
||||
if sh == nil || !sh.Alive {
|
||||
continue
|
||||
}
|
||||
n := len(u.Data)
|
||||
if n > cfg.CommBytes {
|
||||
n = cfg.CommBytes
|
||||
}
|
||||
copy(sh.CPU.RAM[:cfg.CommBytes], u.Data[:n])
|
||||
sh.UplinkNew, sh.UplinkLen = true, int32(n)
|
||||
}
|
||||
|
||||
var sold [world.NumOre]int64
|
||||
for tick := 0; tick < cfg.TicksPerDay; tick++ {
|
||||
now := dayStart + int64(tick)*dt
|
||||
stPos, stVel = s.Station.Orbit.State(now)
|
||||
tc := newTickCtx(s, cfg, tick, now, stPos, stVel, &res, &sold)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, sh := range s.Ships {
|
||||
if sh.Alive {
|
||||
res.Downlinks = append(res.Downlinks, Downlink{
|
||||
ShipID: sh.ID,
|
||||
Data: append([]byte(nil), sh.CPU.RAM[cfg.CommBytes:2*cfg.CommBytes]...),
|
||||
})
|
||||
}
|
||||
sh.UplinkNew = false
|
||||
}
|
||||
s.Market.EndOfDay(sold)
|
||||
s.Day++
|
||||
res.Hash = s.Hash()
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *State) ship(id int64) *Ship {
|
||||
i := sort.Search(len(s.Ships), func(i int) bool { return s.Ships[i].ID >= id })
|
||||
if i < len(s.Ships) && s.Ships[i].ID == id {
|
||||
return s.Ships[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// physics applies gravity and engine thrust for one tick (semi-implicit Euler).
|
||||
func (s *State) physics(cfg config.Config, sh *Ship, dt int64, tick int, res *DayResult) {
|
||||
dtF := fixed.FromInt(dt)
|
||||
// Engine.
|
||||
if sh.Throttle > 0 && sh.Fuel > 0 {
|
||||
th := int64(sh.Throttle)
|
||||
if th > 1000 {
|
||||
th = 1000
|
||||
}
|
||||
thrust := sh.Hull.Thrust * th / 1000
|
||||
burn := fixed.FromInt(thrust * dt).Div(fixed.FromInt(sh.Hull.ExhaustVel))
|
||||
if burn > sh.Fuel {
|
||||
// Partial burn: scale the impulse by the remaining fuel.
|
||||
thrust = thrust * int64(sh.Fuel) / int64(burn)
|
||||
burn = sh.Fuel
|
||||
}
|
||||
// dv (km/s) = thrust*dt / mass / 1000.
|
||||
dv := fixed.FromInt(thrust * dt).Div(sh.Mass()).DivInt(1000)
|
||||
sh.Vel = sh.Vel.Add(thrustDir(sh).Scale(dv))
|
||||
sh.Fuel -= burn
|
||||
}
|
||||
// The star pulls with acceleration v^2/r, so dv = v*v*dt/r toward it.
|
||||
r := sh.Pos.Len()
|
||||
if r < fixed.FromInt(StarRadiusKm) {
|
||||
sh.Alive = false
|
||||
res.Events = append(res.Events, Event{tick, sh.ID, "destroyed", "fell into the star"})
|
||||
return
|
||||
}
|
||||
g := cfg.OrbitSpeed.Mul(cfg.OrbitSpeed).Mul(dtF).Div(r)
|
||||
sh.Vel = sh.Vel.Sub(sh.Pos.Unit().Scale(g))
|
||||
sh.Pos = sh.Pos.Add(sh.Vel.Scale(dtF))
|
||||
}
|
||||
|
||||
func (s *State) canReach(sh *Ship, pos, vel fixed.Vec, rangeKm int64) bool {
|
||||
rel := pos.Sub(sh.Pos)
|
||||
relV := vel.Sub(sh.Vel)
|
||||
return rel.Len() <= fixed.FromInt(rangeKm) &&
|
||||
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)
|
||||
// at which range is evaluated.
|
||||
func (s *State) mine(sh *Ship, at int64) {
|
||||
a := s.asteroid(sh.Target)
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
total := a.TotalOre()
|
||||
room := sh.Hull.CargoCap - sh.CargoTotal()
|
||||
amt := min(sh.Hull.MineRate, room, total)
|
||||
if amt <= 0 {
|
||||
return
|
||||
}
|
||||
pos, vel := a.Orbit.State(at)
|
||||
if !s.canReach(sh, pos, vel, MineRangeKm) {
|
||||
return
|
||||
}
|
||||
var taken [world.NumOre]int64
|
||||
var sum int64
|
||||
for o := range taken {
|
||||
taken[o] = amt * a.Ore[o] / total
|
||||
sum += taken[o]
|
||||
}
|
||||
for o := range taken {
|
||||
extra := min(amt-sum, a.Ore[o]-taken[o])
|
||||
taken[o] += extra
|
||||
sum += extra
|
||||
}
|
||||
for o, kg := range taken {
|
||||
a.Ore[o] -= kg
|
||||
sh.Cargo[o] += kg
|
||||
}
|
||||
}
|
||||
|
||||
// thrustDir converts the ship's azimuth/pitch (milliradians) to a unit vector.
|
||||
func thrustDir(sh *Ship) fixed.Vec {
|
||||
az := fixed.FromRatio(int64(sh.Azimuth), 1000)
|
||||
el := fixed.FromRatio(int64(sh.Pitch), 1000)
|
||||
return fixed.FromSpherical(az, el)
|
||||
}
|
||||
Reference in New Issue
Block a user