On implementing zarf's rule-based programming language

In zarf’s recent blog post http://gameshelf.jmac.org/2015/01/forward-planning-for-if-tools/ he mentioned a rule-based programming language as one of his many cool projects. Unfortunately, it’s so cool it’s not apparent how to move forward with the loose conglomeration of ideas. So I offered to throw together a quick-and-dirty implementation just so we can fiddle around with things.

What I have to show you today is more dirty than anything, but I’ve learned a few things and am ready to ask more questions.

The implementation is written in C# and can compile and run on Visual Studio 2010 Express (i.e., the free one) for Windows XP all the way up to the latest version on Windows 8.1 and presumably anything in-between.

I am not distributing an .EXE file yet. It’s still too broken/incomplete.

While its downloading, please read the documentation in the next post. I promise I didn’t just write it all in a 20-minute word-vomit session, probably.
zarfian.zip (32 KB)

Zarf’s rule-based language for creating I-F: “zarfian”.

It is relation-based, rule-based, and has a touch of aspect-oriented programming.

An example of a “loves” relation in Inform7:

    Loving relates various people to various people.
    Alice loves Bob.
    Charlie loves Donna.

The same example in zarfian lacks the explicit declaration of the relation:

    do loves(Alice,Bob)
    do loves(Charlie, Donna)

(Most statements in zarfian start with the word “do”, per Zarf’s presentation at PenguiCon eblong.com/zarf/essays/rule-based-if/index.html which seeded this quick-n-dirty implementation.)

Inform7 can set relations like so.

    now Alice loves Edward.
    now Charlie does not love Donna.

The equivalent in Zarfian:

    loves(Alice, Edward) := true;
    loves(Charlie, Donna) := false;

Inform7 has several kinds of relations, such as various-to-various, one-to-various, “in groups”, etc. Zarfian has only the most general kind, various-to-various. However, Inform’s relations are always binary: either Alice loves Bob or she doesn’t. Zarfian relations can be anything.

    loves(Alice,Edward) := 80;
    loves(Alice,Edward) := "Well, kinda.";
    loves(Alice,Edward) := { ...code that will do something later... };

(Note that although it can be anything, it can still only be one thing at a time. Those three statements ran in order would be pointless; only the final one’s value would stick.)

Inform7 has computed relations.

    Joining relates a thing (called X) to a thing (called Y) when X is part of Y or Y is part of X.

Zarfian has it as well:

    do joined(thingX, thingY) if IsPart(thingX, thingY) || IsPart(thingY, thingX)

Inform has phrases which can do arbitrary things.

    To prevent (kid - a person) from sticking (inappropriate item - a thing) into (expensive item - a device):
            ...then do whatever...

The zarfian equivalent:

    do PreventStickingInto(kid,item,device) as { ...then do whatever... }

Inform has rules, such as:

    Carry out examining something:  ...do whatever...

Zarfian is all about rules:

    do CarryOutExamining(noun) as { ...do whatever... }

Inform7 groups rules into rulebooks:

    The cat behaviour rules are a rulebook.
    A cat behaviour rule: say "Meow."
    A cat behaviour rule when the laser pointer's dot is visible: "The cat pounces." 

Zarfian does as well, but like relations, does not explicitly declare the container’s existance.

    do CatBehavior as { "Meow."; }
    do CatBehavior as { "The cat pounces."; } if IsVisible(LaserPointerDot)

(In the current implementation, an explicit “say” or “print” statement isn’t needed. Strings within braces, alone between semicolons, display themselves. Literal strings can use the " to contain a double-quote, and \n to represent a newline, but all else is verbatim.)

Rulebooks can be invoked imperatively in Inform7.

    consider the cat behavior rules;

As well as Zarfian.

    CatBehavior;

Inform 7 has headings like Volume, Book, Chapter, and Section, which set off a group of constructs. Zarfian also has them and they work similarly: the initial word must immediately follow a newline, and the entirety of the line is considered the heading. Unlike Inform7, the current implementation of Zarfian doesn’t give numbering any special recognition.

Zarfian can declare a rule private by replacing the word “do”, and can declare everything within a heading private. The current implementation defines Private as only one’s peers can see it. (A private rule can only be seen by other rules in the same section. The contents of a private section can only be seen by the other sections within the same chapter. Etc. The contents of a private volume cannot be seen outside the file.)

    Section for implementation details - private
    
    do FiddleWithValue(value) as { ... } 
    
    private Count as 5

The current implementation doesn’t actually enforce Private, but does understand and record it. It also doesn’t support overriding Private with the “(see )” appellation.

Inform7 has a description-of-objects, to select a subset of objects with which something will be done.

    ...suspicious persons which are in the Conservatory...

Zarfian does not have objects. But since object-based languages are based on the is-a and has-a relationships and zarfian is relation-based, zarfian can in theory support objects.

    do IsA(room, object)
    do IsA(person, thing)
    do IsA(thing, object)
    do HasA(thing, description)

Inform7 consults rulebooks in an order decided by imperative code.

    A specific action processing rule:
            follow the check rules for the action;
            follow the carry out rules for the action;
            follow the report rules for the action.

Zarfian can do so if needed.

    do InsertsInto(noun,noun2) as {
            CheckInsertsInto(noun,noun2);
            CarryOutInsertsInto(noun,noun2);
            ReportInsertsInto(noun,noun2);
    }

But Zarfian prefers a description-of-rules, to select a subset of rules. Currently the only use of such is the “precede” relation for declaring the relative order in which groups of rules run. The current implementation uses the special syntax " precede " instead of the usual “do precede(,)”. This is to indicate the uniqueness of precedence rules as instructions to the compiler. Unlike all other rules, precedence rules do not exist at run-time.

    rules named Check* precede rules named CarryOut*
    rules named CarryOut* precede rules named Report*
    rules in Volume 1 - cloak of darkness precede rules in Volume - the Standard Rules
    rules checking Verb precede rules checking TurnCount
    rules changing Score precede rules reading Score

The clauses in a rule description are “named”, “in”, “checking”, “changing”, and “reading”. “Named” performs a wildcard match on the rulebook’s name. (Individual rules do not have names). The * asterisk can be used anywhere in the pattern, and multiple times, such as Putting to catch CheckPuttingOn and ReportPuttingDown. “In” refers to everything within a heading. “Checking” refers to a variable mentioned in a rule’s “if” clause. “Reading” refers to a variable mentioned in a rule’s “as” clause. “Changing” refers to a variable that appears on the left-hand side of the := assignment operator within a rule’s “as” clause.

In the current implementation, these clauses can be combined, though the same clause can’t be used twice in the same description. I think this is good enough for a prototype though a real implementation would likely want to add that as well. Additionally it might allow a “description-of-expressions” so we could identify rules which do “any math operation” on “any variable named attack*” and other arbitrary selections. But the current implementation gives the flavor of the feature with half the work.

After the compiler gathers all the rules from all the source files, extensions, and the standard library, it then examines the precedence rules. The current implementation first ensures no rule will appear on both side of a precedence rule. For example, there’s probably a rule somewhere that both reads and changes the score, so the above precedence rule is saying that particular rule would have to precede itself. Secondly, the current implementation performs a simple check for cyclical reasoning, such as if the author had also stated “rules named Report* precede rules named Check*” in addition to the above two.

For those who don’t need to download the C# and just want to see some examples of zarfian (or whatever we’ll eventually call the language), here’s some code that compiles and almost kind of works.

Per the Penguicon presentation, rules follow the pattern: do ATOM as CODE if CONDITION

And because it was easier for me to shoehorn it in there, a plain old if-then statement is written

   x > 5 ? { "x is pretty big."; }

instead of the usual

  if (x > 5) { print "x is pretty big."; }

…and the else clause isn’t even possible. I’m not trying to be cute or anything; I just care about my time more than syntax.

Even if you compile and run the code, it will break often and obviously.

x me
You see nothing special about the You are a heroic adventurer!

…for just one example.

The Standard Rules.txt

[spoiler][code]

Volume - the Standard Rules

Book 1 - Basic computer stuff

Chapter 1 - Input

Section 1 - primitives which are hard-coded into the compiler/interpreter - private

do ReadKeyboard
do ReadKey
do Parse

Section 2 - input vars

do ThePlayersCommand as “”
do verb as “”
do prep as “”
do noun as “”
do noun2 as “”

Section 3 - Input interface

do TheCommandPrompt as "> "
do ReadACommand as { newline; TheCommandPrompt; WaitForPlayerInput; }
do WaitForPlayerInput as {ThePlayersCommand := ReadKeyboard;}

Chapter 2 - Output

do newline as “\n”

private Count as 5

Book 2 - Basic game stuff

Chapter 1 - Game skeleton

do ProgramStart as
{
StoryTitle; newline;
"An interactive fiction by "; StoryAuthor; newline; newline;
WhenPlayBegins;
MainLoop;
}

do WhenPlayBegins as { }

do MainLoop as { TurnCount := TurnCount + 1; ReadACommand; Parse; }

do TurnCount as 0

do Win as { newline; newline; " *** You have WON! ***"; newline; newline; "Your final score is “; score; " points!\n”; ThePlayersCommand := “quit”; }

do Lose as { newline; newline; " *** You have lost ***"; newline; newline; "Your score was only “; score; " points.\n”; ThePlayersCommand := “quit”; }

Chapter 3 - Actions

do InsertsInto(noun,noun2) as
{
CheckInsertsInto(noun,noun2);
CarryOutInsertsInto(noun,noun2);
ReportInsertsInto(noun,noun2);
} if noun2 != “” && (verb = “insert” || verb = “put”)

do CheckInsertsInto(noun,noun2) as
{
noun2; " cannot contain things."; newline;
} if CanContain(noun2) = false && noun2 != “” && (verb = “insert” || verb = “put”)

do CarryOutInsertsInto(noun,noun2) as
{
CanContain(noun2) ? {
IsIn(noun,noun2) := true;
}
} if noun2 != “” && (verb = “insert” || verb = “put”)

do ReportInsertsInto(noun,noun2) as
{
"You put the "; noun; " into the "; noun2; “.”; newline;
} if noun2 != “” && (verb = “insert” || verb = “put”)

do CarryOutExamine(noun) as
{
Description(noun) = true ? { Description(noun); };
Description(noun) = false ? { "You see nothing unusual about the "; noun; “.”; newline; }
} if (verb = “x” || verb = “examine”) && noun != “”

do CarryOutExamine as
{
“You didn’t mention what you wished to examine.”; newline;
} if (verb = “x” || verb = “examine”) && noun = “”

do BlankLine as
{
“I beg your pardon?”; newline;
} if verb = “” && noun = “” && prep = “” && noun2 = “”

do CarryOutTakeInventory as
{
"You are carrying: "; newline;

} if (verb = “i”) || (verb = “take” && noun = “inventory”)

do CheckGo(West) as
{
West(location,something) == false ? { “You can’t go west from here.\n”; }
} if verb = “w”

do CheckGo(North) as
{
North(location,something) == false ? { “You can’t go north from here.\n”; }
} if verb = “n”

do CheckGo(East) as
{
East(location,something) == false ? { “You can’t go east from here.\n”; }
} if verb = “e”

do CheckGo(South) as
{
South(location,something) == false ? { “You can’t go south from here.\n”; }
} if verb = “s”

do CarryOutGo(direction) as
{
TheScrawledMessage := {“The message has been carelessly trampled, making it too difficult to read.\n”; Lose;}
“Blundering around in the dark isn’t a good idea!”
} if location = Bar && IsDark(Bar)

rules named Check* precede rules named CarryOut*
rules named CarryOut* precede rules named Report*

[/code][/spoiler]

And Cloak of Darkness.txt

[spoiler][code]

Volume 1 - cloak of darkness

do DarknessMessage as “It’s very dark, and you can’t see a thing.”

do Pi as 3.14159

do StoryTitle as “Cloak of Darkness”

do StoryAuthor as “Ron Newcomb”

do MaximumScore as 2
do score as 0

do Foyer as “You are standing in a spacious hall, splendidly decorated in red and gold, with glittering chandeliers overhead. The entrance from the street is to the north, and there are doorways south and west.”

do location as Foyer

do Cloakroom as “The walls of this small room were clearly once lined with hooks, though now only one remains. The exit is a door to the east.”

do Room(Foyer)
do Room(Cloakroom)
do West(Cloakroom, Foyer)

do Hook as “It’s just a small brass hook, [if something is on the hook]with [a list of things on the hook] hanging on it[otherwise]screwed to the wall[end if].”

do IsIn(Hook,Cloakroom)
do CanSupport(Hook)

do Bar as “The bar, much rougher than you’d have guessed after the opulence of the foyer to the north, is completely empty. There seems to be some sort of message scrawled in the sawdust on the floor.”

do South(Bar,Foyer)

do IsDark(Bar)

do TheScrawledMessage as { score := score + 1; “The message, neatly marked in the sawdust, reads…\n”; Win;}

do OtherThanGo(direction) as
{
TheScrawledMessage := { score := score + 1; “The message, difficult to make out but still legible, reads…\n”; Win;};
“In the dark? You could easily disturb something.”
} if location = Bar && IsDark(Bar)

do CarryOutGo(direction) as
{
TheScrawledMessage := {“The message has been carelessly trampled, making it too difficult to read.\n”; Lose;}
“Blundering around in the dark isn’t a good idea!”
} if location = Bar && IsDark(Bar) && (verb = “go” || verb = “n” || verb = “s” || verb = “e” || verb = “w”)

do CarryOutExamine(TheScrawledMessage) as
{
TheScrawledMessage;
} (if verb = “x” || verb = “examine”) && noun = “message”

do cloak as “A handsome cloak, of velvet trimmed with satin, and slightly splattered with raindrops. Its blackness is so deep that it almost seems to suck light from the room.”

do CanWear(cloak)

do me as “You are a heroic adventurer, dripping wet.”

do IsWearing(me,cloak)

do CarryOutTake(cloak) as
{
IsDark(Bar) := true;
} if IsWearing(me,cloak) = false

do CarryOutPutOn(cloak,something) as
{
IsDark(Bar) := false;
} if location = cloakroom

do CarryOutDrop(cloak) as
{
IsDark(Bar) := false;
} if location = cloakroom

do CloakMsg as “This isn’t the best place to leave a smart cloak lying around.”

do CarryOutPutOn(cloak,something) as
{
CloakMsg;
} if location != cloakroom

do CarryOutDrop(cloak) as
{
CloakMsg;
} if location != cloakroom

do WhenPlayBegins as {"\nHurrying through the rainswept November night, you’re glad to see the bright lights of the Opera House. It’s surprising that there aren’t more people about but, hey, what do you expect in a cheap demo game?\n";}

do Instructions as
{
“Type up to a four-word sentence in the form of VERB NOUN PREPOSITION NOUN. Adjectives and articles are not allowed.”; newline;
} if verb = “” && noun = “” && prep = “” && noun2 = “” && TurnCount = 1

do Price(obj) as 0

do Price(obj) as 10 if Treasure(obj)
do Hit(MingVase) as {“It cracks.”; Price(MingVase):=0;}
do Treasure(obj) as false
do Treasure(HopeDiamond) as true
do Treasure(MingVase) as true

do UserFunc as {}

rules in Volume 1 - cloak of darkness precede rules in Volume - the Standard Rules

rules checking verb precede rules checking TurnCount

do ident

do CanContain(“bag”)

[/code][/spoiler]

The questions I’d like to ask Zarf and anyone are:

  • in Inform if any check rule fails, the associated carry out rules don’t run at all. Maybe the UnsuccessfulAttempt rules need run instead. How to specify this in zarfian?
    ** before calling all the Check rules, set a variable. The final check rule clears the variable. Run the carryout rules if the variable is cleared, or the Unsuccessful rules if the variable is still set. (Obviously requires rules that occlude one another, and requires that the last rule really does remain last.)
    ** when a rulebook is invoked manually, return a value of how many rules ran in it? or allow rules to return values themselves?

  • how to handle lists of stuff? like, what syntax would go into the TakingInventory action here? do we need a special syntax for lists or can they emerge out of the existing features? By using the fill-in-the-blank operator in a relation, which is the sole condition of a “if” statement: IsWearing(me,_) ? { …do something with each eachIsWearing…}

  • how to specify the order that rules within the same book go? Check rules in particular would want to be in a particular order. Is source order OK if there’s no if condition? Within the same book, the specificness of the rule’s IF clause sort the rules. The current implementation simply counts the number of && “and” and || “or” operators in the IF.
    ** how to make it “subclass-smart” when OOP isn’t built-in?
    ** what defines a rulebook? (A “scope of occlusion”?)
    ** sorting rules based on specificness of the IF conditions is basically the same thing as issuing several single-rule-to-single-rule precedence rules in one fell swoop. I think there’s an idea in there somewhere that merges the two concepts.

  • the current implementation tracks what variables were changed, and then only considers the rules that use that varialbe in their IF clause for running, until no more variables were changed, in which it calls MainLoop again. Is that a good idea? This has been removed. Precedence rules decide order of execution “in the large”. (And can do so “in the small” with single-rule-to-single-rule precedence rules. The example does exactly that with BlankLine and Instructions. But it gets verbose really quickly.)

  • relatedly, if a rule without any IF condition at all runs “always”, then I have to tag all rules everywhere with “if verb != ‘examine’” and such. That’s a lot of typing. How to avoid that? Only rules in the current context are considered at a time. Precedence rules construct a series of contexts.

  • is the topological sort to locate cycles in the precedence rules , plus the check against a rule appearing on both sides of the same precendence rule, enough to catch everything? (If not, would the following solve that as well: instead of a description-of-rules (“X”) being a single node in the top-sort, split each into 2 nodes, called “X-begins” and “X-ends”, add an edge “X-begins precedes X-ends”, and let the groups partially overlap.) No. Yes.

  • is it a good idea to allow imperative invoking of rules which are found in a precedence relation? Meaning, if a PRECEDE rule specifies X before Y, but a rule Z’s code has { Y; X; }, then we’re breaking the PRECEDE rule. Should that be a compiler error, or if not then could it contribute to spaghetti code, etc?

  • any problems with a feature to add conditions to pre-existing rules without editing them: if

  • any problems with a feature to add invocations to pre-existing rules without editing them: entail/presage <invocation{s}>
    ** AOP also allows “instead of”, and so does Inform7

so, we’re back to i6 syntax?

still trying to grasp the difference in semantics…

Thanks for putting this together! It’s something of a nuisance for me to run C#, unfortunately. I can try to answer your questions anyhow…

You’ve created a de-facto rule-grouping mechanism with your wildcard syntax (and naming rulebooks CheckGo, CheckExamine, CarryOutGo, CarryOutExamine, etc.) This gets you some distance, but it means that some parts of the action context are variables (noun, noun2) whereas other parts are hard-coded in the rule name (the action, the phase). That will run into trouble. My primary goal here is to handle everything uniformly.

(For example, you can’t write a rule about multiple actions unless you give them a common name. If you think about touching-versus-looking actions, that leads you to action names like TouchPush, TouchPull, TouchTake, ViewExamine… inflexible.)

You’ve also munged together the notions of “rulebook name” and “rule name” in a way that isn’t obvious to me. In the rule:

do OtherThanGo(direction) as
{
    TheScrawledMessage := { score := score + 1; "The message, difficult to make out but still legible, reads...\n"; Win;};
    "In the dark? You could easily disturb something."
} if location = Bar && IsDark(Bar)

…is “OtherThanGo” invoked from somewhere, or does the engine churn through every rule that satisfied the “if” condition?

That’s… not what I was picturing, at least. I was imagining that, as in I7, no rules run until something explicitly invokes their rulebook.

I’m not against implicit code triggered by setting a variable. (I7 lacks this and people often want it.) But as the core mechanic of the whole system? That seems weird. Possibly it will fall away once rules-versus-rulebooks is clarified.

Your “precedes” mechanism covers this, right? Assuming we actually have rule named. You could say

Rule named SpecificRuleX precedes rule named AnotherSpecificRuleY

…with no wildcard.

The idea is that you have an (I7-like) specific-overrides-general mechanism within each rulebook. If rule A is defined “if verb == ‘examine’” and rule B has no condition, then rule B will be run when verb is anything else.

I7 handles this by ordering rules from specific to general, and then executing them in order until one pings. That’s always seemed like a crude approximation of a real logic engine. If the compiler really knows what conditions are subsets of which, it can construct an if-tree (rather than a sequence of if-return, if-return, if-return.)

It’s always exceptions that make the problem fun. (Or hairy.) If I say

Rules named Check* precede rules named CarryOut*
Rule named CarryOutTimeTravel precedes rule named CheckTimeTravel

…does the compiler blow a gasket?

(Note: I’m somewhat worried that by answering all these questions “my way”, I will just lead you down my own dead-end path and we’ll be stuck on the same problems together. Please keep that in mind.)

This gets at the difference between an if-tree and a sequence of unconnected if-return tests.

Say we have an Act rulebook, which invokes two lower-level rulebooks:

do Act as { CarryOutAct; ReportAct; }

In I7 terms, a carry-out rule is one that goes in CarryOutAct and handles that stage in certain circumstances. A report rule is one that goes in ReportAct and handles that stage in certain circumstances. What’s a check rule? It’s a rule that goes in the Act rulebook, and handles the entire sequence in certain circumstances.

If you have a bunch of check rules, that’s a bunch of Act rules (with some ordering). If none of them apply, then the general rule above is run.

The I7 model of rulebook makes sense when you genuinely want to do a bunch of things. Think of the “every turn” rulebook. There, you really do want to specify a bunch of rules with no conditions (or perhaps with conditions); there’s no specific-overrides-general model. My setup doesn’t express that easily.

My original idea was express this in a “super”-like way. You could write a rule

do EveryTurn as { TimeTick; EveryTurn; }

This adds your TimeTick rule to the rulebook without suppressing anything else. More rules of this form add to the rulebook. However, this model necessarily hardcodes the ordering – TimeTime goes at the beginning of the chain. You could put it at the end by saying

do EveryTurn as { EveryTurn; TimeTick; }

…but it’s still a damn mess, integrates weirdly with the rest of the precedence mechanism, and how do you say “put this rule after X but before Y”?

Maybe, instead of simple sequencing, we need first-class operators that say “add parallel to group of rules” and “add exception to group of rules”… Would that be a nice algebraic group? (Oh listen to me)

Speaking of which: is there is standard algorithm for taking a bunch of logical expressions and arranging them in a “stronger-than” partial ordering? I mean, taking “A&B”, “A”, “A|C”, “D” and determining that the first three go in that order (specific to general) whereas “D” is unrelated to any of them.

i6 syntax is easier to parse, so it’s easier to prototype stuff in. The final syntax of zarfian could be quite different. As you say, we’re all still trying to figure out good semantics.

:slight_smile:

Appreciated; I was afraid of that, but I don’t know Python, C/C++ is terrible for prototyping, and I was unsure of what other languages you & others may know. I at least tried making it broadly compile-able, and I think C# source code is readable to anyone who knows C and any OO language. Getting the gist of it is fine at this stage. I hope to have an EXE and playable CoD next time I work on it.

I added name-wildcard very late. But if we consider “do ATOM” to just be syntactic sugar for the atom being just another parameter in the IF clause (per the Penguicon ‘spec’ – see “hang my scarf on”) then the wildcard name match is syntactic sugar for the “checks variable” match. So I’ve partially avoided your argument. Besides, the wildcard name match is easy to implement, easy to understand, and outright stolen from AOP’s pointcuts. But gluing the phase to the action is bad, I see now. Those are different variables.

At first, it wasn’t to me either. I thought ATOM was a rule name when I started, but later, Hit(MingVase) in the spec implied there were a more general Hit(obj), which would make Hit be a rulebook whose more specific MingVase rule occluded the general Hit(obj). So ATOM shifted to being rulebook name. And I think I prefer it that way. My definition of a rule is code that self-invokes “at the right time” based on the state of global variables, etc. Corollary: since it self-invokes, you never need to invoke it from elsewhere, so you don’t need to name it. Like the unnamed Inform rule: Instead of looking, say “It’s too dark”. There’s no rule name there, just a rulebook name.

Maybe: if the ATOM lacks parenthesis, it is unique? If it has parenthesis, it is a rulebook name?

That one doesn’t work at all, currently. :slight_smile: I wrote it partly as a to-do. I believe it will NOT invoked specifically; the if condition will decide that.

It got me up & running quickly, and I’m fine with deleting it. Partly because of the problem: if the last rule in the whole precedence chain changes a variable used by the first rule in the chain, and the whole chain run again, it can look like the Last rule is running right before the First rule. But if the whole chain didn’t rerun, then the implicit trigger looks broken for the Last rule. Blah.

Yeah, but I don’t want to require rule-by-rule precedence a lot. Else we’d be better off with function invocations. On the subject,

Rules named Check* precede rules named CarryOut*
Rule named CarryOutTimeTravel precedes rule named CheckTimeTravel

isn’t caught yet but I think that’s perfectly do-able.

Well OK, so, a rule without any IF condition runs “always”, for certain values of “always”: only if the rulebook is running. So what’s a rulebook? Is it the name like Hit and CheckGoing, or defined ad-hoc by directives like “rules named CheckGoing* occlude each other”, or… both?

As long as we can define what a rulebook is (like say, “ is a rulebook”), we can then define how the rules within it operate (like “ occlude each other” / “ make no decision”).

(my previous post missed this bit)

I don’t know. I took a stab at defining such, but did some of your academic reading on defeasible logic contain any useful nuggets of wisdom?

It does now. The compiler compares all RuleDescriptions to all other RuleDescriptions looking for “contains” relationships, and when it finds one it adds a pair of edges to the digraph sent to TopSort.

Comparing RuleDescriptions is done by exhaustively listing all rules that each description pertains to, then comparing the two lists to each other. I tried being fancy and just comparing the descriptions’ clauses to clauses, but my brain blew a gasket.

Second draft attached. An .EXE is included so anyone on Windows can run

zarfian.exe  CloakOfDarkness.txt

It spits out lots of debugging info before and after the game.

The testing commands it knows are:
RULES ON
RULES OFF
RULES ALL
RELATIONS
RULEBOOKS – meaning, a group of rules named by a precedence rule, which determine order of execution

It still has numerous issues but we can at least X ME, X CLOAK, N, E, S, W, LOOK, and QUIT.

Everything is case-sensitive, and the parser is annoyingly picky and unhelpful. Don’t stick a Volume heading on the first line of a file, for example – headings must be preceded by a newline.

I added a fill-in-the-blank syntax because I needed it: Relation(x,_) will search all relations of the form Relation(a,b) and return whatever B is when a = x. I use this to set the location at one point in the standard rules.
Zarfian.zip (98.1 KB)

Last post before Valentine’s, then my wife owns me. :slight_smile:

I’ve attached draft 3 with an .EXE ready to play. I fixed a navigation bug that prevented me from using the same direction twice. And added testing commands “atoms” and “vars”, and slimmed down the “relations” one.

And, I implemented TakeInventory.

I re-used the “if statement” as the loop statement. When used as a condition, a relation with all values supplied IsWearing(me,cloak) will return true or false if that relation currently holds. If the relation has the fill-in-the-blank character _ as one of the parameters IsWearing(me,_) then one of two things happens. If used in a larger condition x>5 && IsWearing(me,_) then it returns true/false if that relation hold for anything in the 2nd parameter. But if used as a condition itself, it will yield-return the whole list of things that can go in there. Hence:

this is an IF statement:

x = 5 ? { “x is really big.”; }

this is an IF statement:

IsWearing(me,cloak) ? { “But your cloak keeps you warm.”; }

this is a loop:

IsWearing(me,_) ? { " and a "; eachIsWearing; " "; }

this is an IF statement meaning “if any”

IsWearing(me,_) = true ? { “But at least you’re not completely empty-handed.”; }

The loop variable is always “each” prepended to the relation’s name.
Zarfian.zip (65.3 KB)

Draft 4 attached.

Fixed a few bugs, and made the “parameters” to a rule do something: they effectively are shorthand for testing variables. So, actions are now written like:

do Action(“Check”,“Examine”,noun) as { …

instead of

do CheckExamine(noun) as { …

and the standard rules calls

Action(phase,action,noun,noun2);

I have also updated an earlier post in this thread with new questions, etc. (Look for the post with all the struckout text.)
Zarfian.zip (66.1 KB)

I’ve gotten out of date on this thread. But I want to mention a couple of other directions from folks who haven’t posted in this thread.

Chrisamaphone posted this paper on a rule-based language which is sort of a generalization of I7 and PuzzleScript concepts: lambdamaphone.blogspot.com/2015/ … e-for.html

It doesn’t deal with precedence at all. (In fact “rule ordering is not part of the language semantics”, so if more than one rule applies, it’s unspecified which runs first). On the other hand you can specify “stages” (like I7’s check/perform/report) to any degree you want.

The other bit was a conversation I had with Doug (over Korean food) where he described an old multidispatch project. The interesting bit (to me) was working out the order of “A&B”, “A”, “A|C”, “D” by simple truth tables. You just treat the terms as free variables and see if one expression implies another. (If E1 is never false when E2 is true, then E2 implies E1, which is to say E2 is stronger than E1.) This is the sort of thing that is probably obvious if you’re in a graduate CS computer-language program, but I didn’t have the handle to think about it.

Looking more at questions…

“made the “parameters” to a rule do something: they effectively are shorthand for testing variables.” Okay, I’ll buy that.

“or allow rules to return values themselves?” I’m sure this is necessary. We’ve been talking about rules for performing actions, but there could also be a rulebook for “decide how much an object weighs” or “decide what text is an object’s description”.

“how to make it “subclass-smart” when OOP isn’t built-in?” For a first cut, handle a class just as a single-place predicate: IsRoom(o). Some things are rooms, some aren’t. A rule that applies only to rooms takes precedence over a rule that applies to anything. You can describe a single-inheritance class tree this way/

“sorting rules based on specificness of the IF conditions is basically the same thing as issuing several single-rule-to-single-rule precedence rules in one fell swoop. I think there’s an idea in there somewhere that merges the two concepts.” I’ve felt this way for years…

“any problems with a feature to add conditions to pre-existing rules without editing them” It seems like this should be syntactic sugar for some more general declaration. Otherwise you have the question of two different bits of code trying to add conditions to the same rule, with no general way to resolve the conflict.

I just finished reading the 8-page PDF. His way of grouping rules by placing them lexically together between curly braces, called a stage, matches your idea of using Chapter/Section headings to group rules together lexically. But our current implementation of description-of-rules is more powerful.

But, since stages are named, he doesn’t need “metarules” to order the stages; he can use the full power of Ceptre to order stuff. Our current precedence rules are obviously meta. But our rule bodies are imperative, so we can always just call stuff imperatively, in order, if desired.

His idea of re-running all the rules in a stage until all’s quiet is, well, basically what I had done in my first draft here, albeit program-wide. It’s also easy to accidentally loop infinitely, which I discovered empirically a few times. :blush: We haven’t decided if our “rulebooks” run one rule, or all applicable, or continuously until all’s-quiet, or even if we’ll just make it a per-rulebook option kinda like Inform has done. Experience with Inform leans me toward making it an option per-rulebook. I can’t remember ever needing an Inform rulebook to self-recurse, though. Why does Ceptre run that way? Because it’s the only looping/recursion mechanism?

On the whole, I see Ceptre as yet another way of doing things, but I don’t see any OMG features that I’d want to steal.

(I must go to bed now.)

I’ve used recursive rulebooks a little bit in I7. The blatant example was the auto-goal stuff in Hadean Lands, where any goal might require a set of prerequsitive goals. But this is not the common case.

OK.

well ok, but the single condition IsMan(x) would be equally specific to the condition IsPerson(x), not more specific than.

I don’t see it as a conflict. If extension X wants to add condition X and extension y wants to add condition y to a standard rule, I’d AND them both onto the preexisting condition.