crosswords/server/crossword_generator.py

372 lines
11 KiB
Python
Raw Normal View History

2021-06-17 16:02:50 +02:00
import json
import random
import numpy as np
from string import digits
import pathlib
import logging
2021-07-03 20:21:04 +02:00
def get_difficulty_threshold(lang: str, difficulty: int):
return get_difficulty_threshold.thresholds[lang][difficulty]
get_difficulty_threshold.thresholds = {
'de': {
0: 12,
1: 6,
2: 0
},
'en': {
0: 200,
1: 100,
2: 10
}
}
2021-06-17 16:02:50 +02:00
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
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
logging.info("database loaded")
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
return get_database._dbs[lang]
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
get_database._dbs = {}
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
class NoDataException(Exception):
pass
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
class WordInfo(object):
2021-06-23 19:11:34 +02:00
def __init__(self, word: str, y: int, x: int, is_vertical: bool, database: dict, opposite_prefix: str = "opposite of", synonym_prefix: str = "other word for"):
2021-06-17 16:02:50 +02:00
self._dictionary_database = database
self._y = y
self._x = x
self._word = word
self._hint = None
self._is_vertical = is_vertical
2021-06-23 19:11:34 +02:00
self.opposite_prefix = opposite_prefix
self.synonym_prefix = synonym_prefix
2021-06-17 16:02:50 +02:00
self.choose_info()
def get_attribute(self, attr: str):
attr = self._dictionary_database[self._word][attr]
2021-06-23 19:11:34 +02:00
if attr is None or len(attr) == 0:
2021-06-17 16:02:50 +02:00
raise NoDataException
return attr
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
def get_best_antonym(self) -> str:
antonyms = self.get_attribute("antonyms")
return random.choice(antonyms)
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
def get_best_synonym(self) -> str:
synonyms = self.get_attribute("synonyms")
return random.choice(synonyms)
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
def get_best_sense(self) -> str:
senses = self.get_attribute("senses")
return random.choice(senses)
2021-06-23 19:11:34 +02:00
def choose_info(self, n: int = 1):
assert n <= 4
2021-06-17 16:02:50 +02:00
# first choose antonyms, then synonyms, then senses
2021-06-23 19:11:34 +02:00
hints = []
2021-06-17 16:02:50 +02:00
try:
2021-06-23 19:11:34 +02:00
antonyms = self.get_attribute("antonyms")
antonyms = [f"{self.opposite_prefix} {w}" for w in antonyms]
hints = hints + antonyms
2021-06-17 16:02:50 +02:00
except NoDataException:
pass
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
try:
2021-06-23 19:11:34 +02:00
synonyms = self.get_attribute("synonyms")
synonyms = [f"{self.synonym_prefix} {w}" for w in synonyms]
hints = hints + synonyms
2021-06-17 16:02:50 +02:00
except NoDataException:
pass
2021-06-23 19:11:34 +02:00
try:
senses = self.get_attribute("senses")
hints = hints + senses
except NoDataException:
pass
final_hints = []
for i in range(n):
choice = random.choice(hints)
hints.remove(choice)
final_hints.append(choice)
if n == 1:
self._hint = final_hints[0]
return
hint_symbols = ['a)', 'b)', 'c)', 'd)']
self._hint = ""
for i in range(n):
self._hint += hint_symbols[i] + " " + final_hints[i] + ". "
2021-06-17 16:02:50 +02:00
def get_hint(self) -> str:
return self._hint
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
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)
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
def is_vertical(self):
return self._is_vertical
2021-06-23 19:11:34 +02:00
2021-07-03 20:21:04 +02:00
def create_word_grid(w: int, h: int, lang_code: str = "en", target_density: float = 0.5, difficulty: int = 0):
logging.info("generate new crossword with params: w:%s h:%s lang:%s density:%s difficulty:%s",
str(w),
str(h),
lang_code,
str(target_density),
str(difficulty))
t_num_translations = get_difficulty_threshold(lang = lang_code, difficulty = difficulty)
2021-06-17 16:02:50 +02:00
2021-06-23 19:11:34 +02:00
database = get_database(lang=lang_code)
2021-06-17 16:02:50 +02:00
list_words = list(database.keys())
2021-06-23 19:11:34 +02:00
grid = np.full(shape=(h, w), dtype=np.unicode, fill_value=' ')
2021-06-17 16:02:50 +02:00
locations = {}
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
word_hints = {}
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
def store_location(char: str, y: int, x: int):
assert len(char) == 1
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
if char not in locations:
locations[char] = []
2021-06-23 19:11:34 +02:00
locations[char].append([y, x])
2021-06-17 16:02:50 +02:00
remove_digits = str.maketrans('', '', digits)
n_words = len(list_words)
2021-06-23 19:11:34 +02:00
def get_word(max_length: int, min_length=0):
2021-06-17 16:02:50 +02:00
assert max_length > 1
2021-06-23 19:11:34 +02:00
index = random.randint(0, n_words-1)
2021-06-17 16:02:50 +02:00
word = list_words[index][:]
2021-06-23 19:11:34 +02:00
2021-07-03 20:21:04 +02:00
num_translations = database[word]['num_translations']
t = t_num_translations
while len(word) >= max_length or not word.isalnum() or len(word) <= min_length or num_translations < t:
2021-06-23 19:11:34 +02:00
index = random.randint(0, n_words-1)
2021-06-17 16:02:50 +02:00
word = list_words[index][:]
2021-07-03 20:21:04 +02:00
num_translations = database[word]['num_translations']
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
return word
2021-06-23 19:11:34 +02:00
def normalize_word(word: str):
2021-06-17 16:02:50 +02:00
word = word.translate(remove_digits)
return word.lower()
2021-06-23 19:11:34 +02:00
opposite_prefix = "opposite of" if lang_code == "en" else "Gegenteil von"
synonym_prefix = "other word for" if lang_code == "en" else "anderes Wort für"
def place_word(word: str, y: int, x: int, vertical: bool = False):
2021-06-17 16:02:50 +02:00
normalized_word = normalize_word(word)
n = len(normalized_word)
if vertical:
assert grid.shape[0] - n >= y
for i, char in enumerate(normalized_word):
2021-06-23 19:11:34 +02:00
grid[y + i, x] = char
2021-06-17 16:02:50 +02:00
store_location(char, y+i, x)
else:
assert grid.shape[1] - n >= x
for i, char in enumerate(normalized_word):
2021-06-23 19:11:34 +02:00
grid[y, x + i] = char
2021-06-17 16:02:50 +02:00
store_location(char, y, x+i)
2021-06-23 19:11:34 +02:00
word_hints[normalized_word] = WordInfo(
word, y, x, vertical, database, opposite_prefix, synonym_prefix)
2021-06-17 16:02:50 +02:00
def density():
return 1 - (grid == " ").sum() / (w * h)
2021-06-23 19:11:34 +02:00
def check_if_fits(word: str, y: int, x: int, vertical: bool):
2021-06-17 16:02:50 +02:00
n = len(word)
if vertical:
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
# check if there is space before and after
if y - 1 >= 0 and grid[y - 1, x] != " ":
return False
2021-06-24 12:10:59 +02:00
if y + n < grid.shape[0] and grid[y+n, x] != " ":
2021-06-17 16:02:50 +02:00
return False
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
if grid.shape[0] - n < y or y < 0:
# print("over board")
return False
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
for i, char in enumerate(word):
char_x = x
char_y = y + i
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
if not (grid[char_y, char_x] == " " or grid[char_y, char_x] == char):
# print("not matching")
return False
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
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
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
else:
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
# check if there is space before and after
if x - 1 >= 0 and grid[y, x - 1] != " ":
return False
2021-06-24 12:10:59 +02:00
if x + n < grid.shape[1] and grid[y, x + n] != " ":
2021-06-17 16:02:50 +02:00
return False
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
if grid.shape[1] - n < x or x < 0:
# print("over board")
return False
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
for i, char in enumerate(word):
char_x = x + i
char_y = y
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
if not (grid[char_y, char_x] == " " or grid[char_y, char_x] == char):
# print("not matching")
return False
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
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
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
return True
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
def get_crossover(word: str):
# returns Tuple of: (y,x, is_vertical?) or None
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
shuffled_order = list(range(len(word)))
random.shuffle(shuffled_order)
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
for index in shuffled_order:
# check for existing locations
char = word[index]
if char in locations:
char_locations = locations[char]
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
for char_loc in char_locations:
# test vertical
y = char_loc[0] - index
x = char_loc[1]
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
if check_if_fits(word, y, x, vertical=True):
2021-06-23 19:11:34 +02:00
return (y, x, True)
2021-06-17 16:02:50 +02:00
# test horizontal
y = char_loc[0]
x = char_loc[1] - index
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
if check_if_fits(word, y, x, vertical=False):
2021-06-23 19:11:34 +02:00
return (y, x, False)
2021-06-17 16:02:50 +02:00
return None
2021-06-23 19:11:34 +02:00
def get_solution_word(min_length=8, max_length=100):
word = get_word(min_length=min_length, max_length=max_length)
# search for matching characters in locations
locations_cpy = dict(locations)
solution_locations = []
for char in word:
if char not in locations_cpy or len(locations_cpy[char]) == 0:
# next try:
2021-06-23 19:25:51 +02:00
return get_solution_word(min_length=min_length, max_length=max_length)
2021-06-23 19:11:34 +02:00
location_candidates = locations_cpy[char]
n = len(location_candidates)
i = random.randint(0, n-1)
solution_locations.append(location_candidates[i])
del(location_candidates[i])
return solution_locations
min_shape = min(w, h, 30)
2021-06-17 16:02:50 +02:00
# place first word:
2021-06-23 19:11:34 +02:00
first_word = get_word(max_length=min_shape,
min_length=min(10, grid.shape[1] - 2))
2021-06-17 16:02:50 +02:00
# find random place:
x = random.randint(0, grid.shape[1] - len(first_word) - 1)
y = random.randint(0, grid.shape[0] - 1)
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
place_word(first_word, y, x, vertical=False)
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
i = 0
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
current_density = density()
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
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))
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
normalized_word = normalize_word(word)
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
if normalized_word in word_hints:
continue
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
# check if matching characters exist:
crossover = get_crossover(normalized_word)
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
i += 1
2021-06-24 15:59:53 +02:00
if i % 1000 == 0:
2021-06-17 16:02:50 +02:00
print(i)
2021-06-24 15:59:53 +02:00
if i > 1200:
2021-06-17 16:02:50 +02:00
break
2021-06-23 19:11:34 +02:00
2021-06-17 16:02:50 +02:00
if crossover == None:
current_density = density()
continue
2021-06-23 19:11:34 +02:00
y, x, is_vertical = crossover
place_word(word, y, x, is_vertical)
2021-06-17 16:02:50 +02:00
current_density = density()
2021-06-23 19:11:34 +02:00
solution_word_locations = get_solution_word()
2021-06-17 16:02:50 +02:00
logging.info("crossword generation done after %s iterations", str(i))
2021-06-23 19:11:34 +02:00
return grid, word_hints, solution_word_locations