0
|
1 /* |
|
2 * hangman.js -- hangman game for IRC |
|
3 * |
|
4 * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr> |
|
5 * |
|
6 * Permission to use, copy, modify, and/or distribute this software for any |
|
7 * purpose with or without fee is hereby granted, provided that the above |
|
8 * copyright notice and this permission notice appear in all copies. |
|
9 * |
|
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
|
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
|
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
|
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
|
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
|
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
|
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
|
17 */ |
|
18 |
|
19 /* Modules */ |
|
20 var Logger = Irccd.Logger; |
|
21 var File = Irccd.File; |
|
22 var Plugin = Irccd.Plugin; |
|
23 var Server = Irccd.Server; |
|
24 var Unicode = Irccd.Unicode |
|
25 var Util = Irccd.Util; |
|
26 |
|
27 /* Plugin information */ |
|
28 info = { |
|
29 author: "David Demelier <markand@malikania.fr>", |
|
30 license: "ISC", |
|
31 summary: "A hangman game for IRC", |
|
32 version: "@IRCCD_VERSION@" |
|
33 }; |
|
34 |
|
35 /* Default options */ |
|
36 Plugin.config["collaborative"] = "true"; |
|
37 |
|
38 function Hangman(server, channel) |
|
39 { |
|
40 this.server = server; |
|
41 this.channel = channel; |
|
42 this.tries = 10; |
|
43 this.select(); |
|
44 } |
|
45 |
|
46 /** |
|
47 * Map of games. |
|
48 */ |
|
49 Hangman.map = {}; |
|
50 |
|
51 /** |
|
52 * List of words |
|
53 */ |
|
54 Hangman.words = []; |
|
55 |
|
56 /** |
|
57 * Formats for writing. |
|
58 */ |
|
59 Hangman.formats = { |
|
60 "asked": "#{nickname}, '#{letter}' was already asked.", |
|
61 "dead": "#{nickname}, fail the word was: #{word}.", |
|
62 "found": "#{nickname}, nice! the word is now #{word}", |
|
63 "running": "#{nickname}, the game is already running.", |
|
64 "start": "#{nickname}, the game is started, the word to find is: #{word}", |
|
65 "win": "#{nickname}, congratulations, the word is #{word}.", |
|
66 "wrong-word": "#{nickname}, this is not the word.", |
|
67 "wrong-player": "#{nickname}, please wait until someone else proposes.", |
|
68 "wrong-letter": "#{nickname}, there is no '#{letter}'." |
|
69 }; |
|
70 |
|
71 /** |
|
72 * Search for an existing game. |
|
73 * |
|
74 * @param server the server object |
|
75 * @param channel the channel name |
|
76 * @return the hangman instance or undefined if no one exists |
|
77 */ |
|
78 Hangman.find = function (server, channel) |
|
79 { |
|
80 return Hangman.map[server.toString() + '@' + channel]; |
|
81 } |
|
82 |
|
83 /** |
|
84 * Create a new game, store it in the map and return it. |
|
85 * |
|
86 * @param server the server object |
|
87 * @param channel the channel name |
|
88 * @return the hangman object |
|
89 */ |
|
90 Hangman.create = function (server, channel) |
|
91 { |
|
92 return Hangman.map[server.toString() + "@" + channel] = new Hangman(server, channel); |
|
93 } |
|
94 |
|
95 /** |
|
96 * Remove the specified game from the map. |
|
97 * |
|
98 * @param game the game to remove |
|
99 */ |
|
100 Hangman.remove = function (game) |
|
101 { |
|
102 delete Hangman.map[game.server + "@" + game.channel]; |
|
103 } |
|
104 |
|
105 /** |
|
106 * Check if the text is a valid word. |
|
107 * |
|
108 * @param word the word to check |
|
109 * @return true if a word |
|
110 */ |
|
111 Hangman.isWord = function (word) |
|
112 { |
|
113 if (word.length === 0) |
|
114 return false; |
|
115 |
|
116 for (var i = 0; i < word.length; ++i) |
|
117 if (!Unicode.isLetter(word.charCodeAt(i))) |
|
118 return false; |
|
119 |
|
120 return true; |
|
121 } |
|
122 |
|
123 /** |
|
124 * Load all words. |
|
125 */ |
|
126 Hangman.loadWords = function () |
|
127 { |
|
128 var path; |
|
129 |
|
130 /* User specified file? */ |
|
131 if (Plugin.config["file"]) |
|
132 path = Plugin.config["file"]; |
|
133 else |
|
134 path = Plugin.configPath + "words.conf"; |
|
135 |
|
136 try { |
|
137 Logger.info("loading words..."); |
|
138 |
|
139 var file = new File(path, "r"); |
|
140 var line; |
|
141 |
|
142 while ((line = file.readline()) !== undefined) |
|
143 if (Hangman.isWord(line)) |
|
144 Hangman.words.push(line); |
|
145 } catch (e) { |
|
146 throw new Error("could not open '" + path + "'"); |
|
147 } |
|
148 |
|
149 if (Hangman.words.length === 0) |
|
150 throw new Error("empty word database"); |
|
151 |
|
152 Logger.info("number of words in database: " + Hangman.words.length); |
|
153 } |
|
154 |
|
155 /** |
|
156 * Load all formats. |
|
157 */ |
|
158 Hangman.loadFormats = function () |
|
159 { |
|
160 for (var key in Hangman.formats) { |
|
161 var optname = "format-" + key; |
|
162 |
|
163 if (typeof (Plugin.config[optname]) !== "string") |
|
164 continue; |
|
165 |
|
166 if (Plugin.config[optname].length === 0) |
|
167 Logger.warning("skipping empty '" + optname + "' format"); |
|
168 else |
|
169 Hangman.formats[key] = Plugin.config[optname]; |
|
170 } |
|
171 } |
|
172 |
|
173 /** |
|
174 * Select the next word for the game. |
|
175 */ |
|
176 Hangman.prototype.select = function () |
|
177 { |
|
178 /* Reload the words if empty */ |
|
179 if (!this.words || this.words.length === 0) |
|
180 this.words = Hangman.words.slice(0); |
|
181 |
|
182 var i = Math.floor(Math.random() * this.words.length); |
|
183 |
|
184 this.word = this.words[i]; |
|
185 this.words.splice(i, 1); |
|
186 |
|
187 /* Fill table */ |
|
188 this.table = {}; |
|
189 |
|
190 for (var j = 0; j < this.word.length; ++j) |
|
191 this.table[this.word.charCodeAt(j)] = false; |
|
192 } |
|
193 |
|
194 /** |
|
195 * Format the word with underscore and letters. |
|
196 * |
|
197 * @return the secret |
|
198 */ |
|
199 Hangman.prototype.formatWord = function () |
|
200 { |
|
201 var str = ""; |
|
202 |
|
203 for (var i = 0; i < this.word.length; ++i) { |
|
204 var ch = this.word.charCodeAt(i); |
|
205 |
|
206 if (!this.table[ch]) |
|
207 str += "_"; |
|
208 else |
|
209 str += String.fromCharCode(ch); |
|
210 |
|
211 if (i + 1 < this.word.length) |
|
212 str += " "; |
|
213 } |
|
214 |
|
215 return str; |
|
216 } |
|
217 |
|
218 /** |
|
219 * Propose a word or a letter. |
|
220 * |
|
221 * @param ch the code point or the unique word |
|
222 * @param nickname the user trying |
|
223 * @return the status of the game |
|
224 */ |
|
225 Hangman.prototype.propose = function (ch, nickname) |
|
226 { |
|
227 var status = "found"; |
|
228 |
|
229 /* Check for collaborative mode */ |
|
230 if (Plugin.config["collaborative"] === "true") { |
|
231 if (this.last !== undefined && this.last === nickname) |
|
232 return "wrong-player"; |
|
233 |
|
234 this.last = nickname; |
|
235 } |
|
236 |
|
237 if (typeof(ch) == "number") { |
|
238 if (this.table[ch] === undefined) { |
|
239 this.tries -= 1; |
|
240 status = "wrong-letter"; |
|
241 } else { |
|
242 if (this.table[ch]) { |
|
243 this.tries -= 1; |
|
244 status = "asked"; |
|
245 } else { |
|
246 this.table[ch] = true; |
|
247 } |
|
248 } |
|
249 } else { |
|
250 if (this.word != ch) { |
|
251 this.tries -= 1; |
|
252 status = "wrong-word"; |
|
253 } else { |
|
254 status = "win"; |
|
255 } |
|
256 } |
|
257 |
|
258 /* Check if dead */ |
|
259 if (this.tries < 0) |
|
260 status = "dead"; |
|
261 |
|
262 /* Check if win */ |
|
263 var win = true; |
|
264 |
|
265 for (var i = 0; i < this.word.length; ++i) { |
|
266 if (!this.table[this.word.charCodeAt(i)]) { |
|
267 win = false; |
|
268 break; |
|
269 } |
|
270 } |
|
271 |
|
272 if (win) |
|
273 status = "win"; |
|
274 |
|
275 return status; |
|
276 } |
|
277 |
|
278 function onLoad() |
|
279 { |
|
280 Hangman.loadFormats(); |
|
281 Hangman.loadWords(); |
|
282 } |
|
283 |
|
284 onReload = onLoad; |
|
285 |
|
286 function propose(server, channel, origin, game, proposition) |
|
287 { |
|
288 var kw = { |
|
289 channel: channel, |
|
290 command: server.info().commandChar, |
|
291 nickname: Util.splituser(origin), |
|
292 origin: origin, |
|
293 plugin: Plugin.info().name, |
|
294 server: server |
|
295 }; |
|
296 |
|
297 var st = game.propose(proposition, kw.nickname); |
|
298 |
|
299 switch (st) { |
|
300 case "found": |
|
301 kw.word = game.formatWord(); |
|
302 server.message(channel, Util.format(Hangman.formats["found"], kw)); |
|
303 break; |
|
304 case "wrong-letter": |
|
305 case "wrong-player": |
|
306 case "wrong-word": |
|
307 case "asked": |
|
308 kw.letter = String.fromCharCode(proposition); |
|
309 server.message(channel, Util.format(Hangman.formats[st], kw)); |
|
310 break; |
|
311 case "dead": |
|
312 case "win": |
|
313 kw.word = game.word; |
|
314 server.message(channel, Util.format(Hangman.formats[st], kw)); |
|
315 |
|
316 /* Remove the game */ |
|
317 Hangman.remove(game); |
|
318 break; |
|
319 default: |
|
320 break; |
|
321 } |
|
322 } |
|
323 |
|
324 function onCommand(server, origin, channel, message) |
|
325 { |
|
326 var game = Hangman.find(server, channel); |
|
327 var kw = { |
|
328 channel: channel, |
|
329 command: server.info().commandChar, |
|
330 nickname: Util.splituser(origin), |
|
331 origin: origin, |
|
332 plugin: Plugin.info().name, |
|
333 server: server |
|
334 }; |
|
335 |
|
336 if (game) { |
|
337 var list = message.split(" \t"); |
|
338 |
|
339 if (list.length === 0 || String(list[0]).length === 0) { |
|
340 server.message(channel, Util.format(Hangman.formats["running"], kw)); |
|
341 } else { |
|
342 var word = String(list[0]); |
|
343 |
|
344 if (Hangman.isWord(word)) |
|
345 propose(server, channel, origin, game, word); |
|
346 } |
|
347 } else { |
|
348 game = Hangman.create(server, channel); |
|
349 kw.word = game.formatWord(); |
|
350 server.message(channel, Util.format(Hangman.formats["start"], kw)); |
|
351 } |
|
352 } |
|
353 |
|
354 function onMessage(server, origin, channel, message) |
|
355 { |
|
356 var game = Hangman.find(server, channel); |
|
357 |
|
358 if (!game) |
|
359 return; |
|
360 |
|
361 if (message.length === 1 && Unicode.isLetter(message.charCodeAt(0))) |
|
362 propose(server, channel, origin, game, message.charCodeAt(0)); |
|
363 } |