introducing new grid generation
This commit is contained in:
parent
04e82d0d60
commit
51d0610445
1658
data/better_grid_building.ipynb
Normal file
1658
data/better_grid_building.ipynb
Normal file
File diff suppressed because one or more lines are too long
1423
data/better_grid_building2.ipynb
Normal file
1423
data/better_grid_building2.ipynb
Normal file
File diff suppressed because one or more lines are too long
@ -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
|
||||
|
@ -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
|
||||
|
374
server/crossword_generator_old.py
Normal file
374
server/crossword_generator_old.py
Normal 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
|
Loading…
Reference in New Issue
Block a user