복잡한 렌더링 기능이 들어있는 것이 아닌 콘솔을 이용해 로그라이크 게임을 만들어볼 예정입니다.
로그라이크(Roguelike)는 1980년대 초반에 출시된 "Rogue"라는 게임에서 유래한 게임 장르입니다. 이 장르의 게임은 무작위로 생성된 던전이나 맵, 영구적인 캐릭터 죽음(퍼머데스, Permadeath), 턴 기반 전투, 타일 기반 그래픽 등 여러 핵심 특징을 가지고 있습니다. 플레이어는 주어진 자원을 활용해 던전을 탐험하며 적을 물리치고, 아이템을 수집하며, 최종 목표를 달성하기 위해 전략적인 결정을 내려야 합니다. 로그라이크 게임의 핵심 매력은 높은 난이도와 반복 플레이를 통해 얻는 성취감에 있습니다.
게임을 진행하면서 얼마나 진척도가 있었는지 보여주는 척도입니다.
유닛이 갖게 되는 전투에 영향을 끼치는 스탯입니다.
텍스트를 꾸미기 위해 'chalk'를, 입력을 위해 'readline-sync'를 설치 했다.
npm i chalk
npm i readline-sync
분명 다른 한글은 잘 출력하는데 readline-sync를 통해서 출력된 한글만 인코딩이 잘못되어 나오더라고요. 해결방법은 의외로 간단합니다.
윈도우의 경우 "국가 또는 지역"의 관리자 옵션 > 시스템 로캘 변경 > Beta: 세계 언어 지원을 위해 Unicode UTF-8 사용을 체크후 재부팅 하면됩니다.
readline-sync의 각 입력 메소드를 래핑하여 출력만 별도로 하는 것입니다.
console.log()를 사용하면 편하지만 이는 출력후 바로 줄바꿈이 이루어지므로 줄바꿈 없이 출력후 사용자가 엔터를 누르면 자연스럽게 포인터가 줄바꿈 되도록 process.stdout.write()를 사용합니다.
이 방법은 OS 설정이 되어있지 않아도 유효하므로 사용자 친화적 코드로 생각할 수 있습니다.
import readlineSync from ('readline-sync');
//OS에 따라선 readlineSync가 출력한 텍스트에 인코딩이 문제가 발생할 수 있으므로 따로 래핑했습니다.
class Input {
constructor() {
throw new Error('This class cannot be instantiated.');
}
static question(query, options) {
process.stdout.write(query);
return readlineSync.question('', options);
}
static keyIn(query, options) {
process.stdout.write(query);
return readlineSync.keyIn('', options);
}
static questionInt(query, options) {
process.stdout.write(query);
return readlineSync.questionInt('', options);
}
static questionFloat(query, options) {
process.stdout.write(query);
return readlineSync.questionFloat('', options);
}
static questionNewPassword(query, options) {
process.stdout.write(query);
return readlineSync.questionNewPassword('', options);
}
static keyInYN(query) {
process.stdout.write(query);
return readlineSync.keyInYN('');
}
static keyInYNStrict(query) {
process.stdout.write(query);
return readlineSync.keyInYNStrict('');
}
static keyInSelect(items, query, options) {
process.stdout.write(query);
return readlineSync.keyInSelect(items, '', options);
}
static keyInPause(query) {
process.stdout.write(query);
return readlineSync.keyInPause('');
}
static keyInContinue(query, options) {
process.stdout.write(query);
return readlineSync.keyInContinue('', options);
}
static prompt(query, options) {
process.stdout.write(query);
return readlineSync.prompt('', options);
}
}
export default Input;
C나 JAVA 같은 것으로 키보드 입력을 받아본 사람들은 다 겪어봤을텐데요. 엔터를 두번이나 쳐야 입력이 처리되는 문제를 겪어봤을 것입니다. 이번에 저도 문제를 겪어서 수차례 이것저것 해봤는데 결론은 코드 상의 문제는 아님을 알아냈고 방법을 찾았습니다.
프로젝트 최상위(루트) 디렉토리에 nodemon.json을 만들어 설정값을 적용해야합니다.
{
"stdin": false
}
이 값은 nodemon이 표준 입력에 대한 감시를 하지 않도록 하는 것 입니다.
쉽게 말해 프로세스의 표준 입력 스트림을 감시하지 않도록 하여(키보드 입력을 감시하지 않도록 하여) 입력중 예기치 못한 문제가 발생하는 것을 방지한 것입니다.
'nodemon'과 'readline-sync'두 모듈이 입력에 대해 충돌이 발생하여 문제가 생긴 것 같습니다.
'chalk'를 이용해 터미널을 꾸밀 수 있어서 좋다 생각은 들었는데 수기로 하드코딩 하려니 코드 가독성도 떨어지는 것 같아서 테이블을 이용하기로 해봤습니다.
간단하게 csv를 이용해 다음 표를 토대로 텍스트를 매핑할 것 입니다.
id | text |
---|---|
키 | 포맷 설정한 텍스트 |
특수 처리는 중괄호로 감쌉니다.
색상은 반드시 최상위로 둬야하며 작성법은 다음과 같습니다.
"{색상: 적용할 텍스트}"
변수를 적용하려면 중괄호를 두번 감싸야 합니다.
"{{name}}은 인사를 했어요."
색상을 적용하려면 색 괄호를 먼저합니다.
"{blue:{{name}}}은 인사를 했어요."
csv를 사용하기로 했으니 csv-parser를 설치합니다.
npm i csv-parser
이를 토대로 코드를 작성하면 완료입니다~
class 옆에 extends Singleton이란 것이 있는데 이건 다음 차례에 설명하겠습니다.
import fs from 'fs';
import csv from 'csv-parser';
import chalk from 'chalk';
import Singleton from './Singleton.js';
class TextTable extends Singleton {
#textTable;
constructor() {
super();
this.#textTable = {};
}
// CSV 파일을 비동기로 읽어오는 메서드
async Load(filePath) {
return new Promise((resolve, reject) => {
fs.createReadStream(filePath)
.pipe(csv())
.on('data', (row) => {
this.#textTable[row.id] = row.text.replace(/^"|"$/g, '');
})
.on('end', () => {
// console.log('CSV file successfully processed.');
resolve();
})
.on('error', (err) => {
reject(err);
});
});
}
// 텍스트를 포맷팅하고 색상 적용하는 메서드
FormatText(id, variables = {}) {
if (!this.#textTable[id]) {
return 'Text not found';
}
let text = this.#textTable[id];
// 텍스트 내 변수 치환
for (let key in variables) {
text = text.replace(`{{${key}}}`, variables[key]);
}
// 텍스트에서 색상 적용 부분 파싱 및 적용
text = text.replace(/{(.*?):(.*?)}/g, (match, color, content) => {
// 내용이 또 다른 변수일 경우 변수 치환
if (content in variables) {
content = variables[content];
}
// 색상 적용
return chalk[color](content);
});
return text.replace(/\\n/g, '\n');;
}
}
export default new TextTable();;
싱글톤은 쉽게말해서 도플갱어가 돌아다니지 않도록 하는 것 입니다. 위에 TextTable.js에서 적용한 예입니다.
TextTable을 불러올 때마다 new TextTable();을 하므로 매번 생성하니 서로 다른 TextTable이 돌아다닐 수 있습니다. 하지만 Singleton을 적용함으로서 생성되는 대신에 이미 생성된 원본을 참조하도록 작업해준 것 입니다.
class Singleton {
constructor() {
if (this.constructor.instance) {
return this.constructor.instance;
}
this.constructor.instance = this;
}
}
export default Singleton;
코드를 보면 아주 심플합니다. instance(일꾼)가 있으면 바로 주고 없으면 생성자를 통해서 새로 생성된 일꾼을 준다.
이렇게하면 하나의 TextTable을 이곳 저곳에서 일관성 있게 쓸 수 있게 됩니다.
그 외에도 전략 패턴이나 캐릭터를 위한 클래스 디자인 등 여러가지를 했지만 오늘자 글은 줄이려 합니다. 작업할 시간이 부족해서요
다음 글에서 이어서 나가겠습니다.