introducing new grid generation

This commit is contained in:
Jonas Weinz 2021-08-31 13:56:29 +02:00
parent 04e82d0d60
commit 51d0610445
5 changed files with 4151 additions and 215 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -106,7 +106,7 @@ class LetterField(Field):
class Grid(object):
def __init__(self, width: int, height: int, lang_code: str, density=0.55, difficulty: int = 0):
def __init__(self, width: int, height: int, lang_code: str, density=0.8, difficulty: int = 0):
self._width = width
self._height = height
self._lang_code = lang_code

View File

@ -1,7 +1,8 @@
# load stuff
import json
import random
import numpy as np
from string import digits
from string import digits, ascii_lowercase
import pathlib
import logging
@ -12,37 +13,144 @@ def get_difficulty_threshold(lang: str, difficulty: int):
get_difficulty_threshold.thresholds = {
'de': {
0: 12,
0: 10,
1: 6,
2: 0
},
'en': {
0: 200,
0: 150,
1: 100,
2: 10
}
}
def get_database(lang: str = "en") -> dict:
def get_database(lang: str = "en", difficulty: int = -1) -> dict:
if lang not in get_database._dbs:
current_folder = pathlib.Path(__file__).parents[0]
try:
file = __file__
except:
file = "./.tmp"
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
get_database._dbs[lang] = {}
get_database._dbs[lang][-1] = db
logging.info("database loaded")
return get_database._dbs[lang]
if difficulty not in get_database._dbs[lang]:
t = get_difficulty_threshold(lang, difficulty)
logging.info(
"generate sub database for lang %s with difficulty %s", lang, str(difficulty))
db = get_database._dbs[lang][-1]
new_db = {}
for word_key, item in db.items():
num_translations = item['num_translations']
if num_translations >= t:
new_db[word_key] = item
get_database._dbs[lang][difficulty] = new_db
return get_database._dbs[lang][difficulty]
get_database._dbs = {}
def build_inverted_index(db):
inverted_db = {}
inverted_db['#'] = {}
number_db = inverted_db['#']
for letter in ascii_lowercase:
inverted_db[letter] = {}
for key, item in db.items():
try:
word = item['word']
norm_word = normalize_word(word)
n = len(norm_word)
if norm_word.isalnum():
for i, letter in enumerate(norm_word):
letter_db = inverted_db[letter]
if i not in letter_db:
letter_db[i] = {}
letter_db_i = letter_db[i]
if n not in letter_db_i:
letter_db_i[n] = []
if n not in number_db:
number_db[n] = []
letter_db_i[n].append(key)
number_db[n].append(key)
except:
pass
#print("error processing " + word)
return inverted_db
def get_inverted_database(lang: str, difficulty: int = -1) -> dict:
if lang not in get_inverted_database._dbs:
get_inverted_database._dbs[lang] = {}
if difficulty not in get_inverted_database._dbs[lang]:
get_inverted_database._dbs[lang][difficulty] = build_inverted_index(
get_database(lang, difficulty))
return get_inverted_database._dbs[lang][difficulty]
get_inverted_database._dbs = {}
remove_digits = str.maketrans('', '', digits)
def normalize_word(word: str):
word = word.translate(remove_digits)
return word.lower()
def find_suitable_words(constraints: list, db: dict, inverted_db: dict):
sets = []
n = len(constraints)
for i, letter in enumerate(constraints):
if letter == ' ':
continue
letter_db = inverted_db[letter]
if i in letter_db:
i_list = letter_db[i]
if not n in i_list:
return set()
sets.append(set(i_list[n]))
else:
return set()
# at least one constraint must be set
if len(sets) == 0:
# set first letter random and try again
if n in inverted_db['#']:
return inverted_db['#'][n]
return set()
return set.intersection(*sets)
class NoDataException(Exception):
pass
@ -134,7 +242,527 @@ class WordInfo(object):
return self._is_vertical
def create_word_grid(w: int, h: int, lang_code: str = "en", target_density: float = 0.5, difficulty: int = 0):
TYPE_EMPTY = -1
TYPE_NEIGHBOR = -2
TYPE_BLOCKED = -3
class GridCreationWord(object):
def __init__(self, y: int, x: int, length: int, is_vertical: bool, id: int) -> None:
self.y = y
self.x = x
self.length = length
self.is_vertical = is_vertical
self.id = id
self.word_key = None
self.connected_words = []
def get_letters(self, letter_grid: np.ndarray) -> list:
if self.is_vertical:
return letter_grid[self.y:self.y+self.length, self.x].flatten()
return letter_grid[self.y, self.x: self.x + self.length].flatten()
def write(self, word: str, letter_grid: np.ndarray, x_grid: np.ndarray, y_grid: np.ndarray):
letters = list(word)
if self.is_vertical:
xmin = max(self.x - 1, 0)
xmax = min(self.x + 2, letter_grid.shape[1])
ymin = self.y
ymax = self.y + self.length
letter_grid[ymin:ymax, self.x] = letters
conflicts = np.argwhere(
x_grid[ymin:ymax, self.x] == TYPE_NEIGHBOR
)
if len(conflicts) > 0:
corrected_conflicts = np.zeros(
shape=(len(conflicts), 2), dtype=np.int)
corrected_conflicts[:, 0] = ymin + conflicts.flatten()
corrected_conflicts[:, 1] = self.x
conflicts = corrected_conflicts
x_neighbors = x_grid[ymin:ymax, xmin:xmax]
x_neighbors[x_neighbors == TYPE_EMPTY] = TYPE_NEIGHBOR
x_grid[ymin:ymax, xmin:xmax] = x_neighbors
x_grid[ymin:ymax, self.x] = self.id
fields_to_block = y_grid[ymin:ymax, self.x]
fields_to_block[fields_to_block < 0] = TYPE_BLOCKED
y_grid[ymin:ymax, self.x] = fields_to_block
if ymin > 0:
x_grid[ymin - 1, self.x] = TYPE_BLOCKED
y_grid[ymin - 1, self.x] = TYPE_BLOCKED
if ymax < letter_grid.shape[0]:
x_grid[ymax, self.x] = TYPE_BLOCKED
y_grid[ymax, self.x] = TYPE_BLOCKED
else:
xmin = self.x
xmax = self.x + self.length
ymin = max(self.y - 1, 0)
ymax = min(self.y + 2, letter_grid.shape[0])
letter_grid[self.y, xmin:xmax] = letters
conflicts = np.argwhere(
y_grid[self.y, xmin:xmax] == TYPE_NEIGHBOR,
)
if len(conflicts) > 0:
corrected_conflicts = np.zeros(
shape=(len(conflicts), 2), dtype=np.int)
corrected_conflicts[:, 1] = xmin + conflicts.flatten()
corrected_conflicts[:, 0] = self.y
conflicts = corrected_conflicts
y_neighbors = y_grid[ymin:ymax, xmin:xmax]
y_neighbors[y_neighbors == TYPE_EMPTY] = TYPE_NEIGHBOR
y_grid[ymin:ymax, xmin:xmax] = y_neighbors
fields_to_block = x_grid[self.y, xmin:xmax]
fields_to_block[fields_to_block < 0] = TYPE_BLOCKED
x_grid[self.y, xmin:xmax] = fields_to_block
y_grid[self.y, xmin:xmax] = self.id
if xmin > 0:
x_grid[self.y, xmin - 1] = TYPE_BLOCKED
y_grid[self.y, xmin - 1] = TYPE_BLOCKED
if xmax < letter_grid.shape[1]:
x_grid[self.y, xmax] = TYPE_BLOCKED
y_grid[self.y, xmax] = TYPE_BLOCKED
return conflicts
def set_word_key(self, word_key: str):
self.word_key = word_key
def connect_word(self, grid_word):
self.connected_words.append(grid_word)
def get_connected_words(self):
return self.connected_words
def check_connected(self, grid_word):
if self.is_vertical == grid_word.is_vertical:
return False
if self.is_vertical:
if self.y > grid_word.y:
return False
if self.y + self.length <= grid_word.y:
return False
if self.x >= grid_word.x + grid_word.length:
return False
if self.x < grid_word.x:
return False
else:
if self.x > grid_word.x:
return False
if self.x + self.length <= grid_word.x:
return False
if self.y >= grid_word.y + grid_word.length:
return False
if self.y < grid_word.y:
return False
return True
class GridCreationState(object):
def __init__(self, h: int, w: int, db, inverted_db, old_state=None) -> None:
if old_state is not None:
self.h = h
self.w = w
self.db = db
self.inverted_db = inverted_db
self.x_grid = old_state.x_grid.copy()
self.y_grid = old_state.y_grid.copy()
self.letter_grid = old_state.letter_grid.copy()
self.placed_words = old_state.placed_words.copy()
self.used_word_keys = old_state.used_word_keys.copy()
return
self.h = h
self.w = w
self.x_grid = np.full(shape=(h, w), dtype=np.int,
fill_value=TYPE_EMPTY)
self.y_grid = np.full(shape=(h, w), dtype=np.int,
fill_value=TYPE_EMPTY)
self.letter_grid = np.full(
shape=(h, w), dtype=np.unicode, fill_value=' ')
self.placed_words = []
self.used_word_keys = set()
self.db = db
self.inverted_db = inverted_db
def write_word(self, word_key: str, y: int, x: int, is_vertical: bool):
id = len(self.placed_words)
word_raw = self.db[word_key]['word']
word_normalized = normalize_word(word_raw)
grid_word = GridCreationWord(y=y,
x=x,
length=len(word_normalized),
is_vertical=is_vertical, id=id)
grid_word.set_word_key(word_key=word_key)
conflicts = grid_word.write(word=word_normalized,
letter_grid=self.letter_grid,
x_grid=self.x_grid,
y_grid=self.y_grid)
self.placed_words.append(grid_word)
self.used_word_keys.add(word_key)
return conflicts
def copy(self):
return GridCreationState(self.h, self.w, self.db, self.inverted_db, self)
def get_density(self):
blocked_fields_x = np.logical_or(
self.x_grid >= 0, self.x_grid == TYPE_BLOCKED)
blocked_fields_y = np.logical_or(
self.y_grid >= 0, self.y_grid == TYPE_BLOCKED)
blocked_fields = np.logical_or(blocked_fields_x, blocked_fields_y)
return np.sum(blocked_fields) / (self.w * self.h)
def get_letters(self, y: int, x: int, length: int, is_vertical: bool):
if is_vertical:
return self.letter_grid[y:y+length, x].flatten()
return self.letter_grid[y, x:x+length].flatten()
def get_max_extents(self, y: int, x: int, is_vertical: bool):
# check min max offsets
if is_vertical:
min_coord = y - 1
if min_coord < 0 or self.y_grid[min_coord, x] == TYPE_BLOCKED:
min_coord = y
else:
while min_coord > 0 and self.y_grid[min_coord - 1, x] != TYPE_BLOCKED:
min_coord -= 1
max_coord = y + 1
while max_coord < self.h and self.y_grid[max_coord, x] != TYPE_BLOCKED:
max_coord += 1
return min_coord, max_coord
else:
min_coord = x - 1
if min_coord < 0 or self.x_grid[y, min_coord] == TYPE_BLOCKED:
min_coord = x
else:
while min_coord > 0 and self.x_grid[y, min_coord - 1] != TYPE_BLOCKED:
min_coord -= 1
max_coord = x + 1
while max_coord < self.w and self.x_grid[y, max_coord] != TYPE_BLOCKED:
max_coord += 1
return min_coord, max_coord
def expand_coordinates(self, y: int, x: int, length: int, is_vertical: bool):
if is_vertical:
min_coord = y
max_coord = y + length
while min_coord > 0 and self.y_grid[min_coord - 1, x] >= 0:
min_coord -= 1
while max_coord < self.h and self.y_grid[max_coord, x] >= 0:
max_coord += 1
return min_coord, max_coord
else:
min_coord = x
max_coord = x + length
while min_coord > 0 and self.x_grid[y, min_coord - 1] >= 0:
min_coord -= 1
while max_coord < self.w and self.x_grid[y, max_coord] >= 0:
max_coord += 1
return min_coord, max_coord
def place_random_word(self, min_length: int = 4, max_length: int = 15):
# first, find a random intersection
letter_locations = np.argwhere(self.letter_grid != ' ')
if len(letter_locations) == 0:
# if nothing is placed so far, just choose a random place
length = np.random.randint(min_length, max_length)
length = min(length, max_length)
y = np.random.randint(0, self.h - 1)
x = np.random.randint(0, self.w - length)
is_vertical = False
word_template = " " * length
else:
# possible candidates are fields where words are placed
# only horizontally or only vertically
candidates = np.argwhere(
np.logical_xor(self.x_grid >= 0, self.y_grid >= 0)
)
if len(candidates) == 0:
#print("field is full")
return None
candidate_index = random.randint(0, len(candidates) - 1)
y, x = candidates[candidate_index]
is_vertical = self.x_grid[y, x] == TYPE_BLOCKED
min_coord, max_coord = self.get_max_extents(y, x, is_vertical)
extent = max_coord - min_coord
if extent < min_length:
#print("not enough space to place a word")
return None
min_length = min(extent, min_length)
max_length = min(extent, max_length)
length = random.randint(min_length, max_length)
offset = random.randint(0, extent - length)
min_coord += offset
if is_vertical:
if min_coord + length <= y:
min_coord = y - length + 1
max_coord = min_coord + length
if min_coord > y:
min_coord = y
max_coord = min_coord + length
min_coord, max_coord = self.expand_coordinates(y=min_coord,
x=x,
length=length,
is_vertical=is_vertical)
length = max_coord - min_coord
letters = self.get_letters(min_coord, x, length, is_vertical)
y = min_coord
else:
if min_coord + length <= x:
min_coord = x - length + 1
max_coord = min_coord + length
if min_coord > x:
min_coord = x
max_coord = min_coord + length
min_coord, max_coord = self.expand_coordinates(y=y,
x=min_coord,
length=length,
is_vertical=is_vertical)
length = max_coord - min_coord
letters = self.get_letters(y, min_coord, length, is_vertical)
x = min_coord
word_template = "".join(letters)
word_candidates = list(find_suitable_words(
word_template, self.db, self.inverted_db))
if len(word_candidates) == 0:
#print("no word available for given combination")
return None
word_candidate_index = random.randint(0, len(word_candidates) - 1)
word_key = word_candidates[word_candidate_index]
if word_key in self.used_word_keys:
return None
return self.write_word(word_key, y, x, is_vertical)
def solve_conflicts(self, conflicts, n_retries=3, max_depth=5, depth=0):
if len(conflicts) == 0:
return self
# else:
# return None
if depth > max_depth:
return None
new_conflictes = []
for conflict in conflicts:
y, x = conflict
if self.x_grid[y, x] >= 0 and self.y_grid[y, x] >= 0:
# conflict already solved
continue
# find out whether the conflict is vertical or horizontal
is_vertical = self.y_grid[y, x] == TYPE_NEIGHBOR
# calculate the minimum and maximum extend to fix the conflict
if is_vertical:
max_ymin = y
while max_ymin > 0 and self.y_grid[max_ymin-1, x] >= 0:
max_ymin -= 1
min_ymax = y + 1
while min_ymax < self.h and self.y_grid[min_ymax, x] >= 0:
min_ymax += 1
min_ymin = max_ymin
while min_ymin > 0 and self.y_grid[min_ymin - 1, x] != TYPE_BLOCKED:
min_ymin -= 1
max_ymax = min_ymax
while max_ymax < self.h and self.y_grid[max_ymax, x] != TYPE_BLOCKED:
max_ymax += 1
min_coord_min = min_ymin
max_coord_min = max_ymin
min_coord_max = min_ymax
max_coord_max = max_ymax
else:
max_xmin = x
while max_xmin > 0 and self.x_grid[y, max_xmin - 1] >= 0:
max_xmin -= 1
min_xmax = x + 1
while min_xmax < self.w and self.x_grid[y, min_xmax] >= 0:
min_xmax += 1
min_xmin = max_xmin
while min_xmin > 0 and self.x_grid[y, min_xmin - 1] != TYPE_BLOCKED:
min_xmin -= 1
max_xmax = min_xmax
while max_xmax < self.w and self.x_grid[y, max_xmax] != TYPE_BLOCKED:
max_xmax += 1
min_coord_min = min_xmin
max_coord_min = max_xmin
min_coord_max = min_xmax
max_coord_max = max_xmax
n_options = max_coord_max - min_coord_max + max_coord_min - min_coord_min
solved = False
for _ in range(min(n_options, n_retries)):
coord_min = random.randint(min_coord_min, max_coord_min)
coord_max = random.randint(min_coord_max, max_coord_max)
length = coord_max - coord_min
if length < 2:
continue
if is_vertical:
coord_min, coord_max = self.expand_coordinates(y=coord_min,
x=x,
length=length,
is_vertical=is_vertical)
length = coord_max - coord_min
y = coord_min
else:
coord_min, coord_max = self.expand_coordinates(y=y,
x=coord_min,
length=length,
is_vertical=is_vertical)
length = coord_max - coord_min
x = coord_min
letters = self.get_letters(y, x, length, is_vertical)
word_template = "".join(letters)
candidates = list(find_suitable_words(
word_template, self.db, self.inverted_db))
if len(candidates) == 0:
continue
candidate_index = random.randint(0, len(candidates) - 1)
word_key = candidates[candidate_index]
if word_key in self.used_word_keys:
continue
word_conflicts = self.write_word(word_key, y, x, is_vertical)
if len(word_conflicts) > 0:
new_conflictes.append(word_conflicts)
solved = True
break
if not solved:
return None
if len(new_conflictes) == 0:
return self
new_conflictes = np.concatenate(new_conflictes)
for _ in range(n_retries):
next_state = self.copy()
solved_state = next_state.solve_conflicts(
new_conflictes, n_retries, max_depth, depth + 1)
if solved_state is not None:
return solved_state
return None
def fill_grid(self, target_density: float = 0.6, inner_retries: int = 5, conflict_retries: int = 10, conflict_solver_depth=5, min_length: int = 4, max_length: int = 10, max_iterations: int = 1000):
i = 0
state = self.copy()
while i < max_iterations and state.get_density() < target_density:
i += 1
new_state = state.copy()
conflicts = new_state.place_random_word(min_length, max_length)
if conflicts is None:
continue
if len(conflicts) == 0:
state = new_state
if len(conflicts) > 0:
solved_state = new_state.solve_conflicts(
conflicts, inner_retries, conflict_solver_depth)
if solved_state is not None:
state = solved_state
logging.info("finished after %s iterations, with a density of %s", str(
i), str(state.get_density()))
return state
def create_word_grid(w: int,
h: int,
lang_code: str = "en",
target_density: float = 0.8,
difficulty: int = 0):
logging.info("generate new crossword with params: w:%s h:%s lang:%s density:%s difficulty:%s",
str(w),
str(h),
@ -142,233 +770,86 @@ def create_word_grid(w: int, h: int, lang_code: str = "en", target_density: floa
str(target_density),
str(difficulty))
t_num_translations = get_difficulty_threshold(lang = lang_code, difficulty = difficulty)
db = get_database(lang_code, difficulty=difficulty)
inverted_db = get_inverted_database(lang_code, difficulty=difficulty)
database = get_database(lang=lang_code)
list_words = list(database.keys())
base_grid = GridCreationState(h=h, w=w, db=db, inverted_db=inverted_db)
grid = np.full(shape=(h, w), dtype=np.unicode, fill_value=' ')
final_state = base_grid.fill_grid(target_density=target_density,
inner_retries=7,
conflict_solver_depth=20,
min_length=2,
max_iterations=max(75 * (w+h)/2, 1000))
locations = {}
# generate word hints
word_hints = {}
def store_location(char: str, y: int, x: int):
assert len(char) == 1
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:"
if char not in locations:
locations[char] = []
for placed_word in final_state.placed_words:
word_key = placed_word.word_key
word = normalize_word(db[word_key]['word'])
y = placed_word.y
x = placed_word.x
is_vertical = placed_word.is_vertical
if [y,x] not in locations[char]:
locations[char].append([y, x])
word_info = WordInfo(word_key, y, x, is_vertical,
db, opposite_prefix, synonym_prefix)
#logging.info("word: %s, (%s,%s,%s): %s", word, str(y), str(x), str(is_vertical), word_info.get_hint())
word_hints[word_key] = word_info
remove_digits = str.maketrans('', '', digits)
n_words = len(list_words)
# create a solution word
def get_word(max_length: int, min_length=0):
assert max_length > 1
char_locations = {}
for char in list("abcdefghijklmnopqrstuvwxyz"):
char_locations[char] = np.argwhere(
final_state.letter_grid == char).tolist()
index = random.randint(0, n_words-1)
word = list_words[index][:]
words = list(db.keys())
n_words = len(words)
num_translations = database[word]['num_translations']
t = t_num_translations
min_solution_length = 10
max_solution_length = 20
while len(word) >= max_length or not word.isalnum() or len(word) <= min_length or num_translations < t:
index = random.randint(0, n_words-1)
word = list_words[index][:]
num_translations = database[word]['num_translations']
solution_word_locations = None
return word
while solution_word_locations is None:
def normalize_word(word: str):
word = word.translate(remove_digits)
return word.lower()
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):
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, opposite_prefix, synonym_prefix)
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] 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] 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
def get_solution_word(min_length=15, 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:
return get_solution_word(min_length=min_length, max_length=max_length)
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)
# 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:
random_index = random.randint(0, n_words - 1)
random_word_key = words[random_index]
random_word = db[random_word_key]['word']
normalized_random_word = normalize_word(random_word)
if len(normalized_random_word) < min_solution_length or len(normalized_random_word) > max_solution_length:
continue
# check if matching characters exist:
crossover = get_crossover(normalized_word)
char_locations_copy = {}
for char in char_locations:
char_locations_copy[char] = char_locations[char].copy()
i += 1
if i % 1000 == 0:
print(i)
if i > 1200:
break
solution = []
if crossover == None:
current_density = density()
aborted = False
for char in list(normalized_random_word):
if char not in char_locations_copy:
aborted = True
break
locations = char_locations_copy[char]
if len(locations) == 0:
aborted = True
break
i = random.randint(0, len(locations) - 1)
location = locations[i]
del(locations[i])
solution.append(location)
if aborted:
continue
y, x, is_vertical = crossover
solution_word_locations = solution
place_word(word, y, x, is_vertical)
current_density = density()
solution_word_locations = get_solution_word()
logging.info("crossword generation done after %s iterations", str(i))
return grid, word_hints, solution_word_locations
return final_state.letter_grid, word_hints, solution_word_locations

View File

@ -0,0 +1,374 @@
import json
import random
import numpy as np
from string import digits
import pathlib
import logging
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
}
}
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, opposite_prefix: str = "opposite of", synonym_prefix: str = "other word for"):
self._dictionary_database = database
self._y = y
self._x = x
self._word = word
self._hint = None
self._is_vertical = is_vertical
self.opposite_prefix = opposite_prefix
self.synonym_prefix = synonym_prefix
self.choose_info()
def get_attribute(self, attr: str):
attr = self._dictionary_database[self._word][attr]
if attr is None or len(attr) == 0:
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, n: int = 1):
assert n <= 4
# first choose antonyms, then synonyms, then senses
hints = []
try:
antonyms = self.get_attribute("antonyms")
antonyms = [f"{self.opposite_prefix} {w}" for w in antonyms]
hints = hints + antonyms
except NoDataException:
pass
try:
synonyms = self.get_attribute("synonyms")
synonyms = [f"{self.synonym_prefix} {w}" for w in synonyms]
hints = hints + synonyms
except NoDataException:
pass
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] + ". "
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: 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)
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] = []
if [y,x] not in 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][:]
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:
index = random.randint(0, n_words-1)
word = list_words[index][:]
num_translations = database[word]['num_translations']
return word
def normalize_word(word: str):
word = word.translate(remove_digits)
return word.lower()
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):
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, opposite_prefix, synonym_prefix)
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] 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] 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
def get_solution_word(min_length=15, 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:
return get_solution_word(min_length=min_length, max_length=max_length)
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)
# 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 % 1000 == 0:
print(i)
if i > 1200:
break
if crossover == None:
current_density = density()
continue
y, x, is_vertical = crossover
place_word(word, y, x, is_vertical)
current_density = density()
solution_word_locations = get_solution_word()
logging.info("crossword generation done after %s iterations", str(i))
return grid, word_hints, solution_word_locations