【用C语言轻松搞定】三子棋(超详细教程)
一、游戏规则
游戏玩家将会看到屏幕上显现一个 3*3 的网格棋盘,系统默认玩家先下棋,电脑后下棋。
规定:先连成一条直线(3 个棋子)的玩家获胜,直线是行,列,对角线均可。若在棋盘下满时仍未分出胜负,则结果为平局。
二、文件的创建
- test.c(实现游戏的整体思路,用于测试三子棋游戏的逻辑)
- game.c(游戏的实现)
- game.h(声明 .c 文件的函数,包含所有需要用到的头文件以及 define 定义的常量)
注:如果原文件和头文件名字一样,则表示代表同一个模块。
三、初始页面的实现
1、实现主菜单页面
// test.c
(1)menu()
void menu()
{
printf("*****************************\n");
printf("***** 1.Play 0.Exit *****\n");
printf("*****************************\n");
}
运行效果图:
(2)test():
void test()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();// 主菜单
printf("请选择:>");
scanf("%d", &input);// 玩家选择
switch (input)// 判断玩家是否进行游戏以及是否输入合法选项
{
case 1:
game();// 游戏
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择!\n");
break;
}
} while (input);
}
注:这里使用 do while() 循环可以保证至少会进行一次选择。如果选择 0,会被判定为假,则跳出循环,退出游戏;如果输入选择则不会退出游戏,可以1保证游戏能够重复游玩。
(3)main():
int main()
{
test();
return 0;
}
2、在 game.h 中引用所需要的文件和必要的定义
// game.h
#pragma once
#define ROW 3 // 行
#define COL 3 // 列
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
注:优点:如果后期需要改变棋盘的大小,则只需要在头文件这里改,不需要在整个程序中进行修改。define 定义的常量在句末不需要加分号。
四、棋盘函数的声明、创建和实现(打印棋盘函数同理)
1、声明棋盘的函数(附加打印棋盘函数)
// game.h
void InitBoard(char board[ROW][COL], int row, int col);
void DisplayBoard(char board[ROW][COL], int row, int col);
2、棋盘函数的创建 (附加打印功能)
// test.c
void game()
{
char board[ROW][COL] = { 0 };// 存放下棋的数据
InitBoard(board, ROW, COL);// 初始化棋盘为全空格
DisplayBoard(board, ROW, COL);// 打印棋盘
}
3、棋盘函数的实现(附加打印棋盘函数的实现)
// game.c
void InitBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = ' ';// 将棋盘初始化为空格
}
}
}
void DisplayBoard(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++)
{
printf(" %c ", board[i][j]);//打印一行的数据
if (j < col - 1)
printf("|");
}
printf("\n");// 分割行
if (i < row - 1)// --- 只打印两行
{
for (j = 0; j < col; j++)
{
printf("---");// 每行需要3个---
if (j < col - 1)
printf("|");
}
printf("\n");
}
}
}
注:对棋盘进行初始化是为了让 board 中的每一个元素都初始化为 ' ' ,打印棋盘时需要注意的是为了让棋盘看起来更加美观,每一行的最后一个 '|' 不打印,最后一个分割行也不打印,这里是用 if 语句实现的。
棋盘打印效果:
五、下棋
1、玩家下棋
// game.h
void PlayerMove(char board[ROW][COL], int row, int col);
// game.c
void PlayerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("玩家走:>示例(输入:1 1)\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] = '*';
break;// 用break语句跳出循环,防止一直输入坐标
}
else
{
printf("该坐标已被占用,请重新输入!\n");
}
}
else
{
printf("坐标非法,请重新输入!\n");
}
}
}
注:我们在写代码的时候知道第一个位置的坐标是 (0,0),但在玩家的眼里第一个位置的坐标是(1,1),所以玩家输入的数字 (x,y) 是在程序中是 (x-1,y-1) 这个位置。
1、用户输入的坐标棋盘上没有怎么处理?
答:这里使用了 if-else 语句,如果坐标符合要求,那么玩家的棋子就正常落子;如果坐标不符合要求,程序就会提醒 “坐标非法,请重现输入坐标!” 的字样,利用 while() 循环使玩家输入符合要求的坐标落子后再跳出循环。
2、用户输入的坐标已被占用怎么处理?
答:这里依旧使用 if-else 语句,在初始化棋盘的时候用空格填充每个位置。如果这个位置是空格,那么这个位置就可以正常落子 '*' ;如果这个位置是非空格符号(在这里只会是 '*' 或者 '#' ),那么这个位置就是被占用了,程序就会提醒 “该坐标已被占用,请重新输入!”,利用 while() 循环使玩家输入没有被占用且符合要求的坐标正确落子后再跳出循环。
2、电脑下棋
// game.h
void ComputerMove(char board[ROW][COL], int row, int col);
// game.c
void ComputerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑走:>\n");
while (1)
{
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')// 保证电脑也下到空格位置,不是空格就再次循环
{
board[x][y] = '#';
break;// 用break语句跳出循环,防止一直输入坐标
}
}
}
1、如何实现电脑随机输入一个坐标?
答:利用 srand() 函数来实现随机数字,但 srand() 函数需要一个随机值,这里的随机值就是利用时间戳。因为时间时刻改变着,不同的时间经过 srand 处理后就得到了我们所需要的随机值。因为在这里需要用到时间戳,所以需要调用 time 函数。
#include <time.h>
srand() :
srand((unsigned int)time(NULL));
2、如何处理电脑随机坐标的合法性?
答:在使用 srand() 函数处理的随机值后,不再经过处理可能会出现大于棋盘格数的情况,所以需要再对 srand() 函数处理的随机值再进行处理。对 srand 传过来的随机值进行取模处理,我们的棋盘是 3*3 的,所以取模后得到的随机值是在 0~2 范围里。
x = rand() % row;// 0~2 y = rand() % col;// 0~2
3.如果出现坐标被占用的情况该如何处理?
答:使用 while() 循环,如果给出的坐标位置判断不是空格(' '),就说明该坐标被占用,经过循环最后给出符合要求的坐标,实现落子后就会触发 break 跳出循环。
六、判断输赢情况
1、规定
(1)获胜:横行三子 / 竖行三子 / 对角线三子。
(2)平局:棋盘已被占满且无胜利情况。
(3)继续:棋盘未占满且没有胜利情况。
(1)玩家胜 -- 返回 '*'
(2)电脑胜 -- 返回 '#'
(3)平局 -- 返回 'Q'
(4)继续 -- 返回 'C'
2、平局 / 输赢判断
如果棋盘被下满还没有分出胜负便是平局。
如何判断是否棋盘被下满呢?
答:我们遍历棋盘的每一个格子,如果没有一个是空格,就返回数字 1 ,否则返回数字 0 。
(1)IsFull():
//返回1表示棋盘满了
//返回0表示棋盘没满
int IsFull(char board[ROW][COL], int row, int col)// 判断是否平局
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; i++)
{
if (board[i][j] == ' ')
{
return 0;//没满
}
}
}
return 1;//满了
}
(2)IsWin():
char IsWin(char board[ROW][COL], int row, int col);
char IsWin(char board[ROW][COL], int row, int col)
{
int i = 0;
// 判断横三行
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1] != ' ')
{
return board[i][1];
}
}
// 判断竖三列
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[1][i] != ' ')
{
return board[1][i];
}
}
// 判断两条对角线
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1] != ' ')
return board[1][1];
if (board[2][0] == board[1][1] && board[1][1] == board[0][2] && board[1][1] != ' ')
return board[1][1];
// 判断是否平局
if (1 == IsFull(board, ROW, COL))
{
return 'Q';
}
return 'C';
}
在 IsWin() 函数中判断返回值是否为 1 ,如果是则结果为平局。这里只对棋盘格是否被占满作判断,只需要判断返回值是否为 1 。如果是 0 则棋盘没有占满,无需考虑其他,游戏继续即可。
七、代码整合
1、// 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 ret = 0;
char board[ROW][COL] = { 0 };
InitBoard(board, ROW, COL);// 初始化棋盘
DisplayBoard(board, ROW, COL);// 打印棋盘
while (1)
{
PlayerMove(board, ROW, COL);// 玩家下棋
DisplayBoard(board, ROW, COL);// 打印棋盘
ret = IsWin(board, ROW, COL);// 判断玩家是否赢
if (ret != 'C')
{
break;
}
ComputerMove(board, ROW, COL);// 电脑下棋
DisplayBoard(board, ROW, COL);// 打印棋盘
ret = IsWin(board, ROW, COL);// 判断电脑是否赢
if (ret != 'C')
{
break;
}
}
if (ret == '*')
{
printf("玩家赢\n");
}
else if (ret == '#')
{
printf("电脑赢\n");
}
else
{
printf("平局\n");
}
}
void test()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择!\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
2、// game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void InitBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = ' ';// 将棋盘初始化为空格
}
}
}
void DisplayBoard(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++)
{
printf(" %c ", board[i][j]);//打印一行的数据
if (j < col - 1)
printf("|");
}
printf("\n");// 分割行
if (i < row - 1)// --- 只打印两行
{
for (j = 0; j < col; j++)
{
printf("---");// 每行需要3个---
if (j < col - 1)
printf("|");
}
printf("\n");
}
}
}
void PlayerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("玩家走:>示例(输入:1 1)\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] = '*';
break;// 用break语句跳出循环,防止一直输入坐标
}
else
{
printf("该坐标已被占用,请重新输入!\n");
}
}
else
{
printf("坐标非法,请重新输入!\n");
}
}
}
void ComputerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑走:>\n");
while (1)
{
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')// 保证电脑也下到空格位置,不是空格就再次循环
{
board[x][y] = '#';
break;// 用break语句跳出循环,防止一直输入坐标
}
}
}
//返回1表示棋盘满了
//返回0表示棋盘没满
int IsFull(char board[ROW][COL], int row, int col)// 判断是否平局
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; i++)
{
if (board[i][j] == ' ')
{
return 0;//没满
}
}
}
return 1;//满了
}
char IsWin(char board[ROW][COL], int row, int col)
{
int i = 0;
// 判断横三行
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1] != ' ')
{
return board[i][1];
}
}
// 判断竖三列
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[1][i] != ' ')
{
return board[1][i];
}
}
// 判断两条对角线
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1] != ' ')
return board[1][1];
if (board[2][0] == board[1][1] && board[1][1] == board[0][2] && board[1][1] != ' ')
return board[1][1];
// 判断是否平局
if (1 == IsFull(board, ROW, COL))
{
return 'Q';
}
return 'C';
}
3、// game.h
#pragma once
#define ROW 3// 行
#define COL 3// 列
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 声明函数
void InitBoard(char board[ROW][COL], int row, int col);
void DisplayBoard(char board[ROW][COL], int row, int col);
void PlayerMove(char board[ROW][COL], int row, int col);
void ComputerMove(char board[ROW][COL], int row, int col);
char IsWin(char board[ROW][COL], int row, int col);
八、程序展示
运行效果图:
九、改进与建议
因为这里电脑下棋设置的是随机值,所以玩家获胜的概率偏大。可以通过一些 if-else 语句来对电脑下棋设限制,提高其获胜概率,使其游戏公平化。
优化版:
电脑落子有三种可能:(以下三种可能优先级由高到低)
- 下一步可以胜利(在自己已经有两个棋子相连的情况下落下,则一子达成三连)
- 堵住对方即将胜利的棋子(未出现第一种情况时,若对方出现两子相连的情况及时堵住)
- 随机落下一枚棋子(在前两种情况都未出现时,在棋盘中 “随机落下一子”,要保证该位置周围八个位置有对方棋子,避免成为 “废棋” )
参考代码:
//优化版
int ComputerMove2(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
printf("电脑下棋\n");
Sleep(2000);//模拟人类思考时间
//情况1(进攻)
//行判断:
for (int i = 0; i < ROW; i++)
{
if (board[i][0] == board[i][1] && ' ' == board[i][2] && '#' == board[i][0])
{
board[i][2] = '#';
return 0;
}
if (board[i][0] == board[i][2] && ' ' == board[i][1] && '#' == board[i][0])
{
board[i][1] = '#';
return 0;
}
if (board[i][1] == board[i][2] && ' ' == board[i][0] && '#' == board[i][1])
{
board[i][0] = '#';
return 0;
}
}
//列判断
for (int j = 0; j < COL; j++)
{
if (board[0][j] == board[1][j] && ' ' == board[2][j] && '#' == board[1][j])
{
board[2][j] = '#';
return 0;
}
if (board[0][j] == board[2][j] && ' ' == board[1][j] && '#' == board[2][j])
{
board[1][j] = '#';
return 0;
}
if (board[1][j] == board[2][j] && ' ' == board[0][j] && '#' == board[2][j])
{
board[0][j] = '#';
return 0;
}
}
//对角线判断
if (board[0][0] == board[1][1] && ' ' == board[2][2] && '#' == board[0][0])
{
board[2][2] = '#';
return 0;
}
if (board[0][0] == board[2][2] && ' ' == board[1][1] && '#' == board[0][0])
{
board[1][1] = '#';
return 0;
}
if (board[2][2] == board[1][1] && ' ' == board[1][1] && '#' == board[2][2])
{
board[1][1] = '#';
return 0;
}
if (board[0][2] == board[1][1] && ' ' == board[2][0] && '#' == board[0][2])
{
board[2][0] = '#';
return 0;
}
if (board[0][2] == board[2][0] && ' ' == board[1][1] && '#' == board[0][2])
{
board[1][1] = '#';
return 0;
}
if (board[2][0] == board[1][1] && ' ' == board[0][2] && '#' == board[2][0])
{
board[0][2] = '#';
return 0;
}
//情况2(防守)
//行判断:
for (int i = 0; i < ROW; i++)
{
if (board[i][0] == board[i][1] && ' ' == board[i][2] && '*' == board[i][0])
{
board[i][2] = '#';
return 0;
}
if (board[i][0] == board[i][2] && ' ' == board[i][1] && '*' == board[i][0])
{
board[i][1] = '#';
return 0;
}
if (board[i][1] == board[i][2] && ' ' == board[i][0] && '*' == board[i][1])
{
board[i][0] = '#';
return 0;
}
}
//列判断
for (int j = 0; j < COL; j++)
{
if (board[0][j] == board[1][j] && ' ' == board[2][j] && '*' == board[1][j])
{
board[2][j] = '#';
return 0;
}
if (board[0][j] == board[2][j] && ' ' == board[1][j] && '*' == board[2][j])
{
board[1][j] = '#';
return 0;
}
if (board[1][j] == board[2][j] && ' ' == board[0][j] && '*' == board[2][j])
{
board[0][j] = '#';
return 0;
}
}
//对角线判断
if (board[0][0] == board[1][1] && ' ' == board[2][2] && '*' == board[0][0])
{
board[2][2] = '#';
return 0;
}
if (board[0][0] == board[2][2] && ' ' == board[1][1] && '*' == board[0][0])
{
board[1][1] = '#';
return 0;
}
if (board[2][2] == board[1][1] && ' ' == board[1][1] && '*' == board[2][2])
{
board[1][1] = '#';
return 0;
}
if (board[0][2] == board[1][1] && ' ' == board[2][0] && '*' == board[0][2])
{
board[2][0] = '#';
return 0;
}
if (board[0][2] == board[2][0] && ' ' == board[1][1] && '*' == board[0][2])
{
board[1][1] = '#';
return 0;
}
if (board[2][0] == board[1][1] && ' ' == board[0][2] && '*' == board[2][0])
{
board[0][2] = '#';
return 0;
}
//情况3(原始版本)
else
{
int n = 0;
int m = 0;
while (1)
{
n = rand() % row;
m = rand() % col;
int ret = IsHave(board, row, col, n, m);
if (' ' == board[n][m] && ret)
{
board[n][m] = '#';
break;
}
}
}
return 0;
}