【C/C++】题解:三子棋延伸为多子棋
导读
-
该篇文章将会从三子棋延伸到多子棋,介绍如何实现一个多子棋程序,并希望可以体会到如何去设计一个简单的程序,如何去抽象出每个函数模块,掌握编写函数的方法论; -
文章为平常复杂的判断输赢提供了新的思路,相信大家也有更好的方法,欢迎大家可以提出; -
阅读完这篇文章后,更希望读者也可以在无参考的环境下,完成思考并且实现该程序,最后还可以分享更多的新想法,这样会让每个人更有收获;
一. 引入
-
程序要求
三子棋/多子棋:在命令行完成游戏菜单打印,棋盘打印并且进行游戏三个过程
- 菜单栏需要提供玩游戏与退出两个选项,并赋予相应提示信息;
- 棋盘打印需要达到打印工整,棋盘整齐的网格效果;
- 游戏过程是玩家先下,电脑后下,一方成功打印胜负信息;
-
抽象
在这里理解的抽象是指让每个函数尽可能的独立完成某个特定的子功能,这样的独立性高,减少各个模块之间的影响。这些函数可以单独拿出来使用,
二. 程序设计
? 作者一般在写一道稍微复杂的程序时会先通过流程图梳理一遍,希望大家与我一同分析:
? 首先从命题出发,第一步就是打印菜单栏目,该步骤直接打印即可,当选择进入游戏后输入相关信息就可以开始游戏,如果选择退出则直接退出程序,当输入错误信息时就需要重新输入选择信息。由于是三个分支,所以我们在这里考虑用 switch 语句,而由于不是只玩一把,所以通过一个循环语句将其镶嵌。
? 其次是棋盘打印,我们想想一个棋盘用什么数据结构储存,棋盘是一个二维结构,所以我们可以用二维数组来完成储存它,而棋盘的打印我们可以通过两层的for循环完成遍历,并且添加棋盘边界,使得棋盘更加工整
? 最后是游戏环节,我们进行玩家下棋 ——>判断输赢 ——>电脑下棋 ——> 判断输赢 的循环流程,当一方胜利或者是棋盘已满就退出循环。
? 将其三个主要步骤分别写出,再进行组合,得到以下流程图:
三. 书写代码
? 通过流程图完成书写:
-
完成菜单打印及其程序逻辑 菜单打印,直接输出即可 void menu() {
printf("**********菜单**********\n");
printf("***** 1.PLAY *****\n");
printf("***** 0.EXIT *****\n");
printf("***** 请输入选项 *****\n");
printf("************************\n");
}
而在选择菜单的主体部分我们通过switch来完成 menu();
scanf("%d", &choose);
switch (choose)
{
case 1:
game(chess_board,num_chess,size_board);
case 0:
printf("****** 游戏结束 ******");
break;
default:
printf("**** 输入错误,请输入正确选项\n");
break;
}
? 最后我们将循环条件加上,要注意的是,在这里引用了choose作为参数,默认为1,这样可以直接进入循环,而在后面对choose的输入,是用来判断玩家所做选择,从而达到命题的目的、 int choose = 1;
while (choose)
{
menu();
scanf("%d", &choose);
switch (choose)
{
case 1:
game(chess_board,num_chess,size_board);
case 0:
printf("****** 游戏结束 ******");
break;
default:
printf("**** 输入错误,请输入正确选项\n");
break;
}
}
-
实现对于棋盘的打印 要实现对棋盘的打印,我们需要建立棋盘,并完成对棋盘的初始化,在上文已经分析了使用二维数组建立,可是如何完成动态的多子棋的棋盘建立呢,再次我们选择了 malloc 来动态的申请空间,代码如下
char **chess_board;
int size_board = 0;
chess_board = (int**)malloc(sizeof(int*) * size_board);
for (int i = 0; i < size_board; i++)
chess_board[i] = (int*)malloc(sizeof(int) * size_board);
void init_board(char** chessboard,int size_board) {
for (int i = 0; i < size_board; i++){
for (int j = 0; j < size_board; j++) {
chessboard[i][j] = ' ';
}
}
}
for (int i = 0; i < size_board; i++)
free(chess_board[i]);
free(chess_board);
break;
至于棋盘的打印,我们采取简单的添加边界与二维数组元素输出作为棋盘的打印,效果图如下: 要实现此效果,我们只需在每遍历到最后一列时不添加竖线,在每遍历最后一行时不打印横线即可,具体代码如下: void displa_board(char** chessboard, int size_board)
{
for (int i = 0; i < size_board; i++) {
for (int j = 0; j < size_board; j++) {
printf(" %c ", chessboard[i][j]);
if (j < size_board - 1) {
printf("|");
}
}
printf("\n");
if (i < size_board - 1) {
for (int j = 0; j < size_board; j++) {
printf("----");
}
printf("\n");
}
}
printf("\n");
}
-
游戏进行 为了更好的游戏体验,我们在玩家电脑下棋后,需要将棋盘展示出来,最后游戏分出胜负或者是棋盘已满的时候,也将棋盘展示出来,过程非常简单,根据上文,我们可以写出循环程序主体的代码 int x = 0;
int y = 0;
printf("\n****玩家回合,请输入你的坐标:\n");
scanf("%d%d", &x, &y);
play_player(chessboard, &x, &y, '*',size_board);
displa_board(chessboard, size_board);
if (is_win(chessboard, '*', num_chess, x, y,size_board)) {
printf("\n");
printf("******Win!******\n");
break;
}
printf("\n****电脑回合,自动生成坐标\n");
play_computer(chessboard, size_board, &x, &y);
displa_board(chessboard, size_board);
if (is_win(chessboard, '#', num_chess, x, y, size_board)) {
printf("\n");
printf("******Lose!******\n");
break;
}
最后我们加入循环结构,这次通过for循环,因为我们可以通过棋盘大小来判断下棋的次数,代码如下: void game(char** chessboard, int num_chess, int size_board)
{
printf("******游戏开始!******\n");
init_board(chessboard,size_board);
displa_board(chessboard, size_board);
int i = 0;
for (i = 0; i < size_board* size_board; i++) {
int x = 0;
int y = 0;
printf("\n****玩家回合,请输入你的坐标:******\n");
scanf("%d%d", &x, &y);
play_player(chessboard, &x, &y, '*',size_board);
displa_board(chessboard, size_board);
if (is_win(chessboard, '*', num_chess, x, y,size_board)) {
printf("\n");
printf("******Win!******\n");
break;
}
printf("\n****电脑回合,自动生成坐标******\n");
play_computer(chessboard, size_board, &x, &y);
displa_board(chessboard, size_board);
if (is_win(chessboard, '#', num_chess, x, y, size_board)) {
printf("\n");
printf("******Lose!******\n");
break;
}
}
if (i == size_board * size_board)
printf("****平手****!\n");
displa_board(chessboard, size_board);
}
如此游戏进行的大体框架已经完成,现在对循环体中的各个函数实现,这里我们就需要对需求去设计函数 -
各函数实现 函数的设计一般包括主体的实现部分,还需要进行非法检测 关于玩家下棋部分,我们一般只需将坐标输入后,将棋盘完成填子即可,可是我们还要进行完备性检验,此处体现在两方面,一是是否越界,二是是否坐标已有棋子,因此我们知道函数参数需要坐标、棋盘和边界三部分,根据思路我们可以写出代码: void play_player(char** chessboard, int *x, int *y, char chess, int size_board){
while (1) {
if ((*x) - 1 < 0 || (*y) - 1 < 0 || (*x) > size_board || (*y) > size_board) {
printf("越界了,请重新输入\n");
printf("玩家回合\n");
scanf("%d%d", x, y);
}
else if (chessboard[*x - 1][*y - 1] == '*' || chessboard[*x - 1][*y - 1] == '#') {
printf("已经有棋子,请重新输入\n");
printf("玩家回合\n");
scanf("%d%d", x, y);
}
else {
chessboard[*x - 1][*y - 1] = chess;
break;
}
}
}
关于电脑下棋部分,我们需要要让电脑自动完成下棋,我们就需要生成一个随机数,因此就要用来random(注意包含头文件stdlib.h以及引入时间作为种子),而这个随机值需要边界,因此我们需要增加一个边界的参数,所以根据思路,我们可以设计出以下代码: void play_computer(char** chessboard, int size_board,int *px,int *py){
while (1)
{
int x = rand() % size_board;
int y = rand() % size_board;
if (chessboard[x][y] == ' ') {
chessboard[x][y] = '#';
(*px) = x+1;
(*py) = y+1;
break;
}
else
continue;
}
}
最后我们来讨论一下判断输赢的函数,在很多地方我们看到了通过遍历检验来完成该函数的,而在这我希望提出新的想法。 仔细思考,如果一方胜利,那下的最后一步的棋子,一定在完成连线的那一条线之中,所以我想通过最后一步下的棋子来判断是否胜利,主要做法是统计四个方向的相连棋子数,如果有一个方向棋子数大于要求的棋子数,那就判断胜,否则一个方向也不达要求就判断为没有胜利,继续下棋。而边界条件这是需要在棋盘内,所以我们引入参数的时候需要把棋盘大小引入。不仅如此,我们还需要考虑的是判断什么 棋子的输赢,所以还要输入棋子信息。最后整理一下,就是根据棋子类型,通过最后一步统计个方位相连的棋子数,来判断输赢(胜利返回1,否则返回0),代码细节如下(注意一定要判断边界条件): int is_win(char** chessboard, char flag,int num_chess,int x,int y,int size_board){
int x_real = x - 1;
int y_real = y - 1;
int Row_count = 1;
while ( x_real -1 >= 0 && chessboard[x_real - 1][y_real] == flag) {
Row_count++;
x_real = x_real - 1;
}
x_real = x - 1;
y_real = y - 1;
while ( x_real + 1 <= size_board -1 && chessboard[x_real + 1][y_real] == flag ) {
Row_count++;
x_real = x_real + 1;
}
if (Row_count >= num_chess )
return 1;
int Column_count = 1;
x_real = x - 1;
y_real = y - 1;
while (y_real -1 >= 0 && chessboard[x_real][y_real - 1] == flag ) {
Column_count++;
y_real = y_real - 1;
}
x_real = x - 1;
y_real = y - 1;
while ( y_real + 1 <= size_board - 1 && chessboard[x_real][y_real + 1] == flag ) {
Column_count++;
y_real = y_real + 1;
}
if (Column_count >= num_chess)
return 1;
int Left_oblique_count = 1;
x_real = x - 1;
y_real = y - 1;
while (x_real - 1 >= 0 && y_real - 1 >= 0 && chessboard[x_real - 1][y_real - 1] == flag ) {
Left_oblique_count++;
y_real = y_real - 1;
x_real = x_real - 1;
}
x_real = x - 1;
y_real = y - 1;
while (x_real + 1 <= size_board - 1 && y_real + 1 <= size_board - 1 && chessboard[x_real+1][y_real + 1] == flag ) {
Left_oblique_count++;
y_real = y_real + 1;
x_real = x_real + 1;
}
if (Left_oblique_count >= num_chess)
return 1;
int Right_oblique_count = 1;
x_real = x - 1;
y_real = y - 1;
while (x_real + 1 <= size_board - 1 && y_real - 1 >= 0 && chessboard[x_real + 1][y_real - 1] == flag) {
Right_oblique_count++;
y_real = y_real - 1;
x_real = x_real + 1;
}
x_real = x - 1;
y_real = y - 1;
while (x_real - 1 >= 0 && y_real +1 <= size_board - 1 && chessboard[x_real-1][y_real + 1] == flag) {
Right_oblique_count++;
y_real = y_real + 1;
x_real = x_real - 1;
}
if (Right_oblique_count >= num_chess)
return 1;
return 0;
}
-
框架优化 在上面的部分,我们已经把所有函数实现给整理清晰,不过在C语言中,我们还可以对这个程序的框架进行优化整理,而不是像平时练习时那样,将所有代码堆叠在一起。框架的优化我们就需要新建头文件和源文件,头文件放入各个函数的定义,而源文件放入函数的实现,这样的分类有助于我们以后分模块开发,没人负责一部分,最后用引入的方式整合起来。 在此,我们也通过这种方式进行框架优化,具体框架如下:
- nRowChess.c 文件:作为主函数的源文件,包括实现程序逻辑进行的主题过程;
- game.h 与 game.c 文件:作为游戏内容的文件储存,包括实现游戏过程的函数;
-
源码呈现 最后将完整代码进行整合展示:
define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
#include<stdlib.h>
void menu() {
printf("**********菜单**********\n");
printf("***** 1.PLAY *****\n");
printf("***** 0.EXIT *****\n");
printf("***** 请输入选项 *****\n");
printf("************************\n");
}
void game(char** chessboard, int num_chess, int size_board)
{
printf("******游戏开始******!\n");
init_board(chessboard,size_board);
displa_board(chessboard, size_board);
int i = 0;
for (i = 0; i < size_board* size_board; i++) {
int x = 0;
int y = 0;
printf("\n****玩家回合,请输入你的坐标:\n");
scanf("%d%d", &x, &y);
play_player(chessboard, &x, &y, '*',size_board);
displa_board(chessboard, size_board);
if (is_win(chessboard, '*', num_chess, x, y,size_board)) {
printf("\n");
printf("******Win!******\n");
break;
}
printf("\n****电脑回合,自动生成坐标\n");
play_computer(chessboard, size_board, &x, &y);
displa_board(chessboard, size_board);
if (is_win(chessboard, '#', num_chess, x, y, size_board)) {
printf("\n");
printf("******Lose!******\n");
break;
}
}
if (i == size_board * size_board)
printf("****平手****!\n");
displa_board(chessboard, size_board);
}
int main() {
srand((unsigned int)time(NULL));
int choose = 1;
char **chess_board;
int num_chess = 0;
int size_board = 0;
while (choose)
{
menu();
scanf("%d", &choose);
switch (choose)
{
case 1:
printf("**** 请输入要玩n子棋:====》");
scanf("%d", &num_chess);
printf("**** 请输入棋盘大小: ====》");
scanf("%d", &size_board);
while (num_chess>size_board){
printf("请输入更大的棋盘\n");
printf("**** 请输入要玩n子棋:====》");
scanf("%d", &num_chess);
printf("**** 请输入棋盘大小: ====》");
scanf("%d", &size_board);
}
chess_board = (int**)malloc(sizeof(int*) * size_board);
for (int i = 0; i < size_board; i++)
chess_board[i] = (int*)malloc(sizeof(int) * size_board);
game(chess_board,num_chess,size_board);
for (int i = 0; i < size_board; i++)
free(chess_board[i]);
free(chess_board);
break;
case 0:
printf("****** 游戏结束 ******");
break;
default:
printf("**** 输入错误,请输入正确选项\n");
break;
}
}
return 0;
}
#pragma once
#include<stdio.h>
#include <time.h>
void init_board(char** chessboard, int size_board);
void displa_board(char** chessboard, int size_board);
void play_player(char** chessboard,int *x,int *y,char chess, int size_board);
int is_win(char** chessboard,char flag,int num_chess,int x ,int y,int size_board);
void play_computer(char** chessboard, int size_board,int *x,int *y);
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void init_board(char** chessboard,int size_board) {
for (int i = 0; i < size_board; i++){
for (int j = 0; j < size_board; j++) {
chessboard[i][j] = ' ';
}
}
}
void displa_board(char** chessboard, int size_board)
{
for (int i = 0; i < size_board; i++) {
for (int j = 0; j < size_board; j++) {
printf(" %c ", chessboard[i][j]);
if (j < size_board - 1) {
printf("|");
}
}
printf("\n");
if (i < size_board - 1) {
for (int j = 0; j < size_board; j++) {
printf("----");
}
printf("\n");
}
}
printf("\n");
}
void play_player(char** chessboard, int *x, int *y, char chess, int size_board){
while (1) {
if ((*x) - 1 < 0 || (*y) - 1 < 0 || (*x) > size_board || (*y) > size_board) {
printf("越界了,请重新输入\n");
printf("玩家回合\n");
scanf("%d%d", x, y);
}
else if (chessboard[*x - 1][*y - 1] == '*' || chessboard[*x - 1][*y - 1] == '#') {
printf("已经有棋子,请重新输入\n");
printf("玩家回合\n");
scanf("%d%d", x, y);
}
else {
chessboard[*x - 1][*y - 1] = chess;
break;
}
}
}
int is_win(char** chessboard, char flag,int num_chess,int x,int y,int size_board){
int x_real = x - 1;
int y_real = y - 1;
int Row_count = 1;
while ( x_real -1 >= 0 && chessboard[x_real - 1][y_real] == flag) {
Row_count++;
x_real = x_real - 1;
}
x_real = x - 1;
y_real = y - 1;
while ( x_real + 1 <= size_board -1 && chessboard[x_real + 1][y_real] == flag ) {
Row_count++;
x_real = x_real + 1;
}
if (Row_count >= num_chess )
return 1;
int Column_count = 1;
x_real = x - 1;
y_real = y - 1;
while (y_real -1 >= 0 && chessboard[x_real][y_real - 1] == flag ) {
Column_count++;
y_real = y_real - 1;
}
x_real = x - 1;
y_real = y - 1;
while ( y_real + 1 <= size_board - 1 && chessboard[x_real][y_real + 1] == flag ) {
Column_count++;
y_real = y_real + 1;
}
if (Column_count >= num_chess)
return 1;
int Left_oblique_count = 1;
x_real = x - 1;
y_real = y - 1;
while (x_real - 1 >= 0 && y_real - 1 >= 0 && chessboard[x_real - 1][y_real - 1] == flag ) {
Left_oblique_count++;
y_real = y_real - 1;
x_real = x_real - 1;
}
x_real = x - 1;
y_real = y - 1;
while (x_real + 1 <= size_board - 1 && y_real + 1 <= size_board - 1 && chessboard[x_real+1][y_real + 1] == flag ) {
Left_oblique_count++;
y_real = y_real + 1;
x_real = x_real + 1;
}
if (Left_oblique_count >= num_chess)
return 1;
int Right_oblique_count = 1;
x_real = x - 1;
y_real = y - 1;
while (x_real + 1 <= size_board - 1 && y_real - 1 >= 0 && chessboard[x_real + 1][y_real - 1] == flag) {
Right_oblique_count++;
y_real = y_real - 1;
x_real = x_real + 1;
}
x_real = x - 1;
y_real = y - 1;
while (x_real - 1 >= 0 && y_real +1 <= size_board - 1 && chessboard[x_real-1][y_real + 1] == flag) {
Right_oblique_count++;
y_real = y_real + 1;
x_real = x_real - 1;
}
if (Right_oblique_count >= num_chess)
return 1;
return 0;
}
void play_computer(char** chessboard, int size_board,int *px,int *py){
while (1)
{
int x = rand() % size_board;
int y = rand() % size_board;
if (chessboard[x][y] == ' ') {
chessboard[x][y] = '#';
(*px) = x+1;
(*py) = y+1;
break;
}
else
continue;
}
}
四. 总结
-
程序设计的方法 笔者认为,在写程序前对程序进行分析是非常重要的,这样做可以使思路更清晰的书写程序,同时写出来的程序更加规范逻辑清晰。而流程图是我们非常好用的工具,可以通过需求分析来绘制流程图,注意循环,条件等语句之间的逻辑关系,最后书写代码也会变得得心应手。 -
函数的书写 可以从函数的结构来出发,完成分析设计
- 根据需求确定函数要完成什么功能,需要什么参数,确定返回类型
- 进行函数主体设计
- 完备性检验
-
代码细节 void play_player(char** chessboard,int *x,int *y,char chess, int size_board);
void play_computer(char** chessboard, int size_board,int *x,int *y);
在实现下棋的过程中,大家是否考虑过我们为什么用取地址的方式作为参数,原因是在输入坐标的过程中,其实输入的坐标并不应当合法,所以在函数体中,会对其进行修改,为了保留修改的参数,就需要引用地址来传递参数。 -
代码思考 int is_win(char** chessboard,char flag,int num_chess,int x ,int y,int size_board);
查看上文可发现,这段代码是篇幅较长的,而其中重复内容也多,试想是否可以对此那些重复的过程进行再次优化,从而写出给为抽象的代码呢?或者说,是否可以对该思路有更好的实现方法呢?希望读者可以做出思考并分享到评论区中。 -
程序不足 在实现电脑下棋的时候只是进行了简单的随机取坐标下棋,对此完全可以进行优化,欢迎大家评论提出自己的新想法。
声明:
- 作者写博客的C/C++内容主要是想形成一套实用的使用手册,主要追求的方便,以及记录一下自己在重新看C和C++内容的思考,所以常规文章一般都比较追求实用性,深层次内容是不及各位大神所写的博客那样深刻清晰;
- 欢迎各位在评论区中指正指导,非常感谢;
- 更新也已经提上日程,不过因为时间关系,可能无法写出完整完全构建C/C++的内容,因此以后主要会分享一些新颖的、总结性或让自己有收获的内容给大家。
- C/C++的代码将会放到: https://gitee.com/liu-hongtao-1/c–c–review.git ,欢迎查看!
|