【C语言】超详细的----三子棋游戏---- 实现与解析
本次学习写一个三子棋软件,基本上用到了之前所学的所有知识,下面开始解析这个 三子棋游戏。
三子棋从下往上 的组成有:棋盘,我方棋,对方棋。三个部分组成。
棋盘的要素简述
首先棋盘是一个3*3的正方形(所以使用二维数组),每一个格子应该初始化成为 空格 ' '
,以便放入棋子。
第二,方格间应该要有分隔线构成,但是要注意边界没有棋盘的周围没有分割线。
第三,定义棋子实现,我们用 *
来表示我方棋子,用 #
表示电脑棋子。
第四,定义胜利条件,胜利条件,同样的 符号成为 一条对角线,横排或竖排中三个符号成线。
完整游戏展示
在详细的实现游戏之间,我们先完整的看一下我们的代码。
这个游戏总用了 三个文件, game.h
, game.c
, test.c
。它们三个文件都用在一个工程之中,是不可分割的。
test.c 是主文件,存放着主函数 和 game.c 文件的函数调用。
game.h 是头文件,存放着 game.c 函数声明 和整个程序所引用的头文件 和 常量 。
game.c 是用来存放函数主体 的文件
test.c
#include "game.h"
void menu() {
printf("***************************\n");
printf("******** 1.play *********\n");
printf("******** 0.exit *********\n");
printf("***************************\n");
}
void game() {
char board[ROW][COL] = { 0 };
char ret = 0;
Initboard(board, ROW, COL);//初始化数组为 ' '
display_board(board, ROW, COL);//打印棋盘
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);//下棋以后,也要打印棋盘
ret = is_win(board,ROW,COL);
if (ret != 'C') {
break;
}
}
//判断胜利条件
if (ret == 'Q') {
printf("平局!\n");
}
if (ret == '#') {
printf("电脑赢!\n");
}
if (ret == '*') {
printf("玩家赢!\n");
}
}
//主函数
int main() {
int input = 0;
srand((unsigned int)time(NULL));
menu();
do {
printf("请输入:(1 or 0) > ");
scanf("%d", &input);
switch (input) {
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误。请重新输入\n");
break;
}
} while (input);
return 0;
}
game.c (存放函数主体的文件)
#include "game.h"
//初始化棋盘为 ' '
void Initboard(char board[ROW][COL], int row, int col) {
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
board[i][j] = ' ';
}
}
}
//打印棋盘
void display_board(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++) {
printf(" %c ",board[i][j]);
//棋盘的分割线打印
if (j < col - 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) {
printf("玩家走:> ");
int x = 0;
int y = 0;
while (1) {
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("该坐标已被占用");
}
}
else {
printf("输入错误,请重新输入");
}
}
}
//电脑下棋布置。
void computer_move(char board[ROW][COL], int row, int col) {
int x = 0;
int y = 0;
while (1) {
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ') {
board[x][y] = '#';
break;
}
}
}
//is_full()函数是判断棋盘是否被占满的,因为只用在is_win()函数中,所以不需要在头文件game.h中声明。
int is_full(char board[ROW][COL], int row, int col) {
for (int i = 0; i < row; i++) {
for (int 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;
for (i = 0; i < row; i++) {
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][2] != ' ') {
return board[i][1];
}
}
for (j = 0; j < col; j++) {
if (board[0][j] == board[1][j] && board[1][j] == board[2][j] && board[2][j] != ' ') {
return board[1][j];
}
}
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[2][2] != ' ') {
return board[1][1];
}
if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[2][0] != ' ') {
return board[1][1];
}
//如果棋盘满了,就是平局,is_full()函数是判断棋盘是否被占满的
if (is_full(board, row ,col) == 1) {
return 'Q';
}
return 'C';
}
game.h(头文件)
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define ROW 3
#define COL 3
void Initboard(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文件,也就是 函数主体文件!!!
**注意:本次解析的所有函数,都会在头文件 ** game.h 中声明。
在解析主体函数之前,我们先解析一下头文件
game.h
#define _CRT_SECURE_NO_WARNINGS 1
//这三个头文件是整个工程需要用到的
#include <stdio.h> //这是我们最熟悉的 输入输出函数。
#include <time.h>//这个函数表示用来得到系统的时间,本次主要用来配合 <stdlib.h>这个头文件来构成时间戳。
#include <stdlib.h>//主要用到其中的 rand函数 和srand函数,它们是随机数生成器。
#define ROW 3 //定义一个常量 ROW表示 行
#define COL 3 //定义一个常量 COL表示 列
//下面的这些函数都会在之后讲到
void Initboard(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);
test.c
在我们主文件的game()函数中定义一个二维数组。
void game() {
char board[ROW][COL] = { 0 };//定义二维数组,注意是使用 我们在头文件中引用的常量来定义 行和列 的,注意我们这里初始化的是 0 。
}
game.c
第一个函数:Initboard()
初始化棋盘坐标
初始化我们定义的二维数组board[][]
,将数组初始化为 空格 ' '
;
//控制所有 行和列 将数组中 0 都初始化成为 ' '
void Initboard(char board[ROW][COL], int row, int col) {
//在这个之中定义了变量 i 和 j 分别控制 行 和 列。
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
board[i][j] = ' '; // 将数组中 0 都初始化成为空格 ' '
}
}
}
第二个函数: displaybaord()
打印棋盘
这个函数是实现对于新手来说,有些难以设计,所以我们 分为两步解析 ;
第一步,先打印棋盘中的坐标位置
void displayboard(char board[ROW][COL] , int row,int col){
for(int i = 0; i < row; i++){
for(int j = 0 ; j < col ; j++){
printf(" %c ",board[i][j]);//注意 %c 的两边都有一个空格。
}
printf("\n");//每打印完一行后 换行。
}
}
第二步添加坐标位置之间的分割线
这也是完整版的棋盘了
void displayboard(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++)
{
printf(" %c ",board[i][j]);
//下面是新加的代码;列的分割线
if(j<col-1){ //因为每列之间才有分割线,所以必须是 j<col-1 这个条件
printf("|");
}
}
printf("\n");
//下面这些都是新加的代码,打印每行的分割线
if(i < row -1) // 因为只是每行之间有,所有条件是 i<row-1;
{
for( j= 0; j < col; j++) //打印分割线
{
printf("---");
if (j < col - 1) //注意:行的分割线也有 “|” ;
{
printf("|");
}
}
}
printf("\n");没打印一行后,换行;
}
}
第三个函数:player_move
玩家下棋
这个函数是控制玩家的坐标走向的,比如你输入2 2
它就会在第二行第二列下棋。
但我们的这个函数的 坐标范围是 3*3 ,毕竟只是一个三字棋,不是 1 ~ 3的数字就会报出我们写的提示。
void player_move(char board[ROW][COL] , int row ,int col){
int x = 0; //x轴坐标,也就是横坐标
int y = 0;//y轴坐标,也就是纵坐标
//设立循环, 如果没有正确输入 坐标值,就会一直循环。输入无误的坐标就嫩跳出循环。
while(1)
{
printf("玩家下棋:》 ");
scanf("%d %d",&x,&y);
if(x >= 1 && x <= row && y >= 1 && y <= 3)//限定x 和 y 的范围,在这个范围才会生效。
{ //数组的下标是0~2,所以我们的x 和 y 都减去 1 后就能和board[][]数组的下标重合.
if (board[x-1][y-1] == ' ') //如果我们输入的坐标没有被电脑下棋过,我们就可以用 '*' 替换,代表我方下棋。
{
board[x-1][y-1] = '*';
printf("%c",board[x-1][y-1]);
break;
}
else //如果我们输入的坐标,不是‘ ’,会说明别机器人下了。
{
printf("该坐标已被占用,请重新输入坐标");
}
}
else//输入的横纵坐标值大于或小于 我们限定的坐标值了。
{
printf("非法坐标");
}
}
}
第四个函数:computer_move()
电脑下棋
这个函数需要用到 库函数的<stdlib.h>
中的 rand
和srand
。rand
和 srand
是随机数生成器;还有<time.h>
中的 time
函数 ,time
函数 是用于 srand
函数中的时间戳。
void computer_move(char board[ROW][COL],int row,int col){
int x = 0;//电脑下棋的坐标,因为是电脑下棋,所以不用坐标-1。
int y = 0;
printf("电脑下棋 ")
while(1){//与上面的函数同理
x = rand()%row; //rand是随机数生成器,%row可以限制随机数生成的范围,取余row,也就是取余3,,所以随机数的生成范围就是0~2.
y = rand()%col;//%col可以限制随机数生成的范围,取余col,也就是取余3,,所以随机数的生成范围就是0~2.
if (board[x][y] == ' '){
board[x][y] = '#'; //电脑下棋的坐标就是 '#'
break; //如果电脑下棋成功,就退出循环
}
}
printf("\n");
}
第五个函数:in_win()
判断输赢
这个函数是用来判断输赢的,相同字符连成一排,或者一列,又或者对角线,则赢。
而我们可以用这个函数的返回值,在主函数中判断输赢,所以这个函数的返回值就不是void
了,而应该是char
如果返回值是 '*'
则玩家赢。
返回值是'#'
电脑赢
返回值是 'Q'
平局,在判断平局中我们也创造了一个变量is_full()
返回值是 'C'
继续下棋,棋盘未满,也还没人胜利。
int is_full(char board[ROW][COL],int row,int col){
for(int i = 0; i < row; i++){
for(int j = 0; j < col; j++){
if(board[i][j] == ' '){ //遍历整个二维数组,如果数组中有' ' 就返回0,说明棋盘没有被占满。
return 0;
}
}
}
return 1;//如果没有整个二维数组中没有了' ' ,就返回1,说明棋盘被被占满了。
}
char is_win(char board[ROW][COL] ,int row,int col){
for(int i = 0; i < row ; i++){
if(baord[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][2] != ' '){
return board[i][1]; //如果一行里面的字符相同,就返回这个字符
}
for (j = 0; j < col; j++) {
if (board[0][j] == board[1][j] && board[1][j] == board[2][j] && board[2][j] != ' ') {
return board[1][j];//如果一列里面的字符相同,就返回这个字符
}
}
if(board[0][0] == board[1][1] && baord[1][1] == board[2][2] && board[2][2] != ' '){
return board[1][1]; //对角线里面的字符相同,就返回那个字符
}
if(board[0][2] == board[1][1] && baord[1][1] == board[2][0] && board[2][0] != ' '){
return board[1][1];//对角线里面的字符相同,就返回那个字符
}
if(is_full(board[ROW][COL],row,col) == 1){//注意 is_win是个判断棋盘是否被全部占满的函数,但是,不用在game.h头文件中声明。
return 'Q'; //如果is_win返回的值是1,说明棋盘已经被占满,是平局。
}
return 'C';//返回值是 'C' 继续下棋,棋盘未满,也还没人胜利。
}
}
以上是我在game.c
中的所有函数解析,但是伙计们别忘了在game.h
中声明,不然在主函数文件中是用不了的。
虽然我们只是解析了game.c
中的函数,其他两个文件只是简略带过,但当你能看懂 game.c
的时候,就说明能看懂其他两个文件了。
结语
至此我们的三子棋解析也进入了尾声,可能代码中有些许错误,欢迎大家指正。
这个程序并不算难,但基本也是我们所写的第一个程序,本文主要是提供一些鄙人的拙见,如果能带给大家一点点帮助,是我莫大的荣幸。
很期待下次见面,祝大家万事胜意!!!