Implementing achievements in Inform 6


#1

I’m interested in implementing Achievements in a game using Inform 6.

I would like to make the implementation as flexible and general as possible, so that I can add an Achievement for any successful Action done to any object in the game.

Imagine I have a Troll, and the player attacks it. Once the attack takes place, I’d like to notify the player of an achievement such as:[code]>kill the troll
You rush at the troll, swinging your sword ferociously!

Achievement! Troll-hunter!

[/code]To implement this, I’d like to avoid any Action Subroutine-specific code like, [ AttackSub; ... notify_player_of_achievement_unless_already_achieved(actor, action, noun, second); rtrue; ];Instead, I’d like to write a routine which could notice that any Action whatsoever had been invoked, and then check for an available achievement based on a property on the player.

Is there any way to guarantee that a single, generalized routine could be invoked only after the successful invocation of any Action subroutine?

I can already achieve something like this by doing something like this:Class Person with react_after [; check_for_achievement(actor, action, noun, second); rfalse; ];And then having the playerobj be an instance of Person.

Unfortunately this breaks when a designer tries to implement a react_after method on a subclass of the Person Class. Also, I cannot seem to find a way to make it work for any type of object instance other than an instance of Person, which might not be a big deal, unless it were to be important to support achievements for objects other than a Person.

Is there any way to execute a method after every single successful Action (an Action subroutine that completes without interruption) that is invoked by either a player object, a plain old object, or through one of the and <> invocation techniques?

Best regards,
-Nels


[I6] infglk.h
#2

Maybe you could alter InformLibrary.actor_act in parserm.h.


#3

You could try this:

[ TimePasses;
  check_for_achievement(actor, action, noun, second);
];

This will be executed at the end of every turn during which a non-group 1 action is attempted. Group 1 actions are meta-actions like “score” and “save”. If you want to have achievements related to those kinds of actions as well, you’ll need a different solution.

GamePostRoutine is similar, but it’s part of action processing and will only be called if no earlier before routine, action routine, or after routine interrupted the action. This is what you asked for in your post, but read on for why it might not capture all successful actions in practice.

While reaching the after phase is supposed to indicate that an action has succeeded, designers will often complete logically successful actions in the before stage when they want to override default library behavior without Replacing an action routine. These successful actions would “fail” in the before stage and never reach the after stage, as would successful actions that returned true from after routines because they printed a customized action report rather than letting the library print its default msg.

If I were building an achievement system for I6, I’d look at extending or replicating the existing tasks system that’s currently used for scoring. See §22 of the DM4 for details, particularly the Achieved() function.


(Andrew Plotkin) #4

The TimePasses solution has its own problem: it won’t notice implicit actions. (If EAT APPLE triggers an implicit TAKE APPLE, the action variable will be back to ##Eat by the time TimePasses rolls around.)

There really is no place in I6 action processing that you can stick a hook and say “This will be called for every successful action but no failed action.” This is because, as vlaviano says, authors can use the system in several ways and some of them just don’t care about the staging as long as they get the right result. (I am one of those authors.)

So it’s hard to get what you want to work reliably. On the flip side, asking authors to stick in an “achievement_unlocked” call is not that terrible, and it does work reliably.


#5

What if there were a way to add a hook to the indirect function such that after it was invoked, the game was allowed to do something with the parameters of the indirect invocation and the state of the InformLibrary?

I ask this, because begin_action seems to invoke ActionPrimitive which in turns uses indirect to invoke the action. (Not sure if and <> actually use indirect under the hood.)

I assume that indirect is an Inform 6 built-in, since I can’t find the implementation in the libraries. Is it possible to modify indirect in some way to support hooks and so on?

I’m betting such a thing wouldn’t even be recommended as a practical and architectural issue. However, theoretically, might the vicinity of indirect be the best place to put a system-wide action invocation hook, like after_action?


#6

Yeah. Check out the labels IndirectFunctionCallZ and IndirectFunctionCallG in expressc.c for the zcode/glulx code generation that the Inform compiler does to implement the indirect keyword.

Not all Inform programs use the library (see the first chapter of the DM4 for examples), so the core language is probably the wrong layer at which to implement the kind of hook that you want. More importantly, grepping through the latest beta of the library, I don’t see any use of the indirect keyword at all. So, unless there’s something that I’m missing, even if you did implement this, it wouldn’t capture any of the library’s action processing activity anyway. If you insist on this approach rather than just sticking Achieved(task_foo) into the game at those points where the player does something worthy, then I’d try to find an appropriate place in parserm.h to call into the game.


(Andrew Plotkin) #7

"indirect(x) is just an archaic way of writing “x()”, dating back to before Inform 5 supported the latter syntax. It’s used in a handful of places in the parser, not just in ActionPrimitive. They could all be cleaned up (as the beta I6 library has done).


#8

Hmm… I guess the open source maintenance/development of the Inform 6 library hasn’t been something with which I’ve been able to stay up-to-date – I should change that. The indirect function was getting invoked in key places in parser.h in the version with serial number 040227, but that is very old. Also, it did not occur to me that a more in-depth game designed with Inform 6 would use the libraries, but not the surrounding built-ins, I’ll have to read more about that in the DM for sure.

Practically speaking, sticking AchievementUnlocked(action, actor, noun, second) or whatever in every Action Subroutine in the game seems like it would be prohibitively tedious. I’d like to be able to add all sorts of custom achievements for all sorts of actions and object combinations without having to hunt down the locations of either the Action Subroutine responsible or adding code to the behavior/properties/react_after definitions for a given object class.

Ideally it would be as simple as including a file into an existing game, and being able to modify just that file with new entries like,

Array Achievements table ##Attack 'troll' "Troll hunter!" ##Take 'coin' "Saving for a rainy day!"; /overly simplistic example


#9

The achievements in your example are all based on the tuple (actor, action, noun, second), but I would think that most authors would want to award achievements based on more specific conditions – the kind of stuff that you see under “amusing” when you win a game. For example, attack the troll with a fish while he’s on the pier holding a fishing pole and the tide is in.

If you view achievements as very common and only based on the four usual action parameters, then your proposal makes sense because you don’t want custom hooks everywhere and achievements could plausibly be centralized in a table somewhere. If you instead view them as rare and very specific (as I do), then it’s not too much of a burden to place a few custom checks that do deep checking of the game state where appropriate. This is a design choice on the part of the author, but I think it accounts for some of the disconnect here.

In the rare achievement model, if the achievements were centralized, then the game might have to do more state checking vs. doing it inline where some of the conditions are already verified by virtue of the control flow even being in that part of the code. It’s also possible that, by the time the action is over and we’re in the hook that you propose, some of that state might already have changed as a consequence of the action (e.g., the troll fell off the pier into the water and dropped his pole because we hit him with a fish, and now his location is not the pier and he’s not holding a pole).


#10

Hmm… That’s probably very true. More complex achievements based on specific temporal world tree states could be tricky.

Which is why I’d probably have to include recommendation with an Achievement extension that any such complex outcomes being coded under a pretty specific Action (which probably wouldn’t actually do much.) Like this:[code]
Object -> Troll “troll”
with name ‘troll’,
life [;
Attack: ! Or maybe this goes in a react_after on the playerobj? idk
if (attack_successful && second == Fish && self in Pier && FishingPole in self && Sea has hightide) <>;
];

[ AttackFishermanTrollWithFishDuringHighTideSub;
move FishingPole to Sea;
"When you slap ", (the) noun, " in the face with ", (the) second, " ", (the) noun, " drops “, (hisorher) noun, " fishing pole into the sea!”;
];

Array Achievements table
##AttackFishermanTrollWithFishDuringHighTide ‘troll’ “Fisherman troll trout slapper extraordinaire!”[/code]


#11

That could work, but I think that when you find yourself writing something like AttackFishermanTrollWithFishDuringHighTide, it’s worth taking a minute to stop and reflect on what you’re doing. (He said hypocritically…)

At the point where you’re having the author invoke <>, why not just call AwardAchievement(SomethingFishy) rather than torturing the action system? The promise of the centralized system with the external table is that the author can stick achievements in there without messing with the source code proper. If the author has to go in, define these actions, perform the state checks inline, and invoke the actions, it seems like they might as well just define a set of achievements and award them directly.


#12

That’s a really good point. I’d considered that, but I’d like for other things in the game to also be able to react to those strange events.

Object -> ByStanderTroll "witness troll" with react_after [; AttackFishermanTrollWithFishDuringHighTide: (The) self, " stands aghast at the carnage."; ];Now, if I were to really have everything that I liked, then I could start dropping behavior modules onto specific pre-defined NPC objects all over the place from within the Achievements definition file to add all the custom one-off strangeness. But this is Inform 6. So, tiny little conditional inlines which cause highly idiosyncratic actions to be invoked seems like a small price to pay if the strangeness can be used as actual Actions and other objects can respond to the strangeness.


#13

The really interesting thing to me would be if you could have achievements accumulate across playthroughs by having keeping the savefile constant.


#14

Some recent games (e.g., Counterfeit Monkey, Scroll Thief) do this using auxiliary data files that the game maintains alongside the usual zcode or glulx game file.

See page 319 of the DM4 for how to do this for zcode games. For glulx, check out the file streams section of the glk spec.

I7 makes this easier (for glulx). It provides commands like read, write, and append that translate into the appropriate I6-level glk calls. See §23.11 and subsequent sections in Writing with Inform. The I7 extension Recorded Endings uses the I7 file interface to record endings the player has experienced across multiple playthroughs and was adapted into Counterfeit Monkey’s persistent achievements system.


#15

It sounds like you’re interested in an event system that affects both achievements (awarded based on events) and NPCs (who react to events). I don’t think that the achievements and the reactive NPCs need to be coupled so closely together, since it’s not necessary for NPCs to react to all achievements, nor is it necessary for all strange events to which NPCs react to award achievements. I don’t understand why you’d want to put NPC behavior modules into an achievements definition file. It seems to address a scenario where there’s an existing game and someone wants to add achievements and NPC reactions to it, but, for some reason, can’t modify the game’s source code directly.

I still think that achievements should be granted via a direct call to something like AwardAchivement(foo), but I think that weird events to which NPCs react could be modeled as actions**. The action system is a (limited) event system, so, for NPC reactions, the problem with AttackFishermanTrollWithFishDuringHighTide is mostly just the length and awkwardness of the identifier. If you defined a (shorter) fake action and had the bystander respond to that, it would probably work out.

Constant Story "EVENTS AND ACHIEVEMENTS";
Constant Headline "^An Interactive Example^";

Include "Parser";
Include "VerbLib";

Class Achievement
  with earned,
       award [;
           if (self.earned) rfalse;
           self.earned = true;
           "^[You've earned an achievement: ", (name) self, "!]";
       ];

Achievement SomethingFishy "Something Fishy"
  with description "slap the troll fisherman with a fish";

Achievement SelfConscious "Self Conscious"
  with description "check your achievements";

fake_action FishSlap;

Object Pier "Pier"
  with name 'pier' 'water', 
      description "A wooden pier juts out over the water.",
  has light;

Object -> troll_fisherman "troll fisherman"
  with name 'troll' 'fisherman',
       initial "A troll fisherman stands at the edge of the pier, fishing.",
       life [;
           Attack:
               ! relaxing constraints for the sake of shortening the example
               if (second == fish) {
                   remove self;
                   print "You savagely slap the troll fisherman with the giant
                      fish, knocking the pole from his hands. He staggers
                      forward and falls over the edge of the pier and into the
                      water with a splash.^";
                   SomethingFishy.award();
                   <<FishSlap>>;
               }
       ], 
  has animate;

Object -> fish "giant fish"
  with name 'giant' 'fish',
  has edible;

Object -> gnome_bystander "gnome bystander"
  with name 'gnome' 'bystander',
       initial "A gnome is sunbathing here.",
       react_before [;
           FishSlap:
               remove self;
               print "^The gnome is horrified by your act of fishy aggression.
                   He packs up his beachtowel and flees.^";
               ! rfalse so others can also react
       ],
  has animate;

Object -> troll_witness "troll witness"
  with name 'troll' 'witness',
       aghast,
       initial [;
           print "A troll witness is ";
           if (self.aghast) {
               "staring at you, aghast at your brutality.";
           }
           "hanging out here waiting for something to happen.";
       ],
       react_before [;
           FishSlap:
               self.aghast = true;
               print "^", (The) self, " stands aghast at the carnage.^";
       ],
  has animate;

[ Initialise;
  location = Pier;
];

Include "Grammar";

Verb 'slap' = 'attack';

Extend 'attack'
    * creature 'with' held -> Attack;

! list achievements
[ AchievementsSub ach num_ach;
  objectloop (ach ofclass Achievement) {
      if (ach.earned) {
          num_ach++;
      }
  }
  if (num_ach == 0) {
      print "You haven't earned any achievements.^";
  } else {
      print "You have earned the following achievements:^";
      objectloop (ach ofclass Achievement) {
          if (ach.earned) {
              print (name) ach, " (", (string) ach.description, ")^";
          }
      }
  }
  SelfConscious.award(); 
];

Verb meta 'achievements'
    * -> Achievements;

** Note: It’s also possible to implement your own Event and EventListener classes and do an entirely parallel event system. I coded up a brief proof of concept, and it works, but I didn’t find it super compelling for this scenario vs. fake (or fake fake) actions and it muddies the main discussion. The main benefits are: arbitrary state in events vs. (actor, action, noun, second), event delivery not limited by action processing short circuiting, and event delivery not limited by scope (vs. something like react_after).


#16

Oh wow that is nice code! I wouldn’t have thought of coding an achievement as an object but that makes a lot of sense.

Two thoughts (inspired by Khelwood’s DMenus.h):

  • Would it help to define the achievements objects as children of an AchievementList object? That would mean looping on all children of AchievementList instead of looping on all objects in the game, potentially making it faster.
  • How about using the typical properties of objects instead of a “earned” variable? Properties like “locked” (not earned yet) or “concealed” (doesn’t appear in the achievement list) could be an interesting way to do it.

Cheers :slight_smile:


#17

That is a much better idea than my file with an achievements definitions list, vlaviano!

Using vlaviano’s object-oriented approach opens up all sorts of possibilities, like constructing an entire achievements tree such that a root or parent achievement would be required to have been obtained before one of its child achievements.


(Peter Piers) #18

I’m struggling to follow all of this, but I’m still interested, in a distant, “it’s so great to see people who know what they’re doing discuss what they do” way.

Normally I just read and don’t make a peep, but in this case, I just have to ask - if you’re ditching the external file idea for an in-game object, how will the achievements remain between restarts? Wasn’t that the idea?


#19

@Peter: I think we can do both at the same time :slight_smile:

That’s true! Multi-part achievements are very easy to make using that! And achievements with “Shoot 100 ants (23/100)” can be implemented with a local variable! Possibilities!


#20

Yeah, it would help to use a container object. In fact, it might be good to use two: one for unearned achievements (they would all start as children of this object at the beginning of the game) and another for earned achievements (so the code would “move self to EarnedAchievements” rather than “self.earned = true”). That would allow us to loop only over earned achievements and to avoid the first loop in AchievementsSub that’s just counting the number of earned achievements.

No, we’re not ditching the external file idea. Most of the thread has been about how an achievement system should be structured, independent of whether they’re persistent or not. The system in my recent example could be made persistent by dumping a list of earned achievements to a file on exit and reading it in on startup. I didn’t add that because (1) I’m lazy, (2) it wasn’t one of the things that we were debating, (3) I would have had to research some glk stuff to make it platform independent (a special case of (1)). It was only meant to be an illustration of what I was talking about, but if people are interested, I could add persistence and package it up as an I6 library contribution.

Thinking about this also makes me wonder why the I7 file interface is glulx-only. Writing with Inform says (in §21.11) “If the project’s Settings panel has the story file format set to the Z-machine, the story file is so thoroughly boxed in that it cannot even see the bigger computer beyond: it lives in a world of its own. But the Glulx format opens the door a crack, allowing the story file to read and write a small number of data files, which live in a single folder on the bigger computer’s hard drive.”, but this contradicts what we know about I6 and z-assembly. From the DM4 (in §42): "The Z-machine can also load and save “auxiliary files’’ to or from the host machine.” It goes on to describe the @save and @restore zmachine opcodes. The I7 compiler’s FileIO Template doc has a section at the end called “Z-Machine Stubs” showing a set of stubs that do nothing. Why don’t they call @save and @restore? It seems like there’s a need for an I7 extension that does Z-machine file i/o.

Yes, there are some possibilities here, although we’d need to extend the auxiliary file to track partial progress for achievements like this. We could get fancy and add serialize and load routines to the Achievement class. JSON parser in I6 anyone? We could probably get away with a set of name, value string pairs without recursive structure.