def shikaku(givens): """Solver for Shikaku minipuzzles.""" sym = grilops.make_number_range_symbol_set(0, SIZE * SIZE - 1) sg = grilops.SymbolGrid(LATTICE, sym) rc = grilops.regions.RegionConstrainer(LATTICE, solver=sg.solver, rectangular=True) shifter = Shifter(sg.solver) for p in LATTICE.points: sg.solver.add(sg.cell_is(p, rc.region_id_grid[p])) given = givens[p.y][p.x] if given > 0: given = shifter.given(p, given) sg.solver.add(rc.parent_grid[p] == grilops.regions.R) sg.solver.add(rc.region_size_grid[p] == given) else: sg.solver.add(rc.parent_grid[p] != grilops.regions.R) assert sg.solve() sg.print() print() shifter.print_shifts() print() return shifter.eval_binary()
def main(): """Nurikabe solver example.""" sym = grilops.SymbolSet([("B", chr(0x2588)), ("W", " ")]) lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, sym) rc = grilops.regions.RegionConstrainer(lattice, solver=sg.solver) constrain_sea(sym, sg, rc) constrain_islands(sym, sg, rc) constrain_adjacent_cells(sg, rc) def print_grid(): sg.print(lambda p, _: str(GIVENS[(p.y, p.x)]) if (p.y, p.x) in GIVENS else None) if sg.solve(): print_grid() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") print_grid() else: print("No solution")
def main(): """Shikaku solver example.""" sym = grilops.make_number_range_symbol_set(0, HEIGHT * WIDTH - 1) lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, sym) rc = grilops.regions.RegionConstrainer( lattice, solver=sg.solver, rectangular=True, min_region_size=min(GIVENS.values()), max_region_size=max(GIVENS.values()) ) for p in lattice.points: sg.solver.add(sg.cell_is(p, rc.region_id_grid[p])) region_size = GIVENS.get(p) if region_size: sg.solver.add(rc.parent_grid[p] == grilops.regions.R) sg.solver.add(rc.region_size_grid[p] == region_size) if sg.solve(): sg.print() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def region_solve(): """Hex slitherlink solver example using regions.""" lattice = get_lattice() sym = grilops.SymbolSet(["I", "O"]) sg = grilops.SymbolGrid(lattice, sym) rc = grilops.regions.RegionConstrainer(lattice, solver=sg.solver, complete=True) # There must be exactly two connected regions: the inside and the outside. # The outside region ID will be 0 because the top-left-most element will # always be outside. inside_region_id = Int("inside_region_id") sg.solver.add(inside_region_id != 0) for p in lattice.points: # A cell should have symbol I if and only if it's in the inside region. sg.solver.add((sg.grid[p] == sym.I) == ( rc.region_id_grid[p] == inside_region_id)) # If this cell isn't in the givens array, it must be outside the loop. givens_addr = point_to_givens_row_col(p) if givens_addr is None: sg.solver.add(sg.grid[p] == sym.O) continue # Find the given corresponding to this cell. If it's None, we don't # know anything about it. r, c = givens_addr given = GIVENS[r][c] if given is None: continue # The given number must equal the number of adjacent cells on the # opposite side of the loop line. num_different_neighbors_terms = [(n.symbol != sg.grid[p], 1) for n in sg.edge_sharing_neighbors(p)] sg.solver.add(PbEq(num_different_neighbors_terms, given)) def hook_function(p, _): addr = point_to_givens_row_col(p) return " " if addr is None else None if sg.solve(): sg.print(hook_function) print_loop(sg.grid, sg.solver.model()) print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print(hook_function) print_loop(sg.grid, sg.solver.model()) else: print("No solution")
def main(): """Gokigen Naname solver example.""" sym = grilops.SymbolSet([("F", chr(0x2571)), ("B", chr(0x2572))]) lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, sym) # Ensure the given number of line segment constraints are met. for (y, x), v in GIVENS.items(): terms = [] if y > 0: if x > 0: terms.append(sg.cell_is(Point(y - 1, x - 1), sym.B)) if x < WIDTH: terms.append(sg.cell_is(Point(y - 1, x), sym.F)) if y < HEIGHT: if x > 0: terms.append(sg.cell_is(Point(y, x - 1), sym.F)) if x < WIDTH: terms.append(sg.cell_is(Point(y, x), sym.B)) sg.solver.add(PbEq([(term, 1) for term in terms], v)) add_loop_constraints(sym, sg) if sg.solve(): sg.print() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def main(): """Status Park (Loop) solver example.""" for row in GIVENS: for cell in row: sys.stdout.write(cell) print() points = [] for y, row in enumerate(GIVENS): for x, c in enumerate(row): if c != X: points.append(Point(y, x)) lattice = RectangularLattice(points) sym = grilops.loops.LoopSymbolSet(lattice) sym.append("EMPTY", " ") sg = grilops.SymbolGrid(lattice, sym) grilops.loops.LoopConstrainer(sg, single_loop=True) sc = ShapeConstrainer( lattice, [Shape([Vector(y, x) for y, x in shape]) for shape in SHAPES], solver=sg.solver, allow_rotations=True, allow_reflections=True, allow_copies=False) for p in points: if GIVENS[p.y][p.x] == W: # White circles must be part of the loop. sg.solver.add(sym.is_loop(sg.grid[p])) elif GIVENS[p.y][p.x] == B: # Black circles must be part of a shape. sg.solver.add(sc.shape_type_grid[p] != -1) # A cell is part of the loop if and only if it is not part of # any shape. sg.solver.add(sym.is_loop(sg.grid[p]) == (sc.shape_type_grid[p] == -1)) # Orthogonally-adjacent cells must be part of the same shape. for n in sg.edge_sharing_neighbors(p): np = n.location sg.solver.add( Implies( And(sc.shape_type_grid[p] != -1, sc.shape_type_grid[np] != -1), sc.shape_type_grid[p] == sc.shape_type_grid[np])) if sg.solve(): sg.print() print() sc.print_shape_types() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def main(): """Kuromasu solver example.""" sym = grilops.SymbolSet([("B", chr(0x2588) * 2), ("W", " ")]) lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, sym) rc = grilops.regions.RegionConstrainer(lattice, solver=sg.solver, complete=False) for p, c in GIVENS.items(): # Numbered cells may not be black. sg.solver.add(sg.cell_is(p, sym.W)) # Each number on the board represents the number of white cells that can be # seen from that cell, including itself. A cell can be seen from another # cell if they are in the same row or column, and there are no black cells # between them in that row or column. visible_cell_count = 1 + sum( grilops.sightlines.count_cells( sg, n.location, n.direction, stop=lambda c: c == sym.B) for n in sg.edge_sharing_neighbors(p)) sg.solver.add(visible_cell_count == c) # All the white cells must be connected horizontally or vertically. Enforce # this by requiring all white cells to have the same region ID. Force the # root of this region to be the first given, to reduce the space of # possibilities. white_root = min(GIVENS.keys()) white_region_id = lattice.point_to_index(white_root) for p in lattice.points: # No two black cells may be horizontally or vertically adjacent. sg.solver.add( Implies( sg.cell_is(p, sym.B), And(*[n.symbol == sym.W for n in sg.edge_sharing_neighbors(p)]))) # All white cells must have the same region ID. All black cells must not # be part of a region. sg.solver.add( If(sg.cell_is(p, sym.W), rc.region_id_grid[p] == white_region_id, rc.region_id_grid[p] == -1)) def print_grid(): sg.print(lambda p, _: f"{GIVENS[(p.y, p.x)]:02}" if (p.y, p.x) in GIVENS else None) if sg.solve(): print_grid() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") print_grid() else: print("No solution")
def main(): """SLICY solver example.""" sym = grilops.SymbolSet(["S", "L", "I", "C", "Y", ("W", " ")]) lattice = PointyToppedHexagonalLattice([ areas_row_col_to_point(r, c) for r in range(len(AREAS)) for c in range(len(AREAS[r])) ]) sg = grilops.SymbolGrid(lattice, sym) rc = grilops.regions.RegionConstrainer(lattice, solver=sg.solver, complete=True) sc = ShapeConstrainer( lattice, [ # Note that the example shapes are shown as flat-topped hexagons, so # you need to turn the page sideways to see them as pointy-topped ones. Shape([Vector(0, 0), Vector(1, 1), Vector(1, 3), Vector(2, 4)]), # S Shape([Vector(0, 0), Vector(0, 2), Vector(0, 4), Vector(-1, 5)]), # L Shape([Vector(0, 0), Vector(0, 2), Vector(0, 4), Vector(0, 6)]), # I Shape([Vector(0, 0), Vector(1, 1), Vector(1, 3), Vector(0, 4)]), # C Shape([Vector(0, 0), Vector(2, 0), Vector(1, 1), Vector(1, 3)]), # Y ], solver=sg.solver, allow_rotations=True, allow_reflections=True, allow_copies=True) link_symbols_to_shapes(sym, sg, sc) add_area_constraints(lattice, sc) add_sea_constraints(sym, sg, rc) add_adjacent_tetrahex_constraints(lattice, sc) if sg.solve(): sg.print() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() print() else: print("No solution")
def main(): """Halloween Town / Valentine's Day Town solver.""" sg = grilops.SymbolGrid(LATTICE, SYM) lc = grilops.loops.LoopConstrainer(sg, single_loop=True) # Cheat a little bit and force the loop order to start such that the answer # extraction starts at the correct place and proceeds in the correct order. sg.solver.add(lc.loop_order_grid[Point(5, 6)] == 0) sg.solver.add(lc.loop_order_grid[Point(5, 5)] == 1) # Count the number of Os in the puzzle answers. o_count = sum(c == "O" for row in ANSWERS for c in row) # There will be exactly twice as many turns as Os. This constraint is not # strictly necessary to add, but the solver runs faster when it is added. sg.solver.add( PbEq([(sg.cell_is_one_of(p, TURN_SYMBOLS), 1) for p in LATTICE.points], o_count * 2)) # Find the loop order values for all of the turns. turn_loop_orders = [Int(f"tlo-{i}") for i in range(o_count * 2)] for tlo in turn_loop_orders: sg.solver.add(tlo >= 0) sg.solver.add(tlo < 8 * 8) for i in range(len(turn_loop_orders) - 1): sg.solver.add(turn_loop_orders[i] < turn_loop_orders[i + 1]) for p in LATTICE.points: # Figure out each turn's loop order value. sg.solver.add( Implies( sg.cell_is_one_of(p, TURN_SYMBOLS), Or(*[tlo == lc.loop_order_grid[p] for tlo in turn_loop_orders]))) if ANSWERS[p.y][p.x] == "O": # An O must be a turn. sg.solver.add(sg.cell_is_one_of(p, TURN_SYMBOLS)) # An O must be in an odd position in the list of turn loop order values. or_terms = [] for i in range(1, len(turn_loop_orders), 2): or_terms.append(lc.loop_order_grid[p] == turn_loop_orders[i]) sg.solver.add(Or(*or_terms)) if sg.solve(): sg.print() print(extract_answer(sg, lc.loop_order_grid)) print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def main(): """LITS solver example.""" sym = grilops.SymbolSet(["L", "I", "T", "S", ("W", " ")]) lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, sym) rc = grilops.regions.RegionConstrainer(lattice, solver=sg.solver) sc = grilops.shapes.ShapeConstrainer( lattice, [ [Vector(0, 0), Vector(1, 0), Vector(2, 0), Vector(2, 1)], # L [Vector(0, 0), Vector(1, 0), Vector(2, 0), Vector(3, 0)], # I [Vector(0, 0), Vector(0, 1), Vector(0, 2), Vector(1, 1)], # T [Vector(0, 0), Vector(1, 0), Vector(1, 1), Vector(2, 1)], # S ], solver=sg.solver, allow_rotations=True, allow_reflections=True, allow_copies=True) link_symbols_to_shapes(sym, sg, sc) add_area_constraints(lattice, sc) add_nurikabe_constraints(sym, sg, rc) add_adjacent_tetronimo_constraints(lattice, sc) if sg.solve(): sg.print() print() sc.print_shape_types() print() sc.print_shape_instances() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() print() sc.print_shape_types() print() sc.print_shape_instances() print() else: print("No solution")
def main(): """Shape solver example.""" points = [] for y, row in enumerate(GRID): for x, c in enumerate(row): if c == "O": points.append(Point(y, x)) lattice = RectangularLattice(points) shapes = [ Shape([Vector(0, 0), Vector(1, 0), Vector(2, 0), Vector(3, 0)]), # I Shape([Vector(0, 0), Vector(1, 0), Vector(2, 0), Vector(2, 1)]), # L Shape([Vector(0, 1), Vector(0, 2), Vector(1, 0), Vector(1, 1)]), # S ] sym = grilops.SymbolSet([("B", chr(0x2588) * 2), ("W", " ")]) sg = grilops.SymbolGrid(lattice, sym) sc = ShapeConstrainer(lattice, shapes, sg.solver, complete=False, allow_rotations=True, allow_reflections=True, allow_copies=False) for p in points: sg.solver.add(sg.cell_is(p, sym.W) == (sc.shape_type_grid[p] == -1)) for n in sg.vertex_sharing_neighbors(p): np = n.location sg.solver.add( Implies( And(sc.shape_type_grid[p] != -1, sc.shape_type_grid[np] != -1), sc.shape_type_grid[p] == sc.shape_type_grid[np])) if sg.solve(): sg.print() print() sc.print_shape_types() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def main(): """Spiral Galaxies solver example.""" # The grid symbols will be the region IDs from the region constrainer. sym = grilops.make_number_range_symbol_set(0, HEIGHT * WIDTH - 1) lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, sym) rc = grilops.regions.RegionConstrainer(lattice, sg.solver) for p in sg.grid: sg.solver.add(sg.cell_is(p, rc.region_id_grid[p])) # Make the upper-left-most cell covered by a circle the root of its region. roots = {(int(math.floor(y)), int(math.floor(x))) for (y, x) in GIVENS} r = grilops.regions.R for y in range(HEIGHT): for x in range(WIDTH): sg.solver.add((rc.parent_grid[Point(y, x)] == r) == ((y, x) in roots)) # Ensure that each cell has a "partner" within the same region that is # rotationally symmetric with respect to that region's circle. for p in lattice.points: or_terms = [] for (gy, gx) in GIVENS: region_id = lattice.point_to_index( Point(int(math.floor(gy)), int(math.floor(gx)))) partner = Point(int(2 * gy - p.y), int(2 * gx - p.x)) if lattice.point_to_index(partner) is None: continue or_terms.append( And( rc.region_id_grid[p] == region_id, rc.region_id_grid[partner] == region_id, )) sg.solver.add(Or(*or_terms)) def show_cell(unused_p, region_id): rp = lattice.points[region_id] for i, (gy, gx) in enumerate(GIVENS): if int(math.floor(gy)) == rp.y and int(math.floor(gx)) == rp.x: return chr(65 + i) raise Exception("unexpected region id") if sg.solve(): sg.print(show_cell) print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print(show_cell) else: print("No solution")
def main(): """Tapa solver example.""" lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, SYM) rc = grilops.regions.RegionConstrainer(lattice, solver=sg.solver, complete=False) # Ensure the wall consists of a single region and is made up of shaded # symbols. wall_id = Int("wall_id") sg.solver.add(wall_id >= 0) sg.solver.add(wall_id < HEIGHT * WIDTH) for y in range(HEIGHT): for x in range(WIDTH): p = Point(y, x) sg.solver.add( sg.cell_is(p, SYM.B) == (rc.region_id_grid[p] == wall_id)) # Ensure that given clue cells are not part of the wall. if (y, x) in GIVENS: sg.solver.add(sg.cell_is(p, SYM.W)) # Shaded cells may not form a 2x2 square anywhere in the grid. for sy in range(HEIGHT - 1): for sx in range(WIDTH - 1): pool_cells = [ sg.grid[Point(y, x)] for y in range(sy, sy + 2) for x in range(sx, sx + 2) ] sg.solver.add(Not(And(*[cell == SYM.B for cell in pool_cells]))) # Add constraints for the given clues. for y, x in GIVENS.keys(): add_neighbor_constraints(sg, y, x) def show_cell(p, _): given = GIVENS.get((p.y, p.x)) if given is None: return None if len(given) > 1: return "*" return str(given[0]) if sg.solve(): sg.print(show_cell) print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print(show_cell) else: print("No solution")
def main(): """Skyscraper solver example.""" lattice = grilops.get_square_lattice(SIZE) directions = {d.name: d for d in lattice.edge_sharing_directions()} sg = grilops.SymbolGrid(lattice, SYM) # Each row and each column contains each building height exactly once. for y in range(SIZE): sg.solver.add(Distinct(*[sg.grid[Point(y, x)] for x in range(SIZE)])) for x in range(SIZE): sg.solver.add(Distinct(*[sg.grid[Point(y, x)] for y in range(SIZE)])) # We'll use the sightlines accumulator to keep track of a tuple storing: # the tallest building we've seen so far # the number of visible buildings we've encountered Acc = Datatype("Acc") # pylint: disable=C0103 Acc.declare("acc", ("tallest", IntSort()), ("num_visible", IntSort())) Acc = Acc.create() # pylint: disable=C0103 def accumulate(a, height): return Acc.acc( If(height > Acc.tallest(a), height, Acc.tallest(a)), If(height > Acc.tallest(a), Acc.num_visible(a) + 1, Acc.num_visible(a))) for x, c in enumerate(GIVEN_TOP): sg.solver.add(c == Acc.num_visible( grilops.sightlines.reduce_cells(sg, Point(0, x), directions["S"], Acc.acc(0, 0), accumulate))) for y, c in enumerate(GIVEN_LEFT): sg.solver.add(c == Acc.num_visible( grilops.sightlines.reduce_cells(sg, Point(y, 0), directions["E"], Acc.acc(0, 0), accumulate))) for y, c in enumerate(GIVEN_RIGHT): sg.solver.add(c == Acc.num_visible( grilops.sightlines.reduce_cells(sg, Point( y, SIZE - 1), directions["W"], Acc.acc(0, 0), accumulate))) for x, c in enumerate(GIVEN_BOTTOM): sg.solver.add(c == Acc.num_visible( grilops.sightlines.reduce_cells(sg, Point( SIZE - 1, x), directions["N"], Acc.acc(0, 0), accumulate))) if sg.solve(): sg.print() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def main(): """Fillomino solver example.""" givens = [ [0, 0, 0, 3, 0, 0, 0, 0, 5], [0, 0, 8, 3, 10, 0, 0, 5, 0], [0, 3, 0, 0, 0, 4, 4, 0, 0], [1, 3, 0, 3, 0, 0, 2, 0, 0], [0, 2, 0, 0, 3, 0, 0, 2, 0], [0, 0, 2, 0, 0, 3, 0, 1, 3], [0, 0, 4, 4, 0, 0, 0, 3, 0], [0, 4, 0, 0, 4, 3, 3, 0, 0], [6, 0, 0, 0, 0, 1, 0, 0, 0], ] sym = grilops.make_number_range_symbol_set(1, 10) lattice = grilops.get_rectangle_lattice(len(givens), len(givens[0])) sg = grilops.SymbolGrid(lattice, sym) rc = grilops.regions.RegionConstrainer(lattice, solver=sg.solver) for p in lattice.points: # The filled number must match the size of the region. sg.solver.add(sg.grid[p] == rc.region_size_grid[p]) # The size of the region must match the clue. given = givens[p.y][p.x] if given != 0: sg.solver.add(rc.region_size_grid[p] == given) # Different regions of the same size may not be orthogonally adjacent. region_sizes = [ n.symbol for n in lattice.edge_sharing_neighbors(rc.region_size_grid, p) ] region_ids = [ n.symbol for n in lattice.edge_sharing_neighbors(rc.region_id_grid, p) ] for region_size, region_id in zip(region_sizes, region_ids): sg.solver.add( Implies(rc.region_size_grid[p] == region_size, rc.region_id_grid[p] == region_id)) if sg.solve(): sg.print() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def main(): """Star Battle solver example.""" sym = grilops.SymbolSet([("EMPTY", " "), ("STAR", "*")]) lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, sym) # There must be exactly two stars per column. for y in range(HEIGHT): sg.solver.add( Sum(*[ If(sg.cell_is(Point(y, x), sym.STAR), 1, 0) for x in range(WIDTH) ]) == 2) # There must be exactly two stars per row. for x in range(WIDTH): sg.solver.add( Sum(*[ If(sg.cell_is(Point(y, x), sym.STAR), 1, 0) for y in range(HEIGHT) ]) == 2) # There must be exactly two stars per area. area_cells = defaultdict(list) for y in range(HEIGHT): for x in range(WIDTH): area_cells[AREAS[y][x]].append(sg.grid[Point(y, x)]) for cells in area_cells.values(): sg.solver.add(Sum(*[If(c == sym.STAR, 1, 0) for c in cells]) == 2) # Stars may not touch each other, not even diagonally. for y in range(HEIGHT): for x in range(WIDTH): p = Point(y, x) sg.solver.add( Implies( sg.cell_is(p, sym.STAR), And(*[ n.symbol == sym.EMPTY for n in sg.vertex_sharing_neighbors(p) ]))) if sg.solve(): sg.print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def main(): """Hex kakuro solver example.""" points = [] points.extend([Point(1, i) for i in range(-3, 4, 2)]) points.extend([Point(2, i) for i in range(-4, 5, 2)]) points.extend([Point(3, -5), Point(3, -3), Point(3, 3), Point(3, 5)]) points.extend([ Point(4, -6), Point(4, -4), Point(4, -2), Point(4, 2), Point(4, 4), Point(4, 6) ]) points.extend([Point(5, -5), Point(5, -3), Point(5, 3), Point(5, 5)]) points.extend([Point(6, i) for i in range(-4, 5, 2)]) points.extend([Point(7, i) for i in range(-3, 4, 2)]) lattice = PointyToppedHexagonalLattice(points) sym = grilops.make_number_range_symbol_set(1, 9) sg = grilops.SymbolGrid(lattice, sym) dirs_by_name = dict(sg.lattice.edge_sharing_directions()) for entry in SUMS: (y, x), dirname, given = entry d = dirs_by_name[dirname] s = grilops.sightlines.count_cells(sg, Point(y, x), d, count=lambda c: c) sg.solver.add(given == s) for p in lattice.points: for d in [Vector(0, 2), Vector(1, 1), Vector(1, -1)]: if p.translate(d.negate()) not in sg.grid: q = p ps = [] while q in sg.grid: ps.append(sg.grid[q]) q = q.translate(d) sg.solver.add(Distinct(*ps)) if sg.solve(): sg.print() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def main(): """Aquarium solver example.""" sym = grilops.SymbolSet([("B", chr(0x2588)), ("W", " ")]) lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, sym) # Number of shaded cells per row / column must match clues. for y in range(HEIGHT): sg.solver.add( PbEq([(sg.grid[(y, x)] == sym.B, 1) for x in range(WIDTH)], ROW_CLUES[y])) for x in range(WIDTH): sg.solver.add( PbEq([(sg.grid[(y, x)] == sym.B, 1) for y in range(HEIGHT)], COL_CLUES[x])) # The water level in each aquarium is the same across its full width. for y in range(HEIGHT): for al in set(REGIONS[y]): # If any aquarium cell is filled within a row, then all cells of that # aquarium within that row must be filled. cells = [ sg.grid[(y, x)] for x in range(WIDTH) if REGIONS[y][x] == al ] for cell in cells[1:]: sg.solver.add(cell == cells[0]) # If an aquarium is filled within a row, and that aquarium also has # cells in the row below that row, then that same aquarium's cells below # must be filled as well. if y < HEIGHT - 1: cells_below = [ sg.grid[(y + 1, x)] for x in range(WIDTH) if REGIONS[y + 1][x] == al ] if cells_below: sg.solver.add( Implies(cells[0] == sym.B, cells_below[0] == sym.B)) if sg.solve(): sg.print() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def killer_sudoku(cages: List[str], cage_sum_grid: List[List[int]]) -> str: """Solver for Killer Sudoku minipuzzles.""" sym = grilops.make_number_range_symbol_set(1, SIZE) sg = grilops.SymbolGrid(LATTICE, sym) shifter = Shifter(sg.solver) # Add normal sudoku constraints. for y in range(SIZE): sg.solver.add(Distinct(*[sg.grid[Point(y, x)] for x in range(SIZE)])) for x in range(SIZE): sg.solver.add(Distinct(*[sg.grid[Point(y, x)] for y in range(SIZE)])) for z in range(9): top = (z // 3) * 3 left = (z % 3) * 3 cells = [ sg.grid[Point(y, x)] for y in range(top, top + 3) for x in range(left, left + 3) ] sg.solver.add(Distinct(*cells)) # Build a map from each cage label to the cells within that cage. cage_cells = defaultdict(list) for p in LATTICE.points: cage_cells[cages[p.y][p.x]].append(sg.grid[p]) # The digits used in each cage must be unique. for cells_in_cage in cage_cells.values(): sg.solver.add(Distinct(*cells_in_cage)) cage_sums = {} for p in LATTICE.points: cage_sum = cage_sum_grid[p.y][p.x] if cage_sum > 0: shifted_cage_sum = shifter.given(p, cage_sum) cage_label = cages[p.y][p.x] assert cage_label not in cage_sums cage_sums[cage_label] = shifted_cage_sum # Add constraints for cages with given sums. for cage_label, shifted_cage_sum in cage_sums.items(): sg.solver.add(Sum(*cage_cells[cage_label]) == shifted_cage_sum) assert sg.solve() sg.print() print() shifter.print_shifts() print() return shifter.eval_binary()
def main(): """Cave solver example.""" lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, SYM) rc = grilops.regions.RegionConstrainer(lattice, solver=sg.solver) # The cave must be a single connected group. Force the root of this region to # be the top-most, left-most given. cave_root_point = next(p for p in lattice.points if GIVENS[p.y][p.x] != 0) cave_region_id = lattice.point_to_index(cave_root_point) sg.solver.add(rc.parent_grid[cave_root_point] == grilops.regions.R) for p in lattice.points: # Ensure that every cave cell has the same region ID. sg.solver.add( sg.cell_is(p, SYM.W) == (rc.region_id_grid[p] == cave_region_id)) # Every shaded region must connect to an edge of the grid. We'll enforce # this by requiring that the root of a shaded region is along the edge of # the grid. if 0 < p.y < HEIGHT - 1 and 0 < p.x < WIDTH - 1: sg.solver.add( Implies(sg.cell_is(p, SYM.B), rc.parent_grid[p] != grilops.regions.R)) if GIVENS[p.y][p.x] != 0: sg.solver.add(sg.cell_is(p, SYM.W)) # Count the cells visible along sightlines from the given cell. visible_cell_count = 1 + sum( grilops.sightlines.count_cells( sg, n.location, n.direction, stop=lambda c: c == SYM.B) for n in sg.edge_sharing_neighbors(p)) sg.solver.add(visible_cell_count == GIVENS[p.y][p.x]) def print_grid(): sg.print(lambda p, _: str(GIVENS[p.y][p.x]) if GIVENS[p.y][p.x] != 0 else None) if sg.solve(): print_grid() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") print_grid() else: print("No solution")
def skyscraper(givens: Dict[Direction, List[int]]) -> str: """Solver for Skyscraper minipuzzles.""" sym = grilops.make_number_range_symbol_set(1, SIZE) sg = grilops.SymbolGrid(LATTICE, sym) shifter = Shifter(sg.solver) # Each row and each column contains each building height exactly once. for y in range(SIZE): sg.solver.add(Distinct(*[sg.grid[Point(y, x)] for x in range(SIZE)])) for x in range(SIZE): sg.solver.add(Distinct(*[sg.grid[Point(y, x)] for y in range(SIZE)])) # We'll use the sightlines accumulator to keep track of a tuple storing: # the tallest building we've seen so far # the number of visible buildings we've encountered Acc = Datatype("Acc") # pylint: disable=C0103 Acc.declare("acc", ("tallest", IntSort()), ("num_visible", IntSort())) Acc = Acc.create() # pylint: disable=C0103 def accumulate(a, height): return Acc.acc( If(height > Acc.tallest(a), height, Acc.tallest(a)), If(height > Acc.tallest(a), Acc.num_visible(a) + 1, Acc.num_visible(a))) for d, gs in givens.items(): for i, g in enumerate(gs): if d.vector.dy != 0: g = g - shifter.col_shifts.get(i, 0) p = Point(0 if d.vector.dy < 0 else SIZE - 1, i) elif d.vector.dx != 0: g = g - shifter.row_shifts.get(i, 0) p = Point(i, 0 if d.vector.dx < 0 else SIZE - 1) sg.solver.add(g == Acc.num_visible( # type: ignore[attr-defined] grilops.sightlines.reduce_cells( sg, p, LATTICE.opposite_direction(d), Acc.acc(0, 0), # type: ignore[attr-defined] accumulate))) assert sg.solve() sg.print() print() shifter.print_shifts() print() return shifter.eval_binary()
def main(): """Sudoku solver example.""" givens = [ [5, 3, 0, 0, 7, 0, 0, 0, 0], [6, 0, 0, 1, 9, 5, 0, 0, 0], [0, 9, 8, 0, 0, 0, 0, 6, 0], [8, 0, 0, 0, 6, 0, 0, 0, 3], [4, 0, 0, 8, 0, 3, 0, 0, 1], [7, 0, 0, 0, 2, 0, 0, 0, 6], [0, 6, 0, 0, 0, 0, 2, 8, 0], [0, 0, 0, 4, 1, 9, 0, 0, 5], [0, 0, 0, 0, 8, 0, 0, 7, 9], ] sym = grilops.make_number_range_symbol_set(1, 9) sg = grilops.SymbolGrid(grilops.get_square_lattice(9), sym) for y, given_row in enumerate(givens): for x, given in enumerate(given_row): if given != 0: sg.solver.add(sg.cell_is(Point(y, x), sym[given])) for y in range(9): sg.solver.add(Distinct(*[sg.grid[Point(y, x)] for x in range(9)])) for x in range(9): sg.solver.add(Distinct(*[sg.grid[Point(y, x)] for y in range(9)])) for z in range(9): top = (z // 3) * 3 left = (z % 3) * 3 cells = [sg.grid[Point(y, x)] for y in range(top, top + 3) for x in range(left, left + 3)] sg.solver.add(Distinct(*cells)) if sg.solve(): sg.print() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def loop_solve(): """Slitherlink solver example using loops.""" # We'll place symbols at the intersections of the grid lines, rather than in # the spaces between the grid lines where the givens are written. This # requires increasing each dimension by one. lattice = grilops.get_rectangle_lattice(HEIGHT + 1, WIDTH + 1) sym = grilops.loops.LoopSymbolSet(lattice) sym.append("EMPTY", " ") sg = grilops.SymbolGrid(lattice, sym) grilops.loops.LoopConstrainer(sg, single_loop=True) for (y, x), c in GIVENS.items(): # For each side of this given location, add one if there's a loop edge # along that side. We'll determine this by checking the kinds of loop # symbols in the north-west and south-east corners of this given location. terms = [ # Check for east edge of north-west corner (given's north edge). sg.cell_is_one_of(Point(y, x), [sym.EW, sym.NE, sym.SE]), # Check for north edge of south-east corner (given's east edge). sg.cell_is_one_of(Point(y + 1, x + 1), [sym.NS, sym.NE, sym.NW]), # Check for west edge of south-east corner (given's south edge). sg.cell_is_one_of(Point(y + 1, x + 1), [sym.EW, sym.SW, sym.NW]), # Check for south edge of north-west corner (given's west edge). sg.cell_is_one_of(Point(y, x), [sym.NS, sym.SE, sym.SW]), ] sg.solver.add(PbEq([(term, 1) for term in terms], c)) if sg.solve(): sg.print() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def main(): """Castle Wall solver example.""" sg = grilops.SymbolGrid(LATTICE, SYM) lc = LoopConstrainer(sg, single_loop=True) for p, (io, expected_count, direction) in GIVENS.items(): # Constrain whether the given cell is inside or outside of the loop. This # also prevents these cells from containing loop symbols themselves. sg.solver.add(lc.inside_outside_grid[p] == io) if expected_count is not None and direction is not None: # Count and constrain the number of loop segments in the given direction. seg_syms = DIRECTION_SEGMENT_SYMBOLS[direction] actual_count = grilops.sightlines.count_cells( sg, p, direction, lambda c, ss=seg_syms: If(Or(*[c == s for s in ss]), 1, 0) ) sg.solver.add(actual_count == expected_count) def show_cell(p, _): if p in GIVENS: if GIVENS[p][0] == I: return chr(0x25AB) if GIVENS[p][0] == O: return chr(0x25AA) return None if sg.solve(): sg.print(show_cell) print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print(show_cell) else: print("No solution")
def main(): """Araf solver example.""" min_given_value = min(GIVENS.values()) max_given_value = max(GIVENS.values()) # The grid symbols will be the region IDs from the region constrainer. sym = grilops.make_number_range_symbol_set(0, HEIGHT * WIDTH - 1) lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, sym) rc = grilops.regions.RegionConstrainer( lattice, sg.solver, min_region_size=min_given_value + 1, max_region_size=max_given_value - 1 ) for point in lattice.points: sg.solver.add(sg.cell_is(point, rc.region_id_grid[point])) # Exactly half of the givens must be region roots. As an optimization, add # constraints that the smallest givens must not be region roots, and that the # largest givens must be region roots. undetermined_given_locations = [] num_undetermined_roots = len(GIVENS) // 2 for p, v in GIVENS.items(): if v == min_given_value: sg.solver.add(Not(rc.parent_grid[p] == grilops.regions.R)) elif v == max_given_value: sg.solver.add(rc.parent_grid[p] == grilops.regions.R) num_undetermined_roots -= 1 else: undetermined_given_locations.append(p) sg.solver.add( PbEq( [ (rc.parent_grid[p] == grilops.regions.R, 1) for p in undetermined_given_locations ], num_undetermined_roots ) ) # Non-givens must not be region roots for point in lattice.points: if point not in GIVENS: sg.solver.add(Not(rc.parent_grid[point] == grilops.regions.R)) add_given_pair_constraints(sg, rc) region_id_to_label = { lattice.point_to_index(point): chr(65 + i) for i, point in enumerate(GIVENS.keys()) } def show_cell(unused_loc, region_id): return region_id_to_label[region_id] if sg.solve(): sg.print(show_cell) print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print(show_cell) else: print("No solution")
def main(): """Masyu solver example.""" lattice = PointyToppedHexagonalLattice([ givens_row_col_to_point(r, c) for r in range(len(GIVENS)) for c in range(len(GIVENS[r])) ]) sym = grilops.loops.LoopSymbolSet(lattice) sym.append("EMPTY", ". \n ") sg = grilops.SymbolGrid(lattice, sym) grilops.loops.LoopConstrainer(sg, single_loop=True) turns = [sym.NESE, sym.ESW, sym.WSE, sym.NWSW, sym.WNE, sym.ENW] for p in lattice.points: # 60-degree turns are disallowed. sg.solver.add(sg.grid[p] != sym.ESE) sg.solver.add(sg.grid[p] != sym.SESW) sg.solver.add(sg.grid[p] != sym.WSW) sg.solver.add(sg.grid[p] != sym.WNW) sg.solver.add(sg.grid[p] != sym.NENW) sg.solver.add(sg.grid[p] != sym.ENE) r, c = point_to_givens_row_col(p) if GIVENS[r][c] == B: # The loop must turn at a black circle. sg.solver.add(sg.cell_is_one_of(p, turns)) # All connected adjacent cells must contain straight loop segments. for _, d in lattice.edge_sharing_directions(): np = p.translate(d) if np in sg.grid: sg.solver.add(Implies( sg.cell_is_one_of(p, sym.symbols_for_direction(d)), sg.cell_is(np, sym.symbol_for_direction_pair(d, d.negate())) )) elif GIVENS[r][c] == W: # The loop must go straight through a white circle. sg.solver.add(sg.cell_is_one_of(p, [sym.NESW, sym.EW, sym.NWSE])) # At least one connected adjacent cell must turn. for d in [Vector(-1, 1), Vector(0, 2), Vector(1, 1)]: np1 = p.translate(d) np2 = p.translate(d.negate()) if np1 in sg.grid and np2 in sg.grid: sg.solver.add(Implies( sg.cell_is(p, sym.symbol_for_direction_pair(d, d.negate())), Or( sg.cell_is_one_of(np1, turns), sg.cell_is_one_of(np2, turns) ) )) if sg.solve(): solved_grid = sg.solved_grid() def print_function(p): return sym.symbols[solved_grid[p]].label lattice.print(print_function, " \n ") print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") solved_grid = sg.solved_grid() lattice.print(print_function, " \n ") print() else: print("No solution")
def region_solve(): """Slitherlink solver example using regions.""" sym = grilops.SymbolSet([("I", chr(0x2588)), ("O", " ")]) lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, sym) rc = grilops.regions.RegionConstrainer(lattice, solver=sg.solver, complete=False) def constrain_no_inside_diagonal(y, x): """Add constraints for diagonally touching cells. "Inside" cells may not diagonally touch each other unless they also share an adjacent cell. """ nw = sg.grid[Point(y, x)] ne = sg.grid[Point(y, x + 1)] sw = sg.grid[Point(y + 1, x)] se = sg.grid[Point(y + 1, x + 1)] sg.solver.add( Implies(And(nw == sym.I, se == sym.I), Or(ne == sym.I, sw == sym.I))) sg.solver.add( Implies(And(ne == sym.I, sw == sym.I), Or(nw == sym.I, se == sym.I))) region_id = Int("region_id") for p in lattice.points: # Each cell must be either "inside" (part of a single region) or # "outside" (not part of any region). sg.solver.add( Or(rc.region_id_grid[p] == region_id, rc.region_id_grid[p] == -1)) sg.solver.add( (sg.grid[p] == sym.I) == (rc.region_id_grid[p] == region_id)) if p not in GIVENS: continue given = GIVENS[p] neighbors = sg.edge_sharing_neighbors(p) # The number of grid edge border segments adjacent to this cell. num_grid_borders = 4 - len(neighbors) # The number of adjacent cells on the opposite side of the loop line. num_different_neighbors_terms = [(n.symbol != sg.grid[p], 1) for n in neighbors] # If this is an "inside" cell, we should count grid edge borders as loop # segments, but if this is an "outside" cell, we should not. sg.solver.add( If(sg.grid[p] == sym.I, PbEq(num_different_neighbors_terms, given - num_grid_borders), PbEq(num_different_neighbors_terms, given))) # "Inside" cells may not diagonally touch each other unless they also share # an adjacent cell. for y in range(HEIGHT - 1): for x in range(WIDTH - 1): constrain_no_inside_diagonal(y, x) if sg.solve(): sg.print() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def main(): """Masyu solver example.""" e, w, b = " ", chr(0x25e6), chr(0x2022) givens = [ [e, e, w, e, w, e, e, e, e, e], [e, e, e, e, w, e, e, e, b, e], [e, e, b, e, b, e, w, e, e, e], [e, e, e, w, e, e, w, e, e, e], [b, e, e, e, e, w, e, e, e, w], [e, e, w, e, e, e, e, w, e, e], [e, e, b, e, e, e, w, e, e, e], [w, e, e, e, b, e, e, e, e, w], [e, e, e, e, e, e, w, w, e, e], [e, e, b, e, e, e, e, e, e, b], ] for row in givens: for cell in row: sys.stdout.write(cell) print() lattice = grilops.get_rectangle_lattice(len(givens), len(givens[0])) sym = grilops.loops.LoopSymbolSet(lattice) sym.append("EMPTY", " ") sg = grilops.SymbolGrid(lattice, sym) lc = grilops.loops.LoopConstrainer(sg, single_loop=True) # Choose a non-empty cell to have loop order zero, to speed up solving. p = min(p for p in lattice.points if givens[p.y][p.x] != e) sg.solver.add(lc.loop_order_grid[p] == 0) straights = [sym.NS, sym.EW] turns = [sym.NE, sym.SE, sym.SW, sym.NW] for p in lattice.points: given = givens[p.y][p.x] if given == b: # The loop must turn at a black circle. sg.solver.add(sg.cell_is_one_of(p, turns)) # All connected adjacent cells must contain straight loop segments. for n in sg.edge_sharing_neighbors(p): if n.location.y < p.y: sg.solver.add( Implies(sg.cell_is_one_of(p, [sym.NE, sym.NW]), sg.cell_is(n.location, sym.NS))) if n.location.y > p.y: sg.solver.add( Implies(sg.cell_is_one_of(p, [sym.SE, sym.SW]), sg.cell_is(n.location, sym.NS))) if n.location.x < p.x: sg.solver.add( Implies(sg.cell_is_one_of(p, [sym.SW, sym.NW]), sg.cell_is(n.location, sym.EW))) if n.location.x > p.x: sg.solver.add( Implies(sg.cell_is_one_of(p, [sym.NE, sym.SE]), sg.cell_is(n.location, sym.EW))) elif given == w: # The loop must go straight through a white circle. sg.solver.add(sg.cell_is_one_of(p, straights)) # At least one connected adjacent cell must turn. if 0 < p.y < len(givens) - 1: sg.solver.add( Implies( sg.cell_is(p, sym.NS), Or( sg.cell_is_one_of(p.translate(Vector(-1, 0)), turns), sg.cell_is_one_of(p.translate(Vector(1, 0)), turns)))) if 0 < p.x < len(givens[0]) - 1: sg.solver.add( Implies( sg.cell_is(p, sym.EW), Or( sg.cell_is_one_of(p.translate(Vector(0, -1)), turns), sg.cell_is_one_of(p.translate(Vector(0, 1)), turns)))) if sg.solve(): sg.print() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def main(): """Heyawake solver example.""" sym = grilops.SymbolSet([("B", chr(0x2588)), ("W", " ")]) lattice = grilops.get_rectangle_lattice(HEIGHT, WIDTH) sg = grilops.SymbolGrid(lattice, sym) # Rule 1: Painted cells may never be orthogonally connected (they may not # share a side, although they can touch diagonally). for p in lattice.points: sg.solver.add( Implies( sg.cell_is(p, sym.B), And(*[n.symbol != sym.B for n in sg.edge_sharing_neighbors(p)]) ) ) # Rule 2: All white cells must be interconnected (form a single polyomino). rc = grilops.regions.RegionConstrainer( lattice, sg.solver, complete=False) white_region_id = Int("white_region_id") sg.solver.add(white_region_id >= 0) sg.solver.add(white_region_id < HEIGHT * WIDTH) for p in lattice.points: sg.solver.add( If( sg.cell_is(p, sym.W), rc.region_id_grid[p] == white_region_id, rc.region_id_grid[p] == -1 ) ) # Rule 3: A number indicates exactly how many painted cells there must be in # that particular room. region_cells = defaultdict(list) for p in lattice.points: region_cells[REGIONS[p.y][p.x]].append(sg.grid[p]) for region, count in REGION_COUNTS.items(): sg.solver.add(PbEq([(c == sym.B, 1) for c in region_cells[region]], count)) # Rule 4: A room which has no number may contain any number of painted cells, # or none. # Rule 5: Where a straight (orthogonal) line of connected white cells is # formed, it must not contain cells from more than two rooms—in other words, # any such line of white cells which connects three or more rooms is # forbidden. region_names = sorted(list(set(c for row in REGIONS for c in row))) bits = len(region_names) def set_region_bit(bv, p): i = region_names.index(REGIONS[p.y][p.x]) chunks = [] if i < bits - 1: chunks.append(Extract(bits - 1, i + 1, bv)) chunks.append(BitVecVal(1, 1)) if i > 0: chunks.append(Extract(i - 1, 0, bv)) return Concat(*chunks) for p in lattice.points: for n in sg.edge_sharing_neighbors(p): bv = reduce_cells( sg, p, n.direction, set_region_bit(BitVecVal(0, bits), p), lambda acc, c, ap: set_region_bit(acc, ap), lambda acc, c, sp: c == sym.B ) popcnt = Sum(*[BV2Int(Extract(i, i, bv)) for i in range(bits)]) sg.solver.add(Implies(sg.cell_is(p, sym.W), popcnt <= 2)) if sg.solve(): sg.print() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() else: print("No solution")
def main(): """Battleship solver example.""" sg = grilops.SymbolGrid(LATTICE, SYM) sc = grilops.shapes.ShapeConstrainer( LATTICE, [ [Vector(0, i) for i in range(4)], [Vector(0, i) for i in range(3)], [Vector(0, i) for i in range(3)], [Vector(0, i) for i in range(2)], [Vector(0, i) for i in range(2)], [Vector(0, i) for i in range(2)], [Vector(0, i) for i in range(1)], [Vector(0, i) for i in range(1)], [Vector(0, i) for i in range(1)], ], solver=sg.solver, allow_rotations=True ) # Constrain the given ship segment counts and ship segments. for y, count in enumerate(GIVENS_Y): sg.solver.add( PbEq([(Not(sg.cell_is(Point(y, x), SYM.X)), 1) for x in range(WIDTH)], count) ) for x, count in enumerate(GIVENS_X): sg.solver.add( PbEq([(Not(sg.cell_is(Point(y, x), SYM.X)), 1) for y in range(HEIGHT)], count) ) for p, s in GIVENS.items(): sg.solver.add(sg.cell_is(p, s)) for p in LATTICE.points: shape_type = sc.shape_type_grid[p] shape_id = sc.shape_instance_grid[p] touching_types = [ n.symbol for n in LATTICE.vertex_sharing_neighbors(sc.shape_type_grid, p) ] touching_ids = [ n.symbol for n in LATTICE.vertex_sharing_neighbors(sc.shape_instance_grid, p) ] # Link the X symbol to the absence of a ship segment. sg.solver.add( (sc.shape_type_grid[p] == -1) == sg.cell_is(p, SYM.X)) # Ship segments of different ships may not touch. and_terms = [] for touching_id in touching_ids: and_terms.append( Implies( shape_id >= 0, Or(touching_id == shape_id, touching_id == -1) ) ) sg.solver.add(And(*and_terms)) # Choose the correct symbol for each ship segment. touching_count_terms = [(c == shape_type, 1) for c in touching_types] sg.solver.add( Implies( And(shape_type >= 0, PbEq(touching_count_terms, 2)), sg.cell_is(p, SYM.B) ) ) sg.solver.add( Implies( And(shape_type >= 0, PbEq(touching_count_terms, 0)), sg.cell_is(p, SYM.O) ) ) for n in sg.edge_sharing_neighbors(p): sg.solver.add( Implies( And( shape_type >= 0, PbEq(touching_count_terms, 1), sc.shape_type_grid[n.location] == shape_type ), sg.cell_is(p, DIR_TO_OPPOSITE_SYM[n.direction]) ) ) if sg.solve(): sg.print() print() sc.print_shape_instances() print() if sg.is_unique(): print("Unique solution") else: print("Alternate solution") sg.print() print() sc.print_shape_instances() print() else: print("No solution")