1、三子棋的实现流程(按模块化实现)
- 三子棋棋盘的设计
- 初始化棋盘
- 打印棋盘
- 玩家下棋
- 电脑下棋
- 判定输赢
2、三子棋棋盘的设计
三子棋棋盘三行三列,我们可以设计一个二维数组来存放我们要在棋盘中间落子的符号。
char board[ROW][COL] = { 0 };
ROW与COL是我们设定的行与列,已经在一个专门的头文件(.h文件)中用#define的标识符常量定义好为三行三列,如下:
#define ROW 3 #define COL 3
3、初始化棋盘
当棋盘不落子的时候,在我们肉眼看来棋盘上是空的,但是对于计算机来说,棋盘上这个空间一定存放了东西(即这个二维数组一定存放了元素)。所以我们将这个二维数组中的所有元素存放一个空格。
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] = ' '; } } }
4、打印棋盘
我们如何将这个有横有竖的三行三列表现出来?
我们可以将棋盘的落子空间与下面的横线看成是一组元素,用循环分别打印三次。
//第一个版本 void DispalyBoard(char board[ROW][COL], int row, int col) { int i = 0; for (i = 0; i < row; i++) { //打印数据 printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][0]); //打印分割信息 if (i < row - 1) { printf("---|---|---\n"); } } }
打印结果如上图所示
1 . 循环中的 if 语句是为了控制第三行的分割信息不打印。
2. 此种打印方式只能打印三行三列,以后我们想打印五子棋或者十字棋,在头文件中将
#define ROW 3
#define COL 3 改为
#define ROW 5
#define COL 5
并不会出现我们想要的五行五列或者十行十列,而是会出现五行三列或者十行三列。这种打印方式已经将打印的列数固定死了,只能打印三列。
改进版本
void DispalyBoard(char board[ROW][COL], int row, int col) { int i = 0; for (i = 0; i < row; i++) { //打印数据 //printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][0]); int j = 0; for (j = 0; j < col; j++) { printf(" %c ", board[i][j]); if (j < col - 1) { printf("|"); } } printf("\n"); //打印分割信息 //printf("---|---|---\n"); if (i < row - 1) { int j = 0; for (j = 0; j < col; j++) { printf("---"); if (j < col - 1) { printf("|"); } } printf("\n"); } } }
- if (j < col - 1)
{ printf(“|”); }
此条语句是为了控制最后一列的分割信息不打印。因为此时的列数可以随着头文件中的定义 #define COL 3 改变而改变。
如果之前没有对棋盘初始化为空格会造成什么样的结果?
棋盘最开始的初始化为 char board[ROW][COL] = { 0 };,即给每个元素输入了数字零,这是个字符数组,零所对应的字符为空字符,空字符打印出来就是什么都没有,所以棋盘中间的落子部分什么也没有。落子的前后打印的是空格,落子部分什么也没有,相对于棋盘的其他部位就会往右缩进一个字符的距离。
验证一下空字符是不是什么都没有
如图所示,打印空字符就是打印空白。
不以字符的形式输出,我们将数组中元素以整型形式输出,看看结果
可以看到我们之前初试化的零被打印出来了。
char board[ROW][COL] = { 0 };这个代码告诉我们是以空字符初始化了数组,而不是给数组中元素初始化了字符 ‘0’ 。要想给一个字符变量赋值字符 0 ,要写成"00000",以字符串的形式输入一串字符 0 ,或者写成 ‘0’ ,输入一个字符 0 .
这个案例提示我们要注意内存的含义,即1. 内存中存的是什么,2. 怎么理解这块内存
5、玩家下棋
代码展示
void PlayerMove(char board[ROW][COL], int row, int col) { int x = 0; int y = 0; printf("玩家下棋:>\n"); while (1) { printf("请输入坐标:>\n"); 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; } else { printf("坐标被占用,不能下棋,请选择其他位置\n"); } } else { printf("坐标非法,请重新输入"); } } }
打印结果
- 对于三子棋来说,输入的坐标,行和列分别都要符合1 到 3的范围。坐标没有符合要求,循环继续,坐标符合要求,通过 break 跳出循环。
- 将玩家的落子赋值到玩家输入的坐标上去,board[x - 1][y - 1] = ‘*’; 为什么是 x-1 与 y-1 ?
因为从人的视角看,第一个棋盘上的第一个空格的坐标是(1 1),对于数组下标的定义,这个坐标是(0 0)。
6、电脑下棋
代码展示
void ComputerMove(char board[ROW][COL], int row, int col) { printf("电脑下棋:>\n"); int x = 0; int y = 0; while (1) { x = rand() % row; //0~2 y = rand() % col; //0~2 if (board[x][y] == ' ') { board[x][y] = '#'; break; } } }
打印结果
- 此种写法是简单版模式,电脑只是按照随机生成的坐标落子,并没有按照人的思维去尽快连成三子或者去堵对家的棋。
- 如何自动生成坐标,即如何生成随机数。
rand( ); 函数是生成随机数的函数,能够生成 0 ~ 32767 的随机数。rand( )%row 与 rand( )%col,即能生成0 ~ row-1 与0 ~col-1 的随机数。- rand( ); 函数的用法
rand函数的头文件是#include <stdlib.h>。返回值是0~ RAND_MAX,即0 ~ 32767。调用 rand 函数前,要先调用srand( ); 函数,srand函数的括号中要设置一个起点,并且要保证这个这个起点每次都不同,如果起点不变,那么后面再次运行这个程序,调用 rand函数生成的随机数跟一次运行这个程序调用rand函数生成的随机数一一对应相同。(假设第一次运行这个程序,生成的随机数是 8 9 6 7 2 4…… ,第二次运行这个程序,生成是随机数同样是8 9 6 7 2 4……)- srand( );函数的用法
srand函数的头文件是 #include <stdlib.h>,srand函数没有返回值,srand函数的参数是一个可变的起点。给srand函数设置参数这不又回到了生成随机数,难道我们又要用rand函数吗,似乎是在套娃。这里我们用time( );函数的时间戳的返回值来作为srand函数的随机数的起点。- time( );函数
time_t time (time_t* timer);
Get the current calendar time as a value of type time_t.
time函数的头文件是#include <time.h>。time函数的参数是一个time_t类型的指针,返回值也是time_t类型的数。time函数的返回值是根据电脑上流动的时间生成的时间戳 ,时间在不断的变化,生成的时间戳自然也就不同,相当于生成了一系列随机数(伪随机数),这些随机数充当了srand函数可变的起点。- srand((unsigned int)time(NULL))
这就是我们调用rand函数前,调用的srand函数的具体写法。不是每次调用rand函数前都需要调用srand函数,只要在整个工程文件中调用一次就好,所以把这段代码写在主函数模块中。
7、判断输赢
玩家下一次棋后需要判断一下当前棋盘上的情况,电脑下一次棋后同样需要判断一下当前棋盘上的情况。
设定判断输赢函数的四种情况的返回值。
玩家赢 - ‘*’
电脑赢 - ‘#’
平局 - ‘Q’
继续 - ‘C’
那么这个判断输赢的代码怎么写?
//满了返回1 //不满返回0 int IsFull(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 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][0] != ' ') //一行的三个元素相等,并且任何一个元素不等于空格 { return board[i][0]; } } //判断列 int j = 0; for (j = 0; j < col; j++) { if (board[0][j] == board[1][j] && board[1][j] == board[2][j] && board[0][j]!=' ') { return board[0][j]; } } //判断对角线 if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1] != ' ') { return board[1][1]; } if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1] != ' ') { return board[1][1]; } //当前面走完后,没有人赢的情况下,判断一下是否平局 if (IsFull(board, row, col)) { return 'Q'; } //没人赢,棋盘又没满,游戏继续 return 'C'; }
打印结果
- 判断输赢,主要看行、列、对角线上面的三个元素是否一样,并且这三个元素不是空格。满足条件,将这三个元素的任一个作为返回值。
- 当判断完行、列、对角线以后,发现没有产生赢的结果,判断棋盘是否已经下满了?下满了即平局,返回 ‘Q’。
- 当没有人赢,棋盘又没下满,说明游戏还要继续,返回 ‘C’。
8、游戏完整代码
game.h 头文件:包含了库函数的头文件以及自定义函数的声明
#pragma once #include <stdio.h> #include <stdlib.h> #include <time.h> #define ROW 3 #define COL 3 //初始化棋盘 void InitBoard(char board[ROW][COL], int row, int col); //打印棋盘 void DispalyBoard(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); //每下一步棋的结果 //玩家赢 - '* ' //电脑赢 - '# ' //平局 - 'Q ' //继续 - 'C ' char IsWin(char board[ROW][COL], int row, int col);
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 DispalyBoard(char board[ROW][COL], int row, int col) //{ // int i = 0; // for (i = 0; i < row; i++) // { // //打印数据 // printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][0]); // //打印分割信息 // if (i < row - 1) // { // printf("---|---|---\n"); // } // // } //} void DispalyBoard(char board[ROW][COL], int row, int col) { int i = 0; for (i = 0; i < row; i++) { //打印数据 //printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][0]); int j = 0; for (j = 0; j < col; j++) { printf(" %c ", board[i][j]); if (j < col - 1) { printf("|"); } } printf("\n"); //打印分割信息 //printf("---|---|---\n"); if (i < row - 1) { int j = 0; for (j = 0; j < col; j++) { printf("---"); if (j < col - 1) { printf("|"); } } printf("\n"); } } } void PlayerMove(char board[ROW][COL], int row, int col) { int x = 0; int y = 0; printf("玩家下棋:>\n"); while (1) { printf("请输入坐标:>\n"); 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; } else { printf("坐标被占用,不能下棋,请选择其他位置\n"); } } else { printf("坐标非法,请重新输入"); } } } void ComputerMove(char board[ROW][COL], int row, int col) { printf("电脑下棋:>\n"); int x = 0; int y = 0; while (1) { x = rand() % row; //0~2 y = rand() % col; //0~2 if (board[x][y] == ' ') { board[x][y] = '#'; break; } } } //满了返回1 //不满返回0 int IsFull(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 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][0] != ' ') //一行的三个元素相等,并且任何一个元素不等于空格 { return board[i][0]; } } //判断列 int j = 0; for (j = 0; j < col; j++) { if (board[0][j] == board[1][j] && board[1][j] == board[2][j] && board[0][j]!=' ') { return board[0][j]; } } //判断对角线 if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1] != ' ') { return board[1][1]; } if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1] != ' ') { return board[1][1]; } //当前面走完后,没有人赢的情况下,判断一下是否平局 if (IsFull(board, row, col)) { return 'Q'; } //没人赢,棋盘又没满,游戏继续 return 'C'; }
#include “game.h”,相当于将game.h头文件中包含的库函数头文件与自定义函数声明写在了game.c此文件中。
test.c文件:将主函数放在此文件中,整合实现三子棋功能函数的顺序,什么时候实现什么函数的功能。
#define _CRT_SECURE_NO_WARNINGS 1 #include "game.h" menu() { printf("****************************\n"); printf("***** 1.play 0.exit *****\n"); printf("****************************\n"); } void game() { char ret = 0; char board[ROW][COL] = { 0 }; //初始化棋盘的函数 InitBoard(board, ROW, COL); //打印棋盘 DispalyBoard(board, ROW, COL); //下棋 while (1) { PlayerMove(board, ROW, COL); //判断输赢 ret = IsWin(board, ROW, COL); if (ret != 'C') { break; } DispalyBoard(board, ROW, COL); ComputerMove(board, ROW, COL); //判断输赢 ret = IsWin(board, ROW, COL); if (ret != 'C') { break; } DispalyBoard(board, ROW, COL); } if (ret == '*') { printf("玩家赢了\n"); } else if (ret == '#') { printf("电脑赢了\n"); } else { printf("平局\n"); } DispalyBoard(board, ROW, COL); //不论是什么结局,将结局再打印一遍 } int main() { srand((unsigned int)time(NULL)); //设置随机数的生成起点的 int input = 0; do { menu(); printf("请选择:>"); scanf("%d", &input); switch (input) { case 1: game(); break; case 0: printf("退出游戏\n"); break; default: printf("选择错误\n"); break; } } while (input); return 0; }
游戏的三种结局
玩家赢
电脑赢
平局
选择do while循环结构,程序一运行先打印游戏菜单,选择是否玩游戏。将输入决定玩游戏的 input来作为循环条件,input = 1,玩游戏 ;input =0 ,游戏结束,循环结束;input = 其他数字,循环条件非 0 循环依然在继续,重新选择是否玩游戏。