[TIL] RPG 게임 - 필수 기능

티라노·2024년 11월 5일
0

Today I Learned

목록 보기
13/21

기본 클래스와 메서드, 필수 기능 작성

Game 메서드 구상

Game 클래스 객체 안에서 함수 흐름을 정리해보자.

  • startGame : 게임이 끝날 때까지 아래 코드를 반복한다.
    • getRandomMonster : 배틀에 나올 몬스터를 고르고 리스트에서 제거한다.
    • battle : 배틀 시작, 턴 돌리기, 몬스터를 물리칠 시 안내문을 출력한다.
    만약 게임이 끝나면 승패를 가린 뒤 파일에 결과를 저장하고 끝낸다.

이 때 몬스터를 확실히 물리치기 전에 리스트에서 제거하면 안 되는 건가 싶을 수 있지만, 어차피 몬스터를 물리치지 못하고 죽으면 재도전 기회 없이 게임이 끝나기 때문에 상관 없다.


Game 메서드 작성

전투는 CharacterMonster 클래스 내의 공격, 방어 메서드로 진행한다. 주의할 점은 두 클래스는 서로 Game 클래스 안에서 간접적인 영향만 끼칠 수 있다는 점이다.
예시 ) 캐릭터가 방어를 한다면, 몬스터의 공격을 줄이는 것이 아니라 그만큼 본인의 체력을 올린다.

물론 Game 메서드에서 영향을 주고 받을 수 있지만 그러면 switch 문 안에 너무 긴 코드를 써야 하기 때문에 지양하겠다.

// 공격
void attackMonster(Monster monster){
  monster.hp-=atk;
}
// 방어
void defend(int monsterAtk){
  hp+=Random().nextInt(def-4)+5;
}

방어를 선택하면 일정량의 체력을 얻는다. 항상 같은 값으로 방어하는 것보다 변동 있는 편이 좋을 것 같아서 본인의 방어력을 기준으로 Random() 을 돌려 나온 값만큼 체력을 추가하도록 구현했다.

문제

방어 최저값을 5로 두었는데, 방어력이 5보다 낮은 캐릭터가 존재할 수 있다. 최대 방어력을 기준으로 해서 일정 퍼센트로 최저치를 보장하고 싶다.

해결

 void defend(){
    hp+=Random().nextInt(def-(def~/3)+1)+(def~/3);
  }

나눗셈 결과의 정수값만 반환하는 ~/ 연산자를 이용해서 다시 작성해보았다. 그런데 이러면 방어력이 3의 배수가 아닌 경우 손해가 있다.
이 게임에서는 방어력을 3배수로 맞추는 게 좋을 것 같다.

문제

플레이어가 자신의 턴에 1이나 2를 입력해서 행동을 선택할 수 있는데, 이외 값을 입력하면 그대로 턴이 넘어간다.

해결


while 문으로 올바르지 않은 값(정수가 아님, 1 또는 2가 아님)을 걸러내는 과정을 추가했다.

여기까지 개발하고 나니 문제가 2개 있었다.

1. 다음 몬스터와 싸우시겠습니까? 메시지에서 'n'을 선택해도 게임이 끝나지 않는다.

  • 게임을 반복하는 while 문에서 조건이 true 로 되어있었다. while 안에 switch 가 있을 때는 break 를 사용해도 반복문을 탈출할 수 없다. 대신 조건에 boolean 변수를 따로 선언해서 두는 편이 좋다. (수정 완료)

2. 이미 물리친 몬스터가 또 등장한다.

    // 수정 전 getRandomMonster
    Monster getRandomMonster(){
    	// 몬스터 뽑기
        int monsterNum=Random().nextInt(monsterList.length);
        // 뽑은 몬스터 저장
        Monster monsterPicked=monsterList[monsterNum];
        // 뽑은 몬스터 삭제
        monsterList.remove(monsterNum);
        return monsterPicked;
      }
  • remove 는 배열의 특정 값을 제거, removeAt 은 인덱스를 제거한다.
    위 코드에서 monsterNum 은 인덱스 값을 의미하기 때문에 remove 가 아닌 removeAt 을 쓰는 것이 적절하다. (수정 완료)

필수 기능 1 - 파일 읽기


▲ 파일 내 데이터

파일 종류는 txt 이고 데이터는 csv 양식으로 적혀 있다.
csv 파일이 아니기 때문에 Dart에 내장되어있는 csv 패키지를 활용할 수는 없을 것이다. 그래서 아래 과정을 거쳐야 한다.

  1. 파일 전체를 String 으로 읽어온다.
  2. split('\n') 으로 쪼갠 문단을 List<String>에 넣는다.
  3. split(',') 으로 각 문단을 한 번 더 쪼개서 List<List<String>> 에 넣는다.
  4. 체력, 공격력, 방어력 수치는 정수로 형변환한다.
  5. 각 인덱스별로 Monster 객체를 하나씩 형성해서 List<Monster> 에 넣는다.

코드 길이가 길어지는 것 같아서, 혹시 라이브러리나 함수로 간략하게 줄이는 방법이 있을지 찾아보면 좋을 것 같다.
-> transform 함수를 사용할 수도 있지만 우선은 split 만으로 구현했다.

먼저 클래스의 생성자에 파일 객체를 만들고 readAsStringSync() 를 통해 읽은 값을 변수에 저장한다. 이 변수들은 어차피 그대로 쓰지 않고 가공해서 쓸 것이기 때문에 클래스 멤버 변수로 선언해줄 필요는 없다.
(잘못해서 멤버 변수에 한 번, 생성자에 한 번씩 선언 두 번 했다가 테스트가 이상하게 나와서 고민했다.)

// File reading
var characterFile = File('assets/txt/characters.txt');
var monsterFile = File('assets/txt/monsters.txt');

StringcharacterContents = characterFile.readAsStringSync();
String monsterContents = monsterFile.readAsStringSync();

그 다음으로 2~5번 과정을 진행한다.

// (2) 개행 문자로 split
List<String> monsterOnceSplited=monsterContents.split('\n');
List<List<String>> monsterTable=[];

// (3) 쉼표로 split
for(int i=1;i<monsterOnceSplited.length;i++){
	monsterTable.add(monsterOnceSplited[i].split(','));
	// (4) 정수 형변환
	var temp=Monster(monsterTable[i-1][0], 	int.parse(monsterTable[i-1][1]), 
		int.parse(monsterTable[i-1][2]));
	// (5) List<Monster>에 투입
	monsterList.add(temp);
}

파일을 문단별로 잘라서 MonsterOnceSplited 에 저장한 뒤 각 인덱스의 값을 또 쉼표로 잘라서 monsterTable 에 넣는다. 이때 2차원 리스트가 필요해서 이름에 'Table'을 넣었다.
이때 유의할 점은 csv식 구성에서 첫 번째 행은 각 항목의 분류명(name, hp 등...)으로 이루어질 때가 있다는 점이다.
따라서 이 부분을 건너뛰고 monsterTable 의 인덱스[1]부터 시작해야 한다.
분류명이 포함되지 않은 파일이라면 상관 없다.

캐릭터 정보는 몬스터와 달리 하나만 존재하므로 비슷한 과정을 거치되 for 문을 사용할 필요는 없다.


필수 기능 2 - 사용자에게 캐릭터 이름 입력받기

// 기본 형태
print('캐릭터의 이름을 입력하세요.');
String name=stdin.readLineSync().toString();
var game=Game(name);

이제 이 코드에 정규표현식을 적용해서 입력을 제한해보도록 하겠다.
입력 가능한 문자는 영문 대소문자이다. (한글 입력 시 오류 발생)

과정

  1. 먼저 RegExp 클래스인 reg 객체를 선언한다.
  2. 사용자에게 이름을 입력받는다.
  3. hasMatch() 로 사용자의 입력값이 reg에서 허용한 범위 내에 속하는지 알아본다.
  4. 알맞은 값일 시 이름 저장, 그렇지 않을 시 2번으로 돌아간다.

이 때 reg

RegExp reg=RegExp(r'^[a-zA-Z]+$')

처럼 선언해주었다. 프로그램에 적용하면 문제 없이 작동한다. 테스트에는 없지만 영문 대소문자와 불가능한 문자를 섞어 써도 정상적으로 사용 불가 처리가 된다.


필수 기능 3 - 파일에 저장하기

게임이 끝나면 파일에 저장할지 여부를 묻는데, 이를 위해 우선 승패를 가려야 한다.

String fightResult='승리';
if(character.hp<=0&&monsterList.isNotEmpty){
  fightResult='패배';
}
else if(character.hp<=0&&monsterList.isEmpty){
  fightResult='무승부';
}

기본적으로 게임 결과를 '승리'로 설정해두고 이후 캐릭터의 체력과 잔여 몬스터의 수에 따라 패배나 무승부로 바꾼다.

print('결과를 저장하시겠습니까? [y/n]');
  String cmd;
  try{
    cmd=stdin.readLineSync().toString();}
    catch(e){
      print('입력이 잘못되었습니다.');
      continue;
    }

저장 여부를 입력받을 때는 예외 처리를 했고, 이후 switch 문으로 결과를 저장할지 말지 판별한다.
(그런데 지금 보니 예외가 발생하지 않는 상황에서 잘못 선언된 것 같다. 하단 메모에 적어두었다.)

var resultFile = File('assets/txt/result.txt');
String contents='이름: ${character.name}, 남은 체력: ${character.hp}, 
	결과: $fightResult/n';
resultFile.writeAsStringSync(contents);
print('결과를 저장하였습니다.');

File 클래스의 writeAsStringSync 를 이용해서 작성할 수 있었다.
처음 파일 불러오기를 할 때는 헤맸는데, 작성은 오히려 읽기와 비슷한 과정으로 이루어져서 전보다 금방 구현할 수 있었다.

메모

  • 이번에는 동기 프로그래밍으로 File reading 을 구현했지만, 내일은 강의에서 배운 async 라이브러리의 클래스로 바꿔서 비동기로 다시 구현해볼 예정이다.
  • 게임 결과를 저장할지 말지 여부를 입력받는 부분에서 예외처리가 잘못되었으니 수정해야 한다.

0개의 댓글