This commit is contained in:
root
2026-09-19 20:21:47 +02:00
commit 0798933b05
62 changed files with 7658 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
// 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"
}