package puzzle; import module java.base; import lombok.val; import org.junit.jupiter.api.Test; import puzzle.Export.Clued; import static org.junit.jupiter.api.Assertions.*; import static puzzle.Masker.Clues; import static puzzle.SwedishGenerator.*; import static puzzle.Masker.Slot; public class MaskerCluesTest { @Test void testValidRandomMask() { Rng rng = new Rng(42); Masker masker = new Masker(rng, new int[STACK_SIZE], Clues.createEmpty()); for (int i = 0; i < 200; i++) { for (int j = 19; j < 24; j++) { var clues = masker.randomMask(j); assertTrue(clues.isValid(MIN_LEN), "Mask should be valid for length \n" + new Clued(clues).gridToString()); } } } @Test void testValidMutate() { Rng rng = new Rng(42); var cache = Clues.createEmpty(); Masker masker = new Masker(rng, new int[STACK_SIZE], cache); double sim = 0.0; double simCount = 0.0; for (int i = 0; i < 200; i++) { for (int j = 19; j < 24; j++) { var clues = masker.randomMask(j); val orig = cache.from(clues); simCount++; masker.mutate(clues); sim += orig.similarity(clues); assertTrue(clues.isValid(MIN_LEN), "Mask should be valid for length \n" + new Clued(clues).gridToString()); } } System.out.println("Average similarity: " + sim / simCount); } @Test void testCross() { Rng rng = new Rng(42); var cache = Clues.createEmpty(); Masker masker = new Masker(rng, new int[STACK_SIZE], cache); double sim = 0.0; double simCount = 0.0; for (int i = 0; i < 200; i++) { for (int j = 19; j < 24; j++) { var clues = masker.randomMask(j); var clues2 = masker.randomMask(j); simCount++; var cross = masker.crossover(clues, clues2); sim += Math.max(cross.similarity(clues), cross.similarity(clues2)); assertTrue(cross.isValid(MIN_LEN), "Mask should be valid for length \n" + new Clued(cross).gridToString()); } } System.out.println("Average similarity: " + sim / simCount); } @Test void testSimilarity() { Clues a = Clues.createEmpty(); a.setClueLo(1L << 0, (byte) 1); a.setClueLo(1L << 10, (byte) 0); Clues b = Clues.createEmpty(); b.setClueLo(1L << 0, (byte) 1); b.setClueLo(1L << 10, (byte) 0); // Identity assertEquals(1.0, a.similarity(b), 0.001); // Different direction Clues c = Clues.createEmpty(); c.setClueLo(1L << 0, (byte) 0); c.setClueLo(1L << 10, (byte) 0); assertTrue(a.similarity(c) < 1.0); // Completely different Clues d = Clues.createEmpty(); // Matching empty cells count towards similarity. // a has 2 clues, d has 0. They match on 70 empty cells. assertEquals(70.0 / 72.0, a.similarity(d), 0.001); } @Test void testIsValid() { Clues g = Clues.createEmpty(); assertTrue(g.isValid(MIN_LEN)); // Valid clue: Right from (0,0) in 9x8 grid. Length is 8. g.setClueLo(1L << Masker.offset(0, 0), (byte) 1); assertTrue(g.isValid(MIN_LEN)); // Invalid clue: Right from (0,7) in 9x8 grid. Length is 1 (too short if MIN_LEN >= 2). Clues g2 = Clues.createEmpty(); g2.setClueLo(1L << Masker.offset(0, 7), (byte) 1); assertFalse(g2.isValid(MIN_LEN)); } @Test void testHasRoomForClue() { Clues g = Clues.createEmpty(); // Room for Right clue at (0,0) (length 8) assertTrue(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 0), 1))); // No room for Right clue at (0,8) (length 0 < MIN_LEN) assertFalse(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 8), 1))); // Blocked room // Let's place a clue that leaves only 1 cell for another clue. g.setClueLo(1L << Masker.offset(0, 2), (byte) 1); // Now Right at (0,0) only has (0,1) available -> length 1 < MIN_LEN (which is 2) assertFalse(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 0), 1))); // But enough room g.clearClueLo(0L); g.setClueLo(1L << Masker.offset(0, 3), (byte) 1); // Now Right at (0,0) has (0,1), (0,2) -> length 2 == MIN_LEN assertTrue(g.hasRoomForClue(Slot.packSlotKey(Masker.offset(0, 0), 1))); } @Test void testIntersectionConstraint() { Clues g = Clues.createEmpty(); // Clue 1: (0,0) Right. Slot cells: (0,1), (0,2), (0,3), (0,4), (0,5), (0,6), (0,7), (0,8) g.setClueLo(1L << Masker.offset(0, 0), (byte) 1); // Clue 2: (1,2) Up. Slot cells: (0,2) // Intersection is exactly 1 cell (0,2). Valid. g.setClueLo(1L << Masker.offset(2, 2), (byte) 2); assertTrue(g.isValid(MIN_LEN)); // Clue 3: (1,1) Right. Slot cells: (1,2), (1,3), ... // No intersection with Clue 1 or 2. Valid. g.setClueLo(1L << Masker.offset(1, 1), (byte) 1); assertTrue(g.isValid(MIN_LEN)); // Now create a violation: two slots sharing 2 cells. // We can do this with Corner Down and another clue. // Clue A: (0,0) Corner Down. Starts at (0,1) goes down: (0,1), (1,1), (2,1), (3,1), ... // Clue B: (0,2) Corner Down Left. Starts at (0,1) goes down: (0,1), (1,1), (2,1), ... // They share MANY cells starting from (0,1). Clues g3 = Clues.createEmpty(); g3.setClueLo(1L << Masker.offset(0, 0), (byte) 4); // Corner Down g3.setClueLo(1L << Masker.offset(0, 2), (byte) 5); // Corner Down Left assertFalse(g3.isValid(MIN_LEN)); } @Test void testInvalidDirectionBits() { Clues g = Clues.createEmpty(); // Dir 6 (x=1, r=1, v=0) is invalid g.setClueLo(1L << 0, (byte) 6); assertFalse(g.isValid(MIN_LEN)); // Dir 7 (x=1, r=1, v=1) is invalid Clues g2 = Clues.createEmpty(); g2.setClueLo(1L << 0, (byte) 7); assertFalse(g2.isValid(MIN_LEN)); } }