InternBootcamp/internbootcamp/libs/futoshiki/solver.py
2025-05-23 15:27:15 +08:00

632 lines
21 KiB
Python
Executable file

from copy import deepcopy
from libs.futoshiki.Futoshiki import FutoshikiPuzzle
from libs.futoshiki.log import output_log
############################
# Functional Below this line
############################
def empty_array_returner(puzzle_size, item_size, contents):
e = []
for j in range(puzzle_size):
line = []
for i in range(puzzle_size):
if item_size == 1:
item = contents
else:
item = []
for k in range(item_size):
if contents is None:
item.append(contents)
elif contents == "range":
# print("Returning range using dimension sizes to infer length")
item.append(k + 1)
line.append(item)
e.append(line)
return e
def puzzle_printer(puzzle, logic):
for i, l_line in enumerate(logic):
if (i + 1) % 2: # number lines
line = ""
for j in range(len(puzzle[0]) - 1):
line += "{n}{l:1}".format(n=puzzle[i // 2][j], l=l_line[j])
line += "{}".format(puzzle[i // 2][-1])
print(line)
else: # logic lines
line = ""
for l in range(len(l_line) - 1):
line += "{:1} ".format(l_line[l])
line += "{}".format(l_line[-1])
print(line)
def logic_finder(logic):
# Puzzle size is always the same as the length of the second line of logic.
puzzle_size = len(logic[1])
logic_matrix = empty_array_returner(puzzle_size, 4, None)
for j in range(puzzle_size):
for i in range(puzzle_size):
# Finding logic to test this cell against
try:
# right
logic_matrix[j][i][0] = logic[j * 2][i]
except IndexError:
logic_matrix[j][i][0] = None
try:
# below
logic_matrix[j][i][1] = logic[(j * 2) + 1][i]
except IndexError:
logic_matrix[j][i][1] = None
try:
# left
if i - 1 < 0:
logic_matrix[j][i][2] = None
else:
logic_matrix[j][i][2] = logic[j * 2][i - 1]
except IndexError:
logic_matrix[j][i][2] = None
try:
# above
if (j * 2) - 1 < 0:
logic_matrix[j][i][3] = None
else:
logic_matrix[j][i][3] = logic[(j * 2) - 1][i]
except IndexError:
logic_matrix[j][i][3] = None
return logic_matrix
def get_neighbours(puzzle, j, i):
neighbours = [None] * 4
try:
neighbours[0] = puzzle[j][i + 1]
except IndexError:
pass
try:
neighbours[1] = puzzle[j + 1][i]
except IndexError:
pass
try:
neighbours[2] = puzzle[j][i - 1]
except IndexError:
pass
try:
neighbours[3] = puzzle[j - 1][i]
except IndexError:
pass
return neighbours
def single_cell_tester(cell_number, neighbours, nearby_logic, only_check_two=False):
# "Only check two" allows for the fact that checking only right and down for all squares will be equally valid and quicker.
result = True
if nearby_logic[0] == ">" and neighbours[0] is not None:
result = result & (cell_number > neighbours[0])
if nearby_logic[1] == u"\u2228" and neighbours[1] is not None:
result = result & (cell_number > neighbours[1])
if nearby_logic[2] == "<" and neighbours[2] is not None and not only_check_two:
result = result & (cell_number > neighbours[2])
if (
nearby_logic[3] == u"\u2227"
and neighbours[3] is not None
and not only_check_two
):
result = result & (cell_number > neighbours[3])
if nearby_logic[0] == "<" and neighbours[0] is not None and neighbours[0] != 0:
result = result & (cell_number < neighbours[0])
if (
nearby_logic[1] == u"\u2227"
and neighbours[1] is not None
and neighbours[1] != 0
):
result = result & (cell_number < neighbours[1])
if (
nearby_logic[2] == ">"
and neighbours[2] is not None
and neighbours[2] != 0
and not only_check_two
):
result = result & (cell_number < neighbours[2])
if (
nearby_logic[3] == u"\u2228"
and neighbours[3] is not None
and neighbours[3] != 0
and not only_check_two
):
result = result & (cell_number < neighbours[3])
return result
def get_possible_neighbours(p, j, i, index=0):
neighbours = [None] * 4
try:
neighbours[0] = p[j][i + 1][index]
except IndexError:
pass
try:
neighbours[1] = p[j + 1][i][index]
except IndexError:
pass
try:
neighbours[2] = p[j][i - 1][index]
except IndexError:
pass
try:
neighbours[3] = p[j - 1][i][index]
except IndexError:
pass
return neighbours
def valid(puzzle, logic):
tests_to_be_performed = logic_finder(logic)
all_cells_valid = True
zero_flag = False
all_lines_filled = True
logic_failures = []
for j, line in enumerate(puzzle):
for i, n in enumerate(line):
if n == 0:
# any zeroes mean not completed puzzle
print("Not Complete (Zeroes Present) Checking Logic")
zero_flag = True
else:
# Applying the tests to the cell
cell_valid = single_cell_tester(
cell_number=puzzle[j][i],
neighbours=get_neighbours(puzzle, j, i),
nearby_logic=tests_to_be_performed[j][i],
only_check_two=True,
)
if not cell_valid:
print("Logic Failure caused by cell: {},{} (line, index)".format(j, i))
logic_failures.append((j, i))
all_cells_valid = False
l = line.copy()
l.sort()
# Check the line sorted is the same as an array of 1->size of puzzle
if l != [k + 1 for k in range(len(puzzle[0]))]:
print(
"The line {} does not have solely the numbers 1-{} (inclusive), therefore is invalid".format(
puzzle.index(line), len(puzzle[0])
)
)
all_lines_filled = False
return (all_cells_valid, all_lines_filled, zero_flag), logic_failures
def recursive_more_than(logic_matrix, p, j, i):
nearby_logic = logic_matrix[j][i]
# print("Currently in {},{}".format(j, i))
# CHECK WHETHER RECURSION COULD BE MOVED UP
# next cell is less than current cell
if nearby_logic[0] == ">":
try:
p[j][i] = [x for x in p[j][i] if x > min(p[j][i + 1])]
except ValueError:
pass
recursive_more_than(logic_matrix, p, j, i + 1)
if nearby_logic[1] == u"\u2228":
try:
# p[j][i].remove((min(p[j+1][i])))
p[j][i] = [x for x in p[j][i] if x > min(p[j + 1][i])]
except ValueError:
pass
recursive_more_than(logic_matrix, p, j + 1, i)
if nearby_logic[2] == "<":
try:
# p[j][i].remove((min(p[j][i-1])))
p[j][i] = [x for x in p[j][i] if x > min(p[j][i - 1])]
except ValueError:
pass
recursive_more_than(logic_matrix, p, j, i - 1)
if nearby_logic[3] == u"\u2227":
try:
# p[j][i].remove((min(p[j-1][i])))
p[j][i] = [x for x in p[j][i] if x > min(p[j - 1][i])]
except ValueError:
pass
recursive_more_than(logic_matrix, p, j - 1, i)
return p
def recursive_less_than(logic_matrix, p, j, i):
nearby_logic = logic_matrix[j][i]
# print("Currently in {},{}".format(j, i))
# next cell is greater than current cell
if nearby_logic[0] == "<":
try:
p[j][i] = [x for x in p[j][i] if x < max(p[j][i + 1])]
except ValueError:
pass
recursive_less_than(logic_matrix, p, j, i + 1)
if nearby_logic[1] == u"\u2227":
try:
p[j][i] = [x for x in p[j][i] if x < max(p[j + 1][i])]
except ValueError:
pass
recursive_less_than(logic_matrix, p, j + 1, i)
if nearby_logic[2] == ">":
try:
p[j][i] = [x for x in p[j][i] if x < max(p[j][i - 1])]
except ValueError:
pass
recursive_less_than(logic_matrix, p, j, i - 1)
if nearby_logic[3] == u"\u2228":
try:
p[j][i] = [x for x in p[j][i] if x < max(p[j - 1][i])]
except ValueError:
pass
recursive_less_than(logic_matrix, p, j - 1, i)
return p
def line_match(row, match_list):
row_matches = [index for index, val in enumerate(row) if val == match_list]
if len(row_matches) == len(match_list):
non_matched_row_indices = [x for x in range(len(row)) if x not in row_matches]
for index in non_matched_row_indices:
try:
for num in match_list:
row[index].remove(num)
except ValueError:
continue
# Now all exact matches are removed, look for partial matches:
if len(match_list) == 2 and len(row_matches) == 1:
# print("Partial matching: match_list {} and \n row:{} ".format(match_list, row))
rest_of_row = list(filter(lambda a: a != match_list, deepcopy(row)))
third_values_attempted = []
for item in rest_of_row:
if len(item) == 3 and all(
x in item for x in match_list
): # all of the match list is contained
ic = [x for x in deepcopy(item) if x not in match_list]
# ic.remove(match_list[0])
# ic.remove(match_list[1])
# print("Checking {}, Third value in item = {}".format(item, ic[0]))
if ic[0] in third_values_attempted:
pass
else:
third_values_attempted.append(ic[0])
new_match_value = deepcopy(match_list)
new_match_value.append(ic[0])
new_match_value.sort()
new_row_matches = [
index
for index, val in enumerate(row)
if (val == new_match_value) or (val == match_list)
]
if len(new_row_matches) == 3:
# print("Partial matching match_list: {} extended to: {} for \nrow: {}".format(
# match_list, new_match_value, row))
# print("Found {} matches, removing {} from row ".format(
# len(new_row_matches), new_match_value))
non_matched_row_indices = [
x for x in range(len(row)) if x not in new_row_matches
]
for index in non_matched_row_indices:
for num in new_match_value:
try:
row[index].remove(num)
except ValueError:
continue
# print("New row: {}".format(row))
return row
def only_possible_location(line, index):
other_indices = [k for k in range(len(line))]
other_indices.remove(index)
poss_values = line[index]
for val in poss_values:
other_occurences = 0
for i in other_indices:
if val in line[i]:
# Count up if there is another possible location
other_occurences += 1
if other_occurences == 0:
# If none then this place is the only one you can put this value
line[index] = [val]
return line
def poss_locations(pc, j, i):
row = pc[j].copy()
row = only_possible_location(row, i)
pc[j] = row
col = [x[i] for x in pc.copy()]
col = only_possible_location(col, j)
for m in range(len(pc[0])):
pc[m][i] = col[m]
return pc
def find_matches(pc, j, i):
match_list = pc[j][i]
# Match the row first
row = pc[j].copy()
row = line_match(row, match_list)
# Match the column next
col = [x[i] for x in pc.copy()]
col = line_match(col, match_list)
# As shallow copies have been used, the possibility matrix will have been altered
return pc
def corner_rule(logic_matrix, pc, j, i):
match_list = pc[j][i]
if len(match_list) != 2:
return pc
else:
row = pc[j].copy()
col = [x[i] for x in pc.copy()]
row_matches = [index for index, val in enumerate(row) if val == match_list]
if len(row_matches) != 2:
return pc
else:
i1, i2 = row_matches[0], row_matches[1]
if (
logic_matrix[j][i1][1] == u"\u2227"
and logic_matrix[j][i2][3] == u"\u2228"
) and pc[j + 1][i1] == pc[j - 1][i2]:
# print("Both pairs less than same pair - applying corner rule 1")
num_to_remove = max(pc[j + 1][i1])
try:
pc[j + 1][i2].remove(num_to_remove)
except ValueError:
pass
try:
pc[j - 1][i1].remove(num_to_remove)
except ValueError:
pass
if (
logic_matrix[j][i1][3] == u"\u2228"
and logic_matrix[j][i2][1] == u"\u2227"
and pc[j - 1][i1] == pc[j + 1][i2]
):
# print("Both pairs less than same pair - applying corner rule 2")
num_to_remove = max(pc[j - 1][i1])
try:
pc[j + 1][i1].remove(num_to_remove)
except ValueError:
pass
try:
pc[j - 1][i2].remove(num_to_remove)
except ValueError:
pass
# col_matches = [index for index, val in enumerate(col) if val == match_list]
return pc
def possible_values(puzzle, logic, p):
logic_layout = logic_finder(logic)
pc = deepcopy(p)
for j, line in enumerate(puzzle):
for i, number in enumerate(line):
if number != 0 or len(pc[j][i]) == 1:
if number == 0:
no = pc[j][i][0]
else:
no = number
# print('Removing {} from column and line due to appearance at {}, {}'.format(number, j, i))
other_line = [k for k in range(len(pc[0]))]
other_line.remove(i)
for k in other_line:
try:
# print('{},{}'.format(l, i))
pc[j][k].remove(no)
except (ValueError, IndexError):
pass
other_column = [k for k in range(len(pc[0]))]
other_column.remove(j)
for l in other_column:
try:
pc[l][i].remove(no)
except (ValueError, IndexError):
pass
pc[j][i] = [no]
else:
# Removing the maxima from neighbours in 'less than' locations, and minima from 'more than' locations
pc = recursive_more_than(logic_layout, pc, j, i)
# current cell is less than the next cell along
pc = recursive_less_than(logic_layout, pc, j, i)
if len(pc[j][i]) == 2 or len(pc[j][i]) == 3:
pc = find_matches(pc, j, i)
pc = poss_locations(pc, j, i)
pc = corner_rule(logic_layout, pc, j, i)
return pc
def brute_force(puzzle, logic, p):
logic_matrix = logic_finder(logic)
new_p = deepcopy(p)
new_puzzle = deepcopy(puzzle)
# Box lookup is a dictionary of box locations, indexed by number of possible values for each location
box_lookup = {}
# We want boxes with two values, up to boxes with any possible value (ie size of puzzle)
for m in range(2, len(p[0]) + 1):
box_lookup[m] = []
for j in range(len(new_puzzle[0])):
for i in range(len(new_puzzle[0])):
if len(p[j][i]) == 1:
new_puzzle[j][i] = new_p[j][i][0]
else:
box_lookup[len(new_p[j][i])].append((j, i))
# print(box_lookup)
solved = False
for m in range(2, len(p[0]) + 1):
for box in box_lookup[m]:
print("Attempting brute force at cell {},{}".format(box[0], box[1]))
for index in range(len(new_p[box[0]][box[1]])):
test_puzzle = new_puzzle
# Try value at index as the value for this box:
test_puzzle[box[0]][box[1]] = new_p[box[0]][box[1]][index]
pc, t = consistent_past_values(test_puzzle, logic)
# pc, t, solved = solve(test_puzzle, logic, brute_force_if_needed=True)
solved = True
# # TODO fix this bit up so that it brute forces all options below a current option, if all of those don't work then it removes the current option.
# if not solved:
# new_p[box[0]][box[1]].pop(index)
# # remove option that does not solve puzzle
# #####################################################
# # Old stuff down here
solution = []
for j in range(len(new_puzzle[0])):
line = []
for i in range(len(new_puzzle[0])):
if len(pc[j][i]) == 1:
line.append(pc[j][i][0])
else:
solved = False
break
solution.append(line)
#####################################################
if solved and valid(solution, logic):
# print("Problem solved through brute forcing")
# puzzle_printer(solution, logic)
return pc
else:
continue
print("Not able to solve by brute forcing, you might have to figure it out mate.")
return new_p
def sol_print(p):
for line in p:
print(line)
def consistent_past_values(puzzle, logic):
p = empty_array_returner(len(puzzle[0]), len(puzzle[0]), "range")
p_prev = None
t = 0
while p != p_prev and t < 15:
p_prev = p
p = possible_values(puzzle, logic, p_prev)
t += 1
return p, t
def solve(puzzle, logic, brute_force_if_needed=False):
print("Finding a solution to the puzzle:")
puzzle_printer(puzzle, logic)
p, t = consistent_past_values(puzzle, logic)
solved = True
solution = []
for j in range(len(puzzle[0])):
line = []
for i in range(len(puzzle[0])):
if len(p[j][i]) == 1:
line.append(p[j][i][0])
else:
solved = False
break
solution.append(line)
if solved and valid(solution, logic):
# print("Problem solved after {} iterations!".format(t))
# puzzle_printer(solution, logic)
return p, t, solved
else:
print("Problem not solved after {} iterations, possible values:".format(t))
sol_print(p)
if brute_force_if_needed:
print("Brute Forcing")
p = brute_force(puzzle, logic, p)
else:
brute_q = input("Do you want to brute force a solution? (y/n)")
if brute_q == "y":
p = brute_force(puzzle, logic, p)
return p, t, solved
# p1 = FutoshikiPuzzle(puzzle1, logic1)
# # print(p1)
# p1.solve()
# print(p1.solution)
# for x in p1.possible_values:
# print(x)
# solve(puzzle1, logic1)
puzzle11 = [
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 2, 0],
[0, 0, 0, 0, 0]
]
# '/\': u"\u2227"
# '\/': u"\u2228"
logic11 = [
["", "", "", ""],
["", "", "", "", ""],
["", "", "", ""],
["", "", "", "", ""],
["", "", ">", ""],
["", "", "", u"\u2228", ""],
["", ">", "", ""],
[u"\u2227", "", "", "", ""],
["", ">", ">", ""],
["", "", "", "", ""]
]
def test():
p = FutoshikiPuzzle(puzzle11, logic11)
p.solve(output_log())
def solve_futoshiki(puzzle, logic):
# 确保 puzzle 和 logic 不为 None
if puzzle is None:
output_str = "error"
solution = "出现错误,跳过此条训练数据"
print(output_str)
return output_str, solution
raise ValueError("puzzle cannot be None")
if logic is None:
output_str = "error"
solution = "出现错误,跳过此条训练数据"
print(output_str)
return output_str, solution
raise ValueError("logic cannot be None")
# 检查 puzzle 和 logic 的类型,确保它们符合预期
if not isinstance(puzzle, list) or not isinstance(logic, list):
raise TypeError("puzzle and logic must be of type list")
steps = output_log() # 创建日志对象
p = FutoshikiPuzzle(initial_puzzle_numbers=puzzle, puzzle_logic=logic, alog=steps,gen_mode=False)
solution = p.solve()
output_str = steps.get_log()
print(output_str)
return output_str, solution