package puzzle; import module java.base; import lombok.AllArgsConstructor; import lombok.experimental.Delegate; import lombok.val; import precomp.Const9x8.Cell; import puzzle.Export.Puzzle.Replacar.Rell; import puzzle.Meta.ShardLem; import puzzle.SwedishGenerator.Dict; import puzzle.SwedishGenerator.FillResult; import puzzle.SwedishGenerator.Grid; import puzzle.SwedishGenerator.Slotinfo; import static precomp.Const9x8.CLUE_DOWN0; import static precomp.Const9x8.CLUE_RIGHT1; import static precomp.Const9x8.INIT_GRID_OUTPUT; import static precomp.Const9x8.INIT_GRID_OUTPUT_ARR; import static puzzle.Export.Clue.DOWN0; import static puzzle.Export.Clue.RIGHT1; import static puzzle.Clues.createEmpty; import static puzzle.Masker.Slot; import static puzzle.Masker.C; import static puzzle.SwedishGenerator.Lemma; 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[8]); 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 { DOWN0(CLUE_DOWN0, 'B', 'b'), RIGHT1(CLUE_RIGHT1, 'A', 'a'), UP2(CLUE_UP, 'C', 'c'), LEFT3(CLUE_LEFT, 'D', 'd'), LEFT_TOP4(CLUE_LEFT_TOP, 'E', 'e'), RIGHT_TOP5(CLUE_RIGHT_TOP, 'F', 'f'), NONE(CLUE_LEFT, '?', '?'); final byte dir; final char slotChar, clueChar; private static final Clue[] CLUES = new Clue[]{ DOWN0, RIGHT1, UP2, LEFT3, LEFT_TOP4, RIGHT_TOP5, NONE, NONE, NONE }; public static Clue from(int dir) { return CLUES[dir]; } } public record Vestigium(int index, int clue) { } public record Signa(@Delegate Clues c) { public static Signa of(Cell... cells) { var c = createEmpty(); for (var cell : cells) c.setClue(cell); return new Signa(c); } public Signa deepCopyGrid() { return new Signa(new Clues(c.lo, c.hi, c.vlo, c.vhi, c.rlo, c.rhi, c.xlo, c.xhi)); } String gridToString() { var sb = new StringBuilder(INIT_GRID_OUTPUT); Masker.forEachSlot(c, (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 Vestigium(Long.numberOfTrailingZeros(l), RIGHT1.dir)); for (var l = c.lo & ~c.xlo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), DOWN0.dir)); for (var l = c.lo & ~c.xlo & c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), CLUE_UP)); for (var l = c.lo & ~c.xlo & c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), CLUE_LEFT)); for (var l = c.lo & c.xlo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(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 Vestigium(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 Vestigium(HI(Long.numberOfTrailingZeros(h)), CLUE_RIGHT1)); for (var h = c.hi & ~c.xhi & ~c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(HI(Long.numberOfTrailingZeros(h)), CLUE_DOWN0)); for (var h = c.hi & ~c.xhi & c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(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 Vestigium(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 Vestigium(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 Vestigium(HI(Long.numberOfTrailingZeros(h)), CLUE_RIGHT_TOP)); return stream.build(); } public Slotinfo[] slots(Dict D) { return Masker.slots(c, D.index(), D.reversed()); } } record Puzzle(@Delegate Grid grid, Clues cl) implements Stream { public Puzzle(Clues clues) { this(Masker.toGrid(clues), clues); } public Puzzle(Signa clues) { this(clues.c); } public @Delegate Stream stream() { val stream = Stream.builder(); for (var l = grid.lo & ~cl.lo; l != X; l &= l - 1) stream.accept(Lettrix.from(Long.numberOfTrailingZeros(l), grid.g)); for (var h = grid.hi & ~cl.hi & 0xFF; h != X; h &= h - 1) stream.accept(Lettrix.from(64 | Long.numberOfTrailingZeros(h), grid.g)); return stream.build(); } String gridToString() { var sb = INIT_GRID_OUTPUT_ARR.clone(); Masker.forEachSlot(cl, (s, _, _) -> { val idx = Slot.clueIndex(s); val r = idx & 7; val c = idx >>> 3; val dir = Slot.dir(s); sb[r * (C + 1) + c] = (byte) (dir | 48); }); stream().forEach((l) -> sb[l.index(C + 1)] = (byte) l.human()); return new String(sb); } public String[] exportGrid(Replacar clueChar, char emptyFallback) { var sb = INIT_GRID_OUTPUT_ARR.clone(); Masker.forEachSlot(cl, (s, l, a) -> { val idx = Slot.clueIndex(s); val r = idx & 7; val c = idx >>> 3; val dir = Slot.dir(s); sb[r * (C + 1) + c] = (byte) clueChar.replace(new Rell(grid, cl, idx, (byte) (dir | 48))); }); stream().forEach((l) -> sb[l.index(C + 1)] = (byte) l.human()); return new String(sb).replaceAll(" ", String.valueOf(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() { return String.join("\n", exportGrid(_ -> ' ', '#')); } @FunctionalInterface interface Replacar { record Rell(Grid grid, Clues clues, int index, byte data) { } char replace(Rell 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) { record ShaLemma(String word, @Delegate ShardLem rec) { } private static ShaLemma lookup(long w, byte[] bytes) { try { val rec = Meta.lookupSilent(w); System.out.println("\nQuery: w=" + w + " -> i=" + rec.mmap()); var word1 = Lemma.asWord(w, bytes); System.out.println(" word=" + word1 + "\n" + " simpel=" + rec.simpel() + "\n" + " clues=" + Arrays.toString(rec.clues())); return new ShaLemma(word1, rec); } catch (Exception e) { throw new RuntimeException(e); } } static long reverse(long w) { int L = Lemma.unpackSize(w) + 1; long letters = w & Lemma.LETTER_MASK; long rev = 0; for (int i = 0; i < L; i++) { long letter = (letters >>> (5 * i)) & 31; rev |= (letter << (5 * (L - 1 - i))); } return (w & ~Lemma.LETTER_MASK) | rev; } public WordOut(long l, int startRow, int startCol, char d, int arrowRow, int arrowCol, boolean isReversed, byte[] bytes) { val meta = lookup(isReversed ? reverse(l) : l, bytes); this(meta.word, 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(Signa clues, Puzzle grid, Slotinfo[] slots, FillResult filled) { public ExportedPuzzle exportFormatFromFilled(Rewards rewards) { // If nothing placed: return full grid mapped to letters/# only if (slots.length == 0) { return new ExportedPuzzle(grid.exportGrid(_ -> '#', '#'), new WordOut[0], 1, rewards); } var placed = Arrays.stream(slots).map(slot -> new Placed(slot.assign().w, slot.key(), Puzzle.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().collect(Collectors.toMap(Lettrix::index, Lettrix::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( 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); var total = 0.0001d + Arrays.stream(wordsOut).mapToDouble(WordOut::complex).sum(); return new ExportedPuzzle(gridv2, wordsOut, (int) (total / wordsOut.length), rewards); } } record Lettrix(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 Lettrix from(int index, byte[] bytes) { return new Lettrix(index, bytes[index]); } public int index(int cols) { return (row() * cols) + col(); } } }