1package judge23import (4 "context"5 "encoding/json"6 "os"7 "testing"8 "time"910 "mosaic-engine/gen"11 "mosaic-engine/rules"12)1314func generate(t *testing.T, X, n int) *gen.Output {15 t.Helper()16 out, err := gen.Generate(context.Background(), gen.Params{Seed: "judge-test", X: X, N: n})17 if err != nil {18 t.Fatal(err)19 }20 return out21}2223func fixture(t *testing.T) *gen.Output {24 t.Helper()25 data, err := os.ReadFile("../testdata/mosaic-2-14.json")26 if err != nil {27 t.Skip("no 14x14 fixture: " + err.Error())28 }29 var o gen.Output30 if err := json.Unmarshal(data, &o); err != nil {31 t.Fatal(err)32 }33 if o.Manifest.Version != 3 {34 t.Skip("fixture predates manifest v3")35 }36 return &o37}3839// pinnedInput builds the referee's view of one revealed tile: its givens plus40// every cell it shares with another tile fixed (as if all neighbours were41// solved).42func pinnedInput(o *gen.Output, i int, depth int) Input {43 G := o.Manifest.G44 tp := o.Manifest.Tiles[i]45 in := Input{G: G, Shapes: []ShapeSpec{{X0: tp.X0, Y0: tp.Y0, W: tp.W, H: tp.H, Rects: tp.Rects}}, Depth: depth}46 seen := map[int]bool{}47 for _, g := range o.Tiles[i].Givens {48 in.Known = append(in.Known, g)49 seen[g[0]] = true50 }51 sh := rules.Shape{X0: tp.X0, Y0: tp.Y0, W: tp.W, H: tp.H, Rects: tp.Rects}52 for ly := 0; ly < tp.H; ly++ {53 for lx := 0; lx < tp.W; lx++ {54 gx, gy := tp.X0+lx, tp.Y0+ly55 if !sh.Contains(gx, gy) {56 continue57 }58 id := gy*G + gx59 shared := false60 for _, other := range o.Manifest.Tiles {61 osh := rules.Shape{X0: other.X0, Y0: other.Y0, W: other.W, H: other.H, Rects: other.Rects}62 if other.ID != i && osh.Contains(gx, gy) {63 shared = true64 }65 }66 if shared && !seen[id] {67 in.Known = append(in.Known, [2]int{id, int(o.Solution[gy][gx] - '0')})68 seen[id] = true69 }70 }71 }72 return in73}7475func TestJudgeClassifiesForcedGuessContradiction(t *testing.T) {76 // n = 12 is the smallest size the policy holds to the hard standard77 // (needs a bifurcation), so a pinned tile still has guess cells at78 // depth 0; fillers (n <= 8) may be fully forced once pinned79 o := generate(t, 2, 12)80 in := pinnedInput(o, 0, 0)81 G := o.Manifest.G82 tp := o.Manifest.Tiles[0]83 known := map[int]bool{}84 for _, k := range in.Known {85 known[k[0]] = true86 }87 sh := rules.Shape{X0: tp.X0, Y0: tp.Y0, W: tp.W, H: tp.H, Rects: tp.Rects}88 for ly := 0; ly < tp.H; ly++ {89 for lx := 0; lx < tp.W; lx++ {90 id := (tp.Y0+ly)*G + tp.X0 + lx91 if known[id] || !sh.Contains(tp.X0+lx, tp.Y0+ly) {92 continue93 }94 v := int(o.Solution[tp.Y0+ly][tp.X0+lx] - '0')95 in.Candidates = append(in.Candidates, [2]int{id, v}, [2]int{id, 1 - v})96 }97 }98 out, err := Judge(in)99 if err != nil {100 t.Fatal(err)101 }102 forced, guess := 0, 0103 for i := 0; i < len(out.Results); i += 2 {104 right, wrong := out.Results[i], out.Results[i+1]105 switch right.Class {106 case "forced":107 forced++108 if len(right.Rules) == 0 || wrong.Class != "contradiction" {109 t.Fatalf("cell %d: %+v / %+v", right.ID, right, wrong)110 }111 case "guess":112 guess++113 if wrong.Class != "guess" {114 t.Fatalf("cell %d: right is guess but wrong value is %s", right.ID, wrong.Class)115 }116 default:117 t.Fatalf("cell %d: unexpected class %s", right.ID, right.Class)118 }119 }120 if forced == 0 || guess == 0 {121 t.Fatalf("want both forced and guess cells on a pinned hard tile, got forced=%d guess=%d", forced, guess)122 }123 t.Logf("forced=%d guess=%d steps=%d in %.2fms", forced, guess, out.Steps, out.Ms)124}125126func TestJudgeKnownCells(t *testing.T) {127 o := generate(t, 2, 8)128 in := pinnedInput(o, 0, 0)129 k := in.Known[0]130 in.Candidates = [][2]int{k, {k[0], 1 - k[1]}}131 out, err := Judge(in)132 if err != nil {133 t.Fatal(err)134 }135 if out.Results[0].Class != "known" || out.Results[0].Rules[0] != "seam" {136 t.Fatalf("known cell: %+v", out.Results[0])137 }138 if out.Results[1].Class != "contradiction" {139 t.Fatalf("wrong value on a known cell: %+v", out.Results[1])140 }141}142143// TestJudgeUnder50msAt14 is PLAN item 2: a batch at n=14 judged in under144// 50 ms with depth 0, over all four revealed tiles of a 2x2 mosaic.145func TestJudgeUnder50msAt14(t *testing.T) {146 o := fixture(t)147 G := o.Manifest.G148 in := Input{G: G, Depth: 0}149 seen := map[int]bool{}150 for i, tp := range o.Manifest.Tiles {151 in.Shapes = append(in.Shapes, ShapeSpec{X0: tp.X0, Y0: tp.Y0, W: tp.W, H: tp.H, Rects: tp.Rects})152 for _, g := range o.Tiles[i].Givens {153 if !seen[g[0]] {154 seen[g[0]] = true155 in.Known = append(in.Known, g)156 }157 }158 }159 for id := 0; id < G*G; id++ {160 if !seen[id] && o.Solution[id/G][id%G] != '.' {161 in.Candidates = append(in.Candidates, [2]int{id, int(o.Solution[id/G][id%G] - '0')})162 }163 }164 var worst time.Duration165 for i := 0; i < 5; i++ {166 t0 := time.Now()167 out, err := Judge(in)168 if err != nil {169 t.Fatal(err)170 }171 if d := time.Since(t0); d > worst {172 worst = d173 }174 if i == 0 {175 t.Logf("%d candidates, %d steps, %.2fms", len(in.Candidates), out.Steps, out.Ms)176 }177 }178 if worst > 50*time.Millisecond {179 t.Fatalf("judge took %s, want < 50ms", worst)180 }181 in.Depth = 1182 t0 := time.Now()183 out, _ := Judge(in)184 t.Logf("depth 1: %d steps in %s", out.Steps, time.Since(t0))185}