上周末写好了大部分功能函数,今天趁工作做完,把剩下的功能写好。下面先给出效果图:
写下来还是比较顺利的,把整个游戏拆分成一个一个的功能函数,最后只要有选择性的调用就可以了。
总体思路顺着代码注释看应该是比较清晰的。我基本的思想是将整个游戏分为2个部分,第一部分是当前控制的图形,可以左右移动,旋转,当它不能再向下移动时,它就变为了第二部分的内容,同时创建一个新的当前控制图形;第二部分就是所有已经固定位置的格子的集合(只能说固定的格子,因为有消行)。
在判断当前控制的图形是否可以操控时,我是建了一个类似本体镜像的函数mirror(),它会复制一份本体,但不是引用和本体指向相同的堆地址。这样我只需要判断镜像作出动作后,是否有冲突,如果没有就把这个镜像的值赋予当前控制的图形。同时在判断动作发生后图形所在的格子是否已经被之前剩余的格子所占用,是根据这个格子是否存在class为'cell'的。为了方便(为了忽略当前控制图形占用的格子),每次调用都会把当前控制的图形删除掉。
显然是可以优化的:
比如多一次判断:发生动作后的图形所在的格子忽略发生动作前的所在格子,这样如果没有冲突(可以进行动作),再调用擦除和绘制函数。
再看消行函数:
i代表了第几行,第一次n为0,所以i只需要大于最低判定高度,如果存在第n次大循环(最外层大循环的次数即info['lines']内记录的可以消除的行数),那么就代表之前消除了n行,所以可以判定i大于(info['top'] + n),可以减少循环次数。
上图所指的行数即为上图状态下的(info['top']+n)的位置。
代码如下:
<!doctype html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="format-detection" content="telephone=no">
<style>
body {
position: relative;
margin: 0;
width: 100vw;
height: 100vh;
}
.wrapper {
padding: 40px 0;
}
.tetris-box {
position: relative;
margin: 0 auto;
border-collapse: collapse;
border: solid #ddd;
border-width: 1px 0px 0px 1px;
}
.row {
position: relative;
}
.tetris-box td {
padding: 10px 9px 9px 10px;
border: solid #ddd;
border-width: 0px 1px 1px 0px;
}
.cell {
box-shadow: inset 1px 1px 0px 4px rgba(0, 0, 0, 0.4);
/*border: solid #f10 !important;
border-width: 0px 1px 1px 0px !important;*/
}
.color1 {
background-image: linear-gradient(45deg, #f10, #fff 50%, #f10 50%);
}
.color2 {
background-image: linear-gradient(-45deg, #00b0ff 35%, #fff 50%, #00b0ff 65%);
}
.color3 {
background-image: linear-gradient(45deg, #dcad6b 50%, #fff 50%, #dcad6b 100%);
}
.color4 {
background-image: linear-gradient(135deg, #6bdc7d, #fff 55%, #6bdc7d 50%);
}
.color5 {
background-image: linear-gradient(-135deg, #d46bdc, #fff 55%, #d46bdc 50%);
}
.color6 {
background-image: radial-gradient(#00b0ff, #fff 60%, #00b0ff 50%);
}
.color7 {
background-image: radial-gradient(#e6d7d7, #fff 85%, #e6d7d7 100%);
}
.btn-box {
text-align: center;
}
.tetris-box:before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 201px;
height: 60px;
background-color: #fff;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="btn-box">
<p>可以用空格键控制开始、暂停、继续</p>
</div>
<table id="tetris_box" class="tetris-box">
</table>
</div>
<script>
//Array扩展indexOf
Array.prototype.indexOf = function(val) {
for (var i = 0; i < this.length; i++) {
if (this[i] == val) return i;
}
return -1;
};
(function() {
// 基本配置
function settings() {
var settings = {
width: 10,
height: 23,
speed: 500
}
return settings;
}
var o = new settings();
var doc = document; //保存全局document引用
// 存储需要重复使用的全局变量
var g = {
tetrisArray: [], //保存每个格子的颜色
savedArray: [], //保存已经固定的图形的信息
graphArray: [], //保存当前图形的数据
color: 0, //保存上次的颜色
shape: 0, //保存上次的形状
y: {'isY': 0, 'time': 0}, //保存需要第二次旋转y补1的图形信息 isY:是否需要补y,time:是否旋转
start: 0, //记录开始/暂停 0:暂停, 1:开始/继续
timer: null //计时器
}
// 构建UI
var tetrisBox = doc.getElementById('tetris_box');
tetrisBox.innerHTML = null;
var fragment = doc.createDocumentFragment();
for (var j = 0; j < o.height; j++) {
var hTr = doc.createElement('tr');
hTr.setAttribute('class', 'row');
var cellFragment = doc.createDocumentFragment();
g.tetrisArray[j] = [];
for (var i = 0; i < o.width; i++) {
var wTd = doc.createElement('td');
var wId = j + '_' + i;
wTd.setAttribute('id', wId);
// wTd.setAttribute('class', 'cell');
cellFragment.appendChild(wTd);
g.tetrisArray[j][i] = -1;
}
fragment.appendChild(hTr).appendChild(cellFragment);
}
tetrisBox.appendChild(fragment);
// 随机选择一个形状
function createGraph() {
var chance,
center = Math.floor(o.width / 2);
do {
chance = Math.floor(Math.random() * 100);
} while(chance >= 98)
chance %= 7;
g.graphArray.length = 0;
switch(chance) {
/*
****
*/
case 0:
g.graphArray.push({x: 3, y: center - 2}, {x: 3, y: center - 1}, {x: 3, y: center}, {x: 3, y: center + 1});
// console.count('第1个');
break;
/*
**
**
*/
case 1:
g.graphArray.push({x: 2, y: center - 1}, {x: 2, y: center}, {x: 3, y: center - 1}, {x: 3, y: center});
// console.count('第2个');
break;
/*
**
**
*/
case 2:
g.graphArray.push({x: 2, y: center - 1}, {x: 2, y: center}, {x: 3, y: center - 2}, {x: 3, y: center - 1});
// console.count('第3个');
break;
/*
**
**
*/
case 3:
g.graphArray.push({x: 2, y: center - 2}, {x: 2, y: center - 1}, {x: 3, y: center - 1}, {x: 3, y: center});
// console.count('第4个');
break;
/*
***
*
*/
case 4:
g.graphArray.push({x: 2, y: center -2}, {x: 2, y: center - 1}, {x: 2, y: center}, {x: 3, y: center});
// console.count('第5个');
break;
/*
***
*
*/
case 5:
g.graphArray.push({x: 2, y: center - 2}, {x: 2, y: center - 1}, {x: 2, y: center}, {x: 3, y: center - 2});
// console.count('第6个');
break;
/*
*
***
*/
case 6:
g.graphArray.push({x: 3, y: center - 2}, {x: 3, y: center - 1}, {x: 3, y: center}, {x: 2, y: center - 1});
// console.count('第7个')
break;
}
g.shape = chance;
if (chance == 0 || chance == 2 || chance == 3) {
g.y['isY'] = 1;
} else {
g.y['isY'] = 0;
}
g.y['time'] = 0;
}
// 创建一个新的图形
function createNewGraph() {
var n;
do {
n = Math.ceil(Math.random() * 7);
} while (n == g.color);
createGraph();
g.color = n;
drawNowerGraph();
}
createNewGraph();
// 绘制当前控制的图形
function drawNowerGraph() {
var length = g.graphArray.length;
var address;
for (var i = length - 1; i >= 0; i--) {
address = g.graphArray[i].x + '_' + g.graphArray[i].y;
if (doc.getElementById(address).classList.contains('cell')) {
location.reload();
}
doc.getElementById(address).classList.add('cell');
doc.getElementById(address).classList.add('color' + g.color);
}
}
// 擦除当前控制的图形
function eraseNowerGraph() {
var length = g.graphArray.length;
var address;
for (var i = length - 1; i >= 0; i--) {
address = g.graphArray[i].x + '_' + g.graphArray[i].y;
doc.getElementById(address).classList.remove('cell');
doc.getElementById(address).classList.remove('color' + g.color);
}
}
// 绘制已经固定的图形(除当前控制的图形以外的)
function drawSavedGraph() {
var length = g.savedArray.length,
address;
for (var i = length - 1; i >= 0; i--) {
address = g.savedArray[i].x + '_' + g.savedArray[i].y;
doc.getElementById(address).classList.add('cell');
doc.getElementById(address).classList.add('color' + g.savedArray[i].color);
}
}
// 擦除已经固定的图形
function eraseSavedGraph() {
var length = g.savedArray.length,
address;
for (var i = length - 1; i >= 0; i--) {
address = g.savedArray[i].x + '_' + g.savedArray[i].y;
doc.getElementById(address).classList.remove('cell');
doc.getElementById(address).classList.remove('color' + g.savedArray[i].color);
}
}
// 检测是否可以消行
function checkCanDelete() {
var info = {
'lines': [],
'top': 0
},
flagLines, //判断能否消行的辅助标识
flagTop; //如果某一行g.tetrisArray[i][j]均为-1,即可退出循环
for (var i = o.height - 1; i >= 0; i--) {
flagLines = true;
flagTop = 0;
for (var j = o.width - 1; j >= 0; j--) {
if (g.tetrisArray[i][j] == -1) {
flagLines = false;
flagTop++;
}
}
if (flagLines) {
info['lines'].push(i);
}
if (flagTop == o.width) {
info['top'] = i;
break;
}
}
return info;
}
// 消行
function deleteLines(info) {
for (var l = info['lines'].length -1, n = 0; l >= 0; l--, n++) {
for (var i = info['lines'][l]; i > info['top'] + n; i--) {
for (var j = o.width - 1; j >= 0; j--) {
g.tetrisArray[i][j] = g.tetrisArray[i - 1][j];
}
}
}
eraseSavedGraph();
g.savedArray.length = 0;
for (var i = o.height - 1; i >= info['top'] + info['lines'].length; i--) {
for (var j = o.width - 1; j >= 0; j--) {
if (g.tetrisArray[i][j] != -1) {
g.savedArray.push({
x: i,
y: j,
color: g.tetrisArray[i][j]
})
}
}
}
drawSavedGraph();
}
document.onkeydown = control;
// 控制
function control() {
if (event.keyCode == 32) {
if (g.start == 0) {
g.timer = setInterval(down, 500);
g.start = 1;
} else {
clearInterval(g.timer);
g.start = 0;
}
}
if (g.start == 0) {
return false;
}
var dir = event.keyCode;
switch(dir) {
case 37:
left();
break;
case 38:
if (g.shape == 1) break;
rotate(g.z);
break;
case 39:
right();
break;
case 40:
down();
break;
}
}
// 左移动
function left() {
eraseNowerGraph();
var address,
checkArray = mirror(g.graphArray),
length = g.graphArray.length,
can_move = true;
for (var i = length - 1; i >= 0; i--) {
address = checkArray[i].x + '_' + (checkArray[i].y - 1);
if (--checkArray[i].y < 0 || doc.getElementById(address).classList.contains('cell')) {
can_move = false;
}
}
if (can_move == true) {
g.graphArray = checkArray;
}
drawNowerGraph();
}
// 右移动
function right() {
eraseNowerGraph();
var address,
checkArray = mirror(g.graphArray),
length = g.graphArray.length,
can_move = true;
for (var i = length - 1; i >= 0; i--) {
address = checkArray[i].x + '_' + (checkArray[i].y + 1);
if (++checkArray[i].y > o.width - 1 || doc.getElementById(address).classList.contains('cell')) {
can_move = false;
}
}
if (can_move == true) {
g.graphArray = checkArray;
}
drawNowerGraph();
}
// 下移动
function down() {
eraseNowerGraph();
var address,
x,
y,
checkArray = mirror(g.graphArray),
length = g.graphArray.length,
can_move = true;
for (var i = length - 1; i >= 0; i--) {
address = (checkArray[i].x + 1) + '_' + checkArray[i].y;
if (++checkArray[i].x > o.height - 1 || doc.getElementById(address).classList.contains('cell')) {
can_move = false;
}
}
if (can_move == true) {
g.graphArray = checkArray;
}
drawNowerGraph();
if (can_move == false) {
for (var i = length - 1; i >= 0; i--) {
x = g.graphArray[i].x;
y = g.graphArray[i].y;
g.savedArray.push({
x: x,
y: y,
color: g.color
})
g.tetrisArray[x][y] = g.color;
}
var info = checkCanDelete();
if (info['lines'].length) {
deleteLines(info);
}
createNewGraph();
}
}
// 旋转 坐标(x,y)绕(x0,y0)逆时针旋转90度后的坐标为(x0+y0-y,y0-x0+x)
// 一字型、Z字型和方块型采用此旋转算法会下移一格,对这3种图形做了处理
function rotate() {
eraseNowerGraph();
var originX,
originY,
address,
length = g.graphArray.length,
can_rotate = true,
checkArray = mirror(g.graphArray),
centerX = Math.round((g.graphArray[0].x + g.graphArray[1].x + g.graphArray[2].x + g.graphArray[3].x) / 4),
centerY = Math.round((g.graphArray[0].y + g.graphArray[1].y + g.graphArray[2].y + g.graphArray[3].y) / 4);
for (var i = length - 1; i >= 0; i--) {
originX = g.graphArray[i].x;
originY = g.graphArray[i].y;
checkArray[i].x = centerX + centerY - originY - g.y['time'];
checkArray[i].y = centerY - centerX + originX;
address = checkArray[i].x + '_' + checkArray[i].y;
if (checkArray[i].y < 0 || checkArray[i].y > o.width - 1 || checkArray[i].x < 0 || checkArray[i].x > o.height || doc.getElementById(address).classList.contains('cell')) {
can_rotate = false;
}
}
if (can_rotate == true) {
g.graphArray = checkArray;
if (g.y['isY'] == 1) {
g.y['time'] = g.y['time'] == 1 ? 0 : 1;
}
}
drawNowerGraph();
}
// 创建一个保存graphArray数据的继承函数
// 这个函数是为了解决包含引用类型值的属性始终都会共享相应的值。继承函数具体可见《JavaScript高级程序设计》P171。引用赋值可见https://www.zhihu.com/question/27114726。
function mirror(graphArray) {
var checkArray = [{x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}];
for (var i = 0; i < 4; i++) {
checkArray[i].x = graphArray[i].x;
checkArray[i].y = graphArray[i].y;
}
return checkArray;
}
})();
</script>
</body>
</html>
最后编辑于2016年12月15日。
2017年1月12日补充:
代码最后的继承函数在ES6中可以被克隆对象替代:
function clone(obj) {
return Object.assign({}, obj);
}
2017年4月10日补充:
今天学习的时候发现,Object.assign()方法是浅拷贝。
具体可以见这篇博客:http://blog.youkuaiyun.com/waiterwaiter/article/details/50267787,并且该博主在文末提出了一个新的深拷贝的方法,还是很有意思的。