前端实现一个数字华容道游戏

1,353 阅读4分钟

前端实现一个数字华容道游戏

image.png

引言

最近在学习RxJS,感觉对于用户的多次操作使用队列来维护这个点可以有很多应用的点,我们都知道使用RxJS可以不需要维护这个队列,可以省下一大片逻辑,而Rx也最适合这些稍微复杂点的场景了,之前看抖音看到有个人用java实现了一个数字华容道,然后正好在学RxJS就想着用一下,写好了我才发现实际上数字华容道的开发和RxJS基本没半点关系....我以为可以解决的点下面会进行阐述,但是并没什么用哈哈哈哈哈

效果图

QQ20220117-002535-HD.gif

实现

我们用RxJS来优化的点就在于我们每次移动一个方块都是有动画来进行转变的,如果有个高手,在我们动画还在转变的时候就点击了下一个,这时候我们肯定不能够让两个同时进行错位的转变,这在体验上肯定也不是很好,所以就有了本篇文章的优化,使用RxJS来把用户的操作进行合并然后在上一次操作后自动读取下一次,如果对RxJS不是很理解的人可以建议先看上一篇文章哟

话不多说直接来实现吧
首先生成游戏这个,由于这边使用了随机数组的生成,所以可能导致无解,至于为啥,我也不知道具体可以看这边文章:用React写一个数字华容道,你需要知道的秘密

let arr: number[] = [];
const initRandomArr = () => {
arr = new Array(MAX_LENGTH - 1)
.fill("")
.map((_, index) => index + 1)
.sort(() => Math.random() * 2 - 1);
let ans = 0;
const sort_arr = [...arr];
for (let i = 0; i < sort_arr.length; i++) {
    for (let j = sort_arr.length - 1; j > i; j--) {
        if (sort_arr[j] < sort_arr[j - 1]) {
            const temp = sort_arr[j];
            sort_arr[j] = sort_arr[j - 1];
            sort_arr[j - 1] = temp;
            ans++;
        }
    }
}
if (ans % 2 !== 0) {
initRandomArr();
}
return;
};

// 生成表格
const init = () => {
initRandomArr();
arr.forEach((item, index) => {
position[item] = index;
const el = document.createElement("div");
el.setAttribute("class", "box");
el.setAttribute("data-text", "" + item);
el.style.top = `${Math.floor(index / 4) * 100}px`;
el.style.left = `${(index % 4) * 100}px`;
el.innerHTML = "" + item;
// el.style.top("500px');
app?.appendChild(el);
});
};

当然这里还有点样式,就直接贴下面了

#app {
box-sizing: border-box;
width: 400px;
height: 400px;
position: relative;
border: 1px solid red;
}
.box {
font-size: 24px;
cursor: pointer;
transition: all 0.3s;
text-align: center;
line-height: 100px;
box-sizing: border-box;
position: absolute;
width: 100px;
height: 100px;
border: 1px solid black;
/* border-bottom: 1px solid black; */
}

然后我们每次点击都需要判断当前点击的方块是否可以移动,首先我们初始化的时候肯定是默认最后一块是空的,也就是说空的这一块的上下左右四块都是可以移动的,它移动的最终位置就是空白的位置,移动完后空白的位置就变成了移动方块之前的位置了,我们这边写一个方法,我们传入点击方块的位置,与空白位置进行判断,判断是否为相邻,如果相邻则返回位置,否则返回false,这时候就有人问了,点击方块的位置我们该怎么找,其实只要拿个对象记录下就好了,可以在上面初始化代码中看到有一个position对象,它就是用来存每个数字对应的位置的,后续每次移动都是需要更新的

// 判断所点击的是否是可以移动的格子
const isMove = (index: number) => {
if (
space === index - 1 ||
space === index + 1 ||
space === index + 4 ||
space === index - 4
) {
return space;
}
return false;
};

接下来就看我们的移动逻辑了,我们是通过点击最外边的大方块,通过事件委托来获取到我们具体的某个方块的,初始化的时候我们对元素的data-text进行了一个赋值,这边就可以在dataset.text中获取到

const run = (el: any) => {
const p = position[+el.target.dataset.text];
if (isMove(p) === false) {
return false;
}
const real_top = `${Math.floor(space / 4) * 100}px`;
const real_left = `${(space % 4) * 100}px`;
const temp = p;
position[+el.target.dataset.text] = space;
space = temp;
el.target.style.top = real_top;
el.target.style.left = real_left;
return isSuccess();
};

移动逻辑其实很简单,点击方块的位置的top和left属性替换成空白的即可,有transition过渡所以就会有动画啦,当然别忘记了将position中的位置和空白位置进行交换更改 ,最后就是我们的Rx部分代码了,是不是很少,对于我们每次点击产生的数据流,concatMap都会进行连接,类似保存在队列里,由于concatMap是返回一个新的数据流,所以我们返回of(val)对当前点击元素进行一个推流,然后map就是遍历数据流,执行我们的移动逻辑,takeWhile就是判断我们什么时候执行,如果通关了就不执行,finalize就是最后我们会输出通关成功

const source$ = fromEvent(app!, "click").pipe(
concatMap((val) => of(val).pipe(map(run),delay(300))),
takeWhile(() => {
return !isSuccess();
}),
finalize(() => {
console.log("通关成功");
})
);

成功逻辑,这边不多说了,直接判断索引是否对应举行了

const isSuccess = () => {
for (const key in position) {
if (position[key] !== +key - 1) {
console.log(position[key], key);
return false;
}
}
return true;
};

具体效果可以查看sandbox: sandbox