万字详解:C语言三子棋进阶 + N子棋递归动态判断输赢(一)

简介: 三子棋游戏设计的核心是对二维数组的把握和运用。

本文介绍C语言学习阶段的经典项目:三子棋(N子棋)。


文章主要以代码的形式呈现,并附上必要的说明(说明主要以代码注释的风格呈现),力求正确、简洁、清晰。


前言


三子棋游戏设计的核心是对二维数组的把握和运用。


本文分步骤呈现三子棋(N子棋)游戏程序设计代码,以介绍与解析为主。文末另附一份压缩文件,为C语言三子棋课设实验报告(博主本人在学校写的,当初选择了三子棋作为课设项目),需要用作课设参考的同学可以直接下载现成的。希望多诸位读者有所帮助。


**解压缩的文件夹及其内容预告




\C语言课设作业--三子棋



\C语言课设作业--三子棋\课设大作业


此外,本文也对原有的最简单的三子棋游戏进行了优化,增加了电脑“会堵棋”的代码版本以及 N 子棋的输赢判断实现。希望对大家开拓思路有所帮助。


一、游戏简要介绍 -- 三子棋


规则


游戏模式为人机对决。玩家在主菜单界面选择是否要开始一盘游戏。当游戏开始后,由玩家这一方先开局下棋。通过坐标输入的方式将棋子放入玩家想要下的位置。玩家落子后电脑方立刻下棋。双方轮流下棋,直到有一方有3颗棋子连成一线,则率先达到3子连线的一方获得胜利。然后再由玩家决定是否要再开一把游戏。


功能实现


  1. 在菜单界面玩家可选择开始游戏或退出游戏。


  1. 创建一个新的棋盘,并初始化。


  1. 将棋盘打印在屏幕上。


  1. 玩家先开局,通过输入行列坐标的方式来落子,用×表示玩家落子。


  1. 玩家落子后轮到电脑落子,电脑在棋盘随机位置落子,用〇表示电脑落子。


  1. 判定胜负,输或赢或和棋,用q表示和棋。率先连成3子的一方获胜。


  1. 回到步骤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. 我想不到 / 想不清楚怎么办?


——此处运用到的思路详述在另一篇文章中,链接如下:


“盒子思路”解决循环嵌套

http://t.csdn.cn/kZVwx


另一个版本的棋盘打印代码如下,也许会更贴合上述逻辑(其实整合后,就是上面所贴出的代码):



//打印棋盘
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

相关文章
|
4月前
|
机器学习/深度学习 C语言
九/十:《初学C语言》— 扫雷游戏实现和函数递归基础
【8月更文挑战第5天】本篇文章用C语言采用多文件编写实现了一个基础的扫雷游戏(附源码),并讲解了关于函数递归的基础概念及其相对应的习题练习(附源码)
43 1
九/十:《初学C语言》— 扫雷游戏实现和函数递归基础
|
6月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
2月前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
63 7
|
2月前
|
C语言
c语言回顾-函数递归(上)
c语言回顾-函数递归(上)
34 2
|
2月前
|
C语言
c语言回顾-函数递归(下)
c语言回顾-函数递归(下)
40 0
|
4月前
|
机器学习/深度学习 C语言
【C语言篇】递归详细介绍(基础概念习题及汉诺塔等进阶问题)
要保持最小的步数,每一次汉诺塔问题(无论是最初还是递归过程中的),如果此时初始柱盘子数为偶数,我们第一步是把最上面的盘子移动到中转柱,如果为奇数,我们第一步则是将其移动到目标柱。
84 0
【C语言篇】递归详细介绍(基础概念习题及汉诺塔等进阶问题)
|
4月前
|
C语言
C语言中的递归
C语言中的递归
|
5月前
|
存储 编译器 C语言
|
4月前
|
算法 编译器 C语言
【C语言】递归
【C语言】递归
22 0
|
6月前
|
C语言
【海贼王编程冒险 - C语言海上篇】C语言如何实现简单的三子棋游戏?
【海贼王编程冒险 - C语言海上篇】C语言如何实现简单的三子棋游戏?
30 1