Files
collorettoes/D.cpp
2025-12-30 06:54:52 +00:00

561 lines
21 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <random>
#include <ctime>
#include <map>
#include <iomanip>
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<Card> hand;
Player(int _id, bool _human) : id(_id), isHuman(_human), hasTakenRow(false) {}
// 計算分數
int calculateScore() const {
map<Color, int> 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<int> 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<string, int> 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<Player> players;
vector<Card> deck;
vector<vector<Card>> 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<Color> 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<bool> rowTaken 來記錄本回合狀態。
}
// 我們需要在 playRound 內部維護 rowTaken
}
}
// 為了邏輯清晰,重寫 playRound 流程控制
void gameLoop() {
while (!isGameEnded) {
// --- 回合初始化 ---
for (auto& r : rows) r.clear();
for (auto& p : players) p.hasTakenRow = false;
vector<bool> 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<numPlayers; i++) {
if (!rowTaken[i] && rows[i].size() < 3) anyRowPlaceable = true;
}
if (!anyRowPlaceable) canDraw = false;
// 2. 拿牌: 至少有一列沒被拿走且裡面有牌 (規則: 不能拿空列,除非全都是空列且無法執行動作? 不,規則說選擇收集時區域至少要有一張)
bool canTake = false;
for(int i=0; i<numPlayers; i++) {
if (!rowTaken[i] && rows[i].size() > 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<int> 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<numPlayers; i++) {
if(!rowTaken[i] && rows[i].size() < 3) cout << (i+1) << " ";
}
cout << "): ";
while(true) {
if(cin >> 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<int> validRows;
for(int i=0; i<numPlayers; i++) {
if (!rowTaken[i] && rows[i].size() < 3) validRows.push_back(i);
}
rowIdx = validRows[rand() % validRows.size()];
cout << "Bot places card in Row " << (rowIdx + 1) << endl;
}
rows[rowIdx].push_back(c);
} else {
// 拿牌
int rowIdx = -1;
if (currP.isHuman) {
cout << "Select a row to take (";
for(int i=0; i<numPlayers; i++) {
if(!rowTaken[i] && rows[i].size() > 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<int> validRows;
for(int i=0; i<numPlayers; i++) {
if (!rowTaken[i] && rows[i].size() > 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<pair<int, int>> 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<string, int> 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;
}