<!DOCTYPE html>
<!--Created by Priscilla Allan with help from Grok, ChatGPT, and a little Gemini-->
<!--I am learning HTML, Java script & how to harness FREE ai-coding help-->
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
:root{ --tile-bg: rgba(255,255,255,0.2); }
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #f74ff7 0%, #f53da8 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color: #fff;
padding: 20px;
touch-action: none;
overflow-y: auto;
transition: background 600ms ease;
}
.container {
background-color: rgba(255, 255, 255, 0.09);
border-radius: 24px;
padding: 18px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
width: 100%;
max-width: 980px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 1rem;
}
.title {
font-size: 2.25rem;
font-weight: 700;
color: #fff;
text-shadow: 2px 2px 6px rgba(0, 0, 0, 0.35);
}
.score {
font-size: 1.25rem;
font-weight: 700;
color: #fff;
background: rgba(0,0,0,0.28);
padding: 8px 16px;
border-radius: 9999px;
}
.instructions-btn {
background: rgba(0,0,0,0.28);
color: #fff;
padding: 8px 16px;
border-radius: 9999px;
font-weight: 700;
cursor: pointer;
transition: transform 0.15s;
border: none;
}
.instructions-btn:hover {
transform: translateY(-3px);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: #fff;
color: #000;
padding: 20px;
border-radius: 12px;
max-width: 90%;
max-height: 80vh;
overflow-y: auto;
position: relative;
}
.modal-content h2, .modal-content h3 {
font-weight: 700;
}
.modal-close {
position: absolute;
top: 10px;
right: 10px;
background: #e5e7eb;
color: #000;
padding: 8px 12px;
border-radius: 9999px;
cursor: pointer;
font-weight: 700;
border: none;
}
.progress-bar {
display: flex;
gap: 4px;
margin-bottom: 1rem;
width: 100%;
justify-content: center;
}
.progress-square {
width: 30px;
height: 30px;
background: #ffffff;
border-radius: 6px;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.9rem;
font-weight: 700;
color: #000;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: background 0.3s;
}
.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;
}
.word-item-count {
font-size: 1rem;
font-weight: 700;
}
.tick {
font-size: 1.2rem;
margin-left: 6px;
}
.star-container {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(3, 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;
}
.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);
}
.audit-trail-section {
width: 100%;
background-color: rgba(0,0,0,0.14);
border-radius: 12px;
padding: 12px;
display: flex;
flex-direction: column;
align-items: stretch;
margin: 18px auto 0; /* Center horizontally and add top margin */
}
.audit-trail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.bg-level-1 { background: linear-gradient(135deg, #EFDDF8 0%, #9ca3af 100%); }
.bg-level-2 { background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%); }
.bg-level-3 { background: linear-gradient(135deg, #06b6d4 0%, #7dd3fc 100%); }
.bg-level-4 { background: linear-gradient(135deg, #22c55e 0%, #86efac 100%); }
.bg-level-5 { background: linear-gradient(135deg, #16a34a 0%, #4ade80 100%); }
.bg-level-6 { background: linear-gradient(135deg, #facc15 0%, #fef08a 100%); }
.bg-level-7 { background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%); }
.bg-level-8 { background: linear-gradient(135deg, #fb923c 0%, #f97316 100%); }
.bg-level-9 { background: linear-gradient(135deg, #f97316 0%, #fb923c 100%); }
.bg-level-10 { background: linear-gradient(135deg, #ef4444 0%, #f87171 100%); }
.bg-level-11 { background: linear-gradient(135deg, #b91c1c 0%, #ef4444 100%); }
.bg-level-12 { background: linear-gradient(135deg, #ec4899 0%, #f472b6 100%); }
.bg-level-13 { background: linear-gradient(135deg, #db2777 0%, #ec4899 100%); }
.bg-level-14 { background: linear-gradient(135deg, #a855f7 0%, #c084fc 100%); }
.bg-level-15 { background: linear-gradient(135deg, #7e22ce 0%, #a78bfa 100%); }
.bg-level-16 { background: linear-gradient(135deg, #ff0000 0%, #ff7f00 100%); }
.bg-level-17 { background: linear-gradient(135deg, #ff7f00 0%, #ffff00 100%); }
.bg-level-18 { background: linear-gradient(135deg, #ff0000 0%, #ff7f00 50%, #ffff00 100%); }
.bg-level-19 { background: linear-gradient(135deg, #ff0000 0%, #ff7f00 33%, #ffff00 66%, #00ff00 100%); }
.bg-level-20 { background: linear-gradient(135deg, #ff0000 0%, #ff7f00 20%, #ffff00 40%, #00ff00 60%, #0000ff 80%, #4b0082 100%); }
</style>
</head>
<body>
<div class="container">
<div class="header">
<button class="instructions-btn" id="instructions-btn">Instructions</button>
<h1 class="title">Rapu Kupu</h1>
<div class="score" id="score-display">Score: 0</div>
</div>
<div class="progress-bar" id="progress-bar"></div>
<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>
</div>
<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>
<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>
</div>
</div>
</div>
<div class="modal" id="instructions-modal">
<div class="modal-content">
<button class="modal-close" id="modal-close">Close</button>
<h2>How to Play Rapu Kupu</h2>
<p>Find and connect letters to form the hidden words.</p>
<p>Words SNAKE up, down, left, right, or diagonally.</p>
<p> SELECT letters in order to form words.</p>
<h3>Scoring</h3>
<p>+1 point for each letter in the main word list.</p>
<p>+5 points for each letter in bonus words.</p>
<p>Bonus words can come from any level or the bonus word bank, not just the current level.</p>
<h3>Stars</h3>
<p>Stars can be clicked when you get stuck to reset the board.</p>
<h3>Progress Bar</h3>
<p>The progress bar at the top shows your progress through the levels.</p>
<h3>Tips</h3>
<p>Look carefully — bonus words can sneak through the purple filler letters!</p>
<p>The later levels get more colourful and challenging.</p>
<h2>Copy the learning log</h2>
<h3>Copy then paste as an EMBED into your google site</h3>
</div>
</div>
</div>
<div class="audit-trail-section">
<button id="copy-trail" class="win-button" onclick="copyToClipboard()">
Rapu Kupu: Learning Log 2025 [CC BY-NC] Click to COPY
</button>
<div id="learning-log-list" class="word-list"></div>
</div>
<script>
const messageBox = document.getElementById('message-box');
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 progressBar = document.getElementById('progress-bar');
const scoreDisplay = document.getElementById('score-display');
const instructionsBtn = document.getElementById('instructions-btn');
const instructionsModal = document.getElementById('instructions-modal');
const modalClose = document.getElementById('modal-close');
const learningLogList = document.getElementById('learning-log-list');
const copyTrailBtn = document.getElementById('copy-trail');
const GRID_SIZE = 6;
const FIND_COUNT_PER_WORD = 1;
const TOTAL_STARS = 15;
let gameBoard = [];
let selectedTiles = [];
let foundWordCounts = {};
let wordPlacementCounts = {};
let wordsToFind = [];
let wordInfo = {};
let isDragging = false;
let lastTile = null;
let stars = [];
let starsUsedCount = 0;
let completedLevels = new Set();
let score = 0;
let foundWordsTrail = [];
let normalizedTranslations = {};
let selectionStartTime = null; // To track time taken for word selection
const letters = ['a', 'e', 'h', 'i', 'k', 'm', 'n', 'o', 'p', 'r', 't', 'u', 'w'];
let currentLevel = 0;
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]];
}
return array;
}
function createTile(row, col, letter, colorClass) {
const tile = document.createElement('div');
tile.classList.add('tile');
if (colorClass) tile.classList.add(...colorClass.split(' '));
tile.dataset.row = row;
tile.dataset.col = col;
tile.dataset.letter = letter;
tile.innerHTML = `<span>${letter.toUpperCase()}</span>`;
return tile;
}
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));
} else {
star.classList.add('used');
}
starContainer.appendChild(star);
return star;
}
function initStars() {
starContainer.innerHTML = '';
stars = [];
for (let i = 0; i < TOTAL_STARS; i++) {
const star = createStar(i);
stars.push({ element: star, used: starsUsedCount > i });
}
}
function initProgressBar() {
progressBar.innerHTML = '';
for (let i = 0; i < LEVELS.length; i++) {
const square = document.createElement('div');
square.classList.add('progress-square');
square.textContent = i + 1;
if (completedLevels.has(i)) {
square.classList.add(`bg-level-${i + 1}`);
square.style.color = '#ffffff';
}
progressBar.appendChild(square);
}
}
function updateScoreDisplay() {
scoreDisplay.textContent = `Score: ${score}`;
}
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.split(' '));
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 displayLearningLog() {
learningLogList.innerHTML = '';
foundWordsTrail.forEach(item => {
const wordItem = document.createElement('div');
wordItem.classList.add('word-item');
wordItem.innerHTML = `
<span>${item.time}</span> -
<span>${item.level}</span> -
<span>${item.type}</span> -
<span>${item.word}</span> -
<span>${item.translation}</span> -
<span>${item.timeTaken}s</span> -
<span>${item.pointsGained}</span> -
<span>${item.pointsTotal}</span>`;
learningLogList.appendChild(wordItem);
});
}
function placeWord(wordObj, board) {
const word = wordObj.w;
const directions = [[0,1],[0,-1],[1,0],[-1,0],[1,1],[1,-1],[-1,1],[-1,-1]];
let placed = false;
let attempts = 0;
const maxAttempts = 100;
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;
shuffle(directions);
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)) {
const cell = board[newR][newC];
if (cell === null || cell === undefined || cell.word === null) {
visited.add(key);
path.push({r:newR,c:newC});
if (findPath(newR,newC,wordIndex+1,depth+1)) return true;
path.pop();
visited.delete(key);
}
}
}
return false;
};
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++){
const {r,c} = path[i];
board[r][c] = { letter: word[i], word: word, color: wordObj.c };
}
placed = true;
wordPlacementCounts[word] = (wordPlacementCounts[word] || 0) + 1;
}
}
}
attempts++;
}
return placed;
}
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);
}
}
}
return false;
}
for (const word of wordsToFind) {
let found = false;
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 path = [{r,c}];
const visited = new Set([`${r},${c}`]);
if (checkPath(r, c, word, 1, path, visited) && path.length === word.length) {
wordsOnGrid.add(word);
found = true;
}
}
}
}
}
return wordsOnGrid.size === wordsToFind.length;
}
function getAllWords() {
return LEVELS.flatMap(level => level.words.map(w => w.w));
}
function getBonusWords() {
return BONUS_WORDS.map(w => ({ w: w.w, c: null }));
}
function initTranslations() {
normalizedTranslations = {};
LEVELS.forEach(level => {
level.words.forEach(wObj => {
const normalized = wObj.w.replace(/[\s-]/g, '').toLowerCase();
normalizedTranslations[normalized] = wObj.t;
});
});
BONUS_WORDS.forEach(bObj => {
const normalized = bObj.w.replace(/[\s-]/g, '').toLowerCase();
normalizedTranslations[normalized] = bObj.t;
});
}
function getTranslation(word) {
return normalizedTranslations[word.toLowerCase()] || 'bonus word';
}
function refreshBoard() {
const MAX_OVERALL_ATTEMPTS = 50;
const startTime = performance.now();
let overallAttempt = 0;
let success = false;
let words = wordsToFind.map(w => wordInfo[w].obj);
words.sort((a, b) => b.w.length - a.length);
while (!success && overallAttempt < MAX_OVERALL_ATTEMPTS && performance.now() - startTime < 2000) {
const localBoard = Array.from({length:GRID_SIZE}, () => Array(GRID_SIZE).fill(null));
let allPlaced = true;
for (const wobj of words) {
const placed = placeWord(wobj, localBoard);
if (!placed) { allPlaced = false; break; }
}
if (allPlaced && validateBoard(localBoard)) {
let otherWords = shuffle([...getAllWords(), ...getBonusWords().map(w => w.w)]).slice(0, 5);
otherWords.sort((a, b) => b.length - a.length);
const purpleShades = [
'bg-[#c084fc] text-black',
'bg-[#a855f7] text-white',
'bg-[#9333ea] text-white',
'bg-[#7e22ce] text-white',
'bg-[#6b21a8] text-white'
];
for (let i = 0; i < otherWords.length; i++) {
let wordObj = { w: otherWords[i], c: purpleShades[i % purpleShades.length] };
placeWord(wordObj, localBoard);
}
let remainingWords = shuffle(getAllWords().filter(w => w.length === 5 || w.length === 4));
let remainingWordIndex = 0;
const vowels = ['a', 'e', 'i', 'o', 'u', 'ā', 'ē', 'ī', 'ō', 'ū'];
let vowelIndex = 0;
for (let r = 0; r < GRID_SIZE; r++) {
for (let c = 0; c < GRID_SIZE; c++) {
if (localBoard[r][c] === null) {
if (remainingWordIndex < remainingWords.length) {
let wordObj = { w: remainingWords[remainingWordIndex], c: 'bg-[#d8b4fe] text-black' };
if (placeWord(wordObj, localBoard)) {
remainingWordIndex++;
} else {
localBoard[r][c] = { letter: vowels[vowelIndex], word: null, color: 'bg-[#d8b4fe] text-black' };
vowelIndex = (vowelIndex + 1) % vowels.length;
}
} else {
localBoard[r][c] = { letter: vowels[vowelIndex], word: null, color: 'bg-[#d8b4fe] text-black' };
vowelIndex = (vowelIndex + 1) % vowels.length;
}
}
}
}
gameBoard = localBoard;
success = true;
}
overallAttempt++;
}
if (!success) {
gameBoard = Array.from({length:GRID_SIZE}, () => Array(GRID_SIZE).fill(null));
for (const w of words) placeWord(w, gameBoard);
let otherWords = shuffle([...getAllWords(), ...getBonusWords().map(w => w.w)]).slice(0, 5);
otherWords.sort((a, b) => b.length - a.length);
const purpleShades = [
'bg-[#c084fc] text-black',
'bg-[#a855f7] text-white',
'bg-[#9333ea] text-white',
'bg-[#7e22ce] text-white',
'bg-[#6b21a8] text-white'
];
for (let i = 0; i < otherWords.length; i++) {
let wordObj = { w: otherWords[i], c: purpleShades[i % purpleShades.length] };
placeWord(wordObj, gameBoard);
}
let remainingWords = shuffle(getAllWords().filter(w => w.length === 5 || w.length === 4));
let remainingWordIndex = 0;
const vowels = ['a', 'e', 'i', 'o', 'u', 'ā', 'ē', 'ī', 'ō', 'ū'];
let vowelIndex = 0;
for (let r = 0; r < GRID_SIZE; r++) {
for (let c = 0; c < GRID_SIZE; c++) {
if (gameBoard[r][c] === null) {
if (remainingWordIndex < remainingWords.length) {
let wordObj = { w: remainingWords[remainingWordIndex], c: 'bg-[#d8b4fe] text-black' };
if (placeWord(wordObj, gameBoard)) {
remainingWordIndex++;
} else {
gameBoard[r][c] = { letter: vowels[vowelIndex], word: null, color: 'bg-[#d8b4fe] text-black' };
vowelIndex = (vowelIndex + 1) % vowels.length;
}
} else {
gameBoard[r][c] = { letter: vowels[vowelIndex], word: null, color: 'bg-[#d8b4fe] text-black' };
vowelIndex = (vowelIndex + 1) % vowels.length;
}
}
}
}
if (!validateBoard(gameBoard)) {
showMessage('Board may be incomplete. Use a star to refresh.');
}
}
renderBoard();
displayWordList();
}
function initBoard() {
foundWordCounts = {};
wordPlacementCounts = {};
const level = LEVELS[currentLevel];
wordsToFind = level.words.map(o => o.w);
wordInfo = {};
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}`;
initStars();
initProgressBar();
updateScoreDisplay();
refreshBoard();
}
function renderBoard() {
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]) {
const vowels = ['a', 'e', 'i', 'o', 'u', 'ā', 'ē', 'ī', 'ō', 'ū'];
gameBoard[r][c] = { letter: vowels[Math.floor(Math.random() * vowels.length)], word: null, color: 'bg-[#d8b4fe] text-black' };
}
}
gridContainer.innerHTML = '';
gameBoard.forEach((row, r) => {
row.forEach((cell, c) => {
const tile = createTile(r, c, cell.letter, cell.color);
tile.addEventListener('click', () => handleTileClick(r, c));
gridContainer.appendChild(tile);
cell.element = tile;
});
});
}
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'));
selectedTiles = [];
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));
starsUsedCount++;
refreshBoard();
showMessage('Board refreshed!');
}
function handleTileClick(row, col) {
const newTile = { row, col };
const isAlready = selectedTiles.some(t => t.row === row && t.col === col);
if (isAlready) {
// Deselect tiles up to the clicked tile
const index = selectedTiles.findIndex(t => t.row === row && t.col === col);
selectedTiles = selectedTiles.slice(0, index + 1);
document.querySelectorAll('.tile.selected').forEach(t => t.classList.remove('selected'));
selectedTiles.forEach(t => {
const tile = document.querySelector(`.tile[data-row="${t.row}"][data-col="${t.col}"]`);
tile.classList.add('selected');
});
wordBuilderEl.innerHTML = '';
selectedTiles.forEach(t => {
const built = document.createElement('div');
built.classList.add('built-letter');
built.textContent = gameBoard[t.row][t.col].letter.toUpperCase();
wordBuilderEl.appendChild(built);
});
} else if (isValidAdjacency(newTile)) {
if (selectedTiles.length === 0) {
selectionStartTime = Date.now(); // Start timing
}
selectedTiles.push(newTile);
const tile = document.querySelector(`.tile[data-row="${row}"][data-col="${col}"]`);
tile.classList.add('selected');
const built = document.createElement('div');
built.classList.add('built-letter');
built.textContent = gameBoard[row][col].letter.toUpperCase();
wordBuilderEl.appendChild(built);
}
}
function handleStart(e) {
isDragging = true;
selectionStartTime = Date.now(); // Start timing
handleMove(e);
}
function handleMove(e) {
if (!isDragging) return;
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 newTile = {row, col};
const isAlready = selectedTiles.some(t => t.row === row && t.col === col);
if (!isAlready && isValidAdjacency(newTile)) {
selectedTiles.push(newTile);
tile.classList.add('selected');
lastTile = tile;
const built = document.createElement('div');
built.classList.add('built-letter');
built.textContent = gameBoard[row][col].letter.toUpperCase();
wordBuilderEl.appendChild(built);
}
}
}
function handleEnd() {
if (!isDragging) return;
isDragging = false;
lastTile = null;
const timeTaken = Math.round((Date.now() - selectionStartTime) / 1000); // Calculate time taken in seconds
const word = getSelectedWord().toLowerCase();
const foundWord = wordsToFind.find(w => w === word);
const allWords = [...getAllWords(), ...getBonusWords().map(w => w.w)];
const isBonusWord = !foundWord && allWords.includes(word);
const levelName = `Level ${currentLevel + 1} - ${LEVELS[currentLevel].name}`;
if (foundWord && word.length >= 2) {
foundWordCounts[foundWord] = (foundWordCounts[foundWord] || 0) + 1;
const pointsGained = word.length;
score += pointsGained;
showMessage('Mīharo - you got it!');
dropLettersAndRefill(selectedTiles);
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const translation = wordInfo[foundWord].translation;
foundWordsTrail.push({
time,
level: levelName,
type: 'List Word',
word: word.toUpperCase(),
translation,
timeTaken,
pointsGained,
pointsTotal: score
});
displayWordList();
displayLearningLog();
updateScoreDisplay();
} else if (isBonusWord && word.length >= 2) {
const pointsGained = word.length * 5;
score += pointsGained;
const type = getAllWords().includes(word) ? 'Bonus Word (list)' : 'Bonus Word (hidden)';
showMessage('Ka rawe! Bonus word found!');
dropLettersAndRefill(selectedTiles);
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const translation = getTranslation(word);
foundWordsTrail.push({
time,
level: levelName,
type,
word: word.toUpperCase(),
translation,
timeTaken,
pointsGained,
pointsTotal: score
});
displayLearningLog();
updateScoreDisplay();
} else if (word.length > 1) {
showMessage('That is not a valid word.');
}
clearHighlights();
checkWinCondition();
}
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++) {
const columnStack = [];
for (let r = GRID_SIZE - 1; r >= 0; r--) {
const coord = `${r},${c}`;
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];
fillRow--;
}
}
gameBoard = newGameBoard;
const wordsToFindStillRemaining = 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);
let attempts = 0;
const maxAttempts = 100;
const startTime = performance.now();
while (wordsOnGrid.size < wordsToFindStillRemaining.length && attempts < maxAttempts && performance.now() - startTime < 1000) {
let wordsNotOnGrid = wordsToFindStillRemaining.filter(w => !wordsOnGrid.has(w));
wordsNotOnGrid.sort((a, b) => b.length - a.length);
for (const w of wordsNotOnGrid) {
if (placeWord(wordInfo[w].obj, gameBoard)) {
wordsOnGrid.add(w);
}
}
if (wordsOnGrid.size < wordsToFindStillRemaining.length) {
const nonWordTiles = [];
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});
shuffle(nonWordTiles);
const tilesToReplace = nonWordTiles.slice(0, Math.min(20, nonWordTiles.length));
tilesToReplace.forEach(({r, c}) => gameBoard[r][c] = null);
for (const w of wordsNotOnGrid) {
if (placeWord(wordInfo[w].obj, gameBoard)) {
wordsOnGrid.add(w);
}
}
}
attempts++;
}
if (wordsOnGrid.size < wordsToFindStillRemaining.length) {
showMessage('Some main words could not be placed. Use a star to refresh if needed.');
}
let otherWords = shuffle([...getAllWords(), ...getBonusWords().map(w => w.w)]).slice(0, 5);
otherWords.sort((a, b) => b.length - a.length);
const purpleShades = [
'bg-[#c084fc] text-black',
'bg-[#a855f7] text-white',
'bg-[#9333ea] text-white',
'bg-[#7e22ce] text-white',
'bg-[#6b21a8] text-white'
];
for (let i = 0; i < otherWords.length; i++) {
let wordObj = { w: otherWords[i], c: purpleShades[i % purpleShades.length] };
placeWord(wordObj, gameBoard);
}
let remainingWords = shuffle(getAllWords().filter(w => w.length === 5 || w.length === 4));
let remainingWordIndex = 0;
const vowels = ['a', 'e', 'i', 'o', 'u', 'ā', 'ē', 'ī', 'ō', 'ū'];
let vowelIndex = 0;
for (let r = 0; r < GRID_SIZE; r++) {
for (let c = 0; c < GRID_SIZE; c++) {
if (gameBoard[r][c] === null) {
if (remainingWordIndex < remainingWords.length) {
let wordObj = { w: remainingWords[remainingWordIndex], c: 'bg-[#d8b4fe] text-black' };
if (placeWord(wordObj, gameBoard)) {
remainingWordIndex++;
} else {
gameBoard[r][c] = { letter: vowels[vowelIndex], word: null, color: 'bg-[#d8b4fe] text-black' };
vowelIndex = (vowelIndex + 1) % vowels.length;
}
} else {
gameBoard[r][c] = { letter: vowels[vowelIndex], word: null, color: 'bg-[#d8b4fe] text-black' };
vowelIndex = (vowelIndex + 1) % vowels.length;
}
}
}
}
renderBoard();
}
function checkWinCondition() {
const allFound = wordsToFind.every(word => foundWordCounts[word] >= FIND_COUNT_PER_WORD);
if (allFound) {
completedLevels.add(currentLevel);
gameContainer.querySelectorAll('*').forEach(n => n.style.display = '');
winScreen.classList.remove('hidden');
const winText = currentLevel < LEVELS.length - 1 ? LEVEL_COMPLETION_MESSAGES[currentLevel] : 'Ka Mutu Katoa!';
const buttonsHTML = `
<button class="win-button" id="replay-level-btn">Replay</button>
${currentLevel < LEVELS.length - 1 ? '<button class="win-button" id="next-level-btn">Next</button>' : ''}
`;
winScreen.innerHTML = `
<div class="win-overlay">
<div class="win-star"></div>
<div class="win-text">${winText}</div>
<div class="win-buttons">${buttonsHTML}</div>
</div>`;
// Add level completion to learning log
foundWordsTrail.push({
time: '',
level: '',
type: '',
word: `Level ${currentLevel + 1}: ${LEVEL_COMPLETION_MESSAGES[currentLevel]}`,
translation: '',
timeTaken: '',
pointsGained: '',
pointsTotal: score
});
displayLearningLog();
document.getElementById('replay-level-btn').addEventListener('click', () => resetGame());
if (currentLevel < LEVELS.length - 1) {
document.getElementById('next-level-btn').addEventListener('click', () => nextLevel());
}
initProgressBar();
}
}
function resetGame() {
winScreen.classList.add('hidden');
winScreen.innerHTML = '';
initBoard();
}
function prevLevel() {
if (currentLevel > 0) {
currentLevel--;
resetGame();
}
}
function nextLevel() {
if (currentLevel < LEVELS.length - 1) {
currentLevel++;
resetGame();
}
}
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(); });
instructionsBtn.addEventListener('click', () => {
instructionsModal.style.display = 'flex';
});
modalClose.addEventListener('click', () => {
instructionsModal.style.display = 'none';
});
copyTrailBtn.addEventListener('click', copyToClipboard);
function copyToClipboard() {
try {
const now = new Date().toLocaleString('en-NZ', {
weekday: 'long',
month: 'long',
day: '2-digit',
year: 'numeric'
});
const parts = now.split(/[, ]+/);
const weekday = parts[0];
const day = parts[1];
const month = parts[2];
const year = parts[3];
const maoriDate = `${maoriDays[weekday]}, ${maoriMonths[month]} ${day}, ${year}`;
const headers = [
];
const raw = (foundWordsTrail || []);
// use flatMap so we can add spacer rows before/after specials
const rows = raw.flatMap(item => {
if (item.type === '') {
return [
{ spacer: true },
{
special: true,
message: String(item.word || ''),
pointsTotal: item.pointsTotal != null ? String(item.pointsTotal) : ''
},
{ spacer: true }
];
} else {
return [{
special: false,
fields: [
String(item.time || ''),
String(item.level || ''),
String(item.type || ''),
String(item.word || ''),
String(item.translation || ''),
item.timeTaken != null ? String(item.timeTaken) + 's' : '',
item.pointsGained != null ? String(item.pointsGained) : '',
item.pointsTotal != null ? String(item.pointsTotal) : ''
]
}];
}
});
// compute column widths
const colCount = headers.length;
const colWidths = headers.map(h => h.length);
rows.forEach(r => {
if (r.special === false) {
r.fields.forEach((f, i) => {
const len = String(f).length;
if (len > colWidths[i]) colWidths[i] = len;
});
} else if (r.special) {
const ptLen = String(r.pointsTotal || '').length;
if (ptLen > colWidths[colCount - 1]) colWidths[colCount - 1] = ptLen;
}
});
//const GAP = 3; FROM HERE
//for (let i = 0; i < colWidths.length - 1; i++) colWidths[i] += GAP;
// Add GAP to all columns, including the second-to-last, to ensure spacing
const GAP = 3;
for (let i = 0; i < colWidths.length; i++) colWidths[i] += GAP;
const align = ['left', 'left', 'left', 'left', 'left', 'right', 'right', 'right'];
//const align = ['left','left','left','left','left','right','right','right']; TO HERE
const padCell = (s, w, a) => {
const str = String(s || '');
return a === 'right' ? str.padStart(w, ' ') : str.padEnd(w, ' ');
};
// const headerRow = headers.map((h, i) => padCell(h, colWidths[i], 'left')).join('');
const headerRow = headers.map((h, i) => padCell(h, colWidths[i], align[i])).join('');
const underlineRow = colWidths.map(w => '-'.repeat(w)).join('');
const combinedIndexStart = 3;
const combinedIndexEnd = 6;
const bodyRows = rows.map(r => {
if (r.spacer) {
return ''; // blank line
} else if (r.special) {
const firstPart = padCell('', colWidths[0], 'left')
+ padCell('', colWidths[1], 'left')
+ padCell('', colWidths[2], 'left');
const mergedWidth = colWidths.slice(combinedIndexStart, combinedIndexEnd + 1).reduce((a, b) => a + b, 0);
const mergedPart = padCell(r.message, mergedWidth, 'left');
const lastPart = padCell(r.pointsTotal || '', colWidths[colCount - 1], 'right');
return firstPart + mergedPart + lastPart;
} else {
return r.fields.map((f, i) => padCell(f, colWidths[i], align[i])).join('');
}
}).join('\n');
const text = `<pre>Rapu Kupu Learning Log for ${maoriDate}\n\n${headerRow}\n${underlineRow}\n${bodyRows}</pre>`;
const tempTextArea = document.createElement('textarea');
tempTextArea.value = text;
document.body.appendChild(tempTextArea);
tempTextArea.select();
document.execCommand('copy');
document.body.removeChild(tempTextArea);
alert('Copied Learning Log to clipboard!');
} catch (err) {
alert('Failed to copy, aroha mai, select and control C: ' + err);
}
}
}
initTranslations();
initBoard();
attachEventListeners();
}); // Correct closure of DOMContentLoaded
</script>
</body>
</html>