深入理解C语言:开发属于你的三子棋小游戏

三子棋

    • 1. 前言
    • 2. 准备工作
    • 3. 使用二维数组存储下棋的数据
    • 4. 初始化棋盘为全空格
    • 5. 打印棋盘
    • 6. 玩家下棋
    • 7. 电脑下棋
    • 8. 判断输赢
    • 9. 效果展示
    • 10. 完整代码

在这里插入图片描述

1. 前言

大家好,我是努力学习游泳的鱼,今天我们会用C语言实现三子棋。所谓三子棋,就是三行三列的棋盘,玩家可以和电脑下棋,率先连成三个的获胜。话不多说,我们开始吧。
在这里插入图片描述

2. 准备工作

我们可以在一个项目中创建三个文件,分别是:

  1. test.c,测试游戏的逻辑。
  2. game.c,游戏的实现。
  3. game.h,函数声明,符号的定义。

测试这个游戏时,我们玩一把肯定不过瘾,所以需要使用do while循环,每次可以选择继续玩或者退出游戏。先把大致的框架搭出来。

#include <stdio.h>void menu()
{printf("****************************\n");printf("********  1. play     ******\n");printf("********  0. exit     ******\n");printf("****************************\n");
}int main()
{int input = 0;do{menu();printf("请选择(1/0):>");scanf("%d", &input);switch (input){case 1:printf("三子棋\n");break;case 0:printf("退出游戏\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}

在这里插入图片描述

当然,我们的游戏不可能只是打印“三子棋”这三个字这么简单,具体的实现我们会封装成一个函数,暂且取名为game

3. 使用二维数组存储下棋的数据

当我们下三子棋的时候,需要把下棋的数据存起来。由于三子棋的棋盘是3×3的,我们就需要一个三行三列的数组来存储下棋的数据。char board[3][3] = { 0 };

4. 初始化棋盘为全空格

当我们还没开始下棋时,棋盘上应该啥都没有,但是真的是啥都没有吗?事实上,如果我们打印棋盘时,能打印出“没有棋子”的效果,数组里应该是全空格。所以,我们需要写一个函数,初始化棋盘为全空格。InitBoard(board, 3, 3);这个函数的声明,我们会放在game.h里。具体的实现,我们会放在game.c里。以下的函数同理。该函数的声明:void InitBoard(char board[3][3], int row, int col);(以下的函数均省略声明)。该函数的实现,只需遍历这个二维数组,全部赋值为空格。

void InitBoard(char board[3][3], int row, int col)
{int i = 0;for (; i < row; ++i){int j = 0;for (; j < col; ++j){board[i][j] = ' ';}}
}

有没有发现,这个程序中,到处都要用到数组“三行三列”这个特点。如果我们想要改变这一点,比如改成五行五列,就需要改很多地方,非常麻烦。怎么解决这个问题呢?我们可以在game.h里定义两个常量ROWCOL,这样每次只需要改变#define后的值就行了。

#define ROW 3
#define COL 3

这样以上出现的所有3都可以用ROWCOL替代了(此处省略)。注意:如果想使用game.h里的符号,我们需要在game.ctest.c里引用这个头文件。引用自己写的头文件要用双引号。#include "game.h"而对于其他头文件如stdio.h,我们不需要在game.ctest.c里包含两次,只需在game.h里包含就行了。

5. 打印棋盘

我们初始化棋盘后,会想要看一看棋盘长啥样,所以接下来写一个打印棋盘的函数DisplayBoard(board, ROW, COL);
如果你认为打印出数组的数据就行了,从而这样写:

void DisplayBoard(char board[ROW][COL], int row, int col)
{int i = 0;for (; i < row; ++i){int j = 0;for (; j < col; ++j){printf("%c", board[i][j]);}printf("\n");}
}

实际运行时,你会发现,打印了,但没完全打印。
在这里插入图片描述
事实上,此时打印的是一堆空格,非常难看。如果我们想打印得好看点,可以考虑加上一些横向和竖向的分割。比如我设想了这样一种打印的效果:

   |   |   
---|---|---|   |   
---|---|---|   |   

假设把

   |   |   
---|---|---

当成一组,总共需要打印3组,为什么呢?因为有3行。
每一组里面,又分为数据行和分割行。我们需要先打印数据行,再打印分割行。

   |   |    // 数据行
---|---|--- // 分割行

所以一个简单的想法是这么写:

void DisplayBoard(char board[ROW][COL], int row, int col)
{int i = 0;for (; i < row; ++i){// 打印数据printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);// 打印分割的行printf("---|---|---\n");}
}

效果如下:
在这里插入图片描述
我们发现多打印了一行分割行,所以打印分割行时要加一条判断,不是最后一行才打印分割行。

if (i < row - 1)printf("---|---|---\n");

这样子打印就好看多了。

在这里插入图片描述

但是这么写的话,就相当于,一行一定要打印三列,后面就没有改变的可能了。比如我把ROWCOL都改成5,效果如下:
在这里插入图片描述
所以这种写法还是不够好。最好的写法是,再用一层循环控制列的打印。比如对于数据行:

   |   |   

我们可以把

   |  

当成一组数据,打印三组,最后一组就不用加上|了(同上一种写法用一个if语句来控制)。
对于分割行

---|---|---

我们可以把

---|

当成一组数据,打印三组,最后一组就不用加上|了(还是用if语句来控制)。
实现如下:

void DisplayBoard(char board[ROW][COL], int row, int col)
{int i = 0;for (; i < row; ++i){// 打印数据int j = 0;for (; 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");}}
}

效果也没问题:
在这里插入图片描述
若把ROWCOL改成10,打印出来的效果如下:
在这里插入图片描述
这样写出来的代码就比较通用,具有比较强的可维护性。

6. 玩家下棋

接下来写玩家下棋的函数。player_move(board, ROW, COL);
先让玩家输入坐标,若xy都在13之间,则输入的坐标合法,在数组对应的位置是board[x-1][y-1],若该位置仍然是空格,则这个位置没有被下过,就把数组的这个元素改成*。由于若玩家输入的坐标非法或者被占用时,需要重新输入,故需要一个循环。

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);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");}}
}

玩家下棋后再把棋盘打印一下DisplayBoard(board, ROW, COL);效果如下:
在这里插入图片描述

7. 电脑下棋

玩家下完棋后就轮到电脑下棋computer_move(board, ROW, COL);
我们让电脑随机下棋,只需生成两个随机的坐标xy,若该位置是空格,就下在这个位置,如果是空格,那就再次产生随机坐标,可见,这也需要一个循环来实现。

void computer_move(char board[ROW][COL], int row, int col)
{int x = 0;int y = 0;printf("电脑下棋\n");while (1){x = rand() % row; // 0~2y = rand() % col; // 0~2if (board[x][y] == ' '){board[x][y] = '#';break;}}
}

使用rand函数前需要使用srand函数来设置随机数生成器的起点。我们需要给srand函数传一个时间戳,这就要用到time函数。srand((unsigned int)time(NULL));使用randsrand都需要引用头文件stdlib.h,使用time函数需要引用头文件time.h
我们用一个循环,就能实现玩家和电脑轮流下棋的效果。

while (1)
{// 玩家下棋player_move(board, ROW, COL);DisplayBoard(board, ROW, COL);// 电脑下棋computer_move(board, ROW, COL); // 随机下棋DisplayBoard(board, ROW, COL);
}

8. 判断输赢

什么时候游戏就结束了呢?如果玩家赢了,或者电脑赢了,或者平局,游戏就结束了,否则游戏继续。
我们来设计一个is_win函数来判断棋局是上面四种状态的哪一种。我们这么设计is_win函数的返回值:

  1. 玩家赢 - '*'
  2. 电脑赢 - '#'
  3. 平局 - 'Q'
  4. 继续 - 'C'

当棋局状态不是C时说明游戏结束了,就跳出循环,接着根据不同情况打印不同的结果。

char ret = 0;
while (1)
{// 玩家下棋player_move(board, ROW, COL);DisplayBoard(board, ROW, COL);// 判断输赢ret = is_win(board, ROW, COL);if (ret != 'C'){break;}// 电脑下棋computer_move(board, ROW, COL); // 随机下棋DisplayBoard(board, ROW, COL);ret = is_win(board, ROW, COL);if (ret != 'C'){break;}
}
if (ret == '*')
{printf("玩家赢\n");
}
else if (ret == '#')
{printf("电脑赢\n");
}
else
{printf("平局\n");
}

如何实现is_win函数呢?只需判断每行,每列,每条对角线是否有三个同样的棋子就行了。
先判断行(由于玩家赢和电脑赢都是返回对应的棋子——*#,所以直接把对应的数组元素返回就行了。):

int i = 0;
for (; i < row; ++i)
{if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1] != ' '){return board[i][1];}
}

判断列同理:

for (i = 0; i < col; ++i)
{if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[1][i] != ' '){return board[1][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];
}

如果有人赢了,在前面的代码中就会返回。如果没人赢,再来判断是否平局。如果棋盘没有空格了,那就是平局。我们可以写一个is_full函数来判断棋盘是否满了。如果满了就返回1,没满就返回0

if (is_full(board, row, col) == 1)
{return 'Q';
}

由于is_full函数只是写给is_win函数的,只需要在game.c这个文件内使用,所以加上static。具体的实现,只需要遍历数组,若发现空格,说明棋盘没满,就返回0,否则返回1

static int is_full(char board[ROW][COL], int row, int col)
{int i = 0;for (; i < row; ++i){int j = 0;for (; j < col; ++j){if (board[i][j] == ' '){return 0; // 没有满}}}return 1; // 满了
}

而如果没人赢,也不是平局,则游戏继续,return 'C';即可。

9. 效果展示

写到这,我们就把三子棋程序写完啦。接下来看看效果:
玩家赢:
在这里插入图片描述
电脑赢:
在这里插入图片描述
平局:
在这里插入图片描述

10. 完整代码

下面是完整的代码:
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 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);

game.c

#define _CRT_SECURE_NO_WARNINGS 1#include "game.h"void InitBoard(char board[ROW][COL], int row, int col)
{int i = 0;for (; i < row; ++i){int j = 0;for (; j < col; ++j){board[i][j] = ' ';}}
}//void DisplayBoard(char board[ROW][COL], int row, int col)
//{
//	int i = 0;
//	for (; i < row; ++i)
//	{
//		int j = 0;
//		for (; j < col; ++j)
//		{
//			printf("%c", board[i][j]);
//		}
//		printf("\n");
//	}
//}//void DisplayBoard(char board[ROW][COL], int row, int col)
//{
//	int i = 0;
//	for (; i < row; ++i)
//	{
//		// 打印数据
//		printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);
//		// 打印分割的行
//		if (i < row - 1)
//			printf("---|---|---\n");
//	}
//}void DisplayBoard(char board[ROW][COL], int row, int col)
{int i = 0;for (; i < row; ++i){// 打印数据int j = 0;for (; 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)
{int x = 0;int y = 0;printf("玩家下棋\n");while (1){printf("请输入坐标:>");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("坐标非法,请重新输入\n");}}
}void computer_move(char board[ROW][COL], int row, int col)
{int x = 0;int y = 0;printf("电脑下棋\n");while (1){x = rand() % row; // 0~2y = rand() % col; // 0~2if (board[x][y] == ' '){board[x][y] = '#';break;}}
}static int is_full(char board[ROW][COL], int row, int col)
{int i = 0;for (; i < row; ++i){int j = 0;for (; 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 < row; ++i){if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1] != ' '){return board[i][1];}}// 判断列for (i = 0; i < col; ++i){if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[1][i] != ' '){return board[1][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';
}

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()
{// 三子棋小游戏的具体实现char ret = 0;// 存放下棋的数据char board[ROW][COL] = { 0 };// 初始化棋盘为全空格InitBoard(board, ROW, COL);// 打印棋盘DisplayBoard(board, ROW, COL);while (1){// 玩家下棋player_move(board, ROW, COL);DisplayBoard(board, ROW, COL);// 判断输赢ret = is_win(board, ROW, COL);if (ret != 'C'){break;}// 电脑下棋computer_move(board, ROW, COL); // 随机下棋DisplayBoard(board, ROW, COL);ret = is_win(board, ROW, COL);if (ret != 'C'){break;}}if (ret == '*'){printf("玩家赢\n");}else if (ret == '#'){printf("电脑赢\n");}else{printf("平局\n");}//DisplayBoard(board, ROW, COL);
}//
// 什么时候,游戏就结束了
// 玩家赢 - '*'
// 电脑赢 - '#'
// 平局   - 'Q'
// 继续   - 'C'
//
int main()
{int input = 0;srand((unsigned int)time(NULL));do{menu();printf("请选择(1/0):>");scanf("%d", &input);switch (input){case 1:game();break;case 0:printf("退出游戏\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/719063.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android 开发环境搭建的步骤

本文将为您详细讲解 Android 开发环境搭建的步骤。搭建 Android 开发环境需要准备一些软件和工具&#xff0c;以下是一些基础步骤&#xff1a; 1. 安装 Java Development Kit (JDK) 首先&#xff0c;您需要安装 Java Development Kit (JDK)。JDK 是 Android 开发的基础&#xf…

【Android 内存优化】怎么理解Android PLT hook?

文章目录 前言什么是hook?PLT hook作用基本原理PLT hook 总体步骤 代码案例分析方案预研面临的问题怎么做&#xff1f;ELFELF 文件头SHT&#xff08;section header table&#xff09; 链接视图&#xff08;Linking View&#xff09;和执行视图&#xff08;Execution View&…

YOLO-World 简单无需标注无需训练直接可以使用的检测模型

参考: https://github.com/AILab-CVC/YOLO-World YOLO-World 常规的label基本不用训练,直接传入图片,然后写入文本label提示既可 案例demo: 1)官方提供 https://huggingface.co/spaces/stevengrove/YOLO-World https://huggingface.co/spaces/SkalskiP/YOLO-World 检测…

基于信息间隙决策理论的碳捕集电厂优化调度程序代码!

适用平台&#xff1a;MatlabYalmipCplex 程序在建立电厂与碳捕集装置协同调度模型的基础上&#xff0c;引入信息间隙决策理论(information gap decision theory, IGDT)以同时满足系统的鲁棒性和经济性要求&#xff0c;通过风险追求和风险规避&#xff12;种决策角度得到不同的…

真不愧是华为出来的,真的太厉害了。。。

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 实习去了博彦科技&#xff08;外包&#xff09;&#xff0c;做的就是螺丝钉的活&#xff0c;后面…

华为---MSTP(一)---MSTP生成树协议

目录 1. MSTP技术产生背景 2. STP/RSTP的缺陷 ​编辑 2.1 无法均衡流量负载 2.2 数据使用次优路径 3. MSTP生成树协议 3.1 MSTP相关概念 3.2 MSTP树生成的形成过程 4. MSTP报文 1. MSTP技术产生背景 RSTP在STP基础上进行了改进&#xff0c;实现了网络拓扑快速收敛。但…

SpringBoot+Maven项目打包

项目的主POM文件里面添加maven打包插件 <build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.2</version><configuration><sour…

推荐一款新的自动化测试框架:DrissionPage

今天给大家推荐一款基于Python的网页自动化工具&#xff1a;DrissionPage。这款工具既能控制浏览器&#xff0c;也能收发数据包&#xff0c;甚至能把两者合而为一&#xff0c;简单来说&#xff1a;集合了WEB浏览器自动化的便利性和 requests 的高效率优点。 一、DrissionPage框…

【C++庖丁解牛】默认成员函数

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 目录 前言1. 构造函数1.1 …

职场高薪 |「中高级测试」面试题

【软件测试面试突击班】2024吃透软件测试面试最全八股文攻略教程&#xff0c;一周学完让你面试通过率提高90%&#xff01;&#xff08;自动化测试&#xff09; 一.基础题 1.测试用例你一般是怎么设计的&#xff0c;怎么可以提高覆盖率&#xff1f; 有没有形成自己的 一套方法论…

Jmeter接口测试---随机数、加密、cookie鉴权、断言、CSV参数化

随机数 第一步&#xff1a;选择工具-函数助手对话框 第二步&#xff1a;选择random&#xff0c;设置最大值最小值&#xff0c;复制函数字符串到指定位置 加密接口 类型&#xff1a;AES、DES、Base64、RSA&#xff08;可以解密&#xff09; | MD5、SHA、HmacSHA&#xff08;不…

llama.c代码2

1、forward 1.1、复习 encode(tokenizer, prompt, 1, 0, prompt_tokens, &num_prompt_tokens); 在encode函数结尾处(gdb) p *n_tokens $3 2(gdb) p *tokens3 $6 {1, 22172, 417} 在encode调用后 (gdb) print num_prompt_tokens $11 2 (gdb) print *prompt_tokens3 $13 …

JavaScript 中的类型转换机制(详细讲解)

文章目录 一、概述二、显示转换Number()parseInt()String()Boolean() 三、隐式转换自动转换为布尔值自动转换成字符串自动转换成数值 一、概述 前面我们讲到&#xff0c;JS中有六种简单数据类型&#xff1a;undefined、null、boolean、string、number、symbol&#xff0c;以及…

(sub)三次握手四次挥手

TCP的三次握手与四次挥手理解及面试题 序列号seq&#xff1a;占4个字节&#xff0c;用来标记数据段的顺序&#xff0c;TCP把连接中发送的所有数据字节都编上一个序号&#xff0c;第一个字节的编号由本地随机产生&#xff1b;给字节编上序号后&#xff0c;就给每一个报文段指派一…

(学习日记)2024.03.03:UCOSIII第五节:常用汇编指令+OS初始化+启动任务+任务切换

写在前面&#xff1a; 由于时间的不足与学习的碎片化&#xff0c;写博客变得有些奢侈。 但是对于记录学习&#xff08;忘了以后能快速复习&#xff09;的渴望一天天变得强烈。 既然如此 不如以天为单位&#xff0c;以时间为顺序&#xff0c;仅仅将博客当做一个知识学习的目录&a…

双重检验锁

双重检验锁&#xff1a;设计模式中的单例模式&#xff0c;细分为单例模式中的懒加载模式。 单例模式 单例模式&#xff1a;指的是一个类只有一个对象。最简单的实现方式是设一个枚举类&#xff0c;只有一个对象。缺点是当对象还没有被使用时&#xff0c;对象就已经创建存在了…

【扩散模型】生成模型中的Residual Self-Attention UNet 以及 DDPM的pytorch代码

参考&#xff1a; [1] https://github.com/xiaohu2015/nngen/blob/main/models/diffusion_models/ddpm_cifar10.ipynb [2] https://www.bilibili.com/video/BV1we4y1H7gG/?spm_id_from333.337.search-card.all.click&vd_source9e9b4b6471a6e98c3e756ce7f41eb134 TOC 1 UNe…

视黄酸诱导基因-1敲除诱导树突状细胞的不成熟特性并延长异体移植小鼠的存活时间研究【AbMole】

器官移植是一种用于替换因疾病、损伤或其他原因受损的人体器官的医疗程序。尽管器官移植可以挽救生命并显著提高生活质量&#xff0c;但存在供体器官短缺、排斥反应、器官功能障碍、感染和药物副作用等问题。为了提高移植成功率和受体健康&#xff0c;需要有效的免疫策略。树突…

如何使用支付宝沙箱环境本地配置模拟支付并实现公网远程访问【内网穿透】

文章目录 前言1. 下载当面付demo2. 修改配置文件3. 打包成web服务4. 局域网测试5. 内网穿透6. 测试公网访问7. 配置二级子域名8. 测试使用固定二级子域名访问 前言 在沙箱环境调试支付SDK的时候&#xff0c;往往沙箱环境部署在本地&#xff0c;局限性大&#xff0c;在沙箱环境…

数据可视化原理-腾讯-3D网格热力图

在做数据分析类的产品功能设计时&#xff0c;经常用到可视化方式&#xff0c;挖掘数据价值&#xff0c;表达数据的内在规律与特征展示给客户。 可是作为一个产品经理&#xff0c;&#xff08;1&#xff09;如果不能够掌握各类可视化图形的含义&#xff0c;就不知道哪类数据该用…