#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

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.