import json import random import numpy as np from string import digits import pathlib import logging def get_database(lang: str = "en") -> dict: if lang not in get_database._dbs: current_folder = pathlib.Path(__file__).parents[0] db_file = str(current_folder / f"{lang}.json") logging.info("loading database: %s", lang) with open(db_file, "r") as f: db = json.load(f) get_database._dbs[lang] = db logging.info("database loaded") return get_database._dbs[lang] get_database._dbs = {} class NoDataException(Exception): pass class WordInfo(object): def __init__(self, word:str, y:int, x:int, is_vertical: bool, database: dict): self._dictionary_database = database self._y = y self._x = x self._word = word self._hint = None self._is_vertical = is_vertical self.choose_info() def get_attribute(self, attr: str): attr = self._dictionary_database[self._word][attr] if attr is None: raise NoDataException return attr def get_best_antonym(self) -> str: antonyms = self.get_attribute("antonyms") return random.choice(antonyms) def get_best_synonym(self) -> str: synonyms = self.get_attribute("synonyms") return random.choice(synonyms) def get_best_sense(self) -> str: senses = self.get_attribute("senses") return random.choice(senses) def choose_info(self): # first choose antonyms, then synonyms, then senses try: self._hint = f"opposite of {self.get_best_antonym()}" return except NoDataException: pass try: self._hint = f"other word for {self.get_best_synonym()}" return except NoDataException: pass self._hint = self.get_best_sense() def get_hint(self) -> str: return self._hint def get_hint_location(self): x = self._x if self._is_vertical else self._x - 1 y = self._y - 1 if self._is_vertical else self._y return (y, x) def is_vertical(self): return self._is_vertical def create_word_grid(w: int, h: int, lang_code: str = "en", target_density = 0.5): logging.info("generate new crossword") database = get_database(lang = lang_code) list_words = list(database.keys()) grid = np.full(shape=(h,w), dtype=np.unicode, fill_value = ' ') locations = {} word_hints = {} def store_location(char: str, y: int, x: int): assert len(char) == 1 if char not in locations: locations[char] = [] locations[char].append([y,x]) remove_digits = str.maketrans('', '', digits) n_words = len(list_words) def get_word(max_length: int, min_length = 0): assert max_length > 1 index = random.randint(0,n_words-1) word = list_words[index][:] while len(word) >= max_length or not word.isalnum() or len(word) <= min_length: index = random.randint(0,n_words-1) word = list_words[index][:] return word def normalize_word(word:str): word = word.translate(remove_digits) return word.lower() def place_word(word:str, y: int, x:int, vertical:bool = False): normalized_word = normalize_word(word) n = len(normalized_word) if vertical: assert grid.shape[0] - n >= y for i, char in enumerate(normalized_word): grid[y + i,x] = char store_location(char, y+i, x) else: assert grid.shape[1] - n >= x for i, char in enumerate(normalized_word): grid[y,x + i] = char store_location(char, y, x+i) word_hints[normalized_word] = WordInfo(word, y, x, vertical, database) def density(): return 1 - (grid == " ").sum() / (w * h) def check_if_fits(word:str, y:int, x:int, vertical:bool): n = len(word) if vertical: # check if there is space before and after if y - 1 >= 0 and grid[y - 1, x] != " ": return False if y + n < grid.shape[0] - 1 and grid[y+n,x] != " ": return False if grid.shape[0] - n < y or y < 0: # print("over board") return False for i, char in enumerate(word): char_x = x char_y = y + i if not (grid[char_y, char_x] == " " or grid[char_y, char_x] == char): # print("not matching") return False if grid[char_y, char_x] == " ": # check for horizonatal neighbors: if char_x - 1 >= 0 and grid[char_y, char_x - 1] != " ": # print("3") return False if char_x + 1 < grid.shape[1] and grid[char_y, char_x + 1] != " ": # print("4") return False else: # check if there is space before and after if x - 1 >= 0 and grid[y, x - 1] != " ": return False if x + n < grid.shape[1] - 1 and grid[y,x + n] != " ": return False if grid.shape[1] - n < x or x < 0: # print("over board") return False for i, char in enumerate(word): char_x = x + i char_y = y if not (grid[char_y, char_x] == " " or grid[char_y, char_x] == char): # print("not matching") return False if grid[char_y, char_x] == " ": # check for vertical neighbors: if char_y - 1 >= 0 and grid[char_y - 1, char_x] != " ": # print("1") return False if char_y + 1 < grid.shape[0] and grid[char_y + 1, char_x] != " ": # print("2") return False return True def get_crossover(word: str): # returns Tuple of: (y,x, is_vertical?) or None shuffled_order = list(range(len(word))) random.shuffle(shuffled_order) for index in shuffled_order: # check for existing locations char = word[index] if char in locations: char_locations = locations[char] for char_loc in char_locations: # test vertical y = char_loc[0] - index x = char_loc[1] if check_if_fits(word, y, x, vertical=True): return (y,x,True) # test horizontal y = char_loc[0] x = char_loc[1] - index if check_if_fits(word, y, x, vertical=False): return (y,x,False) return None min_shape = min(w,h,30) # place first word: first_word = get_word(max_length=min_shape, min_length=min(10,grid.shape[1] - 2)) # find random place: x = random.randint(0, grid.shape[1] - len(first_word) - 1) y = random.randint(0, grid.shape[0] - 1) place_word(first_word, y, x, vertical=False) i = 0 current_density = density() while current_density < target_density: word = get_word(max_length=(1 - current_density ** 0.4) * min_shape, min_length=max(min(10, 0.5 * (1 - current_density ** 0.3) * min_shape), 2)) normalized_word = normalize_word(word) if normalized_word in word_hints: continue # check if matching characters exist: crossover = get_crossover(normalized_word) i += 1 if i % 100000 == 0: print(i) if i > 100000: break if crossover == None: current_density = density() continue y,x,is_vertical = crossover place_word(word, y,x, is_vertical) current_density = density() logging.info("crossword generation done after %s iterations", str(i)) return grid, word_hints