|
| 1 | +from __future__ import annotations |
| 2 | + |
1 | 3 | import argparse |
| 4 | +import csv |
2 | 5 | import pathlib |
| 6 | +import random |
3 | 7 | import sys |
4 | 8 | from typing import Optional |
5 | 9 |
|
@@ -70,6 +74,52 @@ def cmd_canon(ns: argparse.Namespace) -> int: |
70 | 74 | return 0 |
71 | 75 |
|
72 | 76 |
|
| 77 | +def cmd_gen_batch(ns: argparse.Namespace) -> int: |
| 78 | + """Generate many canonicalized, unique puzzles quickly.""" |
| 79 | + |
| 80 | + out_path = pathlib.Path(ns.out) |
| 81 | + seen: set[str] = set() |
| 82 | + rng = random.Random(ns.seed) |
| 83 | + unique: list[str] = [] |
| 84 | + while len(unique) < ns.count: |
| 85 | + grid = generate( |
| 86 | + seed=rng.randrange(2**31 - 1), |
| 87 | + target_givens=ns.givens, |
| 88 | + minimal=ns.minimal, |
| 89 | + symmetry=ns.symmetry, |
| 90 | + ) |
| 91 | + canon = canonical_form(grid) |
| 92 | + if canon in seen: |
| 93 | + continue |
| 94 | + seen.add(canon) |
| 95 | + unique.append(canon) |
| 96 | + out_path.parent.mkdir(parents=True, exist_ok=True) |
| 97 | + with out_path.open("w", encoding="utf-8") as handle: |
| 98 | + for value in unique: |
| 99 | + handle.write(value + "\n") |
| 100 | + print(f"# generated: {len(unique)}", file=sys.stderr) |
| 101 | + return 0 |
| 102 | + |
| 103 | + |
| 104 | +def cmd_rate_file(ns: argparse.Namespace) -> int: |
| 105 | + inp = pathlib.Path(ns.in_path) |
| 106 | + rows: list[tuple[str, float]] = [] |
| 107 | + with inp.open("r", encoding="utf-8") as handle: |
| 108 | + for line in handle: |
| 109 | + s = "".join(ch for ch in line.strip() if not ch.isspace()) |
| 110 | + if not s: |
| 111 | + continue |
| 112 | + score = rate(from_string(s)) |
| 113 | + rows.append((s, score)) |
| 114 | + print(f"{score:.1f}") |
| 115 | + if ns.csv_path: |
| 116 | + with open(ns.csv_path, "w", newline="", encoding="utf-8") as csv_handle: |
| 117 | + writer = csv.writer(csv_handle) |
| 118 | + writer.writerow(["grid", "score"]) |
| 119 | + writer.writerows(rows) |
| 120 | + return 0 |
| 121 | + |
| 122 | + |
73 | 123 | def cmd_dedupe(ns: argparse.Namespace) -> int: |
74 | 124 | inp = pathlib.Path(ns.in_path) |
75 | 125 | outp = pathlib.Path(ns.out_path) |
@@ -126,20 +176,32 @@ def main(argv: Optional[list[str]] = None) -> int: |
126 | 176 | canon_parser.set_defaults(func=cmd_canon) |
127 | 177 |
|
128 | 178 | dedupe_parser = sub.add_parser( |
129 | | - "dedupe", |
130 | | - help="dedupe puzzles by canonical form (one 81-char grid per line)", |
| 179 | + "dedupe", help="dedupe puzzles by canonical form (one 81-char grid per line)" |
131 | 180 | ) |
| 181 | + dedupe_parser.add_argument("--in", dest="in_path", required=True, help="input text file") |
132 | 182 | dedupe_parser.add_argument( |
133 | | - "--in", dest="in_path", required=True, help="input text file with one grid per line" |
134 | | - ) |
135 | | - dedupe_parser.add_argument( |
136 | | - "--out", |
137 | | - dest="out_path", |
138 | | - required=True, |
139 | | - help="output file path for unique canonical grids", |
| 183 | + "--out", dest="out_path", required=True, help="output file for unique canonical grids" |
140 | 184 | ) |
141 | 185 | dedupe_parser.set_defaults(func=cmd_dedupe) |
142 | 186 |
|
| 187 | + genb_parser = sub.add_parser("gen-batch", help="generate N unique puzzles to a file") |
| 188 | + genb_parser.add_argument("--out", required=True, help="output file (81-char per line)") |
| 189 | + genb_parser.add_argument("--count", type=int, default=100, help="number of puzzles") |
| 190 | + genb_parser.add_argument("--givens", type=int, default=30) |
| 191 | + genb_parser.add_argument( |
| 192 | + "--symmetry", choices=["none", "rot180", "mix"], default="mix" |
| 193 | + ) |
| 194 | + genb_parser.add_argument("--minimal", action="store_true") |
| 195 | + genb_parser.add_argument("--seed", type=int, default=None) |
| 196 | + genb_parser.set_defaults(func=cmd_gen_batch) |
| 197 | + |
| 198 | + ratef_parser = sub.add_parser("rate-file", help="rate each puzzle in a file") |
| 199 | + ratef_parser.add_argument("--in", dest="in_path", required=True) |
| 200 | + ratef_parser.add_argument( |
| 201 | + "--csv", dest="csv_path", help="optional CSV output path" |
| 202 | + ) |
| 203 | + ratef_parser.set_defaults(func=cmd_rate_file) |
| 204 | + |
143 | 205 | gen_parser = sub.add_parser("gen", help="generate a puzzle") |
144 | 206 | gen_parser.add_argument("--seed", type=int, default=None) |
145 | 207 | gen_parser.add_argument("--givens", type=int, default=28, help="target number of clues (approx)") |
|
0 commit comments