In the Knowing the State of a JavaFX Script Animation post I showed you the more succinct syntax for controlling animations. Now I'd like to show you some improvements to the TetrisJFX program that I've been developing in this blog, including switching to the new animation syntax. Here is a screenshot of the TetrisJFX game after the blocks stacked up to the top and the game is over.
By the way, Bruno Ghisi (a fellow Java Champion) and Lucas Torri have developed a Java Bluetooth Framework called Marge. Using that framework they developed a mobile phone game controller for this TetrisJFX program. You can see a video of this in action in one of Bruno's blog posts.
As you can see, I've added some functionality since the previous post, but here are some highlights:
- A preview area in the upper right corner that shows the next tetromino shape that will drop.
- Scoring is more Tetris-like, in that each tetromino is worth 25 points when it is finished dropping, and each row that fills up is worth 100 points. Full rows are removed from the playing field and the tetrominoes above are moved down.
- The button on the left toggles between Play and Stop. If you choose to Stop the game, "Game Over" will appear in the background, followed by which pressing Play will start a new game.
- When the tetrominoes stack up to the top, as shown in the sceenshot above, "Game Over" appears in the background and the game stops.
Here is the code for this version of TetrisJFX. Note that I've added a new class named TetrisNextShapeNode that shows the next tetromino shape to drop.
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 model = TetrisModel {}
var canvas:Canvas
width: 350
height: 500
title: "TetrisJFX"
background: Color.LIGHTGREY
content:
StackPanel {
content: [
Canvas {
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/background.jpg"
}
},
Text {
transform: bind [
Translate.translate(30, 350),
Rotate.rotate(290, 0, 0),
]
content: "Game Over"
fill: Color.WHITE
opacity: bind if (model.timeline.running) 0
else 1
font:
Font {
face: FontFace.SANSSERIF
size: 60
style: FontStyle.BOLD
}
}
]
},
BorderPanel {
center:
FlowPanel {
alignment: Alignment.LEADING
vgap: 10
hgap: 10
content: [
Canvas {
content:
TetrisPlayingField {
model: model
}
}
]
}
right:
GridPanel {
var sansSerif24 =
Font {
face: FontFace.SANSSERIF
size: 24
style: FontStyle.BOLD
}
rows: 2
columns: 1
cells: [
BorderPanel {
top:
FlowPanel {
vgap: 10
alignment: Alignment.LEADING
content:
Label {
foreground: Color.LIGHTBLUE
text: "NEXT:"
font: sansSerif24
}
}
center:
Canvas {
content:
TetrisNextShapeNode {
model: model
}
}
},
BorderPanel {
top:
Label {
foreground: Color.LIGHTBLUE
text: "SCORE:"
font: sansSerif24
}
center:
FlowPanel {
alignment: Alignment.TRAILING
content:
Label {
foreground: Color.LIGHTBLUE
text: bind "{model.score}"
font: sansSerif24
}
}
}
]
}
bottom:
FlowPanel {
alignment: Alignment.CENTER
content: [
Button {
text: bind
if (model.timeline.running)
"Stop"
else
"Play"
action:
function():Void {
if (model.timeline.running) {
model.stopGame();
}
else {
model.startGame();
}
}
},
Button {
text: "Rotate"
enabled: bind not model.stopDropping
action:
function():Void {
model.rotate90();
}
},
Button {
text: "Left"
enabled: bind not model.stopDropping
action:
function():Void {
model.moveLeft();
}
},
Button {
text: "Right"
enabled: bind not model.stopDropping
action:
function():Void {
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;
private attribute numHiddenTopRows = 2;
public function composeNode():Node {
Group {
content: bind [
Rect {
x: 0
y: 0
width: model.NUM_COLS * model.SQUARE_SIZE
height: (model.NUM_ROWS - numHiddenTopRows) * model.SQUARE_SIZE
strokeWidth: 1
stroke: Color.BLACK
fill: Color.BLUE
opacity: bind if (model.timeline.running) .5
else .2
},
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
x: indexof cell % model.NUM_COLS * model.SQUARE_SIZE
y: (yPos - numHiddenTopRows) * model.SQUARE_SIZE
width: model.SQUARE_SIZE
height: model.SQUARE_SIZE
fill: cell.squareColor
stroke: squareOutlineColor
strokeWidth: squareOutlineWidth
}
}
]
}
}
}
TetrisNextShapeNode.fx:
/*
* TetrisNextShapeNode.fx -
* A custom graphical component that shows the next shape to fall
*
* 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 TetrisNextShapeNode 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.PREVIEW_NUM_COLS * model.SQUARE_SIZE
height: model.PREVIEW_NUM_ROWS * model.SQUARE_SIZE
strokeWidth: 1
stroke: Color.BLACK
fill: Color.BLUE
opacity: bind if (model.timeline.running) .7
else .2
},
for (cell in model.nextShapePreviewCells
where cell <> TetrisShapeType.NONE) {
Rect {
var yPos:Integer = indexof cell / model.PREVIEW_NUM_COLS
x: indexof cell % model.PREVIEW_NUM_COLS * 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.animation.*;
import javafx.lang.*;
import java.lang.System;
public class TetrisModel {
/** Size of each tetromino in pixels */
public static attribute SQUARE_SIZE = 19;
/** Number of rows in the playing field */
public static attribute NUM_ROWS = 23;
/** Number of columns in the playing field */
public static attribute NUM_COLS = 10;
/** Number of rows in the next shape viewing area */
public static attribute PREVIEW_NUM_ROWS = 4;
/** Number of columns in the next shape viewing area */
public static attribute PREVIEW_NUM_COLS = 6;
public attribute stopDropping:Boolean;
/**
* Sequence of objects that represent the
* type of shapes (including no shape) in each playing field cell
*/
public attribute fieldCells:TetrisShapeType[];
/**
* Sequence of objects that represent the
* type of shapes (including no shape) in each next shape preview cell
*/
public attribute nextShapePreviewCells:TetrisShapeType[];
/**
* The active tetromino shape type.
*/
public attribute activeShapeType:TetrisShapeType;
public attribute nextShapeType:TetrisShapeType;
public attribute score:Integer = 0;
private attribute pieceScore = 25;
private attribute rowScore = 100;
private attribute startDropRow = 2;
private attribute stopDropRow = NUM_ROWS;
private attribute pieceAppearRow = 3;
private attribute fieldFullRow = 5;
/**
* 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 == pieceAppearRow) {
activeShapeType = nextShapeType;
// Remove the current "next shape type" from the preview area
updateNextShapePreviewCells(true);
nextShapeType = TetrisShapeType.randomShapeType();
// Load the new "next shape type" into the preview area
updateNextShapePreviewCells(false);
tetrominoHorzPos = NUM_COLS / 2 - 1 as Integer;
stopDropping = false;
}
if (not stopDropping) {
// 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 {
score += pieceScore;
updateFieldCells(tetrominoHorzPos, oldVal, tetrominoAngle, false);
stopDropping = true;
if (a <= fieldFullRow) {
// The blocks have stacked up too high, and game is over
timeline.stop();
}
}
removeFilledRows();
}
};
public attribute timeline =
Timeline {
keyFrames: [
KeyFrame {
time: 0s
values: a => startDropRow tween Interpolator.LINEAR
},
KeyFrame {
time: 10s
values: a => stopDropRow tween Interpolator.LINEAR
}
]
repeatCount: Timeline.INDEFINITE
};
public attribute tetrominoAngle:Number;
public attribute tetrominoHorzPos:Number;
public function startGame():Void {
fieldCells = for (i in [1..(NUM_ROWS * NUM_COLS)]) {
TetrisShapeType.NONE
};
nextShapePreviewCells = for (i in [1..(PREVIEW_NUM_ROWS * PREVIEW_NUM_COLS)]) {
TetrisShapeType.NONE
};
nextShapeType = TetrisShapeType.randomShapeType();
activeShapeType = null;
score = 0;
timeline.start();
}
public function stopGame():Void {
timeline.stop();
stopDropping = true;
}
public function rotate90():Void {
if (stopDropping) {return;} // Don't try to process if a piece isn't falling
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, true);
for (oldCell in TetrisShapeType.squarePositionsForRotatedShape(activeShapeType,
computeNewAngle(tetrominoAngle, 90))) {
// First check to see if the rotated tetromino is past the
// left side of the playing field
if (tetrominoHorzPos + oldCell.x < 0) {
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
return;
}
// Then check to see if the tetromino is at the right of
// the playing field
if (tetrominoHorzPos + oldCell.x > NUM_COLS - 1) {
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
return;
}
// Now check to see if another tetromino is preventing it from rotating
if (fieldCells[((a + oldCell.y) * NUM_COLS + tetrominoHorzPos - 1 + oldCell.x) as Integer] <>
TetrisShapeType.NONE) {
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
return;
}
}
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 (stopDropping) {return;} // Don't try to process if a piece isn't falling
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, true);
for (oldCell in TetrisShapeType.squarePositionsForRotatedShape(activeShapeType,
tetrominoAngle)) {
// First check to see if the tetromino is at the left of
// the playing field
if (tetrominoHorzPos + oldCell.x <= 0) {
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
return;
}
// Now check to see if another tetromino is preventing it from moving left
if (fieldCells[((a + oldCell.y) * NUM_COLS + tetrominoHorzPos - 1 + oldCell.x) as Integer] <>
TetrisShapeType.NONE) {
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
return;
}
}
tetrominoHorzPos--;
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
}
// TODO: Refactor with moveLeft method
public function moveRight():Void {
if (stopDropping) {return;} // Don't try to process if a piece isn't falling
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, true);
for (oldCell in TetrisShapeType.squarePositionsForRotatedShape(activeShapeType,
tetrominoAngle)) {
// First check to see if the tetromino is at the left of
// the playing field
if (tetrominoHorzPos + oldCell.x >= NUM_COLS - 1) {
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
return;
}
// Now check to see if another tetromino is preventing it from moving left
if (fieldCells[((a + oldCell.y) * NUM_COLS + tetrominoHorzPos + 1 + oldCell.x) as Integer] <>
TetrisShapeType.NONE) {
updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
return;
}
}
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 {
// Place (or remove) the shape 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;
}
}
/**
* Keeps the nextShapePreviewCells sequence (that represents the
* cells in the next shape preview area) updated to reflect reality.
*/
public function updateNextShapePreviewCells(remove:Boolean):Void {
// Place (or remove) the shape on the next shape preview field
var pivotPosX = 2;
var pivotPosY = 1;
for (oldCell in TetrisShapeType.squarePositionsForRotatedShape(nextShapeType, 0)) {
nextShapePreviewCells[(pivotPosY + oldCell.y) * PREVIEW_NUM_COLS + pivotPosX + oldCell.x] =
if (remove) TetrisShapeType.NONE else nextShapeType;
}
}
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_ROWS) {
return false;
}
// Now check to see if another tetromino is preventing it from moving down
if (fieldCells[(oldY + oldCell.y) * NUM_COLS + oldX + oldCell.x] <>
TetrisShapeType.NONE) {
return false;
}
}
return true;
}
private function removeFilledRows():Void {
if (not stopDropping) {
return;
}
for (row in [0..NUM_ROWS - 1]) {
// First see if the beginning of the row is empty
// before doing the more expensive check of the whole row
//TODO: Consider not doing this, as perhaps the indexOf isn't expensive
if (fieldCells[row * NUM_COLS] <> TetrisShapeType.NONE) {
var fieldRowSlice = fieldCells[row * NUM_COLS .. (row + 1) * NUM_COLS -1];
if (Sequences.indexOf(fieldRowSlice, TetrisShapeType.NONE) < 0) {
score += rowScore;
stopDropping = true;
//TODO: Use fieldRowSlice in delete statement
delete fieldCells[row * NUM_COLS .. (row + 1) * NUM_COLS -1];
insert
for (i in [1..NUM_COLS]) {
TetrisShapeType.NONE
}
before fieldCells[0];
}
}
}
}
}
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 some TODO comments in the code, which will remind me to do some refactoring and add some enhacements (like create a button that will accelerate a tetromino's rate of decent).
If you have any question, please post a comment. Also, if you'll be at JavaOne 2008, please attend my JavaFX Script Progamming Language Tutorial session and introduce yourself afterward!
Regards,
Jim Weaver
JavaFX Script: Dynamic Java Scripting for Rich Internet/Client-side Applications
Immediate eBook (PDF) download available at the book's Apress site
I found a compile error for the codes,
it seems that model.timeline.running is not defined.
Any help
Posted by: Rosso | May 30, 2008 at 03:40 AM