Gather data
This commit is contained in:
14
pom.xml
14
pom.xml
@@ -61,6 +61,12 @@
|
|||||||
<version>1.18.42</version>
|
<version>1.18.42</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>mike.processor</groupId>
|
||||||
|
<artifactId>puzzle-processor</artifactId>
|
||||||
|
<version>1.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@@ -87,11 +93,19 @@
|
|||||||
<version>3.13.0</version>
|
<version>3.13.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<annotationProcessorPaths>
|
<annotationProcessorPaths>
|
||||||
|
<!-- Lombok processor -->
|
||||||
<path>
|
<path>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.42</version>
|
<version>1.18.42</version>
|
||||||
</path>
|
</path>
|
||||||
|
|
||||||
|
<!-- jouw generator processor -->
|
||||||
|
<path>
|
||||||
|
<groupId>mike.processor</groupId>
|
||||||
|
<artifactId>puzzle-processor</artifactId>
|
||||||
|
<version>1.1-SNAPSHOT</version>
|
||||||
|
</path>
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
<source>25</source>
|
<source>25</source>
|
||||||
<target>25</target>
|
<target>25</target>
|
||||||
|
|||||||
222
src/main/java/puzzle/Export.java
Normal file
222
src/main/java/puzzle/Export.java
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package puzzle;
|
||||||
|
|
||||||
|
import lombok.experimental.Delegate;
|
||||||
|
import puzzle.SwedishGenerator.Dict;
|
||||||
|
import puzzle.SwedishGenerator.FillResult;
|
||||||
|
import puzzle.SwedishGenerator.Grid;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import static puzzle.SwedishGenerator.R;
|
||||||
|
import static puzzle.SwedishGenerator.Lemma;
|
||||||
|
import static puzzle.SwedishGenerator.Slot;
|
||||||
|
import static puzzle.SwedishGenerator.C;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExportFormat.java
|
||||||
|
*
|
||||||
|
* Direct port of export_format.js:
|
||||||
|
* - scans filled grid for clue digits '1'..'4'
|
||||||
|
* - extracts placed words in canonical direction (horizontal=right, vertical=down)
|
||||||
|
* - crops to bounding box (words + arrow cells) with 1-cell margin
|
||||||
|
* - outputs gridv2 + words[] (+ difficulty, rewards)
|
||||||
|
*/
|
||||||
|
public record Export() {
|
||||||
|
|
||||||
|
record Strings() {
|
||||||
|
|
||||||
|
static String padRight(String s, int n) {
|
||||||
|
if (s.length() >= n) return s;
|
||||||
|
return s + " ".repeat(n - s.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Gridded(@Delegate Grid grid) {
|
||||||
|
|
||||||
|
String gridToString() {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (var r = 0; r < R; r++) {
|
||||||
|
if (r > 0) sb.append('\n');
|
||||||
|
for (var c = 0; c < C; c++) sb.append((char) grid.byteAt(Grid.offset(r, c)));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
public String renderHuman() {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (var r = 0; r < R; r++) {
|
||||||
|
if (r > 0) sb.append('\n');
|
||||||
|
for (var c = 0; c < C; c++) {
|
||||||
|
sb.append(grid.isDigitAt(r, c) ? ' ' : (char) grid.byteAt(Grid.offset(r, c)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Bit {
|
||||||
|
|
||||||
|
static long pack(int r, int c) { return (((long) r) << 32) ^ (c & 0xFFFFFFFFL); }
|
||||||
|
long l1, l2;
|
||||||
|
public Bit() {
|
||||||
|
l1 = 0L;
|
||||||
|
l2 = 0L;
|
||||||
|
}
|
||||||
|
public boolean get(int bitIndex) {
|
||||||
|
if (bitIndex < 64) return (l1 & (1L << bitIndex)) != 0L;
|
||||||
|
return (l2 & (1L << (bitIndex & 63))) != 0L;
|
||||||
|
}
|
||||||
|
public void set(int bitIndex) {
|
||||||
|
if (bitIndex < 64) this.l1 |= 1L << bitIndex;
|
||||||
|
else this.l2 |= 1L << (bitIndex & 63);
|
||||||
|
}
|
||||||
|
public void clear() {
|
||||||
|
l1 = 0L;
|
||||||
|
l2 = 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Bit1029(long[] bits) {
|
||||||
|
|
||||||
|
public Bit1029() { this(new long[1029]); }
|
||||||
|
static int wordIndex(int bitIndex) { return bitIndex >> 6; }
|
||||||
|
public boolean get(int bitIndex) { return (this.bits[wordIndex(bitIndex)] & 1L << bitIndex) != 0L; }
|
||||||
|
public void set(int bitIndex) { bits[wordIndex(bitIndex)] |= 1L << bitIndex; }
|
||||||
|
public void clear(int bitIndex) { this.bits[wordIndex(bitIndex)] &= ~(1L << bitIndex); }
|
||||||
|
public void clear() { Arrays.fill(bits, 0L); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Placed(Lemma lemma, int startRow, int startCol, String direction, int arrowRow, int arrowCol, int[] cells, boolean isReversed) {
|
||||||
|
|
||||||
|
static final String HORIZONTAL = "h";
|
||||||
|
static final String VERTICAL = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Rewards(int coins, int stars, int hints) { }
|
||||||
|
|
||||||
|
public record WordOut(Lemma lemma, int startRow, int startCol, String direction, int arrowRow, int arrowCol, boolean isReversed, int complex) {
|
||||||
|
|
||||||
|
public String word() { return new String(lemma().word(), java.nio.charset.StandardCharsets.US_ASCII); }
|
||||||
|
public String[] clue() { return lemma.clue(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ExportedPuzzle(List<String> gridv2, WordOut[] words, int difficulty, Rewards rewards) { }
|
||||||
|
|
||||||
|
public record PuzzleResult(SwedishGenerator swe, Dict dict, Gridded mask, FillResult filled) {
|
||||||
|
|
||||||
|
boolean inBounds(int r, int c) { return r >= 0 && r < SwedishGenerator.R && c >= 0 && c < SwedishGenerator.C; }
|
||||||
|
Placed extractPlacedFromSlot(Slot s, Lemma lemma) {
|
||||||
|
var d = s.dir();
|
||||||
|
|
||||||
|
var cells = new int[s.len()];
|
||||||
|
for (int i = 0, len = s.len(); i < len; i++) cells[i] = s.pos(i);
|
||||||
|
|
||||||
|
String direction;
|
||||||
|
var isReversed = false;
|
||||||
|
var startRow = Grid.r(cells[0]);
|
||||||
|
var startCol = Grid.c(cells[0]);
|
||||||
|
if (d == 2) { // right -> horizontal
|
||||||
|
direction = Placed.HORIZONTAL;
|
||||||
|
} else if (d == 3 || d == 5) { // down or down-bent -> vertical
|
||||||
|
direction = Placed.VERTICAL;
|
||||||
|
} else if (d == 4) { // left -> horizontal (REVERSED)
|
||||||
|
direction = Placed.HORIZONTAL;
|
||||||
|
isReversed = true;
|
||||||
|
} else if (d == 1) { // up -> vertical (REVERSED)
|
||||||
|
direction = Placed.VERTICAL;
|
||||||
|
isReversed = true;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Placed(
|
||||||
|
lemma,
|
||||||
|
startRow,
|
||||||
|
startCol,
|
||||||
|
direction,
|
||||||
|
s.clueR(),
|
||||||
|
s.clueC(),
|
||||||
|
cells,
|
||||||
|
isReversed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
public ExportedPuzzle exportFormatFromFilled(int difficulty, Rewards rewards) {
|
||||||
|
var g = filled().grid();
|
||||||
|
var placed = new ArrayList<Placed>();
|
||||||
|
var clueMap = filled().clueMap();
|
||||||
|
g.grid().forEachSlot((int key, long packedPos, int len) -> {
|
||||||
|
var word = clueMap.get(key);
|
||||||
|
if (word != null) {
|
||||||
|
var p = extractPlacedFromSlot(Slot.from(key, packedPos, len), word);
|
||||||
|
if (p != null) placed.add(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If nothing placed: return full grid mapped to letters/# only
|
||||||
|
if (placed.isEmpty()) {
|
||||||
|
var gridv2 = new ArrayList<String>(R);
|
||||||
|
for (var r = 0; r < R; r++) {
|
||||||
|
var sb = new StringBuilder(C);
|
||||||
|
for (var c = 0; c < C; c++) {
|
||||||
|
sb.append(g.isLetterAt(r, c) ? (char) g.byteAt(Grid.offset(r, c)) : '#');
|
||||||
|
}
|
||||||
|
gridv2.add(sb.toString());
|
||||||
|
}
|
||||||
|
return new ExportedPuzzle(gridv2, new WordOut[0], difficulty, rewards);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) bounding box around all word cells + arrow cells, with 1-cell margin
|
||||||
|
|
||||||
|
int minR = Integer.MAX_VALUE, minC = Integer.MAX_VALUE;
|
||||||
|
int maxR = Integer.MIN_VALUE, maxC = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
for (var rc : placed) {
|
||||||
|
for (var c : rc.cells) {
|
||||||
|
minR = Math.min(minR, Grid.r(c));
|
||||||
|
minC = Math.min(minC, Grid.c(c));
|
||||||
|
maxR = Math.max(maxR, Grid.r(c));
|
||||||
|
maxC = Math.max(maxC, Grid.c(c));
|
||||||
|
}
|
||||||
|
minR = Math.min(minR, rc.arrowRow);
|
||||||
|
minC = Math.min(minC, rc.arrowCol);
|
||||||
|
maxR = Math.max(maxR, rc.arrowRow);
|
||||||
|
maxC = Math.max(maxC, rc.arrowCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) map of only used letter cells (everything else becomes '#')
|
||||||
|
var letterAt = new HashMap<Long, Character>();
|
||||||
|
for (var p : placed) {
|
||||||
|
for (var c : p.cells) {
|
||||||
|
int rr = Grid.r(c), cc = Grid.c(c);
|
||||||
|
if (inBounds(rr, cc) && g.isLetterAt(rr, cc)) {
|
||||||
|
letterAt.put(Bit.pack(rr, cc), (char) g.byteAt(Grid.offset(rr, cc)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) render gridv2 over cropped bounds (out-of-bounds become '#')
|
||||||
|
var gridv2 = new ArrayList<String>(Math.max(0, maxR - minR + 1));
|
||||||
|
for (var r = minR; r <= maxR; r++) {
|
||||||
|
var row = new StringBuilder(Math.max(0, maxC - minC + 1));
|
||||||
|
for (var c = minC; c <= maxC; c++) {
|
||||||
|
row.append(letterAt.getOrDefault(Bit.pack(r, c), '#'));
|
||||||
|
}
|
||||||
|
gridv2.add(row.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) words output with cropped coordinates
|
||||||
|
int MIN_R = minR, MIN_C = minC;
|
||||||
|
var wordsOut = placed.stream().map(p -> new WordOut(
|
||||||
|
p.lemma,
|
||||||
|
p.startRow - MIN_R,
|
||||||
|
p.startCol - MIN_C,
|
||||||
|
p.direction,
|
||||||
|
p.arrowRow - MIN_R,
|
||||||
|
p.arrowCol - MIN_C,
|
||||||
|
p.isReversed,
|
||||||
|
p.lemma.simpel()
|
||||||
|
)).toArray(WordOut[]::new);
|
||||||
|
return new ExportedPuzzle(gridv2, wordsOut, difficulty, rewards);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
package puzzle;
|
|
||||||
|
|
||||||
import lombok.experimental.Delegate;
|
|
||||||
import puzzle.Main.PuzzleResult;
|
|
||||||
import puzzle.SwedishGenerator.Grid;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import static puzzle.SwedishGenerator.R;
|
|
||||||
import static puzzle.SwedishGenerator.Lemma;
|
|
||||||
import static puzzle.SwedishGenerator.Slot;
|
|
||||||
import static puzzle.SwedishGenerator.C;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ExportFormat.java
|
|
||||||
*
|
|
||||||
* Direct port of export_format.js:
|
|
||||||
* - scans filled grid for clue digits '1'..'4'
|
|
||||||
* - extracts placed words in canonical direction (horizontal=right, vertical=down)
|
|
||||||
* - crops to bounding box (words + arrow cells) with 1-cell margin
|
|
||||||
* - outputs gridv2 + words[] (+ difficulty, rewards)
|
|
||||||
*/
|
|
||||||
public record ExportFormat() {
|
|
||||||
|
|
||||||
record Gridded(@Delegate Grid grid) {
|
|
||||||
|
|
||||||
String gridToString() {
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
for (var r = 0; r < R; r++) {
|
|
||||||
if (r > 0) sb.append('\n');
|
|
||||||
for (var c = 0; c < C; c++) sb.append((char) grid.byteAt(Grid.offset(r, c)));
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
public String renderHuman() {
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
for (var r = 0; r < R; r++) {
|
|
||||||
if (r > 0) sb.append('\n');
|
|
||||||
for (var c = 0; c < C; c++) {
|
|
||||||
sb.append(grid.isDigitAt(r, c) ? ' ' : (char) grid.byteAt(Grid.offset(r, c)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class Bit {
|
|
||||||
|
|
||||||
long l1, l2;
|
|
||||||
public Bit() {
|
|
||||||
l1 = 0L;
|
|
||||||
l2 = 0L;
|
|
||||||
}
|
|
||||||
public boolean get(int bitIndex) {
|
|
||||||
if (bitIndex < 64) {
|
|
||||||
return (l1 & (1L << bitIndex)) != 0L;
|
|
||||||
}
|
|
||||||
return (l2 & (1L << (bitIndex & 63))) != 0L;
|
|
||||||
}
|
|
||||||
public void set(int bitIndex) {
|
|
||||||
if (bitIndex < 64) {
|
|
||||||
this.l1 |= 1L << bitIndex;
|
|
||||||
} else {
|
|
||||||
this.l2 |= 1L << (bitIndex & 63);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public void clear() {
|
|
||||||
l1 = 0L;
|
|
||||||
l2 = 0L;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record Bit1029(long[] bits) {
|
|
||||||
|
|
||||||
public Bit1029() { this(new long[1029]); }
|
|
||||||
static int wordIndex(int bitIndex) { return bitIndex >> 6; }
|
|
||||||
public boolean get(int bitIndex) { return (this.bits[wordIndex(bitIndex)] & 1L << bitIndex) != 0L; }
|
|
||||||
public void set(int bitIndex) { bits[wordIndex(bitIndex)] |= 1L << bitIndex; }
|
|
||||||
public void clear(int bitIndex) { this.bits[wordIndex(bitIndex)] &= ~(1L << bitIndex); }
|
|
||||||
public void clear() { Arrays.fill(bits, 0L); }
|
|
||||||
}
|
|
||||||
|
|
||||||
static final String HORIZONTAL = "h", VERTICAL = "v";
|
|
||||||
private static boolean inBounds(int r, int c) { return r >= 0 && r < SwedishGenerator.R && c >= 0 && c < SwedishGenerator.C; }
|
|
||||||
|
|
||||||
public static ExportedPuzzle exportFormatFromFilled(PuzzleResult puz, int difficulty, Rewards rewards) {
|
|
||||||
Objects.requireNonNull(puz, "puz");
|
|
||||||
var g = puz.filled().grid();
|
|
||||||
var placed = new ArrayList<Placed>();
|
|
||||||
var clueMap = puz.filled().clueMap();
|
|
||||||
puz.swe().forEachSlot(g.grid(), (int key, long packedPos, int len) -> {
|
|
||||||
var word = clueMap.get(key);
|
|
||||||
if (word != null) {
|
|
||||||
var p = extractPlacedFromSlot(Slot.from(key, packedPos, len), word);
|
|
||||||
if (p != null) placed.add(p);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If nothing placed: return full grid mapped to letters/# only
|
|
||||||
if (placed.isEmpty()) {
|
|
||||||
var gridv2 = new ArrayList<String>(R);
|
|
||||||
for (var r = 0; r < R; r++) {
|
|
||||||
var sb = new StringBuilder(C);
|
|
||||||
for (var c = 0; c < C; c++) {
|
|
||||||
sb.append(g.isLetterAt(r, c) ? (char) g.byteAt(Grid.offset(r, c)) : '#');
|
|
||||||
}
|
|
||||||
gridv2.add(sb.toString());
|
|
||||||
}
|
|
||||||
return new ExportedPuzzle(gridv2, new WordOut[0], difficulty, rewards);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) bounding box around all word cells + arrow cells, with 1-cell margin
|
|
||||||
|
|
||||||
int minR = Integer.MAX_VALUE, minC = Integer.MAX_VALUE;
|
|
||||||
int maxR = Integer.MIN_VALUE, maxC = Integer.MIN_VALUE;
|
|
||||||
|
|
||||||
for (var rc : placed) {
|
|
||||||
for (var c : rc.cells) {
|
|
||||||
minR = Math.min(minR, Grid.r(c));
|
|
||||||
minC = Math.min(minC, Grid.c(c));
|
|
||||||
maxR = Math.max(maxR, Grid.r(c));
|
|
||||||
maxC = Math.max(maxC, Grid.c(c));
|
|
||||||
}
|
|
||||||
minR = Math.min(minR, rc.arrowRow);
|
|
||||||
minC = Math.min(minC, rc.arrowCol);
|
|
||||||
maxR = Math.max(maxR, rc.arrowRow);
|
|
||||||
maxC = Math.max(maxC, rc.arrowCol);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) map of only used letter cells (everything else becomes '#')
|
|
||||||
var letterAt = new HashMap<Long, Character>();
|
|
||||||
for (var p : placed) {
|
|
||||||
for (var c : p.cells) {
|
|
||||||
int rr = Grid.r(c), cc = Grid.c(c);
|
|
||||||
if (inBounds(rr, cc) && g.isLetterAt(rr, cc)) {
|
|
||||||
letterAt.put(pack(rr, cc), (char) g.byteAt(Grid.offset(rr, cc)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) render gridv2 over cropped bounds (out-of-bounds become '#')
|
|
||||||
var gridv2 = new ArrayList<String>(Math.max(0, maxR - minR + 1));
|
|
||||||
for (var r = minR; r <= maxR; r++) {
|
|
||||||
var row = new StringBuilder(Math.max(0, maxC - minC + 1));
|
|
||||||
for (var c = minC; c <= maxC; c++) {
|
|
||||||
row.append(letterAt.getOrDefault(pack(r, c), '#'));
|
|
||||||
}
|
|
||||||
gridv2.add(row.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5) words output with cropped coordinates
|
|
||||||
int MIN_R = minR, MIN_C = minC;
|
|
||||||
var wordsOut = placed.stream().map(p -> new WordOut(
|
|
||||||
p.lemma,
|
|
||||||
p.startRow - MIN_R,
|
|
||||||
p.startCol - MIN_C,
|
|
||||||
p.direction,
|
|
||||||
p.arrowRow - MIN_R,
|
|
||||||
p.arrowCol - MIN_C,
|
|
||||||
p.isReversed,
|
|
||||||
p.lemma.simpel()
|
|
||||||
)).toArray(WordOut[]::new);
|
|
||||||
return new ExportedPuzzle(gridv2, wordsOut, difficulty, rewards);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a generator Slot + assigned word into a Placed object for export.
|
|
||||||
*/
|
|
||||||
private static Placed extractPlacedFromSlot(Slot s, Lemma lemma) {
|
|
||||||
var d = s.dir();
|
|
||||||
|
|
||||||
var cells = new int[s.len()];
|
|
||||||
for (int i = 0, len = s.len(); i < len; i++) cells[i] = s.pos(i);
|
|
||||||
|
|
||||||
// Canonicalize: always output right/down
|
|
||||||
String direction;
|
|
||||||
var isReversed = false;
|
|
||||||
var startRow = Grid.r(cells[0]);
|
|
||||||
var startCol = Grid.c(cells[0]);
|
|
||||||
if (d == 2) { // right -> horizontal
|
|
||||||
direction = HORIZONTAL;
|
|
||||||
} else if (d == 3 || d == 5) { // down or down-bent -> vertical
|
|
||||||
direction = VERTICAL;
|
|
||||||
} else if (d == 4) { // left -> horizontal (REVERSED)
|
|
||||||
direction = HORIZONTAL;
|
|
||||||
isReversed = true;
|
|
||||||
} else if (d == 1) { // up -> vertical (REVERSED)
|
|
||||||
direction = VERTICAL;
|
|
||||||
isReversed = true;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Placed(
|
|
||||||
lemma,
|
|
||||||
startRow,
|
|
||||||
startCol,
|
|
||||||
direction,
|
|
||||||
s.clueR(),
|
|
||||||
s.clueC(),
|
|
||||||
cells,
|
|
||||||
isReversed
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long pack(int r, int c) { return (((long) r) << 32) ^ (c & 0xFFFFFFFFL); }
|
|
||||||
private record Placed(Lemma lemma, int startRow, int startCol, String direction, int arrowRow, int arrowCol, int[] cells, boolean isReversed) { }
|
|
||||||
|
|
||||||
public record Rewards(int coins, int stars, int hints) { }
|
|
||||||
|
|
||||||
public record WordOut(Lemma lemma, int startRow, int startCol, String direction, int arrowRow, int arrowCol, boolean isReversed, int complex) {
|
|
||||||
|
|
||||||
public String word() { return new String(lemma().word(), java.nio.charset.StandardCharsets.US_ASCII); }
|
|
||||||
public String[] clue() { return lemma.clue(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ExportedPuzzle(List<String> gridv2, WordOut[] words, int difficulty, Rewards rewards) { }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -14,15 +14,12 @@ import java.util.concurrent.*;
|
|||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static puzzle.ExportFormat.*;
|
import static puzzle.Export.*;
|
||||||
import static puzzle.SwedishGenerator.*;
|
import static puzzle.SwedishGenerator.*;
|
||||||
import static puzzle.SwedishGenerator.loadWords;
|
import static puzzle.SwedishGenerator.loadWords;
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
|
|
||||||
// ---------------- Top-level generatePuzzle ----------------
|
|
||||||
public record PuzzleResult(SwedishGenerator swe, Dict dict, Gridded mask, FillResult filled) { }
|
|
||||||
|
|
||||||
final static String OUT_DIR = envOrDefault("OUT_DIR", "/data/puzzle");
|
final static String OUT_DIR = envOrDefault("OUT_DIR", "/data/puzzle");
|
||||||
final static Path PUZZLE_DIR = Paths.get(OUT_DIR, "puzzles");
|
final static Path PUZZLE_DIR = Paths.get(OUT_DIR, "puzzles");
|
||||||
static final Path INDEX_FILE = PUZZLE_DIR.resolve("index.json");
|
static final Path INDEX_FILE = PUZZLE_DIR.resolve("index.json");
|
||||||
@@ -32,7 +29,7 @@ public class Main {
|
|||||||
static final String FILE_NAME = FILE_ID + ".json";
|
static final String FILE_NAME = FILE_ID + ".json";
|
||||||
static final Path OUTPUT_PATH = PUZZLE_DIR.resolve(FILE_NAME);
|
static final Path OUTPUT_PATH = PUZZLE_DIR.resolve(FILE_NAME);
|
||||||
static final String DATE_STRING = now.toLocalDate().toString();
|
static final String DATE_STRING = now.toLocalDate().toString();
|
||||||
|
static final boolean VERBOSE = false;
|
||||||
static final AtomicLong TOTAL_NODES = new AtomicLong(0);
|
static final AtomicLong TOTAL_NODES = new AtomicLong(0);
|
||||||
static final AtomicLong TOTAL_BACKTRACKS = new AtomicLong(0);
|
static final AtomicLong TOTAL_BACKTRACKS = new AtomicLong(0);
|
||||||
static final AtomicLong TOTAL_ATTEMPTS = new AtomicLong(0);
|
static final AtomicLong TOTAL_ATTEMPTS = new AtomicLong(0);
|
||||||
@@ -52,6 +49,7 @@ public class Main {
|
|||||||
public boolean reindex = false;
|
public boolean reindex = false;
|
||||||
public int fillTimeout = 20_000;
|
public int fillTimeout = 20_000;
|
||||||
public boolean verbose = false;
|
public boolean verbose = false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void main(String[] args) {
|
public void main(String[] args) {
|
||||||
@@ -92,7 +90,7 @@ public class Main {
|
|||||||
section("Grid (human)");
|
section("Grid (human)");
|
||||||
System.out.print(indentLines(res.filled().grid().renderHuman(), " "));
|
System.out.print(indentLines(res.filled().grid().renderHuman(), " "));
|
||||||
|
|
||||||
var exported = exportFormatFromFilled(res, 1, new Rewards(50, 2, 1));
|
var exported = res.exportFormatFromFilled(1, new Rewards(50, 2, 1));
|
||||||
|
|
||||||
section("Clues");
|
section("Clues");
|
||||||
info("status : generating...");
|
info("status : generating...");
|
||||||
@@ -233,8 +231,6 @@ public class Main {
|
|||||||
i++;
|
i++;
|
||||||
} else if (a.equals("--reindex")) {
|
} else if (a.equals("--reindex")) {
|
||||||
out.reindex = true;
|
out.reindex = true;
|
||||||
} else if (a.equals("--verbose")) {
|
|
||||||
out.verbose = true;
|
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unknown arg: " + a);
|
throw new IllegalArgumentException("Unknown arg: " + a);
|
||||||
}
|
}
|
||||||
@@ -344,8 +340,8 @@ public class Main {
|
|||||||
static PuzzleResult attempt(Rng rng, Dict dict, Opts opts) {
|
static PuzzleResult attempt(Rng rng, Dict dict, Opts opts) {
|
||||||
TOTAL_ATTEMPTS.incrementAndGet();
|
TOTAL_ATTEMPTS.incrementAndGet();
|
||||||
var swe = new SwedishGenerator();
|
var swe = new SwedishGenerator();
|
||||||
var mask = swe.generateMask(rng, dict.lenCounts(), opts.pop, opts.gens, opts.verbose);
|
var mask = swe.generateMask(rng, dict.lenCounts(), opts.pop, opts.gens);
|
||||||
var filled = swe.fillMask(rng, mask, dict.index(), 200, opts.fillTimeout, opts.verbose);
|
var filled = new CSP(rng).fillMask(mask, dict.index(), 200, opts.fillTimeout);
|
||||||
|
|
||||||
TOTAL_NODES.addAndGet(filled.stats().nodes);
|
TOTAL_NODES.addAndGet(filled.stats().nodes);
|
||||||
TOTAL_BACKTRACKS.addAndGet(filled.stats().backtracks);
|
TOTAL_BACKTRACKS.addAndGet(filled.stats().backtracks);
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ package puzzle;
|
|||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.val;
|
import lombok.val;
|
||||||
import puzzle.ExportFormat.Bit;
|
import puzzle.Export.Bit;
|
||||||
import puzzle.ExportFormat.Bit1029;
|
import puzzle.Export.Bit1029;
|
||||||
import puzzle.ExportFormat.Gridded;
|
import puzzle.Export.Gridded;
|
||||||
|
import puzzle.Export.Strings;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -16,6 +17,7 @@ import java.util.Comparator;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.function.IntConsumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,18 +32,6 @@ public record SwedishGenerator() {
|
|||||||
|
|
||||||
record CandidateInfo(int[] indices, int count) { }
|
record CandidateInfo(int[] indices, int count) { }
|
||||||
|
|
||||||
record nbrs_8(int r, int c) { }
|
|
||||||
|
|
||||||
record nbrs_16(int r, int c, int dr, int dc, int d, byte dbyte) {
|
|
||||||
|
|
||||||
public nbrs_16(int r, int c, int dr, int dc, int d) {
|
|
||||||
this(r, c, dr, dc, d, (byte) (48 + d));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record rci(int r, int c, int i, long n1, long n2, int nbrCount, long n8_1, long n8_2) {
|
|
||||||
}
|
|
||||||
|
|
||||||
static final int BAR_LEN = 22;
|
static final int BAR_LEN = 22;
|
||||||
static final int C = Config.PUZZLE_COLS;
|
static final int C = Config.PUZZLE_COLS;
|
||||||
static final double CROSS_Y = (C - 1) / 2.0;
|
static final double CROSS_Y = (C - 1) / 2.0;
|
||||||
@@ -61,8 +51,12 @@ public record SwedishGenerator() {
|
|||||||
static boolean isLetter(byte b) { return (b & 64) != 0; }
|
static boolean isLetter(byte b) { return (b & 64) != 0; }
|
||||||
static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); }
|
static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); }
|
||||||
|
|
||||||
public SwedishGenerator() { }
|
record nbrs_8(int r, int c) { }
|
||||||
public SwedishGenerator(int[] buff) { this(); }
|
|
||||||
|
record nbrs_16(int r, int c, int dr, int dc, int d, byte dbyte) {
|
||||||
|
|
||||||
|
public nbrs_16(int r, int c, int dr, int dc, int d) { this(r, c, dr, dc, d, (byte) (48 + d)); }
|
||||||
|
}
|
||||||
|
|
||||||
// Directions for '1'..'6'
|
// Directions for '1'..'6'
|
||||||
static final nbrs_16[] OFFSETS = new nbrs_16[]{
|
static final nbrs_16[] OFFSETS = new nbrs_16[]{
|
||||||
@@ -91,6 +85,8 @@ public record SwedishGenerator() {
|
|||||||
new nbrs_8(0, 1)
|
new nbrs_8(0, 1)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
record rci(int r, int c, int i, long n1, long n2, int nbrCount, long n8_1, long n8_2) { }
|
||||||
|
|
||||||
static final rci[] IT = new rci[SIZE];
|
static final rci[] IT = new rci[SIZE];
|
||||||
static {
|
static {
|
||||||
for (int i = 0; i < SIZE; i++) {
|
for (int i = 0; i < SIZE; i++) {
|
||||||
@@ -120,6 +116,35 @@ public record SwedishGenerator() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class FillStats {
|
||||||
|
|
||||||
|
public long nodes;
|
||||||
|
public long backtracks;
|
||||||
|
public double seconds;
|
||||||
|
public int lastMRV;
|
||||||
|
}
|
||||||
|
|
||||||
|
record Pick(Slot slot, CandidateInfo info, boolean done) { }
|
||||||
|
|
||||||
|
static final Pick PICK_DONE = new Pick(null, null, true);
|
||||||
|
static final Pick PICK_NOT_DONE = new Pick(null, null, false);
|
||||||
|
|
||||||
|
public static record FillResult(boolean ok,
|
||||||
|
Gridded grid,
|
||||||
|
HashMap<Integer, Lemma> clueMap,
|
||||||
|
FillStats stats,
|
||||||
|
double simplicity) {
|
||||||
|
|
||||||
|
public FillResult(boolean ok, Gridded grid, HashMap<Integer, Lemma> assigned, FillStats stats) {
|
||||||
|
double totalSimplicity = 0;
|
||||||
|
if (ok) {
|
||||||
|
for (var w : assigned.values()) totalSimplicity += w.simpel;
|
||||||
|
totalSimplicity = assigned.isEmpty() ? 0 : totalSimplicity / assigned.size();
|
||||||
|
}
|
||||||
|
this(ok, grid, assigned, stats, totalSimplicity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static record Context(int[] covH,
|
static record Context(int[] covH,
|
||||||
int[] covV,
|
int[] covV,
|
||||||
int[] cellCount,
|
int[] cellCount,
|
||||||
@@ -208,25 +233,15 @@ public record SwedishGenerator() {
|
|||||||
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 / SIZED;
|
return same / SIZED;
|
||||||
}
|
}
|
||||||
int clueCount() {
|
int clueCount() { return Long.bitCount(bo[0]) + Long.bitCount(bo[1]); }
|
||||||
return Long.bitCount(bo[0]) + Long.bitCount(bo[1]);
|
void forEachSetBit71() { forEachSetBit71(null); }
|
||||||
}
|
void forEachSetBit71(IntConsumer consumer) {
|
||||||
void forEachSetBit71(java.util.function.IntConsumer consumer) {
|
for (var lo = bo[0]; lo != 0; lo &= lo - 1) consumer.accept(Long.numberOfTrailingZeros(lo));
|
||||||
long lo = bo[0], hi = bo[1];
|
for (var hi = bo[1]; hi != 0; hi &= hi - 1) consumer.accept(64 + Long.numberOfTrailingZeros(hi));
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
void forEachSlot(SlotVisitor visitor) {
|
||||||
|
for (var lo = bo[0]; lo != 0; lo &= lo - 1) processSlot(this, visitor, Long.numberOfTrailingZeros(lo));
|
||||||
|
for (var hi = bo[1]; hi != 0; hi &= hi - 1) processSlot(this, visitor, 64 + Long.numberOfTrailingZeros(hi));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
static Grid makeEmptyGrid() { return new Grid(new byte[SIZE], new long[2]); }
|
static Grid makeEmptyGrid() { return new Grid(new byte[SIZE], new long[2]); }
|
||||||
@@ -351,57 +366,9 @@ public record SwedishGenerator() {
|
|||||||
return k;
|
return k;
|
||||||
}
|
}
|
||||||
|
|
||||||
CandidateInfo candidateInfoForPattern(Context ctx, DictEntry entry, int len) {
|
|
||||||
var pattern = ctx.pattern;
|
|
||||||
var listBuffer = ctx.intListBuffer;
|
|
||||||
var listCount = 0;
|
|
||||||
for (var i = 0; i < len; i++) {
|
|
||||||
var ch = pattern[i];
|
|
||||||
if (isLetter(ch)) {
|
|
||||||
listBuffer[listCount++] = entry.pos[i][ch - 'A'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listCount == 0) {
|
|
||||||
return new CandidateInfo(null, entry.words.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort constraints by size to optimize intersection
|
|
||||||
for (int i = 0; i < listCount - 1; i++) {
|
|
||||||
for (int j = i + 1; j < listCount; j++) {
|
|
||||||
if (listBuffer[j].size() < listBuffer[i].size()) {
|
|
||||||
var tmp = listBuffer[i];
|
|
||||||
listBuffer[i] = listBuffer[j];
|
|
||||||
listBuffer[j] = tmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cur = listBuffer[0].data();
|
|
||||||
var curLen = listBuffer[0].size();
|
|
||||||
if (listCount == 1) return new CandidateInfo(cur, curLen);
|
|
||||||
|
|
||||||
int[] b1 = ctx.inter1;
|
|
||||||
int[] b2 = ctx.inter2;
|
|
||||||
int[] in = cur;
|
|
||||||
int[] out = b1;
|
|
||||||
|
|
||||||
for (var k = 1; k < listCount; k++) {
|
|
||||||
var nxt = listBuffer[k];
|
|
||||||
curLen = intersectSorted(in, curLen, nxt.data(), nxt.size(), out);
|
|
||||||
in = out;
|
|
||||||
out = (out == b1) ? b2 : b1;
|
|
||||||
if (curLen == 0) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CandidateInfo(in, curLen);
|
|
||||||
}
|
|
||||||
static record Slot(int key, long packedPos) {
|
static record Slot(int key, long packedPos) {
|
||||||
|
|
||||||
static Slot from(int key, long packedPos, int len) {
|
static Slot from(int key, long packedPos, int len) { return new Slot(key, packedPos | ((long) len << 56)); }
|
||||||
return new Slot(key, packedPos | ((long) len << 56));
|
|
||||||
}
|
|
||||||
|
|
||||||
public int len() { return (int) (packedPos >>> 56); }
|
public int len() { return (int) (packedPos >>> 56); }
|
||||||
public int clueR() { return Grid.r((key >>> 4)); }
|
public int clueR() { return Grid.r((key >>> 4)); }
|
||||||
public int clueIndex() { return key >>> 4; }
|
public int clueIndex() { return key >>> 4; }
|
||||||
@@ -413,11 +380,7 @@ public record SwedishGenerator() {
|
|||||||
public static int offset(long packedPos, int i) { return (int) ((packedPos >> (i * 7)) & 127); }
|
public static int offset(long packedPos, int i) { return (int) ((packedPos >> (i * 7)) & 127); }
|
||||||
}
|
}
|
||||||
static void undoPlace(Grid grid, Slot s, int mask) {
|
static void undoPlace(Grid grid, Slot s, int mask) {
|
||||||
for (int i = 0, len = s.len(); i < len; i++) {
|
for (int i = 0, len = s.len(); i < len; i++) if ((mask & (1L << i)) != 0) grid.clear(s.pos(i));
|
||||||
if ((mask & (1L << i)) != 0) {
|
|
||||||
grid.clear(s.pos(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
interface SlotVisitor {
|
interface SlotVisitor {
|
||||||
@@ -425,22 +388,7 @@ public record SwedishGenerator() {
|
|||||||
void visit(int key, long packedPos, int len);
|
void visit(int key, long packedPos, int len);
|
||||||
}
|
}
|
||||||
|
|
||||||
void forEachSlot(Grid grid, SlotVisitor visitor) {
|
private static void processSlot(Grid grid, SlotVisitor visitor, int idx) {
|
||||||
long lo = grid.bo[0], hi = grid.bo[1];
|
|
||||||
int idx;
|
|
||||||
while (lo != 0) {
|
|
||||||
idx = Long.numberOfTrailingZeros(lo);
|
|
||||||
lo &= (lo - 1);
|
|
||||||
processSlot(grid, visitor, idx);
|
|
||||||
}
|
|
||||||
while (hi != 0) {
|
|
||||||
idx = 64 + Long.numberOfTrailingZeros(hi);
|
|
||||||
hi &= (hi - 1);
|
|
||||||
processSlot(grid, visitor, idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processSlot(Grid grid, SlotVisitor visitor, int idx) {
|
|
||||||
var d = grid.digitAt(idx);
|
var d = grid.digitAt(idx);
|
||||||
var nbrs16 = OFFSETS[d];
|
var nbrs16 = OFFSETS[d];
|
||||||
int r = Grid.r(idx), c = Grid.c(idx), rr = r + nbrs16.r, cc = c + nbrs16.c;
|
int r = Grid.r(idx), c = Grid.c(idx), rr = r + nbrs16.r, cc = c + nbrs16.c;
|
||||||
@@ -459,9 +407,9 @@ public record SwedishGenerator() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ArrayList<Slot> extractSlots(Grid grid) {
|
static ArrayList<Slot> extractSlots(Grid grid) {
|
||||||
var slots = new ArrayList<Slot>(32);
|
var slots = new ArrayList<Slot>(32);
|
||||||
forEachSlot(grid, (key, packedPos, len) -> slots.add(Slot.from(key, packedPos, len)));
|
grid.forEachSlot((key, packedPos, len) -> slots.add(Slot.from(key, packedPos, len)));
|
||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
boolean hasRoomForClue(Grid grid, int idx, nbrs_16 nbrs16) {
|
boolean hasRoomForClue(Grid grid, int idx, nbrs_16 nbrs16) {
|
||||||
@@ -642,15 +590,13 @@ public record SwedishGenerator() {
|
|||||||
var nx = Math.cos(theta);
|
var nx = Math.cos(theta);
|
||||||
var ny = Math.sin(theta);
|
var ny = Math.sin(theta);
|
||||||
|
|
||||||
for (var rci : IT) {
|
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));
|
||||||
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);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
Grid hillclimb(Rng rng, Grid start, int[] lenCounts, int limit) {
|
Grid hillclimb(Rng rng, Grid start, int[] lenCounts, int limit) {
|
||||||
var best = start;//.deepCopyGrid();
|
var best = start;
|
||||||
var bestF = maskFitness(best, lenCounts);
|
var bestF = maskFitness(best, lenCounts);
|
||||||
var fails = 0;
|
var fails = 0;
|
||||||
|
|
||||||
@@ -668,7 +614,7 @@ public record SwedishGenerator() {
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Grid generateMask(Rng rng, int[] lenCounts, int popSize, int gens, boolean verbose) {
|
public Grid generateMask(Rng rng, int[] lenCounts, int popSize, int gens) {
|
||||||
class GridAndFit {
|
class GridAndFit {
|
||||||
|
|
||||||
Grid grid;
|
Grid grid;
|
||||||
@@ -679,7 +625,7 @@ public record SwedishGenerator() {
|
|||||||
return this.fite;
|
return this.fite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (verbose) System.out.println("generateMask init pop: " + popSize);
|
if (Main.VERBOSE) System.out.println("generateMask init pop: " + popSize);
|
||||||
var pop = new ArrayList<GridAndFit>();
|
var pop = new ArrayList<GridAndFit>();
|
||||||
for (var i = 0; i < popSize; i++) {
|
for (var i = 0; i < popSize; i++) {
|
||||||
pop.add(new GridAndFit(hillclimb(rng, randomMask(rng), lenCounts, 180)));
|
pop.add(new GridAndFit(hillclimb(rng, randomMask(rng), lenCounts, 180)));
|
||||||
@@ -714,7 +660,7 @@ public record SwedishGenerator() {
|
|||||||
}
|
}
|
||||||
pop = next;
|
pop = next;
|
||||||
|
|
||||||
if (verbose && gen % 10 == 0) {
|
if (Main.VERBOSE && gen % 10 == 0) {
|
||||||
var bestF = pop.get(0).fit();
|
var bestF = pop.get(0).fit();
|
||||||
System.out.println(" gen " + gen + "/" + gens + " bestFitness=" + bestF);
|
System.out.println(" gen " + gen + "/" + gens + " bestFitness=" + bestF);
|
||||||
}
|
}
|
||||||
@@ -723,38 +669,6 @@ public record SwedishGenerator() {
|
|||||||
pop.sort(Comparator.comparingLong(GridAndFit::fit));
|
pop.sort(Comparator.comparingLong(GridAndFit::fit));
|
||||||
return pop.get(0).grid;
|
return pop.get(0).grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- Fill (CSP) ----------------
|
|
||||||
|
|
||||||
public static final class FillStats {
|
|
||||||
|
|
||||||
public long nodes;
|
|
||||||
public long backtracks;
|
|
||||||
public double seconds;
|
|
||||||
public int lastMRV;
|
|
||||||
}
|
|
||||||
|
|
||||||
record Pick(Slot slot, CandidateInfo info, boolean done) { }
|
|
||||||
|
|
||||||
static final Pick PICK_DONE = new Pick(null, null, true);
|
|
||||||
static final Pick PICK_NOT_DONE = new Pick(null, null, false);
|
|
||||||
|
|
||||||
public static record FillResult(boolean ok,
|
|
||||||
Gridded grid,
|
|
||||||
HashMap<Integer, Lemma> clueMap,
|
|
||||||
FillStats stats,
|
|
||||||
double simplicity) {
|
|
||||||
|
|
||||||
public FillResult(boolean ok, Gridded grid, HashMap<Integer, Lemma> assigned, FillStats stats) {
|
|
||||||
double totalSimplicity = 0;
|
|
||||||
if (ok) {
|
|
||||||
for (var w : assigned.values()) totalSimplicity += w.simpel;
|
|
||||||
totalSimplicity = assigned.isEmpty() ? 0 : totalSimplicity / assigned.size();
|
|
||||||
}
|
|
||||||
this(ok, grid, assigned, stats, totalSimplicity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void patternForSlot(Grid grid, Slot s, byte[] pat) {
|
static void patternForSlot(Grid grid, Slot s, byte[] pat) {
|
||||||
for (int i = 0, len = s.len(); i < len; i++) {
|
for (int i = 0, len = s.len(); i < len; i++) {
|
||||||
var ch = grid.byteAt(s.pos(i));
|
var ch = grid.byteAt(s.pos(i));
|
||||||
@@ -789,9 +703,55 @@ public record SwedishGenerator() {
|
|||||||
undoBuffer[offset] = mask;
|
undoBuffer[offset] = mask;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
static CandidateInfo candidateInfoForPattern(Context ctx, DictEntry entry, int len) {
|
||||||
|
var pattern = ctx.pattern;
|
||||||
|
var listBuffer = ctx.intListBuffer;
|
||||||
|
var listCount = 0;
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
var ch = pattern[i];
|
||||||
|
if (isLetter(ch)) {
|
||||||
|
listBuffer[listCount++] = entry.pos[i][ch - 'A'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public FillResult fillMask(Rng rng, Grid mask, DictEntry[] dictIndex,
|
if (listCount == 0) {
|
||||||
int logEveryMs, int timeLimitMs, boolean verbose) {
|
return new CandidateInfo(null, entry.words.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort constraints by size to optimize intersection
|
||||||
|
for (int i = 0; i < listCount - 1; i++) {
|
||||||
|
for (int j = i + 1; j < listCount; j++) {
|
||||||
|
if (listBuffer[j].size() < listBuffer[i].size()) {
|
||||||
|
var tmp = listBuffer[i];
|
||||||
|
listBuffer[i] = listBuffer[j];
|
||||||
|
listBuffer[j] = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cur = listBuffer[0].data();
|
||||||
|
var curLen = listBuffer[0].size();
|
||||||
|
if (listCount == 1) return new CandidateInfo(cur, curLen);
|
||||||
|
|
||||||
|
int[] b1 = ctx.inter1;
|
||||||
|
int[] b2 = ctx.inter2;
|
||||||
|
int[] in = cur;
|
||||||
|
int[] out = b1;
|
||||||
|
|
||||||
|
for (var k = 1; k < listCount; k++) {
|
||||||
|
var nxt = listBuffer[k];
|
||||||
|
curLen = intersectSorted(in, curLen, nxt.data(), nxt.size(), out);
|
||||||
|
in = out;
|
||||||
|
out = (out == b1) ? b2 : b1;
|
||||||
|
if (curLen == 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CandidateInfo(in, curLen);
|
||||||
|
}
|
||||||
|
record CSP(Rng rng) {
|
||||||
|
|
||||||
|
public FillResult fillMask(Grid mask, DictEntry[] dictIndex,
|
||||||
|
int logEveryMs, int timeLimitMs) {
|
||||||
boolean multiThreaded = Thread.currentThread().getName().contains("pool");
|
boolean multiThreaded = Thread.currentThread().getName().contains("pool");
|
||||||
var grid = mask.deepCopyGrid();
|
var grid = mask.deepCopyGrid();
|
||||||
var slots = extractSlots(grid);
|
var slots = extractSlots(grid);
|
||||||
@@ -811,7 +771,7 @@ public record SwedishGenerator() {
|
|||||||
final var TOTAL = slots.size();
|
final var TOTAL = slots.size();
|
||||||
|
|
||||||
Runnable renderProgress = () -> {
|
Runnable renderProgress = () -> {
|
||||||
if (!verbose || multiThreaded) return;
|
if (!Main.VERBOSE || multiThreaded) return;
|
||||||
var now = System.currentTimeMillis();
|
var now = System.currentTimeMillis();
|
||||||
if ((now - lastLog.get()) < logEveryMs) return;
|
if ((now - lastLog.get()) < logEveryMs) return;
|
||||||
lastLog.set(now);
|
lastLog.set(now);
|
||||||
@@ -827,7 +787,7 @@ public record SwedishGenerator() {
|
|||||||
"%s %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %s",
|
"%s %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %s",
|
||||||
bar, done, TOTAL, stats.nodes, stats.backtracks, stats.lastMRV, elapsed
|
bar, done, TOTAL, stats.nodes, stats.backtracks, stats.lastMRV, elapsed
|
||||||
);
|
);
|
||||||
System.out.print("\r" + padRight(msg, 120));
|
System.out.print("\r" + Strings.padRight(msg, 120));
|
||||||
System.out.flush();
|
System.out.flush();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -840,7 +800,7 @@ public record SwedishGenerator() {
|
|||||||
var entry = dictIndex[s.len()];
|
var entry = dictIndex[s.len()];
|
||||||
if (entry == null) return PICK_NOT_DONE;
|
if (entry == null) return PICK_NOT_DONE;
|
||||||
patternForSlot(grid, s, ctx.pattern);
|
patternForSlot(grid, s, ctx.pattern);
|
||||||
var info = candidateInfoForPattern(ctx, entry, s.len());
|
var info = SwedishGenerator.candidateInfoForPattern(ctx, entry, s.len());
|
||||||
|
|
||||||
if (info.count == 0) return PICK_NOT_DONE;
|
if (info.count == 0) return PICK_NOT_DONE;
|
||||||
var slotScore = -1;
|
var slotScore = -1;
|
||||||
@@ -968,7 +928,7 @@ public record SwedishGenerator() {
|
|||||||
var ok = new Solver().backtrack(0);
|
var ok = new Solver().backtrack(0);
|
||||||
// final progress line
|
// final progress line
|
||||||
if (!multiThreaded) {
|
if (!multiThreaded) {
|
||||||
System.out.print("\r" + padRight("", 120) + "\r");
|
System.out.print("\r" + Strings.padRight("", 120) + "\r");
|
||||||
System.out.flush();
|
System.out.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -976,7 +936,7 @@ public record SwedishGenerator() {
|
|||||||
var res = new FillResult(ok, new Gridded(grid), assigned, stats);
|
var res = new FillResult(ok, new Gridded(grid), assigned, stats);
|
||||||
|
|
||||||
// print a final progress line
|
// print a final progress line
|
||||||
if (verbose && !multiThreaded) {
|
if (Main.VERBOSE && !multiThreaded) {
|
||||||
System.out.println(
|
System.out.println(
|
||||||
String.format(Locale.ROOT,
|
String.format(Locale.ROOT,
|
||||||
"[######################] %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %.1fs",
|
"[######################] %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %.1fs",
|
||||||
@@ -987,10 +947,6 @@ public record SwedishGenerator() {
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String padRight(String s, int n) {
|
|
||||||
if (s.length() >= n) return s;
|
|
||||||
return s + " ".repeat(n - s.length());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/main/java/puzzle/TestGen.java
Normal file
15
src/main/java/puzzle/TestGen.java
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package puzzle;
|
||||||
|
|
||||||
|
import precomp.Neighbors9x8;
|
||||||
|
public class TestGen {
|
||||||
|
|
||||||
|
static void main() {
|
||||||
|
var cell = Neighbors9x8.IT[0];
|
||||||
|
long n1 = cell.n1();
|
||||||
|
long n2 = cell.n2();
|
||||||
|
int count = cell.nbrCount();
|
||||||
|
|
||||||
|
// of direct je offsets:
|
||||||
|
var up = Neighbors9x8.OFFSETS[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/main/java/puzzle/Trigger.java
Normal file
6
src/main/java/puzzle/Trigger.java
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// file: app/Trigger.java
|
||||||
|
package puzzle;
|
||||||
|
|
||||||
|
import gen.GenerateNeighbors;
|
||||||
|
@GenerateNeighbors(C = 9, R = 8, packageName = "precomp", className = "Neighbors9x8")
|
||||||
|
public final class Trigger { }
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
package puzzle;
|
package puzzle;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import puzzle.ExportFormat.ExportedPuzzle;
|
import puzzle.Export.Gridded;
|
||||||
import puzzle.ExportFormat.Gridded;
|
import puzzle.Export.Rewards;
|
||||||
import puzzle.ExportFormat.Rewards;
|
import puzzle.Export.PuzzleResult;
|
||||||
import puzzle.Main.PuzzleResult;
|
|
||||||
import puzzle.SwedishGenerator.FillResult;
|
import puzzle.SwedishGenerator.FillResult;
|
||||||
import puzzle.SwedishGenerator.Grid;
|
import puzzle.SwedishGenerator.Grid;
|
||||||
import puzzle.SwedishGenerator.Lemma;
|
import puzzle.SwedishGenerator.Lemma;
|
||||||
@@ -26,8 +25,8 @@ public class ExportFormatTest {
|
|||||||
|
|
||||||
var clueMap = new HashMap<Integer, Lemma>();
|
var clueMap = new HashMap<Integer, Lemma>();
|
||||||
// key = (cellIndex << 4) | direction
|
// key = (cellIndex << 4) | direction
|
||||||
int key = (0 << 4) | 2;
|
var key = (0 << 4) | 2;
|
||||||
Lemma lemma = new Lemma("TEST", 1, "A test word");
|
var lemma = new Lemma("TEST", 1, "A test word");
|
||||||
clueMap.put(key, lemma);
|
clueMap.put(key, lemma);
|
||||||
|
|
||||||
// Manually fill the grid letters for "TEST" at (0,1), (0,2), (0,3), (0,4)
|
// Manually fill the grid letters for "TEST" at (0,1), (0,2), (0,3), (0,4)
|
||||||
@@ -41,8 +40,8 @@ public class ExportFormatTest {
|
|||||||
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);
|
||||||
|
|
||||||
Rewards rewards = new Rewards(10, 5, 1);
|
var rewards = new Rewards(10, 5, 1);
|
||||||
ExportedPuzzle exported = ExportFormat.exportFormatFromFilled(puzzleResult, 2, rewards);
|
var exported = puzzleResult.exportFormatFromFilled(2, rewards);
|
||||||
|
|
||||||
assertNotNull(exported);
|
assertNotNull(exported);
|
||||||
assertEquals(2, exported.difficulty());
|
assertEquals(2, exported.difficulty());
|
||||||
@@ -81,13 +80,13 @@ public class ExportFormatTest {
|
|||||||
var fillResult = new FillResult(true, new Gridded(grid), new HashMap<>(), null);
|
var fillResult = new FillResult(true, new Gridded(grid), new HashMap<>(), null);
|
||||||
var puzzleResult = new PuzzleResult(swe, null, null, fillResult);
|
var puzzleResult = new PuzzleResult(swe, null, null, fillResult);
|
||||||
|
|
||||||
ExportedPuzzle exported = ExportFormat.exportFormatFromFilled(puzzleResult, 1, new Rewards(0, 0, 0));
|
var exported = puzzleResult.exportFormatFromFilled(1, new Rewards(0, 0, 0));
|
||||||
|
|
||||||
assertNotNull(exported);
|
assertNotNull(exported);
|
||||||
assertEquals(0, exported.words().length);
|
assertEquals(0, exported.words().length);
|
||||||
// Should return full grid with '#'
|
// Should return full grid with '#'
|
||||||
assertEquals(SwedishGenerator.R, exported.gridv2().size());
|
assertEquals(SwedishGenerator.R, exported.gridv2().size());
|
||||||
for (String row : exported.gridv2()) {
|
for (var row : exported.gridv2()) {
|
||||||
assertEquals(SwedishGenerator.C, row.length());
|
assertEquals(SwedishGenerator.C, row.length());
|
||||||
assertTrue(row.matches("#+"));
|
assertTrue(row.matches("#+"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package puzzle;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import puzzle.Main.PuzzleResult;
|
import puzzle.Export.PuzzleResult;
|
||||||
import puzzle.SwedishGenerator.Rng;
|
import puzzle.SwedishGenerator.Rng;
|
||||||
import puzzle.SwedishGenerator.Slot;
|
import puzzle.SwedishGenerator.Slot;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
@@ -64,7 +64,7 @@ public class MainTest {
|
|||||||
grid.setClue(0, (byte) '2'); // right
|
grid.setClue(0, (byte) '2'); // right
|
||||||
|
|
||||||
var count = new AtomicInteger(0);
|
var count = new AtomicInteger(0);
|
||||||
generator.forEachSlot(grid, (key, packedPos, len) -> {
|
grid.forEachSlot((key, packedPos, len) -> {
|
||||||
count.incrementAndGet();
|
count.incrementAndGet();
|
||||||
assertEquals(8, len);
|
assertEquals(8, len);
|
||||||
assertEquals(0, Grid.r(Slot.offset(packedPos, 0)));
|
assertEquals(0, Grid.r(Slot.offset(packedPos, 0)));
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package puzzle;
|
|||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import puzzle.SwedishGenerator.*;
|
import puzzle.SwedishGenerator.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
@@ -103,7 +104,7 @@ public class SwedishGeneratorTest {
|
|||||||
@Test
|
@Test
|
||||||
void testLemmaAndDict() {
|
void testLemmaAndDict() {
|
||||||
var l1 = new Lemma("APPLE", 5, "A fruit");
|
var l1 = new Lemma("APPLE", 5, "A fruit");
|
||||||
Assertions.assertArrayEquals("APPLE".getBytes(java.nio.charset.StandardCharsets.US_ASCII), l1.word());
|
Assertions.assertArrayEquals("APPLE".getBytes(StandardCharsets.US_ASCII), l1.word());
|
||||||
assertEquals(5, l1.word().length);
|
assertEquals(5, l1.word().length);
|
||||||
assertEquals(5, l1.simpel());
|
assertEquals(5, l1.simpel());
|
||||||
assertEquals((byte) 'A', l1.byteAt(0));
|
assertEquals((byte) 'A', l1.byteAt(0));
|
||||||
@@ -116,7 +117,7 @@ public class SwedishGeneratorTest {
|
|||||||
|
|
||||||
var entry3 = dict.index()[3];
|
var entry3 = dict.index()[3];
|
||||||
assertEquals(1, entry3.words().size());
|
assertEquals(1, entry3.words().size());
|
||||||
Assertions.assertArrayEquals("AXE".getBytes(java.nio.charset.StandardCharsets.US_ASCII), entry3.words().getFirst().word());
|
Assertions.assertArrayEquals("AXE".getBytes(StandardCharsets.US_ASCII), entry3.words().getFirst().word());
|
||||||
|
|
||||||
// Check pos indexing
|
// Check pos indexing
|
||||||
// AXE: A at 0, X at 1, E at 2
|
// AXE: A at 0, X at 1, E at 2
|
||||||
@@ -181,7 +182,7 @@ public class SwedishGeneratorTest {
|
|||||||
// Pattern "APP--" for length 5
|
// Pattern "APP--" for length 5
|
||||||
var context = new Context();
|
var context = new Context();
|
||||||
context.setPatter(new byte[]{ 'A', 'P', 'P', SwedishGenerator.DASH, SwedishGenerator.DASH });
|
context.setPatter(new byte[]{ 'A', 'P', 'P', SwedishGenerator.DASH, SwedishGenerator.DASH });
|
||||||
var info = gen.candidateInfoForPattern(context, dict.index()[5], 5);
|
var info = SwedishGenerator.candidateInfoForPattern(context, dict.index()[5], 5);
|
||||||
|
|
||||||
assertEquals(2, info.count());
|
assertEquals(2, info.count());
|
||||||
assertNotNull(info.indices());
|
assertNotNull(info.indices());
|
||||||
|
|||||||
Reference in New Issue
Block a user