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