6bb26d806b5dfd6258e9d807f707834ee6fc6958
[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
78 redef var game
79
80 # Uniq ID for this achievement.
81 var id: String
82
83 # Name of this achievement.
84 var name: String
85
86 # Description of the achievement.
87 var desc: String
88
89 # Reward that this achievement give in nitcoins.
90 var reward: Int
91
92 # Is this achievement unlocked by somebody?
93 var is_unlocked: Bool is lazy do return not load_events.is_empty
94
95 # Init `self` from a `json` object.
96 #
97 # Used to load achievements from storage.
98 init from_json(game: Game, json: JsonObject) do
99 init(game, json["id"].to_s, json["name"].to_s, json["desc"].to_s, json["reward"].as(Int))
100 end
101
102 redef fun to_json do
103 var json = super
104 json["id"] = id
105 json["name"] = name
106 json["desc"] = desc
107 json["reward"] = reward
108 return json
109 end
110 end
111
112 redef class Player
113
114 # Is `a` unlocked for this `Player`?
115 fun has_achievement(a: Achievement): Bool do
116 return load_achievement(a.id) != null
117 end
118
119 # Unlocks an achievement for this Player based on a GithubEvent.
120 #
121 # Register the achievement and adds the achievement reward to the player
122 # nitcoins.
123 #
124 # Do nothing is this player has already unlocked the achievement.
125 #
126 # TODO: add abstraction so achievements do not depend on GithubEvent.
127 fun unlock_achievement(a: Achievement, event: GithubEvent) do
128 if has_achievement(a) then return
129 nitcoins += a.reward
130 add_achievement(a)
131 trigger_unlock_event(a, event)
132 end
133
134 # Create a new event that marks the achievement unlocking.
135 fun trigger_unlock_event(achievement: Achievement, event: GithubEvent) do
136 var obj = new JsonObject
137 obj["player"] = name
138 obj["reward"] = achievement.reward
139 obj["achievement"] = achievement.id
140 obj["github_event"] = event.json
141 var ge = new GameEvent(game, "achievement_unlocked", obj)
142 add_event(ge)
143 game.add_event(ge)
144 achievement.add_event(ge)
145 end
146 end
147
148 # `GameReactor` dedicated to achievements unlocking.
149 interface AchievementReactor
150 super GameReactor
151
152 # Unic ID of the achievement this reactor unlocks.
153 fun id: String is abstract
154
155 # Name of the achievement this reactor unlocks.
156 fun name: String is abstract
157
158 # Description of the achievement this reactor unlocks.
159 fun desc: String is abstract
160
161 # Amount of nitcoins rewarded for unlocking the achievement.
162 fun reward: Int is abstract
163
164 # Return a new instance of the achievement to unlock.
165 fun new_achievement(game: Game): Achievement do
166 var achievement = new Achievement(game, id, name, desc, reward)
167 game.add_achievement(achievement)
168 return achievement
169 end
170 end
171
172 #####################
173 ### Issues
174 #####################
175
176 # Unlock achievement after X issues.
177 #
178 # Used to factorize behavior.
179 abstract class PlayerXIssues
180 super AchievementReactor
181
182 # Number of PR required to unlock the achievement.
183 var threshold: Int is noinit
184
185 redef fun react_event(game, event) do
186 if not event isa IssuesEvent then return
187 if not event.action == "opened" then return
188 var player = event.issue.user.player(game)
189 if player.stats["issues"] == threshold then
190 var a = new_achievement(game)
191 player.unlock_achievement(a, event)
192 end
193 end
194 end
195
196 # Player open his first issue.
197 class Player1Issue
198 super PlayerXIssues
199
200 redef var id = "player_1_issue"
201 redef var name = "First complaint"
202 redef var desc = "Open your first issue."
203 redef var reward = 10
204 redef var threshold = 1
205 end
206
207 # Player open 100 issues.
208 class Player100Issues
209 super PlayerXIssues
210
211 redef var id = "player_100_issues"
212 redef var name = "Mature whiner"
213 redef var desc = "Open 100 issues in the game."
214 redef var reward = 100
215 redef var threshold = 100
216 end
217
218 # Player open 1 000 issues.
219 class Player1KIssues
220 super PlayerXIssues
221
222 redef var id = "player_1000_issues"
223 redef var name = "You, sir, complain a lot"
224 redef var desc = "Open 1000 issues in the game."
225 redef var reward = 1000
226 redef var threshold = 1000
227 end
228
229 # Player open an issue about nitdoc.
230 class IssueAboutNitdoc
231 super AchievementReactor
232
233 redef var id = "issue_about_nitdoc"
234 redef var name = "Say nitdoc again, I double dare you!"
235 redef var desc = "Open an issue with \"nitdoc\" in the title."
236 redef var reward = 10
237
238 redef fun react_event(game, event) do
239 if not event isa IssuesEvent then return
240 if not event.action == "opened" then return
241 var player = event.issue.user.player(game)
242 var re = "nitdoc".to_re
243 re.ignore_case = true
244 if event.issue.title.has(re) then
245 var a = new_achievement(game)
246 player.unlock_achievement(a, event)
247 end
248 end
249 end
250
251 # Player open an issue about FFI.
252 class IssueAboutFFI
253 super PlayerXIssues
254
255 redef var id = "issue_about_ffi"
256 redef var name = "Polyglot what?"
257 redef var desc = "Open an issue with `ffi` in the title."
258 redef var reward = 10
259
260 redef fun react_event(game, event) do
261 if not event isa IssuesEvent then return
262 if not event.action == "opened" then return
263 var player = event.issue.user.player(game)
264 var re = "\\bffi\\b".to_re
265 re.ignore_case = true
266 if event.issue.title.has(re) then
267 var a = new_achievement(game)
268 player.unlock_achievement(a, event)
269 end
270 end
271 end
272
273 #####################
274 ### Pull requests
275 #####################
276
277 # Unlock achievement after X pull requests.
278 #
279 # Used to factorize behavior.
280 abstract class PlayerXPulls
281 super AchievementReactor
282
283 # Number of PR required to unlock the achievement.
284 var threshold: Int is noinit
285
286 redef fun react_event(game, event) do
287 if not event isa PullRequestEvent then return
288 if not event.action == "opened" then return
289 var player = event.pull.user.player(game)
290 if player.stats["pulls"] == threshold then
291 var a = new_achievement(game)
292 player.unlock_achievement(a, event)
293 end
294 end
295 end
296
297 # Open your first pull request.
298 class Player1Pull
299 super PlayerXPulls
300
301 redef var id = "player_1_pull"
302 redef var name = "First PR"
303 redef var desc = "Open your first pull request."
304 redef var reward = 10
305 redef var threshold = 1
306 end
307
308 # Author 100 pull requests.
309 class Player100Pulls
310 super PlayerXPulls
311
312 redef var id = "player_100_pulls"
313 redef var name = "100 pull requests!!!"
314 redef var desc = "Open 100 pull requests in the game."
315 redef var reward = 100
316 redef var threshold = 100
317 end
318
319 # Author 1000 pull requests.
320 class Player1KPulls
321 super PlayerXPulls
322
323 redef var id = "player_1000_pulls"
324 redef var name = "1000 PULL REQUESTS!!!"
325 redef var desc = "Open 1000 pull requests in the game."
326 redef var reward = 1000
327 redef var threshold = 1000
328 end
329
330 #####################
331 ### Commits
332 #####################
333
334 # Unlock achievement after X merged commits.
335 #
336 # Used to factorize behavior.
337 abstract class PlayerXCommits
338 super AchievementReactor
339
340 # Number of PR required to unlock the achievement.
341 var threshold: Int is noinit
342
343 redef fun react_event(game, event) do
344 if not event isa PullRequestEvent then return
345 if not event.action == "closed" then return
346 if not event.pull.merged then return
347 var player = event.pull.user.player(game)
348 if player.stats["commits"] == threshold then
349 var a = new_achievement(game)
350 player.unlock_achievement(a, event)
351 end
352 end
353 end
354
355 # Author your first commit in the game.
356 class Player1Commit
357 super PlayerXCommits
358
359 redef var id = "player_1_commit"
360 redef var name = "First blood"
361 redef var desc = "Author your first commit in the game."
362 redef var reward = 10
363 redef var threshold = 1
364 end
365
366 # Author 100 commits.
367 class Player100Commits
368 super PlayerXCommits
369
370 redef var id = "player_100_commits"
371 redef var name = "100 commits"
372 redef var desc = "Author 100 commits in the game."
373 redef var reward = 100
374 redef var threshold = 100
375 end
376
377 # Author 1 000 commits.
378 class Player1KCommits
379 super PlayerXCommits
380
381 redef var id = "player_1000_commits"
382 redef var name = "1000 commits!!!"
383 redef var desc = "Author 1000 commits in the game."
384 redef var reward = 1000
385 redef var threshold = 1000
386 end
387
388 # Author 10 000 commits.
389 class Player10KCommits
390 super PlayerXCommits
391
392 redef var id = "player_10000_commits"
393 redef var name = "10000 COMMITS!!!"
394 redef var desc = "Author 10000 commits in the game."
395 redef var reward = 10000
396 redef var threshold = 10000
397 end
398
399 #####################
400 ### Issue Comments
401 #####################
402
403 # Unlock achievement after X issue comments.
404 #
405 # Used to factorize behavior.
406 abstract class PlayerXComments
407 super AchievementReactor
408
409 # Number of comments required to unlock the achievement.
410 var threshold: Int is noinit
411
412 redef fun react_event(game, event) do
413 if not event isa IssueCommentEvent then return
414 if not event.action == "created" then return
415 var player = event.comment.user.player(game)
416 if player.stats["comments"] == threshold then
417 var a = new_achievement(game)
418 player.unlock_achievement(a, event)
419 end
420 end
421 end
422
423 # Player author his first comment in issues.
424 class Player1Comment
425 super PlayerXComments
426
427 redef var id = "player_1_comment"
428 redef var name = "From lurker to member"
429 redef var desc = "Comment on an issue."
430 redef var reward = 10
431 redef var threshold = 1
432 end
433
434 # Player author 100 issue comments.
435 class Player100Comments
436 super PlayerXComments
437
438 redef var id = "player_100_comments"
439 redef var name = "Chatter"
440 redef var desc = "Comment 100 times on issues."
441 redef var reward = 100
442 redef var threshold = 100
443 end
444
445 # Player author 1000 issue comments.
446 class Player1KComments
447 super PlayerXComments
448
449 redef var id = "player_1000_comments"
450 redef var name = "You sir, talk a lot!"
451 redef var desc = "Comment 1000 times on issues."
452 redef var reward = 1000
453 redef var threshold = 1000
454 end
455
456 # Ping @privat in a comment.
457 class PlayerPingGod
458 super AchievementReactor
459
460 redef var id = "player_ping_god"
461 redef var name = "Ping god"
462 redef var desc = "Ping the owner of the repo for the first time."
463 redef var reward = 50
464
465 redef fun react_event(game, event) do
466 if not event isa IssueCommentEvent then return
467 var owner = game.repo.owner.login
468 if event.comment.body.has("@{owner}".to_re) then
469 var player = event.comment.user.player(game)
470 var a = new_achievement(game)
471 player.unlock_achievement(a, event)
472 end
473 end
474 end
475
476 # Give your first +1
477 class PlayerFirstReview
478 super AchievementReactor
479
480 redef var id = "player_first_review"
481 redef var name = "First +1"
482 redef var desc = "Give a +1 for the first time."
483 redef var reward = 10
484
485 redef fun react_event(game, event) do
486 if not event isa IssueCommentEvent then return
487 # FIXME use a more precise way to locate reviews
488 if event.comment.is_ack then
489 var player = event.comment.user.player(game)
490 var a = new_achievement(game)
491 player.unlock_achievement(a, event)
492 end
493 end
494 end
495
496 # Talk about nitcoin in issue comments.
497 class PlayerSaysNitcoin
498 super AchievementReactor
499
500 redef var id = "player_says_nitcoin"
501 redef var name = "Talking about money"
502 redef var desc = "Say something about nitcoins in a comment."
503 redef var reward = 10
504
505 redef fun react_event(game, event) do
506 if not event isa IssueCommentEvent then return
507 if event.comment.body.has("(n|N)itcoin".to_re) then
508 var player = event.comment.user.player(game)
509 var a = new_achievement(game)
510 player.unlock_achievement(a, event)
511 end
512 end
513 end