快艇骰子 Yahtzee

Chen Chen
海大 SwiftUI iOS / Flutter App 程式設計
11 min readMay 4, 2024

(可能是最終版也可能不是)

https://github.com/ouoxii/yahtzee

功能需求:

  • 基本遊戲分數計算、回合輪流、保留選取骰子、勝負條件判斷、每次骰色子如果該格未被填入會顯示預測分數。

加分:

  • 第一次 Yahtzee 得 50 分,之後每次丟出 Yahtzee 得 100 分。
  • 點選格子後,再點選按鈕完成輸入。以下圖為例,點選 Play 前都可以點選別的格子。
  • 實作兩種模式,跟電腦 PK 和跟朋友 PK,玩家可選擇要玩哪一種模式。
  • 電腦的回合可丟一次骰子就好,不用丟到三次。(難度一顆星)

填入玩家名稱Player1、Player2,如果勾選Player2為電腦默認名稱Computer

初始遊戲畫面布局: 最上方會是遊戲玩家暱稱和目前分數,中間是紀錄玩家每個項目的得分區塊,下方是骰色子區域: Roll Dice是每回合擲骰子、Play是紀錄分數到上方區塊。

上方顯示目前是哪個玩家的回合,點選RollDice後開始

擲骰子後可選擇A.保留特定色子RollDice->最多可重骰兩次 B.選取上方格子填入後點Play->下一個玩家回合

如果是電腦回合,玩家點選RollDice後,Player2會自動顯示分數在上方格子區域,再次點選RollDice輪到玩家回合擲骰子。

當每個格子都被填值時(忽略BONUS)結束遊戲,比較總分。

程式碼解釋:

電腦玩家邏輯

aiMakeMove

  1. 檢查當前玩家是否為電腦玩家,如果是,則繼續執行,否則不執行任何動作。
  2. bestScore 變數來儲存最好的分數,以及一個 bestCategory 變數來儲存最好的類別。
  3. 遍歷了所有的類別(categories 和 categories2)來尋找最好的移動。對於每個類別,檢查是否該類別尚未被玩家所使用。如果是,則計算該類別的潛在分數(使用 calculateScore 函數),並將其與目前的最佳分數比較。如果該潛在分數更高,則更新 bestScorebestCategory 變數。
  4. 找到了一個有效的最佳類別,則將該類別的分數設置為 bestScore,並更新玩家的總分。然後,重置回合的設置(使用 _resetRoundSettings 函數),以便準備下一個回合。
  5. 最後將遊戲移交給下一個玩家( togglePlayer 函數)->檢查遊戲是否結束( checkAndHandleGameEnd 函數)。
ElevatedButton(
onPressed: () {
setState(() {
round++;
for (var i = 0; i < 5; i++) {
if (!_selectedDice[i] && round < 4) {
_diceIndex[i] = Random().nextInt(6) + 1;
}
}
if (currentPlayer.isAI) {
aiMakeMove(); // AI calculates its move immediately after rolling
}
});
},
child: const Text('Roll Dice'),
),

由電腦計算填哪個類別


void aiMakeMove() {
if (currentPlayer.isAI) {
int bestScore = 0;
String? bestCategory; // Make bestCategory nullable

// Loop through all categories to find the best move
for (var category in categories) {
if (!currentPlayer.getScored(category)) {
int potentialScore = calculateScore(_diceIndex, category);
if (potentialScore > bestScore) {
bestScore = potentialScore;
bestCategory = category;
}
}
}

for (var category in categories2) {
if (!currentPlayer.getScored(category)) {
int potentialScore = calculateScore(_diceIndex, category);
if (potentialScore > bestScore) {
bestScore = potentialScore;
bestCategory = category;
}
}
}

// If no best category found, choose the first unscored category
if (bestCategory == null) {
for (var category in categories) {
if (!currentPlayer.getScored(category)) {
bestCategory = category;
break;
}
}
for (var category in categories2) {
if (!currentPlayer.getScored(category)) {
bestCategory = category;
break;
}
}
}

// Execute the best move if there's a valid category selected
if (bestCategory != null) {
currentPlayer.setScore(bestCategory, bestScore);
currentPlayer.score =
currentPlayer.scores.values.reduce((a, b) => a + b);
_resetRoundSettings();
}
}

togglePlayer(); // Move to the next player
checkAndHandleGameEnd(); // Check if the game is over
}

第一次 Yahtzee 得 50 分,之後每次丟出 Yahtzee 得 100 分。

int calculateScore(List<int> dice, String category) {
//...
if (category == 'Yahtzee') {
if (dice.toSet().length == 1) {
// 如果骰子的所有點數都相同,即為 Yahtzee
if (player1.getScored('Yahtzee') && player1.getScore('Yahtzee') == 50) {
// 如果玩家已經得到過 Yahtzee,且得分為 50,則這次得分為 100
return 100;
} else {
// 第一次 Yahtzee 得 50 分
return 50;
}
} else {
return 0;
}
}

return 0;
}

點選格子後,再點選按鈕(RollDice/Play)完成輸入回合。

  • 用ROUND紀錄已骰色子幾次 (>就不能影響目前點數)
ElevatedButton(
onPressed: () {
setState(() {
round++;
for (var i = 0; i < 5; i++) {
if (!_selectedDice[i] && round < 4) {
_diceIndex[i] = Random().nextInt(6) + 1;
}
}
if (currentPlayer.isAI) {
aiMakeMove(); // AI calculates its move immediately after rolling
}
});
},
child: const Text('Roll Dice'),
),
  • CASE1、CASE2區分左右兩邊的分數格子顯示
  • 檢查該值沒被填過才能填入
if (!currentPlayer.getScored(
categories[chooseCategory]))
  • 將已選骰子移除顯示->切換到下一個玩家->重置骰子圖案
_selectedDice = List.filled(5, false);
togglePlayer(); // ToggLE
_diceIndex = List<int>.filled(_diceIndex.length, 0);
  • 此部分程式碼
 ElevatedButton(
onPressed: () {
setState(() {
var mode = 0;
if ((chooseCategory != 7) && (round > 0)) {
(chooseCategory < 6) ? mode = 1 : mode = 2;
switch (mode) {
case 1:
if (!currentPlayer.getScored(
categories[chooseCategory])) {
var total = calculateScore(_diceIndex,
categories[chooseCategory]);
currentPlayer.setScore(
categories[chooseCategory],
total);
round = 0;
currentPlayer.score = currentPlayer
.scores.values
.reduce((a, b) => a + b);
// reset the round settings
chooseCategory = 7;
_diceIndex = List<int>.filled(
_diceIndex.length, 0);
_selectedDice = List.filled(5, false);
togglePlayer(); // Toggle to the other player after a play
}
break;
case 2:
if (!currentPlayer.getScored(
categories2[chooseCategory - 10])) {
var total = calculateScore(_diceIndex,
categories2[chooseCategory - 10]);
currentPlayer.setScore(
categories2[chooseCategory - 10],
total);
round = 0;
currentPlayer.score = currentPlayer
.scores.values
.reduce((a, b) => a + b);
// reset the round settings
_diceIndex = List<int>.filled(
_diceIndex.length, 0);
chooseCategory = 7;
_selectedDice = List.filled(5, false);
togglePlayer(); // Toggle to the other player after a play
}
break;
}
}
});
checkAndHandleGameEnd();
},
child: const Text('Play'),
),

檢查特殊Bonus條件

//player.dart  
void checkAndApplyBonus() {
int upperSectionSum = scores.entries
.where((entry) => ['Ones', 'Twos', 'Threes', 'Fours', 'Fives', 'Sixes']
.contains(entry.key))
.fold(0, (previousValue, element) => previousValue + element.value);

if ((scored['Bonus'] ?? false) == false && upperSectionSum >= 63) {
scores['Bonus'] = 35;
scored['Bonus'] = true;
}
}

--

--