The table is set and the feast is hiding in the letters ๐ฅง! Six Thanksgiving words are tucked into the grid: turkey, gravy, stuffing, pumpkin pie and friends. Tap the first letter of a word, then its last letter, to circle it. Clear the board and you get a fresh one plus 30 bonus seconds, but watch out: the words start hiding diagonally and even backwards as you go. You start with 90 seconds, and the game ends when the timer hits zero. How many words can you gobble up before dinner is served?
.gws-game {
text-align: center;
}
.gws-game .instructions {
max-width: 500px;
margin: 0 auto 15px;
font-size: 1em;
color: #555;
}
.gws-info {
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 12px;
font-size: 1.1em;
}
.gws-grid {
display: grid;
grid-template-columns: repeat(9, 1fr);
gap: 4px;
width: 400px;
max-width: 100%;
margin: 0 auto;
padding: 8px;
box-sizing: border-box;
background: #7a3e12;
border-radius: 12px;
touch-action: manipulation;
user-select: none;
-webkit-user-select: none;
}
.gws-cell {
aspect-ratio: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
font-size: 1.15em;
font-weight: bold;
color: #4a2508;
background: #fff4dc;
border: none;
border-radius: 6px;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.gws-cell:disabled {
cursor: default;
}
.gws-cell.start {
background: #f6b13b;
color: #ffffff;
}
.gws-cell.found {
background: #d9653b;
color: #ffffff;
}
.gws-cell.wrong {
background: #b8b8b8;
}
.gws-words {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 6px 10px;
max-width: 420px;
margin: 14px auto 0;
padding: 0;
list-style: none;
}
.gws-words li {
padding: 4px 10px;
border-radius: 14px;
background: #fbe3c0;
color: #4a2508;
font-weight: bold;
font-size: 0.95em;
}
.gws-words li.done {
background: #d9653b;
color: #ffffff;
text-decoration: line-through;
}
.gws-message {
margin-top: 15px;
font-weight: bold;
min-height: 1.5em;
}
.gws-btn {
margin-top: 12px;
padding: 10px 24px;
font-size: 1em;
background-color: #7a3e12;
color: #ffe2b0;
font-weight: bold;
border: none;
border-radius: 5px;
cursor: pointer;
}
.gws-btn:hover {
background-color: #95501c;
}
.gws-btn:disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
}
<div class="gws-game">
<h1>๐ฆ Gobble Word Search - Thanksgiving Game ๐ฅง</h1>
<div class="instructions">
Find the Thanksgiving words hidden in the grid! Tap the first letter of a word, then its last letter. Clear the board before the timer runs out to get a new one and bonus time.
</div>
<div class="gws-info">
<span>Words: <span id="gwsScore">0</span></span>
<span>Time: <span id="gwsTime">90</span>s</span>
<span>Best: <span id="gwsBest">0</span></span>
</div>
<div class="gws-grid" id="gwsGrid"></div>
<ul class="gws-words" id="gwsWords"></ul>
<div class="gws-message" id="gwsMessage">Press "Start" when the table is set...</div>
<button class="gws-btn" id="gwsStart">Start</button>
</div>
const gwsGridEl = document.getElementById('gwsGrid');
const gwsWordsEl = document.getElementById('gwsWords');
const gwsScoreEl = document.getElementById('gwsScore');
const gwsTimeEl = document.getElementById('gwsTime');
const gwsBestEl = document.getElementById('gwsBest');
const gwsMessageEl = document.getElementById('gwsMessage');
const gwsStartBtn = document.getElementById('gwsStart');
const gwsHiddenScore = document.getElementById('hiddenScore');
const GWS_SIZE = 9;
const GWS_START_SECONDS = 90;
const GWS_BOARD_BONUS = 30;
const GWS_WORDS_PER_BOARD = 6;
const GWS_LETTERS = 'ABCDEFGHIKLMNOPRSTUWY';
const GWS_WORDS = [
'TURKEY', 'GRAVY', 'PUMPKIN', 'STUFFING', 'CORN', 'FEAST', 'THANKS', 'PIE',
'PECAN', 'YAMS', 'HARVEST', 'FAMILY', 'NAPKIN', 'ACORN', 'APPLE', 'BREAD',
'BUTTER', 'GRATEFUL', 'PARADE', 'FOOTBALL', 'LEAVES', 'AUTUMN', 'GOBBLE', 'TABLE'
];
// Directions unlocked board after board: across and down first, then diagonals and backwards words.
const GWS_DIRECTIONS = [
[[0, 1], [1, 0]],
[[0, 1], [1, 0], [1, 1]],
[[0, 1], [1, 0], [1, 1], [-1, 1], [0, -1], [-1, 0]],
[[0, 1], [1, 0], [1, 1], [-1, 1], [0, -1], [-1, 0], [-1, -1], [1, -1]]
];
let gwsGrid = [];
let gwsCells = [];
let gwsPlaced = [];
let gwsScore = 0;
let gwsBoard = 0;
let gwsTimeLeft = GWS_START_SECONDS;
let gwsRunning = false;
let gwsFirst = null;
let gwsFrame = null;
let gwsLastTime = 0;
function gwsReadBest() {
try {
return parseInt(localStorage.getItem('thanksgivingWordSearchBest') || '0', 10) || 0;
} catch (e) {
return 0;
}
}
function gwsSaveBest(value) {
try {
localStorage.setItem('thanksgivingWordSearchBest', String(value));
} catch (e) {
// storage blocked: best score only lasts for this visit
}
}
let gwsBest = gwsReadBest();
gwsBestEl.textContent = gwsBest;
function gwsShuffle(list) {
const copy = list.slice();
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const tmp = copy[i];
copy[i] = copy[j];
copy[j] = tmp;
}
return copy;
}
function gwsTryPlace(word, directions) {
for (let attempt = 0; attempt < 200; attempt++) {
const dir = directions[Math.floor(Math.random() * directions.length)];
const row = Math.floor(Math.random() * GWS_SIZE);
const col = Math.floor(Math.random() * GWS_SIZE);
const endRow = row + dir[0] * (word.length - 1);
const endCol = col + dir[1] * (word.length - 1);
if (endRow < 0 || endRow >= GWS_SIZE || endCol < 0 || endCol >= GWS_SIZE) {
continue;
}
let fits = true;
for (let k = 0; k < word.length; k++) {
const letter = gwsGrid[row + dir[0] * k][col + dir[1] * k];
if (letter !== '' && letter !== word[k]) {
fits = false;
break;
}
}
if (!fits) {
continue;
}
const cells = [];
for (let k = 0; k < word.length; k++) {
gwsGrid[row + dir[0] * k][col + dir[1] * k] = word[k];
cells.push((row + dir[0] * k) * GWS_SIZE + (col + dir[1] * k));
}
return cells;
}
return null;
}
function gwsBuildBoard() {
const directions = GWS_DIRECTIONS[Math.min(gwsBoard, GWS_DIRECTIONS.length - 1)];
let words = [];
do {
gwsGrid = [];
for (let r = 0; r < GWS_SIZE; r++) {
gwsGrid.push(['', '', '', '', '', '', '', '', '']);
}
gwsPlaced = [];
words = gwsShuffle(GWS_WORDS);
for (let i = 0; i < words.length && gwsPlaced.length < GWS_WORDS_PER_BOARD; i++) {
const cells = gwsTryPlace(words[i], directions);
if (cells) {
gwsPlaced.push({word: words[i], cells: cells, found: false});
}
}
} while (gwsPlaced.length < GWS_WORDS_PER_BOARD);
for (let r = 0; r < GWS_SIZE; r++) {
for (let c = 0; c < GWS_SIZE; c++) {
if (gwsGrid[r][c] === '') {
gwsGrid[r][c] = GWS_LETTERS[Math.floor(Math.random() * GWS_LETTERS.length)];
}
}
}
gwsCells.forEach(function (cell, i) {
cell.textContent = gwsGrid[Math.floor(i / GWS_SIZE)][i % GWS_SIZE];
cell.className = 'gws-cell';
cell.disabled = false;
});
gwsWordsEl.innerHTML = '';
gwsPlaced.forEach(function (item) {
const li = document.createElement('li');
li.textContent = item.word;
item.el = li;
gwsWordsEl.appendChild(li);
});
gwsFirst = null;
}
function gwsLine(a, b) {
const r1 = Math.floor(a / GWS_SIZE);
const c1 = a % GWS_SIZE;
const r2 = Math.floor(b / GWS_SIZE);
const c2 = b % GWS_SIZE;
const dr = Math.sign(r2 - r1);
const dc = Math.sign(c2 - c1);
const len = Math.max(Math.abs(r2 - r1), Math.abs(c2 - c1));
// Only straight lines: across, down or exact diagonals.
if ((r1 !== r2 && c1 !== c2 && Math.abs(r2 - r1) !== Math.abs(c2 - c1)) || len === 0) {
return null;
}
const cells = [];
for (let k = 0; k <= len; k++) {
cells.push((r1 + dr * k) * GWS_SIZE + (c1 + dc * k));
}
return cells;
}
function gwsSameCells(a, b) {
if (a.length !== b.length) {
return false;
}
const reversed = b.slice().reverse();
const forward = a.every(function (v, i) { return v === b[i]; });
const backward = a.every(function (v, i) { return v === reversed[i]; });
return forward || backward;
}
function gwsTap(index) {
if (!gwsRunning) {
return;
}
if (gwsFirst === null) {
gwsFirst = index;
gwsCells[index].classList.add('start');
return;
}
const first = gwsFirst;
gwsFirst = null;
gwsCells[first].classList.remove('start');
if (first === index) {
return;
}
const line = gwsLine(first, index);
const match = line ? gwsPlaced.filter(function (item) {
return !item.found && gwsSameCells(item.cells, line);
})[0] : null;
if (!match) {
gwsFlashWrong(line || [first, index]);
gwsMessageEl.textContent = 'Not a word from the list. Try again!';
return;
}
match.found = true;
match.el.classList.add('done');
match.cells.forEach(function (i) {
gwsCells[i].classList.add('found');
});
gwsScore += 1;
gwsScoreEl.textContent = gwsScore;
if (gwsHiddenScore) {
gwsHiddenScore.value = gwsScore;
}
const left = gwsPlaced.filter(function (item) { return !item.found; }).length;
if (left === 0) {
gwsBoard += 1;
gwsTimeLeft += GWS_BOARD_BONUS;
gwsMessageEl.textContent = 'Board cleared! +' + GWS_BOARD_BONUS + 's. The words get trickier... ๐ฅง';
setTimeout(function () {
if (gwsRunning) {
gwsBuildBoard();
}
}, 700);
} else {
gwsMessageEl.textContent = 'Gobble gobble! ' + match.word + ' found. ' + left + ' to go ๐ฆ';
}
}
function gwsFlashWrong(cells) {
cells.forEach(function (i) {
if (!gwsCells[i].classList.contains('found')) {
gwsCells[i].classList.add('wrong');
}
});
setTimeout(function () {
cells.forEach(function (i) {
gwsCells[i].classList.remove('wrong');
});
}, 300);
}
function gwsLoop(now) {
if (!gwsRunning) {
return;
}
gwsTimeLeft -= (now - gwsLastTime) / 1000;
gwsLastTime = now;
if (gwsTimeLeft <= 0) {
gwsTimeLeft = 0;
gwsTimeEl.textContent = '0';
gwsEnd();
return;
}
gwsTimeEl.textContent = Math.ceil(gwsTimeLeft);
gwsFrame = requestAnimationFrame(gwsLoop);
}
function gwsStart() {
gwsScore = 0;
gwsBoard = 0;
gwsTimeLeft = GWS_START_SECONDS;
gwsScoreEl.textContent = '0';
gwsTimeEl.textContent = GWS_START_SECONDS;
if (gwsHiddenScore) {
gwsHiddenScore.value = 0;
}
const share = document.querySelector('.share-buttons');
if (share) {
share.style.display = 'none';
}
gwsBuildBoard();
gwsStartBtn.disabled = true;
gwsMessageEl.textContent = 'Tap the first letter of a word, then its last letter.';
gwsRunning = true;
gwsLastTime = performance.now();
gwsFrame = requestAnimationFrame(gwsLoop);
}
function gwsEnd() {
gwsRunning = false;
if (gwsFrame) {
cancelAnimationFrame(gwsFrame);
}
gwsCells.forEach(function (cell) {
cell.disabled = true;
cell.classList.remove('start');
});
// Show the words that were left so the player learns where they were.
gwsPlaced.forEach(function (item) {
if (!item.found) {
item.cells.forEach(function (i) {
gwsCells[i].classList.add('start');
});
}
});
let text = "Time's up! You found " + gwsScore + ' word' + (gwsScore === 1 ? '' : 's') + '.';
if (gwsScore > gwsBest) {
gwsBest = gwsScore;
gwsBestEl.textContent = gwsBest;
gwsSaveBest(gwsBest);
text += ' New best score! ๐';
}
gwsMessageEl.textContent = text;
gwsStartBtn.disabled = false;
gwsStartBtn.textContent = 'Play again';
const share = document.querySelector('.share-buttons');
if (share) {
share.style.display = 'flex';
}
}
for (let i = 0; i < GWS_SIZE * GWS_SIZE; i++) {
const cell = document.createElement('button');
cell.className = 'gws-cell';
cell.type = 'button';
cell.textContent = 'ยท';
cell.disabled = true;
cell.addEventListener('touchstart', function (e) {
e.preventDefault();
gwsTap(i);
}, {passive: false});
cell.addEventListener('mousedown', function (e) {
e.preventDefault();
gwsTap(i);
});
gwsGridEl.appendChild(cell);
gwsCells.push(cell);
}
gwsStartBtn.addEventListener('click', gwsStart);