diff --git a/src/main/java/puzzle/Export.java b/src/main/java/puzzle/Export.java index ffe0491..b95e237 100644 --- a/src/main/java/puzzle/Export.java +++ b/src/main/java/puzzle/Export.java @@ -98,12 +98,12 @@ public record Export() { public Stream stream(Clues clues) { val stream = Stream.builder(); for (var l = grid.lo & ~clues.lo; l != X; l &= l - 1) stream.accept(LetterAt.from(Long.numberOfTrailingZeros(l), grid.g)); - for (var h = grid.hi & ~clues.hi; h != X; h &= h - 1) stream.accept(LetterAt.from(64 | Long.numberOfTrailingZeros(h), grid.g)); + for (var h = grid.hi & ~clues.hi & 0xFF; h != X; h &= h - 1) stream.accept(LetterAt.from(64 | Long.numberOfTrailingZeros(h), grid.g)); return stream.build(); } public void forEachLetter(Clues clues, LetterVisit visitor) { for (var l = grid.lo & ~clues.lo; l != X; l &= l - 1) visitor.visit(Long.numberOfTrailingZeros(l), grid.g); - for (var h = grid.hi & ~clues.hi; h != X; h &= h - 1) visitor.visit(64 | Long.numberOfTrailingZeros(h), grid.g); + for (var h = grid.hi & ~clues.hi & 0xFF; h != X; h &= h - 1) visitor.visit(64 | Long.numberOfTrailingZeros(h), grid.g); } public static IntStream walk(byte base, long lo, long hi) { if (Slotinfo.increasing(base)) { @@ -243,7 +243,8 @@ public record Export() { return new ExportedPuzzle(grid.exportGrid(clues.c, _ -> '#', '#'), new WordOut[0], difficulty, rewards); } - var placed = Arrays.stream(slots).map(slot -> new Placed(slot.assign().w, slot.key(), Gridded.walk((byte) slot.key(), slot.lo(), slot.hi()).toArray())).toArray(Placed[]::new); + var placed = Arrays.stream(slots).map(slot -> new Placed(slot.assign().w, slot.key(), Gridded.walk((byte) slot.key(), slot.lo(), slot.hi()).toArray())).toArray( + Placed[]::new); // 2) bounding box around all word cells + arrow cells, with 1-cell margin int minR = Integer.MAX_VALUE, minC = Integer.MAX_VALUE; diff --git a/src/main/java/puzzle/Main.java b/src/main/java/puzzle/Main.java index 3f191d2..28afa9a 100644 --- a/src/main/java/puzzle/Main.java +++ b/src/main/java/puzzle/Main.java @@ -47,9 +47,9 @@ public class Main { static int SSIZE = 24; public int seed = (int) (System.nanoTime() ^ System.currentTimeMillis()); public int clueSize = SSIZE; - public int pop = SSIZE*2; - public int offspring = SSIZE*3; - public int gens =600; + public int pop = SSIZE * 2; + public int offspring = SSIZE * 3; + public int gens = 600; public String wordsPath = "nl_score_hints_v3.csv"; public double minSimplicity = 0; // 0 means no limit public int threads = Math.max(1, Runtime.getRuntime().availableProcessors()); @@ -414,8 +414,8 @@ public class Main { if (mask == null) return null; val slotInfo = Masker.slots(mask, dict.index()); - var grid = mask.toGrid(); - var filled = fillMask(rng, slotInfo, grid, (!Main.VERBOSE || multiThreaded)); + var grid = Slotinfo.grid(slotInfo);// mask.toGrid(); + var filled = fillMask(rng, slotInfo, grid, (!Main.VERBOSE || multiThreaded)); if (!multiThreaded) { System.out.print("\r" + Strings.padRight("", 120) + "\r"); diff --git a/src/main/java/puzzle/Masker.java b/src/main/java/puzzle/Masker.java index 77ad97f..42bcb1a 100644 --- a/src/main/java/puzzle/Masker.java +++ b/src/main/java/puzzle/Masker.java @@ -14,7 +14,23 @@ import java.util.stream.IntStream; import static java.lang.Long.*; import static puzzle.SwedishGenerator.*; -public record Masker(Rng rng, int[] stack, Clues cache) { +public final class Masker { + + private final Rng rng; + private final int[] stack; + private final Clues cache; + + private final int[] activeCIdx = new int[SwedishGenerator.SIZE]; + private final long[] activeSLo = new long[SwedishGenerator.SIZE]; + private final long[] activeSHi = new long[SwedishGenerator.SIZE]; + private final long[] adjLo = new long[SwedishGenerator.SIZE]; + private final long[] adjHi = new long[SwedishGenerator.SIZE]; + + public Masker(Rng rng, int[] stack, Clues cache) { + this.rng = rng; + this.stack = stack; + this.cache = cache; + } public static final int[][] MUTATE_RI = new int[SwedishGenerator.SIZE][625]; @@ -109,6 +125,8 @@ public record Masker(Rng rng, int[] stack, Clues cache) { long penalty = (((long) Math.abs(grid.clueCount() - clueSize)) * 16000L); boolean hasSlots = false; if (!grid.isValid(2)) return 1_000_000_000L; + + int numClues = 0; for (long bits = lo_cl; bits != X; bits &= bits - 1) { long lsb = bits & -bits; int clueIdx = numberOfTrailingZeros(lsb); @@ -130,6 +148,12 @@ public record Masker(Rng rng, int[] stack, Clues cache) { int msb = 63 - numberOfLeadingZeros(hLo); rLo &= -(1L << msb << 1); } + + activeCIdx[numClues] = clueIdx; + activeSLo[numClues] = rLo; + activeSHi[numClues] = rHi; + numClues++; + if ((rLo | rHi) != X) { hasSlots = true; if (Slot.horiz(key)) { @@ -167,6 +191,12 @@ public record Masker(Rng rng, int[] stack, Clues cache) { 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)) { @@ -185,63 +215,64 @@ public record Masker(Rng rng, int[] stack, Clues cache) { } if (!hasSlots) return 1_000_000_000L; - long seenLo = X, seenHi = X; - -// loop over beide helften - for (int base = 0, size, sp, cur; base <= 64; base += 64) { - long clueMask = (base == 0) ? lo_cl : hi_cl; - long seenMask = (base == 0) ? seenLo : seenHi; - - // "unseen clues" in deze helft - for (long bits = clueMask & ~seenMask, nLo, nHi; bits != X; bits &= bits - 1) { - int clueIdx = base | numberOfTrailingZeros(bits); - - // start nieuwe component - size = 0; - stack[0] = clueIdx; - sp = 1; - - // mark seen - if ((clueIdx & 64) == 0) seenLo |= 1L << clueIdx; - else seenHi |= 1L << (clueIdx & 63); - - // flood fill / bfs - while (sp > 0) { - cur = stack[--sp]; - size++; - - // neighbors als 2x long masks - nLo = NBR8_PACKED_LO[cur]; - nHi = NBR8_PACKED_HI[cur]; - - // filter: alleen clues, en nog niet seen - nLo &= lo_cl & ~seenLo; - nHi &= hi_cl & ~seenHi; - - // push lo-neighbors - while (nLo != X) { - long lsb = nLo & -nLo; - int nidx = numberOfTrailingZeros(nLo); // 0..63 - seenLo |= lsb; - - stack[sp++] = nidx; - - nLo &= nLo - 1; - } - - // push hi-neighbors - while (nHi != X) { - long lsb = nHi & -nHi; - int nidx = 64 | numberOfTrailingZeros(nHi); // 64..127 - seenHi |= lsb; - - stack[sp++] = nidx; - - nHi &= nHi - 1; + + // Connectiviteitscheck + for (int i = 0; i < numClues; i++) { + adjLo[i] = 0; adjHi[i] = 0; + } + for (int i = 0; i < numClues; i++) { + for (int j = i + 1; j < numClues; j++) { + boolean connected = false; + // 1. Intersectie + if (((activeSLo[i] & activeSLo[j]) | (activeSHi[i] & activeSHi[j])) != 0) { + connected = true; + } else { + // 2. 8-naburigheid van clue cells + int ci = activeCIdx[i]; + int cj = activeCIdx[j]; + if (cj < 64) { + if ((NBR8_PACKED_LO[ci] & (1L << cj)) != 0) connected = true; + } else { + if ((NBR8_PACKED_HI[ci] & (1L << (cj & 63))) != 0) connected = true; } } - if (size >= 2) penalty += (size - 1L) * 120L; + if (connected) { + if (j < 64) adjLo[i] |= (1L << j); + else adjHi[i] |= (1L << (j - 64)); + if (i < 64) adjLo[j] |= (1L << i); + else adjHi[j] |= (1L << (i - 64)); + } + } + } + + if (numClues > 0) { + long reachedLo = 1L, reachedHi = 0L; + stack[0] = 0; + int sp = 1; + while (sp > 0) { + int cur = stack[--sp]; + long nLo = adjLo[cur] & ~reachedLo; + long nHi = adjHi[cur] & ~reachedHi; + while (nLo != 0) { + long lsb = nLo & -nLo; + int idx = numberOfTrailingZeros(lsb); + reachedLo |= lsb; + stack[sp++] = idx; + nLo &= ~lsb; + } + while (nHi != 0) { + long lsb = nHi & -nHi; + int idx = 64 | numberOfTrailingZeros(lsb); + reachedHi |= lsb; + stack[sp++] = idx; + nHi &= ~lsb; + } + } + int count = bitCount(reachedLo) + bitCount(reachedHi); + if (count < numClues) { + penalty += (numClues - count) * 4000; + penalty += 20000; } } @@ -251,7 +282,7 @@ public record Masker(Rng rng, int[] stack, Clues cache) { if ((4 - rci.nbrCount()) + bitCount(rci.n1() & lo_cl) + bitCount(rci.n2() & hi_cl) >= 3) penalty += 400; boolean h = (cHLo & (1L << clueIdx)) != X; boolean v = (cVLo & (1L << clueIdx)) != X; - if (!h && !v) penalty += 1500; + if (!h && !v) penalty += 15000; else if (h && v) { /* ok */ } else if (((h ? cHLo2 : cVLo2) & (1L << clueIdx)) != X) penalty += 600; else penalty += 200; } @@ -261,7 +292,8 @@ public record Masker(Rng rng, int[] stack, Clues cache) { if ((4 - rci.nbrCount()) + bitCount(rci.n1() & lo_cl) + bitCount(rci.n2() & hi_cl) >= 3) penalty += 400; boolean h = (cHHi & (1L << clueIdx)) != X; boolean v = (cVHi & (1L << clueIdx)) != X; - if (!h && !v) penalty += 1500; + if (!h && !v) + penalty += 15000; else if (h && v) { /* ok */ } else if (((h ? cHHi2 : cVHi2) & (1L << clueIdx)) != X) penalty += 600; else penalty += 200; } diff --git a/src/main/java/puzzle/SwedishGenerator.java b/src/main/java/puzzle/SwedishGenerator.java index 989f388..f979414 100644 --- a/src/main/java/puzzle/SwedishGenerator.java +++ b/src/main/java/puzzle/SwedishGenerator.java @@ -50,7 +50,7 @@ public record SwedishGenerator() { public static final int MAX_WORD_LENGTH_PLUS_ONE = MAX_WORD_LENGTH + 1; public static final int MIN_LEN = 3;//Config.MIN_LEN; public static final int MAX_TRIES_PER_SLOT = 700;//Config.MAX_TRIES_PER_SLOT; - public static final int STACK_SIZE = 64; + public static final int STACK_SIZE = 128; public static final char C_DASH = '\0'; public static final byte DASH = (byte) C_DASH; public static final long RANGE_0_SIZE = (long) SIZE_MIN_1 - 0L + 1L; diff --git a/src/test/java/puzzle/ConnectivityTest.java b/src/test/java/puzzle/ConnectivityTest.java new file mode 100644 index 0000000..bcd1da8 --- /dev/null +++ b/src/test/java/puzzle/ConnectivityTest.java @@ -0,0 +1,90 @@ +package puzzle; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import puzzle.Masker.Clues; +import puzzle.SwedishGenerator.Rng; +import static puzzle.SwedishGenerator.STACK_SIZE; + +public class ConnectivityTest { + + @Test + void testConnectivityPenalty() { + Rng rng = new Rng(42); + Masker masker = new Masker(rng, new int[STACK_SIZE], Clues.createEmpty()); + + // 1. Maak een masker met één component van clues (bijv. 3 clues naast elkaar) + Clues singleComp = Clues.createEmpty(); + // Gebruik offsets die dicht bij elkaar liggen + int off1 = SwedishGenerator.Grid.offset(1, 1); + int off2 = SwedishGenerator.Grid.offset(1, 2); + int off3 = SwedishGenerator.Grid.offset(2, 1); + + singleComp.setClueLo(1L << off1, (byte)1); // Right + singleComp.setClueLo(1L << off2, (byte)1); // Right + singleComp.setClueLo(1L << off3, (byte)0); // Down + + long fitnessSingle = masker.maskFitness(singleComp, 3); + + // 2. Maak een masker met twee eilandjes van clues + Clues twoIslands = Clues.createEmpty(); + int offA1 = SwedishGenerator.Grid.offset(1, 1); + int offB1 = SwedishGenerator.Grid.offset(6, 6); // Ver weg + + // We moeten zorgen dat ze elk minstens 1 slot vormen om door isValid(2) te komen + twoIslands.setClueLo(1L << offA1, (byte)1); + twoIslands.setClueLo(1L << offB1, (byte)1); + + long fitnessIslands = masker.maskFitness(twoIslands, 2); + + System.out.println("[DEBUG_LOG] Fitness single component: " + fitnessSingle); + System.out.println("[DEBUG_LOG] Fitness two islands: " + fitnessIslands); + + // De eilandjes moeten een hogere penalty hebben (als clueCount gelijk is) + Clues twoIslands3 = Clues.createEmpty(); + twoIslands3.setClueLo(1L << offA1, (byte)1); + twoIslands3.setClueLo(1L << offB1, (byte)1); + int offB2 = SwedishGenerator.Grid.offset(6, 7); + twoIslands3.setClueLo(1L << offB2, (byte)1); + + long fitnessIslands3 = masker.maskFitness(twoIslands3, 3); + System.out.println("[DEBUG_LOG] Fitness three clues in two islands: " + fitnessIslands3); + + assertTrue(fitnessIslands3 > fitnessSingle, "Islands should have higher penalty than single component"); + } + + @Test + void testIntersectionConnectivity() { + Rng rng = new Rng(42); + Masker masker = new Masker(rng, new int[STACK_SIZE], Clues.createEmpty()); + + // Test of slots die elkaar kruisen als verbonden worden beschouwd, + // zelfs als de clues niet 8-naburig zijn. + Clues crossing = Clues.createEmpty(); + // Clue 1: (0,0) naar rechts. Slot op (0,1), (0,2), (0,3) + // Clue 2: (1,2) omhoog. Slot op (0,2) + // Ze kruisen op (0,2) + crossing.setClueLo(1L << SwedishGenerator.Grid.offset(0,0), (byte)1); // Right + crossing.setClueLo(1L << SwedishGenerator.Grid.offset(1,2), (byte)2); // Up + + // Deze twee clues zijn niet 8-naburig (0,0 en 1,2) + // Maar hun slots kruisen op (0,2) + + long fitness = masker.maskFitness(crossing, 2); + System.out.println("[DEBUG_LOG] Fitness crossing: " + fitness); + + // Als ze als verbonden worden gezien, is er 1 component. + // Penalty voor connectiviteit zou 0 moeten zijn (bovenop andere penalties). + // Als we een derde clue ver weg toevoegen, moet de penalty significant stijgen. + + Clues crossingPlusIsland = Clues.createEmpty(); + crossingPlusIsland.setClueLo(1L << SwedishGenerator.Grid.offset(0,0), (byte)1); + crossingPlusIsland.setClueLo(1L << SwedishGenerator.Grid.offset(1,2), (byte)2); + crossingPlusIsland.setClueLo(1L << SwedishGenerator.Grid.offset(7,7), (byte)1); + + long fitnessIsland = masker.maskFitness(crossingPlusIsland, 3); + System.out.println("[DEBUG_LOG] Fitness crossing plus island: " + fitnessIsland); + + assertTrue(fitnessIsland > fitness + 10000, "Island should add significant penalty"); + } +} diff --git a/src/test/java/puzzle/MainTest.java b/src/test/java/puzzle/MainTest.java index 8cc57d0..2be0cad 100644 --- a/src/test/java/puzzle/MainTest.java +++ b/src/test/java/puzzle/MainTest.java @@ -196,9 +196,7 @@ public class MainTest { Assertions.assertEquals(20, mask.clueCount()); val map = mask.stream().collect(Collectors.toMap(ClueAt::index, ClueAt::clue)); Assertions.assertEquals(20, map.size()); - var slots = Masker.extractSlots(mask.c(), dict.index()); - val slotInfo = Masker.scoreSlots(slots); - var grid = mask.toGrid(); + var slots = Masker.slots(mask.c(), dict.index()); // var filled = fillMask(rng, slotInfo, grid, false); // val res = new PuzzleResult(new Clued(mask), new Gridded(grid), slotInfo, filled).exportFormatFromFilled(0, new Rewards(0, 0, 0)); } @@ -214,15 +212,14 @@ public class MainTest { " 1 \n" + " 1 2\n" + "21 22 3"); - var slots = Masker.extractSlots(mask.c(), dict.index()); - val slotInfo = Masker.scoreSlots(slots); - var grid = mask.toGrid(); + var slotInfo = Masker.slots(mask.c(), dict.index()); + var grid = Slotinfo.grid(slotInfo); var filled = fillMask(rng, slotInfo, grid, false); 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)); - Assertions.assertEquals(-2155876353L, grid.lo); - Assertions.assertEquals(255L, grid.hi); + Assertions.assertEquals(-1L, grid.lo); + Assertions.assertEquals(-1L, grid.hi); var g = new Gridded(grid); g.gridToString(mask.c()); var aa = new PuzzleResult(mask, g, slotInfo, filled).exportFormatFromFilled(1, new Rewards(1, 1, 1));