#app
View Compiled
* {
box-sizing: border-box;
}
:root {
--control: #e69119;
--color: #fff;
}
button {
background: var(--control);
color: var(--color);
padding: 1rem 2rem;
border-radius: 1rem;
border: 4px solid var(--color);
font-family: 'Fredoka One', cursive;
font-size: 1.2rem;
}
body {
background: linear-gradient(#d1e9fa 0 40%, #86c270 40%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Fredoka One', cursive;
}
.end-game {
position: fixed;
top: 1rem;
right: 1rem;
}
.mole {
display: none;
background: url(https://assets.codepen.io/605876/mole-out-of-hole.svg);
background-size: cover;
height: 100%;
width: 100%;
border: 0;
}
.mole-hole {
height: 100%;
width: 100%;
overflow: hidden;
border-bottom: 2vmin solid hsl(35, 50%, 15%);
}
.moles {
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(3, 20vmin);
grid-template-rows: repeat(2, 20vmin);
align-items: center;
}
.moles > *:nth-of-type(4),
.moles > *:nth-of-type(5) {
transform: translate(50%, 0)
}
.info {
position: fixed;
top: 1rem;
left: 1rem;
}
.info-text {
font-size: 2rem;
}
h1 {
font-size: 4rem;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
import React, { Fragment, useEffect, useRef, useState } from 'https://cdn.skypack.dev/react'
import { render } from 'https://cdn.skypack.dev/react-dom'
import gsap from 'https://cdn.skypack.dev/gsap'
const TIME_LIMIT = 30000
const MOLE_SCORE = 100
const NUMBER_OF_MOLES = 5
const POINTS_MULTIPLIER = 0.9
const TIME_MULTIPLIER = 1.25
const generateMoles = amount => new Array(amount).fill().map(() => ({
speed: gsap.utils.random(0.5, 1),
delay: gsap.utils.random(0.5, 4),
points: MOLE_SCORE
}))
const usePersistentState = (key, initialValue) => {
const [state, setState] = useState(
window.localStorage.getItem(key)
? JSON.parse(window.localStorage.getItem(key))
: initialValue
)
useEffect(() => {
window.localStorage.setItem(key, state)
}, [key, state])
return [state, setState]
}
const Moles = ({ children }) => <div className="moles">{children}</div>
const Mole = ({ onWhack, points, delay, speed, pointsMin = 10 }) => {
const [whacked, setWhacked] = useState(false)
const bobRef = useRef(null)
const pointsRef = useRef(points)
const buttonRef = useRef(null)
useEffect(() => {
gsap.set(buttonRef.current, {
yPercent: 100,
display: 'block'
})
bobRef.current = gsap.to(buttonRef.current, {
yPercent: 0,
duration: speed,
yoyo: true,
repeat: -1,
delay: delay,
repeatDelay: delay,
onRepeat: () => {
pointsRef.current = Math.floor(
Math.max(pointsRef.current * POINTS_MULTIPLIER, pointsMin)
)
},
})
return () => {
if (bobRef.current) bobRef.current.kill()
}
}, [pointsMin, delay, speed])
useEffect(() => {
if (whacked) {
pointsRef.current = points
bobRef.current.pause()
gsap.to(buttonRef.current, {
yPercent: 100,
duration: 0.1,
onComplete: () => {
gsap.delayedCall(gsap.utils.random(1, 3), () => {
setWhacked(false)
bobRef.current
.restart()
.timeScale(bobRef.current.timeScale() * TIME_MULTIPLIER)
})
},
})
}
}, [whacked])
const whack = () => {
setWhacked(true)
onWhack(pointsRef.current)
}
return (
<div className="mole-hole">
<button
className="mole"
ref={buttonRef}
onClick={whack}
>
<span className="sr-only">Whack</span>
</button>
</div>
)
}
const Score = ({ value }) => <div className='info-text'>{`Score: ${value}`}</div>
const Timer = ({ time, interval = 1000, onEnd }) => {
const [internalTime, setInternalTime] = useState(time)
const timerRef = useRef(time)
const timeRef = useRef(time)
useEffect(() => {
if (internalTime === 0 && onEnd) {
onEnd()
}
}, [internalTime, onEnd])
useEffect(() => {
timerRef.current = setInterval(
() => setInternalTime((timeRef.current -= interval)),
interval
)
return () => {
clearInterval(timerRef.current)
}
}, [interval])
return <div className='info-text'>{`Time: ${internalTime / 1000}s`}</div>
}
const Game = () => {
const [playing, setPlaying] = useState(false)
const [finished, setFinished] = useState(false)
const [score, setScore] = useState(0)
const [highScore, setHighScore] = usePersistentState('whac-a-mole-hi', 0)
const [newHighScore, setNewHighScore] = useState(false)
const [moles, setMoles] = useState(generateMoles(NUMBER_OF_MOLES))
const onWhack = points => setScore(score + points)
const endGame = () => {
setPlaying(false)
setFinished(true)
if (score > highScore) {
setHighScore(score)
setNewHighScore(true)
}
}
const startGame = () => {
setScore(0)
setPlaying(true)
setFinished(false)
setNewHighScore(false)
}
return (
<Fragment>
{!playing && !finished &&
<Fragment>
<h1>Whac a Mole</h1>
<button onClick={startGame}>Start Game</button>
</Fragment>
}
{playing &&
<Fragment>
<button
className="end-game"
onClick={endGame}
>
End Game
</button>
<div className="info">
<Score value={score} />
<Timer
time={TIME_LIMIT}
onEnd={endGame}
/>
</div>
<Moles>
{moles.map(({delay, speed, points}, index) => (
<Mole
key={index}
onWhack={onWhack}
points={points}
delay={delay}
speed={speed}
/>
))}
</Moles>
</Fragment>
}
{finished &&
<Fragment>
{newHighScore && <div className="info-text">NEW High Score!</div>}
<Score value={score} />
<button onClick={startGame}>Play Again</button>
</Fragment>
}
</Fragment>
)
}
render(<Game/>, document.getElementById('app'))
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.