introduce bitloops

This commit is contained in:
mike
2026-01-20 03:43:02 +01:00
parent 28f448d178
commit 0dcfbebcb0
3 changed files with 263 additions and 180 deletions

View File

@@ -30,39 +30,45 @@ public final class Masker {
public boolean isValid(Clues c, int minLen) { public boolean isValid(Clues c, int minLen) {
return findOffendingClue(c, minLen) == -1; return findOffendingClue(c, minLen) == -1;
} }
public int findOffendingClue(Clues grid, int minLen) { 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.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); if (((grid.xhi & grid.rhi) & grid.hi) != X) return 64 | numberOfTrailingZeros((grid.xhi & grid.rhi) & grid.hi);
int num = 0; int num = 0;
for (long bits = grid.lo; bits != X; bits &= bits - 1) activeCIdx[num++] = numberOfTrailingZeros(bits); 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); for (long bits = grid.hi; bits != X; bits &= bits - 1) activeCIdx[num++] = 64 | numberOfTrailingZeros(bits);
if (num == 0) return -1; if (num == 0) return -1;
int start = rng.randint0_SIZE() % num; int start = rng.randint0_SIZE() % num;
int n = 0; int n = 0;
for (int i = 0; i < num; i++) { for (int i = 0; i < num; i++) {
int idx = activeCIdx[(start + i) % num]; int idx = activeCIdx[(start + i) % num];
int dir = grid.getDir(idx); int dir = grid.getDir(idx);
int key = Slot.packSlotKey(idx, dir); int key = Slot.packSlotKey(idx, dir);
long sLo = PATH_LO[key], sHi = PATH_HI[key]; long sLo = PATH_LO[key], sHi = PATH_HI[key];
long hLo = sLo & grid.lo, hHi = sHi & grid.hi; long hLo = sLo & grid.lo, hHi = sHi & grid.hi;
if (Slotinfo.increasing(key)) { if (Slotinfo.increasing(key)) {
if (hLo != X) { sLo &= (1L << numberOfTrailingZeros(hLo)) - 1; sHi = 0; } if (hLo != X) {
else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; } sLo &= (1L << numberOfTrailingZeros(hLo)) - 1;
sHi = 0;
} else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; }
} else { } else {
if (hHi != X) { sHi &= -(1L << (63 - numberOfLeadingZeros(hHi)) << 1); sLo = 0; } if (hHi != X) {
else if (hLo != X) { sLo &= -(1L << (63 - numberOfLeadingZeros(hLo)) << 1); } 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; 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; 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; return -1;
} }
public void cleanup(Clues c, int minLen) { public void cleanup(Clues c, int minLen) {
int guard = 0; int guard = 0;
while (guard++ < 50) { while (guard++ < 50) {
@@ -255,12 +261,12 @@ public final class Masker {
int msb = 63 - numberOfLeadingZeros(hLo); int msb = 63 - numberOfLeadingZeros(hLo);
rLo &= -(1L << msb << 1); rLo &= -(1L << msb << 1);
} }
activeCIdx[numClues] = 64 | clueIdx; activeCIdx[numClues] = 64 | clueIdx;
activeSLo[numClues] = rLo; activeSLo[numClues] = rLo;
activeSHi[numClues] = rHi; activeSHi[numClues] = rHi;
numClues++; numClues++;
if ((rLo | rHi) != X) { if ((rLo | rHi) != X) {
hasSlots = true; hasSlots = true;
if (Slot.horiz(key)) { if (Slot.horiz(key)) {
@@ -281,7 +287,7 @@ public final class Masker {
} }
if (!hasSlots) return 1_000_000_000L; if (!hasSlots) return 1_000_000_000L;
int[] rCount = new int[8]; int[] rCount = new int[8];
int[] cCount = new int[9]; int[] cCount = new int[9];
for (int i = 0; i < numClues; i++) { for (int i = 0; i < numClues; i++) {
@@ -315,16 +321,15 @@ public final class Masker {
} }
if (numClues > 0) { if (numClues > 0) {
int maxReached = 0; int maxReached = 0;
long totalReachedLo = 0, totalReachedHi = 0; long totalReachedLo = 0, totalReachedHi = 0;
for (int i = 0; i < numClues; i++) { for (int i = 0; i < numClues; i++) {
if (i < 64) { if ((totalReachedLo & (1L << i)) != 0) continue; } if (i < 64) { if ((totalReachedLo & (1L << i)) != 0) continue; } else { if ((totalReachedHi & (1L << (i - 64))) != 0) continue; }
else { if ((totalReachedHi & (1L << (i - 64))) != 0) continue; }
long currentReachedLo = (i < 64) ? (1L << i) : 0; long currentReachedLo = (i < 64) ? (1L << i) : 0;
long currentReachedHi = (i >= 64) ? (1L << (i - 64)) : 0; long currentReachedHi = (i >= 64) ? (1L << (i - 64)) : 0;
stack[0] = i; stack[0] = i;
int sp = 1; int sp = 1;
int count = 0; int count = 0;
while (sp > 0) { while (sp > 0) {
int cur = stack[--sp]; int cur = stack[--sp];
@@ -334,16 +339,16 @@ public final class Masker {
while (nLo != 0) { while (nLo != 0) {
long lsb = nLo & -nLo; long lsb = nLo & -nLo;
int idx = numberOfTrailingZeros(lsb); int idx = numberOfTrailingZeros(lsb);
currentReachedLo |= lsb; currentReachedLo |= lsb;
stack[sp++] = idx; stack[sp++] = idx;
nLo &= ~lsb; nLo &= ~lsb;
} }
while (nHi != 0) { while (nHi != 0) {
long lsb = nHi & -nHi; long lsb = nHi & -nHi;
int idx = 64 | numberOfTrailingZeros(lsb); int idx = 64 | numberOfTrailingZeros(lsb);
currentReachedHi |= lsb; currentReachedHi |= lsb;
stack[sp++] = idx; stack[sp++] = idx;
nHi &= ~lsb; nHi &= ~lsb;
} }
} }
if (count > maxReached) maxReached = count; if (count > maxReached) maxReached = count;
@@ -355,7 +360,7 @@ public final class Masker {
penalty += 20000; penalty += 20000;
} }
} }
for (long bits = ~lo_cl & MASK_LO; bits != X; bits &= bits - 1) { for (long bits = ~lo_cl & MASK_LO; bits != X; bits &= bits - 1) {
int clueIdx = numberOfTrailingZeros(bits); int clueIdx = numberOfTrailingZeros(bits);
var rci = IT[clueIdx]; 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 if (h && v) { /* ok */ } else if (((h ? cHHi2 : cVHi2) & (1L << clueIdx)) != X) penalty += 1000;
else penalty += 1000; else penalty += 1000;
} }
long nclLo = ~lo_cl & MASK_LO; long nclLo = ~lo_cl & MASK_LO;
long nclHi = ~hi_cl & MASK_HI; long nclHi = ~hi_cl & MASK_HI;
long hNbrLo = (nclLo >> 8) | (nclLo << 8) | (nclHi << 56); long hNbrLo = (nclLo >> 8) | (nclLo << 8) | (nclHi << 56);
long hNbrHi = (nclHi >> 8) | (nclLo >> 56); long hNbrHi = (nclHi >> 8) | (nclLo >> 56);
long vNbrLo = ((nclLo & ~0x0101010101010101L) >> 1) | ((nclLo & ~0x8080808080808080L) << 1); long vNbrLo = ((nclLo & ~0x0101010101010101L) >> 1) | ((nclLo & ~0x8080808080808080L) << 1);
long vNbrHi = ((nclHi & ~0x01L) >> 1) | ((nclHi & ~0x80L) << 1); long vNbrHi = ((nclHi & ~0x01L) >> 1) | ((nclHi & ~0x80L) << 1);
penalty += bitCount(nclLo & ~cHLo & hNbrLo) * 800; //penalty += bitCount(nclLo & ~cHLo & hNbrLo) * 800;
penalty += bitCount(nclLo & ~cVLo & vNbrLo) * 800; //penalty += bitCount(nclLo & ~cVLo & vNbrLo) * 800;
penalty += bitCount(nclHi & ~cHHi & hNbrHi) * 800; //penalty += bitCount(nclHi & ~cHHi & hNbrHi) * 800;
penalty += bitCount(nclHi & ~cVHi & vNbrHi) * 800; //penalty += bitCount(nclHi & ~cVHi & vNbrHi) * 800;
return penalty; return penalty;
} }
@@ -424,7 +429,7 @@ public final class Masker {
public Clues mutate(Clues c) { public Clues mutate(Clues c) {
var bytes = MUTATE_RI[rng.randint0_SIZE()]; 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()]; ri = bytes[rng.randint0_624()];
if (c.notClue(ri)) { // ADD if (c.notClue(ri)) { // ADD
byte d = rng.randomClueDir(); byte d = rng.randomClueDir();
@@ -433,17 +438,31 @@ public final class Masker {
if (isLo(ri)) { if (isLo(ri)) {
c.setClueLo(1L << ri, d); c.setClueLo(1L << ri, d);
if (!isValid(c, MIN_LEN)) c.clearClueLo(~(1L << ri)); if (!isValid(c, MIN_LEN)) c.clearClueLo(~(1L << ri));
else continue;
} else { } else {
c.setClueHi(1L << (ri & 63), d); c.setClueHi(1L << (ri & 63), d);
if (!isValid(c, MIN_LEN)) c.clearClueHi(~(1L << (ri & 63))); if (!isValid(c, MIN_LEN)) c.clearClueHi(~(1L << (ri & 63)));
else continue;
} }
} }
} else { // HAS CLUE } else { // HAS CLUE
var op = rng.randomClueDir(); var op = rng.randomClueDir();
if (op < 2) { // REMOVE if (op < 2) { // REMOVE
if (isLo(ri)) c.clearClueLo(~(1L << ri)); byte oldD = c.getDir(ri);
else c.clearClueHi(~(1L << (ri & 63))); if (isLo(ri)) {
} else if (op < 5) { // CHANGE DIRECTION 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(); byte d = rng.randomClueDir();
int key = Slot.packSlotKey(ri, d); int key = Slot.packSlotKey(ri, d);
if (c.hasRoomForClue(key)) { if (c.hasRoomForClue(key)) {
@@ -451,28 +470,29 @@ public final class Masker {
if (isLo(ri)) { if (isLo(ri)) {
c.setClueLo(1L << ri, d); c.setClueLo(1L << ri, d);
if (!isValid(c, MIN_LEN)) c.setClueLo(1L << ri, oldD); if (!isValid(c, MIN_LEN)) c.setClueLo(1L << ri, oldD);
else continue;
} else { } else {
c.setClueHi(1L << (ri & 63), d); c.setClueHi(1L << (ri & 63), d);
if (!isValid(c, MIN_LEN)) c.setClueHi(1L << (ri & 63), oldD); if (!isValid(c, MIN_LEN)) c.setClueHi(1L << (ri & 63), oldD);
else continue;
} }
} }
} else { // MOVE } // MOVE
int nri = bytes[rng.randint0_624()]; int nri = bytes[rng.randint0_624()];
if (c.notClue(nri)) { if (c.notClue(nri)) {
byte d = c.getDir(ri); byte d = c.getDir(ri);
int nkey = Slot.packSlotKey(nri, d); int nkey = Slot.packSlotKey(nri, d);
if (c.hasRoomForClue(nkey)) { if (c.hasRoomForClue(nkey)) {
if (isLo(ri)) c.clearClueLo(~(1L << ri)); if (isLo(ri)) c.clearClueLo(~(1L << ri));
else c.clearClueHi(~(1L << (ri & 63))); else c.clearClueHi(~(1L << (ri & 63)));
if (isLo(nri)) c.setClueLo(1L << nri, d); if (isLo(nri)) c.setClueLo(1L << nri, d);
else c.setClueHi(1L << (nri & 63), d); else c.setClueHi(1L << (nri & 63), d);
if (!isValid(c, MIN_LEN)) { if (!isValid(c, MIN_LEN)) {
if (isLo(nri)) c.clearClueLo(~(1L << nri)); if (isLo(nri)) c.clearClueLo(~(1L << nri));
else c.clearClueHi(~(1L << (nri & 63))); else c.clearClueHi(~(1L << (nri & 63)));
if (isLo(ri)) c.setClueLo(1L << ri, d); if (isLo(ri)) c.setClueLo(1L << ri, d);
else c.setClueHi(1L << (ri & 63), 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); if (((xhi & rhi) & hi) != X) return 64 | numberOfTrailingZeros((xhi & rhi) & hi);
int n = 0; int n = 0;
for (long bits = lo; bits != X; bits &= bits - 1) { for (long bits = lo; bits != X; bits &= bits - 1) {
int idx = numberOfTrailingZeros(bits); int idx = numberOfTrailingZeros(bits);
int dir = getDir(idx); int dir = getDir(idx);
int key = Slot.packSlotKey(idx, dir); int key = Slot.packSlotKey(idx, dir);
long sLo = PATH_LO[key], sHi = PATH_HI[key]; long sLo = PATH_LO[key], sHi = PATH_HI[key];
long hLo = sLo & lo, hHi = sHi & hi; long hLo = sLo & lo, hHi = sHi & hi;
if (Slotinfo.increasing(key)) { if (Slotinfo.increasing(key)) {
if (hLo != X) { sLo &= (1L << numberOfTrailingZeros(hLo)) - 1; sHi = 0; } if (hLo != X) {
else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; } sLo &= (1L << numberOfTrailingZeros(hLo)) - 1;
sHi = 0;
} else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; }
} else { } else {
if (hHi != X) { sHi &= -(1L << (63 - numberOfLeadingZeros(hHi)) << 1); sLo = 0; } if (hHi != X) {
else if (hLo != X) { sLo &= -(1L << (63 - numberOfLeadingZeros(hLo)) << 1); } 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; 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; 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) { for (long bits = hi; bits != X; bits &= bits - 1) {
int idx = 64 | numberOfTrailingZeros(bits); int idx = 64 | numberOfTrailingZeros(bits);
int dir = getDir(idx); int dir = getDir(idx);
int key = Slot.packSlotKey(idx, dir); int key = Slot.packSlotKey(idx, dir);
long sLo = PATH_LO[key], sHi = PATH_HI[key]; long sLo = PATH_LO[key], sHi = PATH_HI[key];
long hLo = sLo & lo, hHi = sHi & hi; long hLo = sLo & lo, hHi = sHi & hi;
if (Slotinfo.increasing(key)) { if (Slotinfo.increasing(key)) {
if (hLo != X) { sLo &= (1L << numberOfTrailingZeros(hLo)) - 1; sHi = 0; } if (hLo != X) {
else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; } sLo &= (1L << numberOfTrailingZeros(hLo)) - 1;
sHi = 0;
} else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; }
} else { } else {
if (hHi != X) { sHi &= -(1L << (63 - numberOfLeadingZeros(hHi)) << 1); sLo = 0; } if (hHi != X) {
else if (hLo != X) { sLo &= -(1L << (63 - numberOfLeadingZeros(hLo)) << 1); } 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; 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; 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; return -1;
} }

View File

@@ -40,7 +40,7 @@ public record SwedishGenerator() {
public static final int MAX_WORD_LENGTH = Config.PUZZLE_ROWS; 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 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 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 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_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; public static final long RANGE_0_624 = Neighbors9x8.RANGE_0_624;//624L - 0L + 1L;

View File

@@ -1,117 +1,168 @@
package puzzle; package puzzle;
import module java.base; import module java.base;
import lombok.val;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import puzzle.Export.Clued;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static puzzle.Masker.Clues; import static puzzle.Masker.Clues;
import static puzzle.SwedishGenerator.*; import static puzzle.SwedishGenerator.*;
import static puzzle.Masker.Slot; import static puzzle.Masker.Slot;
public class MaskerCluesTest { public class MaskerCluesTest {
@Test @Test
void testSimilarity() { void testValidRandomMask() {
Clues a = Clues.createEmpty(); Rng rng = new Rng(42);
a.setClueLo(1L << 0, (byte) 1); Masker masker = new Masker(rng, new int[STACK_SIZE], Clues.createEmpty());
a.setClueLo(1L << 10, (byte) 0); for (int i = 0; i < 200; i++) {
for (int j = 19; j < 24; j++) {
Clues b = Clues.createEmpty(); var clues = masker.randomMask(j);
b.setClueLo(1L << 0, (byte) 1); assertTrue(clues.isValid(MIN_LEN), "Mask should be valid for length \n" + new Clued(clues).gridToString());
b.setClueLo(1L << 10, (byte) 0); }
}
// Identity }
assertEquals(1.0, a.similarity(b), 0.001); @Test
void testValidMutate() {
// Different direction Rng rng = new Rng(42);
Clues c = Clues.createEmpty(); var cache = Clues.createEmpty();
c.setClueLo(1L << 0, (byte) 0); Masker masker = new Masker(rng, new int[STACK_SIZE], cache);
c.setClueLo(1L << 10, (byte) 0); double sim = 0.0;
assertTrue(a.similarity(c) < 1.0); double simCount = 0.0;
for (int i = 0; i < 200; i++) {
// Completely different for (int j = 19; j < 24; j++) {
Clues d = Clues.createEmpty(); var clues = masker.randomMask(j);
// Matching empty cells count towards similarity. val orig = cache.from(clues);
// a has 2 clues, d has 0. They match on 70 empty cells. simCount++;
assertEquals(70.0 / 72.0, a.similarity(d), 0.001); masker.mutate(clues);
} sim += orig.similarity(clues);
assertTrue(clues.isValid(MIN_LEN), "Mask should be valid for length \n" + new Clued(clues).gridToString());
@Test }
void testIsValid() { }
Clues g = Clues.createEmpty(); System.out.println("Average similarity: " + sim / simCount);
assertTrue(g.isValid(MIN_LEN)); }
@Test
// Valid clue: Right from (0,0) in 9x8 grid. Length is 8. void testCross() {
g.setClueLo(1L << Masker.offset(0, 0), (byte) 1); Rng rng = new Rng(42);
assertTrue(g.isValid(MIN_LEN)); var cache = Clues.createEmpty();
Masker masker = new Masker(rng, new int[STACK_SIZE], cache);
// Invalid clue: Right from (0,7) in 9x8 grid. Length is 1 (too short if MIN_LEN >= 2). double sim = 0.0;
Clues g2 = Clues.createEmpty(); double simCount = 0.0;
g2.setClueLo(1L << Masker.offset(0, 7), (byte) 1); for (int i = 0; i < 200; i++) {
assertFalse(g2.isValid(MIN_LEN)); for (int j = 19; j < 24; j++) {
} var clues = masker.randomMask(j);
var clues2 = masker.randomMask(j);
@Test simCount++;
void testHasRoomForClue() { var cross = masker.crossover(clues, clues2);
Clues g = Clues.createEmpty(); 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());
// Room for Right clue at (0,0) (length 8) }
assertTrue(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 0), 1))); }
System.out.println("Average similarity: " + sim / simCount);
// No room for Right clue at (0,8) (length 0 < MIN_LEN) }
assertFalse(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 8), 1))); @Test
void testSimilarity() {
// Blocked room Clues a = Clues.createEmpty();
// Let's place a clue that leaves only 1 cell for another clue. a.setClueLo(1L << 0, (byte) 1);
g.setClueLo(1L << Masker.offset(0, 2), (byte) 1); a.setClueLo(1L << 10, (byte) 0);
// 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))); Clues b = Clues.createEmpty();
b.setClueLo(1L << 0, (byte) 1);
// But enough room b.setClueLo(1L << 10, (byte) 0);
g.clearClueLo(0L);
g.setClueLo(1L << Masker.offset(0, 3), (byte) 1); // Identity
// Now Right at (0,0) has (0,1), (0,2) -> length 2 == MIN_LEN assertEquals(1.0, a.similarity(b), 0.001);
assertTrue(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 0), 1)));
} // Different direction
Clues c = Clues.createEmpty();
@Test c.setClueLo(1L << 0, (byte) 0);
void testIntersectionConstraint() { c.setClueLo(1L << 10, (byte) 0);
Clues g = Clues.createEmpty(); assertTrue(a.similarity(c) < 1.0);
// Clue 1: (0,0) Right. Slot cells: (0,1), (0,2), (0,3), (0,4), (0,5), (0,6), (0,7), (0,8) // Completely different
g.setClueLo(1L << Masker.offset(0, 0), (byte) 1); Clues d = Clues.createEmpty();
// Matching empty cells count towards similarity.
// Clue 2: (1,2) Up. Slot cells: (0,2) // a has 2 clues, d has 0. They match on 70 empty cells.
// Intersection is exactly 1 cell (0,2). Valid. assertEquals(70.0 / 72.0, a.similarity(d), 0.001);
g.setClueLo(1L << Masker.offset(2, 2), (byte) 2); }
assertTrue(g.isValid(MIN_LEN));
@Test
// Clue 3: (1,1) Right. Slot cells: (1,2), (1,3), ... void testIsValid() {
// No intersection with Clue 1 or 2. Valid. Clues g = Clues.createEmpty();
g.setClueLo(1L << Masker.offset(1, 1), (byte) 1); assertTrue(g.isValid(MIN_LEN));
assertTrue(g.isValid(MIN_LEN));
// Valid clue: Right from (0,0) in 9x8 grid. Length is 8.
// Now create a violation: two slots sharing 2 cells. g.setClueLo(1L << Masker.offset(0, 0), (byte) 1);
// We can do this with Corner Down and another clue. assertTrue(g.isValid(MIN_LEN));
// 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), ... // Invalid clue: Right from (0,7) in 9x8 grid. Length is 1 (too short if MIN_LEN >= 2).
// They share MANY cells starting from (0,1). Clues g2 = Clues.createEmpty();
g2.setClueLo(1L << Masker.offset(0, 7), (byte) 1);
Clues g3 = Clues.createEmpty(); assertFalse(g2.isValid(MIN_LEN));
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 testHasRoomForClue() {
Clues g = Clues.createEmpty();
@Test
void testInvalidDirectionBits() { // Room for Right clue at (0,0) (length 8)
Clues g = Clues.createEmpty(); assertTrue(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 0), 1)));
// Dir 6 (x=1, r=1, v=0) is invalid
g.setClueLo(1L << 0, (byte) 6); // No room for Right clue at (0,8) (length 0 < MIN_LEN)
assertFalse(g.isValid(MIN_LEN)); assertFalse(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 8), 1)));
// Dir 7 (x=1, r=1, v=1) is invalid // Blocked room
Clues g2 = Clues.createEmpty(); // Let's place a clue that leaves only 1 cell for another clue.
g2.setClueLo(1L << 0, (byte) 7); g.setClueLo(1L << Masker.offset(0, 2), (byte) 1);
assertFalse(g2.isValid(MIN_LEN)); // 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));
}
} }