My take on an achievement system

For the game I want to create, I needed an achievement system. Seeing as this is something that other people could also need, I want to share it here - unfortunately, I don't have the time to turn this into a proper library, so I can only provide you guys with my little sandbox example. With a little bit of work, you should be able to incorporate it into your game.

I'd be happy if someone actually went and turned this into a library.

Details

  • The custom status pane is used to display your progress in terms of achievements. Clicking on 'Achievements' will allow you to list all achievements you have earned
  • In this example, you will get a code representing your achievements at the end of the game (it ends at the end of the day, just type wait 60
  • You can enter that code at the start of the game to unlock these achievements
  • You earn the achievements by talking to the girl
  • Internally, achievements are objects extended from the custom type achievement(Although it does not have to extend from the type). Unearned achievements reside in the room achievements, earned ones in the room earned achievements. This room is added to the extended scope so that verbs for the achievements and object links will work
  • In this example, every achievement has the verb image. This could be used e.g. for rewarding the player with an image for unlocking the achievement. However, this can be changed or extended easily and you can have different verbs per achievement
  • Theoretically, you could extend this so that achievements give rewards (items, statuses, buffs...)

The code below contains some unrelated stuff that you can ignore (related to ClockLib and ConvLib, which you will need for this example to run).

P.S.: I tried putting <details>...</details> around the code but that destroyed the code formatting.

<asl version="550">
  <include ref="English.aslx" />
  <include ref="Core.aslx" />
  <include ref="ClockLib.aslx" />
  <include ref="ConvLib.aslx" />
  <game name="Achievement Example">
    <inherit name="theme_typewriter" />
    <gameid>4890e3d7-3676-4567-926f-c53abd25fc2e</gameid>
    <version>1.0</version>
    <firstpublished>2017</firstpublished>
    <notarealturn type="boolean">false</notarealturn>
    <statusattributes type="stringdictionary">
      <item>
        <key>clock</key>
        <value>Time: !</value>
      </item>
    </statusattributes>
    <feature_advancedscripts />
    <customstatuspane />
    <borderlesscursor type="boolean">false</borderlesscursor>
    <attr name="feature_pictureframe" type="boolean">false</attr>
    <clearframe type="boolean">false</clearframe>
    <panestheme>Classic</panestheme>
    <gridmap type="boolean">false</gridmap>
    <turnoffplacesandobjects type="boolean">false</turnoffplacesandobjects>
    <showpanes />
    <setcustomwidth type="boolean">false</setcustomwidth>
    <unresolvedcommandhandler type="script">
      game.notarealturn = true
      msg (Template("UnrecognisedCommand"))
    </unresolvedcommandhandler>
    <start type="script">
      CalcAchievementCodes
      skip = false
      msg ("Please enter your name:")
      get input {
        player.alias = result
        ask ("Do you have an achievement code from a previous playthrough?") {
          if (result) {
            msg ("Please enter the code:")
            get input {
              RedeemAchievements (result)
              msg ("")
              msg ("")
              msg ("")
            }
          }
        }
        msg ("")
        msg ("")
        msg ("")
      }
      SetTime ("01:23:00")
      game.clock = TimeAsString()
      game.notarealturn = true
    </start>
    <scopebackdrop type="script">
      foreach (obj, GetDirectChildren(earned achievements)) {
        list add (items, obj)
      }
    </scopebackdrop>
  </game>
  <command name="look">
    <pattern type="string">^look$|^l$</pattern>
    <script>
      ShowRoomDescription
      game.notarealturn = true
    </script>
  </command>
  <command name="help">
    <pattern type="string">^help$|^\?$</pattern>
    <script>
      msg (Template("DefaultHelp"))
      game.notarealturn = true
    </script>
  </command>
  <command name="clock">
    <pattern>clock;time;watch</pattern>
    <script>
      P (Replace(game_clock.clockmsg, "###", TimeAsString()))
      game.notarealturn = true
    </script>
  </command>
  <command name="waitfor">
    <pattern>wait #text#; z #text#</pattern>
    <script><![CDATA[
      if (isNumeric(text)) {
        minutes = ToInt(text)
        if (minutes > 0) {
          WaitForMinutes (ToInt (text))
        }
        else {
          P ("I don't understand how many minutes you want to wait.")
        }
      }
      else {
        P ("I don't understand how many minutes you want to wait.")
      }
      game.notarealturn = true
    ]]></script>
  </command>
  <object name="room">
    <inherit name="editor_room" />
    <usedefaultprefix type="boolean">false</usedefaultprefix>
    <prefix>your</prefix>
    <object name="girl">
      <inherit name="editor_object" />
      <inherit name="namedfemale" />
      <inherit name="talkingchar" />
      <alias>girl</alias>
      <usedefaultprefix type="boolean">false</usedefaultprefix>
      <prefix>the</prefix>
      <object name="girl hi">
        <inherit name="editor_object" />
        <inherit name="startingtopic" />
        <alias>Hi</alias>
        <exchange>"I'm the friendly achievement giver. Just ask me for an achievement and you will get it.", the girl says.</exchange>
        <nowshow type="stringlist">
          <value>girl one</value>
          <value>girl two</value>
          <value>girl three</value>
        </nowshow>
      </object>
      <object name="girl one">
        <inherit name="editor_object" />
        <inherit name="topic" />
        <hideafter />
        <exchange>"Here you go!", she says.</exchange>
        <alias>Achievement #1</alias>
        <talk type="script">
          EarnAchievement (pointlessone)
        </talk>
      </object>
      <object name="girl two">
        <inherit name="editor_object" />
        <inherit name="topic" />
        <hideafter />
        <exchange>"Very well.", the girl says.</exchange>
        <alias>Achievement #2</alias>
        <talk type="script">
          EarnAchievement (pointlesstwo)
        </talk>
      </object>
      <object name="girl three">
        <inherit name="editor_object" />
        <inherit name="topic" />
        <exchange>"Of course.", she says.</exchange>
        <hideafter />
        <alias>Achievement #3</alias>
        <talk type="script">
          EarnAchievement (pointlessthree)
        </talk>
      </object>
    </object>
    <object name="player">
      <inherit name="editor_object" />
      <inherit name="editor_player" />
      <alias>Tom</alias>
    </object>
  </object>
  <object name="events">
    <inherit name="editor_room" />
    <object name="event_01_23_59">
      <inherit name="editor_object" />
      <look type="script">
        EvaluateGame
      </look>
    </object>
  </object>
  <command name="wait">
    <pattern>wait;z</pattern>
    <script>
      WaitForMinutes (game_clock.waittime)
      game.notarealturn = true
    </script>
  </command>
  <object name="achievements">
    <inherit name="editor_room" />
    <object name="pointlessone">
      <inherit name="editor_object" />
      <inherit name="achievement" />
      <alias>pointless achievement</alias>
      <look>Earned by talking to the girl.</look>
      <image type="script">
        msg ("This achievement doesn't have an image.")
        game.notarealturn = true
      </image>
    </object>
    <object name="pointlesstwo">
      <inherit name="editor_object" />
      <inherit name="achievement" />
      <look>Earned by talking to the girl.</look>
      <alias>yet another pointless achievement</alias>
      <image type="script">
        msg ("This achievement doesn't have an image.")
        game.notarealturn = true
      </image>
    </object>
    <object name="pointlessthree">
      <inherit name="editor_object" />
      <inherit name="achievement" />
      <drop type="boolean">false</drop>
      <look>Earned by talking to the girl.</look>
      <alias>the final pointless achievement</alias>
      <usestandardverblist />
      <inventoryverbs type="stringlist">
        <value>Image</value>
      </inventoryverbs>
      <displayverbs type="stringlist">
        <value>Image</value>
      </displayverbs>
      <scenery />
      <image type="script">
        msg ("This achievement doesn't have an image.")
        game.notarealturn = true
      </image>
    </object>
  </object>
  <verb>
    <property>hide</property>
    <pattern>hide</pattern>
    <defaultexpression>"You can't hide " + object.article + "."</defaultexpression>
  </verb>
  <verb>
    <property>info</property>
    <pattern>info</pattern>
    <defaultexpression>"You can't info " + object.article + "."</defaultexpression>
  </verb>
  <verb>
    <property>image</property>
    <pattern>image</pattern>
    <defaultexpression>"You can't image " + object.article + "."</defaultexpression>
  </verb>
  <verb>
    <property>list</property>
    <pattern>list</pattern>
    <defaultexpression>"You can't list " + object.article + "."</defaultexpression>
  </verb>
  <object name="messy room">
    <inherit name="editor_room" />
    <descprefix>You are in</descprefix>
  </object>
  <object name="earned achievements">
    <inherit name="editor_room" />
  </object>
  <type name="achievement">
    <drop type="boolean">false</drop>
    <look>Earned by WIP.</look>
    <alias>WIP</alias>
    <usestandardverblist />
    <inventoryverbs type="stringlist">
      <value>Image</value>
    </inventoryverbs>
    <displayverbs type="stringlist">
      <value>Image</value>
    </displayverbs>
    <scenery />
    <code type="string"></code>
    <image type="script">
      picture ("")
      game.notarealturn = true
    </image>
  </type>
  <function name="InitUserInterface"><![CDATA[
    s = "<table width=\"100%\"><tr>"
    s = s + "   <td style=\"text-align:right;\" width=\"50%\"><a href=\"javascript:void(0);\" onclick=\"ASLEvent('ListAchievements', '');\" style=\"text-decoration:underline\">Achievements:</a></td>"
    s = s + "   <td style=\"text-align:left;\" width=\"50%\"><span id=\"achv-span\">---</span></td>"
    s = s + " </tr>"
    s = s + " <tr>"
    s = s + "   <td colspan=\"2\" style=\"border: thin solid;background:white;text-align:left;\">"
    s = s + "   <span id=\"achv-indicator\" style=\"background-color:black;padding-right:200px;\"></span>"
    s = s + "   </td>"
    s = s + " </tr>"
    s = s + "</table>"
    JS.setCustomStatus (s)
  ]]></function>
  <function name="EvaluateGame">
    SetTime ("02:23:00")
    msg ("Your achievement code is: {b:" + GetAchievementCode() + "}")
    finish
  </function>
  <function name="WaitForMinutes" parameters="minutes"><![CDATA[
    count = minutes
    game_clock.event = false
    while (count > 0 and not game_clock.event) {
      // msg("count = " + count)
      // msg("game_clock.event = " + game_clock.event)
      count = count - 1
      IncTime
    }
    if (not game_clock.event) {
      plural = " minutes"
      if (minutes < 2) {
        plural = " minute"
      }
      P ("You wait " + minutes + plural + ", but nothing happens.")
    }
  ]]></function>
  <function name="EarnAchievement" parameters="achvmnt">
    if (achvmnt.parent = achievements) {
      MoveObject (achvmnt, earned achievements)
      msg ("{b:You earned a new achievement: }" + "\"" + ObjectLink(achvmnt) + "\" (" + "{i:" + achvmnt.look + "}" + ")")
    }
    UpdateAchievementStatus
  </function>
  <function name="CalcAchievementCodes">
    foreach (item, GetDirectChildren(achievements)) {
      request (RunScript, "hashFnv32a;" + item.name)
      msg (item.code)
    }
    UpdateAchievementStatus
  </function>
  <function name="SetAchievementCode" parameters="obj">
    strlist = Split(obj, ";")
    item = GetObject(StringListItem(strlist, 0))
    item.code = StringListItem(strlist, 1)
  </function>
  <function name="RedeemAchievements" parameters="str">
    strlist = Split(str, ";")
    foreach (achvmnt, strlist) {
      foreach (item, GetDirectChildren(achievements)) {
        if (item.code = achvmnt) {
          EarnAchievement (item)
        }
      }
    }
  </function>
  <function name="GetAchievementCode" type="string">
    code = ""
    foreach (item, GetDirectChildren(earned achievements)) {
      code = code + item.code + ";"
    }
    if (code = "") {
      return ("Unfortunately, you do not have any achievements!")
    }
    return (code)
  </function>
  <function name="UpdateAchievementStatus">
    num_achievements = ListCount(GetDirectChildren(earned achievements))
    num_total = ListCount(GetDirectChildren(achievements)) + num_achievements
    percentage = 100 * (1.0 * num_achievements/num_total)
    split_value_list = NewStringList()
    string_value = ToString (percentage)
    split_value_list = split(string_value,".")
    string_integer = ListItem(split_value_list,0)
    result = ToInt(string_integer)
    JS.eval ("$('#achv-span').html('" + num_achievements + "/" + num_total + " (" + result + "%)" + "');")
    JS.eval ("$('#achv-indicator').css('padding-right', '" + (1 + 199 * num_achievements / num_total) + "px');")
  </function>
  <function name="ListAchievements" parameters="notaparameter">
    Ask ("Do you want to list your achievements?") {
      if (result) {
        index = 1
        foreach (item, GetDirectChildren(earned achievements)) {
          msg ("{b:" + DisplayNumber(index, "3.0") + "}" + " " + ObjectLink(item) + ":")
          msg ("{i:" + item.look + "}")
          index = index + 1
        }
        if (index = 1) {
          msg ("{b:You don't have any achievements yet!}")
        }
      }
      game.notarealturn = true
    }
  </function>
  <javascript src="achievements.js" />
</asl>

achievements.js:

/**
 * Calculate a 32 bit FNV-1a hash
 * Found here: https://gist.github.com/vaiorabbit/5657561
 * Ref.: http://isthe.com/chongo/tech/comp/fnv/
 *
 * @param {string} str the input value
 */
function hashFnv32a(str/*, asString*/) {
    /*jshint bitwise:false */
    var i, l,
        hval = 0x811c9dc5;

    for (i = 0, l = str.length; i < l; i++) {
        hval ^= str.charCodeAt(i);
        hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
    }
	
	ASLEvent("SetAchievementCode", str + ";" + ("0000000" + (hval >>> 0).toString(16)).substr(-8));
}

I guess that's cool. Looks rather complicated, really.


I guess that's cool. Looks rather complicated, really.

Well, it does use many of the more advanced features of quest.


awesome library !!!! thanks for making it and providing it to us!


I'm still a coding noob, so I'd create something just using list/dictionary Attributes:

http://textadventures.co.uk/forum/samples/topic/vlpnofgpquokwe_s2yvc9g/quest-task-mission-award-trophy-etc-system
(I'm lazy, so it's just the beginnings/skeleton of it, I think, and/or it's actually completed but very simplistic. My simple attempt at such a system using only list/dictionary attributes, as I'm just a code noob, no fancy data structures and etc advanced designs that you did in your library)


(I'm still stuck on Data Structures/Management, failed the programming class on this type of stuff, sighs: linked lists, trees, maps, stacks, queues, dictionaries, etc. The coding is simple in hindsight, but my brain just gets confused/lost, it's not used to thinking in this structural way, sighs)

(and Assembly Language and the basics of computer architecture: circuitry/digital-design/logic-gates/half-adders/full-adders/encoders/decoders/registers/flip-flops/mutex'es-multiplexors/combinational-circuits/sequential-circuits/etc-etc-etc, last two programming classes, data structures/management and assembly_language+computer_architecture, that I got to pass to finish up lower division programming classes, grr... and than I'll likely have more trouble with the upper division programming classes... lol... sighs)


awesome library !!!! thanks for making it and providing it to us!

Glad that you like it.

I'm still a coding noob, so I'd create something just using list/dictionary Attributes:
http://textadventures.co.uk/forum/samples/topic/vlpnofgpquokwe_s2yvc9g/quest-task-mission-award-trophy-etc-system
(I'm lazy, so it's just the beginnings/skeleton of it, I think, and/or it's actually completed but very simplistic. My simple attempt at such a system using only list/dictionary attributes, as I'm just a code noob, no fancy data structures and etc advanced designs that you did in your library)

Well in this case I didn't actually use any datastructures. What I did was make use of Quest's objects and rooms. Basically, they are the datastructure. And this works quite well with Quest. Maybe you can try this approach too, having custom types for quests, tasks, trophys etc. and then simply creating them as objects in the GUI with the necessary attributes and scripts plus some functions that help you along the way.


ya, I was just speaking in general, if you had a big or complex enough system, then maybe an advanced data structure design would be useful.

I know you used Objects for your design, and with quest, they work very well, especially as you can use Delegates with Script Attributes to give them the same functionalities of 'returning a value' and 'Arguments/Parameters' as Functions have:

// ----------------

// this can kind of be seen as a (it's similar to a) 'prototype', except its for Script Attributes, and not Functions:

<delegate name="return_sum_of_two_integers_delegate" parameters="integer_1_parameter, integer_2_parameter" type="int" />

// -----------------

<object name="sum_of_two_integers_object">
  <attr name="sum_integer_attribute" type="int">0</attr>
  <attr name="set_sum_script_attribute" type="script">
    this.sum_integer_attribute = do (this, "return_sum_of_two_integers_script_attribute", 7, 4)
  </attr>
  <attr name="return_sum_of_two_integers_script_attribute" type="return_sum_of_two_integers_delegate">
    if (IsInt (integer_1_parameter) and IsInt (integer_2_parameter)) {
      return (return integer_1_parameter + integer_2_parameter)
    } else {
      msg ("Error: both of your inputs must be integer values)")
    }
  </attr>
</object>

What I did was make use of Quest's objects and rooms. Basically, they are the datastructure. And this works quite well with Quest.

This is what I do a lot!


For info, I've created two educational games that use the same general achievement approach (Giantkiller Too and L Too). In my approach, the player carries an achievement certificate, which can be consulted at any time (and can't be dropped). There are two types of achievement: (i) number of locations covered; and (ii) puzzles completed, with accompanying score. At the beginning of the game, the number of locations, number of puzzles and the possible total score are shown on the certificate. There is also allowance for 'hints', which reduce the score, and an assumption that a 'reward sound' is played when puzzles are completed. It's probably easier to just play one of the games to see how it works out.

I'm happy to share the code if anyone is interested.


My first attempt used an object in the Inventory as well, but then I changed it to this variant where it is part of the custom status pane.
However, the most important aspect of my achievement system is the ability to restore achievements for the next playthrough. This is something that many people asked and struggled with. The rest of my system is still only basic (and I will probably extend it while creating my game).


Hi Clocktown, I don't understand what you mean by "restoring achievements"? Why are the built-in facilities to save a game and restore it insufficient?


I think maybe Clocktown is refering to doing a 'new game', but wanting to preserve game/character stats/etc from the previous game, for the new game. There's quite a few games like this, where you can replay the game, while still keeping all your stats/equipment/etc

for example, not every achievement or whatever can be done within a single playthrough / game, so you have to do multiple play throughs / games, to get a 100% achievement and/or whatever.

my guess anyways


K.V.

I assume it loads the saved stats via JS functions and ASLEvents?


hegemonkhan got it right. It's meant for games that would have to be played multiple times to get all achievements.

I assume it loads the saved stats via JS functions and ASLEvents?

It uses JS functions, ASLEvents and Callbacks, yes. However, all it does is compute Hashes of the achievement names at the beginning of the game. Once the game ends, you will basically get these Hashes of your earned achievements, separated with a semicolon. When you enter this code at the beginning of the game, these hashes are simply compared to the computed hashes and the respective achievements are earned again.


you can preserve whatever: character stats/equipment, achievements/trophies, team-party-members, whatever else. Just threw out some options/ideas of what some games preserve for multiple playthroughs / games.

I think clocktown is just doing achievements/trophies, not character stats/equipment, nor whatever else. I just mentioned those other things, as examples of other things that multiple-playthrough games can/do preserve (depending on the game of course).


Sorry, still scratching my head here! I'm probably better waiting to see this idea in operation in a full game...or can you point out some existing games where the facility would be useful?


DavyB, e.g. think about some Dating-kind-of game. Depending on your actions, you'll get on the route for character X, and depending on further actions, you might get a bad or a good ending. One possibility to track the overall completion would be Achievements. These could also unlock images in a gallery. However, seeing as the player will always start anew, you somehow need to track this progress across multiple sessions. In my case, I chose this code system. You could of course also save this information in a file and load it on startup. This is entirely different from the built-in save system, which does simply not serve this purpose.


I'm wondering if rather than displaying a code, you could use the Javascript cookie functions (for the web version, at least). Not sure if Quest already uses cookies, but if you can put that code in a cookie, you could have achievements persist transparently between multiple attempts at a game.


I'm wondering if rather than displaying a code, you could use the Javascript cookie functions (for the web version, at least). Not sure if Quest already uses cookies, but if you can put that code in a cookie, you could have achievements persist transparently between multiple attempts at a game.

If anything, I'd probably include both the code and the cookie system. So if someone accidentally eats the cookies, he still might have the code available.
However, as for the Desktop Version; I think it uses Chrome or something as it's base anyway, so maybe cookies work even there? Maybe someone can shed some light on this.


It might be something as simple as JS.eval("if(typeof(localStorage) !== \"undefined\") { localStorage.setItem(\"gamename_achievements\",\""+GetAchievementHash()+"\"); }") in the function that awards a new achievement; and a matching getItem() in the init script to retrieve them again. Noting that for the item name includes the game name; because (on the web version; not sure about offline) all games on the same server would share a localStorage object.

Or, of course, you could define a javascript function for gaining an achievement. Which would make it pop up in the corner of the screen, play a tinny little fanfare, and also save the achievement to local storage if possible. If your list of achievements gained is stored in both the game object, and in a javascript object, web users could look through the list and see which ones they've got without having to wait for a server response … and presumably, a sequel could give you bonus gear if you've got all the endings to the first instalment.


This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums