C语言实现牌面生成、洗牌与连续牌型识别:构建你的扑克游戏基础357


在编程世界中,模拟现实场景是提升技能的绝佳方式。扑克牌游戏作为一种经典的娱乐形式,其背后的逻辑和规则为C语言开发者提供了丰富的实践机会。本文将深入探讨如何使用C语言来生成扑克牌、进行洗牌、发牌,并重点讲解如何识别手中的“连续牌”(即顺子),为你的扑克游戏开发打下坚实的基础。

1. 扑克牌基础:数据结构设计

首先,我们需要设计一个合理的数据结构来表示扑克牌。一副标准扑克牌包含四种花色(黑桃、红桃、梅花、方块)和十三种点数(A, 2-10, J, Q, K)。我们可以使用枚举(enum)来定义花色和点数,再用结构体(struct)将它们组合起来。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h> // For strcmp and strcpy if needed
// 定义花色
typedef enum {
CLUBS, // 梅花
DIAMONDS, // 方块
HEARTS, // 红桃
SPADES, // 黑桃
SUIT_COUNT // 辅助计数,总花色数量
} Suit;
// 定义点数
typedef enum {
RANK_NONE, // 占位符,不使用0
ACE = 1,
TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN,
JACK, QUEEN, KING,
RANK_COUNT // 辅助计数,总点数数量
} Rank;
// 定义扑克牌结构
typedef struct {
Suit suit;
Rank rank;
} Card;
// 定义牌库大小
#define DECK_SIZE ( (RANK_COUNT - 1) * (SUIT_COUNT) ) // 13 * 4 = 52张牌

这里,`RANK_NONE` 和 `SUIT_COUNT`/`RANK_COUNT` 枚举成员是为了方便索引和统计数量而添加的技巧,实际使用时需要注意边界。

2. 牌库的初始化与打印

有了牌的结构,我们就可以初始化一副完整的扑克牌。这通常通过嵌套循环遍历所有花色和点数来完成。同时,为了方便调试和展示,我们需要一个函数来将 `Card` 结构体中的花色和点数转换为易于理解的字符串形式。
// 获取花色字符串
const char* getSuitString(Suit suit) {
switch (suit) {
case CLUBS: return "♣";
case DIAMONDS: return "♦";
case HEARTS: return "♥";
case SPADES: return "♠";
default: return "未知花色";
}
}
// 获取点数字符串
const char* getRankString(Rank rank) {
switch (rank) {
case ACE: return "A";
case TWO: return "2";
case THREE: return "3";
case FOUR: return "4";
case FIVE: return "5";
case SIX: return "6";
case SEVEN: return "7";
case EIGHT: return "8";
case NINE: return "9";
case TEN: return "10";
case JACK: return "J";
case QUEEN: return "Q";
case KING: return "K";
default: return "未知点数";
}
}
// 打印单张牌
void printCard(Card card) {
printf("%s%s ", getSuitString(), getRankString());
}
// 初始化牌库
void initializeDeck(Card deck[]) {
int cardIndex = 0;
for (int s = CLUBS; s < SUIT_COUNT; s++) {
for (int r = ACE; r < RANK_COUNT; r++) {
deck[cardIndex].suit = (Suit)s;
deck[cardIndex].rank = (Rank)r;
cardIndex++;
}
}
}
// 打印整副牌
void printDeck(const Card deck[], int size) {
printf("牌库:");
for (int i = 0; i < size; i++) {
printCard(deck[i]);
if ((i + 1) % 13 == 0) { // 每13张牌(一种花色)换行
printf("");
}
}
printf("");
}

3. 模拟洗牌:Fisher-Yates算法

洗牌是扑克游戏的核心,它确保了牌的随机性。Fisher-Yates洗牌算法是一种高效且广泛使用的随机化算法,其基本思想是从牌堆的末尾开始,随机选择一张牌与当前位置的牌交换,并逐步向前移动。
// 洗牌函数 (Fisher-Yates shuffle)
void shuffleDeck(Card deck[], int size) {
srand(time(NULL)); // 使用当前时间作为随机数种子
for (int i = size - 1; i > 0; i--) {
int j = rand() % (i + 1); // 生成0到i之间的随机索引
// 交换 deck[i] 和 deck[j]
Card temp = deck[i];
deck[i] = deck[j];
deck[j] = temp;
}
printf("牌已洗好。");
}

`srand(time(NULL))` 应该只在程序开始时调用一次,以确保每次运行得到不同的随机序列。

4. 发牌与展示手牌

洗好牌后,就可以从牌堆顶部发牌给玩家。发牌操作相对简单,就是从洗好的牌堆中取出指定数量的牌。
// 发牌函数
void dealHand(Card deck[], int deckStartIdx, Card hand[], int handSize) {
for (int i = 0; i < handSize; i++) {
hand[i] = deck[deckStartIdx + i];
}
}
// 打印手牌
void printHand(const Card hand[], int handSize, const char* playerName) {
printf("%s的手牌: ", playerName);
for (int i = 0; i < handSize; i++) {
printCard(hand[i]);
}
printf("");
}

5. 核心:识别连续牌(顺子)

识别连续牌是本篇文章的重点。一个顺子(Straight)是指五张点数连续的牌,花色可以不同。例如:A-2-3-4-5 或 10-J-Q-K-A。识别顺子需要以下几个步骤:
对手牌进行排序: 按照点数从小到大排序是识别连续牌的基础。
处理A的特殊性: A既可以作为1(A-2-3-4-5),也可以作为14(10-J-Q-K-A)。
去除重复点数: 如果手牌中有对子,它们不影响顺子的构成,但需要确保我们检查的是不重复的连续点数。
检查连续性: 遍历排序后的(去重)点数,检查是否存在连续的序列。

我们首先实现一个简单的手牌排序函数(这里使用冒泡排序,对于小规模手牌足够)。
// 比较两张牌的点数,用于排序
int compareCards(const void* a, const void* b) {
Card* cardA = (Card*)a;
Card* cardB = (Card*)b;
return cardA->rank - cardB->rank;
}
// 对手牌按点数排序
void sortHand(Card hand[], int handSize) {
qsort(hand, handSize, sizeof(Card), compareCards);
}
// 检查手牌中是否存在顺子
// handSize通常为5或7,取决于游戏规则
int checkStraight(Card hand[], int handSize) {
if (handSize < 5) {
return 0; // 至少需要5张牌才能构成顺子
}
// 1. 对手牌进行排序
sortHand(hand, handSize);
// 2. 提取唯一的点数,并处理A的特殊性
// 为了处理A-2-3-4-5和10-J-Q-K-A两种情况
// 我们可以将A看作1,如果10-J-Q-K-A成立,则A也看作14
int uniqueRanks[handSize + 1]; // +1用于可能出现的Ace作为14的情况
int uniqueCount = 0;
int hasAce = 0;
for (int i = 0; i < handSize; i++) {
// 记录A的存在
if (hand[i].rank == ACE) {
hasAce = 1;
}
// 添加唯一的点数
if (uniqueCount == 0 || uniqueRanks[uniqueCount - 1] != hand[i].rank) {
uniqueRanks[uniqueCount++] = hand[i].rank;
}
}
// 如果有A,并且牌数允许,将A也看作14添加进来,并再次排序uniqueRanks
if (hasAce && uniqueCount < handSize + 1) { // 避免重复添加和数组越界
// 检查是否已经存在K
int hasKing = 0;
for (int i = 0; i < uniqueCount; i++) {
if (uniqueRanks[i] == KING) {
hasKing = 1;
break;
}
}
// 如果有A,并且没有K导致无法形成10JQKA,则可以将A作为14加入
// 但是,如果 uniqueRanks 已经是 [A, 2, 3, 4, 5], 那么 Ace=14 会变成 [A, 2, 3, 4, 5, A(14)]
// 这增加了复杂性,更简单的做法是检查两个可能的顺子。
}
// 重新排序uniqueRanks,因为A(14)可能被添加
// 简单起见,我们直接在uniqueRanks数组上进行检测,对于A-2-3-4-5和10-J-Q-K-A分别判断

// 检查普通顺子 (A-2-3-4-5 到 9-10-J-Q-K)
for (int i = 0; i 5 (例如德州扑克),就需要更复杂的组合逻辑
// 对于固定5张牌的手牌,且已经去重和处理A(14)的情况,
// 只需要检查是否至少有5个独特的点数,并且存在一个长度为5的连续序列
if (currentUniqueCount < 5) {
return 0; // 即使有A(14), 独特的牌少于5张,也无法构成顺子
}
// 遍历所有可能的5张牌组合的起始点
for (int i = 0; i

2025-10-15


上一篇:C语言逆序输出:从基础到高级,掌握数字、字符串与数组的反转艺术

下一篇:C语言高效快速排序算法:原理、实现与性能优化