Files
puzzle-generator/src/main/java/puzzle/Export.java
2026-01-12 21:47:27 +01:00

287 lines
11 KiB
Java

package puzzle;
import lombok.Getter;
import lombok.experimental.Accessors;
import lombok.experimental.Delegate;
import puzzle.Export.Gridded.Replacar.Cell;
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.function.IntSupplier;
import java.util.stream.IntStream;
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) {
static boolean isLetter(byte b) { return (b & SwedishGenerator.B64) != SwedishGenerator.B0; }
public static IntStream walk(byte base, long lo, long hi) {
if (Slot.increasing(base)) {
return IntStream.concat(
IntStream.generate(new IntSupplier() {
long temp = lo;
@Override
public int getAsInt() {
int res = Long.numberOfTrailingZeros(temp);
temp &= temp - 1;
return res;
}
}).limit(Long.bitCount(lo)),
IntStream.generate(new IntSupplier() {
long temp = hi;
@Override
public int getAsInt() {
int res = 64 | Long.numberOfTrailingZeros(temp);
temp &= temp - 1;
return res;
}
}).limit(Long.bitCount(hi)));
} else {
return IntStream.concat(
IntStream.generate(new IntSupplier() {
long temp = hi;
@Override
public int getAsInt() {
int msb = 63 - Long.numberOfLeadingZeros(temp);
int res = 64 | msb;
temp &= ~(1L << msb);
return res;
}
}).limit(Long.bitCount(hi)),
IntStream.generate(new IntSupplier() {
long temp = lo;
@Override
public int getAsInt() {
int msb = 63 - Long.numberOfLeadingZeros(temp);
int res = msb;
temp &= ~(1L << msb);
return res;
}
}).limit(Long.bitCount(lo)));
}
}
//public boolean isLetterSet(int idx) { return isLetter(g[idx]); }
char NOT_CLUE_NOT_LETTER_TO(byte b, char fallback) { return isLetter(b) ? (char) b : fallback; }
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() {
return String.join("\n", exportGrid(_ -> ' ', '#'));
}
public boolean notClue(int c) { return grid.notClue(c); }
@FunctionalInterface
interface Replacar {
record Cell(Grid grid, int index, byte data) { }
char replace(Cell c);
}
public String[] exportGrid(Replacar clueChar, char emptyFallback) {
var out = new String[R];
for (var r = 0; r < R; r++) {
var sb = new StringBuilder(C);
for (var c = 0; c < C; c++) {
var offset = Grid.offset(r, c);
if (grid.isClue(offset)) {
sb.append(clueChar.replace(new Cell(grid, offset, grid.byteAt(offset))));
} else {
sb.append(NOT_CLUE_NOT_LETTER_TO(grid.byteAt(offset), emptyFallback));
}
}
out[r] = sb.toString();
}
return out;
}
}
static class Bit {
long l1, l2;
public boolean get(int bitIndex) {
if ((bitIndex & 64) == 0) return (l1 & (1L << bitIndex)) != 0L;
return (l2 & (1L << (bitIndex & 63))) != 0L;
}
public void set(int bitIndex) {
if ((bitIndex & 64) == 0) this.l1 |= 1L << bitIndex;
else this.l2 |= 1L << (bitIndex & 63);
}
public void or(long lo, long hi) {
this.l1 |= lo;
this.l2 |= hi;
}
public void clear() {
l1 = 0L;
l2 = 0L;
}
}
record Bit1029(long[] bits) {
public Bit1029() { this(new long[2048]); }
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); }
}
record Placed(long lemma, int slotKey, int[] cells) {
static final char[] DIRECTION = { '\0', Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL };
public static final char HORIZONTAL = 'h';
static final char VERTICAL = 'v';
public int arrowCol() { return Grid.c(Slot.clueIndex(slotKey)); }
public int arrowRow() { return Grid.r(Slot.clueIndex(slotKey)); }
public int startRow() { return Grid.r(cells[0]); }
public int startCol() { return Grid.c(cells[0]); }
public boolean isReversed() { return !Slot.increasing(slotKey); }
public char direction() { return DIRECTION[Slot.dir(slotKey)]; }
}
public record Rewards(int coins, int stars, int hints) { }
public record WordOut(String word, int[] cell, int startRow, int startCol, char direction, int arrowRow, int arrowCol, boolean isReversed, int complex, String[] clue) {
public WordOut(long l, int startRow, int startCol, char d, int arrowRow, int arrowCol, boolean isReversed) {
this(Lemma.asWord(l), new int[]{ arrowRow, arrowCol, startRow, startCol }, startRow, startCol, d, arrowRow, arrowCol, isReversed, Lemma.simpel(l), Lemma.clue(l));
}
}
public record ExportedPuzzle(String[] grid, WordOut[] words, int difficulty, Rewards rewards) { }
public record PuzzleResult(SwedishGenerator swe, Dict dict, Gridded mask, FillResult filled) {
boolean inBounds(int idx) { return idx >= 0 && idx < SwedishGenerator.SIZE; }
Placed extractPlacedFromSlot(Slot s, long lemma) {
var cells = s.walk().toArray();
return new Placed(
lemma,
s.key(),
cells
);
}
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 lo, long hi) -> {
var word = clueMap.get(key);
if (word != null) {
placed.add(extractPlacedFromSlot(Slot.from(key, lo, hi), word));
} else {
System.err.println("Could not find clue for slot: " + key);
}
});
// If nothing placed: return full grid mapped to letters/# only
if (placed.isEmpty()) {
return new ExportedPuzzle(g.exportGrid(_ -> '#', '#'), 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<Integer, Character>();
for (var p : placed) {
for (var c : p.cells) {
if (inBounds(c) && g.notClue(c)) {
letterAt.put(c, (char) g.byteAt(c));
}
}
}
// 4) render gridv2 over cropped bounds (out-of-bounds become '#')
var gridv2 = new String[Math.max(0, maxR - minR + 1)];
for (int r = minR, i = 0; r <= maxR; r++, i++) {
var row = new StringBuilder(Math.max(0, maxC - minC + 1));
for (var c = minC; c <= maxC; c++) row.append(letterAt.getOrDefault(Grid.offset(r, c), '#'));
gridv2[i] = 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()
)).toArray(WordOut[]::new);
return new ExportedPuzzle(gridv2, wordsOut, difficulty, rewards);
}
}
record DictEntryDTO(LongArrayList words, IntListDTO[][] pos) {
public DictEntryDTO(int L) {
this(new LongArrayList(1024), new IntListDTO[L][26]);
for (var i = 0; i < L; i++) for (var j = 0; j < 26; j++) pos[i][j] = new IntListDTO();
}
}
@Getter
@Accessors(fluent = true)
static final class IntListDTO {
int[] data = new int[8];
int size = 0;
void add(int v) {
if (size >= data.length) data = Arrays.copyOf(data, data.length * 2);
data[size++] = v;
}
}
}