amon01 发表于 2022-11-25 19:43

学习总结——三子棋的逻辑分析与详细运行代码

本帖最后由 苏紫方璇 于 2022-11-25 21:07 编辑

前言
    三子棋,又名井字棋。作为一款简单的小游戏,虽然逻辑并不困难,但是用计算机语言去表达对一个初学C语言的萌新来说还是有一定难度的,需要对函数,数组等知识有一定理解并且有一定的代码能力才可以实现。下面就让我们来分析一下,这款小游戏是如何制作的吧!

一、游戏规则是什么?
         三子棋的游戏规则并不复杂,内容是玩家和电脑之间在一个3×3的棋盘空格处交替落子,如果有一方实现了三颗棋子连成一条直线(横,竖,斜均可),就获得胜利。如果棋子已经填满了棋盘而双方还没有人取胜,就出现平局。接下来就让我们分析一下,该如何实现这样的逻辑。

二、逻辑分析
      1.主函数部分      ——运行test函数即可,把具体内容放到test函数中实现。这样的好处是可以将游戏内容分成不同的模块来实现。

      2. menu函数部分——游戏的菜单,让玩家选择是否进行游戏,在玩家输入开始或者退出之外的内容时给予提示。并且实现循环,玩家进行完一场游戏后可以选择继续游戏。

      3.game函数部分 ——通过将下面完成的具体函数部分进行使用,来实现整个游戏的运行,我们需要运用循环的方式,来实现玩家与电脑交替落子,并保证胜负判定结束后,即时跳出循环并打印结果。

      4.game函数的实现

      (1).棋盘初始化——3×3棋盘的本质为九个空格,因此需要先输入一个二维数组,将每一个元素都设定为一个空格,用空格完成棋盘的初始化。

      (2).棋盘的打印   ——目标是将刚才由空格组成的3×3棋盘打印出来。此时若直接打印,将会是空格的堆叠,什么也看不到,因此需要输入分隔线,来形成完整的棋盘。

      (3).玩 家 下 棋    ——此时需要玩家进行主动输入一组坐标数据,使用星号*或者井号#这样的棋子替代该坐标处原来的空格,落子后再次打印棋盘,实现反馈。

      (4).电 脑 下 棋    ——很遗憾,在初级阶段,我们做不到赋予电脑足够高的智能来通过分析场上的局势判断出最优解,我们所能做的只有让电脑随机生成一组数据的坐标,使用棋子替代原来的空格,并且完成打印。

      (5).判 断 胜 负    ——胜负的规则前文已经提到,我们此处需要做的就是三种情况的判断,第一种:连成三个子的一方判定胜利,游戏结束,宣布胜利者。第二种:棋盘已经填满,但仍没有连成三子的情况,游戏结束,宣布平局。第三种:棋盘尚未填满,也没有出现三子相连,游戏继续。

三、游戏具体代码
      理清了游戏的逻辑,清楚了每一步具体的目标和要求,接下来就是具体代码的实现过程了。
1.主函数部分
      此处的目的是将函数分模块实现,这样便于代码的修改,便于其他人读懂。
int main()
{
    test();
    return 0;
}
2.menu函数部分
         我们想要使用一个菜单函数,就必须在test函数中进行实现,因此需要先创建一个menu函数,再在test函数中完成。
viod menu()

{

printf("*************************\n");//创建一个菜单的界面,并提示玩家进行下一步的操作

printf("******* 1.play *********\n");

printf("******* 2.exit **********\n");

printf("*************************\n");

}



viod test()

{

int input =0;

srand((unsigned int)time(NULL));//定义一个随机数,后续会用在电脑下棋的部分

do

{

menu();//进入游戏后首先打开菜单,让玩家选择是否进行游戏

printf("请输入:>");

scanf("%d",input);//玩家输入0,1或者其他内容进行操作

switch(input)

{case 1:

         game();//具体游戏部分这个函数将在case 1情况下执行

         break;

case 0;

         printf("退出游戏\n");//对于case 0,后面的while将判断为假,跳出循环

         break;

default:

         printf("输入错误,请重新输入\n");}

}

while(input);//do while 结构可以保证我们玩完一把之后再玩下一把

}
3.game函数部分
   game必须保证我们和电脑分出胜负前可以重复下棋,因此可以采用while 语句来写。
void game()

{

char ret=0;//ret用来判断结果

char board={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;
}
}
//break跳出while循环后直接来到这里,开始进行结果的判定
//关于这里的*,#也可以设置成其他的元素,但是最好与我们下棋时使用的元素一致,原因后面会给出。
if (ret=='*')
{
printf("你赢了,请再接再厉\n");
}
else if(ret=='#')
{
printf("你输了,请回家种地\n");
}
else
{
printf("平局,相当于机械飞升\n");
}
}
4.game函数的实现
       通过前三步的处理,我们已经完成了整个游戏的框架搭建和逻辑的实现。接下来就是对具体内容进行实现了。我们将按照步骤,将每一步所需要做的具体内容完成。
(1).棋盘初始化
   3×3棋盘的本质为九个空格,因此需要先输入一个二维数组,将每一个元素都设定为一个空格,用空格完成棋盘的初始化   
void initboard(char board, int row, int col)
//目的是在每一个元素的位置打印上空格
{
int i=0;
for(i=0;i<row;i++)
{
   int j=0;
   for(j=0;j<col;j++)
{
    board=' ';//运用两次for循环,以及输入的行和列的次数,将每一个元素都完成初始化
}

}

}
(2).棋盘的打印
       目标是将由二维数组组成的3×3棋盘打印出来。此时若直接打印,将会是空格的堆叠,什么也看不到,因此需要输入分隔线,来形成完整的棋盘。后续每次输入坐标,都将会使一枚棋子替代当前的空格,打印之后会是棋子落在棋盘上的既视感。

       我们在这里最好选择的是将行和列分别分割,然后打印每一个小块。这样的话我们如果想要改变规则,玩一玩5×5的格子,只需要改变ROW和COL的值即可。

       分割的方法我们选择的是在打印同一行的元素时,每打印一个元素在后面加一个‘|’,每打印完一行之后,在刚才的每个元素下面打印一个‘---’,同时最后一行和最后一列不打印,保持工整。
void DisplayBoard(char board, 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);//输入的元素
                        if (j < col - 1)
                        {
                              printf("|");//每一行两格之间由|隔开
                        }
                }printf("\n");
                if (i < row - 1)//这里的i<row-1目的是保证最后一行不打印---
                {
                        for (j = 0; j < col; j++)
                        {
                              printf("---");//每一列两格之间由---隔开
                              if (j<col-1)//这里的i<col-1目的是保证最后一列不打印|
                              {
                                        printf("|");
                              }
                                       
                              

                        }printf("\n");
                }
      }
}
(3).玩 家 下 棋
       此时需要玩家进行主动输入一组坐标数据,使用星号*或者井号#这样的棋子替代该坐标处原来的空格,落子后再次打印棋盘,实现反馈。   

       下棋时需要注意的几个点在于:

         下过棋的位置不能再下,因此需要进行一次判断。

         下棋时选择的坐标必须满足在当前的行和列范围内,比如三子棋就算3×3,那么输入的坐标如果超过了3×3这样的大小,如(5,2),(1,4)这样的非法坐标,需要判断并提醒玩家重新输入坐标。

          除此之外,我们还要注意在C语言中,数组中的元素下标是从0开始的,也就是说我们的棋盘真实坐标为横坐标0~2,纵坐标0~2,但是玩家可能并不了解。因此在代码阶段,需要对输入的坐标进行调整,保证游戏更符合玩家的理解和操作习惯。
void player_move(char board, 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是因为下标从0开始
                        {
                              board = '*';
                              break;
                        }
                        else
                        {
                              printf("该坐标被占用,请重新输入\n");
                        }
                }
                else
                {
                        printf("坐标非法,请重新输入\n");
                }
      }
}
(4).电 脑 下 棋   
   很遗憾,在初级阶段,我们做不到赋予电脑足够高的智能来通过分析场上的局势判断出最优解,我们所能做的只有让电脑随机生成一组数据的坐标,使用棋子替代原来的空格,并且完成打印。

   对于电脑输入的数据,我们不需要像对玩家一家进行反复的判定是否超出范围,只需要将我们通过time生成的随机数%我们的row或者col,这样就可以保证电脑输入的坐标一定在规定的范围内,但是后续对于是否下在非空格的位置,仍要加以限制。
void computer_move(char board, int row, int col)
{
      int x = 0;
      int y = 0;

      while (1)
      {
                x = rand() % row;//一定是0~2,rand为test中定义的随机数
                y = rand() % col;//一定是0~2
                printf("电脑下棋\n");
                if (board == ' ')//保证电脑下的一定是空格的位置
                {
                        board = '#';
                        break;
                }
      }
}
(5).判 断 胜 负   
             这里我们需要对下棋的结果进行判断,玩家or电脑胜利,平局,继续下。我们可以通过设定不同的返回值,来输出我们的结果。
staticint is_full(char board, int row, int col)//限定在该文件中使用
{
      int i = 0;
      for (i = 0; i < row;i++)
      {
                int j = 0;
                for (j = 0; j < col; j++)
                {
                        if (board==' ')
                        {
                              return 0;//证明还有空格,棋盘未满
                        }
                }
         }
      return 1;//棋盘满了
}
char is_win(char board, int row, int col)
{
      int i = 0;
      //判断行
      for (i = 0; i < row; i++)
      {
                if (board==board&&board ==board&& board!=' ')
                {
                        return board;//之前设定玩家赢返回*或者#的妙用就在此处,可以直接返回,无需
                               //多余的判断
                }
      }
      //判断列
      for (i = 0; i < col; i++)
      {
                if (board == board && board == board && board != ' ')
                {
                        return board;
                }
      }
      //判断对角线
      if (board == board && board == board && board != ' ')
      {
                return board;
      }
      if (board == board && board == board && board != ' ')
      {
                return board;
      }
      //判断平局
if      (is_full(board,row,col)==1)
{
      return 'Q';
}
return 'C';
}
四.完整代码以及运行结果
头文件"game.h"

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 3//定义的行数,可以通过改变3的值改变
#define COL 3//定义的列数,可以通过改变3的值改变
//初始化棋盘
void initboard(char board, int row, int col);
//打印棋盘
void DisplayBoard(char board, int row, int col);
//玩家下棋
void player_move(char board, int row, int col);
//电脑下棋
void computer_move(char board, int row, int col);
//判断输赢
char is_win(char board, int row, int col);

game模块源文件game.c
#define_CRT_SECURE_NO_WARNINGS
#include "game.h"

void initboard(char board, int row, int col)
//目的是在每一个元素的位置打印上空格
{
      int i = 0;
      for (i = 0; i < row; i++)
      {
                int j = 0;
                for (j = 0; j < col; j++)
                {
                        board = ' ';//运用两次for循环,以及输入的行和列的次数,将每一个元素都完成初始化
                }

      }

}
void DisplayBoard(char board, 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);//输入的元素
                        if (j < col - 1)
                        {
                              printf("|");//每一行两格之间由|隔开
                        }
                }printf("\n");
                if (i < row - 1)//这里的i<row-1目的是保证最后一行不打印---
                {
                        for (j = 0; j < col; j++)
                        {
                              printf("---");//每一列两格之间由---隔开
                              if (j < col - 1)//这里的i<col-1目的是保证最后一列不打印|
                              {
                                        printf("|");
                              }



                        }printf("\n");
                }
      }
}

void player_move(char board, 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是因为下标从0开始
                        {
                              board = '*';
                              break;
                        }
                        else
                        {
                              printf("该坐标被占用,请重新输入\n");
                        }
                }
                else
                {
                        printf("坐标非法,请重新输入\n");
                }
      }
}

void computer_move(char board, int row, int col)
{
      int x = 0;
      int y = 0;

      while (1)
      {
                x = rand() % row;//一定是0~2,rand为test中定义的随机数
                y = rand() % col;//一定是0~2
                printf("电脑下棋\n");
                if (board == ' ')//保证电脑下的一定是空格的位置
                {
                        board = '#';
                        break;
                }
      }
}
staticint is_full(char board, int row, int col)//限定在该文件中使用
{
      int i = 0;
      for (i = 0; i < row; i++)
      {
                int j = 0;
                for (j = 0; j < col; j++)
                {
                        if (board == ' ')
                        {
                              return 0;//证明还有空格,棋盘未满
                        }
                }
      }
      return 1;//棋盘满了
}
char is_win(char board, int row, int col)
{
      int i = 0;
      //判断行
      for (i = 0; i < row; i++)
      {
                if (board == board && board == board && board != ' ')
                {
                        return board;//之前设定玩家赢返回*或者#的妙用就在此处,可以直接返回,无需
                                                         //多余的判断
                }
      }
      //判断列
      for (i = 0; i < col; i++)
      {
                if (board == board && board == board && board != ' ')
                {
                        return board;
                }
      }
      //判断对角线
      if (board == board && board == board && board != ' ')
      {
                return board;
      }
      if (board == board && board == board && board != ' ')
      {
                return board;
      }
      //判断平局
      if (is_full(board, row, col) == 1)
      {
                return 'Q';
      }
      return 'C';
}

主函数test.c,实现游戏的逻辑部分
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
//测试三子棋游戏的逻辑
void menu()
{
      printf("**********************\n");
      printf("****** 1.play ********\n");
      printf("****** 0.exit ********\n");
      printf("**********************\n");
}
void game()
{
      char ret = 0;
      //存放下棋的数据
      char board = { 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");
      }
}      
//什么情况游戏结束?
//玩家赢——'*'
//电脑赢——'#'
//平局——'Q'
//继续——'C'
void test()
{
      int input = 0;
      srand((unsigned int)time(NULL));
      do
      {
                menu();//进入游戏后首先打印菜单,让玩家选择是否玩游戏
                printf("请输入:>");
                scanf("%d", &input);//玩家输入1,0,或者其他内容进行选择
                switch(input)
                {
                case 1:
                        game();//游戏这个函数在case 1的情况下可以实现
                        break;
                case 0:
                        printf("退出游戏\n");
                        break;
                default:printf("输入错误,请重新输入\n");
                }
      } while (input);//do while 结构保证我们可以玩完一把再玩下一把
}
int main()
{
      test();
      return 0;
}

运行结果截图如下


五.总结收获
       三子棋作为一个经典的练习代码能力的小游戏,可以帮助我们加深对于函数,数组相关知识的理解以及对for循环,while循环等基本语句的应用。笔者目前掌握的知识,以上是我能提供的全部思路和感悟。学习需要的是滴水穿石,每一段代码写出或者报错都会给我们带来不同的思考与感悟。关于文中对于电脑省略判断的部分,希望大家能从更优秀的作者那获得更好的处理方式。笔者也会不断努力提升自己,希望之后能给出更优的写法。

lzy1983 发表于 2022-11-25 20:09

基础知识很好的总结,谢谢楼主

lfordch 发表于 2022-11-25 20:20

手把手教学,好评

whsstc 发表于 2022-11-25 20:29

无私的奉献,点赞

badwd 发表于 2022-11-25 20:49

感谢分享,希望检查分享

amon01 发表于 2022-11-25 20:59

badwd 发表于 2022-11-25 20:49
感谢分享,希望检查分享

谢谢大佬提醒,确实出现了一些问题,正在复查修改!

wanglong001 发表于 2022-11-26 09:39

向大佬学习
页: [1]
查看完整版本: 学习总结——三子棋的逻辑分析与详细运行代码