clone

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

engine/gen: clue release as a method with a stat; tests

dami committed 18c9c41
Stats.Releases counts repairs that took back neighbours' seam clues;
TestReleaseClues exercises the release on a real generation through a
test hook; TestRepairAtG200 (MOSAIC_SLOW=1) is the recorded G=200 seed-a
failure and must now finish in one attempt.
commit
18c9c41662976513c4a62e2ef373c2513058def7
parent
7cb024ca609560790f6d8a3f1e5ab294eaca80bc
author
dami <gazey55@gmail.com>
2 files changed, +115 -36
Mengine/gen/gen.go82+++++++++++++++++-------------
Mengine/gen/gen_test.go69+++++++++++++++++++++++++
M engine/gen/gen.go +46 -36
@@ -177,6 +177,7 @@ type Stats struct {
177177 Refills int `json:"refills"`178178 Backjumps int `json:"backjumps"`179179 Regivens int `json:"regivens"`180+ Releases int `json:"releases"` // repairs that took back neighbours' seam clues180181 Waves int `json:"waves"`181182 }182183
@@ -556,41 +557,6 @@ func generateAttempt(ctx context.Context, p Params, L *pack.Layout, attempt int)
556557 m.stats.Regivens++557558 return nil558559 }559- // release takes back the seam clues other tiles put on bad's shared560- // cells (they can fix its only pin-eligible overlap outright, leaving561- // nothing for an alternative to differ on) and returns the tiles that562- // lost a clue, so their givens can be rebuilt afterwards563- release := func(bad int) []int {564- g := m.geom[bad]565- var dirty []int566- seen := map[int]bool{}567- for li, ok := range g.mask {568- if !ok || !g.shared[li] {569- continue570- }571- id := g.gid(m, li)572- nb, has := m.seamOwner[id]573- if !has || nb == bad {574- continue575- }576- delete(m.seamGivens, id)577- delete(m.seamOwner, id)578- t := tiles[nb]579- delete(t.ownSeamValue, id)580- for k, v := range t.ownSeam {581- if v == id {582- t.ownSeam = append(t.ownSeam[:k], t.ownSeam[k+1:]...)583- break584- }585- }586- if !seen[nb] {587- seen[nb] = true588- dirty = append(dirty, nb)589- }590- }591- sort.Ints(dirty)592- return dirty593- }594560 released := make([]int, total)595561 // repair rebuilds bad's givens; when that fails because the neighbours'596562 // clues left nothing to pin on, it releases those clues, rebuilds bad
@@ -603,11 +569,12 @@ func generateAttempt(ctx context.Context, p Params, L *pack.Layout, attempt int)
603569 if released[bad] >= 2 {604570 return err605571 }606- dirty := release(bad)572+ dirty := m.releaseClues(tiles, bad)607573 if len(dirty) == 0 {608574 return err609575 }610576 released[bad]++577+ m.stats.Releases++611578 if Verbose {612579 fmt.Fprintf(os.Stderr, "gen: repair tile %d: released %d neighbours' clues (%v)\n", m.geom[bad].pubID, len(dirty), err)613580 }
@@ -669,6 +636,9 @@ func generateAttempt(ctx context.Context, p Params, L *pack.Layout, attempt int)
669636 }670637 }671638 639+ if attemptHook != nil {640+ attemptHook(m, tiles)641+ }672642 commit := sha256.Sum256([]byte(p.Seed + "|" + p.Nonce))673643 mByN := map[string]int{}674644 for n := pack.MinN; n <= pack.MaxN; n += 2 {
@@ -1474,6 +1444,46 @@ func (m *mosaic) removeClues(ctx context.Context, rng *rand.Rand, ti int, S [][]
14741444 return tw, nil14751445 }14761446 1447+// attemptHook, when set (tests), sees the mosaic after every assertion1448+// held and before the output is assembled.1449+var attemptHook func(m *mosaic, tiles []*tileWork)1450+1451+// releaseClues takes back the seam clues other tiles put on bad's shared1452+// cells (they can fix its only pin-eligible overlap outright, leaving1453+// nothing for an alternative to differ on) and returns the tiles that1454+// lost a clue, sorted, so their givens can be rebuilt afterwards.1455+func (m *mosaic) releaseClues(tiles []*tileWork, bad int) []int {1456+ g := m.geom[bad]1457+ var dirty []int1458+ seen := map[int]bool{}1459+ for li, ok := range g.mask {1460+ if !ok || !g.shared[li] {1461+ continue1462+ }1463+ id := g.gid(m, li)1464+ nb, has := m.seamOwner[id]1465+ if !has || nb == bad {1466+ continue1467+ }1468+ delete(m.seamGivens, id)1469+ delete(m.seamOwner, id)1470+ t := tiles[nb]1471+ delete(t.ownSeamValue, id)1472+ for k, v := range t.ownSeam {1473+ if v == id {1474+ t.ownSeam = append(t.ownSeam[:k], t.ownSeam[k+1:]...)1475+ break1476+ }1477+ }1478+ if !seen[nb] {1479+ seen[nb] = true1480+ dirty = append(dirty, nb)1481+ }1482+ }1483+ sort.Ints(dirty)1484+ return dirty1485+}1486+14771487 // tileWithoutWitness finds a tile some alternative of which is not refuted14781488 // by any neighbour that is unanimous on a shared cell where the alternative14791489 // differs from the solution. Returns -1 if every tile has a witness.
M engine/gen/gen_test.go +69 -0
@@ -465,3 +465,72 @@ func TestField2x2(t *testing.T) {
465465 t.Fatal("field differs with jobs=4")466466 }467467 }468+469+// TestReleaseClues checks the repair's first step on a real generation: a470+// seam clue one tile put on a cell it shares with another is taken back471+// from the shared record and from its owner when the other tile is472+// repaired, and the owner is reported dirty.473+func TestReleaseClues(t *testing.T) {474+ checked := false475+ attemptHook = func(m *mosaic, tiles []*tileWork) {476+ for id, owner := range m.seamOwner {477+ var bad = -1478+ for _, o := range m.owners[id] {479+ if o != owner && !m.geom[o].fixed {480+ bad = o481+ break482+ }483+ }484+ if bad < 0 {485+ continue486+ }487+ before := len(tiles[owner].ownSeam)488+ dirty := m.releaseClues(tiles, bad)489+ if _, still := m.seamGivens[id]; still {490+ t.Errorf("clue %d still recorded after release", id)491+ }492+ if _, still := m.seamOwner[id]; still {493+ t.Errorf("clue %d still owned after release", id)494+ }495+ if len(tiles[owner].ownSeam) != before-1 {496+ t.Errorf("owner %d still lists the clue", owner)497+ }498+ found := false499+ for _, d := range dirty {500+ if d == owner {501+ found = true502+ }503+ }504+ if !found {505+ t.Errorf("owner %d not reported dirty: %v", owner, dirty)506+ }507+ checked = true508+ break509+ }510+ }511+ defer func() { attemptHook = nil }()512+ if _, err := Generate(context.Background(), Params{Seed: "release", G: 48, Kind: "scatter"}); err != nil {513+ t.Fatal(err)514+ }515+ if !checked {516+ t.Skip("no seam clue on a cell shared with another tile in this seed")517+ }518+}519+520+// TestRepairAtG200 is the recorded failure of the restart-based repair:521+// seed a at G=200 scatter needed two attempts before the in-place repair522+// (the first died on "an alternative solution differs inside no overlap523+// region" after a regiven). With release it must finish in one.524+func TestRepairAtG200(t *testing.T) {525+ if os.Getenv("MOSAIC_SLOW") == "" {526+ t.Skip("MOSAIC_SLOW=1 runs the G=200 repair check (~10 min)")527+ }528+ o, err := Generate(context.Background(), Params{Seed: "a", G: 200, Kind: "scatter"})529+ if err != nil {530+ t.Fatal(err)531+ }532+ t.Logf("G=200: %d tiles, attempts %d, regivens %d, releases %d, %d ms", len(o.Manifest.Tiles), o.Stats.Attempts, o.Stats.Regivens, o.Stats.Releases, o.Stats.Ms)533+ if o.Stats.Attempts != 1 {534+ t.Fatalf("took %d attempts", o.Stats.Attempts)535+ }536+}