An ongoing side project of mine is to develop a C# based story authoring system. I’ve been using ChatGPT and more recently Claude Opus to cowrite the code.
C# has an interesting capability that aligns with core IF requirements. I’m talking about its ability to return multiple values from a function. So we might do something like this:
Tuples are indeed situationally very useful! I think they’re supported out of the box in TADS 3, and my Data Structures extension added them to Inform 7 (6M62).
Several modern languages support functions with multiple return values, such as Python, Lua and Go. But as pointed out above, often it’s better to define a type that makes it clear what exactly you’re returning: what are all those values supposed to represent? (Robustness is a bonus, too.)
That said, I had no idea C# of all things had this particular feature. Good catch! Wonder when it was added and why.
Tuples have been in C# for about 7 or 8 years, I think. They were kind of half-baked in the beginning, but have gotten better. Rust has them as well.
They’re useful to cut down the number of boilerplate “objects” you need for small things. For instance, when returning both a value and an index when enumerating a collection.
I can’t think of any popular language that doesn’t support multiple return values, except for C and Java.
JavaScript, Python, C++17, PHP, Go, Rust, Kotlin, Ruby, Lua, Dart, and Swift all support it.
Some languages call this feature “destructuring,” where you’re technically returning just one object or array, and then the caller converts the return value into multiple local variables in a single line.
I think the main thing a parser IF platform needs is excellent syntax for large amounts of declarative data, which is where Inform 7 really shines.
I want avoid comparisons to existing platforms and their clear advantages and disadvantages.
My interests are in building something that has modern software development concepts like separation of concerns, 3GL extensibility, and very specific features like the world model contained in an in-memory graph data structure and output structured as a contextual service similar to fyrevm’s channel IO. Allowing for custom user interfaces is a given.
The parser and grammar aspects are likely to mimic Inform 6 and TADS.
Fluid C# syntax can provide the same declarative syntax that Inform 7 has though it’s a challenge to bring all of it together.
My brain has struggled with “what is core functionality?”, “what is standard IF functionality?”, and “how would authors build stories?”.
It turns out, this is the hardest aspect of attempting what I’m doing.
Those are excellent questions, and just like in a general purpose programming language, it can be hard to draw a line. So you’re going for an embedded DSL?
Fluid syntax in the very fragile grammar code I have now:
_grammar = new Grammar();
_world = world;
Player = new Player("player", "Player", "You are a scruffy adventurer.");
_world.AddNode(Player.Id, Player);
// Standard actions
var standardTake = _grammar.Verb("take", Take).Noun.End;
var metaExamine = _grammar.Verb("examine", Examine).Noun.End;
var standardGoInNoun = _grammar.Verb("go", Go).In.Noun.End;
var standardGoNoun = _grammar.Verb("go", Go).On.Noun.End;
// Meta actions
_grammar.MetaVerb("score", Score);
_grammar.MetaVerb("restore", Restore);
_grammar.MetaVerb("restart", Restart);
// Override the "go" action
var goInNoun = new List<Token>
{
new Token(TokenType.Verb, "go", Go, ActionType.Standard),
new Token(TokenType.Preposition, "in"),
new Token(TokenType.Noun)
};
_grammar.OverrideActionDelegate("go", goInNoun, CustomGo);
_world = world;
function x(r) {
r.foo = 1; r.bar = 2;
return (r);
}
r = x(r); // optional return for a pointer :)
console.log(r.foo);
console.log(r.bar);
There’s a necessity of implicit understanding of how the structured variable fit in the context of overall program, but that’s what programmer’s journal is for.
Personally, I don’t even use struct, preferring simple array (tabular) data structure. But ,well, to each his own!
“what is core functionality?”, “what is standard IF functionality?”, and “how would authors build stories?”.
If you’re going to invent a DSL, these questions partition into things written in the DSL and things that are part of the runtime.
So;
core functionality = runtime (C# or whatever).
standard IF functionality = DSL
how authors build = DSL
However;
DSL in (2) winds up in some kind of user-level lib. This works out kind of neat, because although this is meant to be the “standard IF”, it is nevertheless editable for stories that want to change it. Because some always do!
Even if you’re not planning on introducing a DSL, this is an interesting way to think about how it might be partitioned.
In my current design, there are structural components that provide data storage, event management, and the turn loop.
From there I have libraries for Grammar, Parser, and Standard IF.
I would equate those as a sort of DSL since I’m using Fluid design techniques which are not really needed in business applications outside of pipeline patterns.
The hierarchy is:
Data Store
Text Emission Service
Event Broker
Rules Engine
** Grammar
** Parser
*** Standard IF
**** Turn Loop
Story (as a class library)
This is my latest iteration. I think I’ve gone through this about five times now. I’ll probably iterate several more times before solidifying what I want to build.
Looks complex enough. Guess it can’t be helped. Nitpick: you probably mean a fluent interface, not fluid. I was mixing them up, too, and couldn’t figure out why a web search turned out entirely wrong results while working on the Jaiffa library.
I’ve decided (for now) to go down the path of not having declared objects for anything and that everything in the story (the world) will be a node and connected with edges. Nodes have their own properties. Nodes will automatically fire a NodeChanged event for any state change. These change events will get translated to IF constructs at higher layer.
This would be how story state change is handled. The author can identify nodes by name (location, story object, PC, NPC) and catch specific state changes. A node identified as a door would be a node connected (generally) to two location nodes. If the door has a property for Open/Closed and changes, the author could trap when that happens…
Core.Events.Catch("front door", "open", FrontDoorOpened);
private void FrontDoorOpened() {
// what happens when the front door is opened
}
This is being roughed out for now, but I love this path in my R&D. The idea that the world is a true bidirectional graph of IF constructs feels “right”.
The challenge will be to apply all IF properties and functionality to nodes with one abstraction layer. (the nodes and edges contain data, but don’t actually know anything about IF).