Booch大神说过,一个好的软件或者系统应该“为系统的接缝(seam)建模”,我一直奉为经典,想要更好地构建或者说组织好一个程序,减少开发软件的复杂程度,我们当然应该组件化,分解功能,为之后的变化提供便利,毕竟“架构的目的目标必须是减少变化的影响和成本”(--Booch)。
我们知道,一个游戏,或者一个软件,最核心的是其玩法或者说算法。当然,我们应当实现其算法而暂时忽略游戏的外观和表象,我们把2048游戏可以抽象为一个操作矩阵的过程,2048游戏支持四种移动——向上、向下、向左、向右(其实还有随机生成方块的方法),我设计了一个数据结构集成了其中的操作,这样可以使我们的模块“自给自足”,方便测试。
game2048.js
function Game2048(n) {
let mat = []
ds = [
[0, n, 1],
[n - 1, 0, -1],
[0, n, 1],
[n - 1, 0, -1]
],
merge = false;
for(let i=0; i < n; ++i) {
let line = [];
line.length = n;
line.fill(0);
mat.push(line);
}
function compactUp(j) {
let p = 0;
for(let i=0; i < n; ++i) {
if(mat[i][j] != 0) {
let val = mat[i][j];
mat[i][j] = 0;
mat[p++][j] = val;
}
}
}
this.moveUp = () => {
merge = false;
for(let j = 0; j < n; ++j) {
compactUp(j);
for(let i = 0; i < n - 1; ++i) {
if(mat[i][j] != 0 && mat[i][j] == mat[i + 1][j]) {
mat[i][j] <<= 1;
mat[i + 1][j] = 0;
merge = true;
}
}
compactUp(j);
}
}
function compactDown(j) {
let p = n - 1;
for(let i=n - 1; i >= 0; --i) {
if(mat[i][j] != 0) {
let val = mat[i][j];
mat[i][j] = 0;
mat[p--][j] = val;
}
}
}
this.moveDown = () => {
merge = false;
for(let j = 0; j < n; ++j) {
compactDown(j);
for(let i = n - 1; i > 0; --i) {
if(mat[i][j] != 0 && mat[i][j] == mat[i - 1][j]) {
mat[i][j] <<= 1;
mat[i - 1][j] = 0;
merge = true;
}
}
compactDown(j);
}
}
function compactLeft(i) {
let p = 0;
for(let j=0; j < n; ++j) {
if(mat[i][j] != 0) {
let val = mat[i][j];
mat[i][j] = 0;
mat[i][p++] = val;
}
}
}
this.moveLeft = () => {
merge = false;
for(let i=0; i < n; ++i) {
compactLeft(i);
for(let j=0; j < n - 1; ++j) {
if(mat[i][j] != 0 && mat[i][j] == mat[i][j + 1]) {
mat[i][j] <<= 1;
mat[i][j + 1] = 0;
merge = true;
}
}
compactLeft(i);
}
}
function compactRight(i) {
let p = n - 1;
for(let j=n - 1; j >= 0; --j) {
if(mat[i][j] != 0) {
let val = mat[i][j];
mat[i][j] = 0;
mat[i][p--] = val;
}
}
}
this.moveRight = () => {
merge = false;
for(let i=0; i < n; ++i) {
compactRight(i);
for(let j=n - 1; j > 0; --j) {
if(mat[i][j] != 0 && mat[i][j] == mat[i][j - 1]) {
mat[i][j] <<= 1;
mat[i][j - 1] = 0;
merge = true;
}
}
compactRight(i);
}
}
this.randBlock = (seed = 0.5) => {
let sites = [];
for(let i=0; i < n; ++i) {
for(let j=0; j < n; ++j) {
if(mat[i][j] == 0) {
sites.push([i, j]);
}
}
}
if (sites.length) {
utils.shuffle(sites);
mat[sites[0][0]][sites[0][1]] = (Math.random() > seed? 4: 2);
}
}
this.isMerge = () => merge;
this.toMat = () => mat;
this.showMat = () => {
for(let i=0; i < n; ++i) {
let line = [];
for(let j=0; j <n; ++j) {
line.push(mat[i][j]);
}
console.log(line.join(" "));
}
}
} 另外,我把里面的一些核心方法抽了出来,比如高纳德置乱方法,主要是方便我以后写其他小程序使用。
basic.js
class utils {
/**
* 高纳德(Knuth)置乱法
* 在前一次未被选中的概率为 (n - i) / n
* 本次该格被选中的概率为 1 / (n - i)
* 相乘后,即任何元素进任意格内的概率为 1 / n
*/
static shuffle(arr) {
for(let i=arr.length; i > 0; --i) {
let k = parseInt(Math.random() * i),
t = arr[k];
arr[k] = arr[i - 1];
arr[i - 1] = t;
}
}
} 核心算法实现后,我们就可以开始写可视化的部分了,这里我用了Canvas作为画板,为了能更方便操作Canvas绘图,我把一些方法抽了出来,形成了一个子模块。这个模块有两个核心方法,一个是画一个方块,一个是打印文字,之后我们会用到这两个函数。
my_canvas.js
function MyCanvas(canvas) {
console.assert(canvas != null && canvas != undefined);
let ctx = canvas.getContext("2d");
this.clearVis = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
this.rect = (R) => {
ctx.beginPath();
ctx.moveTo(R.x, R.y);
ctx.lineTo(R.x, R.y + R.width);
ctx.lineTo(R.x + R.height, R.y + R.width);
ctx.lineTo(R.x + R.height, R.y);
ctx.closePath();
if(R.fillColor) {
ctx.fillStyle = R.fillColor;
ctx.fill();
}
ctx.lineWidth = Math.max(0, R.lineWidth);
ctx.strokeStyle = R.strokeStyle? R.strokeStyle: "black";
ctx.stroke();
}
this.string = (T) => {
ctx.font = T.font;
ctx.fillStyle = T.fillColor;
ctx.textAlign = T.align? T.align: "center";
ctx.textBaseline = T.baseline? T.baseline: "middle";
ctx.fillText(T.content, T.x, T.y);
}
} 好了,核心算法和可视化的模块都已经实现了,我们还需要把两个模块“胶接”在一起,也就是游戏的控制,比如键盘事件监听、游戏配置、2048里方块的颜色大小偏移、按键方向提示、绘制文字设置等等,这些代码并不是核心,但是比较影响玩家的体验,所以我在这个部分写一下,这个部分代码虽然很多,但大多只是描述以及调用了上面的核心模块实现。
index.js
(function() {
const N = 4;
const SIZE = 50;
const MID = 5;
const LEFT = 500;
const TOP = 200;
const TAR_SCORE = Math.pow(2, 11);
const TWO_RATE = 0.8;
let dirs = [[[1,1,1],[0,1,0],[0,1,0]],[[0,1,0],[0,1,0],[1,1,1]],[[1,0,0],[1,1,1],[1,0,0]],[[0,0,1],[1,1,1],[0,0,1]]];
let can = new MyCanvas(document.getElementById("myCanvas")),
game = new Game2048(N),
lastCode = null,
colors = ["#AAA", "#82D900", "#2894FF", "#004B97", "#4A4AFF", "#613030", "#EAC100", "#FF9224", "#BB5E00", "#B766AD", "#FF5809", "#7E3D76"],
dict = {},
over = false;
dict[0] = colors[0];
for(let i=1; i < colors.length; ++i) dict[Math.pow(2, i)] = colors[i];
// update game canvas
function show() {
let mat = game.toMat();
const my_font = "15px bold 微软雅黑";
const box_size = SIZE + MID;
const txt_center = (box_size >> 1);
for(let i=0; i < N; ++i) {
for(let j=0; j < N; ++j) {
if(mat[i][j] == TAR_SCORE) over = true;
can.rect({
x: LEFT + j * box_size,
y: TOP + i * box_size,
width: SIZE,
height: SIZE,
fillColor: dict[mat[i][j]],
strokeStyle: "#EFEFEF"
});
if(mat[i][j] > 0) {
can.string({
x: LEFT + txt_center + j * box_size,
y: TOP + txt_center + i * box_size,
content: mat[i][j],
font: my_font,
fillColor: "#FFF"
});
}
}
}
}
// show current arrow
function showArrow(i) {
const size = 10;
const top = 200;
const left = 450;
let dir = dirs[i];
for(let i=0; i < 3; ++i) {
for(let j=0; j < 3; ++j) {
can.rect({
x: left + j * size,
y: top + i * size,
width: size,
height: size,
fillColor: dir[i][j]? "#D94600": "#EFEFEF",
strokeStyle: "#FFF"
});
}
}
}
// game converge
function gameConverge(code) {
switch(code) {
case "KeyW":
case "ArrowUp":
game.moveUp();
showArrow(0);
break;
case "ArrowDown":
case "KeyS":
game.moveDown();
showArrow(1);
break;
case "ArrowLeft":
case "KeyA":
game.moveLeft();
showArrow(2);
break;
case "ArrowRight":
case "KeyD":
game.moveRight();
showArrow(3);
break;
default:
return false;
}
return true;
}
document.addEventListener("keydown", (event) => {
if(!over && gameConverge(event.code)) {
if (lastCode != event.code) game.randBlock(TWO_RATE);
lastCode = event.code;
show();
if(over) {
setTimeout(() => {
alert("恭喜你,游戏通关了!");
window.location.reload();
}, 100);
}
}
});
// init game
for(let i=0; i < N; ++i) game.randBlock();
show();
})() 最后,我们用一个HTML把这些都组合起来。
game2048.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>2048</title>
<script type="text/javascript" src="./basic.js"></script>
<script type="text/javascript" src="./game2048.js"></script>
<script type="text/javascript" src="./my_canvas.js"></script>
</head>
<body style="overflow: hidden;">
<canvas id="myCanvas" width="1000" height="1000">
can't support canvas
</canvas>
<script type="text/javascript" src="./index.js"></script>
</body>
</html> 见证奇迹的时刻到了,打开HTML我们就可以愉快玩耍了。

你学会了吗?