[Flutter] Socket.io를 이용한 통신 보드게임 어플 만들기 (게임 로직 구현-3)

강민석·2023년 10월 15일
1

✨주사위 효과


이번 포스팅에서는 주사위 효과 로직을 구현해보겠습니다. 주사위 애니매이션 관련 코드는 앞선 포스팅을 참고해주세요. 만약 오류가 나거나 잘 안되는 부분이 있다면 댓글을 남겨주세요.

📘game.dart (주사위 관련)

import 'player.dart';
import 'center_building_card.dart';
import 'major_building_card.dart';

class MiniVillGame {
  final int numOfPlayers;
  List<Player> players = [];
  List<CenterBuildingCard> centerCards = [];
  List<MajorBuildingCard> majorCards = [];
  int currentPlayerIndex = 0;
  int opponentPlayerIndex = 0;

  bool rollDiceStatus = false;
  bool extraTurn = false;
  bool extraDice = true;

  MiniVillGame(this.numOfPlayers) {
    // Initialize players with default money and centerBuildings
    for (int i = 0; i < numOfPlayers; i++) {
      players.add(Player(id: i));
    }

    // Initialize center cards
    centerCards = [
      ...
    ];

    majorCards = [
      ...
    ];

    players[currentPlayerIndex].currentPlayerTurn = true;
  }

  void rollDice(int diceValue) {
    // 상대의 턴 실행
    // 현재 플레이어 턴 인덱스 값을 중심으로 뺏어오기 위해 선언
    opponentPlayerIndex = currentPlayerIndex;
    // 상대의 턴 먼저 모두 실행(내 다음 순서부터 순차적으로 진행)
    for (int i = 0; i < numOfPlayers - 1; i++) {
      // 상대의 인덱스 값
      opponentPlayerIndex = (opponentPlayerIndex + 1) % numOfPlayers;
      // 상대가 가진 건물 수 만큼 반복
      for (int j = 0; j < players[opponentPlayerIndex].centerBuildings.length; j++) {
        // 현재 나온 주사위 값과 내가 가진 건물의 발동 눈금이 같고 / 발동 조건이 상대 턴이라면 실행
        if (players[opponentPlayerIndex].centerBuildings[j].triggerValue
                .contains(diceValue) && players[opponentPlayerIndex]
            .centerBuildings[j].triggerTurn == TriggerTurn.OpponentTurn) {
          // 쇼핑몰 활성화한 상대라면 건물 당 1원 추가 획득
          // 발동 조건이 상대턴인 것은 카페와 패밀리 레스토랑 밖에 없음
          if(players[opponentPlayerIndex].majorBuildings[1].isActive){
            players[opponentPlayerIndex].money += 1;
          }
          // 뺏어오는 돈 만큼 반복문 실행
          for(int k=0; k< players[opponentPlayerIndex].centerBuildings[j].effectValue; k++){
            // 돈을 뺏기는 플레이어의 돈이 0원이 아니라면 실행
            if(players[currentPlayerIndex].money != 0){
              // 1원씩 뺏어오기
              players[currentPlayerIndex].money -= 1;
              players[opponentPlayerIndex].money += 1;
            }
          }
        }
      }
    }


    // -------------------------------------------------------------------------
    // 모두의 턴 실행
    // 플레이어 인원수 만큼 반복
    for (int i = 0; i < numOfPlayers; i++) {
      // i번째 플레이어의 중앙 건물 갯수 만큼 반복
      for (int j = 0; j < players[i].centerBuildings.length; j++) {
        // i번째 플레이어의 건물 발동 눈금과 주사위 값이 똑같고 / 발동조건이 모두의 턴이라면 실행
        if (players[i].centerBuildings[j].triggerValue.contains(diceValue) &&
            players[i].centerBuildings[j].triggerTurn == TriggerTurn.EveryTurn) {
          // 위 조건 만족하는 건물의 얻는 돈만큼 i번째 플레이어 돈 증가
          players[i].money += players[i].centerBuildings[j].effectValue;
        }
      }
    }


    // -------------------------------------------------------------------------
    // 나의 턴 실행
    // 턴을 진행하고 있는 플레이어의 중앙 건물 개수 만큼 반복
    for (int i = 0; i < players[currentPlayerIndex].centerBuildings.length; i++) {
      // 턴을 진행하고 있는 플레이어 건물의 발동 눈금과 주사위 값이 같고 / 발동조건이 나의 턴이라면 실행
      if (players[currentPlayerIndex]
              .centerBuildings[i]
              .triggerValue
              .contains(diceValue) &&
          players[currentPlayerIndex].centerBuildings[i].triggerTurn ==
              TriggerTurn.MyTurn) {
        // -------------------------------------------------------------------------
        // 건물의 효과가 plus라면 실행
        if(players[currentPlayerIndex].centerBuildings[i].effect =="plus"){
          // 위 조건 만족하는 건물의 얻는 돈 만큼 플레이어 돈 증가
          players[currentPlayerIndex].money += players[currentPlayerIndex].centerBuildings[i].effectValue;
          // 쇼핑몰 활성화한 상태하면 건물 당 1원씩 추가
          // 나의 턴이면서 효과가 plus인것은 빵집, 편의점 뿐
          if(players[currentPlayerIndex].majorBuildings[1].isActive){
            players[currentPlayerIndex].money += 1;
          }
        }
        // -------------------------------------------------------------------------
        // 건물의 효과가 special-building이라면 실행 (전시장, 다음 차례 가장 비싼 건물 뺏기)
        else if(players[currentPlayerIndex].centerBuildings[i].effect=="special-building"){
          int maxCostIndex = 0;
          // 내 다음 차례 플레이어 첫번째 건물의 가격을 maxCost에 저장
          int maxCost = players[(currentPlayerIndex + 1) % numOfPlayers].centerBuildings[0].cost;
          // 내 다음 차례 플레이어 두번째 건물부터 소유 중인 건물 끝까지 반복
          for (int j = 1; j < players[(currentPlayerIndex + 1) % numOfPlayers].centerBuildings.length; j++) {
            // 반복중에 만약 더 비싼 건물이 있다면 실행
            if (players[(currentPlayerIndex + 1) % numOfPlayers].centerBuildings[j].cost > maxCost) {
              // 더 비싼 건물의 가격과 해당 건물의 인덱스 값을 저장
              maxCost = players[(currentPlayerIndex + 1) % numOfPlayers].centerBuildings[j].cost;
              maxCostIndex = j;
            }
          }
          // 현재 턴을 진행하는 플레이어가 다음 차례 플레이어의 가장 비싼 건물을 추가
          players[currentPlayerIndex].centerBuildings.add(players[(currentPlayerIndex + 1) % numOfPlayers].centerBuildings[maxCostIndex]);
          // 현재 턴 다음 플레이어의 가장 비싼 건물 삭제
          players[(currentPlayerIndex + 1) % numOfPlayers].centerBuildings.removeAt(maxCostIndex);
        }
        // -------------------------------------------------------------------------
        // 건물의 효과가 special-steal이라면 실행 (TV방송국, 다음 차례 플레이어에게서 5원 뺏기)
        else if(players[currentPlayerIndex].centerBuildings[i].effect=="special-steal"){
          // 뺏어오는 돈 만큼 반복문 실행
          for(int j=0; j< players[currentPlayerIndex].centerBuildings[i].effectValue; j++){
            // 다음 차례 플레이어의 돈이 0원이 아니라면 실행
            if(players[(currentPlayerIndex + 1) % numOfPlayers].money != 0){
              // 1원씩 뺏어오기
              players[currentPlayerIndex].money += 1;
              players[(currentPlayerIndex + 1) % numOfPlayers].money -= 1;
            }
          }
        }
        // -------------------------------------------------------------------------
        // 건물의 효과가 all-steal이라면 실행 (경기장, 모든 플레이어에게서 2원 뺏기)
        else if(players[currentPlayerIndex].centerBuildings[i].effect=="all-steal"){
          // 내 다음 차례 플레이어 인덱스 값 저장
          int stealPlayerIndex = (currentPlayerIndex + 1) % numOfPlayers;
          // 플레이어 인원수 -1 만큼 반복(본인 제외)
          for(int j=0; j<numOfPlayers-1;j++){
            // 뺏어오는 돈만큼 반복문 실행
            for(int k=0; k< players[currentPlayerIndex].centerBuildings[i].effectValue; k++){
              // 돈을 뺏기는 플레이어의 돈이 0원이 아니라면 실행
              if(players[stealPlayerIndex].money != 0){
                // 1원씩 뺏어오기
                players[currentPlayerIndex].money += 1;
                players[stealPlayerIndex].money -= 1;
              }
            }
            // 그 다음 차례의 플레이어 인덱스 지정
            stealPlayerIndex = (stealPlayerIndex+1) % numOfPlayers;
          }
        }
        // 건물의 효과 중 plus-building이라는 문구가 있다면 실행 (치즈 공장, 가구 공장, 농산물 시장)
        else if(players[currentPlayerIndex].centerBuildings[i].effect.contains("plus-building")){
          // -------------------------------------------------------------------------
          // 건물의 효과 중 cheese라는 문구가 있다면 실행 (치즈 공장)
          if(players[currentPlayerIndex].centerBuildings[i].effect.contains("cheese")){
            int cheeseCount = 0;
            // 현재 플레이어가 가진 중앙 건물 개수 만큼 반복
            for(int j=0;j<players[currentPlayerIndex].centerBuildings.length;j++){
              // 현재 플레이어가 가진 건물의 종류가 가축이라면 cheeseCount 1증가
              if(players[currentPlayerIndex].centerBuildings[j].type == "가축"){
                cheeseCount +=1;
              }
            }
            // 가축 건물 개수 * 치즈 공장 얻는 돈 만큼 현재 플레이어 돈 증가
            players[currentPlayerIndex].money += cheeseCount*players[currentPlayerIndex].centerBuildings[i].effectValue;
          }
          // -------------------------------------------------------------------------
          // 건물의 효과 중 gagoo라는 문구가 있다면 실행 (가구 공장)
          else if(players[currentPlayerIndex].centerBuildings[i].effect.contains("gagoo")){
            int gagooCount = 0;
            // 현재 플레이어가 가진 중앙 건물 개수 만큼 반복
            for(int j=0;j<players[currentPlayerIndex].centerBuildings.length;j++){
              // 현재 플레이어가 가진 건물의 종류가 자원이라면 gagooCount 1증가
              if(players[currentPlayerIndex].centerBuildings[j].type == "자원"){
                gagooCount +=1;
              }
            }
            // 자원 건물 개수 * 가구 공장 얻는 돈 만큼 현재 플레이어 돈 증가
            players[currentPlayerIndex].money += gagooCount*players[currentPlayerIndex].centerBuildings[i].effectValue;
          }
          // -------------------------------------------------------------------------
          // 건물의 효과 중 farm이라는 문구가 있다면 실행 (농산물 시장)
          else if(players[currentPlayerIndex].centerBuildings[i].effect.contains("farm")){
            int farmCount = 0;
            // 현재 플레이어가 가진 중앙 건물 개수 만큼 반복
            for(int j=0;j<players[currentPlayerIndex].centerBuildings.length;j++){
              // 현재 플레이어가 가진 건물의 종류가 작물이라면 farmCount 1증가
              if(players[currentPlayerIndex].centerBuildings[j].type == "작물"){
                farmCount +=1;
              }
            }
            // 작물 건물 개수 * 농산물 시장 얻는 돈 만큼 현재 플레이어 돈 증가
            players[currentPlayerIndex].money += farmCount*players[currentPlayerIndex].centerBuildings[i].effectValue;
          }
        }
      }
    }
  }

  
  void nextTurn() {
    if(extraTurn){
      rollDiceStatus = false;
      extraTurn = false;
    }
    else{
      players[currentPlayerIndex].currentPlayerTurn = false;
      currentPlayerIndex = (currentPlayerIndex + 1) % numOfPlayers;
      players[currentPlayerIndex].currentPlayerTurn = true;
      rollDiceStatus = false;
    }
  }
}

우선적으로 전에 없던 변수들을 추가해줍니다.

opponentPlayerIndex : 카페, 패밀리 레스토랑 같이 상대턴에 효과가 실행되는 카드를 위한 턴 인덱스.

rollDiceStatus: 주사위를 굴렸는지 안굴렸는지를 확인하는 bool타입 변수
true라면 주사위 굴림, false라면 주사위 아직 안굴림

extraTurn: 주요 건물 놀이공원의 효과인 추가턴 진행을 위한 bool타입 변수
true라면 추가턴 진행 가능, false라면 추가턴 진행 불가

extraDice: 주요 건물 라디오 방송국 효과인 추가 주사위 기회를 위한 bool타입 변수
true라면 추가 주사위 굴릴 수 있음, false라면 추가 주사위 못굴림 (한번 더 굴릴지 말지 선택할 수 있어야함)

해당 변수들을 이용하여 주사위 관련 로직을 구성합니다.

그리고 rollDice(int diceValue)함수를 통해 플레이어들이 가진 카드 효과와 주사위 결과를 이용하여 각각의 플레이어들에게 돈을 지급합니다. 카드 발동 순서는 게임 규칙에 따라 실행해 줍니다.

카드 발동 순서
상대의 턴 발동카드 -> 모두의 턴 발동카드 -> 나의턴 발동 카드

특히 돈을 뺏을때는 뺏기는 플레이어의 돈보다 뺏는 돈이 더 커도 0원이 될 때까지 뺏어야하기 때문에 반복문을 이용하여 0원이 될 때까지 뺏을 수 있도록 하였습니다.

rollDice함수는 코드 리팩토링 과정이 완벽하지 않아 굉장히 복잡해보입니다. 주석을 통해 보기 쉽게 정리하였으니 천천히 코드를 읽으면서 분석해주십시오.


추가로 extraTurn변수가 true일 때 nextTurn()함수가 호출되면 턴을 넘기지 않고 추가턴을 진행해야 하기 때문에 nextTurn()함수를 수정해주었습니다.

📕diceEvent(int numberOfDice)

void diceEvent(int numberOfDice) {
    if (!game.rollDiceStatus) {
      if (!game.players[game.currentPlayerIndex].majorBuildings[0].isActive &&
            numberOfDice == 2) {
        _showCustomDialog(context, "기차역을 먼저 구매해야 합니다.");
      } else {
        setState(() {
          diceResult = 0;
          numberOfDiceToRoll = numberOfDice;
        });
        _controller.forward();
      }
    } else {
      _showCustomDialog(context, "주사위를 이미 굴리셨습니다.");
    }
  }

주사위 애니매이션에서 임시로 짰던 주사위 이벤트를 수정해줍니다. rollDiceStatustrue라면 주사위를 못굴리도록 코드를 짜고 주사위를 2개씩 굴리기 위해선 기차역을 구매해야지만 굴릴 수 있도록 하였습니다. 그리고 주사위를 굴릴때마다 이전에 굴렸던 주사위 결과를 0으로 초기화하여 게임이 매끄럽게 흘러가도록 해주었습니다.

📘_controller.addStatusListener

_controller.addStatusListener((status) async {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        // 주사위 결과 표시
        setState(() {
          diceResult = diceNumber1 + (numberOfDiceToRoll == 2 ? diceNumber2 : 0);
        });
		// 라디오 방송국 비활성화 시 바로 주사위 이벤트 진행
        if (!game.players[game.currentPlayerIndex].majorBuildings[3].isActive) {
          // 주사위 결과에 맞게 돈 배분
          game.rollDice(diceResult);
          // 더이상 주사위 못굴리게 주사위 굴렸다고 처리하기
          game.rollDiceStatus = true;
		
          // 놀이공원 구매한 상태에서 주사위 2개 결과값이 같다면 추가턴 진행 가능하게 하기
          extraTurnEvent();
        } 
        // 라디오 방송국 활성화한 상태로 처음 주사위 굴렸을 때 주사위 한번 더 굴릴 것인지
        // 물어보는 코드
        else if (game.players[game.currentPlayerIndex].majorBuildings[3].isActive &&
            game.extraDice) { 
          game.extraDice = false;
          // 주사위 확정할 것인지 한번 더 던질것인지 물어보는 대화상자 열기
          bool shouldRollAgain = await _showDiceRollConfirmationDialog(context);
          // 대화상자에서 주사위 확정했을 때 실행
          if (shouldRollAgain) {
            game.rollDice(diceResult);
            game.rollDiceStatus = true;
            // 다음에도 추가턴 진행 가능하도록 true로 값변경
            game.extraDice = true;
			// 놀이공원 구매한 상태에서 주사위 2개 결과값이 같다면 추가턴 진행 가능하게 하기
            extraTurnEvent();
          }         
        } 
        // 라디오 방송국 활성화한 상태로 주사위 한번 더 굴렸을떄 바로 주사위 이벤트 적용
        else {
          game.rollDice(diceResult);
          game.rollDiceStatus = true;
          game.extraDice = true;
		  // 놀이공원 구매한 상태에서 주사위 2개 결과값이 같다면 추가턴 진행 가능하게 하기
          extraTurnEvent();
        }
        setState(() {});
      }
    });

_controller.forward();코드로 애니매이션을 실행시키고 애니매이션이 끝났을 때 실행되는 코드입니다.


주사위가 위로 올라갔다가 떨어지는 효과를 나타내기 위해서 다음 코드를 추가하였습니다.

if (status == AnimationStatus.completed) {
   _controller.reverse();
}


놀이공원을 구매한 상태에서 주사위 2개 결과값이 같다면 추가턴 진행 가능하게 하는 함수인 extraTurnEvent함수는 다음과 같이 코드를 짰습니다. 해당 함수는 initState()함수 아래에 선언해주십시오.

void extraTurnEvent(){
    if (game.players[game.currentPlayerIndex].majorBuildings[2]
        .isActive &&
        diceNumber1 == diceNumber2 &&
        numberOfDiceToRoll == 2) {
      game.extraTurn = true;
      _showCustomDialog(context, "한턴을 추가로 진행할 수 있어요!");
    }
  }


라디오 방송국이 활성화가 되면 주사위를 한번 더 굴릴 것인지 물어보고 한번 더 굴린다면 주사위 효과를 적용하지 않고 주사위를 한번 더 굴릴 수 있게 해주고 주사위를 확정한다면 주사위를 더 못굴리게 하고 주사위 효과를 적용하기 위해 코드를 짰습니다. 주사위를 더 굴릴 것인지 확정할 것인지 물어보는 대화상자 코드는 다음과 같이 짰습니다.

Future<bool> _showDiceRollConfirmationDialog(BuildContext context) async {
    double screenHeight = MediaQuery.of(context).size.height;
    double screenWidth = MediaQuery.of(context).size.width;
    bool? result = await showDialog<bool>(
      context: context,
      builder: (context) => Dialog(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(20.0),
        ),
        elevation: 5.0,
        child: Container(
            width: screenWidth * 0.5,
            height: screenHeight * 0.3,
            padding: const EdgeInsets.all(16.0),
            child: Column(
              mainAxisSize: MainAxisSize.min, // 컨텐츠에 따라 크기 조절
              children: [
                Expanded(
                  flex: 6,
                  child: AutoSizeText(
                    '주사위를 다시 굴리시겠습니까?',
                    minFontSize: 5,
                    style:
                        TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
                    textAlign: TextAlign.center,
                  ),
                ),
                Expanded(flex: 1, child: SizedBox()),
                Expanded(
                    flex: 4,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                      children: [
                        ElevatedButton(
                          onPressed: () async {
                            Navigator.of(context).pop(false); // 다이얼로그 닫기
                          },
                          child: AutoSizeText(
                            '다시 굴리기',
                            minFontSize: 5,
                          ),
                        ),
                        ElevatedButton(
                          onPressed: () async {
                            Navigator.of(context).pop(true); // 다이얼로그 닫기
                          },
                          child: AutoSizeText(
                            '확정하기',
                            minFontSize: 5,
                          ),
                        ),
                      ],
                    )),
              ],
            )),
      ),
    );

    return result ?? false;
  }

확정하기 버튼을 누른다면 shouldRollAgain변수에 true값이 반환되어 주사위 효과가 바로 적용되고 다시 굴리기 버튼을 누른다면 shouldRollAgain변수에 false값이 반환되어 주사위 효과를 적용하지 않은채로 주사위를 한번 더 굴릴 수 있도록 해줍니다.

해당 코드는 GameScreen의 커스텀 위젯들을 선언하는 부분에 넣어주면 됩니다.

🔍추가 수정내용

ExpandedCardOverlay(
    cardImagePath: selectedCardPath!,
    onClose: () {
       setState(() {
          selectedCardPath = null;
      });
    },
    onPurchase: () {
       setState(() {
       	 if (game.rollDiceStatus) {
        	... // 기존 내용
         }else {
            _showCustomDialog(context, "주사위를 먼저 굴리십시오.");
         }
      });
    },
  )
ElevatedButton(
      onPressed: () {
        // 건물 구매 안하고 턴넘기기
        setState(() {
            if (game.rollDiceStatus) {
              game.nextTurn();
            } else {
              _showCustomDialog(context, "주사위를 먼저 굴리십시오.");
            }
        });
      },
      child: AutoSizeText(
        '턴 넘기기',
        minFontSize: 5,
        style: TextStyle(fontSize: 15.0),
        // 시작할 폰트 크기
        maxLines: 1, // 최대 줄 수
      ),
    ),

주사위를 굴리지 않은 상태로 건물을 구매하거나 턴을 넘길 수 없는 것이 게임 규칙이기 때문에 코드들을 조금 수정해줘야 합니다.




주사위 로직을 끝으로 게임 로직 구현이 모두 끝났습니다!! 게임 로직 관련해서 질문이나 지적할 만한 것들이 있다면 언제든지 댓글 남겨주세요!


이제 남은 것은 socket.io 라이브러리를 이용하여 기기간 통신이 가능하도록 하는 것입니다. 다음 포스팅에는 node.js, express, socket.io를 이용하여 백엔드를 구현해보도록 하겠습니다😎

profile
백석대학교 소프트웨어학과 4학년 재학중

0개의 댓글

관련 채용 정보