diff --git a/README.md b/README.md index e30766f..6bdccc1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ # multiplayer_crosswords +This project is a web-based multiplayer crossword puzzle game that allows multiple users to collaborate in solving crossword puzzles in real-time. It features a user-friendly interface, session management, and real-time updates to enhance the collaborative experience. + +## installation + +1. Clone the repository: + ```bash + git clone https://the-cake-is-a-lie.net/gitea/jonas/multiplayer_crosswords.git + cd multiplayer_crosswords + ``` + +2. Install this repository as a package: + ```bash + pip install . + ``` + +## start the server + +```bash +python -m multiplayer_crosswords.server.main +``` + +## start the webui + +```bash +python -m multiplayer_crosswords.server.serve_frontend +``` diff --git a/multiplayer_crosswords/server/websocket_crossword_server.py b/multiplayer_crosswords/server/websocket_crossword_server.py index 5e02fcb..2515336 100644 --- a/multiplayer_crosswords/server/websocket_crossword_server.py +++ b/multiplayer_crosswords/server/websocket_crossword_server.py @@ -294,39 +294,12 @@ class WebsocketCrosswordServer(object): if current_grid_letter.upper() == msg_letter.upper(): # No change return - crossword.place_letter( - x=message.col, - y=message.row, - letter=msg_letter.lower(), - ) - # now check if the word is solved + # check if the letter already is solved, if so, ignore the update words_at_position = crossword.get_words_by_y_x_position(y=message.row, x=message.col) - is_solved = any(cw.solved for cw in words_at_position) - if is_solved: - logger.info("Word solved at position (%d, %d) in session %s", message.col, message.row, session.session_id) - messages = [] - for cw in words_at_position: - if cw.solved: - logger.info("Solved word: %s", cw.word) - # go through each letter in the word and create a message - for i in range(len(cw.word)): - if cw.orientation == Orientation.HORIZONTAL: - row = cw.start_y - col = cw.start_x + i - else: - row = cw.start_y + i - col = cw.start_x - letter = cw.word[i].upper() - msg = server_messages.LetterUpdateBroadcastServerMessage( - session_id=session.session_id, - row=row, - col=col, - letter=letter, - is_solved=True - ) - messages.append(msg) + if any(cw.solved for cw in words_at_position): + logger.info("Ignoring update to already solved position (%d, %d) in session %s", message.col, message.row, session.session_id) - else: + # send letter again to client to ensure they have the correct letter msg = server_messages.LetterUpdateBroadcastServerMessage( session_id=session.session_id, row=message.row, @@ -335,6 +308,50 @@ class WebsocketCrosswordServer(object): is_solved=is_solved ) messages = [msg] + + else: + # also check if the position is + crossword.place_letter( + x=message.col, + y=message.row, + letter=msg_letter.lower(), + ) + + words_at_position = crossword.get_words_by_y_x_position(y=message.row, x=message.col) + is_solved = any(cw.solved for cw in words_at_position) + if is_solved: + logger.info("Word solved at position (%d, %d) in session %s", message.col, message.row, session.session_id) + messages = [] + for cw in words_at_position: + if cw.solved: + logger.info("Solved word: %s", cw.word) + # go through each letter in the word and create a message + for i in range(len(cw.word)): + if cw.orientation == Orientation.HORIZONTAL: + row = cw.start_y + col = cw.start_x + i + else: + row = cw.start_y + i + col = cw.start_x + letter = cw.word[i].upper() + msg = server_messages.LetterUpdateBroadcastServerMessage( + session_id=session.session_id, + row=row, + col=col, + letter=letter, + is_solved=True + ) + messages.append(msg) + + else: + msg = server_messages.LetterUpdateBroadcastServerMessage( + session_id=session.session_id, + row=message.row, + col=message.col, + letter=msg_letter.upper(), + is_solved=is_solved + ) + messages = [msg] # NOTE: we do this purposefully outside of the session lock to avoid # potential deadlocks if sending messages takes time. @@ -342,9 +359,7 @@ class WebsocketCrosswordServer(object): for broadcast_message in messages: await session.send_message_to_all_clients(message=broadcast_message.model_dump()) - - def __init__(self, host: str, port: int): self._host = host self._port = port diff --git a/multiplayer_crosswords/webui/grid.js b/multiplayer_crosswords/webui/grid.js index 908b2b5..6066ea9 100644 --- a/multiplayer_crosswords/webui/grid.js +++ b/multiplayer_crosswords/webui/grid.js @@ -552,6 +552,36 @@ export class CrosswordGrid extends LitElement { this.requestUpdate(); } + /** + * Calculate completion ratio as percentage (0-100) + */ + _calculateCompletionRatio() { + let totalNonWallCells = 0; + let solvedCells = 0; + + for (let r = 0; r < this.rows; r++) { + for (let c = 0; c < this.cols; c++) { + if (this._grid[r][c] !== '#') { + totalNonWallCells++; + const cellKey = `${r},${c}`; + if (this._solvedCells.has(cellKey)) { + solvedCells++; + } + } + } + } + + if (totalNonWallCells === 0) return 0; + return Math.round((solvedCells / totalNonWallCells) * 100); + } + + /** + * Get current completion ratio (public method) + */ + getCompletionRatio() { + return this._calculateCompletionRatio(); + } + /** * Handle letter updates from server (broadcast messages from other players) */ @@ -589,6 +619,14 @@ export class CrosswordGrid extends LitElement { this.requestUpdate(); + // Calculate and emit completion ratio update + const completionRatio = this._calculateCompletionRatio(); + this.dispatchEvent(new CustomEvent('completion-ratio-changed', { + detail: { completionRatio }, + bubbles: true, + composed: true + })); + // Trigger animation if solution word just completed if (this._isSolutionWordComplete()) { this.updateComplete.then(() => { diff --git a/multiplayer_crosswords/webui/index.html b/multiplayer_crosswords/webui/index.html index 389f4db..a45f841 100644 --- a/multiplayer_crosswords/webui/index.html +++ b/multiplayer_crosswords/webui/index.html @@ -73,6 +73,9 @@ console.log('Subscribing to session:', sessionId); currentSessionId = sessionId; + // Update URL with session ID + updateUrlWithSessionId(sessionId); + // Show game UI immediately menu.style.display = 'none'; gridContainer.style.display = 'block'; @@ -134,7 +137,7 @@ // Create container with close button gridContainer.innerHTML = `