Compiled JavaFX Script

May 01, 2008

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

170x93_speaker_v4_4 If you have any questions or input for improvements, please post a comment.  Also, if you'll be at JavaOne 2008, please attend my JavaFX Script Programming Language Tutorial session and introduce yourself afterward!

 

Have fun!
Jim Weaver
JavaFX Script: Dynamic Java Scripting for Rich Internet/Client-side Applications

Immediate eBook (PDF) download available at the book's Apress site

April 29, 2008

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

 

170x93_speaker_v4_4 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

April 25, 2008

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();
                }
            }
          ]
        }
    }
}

170x93_speaker_v4_4 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

April 16, 2008

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

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

April 10, 2008

Compiled JavaFX Script Tutorial - Lesson #2 is Ready

A few weeks ago, the first lesson in a compiled JavaFX Script tutorial series was made available.  Today, the second lesson is ready for you to take, and is currently available on the Java home page (java.sun.com).

Javasuncom_compiledjavafxscripttuto

Have fun!
Jim Weaver
JavaFX Script: Dynamic Java Scripting for Rich Internet/Client-side Applications

Immediate eBook (PDF) download available at the book's Apress site

April 04, 2008

Powerful good time talking 'bout JavaFX at the Java Ranch

Just wanted to give a hardy "thanks y'all" to the friendly folks at JavaRanch for hosting me in one of their weekly book giveaways. 

Familyportrait

The JavaRanch family and community had lots of good questions, to which I tried to supply good answers, on the HTML and JavaScript forum in the Big Moose Salon.Moosesaloonanimated1

Also, sheriff (and gunslinger) Gregg Bolinger had a JavaFX vs Plain Old Java shootout with me (actually with a code example on one of my blog posts).

Thanks again, JavaRanch!
Jim Weaver
JavaFX Script: Dynamic Java Scripting for Rich Internet/Client-side Applications

Immediate eBook (PDF) download available at the book's Apress site

April 01, 2008

Video from the JavaPolis "Intro to JavaFX" Presentation

In a previous post, I provided a link to the slides from the JavaPolis 2007 "Intro to JavaFX" Presentation.  The video of that presentation is now available at this link on the Parleys classic website, or at this link on the Parleys RIA beta website.
Javapolis_javafx_header_2

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

March 27, 2008

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.

Enjoy!
Jim Weaver
JavaFX Script: Dynamic Java Scripting for Rich Internet/Client-side Applications

Immediate eBook (PDF) download available at the book's Apress site

March 25, 2008

My New Favorite IDE for Compiled JavaFX Script Development

As you may know, in order to develop compiled JavaFX Script applications, it has been necessary to compile and run from the command line.  That is no longer true, as the compiled JavaFX Script plug-in is now available for NetBeans (6.1 beta).  Here's a screenshot of the program example (a Yahtzee dice roller and scorer) for my next blog post being developed in my new favorite IDE :-)

Netbeans_wjavafxplugin_2

The compiled JavaFX Script plug-in is built in the same continuous build that compiled JavaFX Script is, so updates will be available frequently.  To get both NetBeans 6.1 beta and the compiled JavaFX Script plug-in, visit the OpenJFX Community site and check for the JavaFX Script Plugin Daily Builds Available news item.

Enjoy!
Jim Weaver
JavaFX Script: Dynamic Java Scripting for Rich Internet/Client-side Applications

Immediate eBook (PDF) download available at the book's Apress site

March 22, 2008

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[];
}

Good luck!
Jim Weaver
JavaFX Script: Dynamic Java Scripting for Rich Internet/Client-side Applications

Immediate eBook (PDF) download available at the book's Apress site