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