Implementing achievements in Inform 6

The Z-machine @save and @restore opcodes write and read a block of data from memory. This is not compatible with the way I7 writes table data (which is stream-based).

You’d need to reserve a large chunk of RAM to write the table data to before @save, and an I7 Z-machine game is basically out of RAM already.

Hmm. I have an idea. The summary being: I7 Z-machine games are out of RAM, so how about we use the local filesystem instead?

The first version of my idea was: maybe reserve a small buffer and write many little files (one per n rows, where n is small) with structured filenames from which the table data can later be reassembled. These would need to be dumped somewhere out of the way so that they don’t wind up in the user’s homedir.

I realize that this doesn’t address the existing way that I7 writes table data. So, the second, much nuttier incarnation of my idea is: imagine a block file system on top of which a typical posix-style stream-based interface is implemented. Further imagine that the buffer cache is tiny, perhaps only 1 block. What if we were to implement a file system like this where the primitives (“blocks”) were actually little fixed size files with names indicating their “block number” that the z-machine @restores and @saves where a normal file system would read and write a block? We implement directories. On top of this fs, we implement the subset of the glk interface that the FileIO template uses. Then I7 replaces its glk calls with wrapper calls that check the platform and then either call glk directly or call z-fs’s glk API that’s ultimately implemented in terms of @save/@restore.

Once upon a time, we used the term “Z-machine abuse” for this sort of clever idea. :slight_smile:

Ok, well, putting the z-machine abuse on the back burner for the time being, I’ve spent some time building on my earlier achievements code snippet.

I haven’t yet added persistence, but I’ve implemented several flavors of achievements:

  • Achievement: normal achievements
  • CountingAchievement: achievements that require an action to be repeated some number of times
  • MultiAchievement: achievements that require a set of distinct tasks to all be performed
  • MetaAchievement: achievements that require a set of other achievements to be completed (meta achievements can depend on other meta achievements)
  • hidden achievements: don’t show up in the list of available or in-progress achievements (any of the previous types can also be hidden)

The code’s longish, so I’ve zipped it up and attached it. Here’s a brief excerpt showing the creation of several kinds of achievements in the demo program:

Include ">achievements.h";

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

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

CountingAchievement SwatMaster "Swat Master"
  with description "swat 10 flies",
       target_count 10;

! Tasks for ScratchNSniff achievement:
! 0: scratch fisherman troll
! 1: sniff fisherman troll
! 2: scratch gnome
! 3: sniff gnome
! 4: scratch witness troll
! 5: sniff witness troll

MultiAchievement ScratchNSniff "Scratch 'N' Sniff"
  with description "scratch and sniff all other characters",
       num_tasks 6;

Achievement BuyAVowel "Buy a Vowel"
  with description "say the magic word",
  has concealed; ! hidden achievement

Achievement WhatYourMomSays "What Your Mom Says"
  with description "greet the fleet",
  has concealed;

MetaAchievement SupahSecret "Supah Secret"
  with description "earn all hidden achievements",
       depends_on BuyAVowel WhatYourMomSays; 

MetaAchievement DoItAll "Do It All"
  with description "earn all achievements",
       depends_on SomethingFishy SelfConscious SwatMaster ScratchNSniff
           SupahSecret;

My next step will be implementing persistence. In the meantime, I’m interested in any feedback. Thanks.

Edit: Removed outdated snapshot. Look for a fresher one later in the thread.

There is an extension in the IF Archive for implementing a diary or book with missing pages. When the player finds a page, it becomes readable in the book. If I wanted to implement achievements in Inform6, I’d start with that extension.

This looks very good! I’ve only looked at the code, not tested it, but it looks nice (I’ve noted a few things, like the comment in AchievementsInit: have they been tested at all?). I can’t really think of any other achievement behavior… And I like the initialization function that moves everything to UnearnedAchievements, it’s pretty convenient from the coder’s perspective; the rest of the code also looks pretty efficient, especially the use of flags.

I have a couple remarks:

  • Is there a function that would allow me to check the status of a task that’s part of a MultiAchievement? If not, could there be? Something like ScratchNSniff.is_task_completed(4) that would return 0 or 1 depending on whether the witness troll has been scratched. It would be useful if other parts of the code depended on it; then you could do away with declaring an extra variable witness_scratched to remember it. (And it’s not really easy to check it by hand because it is coded in one of several integers.)
  • What’s the use of add_target_task ? And for that matter targetflags? I’m not sure I understood why there are two different arrays in MultiAchievements.
  • The fact that some achievements are hidden, and are marked as so, even after you earned them is slightly odd to me. To me, if you found a hidden achievement, well, you didn’t know you could have it, but you got it; it’s weird that the game sets them apart from the rest after you got them. The games I’ve seen either had an incomplete list of achievements (as in, there are 60 of them and we’ll tell you how to get 30), or more frequently, had secret achievements for which you didn’t know the description, and sometimes not even the names. Something like

Or, instead of “5 hidden achievements”, list their names without description (or better, the description could be “hidden”). I don’t know which behavior is the best, so it could be an option parametrized by a flag - say, if you set SHOW_HIDDEN_ACHIEVEMENTS_NAMES to 0, it writes “5 hidden achievements”, otherwise it writes them as “What Your Mom Says (hidden)” etc. If you see what I mean? And what do you think of the idea?

Thank you so much for this! I’ve been wanting such an extension for a while :slight_smile: I will probably use this soon! (oh, of course, I’m assuming I can use it if I credit you in the acknowledgements?)
Thanks!

Thanks for the pointer. I’ll take a look at it.

Yeah, I’ve tested it and haven’t seen any problems. If I were looping over x in y, it would clearly be an issue. I’m looping over x ofclass y, and I wasn’t sure how it was implemented internally.

No, I’ll add one. In the meantime, you can use FlagOn(ScratchNSniff.&taskflags, 4) to test this.

add_target_task and targetflags are there to support using arbitrary task numbers in the range [0, 63] rather than having to use a contiguous set of task numbers starting at 0. targetflags is a bitvector of tasks that must be completed for the achievement to be completed, and add_target_task sets a task as being required. So, one can create an achievement that requires tasks 1, 3, and 42, by defining the object like this:

MultiAchievement MultiFoo "Multi Foo"
  with description "do some stuff",
          init [;
              self.add_target_task(1);
              self.add_target_task(3);
              self.add_target_task(42);
          ];

Your comment has made me question whether there’s really any benefit in doing this. I think that, unless the range of flags includes the entire range of object IDs (which would be extreme overkill for glulx) and thus facilitates using object IDs as task numbers via add_target_task(self), it’s probably not worth it. I think I’ll get rid of this and mandate that the task numbers must start at 0 and be contiguous. That would allow us to ditch targetflags and add_target_task.

Originally, I didn’t do anything special with hidden achievements other than hiding them. Then, I implemented meta achievements. Initially, Do It All depended on all other achievements directly, including hidden ones. That created some weirdness when Do It All was in-progress. It would show (progress: n/m) when there weren’t m visible achievements. So, I decided to emphasize hidden achievements more. The player might be still be confused when seeing the m, but I hoped that it would at least be more clear once hidden achievements were awarded that they had been hidden.

I like your idea of “5 hidden achievements”. I’m inclined to make that the default behavior. I can also see people wanting hidden achievements to really be hidden in every way. That leaves us with 3 options: “Foo (hidden)”, “5 hidden achievements”, and nothing shown at all. This could either be configured on a per-achievement basis or overall. So maybe we have a style variable that can be set to 0, 1, 2, or 3 (with some symbolic constants defined: HIDDEN_STYLE_DEFAULT, HIDDEN_STYLE_SHOW_NAMES, HIDDEN_STYLE_SUMMARIZE, HIDDEN_STYLE_HIDE). The author can set a default by passing it to AchievementsInit() and individual hidden achievements can override the default by setting a hidden_style property.

Sure. I plan on holding off uploading it to the archive until it’s at 1.0 and has persistence implemented, but feel free to use it w/ acknowledgement (this goes for anyone else reading as well).

Personally, I find achievements one of the worst ideas in today games. There’s a whole host of trophy and achievement hunters these days who believe gaming to be just that: a way to earn trophies and proudly showcase them to online fellows, like having their hard-earned photo in the employee of the day frame. You know, as opposed to simply playing for the fun of it.

Besides, I play IF as a role-playing story, not a game. I don’t have goals, let alone achievements, in mind. I only play to read more of the story.

Anyway, if you insist, the way some (older) puzzlefests inform the score sound remarkly like achievements themselves. In Curses, for instance, you get the usual score points displayed each time you deserve and when you say “full score” you get a nice table of each “achievement”.

Well, your preferences are your preferences. “Playing for the fun of it” can mean experimenting aimlessly or it can mean aggressively competing for status with other players. It depends on what a player considers fun.

I doubt that the authors of the kinds of story-based IF works that you enjoy would choose to add achievements unless they thought that it would add something to the experience. Certainly, the mere existence of an extension like this won’t compel them to do so. And if, against all reason, they add them anyway, you can type “ach off” to turn off achievement notifications and enjoy the game in blissful ignorance of the achievements that you may or may not be earning.

Your observation about the full score system is true and, near the beginning of the thread, I suggested that the OP take a look at that system as a starting point for implementing achievements. However, there are some differences between achievements and “full score”: achievements are decoupled from the score, achievements can tease the player for failures or suboptimal choices without docking their score, achievements can encourage replaying and experimentation in a way that score doesn’t, and persistent achievements can reflect progress across multiple playthroughs of the game and across mutually exclusive paths.

Ah, I understand now - yeah, the ability to choose internally how tasks are numbered doesn’t seem like a big deal :slight_smile: And it makes the code simpler!

As for hiding, I like the “5 hidden achievements” as the default! The ability to override how things are displayed on a per-achievement basis is interesting, but what if I want to change how all of them are displayed? (i.e. make it so they’re all like “What Your Mom Says (hidden)”?) Would I have to change the local variable in every single object?
I think a global switch that changes how they’re all displayed (i.e. “5 hidden” or “ach (hidden)” or nothing) is simpler and probably covers most of people’s needs; if I really want to hide all achievements except one that’ll appear as hidden, I’ll just do

I mean I could be wrong, but I think that saying “pick a style or do a rather simple hack” is better than “don’t forget to change this local variable in all the hidden achievements if you don’t want the default”?

Again, thanks! :slight_smile:

EDIT: oh, duh, of course, I’d just need to change the local variable in the HiddenAchievement class, say in Initialise(). Mmh. I don’t know, then, that is pretty simple… Keep with your original idea :slight_smile: (Although, maybe you need a routine like ChangeDisplayOfAllHiddenAchievements(HIDDEN_STYLE_SHOW_NAMES), for people who have the same brainfart as me? Or maybe it’s fine.)

No. The idea is to pass a default style as a parameter into AchievementsInit (called from Initialise) which will apply to all hidden achievements. Then, any achievements that want behavior other than that default can set a nonzero hidden_style property value to override it. My ramble about hidden achievements was obviously less clear than it could have been. Picking out the relevant piece of it:

Gotcha, thanks! :slight_smile:

I have a quick comment : right now the command “achievements” gives a list like

The last bit sounds weird: it might be better if you wrote “there are no other achievements available”?

I hacked together a little bit of code so that, in Vorple for I6, the achievement notification displays in a little notification window. It’s not very elegant and required me to declare one more array… but at least now I can add achievements to my newest game, which is what I wanted :slight_smile: I don’t know if you’re interested by this little bit of code, but let me know if you are.

Sure.

Yeah, I’d like to see it. If you’re willing and it’s possible (via #ifdef VORPLE or somesuch), I’d be happy to integrate it into the extension.

Since you bumped the topic, I might as well give a little status update.

I’ve implemented your MultiAchievement suggestions from our previous discussion, and I have persistent achievements working for glulx. I still need to do zcode persistence, the hidden achievement style stuff, and some optimizations for persistence so that loads and saves happen less frequently. Right now saves happen whenever ach state changes (but I do batch together the updates for a cascade of meta achievements resulting from one call to award), and loads happen at every restart, restore, and undo.

I’ve also come up with a couple other features that I intend to prototype: failable achievements (do x without ever having done y) and revealable achievements (hidden achievements that are revealed at a certain point in the game; one can build a poor man’s hint system or goals system with this).

I’ll drop another development snapshot here in the next few days.

Failable and revealable achiements sound good! Eagerly expecting the next snapshot :slight_smile:

Here’s the code (you just need to change Achievement.award)

[spoiler][code]
Array achstr buffer 200;

! A vanilla achievement. By default, it has binary state (earned / unearned)
! and is never in progress.
Class Achievement
with award [i; if (self in EarnedAchievements) rfalse; move self to EarnedAchievements; if (achievements_notify) { @output_stream 3 achstr; print “^[You’ve earned a”; if (self has concealed) { print " hidden"; } else { print “n”; } print " achievement: ", (name) self, “!]^”;
@output_stream -3;
#Ifdef VORPLE_LIBRARY;
if (isVorpleSupported()) { ! in-browser version
VorpleNotification(achstr);
} else { ! Z-machine fallback
print (string) achstr;
}
#Ifnot;
print (string) achstr;
#Endif;
}
! notify any meta achievements that depend on us
for (i = 0: i < self.num_dependents: i++) {
(self.&dependents–>i).complete_task(
self.&dependent_tasknums–>i);
}
],
[/code][/spoiler]

Thanks!

Thanks for the patch.

Regarding your proposed ‘achievements’ command output change, what do you think about this scenario:

Does “other” seem weird to you when no specific achievements have been mentioned by the preceding output?

Oh, you’re right. Mmh. Does “There aren’t any achievements available” sound any better? Sorry, the difference might just be in my head, and English isn’t even my first language :slight_smile: I just thought the first one meant (or could be misconstrued as) “There are no achievements in this game”, like " There is no score in this game". Anyway, pick whichever one you want, i’m probably not qualified for this :slight_smile:

I went with “There are no [other] achievements available at this time.”

As promised, a new snapshot is attached. The main new feature is persistent achievements for zcode and glulx. See the README file for details.

The glulx side of things went relatively smoothly, but I had some issues on the zcode side. Most of the interpreters that I tested (unix frotz, gargoyle, spatterlight, zoom) ignore the ‘prompt’ argument to the @save and @restore opcodes and prompt the player every time, which is somewhat of a dealbreaker given the frequency of saves and loads. Frotz also seems to have some bugs in it related to @save/@restore. I’ll make a separate post about that once I’ve investigated further.

Edit: Removed outdated snapshot. Look for a fresher one later in the thread.

I finally got around to doing this. See this thread over on the interpreters board.

*** You have been zarfed ***

Well, it turns out that the issue was with my code rather than with frotz (or, as we used to say, “no, select() isn’t broken.”). So here’s a quick bugfix release. The only change is to pass the achievements auxiliary filename to @save and @restore in the proper spec-compliant way so that interpreters won’t barf on it.

The lack of support for the prompt argument to @save and @restore is still an issue, but there are some security implications to work through there. I expect that looking at the constraints that glk imposes would be a good start. Another thread on that later.

Edit: Removed outdated snapshot. Look for a fresher one later in the thread.