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) }