clone

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

engine/pack: chunk mode (region, fixed context tiles, id base)

dami committed 63f237f
scatter places tiles inside Params.Region against Params.Fixed context
tiles (immutable, counted for contact/overlap/containment, never
emitted); ids start at IDBase; Layout.Overlaps and Owners are indexed by
position in All() (tiles then fixed); coverage and holes are measured
inside the region.
commit
63f237fe7731f12460b3c4da446a2330b252ce19
parent
e03d5a6a3bd814757e235cf4221461c275129a76
author
dami <gazey55@gmail.com>
2 files changed, +223 -48
Mengine/pack/pack.go202++++++++++++++++++++++++------
Mengine/pack/pack_test.go69++++++++++-
M engine/pack/pack.go +159 -43
@@ -64,6 +64,14 @@ type Params struct {
6464 // filled with small tiles under the same rule (default 24); smaller6565 // holes are the gaps and stay6666 MaxHole int `json:"max_hole,omitempty"`67+ // chunk mode (scatter only): tiles are placed inside Region (x0, y0,68+ // w, h on the G field; zero means the whole field); Fixed are immutable69+ // tiles from neighbouring chunks in field coordinates with their real70+ // ids, which count as cluster members for contact, overlap and71+ // containment but are not emitted; new tile ids start at IDBase.72+ Region [4]int `json:"region,omitempty"`73+ Fixed []Tile `json:"fixed,omitempty"`74+ IDBase int `json:"id_base,omitempty"`6775 }6876 6977 // Tile is a shape placed on the field: a bounding box and the rectangles
@@ -86,13 +94,28 @@ type Overlap struct {
8694 Ids []int `json:"-"` // shared global cell ids (y*G+x), sorted8795 }8896 97+// Layout is a packing. Tiles are the emitted tiles, ids IDBase+index;98+// Fixed echoes the immutable context tiles (chunk mode). Overlaps and99+// Owners are indexed by position in All() (Tiles then Fixed), not by id.89100 type Layout struct {90101 G int `json:"G"`91102 Kind string `json:"kind"`92- SeedTile int `json:"seed_tile"`103+ SeedTile int `json:"seed_tile"` // index into Tiles93104 Tiles []Tile `json:"tiles"`94- Overlaps [][]Overlap `json:"overlaps"` // per tile id, sorted by With105+ Overlaps [][]Overlap `json:"overlaps"` // per All() index, sorted by With (an All() index)95106 Report Report `json:"report"`107+ Region [4]int `json:"region,omitempty"`108+ Fixed []Tile `json:"fixed,omitempty"`109+ IDBase int `json:"id_base,omitempty"`110+}111+112+// All lists the emitted tiles followed by the fixed context tiles; it is113+// the index space of Overlaps and Owners.114+func (L *Layout) All() []Tile {115+ if len(L.Fixed) == 0 {116+ return L.Tiles117+ }118+ return append(append([]Tile(nil), L.Tiles...), L.Fixed...)96119 }97120 98121 type Report struct {
@@ -371,18 +394,20 @@ func (t Tile) globalLines(G int) [][]int {
371394 372395 // ---------------------------------------------------------------- layout373396 374-// Owners returns, per field cell, the ids of the tiles containing it.397+// Owners returns, per field cell, the All() indexes of the tiles398+// containing it.375399 func (L *Layout) Owners() [][]int {376400 own := make([][]int, L.G*L.G)377- for _, t := range L.Tiles {401+ for i, t := range L.All() {378402 for _, id := range t.globalCells(L.G) {379- own[id] = append(own[id], t.ID)403+ own[id] = append(own[id], i)380404 }381405 }382406 return own383407 }384408 385-// Neighbours lists the tiles sharing at least one cell with t.409+// Neighbours lists the All() indexes of the tiles sharing at least one410+// cell with the tile at index t.386411 func (L *Layout) Neighbours(t int) []int {387412 out := make([]int, 0, len(L.Overlaps[t]))388413 for _, o := range L.Overlaps[t] {
@@ -405,7 +430,7 @@ func overlaps(L *Layout) [][]Overlap {
405430 }406431 }407432 }408- out := make([][]Overlap, len(L.Tiles))433+ out := make([][]Overlap, len(L.All()))409434 for k, ids := range shared {410435 sort.Ints(ids)411436 out[k[0]] = append(out[k[0]], Overlap{With: k[1], Cells: len(ids), Ids: ids})
@@ -421,13 +446,16 @@ func report(L *Layout) Report {
421446 G := L.G422447 r := Report{Tiles: len(L.Tiles), ByCells: map[string]int{}}423448 own := L.Owners()449+ rx0, ry0, rx1, ry1 := L.regionBox()424450 covered := 0425- for _, os := range own {426- if len(os) > 0 {427- covered++451+ for y := ry0; y < ry1; y++ {452+ for x := rx0; x < rx1; x++ {453+ if len(own[y*G+x]) > 0 {454+ covered++455+ }428456 }429457 }430- r.Coverage = float64(covered) / float64(G*G)458+ r.Coverage = float64(covered) / float64((rx1-rx0)*(ry1-ry0))431459 for _, t := range L.Tiles {432460 r.ByCells[bucket(t.Cells)]++433461 if len(t.Rects) > 1 && t.Cells != t.W*t.H {
@@ -445,7 +473,7 @@ func report(L *Layout) Report {
445473 if r.Overlaps > 0 {446474 r.MeanOverlap = float64(total) / float64(2*r.Overlaps)447475 }448- for _, h := range holesOf(own, G, L.Tiles) {476+ for _, h := range holesOf(own, G, L.Tiles, L.regionSlice()...) {449477 r.Holes++450478 r.HoleCells += len(h.cells)451479 }
@@ -455,6 +483,24 @@ func report(L *Layout) Report {
455483 return r456484 }457485 486+// regionBox is the placement region as a half-open box (the whole field487+// when Region is zero).488+func (L *Layout) regionBox() (x0, y0, x1, y1 int) {489+ return regionBoxOf(L.Region, L.G)490+}491+492+func (L *Layout) regionSlice() []int {493+ x0, y0, x1, y1 := L.regionBox()494+ return []int{x0, y0, x1, y1}495+}496+497+func regionBoxOf(r [4]int, G int) (x0, y0, x1, y1 int) {498+ if r[2] == 0 || r[3] == 0 {499+ return 0, 0, G, G500+ }501+ return r[0], r[1], min(G, r[0]+r[2]), min(G, r[1]+r[3])502+}503+458504 // hole is a 4-connected component of uncovered cells inside the cluster's459505 // bounding box, with its own bounding box.460506 type hole struct {
@@ -462,12 +508,16 @@ type hole struct {
462508 x0, y0, x1, y1 int463509 }464510 465-func holesOf(own [][]int, G int, tiles []Tile) []hole {511+func holesOf(own [][]int, G int, tiles []Tile, box ...int) []hole {466512 bx0, by0, bx1, by1 := G, G, 0, 0467513 for _, t := range tiles {468514 bx0, by0 = min(bx0, t.X0), min(by0, t.Y0)469515 bx1, by1 = max(bx1, t.X0+t.W), max(by1, t.Y0+t.H)470516 }517+ if len(box) == 4 {518+ bx0, by0 = max(bx0, box[0]), max(by0, box[1])519+ bx1, by1 = min(bx1, box[2]), min(by1, box[3])520+ }471521 seen := make([]bool, G*G)472522 var out []hole473523 for y := by0; y < by1; y++ {
@@ -527,7 +577,10 @@ func Pack(p Params) (*Layout, error) {
527577 return nil, fmt.Errorf("G must be at least %d", MinN)528578 }529579 rng := rngFor("pack", p.Seed, p.Kind, fmt.Sprint(p.G))530- L := &Layout{G: p.G, Kind: p.Kind}580+ L := &Layout{G: p.G, Kind: p.Kind, Region: p.Region, Fixed: p.Fixed, IDBase: p.IDBase}581+ if p.Kind != "scatter" && (p.Region != [4]int{} || len(p.Fixed) > 0 || p.IDBase != 0) {582+ return nil, fmt.Errorf("chunk mode (region, fixed, id_base) is scatter only")583+ }531584 switch p.Kind {532585 case "scatter":533586 if err := scatter(L, p, rng); err != nil {
@@ -600,20 +653,36 @@ func squarePack(p Params, rng *rand.Rand) ([]Rect, error) {
600653 // tile, its cells and lines in field ids.601654 type scatterState struct {602655 G int603- own [][]int604- cells [][]int605- lines [][][]int656+ own [][]int // per cell, keys of the tiles covering it657+ tiles map[int]Tile658+ cells map[int][]int659+ lines map[int][][]int606660 stamp []int // per cell, the candidate id that last covered it607661 cand int662+ // placement region (half-open) and the cells counted for coverage663+ rx0, ry0, rx1, ry1 int608664 }609665 610-func (s *scatterState) place(t Tile) {666+func newScatterState(G int, region [4]int) *scatterState {667+ s := &scatterState{G: G, own: make([][]int, G*G), tiles: map[int]Tile{}, cells: map[int][]int{}, lines: map[int][][]int{}}668+ s.rx0, s.ry0, s.rx1, s.ry1 = regionBoxOf(region, G)669+ return s670+}671+672+// place registers a tile under key (fixed context tiles use negative673+// keys, emitted tiles their index).674+func (s *scatterState) place(key int, t Tile) {611675 cells := t.globalCells(s.G)612676 for _, id := range cells {613- s.own[id] = append(s.own[id], t.ID)677+ s.own[id] = append(s.own[id], key)614678 }615- s.cells = append(s.cells, cells)616- s.lines = append(s.lines, t.globalLines(s.G))679+ s.tiles[key] = t680+ s.cells[key] = cells681+ s.lines[key] = t.globalLines(s.G)682+}683+684+func (s *scatterState) inRegion(t Tile) bool {685+ return t.X0 >= s.rx0 && t.Y0 >= s.ry0 && t.X0+t.W <= s.rx1 && t.Y0+t.H <= s.ry1617686 }618687 619688 // geom is a shape's cells and lines as local indexes, computed once per
@@ -640,16 +709,16 @@ func geomOf(t Tile) geom {
640709 // (length >= MinLine) of either tile, are at most a quarter of the smaller641710 // tile, and add at most two cells that would have three or more owners.642711 // Containment is impossible under the quarter rule.643-func (s *scatterState) accepts(t Tile, placed []Tile) (bool, string) {644- return s.acceptsAs(t, geomOf(t), placed, false)712+func (s *scatterState) accepts(t Tile) (bool, string) {713+ return s.acceptsAs(t, geomOf(t), false)645714 }646715 647716 // acceptsAs is accepts with the fill-pass variant: a filler additionally648717 // keeps at least half of its cells unshared in total (pinned on two or649718 // three sides it is still a puzzle), while the per-neighbour quarter cap650719 // and the whole-line requirement stay. g is the candidate's geometry.651-func (s *scatterState) acceptsAs(t Tile, g geom, placed []Tile, filler bool) (bool, string) {652- if t.X0 < 0 || t.Y0 < 0 || t.X0+t.W > s.G || t.Y0+t.H > s.G {720+func (s *scatterState) acceptsAs(t Tile, g geom, filler bool) (bool, string) {721+ if !s.inRegion(t) {653722 return false, "outside"654723 }655724 if s.stamp == nil {
@@ -696,7 +765,7 @@ func (s *scatterState) acceptsAs(t Tile, g geom, placed []Tile, filler bool) (bo
696765 if tc.cells < MinLine {697766 return false, "overlap too small"698767 }699- smaller := min(t.Cells, placed[tc.id].Cells)768+ smaller := min(t.Cells, s.tiles[tc.id].Cells)700769 if tc.cells*4 > smaller {701770 return false, "overlap too large"702771 }
@@ -812,16 +881,59 @@ func scatter(L *Layout, p Params, rng *rand.Rand) error {
812881 return fmt.Errorf("scatter needs G >= %d", MaxN)813882 }814883 const maxReject = 4000815- st := &scatterState{G: G, own: make([][]int, G*G)}884+ st := newScatterState(G, p.Region)885+ if st.rx1-st.rx0 < MaxN || st.ry1-st.ry0 < MaxN {886+ return fmt.Errorf("scatter region %v is smaller than %d cells", p.Region, MaxN)887+ }888+ // fixed context tiles: members of the cluster, never emitted889+ for k, f := range p.Fixed {890+ st.place(-1-k, f)891+ }892+ // anchors are every placed tile, fixed or new893+ anchors := append([]Tile(nil), p.Fixed...)894+ covered := 0895+ for y := st.ry0; y < st.ry1; y++ {896+ for x := st.rx0; x < st.rx1; x++ {897+ if len(st.own[y*G+x]) > 0 {898+ covered++899+ }900+ }901+ }816902 seed := drawShape(rng, p.MinCells, p.MaxCells, p.Compound)817- seed.ID = 0818- seed.X0 = rng.IntN(G - seed.W + 1)819- seed.Y0 = rng.IntN(G - seed.H + 1)903+ seed.ID = p.IDBase904+ seed.X0 = st.rx0 + rng.IntN(st.rx1-st.rx0-seed.W+1)905+ seed.Y0 = st.ry0 + rng.IntN(st.ry1-st.ry0-seed.H+1)906+ if len(p.Fixed) > 0 {907+ // the seed must not sit on the context; draw until it is clear of908+ // every fixed tile (or accepted as an overlap of one)909+ for try := 0; try < 200; try++ {910+ clear := true911+ for _, id := range seed.globalCells(G) {912+ if len(st.own[id]) > 0 {913+ clear = false914+ break915+ }916+ }917+ if clear {918+ break919+ }920+ if ok, _ := st.accepts(seed); ok {921+ break922+ }923+ seed.X0 = st.rx0 + rng.IntN(st.rx1-st.rx0-seed.W+1)924+ seed.Y0 = st.ry0 + rng.IntN(st.ry1-st.ry0-seed.H+1)925+ }926+ }820927 L.Tiles = append(L.Tiles, seed)821- st.place(seed)928+ st.place(0, seed)929+ anchors = append(anchors, seed)822930 L.SeedTile = 0823- covered := seed.Cells824- target := int(p.Coverage * float64(G*G))931+ for _, id := range st.cells[0] {932+ if len(st.own[id]) == 1 && id%G >= st.rx0 && id%G < st.rx1 && id/G >= st.ry0 && id/G < st.ry1 {933+ covered++934+ }935+ }936+ target := int(p.Coverage * float64((st.rx1-st.rx0)*(st.ry1-st.ry0)))825937 rejects := 0826938 for covered < target {827939 if rejects >= maxReject {
@@ -836,20 +948,22 @@ func scatter(L *Layout, p Params, rng *rand.Rand) error {
836948 pc = min(0.95, max(0.05, p.Compound*p.Compound/share))837949 }838950 t := drawShape(rng, p.MinCells, p.MaxCells, pc)839- a := L.Tiles[rng.IntN(len(L.Tiles))]951+ a := anchors[rng.IntN(len(anchors))]840952 // bounding boxes overlap: X0 in [a.X0-t.W+1, a.X0+a.W-1]841953 t.X0 = a.X0 - t.W + 1 + rng.IntN(a.W+t.W-1)842954 t.Y0 = a.Y0 - t.H + 1 + rng.IntN(a.H+t.H-1)843- t.ID = len(L.Tiles)844- ok, _ := st.accepts(t, L.Tiles)955+ t.ID = p.IDBase + len(L.Tiles)956+ ok, _ := st.accepts(t)845957 if !ok {846958 rejects++847959 continue848960 }849961 rejects = 0962+ key := len(L.Tiles)850963 L.Tiles = append(L.Tiles, t)851- st.place(t)852- for _, id := range st.cells[t.ID] {964+ st.place(key, t)965+ anchors = append(anchors, t)966+ for _, id := range st.cells[key] {853967 if len(st.own[id]) == 1 {854968 covered++855969 }
@@ -931,7 +1045,8 @@ func fillHoles(L *Layout, p Params, st *scatterState, rng *rand.Rand) int {
9311045 G := L.G9321046 added := 09331047 bx0, by0, bx1, by1 := clusterBox(L.Tiles, G)934- work := holesOf(st.own, G, L.Tiles)1048+ bx0, by0, bx1, by1 = max(bx0, st.rx0), max(by0, st.ry0), min(bx1, st.rx1), min(by1, st.ry1)1049+ work := holesOf(st.own, G, L.Tiles, bx0, by0, bx1, by1)9351050 for len(work) > 0 {9361051 h := work[len(work)-1]9371052 work = work[:len(work)-1]
@@ -959,13 +1074,14 @@ func fillHoles(L *Layout, p Params, st *scatterState, rng *rand.Rand) int {
9591074 }9601075 t := shape9611076 t.Rects = append([][4]int(nil), shape.Rects...)962- t.ID = len(L.Tiles)1077+ t.ID = p.IDBase + len(L.Tiles)9631078 t.X0, t.Y0 = cx-li%shape.W, cy-li/shape.W964- if ok, _ := st.acceptsAs(t, sg, L.Tiles, true); !ok {1079+ if ok, _ := st.acceptsAs(t, sg, true); !ok {9651080 continue9661081 }1082+ key := len(L.Tiles)9671083 L.Tiles = append(L.Tiles, t)968- st.place(t)1084+ st.place(key, t)9691085 added++9701086 placed = true9711087 break shapes
@@ -1036,7 +1152,7 @@ func (L *Layout) UnfillableHoles(maxHole int) int {
10361152 }10371153 hull := convexHull(pts)10381154 n := 01039- for _, h := range holesOf(L.Owners(), L.G, L.Tiles) {1155+ for _, h := range holesOf(L.Owners(), L.G, L.Tiles, L.regionSlice()...) {10401156 if len(h.cells) <= maxHole {10411157 continue10421158 }
M engine/pack/pack_test.go +64 -5
@@ -248,15 +248,74 @@ func TestSquareKindsUnchanged(t *testing.T) {
248248 // not against the packer's own bookkeeping.249249 func replayAccepts(t *testing.T, L *Layout) {250250 t.Helper()251- st := &scatterState{G: L.G, own: make([][]int, L.G*L.G)}252- st.place(L.Tiles[0])253- for _, tl := range L.Tiles[1:] {254- ok, why := st.accepts(tl, L.Tiles)251+ st := newScatterState(L.G, L.Region)252+ for k, f := range L.Fixed {253+ st.place(-1-k, f)254+ }255+ st.place(0, L.Tiles[0])256+ for i, tl := range L.Tiles[1:] {257+ ok, why := st.accepts(tl)255258 if !ok {256259 t.Fatalf("tile %d fails the placement rule: %s", tl.ID, why)257260 }258- st.place(tl)261+ st.place(i+1, tl)262+ }263+}264+265+// TestScatterChunk packs a region of a field against fixed context tiles266+// (chunk mode): every emitted tile lies inside the region, ids start at267+// IDBase, overlaps with the fixed tiles are recorded (as All() indexes at268+// or past len(Tiles)) and the placement rule holds against them.269+func TestScatterChunk(t *testing.T) {270+ // context: the tiles of an unrelated scatter at G=100 that intersect271+ // the band x in [40, 56), pretending they came from the left chunk272+ base, err := Pack(Params{Seed: "ctx", G: 100, Kind: "scatter"})273+ if err != nil {274+ t.Fatal(err)275+ }276+ var fixed []Tile277+ for _, tl := range base.Tiles {278+ if tl.X0+tl.W > 40 && tl.X0 < 56 && tl.X0 >= 0 {279+ f := tl280+ f.ID = 5000 + tl.ID281+ fixed = append(fixed, f)282+ }283+ }284+ if len(fixed) == 0 {285+ t.Fatal("no context tiles")286+ }287+ // context tiles must lie inside the region to be legal anchors; clip288+ // the region to the whole field width so they do (a real chunk's289+ // fixed tiles straddle its band, which is inside its region)290+ L, err := Pack(Params{Seed: "chunk", G: 100, Kind: "scatter", Region: [4]int{40, 0, 60, 100}, Fixed: fixed, IDBase: 300})291+ if err != nil {292+ t.Fatal(err)293+ }294+ if len(L.Tiles) < 5 {295+ t.Fatalf("only %d tiles in the chunk", len(L.Tiles))296+ }297+ withFixed := 0298+ for i, tl := range L.Tiles {299+ if tl.ID != 300+i {300+ t.Fatalf("tile %d has id %d", i, tl.ID)301+ }302+ if tl.X0 < 40 || tl.X0+tl.W > 100 {303+ t.Fatalf("tile %d outside the region: x0=%d w=%d", tl.ID, tl.X0, tl.W)304+ }305+ for _, o := range L.Overlaps[i] {306+ if o.With >= len(L.Tiles) {307+ withFixed++308+ if L.Fixed[o.With-len(L.Tiles)].ID < 5000 {309+ t.Fatalf("overlap with a non-fixed index %d", o.With)310+ }311+ }312+ }313+ }314+ if withFixed == 0 {315+ t.Fatal("no overlap with a fixed context tile")259316 }317+ replayAccepts(t, L)318+ t.Logf("chunk: %d tiles, %d overlaps with %d fixed tiles, coverage %.3f", len(L.Tiles), withFixed, len(fixed), L.Report.Coverage)260319 }261320 262321 func TestScatter(t *testing.T) {