From ff6dae5673ab6f5ebdb259dc85a7609710c7a0f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20S=2E=20Martins?= Date: Tue, 8 Dec 2020 15:56:01 +0000 Subject: [PATCH] Make system conform to time limits and fix isolated detection. --- basic_ops.py | 70 ++++++++++++++++++++++++------------------ crossword_generator.py | 33 ++++++++++++++++---- grid_generator.py | 10 +++--- 3 files changed, 73 insertions(+), 40 deletions(-) diff --git a/basic_ops.py b/basic_ops.py index bd27974..da15731 100644 --- a/basic_ops.py +++ b/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) diff --git a/crossword_generator.py b/crossword_generator.py index f85967f..693ab1d 100755 --- a/crossword_generator.py +++ b/crossword_generator.py @@ -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() diff --git a/grid_generator.py b/grid_generator.py index 9faeaff..8ddb42d 100644 --- a/grid_generator.py +++ b/grid_generator.py @@ -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. """