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 = 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 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 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 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 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 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 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 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 gridv2, List words, int difficulty, Rewards rewards) { } }