资源描述
综合实例——猜纸牌游戏
问题描述:
该游戏使用52张扑克牌,黑桃、红桃、方片和梅花各13张(A、2,3,4,…,10,J,Q,K)。游戏规则为:计算机随机抽取8张纸牌,以背面显示在屏幕上。使用者可使用鼠标左键单击翻看某张纸牌(最多翻看8张次,看下张牌时以前翻开的牌会自动翻回去)。从翻开第一张牌时开始计时,10秒钟后自动弹出一个对话框,其中显示下列问题之一,并可接受使用者的回答:
这八张牌中有几张是红桃(黑桃,方块,梅花)?
这八张牌中红桃(黑桃,方块,梅花)的点数(A=1,J=Q=K=0)之和是多少?
这八张牌中红桃(黑桃,方块,梅花)的大牌(J,Q,K,A)共几张?
这八张牌中共有几张大牌(J,Q,K,A)?
如果回答正确,则在总分上加10分;否则扣10分。此时游戏会将所有纸牌翻到正面,供使用者复查。
再单击鼠标左键则重新发牌开始新的一局。
编程要求:
(1)扑克牌显示正确,比例恰当。所有牌的背面应有统一的花纹,牌面可使用简化方法显示,即在牌中央显示一个表示花色的图案(黑桃、红桃、梅花和方片,应使用位图资源),牌面左上角显示点数(2~10,J,Q,K,A)。黑桃和梅花用黑色,红桃和方片用红色。在屏幕上分两行共显示8张纸牌,每行4张。
(2)在适当位置显示提示信息,包括已进行的局数、当前得分、当前剩余时间和已翻开的纸牌张数。
(3)用鼠标左键单击某张纸牌可将其翻转(正面变反面或反面变正面)。在一局中最多可翻开8张牌。
(4)第一次单击鼠标左键时开始计时,10秒钟后自动弹出一对话框用于提问和接受使用者的回答。要求对话框设计美观大方,尺寸合理。对话框下方有一个编辑控件(设置为数值属性),用于输入用户的回答。
(5)对话框上能正确显示问题,而具体显示哪个题目,题目中是什么花色或什么大牌均应随机确定。
(6)用户回答问题后将所有纸牌翻为正面,用户再次单击鼠标左键后开始新的一局。
要点分析:
本题主要涉及到的知识点有:鼠标消息、对话框、定时器消息、位图显示、随机函数的使用,同时还有数值和字符串之间的转换,位图显示位置的计算以及较多的逻辑判断工作,涉及的内容较多,难度较大。
该题的难点在于纸牌的显示、纸牌产生的随机性以及提问问题的产生和判断等。
纸牌的背面可以用一个位图显示,而显示正面较为复杂,因为有52张不同的牌,如果使用52个不同的位图表示,工作量就太大了。这里把显示纸牌的正面分成两个部分,首先用四个位图表示四种不同花色的纸牌,然后用A、2~10、J、Q、K在位图的左上角显示表示不同的点,从而显示出一个完整的纸牌正面。
这里给出的随机产生纸牌的算法非常简单,即用0~51这52个整数来表示纸牌,也就是说,0~12表示梅花A~K、13~25表示方块A~K,26~38表示红桃A~K,39~51表示黑桃A~K。这样,表示纸牌的整数对13取模就得到纸牌的花色(0~3分别表示梅花、方块、红桃和黑桃),除以13得到的余数就是纸牌的点数。
问题和答案通过一个对话框表示出来,而相关的操作则都在定时器消息处理函数中进行。问题的产生涉及到两个随机数,一个产生问题的类型,一个产生问题中纸牌的花色,而相关问题的正确答案在纸牌产生时就已经计算出来,就等着根据不同的问题和答题分蘖节 的答案进行判断了。
解题步骤:
(1)首先用AppWizard生成一个名为PuzzleCard的SD1程序框架,各选项均可用缺省设置。
(2)使用Developer Studio菜单的Insert / Resource…选项调出Insert Source对话框,为项目添加5个位图资源,分别表示纸牌的背面和四种花色纸牌的正面。相应位图的资源ID分别为IDB_BACK、IDB_CLUB、IDB_DIAMOND、IDB_HEART和IDB_SPADE。
(3)使用Developer Studio菜单的Insert / Resource…选项调出Insert Source对话框,为项目添加一个对话框模板资源。
(4)修改对话框模板的标识符ID为IDD-QUESTION,适当调整其大小,并在其上添加两个静态文本控件,其中表示问题的文本近代件ID改为IDC_Question, Caption改为空;另一个静态文本控件的Caption改“答案”。添加一个编辑控件,ID改为IDC_Answer。
(5)利用ClassWizard自动建立对话框类。用Ctrl+W键可直接调出ClassWizard,也可以通过Developer Studio菜单的View/ClassWizard…选项调出。进入ClassWizard后,它会发现已建立的对话框模板资源,并弹出一个对话框询问是否要为该对话框模板建立类。按下“OK”按钮,会弹出New Class对话框,在Name栏填写对话框类的名称CQuestion后按“OK”按钮即可为对话框创建出一个对应的类。
(6)利用ClassWizard为对话框类添加与各控件对应的数据成员。选择MemberVariables选项卡,确保Class Name项为对话框CQuestion类,然后在选项卡下方的窗口中选择各近代件的ID并按下“Add Variable…”按钮,为其添加对应成员变量,如下表4所示。
表4 对话框类中与各控件对应的成员变量
Control IDs
Variable Type
Member variable name
IDC_Answer
int
m_nAnser
IDC_Question
CString
m_strQuestion
(7)为视图类添加一个新的成员函数Initial ( ),无返回值和参数。
(8)用ClassWizard在视图类中建立定时器处理函数OnTimer ( )。
(9)用ClassWizard在视图类中建立鼠标左键消息处理函数OnLButtonDown ( )。
(10)完成以上工作后,即可修改程序框架,添加必要的代码。
源程序清单:
为简化编程,本题的所有数据都在视图类中声明,代码也都在视图中编写,无需对其他类进行操作。
在视图类的头文件中加入以下变量定义:
class CPuzzleCardView: public CView
{
//此处略去若干行由系统生成的代码
public:
bool m_bStart;
bool m_bFirstClick;//第一次翻纸牌
int m_nBigNum[4];//各花色大牌数
int m_nTotalPoint[4];//各花色点数和
int m_nCardCount[4];//各花色牌数
int m_nTotalBigNum;//大牌数
CString m_strInfor;//游戏状态
int m_nGameNum;//局数
int m_nMark;//得分
int m_nRemTime;//剩余时间
int m_nCardNum;//翻过纸牌的数目
CRect m_rectInfo;//游戏状态显示我间
int m_nCard[8];//用整数0~51表示52张牌
bool m_bCardFace[8];//桌面纸牌是否显示正面
CBitmap m_bmCardFace[4], m_bmCardBack;//纸牌的位图
int m_nCard, m_nCardh;//纸牌位图的尺寸
};
在视图类cpp文件的最前面添加以下代码:
#include〃Question. h〃
在视图类构造函数中添加以下初始化代码:
CPuzzleCardView :: CPuzzleCardView ( )
{
//载入纸牌的位图资源
m_bmCardFace[0]. LoadBitmap (IDB_CLUB);
m_bmCardFace[1]. LoadBitmap (IDB_DIAMOND);
m_bmCardFace[2]. LoadBitmap (IDB_HEART);
m_bmCardFace[3]. LoadBitmap (IDB_SPADE);
m_bmCardBack. LoadBitmap (IDB_BACK);
//计算纸牌位图的高和宽
BITMAP bmCard;
m_bmCardBack. GetBitmap (&bmCard);
m_nCardw = bmCard. bmWidth;
m_nCardh = bmCard. bmHeight;
m_rectInfo = CRect (49, 454, 797, 500);
m_nGameNum = 1; //局数
m_nMark = 100; //得分
Initial ( );
}
在视图类的OnDraw( )成员函数中添加以下代码:
void CPuzzleCardView :: OnDraw (CDC * pDC)
{
CPuzzleCardDoc * pDoc = GetDocument ( );
ASSERT_VALID (pDoc);
CDC memDC[8];
CBitmap * pOldDC;
for (int i=0; i<8; i++)
{
int nTopLeftx, nTopLefty; //纸牌左上角坐标
CString strNum;
nTopLeftx = 50 + i% 4 * (m_nCardw + 20);
nTopLefty = 50 + i/4 * (m_nCardh + 20);
memDC[i]. CreateCompatibleDC (NULL);
if (m_bCardFace [i])
{ //显示正面
pOldDC = memDC [i]. SelectObject (&m_bmCardFace [m_nCard [i] / 13]);
switch (m_nCard [i] % 13)
{
case 0:
strNum = 〃A〃;
break;
case 10:
strNum = 〃J〃;
break;
case 11:
strNum = 〃Q〃;
break;
case 12:
strNum = 〃K〃;
break;
default:
strNum. Format (〃%d〃, m_nCard [i] % 13 + 1);
}
}
//显示背面
else pOldDC = memDC [i]. SelectObject (&m_bmCardBack);
pDC - > BitBlt (nTopLeftx, nTopLefty,
m_nCardw, m_nCardh, &memDC [i], 0, 0, SRCCOP Y);
memDC [i]. SelectObject (pOldDC);
pDC - > TextOut (nTopLeftx + 10),
nTopLefty + 10, strNum); //显示纸牌点数
}
CString strInfo;
strInfo. Format (〃局数: % d得分: % d剩余时间: % d已翻开纸牌数: % d〃,
m_nGame.Num, m_nMark, m_nRemTime, m_nCardNum);
pC - > TextOut (50, 460, strInfo); //显示游戏状态
}
鼠标、定时器消息处理函数的代码如下:
void CPuzzleCardView :: OnLButtonDown (UINT nFlags, CPoint point)
{
int nTopLeftx, nTopLefty; //纸牌左上角坐标
int nCardFace = -1;
CRect rectCard; //当前点中的纸牌
CRect rectCardFace; //前一次翻开的纸牌
int i = 0;
if (m_bStart)
{
if (m_nCardNum > 7)
return;
if (m_bFirstClick)
{ //第一次翻开,开始计时
SetTimer (1, 1000, NULL);
m_bFirstClick = false;
}
while (i < 8) //找出前一次翻开的纸牌
{
if (m_bCardFace [i])
{
nCardFace = i;
nTopLeftx = 50 +i % 4 * (m_nCardw + 20);
nTopLefty = 50 + i / 4 * (m_nCardh + 20);
rectCardFace = CRect (nTopLeftx, nTopLefty,
nTopLeftx + m_nCardw, nTopLefty + m_nCardh);
break;
}
i + +;
}
for (i=0; i<8; i + +) //找出当前点中的纸牌翻开
{
nTopLeftx = 50 +i % 4 * (m_nCardw + 20);
nTopLeftx = 50 + i / 4 * (m_nCardh + 20);
rectCard = CRect (nTopLeftx, nTopLefty,
nTopLeftx + m_nCardw, nTopLefty + m_nCardh);
if (rectCard. PtInRect (point))
{
if (nCardFace ! = -1)
{
m_bCardFace [nCardFace] = false;
InvalidateRect (rectCardFace);
}
m_bCardFace [i] = true;
m_nCardNum + +;
InvalidateRect (rectCard);
InvalidateRect (& m_rectInfo);
break;
}
}
}
else
{ //重新开始
Initial ( );
m_nGameNum + +;
Invalidate ( );
}
CView :: OnLButtonDown (nFlags, point)
}
void CPuzzleCardView :: OnTimer (UINT nIDEvent)
{
m_nRemTime - -;
InvalidateRect (& m_rectInfo);
if (m_nRemTime = = 0) //时间到,开始提问
{
KillTimer (1);
for (int i=0; i<8; i + +)
m_bCardFace [i] = false; //所有纸牌翻回背面
Invalidae ( );
CQuestion dlgQuestion;
int nCardType;
int nQuestionType;
int nAnswer;
CString strCardType;
srand ((unsigned) time (NULL));
nCardType = rand ( ) % 4; //随机产生纸牌花色
switch (nCardType)
{
case 0:
strCardType = 〃梅花〃;
break;
case 1:
strCardType = 〃方块〃;
break;
case 2:
strCardType = 〃红桃〃;
break;
case 3:
strCardType = 〃黑桃〃;
}
nQuestionType = rand ( ) % 4; //随机产生问题
switch (nQuestionType)
{//根据问题类型构造问题字符串,得到正确答案
case 0:
digQuestion. m_strQuestion. Format
(〃问题:这八张牌中有几张是% s ?〃, strCardType);
nAnaswer = m_nCardCount [nCardType];
break;
case 1:
dlgQuestion. m_strQuestion. Format
(〃问题:这八张牌中% s的点数(A=1, J=Q=k=0)之和是多少?〃,
strCardType);
nAnswer = m_nTotalPoint [nCardType];
break;
case 2:
dlgQuestion. m_strQuestion. Format
(〃问题:这八张牌中% s大牌(J, Q, K, A)共几张?〃, strCardType);
nAnswer = m_nBigNum [nCardType];
break;
case 3:
dlgQuestion. m_strQuestion. Format
(〃问题:这八张牌中共有几张大牌(J, Q, K, A)?〃);
nAnswer = m_nTotalBigNum;
}
if (dlgQuestion. DoModal ( ) = = IDOK)
{
if (dlgQuestion. m_nAnswer = = nAnswer)
{
m_nMark + = 10;
}
else
{
m_nMark - = 10;
}
}
for (i=0; i<8; i + +)//所有纸牌翻开
m_bCardFace [i] = true;
m_bStart = false;
Invalidate ( );
}
CView :: OnTimer (nIDEvent);
}
初始化函数代码如下:
void CPuzzleCardView :: Initial ( )
{
srand ( (unsigned) time (NULL) );
for (int i=0; i < 4; i + +)
{
m_nCardCount [i] = 0;
m_nTotalPoint [i] = 0;
m_nBigNum[i]=0;
}
m_nTotalBigNum = 0;
m_nRemTime = 10;//初始时间10s
m_nCardNum = 0;
m_bFirstClick = true;
m_bStart = true;
for (i =0; i < 8; i + + )
{/ /随机产生8张纸牌,计算问题答案
m_bCardFace [i] = false;
m_nCard [i] = rand ( ) % 52;
int nType = m_nCard [i] / 13;
int nPoint = m_nCard [i] % 13 + 1;
m_nCardCount [nType] + +;
if (nPoint > 10)
{
nPoint = 0;
m_nBigNum [nType] + +;
m_nTotalBigNum + +;
}
if (nPoint = = 1)
{
m_nBigNum [nType] + +;
m_nTotalBigNum + +;
}
m_nTotalPoint [nType] + = nPoint;
}
}
输入输出:
游戏开始时显示8张纸牌的背面,用户可以使用鼠标左键翻看点中的纸牌,同时以前翻开的牌自动翻回去,如下图1、2、3所示。每次游戏最多可以翻看8张次;系统在用户翻开第一张牌后开始计时,10秒钟后自动弹出一个对话框,其中显示一个问题。若使用者回答正确,则在总分上加10分;否则扣10分。此时游戏会将所有纸牌翻到正面,供使用者查看。
图1 翻开一张牌
图2 提问对话框
图3 最终结果图
小结:
本题的题解多处使用到随机函数,附带介绍了使用整型数表示其他信息的方法。同时,通过显示纸牌的正面,也可以了解显示较为复杂的图案的方法。
展开阅读全文