Make system conform to time limits and fix isolated detection.
This commit is contained in:
parent
68dcf78da8
commit
ff6dae5673
3 changed files with 73 additions and 40 deletions
70
basic_ops.py
70
basic_ops.py
|
@ -148,7 +148,7 @@ def is_valid(possibility, grid, words):
|
|||
|
||||
|
||||
def score_candidate(candidate_word, new_words):
|
||||
return len(candidate_word) + 100*len(new_words)
|
||||
return len(candidate_word) + 10*len(new_words)
|
||||
|
||||
|
||||
def add_word_to_grid(possibility, grid):
|
||||
|
@ -193,39 +193,37 @@ def create_empty_grid(dimensions):
|
|||
return [x[:] for x in [[0]*dimensions[1]]*dimensions[0]]
|
||||
|
||||
|
||||
def generate_valid_candidates(grid, words, dim):
|
||||
# Generate new candidates (think tournament selection)
|
||||
def generate_valid_candidates(grid, words, dim, timeout):
|
||||
# Generate new candidates
|
||||
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
|
||||
start_time = time.time()
|
||||
|
||||
# Generate a new candidate
|
||||
while score == None:
|
||||
# Increment search "time"
|
||||
tries += 1
|
||||
# Generate a new candidate
|
||||
while not candidates and time.time() < start_time + timeout:
|
||||
# Increment search "time"
|
||||
tries += 1
|
||||
|
||||
# Get new possibility
|
||||
new = generate_random_possibility(words, dim)
|
||||
# Get new possibility
|
||||
new = generate_random_possibility(words, dim)
|
||||
|
||||
# Evaluate validity
|
||||
if not is_valid(new, grid, words):
|
||||
continue
|
||||
# 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)
|
||||
# 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
|
||||
# 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)
|
||||
# Calculate this possibility's score
|
||||
score = score_candidate(new["word"], new_words)
|
||||
|
||||
# Add to list of candidates
|
||||
candidates.append(new)
|
||||
|
@ -239,6 +237,10 @@ def is_cell_free(line, col, grid):
|
|||
|
||||
Does not throw if the indices are out of bounds. These cases return as free.
|
||||
"""
|
||||
# Negative indices are "legal", but we treat them as out of bounds.
|
||||
if line < 0 or col < 0:
|
||||
return True
|
||||
|
||||
try:
|
||||
return grid[line][col] == 0
|
||||
except IndexError:
|
||||
|
@ -246,27 +248,30 @@ def is_cell_free(line, col, grid):
|
|||
|
||||
|
||||
def is_isolated(possibility, grid):
|
||||
""" Determines whether a given possibility is completely isolated in the given grid.
|
||||
|
||||
It is assumed that the possibility is valid, of course.
|
||||
"""
|
||||
# Import possibility to local vars, for clarity
|
||||
line = possibility["location"][0]
|
||||
column = possibility["location"][1]
|
||||
word = possibility["word"]
|
||||
direction = possibility["D"]
|
||||
|
||||
# The word cannot be isolated if there is something at its ends
|
||||
if not ends_are_isolated(word, line, column, direction, grid):
|
||||
return False
|
||||
|
||||
curr_line = line
|
||||
curr_col = column
|
||||
# Look at the cells that surround the word
|
||||
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):
|
||||
if not is_cell_free(line-1, column+i, grid) or not is_cell_free(line+1, column+i, 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):
|
||||
if not is_cell_free(line+i, column-1, grid) or not is_cell_free(line+i, column+1, grid):
|
||||
return False
|
||||
curr_line += 1
|
||||
|
||||
# If nothing was found, then the word is isolated
|
||||
return True
|
||||
|
||||
|
||||
|
@ -284,7 +289,12 @@ def basic_grid_fill(grid, occ_goal, timeout, dim, words):
|
|||
|
||||
while occupancy < occ_goal and time.time() - start_time < timeout:
|
||||
# Generate some candidates
|
||||
candidates, scores, new_words = generate_valid_candidates(grid, words, dim)
|
||||
# This is limited to 1/10 of the total time we can use.
|
||||
candidates, scores, new_words = generate_valid_candidates(grid, words, dim, timeout/10)
|
||||
|
||||
# If there are no candidates, we move to the next iteration. This ensures that we can actually respect timeouts.
|
||||
if not candidates:
|
||||
continue
|
||||
|
||||
# Select best candidate
|
||||
new, new_score = select_candidate(candidates, scores)
|
||||
|
|
|
@ -18,7 +18,6 @@ from grid_generator import GridGenerator
|
|||
def parse_cmdline_args():
|
||||
""" Uses argparse to get commands line args.
|
||||
"""
|
||||
# Parse arguments
|
||||
parser = argparse.ArgumentParser(description='Generate a crossword puzzle.')
|
||||
parser.add_argument('-f', type=str,
|
||||
default="words.txt",
|
||||
|
@ -38,18 +37,34 @@ def parse_cmdline_args():
|
|||
dest="timeout",
|
||||
help="Maximum execution time, in seconds, per execution loop.")
|
||||
parser.add_argument('-o', type=float,
|
||||
default=0.9,
|
||||
default=1.0,
|
||||
dest="target_occ",
|
||||
help="Desired occupancy of the final grid.")
|
||||
help="Desired occupancy of the final grid. Default is 1.0, which just uses all of the allotted time.")
|
||||
parser.add_argument('-p', type=str,
|
||||
default="out.pdf",
|
||||
dest="out_pdf",
|
||||
help="Name of the output pdf file.")
|
||||
parser.add_argument('-a', type=str,
|
||||
default="basic",
|
||||
dest="algorithm",
|
||||
help="The algorithm to use.")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Parse command line
|
||||
def create_generator(algorithm, word_list, dimensions, n_loops, timeout, target_occupancy):
|
||||
""" Constructs the generator object for the given algorithm.
|
||||
"""
|
||||
algorithm_class_map = {"basic": GridGenerator}
|
||||
|
||||
try:
|
||||
return algorithm_class_map[algorithm](word_list, dimensions, n_loops, timeout, target_occupancy)
|
||||
except KeyError:
|
||||
print("Could not create generator object for unknown algorithm: {}.".format(algorithm))
|
||||
|
||||
|
||||
def main():
|
||||
# Parse args
|
||||
args = parse_cmdline_args()
|
||||
|
||||
# Read words from file
|
||||
|
@ -58,7 +73,9 @@ if __name__ == "__main__":
|
|||
|
||||
# Construct the generator object
|
||||
dim = args.dim if len(args.dim)==2 else [args.dim[0], args.dim[0]]
|
||||
generator = GridGenerator(words, dim, args.n_loops, args.timeout, args.target_occ)
|
||||
generator = create_generator(args.algorithm, words, dim, args.n_loops, args.timeout, args.target_occ)
|
||||
if not generator:
|
||||
return
|
||||
|
||||
# Generate the grid
|
||||
generator.generate_grid()
|
||||
|
@ -68,3 +85,7 @@ if __name__ == "__main__":
|
|||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -10,10 +10,6 @@ class GridGenerator:
|
|||
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
|
||||
|
||||
|
@ -22,6 +18,8 @@ class GridGenerator:
|
|||
|
||||
def generate_grid(self):
|
||||
""" Updates the internal grid with content.
|
||||
|
||||
This is the main outward-facing function
|
||||
"""
|
||||
self.reset()
|
||||
print("Generating {} grid with {} words.".format(self.dimensions, len(self.word_list)))
|
||||
|
@ -37,6 +35,10 @@ class GridGenerator:
|
|||
|
||||
print("Built a grid of occupancy {}.".format(basic_ops.compute_occupancy(self.grid)))
|
||||
|
||||
def reset(self):
|
||||
self.grid = basic_ops.create_empty_grid(self.dimensions)
|
||||
self.words_in_grid = []
|
||||
|
||||
def generate_content_for_grid(self):
|
||||
""" Uses the basic fill algorithm to fill up the crossword grid.
|
||||
"""
|
||||
|
|
Loading…
Reference in a new issue