Last active
September 15, 2024 16:10
-
-
Save NightSling/e170d2fc0434cb14f9c8f02bb7b1edd5 to your computer and use it in GitHub Desktop.
Proof-of-concept for a grid word-finding game.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Interactive Word Search Puzzle</title> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
max-width: 800px; | |
margin: 0 auto; | |
padding: 20px; | |
line-height: 1.6; | |
} | |
.container { | |
margin-bottom: 20px; | |
} | |
#puzzleContainer, | |
#answerContainer { | |
display: inline-block; | |
margin-right: 20px; | |
} | |
.grid { | |
display: inline-grid; | |
gap: 1px; | |
background-color: #f0f0f0; | |
padding: 10px; | |
user-select: none; | |
} | |
.cell { | |
width: 30px; | |
height: 30px; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
background-color: white; | |
border: 1px solid #ddd; | |
font-weight: bold; | |
cursor: pointer; | |
} | |
.highlighted { | |
background-color: rgba(0, 0, 255, 0.2); | |
} | |
.found { | |
font-weight: bold; | |
} | |
#wordList { | |
margin-top: 20px; | |
} | |
.word { | |
display: inline-block; | |
margin-right: 10px; | |
padding: 5px; | |
border-radius: 3px; | |
} | |
input, | |
button { | |
margin: 5px; | |
padding: 5px; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Interactive Word Search Puzzle</h1> | |
<div> | |
<input type="text" id="wordsInput" placeholder="Enter words (comma-separated)"> | |
<input type="number" id="gridSizeInput" placeholder="Grid size (optional)"> | |
<button id="generateButton">Generate Puzzle</button> | |
<button id="showAnswerButton">Show Answer</button> | |
</div> | |
<div class="container"> | |
<div id="puzzleContainer"></div> | |
<div id="answerContainer"></div> | |
</div> | |
<div id="wordList"></div> | |
<script> | |
class WordSearchGenerator { | |
constructor(words, size = null) { | |
this.words = words.map(word => word.toUpperCase()); | |
this.size = size || this.calculateMinGridSize(); | |
this.grid = null; | |
this.solution = null; | |
this.wordColors = {}; | |
} | |
calculateMinGridSize() { | |
const maxWordLength = Math.max(...this.words.map(word => word.length)); | |
const totalLetters = this.words.join('').length; | |
return Math.max(maxWordLength, Math.ceil(Math.sqrt(totalLetters))); | |
} | |
createGrid(size) { | |
return Array(size).fill().map(() => Array(size).fill(' ')); | |
} | |
wordFits(grid, word, row, col, dRow, dCol) { | |
const size = grid.length; | |
if (row + dRow * (word.length - 1) < 0 || row + dRow * (word.length - 1) >= size) return false; | |
if (col + dCol * (word.length - 1) < 0 || col + dCol * (word.length - 1) >= size) return false; | |
for (let i = 0; i < word.length; i++) { | |
if (![' ', word[i]].includes(grid[row + i * dRow][col + i * dCol])) return false; | |
} | |
return true; | |
} | |
placeWord(grid, word) { | |
const size = grid.length; | |
const directions = [[0, 1], [1, 0], [1, 1], [-1, 1]]; | |
let attempts = 0; | |
while (attempts < 100) { | |
const [dRow, dCol] = directions[Math.floor(Math.random() * directions.length)]; | |
const row = Math.floor(Math.random() * size); | |
const col = Math.floor(Math.random() * size); | |
if (this.wordFits(grid, word, row, col, dRow, dCol)) { | |
for (let i = 0; i < word.length; i++) { | |
grid[row + i * dRow][col + i * dCol] = word[i]; | |
} | |
return true; | |
} | |
attempts++; | |
} | |
return false; | |
} | |
fillEmptySpaces(solution) { | |
const grid = JSON.parse(JSON.stringify(solution)); | |
for (let i = 0; i < grid.length; i++) { | |
for (let j = 0; j < grid.length; j++) { | |
if (grid[i][j] === ' ') { | |
grid[i][j] = String.fromCharCode(65 + Math.floor(Math.random() * 26)); | |
} | |
} | |
} | |
return grid; | |
} | |
createWordSearch() { | |
this.solution = this.createGrid(this.size); | |
for (const word of this.words) { | |
if (!this.placeWord(this.solution, word)) { | |
console.log(`Couldn't place word: ${word}`); | |
} | |
this.wordColors[word] = this.getRandomColor(); | |
} | |
this.grid = this.fillEmptySpaces(this.solution); | |
} | |
getRandomColor() { | |
const hue = Math.floor(Math.random() * 360); | |
return `hsl(${hue}, 70%, 80%)`; | |
} | |
generate() { | |
this.createWordSearch(); | |
return { | |
grid: this.grid, | |
solution: this.solution, | |
words: this.words, | |
wordColors: this.wordColors | |
}; | |
} | |
} | |
let currentPuzzle = null; | |
let isMouseDown = false; | |
let startCell = null; | |
let highlightedCells = []; | |
function generatePuzzle() { | |
const wordsInput = document.getElementById('wordsInput').value; | |
const gridSizeInput = document.getElementById('gridSizeInput').value; | |
const words = wordsInput.split(',').map(word => word.trim()).filter(word => word.length > 0); | |
const size = gridSizeInput ? parseInt(gridSizeInput) : null; | |
if (words.length === 0) { | |
alert('Please enter at least one word.'); | |
return; | |
} | |
const generator = new WordSearchGenerator(words, size); | |
currentPuzzle = generator.generate(); | |
renderPuzzle(currentPuzzle.grid, 'puzzleContainer'); | |
renderWordList(currentPuzzle.words, currentPuzzle.wordColors); | |
document.getElementById('answerContainer').innerHTML = ''; | |
} | |
function renderPuzzle(grid, containerId) { | |
const container = document.getElementById(containerId); | |
container.innerHTML = ''; | |
const gridElement = document.createElement('div'); | |
gridElement.className = 'grid'; | |
gridElement.style.gridTemplateColumns = `repeat(${grid.length}, 30px)`; | |
for (let i = 0; i < grid.length; i++) { | |
for (let j = 0; j < grid.length; j++) { | |
const cell = document.createElement('div'); | |
cell.className = 'cell'; | |
cell.textContent = grid[i][j]; | |
cell.dataset.row = i; | |
cell.dataset.col = j; | |
gridElement.appendChild(cell); | |
} | |
} | |
container.appendChild(gridElement); | |
if (containerId === 'puzzleContainer') { | |
gridElement.addEventListener('mousedown', startHighlight); | |
gridElement.addEventListener('mouseover', continueHighlight); | |
gridElement.addEventListener('mouseup', endHighlight); | |
} | |
} | |
function renderWordList(words, wordColors) { | |
const wordList = document.getElementById('wordList'); | |
wordList.innerHTML = ''; | |
words.forEach(word => { | |
const wordElement = document.createElement('span'); | |
wordElement.className = 'word'; | |
wordElement.textContent = word; | |
wordElement.style.backgroundColor = wordColors[word]; | |
wordList.appendChild(wordElement); | |
}); | |
} | |
function startHighlight(e) { | |
if (e.target.classList.contains('cell')) { | |
isMouseDown = true; | |
startCell = e.target; | |
highlightedCells = [startCell]; | |
startCell.classList.add('highlighted'); | |
} | |
} | |
function continueHighlight(e) { | |
if (isMouseDown && e.target.classList.contains('cell')) { | |
const currentCell = e.target; | |
const startRow = parseInt(startCell.dataset.row); | |
const startCol = parseInt(startCell.dataset.col); | |
const currentRow = parseInt(currentCell.dataset.row); | |
const currentCol = parseInt(currentCell.dataset.col); | |
const rowDiff = currentRow - startRow; | |
const colDiff = currentCol - startCol; | |
const maxDiff = Math.max(Math.abs(rowDiff), Math.abs(colDiff)); | |
// Clear previous highlighting | |
document.querySelectorAll('.cell.highlighted').forEach(cell => cell.classList.remove('highlighted')); | |
highlightedCells = [startCell]; | |
if (maxDiff === 0) { | |
// Only highlight the start cell if there's no movement | |
startCell.classList.add('highlighted'); | |
} else { | |
for (let i = 0; i <= maxDiff; i++) { | |
const row = startRow + Math.round(rowDiff * i / maxDiff); | |
const col = startCol + Math.round(colDiff * i / maxDiff); | |
const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`); | |
if (cell) { | |
cell.classList.add('highlighted'); | |
highlightedCells.push(cell); | |
} | |
} | |
} | |
} | |
} | |
function endHighlight() { | |
if (isMouseDown) { | |
isMouseDown = false; | |
const word = highlightedCells.map(cell => cell.textContent).join('').substr(1); | |
const reversedWord = word.split('').reverse().join('').substr(1); | |
console.log(word, reversedWord); | |
if (currentPuzzle.words.includes(word) || currentPuzzle.words.includes(reversedWord)) { | |
const color = currentPuzzle.wordColors[word] || currentPuzzle.wordColors[reversedWord]; | |
highlightedCells.forEach(cell => { | |
cell.classList.remove('highlighted'); | |
cell.classList.add('found'); | |
cell.style.backgroundColor = color; | |
}); | |
} else { | |
highlightedCells.forEach(cell => cell.classList.remove('highlighted')); | |
} | |
highlightedCells = []; | |
} | |
} | |
function showAnswer() { | |
if (currentPuzzle) { | |
renderPuzzle(currentPuzzle.solution, 'answerContainer'); | |
const answerGrid = document.querySelector('#answerContainer .grid'); | |
currentPuzzle.words.forEach(word => { | |
const color = currentPuzzle.wordColors[word]; | |
const result = findWordInGrid(word); | |
if (result) { | |
highlightWord(answerGrid, result.startRow, result.startCol, result.endRow, result.endCol, color); | |
} | |
}); | |
} | |
} | |
function wordStartsAt(word, row, col) { | |
const directions = [[0, 1], [1, 0], [1, 1], [-1, 1], [0, -1], [-1, 0], [-1, -1], [1, -1]]; | |
for (const [dRow, dCol] of directions) { | |
if (checkWordInDirection(word, row, col, dRow, dCol)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
function checkWordInDirection(word, row, col, dRow, dCol) { | |
if (row + dRow * (word.length - 1) < 0 || row + dRow * (word.length - 1) >= currentPuzzle.solution.length) return false; | |
if (col + dCol * (word.length - 1) < 0 || col + dCol * (word.length - 1) >= currentPuzzle.solution.length) return false; | |
for (let i = 0; i < word.length; i++) { | |
if (currentPuzzle.solution[row + i * dRow][col + i * dCol] !== word[i]) { | |
return false; | |
} | |
} | |
return true; | |
} | |
function findWordInGrid(word) { | |
const directions = [ | |
[0, 1], [1, 0], [1, 1], [-1, 1], | |
[0, -1], [-1, 0], [-1, -1], [1, -1] | |
]; | |
for (let row = 0; row < currentPuzzle.solution.length; row++) { | |
for (let col = 0; col < currentPuzzle.solution.length; col++) { | |
for (const [dRow, dCol] of directions) { | |
if (checkWordInDirection(word, row, col, dRow, dCol)) { | |
return { | |
startRow: row, | |
startCol: col, | |
endRow: row + dRow * (word.length - 1), | |
endCol: col + dCol * (word.length - 1) | |
}; | |
} | |
} | |
} | |
} | |
return null; | |
} | |
function highlightWord(grid, startRow, startCol, endRow, endCol, color) { | |
const rowDiff = endRow - startRow; | |
const colDiff = endCol - startCol; | |
const maxDiff = Math.max(Math.abs(rowDiff), Math.abs(colDiff)); | |
for (let i = 0; i <= maxDiff; i++) { | |
const row = startRow + Math.round(rowDiff * i / maxDiff); | |
const col = startCol + Math.round(colDiff * i / maxDiff); | |
const cell = grid.children[row * currentPuzzle.solution.length + col]; | |
cell.style.backgroundColor = color; | |
} | |
} | |
document.getElementById('generateButton').addEventListener('click', generatePuzzle); | |
document.getElementById('showAnswerButton').addEventListener('click', showAnswer); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment