package puzzle; import module java.base; import anno.GenerateShapedCopies; import anno.Shaped; import lombok.experimental.Delegate; import lombok.val; import precomp.Const9x8; import precomp.Mask; import puzzle.Riddle.ClueSign; import puzzle.Riddle.ExportedPuzzle; import puzzle.Riddle.Placed; import puzzle.Riddle.Rewards; import puzzle.Riddle.Signa; import puzzle.Riddle.WordOut; import puzzle.SwedishGenerator.FillResult; import puzzle.SwedishGenerator.Grid; import puzzle.SwedishGenerator.Slotinfo; import static puzzle.Masker.Slot; import static puzzle.SwedishGenerator.X; import java.util.stream.Stream; import java.util.Arrays; @GenerateShapedCopies( packageName = "puzzle", className = "ExportX", shapes = { "precomp.Const9x8", "precomp.Const3x4" } ) public record Export() { public record ExportTemplates(byte[] table, byte[] dashTable, byte[] wordBytes) { } @Shaped static final byte SPACE = Const9x8.SPACE; @Shaped static final byte LINE_BREAK = Const9x8.LINE_BREAK; @Shaped static final byte DASH = Const9x8.DASH; @Shaped static final int SIZE = Const9x8.SIZE; @Shaped static final byte[] INIT_GRID_OUTPUT_ARR = Const9x8.INIT_GRID_OUTPUT_ARR; @Shaped static final byte[] INIT_GRID_OUTPUT_DASH_ARR = Const9x8.INIT_GRID_OUTPUT_DASH_ARR; @Shaped static final long MASK_HI = Const9x8.MASK_HI; @Shaped static final long MASK_LO = Const9x8.MASK_LO; @Shaped static final Mask[] CELLS = Const9x8.CELLS; public static final ThreadLocal BYTES = ThreadLocal.withInitial( () -> new ExportTemplates(INIT_GRID_OUTPUT_ARR, INIT_GRID_OUTPUT_DASH_ARR, new byte[8])); static int HI(int in) { return in | 64; } public static String gridToString(Clues clues) { val chars = BYTES.get().table(); var signa = new Signa(clues).map(v -> CELLS[v.cellIndex()]).toArray(Mask[]::new); Arrays.stream(signa).forEach(v -> chars[v.place()] = v.clueChar()); val result = new String(chars); Arrays.stream(signa).forEach(v -> chars[v.place()] = SPACE); return result; } record Puzzle(@Delegate Grid grid, Mask[] cells, Clues cl) implements Stream { public Puzzle { for (var l = grid.lo & MASK_LO & ~cl.lo; l != X; l &= l - 1) set(Long.numberOfTrailingZeros(l), cells, grid.g); for (var h = grid.hi & MASK_HI & ~cl.hi; h != X; h &= h - 1) set(HI(Long.numberOfTrailingZeros(h)), cells, grid.g); new Signa(cl).forEach(v -> cells[v.index()] = CELLS[v.cellIndex()]); } static void set(int idx, Mask[] cells, byte[] read) { cells[idx] = CELLS[idx * 33 + read[idx]]; } public Puzzle(Grid grid, Clues cl) { this(grid, new Mask[grid.g.length], cl); } public Puzzle(Clues clues) { this(new Grid(new byte[SIZE], clues.lo, clues.hi), new Mask[SIZE], clues); } public Puzzle(Signa clues) { this(clues.c()); } public @Delegate Stream stream() { val stream = Stream.builder(); for (var l = grid.lo & MASK_LO & ~cl.lo; l != X; l &= l - 1) stream.accept(cells[Long.numberOfTrailingZeros(l)]); for (var h = grid.hi & MASK_HI & ~cl.hi; h != X; h &= h - 1) stream.accept(cells[HI(Long.numberOfTrailingZeros(h))]); return stream.build(); } public Puzzle sync() { for (var l = grid.lo & MASK_LO & ~cl.lo; l != X; l &= l - 1) set(Long.numberOfTrailingZeros(l), cells, grid.g); for (var h = grid.hi & MASK_HI & ~cl.hi; h != X; h &= h - 1) set(HI(Long.numberOfTrailingZeros(h)), cells, grid.g); new Signa(cl).forEach(v -> cells[v.index()] = CELLS[v.cellIndex()]); return this; } } public record PuzzleResult(Signa clues, Puzzle puzzle, Slotinfo[] slots, FillResult filled) { public String exportGrid(ClueSign clueChar, byte[] sb) { Arrays.stream(slots).map(s -> puzzle.cells[Slot.clueIndex(s.key())]).forEach(c -> sb[c.place()] = clueChar.replace(c.clueChar())); puzzle.forEach((l) -> sb[l.place()] = l.letter()); return new String(sb); } public String cluesGridToString() { return gridToString(clues.c()); } public String gridRenderHuman() { return exportGrid(_ -> SPACE, INIT_GRID_OUTPUT_DASH_ARR.clone()); } public String gridGridToString() { return exportGrid(d1 -> d1, INIT_GRID_OUTPUT_ARR.clone()); } public ExportedPuzzle exportFormatFromFilled(Rewards rewards) { if (slots.length == 0) { return new ExportedPuzzle(new String(INIT_GRID_OUTPUT_DASH_ARR).split("\n"), new WordOut[0], 1, rewards); } var placed = Arrays.stream(slots) .map(slot -> new Placed(slot.assign().w, slot.key(), Riddle.cellWalk(slot.key(), slot.lo(), slot.hi()) .mapToObj(idx -> puzzle.cells[idx]) .toArray(Mask[]::new))) .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 it : rc.cells()) { minR = Math.min(minR, it.r()); minC = Math.min(minC, it.c()); maxR = Math.max(maxR, it.r()); maxC = Math.max(maxC, it.c()); } } // 3) render grid over cropped bounds (out-of-bounds become '#') final int MINR = minR, MINC = minC; int height = Math.max(0, maxR - minR + 1); int width = Math.max(0, maxC - minC + 1); byte[] template = new byte[height * (width + 1)]; Arrays.fill(template, DASH); for (int i = width; i < template.length; i += width + 1) template[i] = LINE_BREAK; puzzle.forEach(l -> l.letter(template, MINR, MINC, height, width)); var grid = new String(template).split("\n"); // 5) words output with cropped coordinates val bytes = BYTES.get().wordBytes(); var wordsOut = Arrays.stream(placed).map(p -> new WordOut( p.lemma(), p.startRow() - MINR, p.startCol() - MINC, p.direction(), p.arrowRow() - MINR, p.arrowCol() - MINC, p.isReversed(), bytes )).toArray(WordOut[]::new); var total = 0.0001d + Arrays.stream(wordsOut).mapToDouble(Riddle.WordOut::complex).sum(); return new ExportedPuzzle(grid, wordsOut, (int) (total / wordsOut.length), rewards); } } }