diff --git a/src/main/java/puzzle/SwedishGenerator.java b/src/main/java/puzzle/SwedishGenerator.java index 74efb36..6694ed3 100644 --- a/src/main/java/puzzle/SwedishGenerator.java +++ b/src/main/java/puzzle/SwedishGenerator.java @@ -30,7 +30,12 @@ public record SwedishGenerator(int[] buff) { record nbrs_8(int r, int c) { } - record nbrs_16(int r, int c, int dr, int dc) { } + record nbrs_16(int r, int c, int dr, int dc, int d, byte dbyte) { + + public nbrs_16(int r, int c, int dr, int dc, int d) { + this(r, c, dr, dc, d, (byte) (48 + d)); + } + } static final int C = Config.PUZZLE_COLS; static final double CROSS_Y = (C - 1) / 2.0; @@ -54,12 +59,12 @@ public record SwedishGenerator(int[] buff) { // Directions for '1'..'6' static final nbrs_16[] OFFSETS = new nbrs_16[]{ null, - new nbrs_16(-1, 0, -1, 0), // 1: up - new nbrs_16(0, 1, 0, 1), // 2: right - new nbrs_16(1, 0, 1, 0),// 3: down - new nbrs_16(0, -1, 0, -1),// 4: left - new nbrs_16(0, -1, 1, 0),// 5: vertical down, clue is on the right of the first letter - new nbrs_16(0, 1, 1, 0)// 6: vertical down, clue is on the left of the first letter + new nbrs_16(-1, 0, -1, 0, 1), // 1: up + new nbrs_16(0, 1, 0, 1, 2), // 2: right + new nbrs_16(1, 0, 1, 0, 3),// 3: down + new nbrs_16(0, -1, 0, -1, 4),// 4: left + new nbrs_16(0, -1, 1, 0, 5),// 5: vertical down, clue is on the right of the first letter + new nbrs_16(0, 1, 1, 0, 6)// 6: vertical down, clue is on the left of the first letter }; final static nbrs_8[] nbrs8 = new nbrs_8[]{ new nbrs_8(-1, -1), @@ -110,6 +115,11 @@ public record SwedishGenerator(int[] buff) { x = y; return y; } + byte randbyte(int min, int max) { + var u = (nextU32() & 0xFFFFFFFFL); + var range = (long) max - (long) min + 1L; + return (byte) (min + (u % range)); + } int randint(int min, int max) { var u = (nextU32() & 0xFFFFFFFFL); var range = (long) max - (long) min + 1L; @@ -118,18 +128,22 @@ public record SwedishGenerator(int[] buff) { double nextFloat() { return (nextU32() & 0xFFFFFFFFL) / 4294967295.0; } } + static final byte _48 = 48; + record Grid(byte[] g) { - + int digitAt(int r, int c) { return g[offset(r, c)] - 48; } public static int r(int offset) { return offset & 7; } public static int c(int offset) { return offset >>> 3; } static int offset(int r, int c) { return r | (c << 3); } Grid deepCopyGrid() { return new Grid(g.clone()); } char getCharAt(int r, int c) { return (char) (g[offset(r, c)]); } char getCharAt(int pos) { return (char) (g[pos]); } - int digitAt(int r, int c) { return g[offset(r, c)] - 48; } byte byteAt(int r, int c) { return g[offset(r, c)]; } + byte byteAt(int pos) { return g[pos]; } + byte byteAtStatic(int r, int c) { return g[offset(r, c)]; } void setCharAt(int r, int c, char ch) { g[offset(r, c)] = (byte) ch; } void setByteAt(int r, int c, byte ch) { g[offset(r, c)] = ch; } + void setDigitAt(int r, int c, int ch) { g[offset(r, c)] = (byte) (_48 + ch); } void setCharAt(int idx, char ch) { g[idx] = (byte) ch; } void setByteAt(int idx, byte ch) { g[idx] = ch; } void clear(int r, int c) { g[offset(r, c)] = 0; } @@ -391,7 +405,17 @@ public record SwedishGenerator(int[] buff) { forEachSlot(grid, (key, packedPos, len) -> slots.add(Slot.from(key, packedPos, len))); return slots; } - + boolean hasRoomForClue(Grid grid, int idx, nbrs_16 nbrs16) { + int rr = Grid.r(idx) + nbrs16.r, cc = Grid.c(idx) + nbrs16.c; + var run = 0; + while (rr >= 0 && rr < R && cc >= 0 && cc < C && (grid.isLettercell(rr, cc)) && run < MAX_WORD_LENGTH) { + run++; + rr += nbrs16.dr; + cc += nbrs16.dc; + if (run >= MIN_LEN) return true; + } + return false; + } boolean hasRoomForClue(Grid grid, int r, int c, char d) { var nbrs16 = OFFSETS[d - '0']; int rr = r + nbrs16.r, cc = c + nbrs16.c; @@ -516,7 +540,25 @@ public record SwedishGenerator(int[] buff) { } // ---------------- Mask generation ---------------- - + Grid randomMask2(Rng rng) { + var g = makeEmptyGrid(); + + int placed = 0, guard = 0; + + while (placed < TARGET_CLUES && guard++ < 4000) { + var idx = rng.randint(0, SIZE - 1); + if (g.isDigitAt(idx)) continue; + + var d = OFFSETS[rng.randbyte(1, 4)]; + g.setByteAt(idx, d.dbyte); + if (!hasRoomForClue(g, idx, d)) { + g.clear(idx); + continue; + } + placed++; + } + return g; + } Grid randomMask(Rng rng) { var g = makeEmptyGrid(); var targetClues = (int) Math.round(SIZE * 0.25); @@ -537,7 +579,26 @@ public record SwedishGenerator(int[] buff) { } return g; } - + Grid mutate2(Rng rng, Grid grid) { + var g = grid.deepCopyGrid(); + var cx = rng.randint(0, R - 1); + var cy = rng.randint(0, C - 1); + + var steps = 4; + for (var k = 0; k < steps; k++) { + var rr = clamp(cx + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, R - 1); + var cc = clamp(cy + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, C - 1); + + if (g.isDigitAt(rr, cc)) { + g.clear(rr, cc); + } else { + var d = OFFSETS[rng.randint(1, 4)]; + g.setByteAt(rr, cc, d.dbyte); + if (!hasRoomForClue(g, Grid.offset(rr, cc), d)) g.clear(rr, cc); + } + } + return g; + } Grid mutate(Rng rng, Grid grid) { var g = grid.deepCopyGrid(); var cx = rng.randint(0, R - 1); @@ -558,7 +619,6 @@ public record SwedishGenerator(int[] buff) { } return g; } - Grid crossover(Rng rng, Grid a, Grid b) { var out = makeEmptyGrid(); var theta = rng.nextFloat() * Math.PI; diff --git a/src/test/java/puzzle/MainTest.java b/src/test/java/puzzle/MainTest.java index 02819b3..2d5ed1b 100644 --- a/src/test/java/puzzle/MainTest.java +++ b/src/test/java/puzzle/MainTest.java @@ -145,57 +145,47 @@ public class MainTest { Assertions.assertTrue(grid.isDigitAt(1, 1)); } @Test - @Disabled public void testAttempt() { // Arrange var opts = new Main.Opts(); - //seed=1811328180 - opts.seed = -1645461655;// (int) (System.nanoTime() ^ System.currentTimeMillis()); - opts.pop = 18; // Small for micro-scale - opts.gens = 200; + opts.seed = 12347; + opts.pop = 4; // Tiny population + opts.gens = 20; // Very few generations opts.minSimplicity = 0; - opts.fillTimeout = 20_000; + opts.fillTimeout = 10_000; opts.threads = 1; opts.tries = 1; - opts.verbose = true; + opts.verbose = false; - // We need a small dictionary for testing - // Instead of loading from file, we might want a way to create a mock Dict - // But SwedishGenerator.loadWords(path) is what we have. - // Let's try to load a real one or a small subset if possible. - var dict = new Dict(new Lemma[]{ - new Lemma(0, "NU", 1, "NU"), - new Lemma(1, "ET", 2, "ET"), - new Lemma(2, "NUT", 3, "NUT"), - new Lemma(3, "ETE", 4, "ETE"), - new Lemma(4, "IK", 5, "IK"), - new Lemma(5, "IN", 6, "IN"), - new Lemma(6, "AU", 7, "AU"), - new Lemma(7, "JE", 8, "JE"), - new Lemma(8, "AI", 9, "AI"), - new Lemma(9, "NA", 10, "NA"), - new Lemma(10, "AF", 11, "AF"), - new Lemma(11, "AL", 14, "AL"), - new Lemma(12, "EA", 15, "EA"), - new Lemma(13, "AV", 18, "AV"), - new Lemma(14, "IL", 19, "IL"), - new Lemma(15, "EN", 22, "EN") - }); + var dict = SwedishGenerator.loadWords(opts.wordsPath); // Act - for (int i = 0; i < 200; i++) { - int seed = opts.seed + i; - var rng = new Rng(seed); - PuzzleResult res = Main.attempt(rng, dict, opts); - // Assert + PuzzleResult res = null; + int foundSeed = -1; + for (int i = 0; i < 50; i++) { + int seed = opts.seed + i; + var rng = new Rng(seed); + res = Main.attempt(rng, dict, opts); if (res != null && res.filled().ok()) { - System.out.println("Test Passed: Puzzle generated"); - System.out.println("Seed: " + seed); - System.out.print(indentLines(res.swe().renderHuman(res.filled().grid()), " ")); - return; + foundSeed = seed; + System.out.println("[DEBUG_LOG] Seed found: " + seed); + System.out.println("[DEBUG_LOG] Simplicity: " + res.filled().simplicity()); + System.out.println("[DEBUG_LOG] ClueMap Size: " + res.filled().clueMap().size()); + System.out.println("[DEBUG_LOG] Grid:"); + System.out.println(res.swe().renderHuman(res.filled().grid())); + break; } } - System.out.println("Test Note: Puzzle not generated in 1 attempt (this is possible depending on RNG)"); + + // Assert + Assertions.assertNotNull(res, "Puzzle generation failed (null result)"); + Assertions.assertTrue(res.filled().ok(), "Puzzle generation failed (not ok)"); + + // Regression baseline for seed search starting at 12347, pop 4, gens 20 + Assertions.assertEquals(12347, foundSeed, "Found seed changed"); + Assertions.assertEquals(20, res.filled().clueMap().size(), "Number of assigned words changed"); + Assertions.assertEquals(763.8, res.filled().simplicity(), 1e-9, "Simplicity value changed"); + Assertions.assertArrayEquals(new char[]{ 'M', 'A', 'N', 'T', 'A' }, res.filled().clueMap().get(1377).word()); } @Test public void testIsLetterA() {