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