本文介绍C语言学习阶段的经典项目:三子棋(N子棋)。
文章主要以代码的形式呈现,并附上必要的说明(说明主要以代码注释的风格呈现),力求正确、简洁、清晰。
前言
三子棋游戏设计的核心是对二维数组的把握和运用。
本文分步骤呈现三子棋(N子棋)游戏程序设计代码,以介绍与解析为主。文末另附一份压缩文件,为C语言三子棋课设实验报告(博主本人在学校写的,当初选择了三子棋作为课设项目),需要用作课设参考的同学可以直接下载现成的。希望多诸位读者有所帮助。
**解压缩的文件夹及其内容预告
\C语言课设作业--三子棋
\C语言课设作业--三子棋\课设大作业
此外,本文也对原有的最简单的三子棋游戏进行了优化,增加了电脑“会堵棋”的代码版本以及 N 子棋的输赢判断实现。希望对大家开拓思路有所帮助。
一、游戏简要介绍 -- 三子棋
规则
游戏模式为人机对决。玩家在主菜单界面选择是否要开始一盘游戏。当游戏开始后,由玩家这一方先开局下棋。通过坐标输入的方式将棋子放入玩家想要下的位置。玩家落子后电脑方立刻下棋。双方轮流下棋,直到有一方有3颗棋子连成一线,则率先达到3子连线的一方获得胜利。然后再由玩家决定是否要再开一把游戏。
功能实现
- 在菜单界面玩家可选择开始游戏或退出游戏。
- 创建一个新的棋盘,并初始化。
- 将棋盘打印在屏幕上。
- 玩家先开局,通过输入行列坐标的方式来落子,用×表示玩家落子。
- 玩家落子后轮到电脑落子,电脑在棋盘随机位置落子,用〇表示电脑落子。
- 判定胜负,输或赢或和棋,用q表示和棋。率先连成3子的一方获胜。
- 回到步骤1,循环以上步骤。
二、代码呈现 -- 按功能实现函数接口
我们以以上的游戏的功能实现为线索,进行代码编写。下列代码的实验环境为Visual Studio 2022
1. 在菜单界面玩家可选择开始游戏或退出游戏。
void menu() { printf("***************************\n"); printf(" 1. play \n"); printf(" 0. exit \n"); printf("***************************\n"); }
2. 创建一个新的棋盘,并初始化。
//初始化棋盘--将二维数组每个元素赋值为空格 //空格:可以仅占位而不显示,展示出“空白棋盘”的效果 void init_board(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] = ' '; } } }
3. 将棋盘打印在屏幕上。
静态打印棋盘
void print_chessBoard(char chessBoard[ROW][COL]) { printf("棋盘打印\n"); printf("+(0)+(1)+(2)+\n"); printf("+---+---+---+\n"); for (int row = 0; row < ROW; row++) { printf("| %c | %c | %c |(%d)\n", chessBoard[row][0],chessBoard[row][1], chessBoard[row][2],row); printf("+---+---+---+\n"); } }
有些人认为静态打印不如动态打印好,因为棋盘是固定的。但我个人觉得差不太多,因为动态打印棋盘则必须搭配动态判断输赢的办法,而现在能搜到的大部分三子棋代码并不配备动态判断输赢的算法,依旧是以类似于结果枚举的静态效果来判断输赢的。仅仅对于简单的3×3棋盘的三子棋而言,静态也完全够用。
但动态打印的逻辑是非常重要的,它需要同学们对二维数组的使用有清晰的思路和正确的把控,要是把握不好了会打印出歪瓜裂枣。因此动态打印思路也是一定需要介绍的。
动态判断输赢在文章偏后部分进行说明。
动态打印棋盘
void show_board(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) { //printf("---|---|---\n"); for (j = 0; j < col; j++) { printf("---"); if(j<col-1) printf("|"); } printf("\n"); } } }
说明:该处代码较长。如何动态打印棋盘是一个难点。在思考逻辑时,建议大家先以“行”为单位思考每一行内代码要做什么事,实现什么效果;再同样以列为单位进行思考。
注意其中标注“打印格子列”“打印横线列”的代码。此处的思路如下:
1. 打印时,每一行要做哪些事?
——打印二维数组数据,打印分隔竖线"|";打印分隔横线。
2. 数据格子和横线在行中交替出现,怎么解决?
——要做到一行打印格子、一行打印横线交替,不太方便实现,可以转变思路
——将一个格子与它下面的横线看作同一行,如下图是三行,每一行(除最后一行外)包括一层空白格子和一层横线。(一层在上、一层在下用'\n'实现即可)
3. 最后一行没有横线,最后一列也没有竖线,怎么办?
——用if语句控制,只在最后一行和最后一列之前打印横线和竖线。
4. 我想不到 / 想不清楚怎么办?
——此处运用到的思路详述在另一篇文章中,链接如下:
“盒子思路”解决循环嵌套
另一个版本的棋盘打印代码如下,也许会更贴合上述逻辑(其实整合后,就是上面所贴出的代码):
//打印棋盘 void show_board(char board[ROW][COL], int row, int col) { //打印行 for (int i = 0; i < row; i++) { if (i < row - 1) { //打印格子列 for (int j = 0; j < col; j++) { if (j < col - 1) { printf(" %c ", board[i][j]); printf("|"); } else { printf(" %c ", board[i][j]); } } printf("\n"); //打印横线列 for (int j = 0; j < col; j++) { if (j < col - 1) { printf("---"); printf("|"); } else { printf("---"); } } printf("\n"); } else { for (int j = 0; j < col; j++) { if (j < col - 1) { printf(" %c ", board[i][j]); printf("|"); } else { printf(" %c ", board[i][j]); } } printf("\n"); } } }
棋盘效果展示
5*5
10*10
更改用宏定义:
#define ROW 10 #define COL 10
变长数组应该也可以?但我没试过,因为vs 2022仍不支持C99。
4. 玩家先开局,通过输入行列坐标的方式来落子,用 '*' 表示玩家落子。
void player_move(char board[ROW][COL], int row, int col) { int x = 0; int y = 0; printf("玩家下棋:>\n"); while (1) { printf("请输入要下棋的坐标:>"); scanf("%d %d", &x, &y); //1.坐标的合法性 //2.坐标是否被占用 if (x >= 1 && x <= row && y >= 1 && y <= col) //用x而不用x-1:默认玩家玩游戏时,按照习惯从“1”开始计行数和列数,因此不用减一 { if (board[x - 1][y - 1] == ' ') { board[x - 1][y - 1] = '*'; break; } else { printf("该坐标被占用,请重新输入\n"); } } else { printf("坐标非法,重新输入\n"); } } }
说明:注意此处判断非法的逻辑。有些人用if-else if-else将三个判断条件串起来,这时不对的,如下图:
上图代码中,最后一个else永远也不会被执行到。原本我们想要表达的是,如果输入的坐标超出棋盘范围(数组越界),跳出提示:输入位置无效。而超出棋盘范围这一状况,同样满足第二个 else-if (数组坐标越界,该坐标所代表的元素在c语言中并不是不存在,它的内容是脏数据,只是没有被我们当作棋盘打印出来)。
写出这样的代码,是因为思路不清晰。我们进行合法性判断的步骤是这样的:
1. 如果下的棋位置在棋盘上,而且又是空位置,那就落子;
2. 如果下的在棋盘上,但是不是空位置,那就提示该位置已经下过了,落子失败重新输入;
3. 如果下的不在棋盘上,那就提示数组越界,落子失败重新输入。
所以,代码逻辑是这样的:
在棋盘上吗?
- 在,那棋盘上这个位置是不是空的呢?
-- 是 ---> 下棋。
-- 不是 ---> 重新输。
-不在,重新输。
因而,谁和谁共用同一层if - else,显而易见。
5. 玩家落子后轮到电脑落子,电脑在棋盘随机位置落子,用 '#'表示电脑落子。
void computer_move(char board[ROW][COL], int row, int col) { printf("电脑下棋:>\n"); while (1) { int x = rand() % row; //row为3时,取模运算结果为 0~2 ,刚好满足下标规律,所以这里不用像玩家一样用x-1 int y = rand() % col; if (board[x][y] == ' ') { board[x][y] = '#'; break; } } }
说明:此时我们用到了rand()函数来获取电脑要落子的位置。注意:播种随机种子应在main函数内,播种一次即可;另外,要包含头文件time.h(时间戳)与stdlib.h
6. 判定胜负,输或赢或和棋,用Q表示和棋。率先连成3子的一方获胜。
棋盘满了,还未分出胜负——和棋。
//判断棋盘是否已满 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; //判断横三行 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]; } } //判断竖三列 for (i = 0; i < col; i++) { if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ') { return board[0][i]; } } //判断主对角线 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 (is_full(board, row, col) == 1) { return 'Q'; } //继续 return 'C'; }
7. 回到步骤1,循环以上步骤。
在控制游戏流程时,加上死循环while,并用break控制循环的结束。这是C语言设计简单游戏时的常用操作,从需求分析的角度解释为:玩家体验一次可能不过瘾,还想接着再来一句,因而这时while可以实现由玩家自己决定是否还要再来一局(而不是直接结束程序)。
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() { printf("游戏开始!\n\n"); char board[ROW][COL] = { 0 }; //1.初始化棋盘 init_board(board, ROW, COL); //test_print(board, ROW, COL); //打印二维数组 //2.打印棋盘 show_board(board, ROW, COL); //3.下棋 char ret; while (1) { //玩家下棋 player_move(board, ROW, COL); show_board(board, ROW, COL); ret = is_win(board, ROW, COL); if (ret != 'C') { break; } //电脑下棋 computer_move(board, ROW, COL); show_board(board, ROW, COL); ret = is_win(board, ROW, COL); if (ret != 'C') { break; } } //4.判断胜负 if (ret == '*') { printf("玩家获胜!\n"); } else if (ret == '#') { printf("电脑获胜!\n"); } else if (ret == 'Q') { printf("平局!\n"); } } int main() { int input = 0; srand((unsigned)time(NULL)); //主体部分写在循环内 //玩不过瘾还可以接着玩:实现体验多次游戏 do { menu(); printf("请输入正确的选项>:"); scanf("%d", &input); switch (input) { case 1: { //进入游戏 game(); printf("\n"); break; } case 0: { //退出游戏 printf("退出游戏!\n"); break; } default: //输入错误,需要重新输入 printf("输入错误,请重新输入!\n"); break; } } while (input); return 0; }
game.h
#pragma once #include<stdio.h> #include<time.h> #include<stdlib.h> #define ROW 3 #define COL 3 //初始化棋盘 void init_board(char board[ROW][COL], int row, int col); //打印棋盘 void show_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); //判断棋盘是否已满 int is_full(char board[ROW][COL], int row, int col);
game.c
#define _CRT_SECURE_NO_WARNINGS 1 #include "game.h" void init_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++) { board[i][j] = ' '; } } } //void display_board(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][2]); // //---|---|--- // if(i<row-1) // printf("---|---|---\n"); // } //} void display_board(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][2]); 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) { //printf("---|---|---\n"); 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) { int x = 0; int y = 0; printf("玩家下棋:>\n"); while (1) { printf("请输入要下棋的坐标:>"); scanf("%d %d", &x, &y); //1.坐标的合法性 //2.坐标是否被占用 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("坐标非法,重新输入\n"); } } } // //电脑随机下棋 // void computer_move(char board[ROW][COL], int row, int col) { printf("电脑下棋:>\n"); //0~32726 //%3-->0~2 while (1) { int x = rand() % row; int y = rand() % col; if (board[x][y] == ' ') { board[x][y] = '#'; break; } } } //如果棋盘满了,返回1 //不满,返回0 static int is_full(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 is_win(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]; } } for (i = 0; i < col; i++) { if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ') { return board[0][i]; } } 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 (is_full(board, row, col) == 1) { return 'Q'; } //继续 return 'C'; }
万字详解:C语言三子棋进阶 + N子棋递归动态判断输赢(二)
+ https://developer.aliyun.com/article/1522011?spm=a2c6h.13148508.setting.17.439a4f0evqNcHz