As I develop Sharpee, one design decision has clarified significantly.
During the processing of a turn, the system won’t generate or “print” any text. None.
But wait Dave! This IS an interactive (text) fiction authoring thing you’re building, right?
Yes, but is streaming print statements the best way to emit templated text?
In some cases that might be optimal, and I certainly understand that writers might see it that way, but is it the best way overall?
I don’t think so.
During the execution of a “turn”, this is what Sharpee will do…
- parse player’s command into a well-known structure that identifies the action, the adjective+noun(s), articles, and prepositions.
- validate the command and its components with the grammar and world model, including if all the necessary bits are “in scope”
- execute the associated function with the given action (take/drop/throw, etc) which will update the world model which automatically fires events into an event source (a data store similar to a log but for IF events)
- when the turn is complete and all actions have completed, the game loop calls a text service
- the text service will query the event source and the world model for both changes and current state and from that information, create the text to emit to the client. All the formatting, combining, listing, and templating of the text is handled in this text service.
- The text service is likely to be a host to multiple templates (console, web, API) but at first it will just emit standard IF to the console (stdio).
This will completely eliminate the need to “manage” spaces, newlines, punctuation, quotations. types (bold/underline), colors, and other text-specific logic until the text service does its work.
The author will initially only care about execution of the story.
Now, we may be able to add things like emotions or surprises as properties of an action and the template can properly emit bold or caps text, but that’s going to be a contextual thing, not a technical thing. The author might tag an action as “important”, but the meaning (in terms of text emission) of that is not determined until the text service is building its template of output. It could mean bold or a color or caps or whatever the template is designed to emit for that property.
I’m actually excited to write an IF game in this system. I want to see where it all leads.
I asked Claude for a possible example:
public class StandardIFTextBuilder
{
private readonly WorldModel _worldModel;
private readonly List<WorldStateChange> _stateChanges;
private Room _currentRoom;
private Person _player;
public StandardIFTextBuilder(WorldModel worldModel, Guid playerId)
{
_worldModel = worldModel;
_stateChanges = new List<WorldStateChange>();
_player = _worldModel.GetThingById(playerId) as Person;
_currentRoom = _worldModel.GetPlayerLocation(playerId);
// Subscribe to world model events
_worldModel.ThingMoved += OnThingMoved;
_worldModel.PropertyChanged += OnPropertyChanged;
// Add more event subscriptions as needed
}
private void OnThingMoved(object sender, ThingMovedEventArgs e)
{
_stateChanges.Add(new WorldStateChange
{
ChangeType = WorldStateChangeType.ThingMoved,
Thing = e.Thing,
FromLocation = e.FromLocation,
ToLocation = e.ToLocation
});
if (e.Thing == _player)
{
_currentRoom = e.ToLocation as Room;
}
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
_stateChanges.Add(new WorldStateChange
{
ChangeType = WorldStateChangeType.PropertyChanged,
Thing = sender as Thing,
PropertyName = e.PropertyName,
OldValue = e.OldValue,
NewValue = e.NewValue
});
}
public string BuildTurnText(ParsedActionResult actionResult)
{
var textBuilder = new StringBuilder();
// Add action result message
textBuilder.AppendLine(actionResult.Results.FirstOrDefault()?.Parameters["message"] as string);
// Process state changes
foreach (var change in _stateChanges)
{
switch (change.ChangeType)
{
case WorldStateChangeType.ThingMoved:
if (change.Thing == _player)
{
textBuilder.AppendLine(BuildRoomDescription(_currentRoom));
}
else if (change.ToLocation == _currentRoom)
{
textBuilder.AppendLine($"{change.Thing.Name} arrives in the room.");
}
else if (change.FromLocation == _currentRoom)
{
textBuilder.AppendLine($"{change.Thing.Name} leaves the room.");
}
break;
case WorldStateChangeType.PropertyChanged:
if (change.Thing.IsVisible() && (change.Thing == _player || _worldModel.GetRoomContents(_currentRoom).Contains(change.Thing)))
{
textBuilder.AppendLine($"{change.Thing.Name}'s {change.PropertyName} changed from {change.OldValue} to {change.NewValue}.");
}
break;
}
}
// Clear state changes for the next turn
_stateChanges.Clear();
return textBuilder.ToString().Trim();
}
private string BuildRoomDescription(Room room)
{
return _worldModel.GetRoomDescription(room, _player.Id);
}
}
public enum WorldStateChangeType
{
ThingMoved,
PropertyChanged
}
public class WorldStateChange
{
public WorldStateChangeType ChangeType { get; set; }
public Thing Thing { get; set; }
public Thing FromLocation { get; set; }
public Thing ToLocation { get; set; }
public string PropertyName { get; set; }
public object OldValue { get; set; }
public object NewValue { get; set; }
}
Which would be executed in the Text Service like so:
public class TextService
{
private readonly StandardIFTextBuilder _textBuilder;
public TextService(WorldModel worldModel, Guid playerId)
{
_textBuilder = new StandardIFTextBuilder(worldModel, playerId);
}
public string GetTurnText(ParsedActionResult actionResult)
{
return _textBuilder.BuildTurnText(actionResult);
}
}