Solo Project 2

Capstone Project #1 - Tenzies

Step-by-Step Guide to Setting Up the Vite + React Project

Project Setup

(Project Name: react-scrimba-tenzies-app)

Step 1: Create the Vite + React Project

  1. Open your terminal and navigate to the directory where you want to create the project.

  2. Run the following command to create a Vite project with React:

    npm create vite@latest react-scrimba-tenzies-app --template react
    • npm create vite@latest: Initializes a new Vite project.

    • react-scrimba-tenzies-app: Specifies the project name.

    • --template react: Specifies the React template for Vite.

  3. Navigate into the project directory:

    cd react-scrimba-tenzies-app
  4. Install dependencies:

    npm install

Step 2: Modify the Project Structure

  1. Inside the src folder, create a new file for the App component:

    src/App.jsx
  2. Move the existing App component code into this file.

  3. Update the main.jsx file to import and render the App component.

Step 3: Implement the App Component

  1. Create a separate App component (src/App.jsx):

    import React from 'react';
    
    function App() {
      return (
        <main>
          {/* Content for Tenzies App will go here */}
        </main>
      );
    }
    
    export default App;
  2. Update src/main.jsx:

    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App';
    
    ReactDOM.createRoot(document.getElementById('root')).render(
      <React.StrictMode>
        <App />
      </React.StrictMode>
    );

Step 4: Add Styling to the Project

  1. Inside the src folder, create a CSS file for global styles:

    src/index.css
  2. Add the following styles to index.css:

    body {
      margin: 0;
      padding: 20px;
      font-family: Arial, sans-serif;
      background-color: #0B2334;
      height: 100vh;
      display: flex;
      flex-direction: column;
    }
    
    div#root {
      height: 100%;
      width: 100%;
      max-height: 400px;
      max-width: 400px
    }
    
    main {
      background-color: #F5F5F5;
      margin: 0;
      mheight: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      border-radius: 5px;
    }
  3. Import the CSS file into main.jsx:

    import './index.css';

Step 5: Test the Setup

  1. Run the development server:

    npm run dev
  2. Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).

  3. You should see a blank page with the main element styled with a white background, centered on a dark blue body.

Additional Notes

  • This setup serves as a starting point for the "Playing Tenzies" app.

  • You can now proceed to implement the game logic and additional components within the App component.

Next Steps

  • Plan the app layout: Determine what components you’ll need for the Tenzies game.

  • Set up state management: Use React's useState and useEffect hooks as necessary.

  • Add interactivity and styles: Implement the core functionality and style the components.

Create a Die child component that takes a 'value' prop

  • It should render a button with that value displayed.

  • render 10 instances of the Die component

    • Provide a number between 1-6 for the value on each row.

  • Style the <main> element and <Die> component : -Create a container to hold the 10 instances of the Die component, and use CSS grid to lay them out evenly in 2 rows of 5 columns.

  • Use flexbox on main to center the dice container in the center of the page.

Step 1: Update the App Component

  1. Create a separate App component (src/App.jsx):

    import React from 'react';
    import Die from './Die';
    
    function App() {
      const diceValues = [1, 2, 3, 4, 5, 6, 1, 2, 3, 4];
    
      return (
        <main>
          <div className="dice-container">
            {diceValues.map((value, index) => (
              <Die key={index} value={value} />
            ))}
          </div>
        </main>
      );
    }
    
    export default App;
  2. Create the Die component (src/Die.jsx):

    import React from 'react';
    
    function Die({ value }) {
      return (
        <button className="die">
          {value}
        </button>
      );
    }
    
    export default Die;

Step 2: Add Styling to the Project

  1. Inside the src folder, update index.css to include styles for the layout and components:

    body {
      margin: 0;
      font-family: Arial, sans-serif;
      background-color: #0B2334;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
    }
    
    main {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 100%;
      height: 100%;
    }
    
    .dice-container {
      display: grid;
      grid-template-columns: repeat(5, 1fr);
      grid-gap: 10px;
      background-color: #F5F5F5;
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    }
    
    .die {
      width: 50px;
      height: 50px;
      font-size: 20px;
      font-weight: bold;
      color: #333;
      background-color: #fff;
      border: 2px solid #ccc;
      border-radius: 8px;
      display: flex;
      justify-content: center;
      align-items: center;
      cursor: pointer;
      transition: all 0.2s ease-in-out;
    }
    
    .die:hover {
      background-color: #ddd;
      transform: scale(1.1);
    }

Step 3: Test the Setup

  1. Run the development server:

    npm run dev
  2. Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).

  3. You should see 10 dice rendered in a grid layout, centered on the page, with values between 1-6.

Additional Notes

  • This setup creates a reusable Die component.

  • You can now proceed to implement the game logic and interactions.

Next Steps

  • Add interactivity: Implement functionality to toggle dice states (e.g., clicked/unselected).

  • Enhance styles: Customize the appearance to match your desired theme.

Write a function (generateNewDice) that returns an array of 10 random numbers between 1-6 inclusive in App.jsx component.

Step 1: Update the App Component

  1. Create a separate App component (src/App.jsx):

    import React from 'react';
    import Die from './Die';
    
    function generateNewDice() {
      return Array.from({ length: 10 }, () => Math.floor(Math.random() * 6) + 1);
    }
    
    function App() {
      const diceValues = generateNewDice(); // generate new dice
    
      return (
        <main>
          <div className="dice-container">
            {diceValues.map((value, index) => (
              <Die key={index} value={value} />
            ))}
          </div>
        </main>
      );
    }
    
    export default App;

Step 2: Test the Setup

  1. Run the development server:

    npm run dev
  2. Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).

  3. You should see 10 dice rendered in a grid layout, centered on the page, with random values between 1-6.

Update the App.jsx component to use the useState hook for managing the array of 10 random dice values generated by the generateNewDice function.

Step 3: Update the App Component

  1. Modify a App component (src/App.jsx):

    import React, { useState } from 'react';
    import Die from './Die';
    
    function generateNewDice() {
      return Array.from({ length: 10 }, () => Math.floor(Math.random() * 6) + 1);
    }
    
    function App() {
      const [diceValues, setDiceValues] = useState(generateNewDice());//use useState
    
      return (
        <main>
          <div className="dice-container">
            {diceValues.map((value, index) => (
              <Die key={index} value={value} />
            ))}
          </div>
        </main>
      );
    }
    
    export default App;

Add the Roll Dice button functionality to the App.jsx component, including a rollDice function to regenerate the dice values and updated button styles

Step 1: Update the App Component

  1. Modify a App component (src/App.jsx):

    import React, { useState } from 'react';
    import Die from './Die';
    
    function generateNewDice() {
      return Array.from({ length: 10 }, () => Math.floor(Math.random() * 6) + 1);
    }
    
    function App() {
      const [diceValues, setDiceValues] = useState(generateNewDice());
    
      function rollDice() {
        setDiceValues(generateNewDice());
      }
    
      return (
        <main>
          <div className="dice-container">
            {diceValues.map((value, index) => (
              <Die key={index} value={value} />
            ))}
          </div>
          <button className="roll-button" onClick={rollDice}>Roll Dice</button>
        </main>
      );
    }
    
    export default App;

Step 2: Add Styling to the Project

  1. Inside the src folder, update index.css to include styles for the layout and components:

    body {
      margin: 0;
      font-family: Arial, sans-serif;
      background-color: #0B2334;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
    }
    
    main {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      width: 100%;
      height: 100%;
    }
    
    .dice-container {
      display: grid;
      grid-template-columns: repeat(5, 1fr);
      grid-gap: 10px;
      background-color: #F5F5F5;
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    }
    
    .die {
      width: 50px;
      height: 50px;
      font-size: 20px;
      font-weight: bold;
      color: #333;
      background-color: #fff;
      border: 2px solid #ccc;
      border-radius: 8px;
      display: flex;
      justify-content: center;
      align-items: center;
      cursor: pointer;
      transition: all 0.2s ease-in-out;
    }
    
    .die:hover {
      background-color: #ddd;
      transform: scale(1.1);
    }
    
    .roll-button {
      margin-top: 20px;
      padding: 10px 20px;
      font-size: 18px;
      font-weight: bold;
      color: #fff;
      background-color: #4CAF50;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      transition: background-color 0.3s ease;
    }
    
    .roll-button:hover {
      background-color: #45a049;
    }

Step 3: Test the Setup

  1. Run the development server:

    npm run dev
  2. Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).

  3. You should see 10 dice rendered in a grid layout, centered on the page, with random values between 1-6. Clicking the "Roll Dice" button generates a new set of random numbers.

Update the array of numbers in state to be an array of objects instead. Each object should look like: { value: , isHeld: false }.

  • Now diceValues is die object to iterate over to Die component.

  • The Die component now receives these as props : value and isHeld

Step 1: Implement the App Component

  1. Create a separate App component (src/App.jsx):

    import React, { useState } from 'react';
    import Die from './Die';
    
    function generateNewDice() {
      return Array.from({ length: 10 }, () => ({ value: Math.floor(Math.random() * 6) + 1, isHeld: false }));
    } // generate dice object with props value and isHeld
    
    function App() {
      const [diceValues, setDiceValues] = useState(generateNewDice());
    
      function rollDice() {
        setDiceValues(generateNewDice());
      }
    
      return (
        <main>
          <div className="dice-container">
            {diceValues.map((die, index) => (
              <Die key={index} value={die.value} isHeld={die.isHeld} />
            ))}
          </div>
          <button className="roll-button" onClick={rollDice}>Roll Dice</button>
        </main>
      );
    }
    
    export default App;
  2. Update the Die component (src/Die.jsx):

    import React from 'react';
    
    function Die({ value, isHeld }) {
      return (
        <button className={`die ${isHeld ? 'held' : ''}`}>
          {value}
        </button>
      );
    }
    
    export default Die;

Add conditional styling to the Die component so that if it's held (isHeld === true), its background color changes to a light green (#59E391)

  • The Die component now has conditional styling, changing its background color to light green (#59E391) if the isHeld property is true.

// Updated App.jsx
import React, { useState } from 'react';
import Die from './Die';

function generateNewDice() {
  return Array.from({ length: 10 }, () => ({
   value: Math.floor(Math.random() * 6) + 1, 
   isHeld: false })
   );
}

function App() {
  const [diceValues, setDiceValues] = useState(generateNewDice());

  function rollDice() {
    setDiceValues(generateNewDice());
  }

  return (
    <main>
      <div className="dice-container">
        {diceValues.map((die, index) => (
          <Die key={index} value={die.value} isHeld={die.isHeld} />
        ))}
      </div>
      <button className="roll-button" onClick={rollDice}>Roll Dice</button>
    </main>
  );
}

export default App; code

Step 2: Test the Setup

  1. Run the development server:

    npm run dev
  2. Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).

  3. You should see 10 dice rendered in a grid layout, centered on the page, with random values between 1-6. Clicking the "Roll Dice" button generates a new set of random dice objects. If a die is marked as held, its background color changes to light green (#59E391).

Create a function hold that takes id as a parameter at App.jsx parent component. Then, implement how to pass that function down to each instance of the Die component so when each one is clicked, it logs its own unique ID property.

  • The hold function has been added to the App.jsx component, and it logs the unique id of each die when clicked. This function is passed as a prop to the Die components, which call it on click.

dice state at each Die component
dice state is held in the parent by hold function(id)
// Updated App.jsx
import React, { useState } from 'react';
import Die from './Die';

function generateNewDice() {
  return Array.from({ length: 10 }, (_, index) => ({ 
    id: index, 
    value: Math.floor(Math.random() * 6) + 1, 
    isHeld: false 
  }));
}

function App() {
  const [diceValues, setDiceValues] = useState(generateNewDice());

  function rollDice() {
    setDiceValues(generateNewDice());
  }

  function hold(id) {
    console.log(`Die with ID ${id} clicked`);
  }

  return (
    <main>
      <div className="dice-container">
        {diceValues.map((die) => (
          <Die 
            key={die.id} 
            id={die.id} 
            value={die.value} 
            isHeld={die.isHeld} 
            hold={hold} 
          />
        ))}
      </div>
      <button className="roll-button" onClick={rollDice}>Roll Dice</button>
    </main>
  );
}

export default App;

Update the hold function to flip the isHeld property on the object in the array that was clicked, based on the id prop passed into the function.

  • The hold function now toggles the isHeld property for the clicked die, updating the state based on its id. Clicking a die will visually indicate its held state by changing its background color.

Step 1: Update the App Component

  1. Modify a App component (src/App.jsx):

// Updated App.jsx
import React, { useState } from 'react';
import Die from './Die';

function generateNewDice() {
  return Array.from({ length: 10 }, (_, index) => ({ 
    id: index, 
    value: Math.floor(Math.random() * 6) + 1, 
    isHeld: false 
  }));
}

function App() {
  const [diceValues, setDiceValues] = useState(generateNewDice());

  function rollDice() {
    setDiceValues(generateNewDice());
  }

  function hold(id) {
    setDiceValues((prevDiceValues) => 
      prevDiceValues.map((die) => 
        die.id === id ? { ...die, isHeld: !die.isHeld } : die
      )
    );
  } //isHeld is now toggled

  return (
    <main>
      <div className="dice-container">
        {diceValues.map((die) => (
          <Die 
            key={die.id} 
            id={die.id} 
            value={die.value} 
            isHeld={die.isHeld} 
            hold={hold} 
          />
        ))}
      </div>
      <button className="roll-button" onClick={rollDice}>Roll Dice</button>
    </main>
  );
}

export default App;

Update the rollDice function to not just roll all new dice, but instead to look through the existing dice to NOT role any that are being held.

  • The rollDice function has been updated to preserve the isHeld state for dice that are held, re-rolling only those that are not.

Step 1: Update the App Component

  1. Modify a App component (src/App.jsx):

// Updated App.jsx
import React, { useState } from 'react';
import Die from './Die';

function generateNewDice() {
  return Array.from({ length: 10 }, (_, index) => ({ 
    id: index, 
    value: Math.floor(Math.random() * 6) + 1, 
    isHeld: false 
  }));
}

function App() {
  const [diceValues, setDiceValues] = useState(generateNewDice());

  function rollDice() {
    setDiceValues((prevDiceValues) => 
      prevDiceValues.map((die) => 
        die.isHeld ? die : { ...die, value: Math.floor(Math.random() * 6) + 1 }
      )
    );
  }

  function hold(id) {
    setDiceValues((prevDiceValues) => 
      prevDiceValues.map((die) => 
        die.id === id ? { ...die, isHeld: !die.isHeld } : die
      )
    );
  }

  return (
    <main>
      <div className="dice-container">
        {diceValues.map((die) => (
          <Die 
            key={die.id} 
            id={die.id} 
            value={die.value} 
            isHeld={die.isHeld} 
            hold={hold} 
          />
        ))}
      </div>
      <button className="roll-button" onClick={rollDice}>Roll Dice</button>
    </main>
  );
}

export default App;

Step 2. Add Instructions & Styles

// App.jsp code snippet

<main>
   <h1 className="title">Play Tenzies</h1>

   <p className="instructions">Roll until all dice are the same. 
   Click each die to freeze it at its current value between rolls.</p>

   ...
</main>
// App.css snippet
.instructions {
    font-family: 'Inter', sans-serif;
    font-weight: 400;
    margin-top: 0;
    text-align: center;
}

Step 3: Test the Setup

  1. Run the development server:

    npm run dev
  2. Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).

  3. You should see 10 dice rendered in a grid layout, centered on the page. Clicking any die toggles its isHeld state, and clicking the "Roll Dice" button only re-rolls dice that are not held.

We want to indicate to the user that the game is over if (1) all the dice are held, and (2) all the dice have the same value. We need to save a gameWon value in state.

  • The game now checks if all dice are held and have the same value, marking the game as won with a gameWon state.

  • When the game is won, a "You won!" message appears, and the button changes to "New Game."

Step 1: Update the App Component

  1. Modify a App component (src/App.jsx):

  • With useState & useEffect : The game now checks if all dice are held and have the same value, marking the game as won with a gameWon state. When the game is won, a "You won!" message appears, and the button changes to "New Game."

  • Without useState & useEffect : The App.jsx has been updated to indicate the game is over if all dice are held and have the same value, without using useState or useEffect for the game over logic.

// App.jsx with useState and useEffect
import React, { useState, useEffect } from 'react';
import Die from './Die';

function generateNewDice() {
  return Array.from({ length: 10 }, (_, index) => ({ 
    id: index, 
    value: Math.floor(Math.random() * 6) + 1, 
    isHeld: false 
  }));
}

function App() {
  const [diceValues, setDiceValues] = useState(generateNewDice());
  const [gameWon, setGameWon] = useState(false); // ameWon state

  useEffect(() => {
    const allHeld = diceValues.every((die) => die.isHeld);
    const allSameValue = diceValues.every((die) => die.value === diceValues[0].value);
    if (allHeld && allSameValue) {
      setGameWon(true);
    }
  }, [diceValues]);

  function rollDice() {
    if (!gameWon) { // not gameWon
      setDiceValues((prevDiceValues) => 
        prevDiceValues.map((die) => 
          die.isHeld ? die : { ...die, value: Math.floor(Math.random() * 6) + 1 }
        )
      );
    }
  }

  function hold(id) {
    setDiceValues((prevDiceValues) => 
      prevDiceValues.map((die) => 
        die.id === id ? { ...die, isHeld: !die.isHeld } : die
      )
    );
  }

  return (
    <main>
      {gameWon && <h1 className="blinking-text game-won">You won!</h1>}
      <div className="dice-container">
        {diceValues.map((die) => (
          <Die 
            key={die.id} 
            id={die.id} 
            value={die.value} 
            isHeld={die.isHeld} 
            hold={hold} 
          />
        ))}
      </div>
      <button className="roll-button" onClick={rollDice}>
        {gameWon ? "New Game" : "Roll Dice"}
      </button>
    </main>
  );
}

export default App;

Step 2: Add Styling to the Project

  1. Inside the src folder, update index.css to include styles for the layout and components:

// Update App.css 

.game-won {
  font-size: 24px;
  color: #fff;
  margin-bottom: 20px;
}

@keyframes blink {
  0% {
    opacity: 1;
  }

  50% {
    opacity: 0;
  }

  100% {
    opacity: 1;
  }
}

.blinking-text {
  text-align: center;
  margin-top: 20%;
  font-size: 2.8rem;
  font-weight: bold;
  color: green;
  animation: blink 1s infinite;
}

Add a feature to make the confetti drop when the game is won!

Step 1: Update the App Component

  1. Install react-confetti

    npm install react-confetti
  2. Update the App Component (src/App.jsx):

    import React, { useState } from 'react';
    import Die from './Die';
    import Confetti from 'react-confetti'; //import react-confetti package
    
    function generateNewDice() {
      return Array.from({ length: 10 }, (_, index) => ({ 
        id: index, 
        value: Math.floor(Math.random() * 6) + 1, 
        isHeld: false 
      }));
    }
    
    function App() {
      const [diceValues, setDiceValues] = useState(generateNewDice());
    
      function rollDice() {
        setDiceValues((prevDiceValues) => 
          prevDiceValues.map((die) => 
            die.isHeld ? die : { ...die, value: Math.floor(Math.random() * 6) + 1 }
          )
        );
      }
    
      function hold(id) {
        setDiceValues((prevDiceValues) => 
          prevDiceValues.map((die) => 
            die.id === id ? { ...die, isHeld: !die.isHeld } : die
          )
        );
      }
    
      // Check for game over condition without using useState or useEffect
      const allHeld = diceValues.every((die) => die.isHeld);
      const allSameValue = diceValues.every((die) => die.value === diceValues[0].value);
      const gameWon = allHeld && allSameValue;
    
      return (
        <main>
          {gameWon && <Confetti />}
          {gameWon && <h1 className="game-won">You won!</h1>}
          <div className="dice-container">
            {diceValues.map((die) => (
              <Die 
                key={die.id} 
                id={die.id} 
                value={die.value} 
                isHeld={die.isHeld} 
                hold={hold} 
              />
            ))}
          </div>
          <button className="roll-button" onClick={() => setDiceValues(generateNewDice())}>
            {gameWon ? "New Game" : "Roll Dice"}
          </button>
        </main>
      );
    }
    
    export default App;

Step 2: Test the Setup

  1. Run the development server:

    npm run dev
  2. Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).

  3. You should see 10 dice rendered in a grid layout, centered on the page. If all the dice are held and have the same value, a "You won!" message is displayed, a confetti animation drops, and the "Roll Dice" button changes to "New Game."

Allow the user to play a new game when the button is clicked.

Step 1: Implement the App Component

  • The "Roll Dice" button now allows the user to start a new game when the game is won, by generating a fresh set of dice.

  1. Update the App Component (src/App.jsx):

    import React, { useState } from 'react';
    import Die from './Die';
    import Confetti from 'react-confetti';
    
    function generateNewDice() {
      return Array.from({ length: 10 }, (_, index) => ({ 
        id: index, 
        value: Math.floor(Math.random() * 6) + 1, 
        isHeld: false 
      }));
    }
    
    function App() {
      const [diceValues, setDiceValues] = useState(()=>generateNewDice());
    
      function rollDice() {
        if (gameWon) { //add conditional 
          setDiceValues(generateNewDice());
        } else {
          setDiceValues((prevDiceValues) => 
            prevDiceValues.map((die) => 
              die.isHeld ? die : { ...die, value: Math.floor(Math.random() * 6) + 1 }
            )
          );
        }
      }
    
      function hold(id) {
        setDiceValues((prevDiceValues) => 
          prevDiceValues.map((die) => 
            die.id === id ? { ...die, isHeld: !die.isHeld } : die
          )
        );
      }
    
      // Check for game over condition without using useState or useEffect
      const allHeld = diceValues.every((die) => die.isHeld);
      const allSameValue = diceValues.every((die) => die.value === diceValues[0].value);
      const gameWon = allHeld && allSameValue;
    
      return (
        <main>
          {gameWon && <Confetti />}
          {gameWon && <h1 className="game-won">You won!</h1>}
          <div className="dice-container">
            {diceValues.map((die) => (
              <Die 
                key={die.id} 
                id={die.id} 
                value={die.value} 
                isHeld={die.isHeld} 
                hold={hold} 
              />
            ))}
          </div>
          <button className="roll-button" onClick={rollDice}>
            {gameWon ? "New Game" : "Roll Dice"}
          </button>
        </main>
      );
    }
    
    export default App;

Step 2: Test the Setup

  1. Run the development server:

    npm run dev
  2. Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).

  3. You should see 10 dice rendered in a grid layout, centered on the page. If all the dice are held and have the same value, a "You won!" message is displayed, a confetti animation drops, and the "Roll Dice" button changes to "New Game."

There is an issue on App.jsx that allow holding different dice number combination . The hold function should keep tract not only same "isHeld" flag but also should not allow selection and hold for different dice number. Only the same dice numbers are kept and tracked. Modify the code.

  • The hold function has been updated to ensure that only dice with the same value as the first held die can be held.

  1. Update the hold function in App Component (src/App.jsx):

    import React, { useState } from 'react';
    import Die from './Die';
    import Confetti from 'react-confetti';
    
    function generateNewDice() {
      return Array.from({ length: 10 }, (_, index) => ({ 
        id: index, 
        value: Math.floor(Math.random() * 6) + 1, 
        isHeld: false 
      }));
    }
    
    function App() {
      const [diceValues, setDiceValues] = useState(generateNewDice());
    
      function rollDice() {
        if (gameWon) {
          setDiceValues(generateNewDice());
        } else {
          setDiceValues((prevDiceValues) => 
            prevDiceValues.map((die) => 
              die.isHeld ? die : { ...die, value: Math.floor(Math.random() * 6) + 1 }
            )
          );
        }
      }
    
    //The hold function has been updated to ensure that only dice 
    //with the same value as the first held die can be held.
      function hold(id) { //updated - 
        const firstHeldDie = diceValues.find((die) => die.isHeld);
        setDiceValues((prevDiceValues) => 
          prevDiceValues.map((die) => {
            if (die.id === id) {
              if (!firstHeldDie || firstHeldDie.value === die.value) {
                return { ...die, isHeld: !die.isHeld };
              }
            }
            return die;
          })
        );
      }
    
      // Check for game over condition without using useState or useEffect
      const allHeld = diceValues.every((die) => die.isHeld);
      const allSameValue = diceValues.every((die) => die.value === diceValues[0].value);
      const gameWon = allHeld && allSameValue;
    
      return (
        <main>
          {gameWon && <Confetti />}
          {gameWon && <h1 className="game-won">You won!</h1>}
          <div className="dice-container">
            {diceValues.map((die) => (
              <Die 
                key={die.id} 
                id={die.id} 
                value={die.value} 
                isHeld={die.isHeld} 
                hold={hold} 
              />
            ))}
          </div>
          <button className="roll-button" onClick={rollDice}>
            {gameWon ? "New Game" : "Roll Dice"}
          </button>
        </main>
      );
    }
    
    export default App;

Result (Outro)

Challenge: Add Timer Feature to Tenzie Game

Add the timer feature to the game. It updates every second while the game is active and resets when a new game begins. The timer is displayed bellow the "Roll Dice" button.

The timer functionality ensures it stops when the game ends and resumes when a new game starts.

The program updates are needed in the function rollDice() conditioned on gameWon . It needs useEffect to handle the game timer.

// Some code
import React, { useState, useEffect } from 'react'
import Die from './Die'
import Confetti from 'react-confetti'
import './App.css'

function generateNewDice() {
  return Array.from({ length: 10 }, (_, index) => ({
    id: index,
    value: Math.floor(Math.random() * 6) + 1,
    isHeld: false,
  }))
}

function AppTenzies() {
  const [diceValues, setDiceValues] = useState(() => generateNewDice())
  const [timer, setTimer] = useState(0)
  const [isGameActive, setIsGameActive] = useState(false)

  // Check for game over condition without using useState or useEffect
  const allHeld = diceValues.every((die) => die.isHeld)
  const allSameValue = diceValues.every(
    (die) => die.value === diceValues[0].value,
  )
  const gameWon = allHeld && allSameValue

  useEffect(() => {
    let interval
    if (isGameActive && !gameWon) {
      interval = setInterval(() => {
        setTimer((prevTimer) => prevTimer + 1)
      }, 1000)
    }
    return () => clearInterval(interval)
  }, [isGameActive, gameWon])

  function rollDice() {
    if (gameWon) {
      setDiceValues(generateNewDice())
      setTimer(0)
      setIsGameActive(false)
    } else {
      setIsGameActive(true)
      setDiceValues((prevDiceValues) =>
        prevDiceValues.map((die) =>
          die.isHeld
            ? die
            : { ...die, value: Math.floor(Math.random() * 6) + 1 },
        ),
      )
    }
  }

  function hold(id) {
    const firstHeldDie = diceValues.find((die) => die.isHeld)
    setDiceValues((prevDiceValues) =>
      prevDiceValues.map((die) => {
        if (die.id === id) {
          if (!firstHeldDie || firstHeldDie.value === die.value) {
            return { ...die, isHeld: !die.isHeld }
          }
        }
        return die
      }),
    )
  }

  return (
    <main>
      {gameWon && <Confetti />}
      {gameWon && <h1 className="blinking-text game-won">You won!</h1>}

      <h1 className="title">Play Tenzies</h1>
      <p className="instructions">
        Roll until all dice are the same. Click each die to freeze it at its
        current value between rolls.
      </p>

      <div className="dice-container">
        {diceValues.map((die) => (
          <Die
            key={die.id}
            id={die.id}
            value={die.value}
            isHeld={die.isHeld}
            hold={hold}
          />
        ))}
      </div>
      <button className="roll-button" onClick={rollDice}>
        {gameWon ? 'New Game' : 'Roll Dice'}
      </button>

      <p className="timer">Time: {timer} seconds</p>
    </main>
  )
}

export default AppTenzies

Code Explanation

  1. Purpose of useEffect:

    • This useEffect sets up a side effect to handle the game timer.

    • It ensures that the timer starts or continues only when the game is active and stops when the game ends.

  2. Dependency Array:

    • [isGameActive, gameWon] is the dependency array. The effect re-runs whenever:

      • The game becomes active (isGameActive changes to true).

      • The game is won (gameWon changes to true).

  3. Logic Inside useEffect:

  • If isGameActive is true and gameWon is false:

    • A setInterval is created that updates the timer state every second (1000ms).

  • When the game ends (either isGameActive becomes false or gameWon becomes true), the interval is cleared, stopping the timer.

  1. setInterval and Cleanup:

  • setInterval: Creates a repeating task that increments the timer by 1 second (setTimer((prevTimer) => prevTimer + 1)).

  • Cleanup Function:

    • return () => clearInterval(interval);

    • Ensures that the interval is cleared whenever the effect is re-executed or the component unmounts. This prevents multiple intervals from running simultaneously, which could cause memory leaks or unexpected behavior.

  1. Why Include gameWon in the Dependency Array?

  • When the game is won (gameWon becomes true), the timer must stop immediately. Including gameWon ensures the effect re-runs and clears the interval as soon as the game is over.

  1. How It Works During the Game:

  • When the game starts (isGameActive becomes true), the timer begins to increment every second.

  • When the game ends (either manually by winning the game or resetting the game), the interval is cleared to stop the timer.

  • Clicking "New Game" resets the timer to 0 and reactivates the timer with a fresh interval.

Last updated