diff --git a/tetris/Background.js b/tetris/Background.js
new file mode 100644
index 00000000..fd85432b
--- /dev/null
+++ b/tetris/Background.js
@@ -0,0 +1,59 @@
+
+var Background = function (config) {
+ var x, y,
+ curTile;
+
+ config = config || {};
+
+ this.originX = (config.x || 0) + FIELD_OFFSET_X;
+ this.originY = (config.y || 0) + FIELD_OFFSET_Y;
+
+ this.width = 10;
+ this.height = 20;
+
+ this.tiles = [];
+ for (x = 0; x < this.width; x += 1) {
+ for (y = 0; y < this.height; y += 1) {
+ curTile = new Block({ empty: true, blockX: x, blockY: y });
+ this.tiles.push(curTile);
+ }
+ }
+
+ this.backdrop = new jaws.Sprite({image: 'media/background/backdrop.png'});
+ this.backdrop.x = 0;
+ this.backdrop.y = 0;
+
+ this.topBar = new jaws.Sprite({image: 'media/background/topbar.png'});
+ this.topBar.x = 181;
+ this.topBar.y = 0;
+
+ this.fullRedrawNeeded = true;
+};
+
+
+Background.prototype.draw = function (lastPaused) {
+ var i;
+
+ if (this.fullRedrawNeeded || lastPaused) {
+ this.backdrop.draw();
+
+ for (i = 0; i < this.tiles.length; i += 1) {
+ this.tiles[i].draw();
+ }
+
+ this.fullRedrawNeeded = false;
+
+ } else {
+
+ this.topBar.draw();
+
+ // clear the swap group / previews
+ jaws.context.fillstyle = "#000D00";
+ jaws.context.fillRect(24, 42, 118, 60);
+ jaws.context.fillRect(457, 18, 107, 341);
+
+ for (i = 0; i < this.tiles.length; i += 1) {
+ this.tiles[i].drawIfInvalid();
+ }
+ }
+};
\ No newline at end of file
diff --git a/tetris/Block.js b/tetris/Block.js
new file mode 100644
index 00000000..86ef1e08
--- /dev/null
+++ b/tetris/Block.js
@@ -0,0 +1,111 @@
+
+// TODO: constants file???
+var BLOCK_WIDTH = 24;
+
+function Block(config) {
+ var parent, key;
+
+ config = config || {};
+
+ this.boX = (config.boardOriginX || 0) + FIELD_OFFSET_X;
+ this.boY = (config.boardOriginY || 0) + FIELD_OFFSET_Y;
+ this.blockX = config.blockX;
+ this.blockY = config.blockY;
+
+ this.occupiedPositions = config.occupiedPositions;
+ this.addOccupied(this.blockX, this.blockY);
+
+ Block.invalidSpaces[this.blockX + "," + this.blockY] = true;
+
+ config.x = this.boX + BLOCK_WIDTH * this.blockX;
+ config.y = this.boY + BLOCK_WIDTH * this.blockY;
+
+ if (config.preview) {
+ config.image = 'media/greyblock.png';
+ } else if (config.empty) {
+ config.image = 'media/emptyblock.png';
+ }else {
+ config.image = SHAPES[config.shape].image;
+ }
+
+ parent = new jaws.Sprite(config);
+ for (key in parent) {
+ this[key] = parent[key];
+ }
+}
+
+Block.invalidSpaces = {};
+Block.allInvalidated = false;
+Block.invalidFlushed = function() {
+ Block.invalidSpaces = {};
+ Block.allInvalidated = false;
+};
+Block.invalidateAll = function() {
+ Block.allInvalidated = true;
+};
+
+Block.prototype.setColor = function(shape, preview) {
+ if (preview) {
+ this.setImage('media/greyblock.png');
+ } else {
+ this.setImage(SHAPES[shape].image);
+ }
+ Block.invalidSpaces[this.blockX + "," + this.blockY] = true;
+};
+
+Block.prototype.moveBlock = function(dx, dy) {
+ Block.invalidSpaces[this.blockX + "," + this.blockY] = true;
+ this.removeOccupied(this.blockX, this.blockY);
+ this.blockX += dx;
+ this.blockY += dy;
+ Block.invalidSpaces[this.blockX + "," + this.blockY] = true;
+ this.addOccupied(this.blockX, this.blockY);
+ this.x += dx * BLOCK_WIDTH;
+ this.y += dy * BLOCK_WIDTH;
+};
+
+Block.prototype.setPosition = function(blockX, blockY) {
+ Block.invalidSpaces[this.blockX + "," + this.blockY] = true;
+ this.removeOccupied(this.blockX, this.blockY);
+ this.blockX = blockX;
+ this.blockY = blockY;
+ Block.invalidSpaces[this.blockX + "," + this.blockY] = true;
+ this.addOccupied(this.blockX, this.blockY);
+ this.x = this.boX + blockX * BLOCK_WIDTH;
+ this.y = this.boY + blockY * BLOCK_WIDTH;
+};
+
+Block.prototype.getX = function() { return this.blockX; };
+Block.prototype.getY = function() { return this.blockY; };
+
+Block.prototype.isPosition = function(x, y) {
+ return this.blockX === x && this.blockY === y;
+};
+
+Block.prototype.drawIfInvalid = function() {
+ if (Block.invalidSpaces[this.blockX + "," + this.blockY] || Block.allInvalidated || this.blockY < 0) {
+ this.draw();
+ }
+};
+
+Block.prototype.kill = function() {
+ Block.invalidSpaces[this.blockX + "," + this.blockY] = true;
+ this.removeOccupied(this.blockX, this.blockY);
+};
+
+Block.prototype.removeOccupied = function(x, y) {
+ var posString = x + ',' + y;
+ if (this.occupiedPositions && this.occupiedPositions[posString]) {
+ this.occupiedPositions[posString] -= 1;
+ }
+};
+
+Block.prototype.addOccupied = function(x, y) {
+ var posString = x + ',' + y;
+ if (this.occupiedPositions) {
+ if (this.occupiedPositions[posString] === undefined) {
+ this.occupiedPositions[posString] = 0;
+ }
+ this.occupiedPositions[posString] += 1;
+ }
+};
diff --git a/tetris/Button.js b/tetris/Button.js
new file mode 100644
index 00000000..cacb3ca3
--- /dev/null
+++ b/tetris/Button.js
@@ -0,0 +1,13 @@
+function Button(config) {
+
+ var parent = new jaws.Sprite(config),
+ key;
+
+ for (key in parent) {
+ this[key] = parent[key];
+ }
+}
+
+Button.prototype.isClicked = function(x, y) {
+ return this.rect().collidePoint(x, y);
+};
diff --git a/tetris/ControlGroup.js b/tetris/ControlGroup.js
new file mode 100644
index 00000000..ea4468e5
--- /dev/null
+++ b/tetris/ControlGroup.js
@@ -0,0 +1,372 @@
+
+/**
+* The blocks that can be moved nby the user
+* @param {Array} blocks - an array of [Block] of size 4 that can be operated on
+* @param {Char} shape - the block type: i, o, j, l, s, z, t
+* @param {function({Number}x, {Number}y)} isLegalCallback - a function that retursn true if a block can be moved
+* to the new position
+*/
+function ControlGroup(blocks, shape, isLegalCallback) {
+ var i,
+ newX, newY,
+ shapeConf;
+
+ // place the blocks according to the shape
+ shapeConf = SHAPES[shape];
+ this.pos = shapeConf.pos;
+ this.spin = shapeConf.spin;
+ this.bottomed = false;
+
+ this.blocks = blocks;
+ this.baseX = shapeConf.startX;
+ this.baseY = shapeConf.startY;
+
+ this.shape = shape;
+ this.kickOffsets = WALL_KICK_OFFSETS[shapeConf.kickType];
+ this.dir = 0;
+
+ this.isIllegalStart = false;
+
+ this.isLegalCallback = isLegalCallback || function() {return true;};
+
+ this.lastWasSpin = false;
+
+ for (i = 0; i < blocks.length; i += 1) {
+ newX = this.baseX + this.pos[i].x;
+ newY = this.baseY + this.pos[i].y;
+ // see if the block placement is illegal before placing
+ if (!this.isLegalCallback(newX, newY)) {
+ this.isIllegalStart = true;
+ }
+ this.blocks[i].setPosition(newX, newY);
+ }
+
+ this.updateBottomedState();
+}
+
+/**
+* if the position is legal
+* @param {Number} x
+* @param {Number} y
+* @returns {Boolean} true iff the position is legal to move to
+*/
+ControlGroup.prototype.isLegalPosition = function (x, y) {
+ var i,
+ blocks = this.blocks;
+
+ // if it's a currently occupied, it must be legal
+ for (i = 0; i < 4; i += 1) {
+ if (blocks[i].isPosition(x, y)) {
+ return true;
+ }
+ }
+
+ // if it's still not proven legal, then defer to the game to decide
+ return this.isLegalCallback(x, y);
+};
+
+/**
+* Shift the block left or right
+* @param {Boolean} left - true to shift left false to shift right
+* @returns {Boolean} true iff the shift was successful
+*/
+ControlGroup.prototype.shift = function(left) {
+ var dx = (left ? -1 : 1),
+ i;
+
+ for (i = 0; i < 4; i += 1) {
+ if (!this.isLegalPosition(this.blocks[i].getX()+dx, this.blocks[i].getY())) {
+ return false;
+ }
+ }
+
+ this.lastWasSpin = false;
+ this.baseX += dx;
+
+ for (i = 0; i < this.blocks.length; i += 1) {
+ this.blocks[i].moveBlock(dx, 0);
+ }
+ this.updateBottomedState();
+
+ return true;
+};
+
+ControlGroup.prototype.updateBottomedState = function() {
+ var i;
+
+ for (i = 0; i < this.blocks.length; i += 1) {
+ if (!this.isLegalPosition(this.blocks[i].getX(), this.blocks[i].getY() + 1)) {
+ this.bottomed = true;
+ return;
+ }
+ }
+
+ this.bottomed = false;
+};
+
+/**
+* Drop the block by one
+*/
+ControlGroup.prototype.drop = function() {
+ var i;
+
+ // don't drop if bottomed
+ if (this.bottomed) {
+ return;
+ }
+
+ this.lastWasSpin = false;
+ this.baseY += 1;
+
+ for (i = 0; i < this.blocks.length; i += 1) {
+ this.blocks[i].moveBlock(0, 1);
+ }
+ this.updateBottomedState();
+};
+
+/**
+* @returns {Boolean} true if the block is bottomed and another shoudl spawn
+*/
+ControlGroup.prototype.isBottomed = function() {
+ return this.bottomed;
+};
+
+/**
+* Turns the block
+* @param {Boolean} cw - true for clockwise, false for counter-clockwise
+* @returns {Boolean} true iff the block was successfully turned
+*/
+ControlGroup.prototype.turn = function(cw) {
+ var kick,
+ newPos = null,
+ direction = cw ? 'cw' : 'ccw',
+ availableKicks = this.kickOffsets[this.dir][direction],
+ i;
+
+ // for possible each kick offset
+ for (i = 0; i < availableKicks.length; i += 1) {
+ kick = availableKicks[i];
+ newPos = this.tryTurn(cw, kick);
+ if (newPos) {
+ break;
+ }
+ }
+
+ // if there s still no valid rotation, fail
+ if (!newPos) {
+ return false;
+ }
+
+ this.lastWasSpin = true;
+
+ // must be legal at this point move the bocks
+ for (i = 0; i < 4; i += 1) {
+ this.blocks[i].setPosition(newPos[i].x, newPos[i].y);
+ }
+ this.baseX += kick.x;
+ this.baseY += kick.y;
+
+ // keep track of the direction
+ if (cw) {
+ this.dir += 1;
+ if (this.dir === 4) {
+ this.dir = 0;
+ }
+ } else {
+ this.dir -= 1;
+ if (this.dir === -1) {
+ this.dir = 3;
+ }
+ }
+
+ this.updateBottomedState();
+
+ return true;
+};
+
+/**
+* Checks if the given rotation and kick is valid.
+* @param {Boolean} cw - true if cw, false if ccw
+* @param {Object} kick - the kick offset x/y object to try
+* @returns {Array} and array of x/y objects if valid, null if not valid
+*/
+ControlGroup.prototype.tryTurn = function (cw, kick) {
+ var newX, newY,
+ oldX, oldY,
+ i,
+ newPos = [],
+ curPos;
+
+ if (this.spin === 'block') {
+ for (i = 0; i < this.blocks.length; i += 1) {
+ newX = (cw ? -1 : 1) * (this.blocks[i].blockY - this.baseY) + this.baseX + kick.x;
+ newY = (cw ? 1 : -1) * (this.blocks[i].blockX - this.baseX) + this.baseY + kick.y;
+
+ newPos[i] = {x: newX, y: newY};
+ }
+ } else {
+ // point turning
+ for (i = 0; i < this.blocks.length; i += 1) {
+ oldX = this.blocks[i].blockX - this.baseX;
+ oldY = this.blocks[i].blockY - this.baseY;
+
+ if (oldX >= 0) { oldX += 1; }
+ if (oldY >= 0) { oldY += 1; }
+
+ newX = (cw ? -1 : 1) * oldY;
+ newY = (cw ? 1 : -1) * oldX;
+
+ if (newX > 0) { newX -= 1; }
+ if (newY > 0) { newY -= 1; }
+
+ newPos[i] = {x: newX + this.baseX + kick.x, y: newY + this.baseY + kick.y};
+ }
+ }
+
+
+ // for each block
+ for (i = 0; i < 4; i += 1) {
+ curPos = newPos[i];
+ if (!this.isLegalPosition(curPos.x, curPos.y)) {
+ return null;
+ }
+ }
+
+ return newPos;
+
+};
+
+/**
+* Gets the positions that the block will use when it falls
+* @returns {Object} {dist:{Number}, positions: {[Object]} array of hashs of {x: Number, y: Number}}
+*/
+ControlGroup.prototype.getFallPositions = function () {
+ var res = [],
+ dist = 0,
+ i,
+ curBlock,
+ notDone = true;
+
+ while (notDone) {
+ dist += 1;
+
+ // for each block
+ for (i = 0; i < 4 && notDone; i += 1) {
+ curBlock = this.blocks[i];
+ // if it's not a legal position
+ if (!this.isLegalPosition(curBlock.getX(), curBlock.getY() + dist)) {
+ // back up one and stop dropping
+ dist -= 1;
+ notDone = false;
+ }
+ }
+ }
+
+ // for each block
+ for (i = 0; i < 4; i += 1) {
+ curBlock = this.blocks[i];
+ res.push({x: curBlock.getX(), y: curBlock.getY() + dist});
+ }
+
+ return {dist: dist, positions: res};
+};
+
+/**
+* makes the block fall all the way to the bottom
+* forces the next cycle to be recognized as bottomed
+* @returns {Number} the distance fallen
+*/
+ControlGroup.prototype.fall = function() {
+ var fall = this.getFallPositions(),
+ positions = fall.positions,
+ dist = fall.dist,
+ i, curPos;
+
+ if (dist !== 0) {
+ this.lastWasSpin = false;
+ }
+
+ // for each block
+ for (i = 0; i < 4; i += 1) {
+ curPos = positions[i];
+ this.blocks[i].setPosition(curPos.x, curPos.y);
+ }
+
+ this.bottomed = true;
+ return dist;
+};
+
+/**
+* Sets the preview blocks to the approproriate positions
+* @param {[Block]} previews - the 4 blocks to be modified to be put into position as preview blocks
+*/
+ControlGroup.prototype.configurePreviewBlocks = function(previews) {
+ var positions = this.getFallPositions().positions,
+ i;
+
+ for (i = 0; i < 4; i += 1) {
+ previews[i].setPosition(positions[i].x, positions[i].y);
+ }
+};
+
+ControlGroup.prototype.getShape = function () {
+ return this.shape;
+};
+
+ControlGroup.prototype.getBlocks = function () {
+ return this.blocks;
+};
+
+/*
+* Gets the type of T spin that the group is in
+* @returns {String} 'mini' for a mini-t, 'normal' for a normal t, null for not a t spin
+*/
+ControlGroup.prototype.getTSpin = function() {
+ var i,
+ testPoints = [{x:-1,y:-1},{x:1,y:-1},{x:1,y:1},{x:-1,y:1}],
+ count = 0,
+ mini = false,
+ curPoint;
+
+ if (!this.lastWasSpin) {
+ return null;
+ }
+
+ // make sure it's actually a t
+ if (this.shape !== 't') {
+ return null;
+ }
+
+ // t-spin mini tests
+ if (this.dir === 0) {
+ testPoints[0].miniCheck = true;
+ testPoints[1].miniCheck = true;
+ } else if (this.dir === 1) {
+ testPoints[1].miniCheck = true;
+ testPoints[2].miniCheck = true;
+ } else if (this.dir === 2) {
+ testPoints[2].miniCheck = true;
+ testPoints[3].miniCheck = true;
+ } else if (this.dir === 3) {
+ testPoints[3].miniCheck = true;
+ testPoints[0].miniCheck = true;
+ }
+
+ // 3 point t test
+ for (i = 0; i < 4; i += 1) {
+ curPoint = testPoints[i]
+ if (!this.isLegalPosition(this.baseX + curPoint.x, this.baseY + curPoint.y)) {
+ count += 1;
+ } else if (curPoint.miniCheck) {
+ mini = true;
+ }
+ }
+
+ if (count >= 3) {
+ if (mini) {
+ return 'mini';
+ }
+ return 'normal';
+ }
+ return null;
+};
diff --git a/tetris/Game.js b/tetris/Game.js
new file mode 100644
index 00000000..b2b3e1c8
--- /dev/null
+++ b/tetris/Game.js
@@ -0,0 +1,301 @@
+function Game(inputMapping, autoRepeat, threshold) {
+ var thisObject = this,
+ i;
+
+ this.firstLoop = true;
+
+ this.blocks = [];
+ this.controlGroup = null;
+
+ // make the preview blocks
+ this.previewBlocks = [];
+ for (i = 0; i < 4; i += 1) {
+ this.previewBlocks.push(new Block({blockX: -10, blockY: -10, preview: true}));
+ }
+
+ this.scoreOutput = new TtyBlock("scoreDiv", 3);
+ this.linesOutput = new TtyBlock("linesDiv", 3);
+ this.levelOutput = new TtyBlock("levelDiv", 3);
+ this.tickerOutput = new TtyBlock("tickerDiv", 5);
+ this.scoreTracker = new ScoreTracker(this.scoreOutput, this.linesOutput, this.levelOutput, this.tickerOutput);
+
+ this.dropPeriod = this.scoreTracker.getLevelPeriod();
+ this.timeToNextDrop = this.dropPeriod;
+
+ // TODO: find the official values for these constants
+ this.keyChargeTime = threshold;
+ this.keyRepeatTime = autoRepeat;
+
+ this.bottomTimer = null;
+ this.bottomLockTime = 500;
+ this.lastBottomedState = false;
+
+ this.lastTime = null;
+
+ this.gameLost = false;
+
+ // evenly distributed random piece generator
+ this.previewLength = 5;
+ this.randBag = new RandomBag(this.previewLength);
+ // make the preview blocks
+ this.previewGroups = [];
+ for (i = 0; i < this.previewLength; i += 1) {
+ this.previewGroups.push(new PreviewGroup(330, 70 * i + 35));
+ }
+
+ this.swapGroup = null;
+ this.swapAllowed = true;
+
+ // the currently occupied positions, number of blocks at a position
+ // indexed by the position as a string
+ this.occupiedPositions = {};
+
+ this.input = {
+ shiftLeft: {
+ autoRepeat: true,
+ handler: function () {
+ if (thisObject.controlGroup.shift(true)) {
+ thisObject.resetLockCounter(true);
+ }
+ }
+ },
+ shiftRight: {
+ autoRepeat: true,
+ handler: function() {
+ if (thisObject.controlGroup.shift(false)) {
+ thisObject.resetLockCounter(true);
+ }
+ }
+ },
+ softDrop: {
+ autoRepeat: true,
+ preCharged: true,
+ handler: function() {
+ thisObject.dropBlock();
+ thisObject.scoreTracker.softDrop();
+ }
+ },
+ hardDrop: { handler: function() {
+ var dist = thisObject.controlGroup.fall();
+ thisObject.scoreTracker.hardDrop(dist);
+ thisObject.lockBlocks();
+ }},
+ rotateLeft: { handler: function() {
+ if (thisObject.controlGroup.turn(false)) {
+ thisObject.resetLockCounter(true);
+ }
+ }},
+ rotateRight: { handler: function() {
+ if (thisObject.controlGroup.turn(true)) {
+ thisObject.resetLockCounter(true);
+ }
+ }},
+ swap: { handler: function() {
+ thisObject.swap();
+ }}
+ };
+
+ this.inputMapping = inputMapping;
+}
+
+/**
+* drops a new block into the game
+*/
+Game.prototype.newBlock = function (calledBySwap) {
+ var thisObject = this,
+ shape = this.randBag.popQueue(),
+ newBlocks = [],
+ curBlock,
+ i;
+
+ this.dropPeriod = this.scoreTracker.getLevelPeriod();
+
+ // create some new blocks
+ for (i = 0; i < 4; i += 1) {
+ curBlock = new Block({blockX: -10, blockY: -10, shape: shape, occupiedPositions: this.occupiedPositions});
+ newBlocks.push(curBlock);
+ this.blocks.push(curBlock);
+ }
+
+ this.controlGroup = new ControlGroup(newBlocks, shape, function(x, y){
+ return thisObject.isLegalPosition(x, y);
+ });
+
+ if (this.controlGroup.isIllegalStart) {
+ this.gameLost = true;
+ }
+
+ if (!calledBySwap) {
+ // the user is allowed to swap blocks again
+ this.swapAllowed = true;
+ }
+
+ this.updatePreviews(this.randBag.getQueue());
+};
+
+/**
+* processes the input keys
+* @param {Number} dTime - the time in milliseconds since the last frame
+*/
+Game.prototype.processInput = function(dTime) {
+ var curInput,
+ keyName,
+ curKeys,
+ pressed,
+ curInput,
+ i;
+
+ for (actionType in this.inputMapping) {
+ curKeys = this.inputMapping[actionType];
+ curInput = this.input[actionType];
+ pressed = false;
+ for (i = 0; i < curKeys.length; i += 1) {
+ if (jaws.pressed(curKeys[i])) {
+ pressed = true;
+ }
+ }
+
+ // if the key is down
+ if (pressed) {
+ // if it is a 'press' frame
+ if (!curInput.lastState) {
+ curInput.handler();
+ curInput.lastState = true;
+ curInput.charged = (curInput.preCharged ? true : false);
+ curInput.holdTime = 0;
+ }
+ // if it supports auto-repeat
+ if (curInput.autoRepeat) {
+ curInput.holdTime += dTime;
+
+ // if not charged and past the charge time
+ if ((!curInput.charged) && (curInput.holdTime > this.keyChargeTime)) {
+ // call the handler, and reset the hold time
+ curInput.holdTime -= this.keyChargeTime;
+ curInput.handler();
+ curInput.charged = true;
+ }
+ // if charged and past the repeat time
+ if (curInput.charged && (curInput.holdTime > this.keyRepeatTime)) {
+ curInput.holdTime -= this.keyRepeatTime;
+ curInput.handler();
+ }
+ }
+ } else {
+ // it was released
+ curInput.lastState = false;
+ }
+ }
+};
+
+Game.prototype.update = function(time) {
+ var curTime,
+ dTime,
+ i;
+
+ // if the first block needs to be made
+ if (this.firstLoop) {
+ this.firstLoop = false;
+
+ this.newBlock();
+
+ this.lastTime = time;
+ }
+
+ curTime = time;
+ dTime = curTime - this.lastTime;
+ this.lastTime = curTime;
+
+ this.processInput(dTime);
+
+ if (!this.controlGroup.isBottomed()) {
+ this.lastBottomedState = false;
+ this.applyGravity(dTime);
+
+ } else {
+ // if it has just touched hte bottom
+ if (!this.lastBottomedState) {
+ this.resetLockCounter(false);
+ } else {
+ this.bottomTimer -= dTime;
+
+ if (this.bottomTimer <= 0 || this.slideCount >= 15) {
+ this.lockBlocks();
+ }
+ }
+ this.lastBottomedState = true;
+ }
+
+ // update the position of the preview blocks
+ if (this.controlGroup) {
+ // ask the control group to move the preview blocks
+ this.controlGroup.configurePreviewBlocks(this.previewBlocks);
+ } else {
+ // if there is no contorl group, just move them off the screen
+ for (i = 0; i < 4; i += 1) {
+ this.previewBlocks[i].setPosition(-10, -10);
+ }
+ }
+};
+
+/**
+* Renders the entire game scene
+*/
+Game.prototype.draw = function(dTime) {
+ var i;
+
+ this.scoreOutput.draw(dTime);
+ this.linesOutput.draw(dTime);
+ this.levelOutput.draw(dTime);
+ this.tickerOutput.draw(dTime);
+
+ // draw the preview blocks
+ for (i = 0; i < 4; i += 1) {
+ this.previewBlocks[i].drawIfInvalid();
+ }
+
+ // draw the swap block
+ if (this.swapGroup) {
+ this.swapGroup.draw();
+ }
+
+ // draw the queue
+ for (i = 0; i < this.previewGroups.length; i += 1) {
+ this.previewGroups[i].draw();
+ }
+
+ for (i = 0; i < this.blocks.length; i += 1) {
+ this.blocks[i].drawIfInvalid();
+ }
+
+};
+
+/**
+* Returns true iff the given position can be moved into
+* @param {Number} x - the x position
+* @param {Number} y - the y position
+* @returns {Boolean} true iff the new position is legal
+*/
+Game.prototype.isLegalPosition = function (x, y) {
+ // if there is a block in the way
+ if (this.occupiedPositions[x+','+y]) {
+ return false;
+ }
+
+ // if it's on the field
+ if (x >= 10 || x < 0 || y >= 20) {
+ return false;
+ }
+ return true;
+};
+
+/**
+* drops the controlled blocks by one
+*/
+Game.prototype.dropBlock = function (causedByGravity) {
+ if (!causedByGravity) {
+ this.timeToNextDrop = this.dropPeriod;
+ }
+
+ this.controlGroup.drop();
+};
diff --git a/tetris/Game_Logic.js b/tetris/Game_Logic.js
new file mode 100644
index 00000000..2d2f4cae
--- /dev/null
+++ b/tetris/Game_Logic.js
@@ -0,0 +1,209 @@
+
+/**
+* @returns {[Number]} the line numbers of all the completed rows
+*/
+Game.prototype.getRows = function () {
+ var i,
+ rows = [],
+ res = [],
+ curRow;
+
+ // initialize the rows to 0
+ for (i = 0; i < 20; i += 1) {
+ rows[i] = 0;
+ }
+ // for each block
+ for (i = 0; i < this.blocks.length; i += 1) {
+ // increment the appropriate row
+ curRow = this.blocks[i].getY();
+ rows[curRow] += 1;
+ // if the row is full
+ if (rows[curRow] === 10) {
+ res.push(curRow);
+ }
+ }
+
+ return res;
+};
+
+/**
+* Removes the rows from the field
+*/
+Game.prototype.removeRows = function (rows) {
+ var dropDist = {},
+ i, j,
+ remove = {},
+ curBlock,
+ curY;
+
+ // initialize drops to 0
+ for (i = -4; i < 20; i += 1) {
+ dropDist[i] = 0;
+ }
+
+ // for each removed row
+ for (i = 0; i < rows.length; i += 1) {
+ remove[rows[i]] = true;
+
+ // every row above this should be dropped another spot
+ for (j = -4; j < rows[i]; j += 1) {
+ dropDist[j] += 1;
+ }
+ }
+
+ // for each block
+ for (i = 0; i < this.blocks.length; i += 1) {
+ curBlock = this.blocks[i];
+ curY = curBlock.getY();
+
+ // if it is being removed
+ if (remove[curY]) {
+ // remove the block
+ this.removeBlock(i);
+ i -= 1;
+ } else {
+ // it is being dropped
+ curBlock.setPosition(curBlock.getX(), curBlock.getY() + dropDist[curY]);
+ }
+ }
+};
+
+Game.prototype.removeBlock = function(index) {
+ this.blocks[index].kill();
+ return this.blocks.splice(index, 1);
+};
+
+Game.prototype.applyGravity = function (dTime) {
+ this.timeToNextDrop -= dTime;
+
+ // drop until there is a positive time until the next drop time is positive, or the control group s bottomed out
+ while (this.timeToNextDrop < 0 && (!this.controlGroup.isBottomed())) {
+ this.dropBlock(true);
+ this.timeToNextDrop += this.dropPeriod;
+ }
+
+ // if it exited through bottoming, reset the drop period
+ if (this.controlGroup.isBottomed()) {
+ this.timeToNextDrop = this.dropPeriod;
+ }
+};
+
+/**
+* Changes the shapes of the preview along the side
+* @param {[Char]} queue - the queue of pieces
+*/
+Game.prototype.updatePreviews = function(queue) {
+ var i;
+ for (i = 0; i < queue.length; i += 1) {
+ this.previewGroups[i].setShape(queue[i]);
+ }
+};
+
+/**
+* called when the user attempts to swap a block
+*/
+Game.prototype.swap = function() {
+ var i, j,
+ newShape,
+ oldShape = this.controlGroup.getShape(),
+ oldBlocks = this.controlGroup.getBlocks(),
+ newBlocks = [],
+ thisObject = this;
+
+ // can only be called once per drop
+ if (!this.swapAllowed) {
+ return;
+ }
+ this.swapAllowed = false;
+
+ // Reset the locking
+ this.resetLockCounter(false);
+
+ // remove the blocks
+ // for each block on the field
+ for (i = 0; i < this.blocks.length; i += 1) {
+ // if the block is part of the control group, remove it
+ for (j = 0; j < 4; j += 1) {
+ if (oldBlocks[j] === this.blocks[i]) {
+ this.removeBlock(i);
+ i -= 1;
+ }
+ }
+ }
+
+ // if there is a block waiting
+ if (this.swapGroup) {
+ newShape = this.swapGroup.getShape();
+ for (i = 0; i < 4; i += 1) {
+ newBlocks.push(new Block({blockX:-10, blockY:-10, shape: newShape, occupiedPositions: this.occupiedPositions}));
+ this.blocks.push(newBlocks[i]);
+ }
+
+ this.controlGroup = new ControlGroup(newBlocks, newShape, function(x, y){
+ return thisObject.isLegalPosition(x, y);
+ });
+
+ this.swapGroup.setShape(oldShape);
+
+ return;
+ }
+
+ // if there is no block waiting
+ this.swapGroup = new PreviewGroup(-100, 60);
+ this.swapGroup.setShape(oldShape);
+ this.newBlock(true);
+
+};
+
+/**
+* locks the currnt piece in, registers lines and makes a new block
+*/
+Game.prototype.lockBlocks = function() {
+ // figure out if it a t-spin/t-spin mini
+ var tSpinType = this.controlGroup.getTSpin(),
+ scoreObject = {},
+ rows;
+
+ if (tSpinType === 'mini') {
+ scoreObject.miniT = true;
+ } else if (tSpinType === 'normal') {
+ scoreObject.normalT = true;
+ }
+
+ // look for rows
+ rows = this.getRows();
+ scoreObject.lines = rows.length;
+ if (rows.length > 0) {
+ this.removeRows(rows);
+ }
+
+ // apply the score
+ this.scoreTracker.updateScore(scoreObject);
+
+ this.newBlock();
+ this.resetLockCounter(false);
+};
+
+/**
+* Resets the lock counter, and the slide counter if not soft
+* @param {Boolean} soft = true if a soft reset, and the slide counter should not be reset
+*/
+Game.prototype.resetLockCounter = function (soft) {
+ if (soft) {
+ this.slideCount += 1;
+ } else {
+ this.slideCount = 0;
+ }
+ this.bottomTimer = this.bottomLockTime;
+};
+
+/**
+ * Determines if the game is over and returns a score object
+ * if it is. Otherwise, returns null
+ */
+Game.prototype.getResults = function() {
+ if (this.gameLost || this.scoreTracker.gameWon()) {
+ return this.scoreTracker.getResults();
+ }
+ return null;
+};
diff --git a/tetris/PreviewGroup.js b/tetris/PreviewGroup.js
new file mode 100644
index 00000000..71027c0c
--- /dev/null
+++ b/tetris/PreviewGroup.js
@@ -0,0 +1,47 @@
+
+function PreviewGroup(baseX, baseY) {
+ var i;
+
+ this.blocks = [];
+ this.shape = null;
+
+ // create the blocks
+ for (i = 0; i < 4; i += 1) {
+ this.blocks.push(new Block({
+ boardOriginX: baseX,
+ boardOriginY: baseY,
+ blockX: 0,
+ blockY: 0,
+ shape: 'i'
+ }));
+ }
+}
+
+/**
+* Sets the shape and color of the blocks
+* @param {Char} shape - the letter of the new shape
+* @param {Boolean} preview - true if it should have preview colors
+*/
+PreviewGroup.prototype.setShape = function(shape) {
+ var shapeConfig = SHAPES[shape],
+ i;
+
+ this.shape = shape;
+
+ for (i = 0; i < 4; i += 1) {
+ this.blocks[i].setPosition(shapeConfig.pos[i].x, shapeConfig.pos[i].y);
+ this.blocks[i].setColor(shape, false);
+ }
+};
+
+PreviewGroup.prototype.getShape = function () {
+ return this.shape;
+};
+
+PreviewGroup.prototype.draw = function() {
+ var i;
+ for (i = 0; i < 4; i += 1) {
+ this.blocks[i].draw();
+ }
+};
+
\ No newline at end of file
diff --git a/tetris/RandomBag.js b/tetris/RandomBag.js
new file mode 100644
index 00000000..7e17883d
--- /dev/null
+++ b/tetris/RandomBag.js
@@ -0,0 +1,49 @@
+function RandomBag(queueSize) {
+ // start off empty
+ this.available = [];
+ this.queue = [];
+
+ // initialize by refilling the queue
+ while (this.queue.length < queueSize) {
+ this.queue.push(this.nextAvailable());
+ }
+}
+
+RandomBag.initialList = ['i', 'o', 'j', 'l', 'z', 's', 't'];
+
+/**
+* Returns the letters of the queue
+* @returns {[Char]} the letters of the queue in order of oldest to newest
+*/
+RandomBag.prototype.getQueue = function () {
+ return this.queue;
+};
+
+/**
+* Moves the queue forward by one
+* @returns {Char} the poped value
+*/
+RandomBag.prototype.popQueue = function () {
+ var res = this.queue.shift();
+ this.queue.push(this.nextAvailable());
+ return res;
+};
+
+/**
+* gets the next letter for the queue, and updates the random bag state
+* @returns {Char} the next letter for the queue
+* @private
+*/
+RandomBag.prototype.nextAvailable = function() {
+ var index, res;
+
+ // if the available needs to be rebuilt
+ if (this.available.length === 0) {
+ this.available = RandomBag.initialList.slice(0); // shallow copy
+ }
+
+ index = Math.floor(Math.random()*this.available.length);
+ res = this.available.splice(index, 1)[0];
+
+ return res;
+};
\ No newline at end of file
diff --git a/tetris/ScoreTracker.js b/tetris/ScoreTracker.js
new file mode 100644
index 00000000..252332b8
--- /dev/null
+++ b/tetris/ScoreTracker.js
@@ -0,0 +1,213 @@
+function ScoreTracker(scoreOutput, linesOutput, levelOutput, tickerOutput) {
+ this.level = 1;
+ this.score = 0;
+ this.linesRemaining = ScoreTracker.levelLines(this.level);
+
+ this.scoreOutput = scoreOutput;
+ this.linesOutput = linesOutput;
+ this.levelOutput = levelOutput;
+ this.tickerOutput = tickerOutput;
+
+ this.curCombo = -1;
+ this.lastWasBonus = false;
+ this.backToBackCount = 0;
+
+ this.isGameWon = false;
+
+ this.outputScore();
+ this.outputLines();
+ this.outputLevel();
+}
+
+ScoreTracker.levelLines = function (level) {
+ return level*5;
+};
+
+ScoreTracker.prototype.updateScore = function(config) {
+ var linesCleared = 0,
+ isBonus = false,
+ scoreDiff = 0,
+ tickerLines = [],
+ i;
+
+ if (config.miniT) {
+ // mini t spin, 1 for no lines, 2 for 1 line
+ tickerLines.push("T Spin Mini");
+ linesCleared += 1;
+ scoreDiff += 100 * this.level;
+ if (config.lines === 1) {
+ linesCleared += 1;
+ scoreDiff += 100 * this.level;
+ }
+ } else if (config.normalT) {
+ // normal t spin, bonus for eveything but 0 lines
+ switch (config.lines) {
+ case 0:
+ tickerLines.push("T Spin");
+ linesCleared += 4;
+ scoreDiff += 400 * this.level;
+ break;
+ case 1:
+ tickerLines.push("T Spin Single");
+ linesCleared += 8;
+ isBonus = true;
+ scoreDiff += 800 * this.level;
+ break;
+ case 2:
+ tickerLines.push("T Spin Double");
+ linesCleared += 12;
+ isBonus = true;
+ scoreDiff += 1200 * this.level;
+ break;
+ case 3:
+ tickerLines.push("T SPIN TRIPLE");
+ linesCleared += 16;
+ isBonus = true;
+ scoreDiff += 1600 * this.level;
+ break;
+ }
+ } else if (config.lines > 0) {
+ // plain old line clears
+ switch (config.lines) {
+ case 1:
+ tickerLines.push("Single");
+ linesCleared += 1;
+ scoreDiff += 100 * this.level;
+ break;
+ case 2:
+ tickerLines.push("Double");
+ linesCleared += 3;
+ scoreDiff += 300 * this.level;
+ break;
+ case 3:
+ tickerLines.push("Triple");
+ linesCleared += 5;
+ scoreDiff += 500 * this.level;
+ break;
+ case 4:
+ tickerLines.push("TETRIS");
+ linesCleared += 8;
+ isBonus = true;
+ scoreDiff += 800 * this.level;
+ break;
+ }
+ }
+
+ // apply the combo
+ if (linesCleared > 0) {
+ this.curCombo += 1;
+ linesCleared += Math.floor(this.curCombo * 0.5);
+ scoreDiff += 50 * this.curCombo * this.level;
+ if (this.curCombo >= 1) {
+ tickerLines.push("Combo x" + this.curCombo);
+ }
+ } else {
+ this.curCombo = -1;
+ }
+
+ // apply back-to-back bonus
+ if (this.lastWasBonus && isBonus) {
+ tickerLines.push("Back-to-Back");
+ this.backToBackCount += 1;
+ linesCleared = Math.floor(linesCleared * 1.5);
+ scoreDiff += this.backToBackCount * 0.5 * scoreDiff;
+ } else {
+ this.backToBackCount = 0;
+ }
+ // only update the last bonus state if a single through triple was gotten
+ if (config.lines > 0) {
+ this.lastWasBonus = isBonus;
+ }
+
+ // apply the lines cleared
+ this.linesRemaining -= linesCleared;
+ if (this.linesRemaining <= 0) {
+ if (this.level < 15) {
+ this.level += 1;
+ this.linesRemaining = ScoreTracker.levelLines(this.level);
+ } else {
+ this.isGameWon = true;
+ }
+ this.outputLevel();
+ }
+
+ if (linesCleared > 0) {
+ this.outputLines();
+ }
+
+
+ this.score += scoreDiff;
+ this.outputScore();
+
+ if (tickerLines.length === 0) {
+ this.tickerOutput.addLine("");
+ } else {
+ for (i = 0; i < tickerLines.length; i += 1) {
+ this.tickerOutput.addLine(tickerLines[i]);
+ }
+ }
+};
+
+ScoreTracker.prototype.softDrop = function() {
+ this.score += 1;
+};
+
+ScoreTracker.prototype.hardDrop = function(dist) {
+ this.score += 2 * dist;
+};
+
+ScoreTracker.prototype.getLinesRemaining = function() { return this.linesRemaining; };
+ScoreTracker.prototype.getScore = function() { return this.score; };
+ScoreTracker.prototype.getLevel = function() { return this.level; };
+
+ScoreTracker.prototype.getLevelPeriod = function() {
+ var periods = [
+ 1000,
+ 800,
+ 600,
+ 470,
+ 380,
+ 250,
+ 200,
+ 160,
+ 130,
+ 90,
+ 50,
+ 27,
+ 20,
+ 15,
+ 10
+ ],
+ res = periods[(this.level < periods.length) ? this.level : periods.length - 1];
+ return res;
+};
+
+ScoreTracker.prototype.gameWon = function() {
+ return this.isGameWon;
+};
+
+ScoreTracker.prototype.getResults = function() {
+ return {
+ score: this.score,
+ level: this.level,
+ won: this.isGameWon
+ };
+};
+
+ScoreTracker.prototype.outputScore = function() {
+ this.scoreOutput.addLine("Score:");
+ this.scoreOutput.addLine("" + this.score);
+ this.scoreOutput.addLine("");
+};
+
+ScoreTracker.prototype.outputLines = function() {
+ this.linesOutput.addLine("Lines:");
+ this.linesOutput.addLine("" + this.linesRemaining);
+ this.linesOutput.addLine("");
+};
+
+ScoreTracker.prototype.outputLevel = function() {
+ this.levelOutput.addLine("Level:");
+ this.levelOutput.addLine("" + this.level);
+ this.levelOutput.addLine("");
+};
diff --git a/tetris/Shapes.js b/tetris/Shapes.js
new file mode 100644
index 00000000..ebebb291
--- /dev/null
+++ b/tetris/Shapes.js
@@ -0,0 +1,94 @@
+var SHAPES = {
+ i: {
+ spin: 'corner',
+ startX: 5,
+ startY: 0,
+ pos: [
+ { x: -2, y: -1 },
+ { x: -1, y: -1},
+ { x: 0, y: -1 },
+ { x: 1, y: -1 }
+ ],
+ image: 'media/cyanblock.png',
+ kickType: 'i_block'
+ },
+ o: {
+ spin: 'corner',
+ startX: 5,
+ startY: -1,
+ pos: [
+ { x: -1, y: 0 },
+ { x: 0, y: 0},
+ { x: -1, y: -1 },
+ { x: 0, y: -1 }
+ ],
+ image: 'media/yellowblock.png',
+ kickType: 'standard'
+ },
+ j: {
+ spin: 'block',
+ startX: 4,
+ startY: -1,
+ pos: [
+ { x: -1, y: -1 },
+ { x: -1, y: 0 },
+ { x: 0, y: 0 },
+ { x: 1, y: 0 }
+ ],
+ image: 'media/blueblock.png',
+ kickType: 'standard'
+ },
+ l: {
+ spin: 'block',
+ startX: 4,
+ startY: -1,
+ pos: [
+ { x: -1, y: 0 },
+ { x: 0, y: 0 },
+ { x: 1, y: 0 },
+ { x: 1, y: -1 }
+ ],
+ image: 'media/orangeblock.png',
+ kickType: 'standard'
+ },
+ s: {
+ spin: 'block',
+ startX: 4,
+ startY: -1,
+ pos: [
+ { x: -1, y: 0 },
+ { x: 0, y: 0 },
+ { x: 0, y: -1 },
+ { x: 1, y: -1 }
+ ],
+ image: 'media/greenblock.png',
+ kickType: 'standard'
+ },
+ z: {
+ spin: 'block',
+ startX: 4,
+ startY: -1,
+ pos: [
+ { x: -1, y: -1 },
+ { x: 0, y: -1 },
+ { x: 0, y: 0 },
+ { x: 1, y: 0 }
+ ],
+ image: 'media/redblock.png',
+ kickType: 'standard'
+ },
+ t: {
+ spin: 'block',
+ startX: 4,
+ startY: -1,
+ pos: [
+ { x: -1, y: 0 },
+ { x: 0, y: 0 },
+ { x: 0, y: -1 },
+ { x: 1, y: 0 }
+ ],
+ image: 'media/purpleblock.png',
+ kickType: 'standard'
+ }
+
+};
diff --git a/tetris/TtyBlock.js b/tetris/TtyBlock.js
new file mode 100644
index 00000000..5fd1484d
--- /dev/null
+++ b/tetris/TtyBlock.js
@@ -0,0 +1,84 @@
+
+function TtyBlock (divName, numLines, rollOverLength, rollOverRemove) {
+ var i;
+
+ this.elem = document.getElementById(divName);
+
+ // slow scorlling effect variables
+ this.curPos = 0;
+ this.cursorShown = false;
+
+ // TODO: make these random starting values
+ this.timePassedType = 0;
+ this.timePassedFlash = 0;
+
+ // time in ms
+ this.typePeriod = 30;
+ this.flashPeriod = 300;
+
+ this.lines = [];
+ for (i = 0; i < numLines; i += 1) {
+ this.lines.push("");
+ }
+
+ this.rollOverLength = rollOverLength || 9;
+ this.rollOverRemove = rollOverRemove || 3;
+
+ this.backlog = [];
+}
+
+
+/**
+ updates the text block
+ */
+TtyBlock.prototype.draw = function (dTime) {
+ var i,
+ outputString = "",
+ lastLine;
+
+ this.timePassedType += dTime;
+
+ while (this.timePassedType > this.typePeriod) {
+ this.curPos += 1;
+ this.timePassedType -= this.typePeriod;
+ }
+
+ lastLine = this.lines[this.lines.length-1];
+
+ if (this.curPos > lastLine.length) {
+ this.timePassedFlash += dTime;
+ while (this.timePassedFlash > this.flashPeriod) {
+ this.cursorShown = !this.cursorShown;
+ this.timePassedFlash -= this.flashPeriod;
+ }
+ }
+
+ // if I'm past the end of the last line, and there is a backlog, shift all the lines
+ if (this.curPos > lastLine.length && this.backlog.length > 0) {
+ this.lines.shift();
+ lastLine = this.backlog.shift();
+ this.lines.push(lastLine);
+ this.curPos = 0;
+ }
+
+ // print all of the lines but the last one
+ for (i = 0; i < this.lines.length - 1; i += 1) {
+ outputString += this.lines[i] + "
";
+ }
+ outputString += lastLine.slice(0, Math.min(this.curPos, lastLine.length));
+ if (this.cursorShown) {
+ outputString += "_";
+ }
+ // rewirte for html gaurds
+ outputString.replace('>', '>');
+ this.elem.innerHTML = outputString;
+};
+
+TtyBlock.prototype.addLine = function(str) {
+ // if the backlog is too long, then remove the last 3 values
+ if (this.backlog.length > this.rollOverLength) {
+ this.backlog.splice(this.backlog.length - this.rollOverRemove, this.rollOverRemove);
+ }
+
+ this.backlog.push(" > " + str);
+};
diff --git a/tetris/WallKicks.js b/tetris/WallKicks.js
new file mode 100644
index 00000000..7fe3b526
--- /dev/null
+++ b/tetris/WallKicks.js
@@ -0,0 +1,67 @@
+var WALL_KICK_OFFSETS = {};
+
+/*
+0 -> starting orientation
+1 -> 1 turn cw
+2 -> 2 turns
+3-> 1 turn ccw
+
+non-I blocks
+L->2 ( 0, 0) (-1, 0) (-1,-1) ( 0,+2) (-1,+2)
+L->0 ( 0, 0) (-1, 0) (-1,-1) ( 0,+2) (-1,+2)
+2->R ( 0, 0) (-1, 0) (-1,+1) ( 0,-2) (-1,-2)
+2->L ( 0, 0) (+1, 0) (+1,+1) ( 0,-2) (+1,-2)
+R->0 ( 0, 0) (+1, 0) (+1,-1) ( 0,+2) (+1,+2)
+R->2 ( 0, 0) (+1, 0) (+1,-1) ( 0,+2) (+1,+2)
+0->L ( 0, 0) (+1, 0) (+1,+1) ( 0,-2) (+1,-2)
+0->R ( 0, 0) (-1, 0) (-1,+1) ( 0,-2) (-1,-2)
+
+I block
+0->R ( 0, 0) (-2, 0) (+1, 0) (-2,-1) (+1,+2)
+0->L ( 0, 0) (-1, 0) (+2, 0) (-1,+2) (+2,-1)
+
+R->2 ( 0, 0) (-1, 0) (+2, 0) (-1,+2) (+2,-1)
+R->0 ( 0, 0) (+2, 0) (-1, 0) (+2,+1) (-1,-2)
+
+2->L ( 0, 0) (+2, 0) (-1, 0) (+2,+1) (-1,-2)
+2->R ( 0, 0) (+1, 0) (-2, 0) (+1,-2) (-2,+1)
+
+L->0 ( 0, 0) (+1, 0) (-2, 0) (+1,-2) (-2,+1)
+L->2 ( 0, 0) (-2, 0) (+1, 0) (-2,-1) (+1,+2)
+*/
+
+WALL_KICK_OFFSETS.standard = [
+ {
+ cw: [{x:0,y:0}, {x:-1,y:0}, {x:-1,y:-1}, {x:0,y:2}, {x:-1,y:2}],
+ ccw: [{x:0,y:0}, {x:1,y:0}, {x:1,y:-1}, {x:0,y:2}, {x:1,y:2}]
+ },{
+ cw: [{x:0,y:0}, {x:1,y:0}, {x:1,y:1}, {x:0,y:-2}, {x:1,y:-2}],
+ ccw: [{x:0,y:0}, {x:1,y:0}, {x:1,y:1}, {x:0,y:-2}, {x:1,y:-2}]
+ },{
+ cw: [{x:0, y:0}, {x:1,y:0}, {x:1,y:-1}, {x:0,y:2}, {x:1,y:2}],
+ ccw: [{x:0, y:0}, {x:-1, y:0}, {x:-1,y:-1}, {x:0,y:2}, {x:-1,y:2}]
+ },{
+ cw: [{x:0,y:0}, {x:-1,y:0}, {x:-1,y:1}, {x:0,y:-2}, {x:-1,y:-2}],
+ ccw: [{x:0,y:0}, {x:-1,y:0}, {x:-1,y:1}, {x:0,y:-2}, {x:-1,y:-2}]
+ }
+];
+
+WALL_KICK_OFFSETS.i_block = [
+ {
+ cw: [{x:0,y:0}, {x:-2,y:0}, {x:1,y:0}, {x:-2,y:1}, {x:1,y:-2}],
+ ccw: [{x:0,y:0}, {x:-1,y:0}, {x:2,y:0}, {x:-1,y:-2}, {x:2,y:1}]
+ },{
+ cw: [{x:0,y:0}, {x:-1,y:0}, {x:2,y:0}, {x:-1,y:-2}, {x:2,y:1}],
+ ccw: [{x:0,y:0}, {x:2,y:0}, {x:-1,y:0}, {x:2,y:-1}, {x:-1,y:2}]
+ },{
+ cw: [{x:0,y:0}, {x:2,y:0}, {x:-1,y:0}, {x:2,y:-1}, {x:-1,y:2}],
+ ccw: [{x:0,y:0}, {x:1,y:0}, {x:-2,y:0}, {x:1,y:2}, {x:-2,y:-1}]
+ },{
+ cw: [{x:0,y:0}, {x:1,y:0}, {x:-2,y:0}, {x:1,y:2}, {x:-2,y:1}],
+ ccw: [{x:0,y:0}, {x:-2,y:0}, {x:1,y:0}, {x:-2,y:1}, {x:1,y:-2}]
+ }
+
+];
+
+
+
diff --git a/tetris/about.html b/tetris/about.html
new file mode 100644
index 00000000..81191369
--- /dev/null
+++ b/tetris/about.html
@@ -0,0 +1,66 @@
+
+
+ | + +
+
+ > man TwitchTeteris
+ + TwitchTetris is an open-source implementation of Tetris, the classic falling-block game that we all know and love, designed for those who demand the best performace from their tetris game. +TwitchTetris aims to be the fastest implementation of Tetris to be played within a browser. This is possible because it's implemented completely in Html5/JavaScript. It was developed out of the frustration of inconsistent performance of Flash plugins across multiple Operating Systems and Browsers, and the sad state of other available Html5 Tetris games. TwitchTetris should have the fastest reaction time to user input of any complete Tetris implementation, and should perform the most consistently over all modern browsers. + +This project is still in the infancy of development. Please go to the google code page to report any bugs, problems, or suggestions that you have for the game. Contributions from other developers are also welcome. + +While TwitchTetris is not deisgned to generate revenue, ads are displayed on this website in order to cover the potential cost incurring high traffic. There are no plans to expand advertising past one or 2 ads per page, and obtrusive ads which make users wait to play the game will never be shown on this website. + + |
+
+ | + +
+
+
+
+
+ Do you want to use a custom control scheme?
+
+ + + + +
+ To change your controls, select "Custom Controls" from above.
+
+
+ Click on the fields on the right to set your controls.
+
+
+ Press a key to set this field...
+
+ + +
+
+
+
+ Rotate Left:
+
+
+ Rotate Right:
+
+
+ Shift Left:
+
+
+ Shift Right:
+
+
+ Soft Drop:
+
+
+ Hard Drop:
+
+
+ Swap Peice:
+
+
+ Auto-Repeat Times:
+
+ + + +
+
+
+ AutoRepeat: ms
+ + +
+ Repeat Charge: ms
+ + + |
+
+ | + +
+
+ Daily High Scores:
+
+
+ + |
+
# | Name | Score |
' + (i+1) + ' | ' + curScore.name + ' | ' + curScore.score + ' |
+ | + +
+
+
+
+
+
+
+
- showStats();
- addEvents();
+
+
+
+
- var last = now = timestamp();
- function frame() {
- now = timestamp();
- update(Math.min(1, (now - last) / 1000.0));
- draw();
- stats.update();
- last = now;
- requestAnimationFrame(frame, canvas);
- }
+
+
+
+ + Controls: +
+ + + + |
+