Advertisement
jules0707

generate.py

Feb 26th, 2024 (edited)
6
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.60 KB | None | 0 0
  1. import sys
  2.  
  3. from crossword import *
  4. import queue
  5. from operator import itemgetter
  6.  
  7.  
  8. class CrosswordCreator():
  9.  
  10.     def __init__(self, crossword):
  11.         """
  12.        Create new CSP crossword generate.
  13.        """
  14.         self.crossword = crossword
  15.         self.domains = {
  16.             var: self.crossword.words.copy()
  17.             for var in self.crossword.variables
  18.         }
  19.  
  20.     def letter_grid(self, assignment):
  21.         """
  22.        Return 2D array representing a given assignment.
  23.        """
  24.         letters = [
  25.             [None for _ in range(self.crossword.width)]
  26.             for _ in range(self.crossword.height)
  27.         ]
  28.         for variable, word in assignment.items():
  29.             direction = variable.direction
  30.             for k in range(len(word)):
  31.                 i = variable.i + (k if direction == Variable.DOWN else 0)
  32.                 j = variable.j + (k if direction == Variable.ACROSS else 0)
  33.                 letters[i][j] = word[k]
  34.         return letters
  35.  
  36.     def print(self, assignment):
  37.         """
  38.        Print crossword assignment to the terminal.
  39.        """
  40.         letters = self.letter_grid(assignment)
  41.         for i in range(self.crossword.height):
  42.             for j in range(self.crossword.width):
  43.                 if self.crossword.structure[i][j]:
  44.                     print(letters[i][j] or " ", end="")
  45.                 else:
  46.                     print("█", end="")
  47.             print()
  48.  
  49.     def save(self, assignment, filename):
  50.         """
  51.        Save crossword assignment to an image file.
  52.        """
  53.         from PIL import Image, ImageDraw, ImageFont
  54.         cell_size = 100
  55.         cell_border = 2
  56.         interior_size = cell_size - 2 * cell_border
  57.         letters = self.letter_grid(assignment)
  58.  
  59.         # Create a blank canvas
  60.         img = Image.new(
  61.             "RGBA",
  62.             (self.crossword.width * cell_size,
  63.              self.crossword.height * cell_size),
  64.             "black"
  65.         )
  66.         font = ImageFont.truetype("assets/fonts/OpenSans-Regular.ttf", 80)
  67.         draw = ImageDraw.Draw(img)
  68.  
  69.         for i in range(self.crossword.height):
  70.             for j in range(self.crossword.width):
  71.  
  72.                 rect = [
  73.                     (j * cell_size + cell_border,
  74.                      i * cell_size + cell_border),
  75.                     ((j + 1) * cell_size - cell_border,
  76.                      (i + 1) * cell_size - cell_border)
  77.                 ]
  78.                 if self.crossword.structure[i][j]:
  79.                     draw.rectangle(rect, fill="white")
  80.                     if letters[i][j]:
  81.                         _, _, w, h = draw.textbbox(
  82.                             (0, 0), letters[i][j], font=font)
  83.                         draw.text(
  84.                             (rect[0][0] + ((interior_size - w) / 2),
  85.                              rect[0][1] + ((interior_size - h) / 2) - 10),
  86.                             letters[i][j], fill="black", font=font
  87.                         )
  88.  
  89.         img.save(filename)
  90.  
  91.     def solve(self):
  92.         """
  93.        Enforce node and arc consistency, and then solve the CSP.
  94.        """
  95.         self.enforce_node_consistency()
  96.         self.ac3()
  97.         return self.backtrack(dict())
  98.  
  99.     def enforce_node_consistency(self):
  100.         """
  101.        Update `self.domains` such that each variable is node-consistent.
  102.        (Remove any values that are inconsistent with a variable's unary
  103.         constraints; in this case, the length of the word.)
  104.        """
  105.         for variable in self.crossword.variables:
  106.             words = self.domains[variable].copy()
  107.             for word in words:
  108.                 if len(word) != variable.length:
  109.                     self.domains[variable].remove(word)
  110.  
  111.     def revise(self, X, Y):
  112.         """
  113.        Make variable `X` arc consistent with variable `Y`.
  114.        To do so, remove values from `self.domains[X]` for which there is no
  115.        possible corresponding value for `Y` in `self.domains[Y]`.
  116.  
  117.        Return True if a revision was made to the domain of `X`; return
  118.        False if no revision was made.
  119.        """
  120.         revised = False
  121.         overlap = self.crossword.overlaps[X, Y]
  122.  
  123.         if bool(overlap) == False:  # no overlap constraints between the two variables
  124.             return revised
  125.         else:
  126.             X_domain = self.domains[X].copy()
  127.             Y_domain = self.domains[Y].copy()
  128.             for x_word in X_domain:
  129.                 satisfy_constrain = self.satisfy(overlap, Y_domain, x_word)
  130.                 if satisfy_constrain is False:
  131.                     self.domains[X].remove(x_word)
  132.                     revised = True
  133.         return revised
  134.  
  135.     def ac3(self, arcs=None):
  136.         """
  137.        Update `self.domains` such that each variable is arc consistent.
  138.        If `arcs` is None, begin with initial list of all arcs in the problem.
  139.        Otherwise, use `arcs` as the initial list of arcs to make consistent.
  140.  
  141.        Return True if arc consistency is enforced and no domains are empty;
  142.        return False if one or more domains end up empty.
  143.        """
  144.         q = queue.Queue()
  145.  
  146.         if arcs == None:
  147.             arcs = [(X, Y)
  148.                     for X in list(self.crossword.variables)
  149.                     for Y in list(self.crossword.variables)
  150.                     if X != Y]
  151.  
  152.         for arc in arcs:
  153.             q.put(arc)
  154.  
  155.         while not q.empty():
  156.             x_y = q.get()
  157.             x = x_y[0]
  158.             y = x_y[1]
  159.             if self.revise(x, y):
  160.                 if len(self.domains[x]) == 0:
  161.                     return False
  162.                 for z in self.crossword.neighbors(x)-{y}:
  163.                     q.put((z, x))
  164.         return True
  165.  
  166.     def assignment_complete(self, assignment):
  167.         """
  168.        Return True if `assignment` is complete (i.e., assigns a value to each
  169.        crossword variable); return False otherwise.
  170.        """
  171.         return all(
  172.             bool(assignment.get(x)) == True
  173.             for x in self.crossword.variables
  174.         )
  175.  
  176.     def consistent(self, assignment):
  177.         """
  178.        Return True if `assignment` is consistent (i.e., words fit in crossword
  179.        puzzle without conflicting characters); return False otherwise.
  180.        """
  181.         if len(assignment)==0:
  182.             return True
  183.  
  184.         has_correct_length = all(
  185.             len(assignment[x]) == x.length
  186.             for x in assignment)
  187.  
  188.         values_distinct = all(
  189.             assignment[x] != assignment[y]
  190.             for x in assignment
  191.             for y in assignment
  192.             if(x != y))
  193.  
  194.         no_conflicts = all(
  195.             assignment[x][ij[0]] == assignment[y][ij[1]]
  196.             for x in assignment
  197.             for y in assignment
  198.             if( y in self.crossword.neighbors(x)) is True
  199.             if (ij := self.crossword.overlaps[x, y]) is not None
  200.         )
  201.         return has_correct_length and values_distinct and no_conflicts
  202.  
  203.     def order_domain_values(self, var, assignment):
  204.         """
  205.        Return a list of values in the domain of `var`, in order by
  206.        the number of values they rule out for neighboring variables.
  207.        The first value in the list, for example, should be the one
  208.        that rules out the fewest values among the neighbors of `var`.
  209.        """
  210.         ordered_words = {}
  211.         neighbors = self.crossword.neighbors(var)-set(assignment)
  212.         words = self.domains[var]
  213.  
  214.         for neighbor in neighbors:
  215.             ij = self.crossword.overlaps[var, neighbor]
  216.             for word_1 in words:
  217.                 n = 0
  218.                 for word_2 in self.domains[neighbor]:
  219.                     if word_1 != word_2:
  220.                         if word_1[ij[0]] != word_2[ij[1]]:
  221.                             n += 1
  222.                     ordered_words.update({word_1: n})
  223.         res =  dict(sorted(ordered_words.items(), key=itemgetter(1)))
  224.        
  225.         if len(res)!=0:
  226.             return list(res.keys())
  227.         else:
  228.             return words
  229.    
  230.     def select_unassigned_variable(self, assignment):
  231.         """
  232.        Return an unassigned variable not already part of `assignment`.
  233.        Choose the variable with the minimum number of remaining values
  234.        in its domain. If there is a tie, choose the variable with the highest
  235.        degree. If there is a tie, any of the tied variables are acceptable
  236.        return values.
  237.        """
  238.         vars = self.crossword.variables - set(assignment)
  239.         unassigned = []
  240.  
  241.         for var in vars:
  242.             vals = self.order_domain_values(var, assignment)
  243.             val = len(vals)
  244.             degree = len(self.crossword.neighbors(var))
  245.             unassigned.append((var, val, degree))
  246.         unassigned.sort(key=itemgetter(1,2))
  247.         return unassigned[0][0]
  248.    
  249.     def backtrack(self, assignment):
  250.         """
  251.        Using Backtracking Search, take as input a partial assignment for the
  252.        crossword and return a complete assignment if possible to do so.
  253.  
  254.        `assignment` is a mapping from variables (keys) to words (values).
  255.  
  256.        If no assignment is possible, return None.
  257.        """
  258.         if self.assignment_complete(assignment):
  259.             return assignment
  260.        
  261.         var = self.select_unassigned_variable(assignment)
  262.         for value in self.order_domain_values(var, assignment):
  263.             if self.consistent(assignment):
  264.                 assignment.update({var: value})
  265.                 result = self.backtrack(assignment)
  266.                 if result is not None:
  267.                     return result
  268.             del assignment[var]
  269.         return None
  270.  
  271. ################### UTILITY #########################
  272.     def satisfy(self, overlap, Y_domain, x_word):
  273.         satisfy_constrain = any(
  274.             (x_word != y_word)
  275.             and
  276.             (x_word[overlap[0]] == y_word[overlap[1]])
  277.             for y_word in Y_domain
  278.         )
  279.         return satisfy_constrain
  280. ######################################################
  281.  
  282.  
  283. def main():
  284.  
  285.     # Check usage
  286.     if len(sys.argv) not in [3, 4]:
  287.         sys.exit("Usage: python generate.py structure words [output]")
  288.  
  289.     # Parse command-line arguments
  290.     structure = sys.argv[1]
  291.     words = sys.argv[2]
  292.     output = sys.argv[3] if len(sys.argv) == 4 else None
  293.  
  294.     # Generate crossword
  295.     crossword = Crossword(structure, words)
  296.     creator = CrosswordCreator(crossword)
  297.     assignment = creator.solve()
  298.  
  299.     # Print result
  300.     if assignment is None:
  301.         print("No solution.")
  302.     else:
  303.         creator.print(assignment)
  304.         if output:
  305.             creator.save(assignment, output)
  306.  
  307.  
  308. if __name__ == "__main__":
  309.     main()
  310.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement