clone

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

engine: every scatter tile gets a whole-line overlap; gen refuses unpinnable layouts

dami committed 789f1d6
The 9x9 field failed after 13 min because two odd chunks' seed tiles,
placed touching nothing, never received a whole-line overlap from a
later drop, so the per-tile assertion (an alternative differs inside an
overlap region) was unsatisfiable and each chunk burned 20 attempts x 3
re-packings discovering it. scatter now drops a tile onto an unpinnable
seed, checks every tile after the fill pass, and Pack re-draws with a
salted rng (bounded) when the layout still has an unpinnable tile;
gen refuses such a layout before any search. TestEveryTilePinnable
covers free-standing layouts and odd chunks against fixed context.
commit
789f1d6b3c5daf63b3376dae6cda83656ea04795
parent
f111d981ad249d1055688526b9f3fba86e0e4f3b
author
dami <gazey55@gmail.com>
3 files changed, +126 -2
Mengine/gen/gen.go16+++++++
Mengine/pack/pack.go67+++++++++++++++++++++++++++++-
Mengine/pack/pack_test.go45++++++++++++++++++++
M engine/gen/gen.go +16 -0
M engine/pack/pack.go +65 -2
@@ -136,6 +136,7 @@ type Report struct {
136136 MaxHole int `json:"max_hole,omitempty"`137137 Filled int `json:"filled,omitempty"` // tiles added by the hole-fill pass138138 Stopped string `json:"stopped,omitempty"`139+ Salt int `json:"salt,omitempty"` // scatter: rng salt that produced a pinnable layout139140 }140141 141142 // Rect is a rectangle in field or pitch coordinates.
@@ -588,7 +589,21 @@ func Pack(p Params) (*Layout, error) {
588589 }589590 switch p.Kind {590591 case "scatter":591- if err := scatter(L, p, rng); err != nil {592+ // a layout can come out with a tile no neighbour can pin (seed593+ // tile boxed in by context); re-draw with a salted rng, bounded594+ var err error595+ for salt := 0; salt < 8; salt++ {596+ r := rng597+ if salt > 0 {598+ r = rngFor("pack", p.Seed, p.Kind, fmt.Sprint(p.G), fmt.Sprint("salt", salt))599+ }600+ L = &Layout{G: p.G, Kind: p.Kind, Region: p.Region, Fixed: p.Fixed, IDBase: p.IDBase}601+ if err = scatter(L, p, r); err == nil {602+ L.Report.Salt = salt603+ break604+ }605+ }606+ if err != nil {592607 return nil, err593608 }594609 case "grid", "bands", "big-first":
@@ -607,7 +622,7 @@ func Pack(p Params) (*Layout, error) {
607622 }608623 L.Overlaps = overlaps(L)609624 rep := report(L)610- rep.Stopped, rep.MaxHole, rep.Filled = L.Report.Stopped, L.Report.MaxHole, L.Report.Filled625+ rep.Stopped, rep.MaxHole, rep.Filled, rep.Salt = L.Report.Stopped, L.Report.MaxHole, L.Report.Filled, L.Report.Salt611626 L.Report = rep612627 return L, nil613628 }
@@ -989,11 +1004,59 @@ func scatter(L *Layout, p Params, rng *rand.Rand) error {
9891004 if L.Report.Stopped == "" {9901005 L.Report.Stopped = "coverage"9911006 }1007+ // the seed tile is the one tile placed touching nothing; if no later1008+ // drop overlapped it by a whole line it cannot be pinned by any1009+ // neighbour and the generator's "an alternative differs inside an1010+ // overlap region" is unsatisfiable for it. Drop a tile onto it now.1011+ if !st.pinnable(0) {1012+ seed := L.Tiles[0]1013+ placed := false1014+ for try := 0; try < 4000 && !placed; try++ {1015+ t := drawShape(rng, p.MinCells, p.MaxCells, p.Compound)1016+ t.X0 = seed.X0 - t.W + 1 + rng.IntN(seed.W+t.W-1)1017+ t.Y0 = seed.Y0 - t.H + 1 + rng.IntN(seed.H+t.H-1)1018+ t.ID = p.IDBase + len(L.Tiles)1019+ if ok, _ := st.accepts(t); !ok {1020+ continue1021+ }1022+ key := len(L.Tiles)1023+ L.Tiles = append(L.Tiles, t)1024+ st.place(key, t)1025+ placed = st.pinnable(0)1026+ }1027+ if !placed {1028+ return fmt.Errorf("seed tile %d (%dx%d at %d,%d) admits no neighbour with a whole-line overlap", seed.ID, seed.W, seed.H, seed.X0, seed.Y0)1029+ }1030+ }9921031 L.Report.MaxHole = p.MaxHole9931032 L.Report.Filled = fillHoles(L, p, st, rng)1033+ for k := range L.Tiles {1034+ if !st.pinnable(k) {1035+ t := L.Tiles[k]1036+ return fmt.Errorf("tile %d (%dx%d at %d,%d) has no overlap of at least %d cells", t.ID, t.W, t.H, t.X0, t.Y0, MinLine)1037+ }1038+ }9941039 return nil9951040 }9961041 1042+// pinnable reports whether placed tile key shares at least MinLine cells1043+// with some other member of the cluster (fixed or new): the precondition1044+// for a neighbour to be able to pin it.1045+func (s *scatterState) pinnable(key int) bool {1046+ counts := map[int]int{}1047+ for _, id := range s.cells[key] {1048+ for _, o := range s.own[id] {1049+ if o != key {1050+ counts[o]++1051+ if counts[o] >= MinLine {1052+ return true1053+ }1054+ }1055+ }1056+ }1057+ return false1058+}1059+9971060 // fillShapes are the small valid shapes the fill pass draws from, largest9981061 // first: 8x8, 8x6, 6x8, 6x6, the notched 6x6 (four corners), and every L9991062 // or T union of two of those rectangles at an even offset that fits a
M engine/pack/pack_test.go +45 -0
@@ -412,3 +412,48 @@ func TestScatterCoverage85(t *testing.T) {
412412 }413413 }414414 }415+416+// TestEveryTilePinnable: every tile of a scatter layout, free-standing or417+// as an odd chunk packed against fixed context, shares at least MinLine418+// cells with some neighbour (the generator's pin precondition).419+func TestEveryTilePinnable(t *testing.T) {420+ check := func(name string, L *Layout) {421+ t.Helper()422+ for i, tile := range L.Tiles {423+ ok := false424+ for _, o := range L.Overlaps[i] {425+ if o.Cells >= MinLine {426+ ok = true427+ }428+ }429+ if !ok {430+ t.Fatalf("%s: tile %d (%dx%d at %d,%d) has no overlap of %d cells", name, tile.ID, tile.W, tile.H, tile.X0, tile.Y0, MinLine)431+ }432+ }433+ }434+ for _, seed := range []string{"a", "b", "c", "d", "e", "f", "g", "h"} {435+ L, err := Pack(Params{Seed: seed, G: 60, Kind: "scatter"})436+ if err != nil {437+ t.Fatal(seed, err)438+ }439+ check("G=60 "+seed, L)440+ // an odd chunk: region inside a bigger field, fixed context = the441+ // tiles of a free-standing layout that intersect the region's band442+ A, err := Pack(Params{Seed: seed + "-even", G: 148, Kind: "scatter"})443+ if err != nil {444+ t.Fatal(err)445+ }446+ region := [4]int{44, 44, 60, 60}447+ var fixed []Tile448+ for _, ft := range A.Tiles {449+ if ft.X0 < region[0]+region[2] && ft.X0+ft.W > region[0] && ft.Y0 < region[1]+region[3] && ft.Y0+ft.H > region[1] {450+ fixed = append(fixed, ft)451+ }452+ }453+ B, err := Pack(Params{Seed: seed + "-odd", G: 148, Kind: "scatter", Region: region, Fixed: fixed, IDBase: 100000})454+ if err != nil {455+ t.Fatal(seed, err)456+ }457+ check("chunk "+seed, B)458+ }459+}