<m-prompt id="tb-iwb-move-prompt" message="Play a word on the ice board. Format: WORD H8 RIGHT or WORD H8 DOWN. Type RESET to clear." placeholder="TRADE H8 RIGHT" prefill="FIRE F8 DOWN" debug="false"><m-model id="tb-iwb-board-model" src="/assets/tb_ice_scrabble_board_downloads.glb" x="0" y="0" z="0" sx="8" sy="8" sz="8"></m-model><m-label id="tb-iwb-notice" x="0" y="2.05" z="3.95" rx="-24" width="8.4" height="0.48" font-size="20" alignment="center" color="#0b2334" font-color="#f5fbff" emissive="0" visible="false" content=""></m-label></m-prompt><m-light id="tb-iwb-preview-light" type="point" x="0" y="0" z="0" intensity="0" distance="0" color="#000000"></m-light><script> (function() { const BOARD_SIZE = 15; const CENTER = 7; const LETTER_SCORES = { A: 1, B: 3, C: 3, D: 2, E: 1, F: 4, G: 2, H: 4, I: 1, J: 8, K: 5, L: 1, M: 3, N: 1, O: 1, P: 3, Q: 10, R: 1, S: 1, T: 1, U: 1, V: 4, W: 4, X: 8, Y: 4, Z: 10 }; const tripleWord = new Set(["0,0", "0,7", "0,14", "7,0", "7,14", "14,0", "14,7", "14,14"]); const doubleWord = new Set(["1,1", "2,2", "3,3", "4,4", "10,10", "11,11", "12,12", "13,13", "1,13", "2,12", "3,11", "4,10", "10,4", "11,3", "12,2", "13,1", "7,7"]); const tripleLetter = new Set(["1,5", "1,9", "5,1", "5,5", "5,9", "5,13", "9,1", "9,5", "9,9", "9,13", "13,5", "13,9"]); const doubleLetter = new Set(["0,3", "0,11", "2,6", "2,8", "3,0", "3,7", "3,14", "6,2", "6,6", "6,8", "6,12", "7,3", "7,11", "8,2", "8,6", "8,8", "8,12", "11,0", "11,7", "11,14", "12,6", "12,8", "14,3", "14,11"]); const board = Array.from({ length: BOARD_SIZE }, () => Array(BOARD_SIZE).fill("")); const STARTER_CELLS = [{ row: 7, col: 5, letter: "F" }, { row: 7, col: 6, letter: "R" }, { row: 7, col: 7, letter: "O" }, { row: 7, col: 8, letter: "S" }, { row: 7, col: 9, letter: "T" }, { row: 5, col: 7, letter: "S" }, { row: 6, col: 7, letter: "N" }, { row: 8, col: 7, letter: "W" } ]; const state = { total: 0, moves: 0, player: 1 }; const prompt = document.getElementById("tb-iwb-move-prompt"); const notice = document.getElementById("tb-iwb-notice"); const TILE_Y = "1.66"; const TILE_SIZE = "0.42"; const TILE_FONT_SIZE = "56"; const TILE_FONT_COLOR = "#061827"; const TILE_NEW_COLOR = "#fff4bf"; const TILE_EXISTING_COLOR = "#e8fbff"; const TILE_NEW_EMISSIVE = "0"; const TILE_EXISTING_EMISSIVE = "0"; const TILE_SLAB_HEIGHT = "0.2"; const TILE_SLAB_Y = "1.56"; const TILE_SLAB_COLOR = "#aeefff"; const TILE_SLAB_OPACITY = "0.9"; function labelPosition(index) { const value = (index - CENTER) * 0.44; if (Math.abs(value) < 0.001) return "0"; return String(Math.round(value * 100) / 100); } function slabId(row, col) { return "tb-iwb-slab-" + row + "-" + col; } function ensureCellSlab(row, col) { const id = slabId(row, col); let slab = document.getElementById(id); if (!slab && prompt && notice && typeof document.createElement === "function") { slab = document.createElement("m-cube"); slab.setAttribute("id", id); slab.setAttribute("x", labelPosition(col)); slab.setAttribute("y", TILE_SLAB_Y); slab.setAttribute("z", labelPosition(row)); slab.setAttribute("width", TILE_SIZE); slab.setAttribute("height", TILE_SLAB_HEIGHT); slab.setAttribute("depth", TILE_SIZE); slab.setAttribute("color", TILE_SLAB_COLOR); slab.setAttribute("opacity", TILE_SLAB_OPACITY); slab.setAttribute("visible", "false"); prompt.insertBefore(slab, notice); } return slab; } function setCellSlab(row, col, visible, color) { const slab = ensureCellSlab(row, col); if (!slab) return; slab.setAttribute("visible", visible ? "true" : "false"); slab.setAttribute("color", color || TILE_SLAB_COLOR); slab.setAttribute("opacity", visible ? "0.92" : TILE_SLAB_OPACITY); } function configureTileLabel(el) { el.setAttribute("y", TILE_Y); el.setAttribute("rx", "-90"); el.setAttribute("width", TILE_SIZE); el.setAttribute("height", TILE_SIZE); el.setAttribute("font-size", TILE_FONT_SIZE); el.setAttribute("alignment", "center"); el.setAttribute("font-color", TILE_FONT_COLOR); el.setAttribute("emissive", TILE_EXISTING_EMISSIVE); } function ensureCellLabel(row, col) { const id = "tb-iwb-cell-" + row + "-" + col; let el = document.getElementById(id); ensureCellSlab(row, col); if (!el && prompt && notice && typeof document.createElement === "function") { el = document.createElement("m-label"); el.setAttribute("id", id); el.setAttribute("x", labelPosition(col)); el.setAttribute("z", labelPosition(row)); el.setAttribute("color", TILE_EXISTING_COLOR); el.setAttribute("content", ""); el.setAttribute("visible", "false"); setCellSlab(row, col, false, TILE_SLAB_COLOR); prompt.insertBefore(el, notice); } if (el) configureTileLabel(el); return el; } function createBoardLabels() { for (let row = 0; row < BOARD_SIZE; row += 1) { for (let col = 0; col < BOARD_SIZE; col += 1) { ensureCellLabel(row, col); } } } createBoardLabels(); let noticeTimer = null; let lastPromptKey = ""; let lastPromptAt = 0; function setText(el, text) { if (el) el.setAttribute("content", String(text)); } function debug(message) { try { if (typeof console !== "undefined" && console && typeof console.log === "function") { console.log(String(message)); } } catch (error) { } } function clearNoticeTimer() { if (!noticeTimer) return; const clearTimer = typeof clearTimeout === "function" ? clearTimeout : typeof window !== "undefined" && typeof window.clearTimeout === "function" ? window.clearTimeout.bind(window) : null; if (clearTimer) clearTimer(noticeTimer); noticeTimer = null; } function showNotice(message, kind) { if (!notice) return; clearNoticeTimer(); notice.setAttribute("content", String(message)); notice.setAttribute("visible", "true"); if (kind === "error") { notice.setAttribute("color", "#220711"); notice.setAttribute("font-color", "#ffb7ce"); notice.setAttribute("emissive", "0"); } else if (kind === "success") { notice.setAttribute("color", "#061827"); notice.setAttribute("font-color", "#aef4ff"); notice.setAttribute("emissive", "0"); } else { notice.setAttribute("color", "#0b2334"); notice.setAttribute("font-color", "#f5fbff"); notice.setAttribute("emissive", "0"); } const setTimer = typeof setTimeout === "function" ? setTimeout : typeof window !== "undefined" && typeof window.setTimeout === "function" ? window.setTimeout.bind(window) : null; if (setTimer) { noticeTimer = setTimer(function() { notice.setAttribute("visible", "false"); }, 3200); } } function clean(value) { return String(value || "").toUpperCase().replace(/[^A-Z0-9 ]/g, " ").replace(/\s+/g, " ").trim(); } function firstString() { for (let i = 0; i < arguments.length; i += 1) { const value = arguments[i]; if (typeof value === "string" && value.trim()) return value; if (typeof value === "number") return String(value); } return ""; } function readPromptValue(event) { const detail = event && event.detail ? event.detail : {}; const value = firstString( detail.value, detail.text, detail.prompt, detail.input, detail.message, detail.result, event && event.value, event && event.text, typeof event === "string" ? event : "" ); if (value) return value; if (prompt && typeof prompt.getAttribute === "function") { return firstString(prompt.getAttribute("value"), prompt.getAttribute("text")); } return ""; } function readConnectionId(event) { const detail = event && event.detail ? event.detail : {}; return firstString( detail.connectionId, detail.connectionID, detail.connection, event && event.connectionId ); } function isDuplicatePrompt(rawValue) { const key = clean(rawValue); const now = typeof Date !== "undefined" && Date && typeof Date.now === "function" ? Date.now() : 0; if (key && key === lastPromptKey && now && lastPromptAt && now - lastPromptAt < 750) { return true; } lastPromptKey = key; lastPromptAt = now; return false; } function parseCell(token) { let match = /^([A-O])([1-9]|1[0-5])$/.exec(token); if (match) { return { row: Number(match[2]) - 1, col: match[1].charCodeAt(0) - 65, label: match[1] + match[2] }; } match = /^([1-9]|1[0-5])([A-O])$/.exec(token); if (match) { return { row: Number(match[1]) - 1, col: match[2].charCodeAt(0) - 65, label: match[2] + match[1] }; } return null; } function parseMove(raw) { const value = clean(raw); if (!value) return { error: "Type a word, cell, and direction." }; if (value === "RESET" || value === "CLEAR") return { reset: true }; const tokens = value.split(" "); let direction = null; let cell = null; const wordParts = []; for (const token of tokens) { const maybeCell = parseCell(token); if (maybeCell) { cell = maybeCell; continue; } if (["RIGHT", "ACROSS", "HORIZONTAL", "R"].includes(token)) { direction = "RIGHT"; continue; } if (["DOWN", "VERTICAL", "D"].includes(token)) { direction = "DOWN"; continue; } if (/^[A-Z]+$/.test(token)) wordParts.push(token); } const word = wordParts.join(""); if (!word || word.length < 2) return { error: "Use at least two letters. Example: TRADE H8 RIGHT." }; if (word.length > BOARD_SIZE) return { error: "Word is too long for the board." }; if (!cell) return { error: "Missing board cell. Use A1 through O15." }; if (!direction) return { error: "Missing direction: RIGHT or DOWN." }; return { word, row: cell.row, col: cell.col, cell: cell.label, direction }; } function inBounds(row, col) { return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE; } function occupiedCount() { let count = 0; for (let row = 0; row < BOARD_SIZE; row += 1) { for (let col = 0; col < BOARD_SIZE; col += 1) { if (board[row][col]) count += 1; } } return count; } function adjacentOccupied(row, col) { return [ [row - 1, col], [row + 1, col], [row, col - 1], [row, col + 1] ].some(([r, c]) => inBounds(r, c) && board[r][c]); } function validatePlacement(move) { const dr = move.direction === "DOWN" ? 1 : 0; const dc = move.direction === "RIGHT" ? 1 : 0; const cells = []; let touchesExisting = false; let crossesCenter = false; let newLetters = 0; for (let i = 0; i < move.word.length; i += 1) { const row = move.row + dr * i; const col = move.col + dc * i; if (!inBounds(row, col)) return { error: "That word falls off the ice." }; const existing = board[row][col]; const letter = move.word[i]; if (existing && existing !== letter) return { error: "Conflict at " + String.fromCharCode(65 + col) + (row + 1) + "." }; if (existing === letter) touchesExisting = true; if (!existing) { newLetters += 1; if (adjacentOccupied(row, col)) touchesExisting = true; } if (row === CENTER && col === CENTER) crossesCenter = true; cells.push({ row, col, letter, existing: Boolean(existing) }); } if (newLetters === 0) return { error: "That word is already on the board." }; if (occupiedCount() === 0 && !crossesCenter) return { error: "First word must cross H8." }; if (occupiedCount() > 0 && !touchesExisting) return { error: "New word must touch existing ice letters." }; return { cells }; } function scoreMove(cells) { let total = 0; let wordMultiplier = 1; for (const cell of cells) { const key = cell.row + "," + cell.col; let letterScore = LETTER_SCORES[cell.letter] || 1; if (!cell.existing) { if (tripleLetter.has(key)) letterScore *= 3; if (doubleLetter.has(key)) letterScore *= 2; if (tripleWord.has(key)) wordMultiplier *= 3; if (doubleWord.has(key)) wordMultiplier *= 2; } total += letterScore; } return total * wordMultiplier; } function showCells(cells) { for (const cell of cells) { const el = ensureCellLabel(cell.row, cell.col); board[cell.row][cell.col] = cell.letter; if (!el) continue; el.setAttribute("content", cell.letter); el.setAttribute("visible", "true"); setCellSlab(cell.row, cell.col, true, cell.existing ? TILE_EXISTING_COLOR : TILE_NEW_COLOR); el.setAttribute("color", cell.existing ? TILE_EXISTING_COLOR : TILE_NEW_COLOR); el.setAttribute("font-color", TILE_FONT_COLOR); el.setAttribute("emissive", cell.existing ? TILE_EXISTING_EMISSIVE : TILE_NEW_EMISSIVE); } } function seedStarterBoard() { for (const cell of STARTER_CELLS) { const el = ensureCellLabel(cell.row, cell.col); board[cell.row][cell.col] = cell.letter; if (!el) continue; el.setAttribute("content", cell.letter); el.setAttribute("visible", "true"); setCellSlab(cell.row, cell.col, true, TILE_EXISTING_COLOR); el.setAttribute("color", TILE_EXISTING_COLOR); el.setAttribute("font-color", TILE_FONT_COLOR); el.setAttribute("emissive", TILE_EXISTING_EMISSIVE); } } function resetBoard() { for (let row = 0; row < BOARD_SIZE; row += 1) { for (let col = 0; col < BOARD_SIZE; col += 1) { board[row][col] = ""; const el = document.getElementById("tb-iwb-cell-" + row + "-" + col); if (el) { el.setAttribute("content", ""); el.setAttribute("visible", "false"); } setCellSlab(row, col, false, TILE_SLAB_COLOR); } } state.total = 0; state.moves = 0; state.player = 1; seedStarterBoard(); showNotice("Reset. Starter words restored. Try FIRE F8 DOWN", "info"); } function handlePrompt(event) { try { const rawValue = readPromptValue(event); if (isDuplicatePrompt(rawValue)) return; const parsed = parseMove(rawValue); debug("ice-word-board prompt: " + String(rawValue || "")); if (parsed.reset) { resetBoard(); return; } if (parsed.error) { showNotice(parsed.error, "error"); debug("ice-word-board error: " + parsed.error); return; } const check = validatePlacement(parsed); if (check.error) { showNotice(check.error, "error"); debug("ice-word-board error: " + check.error); return; } const points = scoreMove(check.cells); showCells(check.cells); state.total += points; state.moves += 1; const connectionId = readConnectionId(event); const connection = connectionId ? " C" + connectionId : ""; showNotice("P" + state.player + connection + ": " + parsed.word + " " + parsed.cell + " " + parsed.direction + " +" + points + " | Score " + state.total, "success"); debug("ice-word-board move: " + parsed.word + " " + parsed.cell + " " + parsed.direction + " +" + points); state.player = state.player === 1 ? 2 : 1; } catch (error) { const message = error && error.message ? error.message : "Prompt handler failed."; showNotice("Command error: " + message, "error"); debug("ice-word-board exception: " + message); } } if (prompt) { prompt.addEventListener("prompt", handlePrompt); prompt.addEventListener("submit", handlePrompt); } seedStarterBoard(); showNotice("Starter words locked: FROST + SNOW. Try FIRE F8 DOWN", "info"); })(); </script>
