From d1c448e1cbefe3341dc05f891455ec3c1f626e1a Mon Sep 17 00:00:00 2001 From: mike Date: Tue, 20 Jan 2026 01:57:21 +0100 Subject: [PATCH] introduce bitloops --- src/main/java/puzzle/Export.java | 9 +- src/main/java/puzzle/Main.java | 2 +- src/main/java/puzzle/Masker.java | 186 ++++++++++++--------- src/main/java/puzzle/SwedishGenerator.java | 2 +- src/main/java/puzzle/Trigger.java | 2 +- src/test/java/puzzle/MainTest.java | 6 +- src/test/java/puzzle/MaskerCluesTest.java | 117 +++++++++++++ 7 files changed, 239 insertions(+), 85 deletions(-) create mode 100644 src/test/java/puzzle/MaskerCluesTest.java diff --git a/src/main/java/puzzle/Export.java b/src/main/java/puzzle/Export.java index 437bb9b..c1e45d6 100644 --- a/src/main/java/puzzle/Export.java +++ b/src/main/java/puzzle/Export.java @@ -41,6 +41,7 @@ public record Export() { static final byte CLUE_UP = 2; static final byte CLUE_LEFT = 3; static final byte CLUE_LEFT_TOP = 4; + static final byte CLUE_RIGHT_TOP = 5; static int HI(int in) { return in | 64; } static char LETTER(int in) { return (char) (in | 64); } static char CLUE_CHAR(int s) { return (char) (s | 48); } @@ -100,13 +101,15 @@ public record Export() { for (var l = c.lo & ~c.xlo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), DOWN.dir)); for (var l = c.lo & ~c.xlo & c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), CLUE_UP)); for (var l = c.lo & ~c.xlo & c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), CLUE_LEFT)); - for (var l = c.lo & c.xlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), CLUE_LEFT_TOP)); + for (var l = c.lo & c.xlo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), CLUE_LEFT_TOP)); + for (var l = c.lo & c.xlo & ~c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), CLUE_RIGHT_TOP)); for (var h = c.hi & ~c.xhi & ~c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_RIGHT)); for (var h = c.hi & ~c.xhi & ~c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_DOWN)); for (var h = c.hi & ~c.xhi & c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_UP)); for (var h = c.hi & ~c.xhi & c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_LEFT)); - for (var h = c.hi & c.xhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_LEFT_TOP)); + for (var h = c.hi & c.xhi & ~c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_LEFT_TOP)); + for (var h = c.hi & c.xhi & ~c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_RIGHT_TOP)); return stream.build(); } @@ -223,7 +226,7 @@ public record Export() { public static final char HORIZONTAL = 'h'; static final char VERTICAL = 'v'; - static final char[] DIRECTION = { Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL }; + static final char[] DIRECTION = { Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.VERTICAL }; public int arrowCol() { return Masker.IT[Slot.clueIndex(slotKey)].c(); } public int arrowRow() { return Masker.IT[Slot.clueIndex(slotKey)].r(); } diff --git a/src/main/java/puzzle/Main.java b/src/main/java/puzzle/Main.java index e8aaf30..d63599b 100644 --- a/src/main/java/puzzle/Main.java +++ b/src/main/java/puzzle/Main.java @@ -34,7 +34,7 @@ public class Main { @NoArgsConstructor public static class Opts { - static int SSIZE = 24; + static int SSIZE = 23; public int seed = (int) (System.nanoTime() ^ System.currentTimeMillis()); public int clueSize = SSIZE; public int pop = SSIZE * 2; diff --git a/src/main/java/puzzle/Masker.java b/src/main/java/puzzle/Masker.java index d36df96..e188b18 100644 --- a/src/main/java/puzzle/Masker.java +++ b/src/main/java/puzzle/Masker.java @@ -27,6 +27,20 @@ public final class Masker { this.cache = cache; } + public boolean isValid(Clues c, int minLen) { + return c.isValid(minLen, activeSLo, activeSHi); + } + + public void cleanup(Clues c, int minLen) { + int guard = 0; + while (guard++ < 50) { + int offending = c.findOffendingClue(minLen, activeSLo, activeSHi); + if (offending == -1) break; + if ((offending & 64) == 0) c.clearClueLo(~(1L << offending)); + else c.clearClueHi(~(1L << (offending & 63))); + } + } + public static final int[][] MUTATE_RI = new int[SwedishGenerator.SIZE][625]; static { @@ -105,7 +119,7 @@ public final class Masker { } public static Slot[] extractSlots(Clues grid, DictEntry[] index) { var slots = new ArrayList(grid.clueCount()); - grid.forEachSlot((key, lo, hi) -> slots.add(Slot.from(key, lo, hi, Objects.requireNonNull(index[Slot.length(lo, hi)])))); + grid.forEachSlot((key, lo, hi) -> slots.add(Slot.from(key, lo, hi, index[Slot.length(lo, hi)]))); return slots.toArray(Slot[]::new); } public static Slotinfo[] slots(Clues mask, DictEntry[] index) { @@ -121,7 +135,8 @@ public final class Masker { } for (int i = 0; i < slots.length; i++) { var slot = slots[i]; - slotInfo[i] = new Slotinfo(slot.key, slot.lo, slot.hi, slotScore(count, slot.lo, slot.hi), new Assign(), slot.entry); + slotInfo[i] = new Slotinfo(slot.key, slot.lo, slot.hi, slotScore(count, slot.lo, slot.hi), new Assign(), slot.entry, + Math.min(slot.entry.words().length, MAX_TRIES_PER_SLOT)); } return slotInfo; } @@ -141,7 +156,7 @@ public final class Masker { long lo_cl = grid.lo, hi_cl = grid.hi; long penalty = (((long) Math.abs(grid.clueCount() - clueSize)) * 16000L); boolean hasSlots = false; - if (!grid.isValid(2)) return 1_000_000_000L; + if (!isValid(grid, 2)) return 1_000_000_000L; int numClues = 0; for (long bits = lo_cl; bits != X; bits &= bits - 1) { @@ -315,20 +330,20 @@ public final class Masker { ri = rng.randint0_SIZE(); if (isLo(ri)) { if (g.isClueLo(ri)) continue; - var d_idx = rng.randint2bitByte(); + var d_idx = rng.randomClueDir(); int key = Slot.packSlotKey(ri, d_idx); - if (g.hasRoomForClue(key, OFFSETS_D_IDX[key])) { + if (g.hasRoomForClue(key)) { g.setClueLo(1L << ri, d_idx); - if (g.isValid(MIN_LEN)) placed++; + if (isValid(g, MIN_LEN)) placed++; else g.clearClueLo(~(1L << ri)); } } else { if (g.isClueHi(ri)) continue; - var d_idx = rng.randint2bitByte(); + var d_idx = rng.randomClueDir(); int key = Slot.packSlotKey(ri, d_idx); - if (g.hasRoomForClue(key, OFFSETS_D_IDX[key])) { + if (g.hasRoomForClue(key)) { g.setClueHi(1L << (ri & 63), d_idx); - if (g.isValid(MIN_LEN)) placed++; + if (isValid(g, MIN_LEN)) placed++; else g.clearClueHi(~(1L << (ri & 63))); } } @@ -342,33 +357,33 @@ public final class Masker { for (int k = 0, ri; k < 4; k++) { ri = bytes[rng.randint0_624()]; if (c.notClue(ri)) { // ADD - byte d = rng.randint2bitByte(); + byte d = rng.randomClueDir(); int key = Slot.packSlotKey(ri, d); - if (c.hasRoomForClue(key, OFFSETS_D_IDX[key])) { + if (c.hasRoomForClue(key)) { if (isLo(ri)) { c.setClueLo(1L << ri, d); - if (!c.isValid(MIN_LEN)) c.clearClueLo(~(1L << ri)); + if (!isValid(c, MIN_LEN)) c.clearClueLo(~(1L << ri)); } else { c.setClueHi(1L << (ri & 63), d); - if (!c.isValid(MIN_LEN)) c.clearClueHi(~(1L << (ri & 63))); + if (!isValid(c, MIN_LEN)) c.clearClueHi(~(1L << (ri & 63))); } } } else { // HAS CLUE - int op = rng.randint(10); + 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 d = rng.randint2bitByte(); + byte d = rng.randomClueDir(); int key = Slot.packSlotKey(ri, d); - if (c.hasRoomForClue(key, OFFSETS_D_IDX[key])) { + if (c.hasRoomForClue(key)) { byte oldD = c.getDir(ri); if (isLo(ri)) { c.setClueLo(1L << ri, d); - if (!c.isValid(MIN_LEN)) c.setClueLo(1L << ri, oldD); + if (!isValid(c, MIN_LEN)) c.setClueLo(1L << ri, oldD); } else { c.setClueHi(1L << (ri & 63), d); - if (!c.isValid(MIN_LEN)) c.setClueHi(1L << (ri & 63), oldD); + if (!isValid(c, MIN_LEN)) c.setClueHi(1L << (ri & 63), oldD); } } } else { // MOVE @@ -376,12 +391,12 @@ public final class Masker { if (c.notClue(nri)) { byte d = c.getDir(ri); int nkey = Slot.packSlotKey(nri, d); - if (c.hasRoomForClue(nkey, OFFSETS_D_IDX[nkey])) { + 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 (!c.isValid(MIN_LEN)) { + 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); @@ -417,30 +432,10 @@ public final class Masker { (a.rhi & ~maskHi) | (other.rhi & maskHi), (a.xlo & ~maskLo) | (other.xlo & maskLo), (a.xhi & ~maskHi) | (other.xhi & maskHi)); - int guard = 0; - while (!c.isValid(MIN_LEN) && guard++ < 3) { - for (var l = c.lo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) clearCluesLo(c, numberOfTrailingZeros(l), 0); - for (var l = c.lo & ~c.rlo & c.vlo; l != X; l &= l - 1) clearCluesLo(c, numberOfTrailingZeros(l), 1); - for (var l = c.lo & c.rlo & ~c.vlo; l != X; l &= l - 1) clearCluesLo(c, numberOfTrailingZeros(l), 2); - for (var l = c.lo & c.rlo & c.vlo; l != X; l &= l - 1) clearCluesLo(c, numberOfTrailingZeros(l), 3); - for (var h = c.hi & ~c.rhi & ~c.vhi; h != X; h &= h - 1) clearCluesHi(c, numberOfTrailingZeros(h), 0); - for (var h = c.hi & ~c.rhi & c.vhi; h != X; h &= h - 1) clearCluesHi(c, numberOfTrailingZeros(h), 1); - for (var h = c.hi & c.rhi & ~c.vhi; h != X; h &= h - 1) clearCluesHi(c, (numberOfTrailingZeros(h)), 2); - for (var h = c.hi & c.rhi & c.vhi; h != X; h &= h - 1) clearCluesHi(c, (numberOfTrailingZeros(h)), 3); - } + cleanup(c, MIN_LEN); return c; } - public static void clearCluesLo(Clues out, int idx, int d) { - int key = Slot.packSlotKey(idx, d); - if (!out.hasRoomForClue(key, OFFSETS_D_IDX[key])) out.clearClueLo(~(1L << idx)); - } - - public static void clearCluesHi(Clues out, int idx, int d) { - int key = Slot.packSlotKey(64 | idx, d); - if (!out.hasRoomForClue(key, OFFSETS_D_IDX[key])) out.clearClueHi(~(1L << idx)); - } - public Clues hillclimb(Clues start, int clue_size, int limit) { var best = start; var bestF = maskFitness(best, clue_size); @@ -482,8 +477,8 @@ public final class Masker { for (int gen = 0; gen < gens; gen++) { if (Thread.currentThread().isInterrupted()) break; - GridAndFit[] children = new GridAndFit[offspring]; - int childCount = 0; + GridAndFit[] children = new GridAndFit[offspring]; + int childCount = 0; for (int k = 0; k < offspring; k++) { if (Thread.currentThread().isInterrupted()) break; GridAndFit p1 = rng.rand(pop); @@ -498,19 +493,34 @@ public final class Masker { System.arraycopy(children, 0, combined, pop.length, childCount); Arrays.sort(combined, Comparator.comparingLong(GridAndFit::fit)); - GridAndFit[] next = new GridAndFit[offspring]; - int nextCount = 0; + GridAndFit[] next = new GridAndFit[popSize]; + int nextCount = 0; for (GridAndFit cand : combined) { - if (nextCount >= offspring) break; - boolean ok = true; - for (int i = 0; i < nextCount; i++) + if (nextCount >= popSize) break; + boolean unique = true; + for (int i = 0; i < nextCount; i++) { if (cand.grid.similarity(next[i].grid) > 0.92) { - ok = false; + unique = false; break; } - if (ok) next[nextCount++] = cand; + } + if (unique) next[nextCount++] = cand; } - pop = nextCount == offspring ? next : Arrays.copyOf(next, nextCount); + + if (nextCount < popSize) { + for (GridAndFit cand : combined) { + if (nextCount >= popSize) break; + boolean alreadyIn = false; + for (int i = 0; i < nextCount; i++) { + if (cand == next[i]) { + alreadyIn = true; + break; + } + } + if (!alreadyIn) next[nextCount++] = cand; + } + } + pop = nextCount == popSize ? next : Arrays.copyOf(next, nextCount); if (Main.VERBOSE && (gen & 15) == 15) System.out.println(" gen " + gen + "/" + gens + " bestFitness=" + pop[0].fit()); } @@ -531,18 +541,7 @@ public final class Masker { long lo, hi, vlo, vhi, rlo, rhi, xlo, xhi; public static Clues createEmpty() { return new Clues(0, 0, 0, 0, 0, 0, 0, 0); } - public boolean cluelessLo(int idx) { - if (!isClueLo(idx)) return false; - clearClueLo(~(1L << idx)); - return true; - } - public boolean cluelessHi(int idx) { - if (!isClueHi(idx)) return false; - clearClueHi(~(1L << (idx & 63))); - return true; - } - public boolean hasRoomForClue(int key, long packed) { - if (packed == X || !notClue(packed & 0x7FL) || !notClue((packed >>> 7) & 0x7FL)) return false; + public boolean hasRoomForClue(int key) { if (Slotinfo.increasing(key)) if (!validSlot(lo, hi, MIN_LEN, key)) return false; return validSlotRev(lo, hi, MIN_LEN, key); } @@ -579,7 +578,6 @@ public final class Masker { } public boolean isClueLo(int index) { return ((lo >>> index) & 1L) != X; } public boolean isClueHi(int index) { return ((hi >>> (index & 63)) & 1L) != X; } - public boolean notClue(long index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) == X : ((hi >>> (index & 63)) & 1L) == X; } public boolean notClue(int index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) == X : ((hi >>> (index & 63)) & 1L) == X; } public int clueCount() { return bitCount(lo) + bitCount(hi); } @@ -591,18 +589,50 @@ public final class Masker { } public Grid toGrid() { return new Grid(new byte[SwedishGenerator.SIZE], lo, hi); } public boolean isValid(int minLen) { - for (var l = lo & ~xlo & ~rlo & vlo; l != X; l &= l - 1) if (!validSlot(lo, hi, minLen, Slot.packSlotKey(numberOfTrailingZeros(l), 1))) return false; - for (var l = lo & ~xlo & ~rlo & ~vlo; l != X; l &= l - 1) if (!validSlot(lo, hi, minLen, Slot.packSlotKey(numberOfTrailingZeros(l), 0))) return false; - for (var l = lo & ~xlo & rlo & ~vlo; l != X; l &= l - 1) if (!validSlotRev(lo, hi, minLen, Slot.packSlotKey(numberOfTrailingZeros(l), 2))) return false; - for (var l = lo & ~xlo & rlo & vlo; l != X; l &= l - 1) if (!validSlotRev(lo, hi, minLen, Slot.packSlotKey(numberOfTrailingZeros(l), 3))) return false; - for (var l = lo & xlo & ~rlo & ~vlo; l != X; l &= l - 1) if (!validSlot(lo, hi, minLen, Slot.packSlotKey(numberOfTrailingZeros(l), 4))) return false; - - for (var h = hi & ~xhi & ~rhi & vhi; h != X; h &= h - 1) if (!validSlot(lo, hi, minLen, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 1))) return false; - for (var h = hi & ~xhi & ~rhi & ~vhi; h != X; h &= h - 1) if (!validSlot(lo, hi, minLen, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 0))) return false; - for (var h = hi & ~xhi & rhi & ~vhi; h != X; h &= h - 1) if (!validSlotRev(lo, hi, minLen, Slot.packSlotKey((64 | numberOfTrailingZeros(h)), 2))) return false; - for (var h = hi & ~xhi & rhi & vhi; h != X; h &= h - 1) if (!validSlotRev(lo, hi, minLen, Slot.packSlotKey((64 | numberOfTrailingZeros(h)), 3))) return false; - for (var h = hi & xhi & ~rhi & ~vhi; h != X; h &= h - 1) if (!validSlot(lo, hi, minLen, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 4))) return false; - return true; + return findOffendingClue(minLen, new long[SwedishGenerator.SIZE], new long[SwedishGenerator.SIZE]) == -1; + } + public boolean isValid(int minLen, long[] slo, long[] shi) { + return findOffendingClue(minLen, slo, shi) == -1; + } + public int findOffendingClue(int minLen, long[] slo, long[] shi) { + if (((xlo & rlo) & lo) != X) return numberOfTrailingZeros((xlo & rlo) & lo); + 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); + 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; } + } else { + 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++; + } + for (long bits = hi; bits != X; bits &= bits - 1) { + 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; } + } else { + 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++; + } + return -1; } public void forEachSlot(SlotVisitor visitor) { for (var l = lo & ~xlo & ~rlo & vlo; l != X; l &= l - 1) processSlot(this, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 1)); @@ -610,12 +640,14 @@ public final class Masker { for (var l = lo & ~xlo & rlo & ~vlo; l != X; l &= l - 1) processSlotRev(this, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 2)); for (var l = lo & ~xlo & rlo & vlo; l != X; l &= l - 1) processSlotRev(this, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 3)); for (var l = lo & xlo & ~rlo & ~vlo; l != X; l &= l - 1) processSlot(this, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 4)); + for (var l = lo & xlo & ~rlo & vlo; l != X; l &= l - 1) processSlot(this, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 5)); for (var h = hi & ~xhi & ~rhi & vhi; h != X; h &= h - 1) processSlot(this, visitor, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 1)); for (var h = hi & ~xhi & ~rhi & ~vhi; h != X; h &= h - 1) processSlot(this, visitor, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 0)); for (var h = hi & ~xhi & rhi & ~vhi; h != X; h &= h - 1) processSlotRev(this, visitor, Slot.packSlotKey((64 | numberOfTrailingZeros(h)), 2)); for (var h = hi & ~xhi & rhi & vhi; h != X; h &= h - 1) processSlotRev(this, visitor, Slot.packSlotKey((64 | numberOfTrailingZeros(h)), 3)); for (var h = hi & xhi & ~rhi & ~vhi; h != X; h &= h - 1) processSlot(this, visitor, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 4)); + for (var h = hi & xhi & ~rhi & vhi; h != X; h &= h - 1) processSlot(this, visitor, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 5)); } public Clues from(Clues best) { lo = best.lo; @@ -650,7 +682,7 @@ public final class Masker { public static int length(long lo, long hi) { return bitCount(lo) + bitCount(hi); } public static int clueIndex(int key) { return key >>> BIT_FOR_DIR; } public static int dir(int key) { return key & 7; } - public static boolean horiz(int d) { return (d & 1) != 0 && (d & 4) == 0; } + public static boolean horiz(int d) { return (d == 1) || (d == 3); } public static int packSlotKey(int idx, int d) { return (idx << BIT_FOR_DIR) | d; } } } diff --git a/src/main/java/puzzle/SwedishGenerator.java b/src/main/java/puzzle/SwedishGenerator.java index c4420a4..fc1fbbe 100644 --- a/src/main/java/puzzle/SwedishGenerator.java +++ b/src/main/java/puzzle/SwedishGenerator.java @@ -84,7 +84,7 @@ public record SwedishGenerator() { x = y; return y; } - static final byte[] BYTE = new byte[]{ 0, 1, 2, 3/*,4, 5*/ }; + static final byte[] BYTE = new byte[]{ 0, 1, 2, 3, 4, 5 }; public byte randomClueDir() { return rand(BYTE); } public T rand(T[] p) { return p[(int) (((nextU32() & 0xFFFFFFFFL) % ((long) p.length)))]; } public byte rand(byte[] p) { return p[(int) (((nextU32() & 0xFFFFFFFFL) % ((long) p.length)))]; } diff --git a/src/main/java/puzzle/Trigger.java b/src/main/java/puzzle/Trigger.java index 460e38b..f805e81 100644 --- a/src/main/java/puzzle/Trigger.java +++ b/src/main/java/puzzle/Trigger.java @@ -2,5 +2,5 @@ package puzzle; import gen.GenerateNeighbors; -@GenerateNeighbors(C = 9, R = 8, packageName = "precomp", className = "Neighbors9x8", MIN_LEN = 3) +@GenerateNeighbors(C = 9, R = 8, packageName = "precomp", className = "Neighbors9x8", MIN_LEN = 2) public final class Trigger { } diff --git a/src/test/java/puzzle/MainTest.java b/src/test/java/puzzle/MainTest.java index b9b98b6..8e2ec90 100644 --- a/src/test/java/puzzle/MainTest.java +++ b/src/test/java/puzzle/MainTest.java @@ -11,6 +11,7 @@ import puzzle.Export.LetterVisit.LetterAt; import puzzle.Export.PuzzleResult; import puzzle.Export.Rewards; import puzzle.Main.Opts; +import puzzle.Masker.Slot; import puzzle.SwedishGenerator.Rng; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -20,6 +21,7 @@ import static puzzle.Export.Clue.DOWN; import static puzzle.Export.Clue.LEFT; import static puzzle.Export.Clue.RIGHT; import static puzzle.Export.Clue.UP; +import static puzzle.Masker.Slot; import static puzzle.SwedishGenerator.Dict; import static puzzle.SwedishGenerator.Lemma; import static puzzle.SwedishGenerator.Slotinfo; @@ -216,8 +218,8 @@ public class MainTest { var grid = Slotinfo.grid(slotInfo); var filled = fillMask(rng, slotInfo, grid); Assertions.assertTrue(filled.ok(), "Puzzle generation failed (not ok)"); - Assertions.assertEquals(13, Slotinfo.wordCount(0, slotInfo), "Number of assigned words changed"); - Assertions.assertEquals("WAANZIN", Lemma.asWord(slotInfo[0].assign().w, Export.BYTES.get())); + Assertions.assertEquals(17, Slotinfo.wordCount(0, slotInfo), "Number of assigned words changed"); + Assertions.assertEquals("VREEMDS", Lemma.asWord(slotInfo[0].assign().w, Export.BYTES.get())); Assertions.assertEquals(-1L, grid.lo); Assertions.assertEquals(-1L, grid.hi); var g = new Gridded(grid, mask.c()); diff --git a/src/test/java/puzzle/MaskerCluesTest.java b/src/test/java/puzzle/MaskerCluesTest.java new file mode 100644 index 0000000..6c3a0f3 --- /dev/null +++ b/src/test/java/puzzle/MaskerCluesTest.java @@ -0,0 +1,117 @@ +package puzzle; + +import module java.base; +import org.junit.jupiter.api.Test; +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)); + } +}