Welcome to a Limelight production. I am going to go through a step by step introduction to limelight development using a tic tac toe game as an example.
So, lets get started. I am going to create the directory structure and open it up in a text editor:
1 mkdir tictactoe
2 cd tictactoe
3 mate .
Now I need to set up Limelight. You can just download the gem:
1 jruby -S gem install limelight
We can start by creating the props.rb file in the tictactoe
directory. The props.rb file defines the structure of your
application. A prop is named after the theater metaphor. We are going to use
them to define what our scene’s physical structure look like. We can start
with a simple screen with an empty board with the nine cells we need for a
tic tac toe game. Lets create a spec directory to write a test for the props
we are going to create:
1 mkdir spec
2 mkdir spec/props
Now for the spec. In the spec directory, we can name our spec
props_spec.rb. We want to check that there is a cell on the
scene. To be able to run the test, you will need the spec_helper.rb
in your spec directory (not the props directory). You can copy it
from the sample
application. Here is the first test…
1 require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2 describe "Props" do
3 include PropSpecHelper
4 before(:each) do
5 setup_prop_test
6 end
7 it "should have cell_0_0" do
8 @scene.find("cell_0_0").should_not be(nil)
9 end
10 end
…and when we run it (You can copy the Rakefile from the
sample application as well, if you want to have a specs task)…
1 jruby -S rake spec
…we get the failure:
1 1)
2 Errno::ENOENT in 'Props should have cell_0_0'
3 No such file or directory - File not found - /Users/paulwpagel/Desktop/tictactoe/props.rb
4 /Users/paulwpagel/Desktop/tictactoe/spec/spec_helper.rb:18:in `initialize'
5 /Users/paulwpagel/Desktop/tictactoe/spec/spec_helper.rb:18:in `setup_prop_test'
6 /Users/paulwpagel/Desktop/tictactoe/./spec/props/props_spec.rb:6:
7
8 Finished in 0.063 seconds
9
10 1 example, 1 failure
So, lets create the props.rb file in the project root. Now we
should get the error.
1 1)
2 'Props should have cell_0_0' FAILED
3 expected not nil, got nil
4 /Users/paulwpagel/Projects/tictac/./spec/props/prop_spec.rb:12:
5
6 Finished in 0.089 seconds
7
8 1 example, 1 failure
Each of the props accepts a block of code your can give options/structure to.
We can open the props.rb file and add a cell with the id of
cell_0_0 to make this test pass.
1 main do
2 board do
3 cell :id => "cell_0_0"
4 end
5 end
And the test passes. Lets make sure we have the rest of the id's while we are at it. Here is a more exhaustive spec:
1 it "should have cells" do
2 @scene.find("cell_0_0").should_not be(nil)
3 @scene.find("cell_0_1").should_not be(nil)
4 @scene.find("cell_0_2").should_not be(nil)
5 @scene.find("cell_1_0").should_not be(nil)
6 @scene.find("cell_1_1").should_not be(nil)
7 @scene.find("cell_1_2").should_not be(nil)
8 @scene.find("cell_2_0").should_not be(nil)
9 @scene.find("cell_2_1").should_not be(nil)
10 @scene.find("cell_2_2").should_not be(nil)
11 end
And it fails in a similar manner. Lets expand our props.rb file
to make the test pass:
1 main do
2 board do
3 cell :id => "cell_0_0"
4 cell :id => "cell_0_1"
5 cell :id => "cell_0_2"
6 cell :id => "cell_1_0"
7 cell :id => "cell_1_1"
8 cell :id => "cell_1_2"
9 cell :id => "cell_2_0"
10 cell :id => "cell_2_1"
11 cell :id => "cell_2_2"
12 end
13 end
And it passes. However, it is all Ruby code, so I can leverage Ruby functions to help me out. Lets remove the duplication:
1 main do
2 board do
3 3.times do |row|
4 3.times do |col|
5 cell :id => "cell_#{row}_#{col}"
6 end
7 end
8 end
9 end
Much better. Lets now move on to the styles. Nothing will show up without a
few styles. I create a styles.rb file in the project root and
filled it with some simple content.
In Limelight, styles refer to how a prop is aesthetically displayed on the screen. Here is an example which defines the size and gives a border to the board and the cells:
1 board {
2 width 152
3 height 152
4 border_width 1
5 border_color "black"
6 }
7 cell {
8 width 50
9 height 50
10 border_width 1
11 border_color "black"
12 }
We should be able to start up Limelight and see the board. We start Limelight like…
1 jruby -S limelight open .
…and there is your first Limelight screen. Pretty easy, and all Ruby code. Lets make it more interesting. Let us make it such that if you click on one of the squares, the square shows a mark denoting the first move.
First we create a directory called players. Inside go the
players, which contain the actions and behavior of the props for a Limelight
scene (the controllers):
1 mkdir players
We want to now make a player for the cell prop. We create a file inside of
the players directory called cell.rb. The file will start with a
definition by looking like:
1 module Cell
2 end
We define all players in modules of the same name as the file and prop, by convention. This allows Limelight to include this behavior when it needs it. You can specify specific mappings between the props and its players, but we don't need to do that here.
So, let's make the cell more interesting. When we click on the cell, we want it to make a large ‘x’ mark. Lets start by creating a spec for the behavior. I created a new directory for the players spec:
1 mkdir spec/players
We have to add the players directory to the ruby search path, so I added the following line to the spec_helper:
1 << File.expand_path(File.dirname(__FILE__) + "/../players")
My spec is going to find the prop that was clicked on and make that prop
display an ‘x’, denoting the first move. Here is what my first spec looks
like (I call it cell_spec.rb):
1 require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2 require 'cell'
3 describe Cell do
4 include Cell
5 attr_accessor :id
6 it "should make first move an X" do
7 @id = "cell_0_0"
8 @cell_one = Limelight::Prop.new
9 @scene = MockScene.new
10 @scene.register("cell_0_0", @cell_one)
11 self.stub!(:scene).and_return(@scene)
12 mouse_clicked(nil)
13 @cell_one.text.should == "X"
14 end
15 end
Which provides the feedback when run:
1 F
2
3 1)
4 NoMethodError in 'Cell should make first move an X'
5 undefined method `mouse_clicked' for #
6 /Users/paulwpagel/Projects/tictac/spec/players/cell_spec.rb:14:
7
8 Finished in 0.007423 seconds
9
10 1 example, 1 failure
If you have seen an rSpec specification before, this should look syntactically familiar. Before we move on to making the test pass, let us take closer look at a few aspects:
-
@id = "cell_1_1"This line is setting the id of the imaginary prop that the players behavior will be executed against. -
@scene = MockScene.newThis creates a scene to mock out. The scene will be explained later, but for this test we are going to use the find method on scene to find our props. -
@cell_one = MockProp.newCreate a mock prop that will turn to ‘x’ when clicked -
@scene.register("cell_1_1", @cell_one)We are giving the scene the mock prop, so the find method will find it by its id. -
mouse_clicked(nil)Simulates a mouse_click on the cell. It takes an event, but we don't care about that yet, so lets just pass in nil.
All right, time to make this test pass. Lets open up the cell.rb
player and see what we need done to make the test pass:
1 module Cell
2 def mouse_clicked(event)
3 cell_prop = scene.find(id)
4 cell_prop.text = "X"
5 end
6 end
Run the test again, no failures. We needed to find the prop on the screen which we are concerned about. We do this by calling find on a method scene, which will give us any prop by its unique identifier. We are looking for the id of the element we clicked, and then we set the text of that element to ‘x’, which makes the test satisfied.
Now, we can run the application from the root directory:
1 jruby -S limelight open .
If we click on the box that is displayed, a small ‘x’ should appear in the upper right corner.
Congratulations, that is your first piece of Limelight behavior. However,
this is not very interesting yet. Lets take it the next step and make the
tic tac toe game work. I am going to create a lib directory to
hold the game model.
1 mkdir lib
2 $ mkdir spec/lib
And before I write my first spec, I am going to add the new lib
directory to the Ruby search path by adding the following line to the
spec_helper:
1 << File.expand_path(File.dirname(__FILE__) + "/../lib")
So, here is what my first spec looks like:
1 require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2 require 'game'
3 describe Game do
4 it "make a move in the middle square" do
5 game = Game.new
6 game.move(1, 1)
7 game.mark_at(1, 1).should == "X"
8 end
9 end
To make it pass, we need to create a game class in the lib
directory and give it this code:
1 class Game
2 def move(row, column)
3 end
4 def mark_at(row, column)
5 return "X"
6 end
7 end
And we can follow the test driving of the model to make the game class. I have already done this, and you can download the models in the sample application. Lets move past that back to the players and hook up the game.
I am going to create a file init.rb in the root directory. The
init.rb class gets loaded up by Limelight when you start the
application. We want to create a new game and have a way to keep it in
memory for the other classes to use. Here is what the spec looks like in a
init_spec.rb in the spec directory:
1 require File.expand_path(File.dirname(__FILE__) + "/spec_helper")
2 require "game"
3 describe "init" do
4 it "should create new game on initialization" do
5 game = mock('game')
6 Game.should_receive(:new).and_return(game)
7 Game.should_receive(:current=).with(game)
8 require File.expand_path(File.dirname(__FILE__) + "/../init")
9 end
10 end
The simplest way to start that is to have a current_game class
variable. Here is the code for the init.rb:
1 << File.expand_path(File.dirname(__FILE__) + "/lib")
2 require "game"
3 Game.current = Game.new
I add the lib directory to the ruby search path so the Limelight
application would know what a game is when I require it.
Now we need to plug the game model into the cell player. Lets change the spec we made earlier to make the first move depending on the game model. Here is the new version:
1 it "should make first move in a game" do
2 @id = "cell_0_0"
3 @cell_one = Limelight::Prop.new
4 @scene = MockScene.new
5 @scene.register("cell_0_0", @cell_one)
6 self.stub!(:scene).and_return(@scene)
7 game = mock('game')
8 Game.should_receive(:current).and_return(game)
9 game.should_receive(:move).with(0, 0)
10 game.should_receive(:mark).and_return("X")
11 mouse_clicked(nil)
12 @cell_one.text.should == "X"
13 end
I am mocking out the game model and passing the values from the id into the game’s move method. Here is the code that makes this pass:
1 module Cell
2 def mouse_clicked( event)
3 game = Game.current
4 x, y = get_coordinates
5 game.move(x, y)
6 cell_prop = scene.find(id)
7 cell_prop.text = game.mark
8 end
9 private ################################
10 def get_coordinates()
11 x = id[(id.length - 1)..(id.length - 1)].to_i
12 y = id[(id.length - 3)..(id.length - 3)].to_i
13 return x, y
14 end
15 end
Minus the ugly string manipulation, it is a pretty straight forward approach. Now we should be able to start up the application and click on any of the squares and make some moves. There are 2 things left to do for this demo.
We need to make sure that a player can not move on a square that is already
occupied, and we need to display a winner. So for the first task, we need to
write a spec to have some kind of feedback to the players that the move is
invalid. Let's add this spec to the props_spec file.
1 it "should have message center for feedback to the user" do
2 @scene.find("message_center").should_not be(nil)
3 end
Nice and simple. Here is the new props.rb file:
1 main do
2 board do
3 3.times do |row|
4 3.times do |col|
5 cell :id => "cell_#{row}_#{col}"
6 end
7 end
8 end
9 end
10 message_center :id => "message_center"
Now lets write a spec for the cell_spec to make sure that the
move is valid, else we display a message in the message center to the user
they must move somewhere else. Here is the spec:
1 it "should display in the message center if the space is occupied." do
2 @id = "cell_0_0"
3 @cell_one = Limelight::Prop.new
4 @message_center = Limelight::Prop.new
5 @scene = MockScene.new
6 @scene.register("cell_0_0", @cell_one)
7 @scene.register("message_center", @message_center)
8 self.stub!(:scene).and_return(@scene)
9 game = mock('game')
10 Game.should_receive(:occupied?).with(0, 0).and_return(true)
11 mouse_clicked(nil)
12 @message_center.text.should == "This space is occupied, please move in an unoccupied square"
13 end
Same as before, with a new prop added. Here is the new cell.rb
file:
1 module Cell
2 def mouse_clicked( event)
3 game = Game.current
4 x, y = get_coordinates
5 if game.occupied?(x, y)
6 message_center = scene.find("message_center")
7 message_center.text = "This space is occupied, please move in an unoccupied square"
8 else
9 game.move(x, y)
10 cell_prop = scene.find(id)
11 cell_prop.text = game.mark
12 end
13 end
14 private ################################
15 def get_coordinates()
16 x = id[(id.length - 1)..(id.length - 1)].to_i
17 y = id[(id.length - 3)..(id.length - 3)].to_i
18 return x, y
19 end
20 end
Simple if, makes it all work. Lets remove the duplication in the specs. Here is the new spec file:
1 require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2 require 'cell'
3 describe Cell do
4 include Cell
5 attr_accessor :id
6 before(:each) do
7 @id = "cell_0_0"
8 @cell_one = Limelight::Prop.new
9 @scene = MockScene.new
10 @message_center = Limelight::Prop.new
11 @scene.register("message_center", @message_center)
12 @scene.register("cell_0_0", @cell_one)
13 self.stub!(:scene).and_return(@scene)
14 @game = mock('game', :occupied? => false)
15 Game.should_receive(:current).and_return(@game)
16 end
17 it "should make first move in a game" do
18 @game.should_receive(:move).with(0, 0)
19 @game.should_receive(:mark).and_return("X")
20 mouse_clicked(nil)
21 @cell_one.text.should == "X"
22 end
23 it "should display in the message center if the space is occupied." do
24 @game.should_receive(:occupied?).with(0, 0).and_return(true)
25 mouse_clicked(nil)
26 @message_center.text.should == "This space is occupied, please move in an unoccupied square"
27 end
28 end
Much better. Now lets do the case of a winner. Here is the spec for the cell:
1 it "should display there was a winner in the message center" do
2 @game.should_receive(:move).with(0, 0)
3 @game.should_receive(:is_winner?).and_return(true)
4 mouse_clicked(nil)
5 @message_center.text.should == "Player X has won the game, congratulations"
6 end
And here is the new cell.rb:
1 module Cell
2 def mouse_clicked( event)
3 game = Game.current
4 x, y = get_coordinates
5 if game.occupied?(x, y)
6 message_center.text = "This space is occupied, please move in an unoccupied square"
7 else
8 game.move(x, y)
9 cell_prop = scene.find(id)
10 cell_prop.text = game.mark
11 message_center.text = "Player #{game.mark} has won the game, congratulations" if game.is_winner?
12 end
13 end
14 private ################################
15 def get_coordinates()
16 x = id[(id.length - 1)..(id.length - 1)].to_i
17 y = id[(id.length - 3)..(id.length - 3)].to_i
18 return x, y
19 end
20 def message_center
21 return scene.find("message_center")
22 end
23 end
Now we can finish off the application by adding new game functionality, or even a computer player that can not be beaten! However, before I let you go, we have to add some styles to the message center and pretty up the board to make it look better.
Here is a comprehensive
list of the styles supported in Limelight. And here is a new version
of the styles.rb:
1 main {
2 width "100%"
3 horizontal_alignment "center"
4 }
5 board {
6 width 152
7 height 152
8 border_width 1
9 border_color "black"
10 }
11 cell {
12 width 50
13 height 50
14 border_width 1
15 border_color "black"
16 }
17 message_center_container{
18 top_margin 100
19 width "100%"
20 horizontal_alignment "center"
21 }
22 message_center {
23 width 300
24 height 100
25 rounded_corner_radius "10"
26 border_color "black"
27 border_width 2
28 padding 5
29 }
There was one change to the props.rb file, to wrap the
message_center in a prop called message_center_container. Also,
notice the pretty rounded corners. Easy to do. Here is the
props.rb:
1 main do
2 board do
3 3.times do |row|
4 3.times do |col|
5 cell :id => "cell_#{row}_#{col}"
6 end
7 end
8 end
9 end
10 message_center_container do
11 message_center :id => "message_center"
12 end
Happy Limelight coding!