示例#1
0
def solve_puzzle_mosaic(counts, *, height, width):
    board = IntMatrix('b', nb_rows=height, nb_cols=width)
    at_ = lambda l, c: board[l][c]

    # There is no empty cell:
    complete_c = [Xor(cell == WHITE, cell == BLACK) for cell in flatten(board)]

    def valid_cells_in_disk(l, c):
        geom = {'height': height, 'width': width}
        cells = cells_in_disk((l, c), radius=1)
        return [cell for cell in cells if inside_board(cell, **geom)]

    def gen_sing_count_const(l, c):
        cell_vars = [at_(*cell) for cell in valid_cells_in_disk(l, c)]
        # If we don't use sum we may express the constraint with `Exactly()`
        return Sum(cell_vars) == counts[l][c]

    count_c = [
        gen_sing_count_const(l, c)
        for l, c in itertools.product(range(height), range(width))
        if counts[l][c] > 0
    ]

    s = Solver()
    s.add(complete_c + count_c)

    s.check()
    m = s.model()
    return [[m[cell] for cell in row] for row in board]
def solve_killer_sudoku(puzzle, *, sums):
    X = IntMatrix('n', ORDER, ORDER)

    at_ = lambda l, c: X[l][c]

    def get_vars_at(indices):
        return [at_(*ind) for ind in indices]

    latin_c = gen_latin_square_constraints(X, ORDER)

    nonet_c = []
    nonets_inds = get_same_block_indices(NONETS_IDS)
    for board_indices in nonets_inds.values():
        vars_ = get_vars_at(board_indices)
        nonet_c.append(Distinct(vars_))

    #sum of distinct numbers
    cage_c = []
    cages_inds = get_same_block_indices(puzzle)
    for sums_ind, cage_indices in cages_inds.items():
        vars_ = get_vars_at(cage_indices)
        cstrnt = And(Distinct(vars_), Sum(vars_) == sums[sums_ind])
        cage_c.append(cstrnt)

    s = Solver()
    s.add(latin_c + nonet_c + cage_c)
    s.check()

    m = s.model()
    return [[m[cell] for cell in row] for row in X]
def solve_hidato(puzzle, *, order, maximum):
    X = IntMatrix('n', order, order)

    vars_ = list(itertools.chain(*X))
    vals = list(itertools.chain(*puzzle))

    # occupied cells == cells belonging to the solution
    # filled cells == cells already filled
    # a filled cell is a occupied cell.
    # An occupied cell would be a filled cell at the end of the solution

    # occup == -1 for holes, and unoccupied cells
    occupied_cells = [cell for cell, occup in zip(vars_, vals) if occup > -1]

    range_c = [And(cell >= 1, cell <= maximum) for cell in occupied_cells]

    distinct_c = [Distinct(occupied_cells)]

    # cell == 0 for cells not yet filled
    # So cell > 0 contains cells already filled
    instance_c = [cell == val for cell, val in zip(vars_, vals) if val > 0]

    at_ = lambda l, c: X[l][c]

    def get_vars_at(indices):
        return [at_(*ind) for ind in indices]

    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)

    consecutive_c = []

    for l, row_vals in enumerate(puzzle):
        for c, val in enumerate(row_vals):
            if val >= 0:
                constrnt = gen_consecutive_nums_constraint(l, c)
                consecutive_c.append(constrnt)

    s = Solver()
    s.add(range_c + instance_c + distinct_c + consecutive_c)

    s.check()
    m = s.model()
    return [[m[cell] for cell in row] for row in X]
def solve_kakuro(horiz_cages, vertic_cages, horiz_clues, vertic_clues, *,
                 order):
    X = IntMatrix('n', order, order)

    at_ = lambda l, c: X[l][c]

    def get_vars_at(indices):
        return [at_(*ind) for ind in indices]

    # Look for any discrepancies between horiz and vertic occupancies.
    for h_row, v_row in itertools.zip_longest(horiz_cages, vertic_cages):
        for l, c in itertools.zip_longest(h_row, v_row):
            if l < 0 or c < 0:
                assert (l, c) == (-1, -1)

    vars_ = itertools.chain(*X)
    # We have already verified that horiz_cages and vertic_cages have same occupancies
    occupancy = itertools.chain(*horiz_cages)
    # digit
    range_c = [
        And(cell >= 1, cell <= 9) for cell, occup in zip(vars_, occupancy)
        if occup > -1
    ]

    h_cage_c = []
    h_cages_inds = get_same_block_indices(horiz_cages)
    for h_clue_ind, h_cage_indices in h_cages_inds.items():
        if h_clue_ind > -1:  # cell is occupied
            vars_ = get_vars_at(h_cage_indices)
            cnstrnt = And(Distinct(vars_),
                          Sum(vars_) == horiz_clues[h_clue_ind])
            h_cage_c.append(cnstrnt)

    v_cage_c = []
    v_cages_inds = get_same_block_indices(vertic_cages)
    for v_clue_ind, v_cage_indices in v_cages_inds.items():
        if v_clue_ind > -1:  # cell is occupied
            vars_ = get_vars_at(v_cage_indices)
            cnstrnt = And(Distinct(vars_),
                          Sum(vars_) == vertic_clues[v_clue_ind])
            v_cage_c.append(cnstrnt)

    s = Solver()
    s.add(range_c + h_cage_c + v_cage_c)

    s.check()
    m = s.model()

    res = [[m[s] for s in row] for row in X]
    return res
示例#5
0
def solve_pattern(runs_columnwise, runs_rowwise, height, width):
    X = IntMatrix('c', nb_rows=height, nb_cols=width)

    assert len(runs_rowwise) == len(X) == height
    rowwise_c = [ gen_constraints_vars_runs(row, runs)
            for row, runs in zip(X, runs_rowwise)]

    X_trans = transpose(X)

    assert len(runs_columnwise) == len(X_trans) == width
    colwise_c = [ gen_constraints_vars_runs(row, runs)
            for row, runs in zip(X_trans, runs_columnwise)]

    s = Solver()
    s.add( rowwise_c + colwise_c )
    s.check()
    m = s.model()
    return [ [ m[cell] for cell in row] for row in X ]
def solve_puzzle_takuzu(puzzle, *, height, width):
    board = IntMatrix('b', nb_rows=height, nb_cols=width)

    assert width % 2 == 0, 'Width must be pair'
    assert height % 2 == 0, 'Height must be pair'

    vars_ = flatten(board)
    vals = flatten(puzzle)
    instance_c = [var == val for var, val in zip(vars_, vals) if val > -1]

    complete_c = [Xor(cell == 0, cell == 1) for cell in flatten(board)]

    # Equal number of 1s and 0s: so there are n/2 1s and n/2 0s
    _horiz_count_c = [Sum(row) == width / 2 for row in board]
    _vertical_count_c = [Sum(row) == height / 2 for row in transpose(board)]
    count_c = _horiz_count_c + _vertical_count_c

    # Rows or columns are unique
    def get_id(sequence):
        base = 2
        return Sum([val * base**pos for pos, val in enumerate(sequence)])

    row_unique_c = [Distinct([get_id(row) for row in board])]
    col_unique_c = [Distinct([get_id(row) for row in transpose(board)])]

    # No more than 2 adjacent 1s or 0s
    def gen_adj_constraints(sequence):
        adjs = windowed(sequence, 3)
        # if three adjacent boxes are 0(resp 1) , sum is 0(resp 3).
        # Among the eight possibilities of three adjacent boxes
        return [And(Sum(adj) != 0, Sum(adj) != 3) for adj in adjs]

    _row_adj_c = flatten([gen_adj_constraints(row) for row in board])
    _col_adj_c = flatten(
        [gen_adj_constraints(row) for row in transpose(board)])
    adj_c = _row_adj_c + _col_adj_c

    s = Solver()
    s.add(instance_c + complete_c + count_c + row_unique_c + col_unique_c +
          adj_c)

    s.check()
    m = s.model()
    return [[m[cell] for cell in row] for row in board]
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 solve_unequal(puzzle, *, order, lt_inequalities):
    X = IntMatrix('n', order, order)

    latin_c = gen_latin_square_constraints(X, order)

    vars_ = flatten(X)
    vals = flatten(puzzle)
    instance_c = [var == val for var, val in zip(vars_, vals) if val > 0]

    at_ = lambda l, c: X[l][c]
    lesser_than_c = [at_(*lhs) < at_(*rhs) for lhs, rhs in lt_inequalities]

    s = Solver()
    s.add(latin_c + instance_c + lesser_than_c)

    s.check()
    m = s.model()

    res = [[m[s] for s in row] for row in X]
    return res
示例#9
0
def solve_tower_puzzle(n, top, left, right, bottom, instance=None):
    knowl = gen_knowl_dict(n)
    X = IntMatrix('h', n, n)
    X_trans = transpose(X)

    assert len(top) == len(left) == n
    assert len(right) == len(bottom) == n

    latin_c = gen_latin_square_constraints(X, n)

    left_c = [
        constrain_towers(row, h, knowl) for row, h in zip(X, left) if h > 0
    ]
    right_c = [
        constrain_towers(row[::-1], h, knowl) for row, h in zip(X, right)
        if h > 0
    ]

    top_c = [
        constrain_towers(row, h, knowl) for row, h in zip(X_trans, top)
        if h > 0
    ]
    bottom_c = [
        constrain_towers(row[::-1], h, knowl)
        for row, h in zip(X_trans, bottom) if h > 0
    ]

    s = Solver()
    s.add(latin_c + left_c + right_c + top_c + bottom_c)

    if instance is not None:
        for row_v, row in zip(X, instance):
            for var, value in zip(row_v, row):
                if value > 0:
                    s.add(var == value)

    s.check()
    m = s.model()

    res = [[m[s] for s in row] for row in X]
    return res
def solve_puzzle_range(puzzle, *, height, width):
    board = IntMatrix('b', nb_rows=height, nb_cols=width)
    pars = {'board': board, 'width': width, 'height': height}

    contig_c = gen_contiguous_constraints(puzzle, **pars)

    # There is no empty cell:
    complete_c = [Xor(cell == WHITE, cell == BLACK) for cell in flatten(board)]

    # No two black squares are adjacent: horizontal
    horiz_bl_c = [[
        And(cell_1 == BLACK, cell_2 == BLACK)
        for cell_1, cell_2 in zip(row, row[1:])
    ] for row in board]
    horiz_bl_c = flatten(horiz_bl_c)

    horiz_bl_c = AtMost(*horiz_bl_c, 0)

    # No two black squares are adjacent: vertical
    vertic_bl_c = [[
        And(cell_1 == BLACK, cell_2 == BLACK)
        for cell_1, cell_2 in zip(row, row[1:])
    ] for row in transpose(board)]
    vertic_bl_c = flatten(vertic_bl_c)

    vertic_bl_c = AtMost(*vertic_bl_c, 0)

    # No two black squares are adjancent: vertically or horizontall (ortho)
    ortho_bl_c = [horiz_bl_c, vertic_bl_c]

    s = Solver()

    s.add(contig_c + complete_c + ortho_bl_c)

    s.check()
    m = s.model()
    return [[m[cell] for cell in row] for row in board]
示例#11
0
def solve_keen(puzzle, *, order, arithmetic_constraints):
    X = IntMatrix('n', order, order)

    at_ = lambda l, c: X[l][c]

    def get_vars_at(indices):
        return [at_(*ind) for ind in indices]

    arith_c = []
    blocks_inds = get_same_block_indices(puzzle)
    for constr_ind, board_indices in blocks_inds.items():
        op, result = arithmetic_constraints[constr_ind]
        vars_ = get_vars_at(board_indices)
        if op == 'm':
            arith_c.append(Product(vars_) == result)
        elif op == 'a':
            arith_c.append(Sum(vars_) == result)
        elif op == 's':
            assert len(vars_) == 2, 'Subtraction needs exactly two operands'
            a, b = vars_
            arith_c.append(Or(a - b == result, b - a == result))
        elif op == 'd':
            assert len(vars_) == 2, 'Division needs exactly two operands'
            a, b = vars_
            arith_c.append(Or(a / b == result, b / a == result))

    latin_c = gen_latin_square_constraints(X, order)

    s = Solver()
    s.add(latin_c + arith_c)

    s.check()
    m = s.model()

    res = [[m[s] for s in row] for row in X]
    return res
示例#12
0
def solve_puzzle_range(puzzle, *, height, width):
    board = IntMatrix('b', nb_rows=height, nb_cols=width)
    pars = {'board': board, 'width': width, 'height': height}

    vars_ = flatten(board)
    vals = flatten(puzzle)
    instance_c = [var == val for var, val in zip(vars_, vals)
            if val > 0]
    contig_c = gen_contiguous_constraints(puzzle, **pars)

    # There is no empty cell: And cell values range from 1 to height*width
    complete_c = [ And(0 < cell, cell < height * width + 1)
            for cell in flatten(board) ]

    # The model wants the numbers outside the block to be different than
    # the number (count) in the block. But to resolve this constraint it
    # uses big numbers 32, 211 ... until the max we've put.
    # It doesn't try 1 there.
    # So it exhausts (if we have two 1) 221 * 221 = 48841 possbilities.
    # We want to avoid that by saying Except for 1 all other counts have at least
    # one neighbour that is equal to it.
    # We have dodged the problem by using an optimizer (minimize cost).
    # But it is better to encode and incoroporate this constraint in the model.
    geom = { 'width': width, 'height': height }
    at_ = lambda l, c : board[l][c]
    def valid_neighbours(l, c):
        return [ neigh for neigh in neighbours((l, c))
                if inside_board(neigh, **geom) ]
    def at_least_one_neighbour_is_same(l, c):
        return Or([ at_(l, c) == at_(*neigh)
                for neigh in valid_neighbours(l, c) ])
    ortho_neigh_c = [ Xor(
        board[l][c] == 1,
        at_least_one_neighbour_is_same(l, c))
        for l, c in itertools.product(range(height), range(width)) ]

    # s = Solver()
    # s.add(complete_c + contig_c + instance_c + ortho_neigh_c)
    # s.check()
    # m = s.model()
    # solution_model = [ [ m[cell].as_long() for cell in row] for row in board ]
    # return solution_model

    opt = Optimize()
    opt.add(complete_c + contig_c + instance_c + ortho_neigh_c)
    cost = Int('cost')
    opt.add(cost == Sum(vars_))
    h = opt.minimize(cost)
    opt.check()
    opt.lower(h)
    m = opt.model()
    solution_model = [ [ m[cell].as_long() for cell in row] for row in board ]

    optimizer_check_model = opt
    while True:
        print('checking solution')
        print(solution_model)
        optimizer_check_model.push()
        contig_model_c = gen_contiguous_constraints(solution_model, **pars)
        instance_model_c = [ var == val
                for var, val in zip(vars_, flatten(solution_model))
                if val > 0]
        # check that the numbers in the solution verifies the
        # contiguouness constraint
        optimizer_check_model.add(contig_model_c)
        optimizer_check_model.add(instance_model_c) # redundant?
        if optimizer_check_model.check() == sat:
                return solution_model #HERE IS THE RETURN STATEMENT
        print('rejecting solution')
        print(solution_model)

        optimizer_check_model.pop()
        # the rejected solution is added after 'pop'. This is how it is
        # added to the model
        optimizer_check_model.add(Not(And(instance_model_c)))
        optimizer_check_model.check()
        optimizer_check_model.lower(h)
        m = optimizer_check_model.model()
        solution_model = [ [ m[cell].as_long() for cell in row ] for row in board ]
示例#13
0
def solve_magnets(*, tiles, positive_horizontal, positive_vertical,
                  negative_vertical, negative_horizontal, height, width):
    X = IntMatrix('m', nb_rows=height, nb_cols=width)

    # Let us concentrate on edge|pole|half of the tile|domino|magnet
    halves = flatten(X)
    # completeness
    complete_c = [
        Or([edge == POSITIVE, edge == NEGATIVE, edge == NEUTRAL])
        for edge in halves
    ]

    assert len(positive_vertical) == len(X)
    pos_vertic_c = [
        coerce_charge(row, POSITIVE, pos_v)
        for row, pos_v in zip(X, positive_vertical) if pos_v >= 0
    ]

    assert len(negative_vertical) == len(X)
    neg_vertic_c = [
        coerce_charge(row, NEGATIVE, neg_v)
        for row, neg_v in zip(X, negative_vertical) if neg_v >= 0
    ]

    X_trans = transpose(X)

    assert len(positive_horizontal) == len(X_trans)
    pos_horiz_c = [
        coerce_charge(row, POSITIVE, pos_h)
        for row, pos_h in zip(X_trans, positive_horizontal) if pos_h >= 0
    ]

    assert len(negative_horizontal) == len(X_trans)
    neg_horiz_c = [
        coerce_charge(row, NEGATIVE, neg_h)
        for row, neg_h in zip(X_trans, negative_horizontal) if neg_h >= 0
    ]

    def horiz_tile(l, c):
        return X[l][c], X[l][c + 1]

    def vertic_tile(l, c):
        return X[l][c], X[l + 1][c]

    horiz_tiles_c = [
        coerce_tile(*horiz_tile(l, c))
        for l, c in itertools.product(range(height), range(width))
        if tiles[l][c] == '>'
    ]

    vertic_tiles_c = [
        coerce_tile(*vertic_tile(l, c))
        for l, c in itertools.product(range(height), range(width))
        if tiles[l][c] == 'v'
    ]

    # edge1 edge2 may not belong to the same magnet|tile
    horiz_neigh_c = [[
        coerce_neigh(edge1, edge2) for edge1, edge2 in zip(row, row[1:])
    ] for row in X]
    horiz_neigh_c = flatten(horiz_neigh_c)

    vertic_neigh_c = [[
        coerce_neigh(edge1, edge2) for edge1, edge2 in zip(row, row[1:])
    ] for row in X_trans]
    vertic_neigh_c = flatten(vertic_neigh_c)

    s = Solver()
    s.add(complete_c + pos_vertic_c + pos_horiz_c + neg_vertic_c +
          neg_horiz_c + horiz_tiles_c + vertic_tiles_c + horiz_neigh_c +
          vertic_neigh_c)

    s.check()
    m = s.model()
    return [[m[edge] for edge in row] for row in X]
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]
示例#15
0
def solve_tents(board, *, height, width, horizontal_tents, vertical_tents):
    preprocessed = preprocess(board, height=height, width=width)
    X = IntMatrix('t', nb_rows=height, nb_cols=width)

    at_ = lambda l, c: X[l][c]
    is_tree = lambda l, c: preprocessed[l][c] > 0

    # the coupling between a tree and a tent is unique. Think tile|domino
    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)

    # No two tents can be adjacent to each other.
    # None of the 8 directions. (think: King's move in chess).
    # The description above is the tent's point of view
    # If we take the board's point of view:
    # A square of 4 cells can contain at most 1 tree
    def gen_proximity_constraints(l, c):
        square = [
            (l, c),
            (l, c + 1),  # towards right
            (l + 1, c),
            (l + 1, c + 1)
        ]  # towards bottom and right
        val_cells_in_square = [
            cell for cell in square
            if inside_board(cell, height=height, width=width)
        ]
        # cell < 0 : we have encoded as tent
        tents_in_a_square = [at_(*cell) < 0 for cell in val_cells_in_square]
        return AtMost(*tents_in_a_square, 1)

    def coerce_nb_tents(cells, count_):
        tents = [cell < 0 for cell in cells]
        return Exactly(*tents, count_)

    MAX = height * width

    complete_c = [And(-MAX < cell, cell < MAX) for cell in flatten(X)]

    coupling_c = [
        gen_coupling_constraints(l, c)
        for l, c in itertools.product(range(height), range(width))
        if is_tree(l, c)
    ]

    instance_c = [
        at_(l, c) == preprocessed[l][c]
        for l, c in itertools.product(range(height), range(width))
        if preprocessed[l][c] > 0
    ]

    # In generating constraints we wander towards right and towards bottom
    # So : height - 1, width - 1
    tree_proximity_c = [
        gen_proximity_constraints(l, c)
        for l, c in itertools.product(range(height - 1), range(width - 1))
    ]

    NB_TREES = sum(cell > 0 for cell in flatten(preprocessed))
    same_number_trees_tents_c = [coerce_nb_tents(flatten(X), NB_TREES)]

    # vertical tents means nb tents in each line. Noted vertically on the board
    # on the left or right side
    assert len(vertical_tents) == height == len(X)
    row_sums_c = [
        coerce_nb_tents(row, count_v)
        for row, count_v in zip(X, vertical_tents)
    ]

    X_trans = transpose(X)
    # horizontal tents means nb tents in each line. Noted horizontally on the board
    # at the bottom of the board or top of the board
    assert len(horizontal_tents) == width == len(X_trans)
    # row in X_trans means col in X
    col_sums_c = [
        coerce_nb_tents(row, count_h)
        for row, count_h in zip(X_trans, horizontal_tents)
    ]

    # NOTE: if ever col_sums or row_sums are partially erased to add difficulty.
    # use -1. and add here if count_h >= 0

    s = Solver()
    s.add(complete_c + instance_c + coupling_c + tree_proximity_c +
          same_number_trees_tents_c + row_sums_c + col_sums_c)
    s.check()
    m = s.model()
    return [[m[cell] for cell in row] for row in X]
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