97102d275c2f0f7c660380d5906f3779595cefcf
[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
83 # Returns the `GameStats` instance for the overall statistics.
84 var overall: GameStats is lazy do
85 return load_stats_for("all")
86 end
87
88 # Returns the `GameStats` instance for the current year statistics.
89 var yearly: GameStats is lazy do
90 var date = new Tm.gmtime
91 var key = date.strftime("%Y")
92 return load_stats_for(key)
93 end
94
95 # Returns the `GameStats` instance for the current month statistics.
96 var monthly: GameStats is lazy do
97 var date = new Tm.gmtime
98 var key = date.strftime("%Y-%m")
99 return load_stats_for(key)
100 end
101
102 # Returns the `GameStats` instance for the current day statistics.
103 var daily: GameStats is lazy do
104 var date = new Tm.gmtime
105 var key = date.strftime("%Y-%m-%d")
106 return load_stats_for(key)
107 end
108
109 # Returns the `GameStats` instance for the current week statistics.
110 var weekly: GameStats is lazy do
111 var date = new Tm.gmtime
112 var key = date.strftime("%Y-W%U")
113 return load_stats_for(key)
114 end
115
116 # Load statistics for a `period` key.
117 fun load_stats_for(period: String): GameStats do
118 var key = owner.key / self.key / period
119 if not game.store.has_key(key) then
120 return new GameStats(game, period, owner)
121 end
122 var json = game.store.load_object(key)
123 return new GameStats.from_json(game, period, owner, json)
124 end
125
126 redef fun [](key) do return overall[key]
127
128 redef fun []=(key, value) do
129 overall[key] = value
130 yearly[key] = value
131 monthly[key] = value
132 daily[key] = value
133 weekly[key] = value
134 end
135
136 redef fun inc(e) do
137 overall.inc(e)
138 yearly.inc(e)
139 monthly.inc(e)
140 daily.inc(e)
141 weekly.inc(e)
142 end
143
144 redef fun dec(e) do
145 overall.dec(e)
146 yearly.dec(e)
147 monthly.dec(e)
148 daily.dec(e)
149 weekly.dec(e)
150 end
151
152 redef fun save_in(key) do
153 overall.save_in(key / self.key)
154 yearly.save_in(key / self.key)
155 monthly.save_in(key / self.key)
156 daily.save_in(key / self.key)
157 weekly.save_in(key / self.key)
158 end
159
160 redef fun pretty do return overall.pretty
161 end
162
163 # Game statistics structure that can be saved as a `GameEntity`.
164 class GameStats
165 super GameEntity
166 super Counter[String]
167
168 redef var game
169
170 # The pedriod these stats are about.
171 var period: String
172
173
174 # Load `self` from saved data.
175 init from_json(game: Game, period: String, json: JsonObject) do
176 for k, v in json do self[k] = v.as(Int)
177 end
178
179 redef fun to_json do
180 var obj = new JsonObject
181 for k, v in self do obj[k] = v
182 return obj
183 end
184
185 redef fun pretty do
186 var res = new FlatBuffer
187 for k, v in self do
188 res.append "# {v} {k}\n"
189 end
190 return res.write_to_string
191 end
192 end
193
194 # `GameReactor` that computes statistics about the game.
195 class StatisticsReactor
196 super GameReactor
197
198 redef fun react_event(game, e) do e.react_stats_event(game)
199 end
200
201 redef class GithubEvent
202 # Reacts to a statistics related event.
203 #
204 # Called by `StatisticsReactor::react_event`.
205 # No-op by default.
206 private fun react_stats_event(game: Game) do end
207 end
208
209 redef class IssuesEvent
210
211 # Count opened and closed issues.
212 redef fun react_stats_event(game) do
213 var player = issue.user.player(game)
214 if action == "opened" then
215 game.stats.inc("issues")
216 game.stats.inc("issues_open")
217 game.save
218 player.stats.inc("issues")
219 player.stats.inc("issues_open")
220 player.save
221 else if action == "reopened" then
222 game.stats.inc("issues_open")
223 game.save
224 player.stats.inc("issues_open")
225 player.save
226 else if action == "closed" then
227 game.stats.dec("issues_open")
228 game.save
229 player.stats.dec("issues_open")
230 player.save
231 end
232 end
233 end
234
235 redef class PullRequestEvent
236
237 # Count opened and closed pull requests.
238 redef fun react_stats_event(game) do
239 var player = pull.user.player(game)
240 if action == "opened" then
241 game.stats.inc("pulls")
242 game.stats.inc("pulls_open")
243 game.save
244 player.stats.inc("pulls")
245 player.stats.inc("pulls_open")
246 player.save
247 else if action == "reopened" then
248 game.stats.inc("pulls_open")
249 game.save
250 player.stats.inc("pulls_open")
251 player.save
252 else if action == "closed" then
253 game.stats.dec("pulls_open")
254 player.stats.dec("pulls_open")
255 if pull.merged then
256 game.stats["commits"] += pull.commits
257 player.stats["commits"] += pull.commits
258 end
259 game.save
260 player.save
261 end
262 end
263 end
264
265 redef class IssueCommentEvent
266
267 # Count posted comments
268 redef fun react_stats_event(game) do
269 if action == "created" then
270 var player = comment.user.player(game)
271 game.stats.inc("comments")
272 player.stats.inc("comments")
273 # FIXME use a more precise way to locate reviews
274 if comment.is_ack then
275 game.stats.inc("reviews")
276 player.stats.inc("reviews")
277 end
278 game.save
279 player.save
280 end
281 end
282 end