C语言小游戏:三子棋(N子棋)(从头创建思路,保姆级)【菜鸟必看】
前言:学习C语言一段时间后 大家一定迫不及待的想要使用所学的知识来编写一个相对完整的代码。三子棋就非常适合初学者,可以很好地锻炼动写代码的能力。初学者在编写三子棋时,会遇到很多的困难,但是会对函数,数组,实参形参,循环等知识理解更深刻。
思路设计:三子棋,顾名思义三子连珠及获胜,任一横向,任一纵向或对角线方向连成一线就获胜。因此想到用二维数组来存放“棋子”,同时,暂不考虑机器的“聪明程度”,采用随机值的方式落子。玩家通过输入坐标落子,同时打印棋盘,随后机器落子后也打印棋盘。
在整个设计过程中,一定要思路清晰,写一个函数就定义一个函数
首先打开Visual Studio
创建好项目后,建议分开存放相关代码
game.h(非最终版)
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#define ROW 3
#define COL 3
在game.h中,引用头文件和函数定义,并对ROW和COL进行宏定义
宏定义的目的是避免在后续编程中直接使用具体数字,未来若升级为n子棋时,只需要更改宏定义即可
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"//主要头文件都包含在game.h中了,因此仅引用它
//三子棋项目分为
//test.c程序基础逻辑,main函数调用test()实现
//game.c游戏具体实现逻辑,存放各种函数定义
//game.h存放函数的申明
void test()
{
int input = 0;
//进入test函数后,应有一循环,显示菜单,输入1或0,1进入游戏game(),0退出游戏
do
{
menu();//打印菜单
scanf("%d", &input);//此时应有三种情况,1、0、或其他
switch (input)
{
case 1:printf("开始游戏\n");
game();//第一次出现game()函数,由此去设计game函数
break;
case 0:printf("退出游戏\n");
break;
default:printf("输入错误\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
-
由于主要头文件都包含在“game.h”中了,因此仅引用它,注意,自己设置的头文件应使用双引号引用
-
在main函数中,调用test()函数,避免将代码堆在main函数中
-
设计一个0/1选择,选择0时将自动退出,选择1则进入game() ,由此开始设计game() 函数
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void game()
{ //数据存储到一个字符的二维数组中,玩家下棋是'*',电脑下棋是'#'
char board[ROW][COL] = { 0 };//这里不填写[3][3],而使用宏定义ROW和COL,便于后期修改
//初始化棋盘(数组存放空格)
InitBoard(board, ROW, COL);
//打印棋盘
DisplayBoard(board, ROW, COL);
//下棋
//下棋应是循环,并定义玩家和电脑下棋的函数
int ret = 0;
while (1)//循环中还有一个逻辑,即
{
player_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);//操作后,应该打印棋盘状态
//判赢操作,调用is_win函数,返回的字符是赢家的棋子字符
if (is_win(board, ROW, COL) == '*')
{
printf("玩家胜利!\n");
break;
}
else if (is_win(board, ROW, COL) == 'Q')
{
printf("平局!\n");
break;
}
computer_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
if (is_win(board, ROW, COL) == '#')
{
printf("电脑胜利!\n");
break;
}
else if (is_win(board, ROW, COL) == 'Q')
{
printf("平局!\n");
break;
}
}
}
game()函数是整个三子棋系统最为核心的部分,也是整个游戏逻辑的体现。在这一步中,我们通过定义不同的函数实现功能,在需要时引用,避免了代码堆积,更容易看懂。同时,对变量、函数进行命名时应尽量见名知意,给代码进行注释也是程序员良好的习惯,未来无论是自己还是别人,回来修改或查看都会方便很多。
首先,对二维数组进行了定义和初始化。下棋是一个玩家和电脑分别操作一次的过程,因此想到用循环,在棋子达到可以判赢的条件时则跳出循环。首先玩家进行下棋动作,打印棋盘状态,接着进行判赢操作,然后机器进行下棋操作,打印棋盘状态,再次判赢。一旦一方胜利就退出循环,游戏结束。
至此,涉及的动作(函数)有:(注意,函数类型的定义各有不同)
game.h(非最终版)
void menu();//打印菜单界面
void game();
void InitBoard(char board[ROW][COL], int row, int col);//初始化棋盘(数组存放空格)
void DisplayBoard(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);//判赢操作,
function.c
至此,整个项目的基本框架已搭好,接下来在function.c中详细完成所定义的功能(函数)
在日常使用VS中,可以通过“鼠标右键—大纲显示—折叠到定义”折叠我们的代码,通过左侧的加号按钮展开。
menu()
void menu()
{
printf("****************************\n");
printf("********* 请输入1/0 ********\n");
printf("****************************\n");
printf("********** 1.play **********\n");
printf("********** 0.exit **********\n");
printf("****************************\n");
}
打印菜单函数
InitBoard(board,ROW,COL)
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] = ' ';
}
}
}
初始化棋盘,对二维数组元素都赋值为空格
DisplayBoard(board,ROW,COL)
void DisplayBoard(char board[ROW][COL], int row, int rol)
{
//考虑到第三格个数据之后不需要打印"|",因此想到在循环中设置一个if语句
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
printf(" %c ", board[i][j]);
if (j < COL - 1)
printf("|");
}printf("\n");
if (i < ROW - 1)
{
for (int j = 0; j < COL; j++)
{
printf("---");
if (j < COL - 1)
printf("|");
}printf("\n");
}
}printf("\n");
}
想要达到这样的效果,我们在打印元素时,打印“ %c ",每打印一次打印一条竖线,并设置if语句,将竖线的打印次数卡在ROW-1次
player_move(board,ROW,COL)
void player_move(char board[ROW][COL], int row, int col)
{
//操作的逻辑是:输入一个坐标,首先检验坐标的合法性,如果为空,则赋值为*,同时需要考虑到坐标被占用
printf("请输入一个坐标,用空格间隔->");
int x = 0, 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 if (board[x - 1][y - 1] != ' ')
printf("该位置已有棋子!\n请重新输入->"); continue;
}
else
{
printf("输入坐标非法!\n"); continue;
}
}
}
该步骤操作的逻辑是:输入一个坐标,首先检验坐标的合法性,如果为空,则赋值为*,同时需要考虑到坐标被占用
computer_move(board,ROW,COL)
void computer_move(char board[ROW][COL], int row, int col)
{
//在合法的随机位置下棋,用#表示
printf("机器下棋->\n");
//由于随机数第一次不一定就能合法,因此设置一个循环
int x = 0, y = 0;
while (1)
{
x = rand() % row;//0-2
y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = '#'; break;
}
}
}
机器下棋,这里设置一个随机数,由于随机数产生的坐标不一定一次就能符合规则,因此设置一个循环
row_same(board,ROW,COL)
int row_same(char board[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
int count_p = 1; int count_c = 1;
for (int j = 0; j < col - 1; j++)
{
if (board[i][j] == board[i ][j+1] && board[i][j] == '*')
count_p++;
if (board[i][j] == board[i ][j+1] && board[i][j] == '#')
count_c++;
}if (count_p == row) return 1;
if (count_c == row) return 2;
}
return 0;
}
判断是否有一行相同。笔者一开始没有设置count,而是直接返回一个数组元素,发现只要最后两个元素相同,系统就判赢了。为了把最后两次判断之前的判断也算进来,设置了一个count。
同时要注意的是,调用函数时,所有的情况应有返回值,否则编译器会报错,这也是笔者设置return 0的原因。
col_same(board,ROW,COL)
int col_same(char board[ROW][COL], int row, int col)
{
for (int j = 0; j < col; j++)
{
int count_p = 1; int count_c = 1;
for (int i = 0; i < row - 1; i++)
{
if (board[i][j] == board[i + 1][j] && board[i][j] == '*')
count_p++;
if (board[i][j] == board[i + 1][j] && board[i][j] == '#')
count_c++;
}if (count_p == row) return 1;
if (count_c == row) return 2;
}
return 0;
}
判断是否有一列相同,与row_same函数类似
is_full(board,ROW,COL)
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;
}
判满函数,只要有一个元素为空格及说明没有满
diagonal_same1(board,ROW,COL)
int diagonal_same1(char board[ROW][COL], int row, int col)
{
int count_p = 1; int count_c = 1;
for (int i = 0; i < row - 1; i++)
{
if (board[i][i] == board[i + 1][i+1] && board[i][i] == '*')
count_p++;
if (board[i][i] == board[i + 1][i+1] && board[i][i] == '#')
count_c++;
}if (count_p == row) return 1;
if (count_c == row) return 2;
return 0;//必须要设置一个其他情况的返回值,否则会报错
}
捺对角线的判断函数,思路也类似于上面两个函数
diagonal_same2(board,ROW,COL)
int diagonal_same2(char board[ROW][COL], int row, int col)
{
int count_p = 1; int count_c = 1;
for (int i = row-1; i > 0; i--)
{
if (board[i][row -1- i] == board[i - 1][row - i ] && board[i][row -1- i] == '*')
count_p++;
if (board[i][row -1- i] == board[i - 1][row - i] && board[i][row -1- i] == '#')
count_c++;
}if (count_p == row) return 1;
if (count_c == row) return 2;
return 0;
}
撇对角线的判断函数,与捺的判断略有不同。我们发现,捺对角线上的行与列的数字加起来刚好为棋盘的维度+1。例如三子棋中,(3,1),(2,2),(1,3)加起来都是4。但是要注意的是,数组的坐标是从0开始的,因此行与列加起来刚好是维度的值。
is_win(board,ROW,COL)
char is_win(char board[ROW][COL], int row, int col)
{
//一行相同
int a = 0;
a = row_same(board, row, col);
if (a == 1)
return '*';
if (a == 2)
return '#';
//一列相同
int b = 0;
b = col_same(board, row, col);
if (b == 1)
return '*';
if (b == 2)
return '#';
//对角线相同 捺
int c1 = 0;
c1 = diagonal_same1(board, row, col);
if (c1 == 1)
return '*';
if (c1 == 2)
return '#';
//对角线相同 撇
int c2 = 0;
c2 = diagonal_same2(board, row, col);
if (c2 == 1)
return '*';
if (c2 == 2)
return '#';
//平局
if (1 == is_full(board, row, col))
return 'Q';
return 0;
}
把上述的所有判断函数综合起来,组成了is_win函数,通过if函数对各个函数的返回值进行判断,将"*“或”#"返回给game()函数
game.h
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#define ROW 3
#define COL 3
void menu();
void game();
void InitBoard(char board[ROW][COL], int row, int col);
void DisplayBoard(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);
int row_same(char board[ROW][COL], int row, int col);
int col_same(char board[ROW][COL], int row, int col);
int is_full(char board[ROW][COL], int row, int col);
int diagonal_same1(char board[ROW][COL], int row, int col);
int diagonal_same2(char board[ROW][COL], int row, int col);
char is_win(char board[ROW][COL], int row, int col);
完整的game.h头文件
运行
输入1进入游戏,输入坐标与电脑下棋,完美运行
由于整个游戏都对变量进行了充分抽象,因此可以轻松的改为n子棋
小结
三子棋小游戏至此设计完成了,已基本实现了功能。本文仅是作者的思路,还有很多不足之处。本代码的缺点是电脑的落子位置是随机值,是“人工智障”,后期更新版本时可以设置相关算法使其更“聪明”。小白在第一次尝试这么长的代码时还是比较痛苦的,但是如果能凭借思路完整的自己敲出来,收获还是非常大的,尤其是对函数调用,循环,数组等知识理解会更清晰。但是也要清醒得认识到,三子棋代码依然是很简单的函数组合,与未来工作比还有很远的距离,但是只要坚持学习,坚持敲代码,终能达彼岸。
一起加油呀~