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; } } 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 = { Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.HORIZONTAL }; 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(); 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(); 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; } } }