Files
puzzle-generator/src/main/java/puzzle/Export.java
2026-01-24 01:43:41 +01:00

142 lines
6.9 KiB
Java

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<ExportTemplates> 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<Mask> {
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<Mask> stream() {
val stream = Stream.<Mask>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);
}
}
}