开始做的时候还挺接地气的…要不是没有素材也不至于如此…
在线链接
https://linyisonger.github.io/H5.Examples
代码逻辑并不是很难,写的时间不长,可能会有很多,复制的时候注意安全…
定义宽度/高度这个看自己需要,我这边是因为没有素材所有…使用了中文文字,如果有素材的话可以根据素材进行变更。
// 宽度
let width = 20, height = 20;
定义地图枚举,我是这样想的所有可以挪动的,都是在空白格的基础上,不可挪动的一共就也没几种,所以使用枚举的组合,更便于判断。
// 地图枚举
let mapEnum = {
blank: 1, // 空白格
player: 2, // 玩家
box: 4, // 箱子
target: 8, // 目标
wall: 16 // 墙
}
地图配置,这点儿可能会有疑惑,关于数字9呀、5呀、3呀命名枚举上没有写,这里是便是组合。
其中3:代表2玩家踩在1空白格上
其中5:代表4箱子放在1空白格上
其中9:代表8目标放在1空白格上
以此类推…不限于两两组合
let map =
[
[16, 16, 16, 16, 16, 16],
[16, 1, 1, 9, 1, 16],
[16, 1, 5, 3, 1, 16],
[16, 9, 16, 5, 1, 16],
[16, 1, 5, 1, 9, 16],
[16, 16, 16, 16, 16, 16],
]
初始化相关,关于玩家初始位置,方便后续操作,使用双重嵌套循环来捕获地图上的玩家位置。
以及操作按钮的点击事件,按钮的事件。
let player = { x: 0, y: 0 }
let sokobanDom = document.querySelector('.sokoban')
let sokobanMapDom = {}
let operateDom = document.querySelectorAll('.operate > .btn')
// 初始化
function init() {
let ylength = map.length, xlength = 0
for (let y = 0; y < map.length; y++) {
const xmap = map[y];
xlength = Math.max(xlength, xmap.length)
for (let x = 0; x < xmap.length; x++) {
if ((xmap[x] & mapEnum.player) != 0) {
player = { x, y }
}
}
}
sokobanDom.setAttribute('style', `width:${xlength * width}px;height:${ylength * height}px`)
for (let i = 0; i < operateDom.length; i++) {
const btn = operateDom.item(i);
btn.addEventListener('click', function () {
onKeyDown(['ArrowUp', 'ArrowLeft', 'ArrowDown', 'ArrowRight'][i])
})
}
}
function onKeyDown(key) {
let x = 0, y = 0;
if (key == 'ArrowUp') y -= 1
if (key == 'ArrowDown') y += 1
if (key == 'ArrowLeft') x -= 1
if (key == 'ArrowRight') x += 1
try {
if (x || y) walk(x, y)
} catch (error) {
// 报错就不处理,任性哈哈哈哈
}
}
document.addEventListener("keydown", (ev) => onKeyDown(ev.key))
地图渲染,双层嵌套渲染,并不复杂的代码…
// 渲染
function randerMap() {
for (let y = 0; y < map.length; y++) {
const xmap = map[y];
for (let x = 0; x < xmap.length; x++) {
let sokobanItem = null;
if (sokobanMapDom[`${x},${y}`]) {
sokobanItem = sokobanMapDom[`${x},${y}`];
sokobanItem.textContent = ''
}
else {
sokobanItem = document.createElement('div')
sokobanItem.classList.add('sokoban-item')
sokobanItem.setAttribute(`style`, `left:${x * width}px;top:${y * height}px`)
sokobanDom.appendChild(sokobanItem)
sokobanMapDom[`${x},${y}`] = sokobanItem
}
if (xmap[x] == mapEnum.wall) {
sokobanItem.textContent = '墙'
}
if (xmap[x] == (mapEnum.blank + mapEnum.target)) {
sokobanItem.textContent = '口'
}
if (xmap[x] == (mapEnum.blank + mapEnum.box)) {
sokobanItem.textContent = '棺'
}
if (xmap[x] == (mapEnum.blank + mapEnum.player)) {
sokobanItem.textContent = '人'
}
if (xmap[x] == (mapEnum.blank + mapEnum.box + mapEnum.target)) {
sokobanItem.textContent = '坟'
}
if (xmap[x] == (mapEnum.blank + mapEnum.player + mapEnum.target)) {
sokobanItem.textContent = '囚'
}
}
}
}
检测游戏是否结束,就判断了目标是否都包含盒子了,不知道这样写简洁不简洁,应该并不是很简洁吧。
// 检查游戏是否结束
function checkGameOver() {
for (let y = 0; y < map.length; y++) {
const xmap = map[y];
for (let x = 0; x < xmap.length; x++) {
if ((xmap[x] & mapEnum.target) != 0 && (xmap[x] & mapEnum.box) == 0) {
return false;
}
}
}
return true;
}
代码主要逻辑走,其实就是根据走到方向以及下一个点,是否可以去移动,以及下下一个点是否可以移动,然后移动刷新地图。
// 走
function walk(x, y) {
let tx = player.x + x, ty = player.y + y;
// 假如这里是盒子的话
if ((map[ty][tx] & mapEnum.box) != 0) {
let bx = tx + x, by = ty + y;
// 如果是盒子D叠盒子无法移动
if ((map[by][bx] & mapEnum.box) != 0) {
return;
}
// 假如盒子移动的方向包含地板,则允许移动
if ((map[by][bx] & mapEnum.blank) != 0) {
map[ty][tx] -= mapEnum.box;
map[by][bx] += mapEnum.box;
// 假如盒子移动的目标位置包含目标位置则检测游戏是否胜利
if ((map[by][bx] & mapEnum.target) != 0 && checkGameOver()) {
sokobanDom.innerHTML = `恭喜你安置了一切`
}
}
}
// 假如这里包含地板
if ((map[ty][tx] & mapEnum.blank) != 0 && (map[ty][tx] & mapEnum.box) == 0) {
map[player.y][player.x] -= mapEnum.player;
map[ty][tx] += mapEnum.player
player.x = tx;
player.y = ty;
}
randerMap()
}
https://github.com/linyisonger/H5.Examples