From bd6a5a1d2e51a11c7d1c1c19cbd1bb03bc07be5a Mon Sep 17 00:00:00 2001 From: mike Date: Fri, 19 Dec 2025 14:34:13 +0100 Subject: [PATCH] initial commit --- compile.sh | 4 +- export_format.js => node/export_format.js | 0 main.js => node/main.js | 0 .../swedish_generator.js | 0 run.sh | 2 +- src/puzzle/ExportFormat.java | 329 ++++++++++++++++++ src/puzzle/Main.java | 72 ++++ src/{ => puzzle}/SwedishGenerator.java | 77 +--- 8 files changed, 422 insertions(+), 62 deletions(-) rename export_format.js => node/export_format.js (100%) rename main.js => node/main.js (100%) rename swedish_generator.js => node/swedish_generator.js (100%) create mode 100644 src/puzzle/ExportFormat.java create mode 100644 src/puzzle/Main.java rename src/{ => puzzle}/SwedishGenerator.java (93%) diff --git a/compile.sh b/compile.sh index d9d99c9..6e1a939 100755 --- a/compile.sh +++ b/compile.sh @@ -1,3 +1,3 @@ #!/bin/bash -mkdir -p target -javac -d target src/SwedishGenerator.java +mkdir -p ~/dev/.target +javac -d ~/dev/.target src/puzzle/*.java diff --git a/export_format.js b/node/export_format.js similarity index 100% rename from export_format.js rename to node/export_format.js diff --git a/main.js b/node/main.js similarity index 100% rename from main.js rename to node/main.js diff --git a/swedish_generator.js b/node/swedish_generator.js similarity index 100% rename from swedish_generator.js rename to node/swedish_generator.js diff --git a/run.sh b/run.sh index e397e4c..4783f43 100755 --- a/run.sh +++ b/run.sh @@ -1,2 +1,2 @@ #!/bin/bash -java -cp target SwedishGenerator "$@" +java -cp ~/dev/.target puzzle.Main "$@" diff --git a/src/puzzle/ExportFormat.java b/src/puzzle/ExportFormat.java new file mode 100644 index 0000000..af3311b --- /dev/null +++ b/src/puzzle/ExportFormat.java @@ -0,0 +1,329 @@ +package puzzle; +import java.util.*; + +/** + * 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 final class ExportFormat { + + private ExportFormat() { } + + // Directions for digits '1'..'4' + private static final int[][] DIRS = new int[5][2]; + static { + DIRS[1] = new int[]{ -1, 0 }; // up + DIRS[2] = new int[]{ 0, 1 }; // right + DIRS[3] = new int[]{ 1, 0 }; // down + DIRS[4] = new int[]{ 0, -1 }; // left + } + + private static boolean isDigit(char ch) { return ch >= '1' && ch <= '4'; } + private static boolean isLetter(char ch) { return ch >= 'A' && ch <= 'Z'; } + + private static boolean inBounds(int H, int W, int r, int c) { + return r >= 0 && r < H && c >= 0 && c < W; + } + + // ---------- Public API ---------- + + public static ExportedPuzzle exportFormatFromFilled(SwedishGenerator.PuzzleResult puz) { + return exportFormatFromFilled(puz, 1, new Rewards(50, 2, 1)); + } + + public static ExportedPuzzle exportFormatFromFilled(SwedishGenerator.PuzzleResult puz, int difficulty, Rewards rewards) { + Objects.requireNonNull(puz, "puz"); + char[][] g = puz.filled.grid; + int H = g.length; + int W = g[0].length; + + // 1) extract "placed" list from all clue digits in the filled grid + List placed = new ArrayList<>(); + Set seen = new HashSet<>(); + + for (int r = 0; r < H; r++) { + for (int c = 0; c < W; c++) { + char ch = g[r][c]; + if (!isDigit(ch)) continue; + + Placed p = extractPlacedFromClue(g, r, c, ch, 8, 2); + if (p == null) continue; + + String key = p.startRow + "," + p.startCol + ":" + p.direction + ":" + p.word; + if (seen.contains(key)) continue; + seen.add(key); + placed.add(p); + } + } + + // If nothing placed: return full grid mapped to letters/# only + if (placed.isEmpty()) { + List gridv2 = new ArrayList<>(H); + for (int r = 0; r < H; r++) { + StringBuilder sb = new StringBuilder(W); + for (int c = 0; c < W; c++) { + char ch = g[r][c]; + sb.append(isLetter(ch) ? ch : '#'); + } + gridv2.add(sb.toString()); + } + return new ExportedPuzzle(gridv2, List.of(), difficulty, rewards); + } + + // 2) bounding box around all word cells + arrow cells, with 1-cell margin + List allCells = new ArrayList<>(); + for (Placed p : placed) { + allCells.addAll(p.cells); + allCells.add(p.arrow); + } + + int minR = Integer.MAX_VALUE, minC = Integer.MAX_VALUE; + int maxR = Integer.MIN_VALUE, maxC = Integer.MIN_VALUE; + + for (int[] rc : allCells) { + int rr = rc[0], cc = rc[1]; + minR = Math.min(minR, rr); + minC = Math.min(minC, cc); + maxR = Math.max(maxR, rr); + maxC = Math.max(maxC, cc); + } + + minR -= 1; + minC -= 1; + maxR += 1; + maxC += 1; + + // 3) map of only used letter cells (everything else becomes '#') + Map letterAt = new HashMap<>(); + for (Placed p : placed) { + for (int[] rc : p.cells) { + int rr = rc[0], cc = rc[1]; + if (inBounds(H, W, rr, cc) && isLetter(g[rr][cc])) { + letterAt.put(pack(rr, cc), g[rr][cc]); + } + } + } + + // 4) render gridv2 over cropped bounds (out-of-bounds become '#') + List gridv2 = new ArrayList<>(Math.max(0, maxR - minR + 1)); + for (int r = minR; r <= maxR; r++) { + StringBuilder row = new StringBuilder(Math.max(0, maxC - minC + 1)); + for (int c = minC; c <= maxC; c++) { + Character ch = letterAt.get(pack(r, c)); + row.append(ch != null ? ch : '#'); + } + gridv2.add(row.toString()); + } + + // 5) words output with cropped coordinates + List wordsOut = new ArrayList<>(placed.size()); + for (Placed p : placed) { + wordsOut.add(new WordOut( + p.word, + p.clue, // placeholder = word (same as JS) + p.startRow - minR, + p.startCol - minC, + p.direction, + p.word, // answer + p.arrowRow - minR, + p.arrowCol - minC + )); + } + + return new ExportedPuzzle(gridv2, wordsOut, difficulty, rewards); + } + + /** + * Extract a word run for a clue cell at (r,c) with direction digit d. + * Canonical output: + * - direction: "horizontal" (right) or "vertical" (down) + * - startRow/startCol: first letter cell in canonical direction + * - arrowRow/arrowCol: immediately before the start (left or above) + * - word read from grid in canonical order + */ + private static Placed extractPlacedFromClue(char[][] g, int r, int c, char d, int maxLen, int minLen) { + int H = g.length, W = g[0].length; + int di = d - '0'; + int dr = DIRS[di][0], dc = DIRS[di][1]; + + // collect letter cells in ORIGINAL direction away from the clue + List cells = new ArrayList<>(); + int rr = r + dr, cc = c + dc; + while (inBounds(H, W, rr, cc) && isLetter(g[rr][cc]) && cells.size() < maxLen) { + cells.add(new int[]{ rr, cc }); + rr += dr; + cc += dc; + } + + if (cells.size() < minLen) return null; + + // Canonicalize: always output right/down + int startRow, startCol, arrowRow, arrowCol; + String direction; + + if (d == '2') { // right -> horizontal + direction = "horizontal"; + startRow = cells.get(0)[0]; + startCol = cells.get(0)[1]; + arrowRow = r; + arrowCol = c; + } else if (d == '3') { // down -> vertical + direction = "vertical"; + startRow = cells.get(0)[0]; + startCol = cells.get(0)[1]; + arrowRow = r; + arrowCol = c; + } else if (d == '4') { // left -> canonical right + direction = "horizontal"; + int[] farLeft = cells.get(cells.size() - 1); + startRow = farLeft[0]; + startCol = farLeft[1]; + arrowRow = startRow; + arrowCol = startCol - 1; + } else if (d == '1') { // up -> canonical down + direction = "vertical"; + int[] topMost = cells.get(cells.size() - 1); + startRow = topMost[0]; + startCol = topMost[1]; + arrowRow = startRow - 1; + arrowCol = startCol; + } else { + return null; + } + + // Read word from grid in canonical order (right/down) + StringBuilder wordChars = new StringBuilder(); + if ("horizontal".equals(direction)) { + for (int i = 0; i < cells.size(); i++) { + int cc2 = startCol + i; + char ch = (inBounds(H, W, startRow, cc2) ? g[startRow][cc2] : '#'); + if (!isLetter(ch)) break; + wordChars.append(ch); + } + } else { + for (int i = 0; i < cells.size(); i++) { + int rr2 = startRow + i; + char ch = (inBounds(H, W, rr2, startCol) ? g[rr2][startCol] : '#'); + if (!isLetter(ch)) break; + wordChars.append(ch); + } + } + + String word = wordChars.toString(); + if (word.length() < minLen || word.length() > maxLen) return null; + + // Build exact used cells (only for actual word length) + List used = new ArrayList<>(word.length()); + if ("horizontal".equals(direction)) { + for (int i = 0; i < word.length(); i++) used.add(new int[]{ startRow, startCol + i }); + } else { + for (int i = 0; i < word.length(); i++) used.add(new int[]{ startRow + i, startCol }); + } + + return new Placed( + word, + word, // clue placeholder (same as JS) + startRow, + startCol, + direction, + word, // answer + arrowRow, + arrowCol, + used, + new int[]{ arrowRow, arrowCol } + ); + } + + // pack (r,c) into one long key (handles negatives too) + private static long pack(int r, int c) { + return (((long) r) << 32) ^ (c & 0xFFFFFFFFL); + } + + // ---------- Data models ---------- + + private static final class Placed { + + final String word; + final String clue; + final int startRow, startCol; + final String direction; // "horizontal" | "vertical" + final String answer; + final int arrowRow, arrowCol; + final List cells; // word cells + final int[] arrow; // [arrowRow, arrowCol] + + Placed(String word, String clue, int startRow, int startCol, String direction, String answer, + int arrowRow, int arrowCol, List cells, int[] arrow) { + this.word = word; + this.clue = clue; + this.startRow = startRow; + this.startCol = startCol; + this.direction = direction; + this.answer = answer; + this.arrowRow = arrowRow; + this.arrowCol = arrowCol; + this.cells = cells; + this.arrow = arrow; + } + } + + public static final class Rewards { + + public final int coins; + public final int stars; + public final int hints; + + public Rewards(int coins, int stars, int hints) { + this.coins = coins; + this.stars = stars; + this.hints = hints; + } + } + + public static final class WordOut { + + public final String word; + public final String clue; + public final int startRow; + public final int startCol; + public final String direction; // "horizontal" | "vertical" + public final String answer; + public final int arrowRow; + public final int arrowCol; + + public WordOut(String word, String clue, int startRow, int startCol, String direction, + String answer, int arrowRow, int arrowCol) { + this.word = word; + this.clue = clue; + this.startRow = startRow; + this.startCol = startCol; + this.direction = direction; + this.answer = answer; + this.arrowRow = arrowRow; + this.arrowCol = arrowCol; + } + } + + public static final class ExportedPuzzle { + + public final List gridv2; + public final List words; + public final int difficulty; + public final Rewards rewards; + + public ExportedPuzzle(List gridv2, List words, int difficulty, Rewards rewards) { + this.gridv2 = gridv2; + this.words = words; + this.difficulty = difficulty; + this.rewards = rewards; + } + } + + // ---------- Tiny demo (optional) ---------- + +} diff --git a/src/puzzle/Main.java b/src/puzzle/Main.java new file mode 100644 index 0000000..1aba69d --- /dev/null +++ b/src/puzzle/Main.java @@ -0,0 +1,72 @@ +package puzzle; + +public class Main { + // ---------------- CLI ---------------- + + public static class Opts { + public int seed = 1; + public int pop = 18; + public int gens = 100; + public int tries = 50; + public String wordsPath = "./word-list.txt"; + } + + static void usage() { + System.out.println(""" + Usage: + java SwedishGenerator [--seed N] [--pop N] [--gens N] [--tries N] [--words word-list.txt] + + Defaults: + --seed 1 + --pop 18 + --gens 100 + --tries 50 + --words ./word-list.txt + """); + } + + static Opts parseArgs(String[] argv) { + var out = new Opts(); + for (int i = 0; i < argv.length; i++) { + String a = argv[i]; + String v = (i + 1 < argv.length) ? argv[i + 1] : null; + if (a.equals("--help") || a.equals("-h")) { + usage(); + System.exit(0); + } + if (a.equals("--seed")) { out.seed = Integer.parseInt(v); i++; } + else if (a.equals("--pop")) { out.pop = Integer.parseInt(v); i++; } + else if (a.equals("--gens")) { out.gens = Integer.parseInt(v); i++; } + else if (a.equals("--tries")) { out.tries = Integer.parseInt(v); i++; } + else if (a.equals("--words")) { out.wordsPath = v; i++; } + else throw new IllegalArgumentException("Unknown arg: " + a); + } + return out; + } + + public static void main(String[] args) { + var opts = parseArgs(args); + var res = SwedishGenerator.generatePuzzle(opts); + if (res == null) { + System.out.println("No solution found within tries."); + System.exit(1); + } + + System.out.println("\n=== GENERATED MASK ==="); + System.out.println(SwedishGenerator.gridToString(res.mask)); + + System.out.println("\n=== FILLED PUZZLE (RAW) ==="); + System.out.println(SwedishGenerator.gridToString(res.filled.grid)); + + System.out.println("\n=== FILLED PUZZLE (HUMAN) ==="); + System.out.println(SwedishGenerator.renderHuman(res.filled.grid)); + var out = ExportFormat.exportFormatFromFilled(res, 1, new ExportFormat.Rewards(50, 2, 1)); + System.out.println("gridv2:"); + for (String row : out.gridv2) System.out.println(row); + System.out.println("words: " + out.words.size()); + for (var w : out.words) { + System.out.printf("%s %s start=(%d,%d) arrow=(%d,%d)%n", + w.word, w.direction, w.startRow, w.startCol, w.arrowRow, w.arrowCol); + } + } +} \ No newline at end of file diff --git a/src/SwedishGenerator.java b/src/puzzle/SwedishGenerator.java similarity index 93% rename from src/SwedishGenerator.java rename to src/puzzle/SwedishGenerator.java index f03acca..6705d0f 100644 --- a/src/SwedishGenerator.java +++ b/src/puzzle/SwedishGenerator.java @@ -1,3 +1,5 @@ +package puzzle; + import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -29,49 +31,7 @@ public class SwedishGenerator { static boolean isLetter(char ch) { return ch >= 'A' && ch <= 'Z'; } static boolean isLetterCell(char ch) { return ch == '#' || isLetter(ch); } - // ---------------- CLI ---------------- - - static class Opts { - int seed = 1; - int pop = 18; - int gens = 100; - int tries = 50; - String wordsPath = "./word-list.txt"; - } - - static void usage() { - System.out.println(""" - Usage: - java SwedishGenerator [--seed N] [--pop N] [--gens N] [--tries N] [--words word-list.txt] - - Defaults: - --seed 1 - --pop 18 - --gens 100 - --tries 50 - --words ./word-list.txt - """); - } - - static SwedishGenerator.Opts parseArgs(String[] argv) { - var out = new SwedishGenerator.Opts(); - for (int i = 0; i < argv.length; i++) { - String a = argv[i]; - String v = (i + 1 < argv.length) ? argv[i + 1] : null; - if (a.equals("--help") || a.equals("-h")) { - usage(); - System.exit(0); - } - if (a.equals("--seed")) { out.seed = Integer.parseInt(v); i++; } - else if (a.equals("--pop")) { out.pop = Integer.parseInt(v); i++; } - else if (a.equals("--gens")) { out.gens = Integer.parseInt(v); i++; } - else if (a.equals("--tries")) { out.tries = Integer.parseInt(v); i++; } - else if (a.equals("--words")) { out.wordsPath = v; i++; } - else throw new IllegalArgumentException("Unknown arg: " + a); - } - return out; - } - + // ---------------- RNG (xorshift32) ---------------- static final class Rng { @@ -558,18 +518,18 @@ public class SwedishGenerator { // ---------------- Fill (CSP) ---------------- - static final class FillStats { - long nodes; - long backtracks; - double seconds; - int lastMRV; + public static final class FillStats { + public long nodes; + public long backtracks; + public double seconds; + public int lastMRV; } - static final class FillResult { - boolean ok; - char[][] grid; - HashMap clueMap; - FillStats stats; + public static final class FillResult { + public boolean ok; + public char[][] grid; + public HashMap clueMap; + public FillStats stats; } static final class Undo { @@ -825,12 +785,12 @@ public class SwedishGenerator { // ---------------- Top-level generatePuzzle ---------------- - static final class PuzzleResult { - char[][] mask; - FillResult filled; + public static final class PuzzleResult { + public char[][] mask; + public FillResult filled; } - static SwedishGenerator.PuzzleResult generatePuzzle(SwedishGenerator.Opts opts) { + public static PuzzleResult generatePuzzle(Main.Opts opts) { var rng = new Rng(opts.seed); var tLoad0 = System.nanoTime(); @@ -863,8 +823,7 @@ public class SwedishGenerator { // ---------------- main ---------------- - public static void main(String[] args) { - var opts = parseArgs(args); + public static void convert(Main.Opts opts) { var res = generatePuzzle(opts); if (res == null) {