Commit a814f30d authored by Ethan Ordentlich's avatar Ethan Ordentlich
Browse files

Merge branch 'design' into 'master'

Add bulk of the assignment

See merge request !1
parents 670d58e1 95270b70
Showing with 669 additions and 36 deletions
+669 -36
.venv/ # Byte-compiled / optimized / DLL files
genres/norinori/norinori_ref.py __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 poetry.lock
pyproject.toml pyproject.toml
.vscode/ .vscode/
*.pyc
\ No newline at end of file genres/**/*_ref.py
\ 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.
Norinori and Star Battle 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 Ripple Effect test puzzles were drawn from his site, and converted the Puz-Pre v3 format with permission.
from dataclasses import dataclass from dataclasses import dataclass
from typing import Callable, Dict, Generic, List, Literal, NamedTuple, TypeVar from typing import Callable, Generic, List, NamedTuple, Optional, TypeVar
from z3.z3 import And, ArithRef, Bool, BoolRef, Int, ModelRef, Or, Solver from z3.z3 import ArithRef, Bool, BoolRef, Int, ModelRef, Or, Solver
from cs11puzzles.str_helpers import str_matrix_to_text
T = TypeVar("T") T = TypeVar("T")
class Vector(NamedTuple): class Vector(NamedTuple):
"""A vector representing an offset in 2D."""
dy: int dy: int
dx: int dx: int
def negate(self) -> "Vector": def negate(self) -> "Vector":
"""Return a `Vector` that is the negation of this one."""
return Vector(-self.dy, -self.dx) 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): class Point(NamedTuple):
"""A point in 2D, usually the center of a grid cell."""
y: int y: int
x: int x: int
def translate(self, d: Vector) -> "Point": def translate(self, d: Vector) -> "Point":
"""Translate this point by the given `Vector`."""
return Point(self.y + d.dy, self.x + d.dx) return Point(self.y + d.dy, self.x + d.dx)
...@@ -49,8 +57,11 @@ VEC_TO_DIR_NAME = { ...@@ -49,8 +57,11 @@ VEC_TO_DIR_NAME = {
} }
# NamedTuples can't be generic, so use a slotted dataclass instead
@dataclass(frozen=True) @dataclass(frozen=True)
class Neighbor(Generic[T]): class Neighbor(Generic[T]):
"""Container for properties of a cell that is a neighbor of another."""
__slots__ = ("location", "direction", "value") __slots__ = ("location", "direction", "value")
location: Point location: Point
direction: Vector direction: Vector
...@@ -76,13 +87,16 @@ def constrain_bool_different(val: BoolRef, model: ModelRef) -> BoolRef: ...@@ -76,13 +87,16 @@ def constrain_bool_different(val: BoolRef, model: ModelRef) -> BoolRef:
class Grid(Generic[T]): class Grid(Generic[T]):
"""Rectangular grid class that can contain arbitrary values. """Rectangular grid class that can contain arbitrary values.
# Arguments Args:
height (int): The height of the grid. height (int): The height of the grid.
width (int): The width of the grid. width (int): The width of the grid.
default_val (Callable[[Point, int], T]): Function called to create the value default_val (Callable[[Point, int], T], optional): Function called to create
for each Point in the grid. Possible to do extra processing with a 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 closure, but usually just for initializing variables. If you use
multiple grids ensure that the variables do not collide. 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.
""" """
# Uniqueifier - if I want to use multiple grids in one solver for some # Uniqueifier - if I want to use multiple grids in one solver for some
...@@ -94,9 +108,8 @@ class Grid(Generic[T]): ...@@ -94,9 +108,8 @@ class Grid(Generic[T]):
height: int, height: int,
width: int, width: int,
default_val: Callable[[Point, int], T] = point_to_int_var, default_val: Callable[[Point, int], T] = point_to_int_var,
solver: Solver = None, solver: Optional[Solver] = None,
): ):
self.height = height self.height = height
self.width = width self.width = width
self.points = { self.points = {
...@@ -107,10 +120,10 @@ class Grid(Generic[T]): ...@@ -107,10 +120,10 @@ class Grid(Generic[T]):
Grid._instance += 1 Grid._instance += 1
if solver is None: self.solver = solver if solver else Solver()
self.solver = Solver()
def edge_adjacent(self, p: Point) -> List[Neighbor[T]]: def edge_adjacent(self, p: Point) -> List[Neighbor[T]]:
"""Return all cells that share an edge with the given cell."""
return [ return [
Neighbor(p.translate(d), d, self.points[p.translate(d)]) Neighbor(p.translate(d), d, self.points[p.translate(d)])
for d in ORTHO_DIRS for d in ORTHO_DIRS
...@@ -118,15 +131,27 @@ class Grid(Generic[T]): ...@@ -118,15 +131,27 @@ class Grid(Generic[T]):
] ]
def vertex_adjacent(self, p: Point) -> List[Neighbor[T]]: def vertex_adjacent(self, p: Point) -> List[Neighbor[T]]:
"""Return all cells that share a vertex with the given cell."""
return [ return [
Neighbor(p.translate(d), d, self.points[p.translate(d)]) Neighbor(p.translate(d), d, self.points[p.translate(d)])
for d in ALL_DIRS for d in ALL_DIRS
if p.translate(d) in self.points 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( def constrain_new_solution(
self, constrain_point_to_diff_value: Callable[[T, ModelRef], BoolRef] 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 # 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. # T with the ModelRef to constrain the things we care about correctly.
self.solver.add( self.solver.add(
...@@ -138,7 +163,16 @@ class Grid(Generic[T]): ...@@ -138,7 +163,16 @@ class Grid(Generic[T]):
) )
) )
def to_str_matrix(self, label_fn: Callable[[T, ModelRef], str]): 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() model = self.solver.model()
output = [[" " for _ in range(self.width)] for _ in range(self.height)] output = [[" " for _ in range(self.width)] for _ in range(self.height)]
for p, v in self.points.items(): for p, v in self.points.items():
...@@ -146,13 +180,6 @@ class Grid(Generic[T]): ...@@ -146,13 +180,6 @@ class Grid(Generic[T]):
return output return output
def to_text(
self,
label_fn: Callable[[T, ModelRef], str],
padding: Dict[str, str] = None,
align_type: Literal["left", "center", "right"] = "center",
):
return str_matrix_to_text(self.to_str_matrix(label_fn), padding, align_type)
def get_solve_time(self) -> int: def get_solve_time(self) -> int:
"""Get the solve time."""
return self.solver.statistics().get_key_value("time") return self.solver.statistics().get_key_value("time")
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
...@@ -2,8 +2,8 @@ from typing import List, Optional ...@@ -2,8 +2,8 @@ from typing import List, Optional
from cs11puzzles.grid import Grid, point_to_bool_var from cs11puzzles.grid import Grid, point_to_bool_var
from cs11puzzles.str_helpers import str_matrix_to_text from cs11puzzles.str_helpers import str_matrix_to_text
from genres.norinori.norinori_util import NorinoriInstance, load_puzzle from genres.norinori.norinori_util import SHADED, NorinoriInstance, load_puzzle
from z3 import BoolRef, ModelRef, sat from z3 import *
def solve_norinori(puzzle: NorinoriInstance) -> Optional[List[List[str]]]: def solve_norinori(puzzle: NorinoriInstance) -> Optional[List[List[str]]]:
...@@ -32,7 +32,7 @@ def solve_norinori(puzzle: NorinoriInstance) -> Optional[List[List[str]]]: ...@@ -32,7 +32,7 @@ def solve_norinori(puzzle: NorinoriInstance) -> Optional[List[List[str]]]:
def label_fn(cell: BoolRef, m: ModelRef): def label_fn(cell: BoolRef, m: ModelRef):
# For shaded cells, 0x2589 == '▉' # For shaded cells, 0x2589 == '▉'
return chr(0x2589) if bool(m[cell]) else "." return SHADED if bool(m[cell]) else "."
return g.to_str_matrix(label_fn) return g.to_str_matrix(label_fn)
...@@ -40,7 +40,7 @@ def solve_norinori(puzzle: NorinoriInstance) -> Optional[List[List[str]]]: ...@@ -40,7 +40,7 @@ def solve_norinori(puzzle: NorinoriInstance) -> Optional[List[List[str]]]:
if __name__ == "__main__": if __name__ == "__main__":
filename = "tests/norinori/data/norinori_1.txt" filename = "tests/norinori/data/norinori_0.txt"
puzzle = load_puzzle(filename) puzzle = load_puzzle(filename)
soln = solve_norinori(puzzle) soln = solve_norinori(puzzle)
......
from typing import List, NamedTuple, Tuple from typing import List, NamedTuple, Tuple
SHADED = chr(0x2589)
class NorinoriInstance(NamedTuple): class NorinoriInstance(NamedTuple):
areas: List[List[int]] areas: List[List[int]]
...@@ -27,7 +29,7 @@ def load_puzzle_and_solution(fname: str) -> Tuple[NorinoriInstance, List[List[st ...@@ -27,7 +29,7 @@ def load_puzzle_and_solution(fname: str) -> Tuple[NorinoriInstance, List[List[st
] ]
shaded = [ shaded = [
[chr(0x2589) if s == "#" else "." for s in f.readline().strip().split(" ")] [SHADED if s == "#" else "." for s in f.readline().strip().split(" ")]
for _ in range(height) for _ in range(height)
] ]
......
from typing import List, Optional
from cs11puzzles.grid import Grid, point_to_int_var
from cs11puzzles.str_helpers import str_matrix_to_text
from genres.ripple.ripple_util import RippleEffectInstance, load_puzzle
from z3 import *
def solve_ripple(puzzle: RippleEffectInstance) -> Optional[List[List[str]]]:
areas = puzzle.areas
numbers = puzzle.numbers
height = len(areas)
width = len(areas[0])
# Helper library - delete me if you want to roll your own grid!
g = Grid[ArithRef](height, width, point_to_int_var)
# TODO: Set up constraints! If you're using the helper library, Grid stores
# the solver as a field (g.solver)
if g.solver.check() == sat:
def label_fn(cell: ArithRef, m: ModelRef):
return str(m[cell])
return g.to_str_matrix(label_fn)
return None
if __name__ == "__main__":
filename = "tests/ripple/data/ripple_0.txt"
puzzle = load_puzzle(filename)
sol = solve_ripple(puzzle)
if sol:
print(str_matrix_to_text(sol))
else:
print("No solution.")
from genres.starbattle.starbattle_util import StarBattleInstance
from typing import NamedTuple, List, Tuple
from cs11puzzles.pzpr_helpers import borders_to_areas
class RippleEffectInstance(NamedTuple):
numbers: List[List[int]]
areas: List[List[int]]
def load_puzzle_and_solution(fname: str) -> Tuple[StarBattleInstance, List[List[str]]]:
with open(fname) as f:
assert f.readline().strip() == "pzprv3"
assert f.readline().strip() == "ripple"
height = int(f.readline())
_ = int(f.readline()) # width
# Read walls
wall_right = [
[int(n) == 1 for n in f.readline().strip().split(" ")]
for _ in range(height)
]
wall_down = [
[int(n) == 1 for n in f.readline().strip().split(" ")]
for _ in range(height - 1)
]
areas = borders_to_areas(wall_right, wall_down)
given_numbers = [
[int(n) if n != "." else 0 for n in f.readline().strip().split(" ")]
for _ in range(height)
]
soln_numbers = [
[str(n) for n in f.readline().strip().split(" ")] for _ in range(height)
]
# Givens aren't included in solution numbers, so explicitly transfer
for r, row in enumerate(given_numbers):
for c, num in enumerate(row):
if num:
soln_numbers[r][c] = str(num)
return RippleEffectInstance(given_numbers, areas), soln_numbers
def load_puzzle(filename: str):
puzzle, _ = load_puzzle_and_solution(filename)
return puzzle
from typing import List, Optional
from cs11puzzles.grid import Grid, point_to_bool_var
from cs11puzzles.str_helpers import str_matrix_to_text
from genres.starbattle.starbattle_util import STAR, StarBattleInstance, load_puzzle
from z3 import *
def solve_starbattle(puzzle: StarBattleInstance) -> Optional[List[List[str]]]:
areas = puzzle.areas
n_stars = puzzle.n_stars
height = len(areas)
width = len(areas[0])
# Helper library - delete me if you want to roll your own grid!
g = Grid[BoolRef](height, width, point_to_bool_var)
# TODO: Set up constraints! If you're using the helper library, Grid stores
# the solver as a field (g.solver)
if g.solver.check() == sat:
def label_fn(cell: BoolRef, m: ModelRef):
return STAR if bool(m[cell]) else "."
return g.to_str_matrix(label_fn)
return None
if __name__ == "__main__":
filename = "tests/starbattle/data/starbattle_0.txt"
puzzle = load_puzzle(filename)
sol = solve_starbattle(puzzle)
if sol:
print(str_matrix_to_text(sol))
else:
print("No solution.")
from typing import List, NamedTuple, Tuple
STAR = chr(0x2605)
class StarBattleInstance(NamedTuple):
areas: List[List[int]]
n_stars: int
def load_puzzle_and_solution(fname: str) -> Tuple[StarBattleInstance, List[List[str]]]:
with open(fname) as f:
assert f.readline().strip() == "pzprv3"
assert f.readline().strip() == "starbattle"
height = int(f.readline())
_ = int(f.readline()) # width
n_stars = int(f.readline())
_ = int(f.readline()) # n_regions
areas = [
[int(n) for n in f.readline().strip().split(" ")] for _ in range(height)
]
stars = [
[STAR if s == "#" else "." for s in f.readline().strip().split(" ")]
for _ in range(height)
]
return StarBattleInstance(areas, n_stars), stars
def load_puzzle(filename: str):
puzzle, _ = load_puzzle_and_solution(filename)
return puzzle
...@@ -4,12 +4,13 @@ import pytest ...@@ -4,12 +4,13 @@ import pytest
from genres.norinori.norinori import solve_norinori from genres.norinori.norinori import solve_norinori
from genres.norinori.norinori_util import load_puzzle_and_solution from genres.norinori.norinori_util import load_puzzle_and_solution
DATA_DIR = 'tests/norinori/data' DATA_DIR = "tests/norinori/data"
PUZZLES = [f'norinori_{i}.txt' for i in range(1, 6)] PUZZLES = [f"norinori_{i}.txt" for i in range(5)]
@pytest.mark.parametrize('filename', PUZZLES)
@pytest.mark.parametrize("filename", PUZZLES)
@pytest.mark.timeout(10)
def test_norinori(filename: str): def test_norinori(filename: str):
puzzle, solution = load_puzzle_and_solution(os.path.join(DATA_DIR, filename)) puzzle, solution = load_puzzle_and_solution(os.path.join(DATA_DIR, filename))
actual_solution = solve_norinori(puzzle) actual_solution = solve_norinori(puzzle)
# pytest: expected / actual
assert actual_solution == solution assert actual_solution == solution
pzprv3
ripple
7
7
1 1 1 1 1 1
1 0 1 1 1 1
1 1 1 1 1 1
1 1 1 1 1 1
1 1 1 1 1 1
1 1 0 1 1 1
0 0 0 1 1 0
0 1 0 1 0 1 0
0 0 1 0 0 0 0
1 0 0 0 1 0 1
0 1 0 0 1 0 1
0 0 1 0 0 0 0
1 0 1 1 0 1 0
. . . . . . .
. . 2 . . . .
2 . . 5 . . .
. . . . . 5 .
. 6 . . . 4 .
. . . . . 2 .
4 . . . . . .
3 1 5 1 3 1 2
1 3 . 4 1 3 1
. 1 3 . 2 1 3
1 4 1 2 1 . 1
3 . 2 1 3 . 2
2 5 6 3 1 . 4
. 2 3 1 2 1 3
KrazyDad - Easy Ripple Effect, Volume 1, Book 1, Ripple #12
© 2018 KrazyDad.com
pzprv3
ripple
7
7
0 1 0 1 0 1
1 0 1 0 1 1
0 1 0 0 0 1
0 1 0 1 0 1
1 0 1 0 1 0
0 1 0 1 0 1
1 0 1 0 0 0
0 1 0 1 0 1 0
0 1 1 1 1 0 0
1 1 1 1 1 1 0
0 1 0 1 0 1 1
0 1 1 1 1 0 1
0 1 0 1 1 1 1
. . . . . . .
. . . . . . .
. . . 1 . . .
2 3 . . . . .
1 2 . . . . .
. . . . . . .
. . . . . . .
4 2 3 1 2 4 3
5 4 2 3 1 2 4
3 1 4 . 3 5 2
. . 1 4 2 3 1
. . 3 1 4 2 3
4 5 2 3 1 4 1
6 4 1 2 3 1 4
KrazyDad - Easy Ripple Effect, Volume 2, Book 45, Ripple #3
© 2018 KrazyDad.com
pzprv3
ripple
8
8
1 0 1 1 0 0 0
0 0 0 1 1 1 0
0 1 0 1 1 1 1
1 0 1 1 0 0 1
0 0 0 0 1 0 1
1 0 0 0 1 0 1
0 1 1 1 1 1 0
1 0 0 0 0 1 0
1 1 1 0 0 1 1 1
1 1 1 1 0 0 1 0
0 1 0 1 1 0 1 0
1 1 1 0 1 0 0 0
1 1 1 1 1 1 1 0
1 1 0 1 1 0 1 1
0 1 1 1 0 1 0 0
. . . . . . 4 3
. . . 3 . . . 6
. . . . 5 7 . .
. . . . . . . .
. . . . 1 . . .
. . . . . 2 . 5
. . . . . . . 2
. . . . . . . .
1 2 1 5 2 6 . .
4 1 2 . 1 5 2 .
1 3 4 2 . . 1 4
2 1 3 4 6 1 3 1
3 5 2 6 . 4 2 3
1 2 1 3 4 . 1 .
2 3 5 1 2 3 4 .
1 4 3 5 1 6 3 1
KrazyDad - Tough Ripple Effect, Volume 1, Book 50, Ripple #6
© 2018 KrazyDad.com
pzprv3
ripple
8
8
0 1 0 1 0 1 0
1 0 1 0 1 0 1
1 0 1 0 1 0 1
0 0 0 0 1 1 0
0 1 0 0 0 1 1
1 0 1 1 0 1 1
1 0 1 1 1 0 1
1 0 0 1 1 0 1
1 0 1 0 1 0 1 0
0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 0
1 1 1 1 1 0 1 1
1 0 1 0 1 1 0 0
0 0 0 0 1 0 1 0
1 1 1 1 0 0 0 0
1 . . 5 . . 3 .
. 6 . . . . . 1
. . . . . . . .
. . 3 2 . 1 . .
. . . . . . . .
. . . . 4 3 . .
. . 4 . . . 1 .
. . . . . . 5 2
. 3 6 . 4 1 . 2
2 . 4 1 2 3 5 .
1 5 2 4 3 2 6 5
4 1 . . 5 . 4 6
6 2 7 3 2 4 1 3
1 3 5 6 . . 2 1
2 1 . 5 1 2 . 4
1 2 3 1 2 6 . .
KrazyDad - Challenging Ripple Effect, Volume 2, Book 22, Ripple #9
© 2018 KrazyDad.com
pzprv3
ripple
10
10
0 1 0 0 1 0 1 0 1
0 0 1 0 0 1 0 0 1
1 0 1 0 1 0 1 1 1
0 1 1 0 0 1 1 1 1
1 0 1 0 1 0 1 1 1
0 1 0 1 0 1 0 1 1
1 0 1 0 1 1 0 1 1
0 1 1 1 0 1 1 1 1
1 0 1 0 1 0 1 1 0
0 1 0 1 0 0 1 0 0
0 0 1 1 1 1 0 1 1 0
1 1 1 0 0 1 1 0 1 0
0 1 0 1 1 1 0 0 0 0
0 1 0 0 0 1 0 0 0 1
1 1 0 1 1 1 0 1 0 0
0 1 1 1 1 0 1 1 0 0
1 1 0 0 1 0 0 1 0 0
0 1 0 1 1 0 1 0 1 0
1 1 0 1 0 1 1 0 1 1
5 . . . . . 8 . 1 .
. . . 1 . . . . . .
4 6 . . . . . . . 2
. . . . . . . 6 . .
. . . . . . . . . .
. . . . . . 5 . . .
. . . . . . . . . .
. . . . 6 . . . . .
1 . . . . . . 1 . .
. . . . 4 . . 2 . .
. 4 1 2 3 1 . 2 . 3
1 3 2 . 4 3 2 5 7 1
. . 3 5 2 7 1 4 3 .
3 2 5 4 1 2 3 . 5 4
1 7 2 3 5 6 4 3 2 1
2 3 4 1 2 3 . 2 4 6
1 5 1 2 3 4 2 1 6 5
3 2 7 1 . 5 3 4 1 2
. 4 3 5 2 1 7 . 3 4
2 1 6 2 . 3 1 . 5 3
KrazyDad - Super-Tough Ripple Effect, Volume 2, Book 74, Ripple #12
© 2018 KrazyDad.com
\ No newline at end of file
import os
import pytest
from genres.ripple.ripple_ref import solve_ripple
from genres.ripple.ripple_util import load_puzzle_and_solution
DATA_DIR = "tests/ripple/data"
PUZZLES = [f"ripple_{i}.txt" for i in range(5)]
@pytest.mark.parametrize("filename", PUZZLES)
@pytest.mark.timeout(10)
def test_ripple(filename: str):
puzzle, solution = load_puzzle_and_solution(os.path.join(DATA_DIR, filename))
actual_solution = solve_ripple(puzzle)
assert actual_solution == solution
pzprv3
starbattle
5
5
1
5
0 0 0 0 0
1 0 0 0 2
1 1 3 3 2
1 1 3 2 2
4 4 3 2 2
. . . # .
# . . . .
. . # . .
. . . . #
. # . . .
pzprv3
starbattle
9
9
2
9
0 0 0 0 0 0 1 1 1
2 2 0 0 0 1 1 3 1
2 2 2 2 4 4 3 3 3
5 2 2 4 4 4 3 3 3
5 5 4 4 4 4 3 3 3
5 5 4 4 4 6 6 3 3
5 5 4 4 6 6 7 7 7
5 5 8 8 6 7 7 7 7
8 8 8 8 8 7 7 7 7
. . # . # . . . .
. . . . . . # . #
. # . # . . . . .
. . . . . # . # .
. # . # . . . . .
. . . . . . # . #
# . . . # . . . .
. . # . . . . # .
# . . . . # . . .
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