// 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:" and "postgres://...". func DatabaseURL() string { if v := os.Getenv("DATABASE_URL"); v != "" { return v } return "sqlite:wh.db" }