目录

项目要求

要点分析

gamestart游戏启动函数实现

gamestart函数评价

gameinit游戏初始化函数实现

gameinit初始化函数评价

mapreset地图打乱函数实现

mapreset地图打乱函数评价

mapfree地图销毁函数实现

mapfree地图销毁函数评价

地图打印函数mapprint函数实现

黑白方案

彩色方案

mapprint地图打印函数评价

玩家配对函数match实现

match函数的评价

寻路函数mapfind实现

第一个版本

第一个版本的问题总结

第二个版本

优化后的寻路函数

第二个版本的优点

第二个版本的缺点

第三个版本

第三个版本的优点

第三个版本的缺点

第四个版本

第四个版本的优点

第四个版本的缺点

可解判断函数has solution实现

hassolution函数评价

AI gamer实现

AIGamer函数的评价和补充

整个项目的代码实现

三个文件版本

单个文件版本

项目总结


项目要求

要点分析

需要实现的函数

gamestart 开始界面

gameinit 地图组初始化

mapreset地图打乱函数

mapfree 地图释放(销毁)

mapprint 地图打印

match 玩家配对函数

mapfind 寻路函数

hassolution 判定是否可解函数

AIGamer AI模拟玩家进行游玩的函数

gamestart游戏启动函数实现

主要功能 获得地图的大小(为了拓展游戏的功能 我没有设置难度 而是制作了更高级的自定义地图选项 但是这也导致了没有办法做一个排行榜 因为地图大小是不固定的  我改成了只有三条命的生命次数限制)

另外需要增加几个合法性判断 保证地图有偶数个格子以及一些其他的常规合法判断

void gamestart(map_all* A)
{
    printf("欢迎来到连连看小游戏!\n");
    bool flag = false;
    while (!flag) {
        printf("请输入地图的长和宽 中间用空格分开:\n");
        printf("输入两个0以退出游戏");
        scanf("%d %d", &A->length, &A->width);
        if (A->length == 0 && A->width== 0)return;
        else if (A->length * A->width % 2 != 0) {
            printf("错误 地图的长和宽相乘必须是偶数!\n");
            flag = false;
            Sleep(3000);
            system("cls");
        }
        else if (A->length <= 0 || A->width <= 0) {
            printf("错误 地图的长或宽必须是有效值!\n");
            flag = false;
            Sleep(3000);
            system("cls");
        }
        else if (A->length > 50 || A->width > 50) {
            printf("错误 地图的大小超出了屏幕限制50*50\n");
            flag = false;
            Sleep(3000);
            system("cls");
        }
        else {
            printf("合法的输入 将在三秒后自动生成地图\n");
            flag = true;
        }
    }
    A->life = 3;
    A->count = 0;
    system("cls");
}

gamestart函数评价

直接奠定了我整个游戏的制作方案 虽然舍弃了排行榜但我认为是值得的 增加了地图的拓展性 而且更加全能 如果先要增加难度设置 只要设定参数然后往这个函数里面传递就可以了

gameinit游戏初始化函数实现

主要任务:初始化游戏地图

关键问题:

1.怎么保证寻路函数能够正常运行

2.怎么保证初始化的地图是成对的随机地图

3.怎么保证这个地图有解

实现方法:由于懒得计算字母的ASCII码了 所以直接手打了一个字母映射表

1.对于第一个问题 将游戏地图用一层空格边界包围 极大提高游戏可解性

2.对于第二个问题 先两两输入字母 这样就能保证是成对的

然后将地图里面的字母进行2倍于地图大小次数的随机交换 使整个地图是随机的

3.对于第三个问题 调用hassolution函数进行判断 如果地图无法消去 那就直接重新初始化地图

代码实现如下

void gameinit(map_all* a)
{
    int x = a->length, y = a->width;
    a->map = (char**)malloc((x + 2) * sizeof(char*));
    for (int i = 0; i < x + 2; i++) {
        a->map[i] = (char*)malloc((y + 2) * sizeof(char));
    }
    char alpha[53] = { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" };
    srand((unsigned int)time(NULL));
    for (int i = 0; i < x + 2; i++) {
        for (int j = 0; j < y + 2; j++) {
            a->map[i][j] = ' ';
        }
    }
    int count = 0;
    char c;
    for (int i = 1; i < x + 1; i++) {
        for (int j = 1; j < y + 1; j++) {
            count++;
            if (count % 2 == 1) c = alpha[rand() % 52];
            a->map[i][j] = c;
        }
    }
    for (int i = 0; i < x * y * 2; i++) {
        int r1 = rand() % x + 1;
        int c1 = rand() % y + 1;
        int r2 = rand() % x + 1;
        int c2 = rand() % y + 1;
        char temp = a->map[r1][c1];
        a->map[r1][c1] = a->map[r2][c2];
        a->map[r2][c2] = temp;
    }
}

gameinit初始化函数评价

很意外的一个函数 因为这样生成的地图组几乎不会遇到死局的情况 这让我非常的意外

这个函数是第二个版本 之前掏空心思想写一个保证可以完全消去的函数 但是从来没有成功

但是这个随机的函数却基本都能成功 所以它也就是最终的版本

mapreset地图打乱函数实现

哎呀我都懒得详细介绍 直接将初始化函数里面的打乱部分复制粘贴就行

代码实现如下:

void resetmap(map_all* a)
{
    int x = a->length, y = a->width;
    for (int i = 0; i < x * y * 2; i++) {
        int r1 = rand() % x + 1;
        int c1 = rand() % y + 1;
        int r2 = rand() % x + 1;
        int c2 = rand() % y + 1;
        char temp = a->map[r1][c1];
        a->map[r1][c1] = a->map[r2][c2];
        a->map[r2][c2] = temp;
    }
}

mapreset地图打乱函数评价

CV工程师    (狗头保命

mapfree地图销毁函数实现

嗯销毁虽然重要 但是不难 只要注意这是一个二级指针 我们要轮流释放就行了

代码实现如下:

void mapfree(map_all* a)
{
    for (int i = 0; i < a->length + 2; i++) {
        free(a->map[i]);
    }
    free(a->map);
    free(a->hintarr);
    a->length = a->life = a->width = 0;
}

mapfree地图销毁函数评价

嗯 写这个纯粹是为了保持格式整齐 没什么好评价的

地图打印函数mapprint函数实现

这里就是一个简单的函数 不过我还是进行过一次优化 将黑白的交互界面改成了彩色的交互界面

黑白方案

黑白的界面不必多说 代码实现如下 只要用%2d控制间距 然后每次打印之前清理屏幕就可以了

void mapprint(map_all* a)
{
    system("cls");
    for (int i = 0; i < a->length + 1; i++) {
        printf("%2d", i);
    }
    printf("\n");
    for (int i = 1; i < a->width + 1; i++) {
        printf("%2d", i);
        for (int j = 1; j < a->length + 1; j++) {
            printf("%2c", a->map[j][i]);
        }
        printf("\n");
    }
}

彩色方案

第一行的printf("\033[1;1H");用来将光标回到最开始 和system cls相比 这种做法不会清理屏幕 而是覆盖屏幕 这样就成功解决了屏幕在游玩的时候闪烁的问题

当然这些printf的高级用法都是问的AI 我自己肯定是写不出来的

void mapprint(map_all* a) {
    printf("\033[1;1H");
    for (int i = 0; i < a->length + 1; i++) printf("%2d", i);
    printf("\n");

    for (int i = 1; i <= a->width; i++) {
        printf("%2d", i); 
        for (int j = 1; j <= a->length; j++) {
            char c = a->map[j][i];
            if (c == ' ') {
                printf("  ");//空格要单独处理 不能显示
            }
            else {
                int color = (c % 6) + 31;// 根据字符值动态分配颜色 (char % 6 + 31) 可以让颜色在31-36之间循环
                printf("\033[1;%dm%2c\033[0m", color, c);//加上 \033[%dm 来控制颜色
            }
        }
        printf("\n");
    }
}

mapprint地图打印函数评价

做了一个基础的UI体验 虽然说距离用html 和javascript 写的真正的网页UI还查了十万八千里 但是已经比原来好了很多了

玩家配对函数match实现

要点:输入坐标的合法性判断 输入坐标之前的可解性判断(调用hassolution函数)

让玩家可以选择重排  让玩家可以选择需不需要提示 让玩家可以选择要不要结束游戏等等

代码实现如下

void match(map_all* a)
{
    int wid = a->width;
    int len = a->length;
    int x1, x2, y1, y2;
    printf("地图大小为%d * %d,输入坐标的时候请不要越界!\n", len, wid);
    if (hassolution(a))printf("当前状态:有解\n");
    else {
        if (a->count == a->width * a->length / 2) {
            printf("全部消除完成 游戏结束!\n");
            return;
        }
        int restart = 0;
        printf("当前状态:无解 你要重排地图吗 重排地图请输入1 想要直接结束请输入0\n");
        scanf("%d", &restart);
        if (restart == 1)resetmap(a);
        else {
            printf("游戏结束\n");
            Sleep(1000);
            system("cls");
            a->life = 0;
            return;
        }
    }
    int hint;
    printf("请先观察地图 你想要提示吗? 想要提示请输入1 不想要提示输入0\n");
    printf("也可以输入2来结束游戏\n");
    scanf("%d", &hint);
  
    while (hint != 1 && hint != 0) {
        if (hint == 2) {
            a->life = 0;
            return;
        }
        printf("错误的输入 请重新输入!\n");
        scanf("%d", &hint);
    }
    if (hint == 1) {
        for (int i = 0; i < 4; i++) {
            printf("%d ", a->hintarr[i]);
        }
        printf("\n");
    }
    printf("请输入第一个坐标,中间用空格分开:\n ");
    scanf("%d %d", &x1, &y1);
    if (x1 < 1 || x1 > len || y1 < 1 || y1 > wid) {
        printf("错误:第一个坐标超出地图范围!\n");
        return;
    }
    printf("请输入第二个坐标中间用空格分开: ");
    scanf("%d %d", &x2, &y2);
    if (x2 < 1 || x2 > len || y2 < 1 || y2 > wid) {
        printf("第二个坐标不能越界!\n");
        return;
    }
    if (x1 == x2 && y1 == y2) {
        printf("两次的坐标不能相同!\n");
        return;
    }
    if (a->map[x1][y1] == ' ') {
        printf("第一个位置不能为空\n");
        return;
    }
    if (a->map[x2][y2] == ' ') {
        printf("第二个位置不能为空!\n");
        return;
    }
    if (a->map[x1][y1] != a->map[x2][y2]) {
        printf("需要相同的字符!\n");
        return;
    }
    if (mapfind(a, x1, y1, x2, y2, 0, 0)) {
        printf("成功配对 已经消除!\n");
        a->count++;
        a->map[x1][y1] = ' ';
        a->map[x2][y2] = ' ';
    }
    else {
        a->life--;
        printf("无法连接 请重试!当前剩余生命值%d\n",a->life);
        if (a->life == 0)printf("已经错误三次 游戏结束!\n");
    }
    Sleep(3000);
    system("cls");
}

match函数的评价

几乎没有什么算法上的难点 主要是防止非法的输入导致程序崩溃

函数内部额外设置 "一个你需要提示吗"是有原因的 我在AIgamer中会给出解释

match函数其实修改了很多次 越往后面写就越感觉需要补充一些功能和判断 所以这是一个全局性的函数 或者说衔接各个功能函数的一个函数

寻路函数mapfind实现

第一个版本

1.定义结构体map_all 记录长 宽 剩余要消除的对数 整个地图二维数组的指针

2.定义结构体map_node 记录每个节点的坐标和字符 以及能够延伸到的节点

3.定义顺序表list 记录每个节点通过规则能够延伸到的所有节点

通过map_all 我们可以获得地图的边界 通过map_node我们可以获得每个字符的信息

通过list 我们可以获得每个字符可以连接到的字符范围

我们先想:对于一个节点 我们应该如何找到消去的办法呢?

这里我们传入三个参数map_all*a,map_node*b,list*c;

然后将能够遍历到的每个节点都加入顺序表 最后对顺序表进行检测

实际上这个逻辑相当复杂 而且是错误的 不过我们这里还是给出代码实现

lian.h

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <windows.h>
typedef struct map_all
{
	int length;
	int width;
	char** map;
	int count;
}map_all;
typedef struct map_node
{
	int x;
	int y;
	int turns;
	int direction;
}bfsnode;//记录坐标 拐弯次数 行进的指向
typedef struct list
{
	bfsnode** arr;
	int size;
	int capacity;
}list;//利用顺序表存储所有的bfs节点
void bfsinit(bfsnode* b, int x, int y,int turns,int direction);
void listinit(list* a);
void listpush(list* a, bfsnode* b);
void gamestart(map_all* a);
void gameinit(map_all* a);
void mapprint(map_all* a);
void mapfind(map_all*a,bfsnode*b,list* c,int x2,int y2);
void match(map_all* a);

lian.c

void mapfind(map_all* a, bfsnode* b, list* c,int x2,int y2)
{
    //direction 上 1 右 2 下 3 左 4 无方向 0
    if (b->x != 0 && b->x != a->length+1 && b->y != 0 && b->y != a->width+1) {
        listpush(c, b);
    }
    else return;
    int x = b->x, y = b->y;
    int len = a->length,wid = a->width;
    int dir = b->direction, turn = b->turns;
    if (x != 0 && x != len+1 && y != 0 && y != wid+1&&turn <= 2) {
        bfsnode* newnode = (bfsnode*)malloc(sizeof(newnode));
        if (a->map[x - 1][y] == ' '||(x - 1 == x2&&y == y2)) {
            if (dir != 4)bfsinit(newnode, x - 1, y, turn + 1, 4);
            else bfsinit(newnode, x - 1, y, turn, 4);
            mapfind(a, newnode, c,x2,y2);
        }
        if (a->map[x + 1][y] == ' '||(x+1==x2&&y==y2)) {
            if (dir != 2)bfsinit(newnode, x + 1, y, turn + 1, 2);
            else bfsinit(newnode, x + 1, y, turn, 2);
            mapfind(a, newnode, c, x2, y2);
        }
        if (a->map[x][y+1] == ' '||(x == x2&&y+1==y2)) {
            if (dir != 1)bfsinit(newnode, x, y+1, turn + 1, 1);
            else bfsinit(newnode, x, y+1, turn, 1);
            mapfind(a, newnode, c, x2, y2);
        }
        if (a->map[x][y-1] == ' '||(x == x2&&y-1==y2)) {
            if (dir != 3)bfsinit(newnode, x, y-1, turn + 1, 3);
            else bfsinit(newnode, x, y-1, turn, 3);
            mapfind(a, newnode, c, x2, y2);
        }
    }
}

第一个版本的问题总结

1.寻路没有进行内存释放 递归占用大量空间而且效率低下

2.地图初始化的时候不保证有解

3.因为没能高效实现寻路算法 也就没有写AI助手

接下来我们进行优化:

优化点1:寻路算法底层思路

我们完全没有必要将沿途的每个节点都加入顺序表中 这除了占用大量空间以外没有任何作用 不如我们改变函数的返回值类型 然后检查能否到达检测点即可

所以我编写了第二个版本 如下:

第二个版本

取消了顺序表的设计 改为传入两个坐标 当前行进状态 转弯的次数 还有大地图参数 由于我们经过前面严谨的判断 已经能够保证输入的两个坐标是有效的 所以只要判断x1 y1能够经过两折到达x2 y2即可

这里我们给出核心的寻路函数:

优化后的寻路函数

bool mapfind(map_all* a,int x1, int y1, int x2, int y2, int turn, int face)
{
    //up 1 down 2 left 3 right 4
    //找到了返回true 没找到返回false
    //想一下逻辑 只要是空格就可以走 那么我们还要判定一下当前的格子是不是空格 不是空格直接returnfalse
    int len = a->length, wid = a->width;
    if (x1 == 0 || x1 == len || y1 == 0 || y1 == wid || turn == 3)return false;//这行代码放在前面防止在转弯第三次的时候碰到
    if (x1 == x2 && y1 == y2)return true;//代表寻找到了
    if (a->map[x1][y1] != ' '&&turn!=0)return false;//不是空格无法继续寻路
    if (face != 1) {
        if (mapfind(a, x1, y1 + 1, x2, y2, turn + 1, 1))return true;
    }
    else {
        if (mapfind(a, x1, y1 + 1, x2, y2, turn, 1))return true;
    }
    if (face != 2) {
        if (mapfind(a, x1, y1 - 1, x2, y2, turn + 1, 2))return true;
    }
    else {
        if (mapfind(a, x1, y1 - 1, x2, y2, turn, 2))return true;
    }
    if (face != 3) {
        if (mapfind(a, x1-1, y1, x2, y2, turn + 1, 3))return true;
    }
    else {
        if (mapfind(a, x1-1, y1, x2, y2, turn, 3))return true;
    }
    if (face != 4) {
        if (mapfind(a, x1+1, y1, x2, y2, turn + 1, 4))return true;
    }
    else {
        if (mapfind(a, x1+1, y1, x2, y2, turn, 4))return true;
    }//这里我们确保不同的路径都被搜索到之后 如果都没有return 那就只能return false了所以代码才写的这么冗长 后面看看能不能优化
    return false;
}

第二个版本的优点

省略了复杂的顺序表 减少了空间复杂度 同时寻路逻辑更加清晰

第二个版本的缺点

1.很明显这些if else干的事情是大体类似的 应该有一些方法能够简化这个寻路函数

2.这个寻路算法的效率非常低下 有可能上一步是前进 下一步就是后退 有大量的重复情况

因此 我们给出第三个版本

第三个版本

bool mapfind(map_all* a,int x1, int y1, int x2, int y2, int turn, int face)
{
    //up 1 down 2 left 3 right 4
    //找到了返回true 没找到返回false
    //想一下逻辑 只要是空格就可以走 那么我们还要判定一下当前的格子是不是空格 不是空格直接returnfalse
    int len = a->length, wid = a->width;
    if (x1 == 0 || x1 == len || y1 == 0 || y1 == wid || turn > 3)return false;//这行代码放在前面防止在转弯第三次的时候碰到
    if (x1 == x2 && y1 == y2)return true;//代表寻找到了
    if (a->map[x1][y1] != ' '&&turn!=0)return false;//不是空格无法继续寻路
    if (face != 2) {
        if (face != 1) {
            if (mapfind(a, x1, y1 + 1, x2, y2, turn + 1, 1))return true;
        }
        else {
            if (mapfind(a, x1, y1 + 1, x2, y2, turn, 1))return true;
        }
    }
    if (face != 1) {
        if (face != 2) {
            if (mapfind(a, x1, y1 - 1, x2, y2, turn + 1, 2))return true;
        }
        else {
            if (mapfind(a, x1, y1 - 1, x2, y2, turn, 2))return true;
        }
    }
    if (face != 4) {
        if (face != 3) {
            if (mapfind(a, x1 - 1, y1, x2, y2, turn + 1, 3))return true;
        }
        else {
            if (mapfind(a, x1 - 1, y1, x2, y2, turn, 3))return true;
        }
    }
    if (face != 3) {
        if (face != 4) {
            if (mapfind(a, x1 + 1, y1, x2, y2, turn + 1, 4))return true;
        }
        else {
            if (mapfind(a, x1 + 1, y1, x2, y2, turn, 4))return true;
        }//这里我们确保不同的路径都被搜索到之后 如果都没有return 那就只能return false了所以代码才写的这么冗长 后面看看能不能优化
    }
    return false;
}

第三个版本的优点

通过增加if判断防止回头 寻路算法效率明显提升

第三个版本的缺点

代码重复问题没有解决

第四个版本

bool mapfind(map_all* a,int x1, int y1, int x2, int y2, int turn, int face)
{
    //up 1 down 2 left 3 right 4
    //找到了返回true 没找到返回false
    //想一下逻辑 只要是空格就可以走 那么我们还要判定一下当前的格子是不是空格 不是空格直接returnfalse
    int len = a->length, wid = a->width;
    if (x1 == 0 || x1 == len || y1 == 0 || y1 == wid || turn > 3)return false;//这行代码放在前面防止在转弯第三次的时候碰到
    if (x1 == x2 && y1 == y2)return true;//代表寻找到了
    if (a->map[x1][y1] != ' '&&turn!=0)return false;//不是空格无法继续寻路
    int dx[] = { 0, 0, 0, -1, 1 };
    int dy[] = { 0, 1, -1, 0, 0 };
    int opposite[] = { 0, 2, 1, 4, 3 }; // 记录每个方向的对立面
    for (int i = 1; i <= 4; i++) {
        if (i == opposite[face]) continue; // 一行代码搞定“禁止回头”
        int next_turn = (face != 0 && i != face) ? turn + 1 : turn;
        if (mapfind(a, x1 + dx[i], y1 + dy[i], x2, y2, next_turn, i)) return true;
    }
    return false;
}

学习了一个简化代码的方法:映射表

建立了上下左右分别对应的x数组和y数组

搭配for循环进行使用 我们解决了上下左右代码重复的问题

通过一个nextturn变量 一行代码解决turn的增加问题

通过opposite数组进行判断 一行代码解决回头问题

第四个版本的优点

代码非常的简短高效 也是我这个算法的最终版本

第四个版本的缺点

局限于转弯两次 实际上因为剪枝不完全 仍然有重复 当turn较大时 递归开创的函数栈帧应该相当庞大 有导致程序崩溃的风险 但是在当前连连看小游戏的条件下 经过测试 50×50的地图组下运行正常

可能会有更高级的算法 但是没想出来

核心函数实现完成 接下来我们按部就班 实现has solution函数 就能判断游戏是否可解

可解判断函数has solution实现

非常暴力啊 把所有字符相同 坐标不同的所有非空格坐标组全都遍历一遍 只要有一个mapfind返回true就有解 要不然就无解

由于字母表一共有52个字母 这两个前置判断能够将整个算法的效率提升95%以上 因此 即使有4个if循环 真正调用寻路函数的次数并不多

代码实现如下:

bool hassolution(map_all* a)
{
    //这个直接暴力遍历 对于10*10以内大小的地图 暴力遍历也是绰绰有余 对于更大的 估计也没人玩(=_=)
    for (int i = 1; i <= a->length; i++) {
        for (int j = 1; j <= a->width; j++) {
            for (int k = 1; k <= a->length; k++) {
                for (int l = 1; l <= a->width; l++) {
                    if (a->map[i][j] != a->map[k][l])continue;
                    if (i == k && j == l)continue;//防止坐标重复
                    if (a->map[i][j] == ' ' || a->map[k][l] == ' ')continue;//跳过空格检测
                    if (mapfind(a, i, j, k, l, 0, 0)) {
                        a->hintarr[0] = i; a->hintarr[1] = j;
                        a->hintarr[2] = k; a->hintarr[3] = l;
                        return true;
                    }
                }
            }
        }
    }
    return false;
}

hassolution函数评价

优点:额 思路非常的简单 对于连连看这种小游戏也是戳戳有余

缺点:复杂度很高

AI gamer实现

为了设计这个AIgamer 我在match函数中特地预留了一个hint选项 是否需要提示 实际上是存储了一对可以消去的坐标 更加聪明的是 我将这个坐标存储在了map_all结构体中 

所以 只需要将这两个坐标的字符替换成空格 完成一次消去 然后进行mapprint  接下来再调用hassolution更新hint数组 然后重复以上流程即可

AIgamer的聪明程度完全取决于你写的寻路函数的复杂程度 

下面我来给出实现的代码

void AIGamer(map_all* a)
{
    //思路:我们设置了hint数组 就已经很好办了 先消去hint数组 然后进行检测 如果hassolution为真 那就重复 直到hassolution为假
    //我们甚至可以做一个AIGamer_visible 只需要将前面函数复制过来 然后输入的部分全部改成直接输入数组内容就可以了
    //不过为了不影响玩家的游戏体验 我们需要先复制地图进行操作 方便玩家在同一张地图上进行游戏
    //不对 我们直接在玩家失败或者放弃之后 跑一遍AIgamer_visible就可以了
    //那就只做一个AIgamer 然后在原来的地图上操作
    while (hassolution(a)) {
        int x1 = a->hintarr[0], y1 = a->hintarr[1], x2 = a->hintarr[2], y2 = a->hintarr[3];
        a->map[x1][y1] = ' ';
        a->map[x2][y2] = ' ';
        a->count++;
        mapprint(a);
        printf("已经消除了%d次", a->count);
        Sleep(500);
    }
    if (a->count != a->length * a->width / 2) {
        printf("遇到了死局 即将自动刷新棋盘!\n");
        Sleep(3000);
        system("cls");
        resetmap(a);
        AIGamer(a);
    }
    printf("共计消除了%d次 游戏结束\n", a->count);
}

AIGamer函数的评价和补充

实现AIgamer实际上非常简单 但是利用AIgamer进行测试的时候 我发现了一些很有意思的现象:

地图几乎总是可解(完全消去 几乎没有调用resetmap函数的情况)

整个项目的代码实现

三个文件版本

lian.h

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <windows.h>
typedef struct map_all
{
	int count;
	int length;
	int width;
	char** map;
	int life;
	int* hintarr;
}map_all;
void gamestart(map_all* a);
void gameinit(map_all* a);
void mapprint(map_all* a);
bool hassolution(map_all* a);
bool mapfind(map_all*a,int x1, int y1, int x2, int y2, int turn, int face);
void match(map_all* a);
void mapfree(map_all* a);
void AIGamer(map_all* a);
void resetmap(map_all* a);

lian.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "lian.h"
void gamestart(map_all* A)
{
    printf("欢迎来到连连看小游戏!\n");
    bool flag = false;
    while (!flag) {
        printf("请输入地图的长和宽 中间用空格分开:\n");
        printf("输入两个0以退出游戏");
        scanf("%d %d", &A->length, &A->width);
        if (A->length == 0 && A->width== 0)return;
        else if (A->length * A->width % 2 != 0) {
            printf("错误 地图的长和宽相乘必须是偶数!\n");
            flag = false;
            Sleep(3000);
            system("cls");
        }
        else if (A->length <= 0 || A->width <= 0) {
            printf("错误 地图的长或宽必须是有效值!\n");
            flag = false;
            Sleep(3000);
            system("cls");
        }
        else if (A->length > 50 || A->width > 50) {
            printf("错误 地图的大小超出了屏幕限制50*50\n");
            flag = false;
            Sleep(3000);
            system("cls");
        }
        else {
            printf("合法的输入 将在三秒后自动生成地图\n");
            flag = true;
        }
    }
    A->life = 3;
    A->count = 0;
    system("cls");
}
void gameinit(map_all* a)
{
    int x = a->length, y = a->width;
    a->map = (char**)malloc((x + 2) * sizeof(char*));
    for (int i = 0; i < x + 2; i++) {
        a->map[i] = (char*)malloc((y + 2) * sizeof(char));
    }
    char alpha[53] = { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" };
    srand((unsigned int)time(NULL));
    for (int i = 0; i < x + 2; i++) {
        for (int j = 0; j < y + 2; j++) {
            a->map[i][j] = ' ';
        }
    }
    int count = 0;
    char c;
    for (int i = 1; i < x + 1; i++) {
        for (int j = 1; j < y + 1; j++) {
            count++;
            if (count % 2 == 1) c = alpha[rand() % 52];
            a->map[i][j] = c;
        }
    }
    for (int i = 0; i < x * y * 2; i++) {
        int r1 = rand() % x + 1;
        int c1 = rand() % y + 1;
        int r2 = rand() % x + 1;
        int c2 = rand() % y + 1;
        char temp = a->map[r1][c1];
        a->map[r1][c1] = a->map[r2][c2];
        a->map[r2][c2] = temp;
    }
}
//void mapprint(map_all* a)
//{
//    system("cls");
//    for (int i = 0; i < a->length + 1; i++) {
//        printf("%2d", i);
//    }
//    printf("\n");
//    for (int i = 1; i < a->width + 1; i++) {
//        printf("%2d", i);
//        for (int j = 1; j < a->length + 1; j++) {
//            printf("%2c", a->map[j][i]);
//        }
//        printf("\n");
//    }
//}
void mapprint(map_all* a) {
    printf("\033[1;1H");
    for (int i = 0; i < a->length + 1; i++) printf("%2d", i);
    printf("\n");

    for (int i = 1; i <= a->width; i++) {
        printf("%2d", i); 
        for (int j = 1; j <= a->length; j++) {
            char c = a->map[j][i];
            if (c == ' ') {
                printf("  ");//空格要单独处理 不能显示
            }
            else {
                int color = (c % 6) + 31;// 根据字符值动态分配颜色 (char % 6 + 31) 可以让颜色在31-36之间循环
                printf("\033[1;%dm%2c\033[0m", color, c);//加上 \033[%dm 来控制颜色
            }
        }
        printf("\n");
    }
}
void resetmap(map_all* a)
{
    int x = a->length, y = a->width;
    for (int i = 0; i < x * y * 2; i++) {
        int r1 = rand() % x + 1;
        int c1 = rand() % y + 1;
        int r2 = rand() % x + 1;
        int c2 = rand() % y + 1;
        char temp = a->map[r1][c1];
        a->map[r1][c1] = a->map[r2][c2];
        a->map[r2][c2] = temp;
    }
}
void match(map_all* a)
{
    int wid = a->width;
    int len = a->length;
    int x1, x2, y1, y2;
    printf("地图大小为%d * %d,输入坐标的时候请不要越界!\n", len, wid);
    if (hassolution(a))printf("当前状态:有解\n");
    else {
        if (a->count == a->width * a->length / 2) {
            printf("全部消除完成 游戏结束!\n");
            return;
        }
        int restart = 0;
        printf("当前状态:无解 你要重排地图吗 重排地图请输入1 想要直接结束请输入0\n");
        scanf("%d", &restart);
        if (restart == 1)resetmap(a);
        else {
            printf("游戏结束\n");
            Sleep(1000);
            system("cls");
            a->life = 0;
            return;
        }
    }
    int hint;
    printf("请先观察地图 你想要提示吗? 想要提示请输入1 不想要提示输入0\n");
    printf("也可以输入2来结束游戏\n");
    scanf("%d", &hint);
  
    while (hint != 1 && hint != 0) {
        if (hint == 2) {
            a->life = 0;
            return;
        }
        printf("错误的输入 请重新输入!\n");
        scanf("%d", &hint);
    }
    if (hint == 1) {
        for (int i = 0; i < 4; i++) {
            printf("%d ", a->hintarr[i]);
        }
        printf("\n");
    }
    printf("请输入第一个坐标,中间用空格分开:\n ");
    scanf("%d %d", &x1, &y1);
    if (x1 < 1 || x1 > len || y1 < 1 || y1 > wid) {
        printf("错误:第一个坐标超出地图范围!\n");
        return;
    }
    printf("请输入第二个坐标中间用空格分开: ");
    scanf("%d %d", &x2, &y2);
    if (x2 < 1 || x2 > len || y2 < 1 || y2 > wid) {
        printf("第二个坐标不能越界!\n");
        return;
    }
    if (x1 == x2 && y1 == y2) {
        printf("两次的坐标不能相同!\n");
        return;
    }
    if (a->map[x1][y1] == ' ') {
        printf("第一个位置不能为空\n");
        return;
    }
    if (a->map[x2][y2] == ' ') {
        printf("第二个位置不能为空!\n");
        return;
    }
    if (a->map[x1][y1] != a->map[x2][y2]) {
        printf("需要相同的字符!\n");
        return;
    }
    if (mapfind(a, x1, y1, x2, y2, 0, 0)) {
        printf("成功配对 已经消除!\n");
        a->count++;
        a->map[x1][y1] = ' ';
        a->map[x2][y2] = ' ';
    }
    else {
        a->life--;
        printf("无法连接 请重试!当前剩余生命值%d\n",a->life);
        if (a->life == 0)printf("已经错误三次 游戏结束!\n");
    }
    Sleep(3000);
    system("cls");
}
bool hassolution(map_all* a)
{
    //这个直接暴力遍历 对于10*10以内大小的地图 暴力遍历也是绰绰有余 对于更大的 估计也没人玩(=_=)
    for (int i = 1; i <= a->length; i++) {
        for (int j = 1; j <= a->width; j++) {
            for (int k = 1; k <= a->length; k++) {
                for (int l = 1; l <= a->width; l++) {
                    if (a->map[i][j] != a->map[k][l])continue;
                    if (i == k && j == l)continue;//防止坐标重复
                    if (a->map[i][j] == ' ' || a->map[k][l] == ' ')continue;//跳过空格检测
                    if (mapfind(a, i, j, k, l, 0, 0)) {
                        a->hintarr[0] = i; a->hintarr[1] = j;
                        a->hintarr[2] = k; a->hintarr[3] = l;
                        return true;
                    }
                }
            }
        }
    }
    return false;
}
bool mapfind(map_all* a,int x1, int y1, int x2, int y2, int turn, int face)
{
    //up 1 down 2 left 3 right 4
    //找到了返回true 没找到返回false
    //想一下逻辑 只要是空格就可以走 那么我们还要判定一下当前的格子是不是空格 不是空格直接returnfalse
    int len = a->length, wid = a->width;
    if (x1 < 0 || x1 > len + 1 || y1 < 0 || y1 > wid + 1 || turn > 3)return false;//这行代码放在前面防止在转弯第三次的时候碰到
    if (x1 == x2 && y1 == y2)return true;//代表寻找到了
    if (a->map[x1][y1] != ' '&&turn!=0)return false;//不是空格无法继续寻路
    int dx[] = { 0, 0, 0, -1, 1 };
    int dy[] = { 0, 1, -1, 0, 0 };
    int opposite[] = { 0, 2, 1, 4, 3 }; // 记录每个方向的对立面
    for (int i = 1; i <= 4; i++) {
        if (i == opposite[face]) continue; // 一行代码搞定“禁止回头”
        int next_turn = (face != 0 && i != face) ? turn + 1 : turn;
        if (mapfind(a, x1 + dx[i], y1 + dy[i], x2, y2, next_turn, i)) return true;
    }
    return false;
}
void mapfree(map_all* a)
{
    for (int i = 0; i < a->length + 2; i++) {
        free(a->map[i]);
    }
    free(a->map);
    free(a->hintarr);
    a->length = a->life = a->width = 0;
}
void AIGamer(map_all* a)
{
    //思路:我们设置了hint数组 就已经很好办了 先消去hint数组 然后进行检测 如果hassolution为真 那就重复 直到hassolution为假
    //我们甚至可以做一个AIGamer_visible 只需要将前面函数复制过来 然后输入的部分全部改成直接输入数组内容就可以了
    //不过为了不影响玩家的游戏体验 我们需要先复制地图进行操作 方便玩家在同一张地图上进行游戏
    //不对 我们直接在玩家失败或者放弃之后 跑一遍AIgamer_visible就可以了
    //那就只做一个AIgamer 然后在原来的地图上操作
    while (hassolution(a)) {
        int x1 = a->hintarr[0], y1 = a->hintarr[1], x2 = a->hintarr[2], y2 = a->hintarr[3];
        a->map[x1][y1] = ' ';
        a->map[x2][y2] = ' ';
        a->count++;
        mapprint(a);
        printf("已经消除了%d次", a->count);
        Sleep(500);
    }
    if (a->count != a->length * a->width / 2) {
        printf("遇到了死局 即将自动刷新棋盘!\n");
        Sleep(3000);
        system("cls");
        resetmap(a);
        AIGamer(a);
    }
    printf("共计消除了%d次 游戏结束\n", a->count);
}

test.c

#define _CRT_SECURE_NO_WARNINGS 
#include "lian.h"
int main() 
{
	int restart = 1;
	while (restart) {
		map_all a;
		a.hintarr = (int*)calloc(4, sizeof(int));
		a.length = a.width = 0;
		gamestart(&a);
		if (a.length == a.width && a.width == 0) {
			printf("游戏已经退出\n");
			return 0;
		}
		gameinit(&a);
		while (!hassolution(&a)) {
			mapfree(&a);
			gameinit(&a);
			a.hintarr = (int*)calloc(4, sizeof(int));
		}//保证可解
		while (hassolution(&a) && a.life != 0) {
			mapprint(&a);
			match(&a);
		}
		printf("AIGamer已经启动!将为您清理剩下的所有连连看!\n");
		Sleep(3000);
		system("cls");
		if (hassolution(&a))AIGamer(&a);
		mapfree(&a);
		sign:
		printf("想要再来一把吗 输入1来再开一把 输入0退出游戏\n");
		scanf("%d", &restart);
		if (restart != 1 && restart != 0) {
			printf("错误的输入 请重新选择!\n");
			goto sign;//太晚了  我不管了 直接写面条程序
		}
	}
	return 0;
}

单个文件版本

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <windows.h>
typedef struct map_all
{
	int count;
	int length;
	int width;
	char** map;
	int life;
	int* hintarr;
}map_all;
void gamestart(map_all* a);
void gameinit(map_all* a);
void mapprint(map_all* a);
bool hassolution(map_all* a);
bool mapfind(map_all*a,int x1, int y1, int x2, int y2, int turn, int face);
void match(map_all* a);
void mapfree(map_all* a);
void AIGamer(map_all* a);
void resetmap(map_all* a);
void gamestart(map_all* A)
{
    printf("欢迎来到连连看小游戏!\n");
    bool flag = false;
    while (!flag) {
        printf("请输入地图的长和宽 中间用空格分开:\n");
        printf("输入两个0以退出游戏");
        scanf("%d %d", &A->length, &A->width);
        if (A->length == 0 && A->width== 0)return;
        else if (A->length * A->width % 2 != 0) {
            printf("错误 地图的长和宽相乘必须是偶数!\n");
            flag = false;
            Sleep(3000);
            system("cls");
        }
        else if (A->length <= 0 || A->width <= 0) {
            printf("错误 地图的长或宽必须是有效值!\n");
            flag = false;
            Sleep(3000);
            system("cls");
        }
        else if (A->length > 50 || A->width > 50) {
            printf("错误 地图的大小超出了屏幕限制50*50\n");
            flag = false;
            Sleep(3000);
            system("cls");
        }
        else {
            printf("合法的输入 将在三秒后自动生成地图\n");
            flag = true;
        }
    }
    A->life = 3;
    A->count = 0;
    system("cls");
}
void gameinit(map_all* a)
{
    int x = a->length, y = a->width;
    a->map = (char**)malloc((x + 2) * sizeof(char*));
    for (int i = 0; i < x + 2; i++) {
        a->map[i] = (char*)malloc((y + 2) * sizeof(char));
    }
    char alpha[53] = { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" };
    srand((unsigned int)time(NULL));
    for (int i = 0; i < x + 2; i++) {
        for (int j = 0; j < y + 2; j++) {
            a->map[i][j] = ' ';
        }
    }
    int count = 0;
    char c;
    for (int i = 1; i < x + 1; i++) {
        for (int j = 1; j < y + 1; j++) {
            count++;
            if (count % 2 == 1) c = alpha[rand() % 52];
            a->map[i][j] = c;
        }
    }
    for (int i = 0; i < x * y * 2; i++) {
        int r1 = rand() % x + 1;
        int c1 = rand() % y + 1;
        int r2 = rand() % x + 1;
        int c2 = rand() % y + 1;
        char temp = a->map[r1][c1];
        a->map[r1][c1] = a->map[r2][c2];
        a->map[r2][c2] = temp;
    }
}
//void mapprint(map_all* a)
//{
//    system("cls");
//    for (int i = 0; i < a->length + 1; i++) {
//        printf("%2d", i);
//    }
//    printf("\n");
//    for (int i = 1; i < a->width + 1; i++) {
//        printf("%2d", i);
//        for (int j = 1; j < a->length + 1; j++) {
//            printf("%2c", a->map[j][i]);
//        }
//        printf("\n");
//    }
//}
void mapprint(map_all* a) {
    printf("\033[1;1H");
    for (int i = 0; i < a->length + 1; i++) printf("%2d", i);
    printf("\n");

    for (int i = 1; i <= a->width; i++) {
        printf("%2d", i); 
        for (int j = 1; j <= a->length; j++) {
            char c = a->map[j][i];
            if (c == ' ') {
                printf("  ");//空格要单独处理 不能显示
            }
            else {
                int color = (c % 6) + 31;// 根据字符值动态分配颜色 (char % 6 + 31) 可以让颜色在31-36之间循环
                printf("\033[1;%dm%2c\033[0m", color, c);//加上 \033[%dm 来控制颜色
            }
        }
        printf("\n");
    }
}
void resetmap(map_all* a)
{
    int x = a->length, y = a->width;
    for (int i = 0; i < x * y * 2; i++) {
        int r1 = rand() % x + 1;
        int c1 = rand() % y + 1;
        int r2 = rand() % x + 1;
        int c2 = rand() % y + 1;
        char temp = a->map[r1][c1];
        a->map[r1][c1] = a->map[r2][c2];
        a->map[r2][c2] = temp;
    }
}
void match(map_all* a)
{
    int wid = a->width;
    int len = a->length;
    int x1, x2, y1, y2;
    printf("地图大小为%d * %d,输入坐标的时候请不要越界!\n", len, wid);
    if (hassolution(a))printf("当前状态:有解\n");
    else {
        if (a->count == a->width * a->length / 2) {
            printf("全部消除完成 游戏结束!\n");
            return;
        }
        int restart = 0;
        printf("当前状态:无解 你要重排地图吗 重排地图请输入1 想要直接结束请输入0\n");
        scanf("%d", &restart);
        if (restart == 1)resetmap(a);
        else {
            printf("游戏结束\n");
            Sleep(1000);
            system("cls");
            a->life = 0;
            return;
        }
    }
    int hint;
    printf("请先观察地图 你想要提示吗? 想要提示请输入1 不想要提示输入0\n");
    printf("也可以输入2来结束游戏\n");
    scanf("%d", &hint);
  
    while (hint != 1 && hint != 0) {
        if (hint == 2) {
            a->life = 0;
            return;
        }
        printf("错误的输入 请重新输入!\n");
        scanf("%d", &hint);
    }
    if (hint == 1) {
        for (int i = 0; i < 4; i++) {
            printf("%d ", a->hintarr[i]);
        }
        printf("\n");
    }
    printf("请输入第一个坐标,中间用空格分开:\n ");
    scanf("%d %d", &x1, &y1);
    if (x1 < 1 || x1 > len || y1 < 1 || y1 > wid) {
        printf("错误:第一个坐标超出地图范围!\n");
        return;
    }
    printf("请输入第二个坐标中间用空格分开: ");
    scanf("%d %d", &x2, &y2);
    if (x2 < 1 || x2 > len || y2 < 1 || y2 > wid) {
        printf("第二个坐标不能越界!\n");
        return;
    }
    if (x1 == x2 && y1 == y2) {
        printf("两次的坐标不能相同!\n");
        return;
    }
    if (a->map[x1][y1] == ' ') {
        printf("第一个位置不能为空\n");
        return;
    }
    if (a->map[x2][y2] == ' ') {
        printf("第二个位置不能为空!\n");
        return;
    }
    if (a->map[x1][y1] != a->map[x2][y2]) {
        printf("需要相同的字符!\n");
        return;
    }
    if (mapfind(a, x1, y1, x2, y2, 0, 0)) {
        printf("成功配对 已经消除!\n");
        a->count++;
        a->map[x1][y1] = ' ';
        a->map[x2][y2] = ' ';
    }
    else {
        a->life--;
        printf("无法连接 请重试!当前剩余生命值%d\n",a->life);
        if (a->life == 0)printf("已经错误三次 游戏结束!\n");
    }
    Sleep(3000);
    system("cls");
}
bool hassolution(map_all* a)
{
    //这个直接暴力遍历 对于10*10以内大小的地图 暴力遍历也是绰绰有余 对于更大的 估计也没人玩(=_=)
    for (int i = 1; i <= a->length; i++) {
        for (int j = 1; j <= a->width; j++) {
            for (int k = 1; k <= a->length; k++) {
                for (int l = 1; l <= a->width; l++) {
                    if (a->map[i][j] != a->map[k][l])continue;
                    if (i == k && j == l)continue;//防止坐标重复
                    if (a->map[i][j] == ' ' || a->map[k][l] == ' ')continue;//跳过空格检测
                    if (mapfind(a, i, j, k, l, 0, 0)) {
                        a->hintarr[0] = i; a->hintarr[1] = j;
                        a->hintarr[2] = k; a->hintarr[3] = l;
                        return true;
                    }
                }
            }
        }
    }
    return false;
}
bool mapfind(map_all* a,int x1, int y1, int x2, int y2, int turn, int face)
{
    //up 1 down 2 left 3 right 4
    //找到了返回true 没找到返回false
    //想一下逻辑 只要是空格就可以走 那么我们还要判定一下当前的格子是不是空格 不是空格直接returnfalse
    int len = a->length, wid = a->width;
    if (x1 < 0 || x1 > len + 1 || y1 < 0 || y1 > wid + 1 || turn > 3)return false;//这行代码放在前面防止在转弯第三次的时候碰到
    if (x1 == x2 && y1 == y2)return true;//代表寻找到了
    if (a->map[x1][y1] != ' '&&turn!=0)return false;//不是空格无法继续寻路
    int dx[] = { 0, 0, 0, -1, 1 };
    int dy[] = { 0, 1, -1, 0, 0 };
    int opposite[] = { 0, 2, 1, 4, 3 }; // 记录每个方向的对立面
    for (int i = 1; i <= 4; i++) {
        if (i == opposite[face]) continue; // 一行代码搞定“禁止回头”
        int next_turn = (face != 0 && i != face) ? turn + 1 : turn;
        if (mapfind(a, x1 + dx[i], y1 + dy[i], x2, y2, next_turn, i)) return true;
    }
    return false;
}
void mapfree(map_all* a)
{
    for (int i = 0; i < a->length + 2; i++) {
        free(a->map[i]);
    }
    free(a->map);
    free(a->hintarr);
    a->length = a->life = a->width = 0;
}
void AIGamer(map_all* a)
{
    //思路:我们设置了hint数组 就已经很好办了 先消去hint数组 然后进行检测 如果hassolution为真 那就重复 直到hassolution为假
    //我们甚至可以做一个AIGamer_visible 只需要将前面函数复制过来 然后输入的部分全部改成直接输入数组内容就可以了
    //不过为了不影响玩家的游戏体验 我们需要先复制地图进行操作 方便玩家在同一张地图上进行游戏
    //不对 我们直接在玩家失败或者放弃之后 跑一遍AIgamer_visible就可以了
    //那就只做一个AIgamer 然后在原来的地图上操作
    while (hassolution(a)) {
        int x1 = a->hintarr[0], y1 = a->hintarr[1], x2 = a->hintarr[2], y2 = a->hintarr[3];
        a->map[x1][y1] = ' ';
        a->map[x2][y2] = ' ';
        a->count++;
        mapprint(a);
        printf("已经消除了%d次", a->count);
        Sleep(500);
    }
    if (a->count != a->length * a->width / 2) {
        printf("遇到了死局 即将自动刷新棋盘!\n");
        Sleep(3000);
        system("cls");
        resetmap(a);
        AIGamer(a);
    }
    printf("共计消除了%d次 游戏结束\n", a->count);
}
int main() 
{
	int restart = 1;
	while (restart) {
		map_all a;
		a.hintarr = (int*)calloc(4, sizeof(int));
		a.length = a.width = 0;
		gamestart(&a);
		if (a.length == a.width && a.width == 0) {
			printf("游戏已经退出\n");
			return 0;
		}
		gameinit(&a);
		while (!hassolution(&a)) {
			mapfree(&a);
			gameinit(&a);
			a.hintarr = (int*)calloc(4, sizeof(int));
		}//保证可解
		while (hassolution(&a) && a.life != 0) {
			mapprint(&a);
			match(&a);
		}
		printf("AIGamer已经启动!将为您清理剩下的所有连连看!\n");
		Sleep(3000);
		system("cls");
		if (hassolution(&a))AIGamer(&a);
		mapfree(&a);
		sign:
		printf("想要再来一把吗 输入1来再开一把 输入0退出游戏\n");
		scanf("%d", &restart);
		if (restart != 1 && restart != 0) {
			printf("错误的输入 请重新选择!\n");
			goto sign;//太晚了  我不管了 直接写面条程序
		}
	}
	return 0;
}

项目总结

这个实现这个项目大概用了300行代码 极大的巩固了我C语言的知识 同时第一次打出了一个简单的广度优先遍历 基本完成了郭老师的寒假任务  自己看看那个AIgamer玩也是非常好玩 之后将会全力投入C++的学习

这几天都没有发博客(过年了也是懒完了 加上做这个实在是很耗时间)祝大家新年快乐!]

谢谢阅读!!!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐