Friday, December 24, 2010

Red-Green-Refactor in Real Life, part 1

I would never have achieved my initial progress with Ruby'Ai without lessons from The RSpec Book. Only the direction and focus provided by the Behavior-Driven Development (BDD) and Test-Driven Development (TDD) methodologies enabled me to work through the ambiguities of a new project like this, but some of the book's lessons had to wait until this month's alpha-build stage before taking properly.

Just last night, I finally got a handle on the two-stage Red-Green-Refactor (RGR) process enabled by Cucumber, which lets me share a real-life example of the process with all of you. As context, Ruby'Ai uses a domain-specific language (DSL) to describe visual novels (playable here and implemented here). For example (all source code: rgr_part_1.tar.gz, rgr_part_1.zip):
scripts/characters.rb
add_character :sam, "Samantha"
add_character :courtney, "Courtney"
scripts/scenes.rb
add_scene :first_day_of_class do
show tired_sam
narrate "The class bell rings, immediately lost underneath the coordinated shuffling of papers and the shifting of chairs."
sam "sighs at the sight of clock arms that signal an imminent lunch before leaning back in her chair to address the girl standing beside her."
show content_courtney
grateful_sam "You saved my life, really. It's Courtney, right?"
pleased_courtney "Don't worry about it, Sam, okay? I printed off extras for everybody, just in case."
end
I had already implemented some of the basics, like add_character and add_scene, leaving some unexplained code intermingled with the important parts, but these examples will focus on where the RGR process proper started with the description of scene contents. From an extract of the BDD document:
features/scenes.feature
Feature:
In order to have an engaging story
As a visual novel writer
I want to have scenes in my novel
Scenario: minimal scene
Given a character labeled :sam and named "Samantha"
And a scene with following contents:
"""
sam says "Hello!"
smiling_sam "waves enthusiastically."
"""
Then the scene should have the following steps:
| condition | character | behavior | content |
| default | sam | statement | Hello! |
| smiling | sam | action | waves enthusiastically |
To explain the Then step: Ruby'Ai only provides an engine by which plugins export playable games, such as the Javascript-based prototype, so Ruby'Ai stores convertible steps for each part of the novel. These steps describe character speech, actions, narration, changes of location, and other events in a scene, one inseparable section of story and a close analogue to the screenplay equivalent. The condition describes the state of the character as they perform the action, by which the game chooses an image for the character (e.g. sam_smiling.png); the character names the involved character; the behavior describes the nature of the event (which may affect formatting); finally, the content describes what text the player will see associated with the rest of the step.

My major realization manifested here, the point right after writing the above feature example, when I remembered to take the development process directly to the lower-level Rspec stage. Having fallen in love with Cucumber early in the prototyping stage, I had come to rely on it almost exclusively and ended up writing pages of verbose feature lists. These grew unmanageable as they tried to describe every facet of the DSL and export process. The tool eventually interfered with my ability to further develop and debug the engine once I started spending as much time hunting through the output for the error messages as I spent addressing them. Almost all of this example code should have gone into the Rspec test fixtures, it turned out, but this next example from the alpha demonstrates a more proportional usage of these tools. It is a long example, but many of the parts overlap and we will break these parts down piece-by-piece in the second post of this series. Additional explanation available at the end.
spec/scene_spec.rb
require 'rspec/expectations' require 'novel' describe Scene do before(:each) do @novel = Novel.new @novel.evaluate_character_script do add_character :sam, "Samantha" end @novel.add_scene :at_the_park end it "should let a character say something explicitly" do @novel.evaluate_scene_script :at_the_park do sam says "So what do we do now?" end step = @novel.scenes[:at_the_park].steps.first step.should_not == nil step.target.should == @novel.characters[:sam] step.behavior_type.should == :statement step.content.should == "So what do we do now?" end it "should let a character do something explicitly" do @novel.evaluate_scene_script :at_the_park do sam thus "throws a frisbee." end step = @novel.scenes[:at_the_park].steps.first step.should_not == nil step.target.should == @novel.characters[:sam] step.behavior_type.should == :action step.content.should == "throws a frisbee." end it "should recognize conditions added to characters" do @novel.evaluate_scene_script :at_the_park do excited_sam "catches the frisbee as it's thrown back!" end step = @novel.scenes[:at_the_park].steps.first step.should_not == nil step.target.should == @novel.characters[:sam] step.behavior_type.should == :action step.content.should == "catches the frisbee as it's thrown back!" step.condition.should == :excited end end describe SceneBuilder do before(:each) do @character = Character.new :label => :sam, :name => "Samantha" @scene = Scene.new @builder = SceneBuilder.new :characters => { :sam => @character }, :scene => @scene end it "should mark explicitly stated content as such" do statement = @builder.says "I like it here, it's so cozy." statement.behavior_type.should == :statement end it "should mark explicit action content as such" do statement = @builder.thus "settled into the couch." statement.behavior_type.should == :action end it "should interpret behavior types implicitly" do @builder.parse_character_behavior @character, "Can I stay a bit longer?" @builder.scene.steps[0].behavior_type.should == :statement @builder.parse_character_behavior @character, "pleads convincingly." @builder.scene.steps[1].behavior_type.should == :action end end
SceneBuilder might appear redundant but it provides a clear delineation between the Scene - which only needs to store information about part of the novel - and the methods by which that information gets into the novel. For comparison, we do not need to know anything about the workings of a farm upon which an apple is grown to enjoy it and including farming instructions pasted all over the apple would only make it harder to eat. This approach may also facilitate a clearer implementation of the DSL code down the road.