Gamefic

Adventure Games and Interactive Fiction

Gamefic 4 and Upcoming Plans

by Gamefic on February 13, 2025

The latest major update to Gamefic introduces some important structural and architectural changes. Some of them break backwards compatibility, but I felt they were important for long-term maintainability, extensibility, and developer experience.

(Note: this post discusses a lot of technical details that explain the reasoning behind the design decisions, but hopefully they don't need much consideration when authoring games.)

One important goal was to clarify the difference between the script and the model. The script is code: scenes, responses and callbacks that handle game events. The model is data: game entities, counters, and other values that comprise the game world and indicate its current state. This distinction matters because scripts contains procs that can't be marshaled and therefore can't be included in snapshots.

Gamefic 3 took the first steps toward a distinct separation between scripts and models. This was crucial in ensuring that snapshots were capable of saving and restoring a complete game state, including runtime elements like dynamic entities and active subplots. Unfortunately, the separation still had some holes in it. Fixing those holes, along with major changes to custom scene management, comprise the majority of changes that are not backwards compatible.

Scripts and Seeds

Scripting methods such as `respond` used to be wrapped in `script` blocks.

# The old scripting method
class ExamplePlot < Gamefic::Plot
  script do
    introduction do |actor|
      actor.tell 'Hello, world!'
    end

    respond :think do |actor|
      actor.tell 'You ponder your predicament.'
    end
  end
end

Scripting methods are now class methods, so the `script` block is no longer necessary.

# The new scripting method
class ExamplePlot < Gamefic::Plot
  introduction do |actor|
    actor.tell 'Hello, world!'
  end

  respond :think do |actor|
    actor.tell 'You ponder your predicament.'
  end
end

Gamefic 3 started on the path to bypassing `script` blocks, but there were still certain cases where they were necessary. Gamefic 4 eliminates the need for those cases, but breaks backwards compatibility by eliminating the `script` method altogether. Among other things, `respond` is now purely a class method, so the way it used to interact with instance variables is incompatible. An instance variable in response arguments would now refer to a class instance variable, which is not the previously expected behavior. These changes made retaining the concept of `script` blocks untenable.

The `seed` method still exists, but other scripting features (especially `construct`) should reduce the need to define seed blocks.

The construct Method

One long-standing issue with Gamefic has been entity referencing. Historically, the easiest way to manage static entities was with instance variables. This could be cumbersome if you need external access to plot entities, e.g., from chapters, subplots, or unit tests. Authors could solve this problem with instance attributes, but their declarations tended to be verbose.

class ExamplePlot < Gamefic::Plot
  attr_reader :gizmo

  seed do
    @gizmo = make Thing, name: 'a gizmo'
  end
end

plot = ExamplePlot.new
plot.gizmo #=> the gizmo entity

The construct method provides a shorthand way to seed entities with corresponding instance attributes. In technical terms, it memoizes an entity and ensures that the entity gets created when the narrative gets initialized.

class ExamplePlot < Gamefic::Plot
  construct :gizmo, Thing, name: 'a gizmo'
end

plot = ExamplePlot.new
plot.gizmo #=> the gizmo entity

Entity Proxies

Another advantage of the construct method is that it defines a corresponding proxy attribute in the class. This means you can reference constructed entities in class-level script arguments even though they won't exist until the plot gets instantiated.

class ExamplePlot < Gamefic::Plot
  construct :house, Room,
            name: 'a house'
  construct :gizmo, Thing,
            name: 'a gizmo',
            description: 'Some kind of doodad.',
            parent: house # `house` here is a proxy

  introduction do |actor|
    actor.parent = house # `house` here is a Room entity
  end

  respond :look, gizmo do |actor| # `gizmo` here is a proxy
    actor.tell gizmo.description  # `gizmo` here is a Thing entity
  end
end

Gamefic 4 also introduces pick and pick! class methods. At the class level, these methods return a proxy that will be converted to an entity through an instance-level pick(!) call at runtime.

Command Hooks

The before_action and after_action hooks have been replaced with before_command and after_command.

class ExamplePlot < Gamefic::Plot
  before_command do |actor, command|
    actor.tell "You're performing a #{command.verb} command."
    if command.verb == :cheat
      actor.tell "But you're not allowed to cheat!"
      command.cancel
    end
  end
end

Scene Naming

Authoring custom scenes has always been hairy. Gamefic 4 introduces a couple new features to help streamline it. Once again, mostly thanks to the clearer separation of scripts and models, the new patterns aren't fully backwards compatible, but hopefully they're easier to use.

First and foremost, version 4 allows authors to cue a scene subclass directly by referencing its class name.

class ExampleScene < Gamefic::Scene::Activity
  on_start do |actor, props|
    actor.tell "You're in an ExampleScene."
    props.prompt = 'Now what?'
  end

  on_finish do |actor, props|
    actor.tell "You're done with the ExampleScene. The command you entered is #{props.input}"
  end
end

class ExamplePlot < Gamefic::Plot
  respond :example do |actor|
    actor.tell 'Switching scenes...'
    actor.cue ExampleScene
  end
end

ActiveChoice Scenes

Many parser games include multiple-choice components. One of the most common examples is conversation trees. In a MultipleChoice scene, the player must select one of the options in order to proceed. In Gamefic 4's ActiveChoice scene, player input that doesn't match a choice will be treated as a command in the same way as a default Activity scene.

class SaySomething < Gamefic::Scene::ActiveChoice
  on_start do |actor, props|
    actor.tell 'Select an option or enter a command.'
    props.options.push 'Hello, world!', 'Hocus pocus!'
  end
 
  on_finish do |actor, props|
    actor.tell "You say: #{props.selection}"
  end
end

class ExamplePlot < Gamefic::Plot
  respond :speak do |actor|
    actor.cue SaySomething
  end
end

Example gameplay:

> speak

Select an option or enter a command.

1. Hello, world!
2. Hocus pocus!

> 1

You say: Hello, world!

> speak

Select an option or enter a command.

1. Hello, world!
2. Hocus pocus!

> wait

Time passes.

>

Narrators

The new Narrator class is responsible for running plots. The Narrator is essentially a tiny engine that provides a convenient interface for clients, e.g., gamefic-tty and gamefic-driver. This helps ensure that clients are less tightly coupled to plot internals and plots have fewer methods with unexpected side effects.

Parent Relations

The gamefic-standard library provides a Supporter entity, which lets you place things on top of it instead of inserting things inside of it. The entities' parent-child association, however, does not differentiate between placement and insertion. A bowl on top of a table is the table's child in the same way that a bowl inside a cabinet is the cabinet's child.

Gamefic 4 allows you to specify an entity's relation to its parent. The currently supported relationships are :in and :on, with :in being the default.

class ExamplePlot < Gamefic::Plot
  construct :table, Supporter, name: 'table'
  construct :cabinet, Receptacle, name: 'cabinet'
  construct :bowl, Item, name: 'bowl'

  seed do
    bowl.put cabinet, :in
    bowl.parent   #=> cabinet
    bowl.relation #=> :in

    bowl.put table, :on
    bow.parent    #=> table
    bowl.relation #=> :on
  end
end

Integer Queries

Response arguments can include queries for an integer.

class ExamplePlot < Gamefic::Plot
  respond :jog, integer do |actor, number|
    actor.tell "You jog #{number} meters."
  end
end

The original version of this query was implemented for the gamefic-commodities library. It seemed like a useful enough feature to add to the Gamefic core.

Autoloading in Projects

The Gamefic SDK has been updated to leverage the new features in the core library and add some new features of its own. One of the most notable is autoloading. The gamefic-autoload library leverages Zeitwerk to load classes and modules without explicit require calls. The autoload feature is included by default in new game projects.

Web Builds

The SDK's default web build includes a new transcript feature and a few UX improvements. To see what's new, install the new SDK, create a new project, and generate a web build.

What's Next

Over the next few months, I expect to do a lot of minor and patch releases as I refine the new features. The online documentation is up to date, but I won't be surprised if I need to push an occasional correction. I also plan to release some more example games to demo various capabilities.

On a side note, I hope to do a post-comp release and postmortem of Focal Shift sometime in the next couple weeks.