init
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
// Package world defines the static shape of the belt: bodies on analytic
|
||||
// (on-rails) orbits and the procedural generation that creates them.
|
||||
//
|
||||
// The star pulls with acceleration v^2/r (v = config OrbitSpeed), so a circular
|
||||
// orbit at any radius has the same speed v. Every body on rails is therefore
|
||||
// on a circle traversed at speed v; only the period (2*pi*r/v) varies.
|
||||
package world
|
||||
|
||||
import "wh/fixed"
|
||||
|
||||
// Orbit is a circular orbit around the origin (the star).
|
||||
type Orbit struct {
|
||||
A fixed.F // radius, km
|
||||
Period int64 // seconds
|
||||
M0 fixed.F // phase at t=0, radians
|
||||
Inc fixed.F // inclination to the ecliptic (XY plane), radians
|
||||
Node fixed.F // longitude of the ascending node, radians
|
||||
}
|
||||
|
||||
// PeriodFor returns the period in seconds of a circle of radius a at speed v.
|
||||
func PeriodFor(a, v fixed.F) int64 {
|
||||
return fixed.TwoPi.Mul(a).Div(v).Int()
|
||||
}
|
||||
|
||||
// State returns position (km) and velocity (km/s) at absolute time t seconds.
|
||||
func (o Orbit) State(t int64) (pos, vel fixed.Vec) {
|
||||
frac := fixed.FromInt(t % o.Period).Div(fixed.FromInt(o.Period))
|
||||
s, c := fixed.SinCos(o.M0 + fixed.TwoPi.Mul(frac))
|
||||
k := fixed.TwoPi.Mul(o.A).Div(fixed.FromInt(o.Period)) // speed
|
||||
pos = fixed.Vec{X: o.A.Mul(c), Y: o.A.Mul(s)}
|
||||
vel = fixed.Vec{X: -k.Mul(s), Y: k.Mul(c)}
|
||||
return o.orient(pos), o.orient(vel)
|
||||
}
|
||||
|
||||
// orient rotates a vector from the orbital plane into the ecliptic frame:
|
||||
// Rz(Node) * Rx(Inc).
|
||||
func (o Orbit) orient(v fixed.Vec) fixed.Vec {
|
||||
return v.RotateX(o.Inc).RotateZ(o.Node)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"wh/config"
|
||||
"wh/fixed"
|
||||
"wh/rng"
|
||||
)
|
||||
|
||||
// Ore identifies a raw material.
|
||||
type Ore int
|
||||
|
||||
const (
|
||||
Iron Ore = iota
|
||||
Nickel
|
||||
Ice
|
||||
Platinum
|
||||
NumOre
|
||||
)
|
||||
|
||||
func (o Ore) String() string {
|
||||
return [...]string{"iron", "nickel", "ice", "platinum"}[o]
|
||||
}
|
||||
|
||||
type Asteroid struct {
|
||||
ID int64
|
||||
Orbit Orbit
|
||||
Ore [NumOre]int64 // remaining kilograms of each ore
|
||||
}
|
||||
|
||||
func (a *Asteroid) TotalOre() int64 {
|
||||
var t int64
|
||||
for _, v := range a.Ore {
|
||||
t += v
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Station is the dropoff point, on a circular orbit in the ecliptic plane.
|
||||
type Station struct {
|
||||
Orbit Orbit
|
||||
}
|
||||
|
||||
// Stream labels keep independent generation phases from sharing randomness.
|
||||
const (
|
||||
labelBelt = iota + 1
|
||||
labelStation
|
||||
)
|
||||
|
||||
// Generate builds the asteroid field and station from the config seed.
|
||||
func Generate(cfg config.Config) ([]Asteroid, Station) {
|
||||
r := rng.Derive(cfg.Seed, labelBelt)
|
||||
asts := make([]Asteroid, cfg.AsteroidCount)
|
||||
for i := range asts {
|
||||
a := r.FixedRange(cfg.BeltInner, cfg.BeltOuter)
|
||||
asts[i] = Asteroid{
|
||||
ID: int64(i + 1),
|
||||
Orbit: Orbit{
|
||||
A: a,
|
||||
Period: PeriodFor(a, cfg.OrbitSpeed),
|
||||
M0: r.FixedRange(0, fixed.TwoPi),
|
||||
Node: r.FixedRange(0, fixed.TwoPi),
|
||||
// Bias inclination low (squared uniform).
|
||||
Inc: r.Fixed().Mul(r.Fixed()).Mul(cfg.MaxInclination),
|
||||
},
|
||||
}
|
||||
// Each asteroid has a total mass and a random mix of ores.
|
||||
total := int64(50_000) + int64(r.Intn(950_000))
|
||||
var weights [NumOre]uint64
|
||||
var wsum uint64
|
||||
for o := range weights {
|
||||
weights[o] = r.Intn(100) + 1
|
||||
wsum += weights[o]
|
||||
}
|
||||
// Platinum is rare: only a fifth of asteroids carry much of it.
|
||||
if r.Intn(5) != 0 {
|
||||
wsum -= weights[Platinum]
|
||||
weights[Platinum] = 0
|
||||
}
|
||||
for o := range weights {
|
||||
asts[i].Ore[o] = total * int64(weights[o]) / int64(wsum)
|
||||
}
|
||||
}
|
||||
sr := rng.Derive(cfg.Seed, labelStation)
|
||||
a := (cfg.BeltInner + cfg.BeltOuter).DivInt(2)
|
||||
st := Station{Orbit: Orbit{
|
||||
A: a,
|
||||
Period: PeriodFor(a, cfg.OrbitSpeed),
|
||||
M0: sr.FixedRange(0, fixed.TwoPi),
|
||||
}}
|
||||
return asts, st
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"wh/config"
|
||||
"wh/fixed"
|
||||
)
|
||||
|
||||
func TestOrbitsAreCirclesAtCommonSpeed(t *testing.T) {
|
||||
cfg := config.Default()
|
||||
asts, _ := Generate(cfg)
|
||||
want := cfg.OrbitSpeed.Float64()
|
||||
for _, a := range asts {
|
||||
o := a.Orbit
|
||||
for _, ts := range []int64{0, 100_000, 777_777, o.Period / 3, o.Period - 1} {
|
||||
p, v := o.State(ts)
|
||||
if r := p.Len().Float64(); math.Abs(r-o.A.Float64()) > 1e-3 {
|
||||
t.Fatalf("asteroid %d t=%d: radius %g, want %g", a.ID, ts, r, o.A.Float64())
|
||||
}
|
||||
if s := v.Len().Float64(); math.Abs(s-want)/want > 1e-5 {
|
||||
t.Fatalf("asteroid %d t=%d: speed %g, want %g", a.ID, ts, s, want)
|
||||
}
|
||||
if d := p.Dot(v).Float64() / (p.Len().Float64() * want); math.Abs(d) > 1e-4 {
|
||||
t.Fatalf("asteroid %d: velocity not tangent (cos=%g)", a.ID, d)
|
||||
}
|
||||
// Inclination bounds the height above the ecliptic.
|
||||
if z := p.Z.Abs().Float64(); z > o.A.Float64()*math.Sin(o.Inc.Float64())*1.001+1e-3 {
|
||||
t.Fatalf("asteroid %d: |z|=%g exceeds inclination bound", a.ID, z)
|
||||
}
|
||||
}
|
||||
// Periodic.
|
||||
p1, _ := o.State(12345)
|
||||
p2, _ := o.State(12345 + o.Period)
|
||||
if d := p1.Sub(p2).Len().Float64(); d > 1 {
|
||||
t.Fatalf("asteroid %d not periodic, off by %g km", a.ID, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrbitPlaneMatchesInclination(t *testing.T) {
|
||||
o := Orbit{A: fixed.FromInt(2_000_000), Inc: fixed.FromRatio(3, 10), Node: fixed.FromInt(2)}
|
||||
o.Period = PeriodFor(o.A, fixed.FromInt(3))
|
||||
// The orbit normal is Rz(Node)Rx(Inc) applied to +Z; positions must be
|
||||
// perpendicular to it, and the maximum height is A*sin(Inc).
|
||||
n := fixed.Vec{Z: fixed.One}.RotateX(o.Inc).RotateZ(o.Node)
|
||||
var maxZ float64
|
||||
for k := int64(0); k < 50; k++ {
|
||||
p, _ := o.State(k * o.Period / 50)
|
||||
if d := p.Dot(n).Float64(); math.Abs(d) > 1e-2 {
|
||||
t.Fatalf("position not in the orbital plane: p.n = %g", d)
|
||||
}
|
||||
maxZ = math.Max(maxZ, p.Z.Float64())
|
||||
}
|
||||
if want := o.A.Float64() * math.Sin(0.3); math.Abs(maxZ-want)/want > 0.01 {
|
||||
t.Fatalf("max z = %g, want %g", maxZ, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateDeterministic(t *testing.T) {
|
||||
cfg := config.Default()
|
||||
a1, s1 := Generate(cfg)
|
||||
a2, s2 := Generate(cfg)
|
||||
if len(a1) != cfg.AsteroidCount || s1 != s2 {
|
||||
t.Fatal("mismatch")
|
||||
}
|
||||
for i := range a1 {
|
||||
if a1[i] != a2[i] {
|
||||
t.Fatalf("asteroid %d differs", i)
|
||||
}
|
||||
}
|
||||
cfg.Seed = 2
|
||||
a3, _ := Generate(cfg)
|
||||
if a3[0] == a1[0] {
|
||||
t.Fatal("different seeds should differ")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user