Re-organise code in preparation for future optimisation.

Performance didn't really change.
This commit is contained in:
Gonçalo S. Martins 2020-12-08 12:55:03 +00:00
parent 45d5535c44
commit 2f1b728eca
5 changed files with 573 additions and 421 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.txt
*.pdf
*.pyc
*.zip

311
basic_ops.py Normal file
View file

@ -0,0 +1,311 @@
import random
import time
def generate_random_possibility(words, dim):
""" This function returns a randomly-generated possibility, instead of generating all
possible ones.
"""
# Generate possibility
possibility = {"word": words[random.randint(0, len(words)-1)],
"location": [random.randint(0, dim[0]-1), random.randint(0, dim[1]-1)],
"D": "S" if random.random() > 0.5 else "E"}
# Return it
return possibility
def is_within_bounds(word_len, line, column, direction, grid_width, grid_height):
""" Returns whether the given word is withing the bounds of the grid.
"""
return (direction == "E" and column + word_len <= grid_width) or (direction == "S" and line + word_len <= grid_height)
def collides_with_existing_words(word, line, column, direction, grid):
""" Returns whether the given word collides with an existing one.
"""
for k, letter in enumerate(list(word)):
if direction == "E":
# Collisions
if grid[line][column+k] != 0 and grid[line][column+k] != letter:
return True
if direction == "S":
# Collisions
if grid[line+k][column] != 0 and grid[line+k][column] != letter:
return True
return False
def ends_are_isolated(word, line, column, direction, grid):
""" Returns whether the given word is isolated (blank before start and after end).
"""
if direction == "E":
# If the preceding space isn't empty
if not is_cell_free(line, column-1, grid):
return False
# If the succeding space isn't empy
if not is_cell_free(line, column+len(word), grid):
return False
if direction == "S":
# If the preceding space isn't empty
if not is_cell_free(line-1, column, grid):
return False
# If the succeding space isn't empy
if not is_cell_free(line+len(word), column, grid):
return False
return True
def find_new_words(word, line, column, direction, grid, words):
""" Given a new potential word, looks for new words that might have been created by adding it to the grid.
Returns None if new words are (geometrically) created but are not valid.
"""
new_words = []
for k, letter in enumerate(list(word)):
if direction == "E":
# If the space was originally blank and there are adjacent letters
if grid[line][column+k] == 0 and (line > 0 and grid[line-1][column+k] != 0 or line < len(grid)-1 and grid[line+1][column+k]):
# Then we have to extract this new word
poss_word = [letter]
l = 1
while line+l < len(grid[0]) and grid[line+l][column+k] != 0:
poss_word.append(grid[line+l][column+k])
l+=1
l = 1
while line-l > 0 and grid[line-l][column+k] != 0:
poss_word.insert(0, grid[line-l][column+k])
l+=1
poss_word = ''.join(poss_word)
# And check if it exists in the list
if poss_word not in words:
return None
new_words.append({"D": "S", "word":poss_word, "location": [line-l+1, column+k]})
if direction == "S":
# If the space was originally blank and there are adjacent letter
if grid[line+k][column] == 0 and (column > 0 and grid[line+k][column-1] != 0 or column < len(grid[0])-1 and grid[line+k][column+1]):
# Then we have to extract this new word
poss_word = [letter]
l = 1
while column+l < len(grid) and grid[line+k][column+l] != 0:
poss_word.append(grid[line+k][column+l])
l+=1
l = 1
while column-l > 0 and grid[line+k][column-l] != 0:
poss_word.insert(0, grid[line+k][column-l])
l+=1
poss_word = ''.join(poss_word)
# And check if it exists in the list
if poss_word not in words:
return None
new_words.append({"D": "E", "word":poss_word, "location": [line+k,column-l+1]})
return new_words
def is_valid(possibility, grid, words):
""" This function determines whether a possibility is still valid in the
given grid. (see generate_grid)
A possibility is deemed invalid if:
-> it extends out of bounds
-> it collides with any word that already exists, i.e. if any of its
elements does not match the words already in the grid;
-> if the cell that precedes and follows it in its direction is not empty.
The function also analyses how the word interacts with previous adjacent
words, and invalidates the possibility of returns a list with the new
words, if applicable.
"""
# Import possibility to local vars, for clarity
i = possibility["location"][0]
j = possibility["location"][1]
word = possibility["word"]
D = possibility["D"]
# Boundaries
if not is_within_bounds(len(word), i, j, D, len(grid[0]), len(grid)):
return False
# Collisions
if collides_with_existing_words(word, i, j, D, grid):
return False
# Start and End
if not ends_are_isolated(word, i, j, D, grid):
return False
# If we can't find any issues, it must be okay!
return True
def score_candidate(candidate_word, new_words):
return len(candidate_word) + 100*len(new_words)
def add_word_to_grid(possibility, grid):
""" Adds a possibility to the given grid, which is modified in-place.
(see generate_grid)
"""
# Import possibility to local vars, for clarity
i = possibility["location"][0]
j = possibility["location"][1]
word = possibility["word"]
# Word is left-to-right
if possibility["D"] == "E":
grid[i][j:len(list(word))+j] = list(word)
# Word is top-to-bottom
# (I can't seem to be able to use the slicing as above)
if possibility["D"] == "S":
for index, a in enumerate(list(word)):
grid[i+index][j] = a
def select_candidate(candidates, scores):
""" Select the candidate with the maximum score
"""
max_score = max(scores)
idx = scores.index(max_score)
return candidates[idx], scores[idx]
def compute_occupancy(grid):
return 1 - (sum(x.count(0) for x in grid) / (len(grid[0])*len(grid)))
def create_empty_grid(dimensions):
""" Creates an empty grid with the given dimensions.
dimensions[0] -> lines
dimensions[1] -> columns
"""
return [x[:] for x in [[0]*dimensions[1]]*dimensions[0]]
def generate_valid_candidates(grid, words, dim):
# Generate new candidates (think tournament selection)
candidates = []
scores = []
new_words = []
tries = 0
# While we don't have any, or we have and have been searching for a short time
while not candidates or (candidates and tries < 100):
score = None
# Generate a new candidate
while score == None:
# Increment search "time"
tries += 1
# Get new possibility
new = generate_random_possibility(words, dim)
# Evaluate validity
if not is_valid(new, grid, words):
continue
# Find new words that this possibility generates
new_words = find_new_words(new["word"], new["location"][0], new["location"][1], new["D"], grid, words)
# If new_words is None, then the possibility is invalid
if new_words == None:
new_words = []
continue
# Calculate this possibility's score
score = score_candidate(new["word"], new_words)
# Add to list of candidates
candidates.append(new)
scores.append(score)
return candidates, scores, new_words
def is_cell_free(line, col, grid):
""" Checks whether a cell is free.
Does not throw if the indices are out of bounds. These cases return as free.
"""
try:
return grid[line][col] == 0
except IndexError:
return True
def is_isolated(possibility, grid):
# Import possibility to local vars, for clarity
line = possibility["location"][0]
column = possibility["location"][1]
word = possibility["word"]
direction = possibility["D"]
if not ends_are_isolated(word, line, column, direction, grid):
return False
curr_line = line
curr_col = column
for i in range(len(word)):
if direction == "E":
if not is_cell_free(curr_line-1, curr_col, grid) or not is_cell_free(curr_line+1, curr_col, grid):
return False
curr_col += 1
if direction == "S":
if not is_cell_free(curr_line, curr_col+1, grid) or not is_cell_free(curr_line, curr_col+1, grid):
return False
curr_line += 1
return True
def basic_grid_fill(grid, occ_goal, timeout, dim, words):
""" Actually finds valid possibilities, scores them and adds them to the grid.
Algorithm:
This function operates by taking the words it receives randomly generating possibilities
until a valid one is found. It is then added to the grid.
This is done until the grid is above a given completion level.
"""
start_time = time.time()
occupancy = 0
added_words = []
while occupancy < occ_goal and time.time() - start_time < timeout:
# Generate some candidates
candidates, scores, new_words = generate_valid_candidates(grid, words, dim)
# Select best candidate
new, new_score = select_candidate(candidates, scores)
# Add word to grid and to the list of added words
add_word_to_grid(new, grid)
added_words.append(new)
# Add new words to the words list
for word in new_words:
added_words.append(word)
# Remove words from list so we don't repeat ourselves
words.remove(new["word"])
for word in new_words:
words.remove(word["word"])
# Update occupancy
occupancy = compute_occupancy(grid)
print("Word \"{}\" added. Occupancy: {:2.3f}. Score: {}.".format(new["word"], occupancy, new_score))
if new_words:
print("This also created the words:", new_words)
return added_words

View file

@ -2,417 +2,22 @@
""" Crossword Generator
This script takes a list of words and creates a new latex table representing a
crosswod puzzle, which is then printed to PDF, and can be printed to actual
crossword puzzle, which is then printed to PDF. You can then print it to actual
paper, if you're one of those people.
"""
# STL imports
import random
import pprint
import subprocess
import tempfile
import os
import shutil
import time
# Standard imports
import argparse
# Custom imports
import file_ops
import grid_generator
from grid_generator import GridGenerator
# Auxiliary Functions
def generate_single_possibility(words, dim):
""" This function returns a randomly-generated possibility, instead of generating all
possible ones.
def parse_cmdline_args():
""" Uses argparse to get commands line args.
"""
# Generate possibility
possibility = {"word": words[random.randint(0, len(words)-1)],
"location": [random.randint(0, dim[0]-1), random.randint(0, dim[1]-1)],
"D": "S" if random.random() > 0.5 else "E"}
# Return it
return possibility
def is_valid(possibility, grid, words):
""" This function determines whether a possibility is still valid in the
given grid. (see generate_grid)
A possibility is deemed invalid if:
-> it extends out of bounds
-> it collides with any word that already exists, i.e. if any of its
elements does not match the words already in the grid;
-> if the cell that precedes and succedes it in its direction is not empty.
The function also analyses how the word interacts with previous adjacent
words, and invalidates the possibility of returns a list with the new
words, if applicable.
"""
# Import possibility to local vars, for clarity
i = possibility["location"][0]
j = possibility["location"][1]
word = possibility["word"]
D = possibility["D"]
# Boundaries
if (D == "E" and j + len(word) > len(grid[0])) or (D == "S" and i + len(word) > len(grid)):
return [False, []]
# Collisions
for k, letter in enumerate(list(word)):
if D is "E":
# Collisions
if grid[i][j+k] != 0 and grid[i][j+k] != letter:
return [False, []]
if D is "S":
# Collisions
if grid[i+k][j] != 0 and grid[i+k][j] != letter:
return [False, []]
# Start and End
if D is "E":
# If the preceding space isn't empty
if j > 0 and grid[i][j-1] != 0:
return [False, []]
# If the succeding space isn't empy
if j+len(word) < len(grid[0]) and grid[i][j+len(word)] != 0:
return [False, []]
if D is "S":
# If the preceding space isn't empty
if i > 0 and grid[i-1][j] != 0:
return [False, []]
# If the succeding space isn't empy
if i+len(word) < len(grid) and grid[i+len(word)][j] != 0:
return [False, []]
# Detect if new words are formed
new_words = []
for k, letter in enumerate(list(word)):
if D is "E":
# If the space was originally blank and there are adjacent letters
if grid[i][j+k] == 0 and (i > 0 and grid[i-1][j+k] != 0 or i < len(grid)-1 and grid[i+1][j+k]):
# Then we have to extract this new word
poss_word = [letter]
l = 1
while i+l < len(grid[0]) and grid[i+l][j+k] != 0:
poss_word.append(grid[i+l][j+k])
l+=1
l = 1
while i-l > 0 and grid[i-l][j+k] != 0:
poss_word.insert(0, grid[i-l][j+k])
l+=1
poss_word = ''.join(poss_word)
# And check if it exists in the list
if poss_word not in words:
return [False, []]
new_words.append({"D": "S", "word":poss_word, "location": [i-l+1,j+k]})
if D is "S":
# If the space was originally blank and there are adjacent letter
if grid[i+k][j] == 0 and (j > 0 and grid[i+k][j-1] != 0 or j < len(grid[0])-1 and grid[i+k][j+1]):
# Then we have to extract this new word
poss_word = [letter]
l = 1
while j+l < len(grid) and grid[i+k][j+l] != 0:
poss_word.append(grid[i+k][j+l])
l+=1
l = 1
while j-l > 0 and grid[i+k][j-l] != 0:
poss_word.insert(0, grid[i+k][j-l])
l+=1
poss_word = ''.join(poss_word)
# And check if it exists in the list
if poss_word not in words:
return [False, []]
new_words.append({"D": "E", "word":poss_word, "location": [i+k,j-l+1]})
# If we can't find any issues, it must be okay!
return [True, new_words]
def is_disconnected(possibility, grid):
""" This function determines whether a given possibility would be placed as
a disconnected word on the grid, i.e. in a way that it crosses no other
word.
"""
# Import possibility to local vars, for clarity
i = possibility["location"][0]
j = possibility["location"][1]
word = possibility["word"]
D = possibility["D"]
# Detect collisions and proximity
for k, letter in enumerate(list(word)):
if D is "E":
# Collisions
if grid[i][j+k] != 0:
return False
if D is "S":
# Collisions
if grid[i+k][j] != 0:
return False
# If nothing is detected, it must be disconnected!
return True
def add_word_to_grid(possibility, grid):
""" Adds a possibility to the given grid, which is modified in-place.
(see generate_grid)
"""
# Import possibility to local vars, for clarity
i = possibility["location"][0]
j = possibility["location"][1]
word = possibility["word"]
# Word is left-to-right
if possibility["D"] == "E":
grid[i][j:len(list(word))+j] = list(word)
# Word is top-to-bottom
# (I can't seem to be able to use the slicing as above)
if possibility["D"] == "S":
for index, a in enumerate(list(word)):
grid[i+index][j] = a
def read_word_list(filename):
""" This function reads the file and returns the words read. It expects a
file where each word is in a line.
"""
# Initialize words list
words = []
# Quick'n'dirty file reading
with open(filename) as words_file:
for line in words_file:
words.append(line.strip())
# and we're done
return words
# Grid generation
def generate_grid(words, dim, timeout=60, occ_goal=0.9):
""" This function receives a list of words and creates a new grid, which
represents our puzzle. The newly-created grid is of dimensions
dim[0] * dim[1] (rows * columns). The function also receives a timeout,
which is used to control the time-consuming section of the code. If the
timeout is reached, the functions returns the best grid it was able to
achieve thus far. Lastly, occ_goal represents the fraction of squares that
should be, ideally, filled in.
Algorithm:
This function operates by taking the words it receives randomly generating possibilities
until a valid one is found. It is then added to the grid.
This is done until the grid is above a given completion level.
Return:
This function returns a dictionary, in which ["grid"] is the grid, and
"words" is the list of included words. The grid is a simple list of lists,
where zeroes represent the slots that were not filled in, with the
remaining slots containing a single letter each.
Assumptions:
Each possibility is a dictionary of the kind:
p["word"] = the actual string
p["location"] = the [i,j] (i is row and j is col) list with the location
p["D"] = the direction of the possibility (E for ->, S for down)
"""
print("Generating {} grid with {} words.".format(dim, len(words)))
# Initialize grid
grid = [x[:] for x in [[0]*dim[1]]*dim[0]]
# Initialize the list of added words
added_words = []
# Initialize time structure and occupancy
start_time = time.time()
occupancy = 0
# Main loop
while occupancy < occ_goal and time.time() - start_time < timeout:
# Generate new candidates (think tournament selection)
candidates = []
i = 0
# While we don't have any, or we have and have been searching for a short time
new_words = []
while not candidates or (candidates and i < 100):
valid = False
# Generate a new candidate
while not valid:
# Get new possibility
new = generate_single_possibility(words, dim)
# Evaluate validity and get new words generated
valid, new_words = is_valid(new, grid, words)
# Increment search "time"
i += 1
# Add to list of candidates
candidates.append(new)
# Sort candidates by length
candidates = sorted(candidates, key=lambda k: len(k['word']), reverse=True)
new = candidates[0]
# Get possible words generated by new candidate
# TODO: improve this crap
valid, new_words = is_valid(new, grid, words)
# Add word to grid and to the list of added words
add_word_to_grid(new, grid)
added_words.append(new)
# Add new words to the words list
for word in new_words:
added_words.append(word)
# Remove words from list
words.remove(new["word"])
for word in new_words:
words.remove(word["word"])
# Update occupancy
occupancy = 1 - (sum(x.count(0) for x in grid) / (dim[0]*dim[1]))
print("Word \"{}\" added. Occupancy: {:2.3f}.".format(new["word"],occupancy))
if new_words:
print("This also created the words:", new_words)
# Report and return the grid
print("Built a grid of occupancy {}.".format(occupancy))
return {"grid": grid, "words": added_words}
def write_grid(grid, screen=False, out_file="table.tex", out_pdf="out.pdf", keep_tex=False, words=[]):
""" This function receives the generated grid and writes it to the file (or
to the screen, if that's what we want). The grid is expected to be a list
of lists, as used by the remaining functions.
If a list of words is given, it is taken as the words used on the grid and
is printed as such.
"""
if screen is True:
# Print grid to the screen
for line in grid:
for element in line:
print(" {}".format(element), end="")
print()
else:
# Print grid to the file and compile
with open(out_file, "w") as texfile:
# Write preamble
texfile.write("\documentclass[a4paper]{article}" + "\n")
texfile.write(r"\usepackage[utf8]{inputenc}" + "\n")
texfile.write(r"\usepackage[table]{xcolor}" + "\n")
texfile.write(r"\usepackage{multicol}" + "\n")
texfile.write(r"\usepackage{fullpage}" + "\n")
texfile.write(r"\usepackage{graphicx}" + "\n")
texfile.write("\n")
texfile.write(r"\begin{document}" + "\n")
texfile.write(r"\section*{Challenge}" + "\n")
# Resize box
texfile.write(r"\resizebox{\textwidth}{!}{")
# Write table environment and format
texfile.write(r"\begin{tabular}{|")
for i in range(len(grid[0])):
texfile.write(r"c|")
texfile.write("}\n\hline\n")
# Write actual table
for line in grid:
for index, element in enumerate(line):
if element == 0:
texfile.write(r"\cellcolor{black}0")
# This feels a bit hacky, suggestions appreciated
if index != len(line)-1:
texfile.write(" & ")
texfile.write(r"\\ \hline" + "\n")
# End tabular environment
texfile.write("\end{tabular}\n")
texfile.write(r"}" + "\n\n")
# Write the words that were used
if words:
texfile.write(r"\section*{Words used for the problem}" + "\n")
# Write in several columns
texfile.write(r"\begin{multicols}{4}" + "\n")
texfile.write(r"\noindent" + "\n")
# Sort words by size
words.sort(key=lambda word: (len(word), word[0]))
# Write words
for word in words:
texfile.write(word + r"\\" + "\n")
# End multicolumn environment
texfile.write(r"\end{multicols}" + "\n")
# Page break and new section
texfile.write(r"\newpage" + "\n")
texfile.write(r"\section*{Solution}" + "\n")
# Write solution
# Resize box
texfile.write(r"\resizebox{\textwidth}{!}{")
# Write table environment and format
texfile.write(r"\begin{tabular}{|")
for i in range(len(grid[0])):
texfile.write(r"c|")
texfile.write("}\n\hline\n")
# Write actual table
for line in grid:
for index, element in enumerate(line):
if element == 0:
texfile.write(r"\cellcolor{black}0")
else:
texfile.write(str(element))
# This feels a bit hacky, suggestions appreciated
if index != len(line)-1:
texfile.write(" & ")
texfile.write(r"\\ \hline" + "\n")
# End tabular environment
texfile.write("\end{tabular}\n")
texfile.write(r"}")
# End document
texfile.write("\end{document}\n")
# Compile in a temp folder
# (inspired by
# https://stackoverflow.com/questions/19683123/compile-latex-from-python)
if not screen:
print("\n=== Compiling the generated latex file! ===")
with tempfile.TemporaryDirectory() as tmpdir:
# Save current directory
original_dir = os.getcwd()
# Copy the latex source to the temporary directory
shutil.copy(out_file, tmpdir)
# Move to the temp directory
os.chdir(tmpdir)
# Rename .tex file to generic name "out"
os.rename(out_file, "out.tex")
# Compile
proc = subprocess.call(['pdflatex', "out.tex"])
# Copy PDF back to the original directory
shutil.copy("out.pdf", original_dir+"/"+out_pdf)
# Move back to the original directory
os.chdir(original_dir)
print("=== Done! ===\n")
# Remove tex file?
if not keep_tex:
os.remove(out_file)
if __name__ == "__main__":
# Parse arguments
parser = argparse.ArgumentParser(description='Generate a crossword puzzle.')
parser.add_argument('-f', type=str,
@ -424,36 +29,42 @@ if __name__ == "__main__":
default=[20, 20],
dest="dim",
help="Dimensions of the grid to build.")
parser.add_argument('-n', type=int,
default=1,
dest="n_loops",
help="NUmber of execution loops to run.")
parser.add_argument('-t', type=int,
default=10,
dest="timeout",
help="Maximum execution time, in seconds.")
help="Maximum execution time, in seconds, per execution loop.")
parser.add_argument('-o', type=float,
default=0.9,
dest="target_occ",
help="Minimum desired occupancy of the final grid.")
help="Desired occupancy of the final grid.")
parser.add_argument('-p', type=str,
default="out.pdf",
dest="out_pdf",
help="Name of the output pdf file.")
args = parser.parse_args()
return parser.parse_args()
if __name__ == "__main__":
# Parse command line
args = parse_cmdline_args()
# Read words from file
words = read_word_list(args.word_file)
words = file_ops.read_word_list(args.word_file)
print("Read {} words from file.".format(len(words)))
# Filter small words
words = [x for x in words if len(x) > 2]
# Generate grid
# Construct the generator object
dim = args.dim if len(args.dim)==2 else [args.dim[0], args.dim[0]]
print("Making a grid of dimension {}, in {} seconds with a target occupancy of {}.".format(dim, args.timeout, args.target_occ))
grid = generate_grid(words, dim, timeout=args.timeout, occ_goal=args.target_occ)
generator = GridGenerator(words, dim, args.n_loops, args.timeout, args.target_occ)
# Print to file and compile
write_grid(grid["grid"], words=[x["word"] for x in grid["words"]], out_pdf=args.out_pdf)
# Generate the grid
generator.generate_grid()
# Show grid
print("Final grid:")
write_grid(grid["grid"], screen=True)
print("Words:")
pprint.pprint(grid["words"])
# Write it out
grid = generator.get_grid()
words_in_grid = generator.get_words_in_grid()
file_ops.write_grid_to_file(grid, words=[x["word"] for x in words_in_grid], out_pdf=args.out_pdf)
file_ops.write_grid_to_screen(grid, words_in_grid)

160
file_ops.py Normal file
View file

@ -0,0 +1,160 @@
import os
import pprint
import shutil
import subprocess
import tempfile
def read_word_list(filename, min_length=2, min_different_letters=2):
""" This function reads the file and returns the words read. It expects a
file where each word is in a line.
"""
# Initialize words list
words = []
# Get all the words
with open(filename, encoding='latin1') as words_file:
for line in words_file:
word = line.strip()
if len(word) > min_length and len(set(word)) > min_different_letters:
words.append(word)
# and we're done
return words
def write_grid_to_file(grid, out_file="table.tex", out_pdf="out.pdf", keep_tex=False, words=[]):
""" This function receives the generated grid and writes it to the file (or
to the screen, if that's what we want). The grid is expected to be a list
of lists, as used by the remaining functions.
If a list of words is given, it is taken as the words used on the grid and
is printed as such.
"""
# Print grid to the file and compile
with open(out_file, "w") as texfile:
# Write preamble
texfile.write("\documentclass[a4paper]{article}" + "\n")
texfile.write(r"\usepackage[utf8]{inputenc}" + "\n")
texfile.write(r"\usepackage[table]{xcolor}" + "\n")
texfile.write(r"\usepackage{multicol}" + "\n")
texfile.write(r"\usepackage{fullpage}" + "\n")
texfile.write(r"\usepackage{graphicx}" + "\n")
texfile.write("\n")
texfile.write(r"\begin{document}" + "\n")
texfile.write(r"\section*{Challenge}" + "\n")
# Resize box
texfile.write(r"\resizebox{\textwidth}{!}{")
# Write table environment and format
texfile.write(r"\begin{tabular}{|")
for i in range(len(grid[0])):
texfile.write(r"c|")
texfile.write("}\n\hline\n")
# Write actual table
for line in grid:
for index, element in enumerate(line):
if element == 0:
texfile.write(r"\cellcolor{black}0")
# This feels a bit hacky, suggestions appreciated
if index != len(line)-1:
texfile.write(" & ")
texfile.write(r"\\ \hline" + "\n")
# End tabular environment
texfile.write("\end{tabular}\n")
texfile.write(r"}" + "\n\n")
# Write the words that were used
if words:
texfile.write(r"\section*{Words used for the problem}" + "\n")
# Write in several columns
texfile.write(r"\begin{multicols}{4}" + "\n")
texfile.write(r"\noindent" + "\n")
# Sort words by size
words.sort(key=lambda word: (len(word), word[0]))
# Write words
for word in words:
texfile.write(word + r"\\" + "\n")
# End multicolumn environment
texfile.write(r"\end{multicols}" + "\n")
# Page break and new section
texfile.write(r"\newpage" + "\n")
texfile.write(r"\section*{Solution}" + "\n")
# Write solution
# Resize box
texfile.write(r"\resizebox{\textwidth}{!}{")
# Write table environment and format
texfile.write(r"\begin{tabular}{|")
for i in range(len(grid[0])):
texfile.write(r"c|")
texfile.write("}\n\hline\n")
# Write actual table
for line in grid:
for index, element in enumerate(line):
if element == 0:
texfile.write(r"\cellcolor{black}0")
else:
texfile.write(str(element))
# This feels a bit hacky, suggestions appreciated
if index != len(line)-1:
texfile.write(" & ")
texfile.write(r"\\ \hline" + "\n")
# End tabular environment
texfile.write("\end{tabular}\n")
texfile.write(r"}")
# End document
texfile.write("\end{document}\n")
# Compile in a temp folder
# (inspired by https://stackoverflow.com/questions/19683123/compile-latex-from-python)
print("\n=== Compiling the generated latex file! ===")
with tempfile.TemporaryDirectory() as tmpdir:
# Save current directory
original_dir = os.getcwd()
# Copy the latex source to the temporary directory
shutil.copy(out_file, tmpdir)
# Move to the temp directory
os.chdir(tmpdir)
# Rename .tex file to generic name "out"
os.rename(out_file, "out.tex")
# Compile
proc = subprocess.call(['pdflatex', "out.tex"])
# Copy PDF back to the original directory
shutil.copy("out.pdf", original_dir+"/"+out_pdf)
# Move back to the original directory
os.chdir(original_dir)
print("=== Done! ===\n")
# Remove tex file?
if not keep_tex:
os.remove(out_file)
def write_grid_to_screen(grid, words_in_grid):
# Print grid to the screen
print("Final grid:")
for line in grid:
for element in line:
print(" {}".format(element), end="")
print()
print("Words:")
pprint.pprint(words_in_grid)

66
grid_generator.py Normal file
View file

@ -0,0 +1,66 @@
import basic_ops
class GridGenerator:
def __init__(self, word_list, dimensions, n_loops, timeout, target_occupancy):
self.word_list = word_list
self.dimensions = dimensions
self.n_loops = n_loops
self.timeout = timeout
self.target_occupancy = target_occupancy
self.reset()
def reset(self):
self.grid = basic_ops.create_empty_grid(self.dimensions)
self.words_in_grid = []
def get_grid(self):
return self.grid
def get_words_in_grid(self):
return self.words_in_grid
def generate_grid(self):
""" Updates the internal grid with content.
"""
self.reset()
print("Generating {} grid with {} words.".format(self.dimensions, len(self.word_list)))
# Fill it up with the recommended number of loops
for i in range(self.n_loops):
print("Starting execution loop {}:".format(i+1))
self.generate_content_for_grid()
print("Culling isolated words.")
self.cull_isolated_words()
self.reset_grid_to_existing_words()
print("Built a grid of occupancy {}.".format(basic_ops.compute_occupancy(self.grid)))
def generate_content_for_grid(self):
""" Uses the basic fill algorithm to fill up the crossword grid.
"""
self.words_in_grid += basic_ops.basic_grid_fill(self.grid, self.target_occupancy, self.timeout, self.dimensions, self.word_list)
def cull_isolated_words(self):
""" Removes words that are too isolated from the grid
TODO: does not seem to work correctly yet.
"""
isolated_words = []
for word in self.words_in_grid:
if basic_ops.is_isolated(word, self.grid):
print("Culling word: {}.".format(word))
isolated_words.append(word)
for word in isolated_words:
self.words_in_grid.remove(word)
def reset_grid_to_existing_words(self):
""" Resets the stored grid to the words in self.words_in_grid
"""
self.grid = basic_ops.create_empty_grid(self.dimensions)
for word in self.words_in_grid:
basic_ops.add_word_to_grid(word, self.grid)