diff --git a/multiplayer_crosswords/webui/grid.js b/multiplayer_crosswords/webui/grid.js
index 0f70a46..9ef6649 100644
--- a/multiplayer_crosswords/webui/grid.js
+++ b/multiplayer_crosswords/webui/grid.js
@@ -18,6 +18,10 @@ export class CrosswordGrid extends LitElement {
_inputMode: { state: true }, // 'horizontal' or 'vertical'
_solvedCells: { state: true }, // tracks which cells are solved
_clueNumbers: { state: true }, // map of "row,col" -> { across: number, down: number }
+ _solutionIndices: { state: true }, // map of "row,col" -> solution index
+ _solutionWordPositions: { state: true }, // list of [col, row] positions for solution word
+ _solutionWordValues: { state: true }, // map of index -> letter for solution word
+ _solutionWordSolved: { state: true }, // set of solution word indices that are solved
};
// styles moved to webui/styles.css; render into light DOM so external CSS applies
@@ -31,6 +35,10 @@ export class CrosswordGrid extends LitElement {
this._inputMode = 'horizontal'; // default input mode
this._solvedCells = new Set(); // set of "r,c" strings for solved cells
this._clueNumbers = new Map(); // map of "row,col" -> { across: number, down: number }
+ this._solutionIndices = new Map(); // map of "row,col" -> solution index (1-indexed)
+ this._solutionWordPositions = []; // list of [col, row] positions
+ this._solutionWordValues = new Map(); // map of index -> letter
+ this._solutionWordSolved = new Set(); // set of solution word indices that are solved
this.sessionId = null; // Session ID for sending updates to server
}
@@ -73,8 +81,31 @@ export class CrosswordGrid extends LitElement {
this._ensureGrid();
// set CSS variables for cell-size and column count; layout done in external stylesheet
return html`
-
`;
}
/**
@@ -204,6 +242,14 @@ export class CrosswordGrid extends LitElement {
return end - start + 1;
}
+ /**
+ * Check if the entire solution word is solved
+ */
+ _isSolutionWordComplete() {
+ if (this._solutionWordPositions.length === 0) return false;
+ return this._solutionWordPositions.every((_, i) => this._solutionWordSolved.has(i));
+ }
+
/**
* Check if cell (r, c) is part of the vertical line from the selected cell
* (i.e., same column and not blocked by walls above/below this cell)
@@ -228,25 +274,35 @@ export class CrosswordGrid extends LitElement {
return r >= start && r <= end;
}
- _onCellClick(r, c) {
+ _onCellClick(r, c, preferredMode = null) {
// if same cell is clicked again, toggle the input mode
if (this._selected.r === r && this._selected.c === c) {
- this._inputMode = this._inputMode === 'horizontal' ? 'vertical' : 'horizontal';
+ // If a preferred mode is provided, use it (don't toggle)
+ if (preferredMode) {
+ this._inputMode = preferredMode;
+ } else {
+ this._inputMode = this._inputMode === 'horizontal' ? 'vertical' : 'horizontal';
+ }
} else {
// select a new cell
this._selected = { r, c };
- // auto-select mode based on line lengths
- const horizontalLength = this._getHorizontalLineLength(r, c);
- const verticalLength = this._getVerticalLineLength(r, c);
-
- // if one mode only has 1 cell but the other has multiple, use the one with multiple
- if (horizontalLength === 1 && verticalLength > 1) {
- this._inputMode = 'vertical';
- } else if (verticalLength === 1 && horizontalLength > 1) {
- this._inputMode = 'horizontal';
+ // Use preferred mode if provided, otherwise auto-select based on line lengths
+ if (preferredMode) {
+ this._inputMode = preferredMode;
+ } else {
+ // auto-select mode based on line lengths
+ const horizontalLength = this._getHorizontalLineLength(r, c);
+ const verticalLength = this._getVerticalLineLength(r, c);
+
+ // if one mode only has 1 cell but the other has multiple, use the one with multiple
+ if (horizontalLength === 1 && verticalLength > 1) {
+ this._inputMode = 'vertical';
+ } else if (verticalLength === 1 && horizontalLength > 1) {
+ this._inputMode = 'horizontal';
+ }
+ // otherwise keep current mode (both >1 or both =1)
}
- // otherwise keep current mode (both >1 or both =1)
}
this.requestUpdate();
this.dispatchEvent(new CustomEvent('cell-selected', { detail: { row: r, col: c, mode: this._inputMode }, bubbles: true, composed: true }));
@@ -254,6 +310,12 @@ export class CrosswordGrid extends LitElement {
this.focus();
}
+ _onSolutionCellClick(index, position) {
+ // When clicking a solution word cell, select the corresponding grid cell
+ const [col, row] = position;
+ this._onCellClick(row, col);
+ }
+
_onKeydown(e) {
// Only handle keys when the grid has focus
// Map letters, arrows and backspace to our handlers
@@ -483,7 +545,44 @@ export class CrosswordGrid extends LitElement {
this._solvedCells.delete(cellKey);
}
+ // Update solution word if this position is part of it
+ for (let i = 0; i < this._solutionWordPositions.length; i++) {
+ const [col_sw, row_sw] = this._solutionWordPositions[i];
+ if (row === row_sw && col === col_sw) {
+ this._solutionWordValues.set(i, letter);
+ // Mark solution word cell as solved
+ if (is_solved) {
+ this._solutionWordSolved.add(i);
+ } else {
+ this._solutionWordSolved.delete(i);
+ }
+ break;
+ }
+ }
+
this.requestUpdate();
+
+ // Trigger animation if solution word just completed
+ if (this._isSolutionWordComplete()) {
+ this.updateComplete.then(() => {
+ const gridContainer = this.querySelector('.solution-word-grid');
+ if (gridContainer) {
+ // Force reflow to trigger animation
+ gridContainer.offsetHeight;
+ gridContainer.classList.remove('complete');
+ gridContainer.offsetHeight;
+ gridContainer.classList.add('complete');
+ }
+ });
+ }
+
+ // Emit a letter-changed event so solution word can update
+ this.dispatchEvent(new CustomEvent('letter-changed', {
+ detail: { row, col, letter, is_solved },
+ bubbles: true,
+ composed: true
+ }));
+
console.log(`Letter update from server: [${row}, ${col}] = "${letter}" (solved: ${is_solved})`);
}
}
@@ -522,6 +621,49 @@ export class CrosswordGrid extends LitElement {
this.requestUpdate();
}
+
+ /**
+ * Populate solution word indices from server data
+ * @param {Array} solutionPositions - list of [col, row] positions in order
+ */
+ populateSolutionIndices(solutionPositions = []) {
+ this._solutionIndices.clear();
+ this._solutionWordPositions = solutionPositions;
+ this._solutionWordValues.clear();
+ this._solutionWordSolved.clear();
+
+ for (let i = 0; i < solutionPositions.length; i++) {
+ const [col, row] = solutionPositions[i];
+ const cellKey = `${row},${col}`;
+ this._solutionIndices.set(cellKey, i + 1); // 1-indexed
+
+ // Initialize solution word value with current grid letter
+ const letter = this._grid[row][col] || '';
+ this._solutionWordValues.set(i, letter);
+
+ // Check if this position is already solved
+ if (this._solvedCells.has(cellKey)) {
+ this._solutionWordSolved.add(i);
+ }
+ }
+
+ console.log('Solution word initialized. Solved:', this._solutionWordSolved.size, 'Total:', this._solutionWordPositions.length);
+ this.requestUpdate();
+
+ // Trigger animation on init if already complete
+ if (this._isSolutionWordComplete()) {
+ this.updateComplete.then(() => {
+ const gridContainer = this.querySelector('.solution-word-grid');
+ if (gridContainer) {
+ // Force reflow to trigger animation
+ gridContainer.offsetHeight;
+ gridContainer.classList.remove('complete');
+ gridContainer.offsetHeight;
+ gridContainer.classList.add('complete');
+ }
+ });
+ }
+ }
}
customElements.define('crossword-grid', CrosswordGrid);
\ No newline at end of file
diff --git a/multiplayer_crosswords/webui/index.html b/multiplayer_crosswords/webui/index.html
index 30d4252..df004c2 100644
--- a/multiplayer_crosswords/webui/index.html
+++ b/multiplayer_crosswords/webui/index.html
@@ -125,7 +125,14 @@
gridContainer.innerHTML = `
Crossword
- ✕
+
+
+
+
+ ✕
+
@@ -163,6 +170,7 @@
for (let r = 0; r < gridRows; r++) {
for (let c = 0; c < gridCols; c++) {
const cell = message.grid[r][c];
+
// Skip walls and empty cells
if (cell !== '#' && cell !== '') {
gridElement._grid[r][c] = cell;
@@ -181,6 +189,11 @@
// Populate clue numbers for display
gridElement.populateClueNumbers(message.clue_positions_across, message.clue_positions_down);
+ // Populate solution word
+ if (message.solution_word_positions) {
+ gridElement.populateSolutionIndices(message.solution_word_positions);
+ }
+
gridElement.requestUpdate();
console.log(`Grid created: ${gridRows}x${gridCols} with ${wallPositions.length} walls`);
@@ -188,6 +201,9 @@
if (message.solved_positions) {
console.log(`Solved positions: ${message.solved_positions.length}`);
}
+ if (message.solution_word_positions) {
+ console.log(`Solution word positions: ${message.solution_word_positions.length}`);
+ }
}, 0);
// Create and add clue area
@@ -197,9 +213,36 @@
clueArea.cluePositionsAcross = message.clue_positions_across;
clueArea.cluePositionsDown = message.clue_positions_down;
clueArea.grid = message.grid; // Pass grid for dimension calculation
+
+ // Setup gridData for solved clue tracking
+ const walls = new Set();
+ for (let r = 0; r < gridRows; r++) {
+ for (let c = 0; c < gridCols; c++) {
+ if (message.grid[r][c] === '#') {
+ walls.add(`${r},${c}`);
+ }
+ }
+ }
+ const solvedCells = new Set();
+ if (message.solved_positions) {
+ for (const [col, row] of message.solved_positions) {
+ solvedCells.add(`${row},${col}`);
+ }
+ }
+ clueArea.gridData = {
+ rows: gridRows,
+ cols: gridCols,
+ walls: walls,
+ solvedCells: solvedCells
+ };
+
clueArea.selectedRow = 0;
clueArea.selectedCol = 0;
clueArea.selectedMode = 'horizontal';
+
+ // Update solved clues initially
+ clueArea._updateSolvedClues();
+
document.body.insertBefore(clueArea, document.body.firstChild);
// Listen for cell selection changes
@@ -210,12 +253,157 @@
clueArea.requestUpdate();
});
+ // Listen for clue clicks to navigate grid
+ clueArea.addEventListener('clue-selected', (e) => {
+ const { row, col, mode } = e.detail;
+ // Call _onCellClick with preferred mode from the clue
+ gridElement._onCellClick(row, col, mode);
+ gridElement.focus();
+ });
+
+ // Listen for letter updates to update solved clues
+ gridElement.addEventListener('letter-changed', (e) => {
+ const { row, col, is_solved } = e.detail;
+ const cellKey = `${row},${col}`;
+ if (is_solved) {
+ clueArea.gridData.solvedCells.add(cellKey);
+ } else {
+ clueArea.gridData.solvedCells.delete(cellKey);
+ }
+ clueArea._updateSolvedClues();
+ clueArea.requestUpdate();
+ });
+
// Close button handler
closeBtn.addEventListener('click', closeGame);
+ const shareBtn = gridContainer.querySelector('.share-game-btn');
+ shareBtn.addEventListener('click', shareGame);
+
notificationManager.success('Game loaded successfully');
});
+ // Function to share game
+ function shareGame() {
+ console.log('Sharing game with session ID:', currentSessionId);
+
+ // Build URL with session ID
+ const url = `${window.location.origin}${window.location.pathname}?session_id=${currentSessionId}`;
+
+ // Try native share API first (mobile)
+ if (navigator.share) {
+ navigator.share({
+ title: 'Join my Crossword!',
+ text: 'Play crossword with me!',
+ url: url
+ }).then(() => {
+ console.log('Share successful');
+ }).catch(err => {
+ if (err.name !== 'AbortError') {
+ console.error('Error sharing:', err);
+ showShareDialog(url);
+ }
+ });
+ } else {
+ // Fallback: show dialog with link
+ showShareDialog(url);
+ }
+ }
+
+ // Function to show share dialog with copy option
+ function showShareDialog(url) {
+ console.log('Showing share dialog with URL:', url);
+
+ // Create modal dialog
+ const dialog = document.createElement('div');
+ dialog.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0,0,0,0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ `;
+
+ const content = document.createElement('div');
+ content.style.cssText = `
+ background: #2a2520;
+ padding: 1.5rem;
+ border-radius: 0.5rem;
+ max-width: 90%;
+ width: 100%;
+ max-width: 400px;
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
+ `;
+
+ content.innerHTML = `
+
Share Game Link
+
Copy this link and send it to friends:
+
+
+ Copy
+ Close
+
+ `;
+
+ dialog.appendChild(content);
+ document.body.appendChild(dialog);
+
+ // Copy button
+ document.getElementById('copy-btn').addEventListener('click', () => {
+ const input = document.getElementById('share-url-input');
+ input.select();
+ document.execCommand('copy');
+ notificationManager.success('Link copied!');
+ dialog.remove();
+ });
+
+ // Close button
+ document.getElementById('close-share-btn').addEventListener('click', () => {
+ dialog.remove();
+ });
+
+ // Close on background click
+ dialog.addEventListener('click', (e) => {
+ if (e.target === dialog) {
+ dialog.remove();
+ }
+ });
+ }
+
// Function to close game and return to menu
function closeGame() {
console.log('Closing game');
diff --git a/multiplayer_crosswords/webui/keyboard.js b/multiplayer_crosswords/webui/keyboard.js
index a8d69d0..81ae929 100644
--- a/multiplayer_crosswords/webui/keyboard.js
+++ b/multiplayer_crosswords/webui/keyboard.js
@@ -25,72 +25,97 @@ export class MobileKeyboard extends LitElement {
createRenderRoot() { return this; }
- render() {
- // simple QWERTY-like rows
- const rows = [
- 'qwertyuiop'.split(''),
- 'asdfghjkl'.split(''),
- 'zxcvbnm'.split(''),
- ];
+ render() {
+ // simple QWERTY-like rows
+ const rows = [
+ 'qwertyuiop'.split(''),
+ 'asdfghjkl'.split(''),
+ 'zxcvbnm'.split(''),
+ ];
- // compute the maximum number of columns across rows (account for backspace in first row)
- const counts = rows.map((r, idx) => r.length + (idx === 0 ? 1 : 0));
- const arrowCols = 3; // reserve 3 columns on the right for [left][down][right]
- const baseMax = Math.max(...counts, 10);
- const maxCols = baseMax;
+ // compute the maximum number of columns across rows (account for backspace in second row now)
+ const counts = rows.map((r, idx) => r.length + (idx === 1 ? 1 : 0));
+ const arrowCols = 3; // reserve 3 columns on the right for [left][down][right]
+ const baseMax = Math.max(...counts, 10);
+ const maxCols = baseMax;
- return html`
-
- ${html`
${this.collapsed ? '▲' : '▼'}
`}
-
-
- ${rows.map((r, idx) => {
- // center the letter keys leaving the rightmost `arrowCols` for the arrow block
+ return html`
+
+ ${html`
${this.collapsed ? '▲' : '▼'}
`}
+
+
+ ${rows.map((r, idx) => {
+ // center the letter keys leaving the rightmost `arrowCols` for the arrow block
- let rowClasses = 'row';
- if (idx === 1) rowClasses += ' stagger'; // A row
- if (idx === 2) rowClasses += ' stagger-deep'; // Z row needs a larger indent
- return html`