数字华容道「思否猫版」

背景

思否社区首届「猫猫杯」线上代码共创大赛日程已接近尾声,各路大神狂拽炫酷的项目已经出炉了。奈何拖延症,我还在想“创意点”,同时考虑到实现周期要赶上 deadline,纠结之时,无意间在地铁上瞟见有人在看《最强大脑》,灵光一现,对哦!记得有一期有个拼图游戏——数字华容道,自此赌王之子(何猷君)一战成名。游戏原理不难,益智又好玩。
54dce26272be4b91b64a600a90974274.gif
先来了解一下这款游戏规则:

  1. 将面板划分 nxn 个方格,除了最后一格,每一个方格有一个滑块;
  2. 初始游戏会将滑块顺序打乱,空出最后一格;
  3. 空格相邻的滑块才可以滑动,并且只能滑倒空白格里;
  4. 将所有滑块按顺序拼接则游戏闯关成功;

考虑完全这次与思否猫主题结合,可以将滑块编码映射成背景图片的局部编码,这样就变成了拼图版数字华容道游戏,奈斯,想法有了,现在就开撸。

实现基础布局

首先实现基本布局,这里将 600px X 600px 面板平均划分 3x3 方格。

<div class="main">
  <section class="content">
    <ul class="row" v-for="row in level" :key="row" :style="{width: `${appWidth}px`}">
      <li class="col" v-for="col in level" :key="col" :style="{width: `${itemWidth}px`}">
        <div
          class="item"
          :style="{
            height: `${itemWidth}px`,
          }"
        >
          {{row}}:{{col}}
        </div>
      </li>
    </ul>
  </section>
</div>
data() {
  return {
    appWidth: 600,
    level: 3,
  }
},

computed: {
  itemWidth() {
    return this.appWidth / this.level
  },
},

image.png

实现方格块背景图

可能我们会将大图切图然后填充,切成 3x3、4x4...,可以,但是不值得,太繁琐了,后面还会有不同的图片供选择作为拼图背景,工作量是成倍的。偷懒的是将背景图片使用 CSS background-position 进行定位。

<div
  class="item"
  :style="{
    height: `${itemWidth}px`,
    backgroundPosition: getBgPos(row, col)
  }"
></div>

定位逻辑是,将第(row, col)位置的方格的背景图从左上方平((col - 1)*width, (row - 1)*height)距离。比如第 2 行第 3 列的背景图,往左移动 2 个方格宽度,往右移动 1 个方格高度。

// 获取背景图片位置
getBgPos(row, col) {
  const w = this.itemWidth
  const { level } = this
  const offsetX = ((col - 1) % level) * w
  const offsetY = ((row - 1) % level) * w
  return `-${offsetX}px -${offsetY}px`
},
.col .item {
  background-color: #99a9bf;
  background-repeat: no-repeat;
  background-image: url('./issue.png');
  overflow: hidden;
}

image.png

实现方格交换

这里使用 Vue 实现,数据驱动视图:响应式数据(数组)中两个元素交换位置,对应的是视图上的两个方格位置交换。这里我们只需交换背景图位置信息来假装元素移动了。
响应式数组的数据结构定义成这样

;[
  {
    index: 0, // 索引
    bgPos: '0px 0px', // 背景图偏移量
    isSpace: false // 是否是空白格
  },
  ...{
    index: 8, // 索引
    bgPos: '-400px -400px', // 背景图偏移量
    isSpace: true // 是否是空白格
  }
]

其次,生成有序和乱序的数组,分别对应游戏闯关成功状态,和初始未开始状态的视图。
在乱序数组的最后一项预留,作为相邻滑块交换空间,实现方式是复制一份有序数组(除去最后一项),然后插入一个空白项。其中空白项将背景图片移出可视区即可。

// 初始化
init() {
  this.initOrigList()
  this.initMessList()
},

// 初始化原始列表
initOrigList() {
  const { level } = this
  const list = []
  for (let i = 0; i < level; i++) {
    for (let j = 0; j < level; j++) {
      list.push({
        index: i * level + j,
        bgPos: this.getBgPos(i + 1, j + 1),
        isSpace: false
      })
    }
  }
  this.origList = list
},

// 初始化乱序列表
initMessList() {
  // 除去最后一项的列表打乱
  const list = this.sufflex(this.origList.slice(0, -1))

  // 最后一项设置为空白:背景图片移出可视区
  this.messList = [
    ...list,
    {
      index: Math.pow(this.level, 2) - 1,
      bgPos: `-${this.appWidth}px`,
      isSpace: true
    }
  ]
},

有序数组打乱这里采用洗牌算法,原理很简单:从末尾开始往前,前面随机选取一个数与最后的数互换。

// 洗牌算法
sufflex(arr) {
  const len = arr.length
  const cards = [...arr]
  let r = len - 1

  while (r >= 0) {
    const i = Math.floor(Math.random() * (r + 1))
    ;[cards[r], cards[i]] = [cards[i], cards[r]]
    r--
  }
  return cards
},

其次,从生成布局上,每个方格的背景图位置要与该乱序数组相关联。

<div
  class="item"
  @click="handleMove(row, col)"
  :style="{
    height: `${itemWidth}px`,
    backgroundPosition: getMessBgPos(row, col),
  }"
></div>

最后,在点击某一项方格时,判断它能不能移动,要看它的上下左右相邻方格是否有空白格,有则与空白格交换位置,否则,不能移动。

// 点击单元项移动
handleMove(row, col) {
  const targetPos = this.getNearbySpacePos(row - 1, col - 1)
  if (!targetPos.length) return

  this.changePos((row - 1) * this.level + col - 1, targetPos[0] * this.level + targetPos[1])
},

// 获取当前点击模块紧挨着的空白模块位置
// 如果有则可以移动:当前模块上下左右相邻模块有空白模块则可以移动
getNearbySpacePos(row, col) {
  const { level, messList } = this

  // 上
  if (row > 0 && messList[(row - 1) * level + col].isSpace) return [row - 1, col]

  // 下
  if (row < level - 1 && messList[(row + 1) * level + col].isSpace) return [row + 1, col]

  // 左
  if (col > 0 && messList[row * level + col - 1].isSpace) return [row, col - 1]

  // 右
  if (col < level - 1 && messList[row * level + col + 1].isSpace) return [row, col + 1]

  return []
},

// 交换位置
changePos(index, targetIndex) {
  const temp = this.messList[index]
  const targetTemp = this.messList[targetIndex]
  this.$set(this.messList, targetIndex, temp)
  this.$set(this.messList, index, targetTemp)
},

至此,就简单实现了方格与相邻空白格的交换操作。
2022-10-23 10-43-03.2022-10-23 10_44_42.gif

完善游戏功能

作为一款完整的游戏,还有许多细节点需要补充优化。
2022-10-23 10-47-25.2022-10-23 10_50_28.gif

  • 游戏开始和游戏结束开关;
  • 游戏难度(nxn 中 n 越大,难度越大);
  • 支持更换自己喜欢的背景图片;
  • 游戏步数和用时记录;

主体游戏功能实现了,以上拓展功能的实现比较简单,就不一一说明了,感兴趣的可以点这里查看代码,或者直接点这里在线体验。
感谢点赞&关注,最后祝你可以成功闯关 🎉,完~
image.png

本文参与了1024 程序员节,欢迎正在阅读的你也加入。

Code for work, write for progress!

5.8k 声望
2.5k 粉丝
0 条评论
推荐阅读
从开发到小组长角色挣扎的一年
其实从去年年底就“被给”了一个小组长头衔。由于业务领域范畴重新规划,导致组织架构调整,部分研发部门调整。之前的前端团队是研发中心下的共用资源,职责是支撑研发中心其下各个业务研发部门团队,以「大前端团...

wuwhs3阅读 759评论 2

从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木149阅读 12.3k评论 10

正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青54阅读 7.8k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy46阅读 5.9k评论 12

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木66阅读 6.1k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs39阅读 6.3k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木43阅读 7.3k评论 6

Code for work, write for progress!

5.8k 声望
2.5k 粉丝
宣传栏