Files
puzzle-generator/src/main/java/puzzle/ExportFormat.java
2026-01-04 01:04:56 +01:00

206 lines
6.9 KiB
Java

package puzzle;
import java.util.*;
import static puzzle.SwedishGenerator.*;
/**
* 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 final class ExportFormat {
private ExportFormat() { }
private static boolean isLetter(char ch) { return ch >= 'A' && ch <= 'Z'; }
private static boolean inBounds(int H, int W, int r, int c) {
return r >= 0 && r < H && c >= 0 && c < W;
}
// ---------- Public API ----------
public static ExportedPuzzle exportFormatFromFilled(PuzzleResult puz, int difficulty, Rewards rewards) {
Objects.requireNonNull(puz, "puz");
var g = puz.filled().grid;
var H = g.length;
var W = g[0].length;
// 1) extract "placed" list from all clue digits in the filled grid
List<Placed> placed = new ArrayList<>();
var allSlots = extractSlots(g);
var clueMap = puz.filled().clueMap;
for (var s : allSlots) {
var word = clueMap.get(s.key());
if (word == null) continue;
var p = extractPlacedFromSlot(s, word);
if (p == null) continue;
placed.add(p);
}
// If nothing placed: return full grid mapped to letters/# only
if (placed.isEmpty()) {
List<String> gridv2 = new ArrayList<>(H);
for (var chars : g) {
var sb = new StringBuilder(W);
for (var c = 0; c < W; c++) {
var ch = chars[c];
sb.append(isLetter(ch) ? ch : '#');
}
gridv2.add(sb.toString());
}
return new ExportedPuzzle(gridv2, List.of(), difficulty, rewards);
}
// 2) bounding box around all word cells + arrow cells, with 1-cell margin
List<int[]> allCells = new ArrayList<>();
for (var p : placed) {
allCells.addAll(p.cells);
allCells.add(p.arrow);
}
int minR = Integer.MAX_VALUE, minC = Integer.MAX_VALUE;
int maxR = Integer.MIN_VALUE, maxC = Integer.MIN_VALUE;
for (var rc : allCells) {
int rr = rc[0], cc = rc[1];
minR = Math.min(minR, rr);
minC = Math.min(minC, cc);
maxR = Math.max(maxR, rr);
maxC = Math.max(maxC, cc);
}
// 3) map of only used letter cells (everything else becomes '#')
Map<Long, Character> letterAt = new HashMap<>();
for (var p : placed) {
for (var rc : p.cells) {
int rr = rc[0], cc = rc[1];
if (inBounds(H, W, rr, cc) && isLetter(g[rr][cc])) {
letterAt.put(pack(rr, cc), g[rr][cc]);
}
}
}
// 4) render gridv2 over cropped bounds (out-of-bounds become '#')
List<String> gridv2 = new ArrayList<>(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++) {
var ch = letterAt.get(pack(r, c));
row.append(ch != null ? ch : '#');
}
gridv2.add(row.toString());
}
// 5) words output with cropped coordinates
List<WordOut> wordsOut = new ArrayList<>(placed.size());
for (var p : placed) {
wordsOut.add(new WordOut(
p.word,
p.clue, // placeholder = word (same as JS)
p.startRow - minR,
p.startCol - minC,
p.direction,
p.word, // answer
p.arrowRow - minR,
p.arrowCol - minC,
p.isReversed,
puz.dict().words().get(p.word).cross()
));
}
return new ExportedPuzzle(gridv2, wordsOut, difficulty, rewards);
}
static final String HORIZONTAL = "h", VERTICAL = "v";
/**
* Convert a generator Slot + assigned word into a Placed object for export.
*/
private static Placed extractPlacedFromSlot(Slot s, String word) {
int r = s.clueR();
int c = s.clueC();
char d = s.dir();
List<int[]> cells = new ArrayList<>();
for (int i = 0; i < s.len(); i++) {
cells.add(new int[]{ s.rs()[i], s.cs()[i] });
}
// Canonicalize: always output right/down
int startRow, startCol, arrowRow, arrowCol;
String direction;
boolean isReversed = false;
if (d == '2') { // right -> horizontal
direction = HORIZONTAL;
startRow = cells.get(0)[0];
startCol = cells.get(0)[1];
arrowRow = r;
arrowCol = c;
} else if (d == '3' || d == '5') { // down or down-bent -> vertical
direction = VERTICAL;
startRow = cells.get(0)[0];
startCol = cells.get(0)[1];
arrowRow = r;
arrowCol = c;
} else if (d == '4') { // left -> horizontal (REVERSED)
direction = HORIZONTAL;
isReversed = true;
startRow = cells.get(0)[0];
startCol = cells.get(0)[1];
arrowRow = r;
arrowCol = c;
} else if (d == '1') { // up -> vertical (REVERSED)
direction = VERTICAL;
isReversed = true;
startRow = cells.get(0)[0];
startCol = cells.get(0)[1];
arrowRow = r;
arrowCol = c;
} else {
return null;
}
return new Placed(
word,
word, // clue placeholder
startRow,
startCol,
direction,
word, // answer
arrowRow,
arrowCol,
cells,
new int[]{ arrowRow, arrowCol },
isReversed
);
}
// pack (r,c) into one long key (handles negatives too)
private static long pack(int r, int c) {
return (((long) r) << 32) ^ (c & 0xFFFFFFFFL);
}
// ---------- Data models ----------
/**
* @param direction "horizontal" | "vertical"
* @param cells word cells
* @param arrow [arrowRow, arrowCol] */
private record Placed(String word, String clue, int startRow, int startCol, String direction, String answer, int arrowRow, int arrowCol, List<int[]> cells, int[] arrow,
boolean isReversed) { }
public record Rewards(int coins, int stars, int hints) { }
/**
* @param direction "horizontal" | "vertical" */
public record WordOut(String word, String clue, int startRow, int startCol, String direction, String answer, int arrowRow, int arrowCol, boolean isReversed, int complex) { }
public record ExportedPuzzle(List<String> gridv2, List<WordOut> words, int difficulty, Rewards rewards) { }
}