#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;
}

.mole-hole {
  overflow: hidden;
}

.moles {
  display: grid;
  grid-gap: 1rem;
  grid-template-columns: repeat(3, auto);
  grid-template-rows: repeat(2, auto);
  align-items: center;
}

.moles > *:nth-of-type(4),
.moles > *:nth-of-type(5) {
  transform: translate(50%, 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 Moles = ({ children }) => <div className="moles">{children}</div>
const Mole = ({ onWhack }) => {
  const buttonRef = useRef(null)
  useEffect(() => {
    gsap.set(buttonRef.current, {
      yPercent: 100,
      display: 'block'
    })
    gsap.to(buttonRef.current, {
      yPercent: 0,
      yoyo: true,
      repeat: -1,
    })
  }, [])
  return (
    <div className="mole-hole">
      <button className="mole" ref={buttonRef} onClick={() => onWhack(MOLE_SCORE)}>Mole</button>
    </div>
  )
}
const Score = ({ value }) => <div>{`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>{`Time: ${internalTime / 1000}s`}</div>
}

const Game = () => {
  const [playing, setPlaying] = useState(false)
  const [finished, setFinished] = useState(false)
  const [score, setScore] = useState(0)
  
  const onWhack = points => setScore(score + points)
    
  const endGame = () => {
    setPlaying(false)
    setFinished(true)
  }

  const startGame = () => {
    setScore(0)
    setPlaying(true)
    setFinished(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>
          <Score value={score} />
          <Timer
            time={TIME_LIMIT}
            onEnd={endGame}
          />
          <Moles>
            {new Array(NUMBER_OF_MOLES).fill().map((_, index) => (
              <Mole key={index} onWhack={onWhack} />
            ))}
          </Moles>
        </Fragment>
      }
      {finished && 
        <Fragment>
          <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.