<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connect & Fill Grid Game</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>✏️</text></svg>">
</head>
<body>
<h1>Connect & Fill</h1>
<div class="game-info">
<p id="message">Click and drag from dot 1 to connect all dots in order, filling every square!</p>
<p>Next Dot: <span id="next-dot">1</span> | Cells Visited: <span id="cells-visited">0</span> / <span id="total-cells">?</span></p>
<div class="button-group">
<button id="reset-button" title="Reset the grid">Reset Grid</button>
<button id="hint-button" title="Show possible next moves">Show Hint</button>
</div>
</div>
<div id="grid-container">
<!-- Grid cells will be generated by JavaScript -->
</div>
<script src="script.js"></script>
</body>
</html>
/* --- Global Styles --- */
:root {
--cell-size: 50px; /* Default, will be set by JS */
--grid-bg: #1e2127;
--cell-border: #444;
--dot-bg: #111;
--dot-text: #fff;
--dot-border: #fff;
--hint-color: #4a90e2; /* Blue for hint */
--win-color: #4caf50; /* Green */
--error-color: #f44336; /* Red */
--info-color: #eee;
--body-bg: #282c34;
--text-light: #fff;
--text-dark: #333;
--button-reset-bg: #f05a5a;
--button-reset-hover: #d94848;
--button-hint-bg: var(--hint-color);
--button-hint-hover: #3a7bc8;
--info-box-bg: #3a3f47;
--title-color: #61dafb;
/* --- Path Segment Colors (Adjust as desired) --- */
--path-color-1: #d81b60;
--path-color-2: #e91e63;
--path-color-3: #ec407a;
--path-color-4: #f06292;
--path-color-5: #f48fb1;
--path-color-6: #f8bbd0;
--path-color-7: #fce4ec;
--path-color-8: #fff5f7; /* Lighter end */
/* --- Start/Special Path Colors --- */
--path-start-color: #ad1457;
--path-dot-reached-opacity: 0.95;
--path-head-opacity: 1;
--path-default-opacity: 0.75;
}
body {
font-family: system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--body-bg);
color: var(--text-light);
margin: 0;
padding: 20px 15px;
min-height: 100vh;
box-sizing: border-box;
}
h1 {
color: var(--title-color);
margin-bottom: 15px;
text-align: center;
}
.game-info {
margin-bottom: 20px;
text-align: center;
background-color: var(--info-box-bg);
padding: 15px 20px;
border-radius: 8px;
max-width: 600px;
width: 90%;
box-sizing: border-box;
}
#message {
min-height: 2.5em; /* Space for messages */
color: var(--info-color);
margin-bottom: 10px;
}
#message.win { color: var(--win-color); font-weight: bold;}
#message.error { color: var(--error-color); font-weight: bold;}
.game-info p:last-of-type { /* Status line */
margin-bottom: 0;
}
.game-info span { /* Info spans */
font-weight: bold;
}
.button-group {
margin-top: 15px;
display: flex;
justify-content: center;
flex-wrap: wrap; /* Allow wrapping on small screens */
gap: 15px;
}
#reset-button, #hint-button {
padding: 10px 20px;
font-size: 1em;
cursor: pointer;
color: white;
border: none;
border-radius: 5px;
transition: background-color 0.2s, opacity 0.2s;
}
#reset-button { background-color: var(--button-reset-bg); }
#reset-button:hover { background-color: var(--button-reset-hover); }
#hint-button { background-color: var(--button-hint-bg); }
#hint-button:hover { background-color: var(--button-hint-hover); }
#hint-button:disabled { opacity: 0.5; cursor: not-allowed; }
/* --- Grid & Cell Styling --- */
#grid-container {
/* Grid size (width, height, columns, rows) set by JS */
display: grid;
border: 3px solid var(--title-color);
background-color: var(--grid-bg);
user-select: none;
user-select: none;
user-select: none;
cursor: pointer; /* Hand cursor */
touch-action: none; /* Prevent scrolling on touch devices */
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.cell {
border: 1px solid var(--cell-border);
box-sizing: border-box;
position: relative; /* Crucial for pseudo-element & dot */
display: flex; /* Helps center dot if needed, although dot is relative */
justify-content: center;
align-items: center;
background-color: transparent; /* Cell itself is clear */
transition: box-shadow 0.2s ease; /* Hint transition */
}
/* --- Path Styling (using ::before) --- */
.cell.path::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
z-index: 1; /* Below dot */
opacity: var(--path-default-opacity);
transition: background-color 0.1s ease-out, opacity 0.1s ease-out;
/* Default background, will be overridden by segments */
background-color: gray; /* Fallback */
}
/* Path segment colors applied to ::before */
.cell.path-segment-1::before { background-color: var(--path-color-1); }
.cell.path-segment-2::before { background-color: var(--path-color-2); }
.cell.path-segment-3::before { background-color: var(--path-color-3); }
.cell.path-segment-4::before { background-color: var(--path-color-4); }
.cell.path-segment-5::before { background-color: var(--path-color-5); }
.cell.path-segment-6::before { background-color: var(--path-color-6); }
.cell.path-segment-7::before { background-color: var(--path-color-7); }
.cell.path-segment-8::before { background-color: var(--path-color-8); }
/* Add more .path-segment-N::before rules if MAX_DOT > 8 */
/* State overrides for ::before */
.cell.path-start::before {
background-color: var(--path-start-color) !important; /* Override segments */
opacity: var(--path-head-opacity); /* Start is also head initially */
}
.cell.path-dot-reached::before {
opacity: var(--path-dot-reached-opacity);
}
.cell.path-head::before {
opacity: var(--path-head-opacity);
}
/* --- Dot Styling --- */
.dot {
width: 70%;
height: 70%;
background-color: var(--dot-bg);
border-radius: 50%;
color: var(--dot-text);
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
font-size: calc(var(--cell-size) * 0.4);
position: relative; /* Keeps it in flow, respects z-index */
z-index: 2; /* Above path pseudo-element */
border: 2px solid var(--dot-border);
text-shadow: 0 0 3px rgba(0,0,0,0.7);
box-shadow: 0 0 5px rgba(0,0,0,0.5);
opacity: 1 !important; /* Ensure full visibility */
}
/* --- Hint Styling --- */
.hint-cell {
box-shadow: inset 0 0 10px 3px var(--hint-color);
transition: box-shadow 0.3s ease-out;
z-index: 3; /* Above path, below dot maybe? Check visually */
}
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Elements ---
const gridContainer = document.getElementById('grid-container');
const messageElement = document.getElementById('message');
const nextDotElement = document.getElementById('next-dot');
const cellsVisitedElement = document.getElementById('cells-visited');
const totalCellsElement = document.getElementById('total-cells');
const resetButton = document.getElementById('reset-button');
const hintButton = document.getElementById('hint-button');
// --- Game Configuration ---
const GRID_SIZE = 6; // Grid dimensions
const CELL_SIZE_PX = 60; // Visual size of cells
// Dot positions [row, col], 0-indexed for 6x6 grid
const DOT_POSITIONS = {
1: [2, 2], 2: [2, 4], 3: [5, 1], 4: [4, 5],
5: [0, 5], 6: [0, 1], 7: [2, 1], 8: [2, 3]
};
const MAX_DOT = Object.keys(DOT_POSITIONS).length; // Number of dots
const TOTAL_CELLS = GRID_SIZE * GRID_SIZE; // Total cells (36)
const HINT_DURATION = 1500; // How long hints stay visible (ms)
// --- Game State Variables ---
let gridData = []; // 2D array: { visited: bool, dot: num|null, element: node }
let currentPath = []; // Array of [row, col] representing the path
let isDrawing = false; // Is the user currently dragging/drawing?
let currentPos = { row: -1, col: -1 }; // Position of the path's head
let nextExpectedDot = 1; // Which numbered dot is next?
let hintActive = false; // Is a hint currently displayed?
let hintTimeoutId = null; // ID for the hint clearing timeout
// --- Initialization ---
function setupGrid() {
// 1. Reset State
gridContainer.innerHTML = ''; // Clear visual grid
gridData = [];
currentPath = [];
isDrawing = false;
currentPos = { row: -1, col: -1 };
nextExpectedDot = 1;
hintActive = false;
clearTimeout(hintTimeoutId); // Clear any pending hint removal
hintButton.disabled = false; // Re-enable hint button
// 2. Apply Grid Styles (using CSS variables)
gridContainer.style.gridTemplateColumns = `repeat(${GRID_SIZE}, ${CELL_SIZE_PX}px)`;
gridContainer.style.gridTemplateRows = `repeat(${GRID_SIZE}, ${CELL_SIZE_PX}px)`;
gridContainer.style.width = `${GRID_SIZE * CELL_SIZE_PX}px`;
gridContainer.style.height = `${GRID_SIZE * CELL_SIZE_PX}px`;
document.documentElement.style.setProperty('--cell-size', `${CELL_SIZE_PX}px`);
// 3. Create Cells and Dots
for (let r = 0; r < GRID_SIZE; r++) {
gridData[r] = [];
for (let c = 0; c < GRID_SIZE; c++) {
// a. Create Cell Data Structure
gridData[r][c] = {
visited: false,
dot: null,
element: null // Store reference to the DOM element
};
// b. Create Cell DOM Element
const cell = document.createElement('div');
cell.classList.add('cell');
cell.dataset.row = r; // Store row/col for easy access in events
cell.dataset.col = c;
gridData[r][c].element = cell; // Link data structure to DOM element
// c. Check for and Add Dot
for (const dotNumStr in DOT_POSITIONS) {
const dotNum = parseInt(dotNumStr);
if (DOT_POSITIONS[dotNum][0] === r && DOT_POSITIONS[dotNum][1] === c) {
gridData[r][c].dot = dotNum; // Store dot number in data
// Create Dot DOM Element
const dotElement = document.createElement('div');
dotElement.classList.add('dot');
dotElement.textContent = dotNumStr; // Display number
cell.appendChild(dotElement); // Add dot visually inside cell
break; // Only one dot per cell
}
}
// d. Add Cell to Grid Container
gridContainer.appendChild(cell);
}
}
// 4. Update Initial UI Info
updateInfo();
setMessage("Click/Tap and drag from dot 1...");
}
// --- UI Update Functions ---
function updateInfo() {
nextDotElement.textContent = nextExpectedDot > MAX_DOT ? 'Done!' : nextExpectedDot;
cellsVisitedElement.textContent = currentPath.length;
totalCellsElement.textContent = TOTAL_CELLS; // Display total cells (36)
}
function setMessage(msg, type = 'info') { // type can be 'info', 'win', 'error'
messageElement.textContent = msg;
messageElement.className = type; // Applies .win or .error CSS class
}
// --- Path & Cell Styling Helper ---
// Applies/removes necessary CSS classes based on cell's path state
function updateCellPathStyle(row, col, isPath, segmentIndex = 0, isStart = false, isDotReached = false, isHead = false) {
const cell = gridData[row]?.[col]?.element;
if (!cell) return; // Safety check
// Toggle base '.path' class (required for ::before selector)
cell.classList.toggle('path', isPath);
// Toggle specific state classes
cell.classList.toggle('path-start', isPath && isStart);
cell.classList.toggle('path-dot-reached', isPath && isDotReached);
cell.classList.toggle('path-head', isPath && isHead);
// Manage path segment color classes
const maxSegments = MAX_DOT; // Number of color segments needed
for (let i = 1; i <= maxSegments; i++) {
// Remove segment class if it's not the correct one for this state, OR if the cell is no longer part of the path
if ((i !== segmentIndex && cell.classList.contains(`path-segment-${i}`)) || !isPath) {
cell.classList.remove(`path-segment-${i}`);
}
}
// Add the correct segment class if it's part of the path, but NOT the start cell (start has its own override)
if (isPath && segmentIndex > 0 && !isStart) {
cell.classList.add(`path-segment-${segmentIndex}`);
}
// Cleanup: If no longer part of the path, ensure all path-related classes are gone
if (!isPath) {
cell.classList.remove('path-start', 'path-dot-reached', 'path-head');
// Loop again for segment classes just to be absolutely sure on removal
for (let i = 1; i <= maxSegments; i++) {
cell.classList.remove(`path-segment-${i}`);
}
}
}
// --- Event Handlers (Combined Mouse & Touch) ---
// Function to get row/col from either mouse or touch event
function getCoordsFromEvent(event) {
let clientX, clientY;
if (event.type.startsWith('touch')) {
if (!event.touches[0]) return null; // No touch data
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
} else { // Mouse event
clientX = event.clientX;
clientY = event.clientY;
}
const gridRect = gridContainer.getBoundingClientRect();
const x = clientX - gridRect.left;
const y = clientY - gridRect.top;
const col = Math.floor(x / CELL_SIZE_PX);
const row = Math.floor(y / CELL_SIZE_PX);
// Basic bounds check before returning
if (row < 0 || row >= GRID_SIZE || col < 0 || col >= GRID_SIZE) {
return null;
}
return { row, col };
}
function handleInteractionStart(event) {
event.preventDefault(); // Prevent scrolling/default drag
clearHint(); // Clear hint on any interaction
const coords = getCoordsFromEvent(event);
if (!coords) return; // Click/touch was outside grid or invalid
const { row, col } = coords;
const cellData = gridData[row][col];
// Condition 1: Start drawing on dot 1 if path is empty
if (cellData.dot === 1 && currentPath.length === 0) {
isDrawing = true;
currentPos = { row, col };
currentPath = [[row, col]]; // Start path array
cellData.visited = true;
// Style: isPath=true, segment=1, isStart=true, isDotReached=false, isHead=true
updateCellPathStyle(row, col, true, 1, true, false, true);
nextExpectedDot = 2;
updateInfo();
setMessage("Drawing path... Go to dot 2!");
}
// Condition 2: Resume drawing if clicking/tapping the current head
else if (currentPath.length > 0 && row === currentPos.row && col === currentPos.col) {
isDrawing = true;
setMessage(`Continue path... Go to dot ${nextExpectedDot}.`);
// Ensure head styling is correct (find current segment)
const segment = nextExpectedDot > 1 ? nextExpectedDot - 1 : 1;
const headDotValue = cellData.dot;
// Style: isPath=true, segment, isStart=(headDotValue===1), isDotReached=(headDotValue===nextExpectedDot-1), isHead=true
updateCellPathStyle(row, col, true, segment, headDotValue === 1, headDotValue === (nextExpectedDot - 1), true);
}
// Condition 3: Invalid start/resume attempt
else if (currentPath.length > 0) {
setMessage("Click/Tap on the end of the current path to continue or backtrack.", "error");
} else if (cellData.dot !== 1) {
setMessage("You must start on dot 1!", "error");
}
}
function handleInteractionMove(event) {
if (!isDrawing) return; // Only process if actively drawing
event.preventDefault();
clearHint();
const coords = getCoordsFromEvent(event);
if (!coords) return; // Moved outside grid bounds
const { row: targetRow, col: targetCol } = coords;
// Ignore if moved onto the same cell
if (targetRow === currentPos.row && targetCol === currentPos.col) {
return;
}
// --- Check for BACKTRACKING ---
if (currentPath.length >= 2) {
const prevPosArr = currentPath[currentPath.length - 2]; // The cell before the current head
// Is the target cell the one we just came from?
if (targetRow === prevPosArr[0] && targetCol === prevPosArr[1]) {
const lastPosArr = currentPath.pop(); // Remove current head from path
const lastRow = lastPosArr[0];
const lastCol = lastPosArr[1];
gridData[lastRow][lastCol].visited = false; // Mark as unvisited
updateCellPathStyle(lastRow, lastCol, false); // Remove all path styling
// Check if we backtracked over a required dot (decrement expected dot)
const removedDotValue = gridData[lastRow][lastCol].dot;
let segmentForNewHead = 1;
if (removedDotValue !== null && removedDotValue === (nextExpectedDot - 1) && removedDotValue !== 1 ) {
nextExpectedDot--; // Decrement if we un-reached a target dot (excluding dot 1)
segmentForNewHead = (nextExpectedDot > 1) ? nextExpectedDot - 1 : 1; // Segment before the un-reached dot
} else if (currentPath.length > 0){
// If not over a target dot, the segment is the one leading to the current expected dot
segmentForNewHead = (nextExpectedDot > 1) ? nextExpectedDot - 1 : 1;
}
// Update currentPos to the new head (which is prevPosArr)
currentPos = { row: prevPosArr[0], col: prevPosArr[1] };
const headDotValue = gridData[currentPos.row][currentPos.col].dot;
// Restyle the new head correctly
// Style: isPath=true, segment=segmentForNewHead, isStart=(headDotValue===1), isDotReached=(headDotValue===nextExpectedDot-1), isHead=true
updateCellPathStyle(currentPos.row, currentPos.col, true, segmentForNewHead, headDotValue === 1, headDotValue === (nextExpectedDot - 1), true);
updateInfo();
setMessage(`Backtracked. Continue path... Go to dot ${nextExpectedDot}.`);
return; // Exit after handling backtrack
}
}
// --- Check for FORWARD MOVEMENT ---
if (isValidForwardMove(targetRow, targetCol)) {
const currentSegmentIndex = (nextExpectedDot > 1) ? nextExpectedDot - 1 : 1;
const prevHeadDotValue = gridData[currentPos.row][currentPos.col].dot;
// Remove 'head' style from the previously current cell
// Style: isPath=true, segment=currentSegmentIndex, isStart=(prevHeadDotValue===1), isDotReached=(prevHeadDotValue<nextExpectedDot), isHead=false
updateCellPathStyle(currentPos.row, currentPos.col, true, currentSegmentIndex, prevHeadDotValue === 1, prevHeadDotValue !== null && prevHeadDotValue < nextExpectedDot, false);
// Update state for the new cell
currentPos = { row: targetRow, col: targetCol }; // Move head position
currentPath.push([targetRow, targetCol]); // Add to path array
gridData[targetRow][targetCol].visited = true; // Mark as visited
const targetDotValue = gridData[targetRow][targetCol].dot;
let isTargetDotReached = false;
let message = "";
let nextSegmentIndex = currentSegmentIndex; // Assume same segment unless a dot is hit
// Check if the new cell contains a dot
if (targetDotValue === nextExpectedDot) { // Correct dot hit!
isTargetDotReached = true;
if (nextExpectedDot === MAX_DOT) { // Final dot reached
nextExpectedDot++; // Mark as all dots done
message = `Connected final dot ${MAX_DOT}! Release touch/mouse.`;
nextSegmentIndex = MAX_DOT; // Use final segment color
} else { // Intermediate dot reached
nextExpectedDot++; // Increment expected dot
message = `Connected dot ${nextExpectedDot - 1}! Go to dot ${nextExpectedDot}.`;
nextSegmentIndex = nextExpectedDot - 1; // Segment index for the *next* part of path
}
} else if (targetDotValue !== null && targetDotValue !== 1) { // Hit a dot out of order (ignore dot 1)
isDrawing = false; // Stop drawing on error
setMessage(`Wrong dot! Expected ${nextExpectedDot} but landed on ${targetDotValue}. Reset.`, "error");
// Style the error cell as part of the current path segment, but not head/dot-reached
updateCellPathStyle(targetRow, targetCol, true, currentSegmentIndex, false, false, false);
checkWinCondition(); // Force check to potentially show final error message
return; // Stop processing move
} else { // Moved to an empty cell or back over dot 1 (allowed but has no effect)
message = `Continue path... Go to dot ${nextExpectedDot}.`;
nextSegmentIndex = currentSegmentIndex; // Stay on current segment color
}
// Style the new head cell
// Style: isPath=true, segment=nextSegmentIndex, isStart=false, isDotReached, isHead=true
updateCellPathStyle(targetRow, targetCol, true, nextSegmentIndex, false, isTargetDotReached, true);
setMessage(message);
updateInfo();
}
// Else: Invalid forward move (diagonal, already visited, etc.) - do nothing
}
function handleInteractionEnd(event) {
if (!isDrawing) return; // Only act if drawing was active
event.preventDefault();
isDrawing = false; // Stop drawing state
// Remove 'head' styling from the last cell
if(currentPath.length > 0) {
const head = currentPath[currentPath.length - 1];
const segment = (nextExpectedDot > 1) ? nextExpectedDot - 1 : 1;
const headDotValue = gridData[head[0]][head[1]].dot;
// Style: isPath=true, segment, isStart=(headDotValue===1), isDotReached=(headDotValue<nextExpectedDot), isHead=false
updateCellPathStyle(head[0], head[1], true, segment, headDotValue === 1, headDotValue !== null && headDotValue < nextExpectedDot, false);
}
// Check if the game is won or if there are errors
checkWinCondition();
}
function handleMouseLeaveGrid(event) {
// If drawing and mouse leaves the grid container bounds, end interaction
if (isDrawing && (!event.relatedTarget || !gridContainer.contains(event.relatedTarget))) {
handleInteractionEnd(event);
}
}
// --- Game Logic ---
function isValidForwardMove(newRow, newCol) {
// 1. Check bounds (although likely checked before calling)
if (newRow < 0 || newRow >= GRID_SIZE || newCol < 0 || newCol >= GRID_SIZE) return false;
// 2. Check adjacency (only horizontal/vertical)
const rowDiff = Math.abs(newRow - currentPos.row);
const colDiff = Math.abs(newCol - currentPos.col);
if (!((rowDiff === 1 && colDiff === 0) || (rowDiff === 0 && colDiff === 1))) return false;
// 3. Check if already visited
if (gridData[newRow][newCol].visited) return false;
return true; // Passed all checks
}
function checkWinCondition() {
const allDotsVisited = (nextExpectedDot > MAX_DOT);
const allCellsVisited = (currentPath.length === TOTAL_CELLS);
// Only evaluate fully if drawing has stopped
if (!isDrawing) {
if (allDotsVisited && allCellsVisited) {
setMessage("Congratulations! You solved the puzzle!", "win");
hintButton.disabled = true; // Disable hint on win
} else if (currentPath.length > 0) { // Only show errors if a path was started
if (allDotsVisited && !allCellsVisited) {
setMessage(`Connected all dots, but missed ${TOTAL_CELLS - currentPath.length} cells. Path invalid.`, "error");
} else { // Dots not finished OR cells missed
setMessage(`Path incomplete or invalid. Expected dot ${nextExpectedDot}.`, "error");
}
}
// If path length is 0, keep initial message (no error needed)
}
// If still drawing, intermediate messages are handled by handleInteractionMove
}
// --- Hint Logic ---
function showHint() {
if (hintActive || currentPath.length === 0 || nextExpectedDot > MAX_DOT) {
// No hint if already showing, game not started, or game won
return;
}
const head = currentPath[currentPath.length - 1]; // Current end of path
const r = head[0];
const c = head[1];
let foundHint = false;
const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]; // N, S, W, E
directions.forEach(([dr, dc]) => {
const nr = r + dr;
const nc = c + dc;
// Is the adjacent cell a valid FORWARD move?
if (nr >= 0 && nr < GRID_SIZE && nc >= 0 && nc < GRID_SIZE && !gridData[nr][nc].visited) {
gridData[nr][nc].element?.classList.add('hint-cell'); // Add hint style
foundHint = true;
}
});
if (foundHint) {
hintActive = true;
hintButton.disabled = true; // Temporarily disable hint button
setMessage("Showing possible next moves...");
clearTimeout(hintTimeoutId); // Clear any previous hint timeout
hintTimeoutId = setTimeout(clearHint, HINT_DURATION); // Set timer to clear this hint
} else {
setMessage("Stuck! No valid moves from here?", "error"); // Should not happen in solvable puzzle
}
}
function clearHint() {
gridContainer.querySelectorAll('.hint-cell').forEach(cell => cell.classList.remove('hint-cell'));
hintActive = false;
// Re-enable hint button only if the game is still in progress
if (nextExpectedDot <= MAX_DOT && currentPath.length > 0) {
hintButton.disabled = false;
}
// Restore message if it was the hint message
if (messageElement.textContent === "Showing possible next moves...") {
setMessage(`Continue path... Go to dot ${nextExpectedDot}.`);
}
}
// --- Event Listeners ---
// Mouse
gridContainer.addEventListener('mousedown', handleInteractionStart);
gridContainer.addEventListener('mousemove', handleInteractionMove);
window.addEventListener('mouseup', handleInteractionEnd); // Catch mouseup anywhere
gridContainer.addEventListener('mouseleave', handleMouseLeaveGrid); // Handle leaving grid bounds
// Touch
gridContainer.addEventListener('touchstart', handleInteractionStart, { passive: false });
gridContainer.addEventListener('touchmove', handleInteractionMove, { passive: false });
gridContainer.addEventListener('touchend', handleInteractionEnd);
gridContainer.addEventListener('touchcancel', handleInteractionEnd); // Treat cancel like end
// Buttons
resetButton.addEventListener('click', setupGrid);
hintButton.addEventListener('click', showHint);
// --- Initial Setup ---
setupGrid(); // Create the grid and initialize the game when the page loads
}); // End DOMContentLoaded
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.