<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
This Pen doesn't use any external CSS resources.