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