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`
- ${html`
${this.collapsed ? '▲' : '▼'}
`} + ${html`
+ + + + ${this.collapsed ? '▲' : '▼'} +
`}
${rows.map((r, idx) => { diff --git a/multiplayer_crosswords/webui/menu.js b/multiplayer_crosswords/webui/menu.js index 86d1d65..b7a061a 100644 --- a/multiplayer_crosswords/webui/menu.js +++ b/multiplayer_crosswords/webui/menu.js @@ -130,6 +130,13 @@ export class CrosswordMenu extends LitElement { notificationManager.info('Creating session...'); } + _toggleDataInfo() { + const element = this.querySelector('.data-info-details'); + if (element) { + element.style.display = element.style.display === 'none' ? 'block' : 'none'; + } + } + render() { if (this._loading) { return html` @@ -152,7 +159,9 @@ export class CrosswordMenu extends LitElement { return html`
`; diff --git a/multiplayer_crosswords/webui/styles.css b/multiplayer_crosswords/webui/styles.css index fff46e4..f6bc54d 100644 --- a/multiplayer_crosswords/webui/styles.css +++ b/multiplayer_crosswords/webui/styles.css @@ -657,26 +657,38 @@ mobile-keyboard .key-spacer { mobile-keyboard .handle { position: absolute; - top: -2.4rem; - left: 50%; - transform: translateX(-50%); + top: -1.6rem; + left: 0.75rem; + transform: none; background: - repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.02) 3px, rgba(255,255,255,.02) 5px), - repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.015) 4px, rgba(255,255,255,.015) 6px), - repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.02) 3px, rgba(0,0,0,.02) 5px), - repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.015) 4px, rgba(0,0,0,.015) 6px), - repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.02) 1px, transparent 1px), - repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.03) 1px, transparent 1px), - repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.015) 1.5px, transparent 1.5px), - linear-gradient(135deg, #0a0805 0%, #0f0c09 100%); - color: #a89f99; - padding: 0.55rem 0.9rem; + repeating-linear-gradient(87deg, transparent, transparent 3px, rgba(255,255,255,.01) 3px, rgba(255,255,255,.01) 5px), + repeating-linear-gradient(22deg, transparent, transparent 4px, rgba(255,255,255,.008) 4px, rgba(255,255,255,.008) 6px), + repeating-linear-gradient(59deg, transparent, transparent 3px, rgba(0,0,0,.012) 3px, rgba(0,0,0,.012) 5px), + repeating-linear-gradient(-11deg, transparent, transparent 4px, rgba(0,0,0,.008) 4px, rgba(0,0,0,.008) 6px), + repeating-radial-gradient(circle at 12% 18%, rgba(255,255,255,.01) 1px, transparent 1px), + repeating-radial-gradient(circle at 67% 77%, rgba(0,0,0,.015) 1px, transparent 1px), + repeating-radial-gradient(circle at 34% 51%, rgba(255,255,255,.008) 1.5px, transparent 1.5px), + linear-gradient(135deg, #1a1512 0%, #0f0c09 100%); + color: #7a7268; + padding: 0.35rem 0.6rem; border-radius: 0.25rem 0.25rem 0 0; - border: 1px solid #2a2520; + border: 1px solid rgba(58, 53, 48, 0.4); cursor: pointer; - box-shadow: 0 -2px 8px rgba(0,0,0,0.5); - font-size: 0.9rem; - font-weight: 600; + box-shadow: 0 -1px 3px rgba(0,0,0,0.3); + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.05em; + text-transform: uppercase; + opacity: 0.75; + transition: opacity 0.2s ease; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 0.3rem; +} + +mobile-keyboard .handle:hover { + opacity: 0.95; } mobile-keyboard[collapsed] .keyboard-container { transform: translateY(calc(100% - 2.6rem)); } diff --git a/multiplayer_crosswords/webui/sw.js b/multiplayer_crosswords/webui/sw.js index 46f10f3..b62c2dd 100644 --- a/multiplayer_crosswords/webui/sw.js +++ b/multiplayer_crosswords/webui/sw.js @@ -1,8 +1,20 @@ const cacheName = 'pwa-conf-v4'; const staticAssets = [ - './app.js ', - './websocket.js' //TODO: add all necessary files - + './', + './index.html', + './app.js', + './main.js', + './websocket.js', + './grid.js', + './clue_area.js', + './keyboard.js', + './menu.js', + './notification-area.js', + './notification-manager.js', + './styles.css', + './manifest.json', + './favicon.png', + './big_icon.png' ]; diff --git a/pyproject.toml b/pyproject.toml index 1fbad06..5b21faf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,17 @@ pytest = "^7.0" [tool.poetry.group.dev.dependencies] jupyterlab = "^4.4.3" +[[tool.poetry.packages]] +include = "multiplayer_crosswords" + +[[tool.poetry.include]] +path = "multiplayer_crosswords/webui" +format = "sdist" + +[[tool.poetry.include]] +path = "multiplayer_crosswords/webui" +format = "wheel" + [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api"