The tea shop prints a word puzzle on its receipts, and now it wants one on the website. Build WordGuess, the default export: you have six tries to guess a five-letter word.
It takes answer, the word to guess, in any case. It's optional: without it, the game picks a word at random from its own list when it starts, and keeps that word for the whole game.
You type anywhere on the page, so listen for keydown on window. A letter key (a to z, either case) adds that letter, in capitals, to the guess you're typing, up to five letters. Backspace removes the last letter. Enter submits the guess if it has five letters; with fewer, nothing is submitted and the status says Not enough letters. Other keys do nothing, and so do keys pressed with Ctrl, Alt, or Meta held down.
Show the board as an element with role="grid" holding six role="row" elements, each with five role="gridcell" elements. Submitted guesses fill the rows from the top, the guess you're typing shows in the next row, and each cell holds one letter or nothing.
Each cell of a submitted guess gets a data-status: correct when its letter is in that spot in the answer, present when the letter is in the answer somewhere else, and absent when it isn't. Cells that aren't part of a submitted guess have no data-status.
Repeated letters are the tricky part. A letter can only be marked correct or present as many times as it appears in the answer. Exact matches claim their copies first. Then, from left to right, each other copy is present while the answer still has an unclaimed copy of that letter, and absent after that. With the answer BLEND, the guess GEESE marks the middle E correct and both other Es absent. With the answer MATCH, the guess AROMA marks the first A present and the last one absent.
A <p role="status"> shows messages: Not enough letters as above (until the next key), You got it! when a guess matches the answer, and The word was CRANE. (with the answer in capitals) after six wrong guesses. Once the game is won or lost, keys do nothing.
<WordGuess /> // a random word from the game's list
<WordGuess answer="crane" />
// after guessing TRACE, with the answer CRANE:
<div role="row">
<div role="gridcell" data-status="absent">T</div>
<div role="gridcell" data-status="correct">R</div>
<div role="gridcell" data-status="correct">A</div>
<div role="gridcell" data-status="present">C</div>
<div role="gridcell" data-status="correct">E</div>
</div>
render(<App answer="CRANE" />);
expect(screen.getByRole("grid")).toBeInTheDocument();
const rows = screen.getAllByRole("row");
expect(rows.length).toBe(6);
for (const row of rows) {
const cells = within(row).getAllByRole("gridcell");
expect(cells.length).toBe(5);
for (const cell of cells) {
expect(cell.textContent).toBe("");
expect(cell).not.toHaveAttribute("data-status");
}
}
const letters = (n) => within(screen.getAllByRole("row")[n]).getAllByRole("gridcell").map((cell) => cell.textContent).join("");
render(<App answer="CRANE" />);
await user.keyboard("cr");
expect(letters(0)).toBe("CR");
await user.keyboard("{Backspace}");
expect(letters(0)).toBe("C");
await user.keyboard("1 ");
expect(letters(0)).toBe("C");
await user.keyboard("{Control>}a{/Control}{Alt>}b{/Alt}{Meta>}{Backspace}{/Meta}");
expect(letters(0)).toBe("C");
await user.keyboard("RANEXY");
expect(letters(0)).toBe("CRANE");
await user.keyboard("{Backspace}");
expect(letters(0)).toBe("CRAN");
expect(screen.getAllByRole("gridcell")[0]).not.toHaveAttribute("data-status");
const cells = (n) => within(screen.getAllByRole("row")[n]).getAllByRole("gridcell");
render(<App answer="CRANE" />);
await user.keyboard("cra{Enter}");
expect(screen.getByRole("status")).toHaveTextContent("Not enough letters");
expect(cells(0).map((cell) => cell.textContent).join("")).toBe("CRA");
for (const cell of cells(0)) expect(cell).not.toHaveAttribute("data-status");
await user.keyboard("n");
expect(screen.getByRole("status")).not.toHaveTextContent("Not enough letters");
const cells = (n) => within(screen.getAllByRole("row")[n]).getAllByRole("gridcell");
render(<App answer="CRANE" />);
await user.keyboard("trace{Enter}");
expect(cells(0).map((cell) => cell.textContent).join("")).toBe("TRACE");
expect(cells(0).map((cell) => cell.getAttribute("data-status"))).toEqual(["absent", "correct", "correct", "present", "correct"]);
await user.keyboard("s");
expect(cells(1)[0]).toHaveTextContent("S");
expect(cells(1)[0]).not.toHaveAttribute("data-status");
const statuses = (n) => within(screen.getAllByRole("row")[n]).getAllByRole("gridcell").map((cell) => cell.getAttribute("data-status"));
render(<App answer="BLEND" />);
await user.keyboard("geese{Enter}");
expect(statuses(0)).toEqual(["absent", "absent", "correct", "absent", "absent"]);
const statuses = (n) => within(screen.getAllByRole("row")[n]).getAllByRole("gridcell").map((cell) => cell.getAttribute("data-status"));
render(<App answer="MATCH" />);
await user.keyboard("aroma{Enter}");
expect(statuses(0)).toEqual(["present", "absent", "absent", "present", "absent"]);
const letters = (n) => within(screen.getAllByRole("row")[n]).getAllByRole("gridcell").map((cell) => cell.textContent).join("");
render(<App answer="CRANE" />);
await user.keyboard("trace{Enter}");
expect(screen.getByRole("status")).not.toHaveTextContent("You got it!");
await user.keyboard("crane{Enter}");
expect(screen.getByRole("status")).toHaveTextContent("You got it!");
const statuses = within(screen.getAllByRole("row")[1]).getAllByRole("gridcell").map((cell) => cell.getAttribute("data-status"));
expect(statuses).toEqual(["correct", "correct", "correct", "correct", "correct"]);
await user.keyboard("steep{Enter}");
expect(letters(2)).toBe("");
const letters = (n) => within(screen.getAllByRole("row")[n]).getAllByRole("gridcell").map((cell) => cell.textContent).join("");
render(<App answer="CRANE" />);
for (let i = 0; i < 5; i++) await user.keyboard("trace{Enter}");
expect(screen.getByRole("status")).not.toHaveTextContent("The word was");
await user.keyboard("track{Enter}");
expect(screen.getByRole("status")).toHaveTextContent("The word was CRANE.");
expect(letters(5)).toBe("TRACK");
await user.keyboard("{Backspace}");
expect(letters(5)).toBe("TRACK");
render(<App answer="crane" />);
await user.keyboard("CRANE{Enter}");
expect(screen.getByRole("status")).toHaveTextContent("You got it!");
render(<App />);
for (let i = 0; i < 6; i++) await user.keyboard("qqqqq{Enter}");
const status = screen.getByRole("status").textContent;
expect(status).toMatch(/^The word was [A-Z]{5}[.]$/);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Five-letter word game. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.