我之前的游戏,蛇,需要一段时间才能发货。了解完成游戏意味着什么是一次很好的体验,但实际上,这些只是我玩一些基本游戏机制和学习一点游戏编程的副项目。如果我在每个小项目上都花那么多时间,到 2035 年我仍然会这样做……
……考虑到这一点,我需要确保我的下一场比赛是一个短暂的、单一的周末项目,所以我选择了一个简单的项目,而且我没有花任何时间来完善它,超出了……的主要游戏机制。
俄罗斯方块
没有抛光,它有点丑,但它的功能齐全,我可以告诉你如何自己实现它......
实施细则
俄罗斯方块是一个非常容易实现的游戏,只是一个带有一些内联 css/javascript 的简单 html 文件。
唯一棘手的部分是处理 7 个 tetromino 的旋转。
您可以尝试在运行时进行数学旋转,但您会很快发现,一些部分围绕中心块(j、l、s、t 和 z)旋转,而其他部分则围绕块之间的点(i 和 o)旋转)。
您可以对 2 种不同的行为进行特殊处理,或者您可以接受这样一个事实,即俄罗斯方块被硬编码为始终具有 7 个旋转 4 次的片段,并且只需提前对所有 28 个模式进行硬编码。
如果您假设所有部分在概念上都布置在 4x4 网格上,其中每个单元格要么被占用,要么不被占用,则可以避免其他特殊情况代码。有趣的是 4x4 = 16,而“被占用与否”= 1 或 0。
听起来我们可以将每个模式表示为一个简单的 16 位整数,以准确定义我们希望每个部分如何旋转:
var i = { blocks: [0x0F00, 0x2222, 0x00F0, 0x4444], color: 'cyan' };
var j = { blocks: [0x44C0, 0x8E00, 0x6440, 0x0E20], color: 'blue' };
var l = { blocks: [0x4460, 0x0E80, 0xC440, 0x2E00], color: 'orange' };
var o = { blocks: [0xCC00, 0xCC00, 0xCC00, 0xCC00], color: 'yellow' };
var s = { blocks: [0x06C0, 0x8C40, 0x6C00, 0x4620], color: 'green' };
var t = { blocks: [0x0E40, 0x4C40, 0x4E00, 0x4640], color: 'purple' };
var z = { blocks: [0x0C60, 0x4C80, 0xC600, 0x2640], color: 'red' };
然后我们可以提供一个辅助方法,它给出:
- 上面的部分之一
- 旋转方向 (0-3)
- 俄罗斯方块网格上的一个位置
... 将遍历俄罗斯方块网格中该棋子将占据的所有单元格:
function eachblock(type, x, y, dir, fn) {
var bit, result, row = 0, col = 0, blocks = type.blocks[dir];
for(bit = 0x8000 ; bit > 0 ; bit = bit >> 1) {
if (blocks & bit) {
fn(x + col, y + row);
}
if (++col === 4) {
col = 0;
++row;
}
}
};
有效的工件定位
当左右滑动一块或将它放下一行时,我们需要小心我们的边界检查。我们可以在我们的eachblock
助手的基础上提供一个occupied
方法,如果在俄罗斯方块网格上的某个位置放置一块所需的任何块(具有特定的旋转方向)被占用或超出边界,则该方法返回 true:
function occupied(type, x, y, dir) {
var result = false
eachblock(type, x, y, dir, function(x, y) {
if ((x < 0) || (x >= nx) || (y < 0) || (y >= ny) || getBlock(x,y))
result = true;
});
return result;
};
function unoccupied(type, x, y, dir) {
return !occupied(type, x, y, dir);
};
注意:假设
getBlock
返回 true 或 false 以指示俄罗斯方块网格上的该位置是否被占用。
随机化下一块
随机选择下一块是一个有趣的难题。如果我们以纯粹随机的方式选择,例如:
var pieces = [i,j,l,o,s,t,z];
var next = pieces[Math.round(Math.random(0, pieces.length-1))];
……我们发现游戏非常令人沮丧,因为我们经常得到相同类型的长序列,有时很长一段时间都没有得到我们想要的片段。
在俄罗斯方块中挑选下一块的标准方法似乎是想象一个袋子,每块有 4 个实例,我们随机从袋子中取出一件物品,直到它变空,然后冲洗并重复。
这确保了每件作品在每 28 件作品中至少出现 4 次,它还确保同一件作品只会在链中重复,最多 4 次……好吧,从技术上讲,最多可以出现 8 次,因为我们可以得到一个袋子的末端有 4 个链子,下一个袋子的开头是 4 个链子,但这种可能性很小。
这使得游戏更具可玩性,因此我们实现了如下randomPiece
方法:
var pieces = [];
function randomPiece() {
if (pieces.length == 0)
pieces = [i,i,i,i,j,j,j,j,l,l,l,l,o,o,o,o,s,s,s,s,t,t,t,t,z,z,z,z];
var type = pieces.splice(random(0, pieces.length-1), 1)[0]; // remove a single piece
return { type: type, dir: DIR.UP, x: 2, y: 0 };
};
一旦我们有了我们的数据结构和辅助方法,游戏的其余部分就会变得相当简单。
游戏常量和变量
我们声明了一些永远不会改变的常量:
var KEY = { ESC: 27, SPACE: 32, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40 },
DIR = { UP: 0, RIGHT: 1, DOWN: 2, LEFT: 3, MIN: 0, MAX: 3 },
stats = new Stats(),
canvas = get('canvas'),
ctx = canvas.getContext('2d'),
ucanvas = get('upcoming'),
uctx = ucanvas.getContext('2d'),
speed = { start: 0.6, decrement: 0.005, min: 0.1 }, // seconds until current piece drops 1 row
nx = 10, // width of tetris court (in blocks)
ny = 20, // height of tetris court (in blocks)
nu = 5; // width/height of upcoming preview (in blocks)
以及一些会改变的变量,可能reset()
适用于每场比赛:
var dx, dy, // pixel size of a single tetris block
blocks, // 2 dimensional array (nx*ny) representing tetris court - either empty block or occupied by a 'piece'
actions, // queue of user actions (inputs)
playing, // true|false - game is in progress
dt, // time since starting this game
current, // the current piece
next, // the next piece
score, // the current score
rows, // number of completed rows in the current game
step; // how long before current piece drops by 1 row
在更复杂的游戏中,这些都应该封装在一个或多个类中,但为了保持俄罗斯方块的简单,我们使用了简单的全局变量。但这并不意味着我们希望在整个代码中修改它们,因此为大多数可变游戏状态编写 getter 和 setter 方法仍然有意义。
function setScore(n) { score = n; invalidateScore(); };
function addScore(n) { score = score + n; };
function setRows(n) { rows = n; step = Math.max(speed.min, speed.start - (speed.decrement*rows)); invalidateRows(); };
function addRows(n) { setRows(rows + n); };
function getBlock(x,y) { return (blocks && blocks[x] ? blocks[x][y] : null); };
function setBlock(x,y,type) { blocks[x] = blocks[x] || []; blocks[x][y] = type; invalidate(); };
function setCurrentPiece(piece) { current = piece || randomPiece(); invalidate(); };
function setNextPiece(piece) { next = piece || randomPiece(); invalidateNext(); };
这也使我们能够以可控的方式知道值何时发生更改,以便我们可以invalidate
了解 UI 并知道该部分需要重新渲染。这将使我们能够优化我们的渲染,并且只draw
在它们发生变化时进行优化。
游戏循环
核心游戏循环是来自pong、breakout 和snakes的相同循环的简化版本。使用requestAnimationFrame (或 polyfill),我们只需要update
根据时间间隔和draw
结果来确定我们的游戏状态:
var last = now = timestamp();
function frame() {
now = timestamp();
update((now - last) / 1000.0);
draw();
last = now;
requestAnimationFrame(frame, canvas);
}
frame(); // start the first frame
处理键盘输入
我们的键盘处理程序非常简单,我们实际上并没有对按键执行任何操作(除了开始/放弃游戏)。相反,我们只是将动作记录在一个队列中,以便在我们的update
过程中处理。
function keydown(ev) {
if (playing) {
switch(ev.keyCode) {
case KEY.LEFT: actions.push(DIR.LEFT); break;
case KEY.RIGHT: actions.push(DIR.RIGHT); break;
case KEY.UP: actions.push(DIR.UP); break;
case KEY.DOWN: actions.push(DIR.DOWN); break;
case KEY.ESC: lose(); break;
}
}
else if (ev.keyCode == KEY.SPACE) {
play();
}
};
玩游戏
定义了我们的数据结构,设置了我们的常量和变量,提供了我们的 getter 和 setter,开始了一个游戏循环并处理了键盘输入,我们现在可以看看实现俄罗斯方块游戏机制的逻辑:
该update
循环由处理下一个用户动作,并且如果累积的时间比一些变量(基于完成的行的数量)越大,那么我们通过1行删除当前片:
function update(idt) {
if (playing) {
handle(actions.shift());
dt = dt + idt;
if (dt > step) {
dt = dt - step;
drop();
}
}
};
处理用户输入包括向左或向右移动当前块、旋转它或将其再放下 1 行:
function handle(action) {
switch(action) {
case DIR.LEFT: move(DIR.LEFT); break;
case DIR.RIGHT: move(DIR.RIGHT); break;
case DIR.UP: rotate(); break;
case DIR.DOWN: drop(); break;
}
};
move
和rotate
简单地改变当前片x
,y
或dir
可变的,但是它们必须检查,以确保新的位置/方向未被占用:
function move(dir) {
var x = current.x, y = current.y;
switch(dir) {
case DIR.RIGHT: x = x + 1; break;
case DIR.LEFT: x = x - 1; break;
case DIR.DOWN: y = y + 1; break;
}
if (unoccupied(current.type, x, y, current.dir)) {
current.x = x;
current.y = y;
invalidate();
return true;
}
else {
return false;
}
};
function rotate(dir) {
var newdir = (current.dir == DIR.MAX ? DIR.MIN : current.dir + 1);
if (unoccupied(current.type, current.x, current.y, newdir)) {
current.dir = newdir;
invalidate();
}
};
该drop
方法会将当前块向下移动 1 行,但如果那不可能,则当前块尽可能低并且将被分解为单独的块。在这一点上,我们增加分数,检查任何已完成的线条并设置一个新作品。如果新棋子也将处于被占用的位置,则游戏结束:
function drop() {
if (!move(DIR.DOWN)) {
addScore(10);
dropPiece();
removeLines();
setCurrentPiece(next);
setNextPiece(randomPiece());
if (occupied(current.type, current.x, current.y, current.dir)) {
lose();
}
}
};
function dropPiece() {
eachblock(current.type, current.x, current.y, current.dir, function(x, y) {
setBlock(x, y, current.type);
});
};
渲染
渲染成为<canvas>
API 和 DOM 辅助方法的相当直接的使用html
:
function html(id, html) { document.getElementById(id).innerHTML = html; }
由于俄罗斯方块以相当“笨重”的方式移动,我们可以认识到我们不需要以 60fps 的速度重绘每一帧的所有内容。只有当元素发生变化时,我们才能简单地重绘元素。对于这个简单的实现,我们将 UI 分解为 4 个部分,并且仅在检测到以下变化时重新渲染这些部分:
- 分数
- 完成的行数
- 下一块预览显示
- 比赛场地
最后一部分,游戏场地,是一个相当广泛的类别。从技术上讲,我们可以跟踪网格中的每个单独的块,并只重绘发生变化的块,但这会有点过头了。重新绘制整个网格可以在几毫秒内完成,并且确保我们只在发生变化时才这样做意味着我们每秒只接受几次小的点击。
var invalid = {};
function invalidate() { invalid.court = true; }
function invalidateNext() { invalid.next = true; }
function invalidateScore() { invalid.score = true; }
function invalidateRows() { invalid.rows = true; }
function draw() {
ctx.save();
ctx.lineWidth = 1;
ctx.translate(0.5, 0.5); // for crisp 1px black lines
drawCourt();
drawNext();
drawScore();
drawRows();
ctx.restore();
};
function drawCourt() {
if (invalid.court) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (playing)
drawPiece(ctx, current.type, current.x, current.y, current.dir);
var x, y, block;
for(y = 0 ; y < ny ; y++) {
for (x = 0 ; x < nx ; x++) {
if (block = getBlock(x,y))
drawBlock(ctx, x, y, block.color);
}
}
ctx.strokeRect(0, 0, nx*dx - 1, ny*dy - 1); // court boundary
invalid.court = false;
}
};
function drawNext() {
if (invalid.next) {
var padding = (nu - next.type.size) / 2; // half-arsed attempt at centering next piece display
uctx.save();
uctx.translate(0.5, 0.5);
uctx.clearRect(0, 0, nu*dx, nu*dy);
drawPiece(uctx, next.type, padding, padding, next.dir);
uctx.strokeStyle = 'black';
uctx.strokeRect(0, 0, nu*dx - 1, nu*dy - 1);
uctx.restore();
invalid.next = false;
}
};
function drawScore() {
if (invalid.score) {
html('score', ("00000" + Math.floor(score)).slice(-5));
invalid.score = false;
}
};
function drawRows() {
if (invalid.rows) {
html('rows', rows);
invalid.rows = false;
}
};
function drawPiece(ctx, type, x, y, dir) {
eachblock(type, x, y, dir, function(x, y) {
drawBlock(ctx, x, y, type.color);
});
};
function drawBlock(ctx, x, y, color) {
ctx.fillStyle = color;
ctx.fillRect(x*dx, y*dy, dx, dy);
ctx.strokeRect(x*dx, y*dy, dx, dy)
};
改进空间
就像我说的,这只是一个原始的俄罗斯方块游戏机制。如果您要完善此游戏,您可能需要添加以下内容:
- 菜单
- 水平
- 高分数
- 动画和特效
- 音乐和音效
- 触摸支持
- 玩家对玩家
- 玩家对AI
- (等等)
相关链接
游戏在桌面浏览器上玩得最好。抱歉,没有移动支持。
- Chrome、火狐、IE9+、Safari、Opera
index.html
<!DOCTYPE html>
<html>
<head>
<title>Javascript Tetris</title>
<link href="default.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="tetris">
<div id="menu">
<p id="start"><a href="javascript:play();">Press Space to Play.</a></p>
<p><canvas id="upcoming"></canvas></p>
<p>score <span id="score">00000</span></p>
<p>rows <span id="rows">0</span></p>
</div>
<canvas id="canvas">
Sorry, this example cannot be run because your browser does not support the <canvas> element
</canvas>
</div>
<script src="tetris.js"></script>
</body>
</html>
default.css
body { font-family: Helvetica, sans-serif; }
#tetris { margin: 1em auto; padding: 1em; border: 4px solid black; border-radius: 10px; background-color: #F8F8F8; }
#stats { display: inline-block; vertical-align: top; }
#canvas { display: inline-block; vertical-align: top; background: url(texture.jpg); box-shadow: 10px 10px 10px #999; border: 2px solid #333; }
#menu { display: inline-block; vertical-align: top; position: relative; }
#menu p { margin: 0.5em 0; text-align: center; }
#menu p a { text-decoration: none; color: black; }
#upcoming { display: block; margin: 0 auto; background-color: #E0E0E0; }
#score { color: red; font-weight: bold; vertical-align: middle; }
#rows { color: blue; font-weight: bold; vertical-align: middle; }
#stats { position: absolute; bottom: 0em; right: 1em; }
@media screen and (min-width: 0px) and (min-height: 0px) { #tetris { font-size: 0.75em; width: 250px; } #menu { width: 100px; height: 200px; } #upcoming { width: 50px; height: 50px; } #canvas { width: 100px; height: 200px; } } /* 10px chunks */
@media screen and (min-width: 400px) and (min-height: 400px) { #tetris { font-size: 1.00em; width: 350px; } #menu { width: 150px; height: 300px; } #upcoming { width: 75px; height: 75px; } #canvas { width: 150px; height: 300px; } } /* 15px chunks */
@media screen and (min-width: 500px) and (min-height: 500px) { #tetris { font-size: 1.25em; width: 450px; } #menu { width: 200px; height: 400px; } #upcoming { width: 100px; height: 100px; } #canvas { width: 200px; height: 400px; } } /* 20px chunks */
@media screen and (min-width: 600px) and (min-height: 600px) { #tetris { font-size: 1.50em; width: 550px; } #menu { width: 250px; height: 500px; } #upcoming { width: 125px; height: 125px; } #canvas { width: 250px; height: 500px; } } /* 25px chunks */
@media screen and (min-width: 700px) and (min-height: 700px) { #tetris { font-size: 1.75em; width: 650px; } #menu { width: 300px; height: 600px; } #upcoming { width: 150px; height: 150px; } #canvas { width: 300px; height: 600px; } } /* 30px chunks */
@media screen and (min-width: 800px) and (min-height: 800px) { #tetris { font-size: 2.00em; width: 750px; } #menu { width: 350px; height: 700px; } #upcoming { width: 175px; height: 175px; } #canvas { width: 350px; height: 700px; } } /* 35px chunks */
@media screen and (min-width: 900px) and (min-height: 900px) { #tetris { font-size: 2.25em; width: 850px; } #menu { width: 400px; height: 800px; } #upcoming { width: 200px; height: 200px; } #canvas { width: 400px; height: 800px; } } /* 40px chunks */
tetris.js
//-------------------------------------------------------------------------
// base helper methods
//-------------------------------------------------------------------------
function get(id) {
return document.getElementById(id);
}
function hide(id) {
get(id).style.visibility = 'hidden';
}
function show(id) {
get(id).style.visibility = null;
}
function html(id, html) {
get(id).innerHTML = html;
}
function timestamp() {
return new Date().getTime();
}
function random(min, max) {
return (min + (Math.random() * (max - min)));
}
function randomChoice(choices) {
return choices[Math.round(random(0, choices.length - 1))];
}
if (!window.requestAnimationFrame) { // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
window.requestAnimationFrame = window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback, element) {
window.setTimeout(callback, 1000 / 60);
}
}
//-------------------------------------------------------------------------
// game constants
//-------------------------------------------------------------------------
var KEY = {ESC: 27, SPACE: 32, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40},
DIR = {UP: 0, RIGHT: 1, DOWN: 2, LEFT: 3, MIN: 0, MAX: 3},
canvas = get('canvas'),
ctx = canvas.getContext('2d'),
ucanvas = get('upcoming'),
uctx = ucanvas.getContext('2d'),
speed = {start: 0.6, decrement: 0.005, min: 0.1}, // how long before piece drops by 1 row (seconds)
nx = 10, // width of tetris court (in blocks)
ny = 20, // height of tetris court (in blocks)
nu = 5; // width/height of upcoming preview (in blocks)
//-------------------------------------------------------------------------
// game variables (initialized during reset)
//-------------------------------------------------------------------------
var dx, dy, // pixel size of a single tetris block
blocks, // 2 dimensional array (nx*ny) representing tetris court - either empty block or occupied by a 'piece'
actions, // queue of user actions (inputs)
playing, // true|false - game is in progress
dt, // time since starting this game
current, // the current piece
next, // the next piece
score, // the current score
vscore, // the currently displayed score (it catches up to score in small chunks - like a spinning slot machine)
rows, // number of completed rows in the current game
step; // how long before current piece drops by 1 row
//-------------------------------------------------------------------------
// tetris pieces
//
// blocks: each element represents a rotation of the piece (0, 90, 180, 270)
// each element is a 16 bit integer where the 16 bits represent
// a 4x4 set of blocks, e.g. j.blocks[0] = 0x44C0
//
// 0100 = 0x4 << 3 = 0x4000
// 0100 = 0x4 << 2 = 0x0400
// 1100 = 0xC << 1 = 0x00C0
// 0000 = 0x0 << 0 = 0x0000
// ------
// 0x44C0
//
//-------------------------------------------------------------------------
var i = {size: 4, blocks: [0x0F00, 0x2222, 0x00F0, 0x4444], color: 'cyan'};
var j = {size: 3, blocks: [0x44C0, 0x8E00, 0x6440, 0x0E20], color: 'blue'};
var l = {size: 3, blocks: [0x4460, 0x0E80, 0xC440, 0x2E00], color: 'orange'};
var o = {size: 2, blocks: [0xCC00, 0xCC00, 0xCC00, 0xCC00], color: 'yellow'};
var s = {size: 3, blocks: [0x06C0, 0x8C40, 0x6C00, 0x4620], color: 'green'};
var t = {size: 3, blocks: [0x0E40, 0x4C40, 0x4E00, 0x4640], color: 'purple'};
var z = {size: 3, blocks: [0x0C60, 0x4C80, 0xC600, 0x2640], color: 'red'};
//------------------------------------------------
// do the bit manipulation and iterate through each
// occupied block (x,y) for a given piece
//------------------------------------------------
function eachblock(type, x, y, dir, fn) {
var bit, result, row = 0, col = 0, blocks = type.blocks[dir];
for (bit = 0x8000; bit > 0; bit = bit >> 1) {
if (blocks & bit) {
fn(x + col, y + row);
}
if (++col === 4) {
col = 0;
++row;
}
}
}
//-----------------------------------------------------
// check if a piece can fit into a position in the grid
//-----------------------------------------------------
function occupied(type, x, y, dir) {
var result = false
eachblock(type, x, y, dir, function (x, y) {
if ((x < 0) || (x >= nx) || (y < 0) || (y >= ny) || getBlock(x, y))
result = true;
});
return result;
}
function unoccupied(type, x, y, dir) {
return !occupied(type, x, y, dir);
}
//-----------------------------------------
// start with 4 instances of each piece and
// pick randomly until the 'bag is empty'
//-----------------------------------------
var pieces = [];
function randomPiece() {
if (pieces.length == 0)
pieces = [i, i, i, i, j, j, j, j, l, l, l, l, o, o, o, o, s, s, s, s, t, t, t, t, z, z, z, z];
var type = pieces.splice(random(0, pieces.length - 1), 1)[0];
return {type: type, dir: DIR.UP, x: Math.round(random(0, nx - type.size)), y: 0};
}
//-------------------------------------------------------------------------
// GAME LOOP
//-------------------------------------------------------------------------
function run() {
addEvents(); // attach keydown and resize events
var last = now = timestamp();
function frame() {
now = timestamp();
update(Math.min(1, (now - last) / 1000.0)); // using requestAnimationFrame have to be able to handle large delta's caused when it 'hibernates' in a background or non-visible tab
draw();
last = now;
requestAnimationFrame(frame, canvas);
}
resize(); // setup all our sizing information
reset(); // reset the per-game variables
frame(); // start the first frame
}
function addEvents() {
document.addEventListener('keydown', keydown, false);
window.addEventListener('resize', resize, false);
}
function resize(event) {
canvas.width = canvas.clientWidth; // set canvas logical size equal to its physical size
canvas.height = canvas.clientHeight; // (ditto)
ucanvas.width = ucanvas.clientWidth;
ucanvas.height = ucanvas.clientHeight;
dx = canvas.width / nx; // pixel size of a single tetris block
dy = canvas.height / ny; // (ditto)
invalidate();
invalidateNext();
}
function keydown(ev) {
var handled = false;
if (playing) {
switch (ev.keyCode) {
case KEY.LEFT:
actions.push(DIR.LEFT);
handled = true;
break;
case KEY.RIGHT:
actions.push(DIR.RIGHT);
handled = true;
break;
case KEY.UP:
actions.push(DIR.UP);
handled = true;
break;
case KEY.DOWN:
actions.push(DIR.DOWN);
handled = true;
break;
case KEY.ESC:
lose();
handled = true;
break;
}
} else if (ev.keyCode == KEY.SPACE) {
play();
handled = true;
}
if (handled)
ev.preventDefault(); // prevent arrow keys from scrolling the page (supported in IE9+ and all other browsers)
}
//-------------------------------------------------------------------------
// GAME LOGIC
//-------------------------------------------------------------------------
function play() {
hide('start');
reset();
playing = true;
}
function lose() {
show('start');
setVisualScore();
playing = false;
}
function setVisualScore(n) {
vscore = n || score;
invalidateScore();
}
function setScore(n) {
score = n;
setVisualScore(n);
}
function addScore(n) {
score = score + n;
}
function clearScore() {
setScore(0);
}
function clearRows() {
setRows(0);
}
function setRows(n) {
rows = n;
step = Math.max(speed.min, speed.start - (speed.decrement * rows));
invalidateRows();
}
function addRows(n) {
setRows(rows + n);
}
function getBlock(x, y) {
return (blocks && blocks[x] ? blocks[x][y] : null);
}
function setBlock(x, y, type) {
blocks[x] = blocks[x] || [];
blocks[x][y] = type;
invalidate();
}
function clearBlocks() {
blocks = [];
invalidate();
}
function clearActions() {
actions = [];
}
function setCurrentPiece(piece) {
current = piece || randomPiece();
invalidate();
}
function setNextPiece(piece) {
next = piece || randomPiece();
invalidateNext();
}
function reset() {
dt = 0;
clearActions();
clearBlocks();
clearRows();
clearScore();
setCurrentPiece(next);
setNextPiece();
}
function update(idt) {
if (playing) {
if (vscore < score)
setVisualScore(vscore + 1);
handle(actions.shift());
dt = dt + idt;
if (dt > step) {
dt = dt - step;
drop();
}
}
}
function handle(action) {
switch (action) {
case DIR.LEFT:
move(DIR.LEFT);
break;
case DIR.RIGHT:
move(DIR.RIGHT);
break;
case DIR.UP:
rotate();
break;
case DIR.DOWN:
drop();
break;
}
}
function move(dir) {
var x = current.x, y = current.y;
switch (dir) {
case DIR.RIGHT:
x = x + 1;
break;
case DIR.LEFT:
x = x - 1;
break;
case DIR.DOWN:
y = y + 1;
break;
}
if (unoccupied(current.type, x, y, current.dir)) {
current.x = x;
current.y = y;
invalidate();
return true;
} else {
return false;
}
}
function rotate() {
var newdir = (current.dir == DIR.MAX ? DIR.MIN : current.dir + 1);
if (unoccupied(current.type, current.x, current.y, newdir)) {
current.dir = newdir;
invalidate();
}
}
function drop() {
if (!move(DIR.DOWN)) {
addScore(10);
dropPiece();
removeLines();
setCurrentPiece(next);
setNextPiece(randomPiece());
clearActions();
if (occupied(current.type, current.x, current.y, current.dir)) {
lose();
}
}
}
function dropPiece() {
eachblock(current.type, current.x, current.y, current.dir, function (x, y) {
setBlock(x, y, current.type);
});
}
function removeLines() {
var x, y, complete, n = 0;
for (y = ny; y > 0; --y) {
complete = true;
for (x = 0; x < nx; ++x) {
if (!getBlock(x, y))
complete = false;
}
if (complete) {
removeLine(y);
y = y + 1; // recheck same line
n++;
}
}
if (n > 0) {
addRows(n);
addScore(100 * Math.pow(2, n - 1)); // 1: 100, 2: 200, 3: 400, 4: 800
}
}
function removeLine(n) {
var x, y;
for (y = n; y >= 0; --y) {
for (x = 0; x < nx; ++x)
setBlock(x, y, (y == 0) ? null : getBlock(x, y - 1));
}
}
//-------------------------------------------------------------------------
// RENDERING
//-------------------------------------------------------------------------
var invalid = {};
function invalidate() {
invalid.court = true;
}
function invalidateNext() {
invalid.next = true;
}
function invalidateScore() {
invalid.score = true;
}
function invalidateRows() {
invalid.rows = true;
}
function draw() {
ctx.save();
ctx.lineWidth = 1;
ctx.translate(0.5, 0.5); // for crisp 1px black lines
drawCourt();
drawNext();
drawScore();
drawRows();
ctx.restore();
}
function drawCourt() {
if (invalid.court) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (playing)
drawPiece(ctx, current.type, current.x, current.y, current.dir);
var x, y, block;
for (y = 0; y < ny; y++) {
for (x = 0; x < nx; x++) {
if (block = getBlock(x, y))
drawBlock(ctx, x, y, block.color);
}
}
ctx.strokeRect(0, 0, nx * dx - 1, ny * dy - 1); // court boundary
invalid.court = false;
}
}
function drawNext() {
if (invalid.next) {
var padding = (nu - next.type.size) / 2; // half-arsed attempt at centering next piece display
uctx.save();
uctx.translate(0.5, 0.5);
uctx.clearRect(0, 0, nu * dx, nu * dy);
drawPiece(uctx, next.type, padding, padding, next.dir);
uctx.strokeStyle = 'black';
uctx.strokeRect(0, 0, nu * dx - 1, nu * dy - 1);
uctx.restore();
invalid.next = false;
}
}
function drawScore() {
if (invalid.score) {
html('score', ("00000" + Math.floor(vscore)).slice(-5));
invalid.score = false;
}
}
function drawRows() {
if (invalid.rows) {
html('rows', rows);
invalid.rows = false;
}
}
function drawPiece(ctx, type, x, y, dir) {
eachblock(type, x, y, dir, function (x, y) {
drawBlock(ctx, x, y, type.color);
});
}
function drawBlock(ctx, x, y, color) {
ctx.fillStyle = color;
ctx.fillRect(x * dx, y * dy, dx, dy);
ctx.strokeRect(x * dx, y * dy, dx, dy)
}
//-------------------------------------------------------------------------
// FINALLY, lets run the game
//-------------------------------------------------------------------------
run();