def gen_contiguous_constraints_single(index_, count_, *, width, height, board, puzzle): at_ = lambda l, c : board[l][c] at_puzzle = lambda l, c : puzzle[l][c] def is_valid_cell(ind, current_index=index_, height=height, width=width): if ind == current_index: # redundant check at_puzzle(ind) == count_ return True if not inside_board(ind, height=height, width=width): return False # a cell is considered valid neighbour (to spawn) if it is empty # or it has the same count_ return at_puzzle(*ind) == 0 or at_puzzle(*ind) == count_ possibs = [] # possible configurations given the count_ of contiguous cells in the block blocks = get_block(index_, nb_cells=count_, is_valid_cell=is_valid_cell) for block in blocks: block_vars = [ at_(*ind) for ind in block ] # fence can be thought of as completely encircling the block fence = get_fence(enclosure=block) geom = { 'width': width, 'height': height } fence_inside_board = [ ind for ind in fence if inside_board(ind, **geom) ] fence_var = [ at_(*ind) for ind in fence_inside_board ] fenced_enclosure = And( coerce_eq(block_vars, [count_] * len(block_vars)), And([var != count_ for var in fence_var])) possibs.append(simplify(fenced_enclosure)) return Exactly(*possibs, 1) #only one configuration would prevail
def gen_consecutive_nums_constraint(l, c): val_neighs = [ neigh for neigh in neighbours(l, c) if inside_board(neigh, height=order, width=order) ] neighs = get_vars_at(val_neighs) occupied_neighs = set(neighs).intersection(occupied_cells) var = at_(l, c) #content of current cell one_adj_cell_is_consec_c = Exactly(*[ adj == var + 1 for adj in occupied_neighs ], 1) current_cell_is_max_c = var == maximum return Or(current_cell_is_max_c, one_adj_cell_is_consec_c)
def gen_coupling_constraints(l, c): val_neighs = [ neigh for neigh in neighbours((l, c)) if inside_board(neigh, height=height, width=width) ] couplings = [X[l][c] == -at_(*cell) for cell in val_neighs] # we don't add constraint that it is unoccupied by a tree # it's taken care by another set of constraints # return Exactly(*couplings, 1) return Exactly(*couplings, 1)
def gen_consecutive_nums_constraint(l, c): geom = {'height': height, 'width': width} val_neighs = [ neigh for neigh in neighbours((l, c)) if inside_board(neigh, **geom) ] neighs = get_vars_at(val_neighs) var = at_(l, c) #content of current cell one_adj_cell_is_consec_c = Exactly(*[adj == var + 1 for adj in neighs], 1) current_cell_is_max_c = var == MAX unoccupied_c = var == 0 return Or(unoccupied_c, current_cell_is_max_c, one_adj_cell_is_consec_c)
def solve_puzzle_shikaku(puzzle, *, height, width): X = IntMatrix('r', nb_rows=height, nb_cols=width) assert sum( flatten(puzzle) ) == height * width, "Sum of the areas of all rectangles must be equal to board area" at_ = lambda l, c: X[l][c] def get_vars_at(indices): return [at_(*ind) for ind in indices] def get_unique_rect_id(l, c): return l * width + c + 1 rectangle_area_c = [] for l, c in itertools.product(range(height), range(width)): rect_area = puzzle[l][c] if rect_area > 0: rect_id = get_unique_rect_id(l, c) rects = get_possible_rectangles_at((l, c), height=height, width=width, rect_area=rect_area) rectangle_configs_with_this_id = [] for rect in rects: indices = list(get_indices_in_rect(rect)) rect_vars = get_vars_at(indices) # This is like "coloring" the rectangle with id cnstrnt = coerce_eq(rect_vars, [rect_id] * len(rect_vars)) rectangle_configs_with_this_id.append(cnstrnt) rectangle_area_c.append(Exactly(*rectangle_configs_with_this_id, 1)) s = Solver() s.add(rectangle_area_c) s.check() m = s.model() return [[m[cell] for cell in row] for row in X]
def gen_contiguous_constraints_single(index_, count_, *, width, height, board): at_ = lambda l, c: board[l][c] # count_ - 1 because the current square is always counted possibs = [] #possible configurations given the count_ of white squares for distrib in distribute_in_4_directions(count_ - 1): strict_boundaries = boundaries(index_, spawn_direction=distrib) geom = {'width': width, 'height': height} can_spawn = all( (inside_board(ind, **geom) for ind in strict_boundaries)) if can_spawn: enclosure = get_enclosed_space(index_, spawn_direction=distrib) encloure_vars = [at_(*ind) for ind in enclosure] direction_plus_one = tuple(d + 1 for d in distrib) # walls can be thought of as surrounding boundaries walls = boundaries(index_, spawn_direction=direction_plus_one) walls_within_board = [ ind for ind in walls if inside_board(ind, **geom) ] walls_var = [at_(*ind) for ind in walls_within_board] walled_enclosure = And( coerce_eq(encloure_vars, [WHITE] * len(encloure_vars)), coerce_eq(walls_var, [BLACK] * len(walls_var))) possibs.append(simplify(walled_enclosure)) return Exactly(*possibs, 1) #only one configuration would prevail
def gen_count_lightbulb_constraint(l, c, count_lightbulbs): neigh_as_light_bulb = [ at_(*cell) == LIGHT_BULB for cell in valid_neighbours(l, c) ] return Exactly(*neigh_as_light_bulb, count_lightbulbs)
def solve_akari(puzzle, *, height, width): X = IntMatrix('n', nb_rows=height, nb_cols=width) cell_vars = flatten(X) clues = flatten(puzzle) assert len(cell_vars) == len( clues), "Discrepancy between puzzle and cell-variables" complete_c = [] for cell_var, clue in zip(cell_vars, clues): if clue == WALL or clue in (0, 1, 2, 3, 4): complete_c.append(cell_var == WALL) else: complete_c.append( Xor(cell_var == LIGHT_BULB, cell_var == NOT_LIGHT_BULB)) geom = {'width': width, 'height': height} at_ = lambda l, c: X[l][c] def valid_neighbours(l, c): return [ neigh for neigh in neighbours((l, c)) if inside_board(neigh, **geom) ] def gen_count_lightbulb_constraint(l, c, count_lightbulbs): neigh_as_light_bulb = [ at_(*cell) == LIGHT_BULB for cell in valid_neighbours(l, c) ] return Exactly(*neigh_as_light_bulb, count_lightbulbs) count_surrounding_light_bulbs_c = [] for l, c in itertools.product(range(height), range(width)): clue = puzzle[l][c] if clue in (0, 1, 2, 3, 4): cnstr = gen_count_lightbulb_constraint(l, c, count_lightbulbs=clue) count_surrounding_light_bulbs_c.append(cnstr) def get_vars_at(indices): return [at_(*ind) for ind in indices] # think of this as vectorialization of 'value' def vars_eq_scalar(variabs, value): return [var == value for var in variabs] horizontal_cages_indexed = get_horizontal_cages(puzzle) vertical_cages_indexed = get_vertical_cages(puzzle) horizontal_cages = set( tuple(sorted(cage)) for cage in horizontal_cages_indexed.values()) vertical_cages = set( tuple(sorted(cage)) for cage in vertical_cages_indexed.values()) # two lightbulbs can't illuminate each other. territory? forbid encroachment? # there can be at most one bulb in each horizontal cage _h_no_encroachment_c = [] for hcage in horizontal_cages: light_bulbs_in_cage = vars_eq_scalar(get_vars_at(hcage), LIGHT_BULB) cnstrnt = AtMost(*light_bulbs_in_cage, 1) _h_no_encroachment_c.append(cnstrnt) _v_no_encroachment_c = [] for vcage in vertical_cages: light_bulbs_in_cage = vars_eq_scalar(get_vars_at(vcage), LIGHT_BULB) cnstrnt = AtMost(*light_bulbs_in_cage, 1) _v_no_encroachment_c.append(cnstrnt) no_encroachment_c = _h_no_encroachment_c + _v_no_encroachment_c assert sorted(tuple(vertical_cages_indexed.keys())) == sorted( tuple(horizontal_cages_indexed.keys()) ), "verical cages and horizontal cages must have same keys" all_empty_cells_illuminated_c = [] for coord in vertical_cages_indexed.keys(): vcage = vertical_cages_indexed[coord] hcage = horizontal_cages_indexed[coord] horiz_cage_has_lightbulb = Exactly( *vars_eq_scalar(get_vars_at(hcage), LIGHT_BULB), 1) vertic_cage_has_lightbulb = Exactly( *vars_eq_scalar(get_vars_at(vcage), LIGHT_BULB), 1) cnstrnt = Or(horiz_cage_has_lightbulb, vertic_cage_has_lightbulb) all_empty_cells_illuminated_c.append(cnstrnt) s = Solver() s.add(complete_c + count_surrounding_light_bulbs_c + no_encroachment_c + all_empty_cells_illuminated_c) s.check() m = s.model() return [[m[cell] for cell in row] for row in X]
def solve_tracks(*, tracks, start_index, end_index, horizontal_clues, vertical_clues, height, width): X = IntMatrix('t', nb_rows=height, nb_cols=width) assert sum(horizontal_clues) == sum(vertical_clues) nb_occupied_cells = sum(horizontal_clues) MAX = nb_occupied_cells cells = list(itertools.chain(*X)) range_c = [And(n >= 0, n <= nb_occupied_cells) for n in cells] at_ = lambda l, c: X[l][c] extremities_c = [at_(*start_index) == 1, at_(*end_index) == MAX] for index_, pattern in tracks: l, c = index_ bottom, left, top, right = map(int, list(pattern)) bottom, left, top, right = bottom == 1, left == 1, top == 1, right == 1 if index_ == start_index: # the starting track is the leftmost # so it has no left if bottom: nxt = l + 1, c elif top: nxt = l - 1, c elif right: nxt = l, c + 1 curr_var = at_(*index_) nxt_var = at_(*nxt) extremities_c.append(nxt_var == curr_var + 1) elif index_ == end_index: # the ending track is the bottommost # so it has no bottom if top: prv = l - 1, c if right: prv = l, c + 1 if left: prv = l, c - 1 curr_var = at_(*index_) prv_var = at_(*prv) extremities_c.append(curr_var == prv_var + 1) X_trans = transpose(X) # Exactly count_ occupied (> 0) cells in each row row_sums_c = [ Exactly(*[cell > 0 for cell in row], count_) for row, count_ in zip(X, vertical_clues) ] row_sums_c = [ Exactly(*[cell > 0 for cell in row], count_) for row, count_ in zip(X, vertical_clues) ] col_sums_c = [ Exactly(*[cell > 0 for cell in row], count_) for row, count_ in zip(X_trans, horizontal_clues) ] # we give unique_id to unoccupied cell. for using Distinct on occupied cells distinct_c = Distinct( [If(cell > 0, cell, -i) for i, cell in enumerate(cells, 1)]) # Every occupied cell is at a given 'distance' to the start. # No other cell has the same distance distinct_c = [distinct_c] def get_adj_track_indices(index_, pattern): l, c = index_ bottom, left, top, right = map(int, list(pattern)) bottom, left, top, right = bottom == 1, left == 1, top == 1, right == 1 res = [] if left: # the starting track is the leftmost if index_ != start_index: res.append((l, c - 1)) if right: res.append((l, c + 1)) if top: res.append((l - 1, c)) if bottom: # the ending track is the bottommost if index_ != end_index: res.append((l + 1, c)) res.insert(1, index_) return res at_ = lambda l, c: X[l][c] def get_vars_at(indices): return [at_(*ind) for ind in indices] def coerce_sequential(vars_): curr, *succs = vars_ ascending_c = And([nxt == curr + i for i, nxt in enumerate(succs, 1)]) descending_c = And([nxt == curr - i for i, nxt in enumerate(succs, 1)]) return Or(ascending_c, descending_c) # constraint forcing parts of the tracks to be in ascending or descending order sequential_c = [] for index_, pattern in tracks: inds = get_adj_track_indices(index_, pattern) successive_track_vars = get_vars_at(inds) cnstrnt = coerce_sequential(successive_track_vars) sequential_c.append(cnstrnt) # The start and end track produces an absurd condition. but it would be weeded out by other constraints. def gen_consecutive_nums_constraint(l, c): geom = {'height': height, 'width': width} val_neighs = [ neigh for neigh in neighbours((l, c)) if inside_board(neigh, **geom) ] neighs = get_vars_at(val_neighs) var = at_(l, c) #content of current cell one_adj_cell_is_consec_c = Exactly(*[adj == var + 1 for adj in neighs], 1) current_cell_is_max_c = var == MAX unoccupied_c = var == 0 return Or(unoccupied_c, current_cell_is_max_c, one_adj_cell_is_consec_c) # constraint forcing one of the adjacent cells to be the 'successor' successor_c = [ gen_consecutive_nums_constraint(l, c) for l, c in itertools.product(range(height), range(width)) ] s = Solver() s.add(range_c + extremities_c + row_sums_c + col_sums_c + distinct_c + sequential_c + successor_c) s.check() m = s.model() res = [[m[s] for s in row] for row in X] return res
def coerce_tile(edge_1, edge_2): return Exactly(And(edge_1 == POSITIVE, edge_2 == NEGATIVE), And(edge_1 == NEGATIVE, edge_2 == POSITIVE), And(edge_1 == NEUTRAL, edge_2 == NEUTRAL), 1)
def coerce_charge(edges, polarity, count_): edges_with_given_polarity = [edge == polarity for edge in edges] return Exactly(*edges_with_given_polarity, count_)
def solve_puzzle_dominosa(puzzle, *, height, width, order): board = IntMatrix('d', nb_rows=height, nb_cols=width) complete_c = [ Exactly( cell == VERTIC_START, cell == VERTIC_END, cell == HORIZ_START, cell == HORIZ_END, 1) for cell in flatten(board) ] _no_aberrant_horiz_c = [] for row in board: for domino in pairwise(row): # not-head and tail abberrant_1 = And(domino[0] != HORIZ_START, domino[1] == HORIZ_END) # head and not-tail _no_aberrant_horiz_c.append(Not(abberrant_1)) abberrant_2 = And(domino[0] == HORIZ_START, domino[1] != HORIZ_END) _no_aberrant_horiz_c.append(Not(abberrant_2)) _no_aberrant_vertic_c = [] for row in transpose(board): for domino in pairwise(row): # not-head and tail abberrant_1 = And(domino[0] != VERTIC_START, domino[1] == VERTIC_END) _no_aberrant_vertic_c.append(Not(abberrant_1)) # head and not-tail abberrant_2 = And(domino[0] == VERTIC_START, domino[1] != VERTIC_END) _no_aberrant_vertic_c.append(Not(abberrant_2)) _no_aberrant_horiz_border_cell_c = [And(row[0] != HORIZ_END, row[-1] != HORIZ_START) for row in board] _no_aberrant_vertic_border_cell_c = [And(row[0] != VERTIC_END, row[-1] != VERTIC_START) for row in transpose(board)] no_aberrant_c = ( _no_aberrant_horiz_c + _no_aberrant_horiz_border_cell_c + _no_aberrant_vertic_c + _no_aberrant_vertic_border_cell_c ) # By taking both the start_variable and end_variable we may not # need no_aberrant_c ... but it's better separated this way. unique_c = [] locs_h = defaultdict(list) #locations for var_row, row in zip(board, puzzle): for var, dom in zip(var_row, pairwise(row)): locs_h[normalize_domino(dom)].append(var) locs_v = defaultdict(list) for var_row, row in zip(transpose(board), transpose(puzzle)): for var, dom in zip(var_row, pairwise(row)): locs_v[normalize_domino(dom)].append(var) # normalized domino for n_domino in itertools.combinations_with_replacement(range(order + 1), 2): if n_domino in locs_h and n_domino in locs_v: only_one_such_domino = Exactly(*[edge == VERTIC_START for edge in locs_v[n_domino]], *[edge == HORIZ_START for edge in locs_h[n_domino]], 1) unique_c.append(only_one_such_domino) elif n_domino not in locs_h: only_one_such_domino = Exactly(*[edge == VERTIC_START for edge in locs_v[n_domino]], 1) unique_c.append(only_one_such_domino) elif n_domino not in locs_v: only_one_such_domino = Exactly(*[edge == HORIZ_START for edge in locs_h[n_domino]], 1) unique_c.append(only_one_such_domino) s = Solver() s.add(complete_c + no_aberrant_c + unique_c) s.check() m = s.model() return [ [m[cell] for cell in row] for row in board]
def constrain_towers(tower_vars, tower_height, knowl): possiblts = knowl[tower_height] all_possiblts = [coerce_eq(tower_vars, possib) for possib in possiblts] # Exactly one possibility among all would satisfy. return Exactly(*all_possiblts, 1)
def coerce_nb_tents(cells, count_): tents = [cell < 0 for cell in cells] return Exactly(*tents, count_)
def gen_constraints_vars_runs(vars_, runs): possibs = [ coerce_eq(vars_, pat) for pat in get_patterns_given_runs(runs, len(vars_)) ] return Exactly(*possibs, 1)