Write your own achievement¶
Achievement without persistent data¶
Suppose you want to create an achievement Foo
awarded when user successfully run a command on a file foo
. Let’s write this achievement.
Meta-information¶
First, we need to define a class and define meta-information: any achievement is a subclass of Achievement
. Two arguments are compulsory:
title
: ifNone
, your class is an abstract achievement, meant to be subclassed; if a string, your achievement is an actual achievement. See theclass documentation
for other attributes;description: your achievement must have a description. The first non-empty line of your class docstring is used, unless
_description
is defined, when it is used instead.
See the class documentation
to get more information about other attributes.
from clachievements.achievements import Achievement
from clachievements.testutils import test_lock, test_unlock
class Foo(Achievement):
"""Successfully run a command on file `foo`."""
title = "Foo"
Unlocking the achievement¶
Great: you have an achievement. But it is never unlocked: it will be frustrating for the user.
An achievement is a context manager: its __enter__()
and __exit__()
methods are called before and after the actual system call. They can be used to test the command line, the environment before and after the command, etc.
Here, we test that:
foo
is a positional argument;the command did not fail.
If so, we call unlock()
to unlock the argument. It ensures that the argument is marked as unlocked, and it displays a pop-up to notify the user. No need to make sure that parallel calls to your achievement might unlock it at the same time: it is handled within the unlock()
method itself.
from clachievements.achievements import Achievement
from clachievements.testutils import test_lock, test_unlock
class Foo(Achievement):
"""Successfully run a command on file `foo`."""
title = "Foo"
def __exit__(self, exc_type, exc_value, traceback):
if "foo" in self.command.positional:
if isinstance(exc_value, SystemExit):
if exc_value.code == 0:
self.unlock()
Testing¶
If we are done, the achievement will work, but the unit tests will fail. An achievement must define a test that unlock the achievement.
Each achievement must define a static or class method, decorated with test_unlock()
. This method must iterate strings which are shell commands, unlocking the achievement. To be wrapped by CLAchievements, system calls must use string substitution: "foo bar"
will call the foo
binary, not wrapped by CLAchievements, where "{bin.foo} bar"
will call the foo
binary, wrapped by CLAchievements.
You can add as many test methods as you want. You can also define test methods that must not unlock achievements, by decorating them with test_lock()
.
When performing tests, each test method is run inside an empty temporary directory, which will be deleted afterward.
from clachievements.achievements import Achievement
from clachievements.testutils import test_lock, test_unlock
class Foo(Achievement):
"""Successfully run a command on file `foo`."""
title = "Foo"
def __exit__(self, exc_type, exc_value, traceback):
if "foo" in self.command.positional:
if isinstance(exc_value, SystemExit):
if exc_value.code == 0:
self.unlock()
@staticmethod
@test_unlock
def test_touch():
yield "{bin.touch} foo"
@staticmethod
@test_lock
def test_ls():
yield "{bin.ls} foo"
Achievement with persistent data¶
Now, we want a new achievement FooBar
to be triggered when 50 successful commands have been run on a file foo
. Let’s do this.
To do this, we have to store the number of successful commands. A class is defined to ease this process: SimplePersistentDataAchievement
. It is wrong (see below), but is works for simple cases.
When using this class, a row is created in the CLAchievements database with this achievement name.
The first time this achievement is created, this row is filled with the content of attribute
default_data
.When accessing to
data
, data is read from the database.When assigning a value to
data
, data is written to the database.
Any picklable
data can be stored using this method.
This is simple, but this is not robust to concurrent access: if an integrity error occurs when assigning a value to data
, it is silently ignored.
With this example achievement, if I run this argument 50 times in parallel, about 30 of the assignments are ignored. If I were to design a life critical application, this would be a big issues. But this is only a game: it does not work perfectly, but it is so much simpler to implement!
from clachievements.achievements import SimplePersistentDataAchievement
from clachievements.testutils import test_lock, test_unlock
class FooBar(SimplePersistentDataAchievement):
"""Successfully run 50 command on file `foo`."""
title = "FooBar"
default_data = 0
def __exit__(self, exc_type, exc_value, traceback):
if "foo" in self.command.positional:
if isinstance(exc_value, SystemExit):
if exc_value.code == 0:
self.data += 1
if self.data >= 50:
self.unlock()
@staticmethod
@test_lock
def test_touch():
for _ in range(49):
yield "{bin.touch} foo"
@staticmethod
@test_unlock
def test_ls_touch():
for _ in range(25):
yield "{bin.touch} foo"
yield "{bin.ls} foo"
More¶
Suppose this error-prone persistent data management does not suit you. Just write your own: within the achievement, the sqlite3 database connection
is available as self.database.conn
. Do whatever you want with it (without breaking other plugin databases)!
In this case, to be sure not to mess with tables of CLA core or other plugins, use the tables named (case insensitive) achievement_YourPluginName
or achievement_YourPluginName_*
.
Methods first()
and last()
can be used to initialize or clean the achievement: the first one is called the first time the achievement is ever loaded (so it can be used to create some tables into the database), while the last one is called when the achievement has just been unlocked (so it can be used to clean stuff). Both these methods are meant to be subclassed, and are expected to call super().first(...)
at the beginning of their code.