pack.Params.Accept lets the generator refuse a candidate whose lines inside fixed context tiles cannot be completed (a segment of a fixed tile's line is not balanced as the candidate's own line); gen answers with a SAT feasibility check over the context cells. A NOFIT against the fixed context now fails the chunk at once instead of burning attempts, so the field re-packs it.
- commit
- aa2dd90cb58d270d4bfae81001224a600fd83eaf
- parent
- 0cef9c8589b2177a7178e822dbd1ba10b9ac5291
- author
- dami <gazey55@gmail.com>
| M | engine/ | 39 | ++++++++++++++++++++++++++++-- |
| M | engine/ | 17 | +++++++++++++ |
@@ -239,8 +239,33 @@ func Generate(ctx context.Context, p Params) (*Output, error) {
239239 }240240 fixed = append(fixed, f.Tile)241241 }242- L, err := pack.Pack(pack.Params{Seed: p.Seed + "|" + p.Nonce, G: p.G, Kind: p.Kind, N: p.N, BigArea: p.BigArea, Coverage: p.Coverage, Compound: p.Compound,243- Region: p.Region, Fixed: fixed, IDBase: p.IDBase})242+ pp := pack.Params{Seed: p.Seed + "|" + p.Nonce, G: p.G, Kind: p.Kind, N: p.N, BigArea: p.BigArea, Coverage: p.Coverage, Compound: p.Compound,243+ Region: p.Region, Fixed: fixed, IDBase: p.IDBase}244+ if len(fixed) > 0 {245+ // a candidate touching the context must be completable over the246+ // context's cells (a whole line of it can lie inside a fixed tile,247+ // where nothing made that segment balanced); the packer asks248+ ctxBoard := make([]rules.Cell, p.G*p.G)249+ for i := range ctxBoard {250+ ctxBoard[i] = rules.Empty251+ }252+ for _, f := range p.Fixed {253+ sh := rules.Shape{X0: f.Tile.X0, Y0: f.Tile.Y0, W: f.Tile.W, H: f.Tile.H, Rects: f.Tile.Rects}254+ for li, ok := range sh.Mask() {255+ if ok {256+ ctxBoard[(sh.Y0+li/sh.W)*p.G+sh.X0+li%sh.W] = f.Cells[li]257+ }258+ }259+ }260+ pp.Accept = func(t pack.Tile) bool {261+ sh := rules.Shape{X0: t.X0, Y0: t.Y0, W: t.W, H: t.H, Rects: t.Rects}262+ if sh.Valid() != nil {263+ return false264+ }265+ return sat.Feasible(sh.Local(ctxBoard, p.G), sh, rules.All)266+ }267+ }268+ L, err := pack.Pack(pp)244269 if err != nil {245270 return nil, err246271 }@@ -269,6 +294,10 @@ func Generate(ctx context.Context, p Params) (*Output, error) {
269294 if Verbose {270295 fmt.Fprintf(os.Stderr, "gen: attempt %d failed after %s: %v\n", attempt, time.Since(start).Round(time.Millisecond), err)271296 }297+ if errors.Is(err, errFixedNoFit) {298+ // the layout, not the draw, is at fault: attempts cannot help299+ return nil, fmt.Errorf("layout against the fixed context: %w", err)300+ }272301 }273302 return nil, fmt.Errorf("no mosaic after %d attempts: %w", p.MaxAttempts, last)274303 }@@ -757,7 +786,7 @@ func (m *mosaic) fillSolution(ctx context.Context, tag string) error {
757786 }758787 if len(byRank) == 0 {759788 if len(g.deps) > 0 {760- return fmt.Errorf("tile %d admits no completion against the fixed context", g.pubID)789+ return fmt.Errorf("tile %d: %w", g.pubID, errFixedNoFit)761790 }762791 return fmt.Errorf("tile %d admits no completion with nothing prefilled (bug)", i)763792 }@@ -1461,6 +1490,10 @@ func (m *mosaic) removeClues(ctx context.Context, rng *rand.Rand, ti int, S [][]
14611490 return tw, nil14621491 }14631492 1493+// errFixedNoFit: a tile has no completion given only fixed context tiles,1494+// which no redraw can change.1495+var errFixedNoFit = errors.New("admits no completion against the fixed context")1496+14641497 // attemptHook, when set (tests), sees the mosaic after every assertion14651498 // held and before the output is assembled.14661499 var attemptHook func(m *mosaic, tiles []*tileWork)@@ -72,6 +72,11 @@ type Params struct {
7272 Region [4]int `json:"region,omitempty"`7373 Fixed []Tile `json:"fixed,omitempty"`7474 IDBase int `json:"id_base,omitempty"`75+ // Accept, when set, is asked about every candidate that shares a cell76+ // with a fixed tile (the generator answers with a feasibility check of77+ // the candidate over the fixed tiles' cells: a line of the candidate78+ // lying inside a fixed tile is fixed too, and need not be balanced).79+ Accept func(t Tile) bool `json:"-"`7580 }7681 7782 // Tile is a shape placed on the field: a bounding box and the rectangles@@ -661,6 +666,7 @@ type scatterState struct {
661666 cand int662667 // placement region (half-open) and the cells counted for coverage663668 rx0, ry0, rx1, ry1 int669+ accept func(t Tile) bool // extra check for candidates touching fixed tiles (negative keys)664670 }665671 666672 func newScatterState(G int, region [4]int) *scatterState {@@ -814,6 +820,16 @@ func (s *scatterState) acceptsAs(t Tile, g geom, filler bool) (bool, string) {
814820 return false, "no whole line in overlap"815821 }816822 }823+ if s.accept != nil {824+ for _, tc := range touched {825+ if tc.id < 0 {826+ if !s.accept(t) {827+ return false, "infeasible over the fixed context"828+ }829+ break830+ }831+ }832+ }817833 return true, ""818834 }819835 @@ -882,6 +898,7 @@ func scatter(L *Layout, p Params, rng *rand.Rand) error {
882898 }883899 const maxReject = 4000884900 st := newScatterState(G, p.Region)901+ st.accept = p.Accept885902 if st.rx1-st.rx0 < MaxN || st.ry1-st.ry0 < MaxN {886903 return fmt.Errorf("scatter region %v is smaller than %d cells", p.Region, MaxN)887904 }