### 20 点游戏

相信许多人小时候都玩过 20 点游戏。游戏的规则是：双方比较牌面的点数，如果点数在 20 点或 20 点以内，点数大的为赢家。如果点数相同，则庄家输。如果某一方点数大于 20 点，则该方输。其中牌面“1…10”分配对应点数 1…10，牌面为“J、Q、K”，点数均为 1。下面我们根据该游戏规则，设计一个 20 点游戏，效果如图 1 所示。

图 1 运行效果

在设计 20 点游戏时，核心技术就是牌面的设计。每一个牌面需要有一个牌号，标识每一张牌，牌号范围 1…52，牌面还需要有一个 ID 号，也用于标识牌面，但是它不区分牌的类型（黑桃、红桃、梅花、方块），取值范围 1…13，其中 1…10 分配对应牌面 1…10，11…13 对应牌面 J、Q、K。牌面还需要一个属性标识牌值，即牌的点数。牌面 1…10 分别对应牌值 1…10，牌面 J、Q、K 的牌值均为 1。

根据上面的描述，设计一个卡片类 CCard，该类派生于 CStatic 类，用于显示牌面。其主要成员如下：

//黑桃，红桃， 梅花， 方块

enum CardType {CT_Black, CT_Red, CT_Clubs, CT_Diamond};

CImageList* m_pImgList; //图像列表

CardType m_CardType; //牌面类型

int m_nCardNumber; //牌号 1...52

int m_nCardID; //牌 ID,1...10, J ,Q ,K

int m_nCardValue; //牌值, 1...10, 其中 J Q K 牌值为 1

BOOL m_bShowBK; //是否显示背面

为了方便显示图像，在 CCard 类中，规定 m_nCardNumber 成员与 m_nCardID 成员、m_CardType 成员和 m_nCardValue 成员是相关的。例如 m_nCardNumber 为 1，则表示 m_nCardID 为 1（A），m_CardType 为 CT_Black（黑桃），m_nCardValue 为 1。m_nCardNumber 为 2，则表示 m_nCardID 为 1（A），m_CardType 为 CT_Black（红桃），m_nCardValue 为 1。而 m_nCardNumber 为 7，则表示 m_nCardID 为 2，m_CardType 为 CT_Black（梅花），m_nCardValue 为 2。这样，在生成一个牌号时，就可以确定该牌面的类型、牌 ID 和牌值。

接下来的任务是显示牌面的图像。为了方便管理图片，笔者将牌面以 1*4 的形式排列，形成一个位图，如图 2 所示。

图 2 牌面位图显示

程序中使用图像列表数组将 13 个（1...10, J ,Q ,K）这样的位图按顺序加载到图像列表数组的每一个元素中。当需要显示一个牌面时，根据牌号确定该牌面的牌值，从图像列表数组中确定一个图像列表控件，然后根据牌面类型确定输出位图的哪一个部分（共 4 个部分）。

void CCard::OnPaint()

{

CPaintDC dc(this);

if (m_nCardNumber != -1)

{

//根据牌号计算牌的类型

int nDiv = m_nCardNumber / CARD_TYPECOUNT;

int nMod = m_nCardNumber % CARD_TYPECOUNT;

if (nMod==0)

nDiv--;

CPoint pt(0, 0);

if (!m_bShowBK)

{

m_pImgList->Draw(&dc, nDiv, pt, ILD_NORMAL);

}

else

{

CBitmap bmp;

bmp.LoadBitmap(IDB_BK);

CDC memDC;

memDC.CreateCompatibleDC(&dc);

memDC.SelectObject(&bmp);

CRect clientRC;

GetClientRect(clientRC);

dc.BitBlt(0, 0, clientRC.Width(), clientRC.Height(), &memDC, 0, 0, SRCCOPY);

bmp.DeleteObject();

}

}

}

下面我们来介绍游戏的设计过程。

（1）创建一个基于对话框的工程，工程名称为“TwentyPoint”。

（2）向对话框中添加图片、按钮、静态文本和单元按钮等控件，如图 3 所示。

图 3 20 点游戏资源窗口

（3）向对话框中添加成员变量。

CImageList m_ImgList[CARD_TYPECOUNT]; //牌面图像列表

IMAGEINFO imgInfo; //图像信息

CObList m_CardList; //用户牌面列表

CObList m_ComputerList; //电脑牌面列表

int m_CardNumbers[CARD_COUNT]; //记录已经出现的牌面，防止产生重复的牌面

BOOL m_UserPrior; //是否为用户先手

（4）向对话框中添加 CalcNumber 方法，计算牌面的点数。

int CTwentyPointDlg::CalcNumber(BOOL bUser)

{

int nPointCount = 0;

if (bUser) //计算用户的分数

{

for(POSITION pos = m_CardList.GetHeadPosition(); pos != NULL;)

{

CCard* pCard = (CCard*)m_CardList.GetNext(pos);

nPointCount += pCard->m_nCardValue; //累加点数

}

}

else //计算电脑的分数

{

for(POSITION pos = m_ComputerList.GetHeadPosition(); pos != NULL;)

{

CCard* pCard = (CCard*)m_ComputerList.GetNext(pos);

nPointCount += pCard->m_nCardValue; //累加点数

}

}

return nPointCount; //返回结果

}

（5）向对话框中添加 ComputerClubs 方法，用于实现电脑要牌。

void CTwentyPointDlg::ComputerClubs()

{

label:

int nCount = m_ComputerList.GetCount(); //获取当前电脑要牌数量

CCard* pCard;

int nCard = RandomCard(); //产生随机牌面

int nImageType = nCard % CARD_TYPECOUNT; //确定牌面类型

nImageType --;

if (nImageType == -1)

{

nImageType = CARD_TYPECOUNT - 1;

}

pCard = new CCard(&m_ImgList[nImageType]); //构建卡片控件

int nLeft = 0;

nLeft = nCount*15 + 330;

int nTop = 240;

//创建卡片控件

pCard->Create("", WS_VISIBLE|WS_CHILD, CRect(nLeft, nTop, nLeft + CARD_WIDTH, nTop + CARD_HEIGHT), this);

pCard->SetCardNumber(nCard); //设置卡片号码

pCard->ShowCardBK(TRUE); //显示卡片背景

m_ComputerList.AddTail(pCard); //添加卡片到列表中

pCard->Invalidate(); //更新卡片窗口

int nNumber = CalcNumber(FALSE); //计算分数

if (nNumber > 20) //超过了 20 点，用户赢了

{

ShowCard();

MessageBox("您赢了!", "提示");

m_UserPrior = TRUE;

InitCardList();

InitCardNumbers();

}

else

{

if (m_UserPrior) //如果用户先手

{

int nUserNumber = CalcNumber(true); //计算用户分数

if (nNumber > nUserNumber) //比较分数

{

ShowCard(); //电脑看牌,显示电脑牌面

m_UserPrior = TRUE;

MessageBox("您输了!", "提示"); //电脑赢

InitCardNumbers();

InitCardList();

}

else

{

goto label; //继续要牌

}

}

else if (nNumber < 12) //电脑先手，小于 16 点继续要牌

{

goto label; //继续要牌

}

}

}

（6）向对话框中添加 InitCardList 方法，初始化卡片列表。

void CTwentyPointDlg::InitCardList()

{

//遍历用户卡片列表

for(POSITION pos=m_CardList.GetHeadPosition(); pos != NULL;)

{

CCard* pCard = (CCard*)m_CardList.GetNext(pos); //获取卡片

if (pCard != NULL)

{

pCard->DestroyWindow(); //释放卡片窗口

delete pCard; //删除卡片对象

}

}

m_CardList.RemoveAll(); //移除所有的卡片对象

for(pos=m_ComputerList.GetHeadPosition(); pos != NULL;) //遍历电脑卡片列表

{

CCard* pCard = (CCard*)m_ComputerList.GetNext(pos);

if (pCard != NULL)

{

pCard->DestroyWindow(); //释放卡片窗口

delete pCard; //删除卡片对象

}

}

m_ComputerList.RemoveAll(); //移除所有的卡片对象

}

（7）向对话框中添加 InitCardNumbers 方法，初始化卡号。

void CTwentyPointDlg::InitCardNumbers()

{

for(int i=0; i<CARD_COUNT; i++)

{

m_CardNumbers[i] = -1; //设置卡号为-1

}

}

（8）向对话框中添加 RandomCard 方法，随机产生卡片。

int CTwentyPointDlg::RandomCard()

{

int nNumber = 0;

label: //定义标签

nNumber = rand() % 53; //产生随机数

if (nNumber == 0)

goto label;

for(int i=0; i<CARD_COUNT; i++) //判断卡号是否存在，如果存在，重新生成卡片

{

if (m_CardNumbers[i] == nNumber)

goto label;

}

for(i=0; i<CARD_COUNT; i++)

{

if (m_CardNumbers[i] == -1)

{

m_CardNumbers[i] = nNumber; //记录当前生成看的卡片

break;

}

}

return nNumber; //返回卡片号码

}

（9）处理“开始游戏”按钮的单击事件，开始新的游戏。

void CTwentyPointDlg::OnBeginGame()

{

InitCardList(); //初始化卡片列表

InitCardNumbers(); //初始化号码

CButton* pRadioBtn = (CButton*)GetDlgItem(IDC_COMPUTERPRIOR);

if (pRadioBtn != NULL)

{

int nState = pRadioBtn->GetCheck();

if (nState > 0)

{

m_UserPrior = FALSE;

ComputerClubs(); //电脑要牌

}

else

{

m_UserPrior = TRUE;

}

}

}

（10）处理“要牌”按钮的单击事件，开始用于要牌。

void CTwentyPointDlg::OnDeal()

{

int nCount = m_CardList.GetCount();

if (nCount >= 20) //用户要牌不能大于 20 张

{

MessageBox("不能再要牌了!", "提示");

return;

}

CCard* pCard;

int nCard = RandomCard(); //产生随机牌面

int nImageType = nCard % CARD_TYPECOUNT;

nImageType --;

if (nImageType == -1)

{

nImageType = CARD_TYPECOUNT - 1;

}

pCard = new CCard(&m_ImgList[nImageType]); //构建卡片对象

int nLeft = 0;

nLeft = nCount*15 + 50;

int nTop = 240;

//创建卡片窗口

pCard->Create("", WS_VISIBLE|WS_CHILD, CRect(nLeft, nTop, nLeft + CARD_WIDTH, nTop + CARD_HEIGHT), this);

pCard->SetCardNumber(nCard); //设置卡号

m_CardList.AddTail(pCard);

pCard->Invalidate();

if (CalcNumber(TRUE) > 20) //超过了 20 点，用户输了

{

ShowCard();

MessageBox("您输了!", "提示");

InitCardList();

InitCardNumbers();

}

}

（11）处理“看牌”按钮单击事件，比较用户牌面点数和电脑牌面点数大小。

void CTwentyPointDlg::OnLookCard()

{

if (m_CardList.GetCount() > 0)

{

if (m_UserPrior) //如果是用户先手

{

ComputerClubs(); //电脑开始要牌

}

else

{

if (m_ComputerList.GetCount() < 1) //如果电脑没有要牌

ComputerClubs(); //电脑开始要牌

int nUserNumber = CalcNumber(TRUE); //计算用户点数

int nComNumer = CalcNumber(FALSE); //计算电脑点数

if (nUserNumber > nComNumer)

{

ShowCard(); //显示电脑牌面

MessageBox("您赢了!", "提示");

}

else

{

ShowCard(); //显示电脑牌面

MessageBox("您输了!", "提示");

}

m_UserPrior = TRUE;

InitCardList(); //初始化卡片

InitCardNumbers(); //初始化卡号

}

}

}