def constrain_sea(sym, sg, rc): """Add constraints to the sea cells.""" # There must be only one sea, containing all black cells. sea_id = Int("sea-id") sg.solver.add(sea_id >= 0) sg.solver.add(sea_id < HEIGHT * WIDTH) for p in GIVENS: sg.solver.add(sea_id != sg.lattice.point_to_index(p)) for y in range(HEIGHT): for x in range(WIDTH): p = Point(y, x) sg.solver.add( Implies(sg.cell_is(p, sym.B), rc.region_id_grid[p] == sea_id)) sg.solver.add( Implies(sg.cell_is(p, sym.W), rc.region_id_grid[p] != sea_id)) # The sea is not allowed to contain 2x2 areas of black cells. 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])))
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(): """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(): """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 test_covers_point(self): """Unittest for ExpressionQuadTree.covers_point.""" with self.assertRaises(ValueError): ExpressionQuadTree([]) points = [Point(y, x) for y in range(4) for x in range(4)] t = ExpressionQuadTree(points) for p in points: self.assertTrue(t.covers_point(p)) self.assertFalse(t.covers_point(Point(4, 0))) self.assertFalse(t.covers_point(Point(0, 4))) self.assertFalse(t.covers_point(Point(-1, 0))) self.assertFalse(t.covers_point(Point(0, -1)))
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 test_get_point_expr(self): """Unittest for ExpressionQuadTree.get_point_expr.""" points = [Point(y, x) for y in range(2) for x in range(2)] t = ExpressionQuadTree(points) y, x = Int("y"), Int("x") t.add_expr("test", lambda p: And(p.y == y, p.x == x)) for p in points: expr = t.get_point_expr("test", p) self.assertEqual(expr, And(p.y == y, p.x == x)) with self.assertRaises(ValueError): t.get_point_expr("test", Point(3, 3))
def add_sudoku_constraints(sg): """Add constraints for the normal Sudoku rules.""" 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))
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 add_nurikabe_constraints(sym, sg, rc): """Add the nurikabe constraints (one connected sea with no 2x2 regions).""" # There must be only one sea, containing all black cells. sea_id = Int("sea-id") for p in sg.lattice.points: sg.solver.add(sg.cell_is(p, sym.W) == (rc.region_id_grid[p] != sea_id)) # Constrain sea_id to be the index of one of the points in # the smallest area, among those areas of size greater than 4. area_to_points = defaultdict(list) for p in sg.lattice.points: area_to_points[AREAS[p.y][p.x]].append(p) area_points = min((ps for ps in area_to_points.values() if len(ps) > 4), key=len) sg.solver.add( Or(*[sea_id == sg.lattice.point_to_index(p) for p in area_points])) # The sea is not allowed to contain 2x2 areas of black cells. 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(*[Not(cell == sym.W) for cell in pool_cells])))
def constrain_islands(sym, sg, rc): """Add constraints to the island cells.""" # Each numbered cell is an island cell. The number in it is the number of # cells in that island. Each island must contain exactly one numbered cell. for y in range(HEIGHT): for x in range(WIDTH): p = Point(y, x) if (y, x) in GIVENS: sg.solver.add(sg.cell_is(p, sym.W)) # Might as well force the given cell to be the root of the region's tree, # to reduce the number of possibilities. sg.solver.add(rc.parent_grid[p] == grilops.regions.R) sg.solver.add(rc.region_size_grid[p] == GIVENS[(y, x)]) else: # Ensure that cells that are part of island regions are colored white. for gp in GIVENS: island_id = sg.lattice.point_to_index(gp) sg.solver.add( Implies(rc.region_id_grid[p] == island_id, sg.cell_is(p, sym.W))) # Force a non-given white cell to not be the root of the region's tree, # to reduce the number of possibilities. sg.solver.add( Implies(sg.cell_is(p, sym.W), rc.parent_grid[p] != grilops.regions.R))
def parse_url(url: str) -> PuzzleGivens: params = url.split('/') height = int(params[-2]) width = int(params[-3]) payload = params[-1] genre = params[-4].split('?')[-1] if genre not in GENRE_ALIASES: raise ValueError('Given URL is not a masyu') circles: PuzzleGivens = defaultdict(int) cell_num = 0 payload_len = min(int((height * width + 2) / 3), len(payload)) for c in payload[:payload_len]: n = int(c, 27) for t in [3**p for p in range(2, -1, -1)]: v = int(n / t) % 3 if v > 0: y = int(cell_num / width) x = cell_num % width circles[Point(y, x)] = v cell_num += 1 return circles
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)))
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(): """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 test_get_exprs(self): """Unittest for ExpressionQuadTree.get_exprs.""" t = ExpressionQuadTree( [Point(y, x) for y in range(2) for x in range(2)]) t.add_expr("test", lambda p: p.y == 0) exprs = list(t.get_exprs("test")) self.assertEqual(len(exprs), 4) self.assertEqual(exprs.count(False), 2) self.assertEqual(exprs.count(True), 2)
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 is_outside(grid, model, y, x): """Returns whether the given point is effectively outside the loop. Returns 1 if the given point is not in the grid or outside the loop. Returns 0 if the given point is in the grid and inside the loop. """ p = Point(y, x) if p in grid: return model.eval(grid[p]).as_long() return 1
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 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 constrain_adjacent_cells(sg, rc): """Different regions of the same color may not be orthogonally adjacent.""" for y in range(HEIGHT): for x in range(WIDTH): p = Point(y, x) adjacent_cells = [n.symbol for n in sg.edge_sharing_neighbors(p)] adjacent_region_ids = [ n.symbol for n in sg.lattice.edge_sharing_neighbors( rc.region_id_grid, p) ] for cell, region_id in zip(adjacent_cells, adjacent_region_ids): sg.solver.add( Implies(sg.grid[p] == cell, rc.region_id_grid[p] == region_id))
def parse_url(url: str) -> PuzzleGivens: params = url.split('/') height = int(params[-2]) width = int(params[-3]) payload = params[-1] genre = params[-4].split('?')[-1] if genre not in GENRE_ALIASES: raise ValueError('Given URL is not a nurikabe') givens: PuzzleGivens = {} cell_num = 0 def parse_char(payload: str, i: int) -> Tuple[int, int]: c = payload[i] if "0" <= c <= "9" or "a" <= c <= "f": return (int(c, 16), 1) elif c == "-": return (int(payload[i + 1:i + 3], 16), 3) elif c == "+": return (int(payload[i + 1:i + 4], 16), 4) elif c == "=": return (int(payload[i + 1:i + 4], 16) + 4096, 4) elif c == "%": return (int(payload[i + 1:i + 4], 16) + 8192, 4) elif c == ".": return (-2, 1) return (-1, 0) idx = 0 while idx < len(payload) and cell_num < height * width: c = payload[idx] v, step = parse_char(payload, idx) if v != -1: y = int(cell_num / width) x = cell_num % width givens[Point(y, x)] = v idx += step cell_num += 1 elif "g" <= c <= "z": cell_num += int(c, 36) - 15 idx += 1 else: idx += 1 return givens
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 extract_instruction(solved_path): """Extract the instruction phrase from the solved path and letter grid.""" offset_to_directions = { Vector(0, 0): "ES", Vector(0, 1): "SW", Vector(1, 0): "NE", Vector(1, 1): "NW", } extract = "" for y in range(len(letter_grid)): for x in range(len(letter_grid[0])): ok = True for v, ds in offset_to_directions.items(): symbol_name = sym.symbols[solved_path[Point(y, x).translate(v)]].name if not all(d in symbol_name for d in ds): ok = False break if ok: extract += letter_grid[y][x] print(extract) print()
def parse_url(url: str) -> PuzzleGivens: params = url.split('/') width = int(params[-3]) payload = params[-1] genre = params[-4].split('?')[-1] if genre not in GENRE_ALIASES: raise ValueError('Given URL is not a simpleloop') shaded: PuzzleGivens = defaultdict(bool) cell_num = 0 for c in payload: n = int(c, 32) for b in [2**p for p in range(4, -1, -1)]: if n & b: y = int(cell_num / width) x = cell_num % width shaded[Point(y, x)] = True cell_num += 1 return shaded
def test_get_other_points_expr(self): """Unittest for ExpressionQuadTree.get_other_points_expr.""" t = ExpressionQuadTree( [Point(y, x) for y in range(2) for x in range(2)]) y = Int("y") t.add_expr("test", lambda p: p.y == y) expr = t.get_other_points_expr( "test", [Point(0, 1), Point(1, 0), Point(1, 1)]) self.assertEqual(simplify(expr), y == 0) expr = t.get_other_points_expr("test", [Point(1, 0), Point(1, 1)]) self.assertEqual(simplify(expr), y == 0) expr = t.get_other_points_expr("test", []) self.assertEqual(simplify(expr), And(y == 0, y == 1))
def add_neighbor_constraints(sg, y, x): """Add constraints for the given clue at (y, x).""" neighbor_locations = make_neighbor_locations(y, x) # Find all possible ways that neighboring cells could be shaded to match the # given clue. If we have 8 neighboring cells, we need to treat them as a # continuous ring; if we do not have 8 neighboring cells, we can assume that # the beginning and end cells of the neighbor location sequence are not # adjacent. Use set() to dedupe these possibilities. given_run_lengths = GIVENS[(y, x)] neighbor_patterns = set( place_runs([SYM.W] * len(neighbor_locations), given_run_lengths, len(neighbor_locations) == 8)) # Align the neighboring cell locations with the possible patterns with which # the neighboring cells may be filled in, and add constraints for each # possible pattern. or_terms = [] for pattern in neighbor_patterns: and_terms = [] for (ny, nx), symbol in zip(neighbor_locations, pattern): and_terms.append(sg.cell_is(Point(ny, nx), symbol)) or_terms.append(And(*and_terms)) sg.solver.add(Or(*or_terms))
def areas_row_col_to_point(r, c): """Converts a row and column in AREAS to a point.""" y = r + 1 num_givens = len(AREAS[r]) x = 2 * c + 1 - num_givens return Point(y, x)
"""Araf solver example. Example puzzle can be found at https://www.gmpuzzles.com/blog/araf-rules-and-info/. """ from z3 import And, Implies, Not, PbEq import grilops import grilops.regions from grilops.geometry import Point HEIGHT, WIDTH = 7, 7 GIVENS = { Point(0, 0): 1, Point(0, 1): 9, Point(0, 4): 8, Point(1, 6): 8, Point(2, 1): 7, Point(2, 4): 9, Point(2, 6): 7, Point(3, 0): 10, Point(3, 3): 1, Point(4, 0): 3, Point(4, 2): 8, Point(4, 6): 6, Point(5, 0): 2, Point(6, 1): 1, Point(6, 5): 1, Point(6, 6): 3, }