313 lines
14 KiB
Java
313 lines
14 KiB
Java
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<byte[]> 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<Vestigium> stream() {
|
|
val stream = Stream.<Vestigium>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<Lettrix> {
|
|
|
|
public Puzzle(Clues clues) { this(Masker.toGrid(clues), clues); }
|
|
public Puzzle(Signa clues) { this(clues.c); }
|
|
public @Delegate Stream<Lettrix> stream() {
|
|
val stream = Stream.<Lettrix>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(); }
|
|
}
|
|
}
|