diff --git a/4.cpp b/4.cpp new file mode 100644 index 0000000..188e6ec --- /dev/null +++ b/4.cpp @@ -0,0 +1,561 @@ +#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; +} \ No newline at end of file