package net.tongits.tint
{
import flash.display.BitmapData;
import flash.display.Shape;
import flash.display.Sprite;
import flash.events.TimerEvent;
import flash.events.KeyboardEvent;
import flash.geom.Point;
import flash.text.TextField;
import flash.text.TextFormat;
import flash.ui.Keyboard;
import flash.utils.ByteArray;
import flash.utils.Timer;
public class Main extends Sprite
{
private static const COLORS:Array = [0xffffffff, 0xffffc20e, 0xff709ad1, 0xffe5aa7a, 0xfffff200, 0xffff7e00, 0xff546d8e, 0xff9c5a3c, 0xffffffff, 0xff22b14c];
private var _currentBrick:Brick;
private var _nextBrick:Brick;
private var _gameMask:Array = [[8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9], [9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9]];
private var _gameMaskClone:ByteArray = new ByteArray();
private var _scaleBy:Number = 20.0;
private var _gameBoardBitmap:BitmapData;
private var _chessBitmap:BitmapData;
private var _gameboard:Shape;
private var _nextBrickBitmap:BitmapData;
private var _nextBrickDisplay:Shape;
private var _dropTimer:Timer;
private var _gameOver:Boolean;
private var _gamePaused:Boolean;
private var _gameOverScreen:Sprite;
private var _defaultSpeed:Number = 1200;
private var _level:Number;
private var _pointToBeat:Number;
private var _percentToBeat:Number = 1.15;
private var _levelText:TextField;
private var _score:Number;
private var _scoreText:TextField;
public function Main():void {
init();
startGame();
}
private function init():void {
// create a separate sprite for the main game screen and next brick preview
var mainGameScreen:Sprite = addChild(new Sprite) as Sprite;
mainGameScreen.x = 10;
mainGameScreen.y = 10;
mainGameScreen.scaleX = _scaleBy;
mainGameScreen.scaleY = _scaleBy;
_chessBitmap = new BitmapData(2, 2, true);
_chessBitmap.setPixel32(0, 0, 0x07000000);
_chessBitmap.setPixel32(1, 1, 0x07000000);
// backup the original game mask without the dropping brick
_gameMaskClone.writeObject(_gameMask);
_gameBoardBitmap = new BitmapData(_gameMask[0].length, _gameMask.length);
_gameboard = mainGameScreen.addChild(new Shape) as Shape;
// set to largest brick's size
_nextBrickBitmap = new BitmapData(4, 4, false);
_nextBrickDisplay = mainGameScreen.addChild(new Shape) as Shape;
_nextBrickDisplay.x = _gameboard.x + _gameMask[0].length + 1;
_nextBrickDisplay.y = _gameboard.y + 1;
// add the text elements below the main game screen
var textFormat:TextFormat = new TextFormat("Arial", 10);
// no game is complete without scoring
_score = 0;
_scoreText = addChild(new TextField) as TextField;
_scoreText.width = stage.stageWidth;
_scoreText.defaultTextFormat = textFormat;
_scoreText.htmlText = "Score: " + _score.toString();
_scoreText.x = 10;
_scoreText.y = stage.stageHeight - 50;
// and levels
_level = 1;
_pointToBeat = 3000;
_levelText = addChild(new TextField) as TextField;
_levelText.width = stage.stageWidth;
_levelText.defaultTextFormat = textFormat;
_levelText.x = 10;
_levelText.y = stage.stageHeight - 35;
// add instructions
var instText:TextField = addChild(new TextField) as TextField;
instText.defaultTextFormat = textFormat;
instText.x = stage.stageWidth - 95;
instText.y = 130;
instText.multiline = true;
instText.htmlText = "Controls:\n"
+ "up arrow = rotate\n"
+ "spacebar = drop\n"
+ "p = pause|resume\n"
+ "n = start new game";
stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownFunction);
function keyDownFunction(event:KeyboardEvent):void {
if (!_gameOver && (String.fromCharCode(event.charCode).toLowerCase() == "p")) {
pauseGame();
} else if (String.fromCharCode(event.charCode).toLowerCase() == "n") {
startGame();
} else if (!_gameOver && !_gamePaused) {
switch (event.keyCode) {
case Keyboard.LEFT: moveBrick(Brick.LEFT); break;
case Keyboard.UP: rotateBrick(); break;
case Keyboard.RIGHT: moveBrick(Brick.RIGHT); break;
case Keyboard.DOWN:
// score extra points when moving bricks down
updateScore();
moveBrick(Brick.DOWN);
break;
}
}
}
stage.addEventListener(KeyboardEvent.KEY_UP, keyUpFunction);
function keyUpFunction(event:KeyboardEvent):void {
if (!_gameOver && !_gamePaused) {
switch (event.keyCode) {
case Keyboard.SPACE: dropBrick(); break;
default: break;
}
}
}
}
private function updateScore(value:Number = 1):void {
// update the score current level serves as multiplier
_score += value * (_level * 2);
_scoreText.htmlText = "Score: " + _score.toString();
// increase level
if (_score > _pointToBeat) {
_level++;
// increasing the point to beat by a constant value is ugly so we use a percentage value
_pointToBeat += Math.round(_percentToBeat * _pointToBeat);
trace("point to beat: " + _pointToBeat.toString());
// increase drop speed by 150ms per level
_dropTimer.stop();
trace(_dropTimer.delay);
if ((_dropTimer.delay - 150) <= 0) {
_dropTimer.delay = 100;
} else {
_dropTimer.delay -= 150;
}
trace(_dropTimer.delay);
_dropTimer.start();
}
_levelText.htmlText = "Level: " + _level.toString()
+ "\nNext level after " + _pointToBeat.toString() + " points.";
}
private function pauseGame():void {
if (_gamePaused) {
_gamePaused = false;
_dropTimer.start();
removeChildAt(numChildren - 1);
// the pauseText TextField might have stolen the focus so we set this to null
stage.focus = null;
} else {
_gamePaused = true;
_dropTimer.stop();
// show fader
var fader:Sprite = addChild(new Sprite) as Sprite;
fader.graphics.beginFill(0x000000, 0.90);
fader.graphics.drawRect(0, 0, stage.stageWidth, stage.stageHeight);
fader.graphics.endFill();
var pausedText:TextField = fader.addChild(new TextField) as TextField;
pausedText.selectable = false;
pausedText.text = "Game paused.\nPress p to resume.";
pausedText.width = stage.stageWidth;
var tf:TextFormat = new TextFormat("Arial", 14, 0xffffff);
tf.align = "center";
pausedText.selectable = false;
pausedText.setTextFormat(tf);
pausedText.x = 0;
pausedText.y = (stage.stageHeight / 2) - (pausedText.height / 2);
}
}
private function startGame():void {
if (_gameOverScreen != null) {
removeChildAt(numChildren - 1);
_gameOverScreen = null;
}
_score = -2;
_level = 1;
_pointToBeat = 3000;
updateScore();
_gameOver = false;
_gamePaused = false;
// reset the _gameMask, don't include the walls
for (var i:uint = 0; i < _gameMask.length - 1; i++) {
for (var j:uint = 1; j < _gameMask[0].length - 1; j++) {
_gameMask[i][j] = 0;
}
}
// as well as the clone
_gameMaskClone = new ByteArray();
_gameMaskClone.writeObject(_gameMask);
if (_dropTimer == null) {
_dropTimer = new Timer(_defaultSpeed);
_dropTimer.addEventListener(TimerEvent.TIMER, function():void {
moveBrick(Brick.DOWN);
});
} else {
_dropTimer.reset();
_dropTimer.delay = _defaultSpeed;
_dropTimer.start();
}
// create the brick, there is no current brick since this is the start of the game
_nextBrick = new Brick();
addNextBrick();
}
private function refresh():void {
updateGameMask();
renderScreen();
}
/**
* Reset the gameboard mask excluding the currently dropping brick
*/
private function updateGameMask():void {
// reset the _gameMask
_gameMaskClone.position = 0;
_gameMask = _gameMaskClone.readObject() as Array;
// update the _gameMask with the brick's new position
var row:uint, col:uint;
for (row = 0; row < _currentBrick.mask.length; row++) {
for (col = 0; col < _currentBrick.mask[row].length; col++) {
if (_currentBrick.mask[row][col] > 0) {
_gameMask[_currentBrick.point.y + row][_currentBrick.point.x + col + 1] = _currentBrick.mask[row][col];
}
}
}
}
/**
* Fill the shape objects with the updated changes in the game
*/
private function renderScreen():void {
if (_gameMask.length == 0) {
throw new Error("_gameMask is empty!");
}
// the game baord
_gameBoardBitmap.fillRect(_gameBoardBitmap.rect, 0x00000000);
for (var i:uint = 0; i < _gameMask[0].length; i++) {
for (var j:uint = 0; j < _gameMask.length; j++) {
if (_gameMask[j][i] > 0) {
_gameBoardBitmap.setPixel32(i, j, COLORS[_gameMask[j][i]]);
}
}
}
_gameboard.graphics.clear();
_gameboard.graphics.beginBitmapFill(_chessBitmap, null, true);
_gameboard.graphics.drawRect(1, 1, _gameBoardBitmap.width - 2, _gameBoardBitmap.height -2);
_gameboard.graphics.endFill();
_gameboard.graphics.beginBitmapFill(_gameBoardBitmap, null, false);
_gameboard.graphics.drawRect(0, 0, _gameBoardBitmap.width, _gameBoardBitmap.height);
_gameboard.graphics.endFill();
// display the next brick
if (_nextBrick == null) {
throw new Error("next brick not created!");
}
_nextBrickBitmap.fillRect(_nextBrickBitmap.rect, 0xffffffff);
for (i = 0; i < _nextBrick.mask[0].length; i++) {
for (j = 0; j < _nextBrick.mask.length; j++) {
if (_nextBrick.mask[j][i] > 0) {
_nextBrickBitmap.setPixel(i, j, COLORS[_nextBrick.mask[j][i]]);
}
}
}
_nextBrickDisplay.graphics.clear();
_nextBrickDisplay.graphics.beginBitmapFill(_nextBrickBitmap, null, false);
_nextBrickDisplay.graphics.drawRect(0, 0, _nextBrickBitmap.width, _nextBrickBitmap.height);
_nextBrickDisplay.graphics.endFill();
}
/**
* Add the next brick in line to the _gameboard _gameMask
*/
private function addNextBrick():void {
_dropTimer.stop();
if (_nextBrick == null) {
throw new Error("next brick not created!");
}
_currentBrick = _nextBrick;
_nextBrick = new Brick();
var brickX:int = (_gameMask[0].length / 2) - (_currentBrick.mask[0].length / 2);
_currentBrick.point = new Point(brickX, 0);
// update the screen
refresh();
// check for game over
if (checkCollision(Brick.NONE)) {
_gameOver = true;
// display game over screen
if (_gameOverScreen == null) {
_gameOverScreen = addChild(new Sprite) as Sprite;
_gameOverScreen.graphics.beginFill(0x000000, 0.90);
_gameOverScreen.graphics.drawRect(0, 0, stage.stageWidth, stage.stageHeight);
_gameOverScreen.graphics.endFill();
var gameOverText:TextField = _gameOverScreen.addChild(new TextField) as TextField;
gameOverText.selectable = false;
gameOverText.text = "Game over!\nPress n to start new game.";
gameOverText.width = stage.stageWidth;
var tf:TextFormat = new TextFormat("Arial", 14, 0xffffff);
tf.align = "center";
gameOverText.selectable = false;
gameOverText.setTextFormat(tf);
gameOverText.x = 0;
gameOverText.y = (stage.stageHeight / 2) - (gameOverText.height / 2);
}
} else {
_dropTimer.start();
}
}
/**
* Move the brick to the left, right or down
* @param direction An unsigned integer value indicating where the brick should be moved
* @return True if the the move command was successful, False if the brick encountered a collision
*/
private function moveBrick(direction:uint):Boolean {
if (checkCollision(direction) == false) {
_currentBrick.move(direction);
// score points when moving bricks down
if (direction == Brick.DOWN) {
updateScore();
}
// update the screen only when the brick is moved
refresh();
return true;
} else {
// check if the brick has landed
if (direction == Brick.DOWN) {
landBrick();
}
return false;
}
}
/**
* Skip the dropping animation and instantly land the brick
*/
private function dropBrick():void {
while (moveBrick(Brick.DOWN)) {
// score even more extra points when dropping bricks
updateScore(2);
}
}
/**
* Check for collisions and then call the brick's rotate method
*/
private function rotateBrick():void {
_currentBrick.rotate();
// check for collision
if (checkCollision(Brick.NONE)) {
_currentBrick.rotate(Brick.CW);
} else {
refresh();
}
}
/**
* Compare the brick's and the gamboard's masks for collisions
* @param direction Where the brick is headed, either left, right, down or no movement
* @return True if the brick encountered a collision, false otherwise
*/
private function checkCollision(direction:uint):Boolean {
function collided(x:int, y:int):Boolean {
_gameMaskClone.position = 0;
var mask:Array = _gameMaskClone.readObject() as Array;
for (var row:uint = 0; row < _currentBrick.mask.length; row++) {
for (var col:uint = 0; col < _currentBrick.mask[row].length; col++) {
if ((_currentBrick.mask[row][col] > 0) && (mask[y + row][x + col + 1] > 0)) {
return true;
}
}
}
return false;
}
switch (direction) {
case Brick.LEFT: return collided(_currentBrick.point.x - 1, _currentBrick.point.y);
case Brick.RIGHT: return collided(_currentBrick.point.x + 1, _currentBrick.point.y);
case Brick.DOWN: return collided(_currentBrick.point.x, _currentBrick.point.y + 1);
// no movement, when rotating
default: return collided(_currentBrick.point.x, _currentBrick.point.y);
}
return false;
}
/**
* Control the landing event of the brick. Attempt to clear lines
* then add the next brick to the gameboard.
*
*/
private function landBrick():void {
// check if we have cleared a line, don't check the floor
var linesToClear:Array = new Array;
for (var row:uint = 0; row < _gameMask.length - 1; row++) {
var isLine:Boolean = true;
for (var col:uint = 0; col < _gameMask[0].length; col++) {
if (_gameMask[row][col] == 0) {
isLine = false;
}
}
if (isLine) {
linesToClear.push(row);
}
}
// clear the lines
if (linesToClear.length > 0) {
for each (var line:uint in linesToClear) {
_gameMask.splice(line, 1);
// add the removed wall on top
//_gameMask.unshift([9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9]);
_gameMask.splice(1, 0, [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9]);
}
}
// update the score, the number of lines cleared is squared
updateScore((linesToClear.length * linesToClear.length * linesToClear.length) * 10);
// update the _gameMaskClone
_gameMaskClone = new ByteArray();
_gameMaskClone.writeObject(_gameMask);
// send next brick
addNextBrick();
}
}
}
import flash.geom.Point;
class Brick {
public static const LEFT:uint = 0;
public static const RIGHT:uint = 1;
public static const DOWN:uint = 2;
public static const NONE:uint = 3;
public static const CCW:String = "CounterClockWise";
public static const CW:String = "ClockWise";
private static const O_BRICK:Array = [[[1, 1], [1, 1]]];
private static const T_BRICK:Array = [[[2, 2, 2], [0, 2, 0], [0, 0, 0]], [[2, 0], [2, 2], [2, 0]], [[0, 0, 0], [0, 2, 0], [2, 2, 2]], [[0, 0, 2], [0, 2, 2], [0, 0, 2]]];
private static const I_BRICK:Array = [[[0, 3], [0, 3], [0, 3], [0, 3]], [[0, 0, 0, 0], [3, 3, 3, 3]]];
private static const J_BRICK:Array = [[[0, 4, 4], [0, 4, 0], [0, 4, 0]], [[4, 0, 0], [4, 4, 4], [0, 0, 0]], [[0, 4, 0], [0, 4, 0], [4, 4, 0]], [[0, 0, 0], [4, 4, 4], [0, 0, 4]]];
private static const L_BRICK:Array = [[[0, 5, 0], [0, 5, 0], [0, 5, 5]], [[0, 0, 5], [5, 5, 5], [0, 0, 0]], [[5, 5, 0], [0, 5, 0], [0, 5, 0]], [[0, 0, 0], [5, 5, 5], [5, 0, 0]]];
private static const S_BRICK:Array = [[[0, 6, 0], [0, 6, 6], [0, 0, 6]], [[0, 0, 0], [0, 6, 6], [6, 6, 0]], [[0, 6, 0], [0, 6, 6], [0, 0, 6]], [[0, 0, 0], [0, 6, 6], [6, 6, 0]]];
private static const Z_BRICK:Array = [[[0, 7, 0], [7, 7, 0], [7, 0, 0]], [[0, 0, 0], [7, 7, 0], [0, 7, 7]], [[0, 7, 0], [7, 7, 0], [7, 0, 0]], [[0, 0, 0], [7, 7, 0], [0, 7, 7]]];
private static const BRICKS:Array = [O_BRICK, T_BRICK, J_BRICK, L_BRICK, S_BRICK, Z_BRICK, I_BRICK];
private var _type:Array;
private var _x:int;
private var _y:int;
private var _state:uint;
private var _mask:Array;
public function Brick():void {
// choose a random brick
var randBrick:Number = Math.random() * (BRICKS.length - 1);
randBrick = Math.round(randBrick);
_type = BRICKS[randBrick];
// choose a random rotational state
var randState:Number = Math.random() * (BRICKS[randBrick].length - 1);
randState = Math.round(randState);
_state = randState;
// set the brick's _mask
_mask = BRICKS[randBrick][randState];
// position the brick
_x = 0;
_y = 0;
}
public function get point():Point {
return new Point(_x, _y);
}
public function set point(value:Point):void {
_x = value.x;
_y = value.y;
}
public function get mask():Array {
return _mask;
}
/**
* Move the brick to the left, right or down
* @param direction An unsigned integer value indicating where the brick should be moved
*/
public function move(direction:uint):void {
switch (direction) {
case LEFT: _x--; break;
case RIGHT: _x++; break;
case DOWN: _y++; break;
// invalid direction
default:
throw new Error("Invalid direction");
break;
}
}
/**
* Rotate the brick
* @param rotation The direction of the brick's rotation, either counter-clockwise or clockwise
*/
public function rotate(rotation:String = CCW):void {
if (rotation == CCW) {
// check if the brick's state is the last state in the brick's mask array
if (_type.length - 1 == _state) {
_state = 0;
_mask = _type[_state];
} else {
_mask = _type[++_state];
}
} else { // clockwise
// check if the brick's state is the first state in the brick's mask array
if (_state == 0) {
_state = _type.length - 1;
_mask = _type[_state];
} else {
_mask = _type[--_state];
}
}
}
}