From 0dcfbebcb05832ac2298fda55ccea1ac2f7d9809 Mon Sep 17 00:00:00 2001 From: mike Date: Tue, 20 Jan 2026 03:43:02 +0100 Subject: [PATCH] introduce bitloops --- src/main/java/puzzle/Masker.java | 178 ++++++++------ src/main/java/puzzle/SwedishGenerator.java | 2 +- src/test/java/puzzle/MaskerCluesTest.java | 263 ++++++++++++--------- 3 files changed, 263 insertions(+), 180 deletions(-) diff --git a/src/main/java/puzzle/Masker.java b/src/main/java/puzzle/Masker.java index b39b45c..2103544 100644 --- a/src/main/java/puzzle/Masker.java +++ b/src/main/java/puzzle/Masker.java @@ -30,39 +30,45 @@ public final class Masker { public boolean isValid(Clues c, int minLen) { return findOffendingClue(c, minLen) == -1; } - + public int findOffendingClue(Clues grid, int minLen) { if (((grid.xlo & grid.rlo) & grid.lo) != X) return numberOfTrailingZeros((grid.xlo & grid.rlo) & grid.lo); if (((grid.xhi & grid.rhi) & grid.hi) != X) return 64 | numberOfTrailingZeros((grid.xhi & grid.rhi) & grid.hi); - + int num = 0; for (long bits = grid.lo; bits != X; bits &= bits - 1) activeCIdx[num++] = numberOfTrailingZeros(bits); for (long bits = grid.hi; bits != X; bits &= bits - 1) activeCIdx[num++] = 64 | numberOfTrailingZeros(bits); - + if (num == 0) return -1; - + int start = rng.randint0_SIZE() % num; - int n = 0; + int n = 0; for (int i = 0; i < num; i++) { - int idx = activeCIdx[(start + i) % num]; - int dir = grid.getDir(idx); - int key = Slot.packSlotKey(idx, dir); + int idx = activeCIdx[(start + i) % num]; + int dir = grid.getDir(idx); + int key = Slot.packSlotKey(idx, dir); long sLo = PATH_LO[key], sHi = PATH_HI[key]; long hLo = sLo & grid.lo, hHi = sHi & grid.hi; if (Slotinfo.increasing(key)) { - if (hLo != X) { sLo &= (1L << numberOfTrailingZeros(hLo)) - 1; sHi = 0; } - else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; } + if (hLo != X) { + sLo &= (1L << numberOfTrailingZeros(hLo)) - 1; + sHi = 0; + } else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; } } else { - if (hHi != X) { sHi &= -(1L << (63 - numberOfLeadingZeros(hHi)) << 1); sLo = 0; } - else if (hLo != X) { sLo &= -(1L << (63 - numberOfLeadingZeros(hLo)) << 1); } + if (hHi != X) { + sHi &= -(1L << (63 - numberOfLeadingZeros(hHi)) << 1); + sLo = 0; + } else if (hLo != X) { sLo &= -(1L << (63 - numberOfLeadingZeros(hLo)) << 1); } } if (bitCount(sLo) + bitCount(sHi) < minLen) return idx; for (int j = 0; j < n; j++) if (bitCount(sLo & activeSLo[j]) + bitCount(sHi & activeSHi[j]) > 1) return idx; - activeSLo[n] = sLo; activeSHi[n] = sHi; n++; + activeSLo[n] = sLo; + activeSHi[n] = sHi; + n++; } return -1; } - + public void cleanup(Clues c, int minLen) { int guard = 0; while (guard++ < 50) { @@ -255,12 +261,12 @@ public final class Masker { int msb = 63 - numberOfLeadingZeros(hLo); rLo &= -(1L << msb << 1); } - + activeCIdx[numClues] = 64 | clueIdx; activeSLo[numClues] = rLo; activeSHi[numClues] = rHi; numClues++; - + if ((rLo | rHi) != X) { hasSlots = true; if (Slot.horiz(key)) { @@ -281,7 +287,7 @@ public final class Masker { } if (!hasSlots) return 1_000_000_000L; - + int[] rCount = new int[8]; int[] cCount = new int[9]; for (int i = 0; i < numClues; i++) { @@ -315,16 +321,15 @@ public final class Masker { } if (numClues > 0) { - int maxReached = 0; + int maxReached = 0; long totalReachedLo = 0, totalReachedHi = 0; for (int i = 0; i < numClues; i++) { - if (i < 64) { if ((totalReachedLo & (1L << i)) != 0) continue; } - else { if ((totalReachedHi & (1L << (i - 64))) != 0) continue; } - + if (i < 64) { if ((totalReachedLo & (1L << i)) != 0) continue; } else { if ((totalReachedHi & (1L << (i - 64))) != 0) continue; } + long currentReachedLo = (i < 64) ? (1L << i) : 0; long currentReachedHi = (i >= 64) ? (1L << (i - 64)) : 0; stack[0] = i; - int sp = 1; + int sp = 1; int count = 0; while (sp > 0) { int cur = stack[--sp]; @@ -334,16 +339,16 @@ public final class Masker { while (nLo != 0) { long lsb = nLo & -nLo; int idx = numberOfTrailingZeros(lsb); - currentReachedLo |= lsb; + currentReachedLo |= lsb; stack[sp++] = idx; - nLo &= ~lsb; + nLo &= ~lsb; } while (nHi != 0) { long lsb = nHi & -nHi; int idx = 64 | numberOfTrailingZeros(lsb); - currentReachedHi |= lsb; + currentReachedHi |= lsb; stack[sp++] = idx; - nHi &= ~lsb; + nHi &= ~lsb; } } if (count > maxReached) maxReached = count; @@ -355,7 +360,7 @@ public final class Masker { penalty += 20000; } } - + for (long bits = ~lo_cl & MASK_LO; bits != X; bits &= bits - 1) { int clueIdx = numberOfTrailingZeros(bits); var rci = IT[clueIdx]; @@ -377,19 +382,19 @@ public final class Masker { else if (h && v) { /* ok */ } else if (((h ? cHHi2 : cVHi2) & (1L << clueIdx)) != X) penalty += 1000; else penalty += 1000; } - - long nclLo = ~lo_cl & MASK_LO; - long nclHi = ~hi_cl & MASK_HI; + + long nclLo = ~lo_cl & MASK_LO; + long nclHi = ~hi_cl & MASK_HI; long hNbrLo = (nclLo >> 8) | (nclLo << 8) | (nclHi << 56); long hNbrHi = (nclHi >> 8) | (nclLo >> 56); long vNbrLo = ((nclLo & ~0x0101010101010101L) >> 1) | ((nclLo & ~0x8080808080808080L) << 1); long vNbrHi = ((nclHi & ~0x01L) >> 1) | ((nclHi & ~0x80L) << 1); - - penalty += bitCount(nclLo & ~cHLo & hNbrLo) * 800; - penalty += bitCount(nclLo & ~cVLo & vNbrLo) * 800; - penalty += bitCount(nclHi & ~cHHi & hNbrHi) * 800; - penalty += bitCount(nclHi & ~cVHi & vNbrHi) * 800; - + + //penalty += bitCount(nclLo & ~cHLo & hNbrLo) * 800; + //penalty += bitCount(nclLo & ~cVLo & vNbrLo) * 800; + //penalty += bitCount(nclHi & ~cHHi & hNbrHi) * 800; + //penalty += bitCount(nclHi & ~cVHi & vNbrHi) * 800; + return penalty; } @@ -424,7 +429,7 @@ public final class Masker { public Clues mutate(Clues c) { var bytes = MUTATE_RI[rng.randint0_SIZE()]; - for (int k = 0, ri; k < 4; k++) { + for (int k = 0, ri; k < 6; k++) { ri = bytes[rng.randint0_624()]; if (c.notClue(ri)) { // ADD byte d = rng.randomClueDir(); @@ -433,17 +438,31 @@ public final class Masker { if (isLo(ri)) { c.setClueLo(1L << ri, d); if (!isValid(c, MIN_LEN)) c.clearClueLo(~(1L << ri)); + else continue; } else { c.setClueHi(1L << (ri & 63), d); if (!isValid(c, MIN_LEN)) c.clearClueHi(~(1L << (ri & 63))); + else continue; } } } else { // HAS CLUE var op = rng.randomClueDir(); if (op < 2) { // REMOVE - if (isLo(ri)) c.clearClueLo(~(1L << ri)); - else c.clearClueHi(~(1L << (ri & 63))); - } else if (op < 5) { // CHANGE DIRECTION + byte oldD = c.getDir(ri); + if (isLo(ri)) { + c.clearClueLo(~(1L << ri)); + if (!isValid(c, MIN_LEN)) c.setClueLo(1L << ri, oldD); + else continue; + } else { + c.clearClueHi(~(1L << (ri & 63))); + if (!isValid(c, MIN_LEN)) c.setClueHi(1L << (ri & 63), oldD); + else continue; + } + + /* if (isLo(ri)) c.clearClueLo(~(1L << ri)); + else c.clearClueHi(~(1L << (ri & 63)));*/ + } + if (op < 4) { // CHANGE DIRECTION byte d = rng.randomClueDir(); int key = Slot.packSlotKey(ri, d); if (c.hasRoomForClue(key)) { @@ -451,28 +470,29 @@ public final class Masker { if (isLo(ri)) { c.setClueLo(1L << ri, d); if (!isValid(c, MIN_LEN)) c.setClueLo(1L << ri, oldD); + else continue; } else { c.setClueHi(1L << (ri & 63), d); if (!isValid(c, MIN_LEN)) c.setClueHi(1L << (ri & 63), oldD); + else continue; } } - } else { // MOVE - int nri = bytes[rng.randint0_624()]; - if (c.notClue(nri)) { - byte d = c.getDir(ri); - int nkey = Slot.packSlotKey(nri, d); - if (c.hasRoomForClue(nkey)) { - if (isLo(ri)) c.clearClueLo(~(1L << ri)); - else c.clearClueHi(~(1L << (ri & 63))); - if (isLo(nri)) c.setClueLo(1L << nri, d); - else c.setClueHi(1L << (nri & 63), d); - if (!isValid(c, MIN_LEN)) { - if (isLo(nri)) c.clearClueLo(~(1L << nri)); - else c.clearClueHi(~(1L << (nri & 63))); - if (isLo(ri)) c.setClueLo(1L << ri, d); - else c.setClueHi(1L << (ri & 63), d); - } - } + } // MOVE + int nri = bytes[rng.randint0_624()]; + if (c.notClue(nri)) { + byte d = c.getDir(ri); + int nkey = Slot.packSlotKey(nri, d); + if (c.hasRoomForClue(nkey)) { + if (isLo(ri)) c.clearClueLo(~(1L << ri)); + else c.clearClueHi(~(1L << (ri & 63))); + if (isLo(nri)) c.setClueLo(1L << nri, d); + else c.setClueHi(1L << (nri & 63), d); + if (!isValid(c, MIN_LEN)) { + if (isLo(nri)) c.clearClueLo(~(1L << nri)); + else c.clearClueHi(~(1L << (nri & 63))); + if (isLo(ri)) c.setClueLo(1L << ri, d); + else c.setClueHi(1L << (ri & 63), d); + } else continue; } } } @@ -669,38 +689,50 @@ public final class Masker { if (((xhi & rhi) & hi) != X) return 64 | numberOfTrailingZeros((xhi & rhi) & hi); int n = 0; for (long bits = lo; bits != X; bits &= bits - 1) { - int idx = numberOfTrailingZeros(bits); - int dir = getDir(idx); - int key = Slot.packSlotKey(idx, dir); + int idx = numberOfTrailingZeros(bits); + int dir = getDir(idx); + int key = Slot.packSlotKey(idx, dir); long sLo = PATH_LO[key], sHi = PATH_HI[key]; long hLo = sLo & lo, hHi = sHi & hi; if (Slotinfo.increasing(key)) { - if (hLo != X) { sLo &= (1L << numberOfTrailingZeros(hLo)) - 1; sHi = 0; } - else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; } + if (hLo != X) { + sLo &= (1L << numberOfTrailingZeros(hLo)) - 1; + sHi = 0; + } else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; } } else { - if (hHi != X) { sHi &= -(1L << (63 - numberOfLeadingZeros(hHi)) << 1); sLo = 0; } - else if (hLo != X) { sLo &= -(1L << (63 - numberOfLeadingZeros(hLo)) << 1); } + if (hHi != X) { + sHi &= -(1L << (63 - numberOfLeadingZeros(hHi)) << 1); + sLo = 0; + } else if (hLo != X) { sLo &= -(1L << (63 - numberOfLeadingZeros(hLo)) << 1); } } if (bitCount(sLo) + bitCount(sHi) < minLen) return idx; for (int i = 0; i < n; i++) if (bitCount(sLo & slo[i]) + bitCount(sHi & shi[i]) > 1) return idx; - slo[n] = sLo; shi[n] = sHi; n++; + slo[n] = sLo; + shi[n] = sHi; + n++; } for (long bits = hi; bits != X; bits &= bits - 1) { - int idx = 64 | numberOfTrailingZeros(bits); - int dir = getDir(idx); - int key = Slot.packSlotKey(idx, dir); + int idx = 64 | numberOfTrailingZeros(bits); + int dir = getDir(idx); + int key = Slot.packSlotKey(idx, dir); long sLo = PATH_LO[key], sHi = PATH_HI[key]; long hLo = sLo & lo, hHi = sHi & hi; if (Slotinfo.increasing(key)) { - if (hLo != X) { sLo &= (1L << numberOfTrailingZeros(hLo)) - 1; sHi = 0; } - else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; } + if (hLo != X) { + sLo &= (1L << numberOfTrailingZeros(hLo)) - 1; + sHi = 0; + } else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; } } else { - if (hHi != X) { sHi &= -(1L << (63 - numberOfLeadingZeros(hHi)) << 1); sLo = 0; } - else if (hLo != X) { sLo &= -(1L << (63 - numberOfLeadingZeros(hLo)) << 1); } + if (hHi != X) { + sHi &= -(1L << (63 - numberOfLeadingZeros(hHi)) << 1); + sLo = 0; + } else if (hLo != X) { sLo &= -(1L << (63 - numberOfLeadingZeros(hLo)) << 1); } } if (bitCount(sLo) + bitCount(sHi) < minLen) return idx; for (int i = 0; i < n; i++) if (bitCount(sLo & slo[i]) + bitCount(sHi & shi[i]) > 1) return idx; - slo[n] = sLo; shi[n] = sHi; n++; + slo[n] = sLo; + shi[n] = sHi; + n++; } return -1; } diff --git a/src/main/java/puzzle/SwedishGenerator.java b/src/main/java/puzzle/SwedishGenerator.java index 7e9ecc2..fc1fbbe 100644 --- a/src/main/java/puzzle/SwedishGenerator.java +++ b/src/main/java/puzzle/SwedishGenerator.java @@ -40,7 +40,7 @@ public record SwedishGenerator() { public static final int MAX_WORD_LENGTH = Config.PUZZLE_ROWS; public static final int MAX_WORD_LENGTH_PLUS_ONE = MAX_WORD_LENGTH + 1; public static final int MIN_LEN = 2;//Neighbors9x8.MIN_LEN;//Config.MIN_LEN; - public static final int MAX_TRIES_PER_SLOT = 1200;//Config.MAX_TRIES_PER_SLOT; + public static final int MAX_TRIES_PER_SLOT = 700;//Config.MAX_TRIES_PER_SLOT; public static final int STACK_SIZE = 128; public static final long RANGE_0_SIZE = Neighbors9x8.RANGE_0_SIZE;// (long) SIZE_MIN_1 - 0L + 1L public static final long RANGE_0_624 = Neighbors9x8.RANGE_0_624;//624L - 0L + 1L; diff --git a/src/test/java/puzzle/MaskerCluesTest.java b/src/test/java/puzzle/MaskerCluesTest.java index 6c3a0f3..26a7978 100644 --- a/src/test/java/puzzle/MaskerCluesTest.java +++ b/src/test/java/puzzle/MaskerCluesTest.java @@ -1,117 +1,168 @@ package puzzle; import module java.base; +import lombok.val; import org.junit.jupiter.api.Test; +import puzzle.Export.Clued; import static org.junit.jupiter.api.Assertions.*; import static puzzle.Masker.Clues; import static puzzle.SwedishGenerator.*; import static puzzle.Masker.Slot; public class MaskerCluesTest { - - @Test - void testSimilarity() { - Clues a = Clues.createEmpty(); - a.setClueLo(1L << 0, (byte) 1); - a.setClueLo(1L << 10, (byte) 0); - - Clues b = Clues.createEmpty(); - b.setClueLo(1L << 0, (byte) 1); - b.setClueLo(1L << 10, (byte) 0); - - // Identity - assertEquals(1.0, a.similarity(b), 0.001); - - // Different direction - Clues c = Clues.createEmpty(); - c.setClueLo(1L << 0, (byte) 0); - c.setClueLo(1L << 10, (byte) 0); - assertTrue(a.similarity(c) < 1.0); - - // Completely different - Clues d = Clues.createEmpty(); - // Matching empty cells count towards similarity. - // a has 2 clues, d has 0. They match on 70 empty cells. - assertEquals(70.0 / 72.0, a.similarity(d), 0.001); - } - - @Test - void testIsValid() { - Clues g = Clues.createEmpty(); - assertTrue(g.isValid(MIN_LEN)); - - // Valid clue: Right from (0,0) in 9x8 grid. Length is 8. - g.setClueLo(1L << Masker.offset(0, 0), (byte) 1); - assertTrue(g.isValid(MIN_LEN)); - - // Invalid clue: Right from (0,7) in 9x8 grid. Length is 1 (too short if MIN_LEN >= 2). - Clues g2 = Clues.createEmpty(); - g2.setClueLo(1L << Masker.offset(0, 7), (byte) 1); - assertFalse(g2.isValid(MIN_LEN)); - } - - @Test - void testHasRoomForClue() { - Clues g = Clues.createEmpty(); - - // Room for Right clue at (0,0) (length 8) - assertTrue(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 0), 1))); - - // No room for Right clue at (0,8) (length 0 < MIN_LEN) - assertFalse(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 8), 1))); - - // Blocked room - // Let's place a clue that leaves only 1 cell for another clue. - g.setClueLo(1L << Masker.offset(0, 2), (byte) 1); - // Now Right at (0,0) only has (0,1) available -> length 1 < MIN_LEN (which is 2) - assertFalse(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 0), 1))); - - // But enough room - g.clearClueLo(0L); - g.setClueLo(1L << Masker.offset(0, 3), (byte) 1); - // Now Right at (0,0) has (0,1), (0,2) -> length 2 == MIN_LEN - assertTrue(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 0), 1))); - } - - @Test - void testIntersectionConstraint() { - Clues g = Clues.createEmpty(); - - // Clue 1: (0,0) Right. Slot cells: (0,1), (0,2), (0,3), (0,4), (0,5), (0,6), (0,7), (0,8) - g.setClueLo(1L << Masker.offset(0, 0), (byte) 1); - - // Clue 2: (1,2) Up. Slot cells: (0,2) - // Intersection is exactly 1 cell (0,2). Valid. - g.setClueLo(1L << Masker.offset(2, 2), (byte) 2); - assertTrue(g.isValid(MIN_LEN)); - - // Clue 3: (1,1) Right. Slot cells: (1,2), (1,3), ... - // No intersection with Clue 1 or 2. Valid. - g.setClueLo(1L << Masker.offset(1, 1), (byte) 1); - assertTrue(g.isValid(MIN_LEN)); - - // Now create a violation: two slots sharing 2 cells. - // We can do this with Corner Down and another clue. - // Clue A: (0,0) Corner Down. Starts at (0,1) goes down: (0,1), (1,1), (2,1), (3,1), ... - // Clue B: (0,2) Corner Down Left. Starts at (0,1) goes down: (0,1), (1,1), (2,1), ... - // They share MANY cells starting from (0,1). - - Clues g3 = Clues.createEmpty(); - g3.setClueLo(1L << Masker.offset(0, 0), (byte) 4); // Corner Down - g3.setClueLo(1L << Masker.offset(0, 2), (byte) 5); // Corner Down Left - assertFalse(g3.isValid(MIN_LEN)); - } - - @Test - void testInvalidDirectionBits() { - Clues g = Clues.createEmpty(); - // Dir 6 (x=1, r=1, v=0) is invalid - g.setClueLo(1L << 0, (byte) 6); - assertFalse(g.isValid(MIN_LEN)); - - // Dir 7 (x=1, r=1, v=1) is invalid - Clues g2 = Clues.createEmpty(); - g2.setClueLo(1L << 0, (byte) 7); - assertFalse(g2.isValid(MIN_LEN)); - } + + @Test + void testValidRandomMask() { + Rng rng = new Rng(42); + Masker masker = new Masker(rng, new int[STACK_SIZE], Clues.createEmpty()); + for (int i = 0; i < 200; i++) { + for (int j = 19; j < 24; j++) { + var clues = masker.randomMask(j); + assertTrue(clues.isValid(MIN_LEN), "Mask should be valid for length \n" + new Clued(clues).gridToString()); + } + } + } + @Test + void testValidMutate() { + Rng rng = new Rng(42); + var cache = Clues.createEmpty(); + Masker masker = new Masker(rng, new int[STACK_SIZE], cache); + double sim = 0.0; + double simCount = 0.0; + for (int i = 0; i < 200; i++) { + for (int j = 19; j < 24; j++) { + var clues = masker.randomMask(j); + val orig = cache.from(clues); + simCount++; + masker.mutate(clues); + sim += orig.similarity(clues); + assertTrue(clues.isValid(MIN_LEN), "Mask should be valid for length \n" + new Clued(clues).gridToString()); + } + } + System.out.println("Average similarity: " + sim / simCount); + } + @Test + void testCross() { + Rng rng = new Rng(42); + var cache = Clues.createEmpty(); + Masker masker = new Masker(rng, new int[STACK_SIZE], cache); + double sim = 0.0; + double simCount = 0.0; + for (int i = 0; i < 200; i++) { + for (int j = 19; j < 24; j++) { + var clues = masker.randomMask(j); + var clues2 = masker.randomMask(j); + simCount++; + var cross = masker.crossover(clues, clues2); + sim += Math.max(cross.similarity(clues), cross.similarity(clues2)); + assertTrue(cross.isValid(MIN_LEN), "Mask should be valid for length \n" + new Clued(cross).gridToString()); + } + } + System.out.println("Average similarity: " + sim / simCount); + } + @Test + void testSimilarity() { + Clues a = Clues.createEmpty(); + a.setClueLo(1L << 0, (byte) 1); + a.setClueLo(1L << 10, (byte) 0); + + Clues b = Clues.createEmpty(); + b.setClueLo(1L << 0, (byte) 1); + b.setClueLo(1L << 10, (byte) 0); + + // Identity + assertEquals(1.0, a.similarity(b), 0.001); + + // Different direction + Clues c = Clues.createEmpty(); + c.setClueLo(1L << 0, (byte) 0); + c.setClueLo(1L << 10, (byte) 0); + assertTrue(a.similarity(c) < 1.0); + + // Completely different + Clues d = Clues.createEmpty(); + // Matching empty cells count towards similarity. + // a has 2 clues, d has 0. They match on 70 empty cells. + assertEquals(70.0 / 72.0, a.similarity(d), 0.001); + } + + @Test + void testIsValid() { + Clues g = Clues.createEmpty(); + assertTrue(g.isValid(MIN_LEN)); + + // Valid clue: Right from (0,0) in 9x8 grid. Length is 8. + g.setClueLo(1L << Masker.offset(0, 0), (byte) 1); + assertTrue(g.isValid(MIN_LEN)); + + // Invalid clue: Right from (0,7) in 9x8 grid. Length is 1 (too short if MIN_LEN >= 2). + Clues g2 = Clues.createEmpty(); + g2.setClueLo(1L << Masker.offset(0, 7), (byte) 1); + assertFalse(g2.isValid(MIN_LEN)); + } + + @Test + void testHasRoomForClue() { + Clues g = Clues.createEmpty(); + + // Room for Right clue at (0,0) (length 8) + assertTrue(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 0), 1))); + + // No room for Right clue at (0,8) (length 0 < MIN_LEN) + assertFalse(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 8), 1))); + + // Blocked room + // Let's place a clue that leaves only 1 cell for another clue. + g.setClueLo(1L << Masker.offset(0, 2), (byte) 1); + // Now Right at (0,0) only has (0,1) available -> length 1 < MIN_LEN (which is 2) + assertFalse(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 0), 1))); + + // But enough room + g.clearClueLo(0L); + g.setClueLo(1L << Masker.offset(0, 3), (byte) 1); + // Now Right at (0,0) has (0,1), (0,2) -> length 2 == MIN_LEN + assertTrue(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 0), 1))); + } + + @Test + void testIntersectionConstraint() { + Clues g = Clues.createEmpty(); + + // Clue 1: (0,0) Right. Slot cells: (0,1), (0,2), (0,3), (0,4), (0,5), (0,6), (0,7), (0,8) + g.setClueLo(1L << Masker.offset(0, 0), (byte) 1); + + // Clue 2: (1,2) Up. Slot cells: (0,2) + // Intersection is exactly 1 cell (0,2). Valid. + g.setClueLo(1L << Masker.offset(2, 2), (byte) 2); + assertTrue(g.isValid(MIN_LEN)); + + // Clue 3: (1,1) Right. Slot cells: (1,2), (1,3), ... + // No intersection with Clue 1 or 2. Valid. + g.setClueLo(1L << Masker.offset(1, 1), (byte) 1); + assertTrue(g.isValid(MIN_LEN)); + + // Now create a violation: two slots sharing 2 cells. + // We can do this with Corner Down and another clue. + // Clue A: (0,0) Corner Down. Starts at (0,1) goes down: (0,1), (1,1), (2,1), (3,1), ... + // Clue B: (0,2) Corner Down Left. Starts at (0,1) goes down: (0,1), (1,1), (2,1), ... + // They share MANY cells starting from (0,1). + + Clues g3 = Clues.createEmpty(); + g3.setClueLo(1L << Masker.offset(0, 0), (byte) 4); // Corner Down + g3.setClueLo(1L << Masker.offset(0, 2), (byte) 5); // Corner Down Left + assertFalse(g3.isValid(MIN_LEN)); + } + + @Test + void testInvalidDirectionBits() { + Clues g = Clues.createEmpty(); + // Dir 6 (x=1, r=1, v=0) is invalid + g.setClueLo(1L << 0, (byte) 6); + assertFalse(g.isValid(MIN_LEN)); + + // Dir 7 (x=1, r=1, v=1) is invalid + Clues g2 = Clues.createEmpty(); + g2.setClueLo(1L << 0, (byte) 7); + assertFalse(g2.isValid(MIN_LEN)); + } }