Gamefic

Adventure Games and Interactive Fiction

Reevaluating the DSL

by Gamefic on June 21, 2015

This weekend I pushed a new repo branch that includes a major refactoring of the plot DSL. The syntax for plot scripts is largely unchanged, but the new code behind it uses a new module called the Stage. I based it on the Clean Room pattern described in Metaprogramming Ruby and further developed by Seth Vargo. Including the Stage module can make any class capable of loading code from a Ruby DSL.

Clean rooms provide a sandbox for scripts to limit their impact on the program environment. The simplest implementation could call the Kernel#load method with the optional wrap parameter:

load filename, true

While this method protects the global namespace, it's not much help in implementing a useful DSL. In Gamefic, for example, scripts need to manipulate plots. We can make this possible by evaluating the script's code inside the object's scope via the instance_eval method:

class Plot
  def load_script filename
    instance_eval File.read(filename), filename
  end
end

Now the script can access the object's data. The one caveat is the lack of protection. A script executed in the instance's context has complete access to its components, including private methods and instance variables. My goal was to give scripts access to a public interface without letting them intrude on low-level implementation details. The Stage module accomplishes this by instantiating an anonymous class that provides a configurable interface to the script's host. A stage method provides access to the clean room instance that works similarly to instance_eval.

class Plot
  include Stage
  expose :allowed
  def allowed
    "allowed"
  end
  def denied
    "denied"
  end
end

plot = Plot.new
plot.stage File.read(filename), filename # Evaluate a script
plot.stage do # Run a block
  puts allowed #=> prints "allowed"
  puts denied  #=> raises an exception
end

Stage also provides the mount class method for appending modules. It performs an include and also adds all of the module's public instance methods to the stage.

module Features
  def feature_method
    "feature"
  end
  private
  def private_method
    "private"
  end
end

class Plot
  include Stage
  mount Features
  def internal_method
    "internal"
  end
end

plot = Plot.new
puts plot.feature_method #=> prints "feature"
plot.stage do
  puts feature_method  #=> prints "feature"
  puts internal_method #=> raises an exception
  puts private_method  #=> raises an exception
end

Not only does this feature provide a clean room for scripts, it allows for robust configurability of the interface exposed to the DSL.

A few other updates:

  • Undo, save, and restore actions. The undo action is complete. Save and restore are still in progress. Right now they only work on command-line games.
  • Incremental builds for the Web platform. Compiling game code to JavaScript can be a lengthy process, so now the build will only compile scripts that have changed since the last build.
  • More progress on the IDE, including improvements to autocompletion and a more modular plugin architecture to facilitate easier development and deployment.
  • A minor bug fix in the scene method.

If I can maintain my current velocity, I hope to start releasing full games developed in Gamefic this summer.