본문 바로가기
Front-end/Javascript 실습

당근게임 - 리팩토링 편

by warrior.p 2022. 4. 5.

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Fun Game</title>
    <script
      src="https://kit.fontawesome.com/9eb162ac0d.js"
      crossorigin="anonymous"
    ></script>
    <link rel="stylesheet" href="style.css" />
    <script type="module" src="src/main.js" defer></script>
  </head>
  <body>
    <section class="game">
      <header>
        <button class="game__button">
          <i class="fas fa-play"></i>
        </button>
        <span class="game__timer">
          00:00
        </span>
        <span class="game__score">0</span>
      </header>
      <section class="game__field"></section>
    </section>
    <section class="pop-up hide">
      <header>
        <button class="pop-up__refresh">
          <i class="fas fa-redo"></i>
        </button>
        <span class="pop-up__message">You have WON!!!</span>
      </header>
    </section>
  </body>
</html>

 

CSS

body {
  text-align: center;
  background-color: black;
}

button {
  outline: none;
  cursor: pointer;
  border: none;
  background: white;
  border-radius: 20%;
  padding: 0;
}

.game {
  display: flex;
  flex-direction: column;
  position: relative;
  width: 800px;
  height: 550px;
  margin: auto;
  margin-top: 50px;
  background: url(img/background.png) center/cover no-repeat;
}

header {
  text-align: center;
  padding: 8px;
}

.game__button {
  width: 60px;
  height: 60px;
  font-size: 24px;
  background-color: wheat;
  border: 4px solid black;
  transition: transfrom 300ms ease-in;
}

.game__button:hover {
  transform: scale(1.1);
}

.game__timer {
  display: block;
  width: 100px;
  margin: auto;
  margin-top: 8px;
  padding: 0 20px;
  background-color: white;
  border-radius: 10px;
  font-size: 32px;
  border: 5px solid black;
}

.game__score {
  display: inline-block;
  width: 50px;
  height: 50px;
  font-size: 38px;
  margin-top: 4px;
  background-color: darksalmon;
  color: white;
  border-radius: 50%;
  border: 3px solid black;
}

.game__field {
  width: 100%;
  height: 100%;
}

.pop-up {
  border-radius: 20px;
  width: 400px;
  height: 124px;
  margin: auto;
  background-color: #00000090;
  color: white;
  transform: translateY(-150%);
}

.pop-up__refresh {
  width: 60px;
  height: 60px;
  font-size: 24px;
  background-color: wheat;
  border: 2px solid black;
  transition: transfrom 300ms ease-in;
}

.pop-up__refresh:hover {
  transform: scale(1.1);
}

.pop-up__message {
  display: block;
  font-size: 38px;
}

.hide {
  display: none;
}

.carrot:hover,
.bug:hover {
  transform: scale(1.1);
}

 

main.JS

'use strict';
import PopUp from './popup.js';
import { GameBuilder, Reason } from './game.js';

const game = new GameBuilder()
  .gameDuration(60)
  .carrotCount(20)
  .bugCount(10)
  .build();
const gameFinishBanner = new PopUp();

game.setGameStopListener(reason => {
  let message;
  switch (reason) {
    case Reason.win:
      message = 'YOU WON 🎉';
      break;
    case Reason.lose:
      message = 'YOU LOST 💩';
      break;
    case Reason.cancel:
      message = 'Replay❓';
      break;
    default:
      throw new Error('not valid reason');
  }
  gameFinishBanner.showWithText(message);
});

gameFinishBanner.setClickListener(() => {
  game.start();
});

 

PopUp JS

'use strict';

export default class PopUp {  
  constructor() {   
    this.popUp = document.querySelector('.pop-up');
    this.popUpMessage = document.querySelector('.pop-up__message');
    this.popUpRefreshBtn = document.querySelector('.pop-up__refresh');

    this.popUpRefreshBtn.addEventListener('click', () => {
      this._hide();
      this.onClick && this.onClick();
    });
  }

  setClickListener(onClick) { //
    this.onClick = onClick;
  }

  showWithText(text) {
    this.popUpMessage.innerText = text;
    this.popUp.classList.remove('hide');
  }

  _hide() {
    this.popUp.classList.add('hide');
  }
}

 

 

Field JS

'use strict';

import * as sound from './sound.js';

const CARROT_SIZE = 80;
const FIELD_TOP_PADDING = 50;

export const ItemType = Object.freeze({
  carrot: 'carrot',
  bug: 'bug',
});

export class Field {
  constructor(carrotCount, bugCount) {
    this.carrotCount = carrotCount;
    this.bugCount = bugCount;
    this.field = document.querySelector('.game__field');
    this.fieldRect = this.field.getBoundingClientRect();
    this.onFieldClickListener = this.onFieldClickListener.bind(this);
    this.field.addEventListener('click', this.onFieldClickListener);
  }

  init() {
    this.field.innerHTML = '';
    this.addItem(this.carrotCount, 'img/carrot.png', 'carrot');
    this.addItem(this.bugCount, 'img/bug.png', 'bug');
  }

  setItemClickListener(onItemClick) {
    this.onItemClick = onItemClick;
  }

  addItem(count, imgPath, className) {
    const x1 = 0;
    const x2 = this.fieldRect.width - CARROT_SIZE;
    const y1 = this.field.offsetTop + FIELD_TOP_PADDING;
    const y2 = this.field.offsetTop + this.fieldRect.height - CARROT_SIZE;
    for (let i = 0; i < count; i++) {
      const item = document.createElement('img');
      item.setAttribute('class', className);
      item.setAttribute('src', imgPath);
      item.style.position = 'absolute';
      const x = randomNumber(x1, x2);
      const y = randomNumber(y1, y2);
      item.style.left = `${x}px`;
      item.style.top = `${y}px`;
      item.style.userDrag = 'none';
      this.field.appendChild(item);
    }
  }

  onFieldClickListener(event) {
    const target = event.target;
    if (target.matches('.carrot')) {
      sound.playCarrot();
      target.remove();
      this.onItemClick && this.onItemClick(ItemType.carrot);
    } else if (target.matches('.bug')) {
      this.onItemClick && this.onItemClick(ItemType.bug);
    }
  }
}

function randomNumber(min, max) {
  return Math.random() * (max - min) + min;
}

 

Game JS

'use strict';
import { Field, ItemType } from './field.js';
import * as sound from './sound.js';

export class GameBuilder {
  gameDuration(duration) {
    this.gameDuration = duration;
    return this;
  }

  carrotCount(num) {
    this.carrotCount = num;
    return this;
  }

  bugCount(num) {
    this.bugCount = num;
    return this;
  }

  build() {
    return new Game(
      this.gameDuration, //
      this.carrotCount,
      this.bugCount
    );
  }
}

export const Reason = Object.freeze({
  win: 'win',
  lose: 'lose',
  cancel: 'cancel',
});

class Game {
  constructor(gameDuration, carrotCount, bugCount) {
    this.gameDuration = gameDuration;
    this.carrotCount = carrotCount;
    this.bugCount = bugCount;

    this.field = new Field(this.carrotCount, this.bugCount);
    this.field.setItemClickListener(item => this.onItemClick(item));
    this.timerIndicator = document.querySelector('.game__timer');
    this.scoreText = document.querySelector('.game__score');
    this.gameBtn = document.querySelector('.game__button');
    this.gameBtn.addEventListener('click', () => {
      if (this.started) {
        this.stop(Reason.cancel);
        sound.playAlert();
      } else {
        this.start();
      }
    });

    this.started = false;
    this.score = 0;
    this.timer = undefined;
  }

  setGameStopListener(onGameStop) {
    this.onGameStop = onGameStop;
  }

  start() {
    this.started = true;
    this.initGame();
    this.showStopButton();
    this.showTimerAndScore();
    this.startGameTimer();
    sound.playBackground();
  }

  stop(reason) {
    this.started = false;
    this.hideStartButton();
    this.stopGameTimer();
    sound.stopBackground();
    if (reason === Reason.win) {
      sound.playWin();
    } else if (reason === Reason.lose) {
      sound.playLost();
    }
    this.onGameStop && this.onGameStop(reason);
  }

  initGame() {
    this.score = 0;
    this.updateScoreBoard(this.score);
    this.field.init();
  }

  onItemClick(item) {
    if (!this.started) {
      return;
    }
    if (item === ItemType.carrot) {
      this.score++;
      this.updateScoreBoard(this.score);

      if (this.score === this.carrotCount) {
        this.stop(Reason.win);
      }
    } else {
      this.stop(Reason.lose);
    }
  }

  startGameTimer() {
    let remainingTimeSec = this.gameDuration;
    this.updateTimerText(remainingTimeSec);

    this.timer = setInterval(() => {
      if (remainingTimeSec <= 0) {
        clearInterval(this.timer);

        if (this.started) {
          this.stop(this.score === this.carrotCount ? Reason.win : Reason.lose);
        }

        return;
      }
      this.updateTimerText(--remainingTimeSec);
    }, 1000);
  }

  stopGameTimer() {
    clearInterval(this.timer);
  }

  updateScoreBoard(newScore) {
    this.scoreText.innerText = this.carrotCount - newScore;
  }

  showStartButton() {
    const icon = this.gameBtn.querySelector('.fas');
    icon.classList.remove('fa-stop');
    this.gameBtn.style.visibility = 'visible';
  }

  showStopButton() {
    const icon = this.gameBtn.querySelector('.fas');
    icon.classList.add('fa-stop');
    this.gameBtn.style.visibility = 'visible';
  }

  hideStartButton() {
    this.gameBtn.style.visibility = 'hidden';
  }

  showTimerAndScore() {
    this.timerIndicator.style.visibility = 'visible';
    this.scoreText.style.visibility = 'visible';
  }

  hideTimerAndScore() {
    this.timerIndicator.style.visibility = 'hidden';
    this.scoreText.style.visibility = 'hidden';
  }

  updateTimerText(time) {
    const minutes = Math.floor(time / 60);
    const seconds = time % 60;
    this.timerIndicator.innerHTML = `${minutes}:${seconds}`;
  }

  resetScoreText() {
    this.scoreText.innerText = this.carrotCount;
  }
}

 

Sound JS

'use strict';

const carrotSound = new Audio('./sound/carrot_pull.mp3');
const failSound = new Audio('./sound/bug_pull.mp3');
const winSound = new Audio('./sound/game_win.mp3');
const bgSound = new Audio('./sound/bg.mp3');
const alertSound = new Audio('./sound/alert.wav');

export function playCarrot() {
  playSound(carrotSound);
}

export function playBug() {
  playSound(failSound);
}

export function playWin() {
  playSound(winSound);
}

export function playLost() {
  playSound(failSound);
}

export function playBackground() {
  playSound(bgSound);
}

export function stopBackground() {
  bgSound.pause();
}

export function playAlert() {
  playSound(alertSound);
}

function playSound(sound) {
  sound.currentTime = 0;
  sound.play();
}