Why? Because I ask for a change and something gets improved and something else breaks, if I save working code I have something to go back to.
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Kupu Rapu - Te Reo Māori Word Game (20 Levels)</title>
<script src="https://cdn.tailwindcss.com"></script>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
:root{ --tile-bg: rgba(255,255,255,0.2); }
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #f74ff7 0%, #f53da8 100%);
transition: background 600ms ease;
background-color: rgba(255, 255, 255, 0.09);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
text-shadow: 2px 2px 6px rgba(0, 0, 0, 0.35);
.level-controls { display:flex; gap:8px; align-items:center; margin-bottom:8px; }
.level-indicator { font-weight:700; padding:6px 10px; border-radius:9999px; background: rgba(255,255,255,0.12); }
.main-content { display:flex; width:100%; gap:18px; flex-direction:column; }
@media(min-width:768px){ .main-content{ flex-direction:row; align-items:flex-start; } }
.game-section{ flex:1; display:flex; flex-direction:column; align-items:center; }
.word-builder{ display:flex; justify-content:center; min-height:48px; gap:6px; margin-bottom:12px; background-color: rgba(255, 255, 255, 0.14); border-radius:12px; padding:8px; width:100%; text-transform:uppercase; }
.built-letter{ width:34px; height:34px; display:flex; justify-content:center; align-items:center; background:#fff; color:#000; font-weight:700; border-radius:8px; box-shadow:0 2px 6px rgba(0,0,0,0.12); animation:popIn 0.18s ease; }
@keyframes popIn{ 0%{ transform:scale(0); opacity:0 } 100%{ transform:scale(1); opacity:1 } }
.grid{ display:grid; grid-template-columns: repeat(6, 1fr); gap:8px; width:100%; max-width:520px; aspect-ratio: 6 / 5; margin-bottom:12px; position:relative; }
.tile{ position:relative; background-color:var(--tile-bg); color:black; font-size:clamp(0.9rem,3.5vw,1.6rem); font-weight:700; border-radius:12px; cursor:pointer; user-select:none; display:flex; justify-content:center; align-items:center; transition: transform 0.18s, box-shadow 0.18s, opacity 0.25s, top 0.35s ease-out; text-transform:uppercase; box-shadow:0 4px 8px rgba(0,0,0,0.12); }
.tile.hidden-tile{ opacity:0 }
.tile:hover{ transform: translateY(-2px); box-shadow:0 6px 12px rgba(0,0,0,0.16); }
.selected{ transform:scale(1.06); outline:3px solid white; outline-offset:-4px; }
.message-box{ background-color: rgba(0,0,0,0.32); color:white; padding:10px; border-radius:10px; margin-bottom:10px; font-weight:700; width:100%; }
.win-overlay{ position:absolute; top:0; left:0; width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; background-color:rgba(0,0,0,0.3); z-index:10; }
.win-star{ width:60%; height:60%; background-color:rgba(255,255,255,0.2); clip-path: polygon(50% 0%,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%); display:flex; justify-content:center; align-items:center; }
.win-text{ font-size:2.5rem; font-weight:700; color:#fff; text-shadow:2px 2px 6px rgba(0,0,0,0.5); margin-top:16px; }
.win-buttons{ display:flex; gap:8px; margin-top:16px; }
.win-button{ background-color: rgba(0,0,0,0.28); color:white; padding:8px 16px; border-radius:9999px; font-weight:700; transition:transform 0.15s; border:none; }
.win-button:hover{ transform: translateY(-3px); }
.word-list-section{ width:320px; min-width:240px; max-width:340px; background-color: rgba(0,0,0,0.14); border-radius:12px; padding:12px; display:flex; flex-direction:column; align-items:stretch; }
.word-list-heading{ font-size:1.05rem; font-weight:700; margin-bottom:8px; }
.word-list{ display:flex; flex-direction:column; gap:8px; }
.word-item{ border-radius:8px; padding:6px 8px; font-weight:700; display:flex; justify-content:space-between; align-items:center; }
.word-item-text{ font-size:1.05rem; font-weight:800; color:black; }
.word-item-count{ font-size:1rem; font-weight:700; color:black; }
.tick{ font-size:1.2rem; margin-left:6px; color:black; }
.star-container{ display:grid; grid-template-columns: repeat(5, 1fr); grid-template-rows: repeat(2, 1fr); gap:8.4px; margin-top:12px; width:100%; max-width:240px; justify-content:center; margin-left:auto; margin-right:auto; }
.star{ width:32px; height:32px; background-color:#ffffff; clip-path: polygon(50% 0%,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%); cursor:pointer; transition: transform 0.18s, background-color 0.18s; transform:scale(2); }
.star.used{ background-color:#9ca3af; transform:scale(1); cursor:default; }
.bg-red-400{ background-color:#f87171 !important; }
.bg-white-400{ background-color:#f5f5f0 !important; }
.bg-orange-400{ background-color:#fb923c !important; }
.bg-yellow-400{ background-color:#facc15 !important; }
.bg-green-400{ background-color:#4ade80 !important; }
.bg-blue-400{ background-color:#60a5fa !important; }
.bg-purple-400{ background-color:#a78bfa !important; }
.bg-pink-400{ background-color:#f472b6 !important; }
.bg-teal-400{ background-color:#2dd4bf !important; }
.bg-brown-400{ background-color:#ba7f3f !important; }
.bg-black-400{ background-color:#7b7880 !important; }
.text-black{ color:black !important; }
.controls-row{ display:flex; gap:8px; align-items:center; justify-content:center; margin-bottom:8px; }
button{ background-color: rgba(0,0,0,0.28); color:white; padding:8px 14px; border-radius:9999px; font-weight:700; transition:transform 0.15s; border:none; }
button:hover{ transform: translateY(-3px); }
<h1 class="title">Kupu Rapu</h1>
<p class="subtitle">Drag to connect adjacent letters to find words. Complete the level to reveal translations!</p>
<div class="level-controls">
<div class="controls-row">
<button id="prev-level">◀ Prev</button>
<div class="level-indicator" id="level-indicator">Level 1 / 20</div>
<button id="next-level">Next ▶</button>
<div class="main-content">
<div id="game-container" class="game-section">
<div id="word-builder" class="word-builder"></div>
<div id="grid-container" class="grid"></div>
<div id="message-box" class="message-box opacity-0 transition-opacity duration-300"></div>
<div id="win-screen" class="hidden"></div>
<div class="word-list-section">
<div class="word-list-heading">Words to find</div>
<div id="word-list" class="word-list"></div>
<div class="star-container" id="star-container"></div>
document.addEventListener('DOMContentLoaded', () => {
const gridContainer = document.getElementById('grid-container');
const messageBox = document.getElementById('message-box');
const winScreen = document.getElementById('win-screen');
const gameContainer = document.getElementById('game-container');
const wordListEl = document.getElementById('word-list');
const wordBuilderEl = document.getElementById('word-builder');
const starContainer = document.getElementById('star-container');
const prevLevelBtn = document.getElementById('prev-level');
const nextLevelBtn = document.getElementById('next-level');
const levelIndicator = document.getElementById('level-indicator');
const FIND_COUNT_PER_WORD = 1;
let foundWordCounts = {};
let wordPlacementCounts = {};
const letters = ['a','e','h','i','k','m','n','o','p','r','t','u','w'];
bg: 'linear-gradient(170deg, #60E61D 0%, #74F0F0 100%)',
{ w: 'ākonga', t: 'student', c: 'bg-green-400' },
{ w: 'akomanga', t: 'classroom', c: 'bg-pink-400' },
{ w: 'kaiako', t: 'teacher', c: 'bg-blue-400' },
{ w: 'tamariki', t: 'children', c: 'bg-yellow-400' },
{ w: 'korero', t: 'speak', c: 'bg-red-400'}
bg: 'linear-gradient(135deg, #4ECDC4 0%, #1A535C 100%)',
{ w: 'whānau', t: 'family', c: 'bg-yellow-400' },
{ w: 'tūpuna', t: 'ancestors', c: 'bg-teal-400' },
{ w: 'māmā', t: 'mother', c: 'bg-red-400' },
{ w: 'pāpā', t: 'father', c: 'bg-orange-400' },
{ w: 'kuia', t: 'elderly woman', c: 'bg-blue-400' }
bg: 'linear-gradient(135deg, #2A6041 0%, #A8D5BA 100%)',
{ w: 'manu', t: 'bird', c: 'bg-pink-400' },
{ w: 'ngahere', t: 'forest', c: 'bg-yellow-400' },
{ w: 'rākau', t: 'tree', c: 'bg-green-400' },
{ w: 'pipi', t: 'shellfish', c: 'bg-teal-400' },
{ w: 'moana', t: 'sea', c: 'bg-blue-400' }
bg: 'linear-gradient(135deg, #FF9F43 0%, #EE6352 100%)',
{ w: 'kākahu', t: 'clothes', c: 'bg-teal-400' },
{ w: 'panekoti', t: 'skirt', c: 'bg-yellow-400' },
{ w: 'tarau', t: 'trousers', c: 'bg-orange-400' },
{ w: 'pōtae', t: 'hat', c: 'bg-green-400' },
{ w: 'poraka', t: 'jumper', c: 'bg-blue-400' }
bg: 'linear-gradient(135deg, #483D8B 0%, #B0C4DE 100%)',
{ w: 'papatuanuku', t: 'Earth Mother', c: 'bg-green-400' },
{ w: 'ranginui', t: 'Sky Father', c: 'bg-blue-400' },
{ w: 'tāne', t: 'Forest God', c: 'bg-yellow-400' },
{ w: 'tangaroa', t: 'Sea God', c: 'bg-teal-400' },
{ w: 'rongo', t: 'God of Peace', c: 'bg-orange-400' }
bg: 'linear-gradient(135deg, #FF6F61 0%, #D4A5A5 100%)',
{ w: 'kōwhai', t: 'yellow', c: 'bg-yellow-400' },
{ w: 'parauri', t: 'brown', c: 'bg-brown-400' },
{ w: 'karaka', t: 'orange', c: 'bg-orange-400' },
{ w: 'kākāriki', t: 'green', c: 'bg-green-400' },
{ w: 'pango', t: 'black', c: 'bg-black-400' }
bg: 'linear-gradient(135deg, #1B4332 0%, #74C69D 100%)',
{ w: 'tokohia', t: 'many', c: 'bg-green-400' },
{ w: 'tuatahi', t: 'first', c: 'bg-blue-400' },
{ w: 'tuarua', t: 'second', c: 'bg-yellow-400' },
{ w: 'tuawhā', t: 'fourth', c: 'bg-orange-400' },
{ w: 'tokorima', t: 'five', c: 'bg-red-400' }
name: 'Taonga & Pounamu',
bg: 'linear-gradient(135deg, #74C69D 0%, #B5EAD7 100%)',
{ w: 'taonga', t: 'treasure', c: 'bg-yellow-400' },
{ w: 'pounamu', t: 'greenstone', c: 'bg-teal-400' },
{ w: 'ngākau', t: 'heart', c: 'bg-red-400' },
{ w: 'mārama', t: 'clarity', c: 'bg-blue-400' },
{ w: 'pakiwaitara', t: 'story', c: 'bg-orange-400' }
bg: 'linear-gradient(170deg, #60E61D 0%, #74F0F0 100%)',
{ w: 'tupuānuku', t: 'star 1', c: 'bg-pink-400' },
{ w: 'tupuārangi', t: 'star 2', c: 'bg-blue-400' },
{ w: 'waitī', t: 'star 3', c: 'bg-yellow-400' },
{ w: 'waitā', t: 'star 4', c: 'bg-green-400' },
{ w: 'pōhutukawa', t: 'star 5', c: 'bg-orange-400' }
bg: 'linear-gradient(135deg, #FFD700 0%, #FF8C00 100%)',
{w:'rangatahi', t:'youth', c:'bg-white-400'},
{w:'turangawaewae', t:'home', c:'bg-pink-400'},
{w:'whakapapa', t:'genealogy', c:'bg-teal-400'},
{w:'tauira', t:'apprentice', c:'bg-orange-400'},
{w:'akoranga', t:'learning', c:'bg-yellow-400'}
bg: 'linear-gradient(135deg, #483D8B 0%, #B0C4DE 100%)',
{w:'wairua', t:'spirit', c:'bg-yellow-400'},
{w:'karakia', t:'prayer', c:'bg-blue-400'},
{w:'mātauranga', t:'knowledge', c:'bg-orange-400'},
{w:'rongoā', t:'medicine', c:'bg-green-400'},
{w:'tikanga', t:'customs', c:'bg-teal-400'}
bg: 'linear-gradient(135deg, #2A6041 0%, #A8D5BA 100%)',
{w:'aroha', t:'love', c:'bg-pink-400'},
{w:'hīkoi', t:'walk', c:'bg-blue-400'},
{w:'whare', t:'house', c:'bg-green-400'},
{w:'mahi', t:'work', c:'bg-orange-400'},
{w:'waiata', t:'song', c:'bg-teal-400'}
name: 'Community & Place',
bg: 'linear-gradient(135deg, #FF9F43 0%, #EE6352 100%)',
{w:'rohe', t:'region', c:'bg-yellow-400'},
{w:'whenua', t:'land', c:'bg-green-400'},
{w:'marae', t:'Māori community centre', c:'bg-blue-400'},
{w:'mōkai', t:'pet', c:'bg-red-400'},
{w:'iwi', t:'tribe', c:'bg-orange-400'}
bg: 'linear-gradient(135deg, #FDC830 0%, #F37335 100%)',
{ w: 'hiakai', t: 'hungry', c: 'bg-orange-400' },
{ w: 'hiahia', t: 'thirsty', c: 'bg-blue-400' },
{ w: 'ngenge', t: 'tired', c: 'bg-gray-400' },
{ w: 'hari', t: 'happy', c: 'bg-yellow-400' },
{ w: 'hīkaka', t: 'excited', c: 'bg-pink-400' }
bg: 'linear-gradient(135deg, #00C9FF 0%, #92FE9D 100%)',
{ w: 'oma', t: 'run', c: 'bg-pink-400' },
{ w: 'peke', t: 'jump', c: 'bg-green-400' },
{ w: 'kanikani', t: 'dance', c: 'bg-white-400' },
{ w: 'kauhoe', t: 'swim', c: 'bg-blue-400' },
{ w: 'eke', t: 'ride', c: 'bg-yellow-400' }
bg: 'linear-gradient(135deg, #11998E 0%, #38EF7D 100%)',
{ w: 'paihikara', t: 'bike', c: 'bg-orange-400' },
{ w: 'motokā', t: 'car', c: 'bg-blue-400' },
{ w: 'tereina', t: 'train', c: 'bg-red-400' },
{ w: 'tāki', t: 'taxi', c: 'bg-yellow-400' },
{ w: 'pahi', t: 'bus', c: 'bg-green-400' }
name: 'Taniwha & Pūrākau',
bg: 'linear-gradient(135deg, #6A11CB 0%, #2575FC 100%)',
{ w: 'taniwha', t: 'guardian spirit', c: 'bg-green-400' },
{ w: 'patupaiarehe', t: 'fairy', c: 'bg-pink-400' },
{ w: 'pōtiki', t: 'youngest child', c: 'bg-blue-400' },
{ w: 'tipua', t: 'supernatural being', c: 'bg-white-400' },
{ w: 'kaitiaki', t: 'guardian', c: 'bg-yellow-400' }
bg: 'linear-gradient(135deg, #F7971E 0%, #FFD200 100%)',
{ w: 'kai', t: 'food', c: 'bg-red-400' },
{ w: 'miraka', t: 'milk', c: 'bg-blue-400' },
{ w: 'parāoa', t: 'bread', c: 'bg-yellow-400' },
{ w: 'hua rākau', t: 'fruit', c: 'bg-green-400' },
{ w: 'mīti', t: 'meat', c: 'bg-orange-400' }
bg: 'linear-gradient(135deg, #36D1DC 0%, #5B86E5 100%)',
{ w: 'rā', t: 'sun', c: 'bg-yellow-400' },
{ w: 'marama', t: 'moon', c: 'bg-gray-400' },
{ w: 'whetū', t: 'star', c: 'bg-green-400' },
{ w: 'ua', t: 'rain', c: 'bg-blue-400' },
{ w: 'kapua', t: 'cloud', c: 'bg-white' }
bg: 'linear-gradient(170deg, #60E61D 0%, #74F0F0 100%)',
{ w: 'whutupōro', t: 'rugby', c: 'bg-green-400' },
{ w: 'poitarawhiti', t: 'netball', c: 'bg-pink-400' },
{ w: 'poikiri', t: 'soccer', c: 'bg-blue-400' },
{ w: 'kaihaka', t: 'performer', c: 'bg-orange-400' },
{ w: 'tākaro', t: 'game/play', c: 'bg-yellow-400' }
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
function createTile(row, col, letter, colorClass) {
const tile = document.createElement('div');
tile.classList.add('tile');
if (colorClass) tile.classList.add(colorClass);
tile.classList.add('text-black');
tile.dataset.letter = letter;
tile.innerHTML = `<span>${letter.toUpperCase()}</span>`;
function createStar(index) {
const star = document.createElement('div');
star.classList.add('star');
star.dataset.index = index;
star.style.backgroundColor = starsUsedCount <= index ? '#ffffff' : '#9ca3af';
star.style.transform = starsUsedCount <= index ? 'scale(2)' : 'scale(1)';
if (starsUsedCount <= index) {
star.addEventListener('click', () => handleStarClick(index));
star.classList.add('used');
starContainer.appendChild(star);
starContainer.innerHTML = '';
for (let i = 0; i < TOTAL_STARS; i++) {
const star = createStar(i);
stars.push({ element: star, used: starsUsedCount > i });
function displayWordList() {
wordListEl.innerHTML = '';
wordsToFind.forEach(word => {
const count = foundWordCounts[word] || 0;
const info = wordInfo[word];
const wordItem = document.createElement('div');
wordItem.classList.add('word-item');
if (info && info.color) wordItem.classList.add(info.color);
const left = document.createElement('div');
left.style.display = 'flex';
left.style.flexDirection = 'column';
left.style.alignItems = 'flex-start';
const wordText = document.createElement('span');
wordText.classList.add('word-item-text');
wordText.textContent = word.toUpperCase();
const translation = document.createElement('small');
translation.style.fontWeight = '600';
translation.style.marginTop = '4px';
translation.style.opacity = count > 0 ? '1' : '0.0';
translation.textContent = count > 0 ? `(${info ? info.translation : ''})` : '';
left.appendChild(wordText);
left.appendChild(translation);
const wordCount = document.createElement('span');
wordCount.classList.add('word-item-count');
wordCount.innerHTML = count > 0 ? '<span class="tick">✓</span>' : '';
wordItem.appendChild(left);
wordItem.appendChild(wordCount);
wordListEl.appendChild(wordItem);
function placeWord(wordObj, board) {
const directions = [[0,1],[0,-1],[1,0],[-1,0],[1,1],[1,-1],[-1,1],[-1,-1]];
const startTime = performance.now();
while (!placed && attempts < maxAttempts && performance.now() - startTime < 1000) {
const startR = Math.floor(Math.random() * GRID_SIZE);
const startC = Math.floor(Math.random() * GRID_SIZE);
const path = [{r:startR,c:startC}];
const visited = new Set([`${startR},${startC}`]);
const findPath = (r,c,wordIndex,depth=0) => {
if (wordIndex === word.length) return true;
if (depth > word.length * 2) return false;
for (const [dr,dc] of directions) {
const key = `${newR},${newC}`;
if (newR>=0 && newR<GRID_SIZE && newC>=0 && newC<GRID_SIZE && !visited.has(key)) {
const cell = board[newR][newC];
if (cell === null || cell === undefined || cell.word === null) {
path.push({r:newR,c:newC});
if (findPath(newR,newC,wordIndex+1,depth+1)) return true;
const startCell = board[startR][startC];
if (startCell === null || startCell === undefined || startCell.word === null) {
if (findPath(startR,startC,1)) {
if (path.length === word.length) {
for (let i=0;i<word.length;i++){
board[r][c] = { letter: word[i], word: word, color: wordObj.c };
wordPlacementCounts[word] = (wordPlacementCounts[word] || 0) + 1;
function validateBoard(board) {
const wordsOnGrid = new Set();
const directions = [[0,1],[0,-1],[1,0],[-1,0],[1,1],[1,-1],[-1,1],[-1,-1]];
function checkPath(r, c, word, wordIndex, path, visited, depth=0) {
if (wordIndex === word.length) return true;
if (depth > word.length * 2) return false;
for (const [dr, dc] of directions) {
const newR = r + dr; const newC = c + dc;
const key = `${newR},${newC}`;
if (newR >= 0 && newR < GRID_SIZE && newC >= 0 && newC < GRID_SIZE && !visited.has(key)) {
if (board[newR][newC] && board[newR][newC].letter === word[wordIndex]) {
visited.add(key); path.push({r:newR,c:newC});
if (checkPath(newR, newC, word, wordIndex + 1, path, visited, depth+1)) return true;
path.pop(); visited.delete(key);
for (const word of wordsToFind) {
for (let r = 0; r < GRID_SIZE && !found; r++) {
for (let c = 0; c < GRID_SIZE && !found; c++) {
if (board[r][c] && board[r][c].letter === word[0]) {
const visited = new Set([`${r},${c}`]);
if (checkPath(r, c, word, 1, path, visited) && path.length === word.length) {
return wordsOnGrid.size === wordsToFind.length;
function refreshBoard() {
const MAX_OVERALL_ATTEMPTS = 50;
const startTime = performance.now();
let words = wordsToFind.map(w=>wordInfo[w].obj);
words.sort((a, b) => b.w.length - a.w.length);
while (!success && overallAttempt < MAX_OVERALL_ATTEMPTS && performance.now() - startTime < 2000) {
const localBoard = Array.from({length:GRID_SIZE},()=>Array(GRID_SIZE).fill(null));
for (const wobj of words) {
const placed = placeWord(wobj, localBoard);
if (!placed) { allPlaced = false; break; }
if (allPlaced && validateBoard(localBoard)) {
for (let r=0;r<GRID_SIZE;r++){
for (let c=0;c<GRID_SIZE;c++){
if (localBoard[r][c] === null) localBoard[r][c] = { letter: getRandomLetter(), word: null, color: 'bg-purple-400' };
gameBoard = Array.from({length:GRID_SIZE},()=>Array(GRID_SIZE).fill(null));
for (const w of words) placeWord(w, gameBoard);
for (let r=0;r<GRID_SIZE;r++) for (let c=0;c<GRID_SIZE;c++) if (gameBoard[r][c]===null) gameBoard[r][c] = { letter: getRandomLetter(), word:null, color:'bg-purple-400' };
if (!validateBoard(gameBoard)) {
showMessage('Board may be incomplete. Use a star to refresh.');
wordPlacementCounts = {};
const level = LEVELS[currentLevel];
wordsToFind = level.words.map(o => o.w);
level.words.forEach(o => {
wordInfo[o.w] = { translation: o.t, color: o.c, obj: o };
foundWordCounts[o.w] = 0;
wordPlacementCounts[o.w] = 0;
document.body.style.background = level.bg;
levelIndicator.textContent = `Level ${currentLevel+1} / ${LEVELS.length} — ${level.name}`;
for (let r=0;r<GRID_SIZE;r++){
if (!gameBoard[r]) gameBoard[r] = Array(GRID_SIZE).fill(null);
for (let c=0;c<GRID_SIZE;c++) if (!gameBoard[r][c]) gameBoard[r][c] = { letter: getRandomLetter(), word:null, color:'bg-purple-400' };
gridContainer.innerHTML = '';
gameBoard.forEach((row,r)=>{
const tile = createTile(r,c,cell.letter, cell.color);
tile.addEventListener('click', ()=>handleTileClick(r,c));
gridContainer.appendChild(tile);
function getRandomLetter(){ return letters[Math.floor(Math.random()*letters.length)]; }
function getSelectedWord(){ return selectedTiles.map(t=>gameBoard[t.row][t.col].letter).join(''); }
function isValidAdjacency(newTile){ if (selectedTiles.length===0) return true; const last = selectedTiles[selectedTiles.length-1]; const dx=Math.abs(newTile.row-last.row); const dy=Math.abs(newTile.col-last.col); return (dx<=1 && dy<=1 && (dx>0||dy>0)); }
function showMessage(text, duration=1400){
messageBox.textContent = text;
messageBox.style.opacity = '1';
setTimeout(()=>{ messageBox.style.opacity='0'; }, duration);
function clearHighlights(){
document.querySelectorAll('.tile.selected').forEach(t=>t.classList.remove('selected'));
wordBuilderEl.innerHTML='';
function handleStarClick(index) {
if (stars[index].used || isDragging) return;
stars[index].used = true;
stars[index].element.classList.add('used');
stars[index].element.style.backgroundColor = '#9ca3af';
stars[index].element.style.transform = 'scale(1)';
stars[index].element.removeEventListener('click', () => handleStarClick(index));
showMessage('Board refreshed!');
function getAdjacentTiles(row,col){
const dirs=[[0,0],[0,1],[0,-1],[1,0],[-1,0],[1,1],[1,-1],[-1,1],[-1,-1]];
for(const [dr,dc] of dirs){
const nr=row+dr, nc=col+dc;
if(nr>=0 && nr<GRID_SIZE && nc>=0 && nc<GRID_SIZE) tiles.push({row:nr,col:nc});
function handleTileClick(row,col) {}
const clientX = e.clientX !== undefined ? e.clientX : (e.touches && e.touches[0] && e.touches[0].clientX);
const clientY = e.clientY !== undefined ? e.clientY : (e.touches && e.touches[0] && e.touches[0].clientY);
const target = document.elementFromPoint(clientX, clientY);
const tile = target && target.closest('.tile');
if (tile && tile !== lastTile){
const row=parseInt(tile.dataset.row);
const col=parseInt(tile.dataset.col);
const isAlready = selectedTiles.some(t=>t.row===row && t.col===col);
if (!isAlready && isValidAdjacency(newTile)){
selectedTiles.push(newTile);
tile.classList.add('selected');
const built=document.createElement('div');
built.classList.add('built-letter');
built.textContent = gameBoard[row][col].letter.toUpperCase();
wordBuilderEl.appendChild(built);
const word = getSelectedWord().toLowerCase();
const foundWord = wordsToFind.find(w=>w===word);
if (foundWord && word.length >= 2) {
foundWordCounts[foundWord] = (foundWordCounts[foundWord]||0) + 1;
showMessage('Mīharo - you got it!');
dropLettersAndRefill(selectedTiles);
} else if (word.length > 1) {
showMessage('That is not a valid word.');
function tryToPlaceNewWord(){
let wordsNotOnGrid = wordsToFind.filter(w=>foundWordCounts[w] < FIND_COUNT_PER_WORD);
const wordsOnGrid = new Set();
for (let r=0;r<GRID_SIZE;r++) for (let c=0;c<GRID_SIZE;c++) if (gameBoard[r][c] && gameBoard[r][c].word) wordsOnGrid.add(gameBoard[r][c].word);
wordsNotOnGrid = wordsNotOnGrid.filter(w=>!wordsOnGrid.has(w));
wordsNotOnGrid.sort((a, b) => b.length - a.length);
if (wordsNotOnGrid.length>0){
for (const w of wordsNotOnGrid){
if (placeWord(wordInfo[w].obj, gameBoard)) return true;
function replaceRandomLetters(){
for (let r=0;r<GRID_SIZE;r++) for (let c=0;c<GRID_SIZE;c++) if (!gameBoard[r][c] || !gameBoard[r][c].word) nonWordTiles.push({r,c});
const tilesToReplace = nonWordTiles.slice(0, Math.min(20, nonWordTiles.length));
tilesToReplace.forEach(({r,c})=> gameBoard[r][c]=null);
const wordsOnGrid = new Set();
for (let r=0;r<GRID_SIZE;r++) for (let c=0;c<GRID_SIZE;c++) if (gameBoard[r][c] && gameBoard[r][c].word) wordsOnGrid.add(gameBoard[r][c].word);
let wordsNotOnGrid = wordsToFind.filter(w=>foundWordCounts[w] < FIND_COUNT_PER_WORD).filter(w=>!wordsOnGrid.has(w));
wordsNotOnGrid.sort((a, b) => b.length - a.length);
for (const word of wordsNotOnGrid) {
placeWord(wordInfo[word].obj, gameBoard);
for (let r=0;r<GRID_SIZE;r++) for (let c=0;c<GRID_SIZE;c++) if (gameBoard[r][c]===null) gameBoard[r][c] = { letter: getRandomLetter(), word:null, color:'bg-purple-400' };
function dropLettersAndRefill(tilesToRemove){
const newGameBoard = Array.from({length:GRID_SIZE},()=>Array(GRID_SIZE).fill(null));
const removeSet = new Set(tilesToRemove.map(t=>`${t.row},${t.col}`));
for (let c=0;c<GRID_SIZE;c++){
for (let r=GRID_SIZE-1;r>=0;r--){
if (!removeSet.has(coord) && gameBoard[r][c]) columnStack.push(gameBoard[r][c]);
let fillRow = GRID_SIZE-1;
for (let i=0;i<columnStack.length;i++){
newGameBoard[fillRow][c] = columnStack[i];
gameBoard = newGameBoard;
for (let r=0;r<GRID_SIZE;r++) for (let c=0;c<GRID_SIZE;c++) if (gameBoard[r][c]===null) gameBoard[r][c] = { letter: getRandomLetter(), word:null, color:'bg-purple-400' };
const wordsOnGrid = new Set();
for (let r=0;r<GRID_SIZE;r++) for (let c=0;c<GRID_SIZE;c++) if (gameBoard[r][c] && gameBoard[r][c].word) wordsOnGrid.add(gameBoard[r][c].word);
const wordsToFindStillRemaining = wordsToFind.filter(w=>foundWordCounts[w] < FIND_COUNT_PER_WORD);
if (wordsToFindStillRemaining.length > 0 && wordsOnGrid.size < wordsToFindStillRemaining.length){
const startTime = performance.now();
while (wordsOnGrid.size < wordsToFindStillRemaining.length && attempts < maxAttempts && performance.now() - startTime < 1000){
const placed = tryToPlaceNewWord();
if (!placed) replaceRandomLetters();
for (let r=0;r<GRID_SIZE;r++) for (let c=0;c<GRID_SIZE;c++) if (gameBoard[r][c] && gameBoard[r][c].word) wordsOnGrid.add(gameBoard[r][c].word);
if (wordsOnGrid.size < wordsToFindStillRemaining.length) {
showMessage('Some words could not be placed. Use a star to refresh if needed.');
function checkWinCondition(){
const allFound = wordsToFind.every(word => foundWordCounts[word] >= FIND_COUNT_PER_WORD);
gameContainer.querySelectorAll('*').forEach(n => n.style.display = '');
winScreen.classList.remove('hidden');
const winText = currentLevel < LEVELS.length - 1 ? 'Ka Pai!' : 'Ka Mutu Katoa!';
<button class="win-button" id="replay-level-btn">Replay</button>
${currentLevel < LEVELS.length - 1 ? '<button class="win-button" id="next-level-btn">Next</button>' : ''}
<div class="win-overlay">
<div class="win-star"></div>
<div class="win-text">${winText}</div>
<div class="win-buttons">${buttonsHTML}</div>
document.getElementById('replay-level-btn').addEventListener('click', () => resetGame());
if (currentLevel < LEVELS.length - 1) {
document.getElementById('next-level-btn').addEventListener('click', () => nextLevel());
winScreen.classList.add('hidden');
winScreen.innerHTML = '';
if (currentLevel < LEVELS.length - 1) {
function attachEventListeners(){
gridContainer.addEventListener('mousedown', handleStart);
gridContainer.addEventListener('mousemove', handleMove);
gridContainer.addEventListener('mouseup', handleEnd);
gridContainer.addEventListener('mouseleave', handleEnd);
gridContainer.addEventListener('touchstart', (e)=>handleStart(e.touches[0]));
gridContainer.addEventListener('touchmove', (e)=>handleMove(e.touches[0]));
gridContainer.addEventListener('touchend', handleEnd);
prevLevelBtn.addEventListener('click', ()=>{ prevLevel(); });
nextLevelBtn.addEventListener('click', ()=>{ nextLevel(); });