Commit e6778040 authored by Skynet0's avatar Skynet0
Browse files

Initial commit

parents
No related merge requests found
Showing with 1204 additions and 0 deletions
+1204 -0
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Personal unnecessary files
poetry.lock
pyproject.toml
.vscode/
genres/**/*_ref.py
**/ringring/**
**/barns/**
\ No newline at end of file
# Acknowledgements
To store puzzles, I used the Puz-Pre v3 format from by [pzprjs](https://github.com/robx/pzprjs), available under the MIT license.
Country Road, Moon or Sun, and Yajilin test puzzles were generated by a [gacha](https://myamyasdvx.herokuapp.com/sudoku.html) written by [3892myamya](https://3892myamya.github.io/introduction/).
Special thanks to [KrazyDad](https://krazydad.com), who agreed to usage of their puzzles in this course.
The Masyu test puzzles were drawn from his site, and converted the Puz-Pre v3 format with permission.
from dataclasses import dataclass
from typing import Callable, Generic, List, NamedTuple, Optional, TypeVar
from z3.z3 import And, ArithRef, Bool, BoolRef, Int, ModelRef, Or, Solver
T = TypeVar("T")
class Vector(NamedTuple):
"""A vector representing an offset in 2D."""
dy: int
dx: int
def negate(self) -> "Vector":
"""Return a `Vector` that is the negation of this one."""
return Vector(-self.dy, -self.dx)
def times(self, a: int) -> "Vector":
"""Return a `Vector` that is scaled by an integer."""
return Vector(self.dy * a, self.dx * a)
class Point(NamedTuple):
"""A point in 2D, usually the center of a grid cell."""
y: int
x: int
def translate(self, d: Vector) -> "Point":
"""Translate this point by the given `Vector`."""
return Point(self.y + d.dy, self.x + d.dx)
N = Vector(-1, 0)
S = Vector(1, 0)
W = Vector(0, -1)
E = Vector(0, 1)
NE = Vector(-1, 1)
NW = Vector(-1, -1)
SE = Vector(1, 1)
SW = Vector(1, -1)
ORTHO_DIRS = [N, S, W, E]
DIAG_DIRS = [NE, NW, SE, SW]
ALL_DIRS = ORTHO_DIRS + DIAG_DIRS
VEC_TO_DIR_NAME = {
N: "N",
S: "S",
W: "W",
E: "E",
NE: "NE",
NW: "NW",
SE: "SE",
SW: "SW",
}
# NamedTuples can't be generic, so use a slotted dataclass instead
@dataclass(frozen=True)
class Neighbor(Generic[T]):
"""Container for properties of a cell that is a neighbor of another."""
__slots__ = ("location", "direction", "value")
location: Point
direction: Vector
value: T
def point_to_int_var(p: Point, uniqueifier: int) -> ArithRef:
return Int(f"int-u({uniqueifier})-r{p.y}-c{p.x}")
def constrain_int_different(val: ArithRef, model: ModelRef) -> BoolRef:
return val != model.eval(val).as_long()
def point_to_bool_var(p: Point, uniqueifier: int) -> BoolRef:
return Bool(f"bool-u({uniqueifier})-r{p.y}-c{p.x}")
def constrain_bool_different(val: BoolRef, model: ModelRef) -> BoolRef:
return val != bool(model.eval(val))
class Grid(Generic[T]):
"""Rectangular grid class that can contain arbitrary values.
Args:
height (int): The height of the grid.
width (int): The width of the grid.
default_val (Callable[[Point, int], T], optional): Function called to create
the value for each Point in the grid. Possible to do extra processing with a
closure, but usually just for initializing variables. If you use
multiple grids ensure that the variables do not collide via the `int`
argument. Defaults to point_to_int_var.
solver (Optional[Solver]): A `Solver` object. If `None`, a default `Solver`
will be constructed.
track_unsat (bool): Whether to track clauses to construct an unsat core
"""
# Uniqueifier - if I want to use multiple grids in one solver for some
# reason, this keeps the variables from colliding.
_instance = 0
def __init__(
self,
height: int,
width: int,
default_val: Callable[[Point, int], T] = point_to_int_var,
solver: Optional[Solver] = None,
track_unsat: bool = False,
):
self.height = height
self.width = width
self.points = {
Point(y, x): default_val(Point(y, x), Grid._instance)
for y in range(height)
for x in range(width)
}
Grid._instance += 1
self.solver = solver if solver else Solver()
if track_unsat:
self.solver.set(":core.minimize", True)
def edge_adjacent(self, p: Point) -> List[Neighbor[T]]:
"""Return all cells that share an edge with the given cell."""
return [
Neighbor(p.translate(d), d, self.points[p.translate(d)])
for d in ORTHO_DIRS
if p.translate(d) in self.points
]
def vertex_adjacent(self, p: Point) -> List[Neighbor[T]]:
"""Return all cells that share a vertex with the given cell."""
return [
Neighbor(p.translate(d), d, self.points[p.translate(d)])
for d in ALL_DIRS
if p.translate(d) in self.points
]
def contains_point(self, p: Point) -> bool:
"""Return whether the given cell is in the grid."""
return p in self.points
def constrain_new_solution(
self, constrain_point_to_diff_value: Callable[[T, ModelRef], BoolRef]
):
"""Add constraints to make the solver's current solution impossible.
Args:
constrain_point_to_diff_value (Callable[[T, ModelRef], BoolRef]):
Function to call on each value stored in the grid to return a `BoolRef`
that is satisfied if the cell is "different".
"""
# Resolver is because T is not necessarily a Ref. We have to use
# T with the ModelRef to constrain the things we care about correctly.
self.solver.add(
Or(
[
constrain_point_to_diff_value(val, self.solver.model())
for val in self.points.values()
]
)
)
def to_str_matrix(self, label_fn: Callable[[T, ModelRef], str]) -> List[List[str]]:
"""Convert the grid's solution to a string matrix.
Args:
label_fn (Callable[[T, ModelRef], str]): Function to call on each value
stored in the grid and convert it to a string.
Returns:
List[List[str]]: String matrix representation of the grid.
"""
model = self.solver.model()
output = [[" " for _ in range(self.width)] for _ in range(self.height)]
for p, v in self.points.items():
output[p.y][p.x] = label_fn(v, model)
return output
def get_solve_time(self) -> int:
"""Get the solve time."""
return self.solver.statistics().get_key_value("time")
def row_values(self, row: int) -> List[T]:
return [self.points[(row, x)] for x in range(self.height)]
def col_values(self, col: int) -> List[T]:
return [self.points[(y, col)] for y in range(self.width)]
def add_constr(self, *args: BoolRef):
"""Add the constraints to the grid's solver, with tracking if enabled."""
if self.track_unsat:
if len(args) > 1:
constr = And(args)
else:
constr = args[0]
self.solver.assert_and_track(constr, str(constr))
else:
self.solver.add(args)
from typing import List, Dict
from cs11puzzles.grid import ORTHO_DIRS, VEC_TO_DIR_NAME, E, N, Point, S, W
from cs11puzzles.str_helpers import ortho_dirs_sym
def borders_to_areas(
wall_right: List[List[bool]], wall_down: List[List[bool]]
) -> List[List[int]]:
"""Construct areas from walls."""
assert len(wall_right[0]) + 1 == len(wall_down[0])
assert len(wall_right) == len(wall_down) + 1
width = len(wall_down[0])
height = len(wall_right)
areas = [[-1 for _ in range(width)] for _ in range(height)]
to_visit = set([Point(y, x) for y in range(height) for x in range(width)])
area = 0
while to_visit:
curr_area_root = to_visit.pop()
to_flood = set([curr_area_root])
while to_flood:
curr_pt = to_flood.pop()
to_visit.discard(curr_pt)
areas[curr_pt.y][curr_pt.x] = area
for d in ORTHO_DIRS:
n = curr_pt.translate(d)
if n in to_visit and n not in to_flood:
if (
d == N
and not wall_down[n.y][n.x]
or d == S
and not wall_down[curr_pt.y][curr_pt.x]
or d == W
and not wall_right[n.y][n.x]
or d == E
and not wall_right[curr_pt.y][curr_pt.x]
):
to_flood.add(n)
area += 1
return areas
def conns_to_str_grid(conn_right: List[List[bool]],
conn_down: List[List[bool]]) -> List[List[str]]:
assert len(conn_right[0]) + 1 == len(conn_down[0])
assert len(conn_right) == len(conn_down) + 1
width = len(conn_down[0])
height = len(conn_right)
str_grid = [[' ' for _ in range(width)] for _ in range(height)]
points = set([Point(y, x) for y in range(height) for x in range(width)])
for p in points:
dirs: Dict[str, bool] = {}
for d in ORTHO_DIRS:
n = p.translate(d)
dirs[VEC_TO_DIR_NAME[d]] = (n in points) and (
d == N and conn_down[n.y][n.x] or d == S and conn_down[p.y][p.x]
or d == W and conn_right[n.y][n.x] or
d == E and conn_right[p.y][p.x])
str_grid[p.y][p.x] = ortho_dirs_sym(**dirs)
return str_grid
\ No newline at end of file
from typing import Callable, Dict, List, Literal, Text
def str_matrix_to_text(
matrix: List[List[str]],
padding: Dict[str, str] = None,
align_type: Literal["left", "center", "right"] = "center",
) -> Text:
"""Convert a matrix of strings to an aligned multiline string.
Args:
str_grid (List[List[str]]): 2D string matrix
padding (Dict[str, str], optional): Dictionary containing a mapping from
cell contents to padding strs.
align_type (Literal["left", "center", "right"], optional): Type of alignment.
Defaults to "center".
Returns:
Text: Printable multiline string
"""
align_fn: Callable[[str, int, str], str] = {
"left": str.ljust,
"center": str.center,
"right": str.rjust,
}[align_type]
if padding is None:
padding = {}
max_sym_len = max([max([len(s) for s in r]) for r in matrix])
return "\n".join(
[
"".join([align_fn(s, max_sym_len, padding.get(s, " ")) for s in r])
for r in matrix
]
)
def ortho_dirs_sym(
N: bool = False, S: bool = False, W: bool = False, E: bool = False
) -> str:
"""Converts boolean edges that connect cell centers to a Unicode symbol.
Args:
N (bool, optional): Presence of edge exiting to the north (up)
S (bool, optional): Presence of edge exiting to the south (down)
W (bool, optional): Presence of edge exiting to the west (left)
E (bool, optional): Presence of edge exiting to the east (right)
Returns:
str: Unicode symbol for given directions
"""
idx = sum([v * 2 ** i for i, v in enumerate([N, S, W, E])])
# There have to be SO MANY BETTER WAYS TO DO THIS
symbols = [
" ", # ----
chr(0x2575), # ---U
chr(0x2577), # --D-
chr(0x2502), # --DU
chr(0x2574), # -L--
chr(0x2518), # -L-U
chr(0x2510), # -LD-
chr(0x2524), # -LDU
chr(0x2576), # R---
chr(0x2514), # R--U
chr(0x250C), # R-D-
chr(0x251C), # R-DU
chr(0x2500), # RL--
chr(0x2534), # RL-U
chr(0x252C), # RLD-
chr(0x253C), # RLDU
]
return symbols[idx]
from typing import List, Optional
from cs11puzzles.grid import ORTHO_DIRS, VEC_TO_DIR_NAME, Grid, Point
from cs11puzzles.str_helpers import ortho_dirs_sym, str_matrix_to_text
from genres.country.country_util import CountryInstance, load_puzzle
from z3.z3 import Bool, Int, ModelRef, sat
class CountryCell:
def __init__(self, p: Point, u: int):
self.point = p
self.uniquifier = u
self.dirs = {
d: Bool(f"country-dir-u{u}-d{VEC_TO_DIR_NAME[d]}-r{p.y}-c{p.x}")
for d in ORTHO_DIRS
}
self.order = Int(f"country-order-u{u}-r{p.y}-c{p.x}")
def cell_to_str(self, m: ModelRef):
return ortho_dirs_sym(
**{VEC_TO_DIR_NAME[vec]: bool(m[val]) for vec, val in self.dirs.items()}
)
def solve_country(puzzle: CountryInstance) -> Optional[List[List[str]]]:
areas = puzzle.areas
area_nums = puzzle.area_nums
height = len(areas)
width = len(areas[0])
num_cells = height * width
g = Grid[CountryCell](height, width, CountryCell)
# TODO: constraints
if g.solver.check() == sat:
def label_fn(cell: CountryCell, m: ModelRef):
return cell.cell_to_str(m)
return g.to_str_matrix(label_fn)
return None
if __name__ == "__main__":
filename = "tests/country/data/country_0.txt"
puzzle = load_puzzle(filename)
sol = solve_country(puzzle)
if sol:
print(str_matrix_to_text(sol))
else:
print("No solution.")
from typing import Dict, List, NamedTuple, Tuple
from cs11puzzles.pzpr_helpers import conns_to_str_grid
class CountryInstance(NamedTuple):
areas: List[List[int]]
area_nums: Dict[int, int]
def load_puzzle_and_solution(fname: str) -> Tuple[CountryInstance, List[List[str]]]:
with open(fname) as f:
assert f.readline().strip() == "pzprv3.1"
assert f.readline().strip() == "country"
height = int(f.readline())
width = int(f.readline())
f.readline() # empty line for some reason
_ = int(f.readline()) # n_regions
areas = [
[int(n) for n in f.readline().strip().split(" ")] for _ in range(height)
]
numbers = [
[int(n) if n != "." else 0 for n in f.readline().strip().split(" ")]
for _ in range(height)
]
area_nums = {}
# pzpr displays number in leftmost, topmost cell
for x in range(width):
for y in range(height):
if areas[y][x] not in area_nums:
area_nums[areas[y][x]] = numbers[y][x]
conn_right = [
[int(n) == 1 for n in f.readline().strip().split(" ")]
for _ in range(height)
]
conn_down = [
[int(n) == 1 for n in f.readline().strip().split(" ")]
for _ in range(height - 1)
]
return CountryInstance(areas, area_nums), conns_to_str_grid(
conn_right, conn_down
)
def load_puzzle(fname: str):
puzzle, _ = load_puzzle_and_solution(fname)
return puzzle
from typing import List, Optional
from cs11puzzles.grid import ORTHO_DIRS, VEC_TO_DIR_NAME, Grid, Point
from cs11puzzles.str_helpers import ortho_dirs_sym, str_matrix_to_text
from genres.masyu.masyu_util import MasyuInstance, load_puzzle
from z3.z3 import Bool, Int, ModelRef, sat
class MasyuCell:
def __init__(self, p: Point, u: int):
self.point = p
self.uniquifier = u
self.dirs = {
d: Bool(f"masyu-dir-u{u}-d{VEC_TO_DIR_NAME[d]}-r{p.y}-c{p.x}")
for d in ORTHO_DIRS
}
self.order = Int(f"masyu-order-u{u}-r{p.y}-c{p.x}")
def cell_to_str(self, m: ModelRef):
return ortho_dirs_sym(
**{VEC_TO_DIR_NAME[vec]: bool(m[val]) for vec, val in self.dirs.items()}
)
def solve_masyu(puzzle: MasyuInstance) -> Optional[List[List[str]]]:
pearls = puzzle.pearls
height = len(pearls)
width = len(pearls[0])
num_cells = height * width
g = Grid[MasyuCell](height, width, MasyuCell)
# TODO: constraints
if g.solver.check() == sat:
def label_fn(cell: MasyuCell, m: ModelRef):
return cell.cell_to_str(m)
return g.to_str_matrix(label_fn)
return None
if __name__ == "__main__":
filename = "tests/masyu/data/masyu_4.txt"
puzzle = load_puzzle(filename)
sol = solve_masyu(puzzle)
if sol:
print(str_matrix_to_text(sol))
else:
print("No solution.")
from typing import NamedTuple, List, Tuple
from cs11puzzles.pzpr_helpers import conns_to_str_grid
WHITE = 1
BLACK = 2
WHITE_SYM = ""
BLACK_SYM = ""
class MasyuInstance(NamedTuple):
pearls: List[List[int]]
def load_puzzle_and_solution(fname: str) -> Tuple[MasyuInstance, List[List[str]]]:
with open(fname) as f:
assert f.readline().strip() == "pzprv3"
assert f.readline().strip() == "mashu"
height = int(f.readline())
_ = int(f.readline()) # width
pearls = [
[0 if i == "." else int(i) for i in f.readline().strip().split(" ")]
for _ in range(height)
]
conn_right = [
[int(n) == 1 for n in f.readline().strip().split(" ")]
for _ in range(height)
]
conn_down = [
[int(n) == 1 for n in f.readline().strip().split(" ")]
for _ in range(height - 1)
]
return MasyuInstance(pearls), conns_to_str_grid(conn_right, conn_down)
def load_puzzle(fname: str):
puzzle, _ = load_puzzle_and_solution(fname)
return puzzle
from typing import List, Optional
from cs11puzzles.grid import ORTHO_DIRS, VEC_TO_DIR_NAME, Grid, Point
from cs11puzzles.str_helpers import ortho_dirs_sym, str_matrix_to_text
from genres.moonsun.moonsun_util import MOON, SUN, MoonSunInstance, load_puzzle
from z3.z3 import Bool, Int, ModelRef, sat
class MoonSunCell:
def __init__(self, p: Point, u: int):
self.point = p
self.uniquifier = u
# Draw connections between cells
self.dirs = {
d: Bool(f"moonsun-dir-u{u}-d{VEC_TO_DIR_NAME[d]}-r{p.y}-c{p.x}")
for d in ORTHO_DIRS
}
self.order = Int(f"moonsun-order-u{u}-r{p.y}-c{p.x}")
self.moon_region = Bool(f"moonsun-moon_reg-u{u}-r{p.y}-c{p.x}")
def cell_to_str(self, m: ModelRef):
return ortho_dirs_sym(
**{VEC_TO_DIR_NAME[vec]: bool(m[val]) for vec, val in self.dirs.items()}
)
def solve_moonsun(puzzle: MoonSunInstance) -> Optional[List[List[str]]]:
areas = puzzle.areas
moonsun = puzzle.moonsun
height = len(areas)
width = len(areas[0])
num_cells = height * width
g = Grid[MoonSunCell](height, width, MoonSunCell)
# TODO: constraints
if g.solver.check() == sat:
def label_fn(cell: MoonSunCell, m: ModelRef):
return cell.cell_to_str(m)
return g.to_str_matrix(label_fn)
else:
print(g.solver.unsat_core())
return None
if __name__ == "__main__":
filename = "tests/moonsun/data/moonsun_1.txt"
puzzle = load_puzzle(filename)
sol = solve_moonsun(puzzle)
if sol:
print(str_matrix_to_text(sol))
else:
print("No solution.")
from typing import List, NamedTuple, Tuple
from cs11puzzles.pzpr_helpers import conns_to_str_grid
# I don't know _why_, but puzz.link's symbols are backwards.
# These are what they _look_ like, not what they're actually named on the site.
SUN = 1
MOON = 2
class MoonSunInstance(NamedTuple):
areas: List[List[int]]
moonsun: List[List[int]]
def load_puzzle_and_solution(fname: str) -> Tuple[MoonSunInstance, List[List[str]]]:
with open(fname) as f:
assert f.readline().strip() == "pzprv3"
assert f.readline().strip() == "moonsun"
height = int(f.readline())
_ = int(f.readline())
_ = int(f.readline()) # n_regions
areas = [
[int(n) for n in f.readline().strip().split(" ")] for _ in range(height)
]
moonsun = [
[int(n) if n != "." else 0 for n in f.readline().strip().split(" ")]
for _ in range(height)
]
conn_right = [
[int(n) == 1 for n in f.readline().strip().split(" ")]
for _ in range(height)
]
conn_down = [
[int(n) == 1 for n in f.readline().strip().split(" ")]
for _ in range(height - 1)
]
return MoonSunInstance(areas, moonsun), conns_to_str_grid(conn_right, conn_down)
def load_puzzle(fname: str):
puzzle, _ = load_puzzle_and_solution(fname)
return puzzle
from typing import List, Optional
from cs11puzzles.grid import ORTHO_DIRS, VEC_TO_DIR_NAME, Grid, Point
from cs11puzzles.str_helpers import ortho_dirs_sym, str_matrix_to_text
from genres.yajilin.yajilin_util import (
DIR_TO_ARROW,
SHADED,
YajilinInstance,
load_puzzle,
)
from z3.z3 import Bool, Int, ModelRef, sat
class YajilinCell:
def __init__(self, p: Point, u: int):
self.point = p
self.uniquifier = u
# Draw connections between cells
self.dirs = {
d: Bool(f"yajilin-dir-u{u}-d{VEC_TO_DIR_NAME[d]}-r{p.y}-c{p.x}")
for d in ORTHO_DIRS
}
self.order = Int(f"yajilin-order-u{u}-r{p.y}-c{p.x}")
self.shaded = Bool(f"yajilin-shaded-u{u}-r{p.y}-c{p.x}")
def cell_to_str(self, m: ModelRef):
if bool(m[self.shaded]):
return SHADED
return ortho_dirs_sym(
**{VEC_TO_DIR_NAME[vec]: bool(m[val]) for vec, val in self.dirs.items()}
)
def solve_yajilin(puzzle: YajilinInstance) -> Optional[List[List[str]]]:
givens = puzzle.givens
height = len(givens)
width = len(givens[0])
num_cells = height * width
g = Grid[YajilinCell](height, width, YajilinCell)
# TODO: constraints
if g.solver.check() == sat:
def label_fn(cell: YajilinCell, m: ModelRef):
p = cell.point
if givens[p.y][p.x]:
n = givens[p.y][p.x].number
d = givens[p.y][p.x].direction
return f"{n}{DIR_TO_ARROW[d]}"
return cell.cell_to_str(m)
return g.to_str_matrix(label_fn)
return None
if __name__ == "__main__":
filename = "tests/yajilin/data/yajilin_0.txt"
puzzle = load_puzzle(filename)
sol = solve_yajilin(puzzle)
# Printing should look nice!
padding = {
chr(0x2518): chr(0x2500),
chr(0x2510): chr(0x2500),
chr(0x2500): chr(0x2500),
SHADED: chr(0x2588),
}
if sol:
print(str_matrix_to_text(sol, padding=padding, align_type="right"))
else:
print("No solution.")
from typing import NamedTuple, List, Tuple, Optional
from cs11puzzles.pzpr_helpers import conns_to_str_grid
from cs11puzzles.grid import Vector, N, S, W, E
SHADED = chr(0x2589)
DIR_TO_ARROW = {
N: chr(0x2191),
S: chr(0x2193),
W: chr(0x2190),
E: chr(0x2192),
}
# QMARK = -1
class YajilinGiven(NamedTuple):
number: int
direction: Vector
class YajilinInstance(NamedTuple):
givens: List[List[Optional[YajilinGiven]]]
def load_puzzle_and_solution(fname: str) -> Tuple[YajilinInstance, List[List[str]]]:
with open(fname) as f:
assert f.readline().strip() == "pzprv3"
assert f.readline().strip() == "yajilin"
height = int(f.readline())
width = int(f.readline())
def str_to_given(s: str):
dn, n = s.split(",")
return YajilinGiven(int(n), [N, S, W, E][int(dn) - 1])
givens = [
[
None if i == "." else str_to_given(i)
for i in f.readline().strip().split(" ")
]
for _ in range(height)
]
shaded = [
[i == "#" for i in f.readline().strip().split(" ")] for _ in range(height)
]
conn_right = [
[int(n) == 1 for n in f.readline().strip().split(" ")]
for _ in range(height)
]
conn_down = [
[int(n) == 1 for n in f.readline().strip().split(" ")]
for _ in range(height - 1)
]
soln = conns_to_str_grid(conn_right, conn_down)
for y in range(height):
for x in range(width):
if shaded[y][x]:
soln[y][x] = SHADED
if givens[y][x]:
n = givens[y][x].number
d = givens[y][x].direction
soln[y][x] = f"{n}{DIR_TO_ARROW[d]}"
return YajilinInstance(givens), soln
def load_puzzle(fname: str):
puzzle, _ = load_puzzle_and_solution(fname)
return puzzle
pzprv3.1
country
6
6
10
0 0 0 0 1 2
3 3 4 1 1 2
3 3 4 1 1 5
3 3 4 1 5 5
6 4 4 7 8 8
6 9 9 7 8 8
. . . . . .
. . . 3 . .
. . . . . .
. . . . . .
. 3 . . . .
. . . . . .
0 0 1 0 1
1 0 0 1 0
0 0 0 0 0
0 1 0 0 1
0 0 0 1 0
1 1 1 0 0
0 0 1 1 1 1
1 1 1 0 0 1
1 1 1 0 0 1
1 0 0 0 1 0
1 0 0 1 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
pzprv3.1
country
8
8
14
0 1 1 2 2 3 4 4
0 0 0 3 3 3 5 4
6 6 6 6 7 5 5 4
6 6 6 6 7 8 8 8
9 9 7 7 7 7 8 8
9 9 9 7 10 10 11 12
9 9 13 10 10 10 11 12
9 9 13 13 10 11 11 12
2 . . 2 . . . .
. . . . . . . .
8 . . . . . . .
. . . . . . . .
. . 2 . . . . .
. . . . . . . 3
. . 3 . . . . .
. . . . . . . .
0 1 1 1 0 1 1
0 1 0 0 1 0 0
1 0 1 0 1 1 1
0 1 1 0 1 1 0
1 0 0 0 0 0 1
0 1 0 0 1 0 0
0 0 0 1 0 0 0
0 0 1 0 0 1 1
0 1 0 0 1 1 0 1
0 0 1 0 0 0 0 1
1 1 0 1 1 0 0 0
1 0 0 0 0 0 1 0
0 1 0 0 0 0 0 1
0 0 1 0 1 1 0 1
0 0 1 1 0 1 0 1
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
pzprv3.1
country
8
8
14
0 0 0 0 1 2 2 3
4 4 5 5 1 6 3 3
4 4 4 5 7 6 6 6
4 4 4 4 7 6 6 6
4 8 8 8 7 9 10 10
11 8 8 8 8 9 10 12
11 11 8 8 9 9 10 12
11 9 9 9 9 9 13 13
. . . . . . . .
9 . 3 . . 5 . .
. . . . 3 . . .
. . . . . . . .
. 3 . . . . 4 .
. . . . . . . .
. . . . . . . .
. . . . . . . .
0 0 1 1 1 1 1
1 0 1 0 0 1 1
0 0 0 1 0 0 0
0 1 1 0 0 1 1
0 0 0 0 0 0 1
0 0 0 1 0 0 0
1 0 0 0 1 0 1
0 1 1 1 0 1 1
0 0 1 0 0 0 0 1
1 1 0 1 0 1 0 0
1 1 0 0 1 1 0 0
1 0 0 1 1 0 0 1
1 0 0 1 1 0 1 0
1 0 0 0 0 0 1 0
0 1 0 0 1 1 0 1
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
pzprv3.1
country
8
8
16
0 1 2 2 3 3 4 4
0 1 1 5 5 3 4 4
0 0 1 6 6 3 7 4
0 8 1 6 6 7 7 9
10 8 11 12 13 7 7 9
10 11 11 12 13 13 13 9
10 11 11 14 14 15 15 15
10 10 10 14 14 14 15 15
4 . . . 2 . . .
. . . . . . . .
. . . 4 . . . .
. 2 . . . 5 . .
2 . . . 3 . . .
. . . . . . . .
. . . 4 . 5 . .
. . . . . . . .
1 0 0 1 1 1 0
0 1 0 1 0 0 1
0 0 1 0 0 0 1
1 0 0 1 0 1 0
0 1 0 1 0 1 1
0 1 0 0 1 0 0
0 0 0 1 0 1 0
0 1 1 1 0 0 1
1 1 0 1 0 0 1 0
1 0 1 0 1 0 0 1
1 0 0 1 1 0 1 0
0 1 0 0 0 1 0 0
0 0 1 1 1 0 0 1
0 1 0 1 0 1 0 1
0 1 0 0 1 0 1 1
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
pzprv3.1
country
8
8
14
0 0 0 1 1 1 1 2
0 0 3 4 4 1 1 2
5 3 3 3 4 2 2 2
5 3 6 6 7 7 2 2
5 8 9 9 7 7 10 10
8 8 9 9 9 11 11 11
8 8 8 12 12 12 11 11
13 13 12 12 12 12 12 12
4 . . 4 . . . .
. . . 3 . . . .
. . . . . 7 . .
. . 1 . . . . .
. . . . . . . .
3 . . . . 5 . .
. . . . . . . .
. . 5 . . . . .
0 1 1 1 1 1 1
1 0 0 1 0 0 0
0 1 1 0 1 1 0
0 1 0 0 1 0 1
0 0 0 1 0 1 1
0 0 1 0 0 1 0
1 0 0 1 1 0 1
0 1 1 0 0 0 0
0 1 0 0 0 0 0 1
1 0 0 1 1 0 0 1
1 1 0 0 0 0 1 1
1 0 1 0 1 1 0 0
1 0 1 1 0 0 0 1
1 0 0 0 0 1 1 1
0 1 0 1 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
import os
import pytest
from genres.country.country import solve_country
from genres.country.country_util import load_puzzle_and_solution
DATA_DIR = "tests/country/data"
PUZZLES = [f"country_{i}.txt" for i in range(5)]
@pytest.mark.parametrize("filename", PUZZLES)
@pytest.mark.timeout(40)
def test_country(filename: str):
puzzle, solution = load_puzzle_and_solution(os.path.join(DATA_DIR, filename))
actual_solution = solve_country(puzzle)
assert actual_solution == solution
if __name__ == "__main__":
pytest.main([__file__])
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment