用VC++设计与实现扫雷系统

更新时间:2024-03-14 23:37:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

用VC++实现扫雷游戏程序

学生姓名:尹一笑 指导老师:颜宏文 摘要:

本课程设计实现类似于Windows操作系统自带的扫雷游戏。在课程设计中,系统开发平台为Windows XP,程序设计语言采用Visual C++,程序运行平台为Windows 2000/XP。在程序设计中,把整个雷区看成一个二维数组,把雷方块定义为具有所在雷区二维数组的行和列、当前状态、方块属性、历史状态的结构体,采用了MFC机制解决问题的方法。整个游戏程序包括了布雷、扫雷过程和结果三个阶段,在处理鼠标响应事件中伴随着GDI绘图。程序通过调试运行,实现了设计目标,能够同时满足扫雷游戏初学者和高手的需要。 关键词:扫雷游戏程序设计 Visual C++ 6.0 GDI绘图 扫雷游戏历史 Visual C++ 6.0 GDI原理基础

2 需求分析

1 功能概述

扫雷游戏的游戏界面如图1.1所示。在这个界面中,由众多面积均等的小方块所组成的区域称之为雷区,雷区的大小由用户设置的游戏等级决定。

玩家标定 地雷 周围雷数提示 玩家标定未知 未知区

图1.1

游戏开始时,系统会在雷区的某些小方块中随机布下若干个地雷。安放好地雷的小方块称之为雷方块,其他的称之为非雷方块。部署完毕后,系统会在其他非雷方块中填充一些数字。某一个具体数字表示与其紧邻的8个方块中有多少雷方块。玩家可以根据这些信息去判断是否可以打开某些方块,并把认为是地雷的方块打上标识。当玩家将所有地雷找出后,其余的非雷方块区域都已打开,此时

游戏结束。在游戏过程中,一旦错误地打开了雷方块则立即失败,游戏结束;当玩家标识的地雷数超过程序设定,虽然打开了全部其余方块,游戏仍然不会结束。

在游戏开始后,雷区上方有两个计数器。右边的计数器显示用户扫雷所花费的总时间,以秒为单位;左边的计数器显示当前还剩余多少个雷方块。

本课程设计实现类似于Windows操作系统自带的扫雷游戏。

游戏需要提供一个菜单栏,上面有不同的相关选项,如游戏的开始、难度设置、退出等。按功能将游戏区域分成两个区域:雷区和提示区。提示区包括两个计数器和一个按键操作结果图像提示。游戏过程中,当玩家用鼠标点击相应的方块,程序就会作出相应的鼠标响应事件,并伴随着GDI绘图,而众多鼠标事件的处理,都是围绕着实现扫雷程序的算法而衍生的。 系统总框图及每个模块的设计分析 2、框架的搭建 3、菜单的制作

4、布雷,扫雷核心算法的实现 5、界面的制作

3 总体设计

3.1 游戏框架的搭建 3.1.1 工程项目的创建

利用应用程序向导创建一个名称为Mine的工程项目。由于不需要诸如工具栏、状态栏等功能,并且扫雷游戏的框架是不允许改变窗口大小的,所以在向导的第四步里面把所有的选项置空,然后点击“Advanced”按钮,在弹出的对话框中选中“Windows Styles”选项卡,将“Maximize box”项置空,其他均使用默认设置。

3.1.2 框架的改造

通过类向导添加一个继承于CFrameWnd的类,命名为CMineWnd,删除CMineDoc、CMineView和CAboutDlg类,将CMineWnd类代替CFrameWnd,让程序启动的时候以此窗口为主窗口予以显示。结果如图1.2。

图1.2

3.2 菜单的制作

参考Windows自带的扫雷游戏,创建出“游戏”和“帮助”菜单,然后通过菜单资源编辑器设定菜单的功能选项,包括难度级别的选择、颜色和音效是否开启、扫雷英雄榜、使用手册、关于软件的信息等。具体的菜单选项分别如图1.3。

3.2.1难度级别的选择

不同的难度级别有不同的雷区大小和不同的布雷数目,所以通过宏定义预定义不同级别的横向方块数目、纵向方块数目和雷数。并将该宏定义放入新建的头文件“MineDefs.h”中。

窗口除了雷区外至少还包括蓝色窗口边缘Frame_wide、白色的视觉效果区line_wide、3D的外壳边框3D_line_wide、雷区mine_area_wide等。于是还需要定义关于位置的宏变量。

由于难度级别的不同,窗口大小也会随之改变,因此通过在CMineWnd类增加一个改变窗口大小的函数SizeWindow()去实现。 通过ClassWizard分别选择“初级”、“中级”和“高级”菜单资源ID,为它们添加处理函数OnMenuPrimary()、OnMenuSecond() 、OnMenuAdvance()。

OnMenuAdvance()的实现如下,另外两个类似。 void CMineWnd::OnMenuAdvance() {

m_uLevel = LEVEL_ADVANCE; m_uXNum = ADVANCE_XNUM; m_uYNum = ADVANCE_YNUM;

m_uMineNum = ADVANCE_MINENUM;

SetCheckedLevel(); InitGame(); Invalidate(); SizeWindow(); }

3.2.2雷区大小的自定义实现

首先新建一个自定义雷区对话框资源(IDD_DLG_CUSTOM),然后添加高度、宽度、雷数三个静态文本控件和三个对应的(IDC_HEIGHT)、(IDC_WIDTH) 、(IDC_NUMBER)编辑框控件,最后将OK和Cancel按钮分别改名为“确定”和“取消”。如图1.3

接着为该对话框创建CDlgCustom类,然后为三个编辑控件分别添加关联变量m_uHeight、m_uNumber、m_uWidth,最后为OK按钮创建命令消息处理函数OnOK(),代码如下所示。 void CDlgCustom::OnOK() {

UpdateData();

if (m_uWidth < 9) m_uWidth = 9; if (m_uWidth > 30) m_uWidth = 30; if (m_uHeight < 9) m_uHeight = 9; if (m_uHeight > 24) m_uHeight = 24; if (m_uNumber < 10) m_uNumber = 10;

if (m_uNumber > m_uWidth * m_uHeight) m_uNumber = m_uWidth * m_uHeight - 1;

CMineWnd *pMine = (CMineWnd*)AfxGetMainWnd();

pMine->SetCustom(m_uWidth, m_uHeight, m_uNumber);// TODO: Add extra validation here

CDialog::OnOK(); }

3.2.3使用帮助的实现

由于Windows 自带有扫雷游戏,所以直接调用它的使用手。为“使用帮助”菜单选项创建命令消息处理函数OnMemuHelpUse(),代码如下所示。 void CMineWnd::OnMemuHelpUse() {

//在命令行调用HH.exe,并输入参数NTHelp.CHM,

}

3.2.4关于信息的实现

void CMineWnd::OnMemuAbout() {

ShellAbout(this->m_hWnd, \扫雷\}

3.2.5 扫雷英雄榜的实现 首先创建两个对话框模板,一个用作当用户胜利结束游戏并打破历史记录后弹出的签名记录对话框模板IDD_DLG_NEWRECORD,另外一个是用以显示以往最高的游戏记录的对话框模板IDD_DLG_HERO。如图1.3和1.4。

然后为IDD_DLG_HERO对话框模板创建CDlgHero类,分别为编辑框控件添加关联变量m_szBHolder、m_szBRecord、m_szEHolder、m_szERecord、m_szIHolder、m_szIRecord,并将Cancel按钮的ID和标题分别改为IDC_RESET和重新计分,三个静态文本标题设置为初级记录、中级记录、高级记录,最后为重新计分按钮创建命令消息处理函数OnReset()和其他成员函数。对IDD_DLG_NEWRECORD对话框模板类似处理。

3.3布雷,扫雷核心算法的设计与实现 3.3.1 算法的设计

把整个雷区看成一个二维数组,a[i][j]周围的雷个数是由如下8个雷区决定的(如果超出边界,应该再加以判断):

a[i-1][j-1], a[i-1][j], a[i-1][j+1], a[i][], a[i][j+1], a[i+1][ j-1], a[i+1][j], a[i+1][j+1],

在被展开时,检查周围的雷数是否与周围标示出来的雷数相等,如果相等则展开周围未标示的雷区。这样新的雷区展开又触发这个事件,就这样递归下去,一直蔓延到不可展开的雷区。 3.3.2 核心算法的实现

整个游戏程序包含3个阶段:布雷、扫雷过程和结果(并不是操作结果展示,而是在扫雷过程中,玩家通过与游戏交互后的操作结果展示)。 首先定义雷方块的数据结构,具体描述如下所示。 typedef struct {

UINT uRow; //所在雷区二维数组的行 UINT uCol; //所在雷区二位数组的列 UINT uState; //当前状态 UINT uAttrib; //方块属性 UINT uOldState; //历史状态 } MINEWND; // 雷方块结构体

然后定义雷方块的状态类别和属性类别。 (1)布雷

//令其打开该文件,即Windows 自带有扫雷游戏的是使用手册 ::WinExec(\NTHelp.CHM\

随即获取一个状态为非雷的点,将它的属性标志为雷,重复这样的工作,直到布下足够的雷为止,其流程如图1.4所示。

开始 生成随机的雷方块的坐标(x,y) 判断(x,y)区域是否已经布下雷 是 否 在(x,y)区域布雷,修改状态数据 否 判断是否布下所有雷 是 结束

图1.4

在CMineWnd类中添加游戏的布雷模块的处理函数,该函数的实现如下。 void CMineWnd::LayMines(UINT row, UINT col) {

//埋下随机种子

srand( (unsigned)time( NULL ) ); UINT i, j;

for(UINT index = 0; index < m_uMineNum;) { //取随即数 i = rand() % m_uYNum; j = rand() % m_uXNum;

if (i == row && j == col) continue; if(m_pMines[i][j].uAttrib != ATTRIB_MINE) { m_pMines[i][j].uAttrib = ATTRIB_MINE;//修改属性为雷 index++; } } }

(2)扫雷

鼠标左击事件

鼠标左击事件流程如图1.5

开始 否 在雷区 是 雷方块定位 其他区域处理 否 游戏结束 继续处理 胜利 否 打开区域 是 失败处理 胜利处理 拓展最大的可能 显示范围 显示 结束

图1.5

当鼠标左键点击雷区域,并且该区域不是雷方块,需要进行打开以及拓展工作。流程如图1.6

开始 获取该区域周围雷 的数目num 否 Num = 0 是 拓展该区域 修改标志,打开显示该区域 结束

图1.6

鼠标左键点击事件的关键代码如下所示。

void CMineWnd::OnLButtonUp(UINT nFlags, CPoint point) {

//笑脸图按钮所在的区域

CRect rcBtn(m_uBtnRect[1], 15, m_uBtnRect[2], 39); //雷区所在的区域

CRect rcMineArea(MINE_AREA_LEFT, MINE_AREA_TOP, MINE_AREA_LEFT + m_uXNum * MINE_WIDTH, MINE_AREA_TOP + m_uYNum * MINE_HEIGHT);

if (rcBtn.PtInRect(point)) {// 点击笑脸图 Invalidate(); InitGame(); }

else if (rcMineArea.PtInRect(point)) {//点击雷区域 CString value; UINT around = 0;

//根据不同的游戏状态作处理 switch(m_uGameState) { //游戏进行状态

case GS_WAIT: case GS_RUN: // first get the MINEWND which if pushing down m_pOldMine = GetMine(point.x, point.y); if (!m_pOldMine) { ReleaseCapture(); return; }

//检测判断当前状态是否为左右鼠标同时按下 if (m_bLRBtnDown) { m_bLRBtnDown = FALSE; OnLRBtnUp(m_pOldMine->uRow, m_pOldMine->uCol); if (m_uGameState == GS_WAIT) { m_uBtnState = BUTTON_NORMAL; Invalidate(); ReleaseCapture(); return; } //假若周围已经标识的雷=周围真正的雷数,拓展 if (m_pOldMine->uState != STATE_FLAG) { OpenAround(m_pOldMine->uRow, m_pOldMine->uCol); } if (ErrorAroundFlag(m_pOldMine->uRow, m_pOldMine->uCol)) { } } else {

Dead(m_pOldMine->uRow, m_pOldMine->uCol);

ReleaseCapture(); return;

//如果游戏尚未开始,点击左键启动游戏 if (m_uGameState == GS_WAIT) { if (m_uTimer) { KillTimer(ID_TIMER_EVENT); m_uTimer = 0; } m_uSpendTime = 1; Invalidate(); if (m_bSoundful) { sndPlaySound((LPCTSTR)LockResource(m_pSndClock), SND_MEMORY | SND_ASYNC | SND_NODEFAULT); } //启动定时器 m_uTimer = SetTimer(ID_TIMER_EVENT, 1000, NULL); //布雷 LayMines(m_pOldMine->uRow, m_pOldMine->uCol); // lay all the mines down //改变游戏状态为\运行/GS_RUN\ m_uGameState = GS_RUN; } if (m_pOldMine->uOldState == STATE_NORMAL) {//当该雷区域为正常未作标记才打开 //如果该区域为雷,则死亡 if (IsMine(m_pOldMine->uRow, m_pOldMine->uCol)) { Dead(m_pOldMine->uRow, m_pOldMine->uCol); ReleaseCapture(); return; } // the special MINEWND is not a mine //不是雷的时候,获取其周围的雷数目 around = GetAroundNum(m_pOldMine->uRow, m_pOldMine->uCol); // 如果为空白区域,拓展,否则打开该区域(显示周围有多少雷数) if (around == 0) ExpandMines(m_pOldMine->uRow, m_pOldMine->uCol);

//判断是否为胜利 if (Victory()) { Invalidate(); ReleaseCapture(); return; } } break; case GS_VICTORY: case GS_DEAD: ReleaseCapture(); // release the cursor return; default : break; } m_uBtnState = BUTTON_NORMAL; Invalidate(); } else

{//点击非雷区域 if (m_uGameState == GS_WAIT || m_uGameState == GS_RUN) { m_uBtnState = BUTTON_NORMAL; InvalidateRect(rcBtn); } }

ReleaseCapture(); // release the cursor CWnd::OnLButtonUp(nFlags, point); }

在函数体的开始部分,先用rcBtn和rcMineArea两个矩形变量存储游戏的用户提示区域位置中的笑脸图区域以及雷区域的位置。利用接口函数PtInRect()判断当

else DrawDownNum(m_pOldMine, around); }

else if (m_pOldMine->uOldState == STATE_DICEY) {

//标志为“?”问号的时候 m_pOldMine->uState = STATE_DICEY; }

前鼠标的位置(由参数point携带鼠标当前位置信息)是否在这两个区域内,如果检测到鼠标左键点击并释放在笑脸图的按钮区域rcBtn上,则调用初始化函数重新开始游戏,如果检测到鼠标左键点击并释放在雷区域rcMineArea,假若当前游戏状态处于已初始化完成但尚未开始的状态GS_WAIT时,则打开计时器,并且调用LayMines()函数进行布雷,然后修改游戏状态为GS_RUN进入游戏。接着判断点击在小方块的状态是否被用于通过右键标记(可以标记为雷或者未知,此时游戏规则规定左键点击不生效),如果未标记,该状态为普通状态STATE_NORMAL时,先通过IsMine()检测是否点中地雷而失败地结束游戏,如果是,则调用函数Dead()来进行失败后的工作处理,反之对它进行打开显示与拓展操作。先通过GetAroundNum()函数获取当前小方块相邻的8个位置的雷数。如果当前小方块相邻区域的雷数为0,则可以向8个方向进行拓展,并显示该方块区域,直到不可拓展为止;如果当前小方块相邻区域的雷数不为0,则显示该方块区域的相邻雷数,用作提供用户对其他位置的信息判断的提示。拓展操作的实现代码如下。

void CMineWnd::ExpandMines(UINT row, UINT col) {

UINT i, j;

UINT minRow = (row == 0) ? 0 : row - 1; UINT maxRow = row + 2;

UINT minCol = (col == 0) ? 0 : col - 1; UINT maxCol = col + 2;

UINT around = GetAroundNum(row, col);

//显示该区域的方块状态

m_pMines[row][col].uState = 15 - around; m_pMines[row][col].uOldState = 15 - around;

// “打开”该区域,重绘 DrawSpecialMine(row, col);

//对周围一个雷都没有的空白区域 if (around == 0) { for (i = minRow; i < maxRow; i++) { for (j = minCol; j < maxCol; j++) {//对于周围可以拓展的区域进行的规拓展 if (!(i == row && j == col) && m_pMines[i][j].uState == STATE_NORMAL && m_pMines[i][j].uAttrib != ATTRIB_MINE) { if (!IsInMineArea(i, j)) continue;

ExpandMines(i, j); // 递归拓展操作 } } } } }

经过打开或拓展后,最后通过Victory()判断游戏是否已经胜利结束,如果是则作胜利处理。

鼠标右击事件

鼠标右击事件的流程如图1.7所示。

开始 雷方块定位 判断历史属性以及相关状态 修改相关状态 显示 结束

实现代码如下所示。

void CMineWnd::OnRButtonDown(UINT nFlags, CPoint point) {

//笑脸图按钮所在的区域

CRect rcBtn(m_uBtnRect[1], 15, m_uBtnRect[2], 39); //雷区所在的区域

CRect rcMineArea(MINE_AREA_LEFT, MINE_AREA_TOP, MINE_AREA_LEFT + m_uXNum * MINE_WIDTH, MINE_AREA_TOP + m_uYNum * MINE_HEIGHT);

m_bLRBtnDown = FALSE;

if (rcMineArea.PtInRect(point)) {//点击雷区域 if (m_uGameState == GS_WAIT || m_uGameState == GS_RUN) { m_pNewMine = GetMine(point.x, point.y); if (!m_pNewMine) return; //检测判断当前状态是否为左右鼠标同时按下 if (nFlags == (MK_LBUTTON | MK_RBUTTON)) { m_bLRBtnDown = TRUE; OnLRBtnDown(m_pNewMine->uRow, m_pNewMine->uCol); } else { switch(m_pNewMine->uState) {

//普通状态

case STATE_NORMAL: m_pNewMine->uState = STATE_FLAG; m_pNewMine->uOldState = STATE_FLAG; m_nLeaveNum--; break; //标记状态 case STATE_FLAG: m_pNewMine->uState = STATE_DICEY; m_pNewMine->uOldState = STATE_DICEY; m_nLeaveNum++; break; //未知状态 case STATE_DICEY: m_pNewMine->uState = STATE_NORMAL; m_pNewMine->uOldState = STATE_NORMAL; break; default: break; } } Invalidate();

本文来源:https://www.bwwdw.com/article/jm48.html

Top