9b18a6bad6004a6e71d08273b122c7ac61e22590
[nit.git] / contrib / nitrpg / src / achievements.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # `nitrpg` achievements.
18 #
19 # Players can unlock achievements by performing remarkable actions on the repo.
20 # Achievements are rewarded by nitcoins.
21 module achievements
22
23 import events
24 import statistics
25
26 redef class GameEntity
27
28 # Register a new achievement for this game entity.
29 #
30 # Saves the achievement in game data.
31 # Do nothing is the achievement is already registered.
32 #
33 # TODO should update the achievement?
34 fun add_achievement(achievement: Achievement) do
35 var key = self.key / achievement.key
36 if game.store.has_key(key) then return
37 stats.inc("achievements")
38 achievement.save_in(self.key)
39 save
40 end
41
42 # Load the event from its `id`.
43 #
44 # Looks for the event save file in game data.
45 # Returns `null` if the event cannot be found.
46 fun load_achievement(id: String): nullable Achievement do
47 var key = self.key / "achievements" / id
48 if not game.store.has_key(key) then return null
49 var json = game.store.load_object(key)
50 return new Achievement.from_json(game, json)
51 end
52
53 # List all events registered in this entity.
54 #
55 # This list is reloaded from game data each time its called.
56 #
57 # To add events see `add_event`.
58 fun load_achievements: MapRead[String, Achievement] do
59 var res = new HashMap[String, Achievement]
60 var key = self.key / "achievements"
61 if not game.store.has_collection(key) then return res
62 var coll = game.store.list_collection(key)
63 for id in coll do
64 res[id.to_s] = load_achievement(id.to_s).as(not null)
65 end
66 return res
67 end
68 end
69
70 # Achievements are rewarded by `nitcoins`.
71 #
72 # An achievement represents a notable action performed by a `Player`.
73 # Player that `unlock` achievements are rewarded by nitcoins.
74 class Achievement
75 super GameEntity
76
77 redef var key is lazy do return "achievements" / id
78
79 redef var game
80
81 # Uniq ID for this achievement.
82 var id: String
83
84 # Name of this achievement.
85 var name: String
86
87 # Description of the achievement.
88 var desc: String
89
90 # Reward that this achievement give in nitcoins.
91 var reward: Int
92
93 # Is this achievement unlocked by somebody?
94 var is_unlocked: Bool is lazy do return not load_events.is_empty
95
96 # Init `self` from a `json` object.
97 #
98 # Used to load achievements from storage.
99 init from_json(game: Game, json: JsonObject) do
100 init(game, json["id"].to_s, json["name"].to_s, json["desc"].to_s, json["reward"].as(Int))
101 end
102
103 redef fun to_json do
104 var json = super
105 json["id"] = id
106 json["name"] = name
107 json["desc"] = desc
108 json["reward"] = reward
109 return json
110 end
111 end
112
113 redef class Player
114
115 # Is `a` unlocked for this `Player`?
116 fun has_achievement(a: Achievement): Bool do
117 return load_achievement(a.id) != null
118 end
119
120 # Unlocks an achievement for this Player based on a GithubEvent.
121 #
122 # Register the achievement and adds the achievement reward to the player
123 # nitcoins.
124 #
125 # Do nothing is this player has already unlocked the achievement.
126 #
127 # TODO: add abstraction so achievements do not depend on GithubEvent.
128 fun unlock_achievement(a: Achievement, event: GithubEvent) do
129 if has_achievement(a) then return
130 nitcoins += a.reward
131 add_achievement(a)
132 trigger_unlock_event(a, event)
133 end
134
135 # Create a new event that marks the achievement unlocking.
136 fun trigger_unlock_event(achievement: Achievement, event: GithubEvent) do
137 var obj = new JsonObject
138 obj["player"] = name
139 obj["reward"] = achievement.reward
140 obj["achievement"] = achievement.id
141 obj["github_event"] = event.json
142 var ge = new GameEvent(game, "achievement_unlocked", obj)
143 add_event(ge)
144 game.add_event(ge)
145 achievement.add_event(ge)
146 end
147 end
148
149 # `GameReactor` dedicated to achievements unlocking.
150 interface AchievementReactor
151 super GameReactor
152
153 # Unic ID of the achievement this reactor unlocks.
154 fun id: String is abstract
155
156 # Name of the achievement this reactor unlocks.
157 fun name: String is abstract
158
159 # Description of the achievement this reactor unlocks.
160 fun desc: String is abstract
161
162 # Amount of nitcoins rewarded for unlocking the achievement.
163 fun reward: Int is abstract
164
165 # Return a new instance of the achievement to unlock.
166 fun new_achievement(game: Game): Achievement do
167 var achievement = new Achievement(game, id, name, desc, reward)
168 game.add_achievement(achievement)
169 return achievement
170 end
171 end
172
173 #####################
174 ### Issues
175 #####################
176
177 # Unlock achievement after X issues.
178 #
179 # Used to factorize behavior.
180 abstract class PlayerXIssues
181 super AchievementReactor
182
183 # Number of PR required to unlock the achievement.
184 var threshold: Int is noinit
185
186 redef fun react_event(game, event) do
187 if not event isa IssuesEvent then return
188 if not event.action == "opened" then return
189 var player = event.issue.user.player(game)
190 if player.stats["issues"] == threshold then
191 var a = new_achievement(game)
192 player.unlock_achievement(a, event)
193 end
194 end
195 end
196
197 # Player open his first issue.
198 class Player1Issue
199 super PlayerXIssues
200
201 redef var id = "player_1_issue"
202 redef var name = "First complaint"
203 redef var desc = "Open your first issue."
204 redef var reward = 10
205 redef var threshold = 1
206 end
207
208 # Player open 100 issues.
209 class Player100Issues
210 super PlayerXIssues
211
212 redef var id = "player_100_issues"
213 redef var name = "Mature whiner"
214 redef var desc = "Open 100 issues in the game."
215 redef var reward = 100
216 redef var threshold = 100
217 end
218
219 # Player open 1 000 issues.
220 class Player1KIssues
221 super PlayerXIssues
222
223 redef var id = "player_1000_issues"
224 redef var name = "You, sir, complain a lot"
225 redef var desc = "Open 1000 issues in the game."
226 redef var reward = 1000
227 redef var threshold = 1000
228 end
229
230 # Player open an issue about nitdoc.
231 class IssueAboutNitdoc
232 super AchievementReactor
233
234 redef var id = "issue_about_nitdoc"
235 redef var name = "Say nitdoc again, I double dare you!"
236 redef var desc = "Open an issue with \"nitdoc\" in the title."
237 redef var reward = 10
238
239 redef fun react_event(game, event) do
240 if not event isa IssuesEvent then return
241 if not event.action == "opened" then return
242 var player = event.issue.user.player(game)
243 var re = "nitdoc".to_re
244 re.ignore_case = true
245 if event.issue.title.has(re) then
246 var a = new_achievement(game)
247 player.unlock_achievement(a, event)
248 end
249 end
250 end
251
252 # Player open an issue about FFI.
253 class IssueAboutFFI
254 super PlayerXIssues
255
256 redef var id = "issue_about_ffi"
257 redef var name = "Polyglot what?"
258 redef var desc = "Open an issue with `ffi` in the title."
259 redef var reward = 10
260
261 redef fun react_event(game, event) do
262 if not event isa IssuesEvent then return
263 if not event.action == "opened" then return
264 var player = event.issue.user.player(game)
265 var re = "\\bffi\\b".to_re
266 re.ignore_case = true
267 if event.issue.title.has(re) then
268 var a = new_achievement(game)
269 player.unlock_achievement(a, event)
270 end
271 end
272 end
273
274 #####################
275 ### Pull requests
276 #####################
277
278 # Unlock achievement after X pull requests.
279 #
280 # Used to factorize behavior.
281 abstract class PlayerXPulls
282 super AchievementReactor
283
284 # Number of PR required to unlock the achievement.
285 var threshold: Int is noinit
286
287 redef fun react_event(game, event) do
288 if not event isa PullRequestEvent then return
289 if not event.action == "opened" then return
290 var player = event.pull.user.player(game)
291 if player.stats["pulls"] == threshold then
292 var a = new_achievement(game)
293 player.unlock_achievement(a, event)
294 end
295 end
296 end
297
298 # Open your first pull request.
299 class Player1Pull
300 super PlayerXPulls
301
302 redef var id = "player_1_pull"
303 redef var name = "First PR"
304 redef var desc = "Open your first pull request."
305 redef var reward = 10
306 redef var threshold = 1
307 end
308
309 # Author 100 pull requests.
310 class Player100Pulls
311 super PlayerXPulls
312
313 redef var id = "player_100_pulls"
314 redef var name = "100 pull requests!!!"
315 redef var desc = "Open 100 pull requests in the game."
316 redef var reward = 100
317 redef var threshold = 100
318 end
319
320 # Author 1000 pull requests.
321 class Player1KPulls
322 super PlayerXPulls
323
324 redef var id = "player_1000_pulls"
325 redef var name = "1000 PULL REQUESTS!!!"
326 redef var desc = "Open 1000 pull requests in the game."
327 redef var reward = 1000
328 redef var threshold = 1000
329 end
330
331 #####################
332 ### Commits
333 #####################
334
335 # Unlock achievement after X merged commits.
336 #
337 # Used to factorize behavior.
338 abstract class PlayerXCommits
339 super AchievementReactor
340
341 # Number of PR required to unlock the achievement.
342 var threshold: Int is noinit
343
344 redef fun react_event(game, event) do
345 if not event isa PullRequestEvent then return
346 if not event.action == "closed" then return
347 if not event.pull.merged then return
348 var player = event.pull.user.player(game)
349 if player.stats["commits"] == threshold then
350 var a = new_achievement(game)
351 player.unlock_achievement(a, event)
352 end
353 end
354 end
355
356 # Author your first commit in the game.
357 class Player1Commit
358 super PlayerXCommits
359
360 redef var id = "player_1_commit"
361 redef var name = "First blood"
362 redef var desc = "Author your first commit in the game."
363 redef var reward = 10
364 redef var threshold = 1
365 end
366
367 # Author 100 commits.
368 class Player100Commits
369 super PlayerXCommits
370
371 redef var id = "player_100_commits"
372 redef var name = "100 commits"
373 redef var desc = "Author 100 commits in the game."
374 redef var reward = 100
375 redef var threshold = 100
376 end
377
378 # Author 1 000 commits.
379 class Player1KCommits
380 super PlayerXCommits
381
382 redef var id = "player_1000_commits"
383 redef var name = "1000 commits!!!"
384 redef var desc = "Author 1000 commits in the game."
385 redef var reward = 1000
386 redef var threshold = 1000
387 end
388
389 # Author 10 000 commits.
390 class Player10KCommits
391 super PlayerXCommits
392
393 redef var id = "player_10000_commits"
394 redef var name = "10000 COMMITS!!!"
395 redef var desc = "Author 10000 commits in the game."
396 redef var reward = 10000
397 redef var threshold = 10000
398 end
399
400 #####################
401 ### Issue Comments
402 #####################
403
404 # Unlock achievement after X issue comments.
405 #
406 # Used to factorize behavior.
407 abstract class PlayerXComments
408 super AchievementReactor
409
410 # Number of comments required to unlock the achievement.
411 var threshold: Int is noinit
412
413 redef fun react_event(game, event) do
414 if not event isa IssueCommentEvent then return
415 if not event.action == "created" then return
416 var player = event.comment.user.player(game)
417 if player.stats["comments"] == threshold then
418 var a = new_achievement(game)
419 player.unlock_achievement(a, event)
420 end
421 end
422 end
423
424 # Player author his first comment in issues.
425 class Player1Comment
426 super PlayerXComments
427
428 redef var id = "player_1_comment"
429 redef var name = "From lurker to member"
430 redef var desc = "Comment on an issue."
431 redef var reward = 10
432 redef var threshold = 1
433 end
434
435 # Player author 100 issue comments.
436 class Player100Comments
437 super PlayerXComments
438
439 redef var id = "player_100_comments"
440 redef var name = "Chatter"
441 redef var desc = "Comment 100 times on issues."
442 redef var reward = 100
443 redef var threshold = 100
444 end
445
446 # Player author 1000 issue comments.
447 class Player1KComments
448 super PlayerXComments
449
450 redef var id = "player_1000__comments"
451 redef var name = "You sir, talk a lot!"
452 redef var desc = "Comment 1000 times on issues."
453 redef var reward = 1000
454 redef var threshold = 1000
455 end
456
457 # Ping @privat in a comment.
458 class PlayerPingGod
459 super AchievementReactor
460
461 redef var id = "player_ping_god"
462 redef var name = "Ping god"
463 redef var desc = "Ping the owner of the repo for the first time."
464 redef var reward = 50
465
466 redef fun react_event(game, event) do
467 if not event isa IssueCommentEvent then return
468 var owner = game.repo.owner.login
469 if event.comment.body.has("@{owner}".to_re) then
470 var player = event.comment.user.player(game)
471 var a = new_achievement(game)
472 player.unlock_achievement(a, event)
473 end
474 end
475 end
476
477 # Give your first +1
478 class PlayerFirstReview
479 super AchievementReactor
480
481 redef var id = "player_first_review"
482 redef var name = "First +1"
483 redef var desc = "Give a +1 for the first time."
484 redef var reward = 10
485
486 redef fun react_event(game, event) do
487 if not event isa IssueCommentEvent then return
488 # FIXME use a more precise way to locate reviews
489 if event.comment.is_ack then
490 var player = event.comment.user.player(game)
491 var a = new_achievement(game)
492 player.unlock_achievement(a, event)
493 end
494 end
495 end
496
497 # Talk about nitcoin in issue comments.
498 class PlayerSaysNitcoin
499 super AchievementReactor
500
501 redef var id = "player_says_nitcoin"
502 redef var name = "Talking about money"
503 redef var desc = "Say something about nitcoins in a comment."
504 redef var reward = 10
505
506 redef fun react_event(game, event) do
507 if not event isa IssueCommentEvent then return
508 if event.comment.body.has("(n|N)itcoin".to_re) then
509 var player = event.comment.user.player(game)
510 var a = new_achievement(game)
511 player.unlock_achievement(a, event)
512 end
513 end
514 end