[TIL #18]

이상현·2024년 8월 26일

[ TIL ]

목록 보기
18/38

코드 분석

game.js

import chalk from 'chalk';
import readlineSync from 'readline-sync';
import { displayLobby, handleUserInput } from "./server.js";

class Player {
    constructor(stage) {
        this.hp = 100;
        this.stage = stage;
    }

    attack(monster) {
        const basedamage = Math.floor(Math.random() * 10) + 7; // 플레이어의 공격
        const plusdamage = this.stage
        const totaldamage = basedamage + plusdamage;
        monster.hp -= totaldamage;
        return totaldamage;
    }
}

class Monster {
    constructor(stage) { // 스테이지를 인자로 받기
        this.hp = 100 + stage * 10; // 스테이지 별로 체력 10 증가
        this.stage = stage;
    }

    attack(player) {
        const damage = Math.floor(Math.random() * 10) + 2 + this.stage; // 몬스터의 공격
        player.hp -= damage;
        return damage;
    }
}

function displayStatus(stage, player, monster) {
    console.log(chalk.magentaBright(`\n=== Current Status ===`));
    console.log(
        chalk.cyanBright(`| Stage: ${stage} `) +
        chalk.blueBright(
            `| 플레이어 정보 : ${player.hp} `,
        ) +
        chalk.redBright(
            `| 몬스터 정보 : ${monster.hp} | `,
        ),
    );
    console.log(chalk.magentaBright(`=====================\n`));
}

function playerDead(player) {
    let logs = [];
    logs.forEach((log) => console.log(log));
    if (player.hp <= 0) {  // 플레이어 체력이 0 이하인지 확인
        console.clear();
        console.log(chalk.magentaBright(`=====================\n`));
        logs.push(chalk.red(`당신은 눈앞이 캄캄해졌습니다..`));
        logs.push(chalk.red(`재도전하시겠습니까?`));
        console.clear();
        logs.forEach((log) => console.log(log));
        console.log(chalk.green(`\n1. 재도전 2. 로비로 3. 종료`));
        const choice = readlineSync.question('당신의 선택은? ');

        if (choice === '1') {
            console.clear();
            startGame(); // 게임을 다시 시작
        } else if (choice === '2') {
            console.clear();
            displayLobby(); // 로비 화면으로 돌아가기
            handleUserInput(); // 로비에서 입력을 처리
        }
        else {
            console.log(chalk.red('다음에 또 만나요.'));
            process.exit(0); // 게임 종료
        }
    }
}
const battle = async (stage, player, monster) => {
    let logs = [];
    let escape = false;
    while (player.hp > 0 && monster.hp > 0) {
        console.clear();
        displayStatus(stage, player, monster);
        console.log(chalk.yellowBright(`\n${stage} 스테이지 입니다.\n`));
        logs.forEach((log) => console.log(log));

        console.log(
            chalk.green(
                `\n1. 공격한다 2. 연속 공격(35%) 3. 방어하기(65%) 4. 도망친다(20%)`,
            ),
        );

        process.stdout.write('당신의 선택은? : ');
        const choice = readlineSync.question(); // 사용자가 입력한 값을 받음
        // 플레이어의 선택에 따라 다음 행동 처리
        logs.push(chalk.green(`${choice}를 선택하셨습니다.`));
        switch (choice) {
            case "1": {
                const Critical = 0.3; // 재미를 위한 크리티컬 요소
                let damage = player.attack(monster);
                if (Math.random() < Critical) {
                    const CriticalDamage = damage * 1.7;
                    logs.push(chalk.blue(`[크리티컬]플레이어의 공격! ${CriticalDamage.toFixed(0)}만큼 피해를 입혔습니다.`));
                    monster.hp -= (CriticalDamage - damage).toFixed(0);
                    if (monster.hp > 0) {
                        const damage = monster.attack(player);
                        logs.push(chalk.red(`몬스터의 공격! ${damage}만큼 피해를 입혔습니다.\n`));

                        if (player.hp <= 0) {  // 플레이어 체력이 0 이하인지 확인
                            playerDead(player);
                            return;
                        }
                    } else {
                        logs.push(chalk.red(`몬스터를 처치했습니다.`));
                    }
                }
                else {
                    logs.push(chalk.blue(`플레이어의 공격! ${damage}만큼 피해를 입혔습니다.`));
                    if (monster.hp > 0) {
                        const damage = monster.attack(player);
                        logs.push(chalk.red(`몬스터의 공격! ${damage}만큼 피해를 입혔습니다.\n`));

                        if (player.hp <= 0) {  // 플레이어 체력이 0 이하인지 확인
                            playerDead(player);
                            return;
                        }
                    } else {
                        logs.push(chalk.red(`몬스터를 처치했습니다.`));
                    }
                }
                break;
            }
            case "2": {
                const Probability = 0.35;
                if (Math.random() < Probability) { // 연속 공격 성공
                    const attackCount = Math.floor(Math.random() * 3) + 2; // 2~4회 공격
                    logs.push(chalk.blue(`연속 공격이 성공했습니다! ${attackCount}회 공격을 수행합니다.`));
                    for (let i = 0; i < attackCount; i++) {
                        const damage = player.attack(monster);
                        logs.push(chalk.blue(`연속 공격 ${i + 1}! ${damage}만큼 피해를 입혔습니다.`));
                        if (monster.hp <= 0) {
                            logs.push(chalk.red(`몬스터를 처치했습니다.`));
                            break;
                        }
                    }
                    if (monster.hp > 0) {
                        const damage = monster.attack(player);
                        logs.push(chalk.red(`몬스터의 반격! ${damage}만큼 피해를 입혔습니다.\n`));
                        if (player.hp <= 0) {  // 플레이어 체력이 0 이하인지 확인
                            playerDead(player);
                            return;
                        }
                    }

                } else { // 연속 공격 실패
                    logs.push(chalk.red(`연속 공격에 실패했습니다.`));
                    if (monster.hp > 0) {
                        const damage = monster.attack(player);
                        logs.push(chalk.red(`몬스터의 공격! ${damage}만큼 피해를 입혔습니다.\n`));
                        if (player.hp <= 0) {  // 플레이어 체력이 0 이하인지 확인
                            playerDead(player);
                            return;
                        }
                    }
                }
                break;
            }
            case "3": {
                const Probability = 0.65;
                if (Math.random() < Probability) { // 방어 성공
                    logs.push(chalk.blue(`방어 성공! 몬스터의 공격을 완전히 막아냅니다.`));
                } else { // 방어 실패
                    logs.push(chalk.red(`방어 실패.. 몬스터의 공격이 그대로 들어옵니다.`));
                    const damage = monster.attack(player);
                    logs.push(chalk.red(`몬스터의 공격! ${damage}만큼 피해를 입혔습니다.\n`));
                    if (player.hp <= 0) {  // 플레이어 체력이 0 이하인지 확인
                        playerDead(player);
                        return;
                    }
                }
                break;
            }
            case "4": {
                const Probability = 0.2;
                if (Math.random() < Probability) { //도주 성공
                    logs.push(chalk.green(`당신은 재빠르게 도망쳤습니다!`));
                    escape = true;
                    break;
                } else {//도주 실패
                    logs.push(chalk.red(`도주 실패.. 몬스터가 당신을 공격합니다.`));
                    const damage = monster.attack(player);
                    logs.push(chalk.red(`몬스터의 공격! ${damage}만큼 피해를 입혔습니다.\n`));
                    if (player.hp <= 0) {  // 플레이어 체력이 0 이하인지 확인
                        playerDead(player);
                        return;
                    }
                }

            }
        }
        if (escape === true) {
            break;
        }
    }

};

export async function startGame() {
    console.clear();
    let stage = 1;

    const player = new Player(stage);
    while (stage <= 10) {
        const monster = new Monster(stage);
        await battle(stage, player, monster);
        player.hp = 100 + stage * 10;
        // 스테이지 클리어 및 게임 종료 조건
        stage++;
    }
    console.clear();
    console.log(chalk.cyanBright('축하합니다! 모든 스테이지를 클리어했습니다.'));
    console.log(
        chalk.green(
            `\n1. 종료한다 2. 로비로 돌아간다`,
        ),
    );
    process.stdout.write('당신의 선택은?: ');
    const choice = readlineSync.question(); // 사용자가 입력한 값을 받음
    if (choice === '1') {
        console.log(chalk.red('다음에 또 만나요.'));
        process.exit(0); // 게임 종료
    }
    else if(choice ==='2'){
        displayLobby();
        handleUserInput();
    }
}

server.js

import chalk from 'chalk';
import figlet from 'figlet';
import readlineSync from 'readline-sync';
import { startGame } from "./game.js";

// 로비 화면을 출력하는 함수
function displayLobby() {
    console.clear();

    // 타이틀 텍스트
    console.log(
        chalk.cyan(
            figlet.textSync('RL- Javascript', {
                font: 'Standard',
                horizontalLayout: 'default',
                verticalLayout: 'default'
            })
        )
    );

    // 상단 경계선
    const line = chalk.magentaBright('='.repeat(50));
    console.log(line);

    // 게임 이름
    console.log(chalk.yellowBright.bold('CLI 게임에 오신것을 환영합니다!'));

    // 설명 텍스트
    console.log(chalk.green('옵션을 선택해주세요.'));
    console.log();

    // 옵션들
    console.log(chalk.blue('1.') + chalk.white(' 새로운 게임 시작'));
    console.log(chalk.blue('2.') + chalk.white(' 업적 확인하기'));
    console.log(chalk.blue('3.') + chalk.white(' 옵션'));
    console.log(chalk.blue('4.') + chalk.white(' 종료'));

    // 하단 경계선
    console.log(line);

    // 하단 설명
    console.log(chalk.gray('1-4 사이의 수를 입력한 뒤 엔터를 누르세요.'));
}

// 유저 입력을 받아 처리하는 함수
function handleUserInput() {
    process.stdout.write('입력: ');
    const choice = readlineSync.question(); // 사용자가 입력한 값을 받음

    switch (choice) {
        case '1':
            console.log(chalk.green('게임을 시작합니다.'));
            // 여기에서 새로운 게임 시작 로직을 구현
            startGame();
            break;
        case '2':
            console.log(chalk.yellow('구현 준비중입니다.. 게임을 시작하세요'));
            // 업적 확인하기 로직을 구현
            handleUserInput();
            break;
        case '3':
            console.log(chalk.blue('구현 준비중입니다.. 게임을 시작하세요'));
            // 옵션 메뉴 로직을 구현
            handleUserInput();
            break;
        case '4':
            console.log(chalk.red('게임을 종료합니다.'));
            // 게임 종료 로직을 구현
            process.exit(0); // 게임 종료
            break;
        default:
            console.log(chalk.red('올바른 선택을 하세요.'));
            handleUserInput(); // 유효하지 않은 입력일 경우 다시 입력 받음
    }
}

// 게임 시작 함수
function start() {
    displayLobby();
    handleUserInput();
}

// 게임 실행
start();

export { displayLobby, handleUserInput };

코드 분석

터미널에서 진행하는 로그라이크 게임 코드를 작성해 보았다.
이제 코드를 되짚어보자.

  • 우선 우리는 game.js, server.js 두가지 파일을 통해 게임을 실행하기 때문에 각 파일에 접근할 수 있어야 한다. 따라서 그러한 코드들을 추가해준다
//server.js에 있는 코드
import { startGame } from "./game.js";

export { displayLobby, handleUserInput };

//game.js에 있는 코드
import { displayLobby, handleUserInput } from "./server.js";

export async function startGame() {
  
}

이렇게 server에서 game을 사용하고 싶다면 import문을 통해 사용하고 싶은 변수명을 {} 중괄호 안에 넣어주고 game 코드 안에서는 export라는 이름을 통해 참조가 가능하도록 설정해 줘야 한다.

chalk

chalk은 번역하면 분필. 즉 터미널에서 출력되는 문자들을 chalk을 통해 색을 설정해줄 수 있다.

import chalk from 'chalk'; // 사용하기 위한 import 문

console.log(chalk.magentaBright(`\n=== Current Status ===`)); // 사용 방식

console 출력 방식

console.log("안녕하세요");

평소에는 우리가 console을 출력할 때 이와 같은 형태를 따른다.
하지만 이러한 방식은 우리가 진행하려는 로그라이크 게임에서 사용한다면 출력을 통제하기 어렵다는 단점이 있다.

  • 이 코드가 주어졌을 때 출력방식에 대해 생각해 보았다.
let logs = []; //logs 라는 배열 초기화
console.clear(); // 콘솔 메시지란을 초기화 시켜 깔끔하게 만듬

logs.forEach((log) => console.log(log)); // 화살표 함수 형태로 작성되어 logs를 
// 돌아가며 탐색 한후 log를 매개변수로 받아 console로 출력함.

logs.push(`{choice}를 선택했다.); //특정 상황에서 실행

좋은 방법인거 같다.

확률 요소 도입

게임의 재미를 위해 공격이나 방어 등 플레이어의 행동에 확률을 도입했다.

const Probability = 0.35;
    if (Math.random() < Probability) { // 연속 공격 성공
        const attackCount = Math.floor(Math.random() * 3) + 2; // 2~4회 공격
        logs.push(chalk.blue(`연속 공격이 성공했습니다! ${attackCount}회 공격을 수행합니다.`));
        for (let i = 0; i < attackCount; i++) {
            const damage = player.attack(monster);
            logs.push(chalk.blue(`연속 공격 ${i + 1}! ${damage}만큼 피해를 입혔습니다.`));
            if (monster.hp <= 0) {
                logs.push(chalk.red(`몬스터를 처치했습니다.`));
                break;
            }
         }
  • Math.random() -> 0.0~1에 무한히 가까운 수를 제공
  • 이처럼 probability(확률) 값을 설정해주고(0.35, 즉 35%)
    Math.random()가 확률값보다 낮을때 공격이 성공하고
    attackCount또한 Math.random을 통하여 공격횟수를 2~4회로 설정해 주었다.
    또한 이러한 데미지는 const damage = player.attack(monster); 를 통해 몬스터의 체력을 변경시킨다.
class Player {
    constructor(stage) {
        this.hp = 100;
        this.stage = stage;
    }

    attack(monster) {
        const basedamage = Math.floor(Math.random() * 10) + 7; // 플레이어의 공격
        const plusdamage = this.stage
        const totaldamage = basedamage + plusdamage;
        monster.hp -= totaldamage;
        return totaldamage;
    }
} 
  • 참고를 위한 player class를 첨부.
profile
Node.js_6기

0개의 댓글