Really This Time - Part Three of Creating Tetris in Compiled JavaFX Script

As I mentioned in the previous post (which I accidentally published prematurely), I've been speaking on JavaFX Script at Sun Tech Days in Sydney, Australia. This was a great experience, as it was a well-run, informative conference. Of course, Sydney is a beautiful city, filled with wonderful people.

Tetrisstacking_2

Also, that post referenced the first two articles in this Tetris series, so please take a look at these articles to get up to speed on where we're at:

1) Let's Create a Tetris Game in Compiled JavaFX Script

2) Creating a Tetris Program (Part Two) in Compiled JavaFX Script

During the conference, I've made some progress on the Tetris application, and used it in one of my presentations. Please refer to the screenshot shown above.

This version of the Tetris application is not 100% functional, nor is it bug-free, but it did suffice for teaching JavaFX Script concepts in the presentation. One of my next posts will be a more fully functioning Tetris game (e.g. a layer of blocks will disappear when filled). That post will show this game as a JavaFX Script applet, running inside a browser. Take a look at the source code below, and you'll notice that I've changed quite a few things from the last version. For example, the TetrisPlayingField custom component is bound to the model of all of squares in the playing field.

TetrisMain.fx:

/*

* TetrisMain.fx - The main program for a compiled JavaFX Script Tetris game
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a compiled JavaFX Script example.
*/
package tetris_ui;

import javafx.ui.*;
import javafx.ui.canvas.*;
import java.lang.System;
import tetris_model.*;

Frame {
var canvas:Canvas
width: 350
height: 500
title: "TetrisJFX"
background: Color.LIGHTGREY
content:
StackPanel {
content: [
Canvas {
content:
ImageView {
image:
Image {
url: "images/background.jpg"
}
}
},
BorderPanel {
var model =
TetrisModel {}
center:
FlowPanel {
alignment: Alignment.LEADING
vgap: 20
hgap: 20
content: [
Canvas {
content:
TetrisPlayingField {
model: model
}
},
Label {
text: bind "
{model.score}
"
}
]
}
bottom:
FlowPanel {
alignment: Alignment.LEADING
content: [
Button {
text: "Play"
foreground: Color.DARKBLUE
action:
function() {
model.t.start();
}
},
Button {
text: "Rotate"
foreground: Color.DARKBLUE
action:
function() {
model.rotate90();
}
},
Button {
text: "Left"
foreground: Color.DARKBLUE
action:
function() {
model.moveLeft();
}
},
Button {
text: "Right"
foreground: Color.DARKBLUE
action:
function() {
model.moveRight();
}
}
]
}
}
]
}
visible: true
onClose:
function():Void {
System.exit(0);
}
}

TetrisPlayingField.fx:

/*

* TetrisPlayingField.fx -
* A custom graphical component that is the UI for the
* playing field.
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a compiled JavaFX Script example.
*
*/
package tetris_ui;

import javafx.ui.*;
import javafx.ui.canvas.*;
import java.lang.System;
import tetris_model.*;

class TetrisPlayingField extends CompositeNode {
private static attribute squareOutlineColor = Color.BLACK;
private static attribute squareOutlineWidth = 1;
public attribute model:TetrisModel;
public function composeNode():Node {
Group {
content: bind [
Rect {
x: 0
y: 0
width: model.NUM_COLS * model.SQUARE_SIZE
height: model.NUM_ROWS * model.SQUARE_SIZE
strokeWidth: 1
stroke: Color.BLACK
fill: Color.BLUE
opacity: .5
},
for (cell in model.fieldCells
where cell <> TetrisShapeType.NONE) {
Rect {
var yPos:Integer = indexof cell / model.NUM_COLS
x: (indexof cell % model.NUM_COLS).intValue() * model.SQUARE_SIZE
y: yPos * model.SQUARE_SIZE
width: model.SQUARE_SIZE
height: model.SQUARE_SIZE
fill: cell.squareColor
stroke: squareOutlineColor
strokeWidth: squareOutlineWidth
}
}
]
}
}
}

TetrisModel.fx:

/*

* TetrisModel.fx - The model behind the Tetris UI
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a compiled JavaFX Script example.
*
*/
package tetris_model;

import javafx.ui.animation.*;
import java.lang.System;
import com.sun.javafx.runtime.PointerFactory;
import java.lang.Double;

public class TetrisModel {
/** Size of each tetromino in pixels */
public static attribute SQUARE_SIZE = 20;

/** Number of rows in the playing field */
public static attribute NUM_ROWS = 20;

/** Number of columns in the playing field */
public static attribute NUM_COLS = 10;

private attribute stopDropping:Boolean;

/**
* Sequence of objects that represent the
* type of shapes (including no shape) in each playing field cell
*/
public attribute fieldCells:TetrisShapeType[] =
for (i in [1..(NUM_ROWS * NUM_COLS)]) {
TetrisShapeType.NONE;
};


/**
* The active tetromino shape type.
*/
public attribute activeShapeType:TetrisShapeType;

public attribute score:Integer = 10000;

/**
* This value is incremented via the KeyFrame animation mechanism,
* and represents the row in which the pivotal block is currently residing.
*/
public attribute a:Integer on replace (oldVal) {
if (a == 0) {
stopDropping = false;
}
if (not stopDropping) {
// Update score. TODO: Implement real scoring later
score += 100;

// Remove the tetromino from the playing field
updateFieldCells(tetrominoHorzPos, oldVal, tetrominoAngle, true);

if (canMoveDown(tetrominoHorzPos, a, tetrominoAngle)) {
// Was able to move down, so place the tetromino accordingly
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
}
else {
System.out.println("Can't move down!!!!!!!!");
updateFieldCells(tetrominoHorzPos, oldVal, tetrominoAngle, false);
stopDropping = true;
}
}

if (a >= stopValue) {
activeShapeType = TetrisShapeType.randomShapeType();
tetrominoAngle = 0;
}
};
private attribute pf = PointerFactory {};
private attribute bpa = bind pf.make(a);
private attribute pa = bpa.unwrap();
private attribute interpolate = NumberValue.LINEAR;
private attribute stopValue = NUM_ROWS + 2;
public attribute t =
Timeline {
keyFrames: [
KeyFrame {
keyTime: 0s;
keyValues:
NumberValue {
target: pa;
value: 0;
interpolate: bind interpolate
}
},
KeyFrame {
keyTime: 10s;
keyValues:
NumberValue {
target: pa;
value: bind stopValue
interpolate: bind interpolate
}
}
]
repeatCount: Double.POSITIVE_INFINITY
};
public attribute tetrominoAngle:Number;
public attribute tetrominoHorzPos:Number = (NUM_COLS / 2) as Integer;

public function rotate90():Void {
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, true);
tetrominoAngle = computeNewAngle(tetrominoAngle, 90);
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
}

public function computeNewAngle(originalRotationAngle:Integer, degreesToRotate):Integer {
((originalRotationAngle + degreesToRotate) % (activeShapeType.rotateStates * 90)) as Integer;
}

public function moveLeft():Void {
if (tetrominoHorzPos > 0) {
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, true);
tetrominoHorzPos--;
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
}
}

public function moveRight():Void {
//TODO: Need to allow pieces to bump up against the
// right side. For now, hardcode 3.

if (tetrominoHorzPos < NUM_COLS - 3) {
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, true);
tetrominoHorzPos++;
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
}
}

/**
* Keeps the fieldCells sequence (that represents the
* cells in the playing field) updated to reflect reality.
* pass in the row and column that the pivotal block was
* located, as well as the former angle of the tetromino.
* The new row, column, angle, as well as the shape type, is
* held in the model, so don't have to be passed in.
*/
public function updateFieldCells(oldX:Integer, oldY:Integer, oldAngle:Integer, remove:Boolean):Void {
// Take the shape off of its old location on the playing field
for (oldCell in TetrisShapeType.squarePositionsForRotatedShape(activeShapeType,
oldAngle)) {
fieldCells[(oldY + oldCell.y) * NUM_COLS + oldX + oldCell.x] =
if (remove) TetrisShapeType.NONE else activeShapeType;
}
}

public function canMoveDown(oldX:Integer, oldY:Integer, oldAngle:Integer):Boolean {
var retVal = true;
for (oldCell in TetrisShapeType.squarePositionsForRotatedShape(activeShapeType,
oldAngle)) {
// First check to see if the tetromino is at the bottom of
// the playing field
if ((oldY + oldCell.y) * NUM_COLS + oldX + oldCell.x > sizeof fieldCells) {
retVal = false;
}
// Now check to see if another tetromino is preventing it from moving down
else if (fieldCells[(oldY + oldCell.y) * NUM_COLS + oldX + oldCell.x] <>
TetrisShapeType.NONE) {
System.out.println("{(oldY + oldCell.y) * NUM_COLS + oldX + oldCell.x},
{fieldCells[(oldY + oldCell.y) * NUM_COLS +
oldX + oldCell.x].name}" );
retVal = false;
}
}
return retVal;
}
}

TetrisShapeType.fx

/*

* TetrisShapeType.fx - A Tetris shape type, which are
* I, J, L, O, S, T, and Z
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a compiled JavaFX Script example.
*
*/
package tetris_model;

import javafx.ui.*;
import java.awt.Point;
import java.lang.Math;
import java.lang.System;

/**
* This class contains the model information for each type
* of tetromino (Tetris piece).
*/
public class TetrisShapeType {
public attribute id: Integer;
public attribute name: String;

/**
* A sequence containing positions of each square in a
* tetromino type. The first element in the sequence is
* the one around which the tetromino will rotate.
* Note that the "O" tetromino type doesn't rotate.
*/
public attribute squarePositions:Point[];

//public attribute allowRotate:Boolean = true;

/**
* Valid values are 1 (for "O" shape, 2 (for "S", "Z", and "I" shapes)
* and 4 (for "L", "J", and "T" shapes)
*/
public attribute rotateStates:Integer = 4;

public attribute squareColor:Color;

/** The "NONE" shape (represents the absence of a shape) */
public static attribute NONE =
TetrisShapeType {
id: 0
name: "."
squarePositions: [
new Point(0, 0),
new Point(0, 0),
new Point(0, 0),
new Point(0, 0)
]
squareColor: Color.SILVER
};

/** The "I" shape (four squares in a straight line) */
public static attribute I =
TetrisShapeType {
id: 1
name: "I"
squarePositions: [
new Point(0, 0),
new Point(-1, 0),
new Point(1, 0),
new Point(2, 0)
]
squareColor: Color.SILVER
rotateStates: 2
};

/** The "T" shape (looks like a stout "T") */
public static attribute T =
TetrisShapeType {
id: 2
name: "T"
squarePositions: [
new Point(0, 0),
new Point(-1, 0),
new Point(1, 0),
new Point(0, 1)
]
squareColor: Color.HOTPINK
};

/** The "L" shape (looks like an "L") */
public static attribute L =
TetrisShapeType {
id: 3
name: "L"
squarePositions: [
new Point(0, 0),
new Point(-1, 0),
new Point(1, 0),
new Point(-1, 1)
]
squareColor: Color.LIGHTBLUE
};

/** The "J" shape (looks sort of like a "J", but
* more like a backwards "L") */
public static attribute J =
TetrisShapeType {
id: 4
name: "J"
squarePositions: [
new Point(0, 0),
new Point(-1, 0),
new Point(1, 0),
new Point(1, 1)
]
squareColor: Color.YELLOW
};

/** The "S" shape (looks sort of like an "S") */
public static attribute S =
TetrisShapeType {
id: 5
name: "S"
squarePositions: [
new Point(0, 0),
new Point(1, 0),
new Point(-1, 1),
new Point(0, 1)
]
squareColor: Color.LIGHTGREEN
rotateStates: 2
};

public static attribute Z =
TetrisShapeType {
id: 6
name: "Z"
squarePositions: [
new Point(0, 0),
new Point(-1, 0),
new Point(0, 1),
new Point(1, 1)
]
squareColor: Color.ORANGE
rotateStates: 2
};

/** The "O" shape (looks sort of like an "O", but
* more like a square) */
public static attribute O =
TetrisShapeType {
id: 7
name: "O"
squarePositions: [
new Point(0, 0),
new Point(0, 1),
new Point(1, 0),
new Point(1, 1)
]
squareColor: Color.RED
rotateStates: 1
};

/**
* A sequence of the shape types for use in generating a
* random shape type.
*/
private static attribute allShapeTypes:TetrisShapeType[];

/**
* A function that returns a random TetrisShapeType
*/
public static function randomShapeType():TetrisShapeType {
if (sizeof allShapeTypes <= 0) {
insert I into allShapeTypes;
insert T into allShapeTypes;
insert L into allShapeTypes;
insert J into allShapeTypes;
insert S into allShapeTypes;
insert Z into allShapeTypes;
insert O into allShapeTypes;
}
allShapeTypes[(Math.random() * sizeof allShapeTypes) as Integer]
}

/**
* A function that returns the squarePositions of a given TetrisShapeType
* at a given angle of rotation. This could have used trig functions,
* but the cases are simple enough to use if/else expressions.
*/
public static function
squarePositionsForRotatedShape(shapeType:TetrisShapeType, rotAngle:Integer):Point[] {
for (position in shapeType.squarePositions) {
var newX = position.x;
var newY = position.y;
if (rotAngle == 90) {
newX = if (position.y == 0) 0 else position.y * -1;
newY = if (position.x == 0) 0 else position.x;
}
else if (rotAngle == 180) {
newX = position.x * -1;
newY = position.y * -1;
}
else if (rotAngle == 270) {
newX = if (position.y == 0) 0 else position.y;
newY = if (position.x == 0) 0 else position.x * -1;
}
new Point(newX, newY);
}
}
}

You may have noticed that factored out the TetrisShape.fx class by the way. There are many improvements to be made, but we'll get there together! As always, please let me know if you have questions.

0 comments:

Post a Comment