406 lines
16 KiB
Java
406 lines
16 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 puzzle.DictJavaGeneratorMulti.DictEntryDTO.IntListDTO;
|
|
import puzzle.Export.Lettrix;
|
|
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 = 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(Riddle.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 = 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 = 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], 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);
|
|
}
|
|
|
|
} |