1#!/usr/local/bin/perl -T 2# Quizgr script for irssi with "KAOS" questions enabled, modified for greek too 3# copyright Athanasius Emilius Arvanitis 4# arvan@kronos.eng.auth.gr 5# based on quiz.pl version 0.7 6# Quiz script for irssi 7# (C) Simon Huggins 2001 8# huggie@earth.li 9 10# This program is free software; you can redistribute it and/or modify it 11# under the terms of the GNU General Public License as published by the Free 12# Software Foundation; either version 2 of the License, or (at your option) 13# any later version. 14# 15# This program is distributed in the hope that it will be useful, but 16# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 17# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 18# for more details. 19# 20# You should have received a copy of the GNU General Public License along 21# with this program; if not, write to the Free Software Foundation, Inc., 59 22# Temple Place, Suite 330, Boston, MA 02111-1307 USA 23# 24# DONE: 25# - support for many answers (not alternate though) per question 26# - support for hellenic (aka greek) 27# - remembers the questions that were answered (stores them in 28# ./wr/used_questions) 29# - if nobody says anything for a period of time, game ends 30# - added !repeat to repeat the question 31# - it wont crash if you smile! 32# TODO: 33# - known bug: sometimes it ignores some people (they cant join etc). 34# if you join #CHANNEL and the bot is on #channel forget it... 35# - fix kaos hints 36# - if we have kaos, there should be more time to answer 37# - CLEAN up the code 38# - Do something when people quit (remove from team, readd when rejoin?) 39 40use strict; 41use vars qw($VERSION %IRSSI); 42 43use Irssi 20020217.1542 (); # Version 0.8.1 or perhaps get the most up to date irssi version 44$VERSION = "0.7GR03"; 45%IRSSI = ( 46authors => "Athanasius Emilius Arvanitis based on Simon Huggins quiz 0.7", 47contact => "arvan", 48name => "Quizgr", 49description => "Turns irssi into a quiz bot. Has greek language and many answers support", 50license => "GPLv2", 51url => "http://kronos.eng.auth.gr/~arvan/irssi/", 52changed => "Tue Nov 26 13:37:59 EET 2002", 53); 54 55use Irssi::Irc; 56use Data::Dumper; 57 58Irssi::settings_add_str("misc","quiz_admin","jbg"); 59Irssi::settings_add_str("misc","quiz_passwd","stuff"); 60Irssi::settings_add_str("misc","quiz_file","$ENV{HOME}/.irssi/scripts/autorun/gr_quiz_questions"); 61Irssi::settings_add_str("misc","used_file","$ENV{HOME}/.irssi/scripts/autorun/wr/used_questions"); 62 63Irssi::settings_add_int("misc","quiz_qlength",70); 64Irssi::settings_add_int("misc","quiz_hints",7); 65Irssi::settings_add_int("misc","quiz_target_score",50); 66Irssi::settings_add_int("misc","quiz_leave_concealed_chars",1); 67 68Irssi::command("set cmd_queue_speed 2010"); 69 70{ 71# when warnings used $s complains 72my $s; 73my $answerBAKforCHAOS; 74 75sub load_questions($$) { 76 my ($game,$force) = @_; 77 my $tag = $game->{'tag'}; 78 my $channel = $game->{'channel'}; 79 80 $game->{'used_questions'}=[]; 81 82 my $server = Irssi::server_find_tag($tag); 83 84 if (!defined $server) { 85 Irssi::print("Hrm, couldn't find server for tag ($tag) in load_questions"); 86 return; 87 } 88 89 return if $game->{'questions'} and not $force; 90 91 #the next must be checked 92 93 my $file = Irssi::settings_get_str("quiz_file"); 94 if (open(QS, "<", $file)) { #open for QS 95 @{$game->{'questions'}}=sort <QS>; 96 close(QS); 97 Irssi::print("Loaded questions"); 98 99 my $file2 = Irssi::settings_get_str("used_file"); 100 if (open(QS2, "<", $file2)) { #open for QS2 101 @{$game->{'used_questions'}}=sort <QS2>; 102 close(QS2); 103 104 #from perlfaq copy paste 105 @{$game->{'intersection'}} = @{$game->{'difference'}} = (); 106 %{$game->{'count'}} = (); 107 108 my $element; 109 foreach $element (@{$game->{'questions'}}, @{$game->{'used_questions'}}) { ${$game->{'count'}}{$element}++ }; 110 111 foreach $element (keys %{$game->{'count'}}) { #open foreach 112 push @{ ${$game->{'count'}}{$element} > 1 ? \@{$game->{'intersection'}} : \@{$game->{'difference'}} }, $element; 113 } #close foreach 114 115 my $ts = Irssi::settings_get_int("quiz_target_score"); 116 my $qCounter=@{$game->{'questions'}}; 117 ${$game->{'usedCounter'}}=@{$game->{'used_questions'}}; 118 119 my $qGOT=($qCounter - ${$game->{'usedCounter'}}); 120 my $qNEEDED=(2*($ts)+12); 121 Irssi::print("${$game->{'usedCounter'}} used out of $qCounter total questions"); 122 123 if ( $qGOT >= $qNEEDED ) { 124 @{$game->{'questions'}}=@{$game->{'difference'}}; 125 Irssi::print("Loaded not used questions"); 126 return 1;#used 127 } 128 129 if ( $qGOT < $qNEEDED ) { 130 @{$game->{'used_questions'}}=(); 131 Irssi::print("Clearing used questions"); 132 return 1;#used 133 } 134 } #close QS2 135 136 return 1;#questions 137 } #close QS 138 139 else { 140 $server->command("msg $channel Can't find quiz questions, sorry."); 141 return; 142 } 143 144 145 146} 147 148sub start_game($) { 149 my $game = shift; 150 my $tag = $game->{'tag'}; 151 my $channel = $game->{'channel'}; 152 my $server = Irssi::server_find_tag($tag); 153 154 if (!defined $server) { 155 Irssi::print("Hrm, couldn't find server for tag ($tag) in start_game"); 156 return; 157 } 158 159 Irssi::timeout_remove($game->{'timeouttag'}); 160 undef $game->{'timeouttag'}; 161 162 if (!keys %{$game->{'teams'}}) { 163 $server->command("msg $channel Sorry no one joined!"); 164 $game->{'state'} = "over"; 165 game_over($game); 166 return; 167 } 168 169 $game->{'state'} = "game"; 170 171 $server->command("msg $channel Game starts now. Questions last ". 172 Irssi::settings_get_int("quiz_qlength"). 173 " seconds and there are ". 174 (Irssi::settings_get_int("quiz_hints")-1). 175 " hints. First to reach ". 176 Irssi::settings_get_int("quiz_target_score")." wins."); 177 next_question($game); 178} 179 180sub show_scores($) { 181 my $game = shift; 182 my $tag = $game->{'tag'}; 183 my $channel = $game->{'channel'}; 184 my $server = Irssi::server_find_tag($tag); 185 186 if (!defined $server) { 187 Irssi::print("Hrm, couldn't find server for tag ($tag) in show_scores"); 188 return; 189 } 190 191 my (@redscorers,@bluescorers); 192 193 foreach my $score (sort keys %{$game->{'scores'}}) { 194 if ($score =~ /^blue/) { 195 $score =~ s/^blue//; 196 push @bluescorers, "$score(". 197 $game->{'scores'}->{"blue".$score}.")"; 198 } else { 199 $score =~ s/^red//; 200 push @redscorers, "$score(". 201 $game->{'scores'}->{"red".$score}.")"; 202 } 203 } 204 205 $server->command("msg $channel 12Blue: ".$game->{'bluescore'} 206 ." ".join(",",@bluescorers)); 207 $server->command("msg $channel 4Red: ".$game->{'redscore'} 208 ." ".join(",",@redscorers)); 209 210 my $ts = Irssi::settings_get_int("quiz_target_score"); 211 212 if ($game->{'bluescore'} >= $ts or $game->{'redscore'} >= $ts) { 213 if ($game->{'bluescore'} > $game->{'redscore'}) { 214 $server->command("msg $channel 12Blue team wins ". 215 $game->{'bluescore'}." to ". 216 $game->{'redscore'}); 217 } else { 218 $server->command("msg $channel 4Red team wins ". 219 $game->{'redscore'}." to ". 220 $game->{'bluescore'}); 221 } 222 $game->{'state'}="over"; 223 } elsif ($game->{'state'} ne "over") { 224 $game->{'state'}="pause"; 225 $server->command("msg $channel Next question in 6 20 seconds."); 226 if ($game->{'timeouttag'}) { 227 Irssi::timeout_remove($game->{'timeouttag'}); 228 } 229 $game->{'timeouttag'} = Irssi::timeout_add(20000, 230 "next_question",$game); 231 $game->{'timeout'} = time() + 20; 232 } 233 game_over($game); 234} 235 236sub hint($) { 237 my $game = shift; 238 my $tag = $game->{'tag'}; 239 my $channel = $game->{'channel'}; 240 my $server = Irssi::server_find_tag($tag); 241 242 if (!defined $server) { 243 Irssi::print("Hrm, couldn't find server for tag ($tag) in hint"); 244 return; 245 } 246 247 return if game_over($game); 248 if ($game->{'end'} <= time()) { 249 $server->command("msg $channel Time's up. The answer is: 2 ".$game->{'answer'}); 250 show_scores($game); 251 } else { 252 $game->{'hint'}++; 253 my $num = $game->{'current_answer'} =~ s/\*/*/g; 254 if ($num <= Irssi::settings_get_int("quiz_leave_concealed_chars")) { 255 return; 256 } 257 258 my $pos = index($game->{'current_answer'},"*"); 259 if ($pos >= 0) { 260 #$game->{'current_answer'} =~ s/\*/substr($game->{'answer'},$pos,1)/e; 261 $game->{'current_answer'} =~ s/\*/substr($answerBAKforCHAOS,$pos,1)/e; 262 } 263 264 my $hinttime = $game->{'hint'}*$game->{'hintlen'}; 265 if ($hinttime != int($hinttime)) { 266 $hinttime = sprintf("%.2f", $hinttime); 267 } 268 $server->command("msg $channel 2 $hinttime second hint: 6 ". 269 $game->{'current_answer'}); 270 } #else end 271} 272 273sub game_over($) { 274 my $game = shift; 275 my $tag = $game->{'tag'}; 276 my $channel = $game->{'channel'}; 277 my $server = Irssi::server_find_tag($tag); 278 279 if (!defined $server) { 280 Irssi::print("Hrm, couldn't find server for tag ($tag) in game_over"); 281 return; 282 } 283 284 if ($game->{'state'} eq "over") { 285 Irssi::timeout_remove($game->{'timeouttag'}); 286 undef $game->{'timeouttag'}; 287 undef $game->{'state'}; 288 undef $game->{'teams'}; 289 undef $game->{'scores'}; 290 291 #save used questions 292 my $file2 = Irssi::settings_get_str("used_file"); 293 if (open(QS2, ">", $file2)) { 294 my $line; 295 @{$game->{'used_questions'}}=sort @{$game->{'used_questions'}}; 296 foreach $line (@{$game->{'used_questions'}}){ 297 print QS2 $line ; 298 } 299 close(QS2); 300 Irssi::print("Saved used questions"); 301 } 302 303 304 $server->command("msg $channel Trivia is disabled. Use !start or !trivon to restart."); 305 return 1; 306 } 307 return; 308} 309 310sub next_question($) { 311 my $game = shift; 312 my $tag = $game->{'tag'}; 313 my $channel = $game->{'channel'}; 314 my $server = Irssi::server_find_tag($tag); 315 316 if (!defined $server) { 317 Irssi::print("Hrm, couldn't find server for tag ($tag) in next_question"); 318 return; 319 } 320 321 #check previous text time and 322 #if noone says anything for 180 seconds end game 323 if (defined $game->{'time_last_text'}) { 324 my $diff2=time() - $game->{'time_last_text'}; 325 if ( $diff2 > 180) { 326 $game->{'state'}="over"; 327 } 328 } 329 330 331 my $len = Irssi::settings_get_int("quiz_qlength")/ 332 Irssi::settings_get_int("quiz_hints"); 333 if ($game->{'timeouttag'}) { 334 Irssi::timeout_remove($game->{'timeouttag'}); 335 } 336 $game->{'timeouttag'} = Irssi::timeout_add($len*1000, "hint",$game); 337 my $t = time(); 338 $game->{'timeout'} = $t + $len; 339 $game->{'end'} = Irssi::settings_get_int("quiz_qlength")+$t; 340 $game->{'hint'}=0; 341 $game->{'hintlen'} = $len; 342 if (!@{$game->{'questions'}}) { 343 load_questions($game,1); 344 if (!$game->{'questions'}) { 345 $server->command("msg $channel Hmmm, no questions found sorry"); 346 $game->{'state'}="over"; 347 } 348 Irssi::print("Questions looped"); 349 } 350 return if game_over($game); 351 352 #random question 353 ${$game->{'randIDX'}}= @{$game->{'questions'}}; 354 ${$game->{'randIDX'}}=rand(${$game->{'randIDX'}}); 355 my $q = ${$game->{'questions'}}[${$game->{'randIDX'}}]; 356 ${$game->{'the_question'}} = $q; 357 358 #removing it from the questions 359 splice (@{$game->{'questions'}}, ${$game->{'randIDX'}}, 1); 360 361 #see faq for splice/random may be bad 362 #my $q = splice(@{$game->{'questions'}},rand(@{$game->{'questions'}}),1); 363 chomp $q; 364 $q =~ s///; 365 #($game->{'answer'} = $q) =~ s/^(.*)\|//; 366 367 ($game->{'question'}, $game->{'answer'}) = split(/\|/, $q,2); 368 $answerBAKforCHAOS = $game->{'answer'}; 369 if ( $game->{'answer'} =~ /\|/ ) 370 { $server->command("msg $channel KAOS CHAOS ����!!!"); 371 $game->{'answer'} = $game->{'answer'}."|"; 372 373 } 374 $server->command("msg $channel 13Question: 10 $game->{'question'} "); 375 #added �-��-� so it can hide greek too 376 ($game->{'current_answer'} = $game->{'answer'}) =~ s/[a-zA-Z0-9�-��-�]/*/g; 377 #$q = s/^(.*)\|.*?$/$1/; 378 $server->command("msg $channel Answer: ".$game->{'current_answer'}); 379 $game->{'state'}="question"; 380} 381 382sub invite_join($$) { 383 my ($server,$channel) = @_; 384 my $game = $s->{$server->{'tag'}}->{$channel}; 385 386 $server->command("msg $channel Team Trivia thingummie v($VERSION) starts in 1 minute. Type 4!join red or 12!join blue"); 387 $game->{'timeouttag'} = Irssi::timeout_add(60000,"start_game",$game); 388 $game->{'timeout'} = time()+60; 389} 390 391sub secstonormal($) { 392 my $seconds = shift; 393 my ($m,$s); 394 395 $s = $seconds % 60; 396 $m = ($seconds - $s)/60; 397 return sprintf("%02d:%02d",$m,$s); 398} 399 400sub do_pubcommand($$$$) { 401 my ($command,$channel,$server,$nick) = @_; 402 my $game = $s->{$server->{'tag'}}->{$channel}; 403 404 $command = lc $command; 405 $command =~ s/\s*$//; 406 407 if ($command =~ /^!bang$/) { 408 $server->command("msg $channel Dumping..."); 409 foreach (split /\n/,Dumper($s)) { 410 Irssi::print("$_"); 411 } 412 } elsif ($command =~ /^!trivon$|^!start$|!����$/) { 413 if ($s->{$server->{'tag'}}->{$channel}) { 414 if ($s->{$server->{'tag'}}->{$channel}->{'state'}) { 415 $server->command("msg $nick Trivia is already on. Use !trivoff or !stop to remove it."); 416 return; 417 } 418 #undef $s->{$server->{'tag'}}->{$channel}; 419 } else { 420 # create structure magically 421 $game = $s->{$server->{'tag'}}->{$channel} = {}; 422 $game->{'tag'} = $server->{'tag'}; 423 $game->{'channel'} = $channel; 424 } 425 $game->{'teams'}={}; 426 $game->{'redscore'} = 0; 427 $game->{'bluescore'} = 0; 428 load_questions($game,0); 429 $game->{'state'} = "join"; 430 invite_join($server,$channel); 431 } elsif ($command =~ /^!trivoff$|^!stop$|!������$/) { 432 return if !$game->{'state'}; 433 $game->{'state'}="over"; 434 game_over($game); 435 } elsif ($command =~ /^!join/) { 436 if ($command =~ /^!join (red|blue)$/) { 437 return if !$game->{'state'}; 438 $game->{'teams'}->{$nick}=$1; 439 if ($1 eq "blue") { 440 $server->command("notice $nick You have joined the 12Blue team"); 441 } else { 442 $server->command("notice $nick You have joined the 4Red team"); 443 } 444 } 445 } elsif ($command =~ /^!teams/) { 446 return if !$game->{'state'}; 447 my @blue=(); 448 my @red=(); 449 foreach (sort keys %{$game->{'teams'}}) { 450 push @blue, $_ if $game->{'teams'}->{$_} eq "blue"; 451 push @red, $_ if $game->{'teams'}->{$_} eq "red"; 452 } 453 $server->command("msg $channel 12Blue: ".join(",",@blue)); 454 $server->command("msg $channel 4Red : ".join(",",@red)); 455 } elsif ($command =~ /^!repeat$/) { 456 return if !$game->{'state'}; 457 $server->command("msg $channel Question is $game->{'question'}"); 458 } elsif ($command =~ /^!timeleft$/) { 459 if ($game->{'state'} eq "join" and $game->{'timeout'}) { 460 my $diff = $game->{'timeout'} - time(); 461 if ($diff > 0) { 462 $server->command("msg $channel Time left: ".secstonormal($diff)); 463 } else { 464 Irssi::print("Timeleft: $diff ??"); 465 } 466 } 467 } 468} 469 470sub do_command($$$) { 471 my ($command,$nick,$server) = @_; 472 473 $command = lc $command; 474 $command =~ s/\s*$//; 475 476 if ($command =~ /^!bang$/) { 477 $server->command("msg $nick BOOM!"); 478 } elsif ($command =~ /^admin/) { 479 if ($command !~ /^admin (.*)$/) { 480 $server->command("msg $nick admin needs a nick to change the admin user to!"); 481 } else { 482 Irssi::settings_remove("quiz_admin"); 483 Irssi::settings_add_str("misc","quiz_admin",$1); 484 $server->command("msg $nick admin user is now $1"); 485 } 486 } else { 487 #$server->command("msg $nick Unknown command '$command'"); 488 } 489} 490 491#check check_answer for bad { } 492 493sub check_answer($$$$) { 494 my ($server,$channel,$nick,$text) = @_; 495 my $game = $s->{$server->{'tag'}}->{$channel}; 496 497 return if not exists $game->{'teams'}->{$nick}; 498 499 #if $text exist check time and remembers it for end-game 500 if (defined $text) { 501 $game->{'time_last_text'} = time(); 502 } 503 504 505 $text =~ s/\s*$//; 506 $text =~ s/^ //; 507 $text =~ s/ $//; 508 509 #from cgi input-purify / try without it and it will crash with :( 510 #dont know if it needs em all, may check it in future 511 512 if ($text =~ s/([\&;\`'\\\|"*?~<>^\(\)\[\]\{\}\$\n\r])/\\$1/g) 513 { $text="abcdef"; 514 } 515 # is the above the reason it didnt join? 516 517 #if greek supports troubles you comment the next 518 $text =~ y/����������������ڼ�ۿ������������������������/���������������������������������������������/; 519 520 my $answerNOtonos = lc $game->{'answer'}; 521 #if greek supports troubles you comment the next 522 $answerNOtonos =~ y/����������������ڼ�ۿ������������������������/���������������������������������������������/; 523 524 if ( $answerNOtonos =~ /\|/) { 525 526 if ( ($answerNOtonos =~ /\|$text\|/)||($answerNOtonos =~ /^$text\|/) ) 527 { 528 $answerNOtonos =~ s/$text\|//; 529 530 $game->{'answer'}=$answerNOtonos; 531 532 533 $server->command("msg $channel 2Correct answer by ". 534 ($game->{'teams'}->{$nick} eq "blue"?"12":"4"). 535 $nick.": ".$text); 536 537 $game->{$game->{'teams'}->{$nick}."score"}++; 538 $game->{'scores'}->{$game->{'teams'}->{$nick}.$nick}++; 539 540 if ($answerNOtonos eq "") { 541 542 #putting it in used 543 if (@{$game->{'used_questions'}}){ 544 ${$game->{'usedCounter'}} = @{$game->{'used_questions'}}; 545 } else {${$game->{'usedCounter'}}=0;} 546 547 ${$game->{'used_questions'}}[${$game->{'usedCounter'}}]=${$game->{'the_question'}}; 548 549 $game->{'state'}="won"; 550 551 $server->command("msg $channel 2 $answerBAKforCHAOS") ; 552 show_scores($game); 553 return; 554 } 555 556 #show_scores($game); 557 } 558 } 559 560 561 elsif (( $answerNOtonos !~ /\|/) && (lc $text eq $answerNOtonos)) { 562 563 $server->command("msg $channel 2Correct answer by ". 564 ($game->{'teams'}->{$nick} eq "blue"?"12":"4"). 565 $nick.": ".$game->{'answer'}); 566 $game->{'state'}="won"; 567 568 #putting it in used 569 if (@{$game->{'used_questions'}}){ 570 ${$game->{'usedCounter'}} = @{$game->{'used_questions'}}; 571 } else {${$game->{'usedCounter'}} =0;} 572 573 ${$game->{'used_questions'}}[${$game->{'usedCounter'}}]=${$game->{'the_question'}}; 574 575 $game->{$game->{'teams'}->{$nick}."score"}++; 576 $game->{'scores'}->{$game->{'teams'}->{$nick}.$nick}++; 577 show_scores($game); 578 return; 579 } 580 581 582 583 my $show=0; 584 my @chars = split //,$text; 585 586 for (my $i=0; $i<length($game->{'answer'}); $i++) { 587 if (lc $chars[$i] eq lc substr($game->{'answer'},$i,1)) { 588 $show = 1 if substr($game->{'current_answer'},$i,1) 589 eq "*"; 590 substr($game->{'current_answer'},$i,1) = 591 substr($game->{'answer'},$i,1); 592 } 593 } 594 595 $server->command("msg $channel Answer: ".$game->{'current_answer'}) 596 if $show; 597} 598 599 600 601sub event_privmsg { 602 my ($server,$data,$nick,$address) = @_; 603 my ($target, $text) = split / :/,$data,2; 604 my ($command); 605 606 if ($target =~ /^#/) { 607 my $game = $s->{$server->{'tag'}}->{$target}; 608 if ($text =~ /^!/) { 609 do_pubcommand($text,$target,$server,$nick); 610 } elsif ($game->{'state'} eq "question") { 611 check_answer($server,$target,$nick,$text); 612 } 613 } else { 614 if ($nick ne Irssi::settings_get_str("quiz_admin")) { 615 my ($passwd); 616 ($passwd, $command) = split /\s/,$text,2; 617 if ($passwd ne Irssi::settings_get_str("quiz_passwd")) { 618 #Irssi::print("$nick tried to do $command but got the password wrong."); 619 Irssi::print("$nick got the password wrong."); 620 } 621 } else { 622 $command = $text; 623 } 624 do_command($command,$nick,$server); 625 } 626} 627 628sub event_changed_nick { 629 my ($channel,$nick,$oldnick) = @_; 630 my $server = $channel->{'server'}; 631 my $game = $s->{$server->{'tag'}}->{$channel->{'name'}}; 632 633 return if !$game->{'state'}; 634 635 my $nicktxt = $nick->{'nick'}; 636 if ($game->{'teams'}->{$oldnick}) { 637 $game->{'teams'}->{$nicktxt} = $game->{'teams'}->{$oldnick}; 638 delete $game->{'teams'}->{$oldnick}; 639 } 640 641} 642 643 644} 645 646Irssi::signal_add_last("event privmsg", "event_privmsg"); 647# Irssi::signal_add_last("massjoin", "sig_massjoin"); 648#Irssi::signal_add_last("message nick", "on_nick"); #when /nick 649#Irssi::signal_add_last("message part", "on_part"); 650#Irssi::signal_add_last("message join", "on_join"); 651#Irssi::signal_add_last("message quit", "on_quit"); 652 653# Channel::nicks(channel) Return a list of all nicks in channel. 654 655Irssi::signal_add("nicklist changed", "event_changed_nick"); 656