前言:本篇博客针对的是C语言初学者,如电脑的AI化部分只是一些相对简单的改进(进攻,防守,占据有利地形),不涉及智能算法(如博弈树)。其中有部分代码是我仔细琢磨后反复改进写出的代码,包含了我对编程的理解,如有不足,请多多指教。还有,其实也可以用枚举法来实现完美走棋(因为三子棋走法的可能性非常少),类似棋类软件里的开局库,实现方法也非常简单,有兴趣的朋友可以试试。
首先我们定义一些会大量用到的符号,这么做的好处是改动起来会比较方便。这里需要强调的是,我们写代码时一定要保证代码的可复用性。比方说,假如我们把row和col这两个符号都改成5的话,相应的代码不需要做出太多的改动。
#define ROW 3 //行
#define COL 3 //列
定义一个数组来存储下棋的数据。
char board[ROW][COL] = { 0 };
由于刚开始下棋时数组里应该是全空格,我们需要一个函数来初始化这个数组。
init_board(board, ROW, COL);
接着实现这个函数。
void init_board(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
board[i][j] = ' ';
}
}
}
我们还需要一个函数来打印棋盘。
display_board(board, ROW, COL);
接着来实现这个函数。这个函数的实现还是有点复杂的。棋盘的打印分为横向分割线,纵向分割线和数据。横向分割线用外层循环控制,纵向分割线和数据用内层循环来控制。需要注意的是,并不是每一次循环都要打印分割线,不管是横向还是纵向,最后一次循环是不需要打印的,这一点可以用if语句来控制。
void display_board(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
//打印数据
for (j = 0; j < row; j++)
{
printf(" %c ", board[i][j]);
if (j < row - 1)
{
printf("|");
}
}
printf("\n");
//打印分割行
if (i < row - 1)
{
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
{
printf("|");
}
}
}
printf("\n");
}
}
接下来就是玩家下棋,判断局面,电脑下棋,判断局面······这就需要一个循环。玩家先下棋,棋子是O,电脑接着下,棋子是X。(为什么用O和X?三子棋的英文是naughts and crosses,分别代表了“圈”和“叉”)玩家和电脑下棋的函数实现最后再说,先说说判断局面的函数。玩家赢则返回“O”,电脑赢返回“X”,平局返回“T”(tie),如果都不是则返回“C”(continue)。所以整体的框架就很简单了。
char ret = 0;
while (1)
{
//玩家下棋
player_move(board, ROW, COL);
display_board(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
//电脑下棋
computer_move(board, ROW, COL);
display_board(board, ROW, COL);
is_win(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
if (ret == 'O')
{
printf("玩家赢\n");
}
else if (ret == 'X')
{
printf("电脑赢\n");
}
else
{
printf("平局\n");
}
display_board(board, ROW, COL);
先贴下game函数的完整代码再继续
void game()
{
//存储下棋的数据
char board[ROW][COL] = { 0 };
//初始化棋盘为全空格
init_board(board, ROW, COL);
//打印棋盘
display_board(board, ROW, COL);
char ret = 0;
while (1)
{
//玩家下棋
player_move(board, ROW, COL);
display_board(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
//电脑下棋
computer_move(board, ROW, COL);
display_board(board, ROW, COL);
is_win(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
if (ret == 'O')
{
printf("玩家赢\n");
}
else if (ret == 'X')
{
printf("电脑赢\n");
}
else
{
printf("平局\n");
}
display_board(board, ROW, COL);
}
接下来讲讲输赢的判断。只需判断每一行,每一列,对角线是否全为某方棋子即可。简单的方法是直接判断两两之间是否相等,但这样就不够通用(万一是五子棋呢?),所以我采取一种不太容易想到的方法:用计数器判断。
先定义两个计数器,分别对应玩家和电脑
int player_count = 0;//统计玩家棋子的个数
int computer_count = 0;//统计电脑棋子的个数
先检查每一行,如果是某一方的棋子则对应的计数器自增一下,再判断计数器是否与相应的行数(或者列数)相等,如三子棋就判断是否是3。
int i = 0;
int j = 0;
int player_count = 0;//统计玩家棋子的个数
int computer_count = 0;//统计电脑棋子的个数
//判断每行是否全部是某方棋子
for (i = 0; i < row; i++)
{
player_count = 0;
computer_count = 0;
for (j = 0; j < col; j++)
{
if (board[i][j] == 'O')
{
player_count++;
}
if (board[i][j] == 'X')
{
computer_count++;
}
}
if (player_count == col)
{
return 'O';
}
if (computer_count == col)
{
return 'X';
}
}
同理判断每列
//判断每列是否全部是某方棋子
for (i = 0; i < col; i++)
{
player_count = 0;
computer_count = 0;
for (j = 0; j < row; j++)
{
if (board[j][i] == 'O')
{
player_count++;
}
if (board[j][i] == 'X')
{
computer_count++;
}
}
if (player_count == col)
{
return 'O';
}
if (computer_count == col)
{
return 'X';
}
}
主对角线的行标和列标相等
//
//判断主对角线是否全部是某方棋子
// 0 1 2
//0 O
//1 O
//2 O
//
player_count = 0;
computer_count = 0;
for (i = 0; i < row; i++)
{
if (board[i][i] == 'O')
{
player_count++;
}
if (board[i][i] == 'X')
{
computer_count++;
}
}
if (player_count == col)
{
return 'O';
}
if (computer_count == col)
{
return 'X';
}
副对角线的行标和列标和为定值,如(0,2),(1,1),(2,0)行列标之和均为2。
//
//判断副对角线是否全部是某方棋子
// 0 1 2
//0 O 0,2
//1 O 1,1
//2 O 2,0
//
player_count = 0;
computer_count = 0;
for (i = 0; i < row; i++)
{
if (board[i][col - 1 - i] == 'O')
{
player_count++;
}
if (board[i][col - 1 - i] == 'X')
{
computer_count++;
}
}
if (player_count == col)
{
return 'O';
}
if (computer_count == col)
{
return 'X';
}
但是这么写的话代码就有些冗余了。我的想法是可以只用一个变量count来搞定。如果是玩家的棋子count就++,电脑的棋子就--,那么判断输赢的条件就是某一行(或某一列,对角线)的count达到了正负ROW(或者COL)就分胜负。还有就是,此时直接返回数组的元素即可,不需要分类讨论。改进后的代码如下
int i = 0;
int j = 0;
int count = 0;//统计棋子的个数
//判断每行是否全部是某方棋子
for (i = 0; i < row; i++)
{
count = 0;
for (j = 0; j < col; j++)
{
if (board[i][j] == 'O')
{
count++;
}
if (board[i][j] == 'X')
{
count--;
}
if (count == col || count == -col)
{
return board[i][j];
}
}
}
//判断每列是否全部是某方棋子
for (i = 0; i < col; i++)
{
count = 0;
for (j = 0; j < row; j++)
{
if (board[j][i] == 'O')
{
count++;
}
if (board[j][i] == 'X')
{
count--;
}
if (count == row || count == -row)
{
return board[j][i];
}
}
}
//
//判断主对角线是否全部是某方棋子
// 0 1 2
//0 O
//1 O
//2 O
//
count = 0;
for (i = 0; i < row; i++)
{
if (board[i][i] == 'O')
{
count++;
}
if (board[i][i] == 'X')
{
count--;
}
if (count == row || count == -row)
{
return board[i][i];
}
}
//
//判断副对角线是否全部是某方棋子
// 0 1 2
//0 O 0,2
//1 O 1,1
//2 O 2,0
//
count = 0;
for (i = 0; i < row; i++)
{
if (board[i][col - 1 - i] == 'O')
{
count++;
}
if (board[i][col - 1 - i] == 'X')
{
count--;
}
if (count == col || count == -col)
{
return board[i][col - 1 - i];
}
}
但事实上每次返回时只需在内层循环外头返回即可,这样判断次数就会少一些,改进代码如下
int i = 0;
int j = 0;
int count = 0;//统计棋子的个数
//判断每行是否全部是某方棋子
for (i = 0; i < row; i++)
{
count = 0;
for (j = 0; j < col; j++)
{
if (board[i][j] == 'O')
{
count++;
}
if (board[i][j] == 'X')
{
count--;
}
}
if (count == col || count == -col)
{
return board[i][0];
}
}
//判断每列是否全部是某方棋子
for (i = 0; i < col; i++)
{
count = 0;
for (j = 0; j < row; j++)
{
if (board[j][i] == 'O')
{
count++;
}
if (board[j][i] == 'X')
{
count--;
}
}
if (count == row || count == -row)
{
return board[0][i];
}
}
//
//判断主对角线是否全部是某方棋子
// 0 1 2
//0 O
//1 O
//2 O
//
count = 0;
for (i = 0; i < row; i++)
{
if (board[i][i] == 'O')
{
count++;
}
if (board[i][i] == 'X')
{
count--;
}
}
if (count == row || count == -row)
{
return board[0][0];
}
//
//判断副对角线是否全部是某方棋子
// 0 1 2
//0 O 0,2
//1 O 1,1
//2 O 2,0
//
count = 0;
for (i = 0; i < row; i++)
{
if (board[i][col - 1 - i] == 'O')
{
count++;
}
if (board[i][col - 1 - i] == 'X')
{
count--;
}
}
if (count == col || count == -col)
{
return board[0][col - 1];
}
判断平局只需判断棋盘是否填满,为此我们专门写一个函数is_full来实现。这里的static是因为我们只在is_win所在的源文件内使用(因为is_full是专门为is_win写的一个函数)。,
static int is_full(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
{
return 0;//没满
}
}
}
return 1;//满了
}
然后调用is_full判断平局。
//判断平局
if (is_full(board, row, col) == 1)
{
return 'T';//tie 平局
}
如果不分胜负还不是平局,就继续游戏。判断输赢的函数完整代码如下(包括is_full)
static int is_full(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
{
return 0;//没满
}
}
}
return 1;//满了
}
char is_win(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
int count = 0;//统计棋子的个数
//判断每行是否全部是某方棋子
for (i = 0; i < row; i++)
{
count = 0;
for (j = 0; j < col; j++)
{
if (board[i][j] == 'O')
{
count++;
}
if (board[i][j] == 'X')
{
count--;
}
}
if (count == col || count == -col)
{
return board[i][0];
}
}
//判断每列是否全部是某方棋子
for (i = 0; i < col; i++)
{
count = 0;
for (j = 0; j < row; j++)
{
if (board[j][i] == 'O')
{
count++;
}
if (board[j][i] == 'X')
{
count--;
}
}
if (count == row || count == -row)
{
return board[0][i];
}
}
//
//判断主对角线是否全部是某方棋子
// 0 1 2
//0 O
//1 O
//2 O
//
count = 0;
for (i = 0; i < row; i++)
{
if (board[i][i] == 'O')
{
count++;
}
if (board[i][i] == 'X')
{
count--;
}
}
if (count == row || count == -row)
{
return board[0][0];
}
//
//判断副对角线是否全部是某方棋子
// 0 1 2
//0 O 0,2
//1 O 1,1
//2 O 2,0
//
count = 0;
for (i = 0; i < row; i++)
{
if (board[i][col - 1 - i] == 'O')
{
count++;
}
if (board[i][col - 1 - i] == 'X')
{
count--;
}
}
if (count == col || count == -col)
{
return board[0][col - 1];
}
//判断平局
if (is_full(board, row, col) == 1)
{
return 'T';//tie 平局
}
return 'C';//continue
}
前面我们还有玩家和电脑下棋的函数没写。先说玩家下棋的函数。我们需要判断坐标是否被占用以及坐标是否合法。很简单就能实现。
void player_move(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("玩家下棋:>\n");
while (1)
{
printf("请输入坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (board[x - 1][y - 1] == ' ')
{
board[x - 1][y - 1] = 'O';
break;
}
else
{
printf("坐标已占用,请重新输入\n");
}
}
else
{
printf("坐标非法,请重新输入\n");
}
}
}
接着是电脑下棋的函数。如果是随机下棋的话非常简单,只需要产生随机的坐标即可。
//随机下棋
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = 'X';
return;
}
但在此之前要进行一些判断(因为有一些明显不错的着法)。分述如下:
第一:抢占兵家必争之地(中间位置)
//占据中间位置
x = (row - 1) / 2;
y = (col - 1) / 2;
if (board[x][y] == ' ')
{
board[x][y] = 'X';
return;
}
第二:一击必杀位置,不能憨憨的错过(进攻是最好的防守)
具体实现的时候需要假设下某个位置,然后调用判断输赢的函数,如果发现下了就赢了,那就下这里,如果下了没赢就当什么事情都没发生。
//电脑连成2个
for (x = 0; x < row; x++)
{
for (y = 0; y < col; y++)
{
if (board[x][y] == ' ')
{
board[x][y] = 'X';
if (IsWin(board, row, col) == 'X')
{
return;
}
board[x][y] = ' ';
}
}
}
第三:感知危险,堵住对手的棋(防守反击)
有了第二点,这里就很简单了,只不过是假设玩家下了一步棋。
//玩家连成2个
for (x = 0; x < row; x++)
{
for (y = 0; y < col; y++)
{
if (board[x][y] == ' ')
{
board[x][y] = 'O';
if (IsWin(board, row, col) == 'O')
{
board[x][y] = 'X';
return;
}
board[x][y] = ' ';
}
}
}
电脑下棋函数完整的代码如下
void computer_move(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑下棋:>\n");
//占据中间位置
x = (row - 1) / 2;
y = (col - 1) / 2;
if (board[x][y] == ' ')
{
board[x][y] = 'X';
return;
}
//电脑连成2个
for (x = 0; x < row; x++)
{
for (y = 0; y < col; y++)
{
if (board[x][y] == ' ')
{
board[x][y] = 'X';
if (IsWin(board, row, col) == 'X')
{
return;
}
board[x][y] = ' ';
}
}
}
//玩家连成2个
for (x = 0; x < row; x++)
{
for (y = 0; y < col; y++)
{
if (board[x][y] == ' ')
{
board[x][y] = 'O';
if (IsWin(board, row, col) == 'O')
{
board[x][y] = 'X';
return;
}
board[x][y] = ' ';
}
}
}
//随机下棋
while (1)
{
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = 'X';
return;
}
}
}
补充一下:这个算法已经比随机下棋要强上很多了,但是仍然有一些缺陷,赢电脑其实并不难。如果要想实现完美走棋,可以使用枚举法(但代码的适用性会下降,比如五子棋你就很难枚举了),或者用更高阶的算法(比如博弈树),但那样就相对比较复杂了,我后面会单独写一篇博客来讲解。
最后说明一下,这个项目可以分为三个文件来写,分别是game.h(符号的定义和函数的声明),game.c(游戏函数的具体实现),test.c(游戏函数的测试)。代码如下:
game.h
#pragma once
#include
#include
#include
#define ROW 3 //行
#define COL 3 //列
//初始化棋盘
void init_board(char board[ROW][COL], int row, int col);
//打印棋盘
void display_board(char board[ROW][COL], int row, int col);
//玩家下棋
void player_move(char board[ROW][COL], int row, int col);
//电脑下棋
void computer_move(char board[ROW][COL], int row, int col);
//判断输赢
char is_win(char board[ROW][COL], int row, int col);
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
static int is_full(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
{
return 0;//没满
}
}
}
return 1;//满了
}
char is_win(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
int count = 0;//统计棋子的个数
//判断每行是否全部是某方棋子
for (i = 0; i < row; i++)
{
count = 0;
for (j = 0; j < col; j++)
{
if (board[i][j] == 'O')
{
count++;
}
if (board[i][j] == 'X')
{
count--;
}
}
if (count == col || count == -col)
{
return board[i][0];
}
}
//判断每列是否全部是某方棋子
for (i = 0; i < col; i++)
{
count = 0;
for (j = 0; j < row; j++)
{
if (board[j][i] == 'O')
{
count++;
}
if (board[j][i] == 'X')
{
count--;
}
}
if (count == row || count == -row)
{
return board[0][i];
}
}
//
//判断主对角线是否全部是某方棋子
// 0 1 2
//0 O
//1 O
//2 O
//
count = 0;
for (i = 0; i < row; i++)
{
if (board[i][i] == 'O')
{
count++;
}
if (board[i][i] == 'X')
{
count--;
}
}
if (count == row || count == -row)
{
return board[0][0];
}
//
//判断副对角线是否全部是某方棋子
// 0 1 2
//0 O 0,2
//1 O 1,1
//2 O 2,0
//
count = 0;
for (i = 0; i < row; i++)
{
if (board[i][col - 1 - i] == 'O')
{
count++;
}
if (board[i][col - 1 - i] == 'X')
{
count--;
}
}
if (count == col || count == -col)
{
return board[0][col - 1];
}
//判断平局
if (is_full(board, row, col) == 1)
{
return 'T';//tie 平局
}
return 'C';//continue
}
void init_board(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
board[i][j] = ' ';
}
}
}
void display_board(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
//打印数据
for (j = 0; j < row; j++)
{
printf(" %c ", board[i][j]);
if (j < row - 1)
{
printf("|");
}
}
printf("\n");
//打印分割行
if (i < row - 1)
{
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
{
printf("|");
}
}
}
printf("\n");
}
}
void player_move(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("玩家下棋:>\n");
while (1)
{
printf("请输入坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (board[x - 1][y - 1] == ' ')
{
board[x - 1][y - 1] = 'O';
break;
}
else
{
printf("坐标已占用,请重新输入\n");
}
}
else
{
printf("坐标非法,请重新输入\n");
}
}
}
void computer_move(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑下棋:>\n");
//占据中间位置
x = (row - 1) / 2;
y = (col - 1) / 2;
if (board[x][y] == ' ')
{
board[x][y] = 'X';
return;
}
//电脑连成2个
for (x = 0; x < row; x++)
{
for (y = 0; y < col; y++)
{
if (board[x][y] == ' ')
{
board[x][y] = 'X';
if (IsWin(board, row, col) == 'X')
{
return;
}
board[x][y] = ' ';
}
}
}
//玩家连成2个
for (x = 0; x < row; x++)
{
for (y = 0; y < col; y++)
{
if (board[x][y] == ' ')
{
board[x][y] = 'O';
if (IsWin(board, row, col) == 'O')
{
board[x][y] = 'X';
return;
}
board[x][y] = ' ';
}
}
}
//随机下棋
while (1)
{
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = 'X';
return;
}
}
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void menu()
{
printf("*******************************\n");
printf("*********** 1. play ***********\n");
printf("*********** 0. exit ***********\n");
printf("*******************************\n");
}
void game()
{
//存储下棋的数据
char board[ROW][COL] = { 0 };
//初始化棋盘为全空格
init_board(board, ROW, COL);
//打印棋盘
display_board(board, ROW, COL);
char ret = 0;
while (1)
{
//玩家下棋
player_move(board, ROW, COL);
display_board(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
//电脑下棋
computer_move(board, ROW, COL);
display_board(board, ROW, COL);
is_win(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
if (ret == 'O')
{
printf("玩家赢\n");
}
else if (ret == 'X')
{
printf("电脑赢\n");
}
else
{
printf("平局\n");
}
display_board(board, ROW, COL);
}
void test()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请输入:>");
scanf("%d", &input);
switch (input)
{
case 1:
//printf("三子棋\n");
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
其中game.h的第一行是防止头文件被重复引用,可以省略。game.c和test.c的第一行是因为我在VS编译器里测试代码,为了解决scanf函数报错问题而写的。如果不是VS系列编译器可以省略掉这行代码。
创作不易,如果你觉得这篇博客对你有帮助的话,麻烦点个免费的赞,谢啦。