320 lines
13 KiB
Java
320 lines
13 KiB
Java
package puzzle;
|
|
|
|
import lombok.Getter;
|
|
import lombok.experimental.Accessors;
|
|
import lombok.experimental.Delegate;
|
|
import lombok.val;
|
|
import puzzle.Export.Gridded.Replacar.Cell;
|
|
import puzzle.Export.LetterVisit.LetterAt;
|
|
import puzzle.SwedishGenerator.Clues;
|
|
import puzzle.SwedishGenerator.FillResult;
|
|
import puzzle.SwedishGenerator.Grid;
|
|
import puzzle.SwedishGenerator.Slotinfo;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.function.IntSupplier;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.IntStream;
|
|
import java.util.stream.Stream;
|
|
import static puzzle.SwedishGenerator.R;
|
|
import static puzzle.SwedishGenerator.Lemma;
|
|
import static puzzle.SwedishGenerator.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() {
|
|
|
|
record Strings() {
|
|
|
|
static String padRight(String s, int n) {
|
|
if (s.length() >= n) return s;
|
|
return s + " ".repeat(n - s.length());
|
|
}
|
|
}
|
|
|
|
static final String INIT = IntStream.range(0, R).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)); }
|
|
String gridToString() {
|
|
var sb = new StringBuilder(INIT);
|
|
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, (char) (dir | 48));
|
|
});
|
|
return sb.toString();
|
|
}
|
|
public Stream<ClueAt> stream() {
|
|
val stream = Stream.<ClueAt>builder();
|
|
for (var l = c.lo & ~c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), 1));
|
|
for (var l = c.lo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), 0));
|
|
for (var l = c.lo & c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), 2));
|
|
for (var l = c.lo & c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), 3));
|
|
for (var h = c.hi & ~c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(64 | Long.numberOfTrailingZeros(h), 1));
|
|
for (var h = c.hi & ~c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(64 | Long.numberOfTrailingZeros(h), 0));
|
|
for (var h = c.hi & c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt((64 | Long.numberOfTrailingZeros(h)), 2));
|
|
for (var h = c.hi & c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt((64 | Long.numberOfTrailingZeros(h)), 3));
|
|
|
|
return stream.build();
|
|
}
|
|
}
|
|
|
|
@FunctionalInterface
|
|
interface LetterVisit {
|
|
|
|
record LetterAt(int index, byte letter) {
|
|
|
|
static LetterAt from(int index, byte[] bytes) { return new LetterAt(index, bytes[index]); }
|
|
}
|
|
|
|
void visit(int index, byte letter);
|
|
default void visit(int index, byte[] letters) { visit(index, letters[index]); }
|
|
}
|
|
|
|
record Gridded(@Delegate Grid grid) {
|
|
|
|
public Stream<LetterAt> stream(Clues clues) {
|
|
val stream = Stream.<LetterAt>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; h != X; h &= h - 1) stream.accept(LetterAt.from(64 | Long.numberOfTrailingZeros(h), grid.g));
|
|
return stream.build();
|
|
}
|
|
public void forEachLetter(Clues clues, LetterVisit visitor) {
|
|
for (var l = grid.lo & ~clues.lo; l != X; l &= l - 1) visitor.visit(Long.numberOfTrailingZeros(l), grid.g);
|
|
for (var h = grid.hi & ~clues.hi; h != X; h &= h - 1) visitor.visit(64 | Long.numberOfTrailingZeros(h), grid.g);
|
|
}
|
|
public static IntStream walk(byte base, long lo, long hi) {
|
|
if (Slot.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);
|
|
int res = 64 | msb;
|
|
temp &= ~(1L << msb);
|
|
return res;
|
|
}
|
|
}).limit(Long.bitCount(hi)),
|
|
IntStream.generate(new IntSupplier() {
|
|
|
|
long temp = lo;
|
|
@Override
|
|
public int getAsInt() {
|
|
int msb = 63 - Long.numberOfLeadingZeros(temp);
|
|
int res = msb;
|
|
temp &= ~(1L << msb);
|
|
return res;
|
|
}
|
|
}).limit(Long.bitCount(lo)));
|
|
}
|
|
}
|
|
String gridToString(Clues clues) {
|
|
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, (char) (dir | 48));
|
|
});
|
|
forEachLetter(clues, (idx, letter) -> {
|
|
val r = idx & 7;
|
|
val c = idx >>> 3;
|
|
sb.setCharAt(r * (C + 1) + c, (char) (letter | 64));
|
|
});
|
|
return sb.toString();
|
|
}
|
|
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);
|
|
}
|
|
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))));
|
|
});
|
|
forEachLetter(clues, (idx, letter) -> {
|
|
val r = idx & 7;
|
|
val c = idx >>> 3;
|
|
sb.setCharAt(r * (C + 1) + c, (char) (letter | 64));
|
|
});
|
|
return sb.toString().replaceAll(" ", "" + emptyFallback).split("\n");
|
|
}
|
|
}
|
|
|
|
interface Bit1029 {
|
|
|
|
public static long[] bit1029() { return new long[2048]; }
|
|
static int wordIndex(int bitIndex) { return bitIndex >> 6; }
|
|
static public boolean get(long[] bits, int bitIndex) { return (bits[wordIndex(bitIndex)] & 1L << bitIndex) != 0L; }
|
|
static public void set(long[] bits, int bitIndex) { bits[wordIndex(bitIndex)] |= 1L << bitIndex; }
|
|
static public void clear(long[] bits, int bitIndex) { bits[wordIndex(bitIndex)] &= ~(1L << bitIndex); }
|
|
static public void clear(long[] bits) { Arrays.fill(bits, 0L); }
|
|
}
|
|
|
|
record Placed(long lemma, int slotKey, int[] cells) {
|
|
|
|
static final char[] DIRECTION = { Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.HORIZONTAL };
|
|
public static final char HORIZONTAL = 'h';
|
|
static final char VERTICAL = 'v';
|
|
public int arrowCol() { return SwedishGenerator.IT[Slot.clueIndex(slotKey)].c(); }
|
|
public int arrowRow() { return SwedishGenerator.IT[Slot.clueIndex(slotKey)].r(); }
|
|
public int startRow() { return SwedishGenerator.IT[cells[0]].r(); }
|
|
public int startCol() { return SwedishGenerator.IT[cells[0]].c(); }
|
|
public boolean isReversed() { return !Slot.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(long l, int startRow, int startCol, char d, int arrowRow, int arrowCol, boolean isReversed) {
|
|
this(Lemma.asWord(l), new int[]{ arrowRow, arrowCol, startRow, startCol }, startRow, startCol, d, arrowRow, arrowCol, isReversed, Lemma.simpel(l), Lemma.clue(l));
|
|
}
|
|
}
|
|
|
|
public record ExportedPuzzle(String[] grid, WordOut[] words, int difficulty, Rewards rewards) { }
|
|
|
|
public record PuzzleResult(Clued clues, Slotinfo[] slots, FillResult filled) {
|
|
|
|
public ExportedPuzzle exportFormatFromFilled(int difficulty, Rewards rewards) {
|
|
var g = filled().grid();
|
|
var placed = new ArrayList<Placed>();
|
|
for (var slot : slots) {
|
|
placed.add(new Placed(slot.assign().w, slot.key(), Gridded.walk((byte) slot.key(), slot.lo(), slot.hi()).toArray()));
|
|
}
|
|
|
|
/* clues.forEachSlot((int key, long lo, long hi) -> {
|
|
var word = clueMap[key];
|
|
if (word != 0L) {
|
|
placed.add(extractPlacedFromSlot(Slot.from(key, lo, hi, entries[Slot.length(lo, hi)]), word));
|
|
} else {
|
|
System.err.println("Could not find clue for slot: " + key);
|
|
}
|
|
});*/
|
|
|
|
// If nothing placed: return full grid mapped to letters/# only
|
|
if (placed.isEmpty()) {
|
|
return new ExportedPuzzle(g.exportGrid(clues.c, _ -> '#', '#'), new WordOut[0], difficulty, rewards);
|
|
}
|
|
|
|
// 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 = SwedishGenerator.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 letterAt = new HashMap<Integer, Character>();
|
|
g.forEachLetter(clues.c(), (idx, letter) -> {
|
|
if (letter == 0) return;
|
|
letterAt.put(idx, (char) (64 | letter));
|
|
});
|
|
|
|
// 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(letterAt.getOrDefault(Grid.offset(r, c), '#'));
|
|
gridv2[i] = row.toString();
|
|
}
|
|
|
|
// 5) words output with cropped coordinates
|
|
int MIN_R = minR, MIN_C = minC;
|
|
var wordsOut = placed.stream().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()
|
|
)).toArray(WordOut[]::new);
|
|
return new ExportedPuzzle(gridv2, wordsOut, difficulty, rewards);
|
|
}
|
|
}
|
|
|
|
record DictEntryDTO(LongArrayList words, IntListDTO[][] pos) {
|
|
|
|
public DictEntryDTO(int L) {
|
|
this(new LongArrayList(1024), new IntListDTO[L][26]);
|
|
for (var i = 0; i < L; i++) for (var j = 0; j < 26; j++) pos[i][j] = new IntListDTO();
|
|
}
|
|
}
|
|
|
|
@Getter
|
|
@Accessors(fluent = true)
|
|
static final class IntListDTO {
|
|
|
|
int[] data = new int[8];
|
|
int size = 0;
|
|
void add(int v) {
|
|
if (size >= data.length) data = Arrays.copyOf(data, data.length * 2);
|
|
data[size++] = v;
|
|
}
|
|
}
|
|
}
|