#include #include #include #include #include #include #include #include using namespace std; // --- 定義遊戲常數與列舉 --- enum CardType { COLOR_CARD, JOKER, PLUS_2, LAST_ROUND_CARD }; // 7種顏色 enum Color { BROWN = 0, BLUE, GRAY, GREEN, ORANGE, PINK, YELLOW, NONE }; const string COLOR_NAMES[] = { "Brown", "Blue", "Gray", "Green", "Orange", "Pink", "Yellow", "None" }; // 分數表 (對應 0, 1, 2, 3, 4, 5, 6+ 張牌的分數) const int SCORE_TABLE[] = {0, 1, 3, 6, 10, 15, 21}; struct Card { CardType type; Color color; // 顯示卡片資訊 string toString() const { if (type == LAST_ROUND_CARD) return "[LAST ROUND]"; if (type == PLUS_2) return "[+2]"; if (type == JOKER) return "[Joker]"; return "[" + COLOR_NAMES[color] + "]"; } }; // --- 玩家類別 --- class Player { public: int id; bool isHuman; bool hasTakenRow; // 本回合是否已拿牌 vector hand; Player(int _id, bool _human) : id(_id), isHuman(_human), hasTakenRow(false) {} // 計算分數 int calculateScore() const { map colorCounts; int jokerCount = 0; int plus2Count = 0; for (const auto& card : hand) { if (card.type == COLOR_CARD) colorCounts[card.color]++; else if (card.type == JOKER) jokerCount++; else if (card.type == PLUS_2) plus2Count++; } // 簡單的貪婪演算法分配 Joker: // 每次將 Joker 分配給「能獲得最大分數增益」的顏色 // 如果該顏色已達 6 張(21分),則不再增加效益,改分配給其他顏色 for (int i = 0; i < jokerCount; i++) { Color bestColor = NONE; int maxGain = -1; // 嘗試加到現有的顏色堆中 for (auto const& [color, count] : colorCounts) { int currentScore = (count >= 6) ? 21 : SCORE_TABLE[count]; int nextScore = (count + 1 >= 6) ? 21 : SCORE_TABLE[count + 1]; int gain = nextScore - currentScore; if (gain > maxGain) { maxGain = gain; bestColor = color; } } // 如果沒有顏色牌,或者加新顏色(從0變1得1分)效益更好 if (maxGain < 1) { // 這裡簡化處理:若效益小於1(通常是現有牌都滿6張), // 實際上應該創造一個新顏色堆,但這裡我們假設 Joker 加到任意堆 // 為了程式邏輯簡單,我們找一個目前數量最少的顏色加進去,或是視為新顏色 // 在此邏輯下,直接視為一個新顏色(1分) // 為了實作方便,我們不修改 map key,只記錄分數即可,但為了顯示正確,我們需要模擬 // 這裡暫時不實作極端複雜的 Joker 分配,採用: // "優先補滿未滿6張的最高分堆,若都滿則開新堆" // 簡化版:直接加到第一種顏色,計分邏輯會在後面排序處理 if (colorCounts.empty()) { // 沒牌時隨便選個顏色當作 Joker 變成的顏色 colorCounts[BROWN]++; } else { // 加到目前張數最多但 < 6 的堆,若都 >=6 則隨便加 Color target = colorCounts.begin()->first; int maxC = -1; for(auto const& [c, count] : colorCounts){ if(count < 6 && count > maxC) { maxC = count; target = c; } } colorCounts[target]++; } } else { colorCounts[bestColor]++; } } // 計算各顏色分數並排序 vector scores; for (auto const& [color, count] : colorCounts) { scores.push_back((count >= 6) ? 21 : SCORE_TABLE[count]); } // 補齊不足 7 種顏色為 0 分,方便排序 while(scores.size() < 7) scores.push_back(0); // 由大到小排序 sort(scores.rbegin(), scores.rend()); int totalScore = 0; // 前三高分為正分 for (int i = 0; i < 3; i++) totalScore += scores[i]; // 其餘為負分 for (int i = 3; i < scores.size(); i++) totalScore -= scores[i]; // 加上 +2 卡 totalScore += (plus2Count * 2); return totalScore; } void printHand() const { cout << "P" << id << (isHuman ? "(YOU)" : "(BOT)") << " Hand: "; if (hand.empty()) { cout << "Empty"; } else { // 整理顯示,例如: Blue:3, Green:1, +2:1... map summary; for(const auto& c : hand) summary[c.toString()]++; for(const auto& item : summary) { cout << item.first << "x" << item.second << " "; } } cout << " | Current Est. Score: " << calculateScore() << endl; } }; // --- 遊戲控制類別 --- class ColorettoGame { private: int numPlayers; vector players; vector deck; vector> rows; // 場上的牌列 bool lastRoundTriggered; bool isGameEnded; int startPlayerIndex; // 初始化牌堆 void initDeck() { deck.clear(); // 7種顏色,每種9張 for (int i = 0; i < 7; i++) { // 3人遊戲時移除一種顏色(依規則),但作業預設4人,此處寫通用邏輯 if (numPlayers == 3 && i == 6) break; for (int j = 0; j < 9; j++) { deck.push_back({COLOR_CARD, (Color)i}); } } // +2卡 10張 for (int i = 0; i < 10; i++) deck.push_back({PLUS_2, NONE}); // Joker 3張 for (int i = 0; i < 3; i++) deck.push_back({JOKER, NONE}); shuffleDeck(); // 插入「最後回合卡」至倒數第16張 if (deck.size() >= 16) { deck.insert(deck.end() - 15, {LAST_ROUND_CARD, NONE}); } } void shuffleDeck() { random_device rd; mt19937 g(rd()); shuffle(deck.begin(), deck.end(), g); } // 發初始手牌 void dealInitialCards() { cout << "\n--- Dealing Initial Cards ---\n"; vector usedColors; for (auto& p : players) { bool valid = false; // 尋找一張符合條件的牌 (變色龍牌,且顏色不重複) // 這裡為了簡單,直接遍歷牌堆找,找到就移出 for (auto it = deck.begin(); it != deck.end(); ++it) { if (it->type == COLOR_CARD) { bool colorUsed = false; for(Color c : usedColors) if(c == it->color) colorUsed = true; if (!colorUsed) { p.hand.push_back(*it); usedColors.push_back(it->color); deck.erase(it); valid = true; break; } } } // 理論上牌堆剛開始一定夠,不做例外處理 } } public: ColorettoGame() : lastRoundTriggered(false), isGameEnded(false), startPlayerIndex(0) { srand(time(0)); } void setup(int n) { numPlayers = n; players.clear(); rows.clear(); rows.resize(numPlayers); // 列數等於玩家數 // 建立玩家,P1 為人類 players.push_back(Player(1, true)); for (int i = 2; i <= numPlayers; i++) { players.push_back(Player(i, false)); } initDeck(); dealInitialCards(); startPlayerIndex = rand() % numPlayers; // 隨機起始玩家 } // 抽一張牌,處理 Last Round 邏輯 Card drawCard() { if (deck.empty()) return {LAST_ROUND_CARD, NONE}; // 防禦性編程 Card c = deck.front(); deck.erase(deck.begin()); if (c.type == LAST_ROUND_CARD) { cout << "\n!!! LAST ROUND CARD DRAWN !!! The game will end after this round.\n"; lastRoundTriggered = true; // 規則:抽到最後回合卡,該玩家重抽一張 if (deck.empty()) return c; // 沒牌了就只能停 return drawCard(); } return c; } void printBoard() { cout << "\n================ BOARD STATE ================\n"; cout << "Deck remaining: " << deck.size() << " cards\n"; if (lastRoundTriggered) cout << "WARNING: This is the LAST ROUND!\n"; cout << "\n--- Rows ---\n"; for (int i = 0; i < numPlayers; i++) { cout << "Row " << (i + 1) << ": "; if (rows[i].empty()) { // 如果該列已被拿走 (標記邏輯:這裡我們用特殊的空狀態表示) // 但為了簡單,我們可以用一個變數紀錄列是否被拿走 // 在此實作中,我們檢查 rows[i] 是否空。 // *注意*:規則是拿走列後,列變空,玩家退出回合。 // 為了區分「剛開始空」和「被拿走」,我們需要邏輯判斷。 // 這裡簡化:顯示內容即可。只有當有人要拿牌時才判斷是否為空。 cout << "(Empty)"; } else { for (const auto& card : rows[i]) { cout << card.toString() << " "; } } // 檢查這列是否還能放 (最多3張) if (rows[i].size() >= 3) cout << " [FULL]"; cout << endl; } cout << "\n--- Players ---\n"; for (const auto& p : players) { cout << (p.hasTakenRow ? "[OUT] " : "[IN] "); p.printHand(); } cout << "=============================================\n"; } // 檢查是否所有人都拿完牌了 bool isRoundOver() { for (const auto& p : players) { if (!p.hasTakenRow) return false; } return true; } // 執行一個回合 void playRound() { // 重置玩家狀態和場上的列 for (auto& p : players) p.hasTakenRow = false; for (auto& r : rows) r.clear(); // 清空場上的牌 int currentPlayerIdx = startPlayerIndex; cout << "\n>>> NEW ROUND START! Player " << players[currentPlayerIdx].id << " goes first. <<<\n"; while (!isRoundOver()) { Player& currP = players[currentPlayerIdx]; // 如果該玩家已經拿過牌,跳過 if (currP.hasTakenRow) { currentPlayerIdx = (currentPlayerIdx + 1) % numPlayers; continue; } // 顯示盤面 (只在人類回合前顯示,避免洗版,或每步都顯示) if (currP.isHuman) printBoard(); // --- 動作選擇 --- // 判斷可用動作 bool canDraw = (deck.size() > 0); // 檢查是否所有列都滿了,如果全滿就不能抽牌 bool allRowsFull = true; bool allRowsEmptyOrTaken = true; // 用於檢查能不能拿牌 // 檢查列的狀態 // 這裡有個細節:被拿走的列在程式邏輯中是空的,但不能再放牌。 // 為了簡化,我們需要知道哪些列是「此回合已被拿走」的。 // 由於規則:拿走列的人就退出了。所以剩下的玩家面對的列一定是「還沒被拿走的」。 // 但剩下的列可能是空的。 // 修正:我們需要一個狀態來標記 Row 是否被拿走嗎? // 實際上,剩餘玩家人數 = 剩餘未被拿走的列數。 // 所以只要列還沒滿(3張),且還有人在場,就可以抽牌。 // 簡化判定:只要有任一列牌數 < 3,就可以抽牌放置。 allRowsFull = true; for(const auto& r : rows) { // 這邊有個邏輯問題:如果不追蹤哪個列屬於哪個被拿走的,會很難判斷。 // Coloretto 規則:每位玩家拿走一列。列的數量 = 玩家數量。 // 所以,如果 rows[i] 被拿走了,我們應該將其鎖定。 // 為了方便,我們用一個 vector rowTaken 來記錄本回合狀態。 } // 我們需要在 playRound 內部維護 rowTaken } } // 為了邏輯清晰,重寫 playRound 流程控制 void gameLoop() { while (!isGameEnded) { // --- 回合初始化 --- for (auto& r : rows) r.clear(); for (auto& p : players) p.hasTakenRow = false; vector rowTaken(numPlayers, false); // 紀錄哪些列被拿走了 int playersOutCount = 0; int currentPlayerIdx = startPlayerIndex; cout << "\n>>> NEW ROUND START! <<<\n"; // --- 回合行動迴圈 --- while (playersOutCount < numPlayers) { Player& currP = players[currentPlayerIdx]; // 若玩家已出局,換下一位 if (currP.hasTakenRow) { currentPlayerIdx = (currentPlayerIdx + 1) % numPlayers; continue; } if(currP.isHuman) printBoard(); // 檢查合法動作 // 1. 抽牌: 牌堆有牌 AND 至少有一列沒被拿走且沒滿 bool canDraw = !deck.empty(); bool anyRowPlaceable = false; for(int i=0; i 0) canTake = true; } // 強制邏輯:如果不能抽牌(例如全滿),必須拿牌。 // 如果不能拿牌(全都空),必須抽牌。 int action = 0; // 1: Draw, 2: Take if (currP.isHuman) { cout << "\nPlayer " << currP.id << " (YOU), choose action:\n"; if (canDraw) cout << "1. Draw a card\n"; if (canTake) cout << "2. Take a row\n"; while(true) { cout << "> "; if (!(cin >> action)) { cin.clear(); cin.ignore(1000, '\n'); continue; } if (action == 1 && canDraw) break; if (action == 2 && canTake) break; cout << "Invalid action. Try again.\n"; } } else { // 電腦 AI // 簡單隨機:如果能做就隨機選,若只能做一種就做那種 vector possibleActions; if (canDraw) possibleActions.push_back(1); if (canTake) possibleActions.push_back(2); action = possibleActions[rand() % possibleActions.size()]; cout << "Player " << currP.id << " (Bot) chooses action " << action << endl; } // --- 執行動作 --- if (action == 1) { // 抽牌 Card c = drawCard(); cout << "Draws: " << c.toString() << endl; // 選擇放哪一列 int rowIdx = -1; if (currP.isHuman) { cout << "Select a row to place card ("; for(int i=0; i> rowIdx) { rowIdx--; // 轉 0-base if (rowIdx >= 0 && rowIdx < numPlayers && !rowTaken[rowIdx] && rows[rowIdx].size() < 3) break; } else { cin.clear(); cin.ignore(1000, '\n'); } cout << "Invalid row. Must be not taken and not full (<3). Try again: "; } } else { // AI 隨機放 vector validRows; for(int i=0; i 0) cout << (i+1) << " "; } cout << "): "; while(true) { if(cin >> rowIdx) { rowIdx--; if (rowIdx >= 0 && rowIdx < numPlayers && !rowTaken[rowIdx] && rows[rowIdx].size() > 0) break; } else { cin.clear(); cin.ignore(1000, '\n'); } cout << "Invalid row. Must be not taken and have cards. Try again: "; } } else { // AI 隨機拿 vector validRows; for(int i=0; i 0) validRows.push_back(i); } rowIdx = validRows[rand() % validRows.size()]; cout << "Bot takes Row " << (rowIdx + 1) << endl; } // 執行拿牌邏輯 cout << "Player " << currP.id << " took Row " << (rowIdx + 1) << " with " << rows[rowIdx].size() << " cards.\n"; currP.hand.insert(currP.hand.end(), rows[rowIdx].begin(), rows[rowIdx].end()); rowTaken[rowIdx] = true; // 標記該列被拿走 currP.hasTakenRow = true; // 玩家退出本回合 playersOutCount++; // 最後一個拿牌的成為下回合起始玩家 if (playersOutCount == numPlayers) { startPlayerIndex = currentPlayerIdx; } } // 換下一位 currentPlayerIdx = (currentPlayerIdx + 1) % numPlayers; } // 回合結束檢查 if (lastRoundTriggered) { isGameEnded = true; } } finalScoring(); } void finalScoring() { cout << "\n\n================ GAME OVER ================\n"; cout << "Calculating scores...\n"; // 用 pair 存 (分數, 玩家ID) 以便排序 vector> rank; for (const auto& p : players) { int score = p.calculateScore(); rank.push_back({score, p.id}); cout << "Player " << p.id << (p.isHuman ? " (YOU)" : " (Bot)") << " Hand:\n"; // 顯示最終手牌統計 map summary; for(const auto& c : p.hand) summary[c.toString()]++; for(const auto& item : summary) cout << " " << item.first << " x" << item.second; cout << "\n -> Final Score: " << score << "\n\n"; } sort(rank.rbegin(), rank.rend()); cout << "--- FINAL STANDINGS ---\n"; for (int i = 0; i < rank.size(); i++) { cout << (i+1) << ". Player " << rank[i].second << " : " << rank[i].first << " points"; if (rank[i].second == 1) cout << " <--- YOU"; cout << endl; } } }; int main() { cout << "Welcome to Coloretto (C++ Version)!\n"; while(true) { int numP = 4; cout << "Enter number of players (3-5) [Default 4]: "; if (cin >> numP) { if (numP < 3 || numP > 5) numP = 4; } else { cin.clear(); cin.ignore(1000, '\n'); numP = 4; } cout << "Starting game with " << numP << " players (1 Human vs " << (numP-1) << " Bots).\n"; ColorettoGame game; game.setup(numP); game.gameLoop(); cout << "\nPlay again? (y/n): "; char c; cin >> c; if (c != 'y' && c != 'Y') break; } return 0; }