Files
puzzle-generator/src/test/java/puzzle/SwedishGeneratorTest.java
2026-01-23 01:55:12 +01:00

369 lines
14 KiB
Java

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 precomp.Neighbors9x8.rci;
import puzzle.DictJavaGeneratorMulti.DictEntryDTO.IntListDTO;
import puzzle.Export.Signa;
import puzzle.Export.Puzzle;
import puzzle.Export.Lettrix;
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.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.*;
@GenerateNeighbors(@GenerateNeighbor(C = 4, R = 3, packageName = "precomp", className = "Neighbors4x3", 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<Context> 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(Signa.of(r0c0d1));
GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c1.or(r0c2).or(r0c3).lo(), X, ABC);
val map = grid.collect(Collectors.toMap(Lettrix::index, Lettrix::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 = 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 grid = new Puzzle(Clues.createEmpty());
GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0d0.mask, X, WORD_A);
val arr = grid.collect(Collectors.toMap(Lettrix::index, Lettrix::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() {
assertEquals(OFF_2_3, Slot.clueIndex(r2c3d0.slotKey));
assertEquals(CLUE_DOWN0, Slot.dir(r2c3d0.slotKey));
assertFalse(Slot.horiz(r2c3d0.slotKey));
var cells = Puzzle.cellWalk((byte) r2c3d0.slotKey, r2c5.or(r3c5).or(r4c5).lo(), 0L).mapToObj(i -> Masker.IT[i]).toArray(rci[]::new);
assertEquals(2, cells[0].r());
assertEquals(3, cells[1].r());
assertEquals(4, cells[2].r());
assertEquals(5, cells[0].c());
assertEquals(5, cells[1].c());
assertEquals(5, cells[2].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(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], 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 gen = new Masker(new Rng(42), new int[Masker.STACK_SIZE], Masker.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 = grid.collect(Collectors.toMap(Lettrix::index, Lettrix::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, 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 = grid.collect(Collectors.toMap(Lettrix::index, Lettrix::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 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 = grid.stream().collect(Collectors.toMap(Lettrix::index, Lettrix::letter));
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 = grid.collect(Collectors.toMap(Lettrix::index, Lettrix::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.collect(Collectors.toMap(Lettrix::index, Lettrix::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_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], 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);
}
@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);
}
}