<div id="root">
    <!-- This element's contents will be replaced with your component. -->
</div>
* {
  box-sizing: border-box;
}

body {
  font-family: Arial;
  background: #ffcfdf;
  background: linear-gradient(315deg, #ffcfdf 0%, #b0f3f1 74%);
/*   background: linear-gradient(to top right, rgba(0,0,0,0.2), rgba(0,0,0,0.2)), linear-gradient(315deg, #ffcfdf 0%, #b0f3f1 74%); */
  min-height: 100vh;
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

.game {
  margin: 0 auto;
/*   background: rgba(0,0,0,0.3); */
  padding: 20px;
}

.title {
  text-align: center;
  font-size: 3rem;
  letter-spacing: 2px;
  margin: 0;
  padding-bottom: 30px;
  color: #fff;
  text-shadow: 0 0 2px black;
}

.help {
  color: #333;
  margin: 20px 0;
  text-align: center;
}

.body {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}

.left, .right {
  text-align: center;
  padding: 20px 0;
  width: 250px;
  height: 240px;
  border: thin solid #ddd;
  background: white;
}

.star {
  display: inline-block;
  margin: 0 15px;
}

.star:after {
  content: "\2605";
  font-size: 45px;
}

.number {
  background-color: #e5e7eb;
  color: #fff;
  border: none;
  width: 45px;
  height: 45px;
  margin: 10px;
  font-size: 1.5rem;
}

.number:focus, .number:active {
  outline: none;
  border: 1px solid #aaa;
}

.game-done {
  position: relative;
  top: 30px;
}

.game-done .message {
  font-size: 2rem;
  margin: 15px;
}

.game-done button {
  background-color: #e5e7eb;
  color: #fff;
  border: none;
  height: 45px;
  padding: 0 20px;
  font-size: 1.2rem;
  cursor: pointer;
}

.game-done button:hover {
  background-color: #ccc;
}

.game-done button:focus, .game-done button:active {
  outline: none;
  border: 1px solid #aaa;
}

.timer {
  color: #333;
  margin: 20px 0;
  text-align: center;
}

footer {
  padding: 20px 0;
  text-align: center;
  font-size: 12px;
}

footer a {
  color: #333;
}

footer a:hover {
  color: #000;
}
const { useEffect, useState } = React;

// add clue between star count to make it clearer between stars
// improve design
// add start button

const StarsDisplay = props => (
  <>
    {utils.range(1, props.count).map(starId => (
      <div key={starId} className="star" />
    ))}
  </>
);

const PlayNumber = props => (
  <button
    className="number"
    style={{backgroundColor: colors[props.status]}}
    onClick={() => props.onClick(props.number, props.status)}
  >
    {props.number}
  </button>
);

const PlayAgain = props => (
  <div className="game-done">
    <p 
      className="message"
      style={{ color: colors[props.gameStatus]}}
    >
      {props.gameStatus === 'lost' ? 'Game Over' : 'Nice'}
    </p>
    <button onClick={props.onClick}>Play Again</button>
  </div>
);

const useGameState = timeLimit => {
  const [stars, setStars] = useState(utils.random(1, 9));
  const [availableNums, setAvailableNums] = useState(utils.range(1, 9));
  const [candidateNums, setCandidateNums] = useState([]);
  const [secondsLeft, setSecondsLeft] = useState(10);

  useEffect(() => {
    if (secondsLeft > 0 && availableNums.length > 0) {
      const timerId = setTimeout(() => setSecondsLeft(secondsLeft - 1), 1000);
      return () => clearTimeout(timerId);
    }
  }, [secondsLeft]);

  const setGameState = (newCandidateNums) => {
    if (utils.sum(newCandidateNums) !== stars) {
      setCandidateNums(newCandidateNums);
    } else {
      const newAvailableNums = availableNums.filter(
        n => !newCandidateNums.includes(n)
      );
      setStars(utils.randomSumIn(newAvailableNums, 9));
      setAvailableNums(newAvailableNums);
      setCandidateNums([]);
    }
  };

  return { stars, availableNums, candidateNums, secondsLeft, setGameState };
};

const Game = props => {
  const {
    stars,
    availableNums,
    candidateNums,
    secondsLeft,
    setGameState,
  } = useGameState();

  const candidatesAreWrong = utils.sum(candidateNums) > stars;
  const gameStatus = availableNums.length === 0 
    ? 'won'
    : secondsLeft === 0 ? 'lost' : 'active'

  const numberStatus = number => {
    if (!availableNums.includes(number)) {
      return 'right';
    }

    if (candidateNums.includes(number)) {
      return candidatesAreWrong ? 'wrong' : 'candidate';
    }

    return 'available';
  };

  const onNumberClick = (number, currentStatus) => {
    if (currentStatus === 'right' || secondsLeft === 0) {
      return;
    }

    const newCandidateNums =
      currentStatus === 'available'
        ? candidateNums.concat(number)
        : candidateNums.filter(cn => cn !== number);

    setGameState(newCandidateNums);
  };

  return (
    <div className="game">
      <h1 className="title" 
        style={{ color: colors[gameStatus]}}>
        St★r M★tch
      </h1>
      <p className="help">
        Repeat picking 1+ numbers that sum to the number of stars ★ to use all the numbers in less than 10 seconds
      </p>
      <div className="body">
        <div className="left">
          {gameStatus !== 'active' ? (
            <PlayAgain onClick={props.startNewGame} gameStatus={gameStatus} />
          ) : (
            <StarsDisplay count={stars} />
          )}
        </div>
        <div className="right">
          {utils.range(1, 9).map(number => (
            <PlayNumber
              key={number}
              status={numberStatus(number)}
              number={number}
              onClick={onNumberClick}
            />
          ))}
        </div>
      </div>
      <p className="timer">Time Remaining: {secondsLeft}</p>
      <footer>Created by <a href="https://remybeumier.be" target="_blank" rel="noreferrer">Rémy Beumier</a></footer>
    </div>
  );
};

const StarMatch = () => {
  const [gameId, setGameId] = useState(1);
  return <Game key={gameId} startNewGame={() => setGameId(gameId + 1)}/>;
}

// Color Theme
const colors = {
  available: '#e5e7eb',
  right: '#9AE6B4',
  wrong: '#FEB2B2',
  candidate: '#90CDF4',
  won: '#9AE6B4',
  lost: '#FEB2B2',
};

// Math science
const utils = {
  // Sum an array
  sum: arr => arr.reduce((acc, curr) => acc + curr, 0),

  // create an array of numbers between min and max (edges included)
  range: (min, max) => Array.from({length: max - min + 1}, (_, i) => min + i),

  // pick a random number between min and max (edges included)
  random: (min, max) => min + Math.floor(Math.random() * (max - min + 1)),

  // Given an array of numbers and a max...
  // Pick a random sum (< max) from the set of all available sums in arr
  randomSumIn: (arr, max) => {
    const sets = [[]];
    const sums = [];
    for (let i = 0; i < arr.length; i++) {
      for (let j = 0, len = sets.length; j < len; j++) {
        const candidateSet = sets[j].concat(arr[i]);
        const candidateSum = utils.sum(candidateSet);
        if (candidateSum <= max) {
          sets.push(candidateSet);
          sums.push(candidateSum);
        }
      }
    }
    return sums[utils.random(0, sums.length - 1)];
  },
};

ReactDOM.render(<StarMatch />, document.getElementById('root'));  
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/react@18.3.1/umd/react.production.min.js
  2. https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js