Advertisement
Guest User

APS tetris solver

a guest
Mar 4th, 2025
23
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.78 KB | Source Code | 0 0
  1. # Adapted from https://stackoverflow.com/a/47934736
  2. import math
  3. import copy
  4. import subprocess
  5. import time
  6. import numpy as np
  7. import matplotlib.pyplot as plt      # plotting-only
  8. import seaborn as sns                # plotting-only
  9. np.set_printoptions(linewidth=120)   # more nice console-output
  10.  
  11. # 3x3 breaks but whatever
  12. M, N = 29, 29
  13. # T-shaped tetromino (rotations)
  14. T0 = np.array([[1, 1, 1],
  15.                [0, 1, 0]])
  16. T90 = np.array([[0, 1],
  17.                 [1, 1],
  18.                 [0, 1]])
  19. T180 = np.array([[0, 1, 0],
  20.                  [1, 1, 1]])
  21. T270 = np.array([[1, 0],
  22.                  [1, 1],
  23.                  [1, 0]])
  24. polyominos = [T0, T90, T180, T270]
  25. def create_inverted_circle(N):
  26.     if N % 2 == 0:
  27.         raise ValueError("N must be an odd integer")
  28.    
  29.     grid = [[0 for _ in range(N)] for _ in range(N)]
  30.     center =(int) ((N - 1) / 2.0)   # Grid center coordinate
  31.     radius = N / 2.0   # Adjust radius to ensure symmetry
  32.    
  33.     for x in range(N):
  34.         for y in range(N):
  35.             # Calculate distance from cell center to grid center
  36.             distance = math.sqrt((x - center)**2 + (y - center)**2)
  37.             if distance >= radius:
  38.                 grid[x][y] = 1
  39.     grid[center][center] = 1
  40.     return np.array(grid)
  41.  
  42. inverted_circle_grid = create_inverted_circle(N)
  43. """ Preprocessing
  44.        Calculate:
  45.        A: possible placements
  46.        B: covered positions
  47.        C: collisions between placements
  48. """
  49. placements = []
  50. covered = []
  51. for p_ind, p in enumerate(polyominos):
  52.     mP, nP = p.shape
  53.     for x in range(M):
  54.         for y in range(N):
  55.             if x + mP <= M and y + nP <= N:
  56.                 # Check if the placement is valid (not overlapping with cells set to 1)
  57.                 placement_valid = True
  58.                 for i in range(mP):
  59.                     for j in range(nP):
  60.                         if p[i,j] == 1 and inverted_circle_grid[x+i,y+j] == 1:
  61.                             placement_valid = False
  62.                             break
  63.                     if not placement_valid:
  64.                         break
  65.                 if placement_valid:
  66.                     placements.append((p_ind, x, y))
  67.                     cover = np.zeros((M, N), dtype=bool)
  68.                     cover[x:x+mP, y:y+nP] = p
  69.                     covered.append(cover)
  70. covered = np.array(covered)
  71.  
  72. collisions = []
  73. for m in range(M):
  74.     for n in range(N):
  75.         collision_set = np.flatnonzero(covered[:, m, n])
  76.         collisions.append(collision_set)
  77.  
  78. print(f"Placements: {len(placements)}, Collision sets: {len(collisions)}")
  79.  
  80. def parse_solution(output):
  81.     # assumes there is one
  82.     vars = []
  83.     for line in output.split("\n"):
  84.         if line:
  85.             if line[0] == 'v':
  86.                 line_vars = list(map(lambda x: int(x), line.split()[1:]))
  87.                 vars.extend(line_vars)
  88.     return vars
  89.  
  90. def solve(CNF):
  91.     p = subprocess.Popen(["cryptominisat5.exe"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, encoding='utf-8')
  92.     result = p.communicate(input=CNF)[0]
  93.    
  94.     sat_line = result.find('s SATISFIABLE')
  95.     if sat_line != -1:
  96.         # solution found!
  97.         vars = parse_solution(result)
  98.         return True, vars
  99.     else:
  100.         return False, None
  101.  
  102. """ Helper-function: Cardinality constraints """
  103.  
  104. # K-ARY CONSTRAINT GENERATION
  105. # ###########################
  106. # SINZ, Carsten. Towards an optimal CNF encoding of boolean cardinality constraints.
  107. # CP, 2005, 3709. Jg., S. 827-831.
  108.  
  109. def next_var_index(start):
  110.     next_var = start
  111.     while(True):
  112.         yield next_var
  113.         next_var += 1
  114.  
  115. class s_index():
  116.     def __init__(self, start_index):
  117.         self.firstEnvVar = start_index
  118.  
  119.     def next(self,i,j,k):
  120.         return self.firstEnvVar + i*k +j
  121.  
  122. def gen_seq_circuit(k, input_indices, next_var_index_gen):
  123.     cnf_string = ''
  124.     s_index_gen = s_index(next(next_var_index_gen))
  125.  
  126.     if len(input_indices) < 3:
  127.         # Handle cases with fewer than 3 input indices
  128.         for i in range(len(input_indices)):
  129.             cnf_string += (str(-input_indices[i]) + ' ' + str(s_index_gen.next(0, i, k)) + ' 0\n')
  130.         return (cnf_string, len(input_indices) * k, 2 * len(input_indices) * k + len(input_indices) - 3 * k - 1)
  131.  
  132.     # write clauses of first partial sum (i.e. i=0)
  133.     cnf_string += (str(-input_indices[0]) + ' ' + str(s_index_gen.next(0, 0, k)) + ' 0\n')
  134.     for i in range(1, k):
  135.         cnf_string += (str(-s_index_gen.next(0, i, k)) + ' 0\n')
  136.  
  137.     # write clauses for general case (i.e. 0 < i < n-1)
  138.     for i in range(1, len(input_indices) - 1):
  139.         cnf_string += (str(-input_indices[i]) + ' ' + str(s_index_gen.next(i, 0, k)) + ' 0\n')
  140.         cnf_string += (str(-s_index_gen.next(i - 1, 0, k)) + ' ' + str(s_index_gen.next(i, 0, k)) + ' 0\n')
  141.         for u in range(1, k):
  142.             cnf_string += (str(-input_indices[i]) + ' ' + str(-s_index_gen.next(i - 1, u - 1, k)) + ' ' + str(s_index_gen.next(i, u, k)) + ' 0\n')
  143.             cnf_string += (str(-s_index_gen.next(i - 1, u, k)) + ' ' + str(s_index_gen.next(i, u, k)) + ' 0\n')
  144.         cnf_string += (str(-input_indices[i]) + ' ' + str(-s_index_gen.next(i - 1, k - 1, k)) + ' 0\n')
  145.  
  146.     # last clause for last variable
  147.     cnf_string += (str(-input_indices[-1]) + ' ' + str(-s_index_gen.next(len(input_indices) - 2, k - 1, k)) + ' 0\n')
  148.  
  149.     return (cnf_string, (len(input_indices) - 1) * k, 2 * len(input_indices) * k + len(input_indices) - 3 * k - 1)
  150.  
  151. def gen_at_most_n_constraints(vars, start_var, n):
  152.     constraint_string = ''
  153.     used_clauses = 0
  154.     used_vars = 0
  155.     index_gen = next_var_index(start_var)
  156.     circuit = gen_seq_circuit(n, vars, index_gen)
  157.     constraint_string += circuit[0]
  158.     used_clauses += circuit[2]
  159.     used_vars += circuit[1]
  160.     start_var += circuit[1]
  161.  
  162.     return [constraint_string, used_clauses, used_vars, start_var]
  163.  
  164. """ SAT-CNF: BASE """
  165. X = np.arange(1, len(placements)+1)                                     # decision-vars
  166.                                                                         # 1-index for CNF
  167. Y = np.arange(len(placements)+1, len(placements)+1 + M*N).reshape(M,N)
  168. next_var = len(placements)+1 + M*N                                      # aux-var gen
  169. n_clauses = 0
  170.  
  171. cnf = ''                                                                # slow string appends
  172.                                                                         # int-based would be better
  173. # <= 1 for each collision-set
  174. for cset in collisions:
  175.     constraint_string, used_clauses, used_vars, next_var = \
  176.         gen_at_most_n_constraints(X[cset].tolist(), next_var, 1)
  177.     n_clauses += used_clauses
  178.     cnf += constraint_string
  179.  
  180. # if field marked: one of covering placements active
  181. for x in range(M):
  182.     for y in range(N):
  183.         covering_placements = X[np.flatnonzero(covered[:, x, y])]  # could reuse collisions
  184.         clause = str(-Y[x,y])
  185.         for i in covering_placements:
  186.             clause += ' ' + str(i)
  187.         clause += ' 0\n'
  188.         cnf += clause
  189.         n_clauses += 1
  190.  
  191. print('BASE CNF size')
  192. print('clauses: ', n_clauses)
  193. print('vars: ', next_var - 1)
  194.  
  195.  
  196.  
  197. """ SOLVE in loop -> decrease number of placed-fields until SAT """
  198. print('CORE LOOP')
  199. # Calculate area inside circle (non-inverted cells)
  200. circle_area = np.sum(inverted_circle_grid == 0)
  201.  
  202. # Calculate minimum polyomino area
  203. min_poly_area = min(np.sum(p) for p in polyominos)
  204.  
  205. # Floor to nearest multiple of minimum polyomino area
  206. N_FIELD_HIT = (circle_area // min_poly_area) * min_poly_area
  207. max_possible = (int)(N_FIELD_HIT/min_poly_area)
  208.  
  209.  
  210.  
  211. time_start = time.time()
  212. while True:
  213.     print(' N_FIELDS >= ', N_FIELD_HIT)
  214.     # sum(y) >= N_FIELD_HIT
  215.     # == sum(not y) <= M*N - N_FIELD_HIT
  216.     cnf_final = copy.copy(cnf)
  217.     n_clauses_final = n_clauses
  218.  
  219.     if N_FIELD_HIT == M*N:  # awkward special case
  220.         constraint_string = ''.join([str(y) + ' 0\n' for y in Y.ravel()])
  221.         n_clauses_final += N_FIELD_HIT
  222.     else:
  223.         constraint_string, used_clauses, used_vars, next_var = \
  224.             gen_at_most_n_constraints((-Y).ravel().tolist(), next_var, M*N - N_FIELD_HIT)
  225.         n_clauses_final += used_clauses
  226.  
  227.     n_vars_final = next_var - 1
  228.     cnf_final += constraint_string
  229.     cnf_final = 'p cnf ' + str(n_vars_final) + ' ' + str(n_clauses) + \
  230.         ' \n' + cnf_final  # header
  231.  
  232.     status, sol = solve(cnf_final)
  233.     if status:
  234.         print(' SOL found: ', N_FIELD_HIT)
  235.  
  236.         """ Print sol """
  237.         res = np.full((M, N), -1)  # Initialize with -1 for 'X'
  238.         # Replace -1 with 0 where inverted_circle_grid is 0
  239.         res[inverted_circle_grid == 0] = 0
  240.        
  241.         # Create mapping of polyomino types to list of available numbers
  242.         poly_numbers = {}
  243.         current_num = 1
  244.        
  245.         import random
  246.         # Count placements and create a shuffled range of numbers
  247.         n_placements = sum(1 for v in sol[:X.shape[0]] if v > 0)
  248.         numbers = list(range(1, n_placements + 1))
  249.         random.shuffle(numbers)
  250.        
  251.         # Create mapping of placement index to number
  252.         number_map = {}
  253.         number_idx = 0
  254.        
  255.         # First pass: assign numbers to placements
  256.         for i, v in enumerate(sol[:X.shape[0]]):
  257.             if v > 0:
  258.                 number_map[i] = numbers[number_idx]
  259.                 number_idx += 1
  260.        
  261.         # Second pass: fill the result grid
  262.         counter = n_placements + 1
  263.         for i, v in enumerate(sol[:X.shape[0]]):
  264.             if v > 0:
  265.                 p, x, y = placements[i]
  266.                 pM, pN = polyominos[p].shape
  267.                 poly_nnz = np.where(polyominos[p] != 0)
  268.                 x_inds, y_inds = x+poly_nnz[0], y+poly_nnz[1]
  269.                 res[x_inds, y_inds] = number_map[i]
  270.         print(res)
  271.  
  272.         """ Plot """
  273.         ax1 = plt.subplot2grid((5, 12), (0, 0), colspan=11, rowspan=5)
  274.         mask = (res==0)
  275.         # Create custom colormap with black for -1, white for 0, and colors for others
  276.         from matplotlib.colors import ListedColormap
  277.         colors = ['black', 'white'] + sns.color_palette('gist_rainbow', n_colors=counter)
  278.         custom_cmap = ListedColormap(colors)
  279.         sns.heatmap(res, cmap=custom_cmap, mask=mask, cbar=False, square=True, linewidths=.1, ax=ax1, vmin=-1, vmax=counter-1)
  280.         elapsed_time = time.time() - time_start
  281.         plt.title(f"{M}x{N} Placements: {sum(1 for v in sol[:X.shape[0]] if v > 0)}/{max_possible} ({elapsed_time:.2f}s)")
  282.         plt.tight_layout()
  283.         plt.show()
  284.         break
  285.     with open(f"output_{M}X{N}.txt",mode="w") as file:
  286.         file.write(cnf_final)
  287.  
  288.     N_FIELD_HIT -= min_poly_area  # binary-search could be viable in some cases
  289.                       # but beware the empirical asymmetry in SAT-solvers:
  290.                       #    finding solution vs. proving there is none!
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement