introduce bitloops

This commit is contained in:
mike
2026-01-18 03:32:34 +01:00
parent 6daab5ef4e
commit b026ebfbd2
6 changed files with 194 additions and 74 deletions

View File

@@ -98,12 +98,12 @@ public record Export() {
public Stream<LetterAt> stream(Clues clues) { public Stream<LetterAt> stream(Clues clues) {
val stream = Stream.<LetterAt>builder(); val stream = Stream.<LetterAt>builder();
for (var l = grid.lo & ~clues.lo; l != X; l &= l - 1) stream.accept(LetterAt.from(Long.numberOfTrailingZeros(l), grid.g)); for (var l = grid.lo & ~clues.lo; l != X; l &= l - 1) stream.accept(LetterAt.from(Long.numberOfTrailingZeros(l), grid.g));
for (var h = grid.hi & ~clues.hi; h != X; h &= h - 1) stream.accept(LetterAt.from(64 | Long.numberOfTrailingZeros(h), grid.g)); for (var h = grid.hi & ~clues.hi & 0xFF; h != X; h &= h - 1) stream.accept(LetterAt.from(64 | Long.numberOfTrailingZeros(h), grid.g));
return stream.build(); return stream.build();
} }
public void forEachLetter(Clues clues, LetterVisit visitor) { public void forEachLetter(Clues clues, LetterVisit visitor) {
for (var l = grid.lo & ~clues.lo; l != X; l &= l - 1) visitor.visit(Long.numberOfTrailingZeros(l), grid.g); for (var l = grid.lo & ~clues.lo; l != X; l &= l - 1) visitor.visit(Long.numberOfTrailingZeros(l), grid.g);
for (var h = grid.hi & ~clues.hi; h != X; h &= h - 1) visitor.visit(64 | Long.numberOfTrailingZeros(h), grid.g); for (var h = grid.hi & ~clues.hi & 0xFF; h != X; h &= h - 1) visitor.visit(64 | Long.numberOfTrailingZeros(h), grid.g);
} }
public static IntStream walk(byte base, long lo, long hi) { public static IntStream walk(byte base, long lo, long hi) {
if (Slotinfo.increasing(base)) { if (Slotinfo.increasing(base)) {
@@ -243,7 +243,8 @@ public record Export() {
return new ExportedPuzzle(grid.exportGrid(clues.c, _ -> '#', '#'), new WordOut[0], difficulty, rewards); return new ExportedPuzzle(grid.exportGrid(clues.c, _ -> '#', '#'), new WordOut[0], difficulty, rewards);
} }
var placed = Arrays.stream(slots).map(slot -> new Placed(slot.assign().w, slot.key(), Gridded.walk((byte) slot.key(), slot.lo(), slot.hi()).toArray())).toArray(Placed[]::new); var placed = Arrays.stream(slots).map(slot -> new Placed(slot.assign().w, slot.key(), Gridded.walk((byte) slot.key(), slot.lo(), slot.hi()).toArray())).toArray(
Placed[]::new);
// 2) bounding box around all word cells + arrow cells, with 1-cell margin // 2) bounding box around all word cells + arrow cells, with 1-cell margin
int minR = Integer.MAX_VALUE, minC = Integer.MAX_VALUE; int minR = Integer.MAX_VALUE, minC = Integer.MAX_VALUE;

View File

@@ -47,9 +47,9 @@ public class Main {
static int SSIZE = 24; static int SSIZE = 24;
public int seed = (int) (System.nanoTime() ^ System.currentTimeMillis()); public int seed = (int) (System.nanoTime() ^ System.currentTimeMillis());
public int clueSize = SSIZE; public int clueSize = SSIZE;
public int pop = SSIZE*2; public int pop = SSIZE * 2;
public int offspring = SSIZE*3; public int offspring = SSIZE * 3;
public int gens =600; public int gens = 600;
public String wordsPath = "nl_score_hints_v3.csv"; public String wordsPath = "nl_score_hints_v3.csv";
public double minSimplicity = 0; // 0 means no limit public double minSimplicity = 0; // 0 means no limit
public int threads = Math.max(1, Runtime.getRuntime().availableProcessors()); public int threads = Math.max(1, Runtime.getRuntime().availableProcessors());
@@ -414,8 +414,8 @@ public class Main {
if (mask == null) return null; if (mask == null) return null;
val slotInfo = Masker.slots(mask, dict.index()); val slotInfo = Masker.slots(mask, dict.index());
var grid = mask.toGrid(); var grid = Slotinfo.grid(slotInfo);// mask.toGrid();
var filled = fillMask(rng, slotInfo, grid, (!Main.VERBOSE || multiThreaded)); var filled = fillMask(rng, slotInfo, grid, (!Main.VERBOSE || multiThreaded));
if (!multiThreaded) { if (!multiThreaded) {
System.out.print("\r" + Strings.padRight("", 120) + "\r"); System.out.print("\r" + Strings.padRight("", 120) + "\r");

View File

@@ -14,7 +14,23 @@ import java.util.stream.IntStream;
import static java.lang.Long.*; import static java.lang.Long.*;
import static puzzle.SwedishGenerator.*; import static puzzle.SwedishGenerator.*;
public record Masker(Rng rng, int[] stack, Clues cache) { public final class Masker {
private final Rng rng;
private final int[] stack;
private final Clues cache;
private final int[] activeCIdx = new int[SwedishGenerator.SIZE];
private final long[] activeSLo = new long[SwedishGenerator.SIZE];
private final long[] activeSHi = new long[SwedishGenerator.SIZE];
private final long[] adjLo = new long[SwedishGenerator.SIZE];
private final long[] adjHi = new long[SwedishGenerator.SIZE];
public Masker(Rng rng, int[] stack, Clues cache) {
this.rng = rng;
this.stack = stack;
this.cache = cache;
}
public static final int[][] MUTATE_RI = new int[SwedishGenerator.SIZE][625]; public static final int[][] MUTATE_RI = new int[SwedishGenerator.SIZE][625];
@@ -109,6 +125,8 @@ public record Masker(Rng rng, int[] stack, Clues cache) {
long penalty = (((long) Math.abs(grid.clueCount() - clueSize)) * 16000L); long penalty = (((long) Math.abs(grid.clueCount() - clueSize)) * 16000L);
boolean hasSlots = false; boolean hasSlots = false;
if (!grid.isValid(2)) return 1_000_000_000L; if (!grid.isValid(2)) return 1_000_000_000L;
int numClues = 0;
for (long bits = lo_cl; bits != X; bits &= bits - 1) { for (long bits = lo_cl; bits != X; bits &= bits - 1) {
long lsb = bits & -bits; long lsb = bits & -bits;
int clueIdx = numberOfTrailingZeros(lsb); int clueIdx = numberOfTrailingZeros(lsb);
@@ -130,6 +148,12 @@ public record Masker(Rng rng, int[] stack, Clues cache) {
int msb = 63 - numberOfLeadingZeros(hLo); int msb = 63 - numberOfLeadingZeros(hLo);
rLo &= -(1L << msb << 1); rLo &= -(1L << msb << 1);
} }
activeCIdx[numClues] = clueIdx;
activeSLo[numClues] = rLo;
activeSHi[numClues] = rHi;
numClues++;
if ((rLo | rHi) != X) { if ((rLo | rHi) != X) {
hasSlots = true; hasSlots = true;
if (Slot.horiz(key)) { if (Slot.horiz(key)) {
@@ -167,6 +191,12 @@ public record Masker(Rng rng, int[] stack, Clues cache) {
int msb = 63 - numberOfLeadingZeros(hLo); int msb = 63 - numberOfLeadingZeros(hLo);
rLo &= -(1L << msb << 1); rLo &= -(1L << msb << 1);
} }
activeCIdx[numClues] = 64 | clueIdx;
activeSLo[numClues] = rLo;
activeSHi[numClues] = rHi;
numClues++;
if ((rLo | rHi) != X) { if ((rLo | rHi) != X) {
hasSlots = true; hasSlots = true;
if (Slot.horiz(key)) { if (Slot.horiz(key)) {
@@ -185,63 +215,64 @@ public record Masker(Rng rng, int[] stack, Clues cache) {
} }
if (!hasSlots) return 1_000_000_000L; if (!hasSlots) return 1_000_000_000L;
long seenLo = X, seenHi = X;
// loop over beide helften // Connectiviteitscheck
for (int base = 0, size, sp, cur; base <= 64; base += 64) { for (int i = 0; i < numClues; i++) {
long clueMask = (base == 0) ? lo_cl : hi_cl; adjLo[i] = 0; adjHi[i] = 0;
long seenMask = (base == 0) ? seenLo : seenHi; }
for (int i = 0; i < numClues; i++) {
// "unseen clues" in deze helft for (int j = i + 1; j < numClues; j++) {
for (long bits = clueMask & ~seenMask, nLo, nHi; bits != X; bits &= bits - 1) { boolean connected = false;
int clueIdx = base | numberOfTrailingZeros(bits); // 1. Intersectie
if (((activeSLo[i] & activeSLo[j]) | (activeSHi[i] & activeSHi[j])) != 0) {
// start nieuwe component connected = true;
size = 0; } else {
stack[0] = clueIdx; // 2. 8-naburigheid van clue cells
sp = 1; int ci = activeCIdx[i];
int cj = activeCIdx[j];
// mark seen if (cj < 64) {
if ((clueIdx & 64) == 0) seenLo |= 1L << clueIdx; if ((NBR8_PACKED_LO[ci] & (1L << cj)) != 0) connected = true;
else seenHi |= 1L << (clueIdx & 63); } else {
if ((NBR8_PACKED_HI[ci] & (1L << (cj & 63))) != 0) connected = true;
// flood fill / bfs
while (sp > 0) {
cur = stack[--sp];
size++;
// neighbors als 2x long masks
nLo = NBR8_PACKED_LO[cur];
nHi = NBR8_PACKED_HI[cur];
// filter: alleen clues, en nog niet seen
nLo &= lo_cl & ~seenLo;
nHi &= hi_cl & ~seenHi;
// push lo-neighbors
while (nLo != X) {
long lsb = nLo & -nLo;
int nidx = numberOfTrailingZeros(nLo); // 0..63
seenLo |= lsb;
stack[sp++] = nidx;
nLo &= nLo - 1;
}
// push hi-neighbors
while (nHi != X) {
long lsb = nHi & -nHi;
int nidx = 64 | numberOfTrailingZeros(nHi); // 64..127
seenHi |= lsb;
stack[sp++] = nidx;
nHi &= nHi - 1;
} }
} }
if (size >= 2) penalty += (size - 1L) * 120L; if (connected) {
if (j < 64) adjLo[i] |= (1L << j);
else adjHi[i] |= (1L << (j - 64));
if (i < 64) adjLo[j] |= (1L << i);
else adjHi[j] |= (1L << (i - 64));
}
}
}
if (numClues > 0) {
long reachedLo = 1L, reachedHi = 0L;
stack[0] = 0;
int sp = 1;
while (sp > 0) {
int cur = stack[--sp];
long nLo = adjLo[cur] & ~reachedLo;
long nHi = adjHi[cur] & ~reachedHi;
while (nLo != 0) {
long lsb = nLo & -nLo;
int idx = numberOfTrailingZeros(lsb);
reachedLo |= lsb;
stack[sp++] = idx;
nLo &= ~lsb;
}
while (nHi != 0) {
long lsb = nHi & -nHi;
int idx = 64 | numberOfTrailingZeros(lsb);
reachedHi |= lsb;
stack[sp++] = idx;
nHi &= ~lsb;
}
}
int count = bitCount(reachedLo) + bitCount(reachedHi);
if (count < numClues) {
penalty += (numClues - count) * 4000;
penalty += 20000;
} }
} }
@@ -251,7 +282,7 @@ public record Masker(Rng rng, int[] stack, Clues cache) {
if ((4 - rci.nbrCount()) + bitCount(rci.n1() & lo_cl) + bitCount(rci.n2() & hi_cl) >= 3) penalty += 400; if ((4 - rci.nbrCount()) + bitCount(rci.n1() & lo_cl) + bitCount(rci.n2() & hi_cl) >= 3) penalty += 400;
boolean h = (cHLo & (1L << clueIdx)) != X; boolean h = (cHLo & (1L << clueIdx)) != X;
boolean v = (cVLo & (1L << clueIdx)) != X; boolean v = (cVLo & (1L << clueIdx)) != X;
if (!h && !v) penalty += 1500; if (!h && !v) penalty += 15000;
else if (h && v) { /* ok */ } else if (((h ? cHLo2 : cVLo2) & (1L << clueIdx)) != X) penalty += 600; else if (h && v) { /* ok */ } else if (((h ? cHLo2 : cVLo2) & (1L << clueIdx)) != X) penalty += 600;
else penalty += 200; else penalty += 200;
} }
@@ -261,7 +292,8 @@ public record Masker(Rng rng, int[] stack, Clues cache) {
if ((4 - rci.nbrCount()) + bitCount(rci.n1() & lo_cl) + bitCount(rci.n2() & hi_cl) >= 3) penalty += 400; if ((4 - rci.nbrCount()) + bitCount(rci.n1() & lo_cl) + bitCount(rci.n2() & hi_cl) >= 3) penalty += 400;
boolean h = (cHHi & (1L << clueIdx)) != X; boolean h = (cHHi & (1L << clueIdx)) != X;
boolean v = (cVHi & (1L << clueIdx)) != X; boolean v = (cVHi & (1L << clueIdx)) != X;
if (!h && !v) penalty += 1500; if (!h && !v)
penalty += 15000;
else if (h && v) { /* ok */ } else if (((h ? cHHi2 : cVHi2) & (1L << clueIdx)) != X) penalty += 600; else if (h && v) { /* ok */ } else if (((h ? cHHi2 : cVHi2) & (1L << clueIdx)) != X) penalty += 600;
else penalty += 200; else penalty += 200;
} }

View File

@@ -50,7 +50,7 @@ public record SwedishGenerator() {
public static final int MAX_WORD_LENGTH_PLUS_ONE = MAX_WORD_LENGTH + 1; public static final int MAX_WORD_LENGTH_PLUS_ONE = MAX_WORD_LENGTH + 1;
public static final int MIN_LEN = 3;//Config.MIN_LEN; public static final int MIN_LEN = 3;//Config.MIN_LEN;
public static final int MAX_TRIES_PER_SLOT = 700;//Config.MAX_TRIES_PER_SLOT; public static final int MAX_TRIES_PER_SLOT = 700;//Config.MAX_TRIES_PER_SLOT;
public static final int STACK_SIZE = 64; public static final int STACK_SIZE = 128;
public static final char C_DASH = '\0'; public static final char C_DASH = '\0';
public static final byte DASH = (byte) C_DASH; public static final byte DASH = (byte) C_DASH;
public static final long RANGE_0_SIZE = (long) SIZE_MIN_1 - 0L + 1L; public static final long RANGE_0_SIZE = (long) SIZE_MIN_1 - 0L + 1L;

View File

@@ -0,0 +1,90 @@
package puzzle;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import puzzle.Masker.Clues;
import puzzle.SwedishGenerator.Rng;
import static puzzle.SwedishGenerator.STACK_SIZE;
public class ConnectivityTest {
@Test
void testConnectivityPenalty() {
Rng rng = new Rng(42);
Masker masker = new Masker(rng, new int[STACK_SIZE], Clues.createEmpty());
// 1. Maak een masker met één component van clues (bijv. 3 clues naast elkaar)
Clues singleComp = Clues.createEmpty();
// Gebruik offsets die dicht bij elkaar liggen
int off1 = SwedishGenerator.Grid.offset(1, 1);
int off2 = SwedishGenerator.Grid.offset(1, 2);
int off3 = SwedishGenerator.Grid.offset(2, 1);
singleComp.setClueLo(1L << off1, (byte)1); // Right
singleComp.setClueLo(1L << off2, (byte)1); // Right
singleComp.setClueLo(1L << off3, (byte)0); // Down
long fitnessSingle = masker.maskFitness(singleComp, 3);
// 2. Maak een masker met twee eilandjes van clues
Clues twoIslands = Clues.createEmpty();
int offA1 = SwedishGenerator.Grid.offset(1, 1);
int offB1 = SwedishGenerator.Grid.offset(6, 6); // Ver weg
// We moeten zorgen dat ze elk minstens 1 slot vormen om door isValid(2) te komen
twoIslands.setClueLo(1L << offA1, (byte)1);
twoIslands.setClueLo(1L << offB1, (byte)1);
long fitnessIslands = masker.maskFitness(twoIslands, 2);
System.out.println("[DEBUG_LOG] Fitness single component: " + fitnessSingle);
System.out.println("[DEBUG_LOG] Fitness two islands: " + fitnessIslands);
// De eilandjes moeten een hogere penalty hebben (als clueCount gelijk is)
Clues twoIslands3 = Clues.createEmpty();
twoIslands3.setClueLo(1L << offA1, (byte)1);
twoIslands3.setClueLo(1L << offB1, (byte)1);
int offB2 = SwedishGenerator.Grid.offset(6, 7);
twoIslands3.setClueLo(1L << offB2, (byte)1);
long fitnessIslands3 = masker.maskFitness(twoIslands3, 3);
System.out.println("[DEBUG_LOG] Fitness three clues in two islands: " + fitnessIslands3);
assertTrue(fitnessIslands3 > fitnessSingle, "Islands should have higher penalty than single component");
}
@Test
void testIntersectionConnectivity() {
Rng rng = new Rng(42);
Masker masker = new Masker(rng, new int[STACK_SIZE], Clues.createEmpty());
// Test of slots die elkaar kruisen als verbonden worden beschouwd,
// zelfs als de clues niet 8-naburig zijn.
Clues crossing = Clues.createEmpty();
// Clue 1: (0,0) naar rechts. Slot op (0,1), (0,2), (0,3)
// Clue 2: (1,2) omhoog. Slot op (0,2)
// Ze kruisen op (0,2)
crossing.setClueLo(1L << SwedishGenerator.Grid.offset(0,0), (byte)1); // Right
crossing.setClueLo(1L << SwedishGenerator.Grid.offset(1,2), (byte)2); // Up
// Deze twee clues zijn niet 8-naburig (0,0 en 1,2)
// Maar hun slots kruisen op (0,2)
long fitness = masker.maskFitness(crossing, 2);
System.out.println("[DEBUG_LOG] Fitness crossing: " + fitness);
// Als ze als verbonden worden gezien, is er 1 component.
// Penalty voor connectiviteit zou 0 moeten zijn (bovenop andere penalties).
// Als we een derde clue ver weg toevoegen, moet de penalty significant stijgen.
Clues crossingPlusIsland = Clues.createEmpty();
crossingPlusIsland.setClueLo(1L << SwedishGenerator.Grid.offset(0,0), (byte)1);
crossingPlusIsland.setClueLo(1L << SwedishGenerator.Grid.offset(1,2), (byte)2);
crossingPlusIsland.setClueLo(1L << SwedishGenerator.Grid.offset(7,7), (byte)1);
long fitnessIsland = masker.maskFitness(crossingPlusIsland, 3);
System.out.println("[DEBUG_LOG] Fitness crossing plus island: " + fitnessIsland);
assertTrue(fitnessIsland > fitness + 10000, "Island should add significant penalty");
}
}

View File

@@ -196,9 +196,7 @@ public class MainTest {
Assertions.assertEquals(20, mask.clueCount()); Assertions.assertEquals(20, mask.clueCount());
val map = mask.stream().collect(Collectors.toMap(ClueAt::index, ClueAt::clue)); val map = mask.stream().collect(Collectors.toMap(ClueAt::index, ClueAt::clue));
Assertions.assertEquals(20, map.size()); Assertions.assertEquals(20, map.size());
var slots = Masker.extractSlots(mask.c(), dict.index()); var slots = Masker.slots(mask.c(), dict.index());
val slotInfo = Masker.scoreSlots(slots);
var grid = mask.toGrid();
// var filled = fillMask(rng, slotInfo, grid, false); // var filled = fillMask(rng, slotInfo, grid, false);
// val res = new PuzzleResult(new Clued(mask), new Gridded(grid), slotInfo, filled).exportFormatFromFilled(0, new Rewards(0, 0, 0)); // val res = new PuzzleResult(new Clued(mask), new Gridded(grid), slotInfo, filled).exportFormatFromFilled(0, new Rewards(0, 0, 0));
} }
@@ -214,15 +212,14 @@ public class MainTest {
" 1 \n" + " 1 \n" +
" 1 2\n" + " 1 2\n" +
"21 22 3"); "21 22 3");
var slots = Masker.extractSlots(mask.c(), dict.index()); var slotInfo = Masker.slots(mask.c(), dict.index());
val slotInfo = Masker.scoreSlots(slots); var grid = Slotinfo.grid(slotInfo);
var grid = mask.toGrid();
var filled = fillMask(rng, slotInfo, grid, false); var filled = fillMask(rng, slotInfo, grid, false);
Assertions.assertTrue(filled.ok(), "Puzzle generation failed (not ok)"); Assertions.assertTrue(filled.ok(), "Puzzle generation failed (not ok)");
Assertions.assertEquals(13, Slotinfo.wordCount(0, slotInfo), "Number of assigned words changed"); Assertions.assertEquals(13, Slotinfo.wordCount(0, slotInfo), "Number of assigned words changed");
Assertions.assertEquals("WAANZIN", Lemma.asWord(slotInfo[0].assign().w)); Assertions.assertEquals("WAANZIN", Lemma.asWord(slotInfo[0].assign().w));
Assertions.assertEquals(-2155876353L, grid.lo); Assertions.assertEquals(-1L, grid.lo);
Assertions.assertEquals(255L, grid.hi); Assertions.assertEquals(-1L, grid.hi);
var g = new Gridded(grid); var g = new Gridded(grid);
g.gridToString(mask.c()); g.gridToString(mask.c());
var aa = new PuzzleResult(mask, g, slotInfo, filled).exportFormatFromFilled(1, new Rewards(1, 1, 1)); var aa = new PuzzleResult(mask, g, slotInfo, filled).exportFormatFromFilled(1, new Rewards(1, 1, 1));