说在前面

🎈相信大家对于连连看这款游戏都不陌生了吧?还记得在我小时候,有一段时间周边的人都被这游戏给吸引了,那时候我就在想,我点了两个图片,它怎么知道能不能连线,还有明明有很多条路线可以走,为什么就要走那一条?直到后来我学习了BFS算法

体验地址

大家可以先到体验地址试玩一下,欢迎大家的意见反馈。
JYeontuGame在线体验地址:http://jyeontu.xyz/JYeontuGame/#/

jyeontu - Google Chrome 2022-04-13 11-08-38 00_00_00-00_00_30.gif

项目介绍

本游戏是基于vue2.0的一个项目,服务端使用node简单的做了两个接口,数据库则是使用的mysql。

功能实现

1、初始化页面

使用js动态生成连连看看板格子。

//初始化页面
initPage() {
    const row = this.row,
        column = this.column;
    const content = document.getElementById("game-content");
    content.innerHTML = "";
    for (let i = 0; i <= parseInt(column) + 1; i++) {
        const columnDom = document.createElement("div");
        columnDom.classList.add("column");
        columnDom.id = `column-${i}`;
        for (let j = 0; j <= parseInt(row) + 1; j++) {
            const rowDom = document.createElement("div");
            rowDom.classList.add("row");
            rowDom.id = `row-${i}-${j}`;
            const img = document.createElement("img");
            img.id = `img-${i}-${j}`;
            img.classList.add("img-block");
            if (i == 0 || j == 0 || i == column + 1 || j == row + 1) {
                img &&
                    img.setAttribute(
                        "src",
                        require("./img/remove.png")
                    );
            } else {
                img &&
                    img.setAttribute(
                        "src",
                        this.blockList[(i - 1) * row + j - 1]
                    );
                img.onclick = () => {
                    this.imgClick(i, j);
                };
            }
            rowDom.appendChild(img);
            columnDom.appendChild(rowDom);
        }
        content.appendChild(columnDom);
    }
    for (let i = 1; i <= column; i++) {
        for (let j = 1; j <= row; j++) {
            const img = document.getElementById(i + "-" + j);
            img &&
                img.setAttribute(
                    "src",
                    this.blockList[(i - 1) * row + j - 1]
                );
        }
    }
},

2、图片数组初始化

根据行列数初始化图片数组。

//初始化数据
initData() {
    const row = this.row;
    const column = this.column;
    const imgList = this.imgList;
    let blockList = this.blockList;
    this.blockMap = new Array(column + 2);
    for (let i = 0; i < this.blockMap.length; i++) {
        let temp = [];
        for (let j = 0; j < row + 2; j++) {
            if (i == 0 || j == 0 || i == column + 1 || j == row + 1)
                temp.push(true);
            else temp.push(false);
        }
        this.blockMap[i] = temp;
    }

    let nums = row * column;
    if (nums % 2 == 1) {
        alert("个数不能为单数");
        return;
    }
    while (nums / 2 > blockList.length) {
        const dif = nums / 2 - blockList.length;
        blockList.push(...imgList.slice(0, dif));
    }
    blockList.push(...blockList);
    blockList = this.randomSort(blockList);
},
//数组打乱
randomSort(arr) {
    return arr.sort((a, b) => {
        return Math.random() - 0.5;
    });
},

3、BFS判断图片是否可以消除

判断两点之间是否存在可通行路径的方法有两种,dfs和bfs,我们需要找到两点之间的最短路径,所以这里使用bfs算法来寻找路径。
关于bfs和dfs算法我之前也有发过一篇文章,这里我就不再过多描述这个算法的实现了,有兴趣的同学可以去一文带你了解dfs和bfs算法了解一下。

//BFS找出路径
getPath(startX, startY, targetX, targetY) {
    let dx = [0, 1, 0, -1],
        dy = [1, 0, -1, 0];
    let queue = [[startX, startY]];
    let flag = new Array(this.blockMap.length); //标记走过的路径
    let step = new Array(this.blockMap.length); //存储走过的步数
    for (let i = 0; i < flag.length; i++) {
        flag[i] = new Array(this.blockMap[i].length).fill(false);
        step[i] = new Array(this.blockMap[i].length).fill(Infinity);
    }
    step[startX][startY] = 0;
    flag[startX][startY] = true;
    while (queue.length) {
        let p = queue.shift();
        let x = p[0],
            y = p[1];
        if (x == targetX && y == targetY) break;
        for (let i = 0; i < 4; i++) {
            let nx = x + dx[i],
                ny = y + dy[i];
            if (
                nx < 0 ||
                nx >= this.blockMap.length ||
                ny >= this.blockMap[0].length ||
                ny < 0 ||
                (
                    ((nx != targetX || ny != targetY) &&
                        !this.blockMap[nx][ny]) ||
                    flag[nx][ny] == true
                )
            ) {
                continue;
            }
            flag[nx][ny] = true;
            step[nx][ny] = step[x][y] + 1;
            queue.push([nx, ny]);
            if (nx == targetX && ny == targetY) {
                return this.getStep(step, startX, startY, targetX, targetY);
            }
        }
    }
    return false;
},

4、找出最短路径

在上一步骤中我们通过BFS到达了终点,且对沿途经过的路径走进行了步数的标记,所以我们只需要从终点往回走即可找出最短路径的坐标集合。

//找出最短路径
getStep(step, startX, startY, targetX, targetY) {
    let steps = [];
    let dx = [0, 1, 0, -1],
        dy = [1, 0, -1, 0];
    steps.unshift([targetX, targetY]);
    while (targetX != startX || targetY != startY) {
        for (let i = 0; i < 4; i++) {
            let x = targetX + dx[i],
                y = targetY + dy[i];
            if (
                x < 0 ||
                x >= step.length ||
                y < 0 ||
                y >= step[0].length
            )
                continue;
            if (step[x][y] == step[targetX][targetY] - 1) {
                targetX = x;
                targetY = y;
                steps.unshift([x, y]);
            }
        }
    }
    let lines = this.getLine(steps);
},

5、连点成线

在上一步骤中我们获取到了最短路径经过的坐标集合,现在我们需要将同一条轴上的点整合成线,如下图:

1649820682(1).jpg
需要将上图的点集转换成下图的线集

1649820764(1).jpg
我们可以这样做:
遇到’x坐标’相等的做个x标记,并将其作为线的起始端点,直到遇到’x坐标’不等的位置,其即为线的结束端点,这样我们可以将同一水平直线上的点集转换成线。

遇到’y坐标’相等的做个y标记,并将其作为线的起始端点,直到遇到’y坐标’不等的位置,其即为线的结束端点,这样我们可以将同竖直直线上的点集转换成线。

具体代码实现如下:

//获取连线集合
getLine(steps) {
    let lines = [];
    let temp = {
        startX: steps[0][0],
        startY: steps[0][1]
    };
    let flag = "";
    for (let i = 1; i < steps.length; i++) {
        if (
            (steps[i][0] != steps[i - 1][0] && flag == "x") ||
            (steps[i][1] != steps[i - 1][1] && flag == "y")
        ) {
            temp.endX = steps[i - 1][0];
            temp.endY = steps[i - 1][1];
            flag = "";
            lines.push({ ...temp });
            temp = {
                startX: steps[i - 1][0],
                startY: steps[i - 1][1]
            };
        }
        if (steps[i][0] == temp.startX) flag = "x";
        if (steps[i][1] == temp.startY) flag = "y";
    }
    let len = steps.length - 1;
    temp.endX = steps[len][0];
    temp.endY = steps[len][1];
    lines.push({ ...temp });
    return lines;
},

6、绘制连线

在上一步骤中的到线段集合后,我们需要在页面上将其绘制出来,我们可以使用一个div标签来绘制每一条线段。

  • (1)获取两个端点在页面上的具体位置
    我们可以根据端点坐标取得处于端点的两张图片位置。
const img1 = document.getElementById(`row-${p1[0]}-${p1[1]}`);
const img2 = document.getElementById(`row-${p2[0]}-${p2[1]}`);
  • (2)设置线段的长度
    两个端点的距离即为线段的长度。
const div = document.createElement("div");
div.style.top = img1.offsetTop + 20 + "px";
let flag = "";
if (img1.offsetTop > img2.offsetTop) {
    flag = "h";
    div.style.transform = "rotateZ(180deg)";
    div.style.transformOrigin = "top";
}
div.style.left = img1.offsetLeft + 20 + "px";
if (img1.offsetLeft > img2.offsetLeft) {
    flag = "w";
    div.style.transform = "rotateZ(180deg)";
    div.style.transformOrigin = "left";
}
const width = Math.abs(img1.offsetLeft - img2.offsetLeft);
const height = Math.abs(img1.offsetTop - img2.offsetTop);
//绘制连线
drawLine(p1, p2) {
    const content = document.getElementById("game-content");
    const div = document.createElement("div");
    const img1 = document.getElementById(`row-${p1[0]}-${p1[1]}`);
    const img2 = document.getElementById(`row-${p2[0]}-${p2[1]}`);
    div.style.top = img1.offsetTop + 20 + "px";
    let flag = "";
    if (img1.offsetTop > img2.offsetTop) {
        flag = "h";
        div.style.transform = "rotateZ(180deg)";
        div.style.transformOrigin = "top";
    }
    div.style.left = img1.offsetLeft + 20 + "px";
    if (img1.offsetLeft > img2.offsetLeft) {
        flag = "w";
        div.style.transform = "rotateZ(180deg)";
        div.style.transformOrigin = "left";
    }
    const width = Math.abs(img1.offsetLeft - img2.offsetLeft);
    const height = Math.abs(img1.offsetTop - img2.offsetTop);
    if (width == 0) div.style.transition = `height ${this.speed}s`;
    else div.style.transition = `width ${this.speed}s`;
    div.classList.add("line-style");
    content.appendChild(div);
    setTimeout(() => {
        let h = 0,
            w = 0;
        if (flag == "h") h = 4;
        else if (flag == "w") w = 4;
        div.style.width = width - w + "px";
        div.style.height = height - h + "px";
    }, 0);
    this.lineLists.push(div);
},

完整代码

Gitee:https://gitee.com/zheng_yongtao/jyeontu_game.git
GitHub:https://github.com/xitu/game-garden

说在后面

🎉这里是JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球🏸 ,平时也喜欢写些东西,既为自己记录📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解🙇,写错的地方望指出,定会认真改进😊,在此谢谢大家的支持,我们下文再见🙌。

Logo

前往低代码交流专区

更多推荐