adding german option
This commit is contained in:
@ -105,12 +105,13 @@ class LetterField(Field):
|
||||
|
||||
|
||||
class Grid(object):
|
||||
def __init__(self, width: int, height: int, lang_code: str, density=0.5):
|
||||
def __init__(self, width: int, height: int, lang_code: str, density=0.55):
|
||||
self._width = width
|
||||
self._height = height
|
||||
self._lang_code = lang_code
|
||||
self._density = density
|
||||
self._grid = []
|
||||
self._solution_locations = None
|
||||
try:
|
||||
self._build_grid()
|
||||
except Exception as e:
|
||||
@ -245,12 +246,21 @@ class Grid(object):
|
||||
'y': y,
|
||||
'user_input': cell.get_user_content()
|
||||
}]
|
||||
|
||||
|
||||
return revealed_changes
|
||||
|
||||
def get_solution_locations(self):
|
||||
return self._solution_locations
|
||||
|
||||
def _build_grid(self):
|
||||
raw_grid, word_infos = crossword_generator.create_word_grid(
|
||||
self._width - 1, self._height - 1, lang_code="en", target_density=self._density)
|
||||
raw_grid, word_infos, solution_locations = crossword_generator.create_word_grid(
|
||||
self._width - 1, self._height - 1, lang_code=self._lang_code, target_density=self._density)
|
||||
|
||||
self._solution_locations = solution_locations
|
||||
# fix solution locations offsets
|
||||
for i in range(len(self._solution_locations)):
|
||||
self._solution_locations[i][0] += 1
|
||||
self._solution_locations[i][1] += 1
|
||||
|
||||
# note: we will append an additional row and column, to have enough space to place hint fields
|
||||
|
||||
@ -304,17 +314,18 @@ class Crossword(object):
|
||||
return {
|
||||
'w': self._width,
|
||||
'h': self._height,
|
||||
'grid': self._grid.serialize()
|
||||
'grid': self._grid.serialize(),
|
||||
'solution': self._grid.get_solution_locations()
|
||||
}
|
||||
|
||||
def user_input(self, x: int, y: int, letter: str) -> list:
|
||||
return self._grid.user_input(x=x, y=y, letter=letter)
|
||||
|
||||
|
||||
def get_status(self) -> list:
|
||||
return self._grid.get_status()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
cw = Crossword(15, 15, "en")
|
||||
cw = Crossword(30, 30, "de")
|
||||
print(cw.serialize())
|
||||
|
@ -14,7 +14,7 @@ class CrosswordConnection(json_websockets.JsonWebsocketConnection):
|
||||
|
||||
self._session = None
|
||||
|
||||
async def send_crossword(self, sessionId: str):
|
||||
async def send_crossword(self, sessionId: str, lang:str = "en"):
|
||||
if sessionId not in CrosswordConnection.sessions:
|
||||
await self.send_error(msg="unknown session")
|
||||
return
|
||||
@ -28,7 +28,7 @@ class CrosswordConnection(json_websockets.JsonWebsocketConnection):
|
||||
await self.send_error(msg="you are not registered to given session")
|
||||
return
|
||||
|
||||
crossword = sess.get_crossword()
|
||||
crossword = sess.get_crossword(lang=lang)
|
||||
await self.send({
|
||||
'type': 'crossword',
|
||||
'crossword': crossword.serialize()
|
||||
@ -53,7 +53,7 @@ class CrosswordConnection(json_websockets.JsonWebsocketConnection):
|
||||
'updates': update_message
|
||||
})
|
||||
|
||||
async def register(self, sessionId: str = None):
|
||||
async def register(self, sessionId: str = None, lang: str = "en"):
|
||||
|
||||
if sessionId is None:
|
||||
|
||||
@ -68,7 +68,7 @@ class CrosswordConnection(json_websockets.JsonWebsocketConnection):
|
||||
await self.send_error("unknown session id")
|
||||
|
||||
# register with new id:
|
||||
await self.register()
|
||||
await self.register(lang=lang)
|
||||
return
|
||||
|
||||
sess = CrosswordConnection.sessions[sessionId]
|
||||
@ -81,7 +81,7 @@ class CrosswordConnection(json_websockets.JsonWebsocketConnection):
|
||||
'sessionId': sessionId
|
||||
})
|
||||
|
||||
await self.send_crossword(sessionId)
|
||||
await self.send_crossword(sessionId, lang=lang)
|
||||
|
||||
async def send_error(self, msg: str):
|
||||
await self.send({
|
||||
@ -98,9 +98,12 @@ class CrosswordConnection(json_websockets.JsonWebsocketConnection):
|
||||
|
||||
if message['type'] == 'register':
|
||||
sessionId = None
|
||||
lang = "en"
|
||||
if 'sessionId' in message:
|
||||
sessionId = message['sessionId']
|
||||
await self.register(sessionId=sessionId)
|
||||
if "lang" in message:
|
||||
lang = message['lang']
|
||||
await self.register(sessionId=sessionId, lang = lang)
|
||||
return
|
||||
|
||||
if self._session is None:
|
||||
|
@ -16,157 +16,190 @@ def get_database(lang: str = "en") -> dict:
|
||||
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):
|
||||
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:
|
||||
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):
|
||||
|
||||
def choose_info(self, n: int = 1):
|
||||
assert n <= 4
|
||||
# first choose antonyms, then synonyms, then senses
|
||||
|
||||
|
||||
hints = []
|
||||
|
||||
try:
|
||||
self._hint = f"opposite of {self.get_best_antonym()}"
|
||||
return
|
||||
antonyms = self.get_attribute("antonyms")
|
||||
antonyms = [f"{self.opposite_prefix} {w}" for w in antonyms]
|
||||
hints = hints + antonyms
|
||||
except NoDataException:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
self._hint = f"other word for {self.get_best_synonym()}"
|
||||
return
|
||||
synonyms = self.get_attribute("synonyms")
|
||||
synonyms = [f"{self.synonym_prefix} {w}" for w in synonyms]
|
||||
|
||||
hints = hints + synonyms
|
||||
except NoDataException:
|
||||
pass
|
||||
|
||||
self._hint = self.get_best_sense()
|
||||
|
||||
|
||||
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 = 0.5):
|
||||
|
||||
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)
|
||||
database = get_database(lang=lang_code)
|
||||
list_words = list(database.keys())
|
||||
|
||||
grid = np.full(shape=(h,w), dtype=np.unicode, fill_value = ' ')
|
||||
|
||||
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])
|
||||
|
||||
|
||||
locations[char].append([y, x])
|
||||
|
||||
remove_digits = str.maketrans('', '', digits)
|
||||
n_words = len(list_words)
|
||||
|
||||
def get_word(max_length: int, min_length = 0):
|
||||
|
||||
def get_word(max_length: int, min_length=0):
|
||||
assert max_length > 1
|
||||
|
||||
index = random.randint(0,n_words-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)
|
||||
index = random.randint(0, n_words-1)
|
||||
word = list_words[index][:]
|
||||
|
||||
|
||||
return word
|
||||
|
||||
def normalize_word(word:str):
|
||||
|
||||
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):
|
||||
|
||||
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
|
||||
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
|
||||
grid[y, x + i] = char
|
||||
store_location(char, y, x+i)
|
||||
|
||||
word_hints[normalized_word] = WordInfo(word, y, x, vertical, database)
|
||||
|
||||
|
||||
|
||||
|
||||
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):
|
||||
|
||||
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] != " ":
|
||||
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] != " ":
|
||||
@ -175,27 +208,27 @@ def create_word_grid(w: int, h: int, lang_code: str = "en", target_density = 0.5
|
||||
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] != " ":
|
||||
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] != " ":
|
||||
@ -204,83 +237,106 @@ def create_word_grid(w: int, h: int, lang_code: str = "en", target_density = 0.5
|
||||
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)
|
||||
|
||||
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 (y, x, False)
|
||||
|
||||
return None
|
||||
|
||||
min_shape = min(w,h,30)
|
||||
|
||||
|
||||
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:
|
||||
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))
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
return grid, word_hints, solution_word_locations
|
||||
|
BIN
server/de.json
(Stored with Git LFS)
Normal file
BIN
server/de.json
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -36,13 +36,13 @@ class Session(object):
|
||||
def get_datetime_created(self) -> dt.datetime:
|
||||
return self.datetime_created
|
||||
|
||||
def create_crossword(self, width: int = 30, height: int = 30):
|
||||
def create_crossword(self, width: int = 20, height: int = 20, lang: str = "en"):
|
||||
self.crossword = crossword.Crossword(width=width,
|
||||
height=height,
|
||||
lang_code="en")
|
||||
lang_code=lang)
|
||||
|
||||
def get_crossword(self) -> crossword.Crossword:
|
||||
def get_crossword(self, lang: str = "en") -> crossword.Crossword:
|
||||
if self.crossword is None:
|
||||
self.create_crossword()
|
||||
self.create_crossword(lang = lang)
|
||||
|
||||
return self.crossword
|
||||
|
Reference in New Issue
Block a user