连连看中的广度优先搜索算法

Posted by Yinode on Saturday, February 1, 2020

TOC

由于博客数据丢失,只剩下了代码。。。。。之前写的MD都没了。。

const gameConfig = {
  maxLevel: 10// 最大搜索层级 2代表 只搜索经过两次转角能到达的点
}

// 简易队列系统 配合 DFS 广度有限搜索
class Quene {
  constructor() {
    this._quene = []
  }
  put(ele) {
    this._quene.unshift(ele)
  }
  get() {
    if (this._quene.length > 0) {
      return this._quene.pop()
    } else {
      return null
    }
  }
}

// 搜索节点 每一个节点都包含了与起始节点直接的信息
class SearchNode {
  /**
   *
   * @param {number} x  在图中的 x 位置
   * @param {number} y  在图中的 y 位置
   * @param {number} l 该节点的 level 度数 0 代表可以从起始节点直接到达 1代表需要一次转角
   * @param {number} dis 从起始节点到该节点的最短距离
   * @param {x:number,y:number} pre 该节点的上一个父节点(可以通过这个参数不断最终到距离起始节点的最短路径)
   */
  constructor(x, y, l, dis, pre) {
    this.x = x
    this.y = y
    this.l = l
    this.dis = dis
    this.pre = pre
  }
}

/**
 *
 * @param {x:number,y:number} start  要查询的起始位置
 * @param {x:number,y:number} end  要查询的结束位置
 *
 * @return {boolean} 检测两个点能否在相应 level限定 到达
 */
function checkCanbeRecivece(start, end) {
  let quene = new Quene() // 队列放置所有下一次 currentLevel 循环
  let allNode = [] // 最终节点仓库 放在里面代表 level 已经确定 (但是 dis 距离还是有可能会更新)
  let currentLevel = 0 // 当前的步进

  // 初始节点 也就是起始节点 直接放入两个列表中
  let initNode = new SearchNode(start.x, start.y, currentLevel, 0)
  quene.put(initNode)
  allNode.push(initNode)

  // 从宏观上来最多只走规定的最大步数
  while (currentLevel <= gameConfig.maxLevel) {
    // 循环 L1 (到达层级)
    // 遍历队列 对队列中的所有节点进行射线标记
    let curNode = quene.get()
    let temps = [] // 新发现的节点需要放入到临时数组中 循环结束后再放置到队列中(避免一个步进循环直接便利所有节点)
    while (curNode) {
      // 循环 L2 (所有队列中需要进一步射线探测的新节点)

      /**
       *
       * @param {SearchNode} newNode 通过发射发现的新节点
       * @param {boolean} isOhterNode 是否属于一个实体节点 如果实体就没必要放入到
       *
       */
      const next = (newNode, isOhterNode) => {
        if (isOhterNode) {
          allNode.push(newNode)
        } else {
          temps.push(newNode)
        }
      }

      /**
       * @description 检测该节点是否有更近的方式到达
       *
       * @param {number} x 该节点的 X
       * @param {number} y 该节点的 Y
       * @param {number} dis 该节点到起始节点的距离
       * @param {SearchNode} pre 该节点的前置节点
       *
       * @return {boolean} 该节点是否属于第一次发现的新节点
       */
      const updateDistanceAndCheckIsNew = (x, y, dis, pre) => {
        let aNodes = allNode.concat(temps)
        let fIndex = aNodes.findIndex(v => v.x === x && v.y === y)
        let node = allNode[fIndex]

        // 发现有更短的路径 重新标记下距离和前置节点
        if (node && node.dis > dis) {
          node.dis = dis
          node.pre = pre
        }

        return fIndex <= -1
      }

      /* 进行射线发射 GOGOGO */

      // UP 沿着 Y 轴向上发射
      rayTagHelper(
        0,
        -1,
        curNode,
        currentLevel,
        updateDistanceAndCheckIsNew,
        next
      )
      // DOWN 沿着 Y 轴向下发射
      rayTagHelper(
        0,
        1,
        curNode,
        currentLevel,
        updateDistanceAndCheckIsNew,
        next
      )
      // LEFT 沿着 X 轴向左发射
      rayTagHelper(
        -1,
        0,
        curNode,
        currentLevel,
        updateDistanceAndCheckIsNew,
        next
      )
      // RIGHT 沿着 X 轴向右发射
      rayTagHelper(
        1,
        0,
        curNode,
        currentLevel,
        updateDistanceAndCheckIsNew,
        next
      )

      // 取出下一个需要进行发射的节点
      curNode = quene.get()
    }

    // 把所有的临时节点都加入到队列以及 allNode 中
    temps.forEach(v => {
      quene.put(v)
      allNode.push(v)
    })

    // 步进自增
    currentLevel++
  }

  /*
   判断该节点是否能到达,只要检查 allNode 里面是否包含结尾节点就可以
  */
  if (
    allNode.findIndex(v => {
      if (v.x === end.x && v.y === end.y) {
        console.log(v)
        console.log(`可以到达: level - ${v.l} -- dis - ${v.dis}`)
        console.log(allNode)
      }
      return v.x === end.x && v.y === end.y
    }) > -1
  ) {
  } else {
    console.log('无法到达')
  }
}

/**
 * @description 往一个方向上持续探测节点 直到碰到阻挡
 *
 * @param {number} xOffset 每次单次发射 X 轴的持续偏移
 * @param {number} yOffset 每次单次发射 Y 轴的持续偏移
 * @param {SearchNode} startNode 发射起始节点
 * @param {number} currentLevel 当前的步进
 * @param {(x, y, dis, pre) -> boolean} updateDistanceAndCheckIsNew 更新节点最短记录 并检查是否属于新发现的节点
 * @param {*} callBack 如果属于新节点 触发该回调 加入到队列中
 */
function rayTagHelper(
  xOffset,
  yOffset,
  startNode,
  currentLevel,
  updateDistanceAndCheckIsNew,
  callBack
) {
  let { x, y, dis } = startNode
  let pre = null
  let cur = null
  let isEntityNode = false

  do {
    pre = {
      x,
      y
    }
    // 开始发射 定位为特定方向上的第一个节点
    x += xOffset
    y += yOffset
    dis++

    // 避免 JS 错误 查到一个不存在的位置
    if (gameGird[x] === undefined || gameGird[x][y] === undefined) return null
    cur = gameGird[x][y]

    // 检测当前节点是否属于实体节点
    isEntityNode = cur !== 0

    // 只有新节点才触发回调 加入到temps之中 顺便更新一下最短路径
    if (updateDistanceAndCheckIsNew(x, y, dis, pre)) {
      callBack(new SearchNode(x, y, currentLevel, dis, pre), isEntityNode)
    }

    // 当没有触碰到边界和实体节点时 继续发射
  } while (cur !== undefined && !isEntityNode)

  return null
}

// 层级为0的情况
// let gameGird = [
//   [0, 0, 0, 0, 0, 0],
//   [0, 1, 0, 0, 0, 0],
//   [0, 0, 0, 0, 0, 0],
//   [0, 1, 0, 0, 0, 0],
//   [0, 0, 0, 0, 0, 0],
//   [0, 0, 0, 0, 0, 0]
// ]
// checkCanbeRecivece(
//   {
//     y: 1,
//     x: 1
//   },
//   {
//     x: 3,
//     y: 1
//   }
// )

// 查找层级为1的情况
let gameGird = [
  [0, 0, 0, 0, 0, 0],
  [0, 1, 0, 0, 0, 0],
  [0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0]
]
// checkCanbeRecivece(
//   {
//     y: 1,
//     x: 1
//   },
//   {
//     x: 2,
//     y: 2
//   }
// )

// 查找层级为2的情况
// let gameGird = [
//   [0, 0, 0, 0, 0, 0],
//   [0, 1, 2, 0, 1, 0],
//   [0, 0, 0, 0, 0, 0],
//   [0, 0, 0, 0, 0, 0],
//   [0, 0, 0, 0, 0, 0],
//   [0, 0, 0, 0, 0, 0]
// ]
// checkCanbeRecivece(
//   {
//     y: 1,
//     x: 1
//   },
//   {
//     x: 1,
//     y: 4
//   }
// )

// 无法到达的情况 maxLevel = 2 情况下
// let gameGird = [
//   [0, 0, 0, 0, 2, 0],
//   [0, 1, 2, 0, 1, 0],
//   [0, 0, 2, 0, 2, 0],
//   [0, 0, 2, 0, 2, 0],
//   [0, 0, 2, 0, 0, 0],
//   [0, 0, 2, 0, 0, 0]
// ]
// checkCanbeRecivece(
//   {
//     y: 1,
//     x: 1
//   },
//   {
//     x: 1,
//     y: 4
//   }
// )

// 无法到达的情况 没路了 直接死路的情况
// let gameGird = [
//   [0, 0, 2, 0, 2, 0],
//   [0, 1, 2, 0, 1, 0],
//   [0, 0, 2, 0, 2, 0],
//   [0, 0, 2, 0, 2, 0],
//   [0, 0, 2, 0, 0, 0],
//   [0, 0, 2, 0, 0, 0]
// ]
// checkCanbeRecivece(
//   {
//     y: 1,
//     x: 1
//   },
//   {
//     x: 1,
//     y: 4
//   }
// )

// 查找层级为2的情况 测试下最短路径
// let gameGird = [
//   [0, 0, 0, 0, 0, 0],
//   [0, 0, 1, 0, 0, 0],
//   [0, 1, 1, 1, 0, 0],
//   [0, 0, 1, 0, 0, 0],
//   [0, 0, 1, 0, 0, 0],
//   [0, 0, 0, 0, 0, 0]
// ]
// checkCanbeRecivece(
//   {
//     y: 1,
//     x: 2
//   },
//   {
//     x: 2,
//     y: 3
//   }
// )