이번 포스팅에서는 게임 규칙을 제대로 정의하고 플레이어 턴을 구현하는 내용을 다루겠습니다. 기본 레이아웃을 구현하지 못했다면 앞선 포스팅들을 보고 구현해주십시오. 만약 오류가 나거나 잘 안되는 부분이 있다면 댓글을 남겨주세요.
게임 준비:
- 각 플레이어는 초기 자금으로 동전 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종을 모두 구매한 사람이 승리합니다.
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
의 값을 적절히 조절하고 있습니다.
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
레이아웃에서 현재 차례인 플레이어에게 빨간색 테두리를 주어 현재 누구의 차례인지 보여줄 수 있도록 레이아웃을 수정해줘야 합니다. playerID
와 currentPlayerIndex
값을 비교해서 두 값이 똑같은 플레이어가 현재 턴을 진행 중인 것으로 표현합니다. 그리고 돈을 표시하는 텍스트 또한 '${game.players[playerID].money}원',
을 사용해서 해당 플레이어가 가진 돈을 표시합니다.
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
값만 바뀌고 바뀐 결과가 화면에 표시되지 않습니다.
턴 넘기기 버튼을 눌렀을 때 플레이어 위젯의 빨간색 테두리가 정상적으로 바뀐다면 성공입니다!!
만약 오류 해결이 안 되거나 헷갈리는 부분이 있다면 댓글을 달아주세요. 지적 또한 환영입니다.
다음 게임 로직으로는 건물 구매 기능을 구현하겠습니다. 다음 포스팅으로 찾아뵙겠습니다😎