Gather data

This commit is contained in:
mike
2026-01-09 08:18:55 +01:00
parent 850fdb4f67
commit fa806a1078
4 changed files with 134 additions and 76 deletions

View File

@@ -56,6 +56,7 @@ public record SwedishGenerator(int[] buff) {
static final int R = Config.PUZZLE_ROWS; static final int R = Config.PUZZLE_ROWS;
static final double CROSS_X = (R - 1) / 2.0; static final double CROSS_X = (R - 1) / 2.0;
static final int SIZE = C * R;// ~18 static final int SIZE = C * R;// ~18
static final double SIZED = (double) SIZE;// ~18
static final int TARGET_CLUES = SIZE >> 2; static final int TARGET_CLUES = SIZE >> 2;
static final int MAX_WORD_LENGTH = Math.min(C, R); static final int MAX_WORD_LENGTH = Math.min(C, R);
static final int MIN_LEN = Config.MIN_LEN; static final int MIN_LEN = Config.MIN_LEN;
@@ -144,19 +145,34 @@ public record SwedishGenerator(int[] buff) {
static final byte _48 = 48; static final byte _48 = 48;
record Grid(byte[] g) { record Grid(byte[] g, long[] bo) {
public Grid(byte[] g) { this(g, new long[2]); }
int digitAt(int r, int c) { return g[offset(r, c)] - 48; } int digitAt(int r, int c) { return g[offset(r, c)] - 48; }
int digitAt(int index) { return g[index] - 48; } int digitAt(int index) { return g[index] - 48; }
public static int r(int offset) { return offset & 7; } public static int r(int offset) { return offset & 7; }
public static int c(int offset) { return offset >>> 3; } public static int c(int offset) { return offset >>> 3; }
static int offset(int r, int c) { return r | (c << 3); } static int offset(int r, int c) { return r | (c << 3); }
Grid deepCopyGrid() { return new Grid(g.clone()); } Grid deepCopyGrid() { return new Grid(g.clone(), bo.clone()); }
public byte byteAt(int r, int c) { return g[offset(r, c)]; } public byte byteAt(int r, int c) { return g[offset(r, c)]; }
public byte byteAt(int pos) { return g[pos]; } public byte byteAt(int pos) { return g[pos]; }
void setCharAt(int r, int c, char ch) { g[offset(r, c)] = (byte) ch; } void setCharAt(int r, int c, char ch) { g[offset(r, c)] = (byte) ch; }
void setByteAt(int idx, byte ch) { g[idx] = ch; } void setByteAt(int idx, byte ch) { g[idx] = ch; }
void setAt(int idx, byte ch) {
if (isDigit(ch)) setClue(idx, ch);
else g[idx] = ch;
}
void setClue(int idx, byte ch) {
g[idx] = ch;
if (idx < 64) bo[0] |= (1L << idx);
else bo[1] |= (1L << (idx & 63));
}
void clear(int idx) { g[idx] = DASH; } void clear(int idx) { g[idx] = DASH; }
void clearClue(int idx) {
g[idx] = DASH;
if (idx < 64) bo[0] &= ~(1L << idx);
else bo[1] &= ~(1L << (idx & 63));
}
public boolean isLetterAt(int r, int c) { return ((g[offset(r, c)] & 64) != 0); } public boolean isLetterAt(int r, int c) { return ((g[offset(r, c)] & 64) != 0); }
boolean isDigitAt(int r, int c) { return (g[offset(r, c)] & 48) == 48; } boolean isDigitAt(int r, int c) { return (g[offset(r, c)] & 48) == 48; }
boolean isDigitAt(int index) { return (g[index] & 48) == 48; } boolean isDigitAt(int index) { return (g[index] & 48) == 48; }
@@ -166,10 +182,30 @@ public record SwedishGenerator(int[] buff) {
public double similarity(Grid b) { public double similarity(Grid b) {
var same = 0; var same = 0;
for (int i = 0; i < SIZE; i++) if (g[i] == b.g[i]) same++; for (int i = 0; i < SIZE; i++) if (g[i] == b.g[i]) same++;
return same / (double) (SIZE); return same / SIZED;
}
int clueCount() {
return Long.bitCount(bo[0]) + Long.bitCount(bo[1]);
}
void forEachSetBit71(java.util.function.IntConsumer consumer) {
long lo = bo[0], hi = bo[1];
// low 64 bits
while (lo != 0L) {
int bit = Long.numberOfTrailingZeros(lo);
consumer.accept(bit); // 0..63
lo &= (lo - 1); // clear lowest set bit
}
// high 7 bits (positions 64..70)
// hi &= 0x7FL;
while (hi != 0L) {
int bit = Long.numberOfTrailingZeros(hi);
consumer.accept(64 + bit); // 64..70
hi &= (hi - 1L);
} }
} }
static Grid makeEmptyGrid() { return new Grid(new byte[SIZE]); } }
static Grid makeEmptyGrid() { return new Grid(new byte[SIZE], new long[2]); }
static final class IntList { static final class IntList {
@@ -365,7 +401,25 @@ public record SwedishGenerator(int[] buff) {
} }
void forEachSlot(Grid grid, SlotVisitor visitor) { void forEachSlot(Grid grid, SlotVisitor visitor) {
for (var rci : IT) { grid.forEachSetBit71(idx -> {
var d = grid.digitAt(idx);
var nbrs16 = OFFSETS[d];
int r = Grid.r(idx), c = Grid.c(idx), rr = r + nbrs16.r, cc = c + nbrs16.c;
if (rr < 0 || rr >= R || cc < 0 || cc >= C || grid.isDigitAt(rr, cc)) return;
long packedPos = 0;
var n = 0;
while (rr >= 0 && rr < R && cc >= 0 && cc < C && grid.isLettercell(rr, cc) && n < MAX_WORD_LENGTH) {
packedPos |= (long) Grid.offset(rr, cc) << (n * 7);
n++;
rr += nbrs16.dr;
cc += nbrs16.dc;
}
if (n > 0) {
visitor.visit((r << 8) | (c << 4) | d, packedPos, n);
}
});
/* for (var rci : IT) {
if (!grid.isDigitAt(rci.i)) continue; if (!grid.isDigitAt(rci.i)) continue;
var d = grid.digitAt(rci.i); var d = grid.digitAt(rci.i);
var nbrs16 = OFFSETS[d]; var nbrs16 = OFFSETS[d];
@@ -384,7 +438,7 @@ public record SwedishGenerator(int[] buff) {
if (n > 0) { if (n > 0) {
visitor.visit((rci.r << 8) | (rci.c << 4) | d, packedPos, n); visitor.visit((rci.r << 8) | (rci.c << 4) | d, packedPos, n);
} }
} }*/
} }
ArrayList<Slot> extractSlots(Grid grid) { ArrayList<Slot> extractSlots(Grid grid) {
@@ -405,12 +459,10 @@ public record SwedishGenerator(int[] buff) {
} }
long maskFitness(Grid grid, int[] lenCounts) { long maskFitness(Grid grid, int[] lenCounts) {
long penalty = 0; final long[] penalty = { 0 };
var clueCount = 0; var clueCount = grid.clueCount();
for (var v : grid.g) if (Grid.isDigit(v)) clueCount++; penalty[0] += 8L * Math.abs(clueCount - TARGET_CLUES);
penalty += 8L * Math.abs(clueCount - TARGET_CLUES);
var ctx = CTX.get(); var ctx = CTX.get();
var covH = ctx.covH; var covH = ctx.covH;
@@ -418,13 +470,12 @@ public record SwedishGenerator(int[] buff) {
Arrays.fill(covH, 0, SIZE, 0); Arrays.fill(covH, 0, SIZE, 0);
Arrays.fill(covV, 0, SIZE, 0); Arrays.fill(covV, 0, SIZE, 0);
boolean hasSlots = false; boolean[] hasSlots = {false};
for (var rci : IT) { grid.forEachSetBit71(clueIdx -> {
if (!grid.isDigitAt(rci.i)) continue; var d = grid.digitAt(clueIdx);
var d = grid.digitAt(rci.i);
var nbrs16 = OFFSETS[d]; var nbrs16 = OFFSETS[d];
int rr = rci.r + nbrs16.r, cc = rci.c + nbrs16.c; int rr = Grid.r(clueIdx) + nbrs16.r, cc = Grid.c(clueIdx) + nbrs16.c;
if (rr < 0 || rr >= R || cc < 0 || cc >= C || grid.isDigitAt(rr, cc)) continue; if (rr < 0 || rr >= R || cc < 0 || cc >= C || grid.isDigitAt(rr, cc)) return;
long packedPos = 0; long packedPos = 0;
var n = 0; var n = 0;
@@ -436,43 +487,49 @@ public record SwedishGenerator(int[] buff) {
rr += nbrs16.dr; rr += nbrs16.dr;
cc += nbrs16.dc; cc += nbrs16.dc;
} }
if (n == 0) continue; if (n == 0) return;
hasSlots = true; hasSlots[0] = true;
if (n < MIN_LEN) { if (n < MIN_LEN) {
penalty += 8000; penalty[0] += 8000;
} else { } else {
if (lenCounts[n] <= 0) penalty += 12000; if (lenCounts[n] <= 0) penalty[0] += 12000;
} }
var horiz = Slot.horiz(d) ? covH : covV; var horiz = Slot.horiz(d) ? covH : covV;
for (var i = 0; i < n; i++) horiz[Slot.offset(packedPos, i)] += 1; for (var i = 0; i < n; i++) horiz[Slot.offset(packedPos, i)] += 1;
} });
if (!hasSlots) return 1_000_000_000L; if (!hasSlots[0]) return 1_000_000_000L;
/* grid.forEachSetBit71(clueIdx -> {
var h = covH[clueIdx];
var v = covV[clueIdx];
if (h == 0 && v == 0) penalty[0] += 1500;
else if (h > 0 && v > 0) { *//* ok *//* } else if (h + v == 1) penalty[0] += 200;
else penalty[0] += 600;
});*/
int idx, h, v; int idx, h, v;
for (idx = 0; idx < SIZE; idx++) { for (idx = 0; idx < SIZE; idx++) {
if (grid.isDigitAt(idx)) continue; if (grid.isDigitAt(idx)) continue;
h = covH[idx]; h = covH[idx];
v = covV[idx]; v = covV[idx];
if (h == 0 && v == 0) penalty += 1500; if (h == 0 && v == 0) penalty[0] += 1500;
else if (h > 0 && v > 0) { /* ok */ } else if (h + v == 1) penalty += 200; else if (h > 0 && v > 0) { /* ok */ } else if (h + v == 1) penalty[0] += 200;
else penalty += 600; else penalty[0] += 600;
} }
// clue clustering (8-connected) // clue clustering (8-connected)
var seen = ctx.seen; var seen = ctx.seen;
seen.clear(); seen.clear();
var stack = ctx.stack; var stack = ctx.stack;
int sp, nr, nc;
long size; grid.forEachSetBit71(clueIdx -> {
for (var rci : IT) { if (seen.get(clueIdx)) return;
idx = rci.i; var sp = 0;
if (seen.get(idx) || grid.isLetterAt(idx)) continue; stack[sp++] = clueIdx;
sp = 0; seen.set(clueIdx);
stack[sp++] = idx; var size = 0;
seen.set(idx);
size = 0;
while (sp > 0) { while (sp > 0) {
var p = stack[--sp]; var p = stack[--sp];
@@ -480,18 +537,20 @@ public record SwedishGenerator(int[] buff) {
size++; size++;
for (var d : nbrs8) { for (var d : nbrs8) {
nr = rr + d.r; var nr = rr + d.r;
nc = cc + d.c; var nc = cc + d.c;
if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue; if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
idx = Grid.offset(nr, nc); var nidx = Grid.offset(nr, nc);
if (seen.get(idx) || grid.isLetterAt(idx)) continue; if (seen.get(nidx) || grid.isLetterAt(nidx)) continue;
seen.set(idx); seen.set(nidx);
stack[sp++] = idx; stack[sp++] = nidx;
} }
} }
if (size >= 2) penalty += (size - 1L) * 120L; if (size >= 2) penalty[0] += (size - 1L) * 120L;
} });
int sp, nr, nc;
long size;
int walls, wr, wc; int walls, wr, wc;
// dead-end-ish letter cell (3+ walls) // dead-end-ish letter cell (3+ walls)
for (var rci : IT) { for (var rci : IT) {
@@ -502,10 +561,10 @@ public record SwedishGenerator(int[] buff) {
wc = rci.c + d.c; wc = rci.c + d.c;
if (wr < 0 || wr >= R || wc < 0 || wc >= C || grid.isDigitAt(wr, wc)) walls++; if (wr < 0 || wr >= R || wc < 0 || wc >= C || grid.isDigitAt(wr, wc)) walls++;
} }
if (walls >= 3) penalty += 400; if (walls >= 3) penalty[0] += 400;
} }
return penalty; return penalty[0];
} }
Grid randomMask(Rng rng) { Grid randomMask(Rng rng) {
@@ -518,13 +577,11 @@ public record SwedishGenerator(int[] buff) {
if (g.isDigitAt(idx)) continue; if (g.isDigitAt(idx)) continue;
var d = OFFSETS[rng.randbyte(1, 4)]; var d = OFFSETS[rng.randbyte(1, 4)];
g.setByteAt(idx, d.dbyte); if (hasRoomForClue(g, idx, d)) {
if (!hasRoomForClue(g, idx, d)) { g.setClue(idx, d.dbyte);
g.clear(idx);
continue;
}
placed++; placed++;
} }
}
return g; return g;
} }
Grid mutate(Rng rng, Grid grid) { Grid mutate(Rng rng, Grid grid) {
@@ -539,11 +596,10 @@ public record SwedishGenerator(int[] buff) {
clamp(cy + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, C - 1)); clamp(cy + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, C - 1));
if (g.isDigitAt(ri)) { if (g.isDigitAt(ri)) {
g.clear(ri); g.clearClue(ri);
} else { } else {
var d = OFFSETS[rng.randint(1, 4)]; var d = OFFSETS[rng.randint(1, 4)];
g.setByteAt(ri, d.dbyte); if (hasRoomForClue(g, ri, d)) g.setClue(ri, d.dbyte);
if (!hasRoomForClue(g, ri, d)) g.clear(ri);
} }
} }
return g; return g;
@@ -554,8 +610,10 @@ public record SwedishGenerator(int[] buff) {
var nx = Math.cos(theta); var nx = Math.cos(theta);
var ny = Math.sin(theta); var ny = Math.sin(theta);
for (var rci : IT) out.setByteAt(rci.i, ((rci.r - CROSS_X) * nx + (rci.c - CROSS_Y) * ny >= 0) ? a.byteAt(rci.i) : b.byteAt(rci.i)); for (var rci : IT) {
for (var rci : IT) if (out.isDigitAt(rci.i) && !hasRoomForClue(out, rci.i, OFFSETS[out.digitAt(rci.i)])) out.clear(rci.i); out.setAt(rci.i, ((rci.r - CROSS_X) * nx + (rci.c - CROSS_Y) * ny >= 0) ? a.byteAt(rci.i) : b.byteAt(rci.i));
}
for (var rci : IT) if (out.isDigitAt(rci.i) && !hasRoomForClue(out, rci.i, OFFSETS[out.digitAt(rci.i)])) out.clearClue(rci.i);
return out; return out;
} }

View File

@@ -21,7 +21,7 @@ public class ExportFormatTest {
var grid = SwedishGenerator.makeEmptyGrid(); var grid = SwedishGenerator.makeEmptyGrid();
// Place a '2' (right) at (0,0) // Place a '2' (right) at (0,0)
grid.setByteAt(0, (byte) '2'); grid.setClue(0, (byte) '2');
// This creates a slot starting at (0,1) // This creates a slot starting at (0,1)
var clueMap = new HashMap<Integer, Lemma>(); var clueMap = new HashMap<Integer, Lemma>();
@@ -36,7 +36,7 @@ public class ExportFormatTest {
grid.setByteAt(Grid.offset(0, 3), (byte) 'S'); grid.setByteAt(Grid.offset(0, 3), (byte) 'S');
grid.setByteAt(Grid.offset(0, 4), (byte) 'T'); grid.setByteAt(Grid.offset(0, 4), (byte) 'T');
// Terminate thGrid.offset(e slot at) (0,5) with another digit to avoid it extending to MAX_WORD_LENGTH // Terminate thGrid.offset(e slot at) (0,5) with another digit to avoid it extending to MAX_WORD_LENGTH
grid.setByteAt(Grid.offset(0, 5), (byte) '1'); grid.setClue(Grid.offset(0, 5), (byte) '1');
var fillResult = new FillResult(true, new Gridded(grid), clueMap, null); var fillResult = new FillResult(true, new Gridded(grid), clueMap, null);
var puzzleResult = new PuzzleResult(swe, null, null, fillResult); var puzzleResult = new PuzzleResult(swe, null, null, fillResult);

View File

@@ -28,7 +28,7 @@ public class MainTest {
// Set up digits on the grid to create slots. // Set up digits on the grid to create slots.
// '2' (right) at (0,0) -> slot at (0,1), (0,2) // '2' (right) at (0,0) -> slot at (0,1), (0,2)
grid.setCharAt(0, 0, '2'); grid.setClue(0, (byte) '2');
grid.setCharAt(0, 1, 'A'); grid.setCharAt(0, 1, 'A');
grid.setCharAt(0, 2, 'B'); grid.setCharAt(0, 2, 'B');
@@ -61,7 +61,7 @@ public class MainTest {
void testForEachSlot() { void testForEachSlot() {
var generator = new SwedishGenerator(); var generator = new SwedishGenerator();
var grid = makeEmptyGrid(); var grid = makeEmptyGrid();
grid.setCharAt(0, 0, '2'); // right grid.setClue(0, (byte) '2'); // right
var count = new AtomicInteger(0); var count = new AtomicInteger(0);
generator.forEachSlot(grid, (key, packedPos, len) -> { generator.forEachSlot(grid, (key, packedPos, len) -> {
@@ -175,8 +175,8 @@ public class MainTest {
// Regression baseline for seed search starting at 12347, pop 4, gens 20 // Regression baseline for seed search starting at 12347, pop 4, gens 20
Assertions.assertEquals(12347, foundSeed, "Found seed changed"); Assertions.assertEquals(12347, foundSeed, "Found seed changed");
Assertions.assertEquals(20, res.filled().clueMap().size(), "Number of assigned words changed"); Assertions.assertEquals(20, res.filled().clueMap().size(), "Number of assigned words changed");
Assertions.assertEquals(763.8, res.filled().simplicity(), 1e-9, "Simplicity value changed"); Assertions.assertEquals(775.45, res.filled().simplicity(), 1e-9, "Simplicity value changed");
Assertions.assertArrayEquals(new byte[]{ 'M', 'A', 'N', 'T', 'A' }, res.filled().clueMap().get(1377).word()); Assertions.assertArrayEquals(new byte[]{ 'I', 'N', 'E', 'R', 'T' }, res.filled().clueMap().get(1377).word());
} }
@Test @Test
public void testIsLetterA() { public void testIsLetterA() {

View File

@@ -191,7 +191,7 @@ public class SwedishGeneratorTest {
var grid = SwedishGenerator.makeEmptyGrid(); var grid = SwedishGenerator.makeEmptyGrid();
// 3x3 grid (Config.PUZZLE_ROWS/COLS are 3 in test env) // 3x3 grid (Config.PUZZLE_ROWS/COLS are 3 in test env)
// Set '2' (right) at 0,0 // Set '2' (right) at 0,0
grid.setCharAt(0, 0, '2'); grid.setClue(0, (byte) '2');
// This should detect a slot starting at 0,1 with length 2 (0,1 and 0,2) // This should detect a slot starting at 0,1 with length 2 (0,1 and 0,2)
var slots = gen.extractSlots(grid); var slots = gen.extractSlots(grid);
@@ -226,7 +226,7 @@ public class SwedishGeneratorTest {
assertTrue(f1 >= 1_000_000_000L); assertTrue(f1 >= 1_000_000_000L);
// Add a slot // Add a slot
grid.setCharAt(0, 0, '2'); grid.setClue(0, SwedishGenerator.OFFSETS[2].dbyte());
var f2 = gen.maskFitness(grid, lenCounts); var f2 = gen.maskFitness(grid, lenCounts);
assertTrue(f2 < f1); assertTrue(f2 < f1);
} }