keep session id history
This commit is contained in:
@ -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
|
||||
|
||||
35
multiplayer_crosswords/server/serve_frontend.py
Normal file
35
multiplayer_crosswords/server/serve_frontend.py
Normal file
@ -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()
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -109,12 +109,29 @@ 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()
|
||||
max_tries = 4
|
||||
for i in range(max_tries):
|
||||
crossword = Crossword.generate(
|
||||
dictionary=dictionary,
|
||||
seed=None,
|
||||
@ -122,6 +139,8 @@ class MultiplayerSessionManager(object):
|
||||
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,
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 68 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 12 KiB |
@ -41,7 +41,12 @@ export class MobileKeyboard extends LitElement {
|
||||
|
||||
return html`
|
||||
<div class="keyboard-container">
|
||||
${html`<div class="handle" @click=${this._toggleCollapse}>${this.collapsed ? '▲' : '▼'}</div>`}
|
||||
${html`<div class="handle" @click=${this._toggleCollapse}>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" style="vertical-align: middle; margin-right: 0.3rem;">
|
||||
<path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm6 7H5v-2h8v2zm0-4h-2v-2h2v2zm3 0h-2v-2h2v2zm3 0h-2v-2h2v2zm0-3h-2V8h2v2z"/>
|
||||
</svg>
|
||||
${this.collapsed ? '▲' : '▼'}
|
||||
</div>`}
|
||||
<div class="keyboard-wrapper">
|
||||
<div class="keyboard">
|
||||
${rows.map((r, idx) => {
|
||||
|
||||
@ -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`
|
||||
<div class="menu-container">
|
||||
<div class="menu">
|
||||
<h1>Crossword</h1>
|
||||
<h1>Multiplayer Crossword</h1>
|
||||
|
||||
<p class="description">Collaborate with others to solve crosswords in real-time. Create a session and share the link with friends to play together!</p>
|
||||
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ''}
|
||||
|
||||
@ -175,6 +184,17 @@ export class CrosswordMenu extends LitElement {
|
||||
</div>
|
||||
|
||||
<button @click="${this._onCreateCrossword}">Create Crossword</button>
|
||||
|
||||
<div class="data-info">
|
||||
<span class="data-info-toggle" @click="${this._toggleDataInfo}">📋 Data & Privacy</span>
|
||||
<div class="data-info-details" style="display: none;">
|
||||
<ul>
|
||||
<li><strong>Shared Data:</strong> Only the letters you type into the grid during a session are shared with other users and the backend server in that session.</li>
|
||||
<li><strong>Session Lifetime:</strong> Sessions are automatically deleted after 48 hours of inactivity.</li>
|
||||
<li><strong>No Tracking:</strong> No personal data is collected or stored beyond the session duration.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -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)); }
|
||||
|
||||
@ -1,8 +1,20 @@
|
||||
const cacheName = 'pwa-conf-v4';
|
||||
const staticAssets = [
|
||||
'./',
|
||||
'./index.html',
|
||||
'./app.js',
|
||||
'./websocket.js' //TODO: add all necessary files
|
||||
|
||||
'./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'
|
||||
];
|
||||
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user