first version
This commit is contained in:
		
							
								
								
									
										1
									
								
								server/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| en.json filter=lfs diff=lfs merge=lfs -text | ||||
							
								
								
									
										320
									
								
								server/crossword.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										320
									
								
								server/crossword.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,320 @@ | ||||
| import enum | ||||
| import json | ||||
| import logging | ||||
| import numpy as np | ||||
|  | ||||
| from . import crossword_generator | ||||
|  | ||||
| from enum import Enum | ||||
|  | ||||
| import traceback | ||||
|  | ||||
|  | ||||
| class HintOrientation(Enum): | ||||
|     VERTICAL = 0 | ||||
|     HORIZONTAL = 1 | ||||
|     BOTH = 2 | ||||
|  | ||||
|  | ||||
| class FieldType(Enum): | ||||
|     EMPTY = 0 | ||||
|     HINT = 1 | ||||
|     LETTER = 2 | ||||
|  | ||||
|  | ||||
| class Field(object): | ||||
|     def __init__(self, field_type: FieldType = FieldType.EMPTY): | ||||
|         self._field_type = field_type | ||||
|  | ||||
|     def get_type(self) -> FieldType: | ||||
|         return self._field_type | ||||
|  | ||||
|     def get_content(self) -> str: | ||||
|         return None | ||||
|  | ||||
|     def serialize(self): | ||||
|         type_names = { | ||||
|             FieldType.EMPTY: "empty", | ||||
|             FieldType.HINT: "hint", | ||||
|             FieldType.LETTER: "letter" | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             'cell_type': type_names[self._field_type] | ||||
|         } | ||||
|  | ||||
|  | ||||
| class HintField(Field): | ||||
|     def __init__(self, horizontal_hint: str = None, vertical_hint: str = None): | ||||
|         super().__init__(field_type=FieldType.HINT) | ||||
|  | ||||
|         self._horizontal_hint = horizontal_hint | ||||
|         self._vertical_hint = vertical_hint | ||||
|  | ||||
|     def get_horizontal_hint(self) -> str: | ||||
|         return self._horizontal_hint | ||||
|  | ||||
|     def get_vertical_hint(self) -> str: | ||||
|         return self._vertical_hint | ||||
|  | ||||
|     def set_horizintal_hint(self, hint: str): | ||||
|         self._horizontal_hint = hint | ||||
|  | ||||
|     def set_vertical_hint(self, hint: str): | ||||
|         self._vertical_hint = hint | ||||
|  | ||||
|     def serialize(self): | ||||
|         result = super().serialize() | ||||
|         result['vertical_hint'] = self._vertical_hint | ||||
|         result['horizontal_hint'] = self._horizontal_hint | ||||
|  | ||||
|         return result | ||||
|  | ||||
|  | ||||
| class LetterField(Field): | ||||
|     def __init__(self, letter: str): | ||||
|         assert len(letter) <= 1 | ||||
|  | ||||
|         super().__init__(field_type=FieldType.LETTER) | ||||
|  | ||||
|         self._letter = letter.lower() | ||||
|         self._revealed = False | ||||
|         self._user_letter = "" | ||||
|  | ||||
|     def get_content(self) -> str: | ||||
|         return self._letter.upper() | ||||
|  | ||||
|     def get_user_content(self) -> str: | ||||
|         return self._user_letter.upper() | ||||
|  | ||||
|     def reveal(self): | ||||
|         self._revealed = True | ||||
|  | ||||
|     def user_input(self, input_letter): | ||||
|         assert len(input_letter) <= 1 | ||||
|         self._user_letter = input_letter.lower() | ||||
|  | ||||
|     def is_revealed(self) -> bool: | ||||
|         return self._revealed | ||||
|  | ||||
|     def serialize(self): | ||||
|         result = super().serialize() | ||||
|         result['letter'] = self._letter | ||||
|  | ||||
|         return result | ||||
|  | ||||
|  | ||||
| class Grid(object): | ||||
|     def __init__(self, width: int, height: int, lang_code: str, density=0.5): | ||||
|         self._width = width | ||||
|         self._height = height | ||||
|         self._lang_code = lang_code | ||||
|         self._density = density | ||||
|         self._grid = [] | ||||
|         try: | ||||
|             self._build_grid() | ||||
|         except Exception as e: | ||||
|             logging.error("error in generation", str(e)) | ||||
|             traceback.print_exc() | ||||
|  | ||||
|     def serialize(self): | ||||
|         return [ | ||||
|             [cell.serialize() for cell in row] for row in self._grid | ||||
|         ] | ||||
|  | ||||
|     def get_status(self): | ||||
|         status = [] | ||||
|         for y, row in enumerate(self._grid): | ||||
|             for x, cell in enumerate(row): | ||||
|                 if cell.get_type() == FieldType.LETTER: | ||||
|  | ||||
|                     user_content = cell.get_user_content() | ||||
|                     if cell.is_revealed(): | ||||
|                         status.append({ | ||||
|                             'x': x, | ||||
|                             'y': y, | ||||
|                             'revealed': cell.get_content() | ||||
|                         }) | ||||
|                     elif len(user_content) > 0: | ||||
|                         status.append({ | ||||
|                             'x': x, | ||||
|                             'y': y, | ||||
|                             'user_input': user_content | ||||
|                         }) | ||||
|         return status | ||||
|  | ||||
|     def check_and_reveal_horizontal(self, x: int, y: int) -> list: | ||||
|         # TODO: this would be much more perfomant and elegant, if every cell would hold a reference | ||||
|         # to it's own word^^ | ||||
|  | ||||
|         status_update = [] | ||||
|         cells_to_reveal = [] | ||||
|  | ||||
|         x_start = x | ||||
|         while (self._grid[y][x_start-1].get_type() == FieldType.LETTER): | ||||
|             x_start -= 1 | ||||
|  | ||||
|         x_i = x_start - 1 | ||||
|         while(x_i + 1 < self._width and self._grid[y][x_i+1].get_type() == FieldType.LETTER): | ||||
|  | ||||
|             x_i += 1 | ||||
|             cell = self._grid[y][x_i] | ||||
|             if cell.get_user_content() != cell.get_content(): | ||||
|                 return [] | ||||
|             cells_to_reveal.append(cell) | ||||
|  | ||||
|         if x_start - x_i == 0: | ||||
|             # we have a single letter, not a word | ||||
|             return [] | ||||
|  | ||||
|         for i, cell in enumerate(cells_to_reveal): | ||||
|             status_update.append({ | ||||
|                 'x': x_start + i, | ||||
|                 'y': y, | ||||
|                 'revealed': cell.get_content() | ||||
|             }) | ||||
|             cell.reveal() | ||||
|  | ||||
|         return status_update | ||||
|  | ||||
|     def check_and_reveal_vertical(self, x: int, y: int) -> list: | ||||
|         # TODO: this would be much more perfomant and elegant, if every cell would hold a reference | ||||
|         # to it's own word^^ | ||||
|  | ||||
|         status_update = [] | ||||
|         cells_to_reveal = [] | ||||
|  | ||||
|         y_start = y | ||||
|         while (self._grid[y_start - 1][x].get_type() == FieldType.LETTER): | ||||
|             y_start -= 1 | ||||
|  | ||||
|         y_i = y_start - 1 | ||||
|         while(y_i + 1 < self._width and self._grid[y_i+1][x].get_type() == FieldType.LETTER): | ||||
|  | ||||
|             y_i += 1 | ||||
|             cell = self._grid[y_i][x] | ||||
|             if cell.get_user_content() != cell.get_content(): | ||||
|                 return [] | ||||
|             cells_to_reveal.append(cell) | ||||
|  | ||||
|         if y_start - y_i == 0: | ||||
|             # we have a single letter, not a word | ||||
|             return [] | ||||
|  | ||||
|         for i, cell in enumerate(cells_to_reveal): | ||||
|             status_update.append({ | ||||
|                 'x': x, | ||||
|                 'y': y + i, | ||||
|                 'revealed': cell.get_content() | ||||
|             }) | ||||
|             cell.reveal() | ||||
|  | ||||
|         return status_update | ||||
|  | ||||
|     def check_and_reveal_word(self, x: int, y: int): | ||||
|         return self.check_and_reveal_horizontal(x, y) + self.check_and_reveal_vertival(x, y) | ||||
|  | ||||
|     def user_input(self, x: int, y: int, letter: str) -> list: | ||||
|         assert len(letter) <= 1 | ||||
|  | ||||
|         cell = self._grid[y][x] | ||||
|  | ||||
|         if cell.get_type() != FieldType.LETTER: | ||||
|             # should not happen if the client does everything right | ||||
|             logging.warning("try to modify wrong cell") | ||||
|             return [] | ||||
|  | ||||
|         if cell.is_revealed(): | ||||
|             # user tries to modify already revealed change, telling him it's already revealed ;) | ||||
|             return [{ | ||||
|                 'x': x, | ||||
|                 'y': y, | ||||
|                 'revealed': cell.get_content() | ||||
|             }] | ||||
|  | ||||
|         letter = letter.lower() | ||||
|  | ||||
|         cell.user_input(letter.lower()) | ||||
|  | ||||
|         revealed_changes = self.check_and_reveal_vertical( | ||||
|             x, y) + self.check_and_reveal_horizontal(x, y) | ||||
|  | ||||
|         if len(revealed_changes) == 0: | ||||
|             return [{ | ||||
|                 'x': x, | ||||
|                 'y': y, | ||||
|                 'user_input': cell.get_user_content() | ||||
|             }] | ||||
|          | ||||
|         return revealed_changes | ||||
|  | ||||
|     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) | ||||
|  | ||||
|         # note: we will append an additional row and column, to have enough space to place hint fields | ||||
|  | ||||
|         self._grid = [[Field()] * self._width]  # initialize with empty row | ||||
|  | ||||
|         for y in range(self._height - 1): | ||||
|             row = [Field()]  # initialize row with empty column | ||||
|             for x in range(self._width - 1): | ||||
|                 raw_cell = raw_grid[y, x] | ||||
|                 if raw_cell == " ": | ||||
|                     row.append(Field()) | ||||
|                 else: | ||||
|                     row.append(LetterField(raw_cell)) | ||||
|  | ||||
|             self._grid.append(row) | ||||
|  | ||||
|         # place hint fields: | ||||
|         for word, info in word_infos.items(): | ||||
|             y, x = info.get_hint_location() | ||||
|             # correct offset | ||||
|             y += 1 | ||||
|             x += 1 | ||||
|  | ||||
|             cell = self._grid[y][x] | ||||
|  | ||||
|             # check if we already have a hint here: | ||||
|             if cell.get_type() == FieldType.HINT: | ||||
|                 if info.is_vertical(): | ||||
|                     cell.set_vertical_hint(info.get_hint()) | ||||
|                 else: | ||||
|                     cell.set_horizintal_hint(info.get_hint()) | ||||
|             elif cell.get_type() == FieldType.LETTER: | ||||
|                 # edge case: a word has "eaten up" another one, skipping that case | ||||
|                 pass | ||||
|  | ||||
|             else: | ||||
|                 if info.is_vertical(): | ||||
|                     self._grid[y][x] = HintField(vertical_hint=info.get_hint()) | ||||
|                 else: | ||||
|                     self._grid[y][x] = HintField( | ||||
|                         horizontal_hint=info.get_hint()) | ||||
|  | ||||
|  | ||||
| class Crossword(object): | ||||
|     def __init__(self, width: int, height: int, lang_code: str = "en"): | ||||
|         self._width = width | ||||
|         self._height = height | ||||
|         self._grid = Grid(width, height, lang_code) | ||||
|  | ||||
|     def serialize(self): | ||||
|         return { | ||||
|             'w': self._width, | ||||
|             'h': self._height, | ||||
|             'grid': self._grid.serialize() | ||||
|         } | ||||
|  | ||||
|     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") | ||||
|     print(cw.serialize()) | ||||
							
								
								
									
										112
									
								
								server/crossword_connection.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								server/crossword_connection.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,112 @@ | ||||
| import logging | ||||
| import uuid | ||||
|  | ||||
| from . import json_websockets | ||||
| from . import session | ||||
|  | ||||
|  | ||||
| class CrosswordConnection(json_websockets.JsonWebsocketConnection): | ||||
|  | ||||
|     sessions = {} | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|  | ||||
|         self._session = None | ||||
|  | ||||
|     async def send_crossword(self, sessionId: str): | ||||
|         if sessionId not in CrosswordConnection.sessions: | ||||
|             await self.send_error(msg="unknown session") | ||||
|             return | ||||
|  | ||||
|         sess = CrosswordConnection.sessions[sessionId] | ||||
|  | ||||
|         # NOTE: if there will be the possibility of private | ||||
|         # sessions, this has to be changed since this is leaking | ||||
|         # the information that a certain session exists! | ||||
|         if self not in sess.get_sockets(): | ||||
|             await self.send_error(msg="you are not registered to given session") | ||||
|             return | ||||
|  | ||||
|         crossword = sess.get_crossword() | ||||
|         await self.send({ | ||||
|             'type': 'crossword', | ||||
|             'crossword': crossword.serialize() | ||||
|         }) | ||||
|  | ||||
|         # sending also the status as update: | ||||
|         await self.send({ | ||||
|             'type': 'update', | ||||
|             'updates': crossword.get_status() | ||||
|         }) | ||||
|  | ||||
|     async def user_update(self, x: int, y: int, letter: str): | ||||
|         if len(letter) > 1: | ||||
|             await self.send_error(msg="received invalid userinput") | ||||
|             return | ||||
|  | ||||
|         update_message = self._session.get_crossword().user_input(x=x, y=y, letter=letter) | ||||
|  | ||||
|         for connection in self._session.get_sockets(): | ||||
|             await connection.send({ | ||||
|                 'type': 'update', | ||||
|                 'updates': update_message | ||||
|             }) | ||||
|  | ||||
|     async def register(self, sessionId: str = None): | ||||
|  | ||||
|         if sessionId is None: | ||||
|  | ||||
|             sessionId = uuid.uuid4().hex | ||||
|             while sessionId in CrosswordConnection.sessions: | ||||
|                 sessionId = uuid.uuid4().hex | ||||
|  | ||||
|             new_session = session.Session() | ||||
|             CrosswordConnection.sessions[sessionId] = new_session | ||||
|  | ||||
|         if sessionId not in CrosswordConnection.sessions: | ||||
|             await self.send_error("unknown session id") | ||||
|  | ||||
|             # register with new id: | ||||
|             await self.register() | ||||
|             return | ||||
|  | ||||
|         sess = CrosswordConnection.sessions[sessionId] | ||||
|         sess.connect_socket(self) | ||||
|  | ||||
|         self._session = sess | ||||
|  | ||||
|         await self.send({ | ||||
|             'type': 'register', | ||||
|             'sessionId': sessionId | ||||
|         }) | ||||
|  | ||||
|         await self.send_crossword(sessionId) | ||||
|  | ||||
|     async def send_error(self, msg: str): | ||||
|         await self.send({ | ||||
|             'type': 'error', | ||||
|             'message': msg | ||||
|         }) | ||||
|  | ||||
|     async def handle_message(self, message: dict): | ||||
|         logging.info("incoming message: %s", str(message)) | ||||
|         if not "type" in message: | ||||
|             logging.error("received malformated message") | ||||
|             await self.send_error(msg="i do not understand the request") | ||||
|             return | ||||
|  | ||||
|         if message['type'] == 'register': | ||||
|             sessionId = None | ||||
|             if 'sessionId' in message: | ||||
|                 sessionId = message['sessionId'] | ||||
|             await self.register(sessionId=sessionId) | ||||
|             return | ||||
|  | ||||
|         if self._session is None: | ||||
|             await self.send_error(msg="you are not registered properly") | ||||
|             return | ||||
|  | ||||
|         if message['type'] == "update": | ||||
|             await self.user_update(x=message['x'], y=message['y'], letter=message['letter']) | ||||
|             return | ||||
							
								
								
									
										286
									
								
								server/crossword_generator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								server/crossword_generator.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,286 @@ | ||||
| import json | ||||
| import random | ||||
| import numpy as np | ||||
| from string import digits | ||||
| import pathlib | ||||
| import logging | ||||
|  | ||||
|  | ||||
| def get_database(lang: str = "en") -> dict: | ||||
|     if lang not in get_database._dbs: | ||||
|         current_folder = pathlib.Path(__file__).parents[0] | ||||
|         db_file = str(current_folder / f"{lang}.json") | ||||
|  | ||||
|         logging.info("loading database: %s", lang) | ||||
|  | ||||
|         with open(db_file, "r") as f: | ||||
|             db = json.load(f) | ||||
|             get_database._dbs[lang] = db | ||||
|          | ||||
|         logging.info("database loaded") | ||||
|      | ||||
|     return get_database._dbs[lang] | ||||
|  | ||||
| get_database._dbs = {} | ||||
|  | ||||
| class NoDataException(Exception): | ||||
|     pass | ||||
|  | ||||
| class WordInfo(object): | ||||
|     def __init__(self, word:str, y:int, x:int, is_vertical: bool, database: dict): | ||||
|         self._dictionary_database = database | ||||
|         self._y = y | ||||
|         self._x = x | ||||
|         self._word = word | ||||
|         self._hint = None | ||||
|         self._is_vertical = is_vertical | ||||
|          | ||||
|         self.choose_info() | ||||
|  | ||||
|     def get_attribute(self, attr: str): | ||||
|         attr = self._dictionary_database[self._word][attr] | ||||
|         if attr is None: | ||||
|             raise NoDataException | ||||
|         return attr | ||||
|      | ||||
|     def get_best_antonym(self) -> str: | ||||
|         antonyms = self.get_attribute("antonyms") | ||||
|         return random.choice(antonyms) | ||||
|      | ||||
|     def get_best_synonym(self) -> str: | ||||
|         synonyms = self.get_attribute("synonyms") | ||||
|         return random.choice(synonyms) | ||||
|      | ||||
|     def get_best_sense(self) -> str: | ||||
|         senses = self.get_attribute("senses") | ||||
|         return random.choice(senses) | ||||
|          | ||||
|     def choose_info(self): | ||||
|         # first choose antonyms, then synonyms, then senses | ||||
|          | ||||
|         try: | ||||
|             self._hint = f"opposite of {self.get_best_antonym()}" | ||||
|             return | ||||
|         except NoDataException: | ||||
|             pass | ||||
|          | ||||
|         try: | ||||
|             self._hint = f"other word for {self.get_best_synonym()}" | ||||
|             return | ||||
|         except NoDataException: | ||||
|             pass | ||||
|          | ||||
|         self._hint = self.get_best_sense() | ||||
|      | ||||
|     def get_hint(self) -> str: | ||||
|         return self._hint | ||||
|      | ||||
|     def get_hint_location(self): | ||||
|         x = self._x if self._is_vertical else self._x - 1 | ||||
|         y = self._y - 1 if self._is_vertical else self._y | ||||
|         return (y, x) | ||||
|      | ||||
|     def is_vertical(self): | ||||
|         return self._is_vertical | ||||
|  | ||||
| def create_word_grid(w: int, h: int, lang_code: str = "en", target_density = 0.5): | ||||
|     logging.info("generate new crossword") | ||||
|  | ||||
|     database = get_database(lang = lang_code) | ||||
|     list_words = list(database.keys()) | ||||
|  | ||||
|     grid = np.full(shape=(h,w), dtype=np.unicode, fill_value = ' ') | ||||
|      | ||||
|     locations = {} | ||||
|      | ||||
|     word_hints = {} | ||||
|      | ||||
|     def store_location(char: str, y: int, x: int): | ||||
|         assert len(char) == 1 | ||||
|          | ||||
|         if char not in locations: | ||||
|             locations[char] = [] | ||||
|          | ||||
|         locations[char].append([y,x]) | ||||
|          | ||||
|     remove_digits = str.maketrans('', '', digits) | ||||
|     n_words = len(list_words) | ||||
|      | ||||
|     def get_word(max_length: int, min_length = 0): | ||||
|         assert max_length > 1 | ||||
|          | ||||
|         index = random.randint(0,n_words-1) | ||||
|         word = list_words[index][:] | ||||
|          | ||||
|         while len(word) >= max_length or not word.isalnum() or len(word) <= min_length: | ||||
|             index = random.randint(0,n_words-1) | ||||
|             word = list_words[index][:] | ||||
|          | ||||
|         return word | ||||
|              | ||||
|     def normalize_word(word:str): | ||||
|         word = word.translate(remove_digits) | ||||
|         return word.lower() | ||||
|      | ||||
|     def place_word(word:str, y: int, x:int, vertical:bool = False): | ||||
|         normalized_word = normalize_word(word) | ||||
|         n = len(normalized_word) | ||||
|         if vertical: | ||||
|             assert grid.shape[0] - n >= y | ||||
|             for i, char in enumerate(normalized_word): | ||||
|                 grid[y + i,x] = char | ||||
|                 store_location(char, y+i, x) | ||||
|         else: | ||||
|             assert grid.shape[1] - n >= x | ||||
|             for i, char in enumerate(normalized_word): | ||||
|                 grid[y,x + i] = char | ||||
|                 store_location(char, y, x+i) | ||||
|          | ||||
|         word_hints[normalized_word] = WordInfo(word, y, x, vertical, database) | ||||
|          | ||||
|          | ||||
|      | ||||
|     def density(): | ||||
|         return 1 - (grid == " ").sum() / (w * h) | ||||
|      | ||||
|      | ||||
|      | ||||
|     def check_if_fits(word:str, y:int, x:int, vertical:bool): | ||||
|         n = len(word) | ||||
|         if vertical: | ||||
|              | ||||
|             # check if there is space before and after | ||||
|             if y - 1 >= 0 and grid[y - 1, x] != " ": | ||||
|                 return False | ||||
|             if y + n < grid.shape[0] - 1 and grid[y+n,x] != " ": | ||||
|                 return False | ||||
|              | ||||
|             if grid.shape[0] - n < y or y < 0: | ||||
|                 # print("over board") | ||||
|                 return False | ||||
|              | ||||
|             for i, char in enumerate(word): | ||||
|                 char_x = x | ||||
|                 char_y = y + i | ||||
|                  | ||||
|                 if not (grid[char_y, char_x] == " " or grid[char_y, char_x] == char): | ||||
|                     # print("not matching") | ||||
|                     return False | ||||
|                  | ||||
|                 if grid[char_y, char_x] == " ": | ||||
|                     # check for horizonatal neighbors: | ||||
|                     if char_x - 1 >= 0 and grid[char_y, char_x - 1] != " ": | ||||
|                         # print("3") | ||||
|                         return False | ||||
|                     if char_x + 1 < grid.shape[1] and grid[char_y, char_x + 1] != " ": | ||||
|                         # print("4") | ||||
|                         return False | ||||
|          | ||||
|         else: | ||||
|              | ||||
|             # check if there is space before and after | ||||
|             if x - 1 >= 0 and grid[y, x - 1] != " ": | ||||
|                 return False | ||||
|             if x + n < grid.shape[1] - 1 and grid[y,x + n] != " ": | ||||
|                 return False | ||||
|              | ||||
|             if grid.shape[1] - n < x or x < 0: | ||||
|                 # print("over board") | ||||
|                 return False | ||||
|              | ||||
|             for i, char in enumerate(word): | ||||
|                 char_x = x + i | ||||
|                 char_y = y | ||||
|                  | ||||
|                 if not (grid[char_y, char_x] == " " or grid[char_y, char_x] == char): | ||||
|                     # print("not matching") | ||||
|                     return False | ||||
|                  | ||||
|                 if grid[char_y, char_x] == " ": | ||||
|                     # check for vertical neighbors: | ||||
|                     if char_y - 1 >= 0 and grid[char_y - 1, char_x] != " ": | ||||
|                         # print("1") | ||||
|                         return False | ||||
|                     if char_y + 1 < grid.shape[0] and grid[char_y + 1, char_x] != " ": | ||||
|                         # print("2") | ||||
|                         return False | ||||
|          | ||||
|         return True | ||||
|          | ||||
|      | ||||
|     def get_crossover(word: str): | ||||
|         # returns Tuple of: (y,x, is_vertical?) or None | ||||
|          | ||||
|         shuffled_order = list(range(len(word))) | ||||
|         random.shuffle(shuffled_order) | ||||
|          | ||||
|         for index in shuffled_order: | ||||
|             # check for existing locations | ||||
|             char = word[index] | ||||
|             if char in locations: | ||||
|                 char_locations = locations[char] | ||||
|                  | ||||
|                 for char_loc in char_locations: | ||||
|                     # test vertical | ||||
|                     y = char_loc[0] - index | ||||
|                     x = char_loc[1] | ||||
|                      | ||||
|                     if check_if_fits(word, y, x, vertical=True): | ||||
|                         return (y,x,True) | ||||
|                      | ||||
|                     # test horizontal | ||||
|                     y = char_loc[0] | ||||
|                     x = char_loc[1] - index | ||||
|                      | ||||
|                     if check_if_fits(word, y, x, vertical=False): | ||||
|                         return (y,x,False) | ||||
|          | ||||
|         return None | ||||
|      | ||||
|     min_shape = min(w,h,30) | ||||
|      | ||||
|     # place first word: | ||||
|     first_word = get_word(max_length=min_shape, min_length=min(10,grid.shape[1] - 2)) | ||||
|      | ||||
|     # find random place: | ||||
|     x = random.randint(0, grid.shape[1] - len(first_word) - 1) | ||||
|     y = random.randint(0, grid.shape[0] - 1) | ||||
|      | ||||
|     place_word(first_word, y, x, vertical=False) | ||||
|                  | ||||
|     i = 0 | ||||
|      | ||||
|      | ||||
|     current_density = density() | ||||
|      | ||||
|     while current_density < target_density: | ||||
|         word = get_word(max_length=(1 - current_density ** 0.4) * min_shape, | ||||
|                         min_length=max(min(10, 0.5 * (1 - current_density ** 0.3) * min_shape), 2)) | ||||
|          | ||||
|         normalized_word = normalize_word(word) | ||||
|          | ||||
|         if normalized_word in word_hints: | ||||
|             continue | ||||
|          | ||||
|         # check if matching characters exist: | ||||
|         crossover = get_crossover(normalized_word) | ||||
|          | ||||
|         i += 1 | ||||
|         if i % 100000 == 0: | ||||
|             print(i) | ||||
|         if i > 100000: | ||||
|             break | ||||
|          | ||||
|         if crossover == None: | ||||
|             current_density = density() | ||||
|             continue | ||||
|          | ||||
|         y,x,is_vertical = crossover | ||||
|          | ||||
|         place_word(word, y,x, is_vertical) | ||||
|          | ||||
|         current_density = density() | ||||
|      | ||||
|     logging.info("crossword generation done after %s iterations", str(i)) | ||||
|     return grid, word_hints | ||||
|          | ||||
							
								
								
									
										
											BIN
										
									
								
								server/en.json
									 (Stored with Git LFS)
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								server/en.json
									 (Stored with Git LFS)
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										80
									
								
								server/json_websockets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								server/json_websockets.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | ||||
| import websockets | ||||
| import asyncio | ||||
| import json | ||||
| import logging | ||||
|  | ||||
| logging.basicConfig(level=logging.INFO) | ||||
|  | ||||
| class JsonWebsocketConnection(object): | ||||
|  | ||||
|     def __init__(self, | ||||
|                  websocket: websockets.WebSocketServerProtocol): | ||||
|          | ||||
|         logging.info("incoming connection") | ||||
|         self._websocket = websocket | ||||
|         self._is_closed = False; | ||||
|      | ||||
|     def is_closed(self): | ||||
|         return self._is_closed | ||||
|      | ||||
|     async def close(self): | ||||
|         logging.info("closing connection") | ||||
|         try: | ||||
|             if self._is_closed: | ||||
|                 return | ||||
|             self._websocket.close() | ||||
|             self._is_closed = True | ||||
|         except Exception as e: | ||||
|             logging.warning("error closing connection: %s", str(e)) | ||||
|      | ||||
|     async def send(self, message: dict): | ||||
|         string_message = json.dumps(message) | ||||
|         logging.debug("sending message: %s", string_message) | ||||
|         try: | ||||
|             await self._websocket.send(string_message) | ||||
|         except Exception as e: | ||||
|             logging.warning("error sending message: %s", str(e)) | ||||
|      | ||||
|     async def handle_message(self, message: dict): | ||||
|         pass # override this function | ||||
|  | ||||
|     async def run(self): | ||||
|         try: | ||||
|             async for message in self._websocket: | ||||
|                 try: | ||||
|                     json_message = json.loads(message) | ||||
|                     await self.handle_message(json_message)  | ||||
|                 except ValueError as e: | ||||
|                     logging.warning("received unprocessable message %s", str(e)) | ||||
|                  | ||||
|          | ||||
|         except Exception as e: | ||||
|                 logging.warning("error in websocket connection: %s", str(e)) | ||||
|                 self._is_closed = True | ||||
|         finally: | ||||
|             self._is_closed = True | ||||
|          | ||||
|          | ||||
|  | ||||
|  | ||||
|  | ||||
| class JsonWebsocketServer(object): | ||||
|     def __init__(self, handler_class: JsonWebsocketConnection, host:str = 'localhost', port:int = 8765): | ||||
|         self._host = host | ||||
|         self._port = port | ||||
|         self._handler_class = handler_class | ||||
|  | ||||
|     def run(self): | ||||
|         async def main(websocket: websockets.WebSocketServerProtocol, | ||||
|                        path: str): | ||||
|  | ||||
|             connection = self._handler_class(websocket) | ||||
|  | ||||
|             await connection.run() | ||||
|          | ||||
|         start_server = websockets.serve(main, self._host, self._port) | ||||
|  | ||||
|         asyncio.get_event_loop().run_until_complete(start_server) | ||||
|         asyncio.get_event_loop().run_forever() | ||||
|          | ||||
|  | ||||
							
								
								
									
										7
									
								
								server/main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								server/main.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| from . import json_websockets | ||||
| from . import crossword_connection | ||||
|  | ||||
| server = json_websockets.JsonWebsocketServer( | ||||
|     crossword_connection.CrosswordConnection | ||||
| ) | ||||
| server.run() | ||||
							
								
								
									
										48
									
								
								server/session.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								server/session.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| import datetime as dt | ||||
| from . import json_websockets | ||||
| from . import crossword | ||||
|  | ||||
|  | ||||
| class Session(object): | ||||
|     def __init__(self) -> None: | ||||
|         self.crossword = None | ||||
|         self.datetime_created = dt.datetime.utcnow() | ||||
|         self.connected_sockets = set() | ||||
|      | ||||
|     def cleanup(self): | ||||
|         sockets_to_remove = [] | ||||
|         for socket in self.connected_sockets: | ||||
|             if socket.is_closed(): | ||||
|                 sockets_to_remove.append(socket) | ||||
|  | ||||
|         for socket in sockets_to_remove: | ||||
|             self.connected_sockets.remove(socket) | ||||
|  | ||||
|     def connect_socket(self, | ||||
|                        websocket: json_websockets.JsonWebsocketConnection) -> None: | ||||
|          | ||||
|         self.cleanup() | ||||
|         self.connected_sockets.add(websocket) | ||||
|  | ||||
|     def disconnect_socket(self, | ||||
|                           websocket: json_websockets.JsonWebsocketConnection) -> None: | ||||
|         if websocket in self.connected_sockets: | ||||
|             self.connected_sockets.remove(websocket) | ||||
|  | ||||
|     def get_sockets(self) -> json_websockets.JsonWebsocketConnection: | ||||
|         self.cleanup() | ||||
|         return self.connected_sockets | ||||
|  | ||||
|     def get_datetime_created(self) -> dt.datetime: | ||||
|         return self.datetime_created | ||||
|  | ||||
|     def create_crossword(self, width: int = 30, height: int = 30): | ||||
|         self.crossword = crossword.Crossword(width=width, | ||||
|                                              height=height, | ||||
|                                              lang_code="en") | ||||
|  | ||||
|     def get_crossword(self) -> crossword.Crossword: | ||||
|         if self.crossword is None: | ||||
|             self.create_crossword() | ||||
|          | ||||
|         return self.crossword | ||||
							
								
								
									
										154
									
								
								server/websockets.ipynb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								server/websockets.ipynb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,154 @@ | ||||
| { | ||||
|  "metadata": { | ||||
|   "language_info": { | ||||
|    "codemirror_mode": { | ||||
|     "name": "ipython", | ||||
|     "version": 3 | ||||
|    }, | ||||
|    "file_extension": ".py", | ||||
|    "mimetype": "text/x-python", | ||||
|    "name": "python", | ||||
|    "nbconvert_exporter": "python", | ||||
|    "pygments_lexer": "ipython3", | ||||
|    "version": "3.9.5" | ||||
|   }, | ||||
|   "orig_nbformat": 2, | ||||
|   "kernelspec": { | ||||
|    "name": "python395jvsc74a57bd0916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1", | ||||
|    "display_name": "Python 3.9.5 64-bit" | ||||
|   }, | ||||
|   "metadata": { | ||||
|    "interpreter": { | ||||
|     "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" | ||||
|    } | ||||
|   } | ||||
|  }, | ||||
|  "nbformat": 4, | ||||
|  "nbformat_minor": 2, | ||||
|  "cells": [ | ||||
|   { | ||||
|    "cell_type": "code", | ||||
|    "execution_count": 1, | ||||
|    "metadata": {}, | ||||
|    "outputs": [], | ||||
|    "source": [ | ||||
|     "import websockets\n", | ||||
|     "import asyncio\n", | ||||
|     "import json\n", | ||||
|     "import logging" | ||||
|    ] | ||||
|   }, | ||||
|   { | ||||
|    "cell_type": "code", | ||||
|    "execution_count": 2, | ||||
|    "metadata": {}, | ||||
|    "outputs": [], | ||||
|    "source": [ | ||||
|     "class JsonWebsocketConnection(object):\n", | ||||
|     "\n", | ||||
|     "    def __init__(self,\n", | ||||
|     "                 websocket: websockets.WebSocketServerProtocol):\n", | ||||
|     "        \n", | ||||
|     "        logging.debug(\"incoming connection\")\n", | ||||
|     "        self._websocket = websocket\n", | ||||
|     "        self._is_closed = False;\n", | ||||
|     "    \n", | ||||
|     "    def is_closed(self):\n", | ||||
|     "        return self.is_closed\n", | ||||
|     "    \n", | ||||
|     "    async def close(self):\n", | ||||
|     "        logging.debug(\"closing connection\")\n", | ||||
|     "        try:\n", | ||||
|     "            if self._is_closed:\n", | ||||
|     "                return\n", | ||||
|     "            self._websocket.close()\n", | ||||
|     "            self._is_closed = True\n", | ||||
|     "        except Exception as e:\n", | ||||
|     "            logging.warning(\"error closing connection: %s\", str(e))\n", | ||||
|     "    \n", | ||||
|     "    async def send(self, message: dict):\n", | ||||
|     "        string_message = json.dumps(message)\n", | ||||
|     "        logging.debug(\"sending message: %s\", string_message)\n", | ||||
|     "        try:\n", | ||||
|     "            await self._websocket.send(string_message)\n", | ||||
|     "        except Exception as e:\n", | ||||
|     "            logging.warning(\"error sending message: %s\", str(e))\n", | ||||
|     "    \n", | ||||
|     "    async def handle_message(self, message: dict):\n", | ||||
|     "        pass # override this function\n", | ||||
|     "\n", | ||||
|     "    async def run(self):\n", | ||||
|     "        try:\n", | ||||
|     "            async for message in self._websocket:\n", | ||||
|     "                try:\n", | ||||
|     "                    json_message = json.loads(message)\n", | ||||
|     "                except ValueError as e:\n", | ||||
|     "                    logging.warning(\"received unprocessable message %s\", str(e))\n", | ||||
|     "                await self.handle_message(message) \n", | ||||
|     "        \n", | ||||
|     "        except Exception as e:\n", | ||||
|     "                logging.warning(\"error in websocket connection: %s\", str(e))\n", | ||||
|     "\n", | ||||
|     "\n", | ||||
|     "\n", | ||||
|     "class JsonWebsocketServer(object):\n", | ||||
|     "    def __init__(self, handler_class: JsonWebsocketConnection, host:str = 'localhost', port:int = 8765):\n", | ||||
|     "        self._host = host\n", | ||||
|     "        self._port = port\n", | ||||
|     "        self._handler_class = handler_class\n", | ||||
|     "\n", | ||||
|     "    def run(self):\n", | ||||
|     "        async def main(websocket: websockets.WebSocketServerProtocol,\n", | ||||
|     "                       path: str):\n", | ||||
|     "\n", | ||||
|     "            connection = self._handler_class(websocket)\n", | ||||
|     "\n", | ||||
|     "            connection.run()\n", | ||||
|     "        \n", | ||||
|     "        start_server = websockets.serve(main, self._host, self._port)\n", | ||||
|     "\n", | ||||
|     "        asyncio.get_event_loop().run_until_complete(start_server)\n", | ||||
|     "        asyncio.get_event_loop().run_forever()\n", | ||||
|     "        \n", | ||||
|     "\n", | ||||
|     "def CrosswordConnection(JsonWebsocketConnection): \n", | ||||
|     "    async def handle_message(self, message: dict):\n", | ||||
|     "        await self.send({\"message\": \"hello\"})\n", | ||||
|     "    \n", | ||||
|     "\n" | ||||
|    ] | ||||
|   }, | ||||
|   { | ||||
|    "cell_type": "code", | ||||
|    "execution_count": 3, | ||||
|    "metadata": {}, | ||||
|    "outputs": [ | ||||
|     { | ||||
|      "output_type": "error", | ||||
|      "ename": "RuntimeError", | ||||
|      "evalue": "This event loop is already running", | ||||
|      "traceback": [ | ||||
|       "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", | ||||
|       "\u001b[0;31mRuntimeError\u001b[0m                              Traceback (most recent call last)", | ||||
|       "\u001b[0;32m<ipython-input-3-ecab008e957f>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0mserver\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mJsonWebsocketServer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mCrosswordConnection\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mserver\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", | ||||
|       "\u001b[0;32m<ipython-input-2-3c372b72dd90>\u001b[0m in \u001b[0;36mrun\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m     62\u001b[0m         \u001b[0mstart_server\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mwebsockets\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mserve\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmain\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_host\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_port\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     63\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 64\u001b[0;31m         \u001b[0masyncio\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_event_loop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun_until_complete\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstart_server\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     65\u001b[0m         \u001b[0masyncio\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_event_loop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun_forever\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     66\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", | ||||
|       "\u001b[0;32m/usr/lib/python3.9/asyncio/base_events.py\u001b[0m in \u001b[0;36mrun_until_complete\u001b[0;34m(self, future)\u001b[0m\n\u001b[1;32m    616\u001b[0m         \"\"\"\n\u001b[1;32m    617\u001b[0m         \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_check_closed\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 618\u001b[0;31m         \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_check_running\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    619\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    620\u001b[0m         \u001b[0mnew_task\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mfutures\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0misfuture\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfuture\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", | ||||
|       "\u001b[0;32m/usr/lib/python3.9/asyncio/base_events.py\u001b[0m in \u001b[0;36m_check_running\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m    576\u001b[0m     \u001b[0;32mdef\u001b[0m \u001b[0m_check_running\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    577\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mis_running\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 578\u001b[0;31m             \u001b[0;32mraise\u001b[0m \u001b[0mRuntimeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'This event loop is already running'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    579\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0mevents\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_get_running_loop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    580\u001b[0m             raise RuntimeError(\n", | ||||
|       "\u001b[0;31mRuntimeError\u001b[0m: This event loop is already running" | ||||
|      ] | ||||
|     } | ||||
|    ], | ||||
|    "source": [ | ||||
|     "server = JsonWebsocketServer(CrosswordConnection)\n", | ||||
|     "server.run()" | ||||
|    ] | ||||
|   }, | ||||
|   { | ||||
|    "cell_type": "code", | ||||
|    "execution_count": null, | ||||
|    "metadata": {}, | ||||
|    "outputs": [], | ||||
|    "source": [] | ||||
|   } | ||||
|  ] | ||||
| } | ||||
		Reference in New Issue
	
	Block a user