본문 바로가기

프론트엔드/자바스크립트

프로젝트 - 계빨 게임 (Math Sprint)

반응형

가장 짧은 시간 안에 주어진 문제의 답이 맞는지 맞추는 게임입니다.

 


 

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Math Sprint</title>

  <script src="https://kit.fontawesome.com/833be080c6.js" crossorigin="anonymous"></script>
  <link rel="stylesheet" href="./main.css">
  <script defer type="module" src="./index.js"></script>
</head>

<body>
  <!-- Main container -->
  <div class="card">

    <!-- Title -->
    <h1 class="title">math sprint</h1>

    <!-- Stage 1 -->
    <div class="contents">
      <input id="1" name="choice" type="radio" value="10">
      <label for="1">&nbsp;&nbsp;&nbsp;10 questions
        <small>best score: <span class="best-record"></span>''</small>
      </label>
      <input id="2" name="choice" type="radio" value="40">
      <label for="2">&nbsp;&nbsp;&nbsp;40 questions
        <small>best score: <span class="best-record"></span>''</small>
      </label>
      <input id="3" name="choice" type="radio" value="70">
      <label for="3">&nbsp;&nbsp;&nbsp;70 questions
        <small>best score: <span class="best-record"></span>''</small>
      </label>
      <input id="4" name="choice" type="radio" value="120">
      <label for="4">120 questions
        <small>best score: <span class="best-record"></span>''</small>
      </label>
    </div>

    <!-- Countdown -->
    <div class="contents hide">
      <div class="count">
        <div class="num num1">1</div>
        <div class="num num2">2</div>
        <div class="num num3">3</div>
        <div class="num num4">Go</div>
      </div>
    </div>

    <!-- Stage 2 -->
    <div class="contents hide question-box">
      <div class="current-question"></div>
      <div class="question">1</div>
      <div class="question">1</div>
      <div class="question">1</div>
      <div class="question">1</div>
    </div>

    <!-- Stage 3 -->
    <div class="contents hide">
      <p class="result">YOUR TIME</p>
      <h1><span class="result-time"></span>''</h1>
      <p class="result">base time: <span class="result-base"></span>''</p>
      <p class="result">penalty: <span class="result-penalty"></span>''</p>
    </div>

    <!-- Buttons -->
    <div class="btn-container">
      <button class="btn btn-main">start</button>
      <button class="btn btn-wrong" hidden>wrong</button>
      <button class="btn btn-correct" hidden>correct</button>
      <button class="btn btn-reset" hidden>try again</button>
    </div>
  </div>
</body>

</html>

 

CSS

@import url('https://fonts.googleapis.com/css?family=Lato&display=swap');

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  height: 100vh;
  display: flex;
  font-family: 'Lato';
  align-items: center;
  justify-content: center;
  background-color: rgb(154, 218, 218);
  text-transform: capitalize;
}

/* Card styles */
.card {
  height: 400px;
  width: 300px;
  background-color: rgba(255, 255, 255, 0.7);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  border-radius: 5px;
  box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.2);
}

/* Title styles */
h1 {
  background-color: #696969;
  text-align: center;
  padding: 5px;
  color: #fff;
}

/* Stages global styles */
.contents {
  height: 100%;
  overflow: hidden;
}

.contents.hide {
  display: none;
}

.contents label {
  height: 40px;
  display: flex;
  align-items: center;
  margin: 5px;
  padding: 0 0 0 10px;
  border: 1px solid #696969;
  border-radius: 5px;
  position: relative;  
}

/* Stage - 1 styles */
small {
  width: 140px;
  text-align: center;
  display: inline-block;
  position: absolute;
  right: -4px;
  top: -4px;
  background-color: rgb(107, 188, 94);
  border-radius: 2px;
  box-shadow: 1px 1px 1px 1px rgba(154, 218, 218, 0.3);
  color: #fff;
}

.contents input {
  display: none;
}

.contents input:checked + label {
  animation: select 1s ease forwards;
}

@keyframes select {
  from {
    background-color: #696969;
  }

  to {
    background-color: rgb(0, 208, 255);
    color: #fff;
  }
}

/* Countdown styles */
.count {
  height: 60px;
  width: 60px;
  position: absolute;
  top: 50%;
  left: 50%;
  margin: auto;
  overflow: hidden;
  transform: translate(-50%, -50%);
}

.count .num {
  height: 50px;
  position: absolute;
  bottom: -5px;
  left: 50%;
  transform-origin: bottom;
  transform: translateX(-50%) rotate(180deg);
  font-size: 2.5rem;
}

.count .num:nth-child(1) {
  animation: spin 2s cubic-bezier(0.075, 0.82, 0.165, 1) forwards;
}

.count .num:nth-child(2) {
  animation: spin 2s 2s cubic-bezier(0.075, 0.82, 0.165, 1) forwards;
}

.count .num:nth-child(3) {
  animation: spin 2s 4s cubic-bezier(0.075, 0.82, 0.165, 1) forwards;
}

.count .num:nth-child(4) {
  animation: spin 2s 6s cubic-bezier(0.075, 0.82, 0.165, 1) forwards;
}

@keyframes spin {
  0% {
    transform: translateX(-50%) rotate(180deg);
  }

  30% {
    transform: translateX(-50%) rotate(-10deg);
  }

  60% {
    transform: translateX(-50%) rotate(10deg);
  }

  100% {
    transform: translateX(-50%) rotate(-180deg);
  }
}

/* Stage - 2 styles */
.question-box {
  display: flex;
  flex-direction: column;
  justify-content: end;
  position: relative;
}

.current-question {
  height: 42px;
  width: 100%;
  border: 4px solid rgb(134, 205, 161);
  position: absolute;
  top: 132px;
  animation: blink 1s ease infinite;  
}

.question {
  height: 40px;
  display: flex;
  flex-shrink: 0;
  align-items: center;
  padding: 0 10px;
  border: 1px solid rgb(205, 193, 134);
  margin: 3px;
  border-radius: 2px;
}

@keyframes blink {
  from {
    opacity: 0.3;
  }

  to {
    opacity: 1;
  }
}

/* Stage - 3 styles */
.result {
  text-align: center;
  margin: 10px;
  font-size: 1.4rem;
  margin: 30px 0;
}

/* Button styles */
.btn-container {
  height: 50px;
  background-color: #696969;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}

.btn-container .btn {
  height: 32px;
  width: 100px;
  background-color: aliceblue;
  border: none;
  border-radius: 5px;
  transition: 0.2s ease;
  font-size: 1.1rem;
  text-transform: uppercase;
}

.btn-container .btn:hover {
  filter: brightness(110%);
}

.btn-container .btn:active {
  transform: scale(0.97);
}

.btn-container .btn-wrong {
  background-color: rgb(245, 145, 145);
}

.btn-container .btn-correct {
  background-color: greenyellow;
}

index.js

import { chooseOperator, chooseNumber, getAnswer, getWrongAnswer } from './quiz.generator.js'

// Elements
const btnMain = document.querySelector('.btn-main')
const btnWrong = document.querySelector('.btn-wrong')
const btnCorrect = document.querySelector('.btn-correct')
const btnReset = document.querySelector('.btn-reset')
const inputs = document.querySelectorAll('input')
const contents = document.querySelectorAll('.contents')
const questionBox = document.querySelector('.question-box')

// Variables
let questions = []
let answers = []
let contentIndex = 0
let questionMax = 0
let questionId = 0
let questionIdx = 0

let startTime = 0
let record = 0
let penalty = 0

let bestScores = {
  0: 0,
  1: 0,
  2: 0,
  3: 0,
}

// Local storage
function updateRecord() {
  if (localStorage.getItem('bestScores')) {
    const bestEls = document.querySelectorAll('.best-record')
    bestScores = JSON.parse(localStorage.getItem('bestScores'))
    Object.keys(bestScores).forEach((key, idx) => {
      bestEls[idx].textContent = bestScores[key]
    })
  }
}

updateRecord()

// Event handlers
btnMain.addEventListener('click', () => {
  inputs.forEach((input, idx) => {
    if (input.checked) {
      changeContentsBox()
      questionId = idx
      questionMax = input.value
      createQuestions()
    }
  })
  if (questionMax === 0) {
    alert('Select an option')
  }
})

btnWrong.addEventListener('click', () => {
  answers.push(false)
  moveQuestions()
})

btnCorrect.addEventListener('click', () => {
  answers.push(true)
  moveQuestions()
})

btnReset.addEventListener('click', () => {
  questions = []
  answers = []
  contentIndex = -1
  questionMax = 0
  questionId = 0
  questionIdx = 0
  startTime = 0
  record = 0
  penalty = 0

  btnMain.hidden = false
  btnReset.hidden = true

  document.querySelectorAll('.question').forEach((initialQuestionEl) => {
    initialQuestionEl.remove()
  })

  changeContentsBox()
})

// Functions
function beginTimer() {
  startTime = new Date()
}

function endTimer() {
  record = new Date() - startTime
}

function createQuestions() {
  for (let i = 0; i < questionMax; i++) {
    const num1 = chooseNumber()
    const num2 = chooseNumber()
    let operator = chooseOperator()
    let trueFalse = Math.floor(Math.random() * 2) === 0 ? true : false
    const answer = getAnswer(num1, operator, num2)
    questions.push({
      question: `${num1} ${operator} ${num2} = ${
        trueFalse ? answer : getWrongAnswer(answer, operator)
      }`,
      correct: trueFalse,
    })
  }
  addInitialQuiz()
}

function addInitialQuiz() {
  const initialQuestionsEl = document.querySelectorAll('.question')
  if (initialQuestionsEl.length === 0) {
    for (let i = 0; i < 4; i++) {
      const newQuestionEl = document.createElement('div')
      newQuestionEl.className = 'question'
      newQuestionEl.textContent = questions[questionIdx].question
      questionIdx++
      questionBox.appendChild(newQuestionEl)
    }
  }

  initialQuestionsEl.forEach((initialQuestionEl) => {
    initialQuestionEl.textContent = questions[questionIdx].question
    questionIdx++
  })
}

function changeContentsBox() {
  contentIndex++
  if (contentIndex > 3) contentIndex = 0
  contents.forEach((content) => {
    content.classList.add('hide')
  })
  contents[contentIndex].classList.remove('hide')

  if (contentIndex === 1) {
    btnMain.hidden = true
    setTimeout(() => {
      changeContentsBox()
      btnCorrect.hidden = false
      btnWrong.hidden = false
      beginTimer()
    }, 7500)
  }

  if (contentIndex === 3) {
    const recordTime = +(record / 1000).toFixed(2)
    const newRecord = +(recordTime + penalty).toFixed(2)
    document.querySelector('.result-time').textContent = newRecord
    document.querySelector('.result-base').textContent = recordTime
    document.querySelector('.result-penalty').textContent = penalty

    if (bestScores[questionId] > newRecord) {
      bestScores[questionId] = newRecord
    }
    localStorage.setItem('bestScores', JSON.stringify(bestScores))
    btnCorrect.hidden = true
    btnWrong.hidden = true
    btnReset.hidden = false
    updateRecord()
  }
}

function moveQuestions() {
  const newQuestion = document.createElement('div')
  newQuestion.className = 'question'
  if (questionIdx < questions.length) {
    newQuestion.textContent = questions[questionIdx].question
  } else if (questionIdx == questions.length) {
    newQuestion.textContent = 'finished'
    newQuestion.style.display = 'flex'
    newQuestion.style.justifyContent = 'center'
    newQuestion.style.textTransform = 'uppercase'
    newQuestion.style.color = '#fff'
    newQuestion.style.backgroundColor = '#696969'    
  } else {
    newQuestion.textContent = ''
  }

  if (questionIdx > questions.length + 2) {
    getPenalty()
    endTimer()
    changeContentsBox()
  }

  if(questionIdx > 6) document.querySelector('.question').remove()

  questionIdx++
  questionBox.appendChild(newQuestion)
}

function getPenalty() {
  questions.forEach((q, idx) => {
    if (q.correct !== answers[idx]) penalty += 0.5
  })
}

quiz.generator.js

// operation
const operators = ['-', '+', '/', '*']

// Returns a random operator
function chooseOperator() {
  return operators[Math.floor(Math.random() * operators.length)]
}

// Returns a random number
function chooseNumber() {
  return Math.floor(Math.random() * 150)
}

// Returns the correct answer
function getAnswer(num1, operator, num2) {
  switch (operator) {
    case '-':
      return num1 - num2
    case '+':
      return num1 + num2
    case '/':
      return num1 / num2
    case '*':
      return num1 * num2
  }
}

// Returns a wrong answer
function getWrongAnswer(answer, operator) {
  const randomNumber = Math.floor(Math.random() * 10 + 1)
  switch (operator) {
    case '-':
      return answer - randomNumber
    case '+':
      return answer + randomNumber
    case '/':
      return answer / randomNumber
    case '*':
      return answer * randomNumber
  }
}

export { chooseOperator, chooseNumber, getAnswer, getWrongAnswer }

 

데모

 

Document

Math Sprint   10 questions best score:   40 questions best score:   70 questions best score: 120 questions best score: 1 2 3 Go 1 1 1 1 your time 11s base time: penalty: start wrong correct

jin-co-jcg.vercel.app

 

프로젝트 출처

https://www.udemy.com/course/javascript-web-projects-to-build-your-portfolio-resume/

728x90
반응형

'프론트엔드 > 자바스크립트' 카테고리의 다른 글

드래그 앤 드랍  (0) 2022.12.26
프로젝트 - 가위 바위 보 도마뱀 스폭 게임  (0) 2022.12.25
연산자 (Operators)  (0) 2022.12.24
모듈 (Module)  (0) 2022.12.21
웹팩 (webpack)  (0) 2022.12.21