Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import sys
- from crossword import *
- import queue
- from operator import itemgetter
- class CrosswordCreator():
- def __init__(self, crossword):
- """
- Create new CSP crossword generate.
- """
- self.crossword = crossword
- self.domains = {
- var: self.crossword.words.copy()
- for var in self.crossword.variables
- }
- def letter_grid(self, assignment):
- """
- Return 2D array representing a given assignment.
- """
- letters = [
- [None for _ in range(self.crossword.width)]
- for _ in range(self.crossword.height)
- ]
- for variable, word in assignment.items():
- direction = variable.direction
- for k in range(len(word)):
- i = variable.i + (k if direction == Variable.DOWN else 0)
- j = variable.j + (k if direction == Variable.ACROSS else 0)
- letters[i][j] = word[k]
- return letters
- def print(self, assignment):
- """
- Print crossword assignment to the terminal.
- """
- letters = self.letter_grid(assignment)
- for i in range(self.crossword.height):
- for j in range(self.crossword.width):
- if self.crossword.structure[i][j]:
- print(letters[i][j] or " ", end="")
- else:
- print("█", end="")
- print()
- def save(self, assignment, filename):
- """
- Save crossword assignment to an image file.
- """
- from PIL import Image, ImageDraw, ImageFont
- cell_size = 100
- cell_border = 2
- interior_size = cell_size - 2 * cell_border
- letters = self.letter_grid(assignment)
- # Create a blank canvas
- img = Image.new(
- "RGBA",
- (self.crossword.width * cell_size,
- self.crossword.height * cell_size),
- "black"
- )
- font = ImageFont.truetype("assets/fonts/OpenSans-Regular.ttf", 80)
- draw = ImageDraw.Draw(img)
- for i in range(self.crossword.height):
- for j in range(self.crossword.width):
- rect = [
- (j * cell_size + cell_border,
- i * cell_size + cell_border),
- ((j + 1) * cell_size - cell_border,
- (i + 1) * cell_size - cell_border)
- ]
- if self.crossword.structure[i][j]:
- draw.rectangle(rect, fill="white")
- if letters[i][j]:
- _, _, w, h = draw.textbbox(
- (0, 0), letters[i][j], font=font)
- draw.text(
- (rect[0][0] + ((interior_size - w) / 2),
- rect[0][1] + ((interior_size - h) / 2) - 10),
- letters[i][j], fill="black", font=font
- )
- img.save(filename)
- def solve(self):
- """
- Enforce node and arc consistency, and then solve the CSP.
- """
- self.enforce_node_consistency()
- self.ac3()
- return self.backtrack(dict())
- def enforce_node_consistency(self):
- """
- Update `self.domains` such that each variable is node-consistent.
- (Remove any values that are inconsistent with a variable's unary
- constraints; in this case, the length of the word.)
- """
- for variable in self.crossword.variables:
- words = self.domains[variable].copy()
- for word in words:
- if len(word) != variable.length:
- self.domains[variable].remove(word)
- def revise(self, X, Y):
- """
- Make variable `X` arc consistent with variable `Y`.
- To do so, remove values from `self.domains[X]` for which there is no
- possible corresponding value for `Y` in `self.domains[Y]`.
- Return True if a revision was made to the domain of `X`; return
- False if no revision was made.
- """
- revised = False
- overlap = self.crossword.overlaps[X, Y]
- if bool(overlap) == False: # no overlap constraints between the two variables
- return revised
- else:
- X_domain = self.domains[X].copy()
- Y_domain = self.domains[Y].copy()
- for x_word in X_domain:
- satisfy_constrain = self.satisfy(overlap, Y_domain, x_word)
- if satisfy_constrain is False:
- self.domains[X].remove(x_word)
- revised = True
- return revised
- def ac3(self, arcs=None):
- """
- Update `self.domains` such that each variable is arc consistent.
- If `arcs` is None, begin with initial list of all arcs in the problem.
- Otherwise, use `arcs` as the initial list of arcs to make consistent.
- Return True if arc consistency is enforced and no domains are empty;
- return False if one or more domains end up empty.
- """
- q = queue.Queue()
- if arcs == None:
- arcs = [(X, Y)
- for X in list(self.crossword.variables)
- for Y in list(self.crossword.variables)
- if X != Y]
- for arc in arcs:
- q.put(arc)
- while not q.empty():
- x_y = q.get()
- x = x_y[0]
- y = x_y[1]
- if self.revise(x, y):
- if len(self.domains[x]) == 0:
- return False
- for z in self.crossword.neighbors(x)-{y}:
- q.put((z, x))
- return True
- def assignment_complete(self, assignment):
- """
- Return True if `assignment` is complete (i.e., assigns a value to each
- crossword variable); return False otherwise.
- """
- return all(
- bool(assignment.get(x)) == True
- for x in self.crossword.variables
- )
- def consistent(self, assignment):
- """
- Return True if `assignment` is consistent (i.e., words fit in crossword
- puzzle without conflicting characters); return False otherwise.
- """
- if len(assignment)==0:
- return True
- has_correct_length = all(
- len(assignment[x]) == x.length
- for x in assignment)
- values_distinct = all(
- assignment[x] != assignment[y]
- for x in assignment
- for y in assignment
- if(x != y))
- no_conflicts = all(
- assignment[x][ij[0]] == assignment[y][ij[1]]
- for x in assignment
- for y in assignment
- if( y in self.crossword.neighbors(x)) is True
- if (ij := self.crossword.overlaps[x, y]) is not None
- )
- return has_correct_length and values_distinct and no_conflicts
- def order_domain_values(self, var, assignment):
- """
- Return a list of values in the domain of `var`, in order by
- the number of values they rule out for neighboring variables.
- The first value in the list, for example, should be the one
- that rules out the fewest values among the neighbors of `var`.
- """
- ordered_words = {}
- neighbors = self.crossword.neighbors(var)-set(assignment)
- words = self.domains[var]
- for neighbor in neighbors:
- ij = self.crossword.overlaps[var, neighbor]
- for word_1 in words:
- n = 0
- for word_2 in self.domains[neighbor]:
- if word_1 != word_2:
- if word_1[ij[0]] != word_2[ij[1]]:
- n += 1
- ordered_words.update({word_1: n})
- res = dict(sorted(ordered_words.items(), key=itemgetter(1)))
- if len(res)!=0:
- return list(res.keys())
- else:
- return words
- def select_unassigned_variable(self, assignment):
- """
- Return an unassigned variable not already part of `assignment`.
- Choose the variable with the minimum number of remaining values
- in its domain. If there is a tie, choose the variable with the highest
- degree. If there is a tie, any of the tied variables are acceptable
- return values.
- """
- vars = self.crossword.variables - set(assignment)
- unassigned = []
- for var in vars:
- vals = self.order_domain_values(var, assignment)
- val = len(vals)
- degree = len(self.crossword.neighbors(var))
- unassigned.append((var, val, degree))
- unassigned.sort(key=itemgetter(1,2))
- return unassigned[0][0]
- def backtrack(self, assignment):
- """
- Using Backtracking Search, take as input a partial assignment for the
- crossword and return a complete assignment if possible to do so.
- `assignment` is a mapping from variables (keys) to words (values).
- If no assignment is possible, return None.
- """
- if self.assignment_complete(assignment):
- return assignment
- var = self.select_unassigned_variable(assignment)
- for value in self.order_domain_values(var, assignment):
- if self.consistent(assignment):
- assignment.update({var: value})
- result = self.backtrack(assignment)
- if result is not None:
- return result
- del assignment[var]
- return None
- ################### UTILITY #########################
- def satisfy(self, overlap, Y_domain, x_word):
- satisfy_constrain = any(
- (x_word != y_word)
- and
- (x_word[overlap[0]] == y_word[overlap[1]])
- for y_word in Y_domain
- )
- return satisfy_constrain
- ######################################################
- def main():
- # Check usage
- if len(sys.argv) not in [3, 4]:
- sys.exit("Usage: python generate.py structure words [output]")
- # Parse command-line arguments
- structure = sys.argv[1]
- words = sys.argv[2]
- output = sys.argv[3] if len(sys.argv) == 4 else None
- # Generate crossword
- crossword = Crossword(structure, words)
- creator = CrosswordCreator(crossword)
- assignment = creator.solve()
- # Print result
- if assignment is None:
- print("No solution.")
- else:
- creator.print(assignment)
- if output:
- creator.save(assignment, output)
- if __name__ == "__main__":
- main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement