--- id: 67e3a6b7f60b4085588189e6 title: Build a Tic-Tac-Toe Game challengeType: 25 dashedName: build-a-tic-tac-toe-game demoType: onClick saveSubmissionToDB: true --- # --description-- **Objective:** Fulfill the user stories below and get all the tests to pass to complete the lab. **User Stories:** 1. You should create a `Board` component that renders nine `button` elements each with a class of `square` in a 3x3 grid. 2. Clicking `button.square` elements should alternate between displaying an `X` then `O` within the element. 3. Once a player has won the game, clicking on any `button.square` should not cause any further changes. 4. You should create a `button#reset` element that resets the game when clicked. 5. A message should be displayed indicating either `X` or `O` as the winner, or neither if the result is a draw. # --before-all-- ```js let clock = __FakeTimers.install(); async function reset(assert) { const reset = document.querySelector("#reset"); assert.exists(reset, "button#reset should exist"); reset.click(); clock.tick(50); } // Gets the text of the document excluding that of the matching selector. function getInnerTextExcept(removingSelector) { const body = document.body.cloneNode(true); const squareElements = body.querySelectorAll(`${removingSelector},script`); squareElements.forEach(element => { element.parentNode.removeChild(element) }); return body.innerText; } ``` # --after-all-- ```js clock.uninstall(); ``` # --hints-- You should export a `Board` component. ```js const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText; const exports = {}; const a = eval(script); assert.property(exports, "Board"); ``` You should have nine `button.square` elements. ```js const els = document.querySelectorAll("button.square"); assert.equal(els.length, 9); ``` The `button.square` elements should be in a 3x3 grid. ```js // TODO: Maybe enforce buttons to be same size? const els = document.querySelectorAll("button.square"); const squares = Array.from(els); const coords = squares.map((square) => { const rect = square.getBoundingClientRect(); return rect; }); const xTolerance = coords[0].width / 10; const yTolerance = coords[0].height / 10; try { assert.isBelow(coords[0].x, coords[1].x, "First square should be to the left of the second square"); assert.isBelow(coords[1].x, coords[2].x, "Second square should be to the left of the third square"); assert.approximately(coords[0].y, coords[1].y, yTolerance, "First square should be at the same height as the second square"); assert.approximately(coords[1].y, coords[2].y, yTolerance, "Second square should be at the same height as the third square"); assert.isBelow(coords[3].x, coords[4].x, "Fourth square should be to the left of the fifth square"); assert.isBelow(coords[4].x, coords[5].x, "Fifth square should be to the left of the sixth square"); assert.approximately(coords[3].y, coords[4].y, yTolerance, "Fourth square should be at the same height as the fifth square"); assert.approximately(coords[4].y, coords[5].y, yTolerance, "Fifth square should be at the same height as the sixth square"); assert.isBelow(coords[6].x, coords[7].x, "Seventh square should be to the left of the eighth square"); assert.isBelow(coords[7].x, coords[8].x, "Eighth square should be to the left of the ninth square"); assert.approximately(coords[6].y, coords[7].y, yTolerance, "Seventh square should be at the same height as the eighth square"); assert.approximately(coords[7].y, coords[8].y, yTolerance, "Eighth square should be at the same height as the ninth square"); assert.isBelow(coords[0].y, coords[3].y, "First square should be above the fourth square"); assert.isBelow(coords[3].y, coords[6].y, "Fourth square should be above the seventh square"); assert.approximately(coords[0].x, coords[3].x, xTolerance, "First square should be at the same width as the fourth square"); assert.approximately(coords[3].x, coords[6].x, xTolerance, "Fourth square should be at the same width as the seventh square"); assert.isBelow(coords[1].y, coords[4].y, "Second square should be above the fifth square"); assert.isBelow(coords[4].y, coords[7].y, "Fifth square should be above the eighth square"); assert.approximately(coords[1].x, coords[4].x, xTolerance, "Second square should be at the same width as the fifth square"); assert.approximately(coords[4].x, coords[7].x, xTolerance, "Fifth square should be at the same width as the eighth square"); assert.isBelow(coords[2].y, coords[5].y, "Third square should be above the sixth square"); assert.isBelow(coords[5].y, coords[8].y, "Sixth square should be above the ninth square"); assert.approximately(coords[2].x, coords[5].x, xTolerance, "Third square should be at the same width as the sixth square"); assert.approximately(coords[5].x, coords[8].x, xTolerance, "Sixth square should be at the same width as the ninth square"); } catch (e) { console.error(e) throw e; } ``` The first click of a `button.square` element should result in `X` being displayed within the element. ```js const el = document.querySelector("button.square"); el.click(); clock.tick(50); try { assert.include(el.textContent, "X"); } catch(e) { console.error(e); throw e; } ``` Clicking on the `button#reset` element should reset the game. ```js // NOTE: This test is intentionally high-up, because the latter tests rely on the functionality. const el = document.querySelector("button.square"); el.click(); clock.tick(50); try { await reset(assert); assert.notInclude(el.textContent, "X"); } catch(e) { console.error(e); throw e; } ``` The second click of a `button.square` element should result in `O` being displayed within the element. ```js await reset(assert); const els = document.querySelectorAll("button.square"); els[0].click(); clock.tick(50); els[1].click(); clock.tick(50); try { assert.include(els[1].textContent, "O"); } catch(e) { console.error(e); throw e; } ``` All subsequent clicks of a `button.square` element should alternate between displaying `X` and `O` within the element. ```js await reset(assert); const els = document.querySelectorAll("button.square"); try { for (let i = 3; i < els.length + 3; i++) { const wrappedI = i % els.length; els[wrappedI].click(); clock.tick(50); assert.include(els[wrappedI].textContent, (i - 3) % 2 === 0 ? "X" : "O"); } } catch(e) { console.error(e); throw e; } ``` Clicking on an already used `button.square` element should result in no change. ```js await reset(assert); const els = document.querySelectorAll("button.square"); try { for (let i = 3; i < els.length + 3; i++) { const wrappedI = i % els.length; // Click button twice to ensure it does not change els[wrappedI].click(); clock.tick(50); els[wrappedI].click(); clock.tick(50); assert.include(els[wrappedI].textContent, (i - 3) % 2 === 0 ? "X" : "O"); } } catch(e) { console.error(e); throw e; } ``` Clicking on a `button.square` element after the game has ended should result in no change. ```js await reset(assert); const els = document.querySelectorAll("button.square"); try { // Win game, then click empty square and ensure no change for (let i = 0; i < 3; i++) { const x = els[i]; x.click(); clock.tick(50); assert.include(x.textContent, "X"); const o = els[i + 3]; o.click(); clock.tick(50); if (i === 2) { assert.notInclude(o.textContent, "O"); } else { assert.include(o.textContent, "O"); } } } catch(e) { console.error(e); throw e; } ``` The game should display a message indicating the winner to be `X` or `O`. ```js // Get to almost winning state // Check dom // Click winning square // Check dom for change await reset(assert); const els = document.querySelectorAll("button.square"); try { for (let i = 0; i < 2; i++) { const x = els[i]; x.click(); clock.tick(50); const o = els[i + 3]; o.click(); clock.tick(50); } const preXWin = getInnerTextExcept("button.square"); // Click winning button for X els[2].click(); clock.tick(50); const postXWin = getInnerTextExcept("button.square"); assert.notEqual(preXWin, postXWin); await reset(assert); for (let i = 0; i < 2; i++) { const x = els[i]; x.click(); clock.tick(50); const o = els[i + 3]; o.click(); clock.tick(50); } els[6].click(); clock.tick(50); const preOWin = getInnerTextExcept("button.square"); // Click winning button for O els[5].click(); clock.tick(50); const postOWin = getInnerTextExcept("button.square"); assert.notEqual(preOWin, postOWin); // There should be a difference between `O` and `X` winning. assert.notEqual(postXWin, postOWin); // Test diagonal win for X await reset(assert); // X moves: top-left els[0].click(); clock.tick(50); // O moves: top-center els[1].click(); clock.tick(50); // X moves: center els[4].click(); clock.tick(50); // O moves: top-right els[2].click(); clock.tick(50); const preDiagonalWin = getInnerTextExcept("button.square"); // X moves: bottom-right (completes diagonal win) els[8].click(); clock.tick(50); const postDiagonalWin = getInnerTextExcept("button.square"); assert.notEqual(preDiagonalWin, postDiagonalWin); assert.include(postDiagonalWin, "Winner: X"); } catch(e) { console.error(e); throw e; } ``` The game should display a message indicating a draw. ```js // Get to almost draw state // Check dom // Click final square // Check dom for change await reset(assert); const els = document.querySelectorAll("button.square"); try { //use already known draw states const drawMoves = [0, 1, 2, 4, 3, 5, 7, 6, 8]; const snapshot1 = getInnerTextExcept("button.square"); els[drawMoves[0]].click(); clock.tick(50); const snapshot2 = getInnerTextExcept("button.square"); for (let i = 1; i < drawMoves.length - 1; i++) { els[drawMoves[i]].click(); clock.tick(50); } const snapshot3 = getInnerTextExcept("button.square"); els[drawMoves[drawMoves.length - 1]].click(); clock.tick(50); const snapshot4 = getInnerTextExcept("button.square"); assert.notEqual(snapshot4, snapshot1); assert.notEqual(snapshot4, snapshot2); assert.notEqual(snapshot4, snapshot3); } catch(e) { console.error(e); throw e; } ``` # --seed-- ## --seed-contents-- ```html