赞
踩
扫雷游戏大家应该都玩过,虽然win10以后更新了,但还是win7版的好用,在代码写烦了来上几把换换脑子还是不错滴。
下面用c语言模仿win7版的写一个控制台扫雷游戏。
游戏中要对控制台的大小、文字颜色、光标大小、光标位置、鼠标输入等需要进行一些设置,下面列出所需要的一些函数。
控制台大小通过mode命令设置。这个命令中的大小是指行和列的字符数而非像素。
//设置控制台大小
void SetSize(unsigned uCol, unsigned uLine)
{
char cmd[64];
sprintf(cmd, "mode con cols=%d lines=%d", uCol, uLine);
system(cmd);
}
控制台颜色设置详情可查看: 用C代码设置Windows控制台颜色
//更改文字颜色
// color为每一种颜色所代表的数字,范围是0~15
void setColor(WORD color)
{
HANDLE HOutput = GetStdHandle(STD_OUTPUT_HANDLE); //获取标准输出的句柄
SetConsoleTextAttribute(HOutput, color);
}
更改光标样式要用到结构体CONSOLE_CURSOR_INFO 和控制台API函数SetConsoleCursorInfo,通过这个结构体和函数可以设置光标的可见性和大小。
// 设置光标样式
void SetCursorType(BOOL bVisible)
{
HANDLE HOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO curInfo;
curInfo.bVisible = bVisible; //更改光标显示状态,TRUE显示光标,FALSE隐藏光标
curInfo.dwSize = 100; // 光标大小1到100之间。 范围从完全填充单元格到单元底部的水平线条。
SetConsoleCursorInfo(HOutput, &curInfo);
}
这个函数用的比较多,用来定位要输出的字符的位置。
//设置光标位置,起点从1开始
void MoveCursorTo(int nCols, int nRows)
{
COORD crdLocation = {nCols, nRows};
HANDLE HOutput = GetStdHandle(STD_OUTPUT_HANDLE); //获取标准输出的句柄
SetConsoleCursorPosition(HOutput, crdLocation); //设置光标位置
}
这个函数用的比较多,用来定位要输出的字符的位置。
/// 提取出窗口中第x行y列的位置的字符
/// @return 返回指定位置显示的字符,空白处返回空格
char GetCursorStr(int x, int y)
{
COORD pos = {x, y};
char str = 0;
DWORD read;
// HANDLE HOutput = GetStdHandle(STD_OUTPUT_HANDLE); //获取标准输出的句柄
ReadConsoleOutputCharacterA(HOutput, &str, 1, pos, &read);
return str;
}
通过API函数MoveWindow设置窗口的位置。在设置窗口位置之前,要先取得控制台窗口大小和屏幕大小,通过计算得出屏幕居中的位置。
//居中显示控制台 BOOL MoveConsoleWindow() { //获取屏幕大小 HDC hdc = GetDC(NULL); int cx = GetDeviceCaps(hdc, DESKTOPHORZRES); //窗口左侧位置 int cy = GetDeviceCaps(hdc, DESKTOPVERTRES); //窗口顶部位置 ReleaseDC(NULL, hdc); //获取控制台大小 HWND HOutput = GetConsoleWindow(); RECT rect; GetWindowRect(HOutput, &rect); int w = rect.right - rect.left; //窗口宽度 int h = rect.bottom - rect.top; //窗口高度 cx = (cx - w) / 2; cy = (cy - h) / 2; return MoveWindow(HOutput, cx, cy, w, h, TRUE); }
除了上面,还需要对控制台的鼠标操作进行屏蔽。
#define DISABLE_QUICK_EDIT_MODE 0x01 #define DISABLE_INSERT_MODE 0x02 #define DISABLE_MOUSE_INPUT 0x03 #define DISABLE_ALL (DISABLE_QUICK_EDIT_MODE | DISABLE_INSERT_MODE | DISABLE_MOUSE_INPUT) //执行控制台相关设置 void CloseConsoleMode(UINT uTag) { HANDLE HInput = GetStdHandle(STD_INPUT_HANDLE);//这里用标准输入设备 DWORD mode; GetConsoleMode(HInput, &mode); if (uTag & DISABLE_QUICK_EDIT_MODE) mode &= ~ENABLE_QUICK_EDIT_MODE; //移除快速编辑模式 if (uTag & DISABLE_INSERT_MODE) mode &= ~ENABLE_INSERT_MODE; //移除插入模式 if (uTag & DISABLE_MOUSE_INPUT) mode &= ~ENABLE_MOUSE_INPUT; //移除鼠标输入 SetConsoleMode(HInput, mode); }
win版的扫雷有3种难度和自定义难度,这里通过选择界面选择游戏难度。
先看看效果
定义结构体保存游戏的格子数量和地雷数量。
#define CONSOLEWIDTH 80 //控制台宽度 #define CONSOLEHEIGHT 40 //控制台高度 #define DEFCOLOR 0x7 //控制台默认文字颜色 typedef struct _point { short x; short y; } Point; //格子大小 typedef struct _MapSize { Point MapSize; //格子数量 short Mine_Count; //地雷数量 } Map_Size; //按照win扫雷定义尺寸和雷数 Map_Size defMapSize[3] = {{{9, 9}, 10}, {{16, 16}, 40}, {{30, 16}, 99}};
void PrintName()
{
system("cls");
setColor(0x2);
char buf[100];
int l = (CONSOLEWIDTH - 20) / 2 + 20;
sprintf(buf, "\n%%%ds\n", l);
printf(buf, "欢迎来到扫雷小游戏");
sprintf(buf, "%%%ds\n", (CONSOLEWIDTH - 16) / 2 + 16);
printf(buf, "-- by Apull --");
setColor(DEFCOLOR);
}
//难度选择 void Choose() { MapSize = defMapSize[0]; PrintName(); printf("\t\t选择游戏难度:\n"); setColor(0xA); printf("\t\t 1、初级:%dx%d,共%d个地雷\n", defMapSize[0].MapSize.x, defMapSize[0].MapSize.y, defMapSize[0].Mine_Count); setColor(0xE); printf("\t\t 2、中级:%dx%d,共%d个地雷\n", defMapSize[1].MapSize.x, defMapSize[1].MapSize.y, defMapSize[1].Mine_Count); setColor(0xc); printf("\t\t 3、高级:%dx%d,共%d个地雷\n", defMapSize[2].MapSize.x, defMapSize[2].MapSize.y, defMapSize[2].Mine_Count); setColor(0x9); printf("\t\t 4、自定义难度\n"); setColor(DEFCOLOR); printf("\t\t 0、退出\n"); printf("\t\t输入你的选择(回车默认选择1): "); char ch; BOOL inputOK = TRUE; while (inputOK) { ch = _getch(); switch (ch) { case '1': case Key_ENTER: MapSize = defMapSize[0]; inputOK = FALSE; break; case '2': MapSize = defMapSize[1]; inputOK = FALSE; break; case '3': MapSize = defMapSize[2]; inputOK = FALSE; break; case '4': printf("\n"); MapSize.MapSize.y = OptMines("\t\t 高度:", 9, 24); MapSize.MapSize.x = OptMines("\t\t 宽度:", 9, 30); MapSize.Mine_Count = OptMines("\t\t 雷数:", 10, 668); inputOK = FALSE; break; case '0': case Key_ESC: EXIT(0); break; default: break; } } MapSize.MapSize.x = MapSize.MapSize.x * 2 - 1; }
//自定义难度 int OptMines(char *str, int min, int max) { int x; char buf[20] = {0}; if (max > 30) // max > 30 表示输入的是雷数 { max = (int)(MapSize.MapSize.x * MapSize.MapSize.y * 0.2); //地雷数量是所有格子数的20% } while (TRUE) { printf(str); printf("(%d-%d):", min, max); gets_s(buf, 20); if (buf[0] == '0') EXIT(0); x = atoi(buf); if (x < min || x > max) { setColor(0x04); printf("\t\t输入超出范围,请重新输入。\n"); setColor(DEFCOLOR); } else return x; } }
结构体struct _mine中定义一个格子要显示的字符、周围8个方向上地雷数量以及格子本身的状态。
//定义地雷格子状态 enum Mine_STATU { HIDDEN_SAFE, VISIBLE_SAFE, HIDDEN_MINE }; //地雷格子定义 typedef struct _mine { char ch; char hasMines; enum Mine_STATU statu; } Mine; #define FLAG '@' //标记的地雷 #define MAPNORMAL 'o' //正常格子 #define MINEICO '*' //地雷
使用二维数组minefield存储每个格子,根据难度选择给二维数组分配内存大小。
Mine **minefield = NULL; //格子数组 //生成格子数组,并填充显示字符 Mine **initMapArr() { Mine **minefield = (Mine **)calloc(MapSize.MapSize.y, sizeof(Mine *)); if (minefield == NULL) { printf("内存分配错误!"); EXIT(-1); } for (int i = 0; i < MapSize.MapSize.y; i++) { minefield[i] = (Mine *)calloc(MapSize.MapSize.x, sizeof(Mine)); if (minefield == NULL) { printf("内存分配错误!"); EXIT(-1); } for (int j = 0; j < MapSize.MapSize.x; j++) { minefield[i][j].ch = (j % 2) ? ' ' : MAPNORMAL; } } return minefield; }
Point *MinesPos = NULL; //地雷所在位置 Point *FindMinesPos = NULL; //标记的地雷所在位置 int MineCount = 0; //地雷总数 //产生地雷 void init_Mine(Point *point, int MineCount) { Point pot; for (int i = 0, j; MineCount > i;) { do { pot.x = rand() % MapSize.MapSize.x; } while (pot.x % 2 != 0); pot.y = rand() % MapSize.MapSize.y; for (j = 0; j < i; j++) { if (point[j].x == pot.x && point[j].y == pot.y) break; } if (i == j) { point[i].x = pot.x; point[i].y = pot.y; i++; } } }
数组MinesPos中保存着地雷的坐标,直接在格子中把这些坐标标记为地雷。
//写入雷的位置
void init_Map(Mine **minefield, Point *point, int MineCount)
{
for (int i = 0; i < MineCount; i++)
{
minefield[point[i].y][point[i].x].statu = HIDDEN_MINE;
minefield[point[i].y][point[i].x].hasMines = -1;
}
}
//计算周围地雷数量 void calMine(Mine **minefield) { int count; for (int i = 0; i < MapSize.MapSize.y; i++) { for (int j = 0; j < MapSize.MapSize.x; j += 2) { if (minefield[i][j].statu == HIDDEN_MINE) continue; count = 0; if (i > 0 && j > 1 && minefield[i - 1][j - 2].statu == HIDDEN_MINE) //左上角 count++; if (i > 0 && minefield[i - 1][j].statu == HIDDEN_MINE) //上面 count++; if (i > 0 && j < MapSize.MapSize.x - 2 && minefield[i - 1][j + 2].statu == HIDDEN_MINE) //右上角 count++; if (j < MapSize.MapSize.x - 1 && minefield[i][j + 2].statu == HIDDEN_MINE) //右面 count++; if (i < MapSize.MapSize.y - 1 && j < MapSize.MapSize.x - 2 && minefield[i + 1][j + 2].statu == HIDDEN_MINE) //右下角 count++; if (i < MapSize.MapSize.y - 1 && minefield[i + 1][j].statu == HIDDEN_MINE) //下面 count++; if (i < MapSize.MapSize.y - 1 && j > 0 && minefield[i + 1][j - 2].statu == HIDDEN_MINE) //左下角 count++; if (j > 0 && minefield[i][j - 2].statu == HIDDEN_MINE) //左面 count++; minefield[i][j].hasMines = count; } } }
Point StatuRow; //状态显示行位置
clock_t start_t, end_t; //记录游戏时间
//格子范围,限定地雷显示和操作的范围
int top = 0, left = 0, right = 0, bottom = 0;
显示游戏介绍,并返回显示的行数。
// 输出游戏介绍 int intro() { int line = 3; //加上之前的标题信息2行 line += 7; StatuRow.y = line - 2; StatuRow.x = left; PrintName(); //显示标题信息 printf("\t\t游戏方法介绍:\n"); setColor(0xE); printf("\t\t W、S、A、D/↑、↓、←、→"); setColor(DEFCOLOR); printf(":控制光标上下左右移动\n"); setColor(0xE); printf("\t\t PageUP、PageDown、Home、End"); setColor(DEFCOLOR); printf(":控制光标跳到四边位置\n"); setColor(0xE); printf("\t\t 空格"); setColor(DEFCOLOR); printf(":打开当前格子"); setColor(0xE); printf(" R"); setColor(DEFCOLOR); printf(":回到选择界面"); setColor(0xE); printf(" ESC"); setColor(DEFCOLOR); printf(":结束游戏\n"); ShowStatu(); printf("\n\n"); return line; }
显示游戏信息和游戏时间,光标移动到要输出的位置,输出内容覆盖就内容,完成状态刷新。
在输出状态的时候要移动光标,游戏过程中会看到光标跳到状态显示位置后有会跳回格子中,为避免这种情况,在开始输出状态时使用SetCursorType隐藏光标,输出完毕后再显示光标
//显示游戏状态
void ShowStatu()
{
static int total_t = 0;
end_t = clock();
total_t = (end_t - start_t) / CLOCKS_PER_SEC;
SetCursorType(FALSE);
MoveCursorTo(StatuRow.x, StatuRow.y);
printf("\t\t共有雷数:%-3d 剩余雷数:%-3d 用时:%ds ", MapSize.Mine_Count, MineCount, total_t);
SetCursorType(TRUE);
}
//确定绘制地雷格子范围 void DrawMine() { left = right = bottom = 0; top = intro(); drawMap(minefield); right = MapSize.MapSize.x + left; bottom += MapSize.MapSize.y + top - 1; MoveCursorTo(left, top); start_t = clock(); //开始计时 } //绘制地雷格子和外框,设置各边范围 void drawMap(Mine **minefield) { int l = (CONSOLEWIDTH - MapSize.MapSize.x) / 2 - 4; if (l % 2 == 0) l++; int t = top; MoveCursorTo(l, t++); printf("┌"); for (int i = 0; i <= MapSize.MapSize.x; i++) printf("─"); printf("┐\n"); MoveCursorTo(l, t++); // 输出中间部分 for (int i = 0; i < MapSize.MapSize.y; i++) { printf("│ "); for (int j = 0; j < MapSize.MapSize.x; j++) { printf("%c", minefield[i][j].ch); } printf("│\n"); MoveCursorTo(l, t++); } printf("└"); for (int i = 0; i <= MapSize.MapSize.x; i++) printf("─"); printf("┘\n"); top += 1; left = l + 2; }
至此地雷界面绘制基本完成,下面来看看操作部分。
定义出后面要用到达按键变量
//按键定义 enum KB_KEY { Key_FN = 0xE0, // 功能键标志 Key_Up = 0x48, // 向上方向键 Key_Down = 0x50, // 向下方向键 Key_Left = 0x4b, // 向左方向键 Key_Right = 0x4d, // 向右方向键 Key_Home = 0x47, // Home键 Key_End = 0x4f, // End键 Key_PageUp = 0x49, // PageUp键 Key_PageDown = 0x51, // PageDown键 Key_ESC = 0x1B, // ESC键 Key_ENTER = 0xD, // 回车键 Key_SPACE = 0x20, // 空格键 Key_A = 'A', Key_a = 'a', Key_D = 'D', Key_d = 'd', Key_W = 'W', Key_w = 'w', Key_S = 'S', Key_s = 's', Key_R = 'R', Key_r = 'r' } KEY;
int row = top; //行 int col = left; //列 char ch; int chInput = 0; int isGame = 1; //游戏继续标志 int x, y; while (isGame) { if (_kbhit() != 0) //当_kbhit返回值不为0的时候,代表用户有按键按下。 { chInput = _getch(); if (chInput == Key_FN || chInput == 0) // 方向键读2次 chInput = _getch(); switch (chInput) //控制操作 { case 'a': case 'A': case Key_Left: col -= 2; if (col <= left) col = left; break; case 'd': case 'D': case Key_Right: col += 2; if (col >= right) col = right - 1; break; case 'w': case 'W': case Key_Up: row--; if (row <= top) row = top; break; case 's': case 'S': case Key_Down: row++; if (row >= bottom) row = bottom; break; case Key_SPACE: //空格点开 ... break; case Key_ENTER: //标记地雷 ... break; case Key_Home: //跳到第一列 col = left; break; case Key_End: //跳到最后一列 col = right - 1; break; case Key_PageUp: //跳到第一行 row = top; break; case Key_PageDown: //跳到最后一行 row = bottom; break; case Key_ESC: //退出游戏 isGame = 0; break; case Key_R: case Key_r: //复位到选择界面 init(); DrawMine(); break; default: //其他输入字符不动作 break; } MoveCursorTo(col, row); } Sleep(150); ShowStatu(); MoveCursorTo(col, row); }
case Key_ENTER: //标记地雷
if (flagMine(row, col))
GameOver(WIN_GAME);
col += 2;
if (col >= right)
col = right - 1;
break;
//标记地雷 BOOL flagMine(int row, int col) { char ch; BOOL iswin = FALSE; ch = GetCursorStr(col, row); //获取光标处字符 if (ch == FLAG && MineCount < MapSize.Mine_Count) //切换已标记 { FindMinesPos[MineCount].x = FindMinesPos[MineCount].y = -1; setColor(DEFCOLOR); putchar(MAPNORMAL); MineCount++; } else if (ch == MAPNORMAL && MineCount > 0) { MineCount--; FindMinesPos[MineCount].x = col - left; FindMinesPos[MineCount].y = row - top; setColor(0xC); putchar(FLAG); if (MineCount == 0) { int i; for (i = 0; i < MapSize.Mine_Count; i++) // 确定是否标记了所有的雷 { if (findMine4Pos(FindMinesPos[i].x, FindMinesPos[i].y) == FALSE) break; } iswin = i == MapSize.Mine_Count; } } setColor(DEFCOLOR); ShowStatu(); //更新标记的雷数 MoveCursorTo(col, row); return iswin; }
case Key_SPACE: //空格点开 x = col - left; y = row - top; ch = GetCursorStr(col, row); if (ch == FLAG) //忽略点开已标记地雷 break; if (minefield[y][x].statu == HIDDEN_MINE) //点开雷,游戏结束 { isGame = GameOver(LOSS_GAME); row = top; col = left; continue; } else //自动点开连续的安全区 { SetCursorType(FALSE); OpenMine(y, x); SetCursorType(TRUE); if (isWin()) { isGame = GameOver(WIN_GAME); row = top; col = left; continue; } } col += 2; if (col >= right) col = right - 1; break;
//自动打开安全区 void OpenMine(int row, int col) { int x = 0; x = col + left; int y = 0; y = row + top; char ch = 0; if (row < 0 || row >= MapSize.MapSize.y) return; if (col < 0 || col >= MapSize.MapSize.x) return; if (minefield[row][col].statu == VISIBLE_SAFE) return; if (minefield[row][col].hasMines == 0) ch = ' '; else if (minefield[row][col].hasMines > 0) ch = minefield[row][col].hasMines + '0'; else return; if (minefield[row][col].hasMines >= 0) { MoveCursorTo(x, y); int n = minefield[row][col].hasMines; switch (n) { case 1: setColor(0x9); //淡蓝色 break; case 2: setColor(0x2); //绿色 break; case 3: setColor(0xC); //淡红色 break; case 4: setColor(0x1); //蓝色 break; case 5: setColor(0x4); //红色 break; case 6: setColor(0xD); //淡紫色 break; case 7: setColor(0x5); //紫色 break; case 8: setColor(0x6); //黄色 break; } putchar(ch); setColor(DEFCOLOR); minefield[row][col].ch = ch; minefield[row][col].statu = VISIBLE_SAFE; if (ch != ' ') return; } if (row > 1) OpenMine(row - 1, col); if (col < MapSize.MapSize.x) OpenMine(row, col + 2); if (row < MapSize.MapSize.y) OpenMine(row + 1, col); if (col >= 2 && col % 2 == 0) OpenMine(row, col - 2); }
//检查标记地雷是否都正确 BOOL isWin() { BOOL iswin = FALSE; int cnt = 0; for (int i = 0; i < MapSize.MapSize.y; i++) { for (int j = 0; j < MapSize.MapSize.x; j++) { if (minefield[i][j].ch == MAPNORMAL) { if (findMine4Pos(j, i) == FALSE) { i = MapSize.MapSize.y + 1; break; } cnt++; } } } iswin = cnt == MapSize.Mine_Count; return iswin; }
//胜负结果变量 enum GAME_RESULT { WIN_GAME, LOSS_GAME }; //游戏结束画面 BOOL GameOver(enum GAME_RESULT statu) { MoveCursorTo(0, bottom + 2); if (statu == WIN_GAME) { // system("color 2F");//改变背景为绿色 showMine(TRUE); setColor(0x02); printf("\t 恭喜,你找出了所有的地雷!\n"); } if (statu == LOSS_GAME) { // system("color 4F"); //改变背景为红色 showMine(FALSE); setColor(0x04); printf("\t 哦吼,踩到雷了!\n"); } setColor(DEFCOLOR); BOOL rst = FALSE; char ch; printf("\n\t 是否重新开始游戏(Y/N): "); fflush(stdin); do { ch = _getch(); if (ch == 'Y' || ch == 'y' || ch == Key_ENTER) { init(); DrawMine(); rst = TRUE; break; } else if (ch == 'N' || ch == 'n' || ch == Key_ESC) { rst = FALSE; break; } } while (TRUE); return rst; }
胜利界面
失败界面
没有总结
完整代码已上传到GitCode,需要的移步 https://gitcode.net/apull/Mines_Console
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。