Merge: concurrent_collections: Add implementation of has method
[nit.git] / contrib / nitrpg / src / game.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` game structures.
18 #
19 # Here we define the main game entities:
20 #
21 # * `Game` holds all the entities for a game and provides high level services.
22 # * `Player` represents a `Github::User` which plays the `Game`.
23 #
24 # Developpers who wants to extend the game capabilities should look at
25 # the `GameReactor` abstraction.
26 module game
27
28 import mongodb
29 import github::events
30
31 # An entity within a `Game`.
32 #
33 # All game entities can be saved in a json format.
34 interface GameEntity
35 # The game instance containing `self`.
36 fun game: Game is abstract
37
38 # Collection `self` should be saved in.
39 fun collection_name: String is abstract
40
41 # Uniq key of this entity within the collection.
42 fun key: String is abstract
43
44 # Saves `self` in db.
45 fun save do game.db.collection(collection_name).save(to_json_object)
46
47 # Json representation of `self`.
48 fun to_json_object: JsonObject do
49 var json = new JsonObject
50 json["_id"] = key
51 return json
52 end
53
54 # Pretty print `self` to be displayed in a terminal.
55 fun pretty: String is abstract
56 end
57
58 # Holder for game data and main services.
59 #
60 # Game is a `GameEntity` so it can be saved.
61 class Game
62 super GameEntity
63
64 redef fun game do return self
65
66 # We need a `GithubAPI` client to load Github data.
67 var api: GithubAPI
68
69 # A game takes place in a `github::Repo`.
70 var repo: Repo
71
72 # Game name
73 var name: String = repo.full_name is lazy
74
75 redef var key = name is lazy
76
77 # Mongo server url where this game data are stored.
78 var mongo_url = "mongodb://mongo:27017" is writable
79
80 # Mongo db client.
81 var client = new MongoClient(mongo_url) is lazy
82
83 # Mongo db name where this game data are stored.
84 var db_name = "nitrpg" is writable
85
86 # Mongo db instance for this game.
87 var db: MongoDb is lazy do return client.database(db_name)
88
89 redef var collection_name = "games"
90
91 # Init the Game and try to load saved data.
92 init from_mongo(api: GithubAPI, repo: Repo) do
93 init(api, repo)
94 var req = new JsonObject
95 req["name"] = repo.full_name
96 var res = db.collection("games").find(req)
97 if res != null then from_json(res)
98 end
99
100 # Init `self` from a JsonObject.
101 #
102 # Used to load entities from saved data.
103 fun from_json(json: JsonObject) do end
104
105 redef fun to_json_object do
106 var json = super
107 json["name"] = name
108 return json
109 end
110
111 # Create a player from a Github `User`.
112 #
113 # Or return the existing one from game data.
114 fun add_player(user: User): Player do
115 # check if player already exists
116 var player = load_player(user.login)
117 if player != null then return player
118 # create and store new player
119 player = new Player(self, user.login)
120 player.save
121 return player
122 end
123
124 # Get a Player from his `name` or null if no player was found.
125 #
126 # Looks for the player save file in game data.
127 #
128 # Returns `null` if the player cannot be found.
129 # In this case, the player can be created with `add_player`.
130 fun load_player(name: String): nullable Player do
131 var req = new JsonObject
132 req["name"] = name
133 req["game"] = game.key
134 var res = db.collection("players").find(req)
135 if res != null then return new Player.from_json(self, res)
136 return null
137 end
138
139 # List known players.
140 #
141 # This list is reloaded from game data each time its called.
142 #
143 # To add players see `add_player`.
144 fun load_players: MapRead[String, Player] do
145 var req = new JsonObject
146 req["game"] = game.key
147 var res = new HashMap[String, Player]
148 for obj in db.collection("players").find_all(req) do
149 var player = new Player.from_json(self, obj)
150 res[player.name] = player
151 end
152 return res
153 end
154
155 # Return a list of player name associated to their rank in the game.
156 fun player_ranking: MapRead[String, Int] do
157 var arr = load_players.values.to_a
158 var res = new HashMap[String, Int]
159 (new PlayerCoinComparator).sort(arr)
160 var rank = 1
161 for player in arr do
162 res[player.name] = rank
163 rank += 1
164 end
165 return res
166 end
167
168 # Erase all saved data for this game.
169 fun clear do db.collection(collection_name).remove(to_json_object)
170
171 # Verbosity level used fo stdout.
172 #
173 # * `-1` quiet
174 # * `0` error and warnings
175 # * `1` info
176 # * `2` debug
177 var verbose_lvl = 0 is writable
178
179 # Display `msg` if `lvl` >= `verbose_lvl`
180 fun message(lvl: Int, msg: String) do
181 if lvl > verbose_lvl then return
182 print msg
183 end
184
185 redef fun pretty do
186 var res = new FlatBuffer
187 res.append "-------------------------\n"
188 res.append "{repo.full_name}\n"
189 res.append "-------------------------\n"
190 res.append "# {load_players.length} players \n"
191 return res.write_to_string
192 end
193 end
194
195 # Players can battle on nitrpg for nitcoins and glory.
196 #
197 # A `Player` is linked to a `Github::User`.
198 class Player
199 super GameEntity
200
201 # Stored in collection `players`.
202 redef var collection_name = "players"
203
204 redef var game
205
206 # FIXME contructor should be private
207
208 # Player name.
209 #
210 # This is the unic key for this player.
211 # Should be equal to the associated `Github::User::login`.
212 #
213 # The name is also used to load the user data lazilly from Github API.
214 var name: String
215
216 redef var key = name is lazy
217
218 # Player amount of nitcoins.
219 #
220 # Nitcoins is the currency used in nitrpg.
221 # They can be obtained by performing actions on the `Game::Repo`.
222 var nitcoins: Int = 0 is public writable
223
224 # `Github::User` linked to this player.
225 var user: User is lazy do
226 var user = game.api.load_user(name)
227 assert user isa User
228 return user
229 end
230
231 # Init `self` from a `json` object.
232 #
233 # Used to load players from saved data.
234 init from_json(game: Game, json: JsonObject) do
235 init(game, json["name"].as(String))
236 nitcoins = json["nitcoins"].as(Int)
237 end
238
239 redef fun to_json_object do
240 var json = super
241 json["game"] = game.key
242 json["name"] = name
243 json["nitcoins"] = nitcoins
244 return json
245 end
246
247 redef fun pretty do
248 var res = new FlatBuffer
249 res.append "-- {name} ({nitcoins} $)\n"
250 return res.write_to_string
251 end
252
253 redef fun to_s do return name
254 end
255
256 redef class User
257 # The player linked to `self`.
258 fun player(game: Game): Player do
259 var player = player_cache.get_or_null(game)
260 if player != null then return player
261 player = game.load_player(login)
262 if player == null then player = game.add_player(self)
263 player_cache[game] = player
264 return player
265 end
266
267 private var player_cache = new HashMap[Game, Player]
268 end
269
270 # A GameReactor reacts to event sent by a `Github::HookListener`.
271 #
272 # Subclasses of `GameReactor` are implemented to handle all kind of
273 # `GithubEvent`.
274 # Depending on the received event, the reactor is used to update game data.
275 #
276 # Reactors are mostly used with a `Github::HookListener` that dispatchs received
277 # events from the Github API.
278 #
279 # Example:
280 #
281 # ~~~
282 # import github::hooks
283 #
284 # # Reactor that prints received events in console.
285 # class PrintReactor
286 # super GameReactor
287 #
288 # redef fun react_event(game, e) do print e
289 # end
290 #
291 # # Hook listener that redirect events to reactors.
292 # class RpgHookListener
293 # super HookListener
294 #
295 # redef fun apply_event(event) do
296 # var game = new Game(api, event.repo)
297 # var reactor = new PrintReactor
298 # reactor.react_event(game, event)
299 # end
300 # end
301 # ~~~
302 #
303 # See module `reactors` and `listener` for more examples.
304 interface GameReactor
305
306 # Reacts to this `event` and update `game` accordingly.
307 #
308 # Concrete `GameReactor` implement this method to update game data
309 # for each specific GithubEvent.
310 fun react_event(game: Game, event: GithubEvent) is abstract
311 end
312
313 # utils
314
315 # Sort games by descending number of players.
316 #
317 # The first in the list is the game with the more players.
318 class GamePlayersComparator
319 super Comparator
320
321 redef type COMPARED: Game
322
323 redef fun compare(a, b) do
324 return b.load_players.length <=> a.load_players.length
325 end
326 end
327
328 # Sort players by descending number of nitcoins.
329 #
330 # The first in the list is the player with the more of nitcoins.
331 class PlayerCoinComparator
332 super Comparator
333
334 redef type COMPARED: Player
335
336 redef fun compare(a, b) do return b.nitcoins <=> a.nitcoins
337 end