clone

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

engine: tile commitments (mosaic-tile-v1) under a private day key; day seed committed, released at archive

dami committed 8121c97
manifest.tiles[].hash was sha256 over the tile's solution, so an agent
enumerating a revealed tile's m candidates could find the truth by
hashing them. Now: seed_commit = sha256("mosaic-day-seed-v1|" + day_seed)
is the manifest id; day_key = HMAC("mosaic-day-key-v1", day_seed) keys
every tile hash and sha256(day_key) is published as hash_key_commit; the
hash is HMAC-SHA256 over a versioned length-prefixed encoding of the game
id, tile id, geometry, packed solution bits and packed givens mask
(mosaic-tile-v1). day_seed and day_key live in the private output and
are released with the archive. Fields rehash tiles after the merge with
their final ids and coordinates. TestHashIsACommitment: no candidate
matches without the key, exactly one with it; the checker verifies seed
and key commitments from the released values. gen-v7; 2x2 fixture
regenerated (X=15 follows).
commit
8121c976bc79725e8e784d9a4fff46eee0937e80
parent
98712fd72ca842e32bb7696b3735676521ba5688
author
dami <gazey55@gmail.com>
4 files changed, +281 -49
Mengine/gen/field.go71+++++++++++---
Mengine/gen/gen.go154+++++++++++++++++++++++-------
Mengine/gen/gen_test.go103++++++++++++++++++++
Mengine/testdata/mosaic-2-14.json2+-
M engine/gen/field.go +58 -13
@@ -291,6 +291,9 @@ func fieldDefaults(p FieldParams) FieldParams {
291291 // No existing tile's cell changes; that is asserted. When no chunk of the292292 // ring fits on the field, the delta is empty and Stats.Exhausted is set.293293 func GrowField(ctx context.Context, state *Output, p FieldParams, ring int, run Runner) (*Output, error) {294+ if state.DaySeed != "" {295+ p.Seed = state.DaySeed296+ }294297 p = fieldDefaults(p)295298 plan, err := planOf(&state.Manifest)296299 if err != nil {
@@ -625,7 +628,9 @@ func mergeChunks(p FieldParams, plan FieldPlan, chunks []latticeChunk, outs []*O
625628 }626629 }627630 nonce := chunkNonce(p.Seed, -1, 0)628- commit := sha256.Sum256([]byte(p.Seed + "|" + nonce))631+ _ = nonce632+ commit := DayCommit(p.Seed)633+ dayKey := DayKey(p.Seed)629634 mByN := map[string]int{}630635 for n := pack.MinN; n <= pack.MaxN; n += 2 {631636 mByN[fmt.Sprint(n)] = pack.MByN(n)
@@ -652,26 +657,66 @@ func mergeChunks(p FieldParams, plan FieldPlan, chunks []latticeChunk, outs []*O
652657 }653658 out := &Output{654659 Manifest: Manifest{655- Version: 3,656- ID: hex.EncodeToString(commit[:]),657- SeedCommit: hex.EncodeToString(commit[:]),658- G: G,659- Packer: PackerInfo{Kind: "field", Params: map[string]any{"kind": p.Kind, "chunks": p.Chunks, "chunk_g": plan.ChunkG, "band": plan.Band, "coverage": p.Coverage, "compound": p.Compound}},660- MByN: mByN,661- Rules: Rules{Overlaps: true, CrossTile: false, SeamClues: seamClues},662- SeedTile: seedTiles[0],663- Tiles: tiles,664- K: "unmeasured",665- Generator: generatorHash(),666- Field: &FieldPublic{Chunks: plan.Chunks, ChunkG: plan.ChunkG, Band: plan.Band, Origin: plan.Origin, SeedTiles: seedTiles},660+ Version: 3,661+ HashScheme: "mosaic-tile-v1",662+ HashKeyCommit: HashKeyCommit(dayKey),663+ ID: commit,664+ SeedCommit: commit,665+ G: G,666+ Packer: PackerInfo{Kind: "field", Params: map[string]any{"kind": p.Kind, "chunks": p.Chunks, "chunk_g": plan.ChunkG, "band": plan.Band, "coverage": p.Coverage, "compound": p.Compound}},667+ MByN: mByN,668+ Rules: Rules{Overlaps: true, CrossTile: false, SeamClues: seamClues},669+ SeedTile: seedTiles[0],670+ Tiles: tiles,671+ K: "unmeasured",672+ Generator: generatorHash(),673+ Field: &FieldPublic{Chunks: plan.Chunks, ChunkG: plan.ChunkG, Band: plan.Band, Origin: plan.Origin, SeedTiles: seedTiles},667674 },668675 Tiles: priv,669676 Stats: Stats{Attempts: attempts, Regivens: regivens, Backjumps: backjumps, Waves: waves},670677 Layout: pack.Report{Tiles: idBase + len(tiles), Coverage: float64(covered) / float64(G*G)},671678 ChunkStats: chunkStats,679+ DaySeed: p.Seed,680+ HashKey: hex.EncodeToString(dayKey),672681 }673682 for y := range sol {674683 out.Solution = append(out.Solution, string(sol[y]))675684 }685+ rehashTiles(out, dayKey)676686 return out, nil677687 }688+689+// rehashTiles recomputes every emitted tile's commitment over the merged690+// output: a chunk hashes with its local ids and coordinates, the field691+// renumbers and translates them, and the commitment binds the final id692+// and position.693+func rehashTiles(out *Output, key []byte) {694+ G := out.Manifest.G695+ for i := range out.Manifest.Tiles {696+ tp := &out.Manifest.Tiles[i]697+ sh := rules.Shape{X0: tp.X0, Y0: tp.Y0, W: tp.W, H: tp.H, Rects: tp.Rects}698+ given := map[int]bool{}699+ if i < len(out.Tiles) {700+ for _, g := range out.Tiles[i].Givens {701+ given[g[0]] = true702+ }703+ }704+ sol := make([]byte, tp.W*tp.H)705+ giv := make([]byte, tp.W*tp.H)706+ for ly := 0; ly < tp.H; ly++ {707+ for lx := 0; lx < tp.W; lx++ {708+ li := ly*tp.W + lx709+ sol[li], giv[li] = '.', '.'710+ if !sh.Contains(tp.X0+lx, tp.Y0+ly) {711+ continue712+ }713+ sol[li] = out.Solution[tp.Y0+ly][tp.X0+lx]714+ giv[li] = '0'715+ if given[(tp.Y0+ly)*G+tp.X0+lx] {716+ giv[li] = '1'717+ }718+ }719+ }720+ tp.Hash = TileHash(key, out.Manifest.ID, tp.ID, tp.X0, tp.Y0, tp.W, tp.H, tp.Rects, sol, giv)721+ }722+}
M engine/gen/gen.go +119 -35
@@ -10,6 +10,7 @@ package gen
1010 import (1111 "bytes"1212 "context"13+ "crypto/hmac"1314 "crypto/sha256"1415 "encoding/binary"1516 "encoding/hex"
@@ -61,7 +62,7 @@ func PolicyFor(cells int) Policy {
6162 var Verbose = os.Getenv("MOSAIC_DEBUG") != ""6263 6364 // Version changes whenever the generator's output for a seed could change.64-const Version = "gen-v6" // v6: chunks filled by one joint SAT completion (v5: manifest v3, shapes and overlap regions)65+const Version = "gen-v7" // v7: tile hashes are HMAC commitments (v6: chunks filled by one joint SAT completion)6566 6667 type Params struct {6768 Seed string `json:"seed"`
@@ -140,19 +141,21 @@ type TilePublic struct {
140141 }141142 142143 type Manifest struct {143- Version int `json:"version"`144- ID string `json:"id"`145- SeedCommit string `json:"seed_commit"`146- G int `json:"G"`147- Packer PackerInfo `json:"packer"`148- MByN map[string]int `json:"m_by_n"`149- Rules Rules `json:"rules"`150- SeedTile int `json:"seed_tile"`151- Tiles []TilePublic `json:"tiles"`152- K string `json:"k"`153- Generator string `json:"generator"`154- Library *string `json:"library"`155- Field *FieldPublic `json:"field,omitempty"`144+ Version int `json:"version"`145+ ID string `json:"id"`146+ SeedCommit string `json:"seed_commit"`147+ G int `json:"G"`148+ Packer PackerInfo `json:"packer"`149+ MByN map[string]int `json:"m_by_n"`150+ Rules Rules `json:"rules"`151+ SeedTile int `json:"seed_tile"`152+ Tiles []TilePublic `json:"tiles"`153+ K string `json:"k"`154+ Generator string `json:"generator"`155+ Library *string `json:"library"`156+ Field *FieldPublic `json:"field,omitempty"`157+ HashScheme string `json:"hash_scheme"` // "mosaic-tile-v1"158+ HashKeyCommit string `json:"hash_key_commit"` // sha256 of the day key, released at archive156159 }157160 158161 // TilePrivate is the generator's per-tile record: never in the public log.
@@ -252,6 +255,8 @@ type Output struct {
252255 Stats Stats `json:"stats"`253256 Layout pack.Report `json:"layout"`254257 ChunkStats []Stats `json:"chunk_stats,omitempty"` // fields: per chunk, in chunk order258+ DaySeed string `json:"day_seed,omitempty"` // private until the day is archived; seed_commit = sha256("mosaic-day-seed-v1|" + day_seed)259+ HashKey string `json:"hash_key,omitempty"` // hex day key; private until the day is archived255260 }256261 257262 func defaults(p Params) (Params, error) {
@@ -478,8 +483,8 @@ func generate(ctx context.Context, p Params) (*Output, error) {
478483 }479484 if len(L.Tiles) == 0 {480485 // a chunk region the context already covers: nothing to generate481- commit := sha256.Sum256([]byte(p.Seed + "|" + p.Nonce))482- out := &Output{Manifest: Manifest{Version: 3, ID: hex.EncodeToString(commit[:]), SeedCommit: hex.EncodeToString(commit[:]), G: p.G,486+ commit := DayCommit(p.Seed)487+ out := &Output{Manifest: Manifest{Version: 3, ID: commit, SeedCommit: commit, G: p.G,483488 Packer: PackerInfo{Kind: L.Kind, Params: map[string]any{}}, MByN: map[string]int{}, Rules: Rules{Overlaps: true}, SeedTile: -1, Tiles: []TilePublic{}, K: "unmeasured", Generator: generatorHash()},484489 Tiles: []TilePrivate{}, Layout: L.Report}485490 for y := 0; y < p.G; y++ {
@@ -562,6 +567,8 @@ func (g *tileGeom) gid(m *mosaic, local int) int {
562567 }563568 564569 type mosaic struct {570+ hashKey []byte // keys the tiles' HMAC hashes571+ gameID string // the day's seed commitment, bound into every tile hash565572 p Params566573 L *pack.Layout567574 G int
@@ -591,6 +598,8 @@ func (m *mosaic) addTimer(i int, f func(t *TileTimer)) {
591598 func newMosaic(p Params, L *pack.Layout) *mosaic {592599 m := &mosaic{p: p, L: L, G: L.G, n: len(L.Tiles), bandUniqueHeld: p.BandUnique,593600 seamGivens: map[int]rules.Cell{}, seamOwner: map[int]int{}, used: map[int]bool{}}601+ m.hashKey = DayKey(p.Seed)602+ m.gameID = DayCommit(p.Seed)594603 m.global = rules.NewBoard(m.G, m.G)595604 m.owners = L.Owners()596605 all := L.All()
@@ -1030,25 +1039,29 @@ func generateAttempt(ctx context.Context, p Params, L *pack.Layout, attempt int)
10301039 if attemptHook != nil {10311040 attemptHook(m, tiles)10321041 }1033- commit := sha256.Sum256([]byte(p.Seed + "|" + p.Nonce))1042+ commit := DayCommit(p.Seed)10341043 mByN := map[string]int{}10351044 for n := pack.MinN; n <= pack.MaxN; n += 2 {10361045 mByN[fmt.Sprint(n)] = pack.MByN(n)10371046 }10381047 out := &Output{10391048 Manifest: Manifest{1040- Version: 3,1041- ID: hex.EncodeToString(commit[:]),1042- SeedCommit: hex.EncodeToString(commit[:]),1043- G: m.G,1044- Packer: PackerInfo{Kind: L.Kind, Params: map[string]any{"n": p.N, "coverage": p.Coverage, "compound": p.Compound}},1045- MByN: mByN,1046- Rules: Rules{Overlaps: true, CrossTile: false, SeamClues: len(m.seamGivens) > 0, BandUnique: m.bandUniqueHeld},1047- SeedTile: m.geom[L.SeedTile].pubID,1048- K: "unmeasured",1049- Generator: generatorHash(),1049+ HashScheme: "mosaic-tile-v1",1050+ HashKeyCommit: HashKeyCommit(m.hashKey),1051+ Version: 3,1052+ ID: commit,1053+ SeedCommit: commit,1054+ G: m.G,1055+ Packer: PackerInfo{Kind: L.Kind, Params: map[string]any{"n": p.N, "coverage": p.Coverage, "compound": p.Compound}},1056+ MByN: mByN,1057+ Rules: Rules{Overlaps: true, CrossTile: false, SeamClues: len(m.seamGivens) > 0, BandUnique: m.bandUniqueHeld},1058+ SeedTile: m.geom[L.SeedTile].pubID,1059+ K: "unmeasured",1060+ Generator: generatorHash(),10501061 },1051- Stats: m.stats,1062+ Stats: m.stats,1063+ DaySeed: p.Seed,1064+ HashKey: hex.EncodeToString(m.hashKey),10521065 }10531066 for y := 0; y < m.G; y++ {10541067 row := make([]byte, m.G)
@@ -1558,9 +1571,7 @@ func (m *mosaic) settle(t *tileWork) error {
15581571 if t.private.Blank < PolicyFor(g.cells).BlankMin {15591572 return fmt.Errorf("tile %d: %.0f%% blank after neighbours' seam clues", t.id, t.private.Blank*100)15601573 }1561- // content hash: shape, solution bits, givens mask, shared mask1562- h := sha256.New()1563- h.Write([]byte(fmt.Sprintf("%d,%d,%v|", g.W, g.H, g.sh.Rects)))1574+ // content hash: shape, solution bits, givens mask, shared mask, keyed15641575 sol := make([]byte, len(p))15651576 giv := make([]byte, len(p))15661577 shared := make([]byte, len(p))
@@ -1581,13 +1592,86 @@ func (m *mosaic) settle(t *tileWork) error {
15811592 shared[li] = '0'15821593 }15831594 }1584- h.Write(sol)1585- h.Write(giv)1586- h.Write(shared)1587- t.public.Hash = hex.EncodeToString(h.Sum(nil))1595+ _ = shared1596+ t.public.Hash = TileHash(m.hashKey, m.gameID, t.public.ID, g.sh.X0, g.sh.Y0, g.W, g.H, g.sh.Rects, sol, giv)15881597 return nil15891598 }15901599 1600+// DayCommit is the public commitment to a day's seed: the manifest's id and1601+// seed_commit. The seed itself (random, never derived from the date) is1602+// private until the day is archived.1603+func DayCommit(seed string) string {1604+ h := sha256.Sum256([]byte("mosaic-day-seed-v1|" + seed))1605+ return hex.EncodeToString(h[:])1606+}1607+1608+// DayKey derives the day key that keys every tile commitment:1609+// HMAC-SHA256 with the label as key over the seed (a labelled one-step1610+// KDF; the label domain-separates it from every other stream). Private1611+// until the day is archived; sha256(DayKey) is published at start.1612+func DayKey(seed string) []byte {1613+ h := hmac.New(sha256.New, []byte("mosaic-day-key-v1"))1614+ h.Write([]byte(seed))1615+ return h.Sum(nil)1616+}1617+1618+// HashKeyCommit is what the manifest publishes about the day key.1619+func HashKeyCommit(key []byte) string {1620+ h := sha256.Sum256(key)1621+ return hex.EncodeToString(h[:])1622+}1623+1624+// TileHash is a tile's commitment, scheme "mosaic-tile-v1": HMAC-SHA2561625+// under the day key over a versioned, length-prefixed encoding of the1626+// game id, the tile id, its geometry, its solution bits and its givens1627+// mask (both packed over the shape's cells in raster order). Without the1628+// key a tile's m candidate solutions cannot be told apart by hashing1629+// them; with it (released with the archive) the record is verifiable.1630+// The shared mask is not part of it: in a field, neighbours from later1631+// chunks and rings change which cells are shared after the tile was1632+// hashed. sol and giv are row-major W*H byte strings, '.' outside the1633+// shape; sol holds '0'/'1', giv holds '1' for a given.1634+func TileHash(key []byte, gameID string, id, x0, y0, W, H int, rects [][4]int, sol, giv []byte) string {1635+ var buf []byte1636+ u32 := func(v int) { buf = append(buf, byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) }1637+ str := func(v string) { u32(len(v)); buf = append(buf, v...) }1638+ bits := func(src []byte, one byte) {1639+ var cells []bool1640+ for i := range src {1641+ if src[i] != '.' {1642+ cells = append(cells, src[i] == one)1643+ }1644+ }1645+ u32(len(cells))1646+ packed := make([]byte, (len(cells)+7)/8)1647+ for i, b := range cells {1648+ if b {1649+ packed[i/8] |= 1 << (7 - uint(i%8))1650+ }1651+ }1652+ u32(len(packed))1653+ buf = append(buf, packed...)1654+ }1655+ str("mosaic-tile-v1")1656+ str(gameID)1657+ u32(id)1658+ u32(x0)1659+ u32(y0)1660+ u32(W)1661+ u32(H)1662+ u32(len(rects))1663+ for _, r := range rects {1664+ for _, v := range r {1665+ u32(v)1666+ }1667+ }1668+ bits(sol, '1')1669+ bits(giv, '1')1670+ h := hmac.New(sha256.New, key)1671+ h.Write(buf)1672+ return hex.EncodeToString(h.Sum(nil))1673+}1674+15911675 // tileGrid reads a tile's solution as H rows of W local cells (Empty15921676 // outside the shape).15931677 func (m *mosaic) tileGrid(i int) [][]rules.Cell {
M engine/gen/gen_test.go +103 -0
@@ -2,6 +2,7 @@ package gen
22 33 import (44 "context"5+ "encoding/hex"56 "encoding/json"67 "os"78 "testing"
@@ -264,6 +265,56 @@ func checkAssertions(t *testing.T, o *Output) {
264265 if tp.Hash == "" {265266 t.Fatalf("tile %d: no hash", i)266267 }268+ // the hash is a keyed commitment: recompute it from the JSON with269+ // the private key and it must match; without the key it must not270+ if man.HashScheme != "mosaic-tile-v1" || o.HashKey == "" || o.DaySeed == "" {271+ t.Fatalf("tile %d: hash scheme %q, key present %v, seed present %v", i, man.HashScheme, o.HashKey != "", o.DaySeed != "")272+ }273+ if i == 0 {274+ // the day's commitments verify from the released seed and key275+ if DayCommit(o.DaySeed) != man.SeedCommit || man.ID != man.SeedCommit {276+ t.Fatalf("seed_commit does not verify from the released day seed")277+ }278+ if kb, _ := hex.DecodeString(o.HashKey); HashKeyCommit(kb) != man.HashKeyCommit {279+ t.Fatalf("hash_key_commit does not verify from the released key")280+ }281+ if hex.EncodeToString(DayKey(o.DaySeed)) != o.HashKey {282+ t.Fatalf("the day key is not the labelled derivation of the day seed")283+ }284+ }285+ {286+ key, err := hex.DecodeString(o.HashKey)287+ if err != nil || len(key) != 32 {288+ t.Fatalf("bad hash key")289+ }290+ sol := make([]byte, tp.W*tp.H)291+ giv := make([]byte, tp.W*tp.H)292+ given := map[int]bool{}293+ for _, g := range priv.Givens {294+ given[g[0]] = true295+ }296+ for ly := 0; ly < tp.H; ly++ {297+ for lx := 0; lx < tp.W; lx++ {298+ li := ly*tp.W + lx299+ sol[li], giv[li] = '.', '.'300+ if !sh.Contains(tp.X0+lx, tp.Y0+ly) {301+ continue302+ }303+ gid := (tp.Y0+ly)*G + tp.X0 + lx304+ sol[li] = o.Solution[tp.Y0+ly][tp.X0+lx]305+ giv[li] = '0'306+ if given[gid] {307+ giv[li] = '1'308+ }309+ }310+ }311+ if got := TileHash(key, man.ID, tp.ID, tp.X0, tp.Y0, tp.W, tp.H, tp.Rects, sol, giv); got != tp.Hash {312+ t.Fatalf("tile %d: hash does not verify under the key", i)313+ }314+ if TileHash(nil, man.ID, tp.ID, tp.X0, tp.Y0, tp.W, tp.H, tp.Rects, sol, giv) == tp.Hash {315+ t.Fatalf("tile %d: hash verifies without the key", i)316+ }317+ }267318 sols[i] = s268319 }269320 for id, listed := range seamGiven {
@@ -568,3 +619,55 @@ func TestRepairAtG200(t *testing.T) {
568619 t.Fatalf("took %d attempts", o.Stats.Attempts)569620 }570621 }622+623+// TestHashIsACommitment: an agent that enumerates a revealed tile's m624+// solutions from its givens and hashes each cannot tell the truth by625+// matching the manifest's hash (the attack an agent reported); with the626+// archived key exactly one candidate matches.627+func TestHashIsACommitment(t *testing.T) {628+ o := loadFixture(t, "testdata/mosaic-2-14.json")629+ key, _ := hex.DecodeString(o.HashKey)630+ G := o.Manifest.G631+ for i, tp := range o.Manifest.Tiles {632+ sh := rules.Shape{X0: tp.X0, Y0: tp.Y0, W: tp.W, H: tp.H, Rects: tp.Rects}633+ puzzle := make([]rules.Cell, tp.W*tp.H)634+ giv := make([]byte, tp.W*tp.H)635+ for li := range puzzle {636+ puzzle[li] = rules.Empty637+ giv[li] = '.'638+ lx, ly := li%tp.W, li/tp.W639+ if sh.Contains(tp.X0+lx, tp.Y0+ly) {640+ giv[li] = '0'641+ }642+ }643+ for _, g := range o.Tiles[i].Givens {644+ gx, gy := g[0]%G, g[0]/G645+ li := (gy-tp.Y0)*tp.W + gx - tp.X0646+ puzzle[li] = rules.Cell(g[1])647+ giv[li] = '1'648+ }649+ cands, _ := sat.Enumerate(puzzle, sh, rules.All, tp.M+1)650+ if len(cands) < 2 {651+ t.Fatalf("tile %d: %d candidates", i, len(cands))652+ }653+ plain, keyed := 0, 0654+ for _, c := range cands {655+ sol := make([]byte, tp.W*tp.H)656+ for li := range sol {657+ sol[li] = '.'658+ if giv[li] != '.' {659+ sol[li] = byte('0' + c[li])660+ }661+ }662+ if TileHash(nil, o.Manifest.ID, tp.ID, tp.X0, tp.Y0, tp.W, tp.H, tp.Rects, sol, giv) == tp.Hash {663+ plain++664+ }665+ if TileHash(key, o.Manifest.ID, tp.ID, tp.X0, tp.Y0, tp.W, tp.H, tp.Rects, sol, giv) == tp.Hash {666+ keyed++667+ }668+ }669+ if plain != 0 || keyed != 1 {670+ t.Fatalf("tile %d: %d candidates match unkeyed, %d keyed (want 0 and 1)", i, plain, keyed)671+ }672+ }673+}
M engine/testdata/mosaic-2-14.json +1 -1
@@ -1 +1 @@
1-{"manifest":{"version":3,"id":"3e47144edce6e1c6d8f489a30b2327fd04f0d32e4053e125d6e734cfbdadff75","seed_commit":"3e47144edce6e1c6d8f489a30b2327fd04f0d32e4053e125d6e734cfbdadff75","G":27,"packer":{"kind":"grid","params":{"big_area":0,"compound":0,"coverage":0,"n":14}},"m_by_n":{"10":3,"12":3,"14":3,"16":4,"6":2,"8":2},"rules":{"overlaps":true,"cross_tile":false,"seam_clues":true,"band_unique":false},"seed_tile":0,"tiles":[{"id":0,"x0":0,"y0":0,"w":14,"h":14,"rects":[[0,0,14,14]],"cells":196,"hash":"eac3d2f1ac703f689faf383270dad4bd58607c27518650b7527ce73f7a4e22b8","m":3,"overlaps":[{"with":1,"cells":14},{"with":2,"cells":14},{"with":3,"cells":1}],"pins":[0,1]},{"id":1,"x0":13,"y0":0,"w":14,"h":14,"rects":[[0,0,14,14]],"cells":196,"hash":"67d62e2ece0609f61b0bf26a0899fbc49d10705bd4f7db5082e0b01b0c5b27c7","m":3,"overlaps":[{"with":0,"cells":14},{"with":2,"cells":1},{"with":3,"cells":14}],"pins":[0]},{"id":2,"x0":0,"y0":13,"w":14,"h":14,"rects":[[0,0,14,14]],"cells":196,"hash":"ce70dbf666d311c9697dae8fdc0389a54ea74ae71b09f97b297ae60c88bbd5ad","m":3,"overlaps":[{"with":0,"cells":14},{"with":1,"cells":1},{"with":3,"cells":14}],"pins":[0,2]},{"id":3,"x0":13,"y0":13,"w":14,"h":14,"rects":[[0,0,14,14]],"cells":196,"hash":"99251ff6252a0206bd86779530fca78835d0c8b8fcd0bad6217e25eadc5dcce9","m":3,"overlaps":[{"with":0,"cells":1},{"with":1,"cells":14},{"with":2,"cells":14}],"pins":[1,2]}],"k":"unmeasured","generator":"73797f83aaaef481","library":null},"solution":["001101101010101101010010011","100100110101010110100110010","011011001001101001101001101","010110011010011010011010010","101101100101000101100101011","110010010110101101001100101","010011011001010010011011010","101100100110010100110100101","011001010110101011001101100","100110101001101100110011010","011010010110010011001010101","101001001011011010100100110","100101101001100101011011001","010010110100110010110101100","101010011010010100100110011","001101101001101011001001011","010010010110110110110010100","110101101001001101101100100","101011011001001001011001011","010100100110110110010110100","001001010110111010101010010","110110101001001001010101101","001010110100110110110100100","101101001011000101001011011","110101100101001001100101011","001010010110110110011010100","110101001011001001001011011"],"tiles":[{"id":0,"givens":[[0,0],[1,0],[3,1],[6,1],[8,1],[12,1],[28,0],[33,1],[38,1],[54,0],[60,0],[65,1],[81,0],[82,1],[86,0],[87,0],[92,0],[93,0],[114,1],[116,0],[118,0],[144,1],[162,0],[164,0],[166,1],[168,0],[170,1],[172,0],[175,1],[194,0],[217,1],[226,1],[249,1],[251,1],[252,0],[255,1],[272,1],[277,1],[297,1],[299,1],[302,1],[304,0],[307,1],[308,1],[325,0],[328,0],[331,0],[333,0],[335,1],[336,1]],"blank":0.7448979591836735,"needs_unique":true,"needs_bifurcation":true,"flip":[201,202,228,229],"ladder":{"used_unique":false,"used_bifurcation":true,"bifurcations":3,"complete":false}},{"id":1,"givens":[[14,1],[15,1],[25,1],[26,1],[46,0],[48,1],[50,0],[52,1],[68,1],[70,0],[72,1],[80,1],[95,1],[97,1],[100,1],[104,0],[105,0],[127,0],[132,0],[134,1],[150,1],[152,1],[155,1],[157,0],[159,1],[160,0],[161,1],[175,1],[182,1],[205,0],[211,0],[214,0],[230,1],[239,1],[257,1],[260,0],[263,0],[266,1],[268,1],[285,0],[292,1],[293,0],[313,1],[316,0],[317,0],[321,1],[338,0],[346,1],[349,0]],"blank":0.75,"needs_unique":true,"needs_bifurcation":true,"flip":[94,98,121,125],"ladder":{"used_unique":false,"used_bifurcation":true,"bifurcations":1,"complete":false}},{"id":2,"givens":[[380,1],[383,0],[385,1],[386,1],[387,0],[389,0],[410,1],[417,1],[433,1],[438,0],[459,1],[460,1],[464,1],[468,0],[470,1],[486,1],[493,1],[517,0],[518,0],[522,1],[523,1],[540,0],[548,0],[570,1],[572,0],[573,1],[605,0],[621,1],[623,1],[624,1],[626,1],[631,1],[633,0],[648,1],[650,0],[651,1],[656,0],[679,1],[680,0],[681,0],[683,0],[685,1],[702,1],[703,1],[706,0],[707,1],[712,1],[713,1],[714,0]],"blank":0.75,"needs_unique":true,"needs_bifurcation":true,"flip":[362,363,659,660],"ladder":{"used_unique":true,"used_bifurcation":true,"bifurcations":4,"complete":false}},{"id":3,"givens":[[395,0],[400,1],[402,0],[419,1],[420,0],[421,1],[430,1],[447,1],[453,0],[458,0],[473,1],[474,1],[478,0],[479,1],[485,0],[502,0],[505,1],[511,1],[512,1],[528,1],[532,1],[533,0],[535,1],[538,0],[539,0],[556,1],[562,1],[563,0],[564,0],[565,1],[566,0],[586,1],[587,0],[592,0],[609,1],[613,1],[618,1],[619,0],[636,1],[639,0],[643,1],[647,1],[671,1],[673,1],[691,1],[693,0],[701,0],[716,1],[720,0]],"blank":0.75,"needs_unique":true,"needs_bifurcation":true,"flip":[607,617,634,644],"ladder":{"used_unique":false,"used_bifurcation":true,"bifurcations":1,"complete":false}}],"stats":{"ms":1064,"attempts":1,"refills":0,"backjumps":0,"regivens":0,"releases":0,"waves":4,"wave_retries":0,"redraws":0,"fill_retries":0,"fill_ms":29,"givens_ms":995,"settle_ms":39,"wasted_ms":0,"wasted":0,"sat_calls":2917,"sat_ms":554,"ladder_ms":129,"remove_ms":8504,"tile_timers":[{"id":0,"cells":196,"fill_ms":0,"givens_ms":245,"rounds":1,"trials":10,"sat_calls":547,"ladder_ms":37,"remove_ms":2130},{"id":1,"cells":196,"fill_ms":0,"givens_ms":238,"rounds":1,"trials":10,"sat_calls":953,"ladder_ms":40,"remove_ms":2034},{"id":2,"cells":196,"fill_ms":0,"givens_ms":257,"rounds":1,"trials":10,"sat_calls":649,"ladder_ms":33,"remove_ms":2221},{"id":3,"cells":196,"fill_ms":0,"givens_ms":253,"rounds":1,"trials":10,"sat_calls":658,"ladder_ms":19,"remove_ms":2119}]},"layout":{"tiles":4,"coverage":1,"holes":0,"hole_cells":0,"mean_hole":0,"by_cells":{"129-256":4},"overlaps":6,"mean_overlap":9.666666666666666,"compound":0}}1+{"manifest":{"version":3,"id":"d76be464b4f4ef3e4564549e371262aac7fa885361e98fae4f0d6f3da11cd7a0","seed_commit":"d76be464b4f4ef3e4564549e371262aac7fa885361e98fae4f0d6f3da11cd7a0","G":27,"packer":{"kind":"grid","params":{"compound":0,"coverage":0,"n":14}},"m_by_n":{"10":3,"12":3,"14":3,"16":4,"6":2,"8":2},"rules":{"overlaps":true,"cross_tile":false,"seam_clues":true,"band_unique":false},"seed_tile":0,"tiles":[{"id":0,"x0":0,"y0":0,"w":14,"h":14,"rects":[[0,0,14,14]],"cells":196,"hash":"7036dd9b46d6d120e60f5ffd2b1fd3d2d4febeb1de9090f50913032df001b413","m":3,"overlaps":[{"with":1,"cells":14},{"with":2,"cells":14},{"with":3,"cells":1}],"pins":[0,1]},{"id":1,"x0":13,"y0":0,"w":14,"h":14,"rects":[[0,0,14,14]],"cells":196,"hash":"edff59771ad88ca8fbfdf44c36b4d75f07aae7c0e48dbac285809696dd0b631f","m":3,"overlaps":[{"with":0,"cells":14},{"with":2,"cells":1},{"with":3,"cells":14}],"pins":[0]},{"id":2,"x0":0,"y0":13,"w":14,"h":14,"rects":[[0,0,14,14]],"cells":196,"hash":"530ab2d1cc40f02405b294721ef38fc61d0881cc4263e30257e0dd382960aec5","m":3,"overlaps":[{"with":0,"cells":14},{"with":1,"cells":1},{"with":3,"cells":14}],"pins":[0,2]},{"id":3,"x0":13,"y0":13,"w":14,"h":14,"rects":[[0,0,14,14]],"cells":196,"hash":"b5bfb483f17a851ee7fdfca9ec60524992ea66d7ec3c756aa803a7cff6ea9f73","m":3,"overlaps":[{"with":0,"cells":1},{"with":1,"cells":14},{"with":2,"cells":14}],"pins":[1,2]}],"k":"unmeasured","generator":"1f06f1a2d5e8df1e","library":null,"hash_scheme":"mosaic-tile-v1","hash_key_commit":"0e699403d0690c8bad67c48d5275e94a95b46aa50cef1e45aab586b72fa7be53"},"solution":["001101101010101101010010011","100100110101010110100110010","011011001001101001101001101","010110011010011010011010010","101101100101000101100101011","110010010110101101001100101","010011011001010010011011010","101100100110010100110100101","011001010110101011001101100","100110101001101100110011010","011010010110010011001010101","101001001011011010100100110","100101101001100101011011001","010010110100110010110101100","101010011010010100100110011","001101101001101011001001011","010010010110110110110010100","110101101001001101101100100","101011011001001001011001011","010100100110110110010110100","001001010110111010101010010","110110101001001001010101101","001010110100110110110100100","101101001011000101001011011","110101100101001001100101011","001010010110110110011010100","110101001011001001001011011"],"tiles":[{"id":0,"givens":[[0,0],[1,0],[3,1],[6,1],[8,1],[12,1],[28,0],[33,1],[38,1],[54,0],[60,0],[65,1],[81,0],[82,1],[86,0],[87,0],[92,0],[93,0],[114,1],[116,0],[118,0],[144,1],[162,0],[164,0],[166,1],[168,0],[170,1],[172,0],[175,1],[194,0],[217,1],[226,1],[249,1],[251,1],[252,0],[255,1],[272,1],[277,1],[297,1],[299,1],[302,1],[304,0],[307,1],[308,1],[325,0],[328,0],[331,0],[333,0],[335,1],[336,1]],"blank":0.7448979591836735,"needs_unique":true,"needs_bifurcation":true,"flip":[201,202,228,229],"ladder":{"used_unique":false,"used_bifurcation":true,"bifurcations":3,"complete":false}},{"id":1,"givens":[[14,1],[15,1],[25,1],[26,1],[46,0],[48,1],[50,0],[52,1],[68,1],[70,0],[72,1],[80,1],[95,1],[97,1],[100,1],[104,0],[105,0],[127,0],[132,0],[134,1],[150,1],[152,1],[155,1],[157,0],[159,1],[160,0],[161,1],[175,1],[182,1],[205,0],[211,0],[214,0],[230,1],[239,1],[257,1],[260,0],[263,0],[266,1],[268,1],[285,0],[292,1],[293,0],[313,1],[316,0],[317,0],[321,1],[338,0],[346,1],[349,0]],"blank":0.75,"needs_unique":true,"needs_bifurcation":true,"flip":[94,98,121,125],"ladder":{"used_unique":false,"used_bifurcation":true,"bifurcations":1,"complete":false}},{"id":2,"givens":[[380,1],[383,0],[385,1],[386,1],[387,0],[389,0],[410,1],[417,1],[433,1],[438,0],[459,1],[460,1],[464,1],[468,0],[470,1],[486,1],[493,1],[517,0],[518,0],[522,1],[523,1],[540,0],[548,0],[570,1],[572,0],[573,1],[605,0],[621,1],[623,1],[624,1],[626,1],[631,1],[633,0],[648,1],[650,0],[651,1],[656,0],[679,1],[680,0],[681,0],[683,0],[685,1],[702,1],[703,1],[706,0],[707,1],[712,1],[713,1],[714,0]],"blank":0.75,"needs_unique":true,"needs_bifurcation":true,"flip":[362,363,659,660],"ladder":{"used_unique":true,"used_bifurcation":true,"bifurcations":4,"complete":false}},{"id":3,"givens":[[395,0],[400,1],[402,0],[419,1],[420,0],[421,1],[430,1],[447,1],[453,0],[458,0],[473,1],[474,1],[478,0],[479,1],[485,0],[502,0],[505,1],[511,1],[512,1],[528,1],[532,1],[533,0],[535,1],[538,0],[539,0],[556,1],[562,1],[563,0],[564,0],[565,1],[566,0],[586,1],[587,0],[592,0],[609,1],[613,1],[618,1],[619,0],[636,1],[639,0],[643,1],[647,1],[671,1],[673,1],[691,1],[693,0],[701,0],[716,1],[720,0]],"blank":0.75,"needs_unique":true,"needs_bifurcation":true,"flip":[607,617,634,644],"ladder":{"used_unique":false,"used_bifurcation":true,"bifurcations":1,"complete":false}}],"stats":{"ms":1047,"attempts":1,"refills":0,"backjumps":0,"regivens":0,"releases":0,"waves":4,"wave_retries":0,"redraws":0,"fill_retries":0,"fill_ms":30,"givens_ms":978,"settle_ms":39,"wasted_ms":0,"wasted":0,"sat_calls":2917,"sat_ms":547,"ladder_ms":137,"remove_ms":8387,"tile_timers":[{"id":0,"cells":196,"fill_ms":0,"givens_ms":239,"rounds":1,"trials":10,"sat_calls":547,"ladder_ms":37,"remove_ms":2090},{"id":1,"cells":196,"fill_ms":0,"givens_ms":234,"rounds":1,"trials":10,"sat_calls":953,"ladder_ms":42,"remove_ms":1998},{"id":2,"cells":196,"fill_ms":0,"givens_ms":255,"rounds":1,"trials":10,"sat_calls":649,"ladder_ms":37,"remove_ms":2198},{"id":3,"cells":196,"fill_ms":0,"givens_ms":248,"rounds":1,"trials":10,"sat_calls":658,"ladder_ms":21,"remove_ms":2101}]},"layout":{"tiles":4,"coverage":1,"holes":0,"hole_cells":0,"mean_hole":0,"by_cells":{"129-256":4},"overlaps":6,"mean_overlap":9.666666666666666,"compound":0},"day_seed":"a","hash_key":"9a803d54a4955960281a230815ca074d8fc223277b1adf0c370a886ff1a9ee06"}