package puzzle; import module java.base; import lombok.AllArgsConstructor; import lombok.experimental.Delegate; import lombok.val; import puzzle.Export.Gridded.Replacar.Cell; import puzzle.Export.LetterVisit.LetterAt; import puzzle.Masker.Clues; import puzzle.SwedishGenerator.FillResult; import puzzle.SwedishGenerator.Grid; import puzzle.SwedishGenerator.Slotinfo; import static puzzle.Export.Clue.DOWN; import static puzzle.Export.Clue.RIGHT; import static puzzle.Masker.Clues.createEmpty; import static puzzle.SwedishGenerator.R; import static puzzle.SwedishGenerator.Lemma; import static puzzle.Masker.Slot; import static puzzle.SwedishGenerator.C; import static puzzle.SwedishGenerator.X; /** * 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() { public static final ThreadLocal BYTES = ThreadLocal.withInitial(() -> new byte[SwedishGenerator.MAX_WORD_LENGTH]); static final byte CLUE_DOWN = 0; static final byte CLUE_RIGHT = 1; static final byte CLUE_UP = 2; static final byte CLUE_LEFT = 3; static final byte CLUE_LEFT_TOP = 4; static final byte CLUE_RIGHT_TOP = 5; static int HI(int in) { return in | 64; } static char LETTER(int in) { return (char) (in | 64); } static char CLUE_CHAR(int s) { return (char) (s | 48); } static int INDEX_ROW(int idx) { return idx & 7; } static int INDEX_COL(int idx) { return idx >>> 3; } static int INDEX(int r, int cols, int c) { return r * cols + c; } @AllArgsConstructor enum Clue { DOWN(CLUE_DOWN), RIGHT(CLUE_RIGHT), UP(CLUE_UP), LEFT(CLUE_LEFT); final byte dir; } record Strings() { static String padRight(String s, int n) { return s.length() >= n ? s : s + " ".repeat(n - s.length()); } } static final String INIT = IntStream.range(0, Config.PUZZLE_ROWS).mapToObj(l_ -> " ").collect(Collectors.joining("\n")); public record ClueAt(int index, int clue) { } public record Clued(@Delegate Clues c) { public Clued deepCopyGrid() { return new Clued(new Clues(c.lo, c.hi, c.vlo, c.vhi, c.rlo, c.rhi, c.xlo, c.xhi)); } public static Clued parse(String s) { var c = createEmpty(); var lines = s.split("\n"); for (int r = 0; r < Math.min(lines.length, R); r++) { var line = lines[r]; for (int col = 0; col < Math.min(line.length(), C); col++) { char ch = line.charAt(col); if (ch >= '0' && ch <= '4') { int idx = Masker.offset(r, col); byte dir = (byte) (ch - '0'); if ((idx & 64) == 0) c.setClueLo(1L << idx, dir); else c.setClueHi(1L << (idx & 63), dir); } } } return new Clued(c); } String gridToString() { var sb = new StringBuilder(INIT); forEachSlot((s, _, _) -> { val idx = Slot.clueIndex(s); val dir = Slot.dir(s); sb.setCharAt(INDEX(INDEX_ROW(idx), C + 1, INDEX_COL(idx)), CLUE_CHAR(dir)); }); return sb.toString(); } public Stream stream() { val stream = Stream.builder(); for (var l = c.lo & ~c.xlo & ~c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), RIGHT.dir)); for (var l = c.lo & ~c.xlo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), DOWN.dir)); for (var l = c.lo & ~c.xlo & c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), CLUE_UP)); for (var l = c.lo & ~c.xlo & c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), CLUE_LEFT)); for (var l = c.lo & c.xlo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), CLUE_LEFT_TOP)); for (var l = c.lo & c.xlo & ~c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), CLUE_RIGHT_TOP)); for (var h = c.hi & ~c.xhi & ~c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_RIGHT)); for (var h = c.hi & ~c.xhi & ~c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_DOWN)); for (var h = c.hi & ~c.xhi & c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_UP)); for (var h = c.hi & ~c.xhi & c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_LEFT)); for (var h = c.hi & c.xhi & ~c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_LEFT_TOP)); for (var h = c.hi & c.xhi & ~c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(HI(Long.numberOfTrailingZeros(h)), CLUE_RIGHT_TOP)); return stream.build(); } } @FunctionalInterface interface LetterVisit { record LetterAt(int index, byte letter) { public int row() { return INDEX_ROW(index); } public int col() { return INDEX_COL(index); } public char human() { return LETTER(letter); } static LetterAt from(int index, byte[] bytes) { return new LetterAt(index, bytes[index]); } public int index(int cols) { return (row() * cols) + col(); } } void visit(int index, byte letter); default void visit(int index, byte[] letters) { visit(index, letters[index]); } } record Gridded(@Delegate Grid grid, Clues cl) { public Gridded(Clues clues) { this(clues.toGrid(), clues); } public Stream stream(Clues clues) { val stream = Stream.builder(); for (var l = grid.lo & ~clues.lo; l != X; l &= l - 1) stream.accept(LetterAt.from(Long.numberOfTrailingZeros(l), grid.g)); for (var h = grid.hi & ~clues.hi & 0xFF; h != X; h &= h - 1) stream.accept(LetterAt.from(64 | Long.numberOfTrailingZeros(h), grid.g)); return stream.build(); } String gridToString(Clues clues) { var sb = new StringBuilder(INIT); clues.forEachSlot((s, _, _) -> { val idx = Slot.clueIndex(s); val r = idx & 7; val c = idx >>> 3; val dir = Slot.dir(s); sb.setCharAt(r * (C + 1) + c, (char) (dir | 48)); }); stream(clues).forEach((l) -> sb.setCharAt(l.index(C + 1), l.human())); return sb.toString(); } public String[] exportGrid(Clues clues, Replacar clueChar, char emptyFallback) { var sb = new StringBuilder(INIT); clues.forEachSlot((s, l, a) -> { val idx = Slot.clueIndex(s); val r = idx & 7; val c = idx >>> 3; val dir = Slot.dir(s); sb.setCharAt(r * (C + 1) + c, clueChar.replace(new Cell(grid, clues, idx, (byte) (dir | 48)))); }); stream(clues).forEach((l) -> sb.setCharAt(l.index(C + 1), l.human())); return sb.toString().replaceAll(" ", "" + emptyFallback).split("\n"); } public static IntStream cellWalk(byte base, long lo, long hi) { if (Slotinfo.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); temp &= ~(1L << msb); return 64 | msb; } }).limit(Long.bitCount(hi)), IntStream.generate(new IntSupplier() { long temp = lo; @Override public int getAsInt() { int msb = 63 - Long.numberOfLeadingZeros(temp); temp &= ~(1L << msb); return msb; } }).limit(Long.bitCount(lo))); } } public String renderHuman(Clues clues) { return String.join("\n", exportGrid(clues, _ -> ' ', '#')); } @FunctionalInterface interface Replacar { record Cell(Grid grid, Clues clues, int index, byte data) { } char replace(Cell c); } } record Placed(long lemma, int slotKey, int[] cells) { public static final char HORIZONTAL = 'h'; static final char VERTICAL = 'v'; static final char[] DIRECTION = { Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.VERTICAL }; public int arrowCol() { return Masker.IT[Slot.clueIndex(slotKey)].c(); } public int arrowRow() { return Masker.IT[Slot.clueIndex(slotKey)].r(); } public int startRow() { return Masker.IT[cells[0]].r(); } public int startCol() { return Masker.IT[cells[0]].c(); } public boolean isReversed() { return !Slotinfo.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(Path shard,long l, int startRow, int startCol, char d, int arrowRow, int arrowCol, boolean isReversed, byte[] bytes) { val meta = Meta.readRecord(shard, Lemma.unpackShardIndex(l)); this(Lemma.asWord(l, bytes), new int[]{ arrowRow, arrowCol, startRow, startCol }, startRow, startCol, d, arrowRow, arrowCol, isReversed, meta.simpel(), meta.clues()); } } public record ExportedPuzzle(String[] grid, WordOut[] words, int difficulty, Rewards rewards) { } public record PuzzleResult(Clued clues, Gridded grid, Slotinfo[] slots, FillResult filled, int thress) { public Path shardKey(long word) { return shardKey(this.thress, word); } public static Path shardKey(int thress, long word) { if (thress <= 0) return Path.of("src/main/generated-sources/puzzle").resolve(Lemma.unpackSize(word) + 1 + ".idx"); else return Path.of("src/main/generated-sources/puzzle/dict" + thress).resolve(Lemma.unpackSize(word) + 1 + ".idx"); } public long calcSimpel( Slotinfo[] slots) { return calcSimpel(thress, slots); } static public long calcSimpel(int thress, Slotinfo[] slots) { int k = 0; long simpel = 0L; for (var n = 1; n < slots.length; n++) { if (slots[n].assign().w != X) { k++; simpel += Meta.readRecord(shardKey(thress,slots[n].assign().w), Lemma.unpackShardIndex(slots[n].assign().w)).simpel(); } } simpel = k == 0 ? 0 : simpel / k; return simpel; } public ExportedPuzzle exportFormatFromFilled(int difficulty, Rewards rewards) { // If nothing placed: return full grid mapped to letters/# only if (slots.length == 0) { return new ExportedPuzzle(grid.exportGrid(clues.c, _ -> '#', '#'), new WordOut[0], difficulty, rewards); } var placed = Arrays.stream(slots).map(slot -> new Placed(slot.assign().w, slot.key(), Gridded.cellWalk((byte) slot.key(), slot.lo(), slot.hi()).toArray())).toArray( Placed[]::new); // 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) { val it = Masker.IT[c]; minR = Math.min(minR, it.r()); minC = Math.min(minC, it.c()); maxR = Math.max(maxR, it.r()); maxC = Math.max(maxC, it.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 map = grid.stream(clues.c()).collect(Collectors.toMap(LetterAt::index, LetterAt::human)); // 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(map.getOrDefault(Masker.offset(r, c), '#')); gridv2[i] = row.toString(); } // 5) words output with cropped coordinates int MIN_R = minR, MIN_C = minC; val bytes = BYTES.get(); var wordsOut = Arrays.stream(placed).map(p -> new WordOut( shardKey( p.lemma), p.lemma, p.startRow() - MIN_R, p.startCol() - MIN_C, p.direction(), p.arrowRow() - MIN_R, p.arrowCol() - MIN_C, p.isReversed(), bytes )).toArray(WordOut[]::new); return new ExportedPuzzle(gridv2, wordsOut, difficulty, rewards); } } }