html/quiz.pl
1 # Quiz script for irssi
2 # (C) Simon Huggins 2001
3 # huggie@earth.li
4
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the Free
7 # Software Foundation; either version 2 of the License, or (at your option)
8 # any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13 # for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc., 59
17 # Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 #
19 # TODO:
20 # - Do something when people quit (remove from team, readd when
21 # rejoin?)
22 # - Store questions asked in a file rather than just in memory so it
23 # can be restarted without a problem.
24
25 use strict;
26 use vars qw($VERSION %IRSSI);
27
28 use Irssi 20020217.1542 (); # Version 0.8.1
29 $VERSION = "0.7";
30 %IRSSI = (
31 authors => "Simon Huggins",
32 contact => "huggie-irssi\@earth.li",
33 name => "Quiz",
34 description => "Turns irssi into a quiz bot",
35 license => "GPLv2",
36 url => "http://the.earth.li/~huggie/irssi/",
37 changed => "Wed Apr 24 01:12:01 BST 2002",
38 );
39
40 use Irssi::Irc;
41 use Data::Dumper;
42
43 Irssi::settings_add_str("misc","quiz_admin","huggie");
44 Irssi::settings_add_str("misc","quiz_passwd","stuff");
45 Irssi::settings_add_str("misc","quiz_file","/home/huggie/.irssi/questions");
46
47 Irssi::settings_add_int("misc","quiz_qlength",60);
48 Irssi::settings_add_int("misc","quiz_hints",4);
49 Irssi::settings_add_int("misc","quiz_target_score",10);
50 Irssi::settings_add_int("misc","quiz_leave_concealed_chars",1);
51
52 Irssi::command("set cmd_queue_speed 2010");
53
54 {
55 my $s;
56
57 sub load_questions($$) {
58 my ($game,$force) = @_;
59 my $tag = $game->{'tag'};
60 my $channel = $game->{'channel'};
61
62 my $server = Irssi::server_find_tag($tag);
63
64 if (!defined $server) {
65 Irssi::print("Hrm, couldn't find server for tag ($tag) in load_questions");
66 return;
67 }
68
69 return if $game->{'questions'} and not $force;
70
71 my $file = Irssi::settings_get_str("quiz_file");
72 if (open(QS, "<$file")) {
73 @{$game->{'questions'}}=<QS>;
74 close(QS);
75 Irssi::print("Loaded questions");
76 return 1;
77 } else {
78 $server->command("msg $channel Can't find quiz questions, sorry.");
79 return;
80 }
81 }
82
83 sub start_game($) {
84 my $game = shift;
85 my $tag = $game->{'tag'};
86 my $channel = $game->{'channel'};
87 my $server = Irssi::server_find_tag($tag);
88
89 if (!defined $server) {
90 Irssi::print("Hrm, couldn't find server for tag ($tag) in start_game");
91 return;
92 }
93
94 Irssi::timeout_remove($game->{'timeouttag'});
95 undef $game->{'timeouttag'};
96
97 if (!keys %{$game->{'teams'}}) {
98 $server->command("msg $channel Sorry no one joined!");
99 $game->{'state'} = "over";
100 game_over($game);
101 return;
102 }
103 $game->{'state'} = "game";
104
105 $server->command("msg $channel Game starts now. Questions last ".
106 Irssi::settings_get_int("quiz_qlength").
107 " seconds and there are ".
108 (Irssi::settings_get_int("quiz_hints")-1).
109 " hints. First to reach ".
110 Irssi::settings_get_int("quiz_target_score")." wins.");
111 next_question($game);
112 }
113
114 sub show_scores($) {
115 my $game = shift;
116 my $tag = $game->{'tag'};
117 my $channel = $game->{'channel'};
118 my $server = Irssi::server_find_tag($tag);
119
120 if (!defined $server) {
121 Irssi::print("Hrm, couldn't find server for tag ($tag) in show_scores");
122 return;
123 }
124
125 my (@redscorers,@bluescorers);
126
127 foreach my $score (sort keys %{$game->{'scores'}}) {
128 if ($score =~ /^blue/) {
129 $score =~ s/^blue//;
130 push @bluescorers, "$score(".
131 $game->{'scores'}->{"blue".$score}.")";
132 } else {
133 $score =~ s/^red//;
134 push @redscorers, "$score(".
135 $game->{'scores'}->{"red".$score}.")";
136 }
137 }
138
139 $server->command("msg $channel 12Blue: ".$game->{'bluescore'}
140 ." ".join(",",@bluescorers));
141 $server->command("msg $channel 4Red : ".$game->{'redscore'}
142 ." ".join(",",@redscorers));
143
144 my $ts = Irssi::settings_get_int("quiz_target_score");
145 if ($game->{'bluescore'} == $ts or $game->{'redscore'} == $ts) {
146 if ($game->{'bluescore'} == $ts) {
147 $server->command("msg $channel 12Blue team wins ".
148 $game->{'bluescore'}." to ".
149 $game->{'redscore'});
150 } else {
151 $server->command("msg $channel 4Red team wins ".
152 $game->{'redscore'}." to ".
153 $game->{'bluescore'});
154 }
155 $game->{'state'}="over";
156 } elsif ($game->{'state'} ne "over") {
157 $game->{'state'}="pause";
158 $server->command("msg $channel Next question in 20 seconds.");
159 if ($game->{'timeouttag'}) {
160 Irssi::timeout_remove($game->{'timeouttag'});
161 }
162 $game->{'timeouttag'} = Irssi::timeout_add(20000,
163 "next_question",$game);
164 $game->{'timeout'} = time() + 20;
165 }
166 game_over($game);
167 }
168
169 sub hint($) {
170 my $game = shift;
171 my $tag = $game->{'tag'};
172 my $channel = $game->{'channel'};
173 my $server = Irssi::server_find_tag($tag);
174
175 if (!defined $server) {
176 Irssi::print("Hrm, couldn't find server for tag ($tag) in hint");
177 return;
178 }
179
180 return if game_over($game);
181 if ($game->{'end'} <= time()) {
182 $server->command("msg $channel Time's up. The answer is: ".$game->{'answer'});
183 show_scores($game);
184 } else {
185 $game->{'hint'}++;
186 my $num = $game->{'current_answer'} =~ s/\*/*/g;
187 if ($num <= Irssi::settings_get_int("quiz_leave_concealed_chars")) {
188 return;
189 }
190 my $pos = index($game->{'current_answer'},"*");
191 if ($pos >= 0) {
192 $game->{'current_answer'} =~ s/\*/substr($game->{'answer'},$pos,1)/e;
193 }
194 my $hinttime = $game->{'hint'}*$game->{'hintlen'};
195 if ($hinttime != int($hinttime)) {
196 $hinttime = sprintf("%.2f", $hinttime);
197 }
198 $server->command("msg $channel $hinttime second hint: ".
199 $game->{'current_answer'});
200 }
201 }
202
203 sub game_over($) {
204 my $game = shift;
205 my $tag = $game->{'tag'};
206 my $channel = $game->{'channel'};
207 my $server = Irssi::server_find_tag($tag);
208
209 if (!defined $server) {
210 Irssi::print("Hrm, couldn't find server for tag ($tag) in game_over");
211 return;
212 }
213
214 if ($game->{'state'} eq "over") {
215 Irssi::timeout_remove($game->{'timeouttag'});
216 undef $game->{'timeouttag'};
217 undef $game->{'state'};
218 undef $game->{'teams'};
219 undef $game->{'scores'};
220 $server->command("msg $channel Trivia is disabled. Use !trivon to restart.");
221 return 1;
222 }
223 return;
224 }
225
226 sub next_question($) {
227 my $game = shift;
228 my $tag = $game->{'tag'};
229 my $channel = $game->{'channel'};
230 my $server = Irssi::server_find_tag($tag);
231
232 if (!defined $server) {
233 Irssi::print("Hrm, couldn't find server for tag ($tag) in next_question");
234 return;
235 }
236
237 my $len = Irssi::settings_get_int("quiz_qlength")/
238 Irssi::settings_get_int("quiz_hints");
239 if ($game->{'timeouttag'}) {
240 Irssi::timeout_remove($game->{'timeouttag'});
241 }
242 $game->{'timeouttag'} = Irssi::timeout_add($len*1000, "hint",$game);
243 my $t = time();
244 $game->{'timeout'} = $t + $len;
245 $game->{'end'} = Irssi::settings_get_int("quiz_qlength")+$t;
246 $game->{'hint'}=0;
247 $game->{'hintlen'} = $len;
248 if (!@{$game->{'questions'}}) {
249 load_questions($game,1);
250 if (!$game->{'questions'}) {
251 $server->command("msg $channel Hmmm, no questions found sorry");
252 $game->{'state'}="over";
253 }
254 Irssi::print("Questions looped");
255 }
256 return if game_over($game);
257 my $q = splice(@{$game->{'questions'}},rand(@{$game->{'questions'}}),1);
258 chomp $q;
259 $q =~ s/
//;
260 ($game->{'answer'} = $q) =~ s/^(.*)\|//;
261 $server->command("msg $channel Question: $1");
262 ($game->{'current_answer'} = $game->{'answer'}) =~ s/[a-zA-Z0-9]/*/g;
263 $q = s/^(.*)\|.*?$/$1/;
264 $server->command("msg $channel Answer: ".$game->{'current_answer'});
265 $game->{'state'}="question";
266 }
267
268 sub invite_join($$) {
269 my ($server,$channel) = @_;
270 my $game = $s->{$server->{'tag'}}->{$channel};
271
272 $server->command("msg $channel Team Trivia thingummie v($VERSION) starts in 1 minute. Type 4!join red or 12!join blue");
273 $game->{'timeouttag'} = Irssi::timeout_add(60000,"start_game",$game);
274 $game->{'timeout'} = time()+60;
275 }
276
277 sub secstonormal($) {
278 my $seconds = shift;
279 my ($m,$s);
280
281 $s = $seconds % 60;
282 $m = ($seconds - $s)/60;
283 return sprintf("%02d:%02d",$m,$s);
284 }
285
286 sub do_pubcommand($$$$) {
287 my ($command,$channel,$server,$nick) = @_;
288 my $game = $s->{$server->{'tag'}}->{$channel};
289
290 $command = lc $command;
291 $command =~ s/\s*$//;
292
293 if ($command =~ /^!bang$/) {
294 $server->command("msg $channel Dumping...");
295 foreach (split /\n/,Dumper($s)) {
296 Irssi::print("$_");
297 }
298 } elsif ($command =~ /^!trivon$/) {
299 if ($s->{$server->{'tag'}}->{$channel}) {
300 if ($s->{$server->{'tag'}}->{$channel}->{'state'}) {
301 $server->command("msg $nick Trivia is already on. Use !trivoff to remove it.");
302 return;
303 }
304 #undef $s->{$server->{'tag'}}->{$channel};
305 } else {
306 # create structure magically
307 $game = $s->{$server->{'tag'}}->{$channel} = {};
308 $game->{'tag'} = $server->{'tag'};
309 $game->{'channel'} = $channel;
310 }
311 $game->{'teams'}={};
312 $game->{'redscore'} = 0;
313 $game->{'bluescore'} = 0;
314 load_questions($game,0);
315 $game->{'state'} = "join";
316 invite_join($server,$channel);
317 } elsif ($command =~ /^!trivoff$/) {
318 return if !$game->{'state'};
319 $game->{'state'}="over";
320 game_over($game);
321 } elsif ($command =~ /^!join/) {
322 if ($command =~ /^!join (red|blue)$/) {
323 return if !$game->{'state'};
324 $game->{'teams'}->{$nick}=$1;
325 if ($1 eq "blue") {
326 $server->command("msg $nick You have joined the 12Blue team");
327 } else {
328 $server->command("msg $nick You have joined the 4Red team");
329 }
330 }
331 } elsif ($command =~ /^!teams/) {
332 return if !$game->{'state'};
333 my @blue=();
334 my @red=();
335 foreach (sort keys %{$game->{'teams'}}) {
336 push @blue, $_ if $game->{'teams'}->{$_} eq "blue";
337 push @red, $_ if $game->{'teams'}->{$_} eq "red";
338 }
339 $server->command("msg $channel 12Blue: ".join(",",@blue));
340 $server->command("msg $channel 4Red : ".join(",",@red));
341 } elsif ($command =~ /^!timeleft$/) {
342 if ($game->{'state'} eq "join" and $game->{'timeout'}) {
343 my $diff = $game->{'timeout'} - time();
344 if ($diff > 0) {
345 $server->command("msg $channel Time left: ".secstonormal($diff));
346 } else {
347 Irssi::print("Timeleft: $diff ??");
348 }
349 }
350 }
351 }
352
353 sub do_command($$$) {
354 my ($command,$nick,$server) = @_;
355
356 $command = lc $command;
357 $command =~ s/\s*$//;
358
359 if ($command =~ /^!bang$/) {
360 $server->command("msg $nick BOOM!");
361 } elsif ($command =~ /^admin/) {
362 if ($command !~ /^admin (.*)$/) {
363 $server->command("msg $nick admin needs a nick to change the admin user to!");
364 } else {
365 Irssi::settings_remove("quiz_admin");
366 Irssi::settings_add_str("misc","quiz_admin",$1);
367 $server->command("msg $nick admin user is now $1");
368 }
369 } else {
370 $server->command("msg $nick Unknown command '$command'");
371 }
372 }
373
374 sub check_answer($$$$) {
375 my ($server,$channel,$nick,$text) = @_;
376 my $game = $s->{$server->{'tag'}}->{$channel};
377
378 return if not exists $game->{'teams'}->{$nick};
379
380 $text =~ s/\s*$//;
381
382 if (lc $text eq lc $game->{'answer'}) {
383 $server->command("msg $channel Correct answer by ".
384 ($game->{'teams'}->{$nick} eq "blue"?"12":"4").
385 $nick.": ".$game->{'answer'});
386 $game->{'state'}="won";
387 $game->{$game->{'teams'}->{$nick}."score"}++;
388 $game->{'scores'}->{$game->{'teams'}->{$nick}.$nick}++;
389 show_scores($game);
390 return;
391 }
392
393 my $show=0;
394 my @chars = split //,$text;
395
396 for (my $i=0; $i<length($game->{'answer'}); $i++) {
397 if (lc $chars[$i] eq lc substr($game->{'answer'},$i,1)) {
398 $show = 1 if substr($game->{'current_answer'},$i,1)
399 eq "*";
400 substr($game->{'current_answer'},$i,1) =
401 substr($game->{'answer'},$i,1);
402 }
403 }
404 $server->command("msg $channel Answer: ".$game->{'current_answer'})
405 if $show;
406 }
407
408 sub event_privmsg {
409 my ($server,$data,$nick,$address) = @_;
410 my ($target, $text) = split / :/,$data,2;
411 my ($command);
412
413 if ($target =~ /^#/) {
414 my $game = $s->{$server->{'tag'}}->{$target};
415 if ($text =~ /^!/) {
416 do_pubcommand($text,$target,$server,$nick);
417 } elsif ($game->{'state'} eq "question") {
418 check_answer($server,$target,$nick,$text);
419 }
420 } else {
421 if ($nick ne Irssi::settings_get_str("quiz_admin")) {
422 my ($passwd);
423 ($passwd, $command) = split /\s/,$text,2;
424 if ($passwd ne Irssi::settings_get_str("quiz_passwd")) {
425 Irssi::print("$nick tried to do $command but got the password wrong.");
426 }
427 } else {
428 $command = $text;
429 }
430 do_command($command,$nick,$server);
431 }
432 }
433
434 sub event_changed_nick {
435 my ($channel,$nick,$oldnick) = @_;
436 my $server = $channel->{'server'};
437 my $game = $s->{$server->{'tag'}}->{$channel->{'name'}};
438
439 return if !$game->{'state'};
440
441 my $nicktxt = $nick->{'nick'};
442 if ($game->{'teams'}->{$oldnick}) {
443 $game->{'teams'}->{$nicktxt} = $game->{'teams'}->{$oldnick};
444 delete $game->{'teams'}->{$oldnick};
445 }
446 }
447
448 }
449
450 Irssi::signal_add_last("event privmsg", "event_privmsg");
451 Irssi::signal_add("nicklist changed", "event_changed_nick");