为了简单起见,我们将棋盘的二维数组定义为全局变量。除此之外还要定义一个char类型的全局变量来识别当前的落子类型,我们将其初始化为‘O’。
char Board_data[3][3] = {
{'-', '-', '-'},
{'-', '-', '-'},
{'-', '-', '-'},
};
char Cur_piece = 'O';
现在回到“读取操作”部分,通过msg的x与y字段来获取鼠标点击的位置,但是现在需要将鼠标点击的位置映射到数组的索引中,一开始将数组初始化为600*600,九等分后每个格子都是200*200,单看水平方向的二维数组索引便是鼠标点击位置除以200的整数部分,竖直方向一样。
接下来便是尝试落子,也即尝试修改对应数组索引位置的值,记住二维数组是先行后列,也即先Y后X确定落子位置,并在落子后切换棋子类型。
然后是完善功能函数,首先是CheckWin函数,先前已经讨论了所有8种情况:
bool CheckWin(char c) {
if (Board_data[0][0] == c && Board_data[0][1] == c && Board_data[0][2] == c) return true;
if (Board_data[1][0] == c && Board_data[1][1] == c && Board_data[1][2] == c) return true;
if (Board_data[2][0] == c && Board_data[2][1] == c && Board_data[2][2] == c) return true;
if (Board_data[0][0] == c && Board_data[1][0] == c && Board_data[2][0] == c) return true;
if (Board_data[0][1] == c && Board_data[1][1] == c && Board_data[2][1] == c) return true;
if (Board_data[0][2] == c && Board_data[1][2] == c && Board_data[2][2] == c) return true;
if (Board_data[0][0] == c && Board_data[1][1] == c && Board_data[2][2] == c) return true;
if (Board_data[0][2] == c && Board_data[1][1] == c && Board_data[2][0] == c) return true;
return false;
}
任何条件被满足都会被短路,然后返回true并不再向下执行。所有条件检测失败后函数一直向下执行到返回false。
CheckDraw函数与CheckWin函数思想相似,遍历整个棋盘如果还有空则返回false,否则返回true:
bool CheckDraw() {
for (size_t i = 0; i < 3; i++) {
for (size_t j = 0; j < 3; j++) {
if (Board_data[i][j] == '-') return false;
}
}
return true;
}
接下来便是绘制棋盘、棋子、提示信息:
棋盘被四条线切割为九份,使用line函数切割:
void DrawBoard() {
line(0, 200, 600, 200);
line(0, 400, 600, 400);
line(200, 0, 200, 600);
line(400, 0, 400, 600);
}
然后是DrawPiece函数,先遍历整个棋盘,然后用switch函数判断所有情况,i对应y,j对应x:
void DrawPiece() {
for (size_t i = 0; i < 3; i++) {
for (size_t j = 0; j < 3; j++) {
switch (Board_data[i][j]) {
case 'O':
circle(200 * j + 100, 200 * i + 100, 100);
break;
case 'X':
line(200 * j, 200 * i, 200 * (j + 1), 200 * (i + 1));
line(200 * (j + 1), 200 * i, 200 * j, 200 * (i + 1));
break;
case '-':
break;
}
}
}
}
最后是绘制提示信息DrawTipText:
void DrawTipText() {
static TCHAR str[64];
_stprintf_s(str, _T("当前棋子类型: %c"), Cur_piece);
settextcolor(RGB(225, 175, 45));
outtextxy(0, 0, str);
}
这里使用了_stprintf_s这个字符串格式化函数,并且定义了TCHAR的字符数组作为格式化的缓冲区,这与printf和sprintf很像,只不过是为了在更通用的编码环境下使用。
话接上文,EasyX可以使用outtextxy函数在窗口的指定坐标处绘制文本字符串,不过在绘制前我们使用了settextcolor函数将文本绘制颜色改为橙黄色使其醒目。settextcolor接受一个COLORREF类型的参数,我们可以通过RGB宏传入对应颜色分量组合出COLORREF类型的值。
至此,我们完成了所有代码。
测试:
最后一个棋子在绘制上去前就判断了胜负,有点影响美感,所以应该在绘制后再判断胜负。所以绘制函数应该在判断胜负之前:
改进后效果显著:
完善:发现井字棋程序占用CPU很大
这是因为计算机在执行while循环时非常快,主循环在顷刻间执行成千上万次,占用了大量的CPU时间片,对于机器是一种性能浪费。所以我们可以使用sleep函数来让程序执行完一次循环后休眠一小段时间从而减少计算资源的浪费。
那么该休眠多久呢?
随着游戏体量的增大,程序每次执行主循环所执行的计算任务可能是不同的,以及涉及到操作系统CPU计算资源的分配,这就导致每次执行主循环所消耗的实际时间可能是不一样的,所以我们要根据每一帧执行的实际耗时动态地计算在这之后要休眠多长的时间,所以引入函数GetTickCount,可以使用它获取程序自运行开始以来到现在的毫秒数:
所以我们在循环开头和结尾各调用一次,然后通过相减得出这次循环实际消耗的毫秒数。
/*#include<graphics.h>
char Board_data[3][3] = {
{'-', '-', '-'},
{'-', '-', '-'},
{'-', '-', '-'},
};
char Cur_piece = 'O';
bool CheckWin(char c) {
if (Board_data[0][0] == c && Board_data[0][1] == c && Board_data[0][2] == c) return true;
if (Board_data[1][0] == c && Board_data[1][1] == c && Board_data[1][2] == c) return true;
if (Board_data[2][0] == c && Board_data[2][1] == c && Board_data[2][2] == c) return true;
if (Board_data[0][0] == c && Board_data[1][0] == c && Board_data[2][0] == c) return true;
if (Board_data[0][1] == c && Board_data[1][1] == c && Board_data[2][1] == c) return true;
if (Board_data[0][2] == c && Board_data[1][2] == c && Board_data[2][2] == c) return true;
if (Board_data[0][0] == c && Board_data[1][1] == c && Board_data[2][2] == c) return true;
if (Board_data[0][2] == c && Board_data[1][1] == c && Board_data[2][0] == c) return true;
return false;
}
bool CheckDraw() {
for (size_t i = 0; i < 3; i++) {
for (size_t j = 0; j < 3; j++) {
if (Board_data[i][j] == '-') return false;
}
}
return true;
}
void DrawBoard() {
line(0, 200, 600, 200);
line(0, 400, 600, 400);
line(200, 0, 200, 600);
line(400, 0, 400, 600);
}
void DrawPiece() {
for (size_t i = 0; i < 3; i++) {
for (size_t j = 0; j < 3; j++) {
switch (Board_data[i][j]) {
case 'O':
circle(200 * j + 100, 200 * i + 100, 100);
break;
case 'X':
line(200 * j, 200 * i, 200 * (j + 1), 200 * (i + 1));
line(200 * (j + 1), 200 * i, 200 * j, 200 * (i + 1));
break;
}
}
}
}
void DrawTipText() {
static TCHAR str[64];
_stprintf_s(str, _T("当前棋子类型: %c"), Cur_piece);
settextcolor(RGB(225, 175, 45));
outtextxy(0, 0, str);
}
int main() {
initgraph(600, 600);
ExMessage msg;
BeginBatchDraw();
bool running = true;*/
while (running) {
DWORD start_time = GetTickCount();
/*while (peekmessage(&msg)) {
if (msg.message == WM_LBUTTONDOWN) {
int x = msg.x;
int y = msg.y;
int index_x = x / 200;
int index_y = y / 200;
if (Board_data[index_y][index_x] == '-') {
Board_data[index_y][index_x] = Cur_piece;
if (Cur_piece == 'O') Cur_piece = 'X';
else Cur_piece = 'O';
}
}
}
cleardevice();
DrawBoard();
DrawPiece();
DrawTipText();
FlushBatchDraw();
if (CheckWin('X')) {
MessageBox(GetHWnd(), _T("X玩家获胜"), _T("游戏结束"), MB_OK);
running = false;
}
else if (CheckWin('O')) {
MessageBox(GetHWnd(), _T("O玩家获胜"), _T("游戏结束"), MB_OK);
running = false;
}
else if (CheckDraw()) {
MessageBox(GetHWnd(), _T("平局!"), _T("游戏结束"), MB_OK);
running = false;
}*/
DWORD end_time = GetTickCount();
DWORD delta_time = end_time - start_time;
/*}
EndBatchDraw();
return 0;
}*/
如果要确保画面以最高60帧的速度刷新,那么每次循环的总时间应该是1000 / 60,如果实际消耗的毫秒数小于1000 / 60,便可以通过运用sleep()延时剩下的时间,如果超过了,就直接进入下一次循环:
可以发现CPU占用率显著下降。
完整代码如下:
#include<graphics.h>
char Board_data[3][3] = {
{'-', '-', '-'},
{'-', '-', '-'},
{'-', '-', '-'},
};
char Cur_piece = 'O';
bool CheckWin(char c) {
if (Board_data[0][0] == c && Board_data[0][1] == c && Board_data[0][2] == c) return true;
if (Board_data[1][0] == c && Board_data[1][1] == c && Board_data[1][2] == c) return true;
if (Board_data[2][0] == c && Board_data[2][1] == c && Board_data[2][2] == c) return true;
if (Board_data[0][0] == c && Board_data[1][0] == c && Board_data[2][0] == c) return true;
if (Board_data[0][1] == c && Board_data[1][1] == c && Board_data[2][1] == c) return true;
if (Board_data[0][2] == c && Board_data[1][2] == c && Board_data[2][2] == c) return true;
if (Board_data[0][0] == c && Board_data[1][1] == c && Board_data[2][2] == c) return true;
if (Board_data[0][2] == c && Board_data[1][1] == c && Board_data[2][0] == c) return true;
return false;
}
bool CheckDraw() {
for (size_t i = 0; i < 3; i++) {
for (size_t j = 0; j < 3; j++) {
if (Board_data[i][j] == '-') return false;
}
}
return true;
}
void DrawBoard() {
line(0, 200, 600, 200);
line(0, 400, 600, 400);
line(200, 0, 200, 600);
line(400, 0, 400, 600);
}
void DrawPiece() {
for (size_t i = 0; i < 3; i++) {
for (size_t j = 0; j < 3; j++) {
switch (Board_data[i][j]) {
case 'O':
circle(200 * j + 100, 200 * i + 100, 100);
break;
case 'X':
line(200 * j, 200 * i, 200 * (j + 1), 200 * (i + 1));
line(200 * (j + 1), 200 * i, 200 * j, 200 * (i + 1));
break;
}
}
}
}
void DrawTipText() {
static TCHAR str[64];
_stprintf_s(str, _T("当前棋子类型: %c"), Cur_piece);
settextcolor(RGB(225, 175, 45));
outtextxy(0, 0, str);
}
int main(){
initgraph(600, 600);
ExMessage msg;
BeginBatchDraw();
bool running = true;
while (running) {
DWORD start_time = GetTickCount();
while (peekmessage(&msg)) {
if (msg.message == WM_LBUTTONDOWN) {
int x = msg.x;
int y = msg.y;
int index_x = x / 200;
int index_y = y / 200;
if (Board_data[index_y][index_x] == '-') {
Board_data[index_y][index_x] = Cur_piece;
if (Cur_piece == 'O') Cur_piece = 'X';
else Cur_piece = 'O';
}
}
}
cleardevice();
DrawBoard();
DrawPiece();
DrawTipText();
FlushBatchDraw();
if (CheckWin('X')) {
MessageBox(GetHWnd(), _T("X玩家获胜"), _T("游戏结束"), MB_OK);
running = false;
}
else if (CheckWin('O')) {
MessageBox(GetHWnd(), _T("O玩家获胜"), _T("游戏结束"), MB_OK);
running = false;
}
else if (CheckDraw()) {
MessageBox(GetHWnd(), _T("平局!"), _T("游戏结束"), MB_OK);
running = false;
}
DWORD end_time = GetTickCount();
DWORD delta_time = end_time - start_time;
if (delta_time < (1000 / 60)) {
Sleep((1000 / 60) - delta_time);
}
}
EndBatchDraw();
return 0;
}