在开始和完成这个项目时,本人对C++的理解极为浅显,对软件工程更是没有概念,所以这个项目实现的比较糟糕,因此本文的价值只在于让C++新手开始入门项目
注意:本项目实现的是无人机的五子棋,也就是双人对战的五子棋,没有实现机器
做项目前的准备:本项目需要用到EasyX库,如果想看着本文章做项目需要去下载安装EasyX库。
下载过程很简单,可自行下载,这里不介绍。关于使用到的关于EasyX库的内容我都会在代码上有详细的注释,无需担心。也可以下载一个EasyX库的api帮助文档,方便查看函数的使用方法
这个是EasyX库的官网:EasyX Graphics Library for C++
1.根据需求设计程序类
不需要照着我的类设计,因为我的类的设计未必很专业,你根据你的想法设计就好了
因为这是一个小项目,所以就粗略的分为两个类了。而且现在的我也不知道该怎么给这个项目分类
Board 类:这个类代表整个五子棋程序的类。它可以存储棋盘的状态,检查玩家的落子是否合法, 更新棋盘状态,并检查是否有玩家获胜。棋盘可以是一个二维数组,每个格子可以代表棋子 的状态(黑子、白子、空)。
Player 类:这个类代表游戏的玩家。它可能包含玩家的姓名,棋子的颜色,编号。
Tip:
为了跟着我做这个项目的新手不会看的迷迷糊糊,我这里先给出所有类的头文件的所有内容,你可以直接就照着我的头文件的内容来实现这个项目。你可以自己尝试按什么顺序去实现这些类
board.h
#ifndef __BOARD_H__ // 保护头文件以防被多次编译
#define __BOARD_H__
#include "player.h"
using type_size = int;
using type_game = char;
class Board
{
public:
Board(type_size row, type_size col, type_size blocksize);
~Board();
void Game(); // 开始游戏
private:
void Init(); // 初始化窗口
void DrawBoard(); // 绘制棋盘
void ProcessInput(); // 处理游戏输入
void showCursor(short x, short y); // 光标提示
void drawCursor(short x, short y, COLORREF color); // 绘制光标
void DropChess(short x, short y); // 落子
void DrawCh(short x, short y, COLORREF color); // 绘制棋子
int checkGameOver(short col, short row, char s); // 检查游戏结束
int getChessNum(short col, short row, char s, int index);// 返回某方向上的棋子个数
void Restart(); // 重新开始游戏
bool checkEffBoard(short x, short y); // 检查坐标是否在棋盘有效范围
bool isNullBoard(short col, short row); // 检查该坐标位置是否为空
type_size rows; // 行数
type_size cols; // 列数
type_size blocksize; // 方块大小
type_game** gameboard; // 棋盘的落子信息
ExMessage* mouse; // 获取鼠标的信息
Player* playerA; // 用户A
Player* playerB; // 用户B
bool chessOrder; // 落子顺序 0-A,1-B 默认A
type_size chessNum; // 已落棋子的数目
};
#endif // !__BOARD_H__
player.h
#ifndef __PLAYER__H
#define __PLAYER__H
#include <graphics.h>
class Player
{
public:
Player(COLORREF color, LPCWSTR name = L"");// L""转为宽字符
COLORREF getColor();
LPCWSTR getName();
char getNo();
private:
COLORREF color; // 棋子颜色
LPCWSTR name; // 名称
char No; // 编号
};
#endif // !__PLAYER__H
main.cpp
#include "board.h"
int main()
{
Board board(14,14,46);
board.Game();
return 0;
}
2.在VS着手实现上述类
2.1 具体实现Board类 在实现过程中根据需求去实现Player类
2.1.1 void Init()实现窗口的初始化
五子棋有一个15行15列的棋盘,我们将它的方块大小设为46
minWidth = 15 * 46 = 690 minHeight = 15 * 46= 690
所以我们把窗口的宽和高设为(1080,960)(没什么特别,只是喜欢这个数字😂)
接下来用得到的函数:
头文件 graphics.h Windows.h conio.h
在easyX库中初始化窗口的函数为 initgraph(); 里面有三个参数,第一个参数是窗口宽度,二是窗口高度,三是绘图窗口的样式 返回一个句柄
SetWindowText(); 设置窗口的标题 两个参数:一是窗口句柄,二是LPCWSTR的字符串
setbkcolor(); 设置当前窗口的背景颜色,它不会直接改变窗口的现有背景颜色;一个颜色参数
cleardevice(); 用背景颜色清空屏幕 无参数
setlinecolor(); 设置设备画线颜色 一个颜色参数
rectangle(); 绘制无填充矩形。四个坐标参数。分别为x起始坐标,y起始坐标,x终点坐标,y终点坐标。一般窗口的左上角为坐标原点,横向向右为X轴,竖向向下为Y轴坐标
line(); 画一条线,四个坐标参数,同上
_getch(); 从控制台读取字符,也可用于简单游戏暂停的手段。无参数
现在你可以试着用以上的函数去绘制五子棋的棋盘。开始动手吧!!
这里先把棋盘绘制出来
board.h
#ifndef __BOARD_H__ // 保护头文件以防被多次编译
#define __BOARD_H__
using type_size = int;
using type_game = char;
class Board
{
public:
Board(type_size row, type_size col, type_size blocksize);
~Board();
void Game(); // 开始游戏
private:
void Init(); // 初始化窗口
void DrawBoard(); // 绘制棋盘
type_size rows; // 行数
type_size cols; // 列数
type_size blocksize; // 方块大小
type_game** gameboard; // 棋盘的落子信息
};
#endif // !__BOARD_H__
board.cpp
#include "board.h"
// 一般情况下
// 如果在类的头文件用不到的外部头文件,都尽量在类的源文件里引用,避免被不需要的文件引用
// 这会是一个好习惯
#include <Windows.h> // 改变窗口标题
#include <graphics.h> // 创建窗口需要的头文件
#include <conio.h> // 控制台输入输出
#include <algorithm> // 算法库
const COLORREF VIEWBACKCOLOR = RGB(255, 213, 109); // 背景颜色
const type_size WINDOWWIDTH = 1080; // 窗口的宽
const type_size WINDOWHEIGHT = 960; // 窗口的高
type_size STATX; // 棋盘在窗口开始绘制的起点 x 坐标
type_size STATY; // 棋盘在窗口开始绘制的起点 y 坐标
const type_size & StatX = STATX; // STATX还需初始化计算,通过 const type & 来访问将无法改变其值
const type_size & StatY = STATY; // 同上
Board::Board(type_size row, type_size col, type_size blocksize) :
rows(row+1),// 加1 是因为14个格子,15个位置落子
cols(col+1),// 所以总共是15列15行 225个位置落子
blocksize(blocksize)
{
STATX = (WINDOWWIDTH - col * blocksize) / 2; // 初始化STATX & Y
STATY = (WINDOWHEIGHT - row * blocksize) / 2; // 目的是把棋盘放在窗口正中间
gameboard = new type_game * [row+1]; // 申请动态内存
for (int i = 0; i <= row; i++) {
gameboard[i] = new type_game[col+1];
std::fill(gameboard[i], gameboard[i] + col+1, 0); // 将数组的每个元素初始化为 0
}
}
Board::~Board()
{
for (int i = 0; i < rows; i++) {
delete[] gameboard[i];
}
delete[] gameboard;
}
void Board::Game()
{
Init();
}
void Board::Init()
{
HWND hwnd = initgraph(WINDOWWIDTH, WINDOWHEIGHT); // 初始化绘图窗口
//HWND hwnd = GetHWnd(); // 获取当前活动窗口的句柄
SetWindowText(hwnd, L"五子棋"); // 设置窗口标题 L表示转换宽字符
// 绘制棋盘
DrawBoard();
_getch();// 从控制台读取字符,也可用于简单游戏暂停的手段 这里是暂停一下,以免程序直接运行至结束
}
void Board::DrawBoard()
{
setbkcolor(VIEWBACKCOLOR); // 设置当前窗口背景颜色
cleardevice(); // 使用当前背景颜色清除绘图设备
setlinecolor(BLACK); // 设置设备画线颜色
// 以行列矩形为单位绘制棋盘
// 每隔一列绘制一个 从 top 到 bottom 的矩形,绘制八个即可
// col 为x起始坐标, row 为y起始坐标,col+blocksize 为x终点坐标 ,endRow 为y终点坐标
for (int col = StatX, row = StatY, endRow = WINDOWHEIGHT-StatY; col < WINDOWWIDTH - StatX; col= col + 2*blocksize) {
// 绘制无填充矩形四个参数为 x起始坐标,y起始坐标,x终点坐标,y终点坐标
rectangle(col, row, col + blocksize, endRow);
}
line(StatX, WINDOWHEIGHT - StatY, WINDOWWIDTH - StatX, WINDOWHEIGHT - StatY);
// 每隔一行绘制一个 从 left 到 right 的矩形,绘制八个即可
for (int col = StatX, row = StatY, endCol = WINDOWWIDTH - StatX; row < WINDOWHEIGHT - StatY; row = row + 2 * blocksize) {
// 绘制无填充矩形四个参数为 x起始坐标,y起始坐标,x终点坐标,y终点坐标
rectangle(col, row, endCol, row+blocksize);
}
line(WINDOWWIDTH - StatX, StatY, WINDOWWIDTH - StatX, WINDOWHEIGHT - StatY);
// 以方格为单位绘制棋盘
//auto y = StatY;
//for (int i = 0; i < rows; i++) { // 行
// auto x = StatX;
// for (int j = 0; j < cols; j++) { // 列
// // 绘制无填充矩形四个参数为 x起始坐标,y起始坐标,x终点坐标,y终点坐标
// rectangle(x, y, x + blocksize, y + blocksize);
// x += blocksize; // 让x按列数往右推进
// }
//}
}
下面是需要更新的文件
// main.cpp
#include "board.h"
int main()
{
Board board(14,14,46);
board.Game();
return 0;
}
进行测试
2.1.2 鼠标的定位和光标提示用户
说到鼠标的定位需要在EasyX库的帮助文档中,搜ExMessage,然后了解一下这个类型,下面是关于ExMessage的一个案例(如果你看不懂我需要实现什么功能,可以先看下面的测试结果)
ExMessage msg;
while (true)
{
// 获取消息
msg = getmessage(EX_MOUSE | EX_KEY);
// 判断消息类型
switch (msg.message)
{
case WM_MOUSEMOVE:
{
// 鼠标移动事件
int x = msg.x; // 获取鼠标的 X 坐标
int y = msg.y; // 获取鼠标的 Y 坐标
// 在控制台输出鼠标位置
printf("Mouse Position: (%d, %d)\n", x, y);
break;
}
case WM_LBUTTONDOWN:
{
// 左键点击事件
break; // 当左键点击时,退出循环
}
}
}
照着上面的样例,我们在Board类中增加私有成员函数ProcessInput()、showCursor()、drawCursor()
// 需要添加的新成员变量
private:
// 为什么使用指针是因为好像在哪见过说 类成员变量建议用指针,所有我一般都会用指针
ExMessage* mouse; // 获取鼠标的信息
// 更新构造函数
mouse(new ExMessage)
// 更新虚构函数
delete mouse;
然后进行绘制光标之前,我们还需要计算出需要绘制光标的范围,也就是算出棋盘的有效范围。棋盘有效范围我是在原有的边长再加了一个blocksize
// 在 board.cpp文件的全局区域声明的
type_size SX_CUR; // 棋盘的有效范围 left right top bottom
type_size EX_CUR;
type_size SY_CUR;
type_size EY_CUR;
// 在构造函数里面进行计算
SX_CUR = StatX - blocksize / 2; // 初始化棋盘的有效范围
EX_CUR = StatX + col * blocksize + blocksize / 2;
SY_CUR = StatY - blocksize / 2;
EY_CUR = StatY + row * blocksize + blocksize / 2;
// 在类里面添加成员函数
void ProcessInput(); // 处理游戏输入
void showCursor(short x, short y);// 光标提示
void drawCursor(short x, short y, COLORREF color);// 绘制光标
bool checkEffBoard(short x, short y);// 检查坐标是否在棋盘有效范围
void Board::ProcessInput()
{
while (mouse->vkcode != VK_ESCAPE) // 当按下ESC按键时,退出循环。然后退出游戏
{
*mouse = getmessage(EX_MOUSE | EX_KEY); // 获取鼠标的信息数据 或 按键的信息
switch (mouse->message)
{
case WM_MOUSEMOVE: // 鼠标移动 显示光标
showCursor(mouse->x,mouse->y); // 传入坐标
break;
case WM_LBUTTONDOWN: // 鼠标左键按下 落子
break;
case WM_KEYDOWN: // 按键信息
break;
default:
break;
}
}
}
void Board::showCursor(short x, short y)
{
// Lx,Ly 用来记录光标的上一个位置
static short Lx = -2, Ly = -2;
if (checkEffBoard(x,y)) {
x -= SX_CUR;
y -= SY_CUR;
auto col = x / blocksize; // 算出是第几列
auto row = y / blocksize; // 算出是第几行
x = blocksize * col + SX_CUR; // 算出光标所在方格的左上角坐标位置
y = blocksize * row + SY_CUR;
// 和上一步的位置不一样才能才能绘制这一步的光标
if (Lx != x || Ly != y) {
// 在EasyX库中 消除上一步的绘制图像的方法主要是绘制新的图像覆盖上一步绘制的图像
drawCursor(Lx, Ly, VIEWBACKCOLOR); // 清除上一个光标
Lx = x; // 保存这一步的位置信息,在下一步中比对
Ly = y;
drawCursor(Lx, Ly, RGB(255, 0, 0)); // 绘制光标
}
}
else {
drawCursor(Lx, Ly, VIEWBACKCOLOR); // 不在棋盘内时,清除光标
// 重置 Lx,Ly
Lx = -2;
Ly = -2;
}
}
void Board::drawCursor(short x, short y, COLORREF color)
{
/*画八条直线 如下 7,6,7是长度
7 6 7
线线线 线线线
线 线
线 线
线 线
线 线
线线线 线线线
*/
setfillcolor(color); // 设置填充颜色
// 下面未注释的函数都是无边框填充矩形 不用线条的原因是线条太细了,看起来不够醒目
// 横向的矩形都是长为11,宽为2; 竖向的矩形是长为9,宽为2 这样出来的光标的角没有缺口
// x 和 y 都是处理过的方格左上角的坐标,可直接使用 如果你的坐标是照着我的方式处理的可直接使用下面的代码
// 上边 top
solidrectangle(x + 3, y + 3, x + 14, y + 5);
solidrectangle(x + 32, y + 3, x + 43, y + 5);
/*line(x + 5, y + 5, x + 14, y + 5);
line(x + 32, y + 5, x + 41, y + 5);*/
// 左边 left
solidrectangle(x + 3, y + 5, x + 5, y + 14);
solidrectangle(x + 3, y + 32, x + 5, y + 41);
/*line(x + 5, y + 5, x + 5, y + 14);
line(x + 5, y + 32, x + 5, y + 41);*/
// 底边 bottom
solidrectangle(x + 3, y + 41, x + 14, y + 43);
solidrectangle(x + 32, y + 41, x + 43, y + 43);
/*line(x + 5, y + 41, x + 14, y + 41);
line(x + 32, y + 41, x + 41, y + 41);*/
// 右边 right
solidrectangle(x + 41, y + 5, x + 43, y + 14);
solidrectangle(x + 41, y + 32, x + 43, y + 41);
/*line(x + 41, y + 5, x + 41, y + 14);
line(x + 41, y + 32, x + 41, y + 41);*/
}
bool Board::checkEffBoard(short x, short y)
{
return x > SX_CUR && x<EX_CUR && y>SY_CUR && y < EY_CUR;// 这里你可以试着全改成 >= 或 <= 看看会怎样
}
把ProcessInput();这个函数加进Game();函数里面。然后进行测试。不过要记得把Init()函数最后的_getch();语句注释掉或删掉。因为它会等待用户的输入而暂停程序,而且它本来就是为了测试代码才写的。
测试结果:
2.1.3 落子——轮番落子
在这里需要完成Player类的实现
Player类需要有三个成员变量——棋子颜色,玩家名称,玩家编号,因此也将有三个普通成员函数返回成员变量的值。实现如下
player.h
#ifndef __PLAYER__H
#define __PLAYER__H
#include <graphics.h>
class Player
{
public:
Player(COLORREF color, LPCWSTR name = L"");// L""转为宽字符
COLORREF getColor();
LPCWSTR getName();
char getNo();
private:
COLORREF color; // 棋子颜色
LPCWSTR name; // 名称
char No; // 编号
};
#endif // !__PLAYER__H
player.cpp
#include "player.h"
static char SNO = 1; // 不重复编号的源头
Player::Player(COLORREF color, LPCWSTR name) :
color(color),
name(name),
No(SNO++) // 将源编号后置递增
{
}
COLORREF Player::getColor()
{
return color;
}
LPCWSTR Player::getName()
{
return name;
}
char Player::getNo()
{
return No;
}
在Board中需要增加以下三个成员变量以及三个成员函数,在类头文件中需要引入Player类的头文件
private:
Player* playerA; // 用户A
Player* playerB; // 用户B
bool chessOrder; // 落子顺序 0-A,1-B 默认A
// 成员函数
private:
void DropChess(short x, short y);// 落子
void DrawCh(short x, short y, COLORREF color); // 绘制棋子
bool isNullBoard(short col, short row);// 检查该坐标位置是否有棋子
函数的具体实现
void Board::DropChess(short x, short y)
{
if (checkEffBoard(x, y)) {
x -= SX_CUR;
y -= SY_CUR;
auto col = x / blocksize; // 算出是第几列
auto row = y / blocksize; // 算出是第几行
if(isNullBoard(col,row)) // 检查位置是否有棋子
{
Player* p;
if (chessOrder) { // 检查该回合是哪个玩家的回合
p = playerB;
}
else {
p = playerA;
}
chessOrder = !chessOrder; // 修改顺序值
gameboard[row][col] = p->getNo(); // 在棋盘表上
DrawCh(blocksize * col + SX_CUR, blocksize * row + SY_CUR, p->getColor());//绘制棋子
}
}
}
void Board::DrawCh(short x, short y, COLORREF color)
{
setfillcolor(color); // 设置填充颜色
// 绘制无边框填充圆
solidcircle(x + blocksize / 2, y + blocksize / 2, 19);//圆心在方块中心
}
bool Board::isNullBoard(short col, short row)
{
return !gameboard[row][col]; // 为空时返回true
测试结果:
2.1.4 检查游戏输赢
游戏的状态有三种:对弈中,一方输另一方赢,双方平局。
在检测第二种游戏状态时,我们需要检查落子的四个方向上的棋子情况:水平、垂直、左上至右下、左下至右上。
虽然这样看是四种方向,但是以落子为原点则实际需要检查八个方向的棋子情况。所以我才用的是4*2 的双层循环来检查。接着我用一个数组来保存八个方向上的增量,来掌握 检查棋子情况的函数的检查方向。数组如下:
// 这个数组我放在类源文件的顶部全局声明
const int directions[8][2] = {
// 垂直方向 水平方向 左下至右上 左上至右下
{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {1, 1}, {-1, 1}, {1, -1}
}; // 检查游戏是否结束的方向增量
游戏平局的判断则是检查落子的数量是否已经达到最大且未有一方胜出的情况
话不多说,上代码:
board.h
// 这是新增成员函数
int checkGameOver(short col, short row, char s); // 检查游戏结束
int getChessNum(short col, short row, char s, int index);// 返回某方向上的棋子个数
void Restart(); // 重新开始游戏
// 这是新增成员变量
type_size chessNum; // 已落棋子的数目
board.cpp
// 新增的全局变量,实际上这些所有的全局变量都可以定义为类的成员变量
type_size CHESSNUMSUM; // 棋盘交点的总数
const type_size& ChessNumSum = CHESSNUMSUM;
enum GAMEOVER {NOTOVER,YESOVER,SCOREDRAW}; // 检测游戏状态的枚举变量
const int directions[8][2] = {
{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {1, 1}, {-1, 1}, {1, -1}
}; // 检查游戏是否结束的方向增量
// 新增的函数实现
int Board::checkGameOver(short col, short row, char s)
{
// 检查四个方向 水平、垂直、左上至右下、左下至右上
for (int i = 0; i < 4; i++) {
int count = 1; // col,row 坐标的棋子算一个
for (int j = 0; j < 2; j++) {
// 返回该方向上的相同棋子数目
count += getChessNum(col, row, s, i * 2 + j);// i*2+j 可以按顺序遍历各个方向的增量的下标
}
if (count == 5) {
return YESOVER; // 游戏结束
}
}
if (chessNum == ChessNumSum) {
return SCOREDRAW; // 游戏平局
}
return NOTOVER; // 游戏继续
}
int Board::getChessNum(short col, short row, char s, int index)
{
int dr = directions[index][0]; // 行方向的增量
int dc = directions[index][1]; // 列方向的增量
int count = -1; // 循环是do-while,所以在开始前是-1
// 沿着当前方向延伸,检查是否存在连续的五个相同棋子
do{
row += dr; // 在方向上增量,然后比较
col += dc;
count++;
} while (row >= 0 && row < 15 && col >= 0 && col < 15 && gameboard[row][col] == s);
return count;
}
void Board::Restart()
{
DrawBoard(); // 绘制新棋盘覆盖旧棋盘
// 重置变量
chessOrder = 0; // 默认为A先
chessNum = 0; // 已下棋子数量重置
for (int i = 0; i < rows; i++) { // 重置棋盘
std::fill(gameboard[i], gameboard[i] + cols, 0);
}
}
检查游戏的状态的时机应该是在每一次落子之后,所以要在DropChess();函数里面新增内容:
if (int a =checkGameOver(col, row, p->getNo())) {// 检查游戏状态
setbkcolor(VIEWBACKCOLOR); // 设置文本框的填充颜色
settextcolor(RGB(225,0,0)); // 设置文字颜色
LPCTSTR text;
if (a == SCOREDRAW) {
text = L"双方平局!!!";
}
else if (!chessOrder) { // 顺序值已经修改了 所以需要取反值
text = L"白子获胜!!!";
}
else{
text = L"黑子获胜!!!";
}
settextstyle(27, 15, _T("宋体")); // 设置文字样式
outtextxy(WINDOWWIDTH / 5 * 2, SY_CUR / 3, text);// 输出文本
int rubbish = _getch(); // 等待用户输入
Restart(); // 重置游戏
} // !检查游戏是否结束
把上面的这段代码加在DropChess();函数里面就行了,至于什么位置我已经说过,自己添加就行了
3.总结
3.1 这个项目的收获
这个项目主要时是对鼠标信息的一个处理方式的练习。
3.2 为什么要把一些坐标数据声明为普通变量或常量而不直接使用坐标数据
因为这些数据的关联性极强,修改一个数据可能就需要改变所有的数据。所以我把这些坐标类的数据声明成变量,再用一些固定的算式计算出来,这样一改变某个数据时,其它数据由于算式的关系,会自动变成合适的相对应的值。 如果不声明成变量或常量,而是直接用数字填进去,一旦发生改变时,就需要手动改变多个值。亦或者某个值用的多时,也需要一一去手改值。比如绘制光标图形的那些坐标,我就手动的改了多次的值,可谓是牵一发而动全身。 所以建议一些重要的数据都去声明成变量,哪怕是固定不变的值也应该尽量声明成常量。
4. 项目的源文件汇总
然后这个项目就完成了,最后这里给出所有的源代码和源文件:
这个是百度网盘的链接:
链接:https://pan.baidu.com/s/17bxWvTzjPapBoYA0DX9y-Q?pwd=wzqy
提取码:wzqy
main.cpp
#include "board.h"
int main()
{
Board board(14,14,46);
board.Game();
return 0;
}
player.h
#ifndef __BOARD_H__ // 保护头文件以防被多次编译
#define __BOARD_H__
#include "player.h"
using type_size = int;
using type_game = char;
class Board
{
public:
Board(type_size row, type_size col, type_size blocksize);
~Board();
void Game(); // 开始游戏
private:
void Init(); // 初始化窗口
void DrawBoard(); // 绘制棋盘
void ProcessInput(); // 处理游戏输入
void showCursor(short x, short y); // 光标提示
void drawCursor(short x, short y, COLORREF color); // 绘制光标
void DropChess(short x, short y); // 落子
void DrawCh(short x, short y, COLORREF color); // 绘制棋子
int checkGameOver(short col, short row, char s); // 检查游戏结束
int getChessNum(short col, short row, char s, int index);// 返回某方向上的棋子个数
void Restart(); // 重新开始游戏
bool checkEffBoard(short x, short y); // 检查坐标是否在棋盘有效范围
bool isNullBoard(short col, short row); // 检查该坐标位置是否为空
type_size rows; // 行数
type_size cols; // 列数
type_size blocksize; // 方块大小
type_game** gameboard; // 棋盘的落子信息
ExMessage* mouse; // 获取鼠标的信息
Player* playerA; // 用户A
Player* playerB; // 用户B
bool chessOrder; // 落子顺序 0-A,1-B 默认A
type_size chessNum; // 已落棋子的数目
};
#endif // !__BOARD_H__
player.cpp
#include "player.h"
static char SNO = 1; // 不重复编号的源头
Player::Player(COLORREF color, LPCWSTR name) :
color(color),
name(name),
No(SNO++) // 将源编号后置递增
{
}
COLORREF Player::getColor()
{
return color;
}
LPCWSTR Player::getName()
{
return name;
}
char Player::getNo()
{
return No;
}
board.h
#ifndef __BOARD_H__ // 保护头文件以防被多次编译
#define __BOARD_H__
#include "player.h"
using type_size = int;
using type_game = char;
class Board
{
public:
Board(type_size row, type_size col, type_size blocksize);
~Board();
void Game(); // 开始游戏
private:
void Init(); // 初始化窗口
void DrawBoard(); // 绘制棋盘
void ProcessInput(); // 处理游戏输入
void showCursor(short x, short y); // 光标提示
void drawCursor(short x, short y, COLORREF color); // 绘制光标
void DropChess(short x, short y); // 落子
void DrawCh(short x, short y, COLORREF color); // 绘制棋子
int checkGameOver(short col, short row, char s); // 检查游戏结束
int getChessNum(short col, short row, char s, int index);// 返回某方向上的棋子个数
void Restart(); // 重新开始游戏
bool checkEffBoard(short x, short y); // 检查坐标是否在棋盘有效范围
bool isNullBoard(short col, short row); // 检查该坐标位置是否为空
type_size rows; // 行数
type_size cols; // 列数
type_size blocksize; // 方块大小
type_game** gameboard; // 棋盘的落子信息
ExMessage* mouse; // 获取鼠标的信息
Player* playerA; // 用户A
Player* playerB; // 用户B
bool chessOrder; // 落子顺序 0-A,1-B 默认A
type_size chessNum; // 已落棋子的数目
};
#endif // !__BOARD_H__
board.cpp
#include "board.h"
#include <Windows.h> // 改变窗口标题
#include <graphics.h> // 创建窗口需要的头文件
#include <conio.h> // 控制台输入输出
#include <algorithm> // 算法库
const COLORREF VIEWBACKCOLOR = RGB(255, 213, 109); // 背景颜色
//const COLORREF VIEWBACKCOLOR = WHITE;
const type_size WINDOWWIDTH = 1080; // 窗口的宽
const type_size WINDOWHEIGHT = 960; // 窗口的高
type_size STATX; // 棋盘在窗口开始绘制的起点 x 坐标
type_size STATY; // 棋盘在窗口开始绘制的起点 y 坐标
const type_size & StatX = STATX; // STATX还需初始化计算,通过 const type & 来访问将无法改变其值
const type_size & StatY = STATY; // 同上
type_size CHESSNUMSUM; // 棋盘交点的总数
const type_size& ChessNumSum = CHESSNUMSUM;
type_size SX_CUR; // 棋盘的有效范围 left right top bottom
type_size EX_CUR;
type_size SY_CUR;
type_size EY_CUR;
enum GAMEOVER {NOTOVER,YESOVER,SCOREDRAW}; // 检测游戏状态的枚举变量
const int directions[8][2] = {
{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {1, 1}, {-1, 1}, {1, -1}
}; // 检查游戏是否结束的方向增量
Board::Board(type_size row, type_size col, type_size blocksize) :
rows(row+1),
cols(col+1),
blocksize(blocksize),
mouse(new ExMessage),
playerA(new Player(BLACK)),
playerB(new Player(WHITE)),
chessOrder(0),
chessNum(0)
{
STATX = (WINDOWWIDTH - col * blocksize) / 2; // 初始化STATX & Y
STATY = (WINDOWHEIGHT - row * blocksize) / 2;
CHESSNUMSUM = (col + 1) * (row + 1); // 初始棋盘的交点总数
SX_CUR = StatX - blocksize / 2; // 初始化棋盘的有效范围
EX_CUR = StatX + col * blocksize + blocksize / 2;
SY_CUR = StatY - blocksize / 2;
EY_CUR = StatY + row * blocksize + blocksize / 2;
gameboard = new type_game * [row+1]; // 申请动态内存
for (int i = 0; i <= row; i++) {
gameboard[i] = new type_game[col+1];
std::fill(gameboard[i], gameboard[i] + col + 1, 0); // 将数组的每个元素初始化为 0
}
}
Board::~Board()
{
for (int i = 0; i < rows; i++) {
delete[] gameboard[i];
}
delete[] gameboard;
delete mouse;
delete playerA;
delete playerB;
}
void Board::Game()
{
Init();
ProcessInput();
}
void Board::Init()
{
HWND hwnd = initgraph(WINDOWWIDTH, WINDOWHEIGHT); // 初始化绘图窗口
//GetHWnd(); // 获取当前活动窗口的句柄
SetWindowText(hwnd, L"五子棋"); // 设置窗口标题 L表示转换宽字符
// 绘制棋盘
DrawBoard();
//_getch(); // 从控制台读取字符,也可用于简单游戏暂停的手段 这里是暂停一下,以免程序直接运行至结束
}
void Board::DrawBoard()
{
setbkcolor(VIEWBACKCOLOR); // 设置当前窗口背景颜色
cleardevice(); // 使用当前背景颜色清除绘图设备
setlinecolor(BLACK); // 设置设备画线颜色
// 以行列矩形为单位绘制棋盘
// 每隔一列绘制一个 从 top 到 bottom 的矩形,绘制八个即可
// col 为x起始坐标, row 为y起始坐标,col+blocksize 为x终点坐标 ,endRow 为y终点坐标
for (int col = StatX, row = StatY, endRow = WINDOWHEIGHT-StatY; col < WINDOWWIDTH - StatX; col= col + 2*blocksize) {
// 绘制无填充矩形四个参数为 x起始坐标,y起始坐标,x终点坐标,y终点坐标
rectangle(col, row, col + blocksize, endRow);
}
line(StatX, WINDOWHEIGHT - StatY, WINDOWWIDTH - StatX, WINDOWHEIGHT - StatY);
// 每隔一行绘制一个 从 left 到 right 的矩形,绘制八个即可
for (int col = StatX, row = StatY, endCol = WINDOWWIDTH - StatX; row < WINDOWHEIGHT - StatY; row = row + 2 * blocksize) {
// 绘制无填充矩形四个参数为 x起始坐标,y起始坐标,x终点坐标,y终点坐标
rectangle(col, row, endCol, row+blocksize);
}
line(WINDOWWIDTH - StatX, StatY, WINDOWWIDTH - StatX, WINDOWHEIGHT - StatY);
// 以方格为单位绘制棋盘
//auto y = StatY;
//for (int i = 0; i < rows; i++) { // 行
// auto x = StatX;
// for (int j = 0; j < cols; j++) { // 列
// // 绘制无填充矩形四个参数为 x起始坐标,y起始坐标,x终点坐标,y终点坐标
// rectangle(x, y, x + blocksize, y + blocksize);
// x += blocksize; // 让x按列数往右推进
// }
//}
}
void Board::ProcessInput()
{
while (mouse->vkcode != VK_ESCAPE)
{
*mouse = getmessage(EX_MOUSE | EX_KEY); // 获取鼠标的信息数据 或 按键的信息
switch (mouse->message)
{
case WM_MOUSEMOVE: // 鼠标移动 显示光标
showCursor(mouse->x,mouse->y);
break;
case WM_LBUTTONDOWN: // 鼠标左键按下 落子
DropChess(mouse->x, mouse->y);
break;
case WM_KEYDOWN:
{
if (mouse->vkcode == 'R') { // 按下R键重新开始游戏
Restart();
}
break;
}
default:
break;
}
}
}
void Board::showCursor(short x, short y)
{
// Lx,Ly 用来记录光标的上一个位置
static short Lx = -2, Ly = -2;
if (checkEffBoard(x,y)) {
x -= SX_CUR;
y -= SY_CUR;
auto col = x / blocksize; // 算出是第几列
auto row = y / blocksize; // 算出是第几行
x = blocksize * col + SX_CUR; // 算出光标所在方格的左上角坐标位置
y = blocksize * row + SY_CUR;
// 和上一步的位置不一样才能才能绘制这一步的光标
if (Lx != x || Ly != y) {
// 在EasyX库中 消除上一步的绘制图像的方法主要是绘制新的图像覆盖上一步绘制的图像
drawCursor(Lx, Ly, VIEWBACKCOLOR); // 清除上一个光标
Lx = x; // 保存这一步的位置信息,在下一步中比对
Ly = y;
drawCursor(Lx, Ly, RGB(255, 0, 0)); // 绘制光标
}
}
else {
drawCursor(Lx, Ly, VIEWBACKCOLOR); // 不在棋盘内时,清除光标
// 重置 Lx,Ly
Lx = -2;
Ly = -2;
}
}
void Board::drawCursor(short x, short y, COLORREF color)
{
/*画八条直线 如下 7,6,7是长度
7 6 7
线线线 线线线
线 线
线 线
线 线
线 线
线线线 线线线
*/
setfillcolor(color); // 设置填充颜色
// 下面未注释的函数都是无边框填充矩形 不用线条的原因是线条太细了,看起来不够醒目
// 横向的矩形都是长为11,宽为2; 竖向的矩形是长为9,宽为2 这样出来的光标的角没有缺口
// x 和 y 都是处理过的方格左上角的坐标,可直接使用 如果你的坐标是照着我的方式处理的可直接使用下面的代码
// 上边 top
solidrectangle(x + 3, y + 3, x + 14, y + 5);
solidrectangle(x + 32, y + 3, x + 43, y + 5);
/*line(x + 5, y + 5, x + 14, y + 5);
line(x + 32, y + 5, x + 41, y + 5);*/
// 左边 left
solidrectangle(x + 3, y + 5, x + 5, y + 14);
solidrectangle(x + 3, y + 32, x + 5, y + 41);
/*line(x + 5, y + 5, x + 5, y + 14);
line(x + 5, y + 32, x + 5, y + 41);*/
// 底边 bottom
solidrectangle(x + 3, y + 41, x + 14, y + 43);
solidrectangle(x + 32, y + 41, x + 43, y + 43);
/*line(x + 5, y + 41, x + 14, y + 41);
line(x + 32, y + 41, x + 41, y + 41);*/
// 右边 right
solidrectangle(x + 41, y + 5, x + 43, y + 14);
solidrectangle(x + 41, y + 32, x + 43, y + 41);
/*line(x + 41, y + 5, x + 41, y + 14);
line(x + 41, y + 32, x + 41, y + 41);*/
}
void Board::DropChess(short x, short y)
{
if (checkEffBoard(x, y)) { // 检查是否在棋盘有效范围内
x -= SX_CUR;
y -= SY_CUR;
auto col = x / blocksize; // 算出是第几列
auto row = y / blocksize; // 算出是第几行
if(isNullBoard(col,row)) // 检查位置是否有棋子
{
Player* p;
if (chessOrder) { // 检查该回合是哪个玩家的回合
p = playerB;
}
else {
p = playerA;
}
chessOrder = !chessOrder; // 修改顺序值
gameboard[row][col] = p->getNo(); // 在棋盘表上做标记
DrawCh(blocksize * col + SX_CUR, blocksize * row + SY_CUR, p->getColor());//绘制棋子
if (int a =checkGameOver(col, row, p->getNo())) {// 检查游戏状态
setbkcolor(VIEWBACKCOLOR); // 设置文本框的填充颜色
settextcolor(RGB(225,0,0)); // 设置文字颜色
LPCTSTR text;
if (a == SCOREDRAW) {
text = L"双方平局!!!";
}
else if (!chessOrder) { // 顺序值已经修改了 所以需要取反值
text = L"白子获胜!!!";
}
else{
text = L"黑子获胜!!!";
}
settextstyle(27, 15, _T("宋体")); // 设置文字样式
outtextxy(WINDOWWIDTH / 5 * 2, SY_CUR / 3, text);// 输出文本
int rubbish = _getch(); // 等待用户输入
Restart();
} // !检查游戏是否结束
}// !检查位置是否有棋子
}
}
void Board::DrawCh(short x, short y, COLORREF color)
{
setfillcolor(color); // 设置填充颜色
// 绘制无边框填充圆
solidcircle(x + blocksize / 2, y + blocksize / 2, 19);//圆心在方块中心
chessNum++;
}
int Board::checkGameOver(short col, short row, char s)
{
// 检查四个方向 水平、垂直、左上至右下、左下至右上
for (int i = 0; i < 4; i++) {
int count = 1; // col,row 坐标的棋子算一个
for (int j = 0; j < 2; j++) {
// 返回该方向上的相同棋子数目
count += getChessNum(col, row, s, i * 2 + j);// i*2+j 可以按顺序遍历各个方向的增量的下标
}
if (count == 5) {
return YESOVER; // 游戏结束
}
}
if (chessNum == ChessNumSum) {
return SCOREDRAW; // 游戏平局
}
return NOTOVER; // 游戏继续
}
int Board::getChessNum(short col, short row, char s, int index)
{
int dr = directions[index][0]; // 行方向的增量
int dc = directions[index][1]; // 列方向的增量
int count = -1; // 循环是do-while,所以在开始前是-1
// 沿着当前方向延伸,检查是否存在连续的五个相同棋子
do{
row += dr; // 在方向上增量,然后比较
col += dc;
count++;
} while (row >= 0 && row < 15 && col >= 0 && col < 15 && gameboard[row][col] == s);
return count;
}
void Board::Restart()
{
DrawBoard(); // 绘制新棋盘覆盖旧棋盘
// 重置变量
chessOrder = 0; // 默认为A先
chessNum = 0; // 已下棋子数量重置
for (int i = 0; i < rows; i++) { // 重置棋盘
std::fill(gameboard[i], gameboard[i] + cols, 0);
}
}
bool Board::checkEffBoard(short x, short y)
{
//short x = Lx, y = Ly;
return x > SX_CUR && x<EX_CUR && y>SY_CUR && y < EY_CUR;// 这里你可以试着全改成 >= 或 <= 看看会怎样
}
bool Board::isNullBoard(short col, short row)
{
return !gameboard[row][col]; // 为空时返回true
}