package puzzle; import module java.base; import lombok.val; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import puzzle.Export.Clued; import puzzle.Export.Gridded; import puzzle.DictJavaGeneratorMulti.DictEntryDTO.IntListDTO; import puzzle.Export.LetterVisit.LetterAt; import puzzle.Masker.Clues; import puzzle.Masker.Slot; import static org.junit.jupiter.api.Assertions.*; import static precomp.Const9x8.*; import static precomp.Const9x8.Cell.*; import static puzzle.SwedishGenerator.*; public class SwedishGeneratorTest { static Grid createEmpty() { return new Grid(new byte[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 TEST = Lemma.from("TEST"); static final long IN = Lemma.from("IN"); static final long INER = Lemma.from("INER"); static final long INEREN = Lemma.from("INEREN"); static final long INERENA = Lemma.from("INERENA"); static final long INERENAE = Lemma.from("INERENAE"); static final long APPLE = Lemma.from("APPLE"); static final long EXE = Lemma.from("AXE"); static final long ABC = Lemma.from("ABC"); static final long ABD = Lemma.from("ABD"); static final long AZ = Lemma.from("AZ"); static final long AB = Lemma.from("AB"); static final long[] WORDS = new long[]{ Lemma.from("AT"), Lemma.from("CAT"), Lemma.from("DOGS"), APPLE, Lemma.from("APPLY"), Lemma.from("BANAN"), Lemma.from("BANANA"), Lemma.from("BANANAS"), Lemma.from("BANANASS") // length 8 }; static final long[] WORDS2 = new long[]{ IN, APPLE, Lemma.from("APPLY"), Lemma.from("BANAN"), Lemma.from("INE"), INER, INEREN, INERENA, INERENAE }; static final byte LETTER_A = ((byte) 'A') & 31; static final byte LETTER_P = ((byte) 'P') & 31; static final byte LETTER_L = ((byte) 'L') & 31; static final byte LETTER_B = ((byte) 'B') & 31; static final byte LETTER_C = ((byte) 'C') & 31; static final byte LETTER_E = ((byte) 'E') & 31; static final byte LETTER_I = ((byte) 'I') & 31; static final byte LETTER_N = ((byte) 'N') & 31; static final byte LETTER_X = ((byte) 'X') & 31; static final byte LETTER_R = ((byte) 'R') & 31; static final byte LETTER_Z = ((byte) 'Z') & 31; static final byte CLUE_DOWN = 0; static final byte CLUE_RIGHT = 1; static final byte CLUE_UP = 2; static final byte CLUE_LEFT = 3; static final byte D_BYTE_2 = CLUE_RIGHT; @Test void testPatternForSlotAllLetters() { var key = r0c0d1.slotKey; val clues = Clues.of(r0c0d1); var grid = new Gridded(clues); GridBuilder.placeWord(grid.grid(), grid.grid().g, key, (1L << OFF_0_1) | (1L << OFF_0_2) | (1L << OFF_0_3), 0L, ABC); val map = grid.stream(clues).collect(Collectors.toMap(LetterAt::index, LetterAt::letter)); 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 = createEmpty(); GridBuilder.placeWord(grid, grid.g, r0c0d1.slotKey, 1L << OFF_0_0, 0, Lemma.from("A")); GridBuilder.placeWord(grid, grid.g, r0c0d1.slotKey, 1L << OFF_2_0, 0, Lemma.from("C")); var key = Slot.packSlotKey(OFF_1_0, CLUE_RIGHT); var pattern = patternForSlot(grid.lo, grid.hi, grid.g, key, 7L, 0L); assertEquals(14081L, pattern); } @Test void testPatternForSlotAllDashes() { var grid = createEmpty(); var key = Slot.packSlotKey(1 << Slot.BIT_FOR_DIR, CLUE_RIGHT); var pattern = patternForSlot(grid.lo, grid.hi, grid.g, key, 7L, 0L); assertEquals(0L, pattern); } @Test void testPatternForSlotSingleLetter() { var grid = createEmpty(); //Slot.packSlotKey(0, CLUE_RIGHT) GridBuilder.placeWord(grid, grid.g, r0c0d1.slotKey, 1L << OFF_0_0, 0, Lemma.from("A")); var key = Slot.packSlotKey(1, CLUE_RIGHT); var pattern = patternForSlot(grid.lo, grid.hi, grid.g, key, 7L, 0L); assertEquals(1L, pattern); } @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); } @Test void testGrid() { var empty = Clues.createEmpty(); var grid = new Gridded(empty); GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, 1L << OFF_0_0, 0, Lemma.from("A")); val arr = grid.stream(empty).collect(Collectors.toMap(LetterAt::index, LetterAt::letter)); 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() { System.out.println("[DEBUG_LOG] Slot.BIT_FOR_DIR = " + Slot.BIT_FOR_DIR); // key = (r << 8) | (c << 4) | d var offset = OFF_2_3; System.out.println("[DEBUG_LOG] Grid.offset(2, 3) = " + offset); var key = Slot.packSlotKey(offset, CLUE_DOWN); System.out.println("[DEBUG_LOG] key = " + key); long lo = 0; // pos 0: (2, 5) lo |= 1L << OFF_2_5; // pos 1: (3, 5) lo |= 1L << OFF_3_5; // pos 2: (4, 5) lo |= 1L << OFF_4_5; System.out.println("[DEBUG_LOG] s.dir() = " + Slot.dir(key)); assertEquals(OFF_2_3, Slot.clueIndex(key)); assertEquals(CLUE_DOWN, Slot.dir(key)); assertFalse(Slot.horiz(key)); var cells = Gridded.cellWalk((byte) key, lo, 0L).toArray(); assertEquals(2, Masker.IT[cells[0]].r()); assertEquals(3, Masker.IT[cells[1]].r()); assertEquals(4, Masker.IT[cells[2]].r()); assertEquals(5, Masker.IT[cells[0]].c()); assertEquals(5, Masker.IT[cells[1]].c()); assertEquals(5, Masker.IT[cells[2]].c()); assertTrue(Slot.horiz(CLUE_RIGHT)); // right assertFalse(Slot.horiz(CLUE_DOWN)); // 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 clues = Clues.of(r0c0d1); var dict = DictJavaGeneratorMulti.Dicts.makeDict(WORDS2); var slots = Masker.extractSlots(clues, dict.index()); 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_RIGHT, Slot.dir(s.key())); } @Test void testMaskFitnessBasic() { var gen = new Masker(new Rng(0), new int[STACK_SIZE], Masker.Clues.createEmpty()); var grid = Masker.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 rng = new Rng(42); var gen = new Masker(rng, new int[STACK_SIZE], Masker.Clues.createEmpty()); var c1 = new Clued(gen.randomMask(18)); assertNotNull(c1); var g2 = new Clued(gen.mutate(c1.deepCopyGrid().c())); assertNotNull(g2); assertNotSame(c1.c(), g2.c()); assertNotNull(gen.crossover(c1.c(), g2.c())); var g4 = gen.hillclimb(c1.c(), 18, 10); assertNotNull(g4); } @Test void testPlaceWord() { var empty = Clues.createEmpty(); var grid = new Gridded(empty); // Slot at OFF_0_0 length 3, horizontal (right) var key = Slot.packSlotKey(0, CLUE_RIGHT); var lo = (1L << OFF_0_0) | (1L << OFF_0_1) | (1L << OFF_0_2); val hi = 0L; var w1 = ABC; // 1. Successful placement in empty grid assertTrue(GridBuilder.placeWord(grid.grid(), grid.grid().g, key, lo, hi, w1)); var map = grid.stream(empty).collect(Collectors.toMap(LetterAt::index, LetterAt::letter)); 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, key, lo, hi, w1)); // 3. Conflict: place "ABD" where "ABC" is assertFalse(GridBuilder.placeWord(grid.grid(), grid.grid().g, key, lo, hi, ABD)); // Verify grid is unchanged (still "ABC") map = grid.stream(Masker.Clues.createEmpty()).collect(Collectors.toMap(LetterAt::index, LetterAt::letter)); 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 Gridded(Clues.createEmpty()); GridBuilder.placeWord(grid.grid(), grid.grid().g, Slot.packSlotKey(0, CLUE_RIGHT), 1L << OFF_0_2, 0, Lemma.from("X")); // Conflict at the end assertFalse(GridBuilder.placeWord(grid.grid(), grid.grid().g, key, lo, hi, w1)); map = grid.stream(Masker.Clues.createEmpty()).collect(Collectors.toMap(LetterAt::index, LetterAt::letter)); assertEquals(1, map.size()); assertEquals(LETTER_X, map.get(OFF_0_2)); } @Test void testBacktrackingHelpers() { var clues = Clues.createEmpty(); var grid = new Gridded(clues); // Slot at 0,1 length 2 var key = Slot.packSlotKey(0, CLUE_RIGHT); var lo = (1L << OFF_0_1) | (1L << OFF_0_2); var w = AZ; val low = grid.grid().lo; val top = grid.grid().hi; var placed = GridBuilder.placeWord(grid.grid(), grid.grid().g, key, lo, 0L, w); assertTrue(placed); var map = grid.stream(clues).collect(Collectors.toMap(LetterAt::index, LetterAt::letter)); 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.stream(Masker.Clues.createEmpty()).collect(Collectors.toMap(LetterAt::index, LetterAt::letter)); 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_LEFT)); // Left assertTrue(Slotinfo.increasing(CLUE_RIGHT)); // Right assertTrue(Slotinfo.increasing(CLUE_DOWN)); // Down assertFalse(Slotinfo.increasing(CLUE_UP)); // Up assertTrue(Slotinfo.increasing(Slot.packSlotKey(0, CLUE_RIGHT))); assertFalse(Slotinfo.increasing(Slot.packSlotKey(0, CLUE_LEFT))); // 2. Test slotScore val counts = new byte[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[STACK_SIZE], Masker.Clues.createEmpty()); var grid = Masker.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); } }