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