[2024.08.22 TIL] 로그라이크 텍스트 게임

박지영·2024년 8월 22일
0

Today I Learned

목록 보기
29/88
post-thumbnail

오늘 구현한 기능 및 해본 것

자고 일어난 후 어제 만들었던 코드를 다시 보니 너무 난잡하고 중복된 코드가 많았다.

오늘은 코드를 리팩토링 하는 것을 주력으로 하고 추가로 기능을 구현해보려고 한다.

클래스 리팩토링

  • 공격력 계산과 관련된 부분에 중복 코드가 많음
  • 공격력 계산 로직을 메소드로 구현
  calculDamage(monster, counter = false) {
    let damage =
      // 최소 공격력 + 난수 * 공격력 편차(최소공 * 최대공 배율 - 최소공)
      this.damage +
      Math.round(Math.random() * (this.damage * this.maxDamageMag - this.damage));

    // 반격 대미지 계산
    if (counter) damage = Math.round(damage * 0.6);

    return damage > 0 ? damage : 0;
  }
  • 입은 피해 계산 메소드
// 입은 피해 계산
  takeDamage(damage) {
    // 대미지가 0보다 낮을경우 0 = 최소 피해량
    const receivedDamage = Math.max(damage - this.defense, 0);
    this.hp -= receivedDamage;
    return receivedDamage;
 }
  • 공격 메소드 수정
// 공격
  attack(monster) {
    let damage = this.calculDamage(monster);
    const isCri = this.isCri();

    // 치명타 시 데미지 * 치명타 배율
    if (isCri) damage *= this.criticalMag;

    damage = monster.takeDamage(damage, isCri);

    return [damage, isCri];
  }

game.js 리팩토링

  • game.js에 로그 처리 함수 정의
// 플레이어 로그 처리
function handlePlayerLog(turnCnt, result, logs) {
  logs.push(
    chalk.green(
      `[${turnCnt}] 몬스터에게 ${result[1] ? '치명타로' : ''} ${result[0]}의 피해를 입혔습니다!`,
    ),
  );
}
// 몬스터 로그 처리
function handleMonsterLog(turnCnt, result, logs) {
  logs.push(
    chalk.red(
      `[${turnCnt}] 플레이어가 ${result[1] ? '치명타로' : ''} ${result[0]}의 피해를 입었습니다!`,
    ),
  );
}
  • 함수 및 클래스의 메소드 추가로 개선된 점
    • 모든 switch case에서 중복된 조건문 코드가 존재
// 기존 코드
// 공격
const paResult = player.attack(monster);
if (paResult[1]) {
   logs.push(
        chalk.green(`[${turnCnt}] 몬스터에게 치명타로 ${paResult[0]}의 피해를 입혔습니다!`),
      );
} else {
   logs.push(chalk.green(`[${turnCnt}] 몬스터에게 ${paResult[0]}의 피해를 입혔습니다.`));
}
// 개선된 코드
// 공격
const paResult = player.attack(monster);
handlePlayerLog(turnCnt, paResult, logs);
// 몬스터 공격
handleMonsterLog(turnCnt, maResult, logs);
  • 보상 로직 클래스로 이관
// game.js
 while (stage <= 10) {
    const monster = new Monster(stage);
    const clear = await battle(stage, player, monster, result, increase);

    // 스테이지 클리어 및 게임 종료 조건
    // 게임 종료
    if (clear === false) {
      break;
    }
    // 몬스터 hp가 0 이하가 되면 스테이지 클리어
    if (clear === 0) {
      stage++;

      // 스테이지 클리어 보상 로그
      if (stage > 1) {
        chalk.green(`${increase}이/가 ${result} 상승했습니다.`);
        player.heal(50);
        chalk.green(`체력이 50 회복되었습니다.`);
      }

      // 기본 보상
      player.reward();

      // 클리어 보상
      // 6가지 중 한가지 랜덤으로 선정
      let rn = Math.floor(Math.random() * (Object.keys(player).length - 1));

      // player 인스턴스에 키 배열의 인덱스 키 이름 구하기
      const stat = Object.keys(player)[rn];

      // 클리어 보상 랜덤 로직 시행
      switch (rn) {
        // 체력
        case 0:
          // 20 ~ 50
          increase = '체력';
          result = 20 + Math.round(Math.random() * 30);
          player[stat] += result;
          break;
        // 최소 공격력
        case 1:
          // 5 ~ 10
          increase = '최소 공격력';
          result = 5 + Math.round(Math.random() * 10);
          player[stat] += result;
          break;
        // 최대 공격력 배율
        case 2:
          // 0.1 ~ 1
          increase = '최대 공격력 배율';
          result = Math.ceil(Math.random() * 100) / 100;
          player[stat] += result;
          break;
        // 방어 확률
        case 3:
          // 3 ~ 10
          increase = '방어 확률';
          result = 3 + Math.round(Math.random() * 7);
          player[stat] += result;
          break;
        // 도망 확률
        case 4:
          // 1 ~ 3
          increase = '도망 확률';
          result = 1 + Math.round(Math.random() * 2);
          player[stat] += result;
          break;
        // 연속 공격 확률
        case 5:
          // 3 ~ 7
          increase = '연속 공격 확률';
          result = 3 + Math.round(Math.random() * 4);
          player[stat] += result;
          break;
        // 방어 수치
        case 6:
          // 1 ~ 3
          increase = '방어 수치';
          result = 1 + Math.round(Math.random() * 2);
          player[stat] += result;
          break;
        // 치명타 확률
        case 7:
          // 3 ~ 7
          increase = '치명타 확률';
          result = 3 + Math.round(Math.random() * 4);
          player[stat] += result;
          break;
      }
    }
  }
  • 수정 후 game.js
while (stage <= 10) {
    const monster = new Monster(stage);
    const clear = await battle(stage, player, monster, result, increase);

    // 스테이지 클리어 및 게임 종료 조건
    // 게임 종료
    if (clear === false) {
      break;
    }
    // 몬스터 hp가 0 이하가 되면 스테이지 클리어
    if (clear === 0) {
      stage++;
    }
  }
  • 수정 후 player 클래스
// 스테이지 클리어 보상
  reward(stage) {
    // 기본 보상
    this.damage += Math.round(stage / 2);
    this.defense += Math.round(stage / 2);

    // 랜덤 추가 보상
    const rewardTable = this.rewardTable[Math.floor(Math.random() * this.rewardTable.length)];

    switch (rewardTable) {
      // 체력
      case 'hp':
        // 20 ~ 50
        const hp = 20 + Math.round(Math.random() * 30);
        this.hp += hp;
        return { type: '체력', amount: hp };
      // 최소 공격력
      case 'damage':
        // 1 ~ 10
        const damage = 1 + Math.round(Math.random() * 10);
        this.damage += damage;
        return { type: '최소 공격력', amount: damage };
      // 최대 공격력 배율
      case 'maxDamageMag':
        // 0.1 ~ 1
        const maxDamageMag = Math.ceil(Math.random() * 100) / 100;
        this.maxDamageMag += maxDamageMag;
        return { type: '최대 공격력 배율', amount: maxDamageMag };
      // 방어 확률
      case 'counterChance':
        // 3 ~ 10
        const counterChance = 3 + Math.round(Math.random() * 7);
        this.counterChance += counterChance;
        return { type: '방어 확률', amount: counterChance };
      // 도망 확률
      case 'runChance':
        // 1 ~ 3
        increase = '도망 확률';
        const runChance = 1 + Math.round(Math.random() * 2);
        this.runChance += runChance;
        return { type: '도망 확률', amount: runChance };
      // 연속 공격 확률
      case 'doubleAttackChance':
        // 3 ~ 7
        const doubleAttackChance = 3 + Math.round(Math.random() * 4);
        this.doubleAttackChance += doubleAttackChance;
        return { type: '연속 공격 확률', amount: doubleAttackChance };
      // 방어 수치
      case 'defense':
        // 1 ~ 3
        const defense = 1 + Math.round(Math.random() * 2);
        this.defense += defense;
        return { type: '방어 수치', amount: defense };
      // 치명타 확률
      case 'criticalChance':
        // 3 ~ 7
        const criticalChance = 3 + Math.round(Math.random() * 4);
        this.criticalChance += criticalChance;
        return { type: '치명타 확률', amount: criticalChance };
    }
  }

크리티컬 기능

  • 생성자에 크리티컬 확률을 추가하고 랜덤 함수로 크리티컬 확률 보다 낮으면 크리티컬 성공
// 치명타 확률
this.criticalChance = 10;
// 치명타 배율
this.criticalMag = 2;
  • player와 monster 클래스에 isCri 메소드 추가, true면 크리티컬 성공
  isCri() {
    return Math.random() * 100 < this.criticalChance;
  }

회복하기 기능

  • 최소 1 ~ 최대 (현재 스테이지 * 5) 만큼 회복
  • 기존에 만들어놓았던 heal 메소드 활용
heal(amount) {
    this.hp += amount;
    return amount;
  }

case '5':
        // 회복
        const healAmount = player.heal(1 + Math.floor(Math.random() * (5 * stage)));
        logs.push(chalk.yellow(`[${turnCnt}] ${healAmount}만큼 회복에 성공했습니다!`));
        // 몬스터 공격
        handleMonsterLog(turnCnt, maResult, logs);
        break;

아이템 기능

  • 아이템의 속성

    • name = 이름
    • stat = 공격력, 치명타 확률, 최대 공격력 배율, 방어력 등 능력치
    class Item {
      constructor(name, stat) {
        this.name = name;
        this.stat = stat;
      }
    }
  • 스테이지 클리어 시 낮은 확률로 드랍 / 스테이지가 거듭 될 수록 드랍율 상승

    // 2 ~ 20% 확률로 아이템 드랍
    if (Math.random() * 100 < 2 * stage) {
     const item = Item.dropItem();
     console.log(chalk.yellow(`몬스터가 ${item.name}을/를 드랍했습니다!`));
     item.equipItem(player, item);
    }
  • 드랍 로직 메소드

    // item 드랍 메소드
     // 인스턴스화하기 전에 사용하는 정적 메소드
     static dropItem() {
       const name = `전설적인 무기`;
       const stats = Math.floor(Math.random() * 3);
       const itemStats = {};
    
       if (stats === 0) itemStats.damage = Math.ceil(Math.random() * 10);
       if (stats === 1) itemStats.defense = Math.ceil(Math.random() * 10);
       if (stats === 2) itemStats.criticalChance = Math.ceil(Math.random() * 40);
    
       const item = new Item(name, itemStats);
       return item;
     }
  • 아이템 효과 적용 메소드

    // item 장착 메소드 = player 객체에 능력치 추가
     equipItem(player, item) {
       player.item = item;
       if (item.stat.damage) player.damage += item.stat.damage;
       if (item.stat.defense) player.defense += item.stat.defense;
       if (item.stat.criticalChance) player.criticalChance += item.stat.criticalChance;
     }

트러블 슈팅

방어의 반격 로직 오류

  • 현상

    • 방어가 성공하면 터미널이 멈추는 오류
  • 접근

    • player 클래스의 방어 로직 수정 해보았음.
    • 수정 전
    // 방어/반격
    counter(monster) {
      const roll = Math.random() * 100 < this.defenseChance;
      // 확률 체크
      if (roll) {
        let counter = this.calculDamage(monster, true);
        const isCri = this.isCri();
        // 60% 데미지만
        counter = monster.takeDamage(counter, isCri) * 0.6;
        return [true, counter];
      }
    
      return [false, 0];
    }
    • 수정 후
    counter(monster) {
      const roll = Math.random() * 100 < this.defenseChance;
      // 확률 체크
      if (roll) {
        let damage = this.calculDamage();
        // 60% 데미지만
        damage = Math.round(monster.takeDamage(counter, isCri) * 0.6);
        return [true, damage];
      }

    해결 실패... 클래스단의 문제가 아닌 듯하다

    • game.js 파일의 방어 로직 부분
    case '3':
          // 방어
          const defResult = player.counter(monster);
          if (defResult[0]) {
            player.hp += maResult;
    
            logs.push(chalk.gray(`[${turnCnt}] 방어에 성공했습니다!`));
            logs.push(chalk.green(`[${turnCnt}] 몬스터에게 ${defResult[1]}의 피해를 입혔습니다!`));
          } else {
            logs.push(chalk.yellow(`[${turnCnt}] 방어에 실패했습니다!`));
    
            // 몬스터 공격
            handleMonsterLog(turnCnt, maResult, logs);
          }
          break;
    • defResult[0]의 값과 defResult[1]의 값 체크
    console.log(chalk.gray(`[${turnCnt}] ${defResult}`));
    [1] [ true, 3 ]

    여기도 문제가 없다... 그래서 하나하나 주석으로 바꾸면서 체크해보았다.

  • 해결

    • 플레이어의 체력 추가의 문제였다.
     player.hp += maResult;
    • 몬스터 클래스를 수정하면서 어택의 리턴 값을 배열로 바꿨는데 이 부분을 수정하지 않아서 일어나는 문제였다.
    player.hp += maResult[0];
    • 정상 동작한다. 하지만 일일히 수정하는게 번거롭다. 체력 갱신 부분도 클래스단에서 처리하도록 메소드로 구현해보았다.
    heal(amount) {
      this.hp += amount;
    }
    player.heal(maResult[0]);

아이템 기능 오류

  • 현상
    • dropItem 메소드를 사용할 수 없음. (로직을 메소드화 해서 item class 파일로 옮겼더니 발생)

 // 20% 확률로 아이템 드랍
 if (Math.random() * 100 < 100) {
   const item = Item.dropItem();
   console.log(chalk.yellow(`몬스터가 ${item.name}을/를 드랍했습니다!`));
   item.equipItem(player, item);
 }
// TypeError: Item.dropItem is not a function

   const item = dropItem();
// ReferenceError: dropItem is not defined
  • 접근

    • 메소드화 하지말고 그대로 로직으로 사용하는게 맞는건가?

    • 왜 사용못할까?

      • class.메소드() 방식으로 사용해야하는데 class가 존재하지 않는다.
    • 미리 만들지는 못하는가?

      • dropItem 메소드가 인스턴스를 만드는 메소드이다. 불가능.
    • 메소드를 함수처럼 독립적으로 사용하는 방법은 없는가?

      • 없다. 하지만 찾아보니 인스턴스 클래스가 없어도 사용하는 방법이 있었다.
        메소드를 static으로 선언하면 인스턴스가 없어도 클래스 메소드로 사용할 수 있었다.
  • 수정 전

// item 드랍 메소드
   dropItem() {
    const name = `리치왕의 분노`;
    const stats = Math.floor(Math.random() * 3);
    const itemStats = {};

    if (stats === 0) itemStats.damage = Math.ceil(Math.random() * 5);
    if (stats === 1) itemStats.defense = Math.ceil(Math.random() * 5);
    if (stats === 2) itemStats.criticalChance = Math.ceil(Math.random() * 20);

    const item = new Item(name, itemStats);
    return item;
  }
  • 수정 후
// item 드랍 메소드
  static dropItem() {
    const name = `리치왕의 분노`;
    const stats = Math.floor(Math.random() * 3);
    const itemStats = {};

    if (stats === 0) itemStats.damage = Math.ceil(Math.random() * 5);
    if (stats === 1) itemStats.defense = Math.ceil(Math.random() * 5);
    if (stats === 2) itemStats.criticalChance = Math.ceil(Math.random() * 20);

    const item = new Item(name, itemStats);
    return item;
  }
  • 해결
const item = Item.dropItem();

알고 보니 아주 간단한 문제였다. 괜히 시간을 버린 기분이지만 클래스 문법의 static에 대해서 알게되었다.

static (정적 메소드)

  • 클래스의 인스턴스 없이 호출가능하나 클래스가 인스턴스화되면 호출할 수 없다.

  • 동일한 클래스 내의 다른 정적 메서드 내에서 정적 메서드를 호출하는 경우 키워드 this를 사용할 수 있다.

class StaticMethodCall {
  static staticMethod() {
    return "정적 메소드";
  }
  static anotherStaticMethod() {
    return this.staticMethod() + "와 다른 정적 메소드";
  }
}
StaticMethodCall.staticMethod();
// '"정적 메소드"

StaticMethodCall.anotherStaticMethod();
// '"정적 메소드와 다른 정적 메소드"
profile
신입 개발자

0개의 댓글