Linux项目实战——五子棋(单机人人对战版)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

Linux操作系统项目实战——五子棋

GIF

目录

           Linux操作系统项目——五子棋

一、问题导引

二、实现要求

三、五子棋原理

1.落子数据信息保存载体

2.落子思路

3.判断“五子连珠”

四、项目实现步骤

Ⅰ.创建目录及文件:

1.在Linux环境下创建名为Gobang的文件目录

Ⅱ、输入Linux环境指令进入目录及文件

Ⅲ、Linux环境编程实现

1.顶层逻辑设计

2.模块化设计

3.联动模块组合封装

        完整代码

1game.c

2main.c

3game.h

4Makefile

 五、程序调试

1.程序界面及说明

2.测试用例

六、程序功能扩展

1.扩展思路

2.扩展实现

引入进度条​

1更改代码

2扩展效果演示​

七、项目总结

1.核心逻辑

2.项目体现思想

八、项目改编

后记●由于作者水平有限文章难免存在谬误之处敬请读者斧正俚语成篇恳望指教                                                       ——By 作者新晓·故知


一、问题导引

在进行Linux入门学习后通过运用Linux环境指令操作、Vim编辑器、学习五子棋的基本原理等进行Linux环境下的简单的五子棋项目实现。

生活中的五子棋

   

从上图可以看出五子棋这类棋有先手、后手之分(当然也有禁手、俗手、妙手等这里只进行简单的交互式、窗口化项目实现。上图涉及GUI技术、图像渲染技术等太过高深

二、实现要求

  • 熟悉Linux环境指令操作

  • Linux中相关开发工具的基本使用

  • 熟悉在Linux环境当中进行C编程

  • 掌握五子棋的基本原理

  • 编写五子棋程序的上层基本调用框架

  • 等等

说明本篇博客采用VMware Workstation 16+CentOS7作为实现演示示例工具

三、五子棋原理

1.落子数据信息保存载体

需要记录每个玩家的落子位置用来判断谁赢谁输。

可采用二维数组保存棋盘信息棋盘上面的任何一个位置里面可以放置三类信息

  • 空没有落子

  • 玩家1的落子黑子

  • 玩家2的落子白子

2.落子思路

1.如果下一步能赢就走这一步

2.如果下一步会输就阻止对方赢

3.统计棋子数决定在哪里落子。

其中 阻止对方赢就是在对方能赢的点上落子一般会有1到3个点。

3.判断“五子连珠”

下棋就是在二维数组中找对应的空位置进行落子

落完子之后下来就要考虑该落子位置是否有”五子连珠“进而进行输赢判定每一次走棋多会有四种情况

  • 玩家1获胜

  • 玩家2获胜

  • 平局棋盘满了等

  • 未出结果游戏继续其中“未出结果”游戏要继续其他三种情况游戏不用继续

四、项目实现步骤

Ⅰ.创建目录及文件:

1.在Linux环境下创建名为Gobang的文件目录

mkdir Gobang

(目录名随意这里采用五子棋的英文命名目录)

2.创建多文本编辑

touch Makefile

3.分别创建子文件

touch game.h
touch game.c
touch main.c

 Ⅱ、输入Linux环境指令进入目录及文件

Ⅲ、Linux环境编程实现

1.顶层逻辑设计

输入Linux指令

(1)

vim Makefile

(2)

vim game.h

 (3)

vim game.c

(4)

vim main.c

2.模块化设计

第一步. main构建游戏起始逻辑
第二步 . 构建游戏入口 Game() 函数

第三步 . 编写核心函数定义全局游戏信息

第四步 . 编写 Makefifile 完成第一步项目构建

第五步 . 实现游戏落子逻辑

第六步 . 实现游戏的显示逻辑

第七步 . 判定五子连珠然后判定游戏结果

3.联动模块组合封装

完整代码

1game.c

#include "game.h"

/*说明
1.定义全局变量保证代码逻辑尽可能简化传参不那么复杂
2.代表玩家输入位置的最近的位置下标
3.不是说一定要定义成全局变量而是为了减少传参等降低复杂度*/
int x = 0;
int y = 0;

//按照x,y作为起点按照特定的方向求连续相对的最大格式
int ChessCount(int board[][COL], int row, int col, enum Dir d)
{
    int _x = x - 1;     //棋盘行标从1开始
    int _y = y - 1;     //棋盘列标从1开始
    //统计以当前棋子为起始位置8个方向的棋子数
    int count = 0;
    while (1) {
        switch (d) {
        case LEFT:
            _y--;
            break;
        case RIGHT:
            _y++;
            break;
        case UP:
            _x--;
            break;
        case DOWN:
            _x++;
            break;
        case LEFT_UP:
            _x--, _y--;
            break;
        case LEFT_DOWN:
            _x++, _y--;
            break;
        case RIGHT_UP:
            _x--, _y++;
            break;
        case RIGHT_DOWN:
            _x++, _y++;
            break;
        default:
            //Do Nothing
            break;
        }
        //坐标移动完成一定要先保证没有越界
        if (_x < 0 || _x > row - 1 || _y < 0 || _y > col - 1) {
            break;
        }
        //合法
        //判断指定位置和原始位置的棋子是否相同“连珠”就体现在这里
        if (board[x - 1][y - 1] == board[_x][_y]) {
            count++;
        }
        else {
            break;
        }
    }
    return count;
}
/*说明IsOver的返回值有4种情况
1.NEXT:表明要继续
2.PLAYER1_WIN: 玩家1获胜
3.PLAYER2_WIN玩家2获胜
4.DRAW: 平局*/
int IsOver(int board[][COL], int row, int col)
{
    /*棋子统计逻辑
    1.以落子位置作为起点进行8个方向的相同玩家方棋子统计
      落子位置下标与方向统计有很强的关联性
    2.当玩家2落子说明玩家1没有赢因此需要先走一步就先判断一次走到当前才有可能赢
    3.+1是统计被忽略的当前落子*/
    int count1 = ChessCount(board, row, col, LEFT) + ChessCount(board, row, col, RIGHT) + 1; 
    int count2 = ChessCount(board, row, col, UP) + ChessCount(board, row, col, DOWN) + 1; 
    int count3 = ChessCount(board, row, col, LEFT_UP) + ChessCount(board, row, col, RIGHT_DOWN) + 1; 
    int count4 = ChessCount(board, row, col, LEFT_DOWN) + ChessCount(board, row, col, RIGHT_UP) + 1; 
    //判断五子连珠 
    if (count1 >= 5 || count2 >= 5 || count3 >= 5 || count4 >= 5) {
        //有五子连珠则一定有人赢
        //x, y保存的是玩家最近一次落子
        if (board[x - 1][y - 1] == PLAYER1) {
            return PLAYER1_WIN;
        }
        else {
            return PLAYER2_WIN;
        }
        //这个if-else语句可换成 return board[x-1][y-1];
    }
    int i = 0;
    for (; i < row; i++) {
        int j = 0;
        for (; j < col; j++) {
            if (board[i][j] == 0) {
                return NEXT;
            }
        }
    }

    return DRAW;
}

void ShowBoard(int board[][COL], int row, int col)
{ 
    //C语言清屏减少每次棋盘打印实现原地刷新效果
   
    printf("\e[1;1H\e[2J");  //Linux环境下,C语言实现清屏
    printf("\n");
    //将数组内容进行可视化
    printf("  "); //优化棋盘使其美观
    int i = 1;
    for (; i <= col; i++) {
        printf("%3d", i);
    }
    printf("\n");

    for (i = 0; i < row; i++) {
        int j = 0;
        printf("%2d ", i + 1);
        for (; j < col; j++) {
            if (board[i][j] == 0) {
                printf(" . ");
            }
            else if (board[i][j] == PLAYER1) {
                printf("● ");
            }
            else {
                printf("○ ");
            }
        }
        printf("\n");
    }
}

void PlayerMove(int board[][COL], int row, int col, int who)
{
    //判断玩家的落子位置合法性、去重
    while (1) {
        printf("玩家[%d] 请输入你的落子坐标 ", who);
        scanf("%d %d", &x, &y);
        //判断输入坐标的合法性行列
        if (x < 1 || x > row || y < 1 || y > col) {
            printf("位置错误!\n");
            continue;
        }
        //在数组当当中的下标值
        else if (board[x - 1][y - 1] != 0) {
            printf("此位置已被占用!\n");
            continue;
        }
        else {
            //合法性去重
            board[x - 1][y - 1] = who;
            break;
        }
    }
}

void Game()
{ 
    int board[ROW][COL];   //在函数中定义数组其实是一个随机数需要清空以确定坐标数
    /*1.采用ROWCOL型的二维数组进行游戏信息的保存
    2.全部清空有许多种做法采用双循环采用Init函数等                    
    3.注memset按照字节为单位操作menset内存设置
    4.使用memset清空默认棋盘数据都是0*/
    memset(board, 0, sizeof(board));
    int result = NEXT;
    do {
        ShowBoard(board, ROW, COL);          //显示棋盘
        PlayerMove(board, ROW, COL, PLAYER1);//玩家1先走也可以随机选择先走——“先手”
        result = IsOver(board, ROW, COL);    //判断游戏是否结束
        if (NEXT != result) {
            break;
        }
        ShowBoard(board, ROW, COL);
        PlayerMove(board, ROW, COL, PLAYER2);
        result = IsOver(board, ROW, COL);
        if (NEXT != result) {
            break;
        }
    } while (1);
    //Player1 win, Player2 win, Draw
    switch (result) {
         case PLAYER1_WIN:
            printf("恭喜玩家1获胜!\n");
            break;
         case PLAYER2_WIN:
            printf("恭喜玩家2获胜!\n");
             break;
         case DRAW:
            printf("游戏平局和气生财!\n");
             break;
         default:
            //这种情况不会出现do nothing!
            break;
    }
    //ShowBoard(board, ROW, COL);
}

2main.c

#include "game.h"
//游戏菜单
void Menu()
{
    printf("############################\n");
    printf("## 1. 开始        0. 退出 ##\n");
    printf("############################\n");
    printf("                 ————By 作者新晓·故知\n");
    printf("请选择");
}

int main()
{
    int quit = 0;                 //用来判断游戏是否退出
    int select = 0;
    //使得游戏可以多次进行
    while (!quit) {
        Menu();                  //游戏菜单
        scanf("%d", &select);
        switch (select) {        //根据用户选择执行合适的游戏逻辑代码
        case 1:
            Game();
            break;
        case 0:
            quit = 1;
            printf("退出游戏再见!\n");
            break;
        default:
            printf("输入错误请重新输入!\n");
            break;
        }
    }
    return 0;
}

3game.h

#pragma once

#include <stdio.h>
#include <string.h>


//增强代码可维护性
#define ROW 10
#define COL 10

#define PLAYER1 1  //玩家1编号默认棋盘数据是0玩家1落子该位置被改成1
#define PLAYER2 2  //玩家2编号默认棋盘数据是0玩家2落子该位置被改成2

#define NEXT        0   //游戏继续
#define PLAYER1_WIN 1   
#define PLAYER2_WIN 2
#define DRAW        3

//使用枚举代表方向
enum Dir {
    LEFT,
    RIGHT,
    UP,
    DOWN,
    LEFT_UP,
    LEFT_DOWN,
    RIGHT_UP,
    RIGHT_DOWN
};

void Menu();   //菜单功能声明
void Game();   //游戏功能声明

4Makefile

game:game.c main.c ProcBar.c                               
    gcc $^ -o $@ 
 
.PHONY:clean                                             
clean:                                                      
    rm -f game

 五、程序调试

1.程序界面及说明

1进入游戏主程序 

2默认玩家1为先手

 3输入玩家坐标

2.测试用例

1输入错误坐标测试

2输入已被占用坐标测试

3实现“五子连珠”判断

六、程序功能扩展

1.扩展思路

1.引入进度条

2.尝试人机对战
3.功能扩展颜色提示步数记录先手随机交换等
4.网络版本
5. 将走过的步骤保存在文件里实现自动下棋以及扩展至数据库

6.nucurses实现鼠标点击

等等

2.扩展实现

引入进度条

1更改代码

这里我们需要像之前那样创建ProcBar的文件这里就直接在Gobang目录下创建ProcBar项目

touch ProcBar.h
touch ProcBar.c

编辑ProcBar内容

vim ProcBar.h
vim ProcBar.c

 这个内容见博客链接在这篇博客的最后一项Linux第一个小程序——ProcBar进度条<Linux常用开发工具使用yum、vim、gcc/g++、gdb、make/Makefile等>——《Linux》_新晓·故知的博客-CSDN博客

 

在Gobang的主程序里main.c 加入ProcBar的调用

vim main.c

把process_bar()添加到 main.c。

#include "game.h"
//游戏菜单
void Menu()
{
    printf("############################\n");
    printf("## 1. 开始        0. 退出 ##\n");
    printf("############################\n");
    printf("                 ————By 作者新晓·故知\n");
    printf("请选择");
}

int main()
{
    int quit = 0;                 //用来判断游戏是否退出
    int select = 0;

    process_bar();

    //使得游戏可以多次进行
    while (!quit) {
        Menu();                  //游戏菜单
        scanf("%d", &select);
        switch (select) {        //根据用户选择执行合适的游戏逻辑代码
        case 1:
            Game();
            break;
        case 0:
            quit = 1;
            printf("退出游戏再见!\n");
            break;
        default:
            printf("输入错误请重新输入!\n");
            break;
        }
    }
    return 0;
}

然后修改其他地方进行联合编译

Makefile 

game:game.c main.c ProcBar.c
   gcc $^ -o $@

.PHONY : clean
clean :
    rm - f game

main.c

#include "game.h"
#include "ProcBar.h"
//游戏菜单
void Menu()
{
    printf("############################\n");
    printf("## 1. 开始        0. 退出 ##\n");
    printf("############################\n");
    printf("请选择");
}

2扩展效果演示

七、项目总结

1.核心逻辑

1.基于二维数组保存数据信息

2.将二维数组可视化

3.每次落子存入数据都需要进行更新可视化

4.落子和判断是强相关的换句话说玩家1落子的时候说明上一步玩家二没有赢每个玩家落子立即判断输赢

2.项目体现思想

从五子棋项目中体现出对大型项目先进行顶层逻辑设计再将任务模块化最后联动组合封装优化。

八、项目改编

将Linux环境下的C语言项目改编在Windows环境下实现运行借助C语言的跨平台性进行跨平台改动。

后记
●由于作者水平有限文章难免存在谬误之处敬请读者斧正俚语成篇恳望指教
                                                                                    ——By 作者新晓·故知

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux