前言
分享一个自己写的3*3拼图小游戏,类似于华容道的玩法,是使用html实现的。
正文
实现过程参考了 三阶数字华容道最优解 这篇文章。
实现功能
- 随机生成有解的数据
- 下一步提示
- 自动拼图
- 重置
实现过程
实现过程主要分为:
- 页面生成
- 保存所有有解的数据,以及成功所需的步数
- 实现按⬅、⬆、➡、⬇等按键挪动、替换拼图数据
- 验证是否成功
- 实现重置
- 实现下一步提示
- 实现自动拼图
待改进
可改进优化以下功能
- 有解数据判断
- 扩展历史记录信息
- 实现回溯功能
- 页面优化、提示优化
- 优化核心实现逻辑,使用合适的算法实现功能
代码
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>拼图</title><style>.game-box {width: 400px;height: 400px;display: flex;flex-wrap: wrap;margin: 24px;border: 1px solid #999;background-size: 400px;}.box {flex: 1 1 auto;border: 1px solid #eee;color: #fff;}.handle-box {display: inline-block;height: 200px;margin-right: 24px;overflow: hidden;}.logs-dom {display: inline-block;height: 200px;overflow-y: auto;}</style></head><body><div class="game-box"></div><div class="handle-box"><button class="hint-btn" type="button">提示</button><button class="auto-btn" type="button">自动拼图</button><button class="reset-btn" type="button">重置</button><p class="hint"></p></div><ol class="logs-dom"></ol><script>const LEVEL = {easy: 1, // 容易medium: 2, // 中等hard: 3, // 困难}window.addEventListener("DOMContentLoaded", () =>{const mainObj = new Main(LEVEL.medium, 3)const hintBtn = document.querySelector('.hint-btn')const hintP = document.querySelector('.hint')const autoBtn = document.querySelector('.auto-btn')const resetBtn = document.querySelector('.reset-btn')hintBtn.onclick = () =>{const step = mainObj.tuArrStatMap.get(mainObj.tuArr.join(','))if (step <= 0) {hintP.innerText = `太棒了,成功了!`mainObj.addLogInfo(hintP.innerText)return true}const okKey = getHintContent(step)hintP.innerText = `当前还需${step}步可以成功,可以按${okKey}进行下一步`}autoBtn.onclick = () =>{let timer = nulltimer = setInterval(() =>{const step = mainObj.tuArrStatMap.get(mainObj.tuArr.join(','))console.log(step, mainObj.tuArr)if (step <= 0) {hintP.innerText = `太棒了,成功了!`mainObj.addLogInfo(hintP.innerText)clearInterval(timer)timer = nullreturn}getHintContent(step, true)}, 300)}resetBtn.onclick = () =>{mainObj.reset()}const getHintContent = (step = 1, auto = false) =>{let okHintStr = ''const upSetupArr = [...mainObj.tuArrStatTree[step - 1]]const handlArr = ['left', 'right', 'top', 'bottom']for (let nowArr of upSetupArr) {for (let v of handlArr) {const arrCopy = [...nowArr]const res = mainObj.tuArrChange(v, arrCopy)if (res && arrCopy.join(',') === mainObj.tuArr.join(',')) {let keycode = nullswitch (v) {case 'right':okHintStr = '➡按键'keyCode = 39breakcase 'left':okHintStr = '⬅按键'keyCode = 37breakcase 'bottom':okHintStr = '⬇按键'keyCode = 40breakcase 'top':okHintStr = '⬆按键'keyCode = 38break}if (auto) {mainObj.keyClick({keyCode})mainObj.addLogInfo(`当前还剩${step}步,自动执行${okHintStr}::`)}}}}return okHintStr}})class Main{gameBox = document.querySelector(".game-box")gBw = 0gBH = 0bW = 0bH = 0level = 1tuArr = []tuArrStatMap = new Map()tuArrStatTree = []clickEvent = nulllogs = []logDom = nullconstructor(level, count = 2){if (count < 2) return falsethis.column = countthis.row = countthis.level = levelthis.logs = []this.logDom = document.querySelector(".logs-dom")this.initDom()this.clickEvent = this.keyClick.bind(this)document.addEventListener("keyup", this.clickEvent)}// 随机生成randomUpdate (){const res = []for (let i = 1; i < this.column * this.row; i++) {res.push(i)}res.push(-1)this.tuArrStatTree.length <= 0 && this.createTree([...res]) // 有映射的话不需要再次生成const randomArr = []let index = 1while (index !== -1) {console.log('不可成功,重新生成', index,)randomArr.length = 0randomArr.push(...res.sort(() => Math.random() - 0.5))const isWithLevel = this.tuArrStatMap.get(randomArr.join(",")) / this.tuArrStatTree.length >= (this.level - 1) * (1 / 3) && this.tuArrStatMap.get(randomArr.join(",")) / this.tuArrStatTree.length < (this.level) * (1 / 3)if (!this.tuArrStatMap.has(randomArr.join(",")) || !isWithLevel || this.tuArrStatMap.get(randomArr.join(",")) == 0) index++else {console.log(`需要步数:`, this.tuArrStatMap.get(randomArr.join(",")))index = -1}}return randomArr}keyClick (e){const { keyCode } = elet handleRes = falselet stepStr = ''switch (keyCode) {// ArrowLeft 空白块右移case 37:handleRes = this.blankRight()stepStr = `执行⬅,当前 ${this.tuArr.join(',')}`break// ArrowUp 空白块下移case 38:handleRes = this.blankBottom()stepStr = `执行⬆,当前 ${this.tuArr.join(',')}`break// ArrowRightcase 39:handleRes = this.blankLeft()stepStr = `执行➡,当前 ${this.tuArr.join(',')}`break// ArrowDowncase 40:stepStr = `执行⬇,当前 ${this.tuArr.join(',')}`handleRes = this.blankTop()break}if (handleRes) {this.logs.push(stepStr)this.updateLogDom()const res = this.verify()if (res) {this.success()}}}initDom (){this.gBw = this.gameBox.clientWidththis.gBH = this.gameBox.clientHeightthis.bW = Math.floor(1 / this.column * this.gBw)this.bH = Math.floor(1 / this.row * this.gBH)const initStateArr = this.randomUpdate()const fragment = document.createDocumentFragment()this.tuArr.length = 0for (let r = 0; r < this.row; r++) {for (let c = 0; c < this.column; c++) {const stateV = initStateArr.shift()const pw = - this.bW * ((stateV - 1) % this.column)const pH = -this.bH * Math.floor((stateV - 1) / this.row)const div = document.createElement('div')div.classList.add("box")stateV == -1 && div.classList.add("blank")div.style = `width:${Math.floor((this.gBw - 6) / this.column)}px;height:${Math.floor((this.gBH - 6) / this.row)}px;background: ${stateV == -1 ? 'transparent;' :` url('https://img0.baidu.com/it/u=2019423475,2066895883&fm=253&fmt=auto&app=138&f=JPEG?w=600&h=400') no-repeat`};background-position: ${pw}px ${pH}px ;background-size: ${this.gBw}px ${this.gBH}px;`div.innerText = `${stateV}`this.tuArr.push(stateV)fragment.appendChild(div)}}this.gameBox.innerHTML = ''this.gameBox.appendChild(fragment)}updateDom (newChild, oldChild){this.gameBox.insertBefore(newChild, oldChild)}updateLogDom (){if (this.logs.length <= 0) return falseconst log = this.logs.pop()const li = document.createElement("li")li.innerText = logthis.logDom.appendChild(li)}addLogInfo (str = ''){this.logs.push(str)this.updateLogDom()}// 数组更改tuArrChange (type = "left", arr = this.tuArr){const blankIndex = arr.findIndex((v) => v == -1)let res = falseswitch (type) {case "left":if (blankIndex % this.column === 0) breakarr[blankIndex] = arr[blankIndex - 1]arr[blankIndex - 1] = -1res = truebreakcase "right":if ((blankIndex + 1) % this.column === 0) breakarr[blankIndex] = arr[blankIndex + 1]arr[blankIndex + 1] = -1res = truebreakcase "top":if (Math.floor(blankIndex / this.column) <= 0) breakarr[blankIndex] = arr[blankIndex - this.column]arr[blankIndex - this.column] = -1res = truebreakcase "bottom":if ((Math.floor((blankIndex) / this.column)) >= this.row - 1) {break}arr[blankIndex] = arr[blankIndex + this.column]arr[blankIndex + this.column] = -1res = truebreak}return res}// 左移blankLeft () {const blankIndex = this.tuArr.findIndex((v) => v == -1)if (blankIndex % this.column === 0) return falsethis.tuArrChange("left")const blankDom = document.querySelector('.blank')this.updateDom(blankDom, this.getinsertBeforDom(blankDom, 'left'))return true}// 右移blankRight () {const blankIndex = this.tuArr.findIndex((v) => v == -1)if ((blankIndex + 1) % this.column === 0) return falsethis.tuArrChange("right")const blankDom = document.querySelector('.blank')this.updateDom(blankDom, this.getinsertBeforDom(blankDom, 'right'))return true}// 上移blankTop () {const blankIndex = this.tuArr.findIndex((v) => v == -1)if (Math.floor(blankIndex / this.column) <= 0) return falsethis.tuArrChange("top")const blankDom = document.querySelector('.blank')const replaceDom = this.getinsertBeforDom(blankDom, 'top')const blankDomNext = blankDom.nextElementSiblingthis.updateDom(blankDom, replaceDom)this.updateDom(replaceDom, blankDomNext)return true}// 下移blankBottom () {const blankIndex = this.tuArr.findIndex((v) => v == -1)if ((Math.floor((blankIndex) / this.column)) >= this.row - 1) return falsethis.tuArrChange("bottom")const blankDom = document.querySelector('.blank')const replaceDom = this.getinsertBeforDom(blankDom, 'bottom')const blankDomNext = blankDom.nextElementSiblingthis.updateDom(blankDom, replaceDom)this.updateDom(replaceDom, blankDomNext)return true}getinsertBeforDom (blankDom, type = 'left'){let count = 1let handle = 'previousElementSibling'switch (type) {case "left":count = 1handle = 'previousElementSibling'breakcase "right":count = 2handle = 'nextElementSibling'breakcase "top":count = this.columnhandle = 'previousElementSibling'breakcase "bottom":count = this.columnhandle = 'nextElementSibling'break}let dom = nulldom = blankDom[handle] || nullcount--while (count > 0) {dom = dom[handle] || nullcount--}return dom}// 验证verify (){let res = trueconsole.log(this.tuArr)for (let k = 0; k < this.tuArr.length; k++) {if (k == 0 && this.tuArr[k] !== 1) {res = falsebreak}if (k > 0 && k < this.tuArr.length - 1 && this.tuArr[k] - this.tuArr[k - 1] !== 1) {res = falsebreak}if (k == this.tuArr.length - 1 && this.tuArr[k] !== -1) {res = false}}return res}createTree (arr){console.log('生成映射')let index = 0this.tuArrStatTree[index] = [[...arr]]this.tuArrStatMap.set([...arr].join(','), index)while (this.tuArrStatTree[index] && this.tuArrStatTree[index].length > 0 && index != -1) {index = index + 1this.tuArrStatTree[index] = []const handlArr = ['left', 'right', 'top', 'bottom']for (let nowArr of this.tuArrStatTree[index - 1]) {for (let v of handlArr) {const arrCopy = [...nowArr]const res = this.tuArrChange(v, arrCopy)if (res && !this.tuArrStatMap.has(arrCopy.join(','))) {this.tuArrStatTree[index].push(arrCopy)this.tuArrStatMap.set(arrCopy.join(','), index)}}}if (this.tuArrStatTree[index].length <= 0) {console.log('可以中止了')index = -1this.tuArrStatTree.pop()}}console.log('this.tuArrStatTree', this.tuArrStatTree)console.log('this.tuArrStatMap', this.tuArrStatMap)}success (){this.gameBox.style.borderColor = '#0f0'this.destroy()}destroy (){console.log('销毁监听事件')document.removeEventListener("keyup", this.clickEvent)}reset (){this.logs = []this.initDom()document.addEventListener("keyup", this.clickEvent)}}</script>
</body></html>
结语
还挺好玩嘞。