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 double CROSS_X = (R - 1) / 2.0;
static final int SIZE = C * R;// ~18
static final double SIZED = (double) SIZE;// ~18
static final int TARGET_CLUES = SIZE >> 2;
static final int MAX_WORD_LENGTH = Math.min(C, R);
static final int MIN_LEN = Config.MIN_LEN;
@@ -144,19 +145,34 @@ public record SwedishGenerator(int[] buff) {
static final byte _48 = 48;
record Grid(byte[] g) {
record Grid(byte[] g, long[] bo) {
int digitAt(int r, int c) { return g[offset(r, c)] - 48; }
int digitAt(int index) { return g[index] - 48; }
public static int r(int offset) { return offset & 7; }
public static int c(int offset) { return offset >>> 3; }
static int offset(int r, int c) { return r | (c << 3); }
Grid deepCopyGrid() { return new Grid(g.clone()); }
public byte byteAt(int r, int c) { return g[offset(r, c)]; }
public byte byteAt(int pos) { return g[pos]; }
void setCharAt(int r, int c, char ch) { g[offset(r, c)] = (byte) ch; }
void setByteAt(int idx, byte ch) { g[idx] = ch; }
void clear(int idx) { g[idx] = DASH; }
public Grid(byte[] g) { this(g, new long[2]); }
int digitAt(int r, int c) { return g[offset(r, c)] - 48; }
int digitAt(int index) { return g[index] - 48; }
public static int r(int offset) { return offset & 7; }
public static int c(int offset) { return offset >>> 3; }
static int offset(int r, int c) { return r | (c << 3); }
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 pos) { return g[pos]; }
void setCharAt(int r, int c, char ch) { g[offset(r, c)] = (byte) 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 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); }
boolean isDigitAt(int r, int c) { return (g[offset(r, c)] & 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) {
var same = 0;
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 {
@@ -365,7 +401,25 @@ public record SwedishGenerator(int[] buff) {
}
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;
var d = grid.digitAt(rci.i);
var nbrs16 = OFFSETS[d];
@@ -384,7 +438,7 @@ public record SwedishGenerator(int[] buff) {
if (n > 0) {
visitor.visit((rci.r << 8) | (rci.c << 4) | d, packedPos, n);
}
}
}*/
}
ArrayList<Slot> extractSlots(Grid grid) {
@@ -405,12 +459,10 @@ public record SwedishGenerator(int[] buff) {
}
long maskFitness(Grid grid, int[] lenCounts) {
long penalty = 0;
final long[] penalty = { 0 };
var clueCount = 0;
for (var v : grid.g) if (Grid.isDigit(v)) clueCount++;
penalty += 8L * Math.abs(clueCount - TARGET_CLUES);
var clueCount = grid.clueCount();
penalty[0] += 8L * Math.abs(clueCount - TARGET_CLUES);
var ctx = CTX.get();
var covH = ctx.covH;
@@ -418,13 +470,12 @@ public record SwedishGenerator(int[] buff) {
Arrays.fill(covH, 0, SIZE, 0);
Arrays.fill(covV, 0, SIZE, 0);
boolean hasSlots = false;
for (var rci : IT) {
if (!grid.isDigitAt(rci.i)) continue;
var d = grid.digitAt(rci.i);
boolean[] hasSlots = {false};
grid.forEachSetBit71(clueIdx -> {
var d = grid.digitAt(clueIdx);
var nbrs16 = OFFSETS[d];
int rr = rci.r + nbrs16.r, cc = rci.c + nbrs16.c;
if (rr < 0 || rr >= R || cc < 0 || cc >= C || grid.isDigitAt(rr, cc)) continue;
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)) return;
long packedPos = 0;
var n = 0;
@@ -436,43 +487,49 @@ public record SwedishGenerator(int[] buff) {
rr += nbrs16.dr;
cc += nbrs16.dc;
}
if (n == 0) continue;
hasSlots = true;
if (n == 0) return;
hasSlots[0] = true;
if (n < MIN_LEN) {
penalty += 8000;
penalty[0] += 8000;
} else {
if (lenCounts[n] <= 0) penalty += 12000;
if (lenCounts[n] <= 0) penalty[0] += 12000;
}
var horiz = Slot.horiz(d) ? covH : covV;
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;
for (idx = 0; idx < SIZE; idx++) {
if (grid.isDigitAt(idx)) continue;
h = covH[idx];
v = covV[idx];
if (h == 0 && v == 0) penalty += 1500;
else if (h > 0 && v > 0) { /* ok */ } else if (h + v == 1) penalty += 200;
else penalty += 600;
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;
}
// clue clustering (8-connected)
var seen = ctx.seen;
seen.clear();
var stack = ctx.stack;
int sp, nr, nc;
long size;
for (var rci : IT) {
idx = rci.i;
if (seen.get(idx) || grid.isLetterAt(idx)) continue;
sp = 0;
stack[sp++] = idx;
seen.set(idx);
size = 0;
var stack = ctx.stack;
grid.forEachSetBit71(clueIdx -> {
if (seen.get(clueIdx)) return;
var sp = 0;
stack[sp++] = clueIdx;
seen.set(clueIdx);
var size = 0;
while (sp > 0) {
var p = stack[--sp];
@@ -480,19 +537,21 @@ public record SwedishGenerator(int[] buff) {
size++;
for (var d : nbrs8) {
nr = rr + d.r;
nc = cc + d.c;
var nr = rr + d.r;
var nc = cc + d.c;
if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
idx = Grid.offset(nr, nc);
if (seen.get(idx) || grid.isLetterAt(idx)) continue;
seen.set(idx);
stack[sp++] = idx;
var nidx = Grid.offset(nr, nc);
if (seen.get(nidx) || grid.isLetterAt(nidx)) continue;
seen.set(nidx);
stack[sp++] = nidx;
}
}
if (size >= 2) penalty += (size - 1L) * 120L;
}
int walls, wr, wc;
if (size >= 2) penalty[0] += (size - 1L) * 120L;
});
int sp, nr, nc;
long size;
int walls, wr, wc;
// dead-end-ish letter cell (3+ walls)
for (var rci : IT) {
if (grid.isDigitAt(rci.i)) continue;
@@ -502,10 +561,10 @@ public record SwedishGenerator(int[] buff) {
wc = rci.c + d.c;
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) {
@@ -518,12 +577,10 @@ public record SwedishGenerator(int[] buff) {
if (g.isDigitAt(idx)) continue;
var d = OFFSETS[rng.randbyte(1, 4)];
g.setByteAt(idx, d.dbyte);
if (!hasRoomForClue(g, idx, d)) {
g.clear(idx);
continue;
if (hasRoomForClue(g, idx, d)) {
g.setClue(idx, d.dbyte);
placed++;
}
placed++;
}
return g;
}
@@ -539,11 +596,10 @@ public record SwedishGenerator(int[] buff) {
clamp(cy + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, C - 1));
if (g.isDigitAt(ri)) {
g.clear(ri);
g.clearClue(ri);
} else {
var d = OFFSETS[rng.randint(1, 4)];
g.setByteAt(ri, d.dbyte);
if (!hasRoomForClue(g, ri, d)) g.clear(ri);
if (hasRoomForClue(g, ri, d)) g.setClue(ri, d.dbyte);
}
}
return g;
@@ -554,13 +610,15 @@ public record SwedishGenerator(int[] buff) {
var nx = Math.cos(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) if (out.isDigitAt(rci.i) && !hasRoomForClue(out, rci.i, OFFSETS[out.digitAt(rci.i)])) out.clear(rci.i);
for (var rci : IT) {
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;
}
Grid hillclimb(Rng rng, Grid start, int[] lenCounts, int limit) {
var best = start;//.deepCopyGrid();
var best = start;//.deepCopyGrid();
var bestF = maskFitness(best, lenCounts);
var fails = 0;

View File

@@ -21,7 +21,7 @@ public class ExportFormatTest {
var grid = SwedishGenerator.makeEmptyGrid();
// 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)
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, 4), (byte) 'T');
// 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 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.
// '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, 2, 'B');
@@ -61,7 +61,7 @@ public class MainTest {
void testForEachSlot() {
var generator = new SwedishGenerator();
var grid = makeEmptyGrid();
grid.setCharAt(0, 0, '2'); // right
grid.setClue(0, (byte) '2'); // right
var count = new AtomicInteger(0);
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
Assertions.assertEquals(12347, foundSeed, "Found seed 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.assertArrayEquals(new byte[]{ 'M', 'A', 'N', 'T', 'A' }, res.filled().clueMap().get(1377).word());
Assertions.assertEquals(775.45, res.filled().simplicity(), 1e-9, "Simplicity value changed");
Assertions.assertArrayEquals(new byte[]{ 'I', 'N', 'E', 'R', 'T' }, res.filled().clueMap().get(1377).word());
}
@Test
public void testIsLetterA() {

View File

@@ -191,7 +191,7 @@ public class SwedishGeneratorTest {
var grid = SwedishGenerator.makeEmptyGrid();
// 3x3 grid (Config.PUZZLE_ROWS/COLS are 3 in test env)
// 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)
var slots = gen.extractSlots(grid);
@@ -226,7 +226,7 @@ public class SwedishGeneratorTest {
assertTrue(f1 >= 1_000_000_000L);
// Add a slot
grid.setCharAt(0, 0, '2');
grid.setClue(0, SwedishGenerator.OFFSETS[2].dbyte());
var f2 = gen.maskFitness(grid, lenCounts);
assertTrue(f2 < f1);
}