JavaScript Game Tutorial
How to Make a Wordle-Style Word Game in JavaScript
A five-letter word game is mostly user interface: tiles, a keyboard and colour feedback. The one tricky part is scoring guesses with repeated letters correctly, and this tutorial shows the two-pass algorithm that gets it right.
Production context
This guide studies Word Guess, a published Supagames game. Repository source: games/lvl01/74-word-guess.html.
Play a round of Word Guess on Supagames: type a five-letter word, press Enter and read the colours. Green means the right letter in the right place, yellow means the letter is in the word somewhere else, and grey means it is not in the word.
The game is built with DOM elements: a 6x5 grid of tiles, a message line and an on-screen keyboard generated from three rows of letters.
1. Pick a Target Word From a List
The game keeps an array of uppercase five-letter answers and picks one at random for each round. Storing words in uppercase avoids case conversions everywhere else.
The current row and column describe where the next typed letter goes, and gameOver blocks input once the round is decided.
const WORDS = ["APPLE", "BRAIN", "CHAIR", "DANCE", "EARTH", "FLAME", "GRACE" /* ... */];
let targetWord, currentRow = 0, currentCol = 0, gameOver = false;
let gridTiles = []; // 6 rows x 5 tiles: { el, letter }
function init() {
targetWord = WORDS[Math.floor(Math.random() * WORDS.length)];
currentRow = 0;
currentCol = 0;
gameOver = false;
buildGrid();
buildKeyboard();
}
2. Build the 6x5 Tile Grid
Each tile is a div inside a CSS grid. The code keeps a reference to every tile element together with its letter, so typing and colouring never have to search the DOM.
Six rows give the player six attempts, which is the classic balance between challenge and fairness for five-letter words.
function buildGrid() {
gridEl.innerHTML = "";
gridTiles = [];
for (let r = 0; r < 6; r++) {
const row = [];
for (let c = 0; c < 5; c++) {
const tile = document.createElement("div");
tile.className = "tile";
gridEl.appendChild(tile);
row.push({ el: tile, letter: "" });
}
gridTiles.push(row);
}
}
3. Type With the Physical and On-Screen Keyboards
Both keyboards call the same three functions: add a letter, delete a letter and submit. The physical keyboard accepts any single letter, Backspace and Enter, and calls preventDefault so Backspace does not navigate away and letters do not trigger browser shortcuts.
The on-screen keyboard is generated from the rows QWERTYUIOP, ASDFGHJKL and ZXCVBNM, with Enter and Backspace keys added to the last row. It makes the game fully playable on phones.
function handleKey(key) {
if (gameOver || currentCol >= 5) return;
const tile = gridTiles[currentRow][currentCol];
tile.letter = key;
tile.el.textContent = key;
currentCol++;
}
document.addEventListener("keydown", (e) => {
if (gameOver) return;
if (e.key === "Enter") { e.preventDefault(); handleEnter(); }
else if (e.key === "Backspace") { e.preventDefault(); handleBackspace(); }
else if (/^[a-zA-Z]$/.test(e.key)) { e.preventDefault(); handleKey(e.key.toUpperCase()); }
});
4. Score Guesses Correctly With Two Passes
Repeated letters are where most implementations go wrong. If the answer is CRANE and the guess is EERIE, only one E may be marked, because the answer contains a single E. A one-pass check would wrongly colour several of them.
The fix is two passes. First mark exact matches green and remove those letters from both words. Then, for each remaining guess letter, look for the same letter in what is left of the target, mark it yellow and remove it so it cannot be used twice.
function getResult(guess) {
const result = Array(5).fill("gray");
const target = targetWord.split("");
const letters = guess.split("");
for (let i = 0; i < 5; i++) { // pass 1: right letter, right place
if (letters[i] === target[i]) {
result[i] = "green";
target[i] = null;
letters[i] = null;
}
}
for (let i = 0; i < 5; i++) { // pass 2: right letter, wrong place
if (letters[i] === null) continue;
const idx = target.indexOf(letters[i]);
if (idx !== -1) {
result[i] = "yellow";
target[idx] = null; // each target letter counts once
}
}
return result;
}
// answer CRANE, guess EERIE -> gray, gray, yellow (R), gray, green (last E)
getResult("EERIE");
5. Reveal Tiles and Colour the Keyboard
Tiles flip one after another with a 100 millisecond delay each, which builds a little tension. Each flip adds a CSS class for the result colour.
Keyboard keys keep the best result seen so far: green beats yellow, and yellow beats grey. Without that priority, a later grey result could overwrite a green key and mislead the player.
function revealRow(row, result) {
row.forEach((tile, i) => {
setTimeout(() => {
tile.el.classList.add("flip-" + result[i]);
updateKeyboard(tile.letter, result[i]);
}, i * 100);
});
}
function updateKeyboard(letter, type) {
const btn = keyboardMap[letter];
const priority = { green: 3, yellow: 2, gray: 1, none: 0 };
if (priority[type] > priority[btn.dataset.state || "none"]) btn.dataset.state = type;
}
6. Decide the Round and Keep a Best Score
A guess equal to the target wins. The Supagames version awards more points for faster solves: 600 points in one guess, 500 in two, down to 100 in six. After the sixth wrong guess, the round ends and the answer is revealed.
The best total is stored in localStorage, so it survives page reloads without any account or server.
function evaluateGuess(guess) {
revealRow(gridTiles[currentRow], getResult(guess));
if (guess === targetWord) {
gameOver = true;
totalScore += (7 - (currentRow + 1)) * 100;
showMessage("You win!");
} else if (currentRow === 5) {
gameOver = true;
showMessage("The word was: " + targetWord);
} else {
currentRow++;
currentCol = 0;
}
const best = parseInt(localStorage.getItem("wordGuessBest") || "0", 10);
if (totalScore > best) localStorage.setItem("wordGuessBest", totalScore);
}
7. Only Accept Real Words
The Supagames version accepts any five letters, which keeps it relaxed. Stricter games reject guesses that are not real words, which stops players from typing letter combinations just to test vowels.
Load a list of allowed guesses into a Set for fast lookups, and shake the row with a message when the word is unknown. The allowed list is usually much larger than the list of possible answers.
const ALLOWED = new Set(["APPLE", "BRAIN", "CRANE", "SLATE" /* ...thousands more */]);
function handleEnter() {
if (currentCol < 5) return showMessage("Not enough letters");
const guess = gridTiles[currentRow].map((t) => t.letter).join("");
if (!ALLOWED.has(guess)) {
shakeRow(currentRow);
return showMessage("Not in word list");
}
evaluateGuess(guess);
}
8. Build checklist
- Pick the answer from an uppercase word list.
- Keep references to every tile and its letter.
- Route physical and on-screen keys through the same functions.
- Score with two passes so repeated letters are coloured correctly.
- Let keyboard colours only upgrade: grey, then yellow, then green.
- Store the best score in localStorage; optionally validate guesses with a Set.
9. FAQ
How do repeated letters work in Wordle-style games?
Each letter in the answer can be matched only once. Exact matches are marked first, then remaining letters are matched left to right, so a guess with two Es against an answer with one E shows only one coloured E.
Who created Wordle?
Wordle was created by software engineer Josh Wardle and released in October 2021. The New York Times bought it in January 2022. The name is now a trademark, so clones use their own names.
How do I make a daily word that is the same for everyone?
Use the date as the index: count the days since a fixed start date and take that position in the answer list, wrapping around with the modulo operator. Every player gets the same word on the same day.
Why does my game colour too many letters yellow?
Because it checks whether a letter appears anywhere in the answer without removing letters that were already matched. Use the two-pass method so each answer letter is used at most once.