clone

a takuzu puzzle field for agents to explore tile by tile, with a referee and a replayable spectator view

main
mosaic / engine / main.go
main.go 7.9K
1// mosaic-engine: pure functions over JSON. No state, no ports.2//3// mosaic-engine gen [--seed s --G 100 --kind scatter --coverage 0.72 --compound 0.4 | --X 2 --n 14] (or JSON params on stdin)4// mosaic-engine judge (JSON on stdin)5// mosaic-engine pack [--seed s --G 100 --kind scatter|grid --n 14]6// mosaic-engine field --seed s --chunks 9 --chunk-G 60 [--band 16 --jobs 8 --coverage --compound]7// mosaic-engine version (generator version; cache key)8//9// gen with no --seed reads its Params as JSON on stdin (the chunk mode10// fields region, fixed, id_base, chunk included); field runs one such11// process per chunk.12package main1314import (15 "context"16 "encoding/json"17 "flag"18 "fmt"19 "io"20 "os"21 "runtime/debug"22 "time"2324 "mosaic-engine/gen"25 "mosaic-engine/judge"26 "mosaic-engine/pack"27)2829func main() {30 if len(os.Args) < 2 {31 fmt.Fprintln(os.Stderr, "usage: mosaic-engine gen|judge")32 os.Exit(2)33 }34 switch os.Args[1] {35 case "gen":36 runGen(os.Args[2:])37 case "judge":38 runJudge()39 case "pack":40 runPack(os.Args[2:])41 case "field":42 runField(os.Args[2:])43 case "version":44 fmt.Println(gen.Version)45 default:46 fmt.Fprintln(os.Stderr, "unknown command", os.Args[1])47 os.Exit(2)48 }49}5051// chunk processes run under a soft memory limit so the collector keeps52// the heap near live data instead of letting garbage double it (GOMEMLIMIT53// overrides; the hosting box has 2 GB for everything)54func memoryLimit() {55 if os.Getenv("GOMEMLIMIT") != "" {56 return57 }58 debug.SetMemoryLimit(512 << 20)59}6061func runGen(args []string) {62 memoryLimit()63 fs := flag.NewFlagSet("gen", flag.ExitOnError)64 seed := fs.String("seed", "", "seed string; empty reads JSON params from stdin")65 nonce := fs.String("nonce", "", "commitment nonce (derived from seed when empty)")66 G := fs.Int("G", 0, "cells per side (packed layouts)")67 kind := fs.String("kind", "scatter", "scatter | grid")68 n := fs.Int("n", 14, "tile size for kind=grid (even, 6..16)")69 X := fs.Int("X", 0, "legacy grid shorthand: X tiles per side of size n")70 coverage := fs.Float64("coverage", 0, "scatter: target share of the field covered (default 0.72)")71 compound := fs.Float64("compound", 0, "scatter: share of drops that are unions of two rectangles (default 0.4)")72 band := fs.Bool("band-unique", false, "reject tiles whose rows repeat within the band (grid only)")73 timeout := fs.Duration("timeout", 30*time.Minute, "overall budget")74 _ = fs.Parse(args)75 p := gen.Params{Seed: *seed, Nonce: *nonce, G: *G, Kind: *kind, N: *n, X: *X, Coverage: *coverage, Compound: *compound, BandUnique: *band}76 if *seed == "" {77 if err := json.NewDecoder(os.Stdin).Decode(&p); err != nil {78 fail(err)79 }80 }81 if p.G == 0 && p.X == 0 {82 fail(fmt.Errorf("gen needs --G (packed) or --X with --n (grid)"))83 }84 ctx, cancel := context.WithTimeout(context.Background(), *timeout)85 defer cancel()86 out, err := gen.Generate(ctx, p)87 if err != nil {88 fail(err)89 }90 emit(out)91}9293func runField(args []string) {94 fs := flag.NewFlagSet("field", flag.ExitOnError)95 seed := fs.String("seed", "a", "field seed")96 G := fs.Int("G", 0, "cells per side of the whole field (default: what the chunk grid needs; larger centres the grid and leaves room to grow)")97 chunks := fs.Int("chunks", 3, "chunks per side of the initial grid")98 chunkG := fs.Int("chunk-G", 60, "cells per side of a chunk region")99 band := fs.Int("band", 16, "overlap band between neighbouring chunk regions")100 jobs := fs.Int("jobs", 4, "chunks generated at once (each is a process)")101 retries := fs.Int("retries", 3, "re-packs of a chunk whose generation fails")102 coverage := fs.Float64("coverage", 0, "scatter: target share of a region covered (default 0.72)")103 compound := fs.Float64("compound", 0, "scatter: share of drops that are unions of two rectangles (default 0.4)")104 grow := fs.Bool("grow", false, "grow an existing field: read its private output from --state and emit the delta for the next ring")105 rings := fs.String("rings", "", "with the initial generation: also generate this many rings (or 'all' until the field is exhausted) and emit {base, rings: [delta...]}")106 state := fs.String("state", "", "with --grow: file holding the field's private output (gen JSON)")107 ring := fs.Int("ring", 1, "with --grow: how many rings of chunks to add")108 stateOut := fs.String("state-out", "", "with --grow: also write the merged state (base + delta) to this file, for chaining")109 timeout := fs.Duration("timeout", 2*time.Hour, "overall budget")110 chunkTimeout := fs.Duration("chunk-timeout", 30*time.Minute, "budget per chunk process")111 _ = fs.Parse(args)112 exe, err := os.Executable()113 if err != nil {114 fail(err)115 }116 ctx, cancel := context.WithTimeout(context.Background(), *timeout)117 defer cancel()118 fp := gen.FieldParams{119 Seed: *seed, G: *G, Chunks: *chunks, ChunkG: *chunkG, Band: *band, Kind: "scatter",120 Coverage: *coverage, Compound: *compound, Jobs: *jobs, Retries: *retries,121 }122 run := gen.SubprocessRunner(exe, *chunkTimeout)123 if *grow {124 if *state == "" {125 fail(fmt.Errorf("--grow needs --state <private output json>"))126 }127 data, err := os.ReadFile(*state)128 if err != nil {129 fail(err)130 }131 var st gen.Output132 if err := json.Unmarshal(data, &st); err != nil {133 fail(fmt.Errorf("state: %w", err))134 }135 delta, err := gen.GrowField(ctx, &st, fp, *ring, run)136 if err != nil {137 fail(err)138 }139 if *stateOut != "" {140 merged, err := gen.ApplyGrowth(&st, delta)141 if err != nil {142 fail(err)143 }144 data, err := json.Marshal(merged)145 if err != nil {146 fail(err)147 }148 if err := os.WriteFile(*stateOut, data, 0o644); err != nil {149 fail(err)150 }151 }152 emit(delta)153 return154 }155 out, err := gen.GenerateField(ctx, fp, run)156 if err != nil {157 fail(err)158 }159 if *rings == "" {160 emit(out)161 return162 }163 // the nightly job: the initial cluster and every ring, in one document,164 // each ring exactly the delta --grow would emit165 want := -1166 if *rings != "all" {167 if _, err := fmt.Sscan(*rings, &want); err != nil || want < 0 {168 fail(fmt.Errorf("--rings wants a count or 'all', got %q", *rings))169 }170 }171 cur := out172 var deltas []*gen.Output173 for k := 1; want < 0 || k <= want; k++ {174 delta, err := gen.GrowField(ctx, cur, fp, 1, run)175 if err != nil {176 fail(err)177 }178 if delta.Stats.Exhausted {179 break180 }181 deltas = append(deltas, delta)182 if cur, err = gen.ApplyGrowth(cur, delta); err != nil {183 fail(err)184 }185 if gen.Verbose {186 fmt.Fprintf(os.Stderr, "field: ring %d: +%d tiles (%d total) in %d ms\n", k, len(delta.Manifest.Tiles), len(cur.Manifest.Tiles), delta.Stats.Ms)187 }188 }189 emit(map[string]any{"base": out, "rings": deltas})190}191192func runPack(args []string) {193 fs := flag.NewFlagSet("pack", flag.ExitOnError)194 seed := fs.String("seed", "a", "seed string")195 G := fs.Int("G", 100, "cells per side")196 kind := fs.String("kind", "scatter", "scatter | grid")197 n := fs.Int("n", 14, "tile size for kind=grid")198 coverage := fs.Float64("coverage", 0, "scatter: target share of the field covered (default 0.72)")199 compound := fs.Float64("compound", 0, "scatter: share of drops that are unions of two rectangles (default 0.4)")200 maxHole := fs.Int("max-hole", 0, "scatter: fill holes larger than this many cells (default 24)")201 minCells := fs.Int("min-cells", 0, "scatter: smallest tile (cells)")202 maxCells := fs.Int("max-cells", 0, "scatter: largest tile (cells)")203 _ = fs.Parse(args)204 L, err := pack.Pack(pack.Params{Seed: *seed, G: *G, Kind: *kind, N: *n, Coverage: *coverage, Compound: *compound, MaxHole: *maxHole, MinCells: *minCells, MaxCells: *maxCells})205 if err != nil {206 fail(err)207 }208 emit(L)209}210211func runJudge() {212 data, err := io.ReadAll(os.Stdin)213 if err != nil {214 fail(err)215 }216 var in judge.Input217 if err := json.Unmarshal(data, &in); err != nil {218 fail(err)219 }220 out, err := judge.Judge(in)221 if err != nil {222 fail(err)223 }224 emit(out)225}226227func emit(v any) {228 enc := json.NewEncoder(os.Stdout)229 if err := enc.Encode(v); err != nil {230 fail(err)231 }232}233234func fail(err error) {235 _ = json.NewEncoder(os.Stderr).Encode(map[string]string{"error": err.Error()})236 os.Exit(1)237}