contrib/nitrpg: use stats to store player nitcoins
[nit.git] / contrib / nitrpg / src / statistics.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 # Statistics about the Game.
18 #
19 # This module uses `GameReactor` to extract statistics about the game from
20 # triggered `Github::Event`.
21 module statistics
22
23 import game
24 import github::hooks
25 import counter
26
27 redef class GameEntity
28
29 # Statistics manager for this entity.
30 fun stats: GameStatsManager is abstract
31 end
32
33 redef class Game
34
35 redef var stats is lazy do return new GameStatsManager(game, self)
36
37 redef fun save do
38 super
39 stats.save_in(self.key)
40 end
41
42 redef fun pretty do
43 var res = new FlatBuffer
44 res.append super
45 res.append "# stats:\n"
46 res.append stats.pretty
47 return res.write_to_string
48 end
49 end
50
51 redef class Player
52
53 redef var stats is lazy do return new GameStatsManager(game, self)
54
55 redef fun save do
56 super
57 stats.save_in(self.key)
58 end
59
60 redef fun nitcoins do return stats["nitcoins"]
61 redef fun nitcoins=(nc) do stats["nitcoins"] = nc
62
63 redef fun pretty do
64 var res = new FlatBuffer
65 res.append super
66 res.append "# stats:\n"
67 res.append stats.pretty
68 return res.write_to_string
69 end
70 end
71
72 # Store game stats for defined period.
73 class GameStatsManager
74 super GameEntity
75 super Counter[String]
76
77 redef var game
78
79 # The GameEntity monitored by these statistics.
80 var owner: GameEntity
81
82 redef var key = "stats"
83
84 # Returns the `GameStats` instance for the overall statistics.
85 var overall: GameStats is lazy do
86 return load_stats_for("all")
87 end
88
89 # Returns the `GameStats` instance for the current year statistics.
90 var yearly: GameStats is lazy do
91 var date = new Tm.gmtime
92 var key = date.strftime("%Y")
93 return load_stats_for(key)
94 end
95
96 # Returns the `GameStats` instance for the current month statistics.
97 var monthly: GameStats is lazy do
98 var date = new Tm.gmtime
99 var key = date.strftime("%Y-%m")
100 return load_stats_for(key)
101 end
102
103 # Returns the `GameStats` instance for the current day statistics.
104 var daily: GameStats is lazy do
105 var date = new Tm.gmtime
106 var key = date.strftime("%Y-%m-%d")
107 return load_stats_for(key)
108 end
109
110 # Returns the `GameStats` instance for the current week statistics.
111 var weekly: GameStats is lazy do
112 var date = new Tm.gmtime
113 var key = date.strftime("%Y-W%U")
114 return load_stats_for(key)
115 end
116
117 # Load statistics for a `period` key.
118 fun load_stats_for(period: String): GameStats do
119 var key = owner.key / self.key / period
120 if not game.store.has_key(key) then
121 return new GameStats(game, period)
122 end
123 var json = game.store.load_object(key)
124 return new GameStats.from_json(game, period, json)
125 end
126
127 redef fun [](key) do return overall[key]
128
129 redef fun []=(key, value) do
130 overall[key] = value
131 yearly[key] = value
132 monthly[key] = value
133 daily[key] = value
134 weekly[key] = value
135 end
136
137 redef fun inc(e) do
138 overall.inc(e)
139 yearly.inc(e)
140 monthly.inc(e)
141 daily.inc(e)
142 weekly.inc(e)
143 end
144
145 redef fun dec(e) do
146 overall.dec(e)
147 yearly.dec(e)
148 monthly.dec(e)
149 daily.dec(e)
150 weekly.dec(e)
151 end
152
153 redef fun save_in(key) do
154 overall.save_in(key / self.key)
155 yearly.save_in(key / self.key)
156 monthly.save_in(key / self.key)
157 daily.save_in(key / self.key)
158 weekly.save_in(key / self.key)
159 end
160
161 redef fun pretty do return overall.pretty
162 end
163
164 # Game statistics structure that can be saved as a `GameEntity`.
165 class GameStats
166 super GameEntity
167 super Counter[String]
168
169 redef var game
170
171 # The pedriod these stats are about.
172 var period: String
173
174 redef fun key do return period
175
176 # Load `self` from saved data.
177 init from_json(game: Game, period: String, json: JsonObject) do
178 for k, v in json do self[k] = v.as(Int)
179 end
180
181 redef fun to_json do
182 var obj = new JsonObject
183 for k, v in self do obj[k] = v
184 return obj
185 end
186
187 redef fun pretty do
188 var res = new FlatBuffer
189 for k, v in self do
190 res.append "# {v} {k}\n"
191 end
192 return res.write_to_string
193 end
194 end
195
196 # `GameReactor` that computes statistics about the game.
197 class StatisticsReactor
198 super GameReactor
199
200 redef fun react_event(game, e) do e.react_stats_event(game)
201 end
202
203 redef class GithubEvent
204 # Reacts to a statistics related event.
205 #
206 # Called by `StatisticsReactor::react_event`.
207 # No-op by default.
208 private fun react_stats_event(game: Game) do end
209 end
210
211 redef class IssuesEvent
212
213 # Count opened and closed issues.
214 redef fun react_stats_event(game) do
215 var player = issue.user.player(game)
216 if action == "opened" then
217 game.stats.inc("issues")
218 game.stats.inc("issues_open")
219 game.save
220 player.stats.inc("issues")
221 player.stats.inc("issues_open")
222 player.save
223 else if action == "reopened" then
224 game.stats.inc("issues_open")
225 game.save
226 player.stats.inc("issues_open")
227 player.save
228 else if action == "closed" then
229 game.stats.dec("issues_open")
230 game.save
231 player.stats.dec("issues_open")
232 player.save
233 end
234 end
235 end
236
237 redef class PullRequestEvent
238
239 # Count opened and closed pull requests.
240 redef fun react_stats_event(game) do
241 var player = pull.user.player(game)
242 if action == "opened" then
243 game.stats.inc("pulls")
244 game.stats.inc("pulls_open")
245 game.save
246 player.stats.inc("pulls")
247 player.stats.inc("pulls_open")
248 player.save
249 else if action == "reopened" then
250 game.stats.inc("pulls_open")
251 game.save
252 player.stats.inc("pulls_open")
253 player.save
254 else if action == "closed" then
255 game.stats.dec("pulls_open")
256 player.stats.dec("pulls_open")
257 if pull.merged then
258 game.stats["commits"] += pull.commits
259 player.stats["commits"] += pull.commits
260 end
261 game.save
262 player.save
263 end
264 end
265 end
266
267 redef class IssueCommentEvent
268
269 # Count posted comments
270 redef fun react_stats_event(game) do
271 if action == "created" then
272 var player = comment.user.player(game)
273 game.stats.inc("comments")
274 player.stats.inc("comments")
275 # FIXME use a more precise way to locate reviews
276 if comment.has_ok_review then
277 game.stats.inc("reviews")
278 player.stats.inc("reviews")
279 end
280 game.save
281 player.save
282 end
283 end
284 end
285
286 redef class IssueComment
287 # Does this comment contain a "+1"?
288 fun has_ok_review: Bool do return body.has("\\+1\\b".to_re)
289 end