diff --git a/multiplayer_crosswords/crossword.py b/multiplayer_crosswords/crossword.py index 90aea01..470851d 100644 --- a/multiplayer_crosswords/crossword.py +++ b/multiplayer_crosswords/crossword.py @@ -140,9 +140,9 @@ class Crossword: # Now find a solution word: generate a random word from the dictionary (with 10-20 letters) # and try to find the necessary letter positions in the grid. if we fail we repeat with another word (max 10 tries) solution_word_positions: Optional[List[Tuple[int, int]]] = None - max_solution_word_attempts = 10 - for _ in range(max_solution_word_attempts): - random_length = random.randint(10, 20) + max_solution_word_attempts = 20 + for i in range(max_solution_word_attempts): + random_length = random.randint(10, 27 - (i // 2)) possible_words = dictionary.find_by_pattern('*' * random_length) if not possible_words: continue diff --git a/multiplayer_crosswords/server/serve_frontend.py b/multiplayer_crosswords/server/serve_frontend.py new file mode 100644 index 0000000..aa09771 --- /dev/null +++ b/multiplayer_crosswords/server/serve_frontend.py @@ -0,0 +1,35 @@ +from pathlib import Path +import argparse +from http import server as http +import http.server + +BASE_DIR = Path(__file__).resolve().parent.parent / "webui" + +def main(): + parser = argparse.ArgumentParser(description="Serve Multiplayer Crossword Frontend") + parser.add_argument("--host", type=str, default="localhost") + parser.add_argument("--port", type=int, default=8000) + parser.add_argument("--no-file-list", action="store_true", help="Disable directory listing") + + args = parser.parse_args() + + path = BASE_DIR + host = args.host + port = args.port + no_file_list = args.no_file_list + + class CustomHandler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=str(path), **kwargs) + + if no_file_list: + def list_directory(self, path): + self.send_error(403, "Directory listing not allowed") + return None + server_address = (host, port) + httpd = http.server.HTTPServer(server_address, CustomHandler) + print(f"Serving frontend at http://{host}:{port}/ from {path}") + httpd.serve_forever() + +if __name__ == "__main__": + main() diff --git a/multiplayer_crosswords/server/server_config.py b/multiplayer_crosswords/server/server_config.py index 0b0458f..e3e5dec 100644 --- a/multiplayer_crosswords/server/server_config.py +++ b/multiplayer_crosswords/server/server_config.py @@ -3,8 +3,8 @@ from pydantic import BaseModel DEFAULT_WEBSOCKET_HOST = "0.0.0.0" DEFAULT_WEBSOCKET_PORT = 8765 -DEFAULT_MIN_GRID_SIZE = 10 -DEFAULT_MAX_GRID_SIZE = 30 +DEFAULT_MIN_GRID_SIZE = 12 +DEFAULT_MAX_GRID_SIZE = 25 DEFAULT_GRID_BLOCK_RATIO = 0.39 diff --git a/multiplayer_crosswords/server/server_utils.py b/multiplayer_crosswords/server/server_utils.py index 131c112..278b4a8 100644 --- a/multiplayer_crosswords/server/server_utils.py +++ b/multiplayer_crosswords/server/server_utils.py @@ -34,7 +34,7 @@ class BoardSizePreset(str, Enum): elif self == BoardSizePreset.SMALL: return (min_size + (max_size - min_size) // 4, min_size + (max_size - min_size) // 4) elif self == BoardSizePreset.MEDIUM: - return (max_size // 2, max_size // 2) + return (min_size + (max_size - min_size) // 2, min_size + (max_size - min_size) // 2) elif self == BoardSizePreset.LARGE: return (min_size + 3 * (max_size - min_size) // 4, min_size + 3 * (max_size - min_size) // 4) elif self == BoardSizePreset.VERY_LARGE: diff --git a/multiplayer_crosswords/server/websocket_crossword_server.py b/multiplayer_crosswords/server/websocket_crossword_server.py index 804e299..3a1f142 100644 --- a/multiplayer_crosswords/server/websocket_crossword_server.py +++ b/multiplayer_crosswords/server/websocket_crossword_server.py @@ -109,19 +109,38 @@ class MultiplayerSessionManager(object): self._sessions_lock = asyncio.Lock() self._grid_block_ratio = ServerConfig.get_config().GRID_BLOCK_RATIO + async def remove_idle_sessions(self): + """Remove all idle sessions that have exceeded their max idle time.""" + async with self._sessions_lock: + idle_session_ids = [ + session_id for session_id, session in self._sessions.items() + if session.is_idle + ] + for session_id in idle_session_ids: + logger.info("Removing idle session %s", session_id) + del self._sessions[session_id] + if idle_session_ids: + logger.info("Removed %d idle sessions", len(idle_session_ids)) async def create_session(self, lang: str | Languages, grid_w: int, grid_h: int) -> MultiplayerSession: async with self._sessions_lock: + # Remove idle sessions before creating a new one + await self.remove_idle_sessions() + if isinstance(lang, str): lang = Languages(lang) dictionary = lang.load_dictionary() - crossword = Crossword.generate( - dictionary=dictionary, - seed=None, - grid_width=grid_w, - grid_height=grid_h, - grid_block_ratio=self._grid_block_ratio, - ) + max_tries = 4 + for i in range(max_tries): + crossword = Crossword.generate( + dictionary=dictionary, + seed=None, + grid_width=grid_w, + grid_height=grid_h, + grid_block_ratio=self._grid_block_ratio, + ) + if crossword is not None: + break if crossword is None: raise RuntimeError("Failed to generate crossword for the given parameters.") session_id = str(uuid.uuid4()) @@ -146,7 +165,7 @@ class WebsocketCrosswordServer(object): async def handle_request_available_session_properties(handler: WebsocketConnectionHandler, message: client_messages.RequestAvailableSessionPropertiesClientMessage): server_config = ServerConfig.get_config() response = server_messages.AvailableSessionPropertiesServerMessage( - supported_languages=[lang.value for lang in Languages], + supported_languages=list(reversed([lang.value for lang in Languages])), min_grid_size=server_config.MIN_GRID_SIZE, max_grid_size=server_config.MAX_GRID_SIZE, board_size_presets={ @@ -227,7 +246,9 @@ class WebsocketCrosswordServer(object): solution_word_positions = [] positions = session.crossword.solution_word_positions for pos in positions: - solution_word_positions.append((pos[1], pos[0])) # convert (row, col) to (col, row) + # Convert from (row, col) to (col, row) for client + row, col = pos + solution_word_positions.append((col, row)) response = server_messages.SendFullSessionStateServerMessage( session_id=session.session_id, diff --git a/multiplayer_crosswords/utils.py b/multiplayer_crosswords/utils.py index c0bea7f..9e4f133 100644 --- a/multiplayer_crosswords/utils.py +++ b/multiplayer_crosswords/utils.py @@ -21,7 +21,48 @@ def load_dictionary(p: str | Path) -> Dictionary: if not word.isalpha(): continue word = word.lower() - dict_obj.add_word(Word(word=word, hints=[], difficulty=1)) + + hints = [] + try: + if "senses" in obj: + for sense in obj["senses"]: + text = sense + if word in text.lower(): + continue + hints.append(text) + except Exception: + pass + + try: + if "synonyms" in obj: + for synonym in obj["synonyms"]: + text = synonym + if word in text.lower(): + continue + hints.append( text) + except Exception: + pass + + try: + if "antonyms" in obj: + for antonym in obj["antonyms"]: + text = antonym + if word in text.lower(): + continue + if "de" in p.name: + text = "Gegenteil von " + text + else: + text = "Opposite of " + text + hints.append( text) + except Exception: + pass + + if len(hints) > 0: + + w = Word(word=word, + hints=hints, + difficulty=1) + dict_obj.add_word(w) load_dictionary._cache[cache_key] = dict_obj return dict_obj diff --git a/multiplayer_crosswords/webui/big_icon.png b/multiplayer_crosswords/webui/big_icon.png index f479724..01f7750 100644 Binary files a/multiplayer_crosswords/webui/big_icon.png and b/multiplayer_crosswords/webui/big_icon.png differ diff --git a/multiplayer_crosswords/webui/favicon.png b/multiplayer_crosswords/webui/favicon.png index 4af4d5f..b449522 100644 Binary files a/multiplayer_crosswords/webui/favicon.png and b/multiplayer_crosswords/webui/favicon.png differ diff --git a/multiplayer_crosswords/webui/keyboard.js b/multiplayer_crosswords/webui/keyboard.js index 81ae929..1035d82 100644 --- a/multiplayer_crosswords/webui/keyboard.js +++ b/multiplayer_crosswords/webui/keyboard.js @@ -41,7 +41,12 @@ export class MobileKeyboard extends LitElement { return html`