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