146 lines
4.7 KiB
Go
146 lines
4.7 KiB
Go
// Package config holds tunable simulation parameters.
|
|
//
|
|
// Units used throughout the simulation: distance in kilometres, time in
|
|
// seconds, mass in kilograms, force in newtons. Angles are radians.
|
|
package config
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
|
|
"wh/fixed"
|
|
)
|
|
|
|
const SecondsPerDay = 86400
|
|
|
|
type Config struct {
|
|
// Seed identifies the world. The same seed and inputs always produce the
|
|
// same simulation.
|
|
Seed uint64
|
|
|
|
// Time resolution.
|
|
TicksPerDay int // must divide SecondsPerDay
|
|
CyclesPerTick int // VM instructions each ship may execute per tick
|
|
|
|
// Ship computer sizes.
|
|
ProgramBytes int // max program size
|
|
RAMBytes int // data RAM, includes comm buffers
|
|
CommBytes int // size of each of the uplink and downlink buffers
|
|
|
|
// World.
|
|
OrbitSpeed fixed.F // km/s; speed of every circular orbit (star pulls with v^2/r)
|
|
AsteroidCount int
|
|
BeltInner fixed.F // km
|
|
BeltOuter fixed.F // km
|
|
MaxInclination fixed.F // radians; asteroid inclinations are drawn up to this
|
|
}
|
|
|
|
func Default() Config {
|
|
return Config{
|
|
Seed: 1,
|
|
TicksPerDay: 1440,
|
|
CyclesPerTick: 2000,
|
|
ProgramBytes: 4096,
|
|
RAMBytes: 8192,
|
|
CommBytes: 1024,
|
|
OrbitSpeed: fixed.FromInt(3),
|
|
AsteroidCount: 500,
|
|
BeltInner: fixed.FromInt(1_500_000),
|
|
BeltOuter: fixed.FromInt(3_000_000),
|
|
MaxInclination: fixed.FromRatio(1, 5), // ~11.5 degrees
|
|
}
|
|
}
|
|
|
|
// TickSeconds is the simulated duration of one tick.
|
|
func (c Config) TickSeconds() int { return SecondsPerDay / c.TicksPerDay }
|
|
|
|
func (c Config) Validate() error {
|
|
switch {
|
|
case c.TicksPerDay <= 0 || SecondsPerDay%c.TicksPerDay != 0:
|
|
return fmt.Errorf("TicksPerDay must divide %d, got %d", SecondsPerDay, c.TicksPerDay)
|
|
case c.CyclesPerTick <= 0:
|
|
return fmt.Errorf("CyclesPerTick must be positive")
|
|
case c.ProgramBytes <= 0 || c.ProgramBytes%4 != 0:
|
|
return fmt.Errorf("ProgramBytes must be a positive multiple of 4")
|
|
case c.CommBytes <= 0 || c.RAMBytes < 2*c.CommBytes:
|
|
return fmt.Errorf("RAMBytes must hold both comm buffers")
|
|
case c.MaxInclination < 0 || c.MaxInclination > fixed.HalfPi:
|
|
return fmt.Errorf("MaxInclination must be within [0, pi/2]")
|
|
case c.OrbitSpeed <= 0:
|
|
return fmt.Errorf("OrbitSpeed must be positive")
|
|
case c.BeltInner <= 0 || c.BeltOuter <= c.BeltInner:
|
|
return fmt.Errorf("invalid belt radii")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Bind registers a flag for each tunable on fs and returns the config that
|
|
// the flags will fill in when fs is parsed. Each tunable can also be set from
|
|
// an environment variable (see EnvVars); a flag on the command line overrides
|
|
// the environment. A malformed environment value is an error.
|
|
func Bind(fs *flag.FlagSet) (*Config, error) {
|
|
c := Default()
|
|
if err := c.applyEnv(os.Getenv); err != nil {
|
|
return nil, err
|
|
}
|
|
fs.Uint64Var(&c.Seed, "seed", c.Seed, "world seed (env WH_SEED)")
|
|
fs.IntVar(&c.TicksPerDay, "ticks-per-day", c.TicksPerDay, "simulation ticks per day (env WH_TICKS_PER_DAY)")
|
|
fs.IntVar(&c.CyclesPerTick, "cycles-per-tick", c.CyclesPerTick, "VM cycles per ship per tick (env WH_CYCLES_PER_TICK)")
|
|
fs.IntVar(&c.ProgramBytes, "program-bytes", c.ProgramBytes, "max ship program size (env WH_PROGRAM_BYTES)")
|
|
fs.IntVar(&c.RAMBytes, "ram-bytes", c.RAMBytes, "ship RAM size (env WH_RAM_BYTES)")
|
|
fs.IntVar(&c.CommBytes, "comm-bytes", c.CommBytes, "uplink/downlink buffer size (env WH_COMM_BYTES)")
|
|
fs.IntVar(&c.AsteroidCount, "asteroids", c.AsteroidCount, "number of asteroids (env WH_ASTEROIDS)")
|
|
return &c, nil
|
|
}
|
|
|
|
// EnvVars lists the environment variables that set the world, for documentation.
|
|
var EnvVars = []string{
|
|
"WH_SEED", "WH_TICKS_PER_DAY", "WH_CYCLES_PER_TICK", "WH_PROGRAM_BYTES",
|
|
"WH_RAM_BYTES", "WH_COMM_BYTES", "WH_ASTEROIDS",
|
|
}
|
|
|
|
func (c *Config) applyEnv(get func(string) string) error {
|
|
var err error
|
|
num := func(name string, dst any) {
|
|
v := get(name)
|
|
if v == "" || err != nil {
|
|
return
|
|
}
|
|
switch d := dst.(type) {
|
|
case *uint64:
|
|
var n uint64
|
|
if n, err = strconv.ParseUint(v, 10, 64); err == nil {
|
|
*d = n
|
|
}
|
|
case *int:
|
|
var n int
|
|
if n, err = strconv.Atoi(v); err == nil {
|
|
*d = n
|
|
}
|
|
}
|
|
if err != nil {
|
|
err = fmt.Errorf("%s=%q: %w", name, v, err)
|
|
}
|
|
}
|
|
num("WH_SEED", &c.Seed)
|
|
num("WH_TICKS_PER_DAY", &c.TicksPerDay)
|
|
num("WH_CYCLES_PER_TICK", &c.CyclesPerTick)
|
|
num("WH_PROGRAM_BYTES", &c.ProgramBytes)
|
|
num("WH_RAM_BYTES", &c.RAMBytes)
|
|
num("WH_COMM_BYTES", &c.CommBytes)
|
|
num("WH_ASTEROIDS", &c.AsteroidCount)
|
|
return err
|
|
}
|
|
|
|
// DatabaseURL returns the default database location: $DATABASE_URL if set,
|
|
// else a SQLite file in the working directory. The -db flag overrides it.
|
|
// Accepted forms are "sqlite:<path>" and "postgres://...".
|
|
func DatabaseURL() string {
|
|
if v := os.Getenv("DATABASE_URL"); v != "" {
|
|
return v
|
|
}
|
|
return "sqlite:wh.db"
|
|
}
|