clone

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

engine: a drop must be completable with the new tiles it touches; a one-hop UNSAT is a NOFIT

dami committed 42c1db7
The remaining pathological chunks (ring 1 of the daily: 30 min; a
2x2 ring-1 chunk: minutes) were fills thrashing between two tiles whose
only redrawable dependencies could never help: a filler accepted
against the fixed context alone had no completion together with the
new neighbour it touched, and the fill's 'complete the tile alone'
fallback then wrote that neighbour anyway. The packer's hook now asks
gen for a joint completion of the candidate with the placed new tiles
it touches over the fixed context (one SAT call per geometrically
accepted candidate), and the fill treats a one-hop joint UNSAT as a
real NOFIT. The dumped slow chunk: >2 min looping -> 1 s, one attempt.
commit
42c1db7a8e2297d6fc7c6f4e582de116d4ad68b7
parent
835132ad7a94b6ffa42664b580d07ae5dd775fd0
author
dami <gazey55@gmail.com>
2 files changed, +30 -21
Mengine/gen/gen.go31+++++++++++++++++-------------
Mengine/pack/pack.go20++++++++++++--------
M engine/gen/gen.go +18 -13
@@ -382,12 +382,21 @@ func generate(ctx context.Context, p Params) (*Output, error) {
382382 }383383 }384384 }385- pp.Accept = func(t pack.Tile) bool {385+ // a candidate must be completable together with the placed new386+ // tiles it touches over the fixed context: feasibility against the387+ // context alone let through tiles that no completion of their new388+ // neighbours could accommodate, and the fill then thrashed on them389+ pp.Accept = func(t pack.Tile, touched []pack.Tile) bool {386390 sh := rules.Shape{X0: t.X0, Y0: t.Y0, W: t.W, H: t.H, Rects: t.Rects}387391 if sh.Valid() != nil {388392 return false389393 }390- return sat.Feasible(sh.Local(ctxBoard, p.G), sh, rules.All)394+ others := make([]rules.Shape, 0, len(touched))395+ for _, o := range touched {396+ others = append(others, rules.Shape{X0: o.X0, Y0: o.Y0, W: o.W, H: o.H, Rects: o.Rects})397+ }398+ _, ok := sat.CompleteRegion(ctxBoard, p.G, others, sh, rngFor("accept", fmt.Sprint(t.ID)))399+ return ok391400 }392401 }393402 packLayout := func(salt int) (*pack.Layout, error) {
@@ -1338,17 +1347,13 @@ func (m *mosaic) fillTile(ctx context.Context, tag string, i, refill int) (bool,
13381347 }13391348 rng := rngFor(tag, "fill", fmt.Sprint(i), fmt.Sprint(refill), fmt.Sprint(t))13401349 full, ok := sat.CompleteRegion(m.global.Cells, m.G, later, g.sh, rng)1341- if !ok && len(later) > 0 {1342- // the joint region refused; the tile alone may still complete1343- // (the neighbours' joint infeasibility is theirs to backjump1344- // on, and this tile, unlike fixed context, can be redrawn).1345- // Try one hop, then the tile by itself.1346- if len(hop1Shapes) < len(later) {1347- full, ok = sat.CompleteRegion(m.global.Cells, m.G, hop1Shapes, g.sh, rng)1348- }1349- if !ok {1350- full, ok = sat.CompleteRegion(m.global.Cells, m.G, nil, g.sh, rng)1351- }1350+ if !ok && len(hop1Shapes) < len(later) {1351+ // the two-hop region refused; the direct neighbours may still1352+ // admit a completion together with this tile (the second hop's1353+ // infeasibility is theirs to backjump on). A one-hop UNSAT is1354+ // a real NOFIT: no completion of this tile leaves its1355+ // neighbours completable, so backjump rather than write it.1356+ full, ok = sat.CompleteRegion(m.global.Cells, m.G, hop1Shapes, g.sh, rng)13521357 }13531358 if !ok {13541359 return false, nil
M engine/pack/pack.go +12 -8
@@ -77,7 +77,10 @@ type Params struct {
7777 // with a fixed tile (the generator answers with a feasibility check of7878 // the candidate over the fixed tiles' cells: a line of the candidate7979 // lying inside a fixed tile is fixed too, and need not be balanced).80- Accept func(t Tile) bool `json:"-"`80+ // Accept, when set, is asked whether a candidate that touches placed tiles81+ // can be completed together with the placed NEW tiles it touches (fixed82+ // context is the caller's business); a false rejects the candidate83+ Accept func(t Tile, touched []Tile) bool `json:"-"`8184 }8285 8386 // Tile is a shape placed on the field: a bounding box and the rectangles
@@ -686,7 +689,7 @@ type scatterState struct {
686689 cand int687690 // placement region (half-open) and the cells counted for coverage688691 rx0, ry0, rx1, ry1 int689- accept func(t Tile) bool // extra check for candidates touching fixed tiles (negative keys)692+ accept func(t Tile, touched []Tile) bool // joint feasibility of a candidate with the placed new tiles it touches690693 }691694 692695 func newScatterState(G int, region [4]int) *scatterState {
@@ -840,15 +843,16 @@ func (s *scatterState) acceptsAs(t Tile, g geom, filler bool) (bool, string) {
840843 return false, "no whole line in overlap"841844 }842845 }843- if s.accept != nil {846+ if s.accept != nil && len(touched) > 0 {847+ var news []Tile844848 for _, tc := range touched {845- if tc.id < 0 {846- if !s.accept(t) {847- return false, "infeasible over the fixed context"848- }849- break849+ if tc.id >= 0 {850+ news = append(news, s.tiles[tc.id])850851 }851852 }853+ if !s.accept(t, news) {854+ return false, "infeasible with its neighbours over the fixed context"855+ }852856 }853857 return true, ""854858 }