kenken/main.py

210 lines
7.1 KiB
Python
Raw Normal View History

2022-04-04 20:41:30 +02:00
import itertools
2022-04-09 00:57:08 +02:00
from copy import deepcopy
2022-04-04 20:41:30 +02:00
from itertools import combinations, combinations_with_replacement
OP_PLUS = "PLUS"
OP_MINUS = "MINUS"
OP_MULTIPLY = "MULTIPLY"
OP_DIVIDE = "DIVIDE"
OP_NONE = "NONE"
def lambda_mul(x):
rv = 1
for v in x:
rv *= v
return rv
OP_LAMBDAS = {
OP_PLUS: sum,
OP_MINUS: lambda x: abs(x[0] - x[1]),
OP_MULTIPLY: lambda_mul,
OP_DIVIDE: lambda x: x[0] / x[1] if x[0] % x[1] == 0 else x[1] / x[0],
OP_NONE: lambda x: x[0],
}
BOX_H_LABELS = 'ABCDEF'
2022-04-09 00:57:08 +02:00
BOX_V_LABELS = '654321'
2022-04-04 20:41:30 +02:00
2022-04-09 01:19:33 +02:00
def translate_box_to_rc(box):
return BOX_V_LABELS.index(box[1]), BOX_H_LABELS.index(box[0])
def has_line_integrity(line):
existing_values = set()
for value in line:
if value in existing_values:
return False
if value != 0:
existing_values.add(value)
return True
def fill_grid(grid, solution):
box_values = []
for box_value in solution:
box_values.append(box_value)
for box, value in box_values:
br, bc = translate_box_to_rc(box)
grid[br][bc] = value
return grid
def check_grid(grid):
for row in grid:
if not has_line_integrity(row):
return False
for col_id in range(len(grid)):
col = [grid[r][col_id] for r in range(len(grid))]
if not has_line_integrity(col):
return False
return True
def init_grid(grid_size):
return [[0] * grid_size for _ in range(grid_size)]
2022-04-04 20:41:30 +02:00
class Block(object):
def __init__(self, boxes, operation, result):
self.boxes = boxes
self.operation = operation
self.result = result
2022-04-09 00:57:08 +02:00
self.solutions = set()
2022-04-04 20:41:30 +02:00
self.verify()
def verify(self):
2022-04-09 01:19:33 +02:00
"""
Verifies that the box location are valid in the range A-F and 1-6
Operations are in +-*/ or None.
Where -/ have exactly 2, None has exactly 1.
"""
2022-04-04 20:41:30 +02:00
assert self.operation in OP_LAMBDAS
if self.operation in [OP_NONE]:
assert len(self.boxes) == 1
elif self.operation in [OP_MINUS, OP_DIVIDE]:
assert len(self.boxes) == 2
else:
assert len(self.boxes) > 1
for box in self.boxes:
assert len(box) == 2
assert box[0] in BOX_H_LABELS
assert box[1] in BOX_V_LABELS
def all_boxes_in_one_row_or_column(self):
2022-04-09 01:19:33 +02:00
"""Returns true if all of the boxes are in the same row OR the same column"""
2022-04-04 20:41:30 +02:00
is_same_column, is_same_row = True, True
for box in self.boxes[1:]:
if self.boxes[0][0] != box[0]:
is_same_column = False
if self.boxes[0][1] != box[1]:
is_same_row = False
return is_same_row or is_same_column
def generate_combinations(self, k, n):
2022-04-09 01:19:33 +02:00
"""Generates k|n combinations for 2 boxes or if they are in the same row OR column.
Generates k|n combinations with replacements for other cases."""
2022-04-04 20:41:30 +02:00
assert 1 <= k < n
# Exploit the structure of the problem
# For block of size 2, we don't need comb with replacement as they will
# neccesarily be in different columns/rows
if k == 2:
return list(combinations(range(1, n + 1), k))
# for 3 and more we don't need if all of the boxes are on the
# same row or same column
elif k > 2:
if self.all_boxes_in_one_row_or_column():
return list(combinations(range(1, n + 1), k))
return list(combinations_with_replacement(range(1, n + 1), k))
def generate_hypotheses(self, grid_size):
2022-04-09 01:19:33 +02:00
"""Generates hypothesis for this block based on the operation and result."""
2022-04-04 20:41:30 +02:00
rv = []
op = OP_LAMBDAS[self.operation]
hypotheses = self.generate_combinations(k=len(self.boxes), n=grid_size)
for hypothesis in hypotheses:
if op(hypothesis) == self.result:
rv.append(hypothesis)
return rv
def generate_solutions(self, grid_size):
2022-04-09 01:19:33 +02:00
"""Generates possible solutions for the block, including the limiting row/column requirement."""
2022-04-04 20:41:30 +02:00
self.generate_hypotheses(grid_size)
for hyp in self.generate_hypotheses(grid_size):
for perm in itertools.permutations(hyp):
2022-04-09 00:57:08 +02:00
sol = tuple(zip(self.boxes, perm))
2022-04-09 01:19:33 +02:00
if check_grid(fill_grid(init_grid(grid_size), sol)):
self.solutions.add(sol)
2022-04-04 20:41:30 +02:00
def __repr__(self):
return 'Block {}'.format(self.boxes)
class Game(object):
def __init__(self, grid_size, blocks):
self.blocks = blocks
self.grid_size = grid_size
self.verify()
2022-04-09 00:57:08 +02:00
for block in self.blocks:
block.generate_solutions(self.grid_size)
2022-04-04 20:41:30 +02:00
def verify(self):
2022-04-09 01:19:33 +02:00
"""Check that each block is found exactly once in the grid."""
2022-04-04 20:41:30 +02:00
found = set()
for block in self.blocks:
for box in block.boxes:
assert box not in found
found.add(box)
assert len(found) == self.grid_size ** 2
2022-04-09 00:57:08 +02:00
def solve(self, grid, current_block_id=0):
2022-04-09 01:19:33 +02:00
"""
Recursive solution - Start by filling up the first block.
For each solution that passes the row/column requirement, check the recursive solution
with the next block. Break when it's the last block and the row/col requirement is filled.
"""
2022-04-09 00:57:08 +02:00
block = self.blocks[current_block_id]
for solution in block.solutions:
prev_grid = deepcopy(grid)
grid = fill_grid(grid, solution)
2022-04-09 01:19:33 +02:00
if not check_grid(grid):
2022-04-09 00:57:08 +02:00
grid = prev_grid
else:
if current_block_id == len(self.blocks) - 1:
return grid
sol = self.solve(deepcopy(grid), current_block_id + 1)
if sol:
return sol
2022-04-04 20:41:30 +02:00
def main():
game = Game(grid_size=6,
blocks=[
2022-04-09 00:57:08 +02:00
Block(boxes=['A6', 'B6', 'C6', 'D6'], operation=OP_MULTIPLY, result=120),
Block(boxes=['E6', 'E5', 'E4', 'F4'], operation=OP_PLUS, result=17),
Block(boxes=['F6', 'F5'], operation=OP_PLUS, result=5),
Block(boxes=['A5', 'B5'], operation=OP_MINUS, result=1),
Block(boxes=['C5', 'C4'], operation=OP_DIVIDE, result=2),
Block(boxes=['D5', 'D4'], operation=OP_PLUS, result=3),
Block(boxes=['A4', 'A3'], operation=OP_PLUS, result=5),
Block(boxes=['B4', 'B3', 'C3'], operation=OP_MULTIPLY, result=30),
Block(boxes=['D3', 'D2', 'E2'], operation=OP_MULTIPLY, result=15),
Block(boxes=['B2', 'C2', 'C1'], operation=OP_MULTIPLY, result=10),
Block(boxes=['A2'], operation=OP_NONE, result=6),
Block(boxes=['A1', 'B1'], operation=OP_DIVIDE, result=3),
Block(boxes=['D1', 'E1'], operation=OP_MINUS, result=1),
Block(boxes=['F1', 'F2', 'F3', 'E3'], operation=OP_PLUS, result=16),
2022-04-04 20:41:30 +02:00
])
2022-04-09 01:19:33 +02:00
grid = game.solve(grid=init_grid(game.grid_size))
2022-04-09 00:57:08 +02:00
for line in grid:
print(line)
2022-04-04 20:41:30 +02:00
if __name__ == '__main__':
main()