introduce bitloops
This commit is contained in:
@@ -163,17 +163,8 @@ public record Export() {
|
|||||||
|
|
||||||
public record PuzzleResult(SwedishGenerator swe, Dict dict, Gridded mask, FillResult filled) {
|
public record PuzzleResult(SwedishGenerator swe, Dict dict, Gridded mask, FillResult filled) {
|
||||||
|
|
||||||
boolean inBounds(int idx) { return idx >= 0 && idx < SwedishGenerator.SIZE; }
|
boolean inBounds(int idx) { return idx >= 0 && idx < SwedishGenerator.SIZE; }
|
||||||
Placed extractPlacedFromSlot(Slot s, long lemma) {
|
Placed extractPlacedFromSlot(Slot s, long lemma) { return new Placed(lemma, s.key(), s.walk().toArray()); }
|
||||||
|
|
||||||
var cells = s.walk().toArray();
|
|
||||||
|
|
||||||
return new Placed(
|
|
||||||
lemma,
|
|
||||||
s.key(),
|
|
||||||
cells
|
|
||||||
);
|
|
||||||
}
|
|
||||||
public ExportedPuzzle exportFormatFromFilled(int difficulty, Rewards rewards) {
|
public ExportedPuzzle exportFormatFromFilled(int difficulty, Rewards rewards) {
|
||||||
var g = filled().grid();
|
var g = filled().grid();
|
||||||
var placed = new ArrayList<Placed>();
|
var placed = new ArrayList<Placed>();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package puzzle;
|
package puzzle;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.val;
|
||||||
import puzzle.SwedishGenerator.Rng;
|
import puzzle.SwedishGenerator.Rng;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -346,7 +347,7 @@ public class Main {
|
|||||||
|
|
||||||
static PuzzleResult attempt(Rng rng, Dict dict, Opts opts) {
|
static PuzzleResult attempt(Rng rng, Dict dict, Opts opts) {
|
||||||
try {
|
try {
|
||||||
return _attempt(rng, dict, opts);
|
return _attempt(rng, dict, opts);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
System.err.println("Failed to operate" + e.getMessage());
|
System.err.println("Failed to operate" + e.getMessage());
|
||||||
@@ -356,8 +357,9 @@ public class Main {
|
|||||||
static PuzzleResult _attempt(Rng rng, Dict dict, Opts opts) {
|
static PuzzleResult _attempt(Rng rng, Dict dict, Opts opts) {
|
||||||
TOTAL_ATTEMPTS.incrementAndGet();
|
TOTAL_ATTEMPTS.incrementAndGet();
|
||||||
var swe = new SwedishGenerator(rng);
|
var swe = new SwedishGenerator(rng);
|
||||||
var mask = swe.generateMask(opts.pop, opts.gens, Math.max(opts.pop, (int) Math.floor(opts.pop * 1.5)));
|
val stack = new int[STACK_SIZE];
|
||||||
var filled = swe.fillMask(mask, dict.index(), opts.fillTimeout);
|
var mask = swe.generateMask(stack, opts.pop, opts.gens, Math.max(opts.pop, (int) Math.floor(opts.pop * 1.5)));
|
||||||
|
var filled = fillMask(rng, mask, dict.index());
|
||||||
|
|
||||||
TOTAL_NODES.addAndGet(filled.stats().nodes);
|
TOTAL_NODES.addAndGet(filled.stats().nodes);
|
||||||
TOTAL_BACKTRACKS.addAndGet(filled.stats().backtracks);
|
TOTAL_BACKTRACKS.addAndGet(filled.stats().backtracks);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package puzzle;
|
package puzzle;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
import lombok.experimental.Delegate;
|
||||||
import lombok.val;
|
import lombok.val;
|
||||||
import precomp.Neighbors9x8;
|
import precomp.Neighbors9x8;
|
||||||
import precomp.Neighbors9x8.rci;
|
import precomp.Neighbors9x8.rci;
|
||||||
@@ -59,9 +62,11 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
static final int MAX_WORD_LENGTH_PLUS_ONE = MAX_WORD_LENGTH + 1;
|
static final int MAX_WORD_LENGTH_PLUS_ONE = MAX_WORD_LENGTH + 1;
|
||||||
static final int MIN_LEN = Config.MIN_LEN;
|
static final int MIN_LEN = Config.MIN_LEN;
|
||||||
static final int MAX_TRIES_PER_SLOT = Config.MAX_TRIES_PER_SLOT;
|
static final int MAX_TRIES_PER_SLOT = Config.MAX_TRIES_PER_SLOT;
|
||||||
|
static final int STACK_SIZE = 64;
|
||||||
static final char C_DASH = '\0';
|
static final char C_DASH = '\0';
|
||||||
static final byte _1 = 49, _9 = 57, A = 65, Z = 90, DASH = (byte) C_DASH;
|
static final byte _1 = 49, _9 = 57, A = 65, Z = 90, DASH = (byte) C_DASH;
|
||||||
|
//72 << 3;
|
||||||
|
static final int CLUE_INDEX_MAX_SIZE = (288 | 3) + 1;
|
||||||
static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); }
|
static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); }
|
||||||
record Pick(Slot slot, CandidateInfo info, boolean done) { }
|
record Pick(Slot slot, CandidateInfo info, boolean done) { }
|
||||||
// 0b11
|
// 0b11
|
||||||
@@ -94,19 +99,22 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
static final Pick PICK_DONE = new Pick(null, null, true);
|
static final Pick PICK_DONE = new Pick(null, null, true);
|
||||||
static final Pick PICK_NOT_DONE = new Pick(null, null, false);
|
static final Pick PICK_NOT_DONE = new Pick(null, null, false);
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Accessors(fluent = true)
|
||||||
public static final class FillStats {
|
public static final class FillStats {
|
||||||
|
|
||||||
public long nodes;
|
final public long nodes;
|
||||||
public long backtracks;
|
final public long backtracks;
|
||||||
public double seconds;
|
final public double seconds;
|
||||||
public int lastMRV;
|
final public int lastMRV;
|
||||||
public double simplicity;
|
public double simplicity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static record FillResult(boolean ok,
|
public static record FillResult(boolean ok,
|
||||||
Gridded grid,
|
Gridded grid,
|
||||||
long[] clueMap,
|
long[] clueMap,
|
||||||
FillStats stats) {
|
@Delegate FillStats stats) {
|
||||||
|
|
||||||
public void calcSimpel() {
|
public void calcSimpel() {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -131,9 +139,9 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static final record Context(int[] stack, long[] undo, long[] bitset) {
|
static final record Context(long[] bitset) {
|
||||||
|
|
||||||
public Context() { this(new int[SIZE], new long[128], new long[2500]); }
|
public Context() { this(new long[2500]); }
|
||||||
private static final ThreadLocal<Context> CTX = ThreadLocal.withInitial(Context::new);
|
private static final ThreadLocal<Context> CTX = ThreadLocal.withInitial(Context::new);
|
||||||
|
|
||||||
public static Context get() { return CTX.get(); }
|
public static Context get() { return CTX.get(); }
|
||||||
@@ -381,18 +389,18 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((rayLo | rayHi) != 0) {
|
visitor.visit(key, rayLo, rayHi);
|
||||||
visitor.visit(key, rayLo, rayHi);
|
//if ((rayLo | rayHi) == 0L) throw new RuntimeException()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static ArrayList<Slot> extractSlots(Grid grid) {
|
static Slot[] extractSlots(Grid grid) {
|
||||||
var slots = new ArrayList<Slot>(32);
|
var slots = new Slot[grid.clueCount()];
|
||||||
grid.forEachSlot((key, lo, hi) -> slots.add(Slot.from(key, lo, hi)));
|
int[] N = new int[]{ 0 };
|
||||||
|
grid.forEachSlot((key, lo, hi) -> slots[N[0]++] = Slot.from(key, lo, hi));
|
||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
|
|
||||||
long maskFitness(Grid grid) {
|
long maskFitness(Grid grid, int[] stack) {
|
||||||
|
|
||||||
long cHLo = 0L, cHHi = 0L, cVLo = 0L, cVHi = 0L;
|
long cHLo = 0L, cHHi = 0L, cVLo = 0L, cVHi = 0L;
|
||||||
long lo_cl = grid.lo, hi_cl = grid.hi;
|
long lo_cl = grid.lo, hi_cl = grid.hi;
|
||||||
@@ -437,8 +445,6 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasSlots) return 1_000_000_000L;
|
if (!hasSlots) return 1_000_000_000L;
|
||||||
var ctx = Context.get();
|
|
||||||
var stack = ctx.stack;
|
|
||||||
long seenLo = 0L, seenHi = 0L;
|
long seenLo = 0L, seenHi = 0L;
|
||||||
|
|
||||||
// loop over beide helften
|
// loop over beide helften
|
||||||
@@ -448,7 +454,7 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
|
|
||||||
// "unseen clues" in deze helft
|
// "unseen clues" in deze helft
|
||||||
for (long bits = clueMask & ~seenMask; bits != 0L; bits &= bits - 1) {
|
for (long bits = clueMask & ~seenMask; bits != 0L; bits &= bits - 1) {
|
||||||
int clueIdx = base + Long.numberOfTrailingZeros(bits);
|
int clueIdx = base | Long.numberOfTrailingZeros(bits);
|
||||||
|
|
||||||
// start nieuwe component
|
// start nieuwe component
|
||||||
int size = 0;
|
int size = 0;
|
||||||
@@ -543,7 +549,7 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
for (var k = 0; k < 4; k++) {
|
for (var k = 0; k < 4; k++) {
|
||||||
ri = bytes[rng.randint(0, 624)];
|
ri = bytes[rng.randint(0, 624)];
|
||||||
if (!g.clueless(ri)) {
|
if (!g.clueless(ri)) {
|
||||||
var d_idx = rng.randint2bitByte();
|
var d_idx = rng.randint2bitByte();
|
||||||
if (g.hasRoomForClue(OFFSETS_D_IDX[Slot.packSlotDir(ri, d_idx)])) g.setClue(ri, d_idx);
|
if (g.hasRoomForClue(OFFSETS_D_IDX[Slot.packSlotDir(ri, d_idx)])) g.setClue(ri, d_idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -580,14 +586,14 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
}
|
}
|
||||||
public static void clearClues(Grid out, int idx) { if (!out.hasRoomForClue(OFFSETS_D_IDX[Slot.packSlotDir(idx, out.digitAt(idx))])) out.clearClue(idx); }
|
public static void clearClues(Grid out, int idx) { if (!out.hasRoomForClue(OFFSETS_D_IDX[Slot.packSlotDir(idx, out.digitAt(idx))])) out.clearClue(idx); }
|
||||||
|
|
||||||
Grid hillclimb(Grid start, int limit) {
|
Grid hillclimb(int[] stack, Grid start, int limit) {
|
||||||
var best = start;
|
var best = start;
|
||||||
var bestF = maskFitness(best);
|
var bestF = maskFitness(best, stack);
|
||||||
var fails = 0;
|
var fails = 0;
|
||||||
|
|
||||||
while (fails < limit) {
|
while (fails < limit) {
|
||||||
var cand = mutate(best);
|
var cand = mutate(best);
|
||||||
var f = maskFitness(cand);
|
var f = maskFitness(cand, stack);
|
||||||
if (f < bestF) {
|
if (f < bestF) {
|
||||||
best = cand;
|
best = cand;
|
||||||
bestF = f;
|
bestF = f;
|
||||||
@@ -599,21 +605,21 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Grid generateMask(int popSize, int gens, int pairs) {
|
public Grid generateMask(int[] stack, int popSize, int gens, int pairs) {
|
||||||
class GridAndFit {
|
class GridAndFit {
|
||||||
|
|
||||||
Grid grid;
|
Grid grid;
|
||||||
long fite = -1;
|
long fite = -1;
|
||||||
GridAndFit(Grid grid) { this.grid = grid; }
|
GridAndFit(Grid grid) { this.grid = grid; }
|
||||||
long fit() {
|
long fit() {
|
||||||
if (fite == -1) this.fite = maskFitness(grid);
|
if (fite == -1) this.fite = maskFitness(grid, stack);
|
||||||
return this.fite;
|
return this.fite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Main.VERBOSE) System.out.println("generateMask init pop: " + popSize);
|
if (Main.VERBOSE) System.out.println("generateMask init pop: " + popSize);
|
||||||
var pop = new ArrayList<GridAndFit>();
|
var pop = new ArrayList<GridAndFit>();
|
||||||
for (var i = 0; i < popSize; i++) {
|
for (var i = 0; i < popSize; i++) {
|
||||||
pop.add(new GridAndFit(hillclimb(randomMask(), 180)));
|
pop.add(new GridAndFit(hillclimb(stack, randomMask(), 180)));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var gen = 0; gen < gens; gen++) {
|
for (var gen = 0; gen < gens; gen++) {
|
||||||
@@ -624,7 +630,7 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
var p1 = pop.get(rng.randint(0, pop.size() - 1));
|
var p1 = pop.get(rng.randint(0, pop.size() - 1));
|
||||||
var p2 = pop.get(rng.randint(0, pop.size() - 1));
|
var p2 = pop.get(rng.randint(0, pop.size() - 1));
|
||||||
var child = crossover(p1.grid, p2.grid);
|
var child = crossover(p1.grid, p2.grid);
|
||||||
children.add(new GridAndFit(hillclimb(child, 70)));
|
children.add(new GridAndFit(hillclimb(stack, child, 70)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pop.addAll(children);
|
pop.addAll(children);
|
||||||
@@ -696,7 +702,7 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
static int slotScore(int[] count, Slot s) {
|
static int slotScore(byte[] count, Slot s) {
|
||||||
int cross = 0;
|
int cross = 0;
|
||||||
for (long b = s.lo; b != 0; b &= b - 1) cross += (count[Long.numberOfTrailingZeros(b)] - 1);
|
for (long b = s.lo; b != 0; b &= b - 1) cross += (count[Long.numberOfTrailingZeros(b)] - 1);
|
||||||
for (long b = s.hi; b != 0; b &= b - 1) cross += (count[64 | Long.numberOfTrailingZeros(b)] - 1);
|
for (long b = s.hi; b != 0; b &= b - 1) cross += (count[64 | Long.numberOfTrailingZeros(b)] - 1);
|
||||||
@@ -761,9 +767,8 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static CandidateInfo candidateInfoForPattern(Context ctx, long pattern, DictEntry entry, int lenb) {
|
static CandidateInfo candidateInfoForPattern(long[] res, long pattern, DictEntry entry, int lenb) {
|
||||||
int numLongs = entry.numlong;
|
int numLongs = entry.numlong;
|
||||||
long[] res = ctx.bitset;
|
|
||||||
boolean first = true;
|
boolean first = true;
|
||||||
|
|
||||||
for (long p = pattern; p != 0; ) {
|
for (long p = pattern; p != 0; ) {
|
||||||
@@ -799,15 +804,15 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
return new CandidateInfo(indices, count);
|
return new CandidateInfo(indices, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int candidateCountForPattern(Context ctx, long pattern, DictEntry entry, int lenb) {
|
static int candidateCountForPattern(final long[] res, final long pattern, final DictEntry entry, final int lenb) {
|
||||||
int numLongs = entry.numlong;
|
val numLongs = entry.numlong;
|
||||||
long[] res = ctx.bitset;
|
val posBitsets = entry.posBitsets;
|
||||||
boolean first = true;
|
boolean first = true;
|
||||||
|
|
||||||
for (long p = pattern; p != 0; ) {
|
for (long p = pattern; p != 0; ) {
|
||||||
int combined = (int) (p & 0xFF);
|
int combined = (int) (p & 0xFF);
|
||||||
if (combined != 0) {
|
if (combined != 0) {
|
||||||
long[] bs = entry.posBitsets[combined - 1];
|
long[] bs = posBitsets[combined - 1];
|
||||||
if (first) {
|
if (first) {
|
||||||
System.arraycopy(bs, 0, res, 0, numLongs);
|
System.arraycopy(bs, 0, res, 0, numLongs);
|
||||||
first = false;
|
first = false;
|
||||||
@@ -824,36 +829,40 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
for (int k = 0; k < numLongs; k++) count += Long.bitCount(res[k]);
|
for (int k = 0; k < numLongs; k++) count += Long.bitCount(res[k]);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
//72 << 3;
|
|
||||||
static final int BIGG = (288 | 3) + 1;
|
|
||||||
public FillResult fillMask(Grid mask, DictEntry[] dictIndex,
|
|
||||||
int timeLimitMs) {
|
|
||||||
val multiThreaded = Thread.currentThread().getName().contains("pool");
|
|
||||||
val grid = mask.deepCopyGrid();
|
|
||||||
val used = new Bit1029();
|
|
||||||
|
|
||||||
long[] assigned = new long[BIGG];
|
static void scoreSlots(int[] slotScores, Slot[] slots) {
|
||||||
val ctx = Context.get();
|
val count = new byte[SIZE];
|
||||||
val count = new int[SIZE];
|
|
||||||
|
|
||||||
val slots = extractSlots(grid);
|
|
||||||
for (var s : slots) {
|
for (var s : slots) {
|
||||||
for (long b = s.lo; b != 0; b &= b - 1) count[Long.numberOfTrailingZeros(b)]++;
|
for (long b = s.lo; b != 0; b &= b - 1) count[Long.numberOfTrailingZeros(b)]++;
|
||||||
for (long b = s.hi; b != 0; b &= b - 1) count[64 | Long.numberOfTrailingZeros(b)]++;
|
for (long b = s.hi; b != 0; b &= b - 1) count[64 | Long.numberOfTrailingZeros(b)]++;
|
||||||
}
|
}
|
||||||
|
for (int i = 0; i < slots.length; i++) slotScores[i] = slotScore(count, slots[i]);
|
||||||
|
|
||||||
val slotScores = new int[slots.size()];
|
}
|
||||||
for (int i = 0; i < slots.size(); i++) slotScores[i] = slotScore(count, slots.get(i));
|
public static FillResult fillMask(Rng rng, Grid mask, DictEntry[] dictIndex) {
|
||||||
|
val multiThreaded = Thread.currentThread().getName().contains("pool");
|
||||||
|
val NO_LOG = (!Main.VERBOSE || multiThreaded);
|
||||||
|
val grid = mask.deepCopyGrid();
|
||||||
|
val used = new Bit1029();
|
||||||
|
val assigned = new long[CLUE_INDEX_MAX_SIZE];
|
||||||
|
val bitset = new long[2500];
|
||||||
|
val undo = new long[64];
|
||||||
|
|
||||||
val t0 = System.currentTimeMillis();
|
val slots = extractSlots(grid);
|
||||||
val stats = new FillStats();
|
val TOTAL = slots.length;
|
||||||
val TOTAL = slots.size();
|
val slotScores = new int[TOTAL];
|
||||||
|
|
||||||
|
scoreSlots(slotScores, slots);
|
||||||
|
|
||||||
|
val t0 = System.currentTimeMillis();
|
||||||
|
|
||||||
class Solver {
|
class Solver {
|
||||||
|
|
||||||
|
long nodes;
|
||||||
|
long backtracks;
|
||||||
|
int lastMRV;
|
||||||
long lastLog = t0;
|
long lastLog = t0;
|
||||||
void renderProgress() {
|
void renderProgress() {
|
||||||
if (!Main.VERBOSE || multiThreaded) return;
|
|
||||||
var now = System.currentTimeMillis();
|
var now = System.currentTimeMillis();
|
||||||
if ((now - lastLog) < LOG_EVERY_MS) return;
|
if ((now - lastLog) < LOG_EVERY_MS) return;
|
||||||
lastLog = (now);
|
lastLog = (now);
|
||||||
@@ -869,19 +878,19 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
var msg = String.format(
|
var msg = String.format(
|
||||||
Locale.ROOT,
|
Locale.ROOT,
|
||||||
"%s %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %s",
|
"%s %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %s",
|
||||||
bar, done, TOTAL, stats.nodes, stats.backtracks, stats.lastMRV, elapsed
|
bar, done, TOTAL, nodes, backtracks, lastMRV, elapsed
|
||||||
);
|
);
|
||||||
System.out.print("\r" + Strings.padRight(msg, 120));
|
System.out.print("\r" + Strings.padRight(msg, 120));
|
||||||
System.out.flush();
|
System.out.flush();
|
||||||
}
|
}
|
||||||
Pick chooseMRV() {
|
Pick chooseMRV() {
|
||||||
Slot best = null;
|
Slot best = null;
|
||||||
for (int i = 0, count, count2 = -1, bestScore = -1, n = slots.size(); i < n; i++) {
|
for (int i = 0, count, count2 = -1, bestScore = -1, n = TOTAL; i < n; i++) {
|
||||||
var s = slots.get(i);
|
var s = slots[i];
|
||||||
if (assigned[s.key] != X) continue;
|
if (assigned[s.key] != X) continue;
|
||||||
var pattern = patternForSlot(grid, s);
|
var pattern = patternForSlot(grid, s);
|
||||||
var index = dictIndex[s.length()];
|
var index = dictIndex[s.length()];
|
||||||
count = pattern == X ? index.length : candidateCountForPattern(ctx, pattern, index, s.length());
|
count = pattern == X ? index.length : candidateCountForPattern(bitset, pattern, index, s.length());
|
||||||
|
|
||||||
if (count == 0) return PICK_NOT_DONE;
|
if (count == 0) return PICK_NOT_DONE;
|
||||||
if (best == null
|
if (best == null
|
||||||
@@ -898,28 +907,27 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
var pattern = patternForSlot(grid, best);
|
var pattern = patternForSlot(grid, best);
|
||||||
var index = dictIndex[best.length()];
|
var index = dictIndex[best.length()];
|
||||||
if (pattern == X) return new Pick(best, index.empty, false);
|
if (pattern == X) return new Pick(best, index.empty, false);
|
||||||
return new Pick(best, candidateInfoForPattern(ctx, pattern, index, best.length()), false);
|
return new Pick(best, candidateInfoForPattern(bitset, pattern, index, best.length()), false);
|
||||||
}
|
}
|
||||||
boolean backtrack(int depth) {
|
boolean backtrack(int depth) {
|
||||||
if (Thread.currentThread().isInterrupted()) return false;
|
if (Thread.currentThread().isInterrupted()) return false;
|
||||||
stats.nodes++;
|
nodes++;
|
||||||
|
|
||||||
if (timeLimitMs > 0 && (System.currentTimeMillis() - t0) > timeLimitMs) return false;
|
if (20_000 > 0 && (System.currentTimeMillis() - t0) > 20_000) return false;
|
||||||
|
|
||||||
var pick = chooseMRV();
|
var pick = chooseMRV();
|
||||||
if (pick.done) return true;
|
if (pick.done) return true;
|
||||||
if (pick.slot == null) {
|
if (pick.slot == null) {
|
||||||
stats.backtracks++;
|
backtracks++;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
val info = pick.info;
|
val info = pick.info;
|
||||||
stats.lastMRV = info.count;
|
lastMRV = info.count;
|
||||||
renderProgress();
|
if (!NO_LOG) renderProgress();
|
||||||
|
|
||||||
var s = pick.slot;
|
val s = pick.slot;
|
||||||
var k = s.key;
|
val k = s.key;
|
||||||
int patLen = s.length();
|
val entry = dictIndex[s.length()];
|
||||||
var entry = dictIndex[patLen];
|
|
||||||
|
|
||||||
if (info.indices != null && info.indices.length > 0) {
|
if (info.indices != null && info.indices.length > 0) {
|
||||||
var idxs = info.indices;
|
var idxs = info.indices;
|
||||||
@@ -935,7 +943,7 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
var lemIdx = Lemma.unpackIndex(w);
|
var lemIdx = Lemma.unpackIndex(w);
|
||||||
if (used.get(lemIdx)) continue;
|
if (used.get(lemIdx)) continue;
|
||||||
|
|
||||||
if (!placeWord(grid, s, w, ctx.undo, depth)) continue;
|
if (!placeWord(grid, s, w, undo, depth)) continue;
|
||||||
|
|
||||||
used.set(lemIdx);
|
used.set(lemIdx);
|
||||||
assigned[k] = w;
|
assigned[k] = w;
|
||||||
@@ -944,9 +952,9 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
|
|
||||||
assigned[k] = X;
|
assigned[k] = X;
|
||||||
used.clear(lemIdx);
|
used.clear(lemIdx);
|
||||||
grid.undoPlace(ctx.undo[depth << 1], ctx.undo[(depth << 1) | 1]);
|
grid.undoPlace(undo[depth << 1], undo[(depth << 1) | 1]);
|
||||||
}
|
}
|
||||||
stats.backtracks++;
|
backtracks++;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -960,7 +968,7 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
var lemIdx = Lemma.unpackIndex(w);
|
var lemIdx = Lemma.unpackIndex(w);
|
||||||
if (used.get(lemIdx)) continue;
|
if (used.get(lemIdx)) continue;
|
||||||
|
|
||||||
if (!placeWord(grid, s, w, ctx.undo, depth)) continue;
|
if (!placeWord(grid, s, w, undo, depth)) continue;
|
||||||
|
|
||||||
used.set(lemIdx);
|
used.set(lemIdx);
|
||||||
assigned[k] = w;
|
assigned[k] = w;
|
||||||
@@ -969,33 +977,31 @@ public record SwedishGenerator(Rng rng) {
|
|||||||
|
|
||||||
assigned[k] = X;
|
assigned[k] = X;
|
||||||
used.clear(lemIdx);
|
used.clear(lemIdx);
|
||||||
grid.undoPlace(ctx.undo[depth << 1], ctx.undo[(depth << 1) | 1]);
|
grid.undoPlace(undo[depth << 1], undo[(depth << 1) | 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.backtracks++;
|
backtracks++;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// initial render (same feel)
|
// initial render (same feel)
|
||||||
var solver = new Solver();
|
var solver = new Solver();
|
||||||
solver.renderProgress();
|
if (!NO_LOG) solver.renderProgress();
|
||||||
var ok = solver.backtrack(0);
|
var ok = solver.backtrack(0);
|
||||||
// final progress line
|
// final progress line
|
||||||
|
|
||||||
|
var res = new FillResult(ok, new Gridded(grid), assigned, new FillStats(solver.nodes, solver.backtracks, (System.currentTimeMillis() - t0) / 1000.0, solver.lastMRV));
|
||||||
if (!multiThreaded) {
|
if (!multiThreaded) {
|
||||||
System.out.print("\r" + Strings.padRight("", 120) + "\r");
|
System.out.print("\r" + Strings.padRight("", 120) + "\r");
|
||||||
System.out.flush();
|
System.out.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.seconds = (System.currentTimeMillis() - t0) / 1000.0;
|
|
||||||
var res = new FillResult(ok, new Gridded(grid), assigned, stats);
|
|
||||||
|
|
||||||
// print a final progress line
|
// print a final progress line
|
||||||
if (Main.VERBOSE && !multiThreaded) {
|
if (Main.VERBOSE && !multiThreaded) {
|
||||||
System.out.println(
|
System.out.println(
|
||||||
String.format(Locale.ROOT,
|
String.format(Locale.ROOT,
|
||||||
"[######################] %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %.1fs",
|
"[######################] %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %.1fs",
|
||||||
res.wordCount(), TOTAL, stats.nodes, stats.backtracks, stats.lastMRV, stats.seconds
|
res.wordCount(), TOTAL, res.nodes(), res.backtracks(), res.lastMRV(), res.seconds()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class ExportFormatTest {
|
|||||||
|
|
||||||
var clueMap = new long[300];
|
var clueMap = new long[300];
|
||||||
// key = (cellIndex << 2) | (direction)
|
// key = (cellIndex << 2) | (direction)
|
||||||
var key = (0 << 2) | (CLUE_RIGHT);
|
var key = (0) | (CLUE_RIGHT);
|
||||||
clueMap[key] = Lemma.from("TEST");
|
clueMap[key] = Lemma.from("TEST");
|
||||||
|
|
||||||
// Manually fill the grid letters for "TEST" at (0,1), (0,2), (0,3), (0,4)
|
// Manually fill the grid letters for "TEST" at (0,1), (0,2), (0,3), (0,4)
|
||||||
@@ -44,9 +44,9 @@ public class ExportFormatTest {
|
|||||||
grid.setLetter(Grid.offset(0, 3), (byte) 'S');
|
grid.setLetter(Grid.offset(0, 3), (byte) 'S');
|
||||||
grid.setLetter(Grid.offset(0, 4), (byte) 'T');
|
grid.setLetter(Grid.offset(0, 4), (byte) 'T');
|
||||||
// Terminate the slot at (0,5) with another digit to avoid it extending to MAX_WORD_LENGTH
|
// Terminate the slot at (0,5) with another digit to avoid it extending to MAX_WORD_LENGTH
|
||||||
grid.setClue(Grid.offset(0, 5), CLUE_UP);
|
grid.setClue(Grid.offset(0, 5), CLUE_LEFT);
|
||||||
|
|
||||||
var fillResult = new FillResult(true, new Gridded(grid), clueMap, new FillStats());
|
var fillResult = new FillResult(true, new Gridded(grid), clueMap, new FillStats(0, 0, 0, 0));
|
||||||
var puzzleResult = new PuzzleResult(swe, null, null, fillResult);
|
var puzzleResult = new PuzzleResult(swe, null, null, fillResult);
|
||||||
|
|
||||||
var rewards = new Rewards(10, 5, 1);
|
var rewards = new Rewards(10, 5, 1);
|
||||||
@@ -86,7 +86,7 @@ public class ExportFormatTest {
|
|||||||
void testExportFormatEmpty() {
|
void testExportFormatEmpty() {
|
||||||
var swe = new SwedishGenerator(new Rng(0));
|
var swe = new SwedishGenerator(new Rng(0));
|
||||||
var grid = Grid.createEmpty();
|
var grid = Grid.createEmpty();
|
||||||
var fillResult = new FillResult(true, new Gridded(grid), new long[300], new FillStats());
|
var fillResult = new FillResult(true, new Gridded(grid), new long[300], new FillStats(0, 0, 0, 0));
|
||||||
var puzzleResult = new PuzzleResult(swe, null, null, fillResult);
|
var puzzleResult = new PuzzleResult(swe, null, null, fillResult);
|
||||||
|
|
||||||
var exported = puzzleResult.exportFormatFromFilled(1, new Rewards(0, 0, 0));
|
var exported = puzzleResult.exportFormatFromFilled(1, new Rewards(0, 0, 0));
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ public class MainTest {
|
|||||||
grid.setLetter(OFF_0_2, LETTER_B);
|
grid.setLetter(OFF_0_2, LETTER_B);
|
||||||
|
|
||||||
var slots = extractSlots(grid);
|
var slots = extractSlots(grid);
|
||||||
assertEquals(1, slots.size());
|
assertEquals(1, slots.length);
|
||||||
var s = slots.getFirst();
|
var s = slots[0];
|
||||||
assertEquals(8, s.length());
|
assertEquals(8, s.length());
|
||||||
var cells = s.walk().toArray();
|
var cells = s.walk().toArray();
|
||||||
assertEquals(0, Grid.r(cells[0]));
|
assertEquals(0, Grid.r(cells[0]));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package puzzle;
|
package puzzle;
|
||||||
|
|
||||||
|
import lombok.val;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import puzzle.Export.IntListDTO;
|
import puzzle.Export.IntListDTO;
|
||||||
@@ -35,36 +36,36 @@ public class SwedishGeneratorTest {
|
|||||||
static final byte D_BYTE_2 = CLUE_RIGHT;
|
static final byte D_BYTE_2 = CLUE_RIGHT;
|
||||||
@Test
|
@Test
|
||||||
void testPatternForSlotAllLetters() {
|
void testPatternForSlotAllLetters() {
|
||||||
var grid = new Grid(new byte[]{ LETTER_A, LETTER_B, LETTER_C });
|
var grid = new Grid(new byte[]{ LETTER_A, LETTER_B, LETTER_C });
|
||||||
var slot = Slot.from(18 << Slot.BIT_FOR_DIR | (CLUE_RIGHT), 7L, 0L);
|
var slot = Slot.from(18 << Slot.BIT_FOR_DIR | (CLUE_RIGHT), 7L, 0L);
|
||||||
long pattern = patternForSlot(grid, slot);
|
var pattern = patternForSlot(grid, slot);
|
||||||
|
|
||||||
assertEquals(1L | (28L << 8) | (55L << 16), pattern);
|
assertEquals(1L | (28L << 8) | (55L << 16), pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPatternForSlotMixed() {
|
void testPatternForSlotMixed() {
|
||||||
var grid = new Grid(new byte[]{ LETTER_A, DASH, LETTER_C });
|
var grid = new Grid(new byte[]{ LETTER_A, DASH, LETTER_C });
|
||||||
var slot = Slot.from(1 << Slot.BIT_FOR_DIR | (CLUE_RIGHT), 7L, 0L);
|
var slot = Slot.from(1 << Slot.BIT_FOR_DIR | (CLUE_RIGHT), 7L, 0L);
|
||||||
long pattern = patternForSlot(grid, slot);
|
var pattern = patternForSlot(grid, slot);
|
||||||
|
|
||||||
assertEquals(1L | (0L << 8) | (55L << 16), pattern);
|
assertEquals(1L | (0L << 8) | (55L << 16), pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPatternForSlotAllDashes() {
|
void testPatternForSlotAllDashes() {
|
||||||
var grid = new Grid(new byte[]{ DASH, DASH, DASH }); // - - -
|
var grid = new Grid(new byte[]{ DASH, DASH, DASH }); // - - -
|
||||||
var slot = Slot.from(1 << Slot.BIT_FOR_DIR | (CLUE_RIGHT), 7L, 0L);
|
var slot = Slot.from(1 << Slot.BIT_FOR_DIR | (CLUE_RIGHT), 7L, 0L);
|
||||||
long pattern = patternForSlot(grid, slot);
|
var pattern = patternForSlot(grid, slot);
|
||||||
|
|
||||||
assertEquals(0L, pattern);
|
assertEquals(0L, pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPatternForSlotSingleLetter() {
|
void testPatternForSlotSingleLetter() {
|
||||||
var grid = new Grid(new byte[]{ LETTER_A, DASH, DASH });
|
var grid = new Grid(new byte[]{ LETTER_A, DASH, DASH });
|
||||||
var slot = Slot.from(1 << Slot.BIT_FOR_DIR | (CLUE_RIGHT), 7L, 0L);
|
var slot = Slot.from(1 << Slot.BIT_FOR_DIR | (CLUE_RIGHT), 7L, 0L);
|
||||||
long pattern = patternForSlot(grid, slot);
|
var pattern = patternForSlot(grid, slot);
|
||||||
|
|
||||||
assertEquals(1L, pattern);
|
assertEquals(1L, pattern);
|
||||||
}
|
}
|
||||||
@@ -186,17 +187,17 @@ public class SwedishGeneratorTest {
|
|||||||
static int intersectSorted(int[] a, int aLen, int[] b, int bLen, int[] out) {
|
static int intersectSorted(int[] a, int aLen, int[] b, int bLen, int[] out) {
|
||||||
if (aLen == 0 || bLen == 0) return 0;
|
if (aLen == 0 || bLen == 0) return 0;
|
||||||
if (aLen < bLen >>> 4) {
|
if (aLen < bLen >>> 4) {
|
||||||
int k = 0;
|
var k = 0;
|
||||||
for (int i = 0; i < aLen; i++) {
|
for (var i = 0; i < aLen; i++) {
|
||||||
int x = a[i];
|
var x = a[i];
|
||||||
if (Arrays.binarySearch(b, 0, bLen, x) >= 0) out[k++] = x;
|
if (Arrays.binarySearch(b, 0, bLen, x) >= 0) out[k++] = x;
|
||||||
}
|
}
|
||||||
return k;
|
return k;
|
||||||
}
|
}
|
||||||
if (bLen < aLen >>> 4) {
|
if (bLen < aLen >>> 4) {
|
||||||
int k = 0;
|
var k = 0;
|
||||||
for (int i = 0; i < bLen; i++) {
|
for (var i = 0; i < bLen; i++) {
|
||||||
int y = b[i];
|
var y = b[i];
|
||||||
if (Arrays.binarySearch(a, 0, aLen, y) >= 0) out[k++] = y;
|
if (Arrays.binarySearch(a, 0, aLen, y) >= 0) out[k++] = y;
|
||||||
}
|
}
|
||||||
return k;
|
return k;
|
||||||
@@ -233,10 +234,10 @@ public class SwedishGeneratorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static long packPattern(String s) {
|
static long packPattern(String s) {
|
||||||
long p = 0;
|
long p = 0;
|
||||||
byte[] b = s.getBytes(StandardCharsets.US_ASCII);
|
var b = s.getBytes(StandardCharsets.US_ASCII);
|
||||||
for (int i = 0; i < b.length; i++) {
|
for (var i = 0; i < b.length; i++) {
|
||||||
int val = b[i] & 31;
|
var val = b[i] & 31;
|
||||||
if (val != 0) {
|
if (val != 0) {
|
||||||
p |= (long) (i * 26 + val) << (i << 3);
|
p |= (long) (i * 26 + val) << (i << 3);
|
||||||
}
|
}
|
||||||
@@ -258,7 +259,7 @@ public class SwedishGeneratorTest {
|
|||||||
var dict = new Dict(new long[]{ l0, l1, l2, l3, l3a, l4a, l6a, l7a, l8a });
|
var dict = new Dict(new long[]{ l0, l1, l2, l3, l3a, l4a, l6a, l7a, l8a });
|
||||||
|
|
||||||
// Pattern "APP--" for length 5
|
// Pattern "APP--" for length 5
|
||||||
var info = candidateInfoForPattern(Context.get(), packPattern("APP"), dict.index()[5], 5);
|
var info = candidateInfoForPattern(Context.get().bitset(), packPattern("APP"), dict.index()[5], 5);
|
||||||
|
|
||||||
assertEquals(2, info.count());
|
assertEquals(2, info.count());
|
||||||
assertNotNull(info.indices());
|
assertNotNull(info.indices());
|
||||||
@@ -274,8 +275,8 @@ public class SwedishGeneratorTest {
|
|||||||
// This should detect a slot starting at 0,1 with length 2 (0,1 and 0,2)
|
// This should detect a slot starting at 0,1 with length 2 (0,1 and 0,2)
|
||||||
|
|
||||||
var slots = extractSlots(grid);
|
var slots = extractSlots(grid);
|
||||||
assertEquals(1, slots.size());
|
assertEquals(1, slots.length);
|
||||||
var s = slots.getFirst();
|
var s = slots[0];
|
||||||
|
|
||||||
assertTrue(s.length() >= 2);
|
assertTrue(s.length() >= 2);
|
||||||
assertEquals(OFF_0_0, s.clueIndex());
|
assertEquals(OFF_0_0, s.clueIndex());
|
||||||
@@ -284,19 +285,16 @@ public class SwedishGeneratorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testMaskFitnessBasic() {
|
void testMaskFitnessBasic() {
|
||||||
var gen = new SwedishGenerator(new Rng(0));
|
var gen = new SwedishGenerator(new Rng(0));
|
||||||
var grid = Grid.createEmpty();
|
var grid = Grid.createEmpty();
|
||||||
var lenCounts = new int[12];
|
var stack = new int[STACK_SIZE];
|
||||||
lenCounts[2] = 10;
|
|
||||||
lenCounts[8] = 10; // In case MAX_WORD_LENGTH is 8
|
|
||||||
|
|
||||||
// Empty grid should have high penalty (no slots)
|
// Empty grid should have high penalty (no slots)
|
||||||
var f1 = gen.maskFitness(grid);
|
var f1 = gen.maskFitness(grid, stack);
|
||||||
assertTrue(f1 >= 1_000_000_000L);
|
assertTrue(f1 >= 1_000_000_000L);
|
||||||
|
|
||||||
// Add a slot
|
// Add a slot
|
||||||
grid.setClue(OFF_0_0, D_BYTE_2);
|
grid.setClue(OFF_0_0, D_BYTE_2);
|
||||||
var f2 = gen.maskFitness(grid);
|
var f2 = gen.maskFitness(grid, stack);
|
||||||
assertTrue(f2 < f1);
|
assertTrue(f2 < f1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,12 +310,10 @@ public class SwedishGeneratorTest {
|
|||||||
assertNotNull(g2);
|
assertNotNull(g2);
|
||||||
assertNotSame(g1, g2);
|
assertNotSame(g1, g2);
|
||||||
|
|
||||||
var g3 = gen.crossover(g1, g2);
|
assertNotNull(gen.crossover(g1, g2));
|
||||||
assertNotNull(g3);
|
|
||||||
|
|
||||||
var lenCounts = new int[12];
|
val stack = new int[STACK_SIZE];
|
||||||
Arrays.fill(lenCounts, 10);
|
var g4 = gen.hillclimb(stack, g1, 10);
|
||||||
var g4 = gen.hillclimb(g1, 10);
|
|
||||||
assertNotNull(g4);
|
assertNotNull(g4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +391,7 @@ public class SwedishGeneratorTest {
|
|||||||
assertFalse(sDec.increasing());
|
assertFalse(sDec.increasing());
|
||||||
|
|
||||||
// 2. Test slotScore
|
// 2. Test slotScore
|
||||||
int[] counts = new int[SIZE];
|
val counts = new byte[SIZE];
|
||||||
counts[1] = 2;
|
counts[1] = 2;
|
||||||
counts[2] = 3;
|
counts[2] = 3;
|
||||||
var sScore = Slot.from(0, (1L << 1) | (1L << 2), 0L);
|
var sScore = Slot.from(0, (1L << 1) | (1L << 2), 0L);
|
||||||
@@ -419,29 +415,29 @@ public class SwedishGeneratorTest {
|
|||||||
var dict = new Dict(words);
|
var dict = new Dict(words);
|
||||||
var entry5 = dict.index()[5];
|
var entry5 = dict.index()[5];
|
||||||
|
|
||||||
var ctx = Context.get();
|
var ctx = Context.get();
|
||||||
long pattern = packPattern("APP");
|
var pattern = packPattern("APP");
|
||||||
assertEquals(2, candidateCountForPattern(ctx, pattern, entry5, 3));
|
assertEquals(2, candidateCountForPattern(ctx.bitset(), pattern, entry5, 3));
|
||||||
|
|
||||||
pattern = packPattern("BAN");
|
pattern = packPattern("BAN");
|
||||||
assertEquals(1, candidateCountForPattern(ctx, pattern, entry5, 3));
|
assertEquals(1, candidateCountForPattern(ctx.bitset(), pattern, entry5, 3));
|
||||||
|
|
||||||
pattern = packPattern("CAT");
|
pattern = packPattern("CAT");
|
||||||
assertEquals(0, candidateCountForPattern(ctx, pattern, entry5, 3));
|
assertEquals(0, candidateCountForPattern(ctx.bitset(), pattern, entry5, 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testMaskFitnessDetailed() {
|
void testMaskFitnessDetailed() {
|
||||||
var gen = new SwedishGenerator(new Rng(42));
|
var gen = new SwedishGenerator(new Rng(42));
|
||||||
var grid = Grid.createEmpty();
|
var grid = Grid.createEmpty();
|
||||||
|
val stack = new int[STACK_SIZE];
|
||||||
// Empty grid: huge penalty
|
// Empty grid: huge penalty
|
||||||
long fitEmpty = gen.maskFitness(grid);
|
var fitEmpty = gen.maskFitness(grid, stack);
|
||||||
assertTrue(fitEmpty >= 1_000_000_000L);
|
assertTrue(fitEmpty >= 1_000_000_000L);
|
||||||
|
|
||||||
// Grid with one short slot: still high penalty but less than empty
|
// Grid with one short slot: still high penalty but less than empty
|
||||||
grid.setClue(0, D_BYTE_2); // Right from 0,0. Len 2 if 3x3.
|
grid.setClue(0, D_BYTE_2); // Right from 0,0. Len 2 if 3x3.
|
||||||
long fitOne = gen.maskFitness(grid);
|
var fitOne = gen.maskFitness(grid, stack);
|
||||||
assertTrue(fitOne < fitEmpty);
|
assertTrue(fitOne < fitEmpty);
|
||||||
|
|
||||||
// Test penalty for TARGET_CLUES
|
// Test penalty for TARGET_CLUES
|
||||||
|
|||||||
Reference in New Issue
Block a user