前言
分享一个自己写的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 = null
timer = 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 = null
return
}
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 = null
switch (v) {
case 'right':
okHintStr = '➡按键'
keyCode = 39
break
case 'left':
okHintStr = '⬅按键'
keyCode = 37
break
case 'bottom':
okHintStr = '⬇按键'
keyCode = 40
break
case 'top':
okHintStr = '⬆按键'
keyCode = 38
break
}
if (auto) {
mainObj.keyClick({
keyCode
})
mainObj.addLogInfo(`当前还剩${step}步,自动执行${okHintStr}::`)
}
}
}
}
return okHintStr
}
})
class Main
{
gameBox = document.querySelector(".game-box")
gBw = 0
gBH = 0
bW = 0
bH = 0
level = 1
tuArr = []
tuArrStatMap = new Map()
tuArrStatTree = []
clickEvent = null
logs = []
logDom = null
constructor(level, count = 2)
{
if (count < 2) return false
this.column = count
this.row = count
this.level = level
this.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 = 1
while (index !== -1) {
console.log('不可成功,重新生成', index,)
randomArr.length = 0
randomArr.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 } = e
let handleRes = false
let 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
// ArrowRight
case 39:
handleRes = this.blankLeft()
stepStr = `执行➡,当前 ${this.tuArr.join(',')}`
break
// ArrowDown
case 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.clientWidth
this.gBH = this.gameBox.clientHeight
this.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 = 0
for (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 false
const log = this.logs.pop()
const li = document.createElement("li")
li.innerText = log
this.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 = false
switch (type) {
case "left":
if (blankIndex % this.column === 0) break
arr[blankIndex] = arr[blankIndex - 1]
arr[blankIndex - 1] = -1
res = true
break
case "right":
if ((blankIndex + 1) % this.column === 0) break
arr[blankIndex] = arr[blankIndex + 1]
arr[blankIndex + 1] = -1
res = true
break
case "top":
if (Math.floor(blankIndex / this.column) <= 0) break
arr[blankIndex] = arr[blankIndex - this.column]
arr[blankIndex - this.column] = -1
res = true
break
case "bottom":
if ((Math.floor((blankIndex) / this.column)) >= this.row - 1) {
break
}
arr[blankIndex] = arr[blankIndex + this.column]
arr[blankIndex + this.column] = -1
res = true
break
}
return res
}
// 左移
blankLeft ()
{
const blankIndex = this.tuArr.findIndex((v) => v == -1)
if (blankIndex % this.column === 0) return false
this.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 false
this.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 false
this.tuArrChange("top")
const blankDom = document.querySelector('.blank')
const replaceDom = this.getinsertBeforDom(blankDom, 'top')
const blankDomNext = blankDom.nextElementSibling
this.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 false
this.tuArrChange("bottom")
const blankDom = document.querySelector('.blank')
const replaceDom = this.getinsertBeforDom(blankDom, 'bottom')
const blankDomNext = blankDom.nextElementSibling
this.updateDom(blankDom, replaceDom)
this.updateDom(replaceDom, blankDomNext)
return true
}
getinsertBeforDom (blankDom, type = 'left')
{
let count = 1
let handle = 'previousElementSibling'
switch (type) {
case "left":
count = 1
handle = 'previousElementSibling'
break
case "right":
count = 2
handle = 'nextElementSibling'
break
case "top":
count = this.column
handle = 'previousElementSibling'
break
case "bottom":
count = this.column
handle = 'nextElementSibling'
break
}
let dom = null
dom = blankDom[handle] || null
count--
while (count > 0) {
dom = dom[handle] || null
count--
}
return dom
}
// 验证
verify ()
{
let res = true
console.log(this.tuArr)
for (let k = 0; k < this.tuArr.length; k++) {
if (k == 0 && this.tuArr[k] !== 1) {
res = false
break
}
if (k > 0 && k < this.tuArr.length - 1 && this.tuArr[k] - this.tuArr[k - 1] !== 1) {
res = false
break
}
if (k == this.tuArr.length - 1 && this.tuArr[k] !== -1) {
res = false
}
}
return res
}
createTree (arr)
{
console.log('生成映射')
let index = 0
this.tuArrStatTree[index] = [[...arr]]
this.tuArrStatMap.set([...arr].join(','), index)
while (this.tuArrStatTree[index] && this.tuArrStatTree[index].length > 0 && index != -1) {
index = index + 1
this.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 = -1
this.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>
结语
还挺好玩嘞。