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 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(); 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(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(); 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(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); } } }