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
25use strict;
26use vars qw($VERSION %IRSSI);
27
28use Irssi 20020217.1542 (); # Version 0.8.1
29$VERSION = "0.8";
30%IRSSI = (
31authors     => "Simon Huggins",
32contact     => "huggie-irssi\@earth.li",
33name        => "Quiz",
34description => "Turns irssi into a quiz bot",
35license     => "GPLv2",
36url         => "http://the.earth.li/~huggie/irssi/",
37changed     => "2017-04-03",
38);
39
40use Irssi::Irc;
41use Data::Dumper;
42
43Irssi::settings_add_str("misc","quiz_admin","huggie");
44Irssi::settings_add_str("misc","quiz_passwd","stuff");
45Irssi::settings_add_str("misc","quiz_file","/home/huggie/.irssi/questions");
46
47Irssi::settings_add_int("misc","quiz_qlength",60);
48Irssi::settings_add_int("misc","quiz_hints",4);
49Irssi::settings_add_int("misc","quiz_target_score",10);
50Irssi::settings_add_int("misc","quiz_leave_concealed_chars",1);
51
52Irssi::command("set cmd_queue_speed 2010");
53
54{
55my $s;
56
57sub 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
83sub 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
114sub 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
169sub 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
203sub 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
226sub 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//;
261	($game->{'answer'} = $q) =~ s/^(.*)\|//;
262	$server->command("msg $channel Question:  $1");
263	($game->{'current_answer'} = $game->{'answer'}) =~ s/[a-zA-Z0-9]/*/g;
264	$q = s/^(.*)\|.*?$/$1/;
265	$server->command("msg $channel Answer:  ".$game->{'current_answer'});
266	$game->{'state'}="question";
267}
268
269sub invite_join($$) {
270	my ($server,$channel) = @_;
271	my $game = $s->{$server->{'tag'}}->{$channel};
272
273	$server->command("msg $channel Team Trivia thingummie v($VERSION) starts in 1 minute.  Type 4!join red or 12!join blue");
274	$game->{'timeouttag'} = Irssi::timeout_add(60000,"start_game",$game);
275	$game->{'timeout'} = time()+60;
276}
277
278sub secstonormal($) {
279	my $seconds = shift;
280	my ($m,$s);
281
282	$s = $seconds % 60;
283	$m = ($seconds - $s)/60;
284	return sprintf("%02d:%02d",$m,$s);
285}
286
287sub do_pubcommand($$$$) {
288	my ($command,$channel,$server,$nick) = @_;
289	my $game = $s->{$server->{'tag'}}->{$channel};
290
291	$command = lc $command;
292	$command =~ s/\s*$//;
293
294	if ($command =~ /^!bang$/) {
295		$server->command("msg $channel Dumping...");
296		foreach (split /\n/,Dumper($s)) {
297			Irssi::print("$_");
298		}
299	} elsif ($command =~ /^!trivon$/) {
300		if ($s->{$server->{'tag'}}->{$channel}) {
301			if ($s->{$server->{'tag'}}->{$channel}->{'state'}) {
302				$server->command("msg $nick Trivia is already on.  Use !trivoff to remove it.");
303				return;
304			}
305			#undef $s->{$server->{'tag'}}->{$channel};
306		} else {
307			# create structure magically
308			$game = $s->{$server->{'tag'}}->{$channel} = {};
309			$game->{'tag'} = $server->{'tag'};
310			$game->{'channel'} = $channel;
311		}
312		$game->{'teams'}={};
313		$game->{'redscore'} = 0;
314		$game->{'bluescore'} = 0;
315		load_questions($game,0);
316		$game->{'state'} = "join";
317		invite_join($server,$channel);
318	} elsif ($command =~ /^!trivoff$/) {
319		return if !$game->{'state'};
320		$game->{'state'}="over";
321		game_over($game);
322	} elsif ($command =~ /^!join/) {
323		if ($command =~ /^!join (red|blue)$/) {
324			return if !$game->{'state'};
325			$game->{'teams'}->{$nick}=$1;
326			if ($1 eq "blue") {
327				$server->command("msg $nick You have joined the 12Blue team");
328			} else {
329				$server->command("msg $nick You have joined the 4Red team");
330			}
331		}
332	} elsif ($command =~ /^!teams/) {
333		return if !$game->{'state'};
334		my @blue=();
335		my @red=();
336		foreach (sort keys %{$game->{'teams'}}) {
337			push @blue, $_ if $game->{'teams'}->{$_} eq "blue";
338			push @red,  $_ if $game->{'teams'}->{$_} eq "red";
339		}
340		$server->command("msg $channel 12Blue: ".join(",",@blue));
341		$server->command("msg $channel 4Red : ".join(",",@red));
342	} elsif ($command =~ /^!timeleft$/) {
343		if ($game->{'state'} eq "join" and $game->{'timeout'}) {
344			my $diff = $game->{'timeout'} - time();
345			if ($diff > 0) {
346				$server->command("msg $channel Time left: ".secstonormal($diff));
347			} else {
348				Irssi::print("Timeleft: $diff ??");
349			}
350		}
351	}
352}
353
354sub do_command($$$) {
355	my ($command,$nick,$server) = @_;
356
357	$command = lc $command;
358	$command =~ s/\s*$//;
359
360	if ($command =~ /^!bang$/) {
361		$server->command("msg $nick BOOM!");
362	} elsif ($command =~ /^admin/) {
363		if ($command !~ /^admin (.*)$/) {
364			$server->command("msg $nick admin needs a nick to change the admin user to!");
365		} else {
366			Irssi::settings_remove("quiz_admin");
367			Irssi::settings_add_str("misc","quiz_admin",$1);
368			$server->command("msg $nick admin user is now $1");
369		}
370	} else {
371		$server->command("msg $nick Unknown command '$command'");
372	}
373}
374
375sub check_answer($$$$) {
376	my ($server,$channel,$nick,$text) = @_;
377	my $game = $s->{$server->{'tag'}}->{$channel};
378
379	return if not exists $game->{'teams'}->{$nick};
380
381	$text =~ s/\s*$//;
382
383	if (lc $text eq lc $game->{'answer'}) {
384		$server->command("msg $channel Correct answer by ".
385		($game->{'teams'}->{$nick} eq "blue"?"12":"4").
386		$nick.": ".$game->{'answer'});
387		$game->{'state'}="won";
388		$game->{$game->{'teams'}->{$nick}."score"}++;
389		$game->{'scores'}->{$game->{'teams'}->{$nick}.$nick}++;
390		show_scores($game);
391		return;
392	}
393
394	my $show=0;
395	my @chars = split //,$text;
396
397	for (my $i=0; $i<length($game->{'answer'}); $i++) {
398		if (lc $chars[$i] eq lc substr($game->{'answer'},$i,1)) {
399			$show = 1 if substr($game->{'current_answer'},$i,1)
400				eq "*";
401			substr($game->{'current_answer'},$i,1) =
402				substr($game->{'answer'},$i,1);
403		}
404	}
405	$server->command("msg $channel Answer: ".$game->{'current_answer'})
406		if $show;
407}
408
409sub event_privmsg {
410	my ($server,$data,$nick,$address) = @_;
411	my ($target, $text) = split / :/,$data,2;
412	my ($command);
413
414	if ($target =~ /^#/) {
415		my $game = $s->{$server->{'tag'}}->{$target};
416		if ($text =~ /^!/) {
417			do_pubcommand($text,$target,$server,$nick);
418		} elsif ($game->{'state'} eq "question") {
419			check_answer($server,$target,$nick,$text);
420		}
421	} else {
422		if ($nick ne Irssi::settings_get_str("quiz_admin")) {
423			my ($passwd);
424			($passwd, $command) = split /\s/,$text,2;
425			if ($passwd ne Irssi::settings_get_str("quiz_passwd")) {
426				Irssi::print("$nick tried to do $command but got the password wrong.");
427			}
428		} else {
429			$command = $text;
430		}
431		do_command($command,$nick,$server);
432	}
433}
434
435sub event_changed_nick {
436	my ($channel,$nick,$oldnick) = @_;
437	my $server = $channel->{'server'};
438	my $game = $s->{$server->{'tag'}}->{$channel->{'name'}};
439
440	return if !$game->{'state'};
441
442	my $nicktxt = $nick->{'nick'};
443	if ($game->{'teams'}->{$oldnick}) {
444		$game->{'teams'}->{$nicktxt} = $game->{'teams'}->{$oldnick};
445		delete $game->{'teams'}->{$oldnick};
446	}
447}
448
449}
450
451Irssi::signal_add_last("event privmsg", "event_privmsg");
452Irssi::signal_add("nicklist changed", "event_changed_nick");
453