package puzzle; import module java.base; import anno.GenerateNeighbor; import anno.GenerateNeighbors; import anno.LemmaGen; import lombok.val; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import precomp.Neighbors9x8; import puzzle.DictJavaGeneratorMulti.DictEntryDTO.IntListDTO; import precomp.Mask; import puzzle.Export.Puzzle; import puzzle.Riddle.Signa; import puzzle.Masker.Slot; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static precomp.Const9x8.CLUE_DOWN0; import static precomp.Const9x8.CLUE_LEFT3; import static precomp.Const9x8.CLUE_RIGHT1; import static precomp.Const9x8.CLUE_UP2; import static precomp.Const9x8.Cell.r0c0; import static precomp.Const9x8.Cell.r0c0d0; import static precomp.Const9x8.Cell.r0c0d1; import static precomp.Const9x8.Cell.r0c0d3; import static precomp.Const9x8.Cell.r0c1; import static precomp.Const9x8.Cell.r0c2; import static precomp.Const9x8.Cell.r0c3; import static precomp.Const9x8.Cell.r2c0; import static precomp.Const9x8.Cell.r2c3d0; import static precomp.Const9x8.Cell.r2c5; import static precomp.Const9x8.Cell.r3c5; import static precomp.Const9x8.Cell.r4c5; import static precomp.Const9x8.LETTER_A; import static precomp.Const9x8.LETTER_B; import static precomp.Const9x8.LETTER_C; import static precomp.Const9x8.LETTER_E; import static precomp.Const9x8.LETTER_I; import static precomp.Const9x8.LETTER_N; import static precomp.Const9x8.LETTER_R; import static precomp.Const9x8.LETTER_X; import static precomp.Const9x8.LETTER_Z; import static precomp.Const9x8.OFF_0_0; import static precomp.Const9x8.OFF_0_1; import static precomp.Const9x8.OFF_0_2; import static precomp.Const9x8.OFF_0_3; import static precomp.Const9x8.OFF_2_3; import static puzzle.LemmaData.ABC; import static puzzle.LemmaData.ABD; import static puzzle.LemmaData.APPLE; import static puzzle.LemmaData.APPLY; import static puzzle.LemmaData.AT; import static puzzle.LemmaData.AZ; import static puzzle.LemmaData.BANAN; import static puzzle.LemmaData.BANANA; import static puzzle.LemmaData.BANANAS; import static puzzle.LemmaData.BANANASS; import static puzzle.LemmaData.CAT; import static puzzle.LemmaData.DOGS; import static puzzle.LemmaData.EXE; import static puzzle.LemmaData.IN; import static puzzle.LemmaData.INE; import static puzzle.LemmaData.INER; import static puzzle.LemmaData.INEREN; import static puzzle.LemmaData.INERENA; import static puzzle.LemmaData.INERENAE; import static puzzle.LemmaData.WORD_A; import static puzzle.LemmaData.WORD_C; import static puzzle.LemmaData.WORD_X; import static puzzle.SwedishGenerator.Grid; import static puzzle.SwedishGenerator.Lemma; import static puzzle.SwedishGenerator.Rng; import static puzzle.SwedishGenerator.Slotinfo; import static puzzle.SwedishGenerator.X; import static puzzle.SwedishGenerator.candidateCountForPattern; import static puzzle.SwedishGenerator.candidateInfoForPattern; import static puzzle.SwedishGenerator.patternForSlot; @GenerateNeighbors(@GenerateNeighbor(C = 3, R = 4, packageName = "precomp", className = "Neighbors3x4", MIN_LEN = 2)) @LemmaGen( packageName = "puzzle", className = "LemmaData", words = { "EEN", "NAAR", "IEDEREEN", "A", "C", "X", "TEST", "IN", "INE", "INER", "INEREN", "INERENA", "INERENAE", "APPLE", "AXE", "ABC", "ABD", "AZ", "AB", "AT", "CAT", "DOGS", "APPLY", "BANAN", "BANANA", "BANANAS", "BANANASS" }, minLen = 2, maxLen = 8 ) public class SwedishGeneratorTest { static Grid createEmpty9x8() { return new Grid(new byte[Neighbors9x8.SIZE], X, X); } record Context(long[] bitset) { public Context() { this(new long[2500]); } private static final ThreadLocal CTX = ThreadLocal.withInitial(Context::new); public static Context get() { return CTX.get(); } } static final long[] WORDS = new long[]{ AT, CAT, DOGS, APPLE, APPLY, BANAN, BANANA, BANANAS, BANANASS }; static final long[] WORDS2 = new long[]{ IN, APPLE, APPLY, BANAN, INE, INER, INEREN, INERENA, INERENAE }; @Test void testPatternForSlotAllLetters() { var grid = new Puzzle(Riddle.Signa.of(r0c0d1)); GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c1.or(r0c2).or(r0c3).lo(), X, ABC); val map = grid.sync().collect(Collectors.toMap(Mask::index, Mask::d)); assertEquals(LETTER_A, map.get(OFF_0_1)); assertEquals(LETTER_B, map.get(OFF_0_2)); assertEquals(LETTER_C, map.get(OFF_0_3)); } @Test void testPatternForSlotMixed() { var grid = createEmpty9x8(); GridBuilder.placeWord(grid, grid.g, r0c0d1.slotKey, r0c0.mask, X, WORD_A); GridBuilder.placeWord(grid, grid.g, r0c0d1.slotKey, r2c0.mask, X, WORD_C); var pattern = patternForSlot(grid.lo, grid.hi, grid.g, 7L, X); assertEquals(14081L, pattern); } @Test void testPatternForSlotAllDashes() { var grid = createEmpty9x8(); var pattern = patternForSlot(grid.lo, grid.hi, grid.g, 7L, X); assertEquals(X, pattern); } @Test void testPatternForSlotSingleLetter() { var grid = createEmpty9x8(); GridBuilder.placeWord(grid, grid.g, r0c0d1.slotKey, r0c0.mask, X, WORD_A); var pattern = patternForSlot(grid.lo, grid.hi, grid.g, 7L, X); assertEquals(1L, pattern); } @Test void testGrid() { var p = new Puzzle(Clues.createEmpty()); GridBuilder.placeWord(p.grid(), p.grid().g, r0c0d1.slotKey, r0c0d0.mask, X, WORD_A); val arr = p.sync().collect(Collectors.toMap(Mask::index, Mask::d)); assertEquals(1, arr.size()); assertEquals(LETTER_A, arr.get(OFF_0_0)); } @Test void testIntList() { var list = new IntListDTO(); assertEquals(0, list.size()); for (var i = 0; i < 10; i++) { list.add(i); } assertEquals(10, list.size()); assertEquals(0, list.data()[0]); assertEquals(9, list.data()[9]); } @Test void testLemmaAndDict() { Assertions.assertEquals(Lemma.packShiftIn("APPLE".getBytes(StandardCharsets.US_ASCII)), Lemma.unpackLetters(APPLE)); assertEquals(4, Lemma.unpackSize(APPLE)); assertEquals(LETTER_I, Lemma.byteAt(INERENAE, 0)); assertEquals(LETTER_N, Lemma.byteAt(INERENAE, 1)); assertEquals(LETTER_E, Lemma.byteAt(INERENAE, 2)); assertEquals(LETTER_R, Lemma.byteAt(INERENAE, 3)); assertEquals(LETTER_E, Lemma.byteAt(INERENAE, 4)); assertEquals(LETTER_N, Lemma.byteAt(INERENAE, 5)); assertEquals(LETTER_A, Lemma.byteAt(INERENAE, 6)); assertEquals(LETTER_E, Lemma.byteAt(INERENAE, 7)); var dict = DictJavaGeneratorMulti.Dicts.makeDict(new long[]{ APPLE, EXE, IN, INER, INEREN, INERENA, INERENAE }); assertEquals(1, dict.index()[3].words().length); assertEquals(1, dict.index()[5].words().length); var entry3 = dict.index()[3]; assertEquals(1, entry3.words().length); assertEquals(Lemma.packShiftIn("AXE".getBytes(StandardCharsets.US_ASCII)), Lemma.unpackLetters(entry3.words()[0])); } @Test void testSlot() { assertEquals(OFF_2_3, Slot.clueIndex(r2c3d0.slotKey)); assertEquals(CLUE_DOWN0, Slot.dir(r2c3d0.slotKey)); assertFalse(Slot.horiz(r2c3d0.slotKey)); var cells = Riddle.cellWalk(r2c3d0.slotKey, r2c5.or(r3c5).or(r4c5).lo(), 0L).mapToObj(i -> Masker.IT[i]).toArray(rci[]::new); assertEquals(2, cells[1].r()); assertEquals(5, cells[1].c()); assertEquals(3, cells[2].r()); assertEquals(5, cells[2].c()); assertEquals(4, cells[3].r()); assertEquals(5, cells[3].c()); assertTrue(Slot.horiz(CLUE_RIGHT1)); // right assertFalse(Slot.horiz(CLUE_DOWN0)); // down } static long packPattern(String s) { long p = 0; var b = s.getBytes(StandardCharsets.US_ASCII); for (var i = 0; i < b.length; i++) { var val = b[i] & 31; if (val != 0) { p |= (i * 26L + val) << (i << 3); } } return p; } @Test void testCandidateInfoForPattern() { var dict = DictJavaGeneratorMulti.Dicts.makeDict(WORDS2); // Pattern "APP--" for length 5 var info = candidateInfoForPattern(Context.get().bitset(), packPattern("APP"), dict.index()[5].posBitsets(), dict.index()[5].numlong()); assertEquals(2, info.length); assertNotNull(info); } @Test void testForEachSlotAndExtractSlots() { // This should detect a slot starting at 0,1 with length 2 (0,1 and 0,2) var dict = DictJavaGeneratorMulti.Dicts.makeDict(WORDS2); var slots = Masker.extractSlots(Riddle.Signa.of(r0c0d1).c(), dict.index(), dict.reversed()); assertEquals(1, slots.length); var s = slots[0]; assertTrue(Slot.length(s.lo(), s.hi()) >= 2); assertEquals(OFF_0_0, Slot.clueIndex(s.key())); assertEquals(CLUE_RIGHT1, Slot.dir(s.key())); } @Test void testMaskFitnessBasic() { var gen = new Masker(new Rng(0), new int[Masker.STACK_SIZE], Clues.createEmpty()); var grid = Clues.createEmpty(); // Empty grid should have high penalty (no slots) var f1 = gen.maskFitness(grid, 18); assertTrue(f1 >= 1_000_000_000L); // Add a slot grid.setClue(r0c0d1); var f2 = gen.maskFitness(grid, 18); assertTrue(f2 < f1); } @Test void testGeneticAlgorithmComponents() { var gen = new Masker(new Rng(42), new int[Masker.STACK_SIZE], Clues.createEmpty()); var c1 = new Signa(gen.randomMask(18)); assertNotNull(c1); var g2 = new Signa(gen.mutate(c1.deepCopyGrid().c())); assertNotNull(g2); assertNotSame(c1.c(), g2.c()); assertNotNull(gen.crossover(c1.c(), g2.c())); assertNotNull(gen.hillclimb(c1.c(), 18, 10)); } @Test void testPlaceWord() { var grid = new Puzzle(Clues.createEmpty()); assertTrue(GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0.or(r0c1).or(r0c2).lo(), X, ABC)); var map = new Puzzle(grid.grid(), grid.cl()).collect(Collectors.toMap(Mask::index, Mask::d)); assertEquals(3, map.size()); assertEquals(LETTER_A, map.get(OFF_0_0)); assertEquals(LETTER_B, map.get(OFF_0_1)); assertEquals(LETTER_C, map.get(OFF_0_2)); // 2. Successful placement with partial overlap (same characters) assertTrue(GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0.or(r0c1).or(r0c2).lo(), X, ABC)); // 3. Conflict: place "ABD" where "ABC" is assertFalse(GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0.or(r0c1).or(r0c2).lo(), X, ABD)); // Verify grid is unchanged (still "ABC") map = new Puzzle(grid.grid(), grid.cl()).collect(Collectors.toMap(Mask::index, Mask::d)); assertEquals(3, map.size()); assertEquals(LETTER_A, map.get(OFF_0_0)); assertEquals(LETTER_B, map.get(OFF_0_1)); assertEquals(LETTER_C, map.get(OFF_0_2)); // 4. Partial placement then conflict (rollback) grid = new Puzzle(Clues.createEmpty()); GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, 1L << OFF_0_2, 0, WORD_X); // Conflict at the end assertFalse(GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0.or(r0c1).or(r0c2).lo(), X, ABC)); map = new Puzzle(grid.grid(), grid.cl()).collect(Collectors.toMap(Mask::index, Mask::d)); assertEquals(1, map.size()); assertEquals(LETTER_X, map.get(OFF_0_2)); } @Test void testBacktrackingHelpers() { var grid = new Puzzle(Clues.createEmpty()); // Slot at 0,1 length 2 val low = grid.grid().lo; val top = grid.grid().hi; var placed = GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c1.or(r0c2).lo(), X, AZ); assertTrue(placed); var map = new Puzzle(grid.grid(), grid.cl()).collect(Collectors.toMap(Mask::index, Mask::d)); assertEquals(2, map.size()); assertEquals(LETTER_A, map.get(OFF_0_1)); assertEquals(LETTER_Z, map.get(OFF_0_2)); grid.grid().hi = top; grid.grid().lo = low; map = grid.collect(Collectors.toMap(Mask::index, Mask::d)); assertEquals(0, map.size()); assertFalse(map.containsKey(OFF_0_1)); assertFalse(map.containsKey(OFF_0_2)); } @Test void testInnerWorkings() { // 1. Test Slot.increasing assertFalse(Slotinfo.increasing(CLUE_LEFT3)); // Left assertTrue(Slotinfo.increasing(CLUE_RIGHT1)); // Right assertTrue(Slotinfo.increasing(CLUE_DOWN0)); // Down assertFalse(Slotinfo.increasing(CLUE_UP2)); // Up assertTrue(Slotinfo.increasing(r0c0d1.slotKey)); assertFalse(Slotinfo.increasing(r0c0d3.slotKey)); // 2. Test slotScore val counts = new byte[Neighbors9x8.SIZE]; counts[1] = 2; counts[2] = 3; var dict = DictJavaGeneratorMulti.Dicts.makeDict(WORDS); var entry5 = dict.index()[5]; // cross = (counts[1]-1) + (counts[2]-1) = 1 + 2 = 3 // score = 3 * 10 + len(2) = 32 assertEquals(32, Masker.slotScore(counts, (1L << 1) | (1L << 2), 0L)); // 3. Test candidateCountForPattern var ctx = Context.get(); var pattern = packPattern("APP"); assertEquals(2, candidateCountForPattern(ctx.bitset(), pattern, entry5.posBitsets(), entry5.numlong())); pattern = packPattern("BAN"); assertEquals(1, candidateCountForPattern(ctx.bitset(), pattern, entry5.posBitsets(), entry5.numlong())); pattern = packPattern("CAT"); assertEquals(0, candidateCountForPattern(ctx.bitset(), pattern, entry5.posBitsets(), entry5.numlong())); } @Test void testMaskFitnessDetailed() { var gen = new Masker(new Rng(42), new int[Masker.STACK_SIZE], Clues.createEmpty()); var grid = Clues.createEmpty(); // Empty grid: huge penalty var fitEmpty = gen.maskFitness(grid, 18); assertTrue(fitEmpty >= 1_000_000_000L); grid.setClue(r0c0d1); // Right from 0,0. Len 2 if 3x3. var fitOne = gen.maskFitness(grid, 18); assertTrue(fitOne < fitEmpty); } @Test void testRng() { var rng = new Rng(123); var val1 = rng.nextU32(); var val2 = rng.nextU32(); assertNotEquals(val1, val2); var rng2 = new Rng(123); assertEquals(val1, rng2.nextU32()); for (var i = 0; i < 100; i++) { var r = rng.randomClueDir(); assertTrue(r >= 0 && r <= 5); var f = rng.nextFloat(); assertTrue(f >= 0.0 && f <= 1.0); } assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100); assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100); assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100); assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100); assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100); assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100); assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100); } }