diff --git a/client/i18n/locales/english/intro.json b/client/i18n/locales/english/intro.json index 2c3dbb6bbc8..550cfa88408 100644 --- a/client/i18n/locales/english/intro.json +++ b/client/i18n/locales/english/intro.json @@ -3560,9 +3560,12 @@ "In these lecture videos, you will learn about routing in React, React frameworks, and dependency management tools." ] }, - "vyzp": { - "title": "281", - "intro": [] + "lab-tic-tac-toe": { + "title": "Build a Tic-Tac-Toe Game", + "intro": [ + "In this lab, you'll build a Tic-Tac-Toe game using React.", + "You'll practice managing state, handling user interactions, and updating the UI dynamically." + ] }, "lecture-react-strategies-and-debugging": { "title": "React Strategies and Debugging", diff --git a/client/src/pages/learn/full-stack-developer/lab-tic-tac-toe/index.md b/client/src/pages/learn/full-stack-developer/lab-tic-tac-toe/index.md new file mode 100644 index 00000000000..4e142b76455 --- /dev/null +++ b/client/src/pages/learn/full-stack-developer/lab-tic-tac-toe/index.md @@ -0,0 +1,9 @@ +--- +title: Introduction to the Build a Tic-Tac-Toe Game +block: lab-tic-tac-toe +superBlock: full-stack-developer +--- + +## Introduction to the Build a Tic-Tac-Toe Game + +In this lab, you'll build a Tic-Tac-Toe game using React. You'll practice managing state, handling user interactions, and updating the UI dynamically. diff --git a/curriculum/challenges/_meta/lab-tic-tac-toe/meta.json b/curriculum/challenges/_meta/lab-tic-tac-toe/meta.json new file mode 100644 index 00000000000..1cd2952a19d --- /dev/null +++ b/curriculum/challenges/_meta/lab-tic-tac-toe/meta.json @@ -0,0 +1,16 @@ +{ + "name": "Build a Tic-Tac-Toe Game", + "usesMultifileEditor": true, + "dashedName": "lab-tic-tac-toe", + "superBlock": "full-stack-developer", + "challengeOrder": [ + { + "id": "67e3a6b7f60b4085588189e6", + "title": "Build a Tic-Tac-Toe Game" + } + ], + "helpCategory": "JavaScript", + "isUpcomingChange": false, + "blockLayout": "link", + "blockType": "lab" +} diff --git a/curriculum/challenges/english/25-front-end-development/lab-tic-tac-toe/67e3a6b7f60b4085588189e6.md b/curriculum/challenges/english/25-front-end-development/lab-tic-tac-toe/67e3a6b7f60b4085588189e6.md new file mode 100644 index 00000000000..e2631551e65 --- /dev/null +++ b/curriculum/challenges/english/25-front-end-development/lab-tic-tac-toe/67e3a6b7f60b4085588189e6.md @@ -0,0 +1,543 @@ +--- +id: 67e3a6b7f60b4085588189e6 +title: Build a Tic-Tac-Toe Game +challengeType: 25 +dashedName: build-a-tic-tac-toe-game +demoType: onClick +--- + +# --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. + +# --hints-- + +You should export a `Board` component. + +```js +async () => { + 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 +async () => { + const el = document.querySelector("button.square"); + el.click(); + + await delay(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 +async () => { + // NOTE: This test is intentionally high-up, because the latter tests rely on the functionality. + const el = document.querySelector("button.square"); + el.click(); + await delay(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 +async () => { + await reset(assert); + const els = document.querySelectorAll("button.square"); + els[0].click(); + await delay(50); + els[1].click(); + + await delay(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 +async () => { + 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(); + await delay(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 +async () => { + 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(); + await delay(50); + els[wrappedI].click(); + await delay(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 +async () => { + 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(); + await delay(50); + assert.include(x.textContent, "X"); + + const o = els[i + 3]; + o.click(); + await delay(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 +async () => { + // 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(); + await delay(50); + const o = els[i + 3]; + o.click(); + await delay(50); + } + + const preXWin = getInnerTextExcept("button.square"); + // Click winning button for X + els[2].click(); + await delay(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(); + await delay(50); + const o = els[i + 3]; + o.click(); + await delay(50); + } + els[6].click(); + await delay(50); + + const preOWin = getInnerTextExcept("button.square"); + // Click winning button for O + els[5].click(); + await delay(50); + + const postOWin = getInnerTextExcept("button.square"); + assert.notEqual(preOWin, postOWin); + // There should be a difference between `O` and `X` winning. + assert.notEqual(postXWin, postOWin); + } catch(e) { + console.error(e); + throw e; + } +} +``` + +The game should display a message indicating a draw. + +```js +async () => { + // Get to almost draw state + // Check dom + // Click final square + // Check dom for change + await reset(assert); + const els = document.querySelectorAll("button.square"); + try { + for (let i = 3; i < els.length + 2; i++) { + const wrappedI = i % els.length; + els[wrappedI].click(); + await delay(50); + } + + const pre = getInnerTextExcept("button.square"); + els[2].click(); + await delay(50); + + const post = getInnerTextExcept("button.square"); + assert.notEqual(pre, post); + } catch(e) { + console.error(e); + throw e; + } +} +``` + +# --before-all-- + +```js +async function delay(time) { + return new Promise((resolve) => setTimeout(resolve, time)); +} + +async function reset(assert) { + const reset = document.querySelector("#reset"); + assert.exists(reset, "button#reset should exist"); + reset.click(); + await delay(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; +} +``` + +# --seed-- + +## --seed-contents-- + +```html + + + +
+ +