Re-organise code in preparation for future optimisation.
Performance didn't really change.
This commit is contained in:
parent
45d5535c44
commit
2f1b728eca
5 changed files with 573 additions and 421 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
*.txt
|
||||
*.pdf
|
||||
*.pyc
|
||||
*.zip
|
311
basic_ops.py
Normal file
311
basic_ops.py
Normal 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
|
|
@ -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
160
file_ops.py
Normal 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
66
grid_generator.py
Normal 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)
|
Loading…
Reference in a new issue