Watch for Falling Blocks: Take TetrisJFX for a Spin!

You may know that I've been progressively building a Tetris game in JavaFX on this blog, the most recent post being Game Over: Improving upon the Compiled JavaFX Tetris Program. In each post I've shown you the code and pointed out some highlights. Since then I've added some finishing touches, and would be honored if you've try it out and give me (kind) feedback for improving it further. The screenshot below shows what TetrisJFX should look like when you click this Java Web Start link. By the way, you'll need the JRE (Java Runtime Environment) 1.5 or later. Also, please keep in mind that the JavaFX Script JAR files will be included with the JRE at some point. Until then please understand that when you click this link those JAR files will be downloaded as well, causing a bit of a delay.

Tetrisjfx_w_image_buttons

In addition to clicking the image buttons at the bottom of the game, you can use keystrokes (hover over the images to see tooltips that tell you what the keystrokes are). In a future version, the arrow keys will be used for game control. I also plan to provide the ability to cause the current tetromino to fall faster. By the way, the tetrominoes fall progressively faster as your score increases, so be warned. :-)

Game Over: Improving upon the Compiled JavaFX Tetris Program

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.

Tetrisjfx_4_gameover

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).

Knowing the State of a JavaFX Script Animation

In the Reading 'tween the Lines - Simplified JavaFX Script Animation Syntax post, I showed you how to start, stop, pause and resume an animation. In this post I'm going to show you how to read the state of the animation (i.e. whether is it running, and whether it is paused). To demonstrate this, I modified the metronome-like example from the previous post. Here's a screenshot of today's example when it first starts up:

Metronome_stopped

Notice that only the Start button is enabled. As shown in the screenshot below, when you click the Start button the animation starts, and the enabled state of some of the buttons change:

Metronome_running

When you click the Pause button, it becomes enabled and the Resume button is disabled:

Metronome_paused

Clicking the Stop button causes the animation to stop and for the buttons to have the same states shown in the first screenshot. In the code for this example, notice that the buttons' enabled attributes are bound to the running and paused attributes of the Timeline instance:

/*
* Metronome.fx
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a compiled JavaFX Script example.
*/

import javafx.ui.*;
import javafx.ui.canvas.*;
import javafx.animation.*;

class MetronomeModel {
public attribute x2Val = 100;
public attribute anim =
Timeline {
autoReverse: true
keyFrames: [
KeyFrame {
time: 0s
values: x2Val => 100
},
KeyFrame {
time: 1s
values: x2Val => 300 tween Interpolator.LINEAR
}
]
repeatCount: Timeline.INDEFINITE
};
}

Frame {
var metroModel =
MetronomeModel {}
title: "Animation Example"
width: 400
height: 500
visible: true
content:
BorderPanel {
center:
Canvas {
content:
Line {
x1: 200
y1: 400
x2: bind metroModel.x2Val
y2: 100
strokeWidth: 5
stroke: Color.RED
}
}
bottom:
FlowPanel {
content: [
Button {
text: "Start"
enabled: bind not metroModel.anim.running
action:
function():Void {
metroModel.anim.start();
}
},
Button {
text: "Pause"
enabled: bind not metroModel.anim.paused and
metroModel.anim.running
action:
function():Void {
metroModel.anim.pause();
}
},
Button {
text: "Resume"
enabled: bind metroModel.anim.paused
action:
function():Void {
metroModel.anim.resume();
}
},
Button {
text: "Stop"
enabled: bind metroModel.anim.running
action:
function():Void {
metroModel.anim.stop();
}
}
]
}
}
}

Reading 'tween the Lines - Simplified JavaFX Script Animation Syntax

As mentioned in the first Tetris post, the JavaFX Script animation syntax is undergoing simplification. Today, I'd like to show you a very basic example of this simplified syntax. As shown in the screenshot below, this example consists of a line that moves back and forth like a metronome:

Metronome

When you click the Start button, the top of the line will move from one side to the other in one second, and then travel back to its starting place in one second, repeating this indefinitely. Clicking the Pause button causes the animation to pause, and clicking Resume causes a paused animation to resume. Clicking the Stop button stops the animation, requiring it to be started again. Take a look at the code for this example to see the animation-related syntax and functions:

/*
* AnimationExample.fx
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a compiled JavaFX Script example.
*/

import javafx.ui.*;
import javafx.ui.canvas.*;
import javafx.animation.*;

class MetronomeModel {
public attribute x2Val = 100;
public attribute anim =
Timeline {
autoReverse: true
keyFrames: [
KeyFrame {
time: 0s
values: x2Val => 100
},
KeyFrame {
time: 1s
values: x2Val => 300 tween Interpolator.LINEAR
}
]
repeatCount: Timeline.INDEFINITE
};
}

Frame {
var metroModel =
MetronomeModel {}
title: "Animation Example"
width: 400
height: 500
visible: true
content:
BorderPanel {
center:
Canvas {
content:
Line {
x1: 200
y1: 400
x2: bind metroModel.x2Val
y2: 100
strokeWidth: 5
stroke: Color.BLUE
}
}
bottom:
FlowPanel {
content: [
Button {
text: "Start"
action:
function():Void {
metroModel.anim.start();
}
},
Button {
text: "Pause"
action:
function():Void {
metroModel.anim.pause();
}
},
Button {
text: "Resume"
action:
function():Void {
metroModel.anim.resume();
}
},
Button {
text: "Stop"
action:
function():Void {
metroModel.anim.stop();
}
}
]
}
}
}


Animation related concepts in this example

The Timeline class allows you to articulate the "key frames" that will be in the animation. You can have as many as you need, but in this simple case we have two:

  • One KeyFrame that occurs at the beginning of the animation. Note the use of the new literal syntax for durations, in this case 0s, which means 0 seconds. 100ms would be 100 milliseconds, and 3m would represent 3 minutes.
  • One KeyFrame that occurs 1 second after the animation starts.

The autoReverse attribute allows you to specify that the animation should run in reverse when it reaches the last KeyFrame. The repeatCount attribute allows you to control how many times the animation will run, in this case, indefinitely.

The values attribute of the KeyFrame uses the new, more concise, animation syntax. The values attribute in the first KeyFrame sets the inital value of the x2Val variable to 100. The values attribute in the second KeyFrame causes the x2Val variable to change in value between its previous value to 300, 1 second after the animation started. Because of the Interpolator.LINEAR constant, this change of value will be linear (as opposed to, for example, slowing down at the end, which is what Interpolator.EASEOUT would do). The x2 attribute of the Line is bound to this changing x2Val variable, which is what causes the line to move on the screen.

By the way, I'm furiously preparing JavaFX presentations and demos (as are lots of other folks) for JavaOne, so please excuse the lapse of a few days here and there between posts. I'll be blogging daily from JavaOne, as there will be lots to tell you ;-)

Create a Yahtzee Dice Roller and Scorer in Compiled JavaFX Script

I'm still at The Server Side Java Symposium in Las Vegas, and I think that walking by all these slot machines has inspired this blog post. Today's example builds on the program in the Roll the Dice post in order to create a Yahtzee dice roller and scorer. Each time you click the Roll button, the five dice assume random values, and the combination of values is scored according to the possible categories in the bottom portion of a Yahtzee scoring sheet. Here's a screenshot:

Yahtzee_2

In addition to the Dice.fx and PipPlacement.fx files from the Roll the Dice post, this program consists of the following source code files.

YahtzeeMain.fx

/*
* YahtzeeMain.fx -
* A compiled JavaFX program that demonstrates creating custom
* components with CompositeNode and evaluates Yahtzee dice rolls.
*
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a JavaFX Script example.
*/

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

Frame {
var model = YahtzeeModel {}
width: 510
height: 400
title: "Roll Dice and Evaluate Yahtzee Combinations"
background: Color.WHITE
content:
BorderPanel {
center:
Box {
var evalFont =
Font {
size: 20
}
orientation: Orientation.VERTICAL
content: [
Canvas {
content:
for (diceNum in [0 .. model.numDice - 1]) {
model.newDice =
Dice {
x: diceNum * 100 + 10
y: 10
width: 80
height: 80
faceColor: Color.RED
pipColor: Color.WHITE
}
}
},
GroupPanel {
var fiveOfKindRow = Row { alignment: Alignment.BASELINE }
var largeStraightRow = Row { alignment: Alignment.BASELINE }
var smallStraightRow = Row { alignment: Alignment.BASELINE }
var fullHouseRow = Row { alignment: Alignment.BASELINE }
var fourOfKindRow = Row { alignment: Alignment.BASELINE }
var threeOfKindRow = Row { alignment: Alignment.BASELINE }
var chanceRow = Row { alignment: Alignment.BASELINE }

var labelsColumn = Column {
alignment: Alignment.TRAILING
}
var fieldsColumn = Column {
alignment: Alignment.LEADING
}
rows: [
fiveOfKindRow,
largeStraightRow,
smallStraightRow,
fullHouseRow,
fourOfKindRow,
threeOfKindRow,
chanceRow
]
columns: [
labelsColumn,
fieldsColumn
]
content: [
SimpleLabel {
font: evalFont
row: fiveOfKindRow
column: labelsColumn
text: "Five of a Kind (Yahtzee):"
},
SimpleLabel {
font: evalFont
row: fiveOfKindRow
column: fieldsColumn
text: bind
if (model.fiveOfKind)
"{model.fiveOfKindScore}"
else "N/A"
},

SimpleLabel {
font: evalFont
row: largeStraightRow
column: labelsColumn
text: "Large Straight:"
},
SimpleLabel {
font: evalFont
row: largeStraightRow
column: fieldsColumn
text: bind
if (model.largeStraight)
"{model.largeStraightScore}"
else "N/A"
},

SimpleLabel {
font: evalFont
row: smallStraightRow
column: labelsColumn
text: "Small Straight:"
},
SimpleLabel {
font: evalFont
row: smallStraightRow
column: fieldsColumn
text: bind
if (model.smallStraight)
"{model.smallStraightScore}"
else "N/A"
},

SimpleLabel {
font: evalFont
row: fullHouseRow
column: labelsColumn
text: "Full House:"
},
SimpleLabel {
font: evalFont
row: fullHouseRow
column: fieldsColumn
text: bind
if (model.fullHouse)
"{model.fullHouseScore}"
else "N/A"
},

SimpleLabel {
font: evalFont
row: fourOfKindRow
column: labelsColumn
text: "Four of a Kind:"
},
SimpleLabel {
font: evalFont
row: fourOfKindRow
column: fieldsColumn
text: bind
if (model.fourOfKind)
"{model.sumOfDiceValues}"
else "N/A"
},

SimpleLabel {
font: evalFont
row: threeOfKindRow
column: labelsColumn
text: "Three of a Kind:"
},
SimpleLabel {
font: evalFont
row: threeOfKindRow
column: fieldsColumn
text: bind
if (model.threeOfKind)
"{model.sumOfDiceValues}"
else "N/A"
},

SimpleLabel {
font: evalFont
row: chanceRow
column: labelsColumn
text: "Chance:"
},
SimpleLabel {
font: evalFont
row: chanceRow
column: fieldsColumn
text: bind "{model.sumOfDiceValues}"
},
]
},
]
}
bottom:
FlowPanel {
content:
Button {
text: "Roll"
defaultButton: true
action:
function():Void {
model.roll();
}
}
}
}
visible: true
onClose:
function():Void {
System.exit(0);
}
}

YahtzeeModel.fx

/*
* YahtzeeModel.fx -
* The model behind the Yahtzee dice roll and combination evaluation
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a JavaFX Script example.
*/
import javafx.lang.Sequences;
import java.lang.System;

class YahtzeeModel {
attribute numDice:Integer = 5;
attribute diceDistribution:Integer[];
attribute newDice:Dice on replace {
insert newDice into dice;
}
attribute dice:Dice[];

attribute fiveOfKind:Boolean;
attribute largeStraight:Boolean;
attribute smallStraight:Boolean;
attribute fullHouse:Boolean;
attribute fourOfKind:Boolean;
attribute threeOfKind:Boolean;

attribute fiveOfKindScore:Integer = 50;
attribute largeStraightScore:Integer = 40;
attribute smallStraightScore:Integer = 30;
attribute fullHouseScore:Integer = 25;

attribute sumOfDiceValues:Integer = 0;

function roll():Void {
for (die in dice) {
die.roll();
}
evalYahtzeeCombos();
}

function evalYahtzeeCombos() {
var values =
for (val in dice) {
val.value;
}

var maxVal:Integer = Sequences.max(values, null) as Integer;
var minVal:Integer = Sequences.min(values, null) as Integer;

// Create a sequence that contains the distribution of values
// and Calclulate the sum of the dice values
diceDistribution =
for (i in [1 .. 6]) 0;

sumOfDiceValues = 0;

for (val in values) {
diceDistribution[val - 1]++;
sumOfDiceValues += val;
}

// Determine if five-of-a-kind
fiveOfKind =
((for (occurance in diceDistribution
where occurance >= 5) occurance) <> []);

// Determine if four-of-a-kind
fourOfKind =
((for (occurance in diceDistribution
where occurance >= 4) occurance) <> []);

// Determine if three-of-a-kind
threeOfKind =
sizeof (for (occurance in diceDistribution
where occurance >= 3) occurance) > 0;

// Determine if full house
fullHouse =
sizeof (for (occurance in diceDistribution
where occurance == 3) occurance) > 0 and
sizeof (for (occurance in diceDistribution
where occurance == 2) occurance) > 0;

// Determine if large straight
largeStraight =
sizeof (for (occurance in diceDistribution
where occurance > 1) occurance) == 0 and
(maxVal - minVal == 4);

// Determine if small straight
smallStraight =
sizeof (for (occurance in diceDistribution
where occurance == 2) occurance) == 1 and
(maxVal - minVal == 3);
}
}

A JavaFX Script concept that we haven't covered yet is the set of functions available in the new javafx.lang.Sequences package. These functions enable you to perform operations (such as sorting) on a sequence. In this program I'm using the max and min functions of that class to find the largest and smallest value in a dice roll.

Roll the Dice - A Compiled JavaFX Script Example

I'm on my way to Vegas to speak at The Server Side Java Symposium, so in keeping with that theme I wanted to create a simple dice program as today's example. When you click the Roll button, random values appear on the dice. Here's a screenshot of this application:

Dice

GameMain.fx

/*
* GameMain.fx -
* A compiled JavaFX program that demonstrates creating custom
* components with CompositeNode
*
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a JavaFX Script example.
*/

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

Frame {
var model = GameModel {}
width: 210
height: 170
title: "Dice"
background: Color.WHITE
content:
BorderPanel {
center:
Canvas {
content:
for (diceNum in [0 .. model.numDice - 1]) {
model.newDice =
Dice {
x: diceNum * 100 + 10
y: 10
width: 80
height: 80
faceColor: Color.RED
pipColor: Color.WHITE
}
}
}
bottom:
FlowPanel {
content:
Button {
text: "Roll"
defaultButton: true
action:
function():Void {
model.roll();
}
}
}
}
visible: true
onClose:
function():Void {
System.exit(0);
}
}

Dice.fx

/*
* Dice.fx -
* A compiled JavaFX program that demonstrates creating custom
* components with CompositeNode
*
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a JavaFX Script example.
*/

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

public class Dice extends CompositeNode {
public attribute value:Integer = 5;
public attribute x:Integer;
public attribute y:Integer;
public attribute width:Integer;
public attribute height:Integer;
public attribute faceColor:Color;
public attribute pipColor:Color;

attribute pipPlace:PipPlacement[];

postinit {
insert
PipPlacement {
pipLocsX: [
.5, 0, 0, 0, 0, 0
]
pipLocsY: [
.5, 0, 0, 0, 0, 0
]
}
into pipPlace;
insert
PipPlacement {
pipLocsX: [
0.3, 0.7, 0, 0, 0, 0
]
pipLocsY: [
0.7, 0.3, 0, 0, 0, 0
]
}
into pipPlace;
insert
PipPlacement {
pipLocsX: [
0.2, 0.5, 0.8, 0, 0, 0
]
pipLocsY: [
0.8, 0.5, 0.2, 0, 0, 0
]
}
into pipPlace;
insert
PipPlacement {
pipLocsX: [
0.25, 0.25, 0.75, 0.75, 0, 0
]
pipLocsY: [
0.25, 0.75, 0.25, 0.75, 0, 0
]
}
into pipPlace;
insert
PipPlacement {
pipLocsX: [
0.2, 0.5, 0.8, 0.8, 0.2, 0
]
pipLocsY: [
0.8, 0.5, 0.2, 0.8, 0.2, 0
]
}
into pipPlace;
insert
PipPlacement {
pipLocsX: [
0.3, 0.3, 0.3, 0.7, 0.7, 0.7
]
pipLocsY: [
0.8, 0.5, 0.2, 0.8, 0.5, 0.2
]
}
into pipPlace;
}

public function roll():Void {
value = (Math.random() * 6 + 1) as Integer;
}

public function composeNode():Node {
Group {
transform: bind [
Translate.translate(x, y)
]
content: bind [
Rect {
x: 0
y: 0
width: this.width;
height: this.height;
fill: faceColor
},
for (pipIdx in [0 .. value-1]) {
Circle {
cx: bind pipPlace[value - 1].pipLocsX[pipIdx] * width
cy: bind pipPlace[value - 1].pipLocsY[pipIdx] * height
radius: width * .1
fill: pipColor
}
}
]
}
}
}

GameModel.fx

/*
* GameModel.fx -
* The model behind the dice game
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a JavaFX Script example.
*/

class GameModel {
attribute numDice:Integer = 2;
attribute newDice:Dice on replace {
insert newDice into dice;
}
attribute dice:Dice[];

function roll():Void {
for (die in dice) {
die.roll();
}
}
}


PipPlacement.fx

/*
* PipPlacement.fx -
* The placement of the pips on a dice
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a JavaFX Script example.
*/

public class PipPlacement {
public attribute pipLocsX:Number[];
public attribute pipLocsY:Number[];
}

0 comments:

Post a Comment