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

강민석·2023년 10월 8일
1

✨게임 규칙 정의 및 플레이어 턴 구현

이번 포스팅에서는 게임 규칙을 제대로 정의하고 플레이어 턴을 구현하는 내용을 다루겠습니다. 기본 레이아웃을 구현하지 못했다면 앞선 포스팅들을 보고 구현해주십시오. 만약 오류가 나거나 잘 안되는 부분이 있다면 댓글을 남겨주세요.

🎨게임 규칙 정의

미니빌 카드 구성 및 정보

게임 준비:

  • 각 플레이어는 초기 자금으로 동전 3개를 받습니다.
  • 각 플레이어는 초기 건물로 "밀밭"과 "빵집" 카드를 하나씩 받습니다.
  • 주요 건물 4종 (기차역, 쇼핑몰, 놀이공원, 라디오 방송국)이 각 플레이어 앞에 뒤집어져 놓입니다. 이 건물들은 게임 도중 구매하여 앞면으로 뒤집을 수 있다.
  • 주요 건물을 앞면으로 뒤집으면 건물의 효과가 적용된다.
  • 나머지 건물 카드들은 중앙에 공개적으로 놓이며, 플레이어들은 이 카드들을 구매할 수 있습니다.

진행 방식:

  • 플레이어는 차례대로 주사위를 굴립니다.
  • 주사위를 굴린 결과와 건물 상황에 따라 플레이어는 돈을 얻거나 빼앗길 수 있습니다.
  • 주사위를 굴린 후, 플레이어는 중앙에 놓인 건물 카드 중 하나를 구매하거나, 주요 건물 중 하나를 구매할 수 있습니다. (단, 구매 비용이 충분해야 합니다.)
  • 구매하지 않을 경우, 차례를 다음 플레이어에게 넘깁니다.

주요건물 카드 4장(각 플레이어 뒷면으로 기본 지급)

기차역(4원)
효과: 주사위를 2개 굴릴수 있다.

쇼핑몰(10원)
효과: 카페 or 서비스의 경우, 카드당 1원씩 추가 획득

놀이공원(16원)
효과: 굴린 주사위 눈금이 같은 경우, 1턴 추가 진행

라디오 방송국(22원)
효과: 주사위를 1회 다시 굴릴 수 있음

중앙 건물 카드

중앙 건물 종류
작물, 가축, 자원, 서비스, 공장, 커피, 시장, 특수

밀밭/center_card_1 (작물)

  • 가격: 1원
  • 발동 눈금: 1
  • 발동 조건: 모두의 턴
  • 효과: 1원 획득
  • 구매 가능 개수: 6개

목장/center_card_2 (가축)

  • 가격: 1원
  • 발동 눈금: 2
  • 발동 조건: 모두의 턴
  • 효과: 1원 획득
  • 구매 가능 개수: 6개

빵집/center_card_3 (서비스)

  • 가격: 1원
  • 발동 눈금: 2~3
  • 발동 조건: 나의 턴
  • 효과: 1원 획득
  • 구매 가능 개수: 6개

카페/center_card_4 (커피)

  • 가격: 2원
  • 발동 눈금: 3
  • 발동 조건: 상대의 턴
  • 효과: 1원 뺏어오기
  • 구매 가능 개수: 6개

편의점/center_card_5 (서비스)

  • 가격: 2원
  • 발동 눈금: 4
  • 발동 조건: 나의 턴
  • 효과: 3원 획득
  • 구매 가능 개수: 6개

숲/center_card_6 (자원)

  • 가격: 3원
  • 발동 눈금: 5
  • 발동 조건: 모두의 턴
  • 효과: 1원 획득
  • 구매 가능 개수: 6개

전시장/center_card_7 (특수)

  • 가격: 8원
  • 발동 눈금: 6
  • 발동 조건: 나의 턴
  • 효과: 내 다음 턴 플레이어의 가장 비싼 카드 뺏어오기
  • 구매 가능 개수: 4개

TV 방송국/center_card_8 (특수)

  • 가격: 7원
  • 발동 눈금: 6
  • 발동 조건: 나의 턴
  • 효과: 내 다음 턴 플레이어에게서 5원 뺏어오기
  • 구매 가능 개수: 4개

경기장/center_card_9 (특수)

  • 가격: 6원
  • 발동 눈금: 6
  • 발동 조건: 나의 턴
  • 효과: 모든 상대에게 2원씩 뺏어오기
  • 구매 가능 개수: 4개

치즈 공장/center_card_10 (공장)

  • 가격: 5원
  • 발동 눈금: 7
  • 발동 조건: 나의 턴
  • 효과: 가축카드 1장 당 3원 획득
  • 구매 가능 개수: 6개

가구 공장/center_card_11 (공장)

  • 가격: 3원
  • 발동 눈금: 8
  • 발동 조건: 나의 턴
  • 효과: 자원카드 1장 당 3원 획득
  • 구매 가능 개수: 6개

광산/center_card_12 (자원)

  • 가격: 6원
  • 발동 눈금: 9
  • 발동 조건: 모두의 턴
  • 효과: 5원 획득
  • 구매 가능 개수: 6개

패밀리 레스토랑/center_card_13 (커피)

  • 가격: 3원
  • 발동 눈금: 9~10
  • 발동 조건: 상대의 턴
  • 효과: 2원 뺏어오기
  • 구매 가능 개수: 6개

사과밭/center_card_14 (작물)

  • 가격: 3원
  • 발동 눈금: 10
  • 발동 조건: 모두의 턴
  • 효과: 3원 획득
  • 구매 가능 개수: 6개

농산물 시장/center_card_15 (시장)

  • 가격: 2원
  • 발동 눈금: 11~12
  • 발동 조건: 나의 턴
  • 효과: 작물카드 1장 당 2원 획득
  • 구매 가능 개수: 6개

승리 조건:

  • 플레이어 중 먼저 주요 건물 4종을 모두 구매한 사람이 승리합니다.

🎮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 = [];
  
  // 현재 플레이어 턴 인덱스 (0번째 플레이어부터 시작)
  int currentPlayerIndex = 0;

  MiniVillGame(this.numOfPlayers) {
    // MainScreen에서 설정한 인원수 만큼 플레이어 객체 생성
    for (int i = 0; i < numOfPlayers; i++) {
      players.add(Player(id: i));
    }

    centerCards = [
      ...
    ];

    majorCards = [
      ...
    ];

	// 게임 시작 시 카드 나눠주기
    for (int i = 0; i < numOfPlayers; i++) {
      players[i].centerBuildings.add(centerCards[0]);
      players[i].centerBuildings.add(centerCards[2]);

      players[i].majorBuildings.add(MajorBuildingCard(
          name: '기차역',
          cost: 4,
          backImagePath: 'assets/major_card/major_card_back_0.jpg',
          frontImagePath: 'assets/major_card/major_card_front_0.jpg'));
      players[i].majorBuildings.add(MajorBuildingCard(
          name: '쇼핑몰',
          cost: 10,
          backImagePath: 'assets/major_card/major_card_back_1.jpg',
          frontImagePath: 'assets/major_card/major_card_front_1.jpg'));
      players[i].majorBuildings.add(MajorBuildingCard(
          name: '놀이공원',
          cost: 16,
          backImagePath: 'assets/major_card/major_card_back_2.jpg',
          frontImagePath: 'assets/major_card/major_card_front_2.jpg'));
      players[i].majorBuildings.add(MajorBuildingCard(
          name: '라디오 방송국',
          cost: 22,
          backImagePath: 'assets/major_card/major_card_back_3.jpg',
          frontImagePath: 'assets/major_card/major_card_front_3.jpg'));
    }

  }

  void rollDice(int diceValue) {
    // 주사위 효과 이벤트
  }

  void nextTurn() {
    // 턴 넘기기 이벤트
    players[currentPlayerIndex].currentPlayerTurn = false;
    currentPlayerIndex = (currentPlayerIndex + 1) % numOfPlayers;
    players[currentPlayerIndex].currentPlayerTurn = true;
  }

  void socketNextTurn(){
    // 서버 통신으로 턴 넘기기 이벤트
  }
}

...으로 생략된 부분은 게임 구성 세팅 포스팅을 참고해주세요.

플레이어의 턴은 currentPlayerIndex변수 값을 이용해 구현할 것입니다. currentPlayerIndex가 0이라면 0번째 플레이어의 차례, currentPlayerIndex가 1이라면 1번째 플레이어의 차례가 되도록 코드를 짰습니다.

nextTurn()함수가 호출될 때 마다 currentPlayerIndex의 값을 1만큼 늘립니다. 여기서 currentPlayerIndex의 값이 인원수를 초과하면 안되기 때문에 %연산자를 이용해줬습니다.

예를들어, 인원수가 4명이고 currentPlayerIndex가 3일때 다음 차례가 되면 currentPlayerIndex가 다시 0이 되어야 하기 때문에 currentPlayerIndex = (currentPlayerIndex + 1) % numOfPlayers;부분을 통해 currentPlayerIndex의 값을 적절히 조절하고 있습니다.

📕_playerWidget(턴 구현)

 Widget _playerWidget(
      String playerName, int playerID, int currentPlayerIndex) {
    bool nowTurn = false;
    if (playerID == currentPlayerIndex) {
      nowTurn = true;
    }
    return Container(
      padding: EdgeInsets.all(5.0),
      // 노치 부분만큼 Margin값 주기
      // margin: EdgeInsets.only(
      //   left: 40,
      // ),
      decoration: BoxDecoration(
        border: nowTurn
            ? Border.all(
                color: Colors.red, width: 3) // 현재 플레이어의 차례일 때 빨간색 테두리 적용
            : Border.all(color: Colors.transparent), // 그렇지 않으면 투명한 테두리 적용
      ),
      child: Column(
        children: [
          // 프로필 아이콘과 이름
          Expanded(
            flex: 1,
            child: Row(
              children: [
                Expanded(
                  flex: 2,
                  child: AutoSizeText(
                    playerName,
                    minFontSize: 5,
                    style: TextStyle(fontSize: 20.0), // 시작할 폰트 크기
                    maxLines: 1, // 최대 줄 수
                  ),
                ),
                Expanded(
                  flex: 1,
                  child: SizedBox(),
                ),
                Expanded(
                  flex: 1,
                  child: AutoSizeText(
                    '${game.players[playerID].money}원',
                    minFontSize: 5,
                    style: TextStyle(fontSize: 20.0), // 시작할 폰트 크기
                    maxLines: 1, // 최대 줄 수
                  ),
                ),
                Expanded(
                  flex: 1,
                  child: ElevatedButton(
                    onPressed: () {
                      _showPlayerBuildingStatusDialog(context, playerID);
                    },
                    child: FittedBox(
                      fit: BoxFit.cover,
                      child: Icon(Icons.check, size: 100),
                    ),
                  ),
                ),
              ],
            ),
          ),
          // 4장의 카드
          Expanded(
            flex: 2,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                Expanded(
                  flex: 1,
                  child: _playerMajorCardImage(playerID, 0),
                ),
                Expanded(
                  flex: 1,
                  child: _playerMajorCardImage(playerID, 1),
                ),
                Expanded(
                  flex: 1,
                  child: _playerMajorCardImage(playerID, 2),
                ),
                Expanded(
                  flex: 1,
                  child: _playerMajorCardImage(playerID, 3),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

_playerWidget레이아웃에서 현재 차례인 플레이어에게 빨간색 테두리를 주어 현재 누구의 차례인지 보여줄 수 있도록 레이아웃을 수정해줘야 합니다. playerIDcurrentPlayerIndex값을 비교해서 두 값이 똑같은 플레이어가 현재 턴을 진행 중인 것으로 표현합니다. 그리고 돈을 표시하는 텍스트 또한 '${game.players[playerID].money}원',을 사용해서 해당 플레이어가 가진 돈을 표시합니다.

📘nextTurn()

class _GameScreenState extends State<GameScreen>
    with SingleTickerProviderStateMixin, WidgetsBindingObserver {
 
 ...

  
  Widget build(BuildContext context) {
    double screenWidth = MediaQuery.of(context).size.width;
    double sideSpaceWidth = screenWidth * 0.2;

    ...

    return Scaffold(
        body: Row(
          children: [
            //게임스크린 (플레이어 영역)
            Container(
              ...
            ),
            //게임스크린 (게임 보드 영역)
            Expanded(
                  child: Padding(
                padding: EdgeInsets.symmetric(horizontal: 10.0),
                child: Stack(
                  children: [
                    Column(
                      children: [
                        // 그리드 뷰 영역
                        Expanded(
                          flex: 3,
                          ...
                        ),
                        // 주요건물 및 버튼 영역
                        Expanded(
                            flex: 1,
                            child: Row(
                              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                              children: [
                              	//player의 인덱스로 알맞는 값을 집어넣어야함
                                Expanded(
                                  flex: 1,
                                  child: _boardMajorCardImage(
                                      game.players[0].id, 0),
                                ),
                                Expanded(
                                  flex: 1,
                                  child: _boardMajorCardImage(
                                      game.players[0].id, 1),
                                ),
                                Expanded(
                                  flex: 1,
                                  child: _boardMajorCardImage(
                                      game.players[0].id, 2),
                                ),
                                Expanded(
                                  flex: 1,
                                  child: _boardMajorCardImage(
                                      game.players[0].id, 3),
                                ),
                                Expanded(
                                  flex: 1,
                                  child: Column(
                                    mainAxisAlignment: MainAxisAlignment.center,
                                    children: [
                                      Expanded(
                                        flex: 9,
                                        child: Row(
                                          mainAxisAlignment:
                                          MainAxisAlignment.center,
                                          children: [
                                            Expanded(
                                              flex: 9,
                                              child: FittedBox(
                                                child: Icon(Icons.attach_money),
                                              ),
                                            ),
                                            Expanded(
                                              flex: 1,
                                              child: SizedBox(),
                                            ),
                                            Expanded(
                                              flex: 9,
                                              child: AutoSizeText(
                                                // 플레이어 정보를 담아야함
                                                '3원',
                                                minFontSize: 5,
                                                style: TextStyle(fontSize: 20.0),
                                                // 시작할 폰트 크기
                                                maxLines: 1, // 최대 줄 수
                                              ),
                                            ),
                                          ],
                                        ),
                                      ),
                                      Expanded(
                                        flex: 1,
                                        child: SizedBox(),
                                      ),
                                      Expanded(
                                        flex: 9,
                                        child: ElevatedButton(
                                          onPressed: () {
                                            // 건물 현황 보여주는 Dialog
                                          },
                                          child: AutoSizeText(
                                            '건물 현황',
                                            minFontSize: 5,
                                            style: TextStyle(fontSize: 15.0),
                                            // 시작할 폰트 크기
                                            maxLines: 1, // 최대 줄 수
                                          ),
                                        ),
                                      ),
                                      Expanded(
                                        flex: 1,
                                        child: SizedBox(),
                                      ),
                                      Expanded(
                                        flex: 9,
                                        child: ElevatedButton(
                                          onPressed: () {
                                            // 건물 구매 안하고 턴넘기기
                                            setState(() {
                                            	game.nextTurn();
                                            });
                                           
                                          },
                                          child: AutoSizeText(
                                            '턴 넘기기',
                                            minFontSize: 5,
                                            style: TextStyle(fontSize: 15.0),
                                            // 시작할 폰트 크기
                                            maxLines: 1, // 최대 줄 수
                                          ),
                                        ),
                                      ),
                                      Expanded(
                                        flex: 1,
                                        child: SizedBox(),
                                      ),
                                    ],
                                  ),
                                ),
                              ],
                            ),
                          ),
                      ],
                    ),
                   ...
                  ],
                ),
              )),
            //게임스크린 (주사위 영역)
            Container(
              ...
            ),
          ],
        ),
      );
  }
  
  ... 

}

턴 넘기기 버튼을 누르거나 건물을 구매할 때마다 해당 함수를 호출하여 턴이 넘어가도록 만들어줍니다. 카드 구매에 대한 로직은 아직 구현하지 않았기 때문에 우선적으로 턴 넘기기 버튼에만 nextTurn() 함수를 적용시켜줍니다.

여기서 nextTurn()setState()함수를 이용해서 호출해야 플레이어 위젯의 빨간색 테두리가 정상적으로 바뀝니다. 만약 setState()함수를 이용하지 않는다면 currentPlayerIndex값만 바뀌고 바뀐 결과가 화면에 표시되지 않습니다.

턴 넘기기 버튼을 눌렀을 때 플레이어 위젯의 빨간색 테두리가 정상적으로 바뀐다면 성공입니다!!
만약 오류 해결이 안 되거나 헷갈리는 부분이 있다면 댓글을 달아주세요. 지적 또한 환영입니다.



다음 게임 로직으로는 건물 구매 기능을 구현하겠습니다. 다음 포스팅으로 찾아뵙겠습니다😎

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

0개의 댓글

관련 채용 정보