Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).
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.
Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).
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
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;
Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).
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.
// 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;
// Some code
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
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;
// Die.jsx - not needed to be updated
import React from 'react';
function Die({ id, value, isHeld, hold }) {
const styles = {
backgroundColor: isHeld ? '#59E391' : '#fff',
};
return (
<button
className="die"
style={styles}
onClick={() => hold(id)}
>
{value}
</button>
);
}
export default Die;
// 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>
Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).
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
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.
Add a feature to make the confetti drop when the game is won!
Step 1: Update the App Component
Install react-confetti
npm install react-confetti
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
Run the development server:
npm run dev
Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).
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.
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
Run the development server:
npm run dev
Open your browser and navigate to the URL provided by the terminal (usually http://localhost:5173).
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.
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
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.
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).
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.
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.
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.
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.
dice state at each Die component
dice state is held in the parent by hold function(id)