2019-05-06 09:41:12 +00:00
import threading
import time
2019-08-26 18:09:10 +00:00
from collections import namedtuple
2019-05-06 09:41:12 +00:00
import channel
import gameloop
2019-05-13 18:39:22 +00:00
import random
import re
2019-05-06 09:41:12 +00:00
nickserv_pass = None
irc_chan = None
game_channel = None
2019-08-26 18:09:10 +00:00
HelpEntry = namedtuple ( ' HelpEntry ' , [ ' synopsis ' , ' desc ' , ' is_sub ' ] )
2019-05-06 13:00:55 +00:00
def chunk ( l , n ) :
assert 0 < n
chunked = [ ]
item = [ ]
for i in l :
if len ( item ) > = n :
chunked . append ( item )
item = [ ]
item . append ( i )
if len ( item ) > 0 :
chunked . append ( item )
return chunked
2019-05-06 09:41:12 +00:00
class GameLoop ( threading . Thread ) :
def __init__ ( self , irc , chan , irc_chan ) :
self . irc = irc
self . chan = chan
self . irc_chan = irc_chan
threading . Thread . __init__ ( self )
def send ( self , message ) :
2019-05-06 11:13:13 +00:00
message_parts = message . encode ( ) . split ( b ' ' )
line = [ ]
line_len = 0
for part in message_parts :
if len ( part ) + line_len > 440 :
self . irc . bot_response ( self . irc_chan , b ' ' . join ( line ) )
line = [ ]
line_len = 0
line . append ( part )
line_len + = len ( part ) + 1
if len ( line ) > 0 :
self . irc . bot_response ( self . irc_chan , b ' ' . join ( line ) )
2019-05-06 09:41:12 +00:00
def notice ( self , recipient , message ) :
2019-05-06 11:13:13 +00:00
recipient = recipient . encode ( )
message_parts = message . encode ( ) . split ( b ' ' )
line = [ ]
line_len = 0
for part in message_parts :
if len ( part ) + line_len > 440 :
2019-05-09 07:35:34 +00:00
self . irc . send_raw ( b ' NOTICE %s : %s %s ' % ( recipient , self . irc_chan , b ' ' . join ( line ) ) )
2019-05-06 11:13:13 +00:00
line = [ ]
line_len = 0
line . append ( part )
line_len + = len ( part ) + 1
if len ( line ) > 0 :
2019-05-10 16:34:04 +00:00
self . irc . send_raw ( b ' NOTICE %s : %s %s ' % ( recipient , self . irc_chan , b ' ' . join ( line ) ) )
2019-05-06 09:41:12 +00:00
def get_event ( self ) :
event = self . chan . recv ( )
return event
2019-05-06 13:00:55 +00:00
def voice ( self , nicks ) :
if type ( nicks ) == str : nicks = [ nicks ]
for nicks in chunk ( nicks , 4 ) :
self . irc . send_raw ( b ' MODE %s + %s %s ' % ( self . irc_chan , b ' v ' * len ( nicks ) , b ' ' . join ( i . encode ( ) for i in nicks ) ) )
def devoice ( self , nicks ) :
if type ( nicks ) == str : nicks = [ nicks ]
for nicks in chunk ( nicks , 4 ) :
self . irc . send_raw ( b ' MODE %s - %s %s ' % ( self . irc_chan , b ' v ' * len ( nicks ) , b ' ' . join ( i . encode ( ) for i in nicks ) ) )
2019-05-06 09:41:12 +00:00
def run ( self ) :
try :
2019-05-06 13:00:55 +00:00
gameloop . game ( self . send , self . notice , self . voice , self . devoice , self . get_event )
2019-05-06 09:41:12 +00:00
except Exception as err :
self . send ( ' Crash! ( %s , %s ) ' % ( type ( err ) , repr ( err ) ) )
finally :
self . chan . close ( )
def start_gameloop ( irc ) :
global game_channel , irc_chan
if game_channel is not None :
return
chan = channel . Channel ( )
GameLoop ( irc , chan , irc_chan ) . start ( )
game_channel = chan
def stop_gameloop ( ) :
global game_channel
if game_channel is None :
return
game_channel . send ( ( gameloop . events . quit , ) )
game_channel = None
def send_event ( event ) :
global game_channel
game_channel . send ( event )
2019-05-12 11:30:13 +00:00
def usage ( command ) :
2019-08-26 18:09:10 +00:00
entries = {
( ' status ' , ) : HelpEntry ( ' !status ' , ' Show the game status. ' , False ) ,
( ' ready ' , ) : HelpEntry ( ' !ready ' , ' Mark yourself as ready. ' , False ) ,
( ' unready ' , ) : HelpEntry ( ' !unready ' , ' Mark yourself as not ready. ' , False ) ,
( ' kill ' , ) : HelpEntry ( ' !kill ' , ' Stop the game. ' , False ) ,
( ' leave ' , ) : HelpEntry ( ' !leave ' , ' Leave the game. ' , False ) ,
( ' players ' , ) : HelpEntry ( ' !players ' , ' Show a list of players. ' , False ) ,
( ' cards ' , ) : HelpEntry ( ' !cards ' , ' Show the cards in your hand. ' , False ) ,
( ' origins ' , ) : HelpEntry ( ' !origins ' , ' Show the cardcast codes of the decks containing the cards that can be picked currently. ' , False ) ,
( ' redeal ' , ) : HelpEntry ( ' !redeal ' , ' Remove all cards from your hand and redeal. ' , False ) ,
( ' start ' , ) : HelpEntry ( ' !start [<preset>] ' , ' Start a game with the specified preset. If no preset is given, use " default " . Available presets are: default, empty, offtopia, offtopia-random ' , False ) ,
( ' join ' , ) : HelpEntry ( ' !join [<message>] ' , ' Join the game with the specified message. ' , False ) ,
( ' kick ' , ) : HelpEntry ( ' !kick <nick> ' , ' Kick the specified player from the game. ' , False ) ,
( ' card ' , ) : HelpEntry ( ' [!card] <number[,number,...]> ... ' , ' Pick the specified cards. If multiple numbers are specified for a single pick, play one of them at random. ' , False ) ,
( ' jape ' , ) : HelpEntry ( ' [!jape] <number[,number,...]> ... ' , ' See !card. ' , False ) ,
( ' deck ' , ) : HelpEntry ( ' !deck add | remove | list ' , ' Manage decks. ' , True ) ,
( ' bot ' , ) : HelpEntry ( ' !bot add | remove ' , ' Manage bots. ' , True ) ,
( ' limit ' , ) : HelpEntry ( ' !limit [<number> [<type>]] ' , ' Show or adjust the win limit. Type can be " r " for rounds and " p " for points. ' , False ) ,
( ' help ' , ) : HelpEntry ( ' !help [command [subcommand]] ' , ' Show a synopsis and description for the specified command. ' , False ) ,
( ' deck ' , ' add ' ) : HelpEntry ( ' !deck add <code> | random ' , ' Add the deck with the specified cardcast code (or pick one randomly). ' , False ) ,
( ' deck ' , ' remove ' ) : HelpEntry ( ' !deck remove <code> ' , ' Remove the deck with the specified cardcast code. ' , False ) ,
( ' deck ' , ' list ' ) : HelpEntry ( ' !deck list ' , ' List selected decks. ' , False ) ,
( ' bot ' , ' add ' ) : HelpEntry ( ' !bot add <type> [<name>] ' , ' Add a bot of the specified type and name. If the name is omitted, name the bot after its type. ' , False ) ,
( ' bot ' , ' remove ' ) : HelpEntry ( ' !bot remove <name> ' , ' Remove the specified bot. ' , False ) ,
}
2019-05-12 11:30:13 +00:00
if type ( command ) == str :
command = [ command ]
if len ( command ) > 0 :
if command [ 0 ] [ 0 ] == ' ! ' :
command [ 0 ] = command [ 0 ] [ 1 : ]
if len ( command ) == 0 :
2019-08-26 18:09:10 +00:00
return ' ' . join ( sorted ( set ( ' ! ' + cmd [ 0 ] for cmd in entries . keys ( ) ) ) )
elif len ( command ) > 2 :
return ' Uh, how did we get %i args? ' % len ( command )
2019-05-12 11:30:13 +00:00
2019-08-26 18:09:10 +00:00
key = tuple ( command )
if key in entries :
e = entries [ key ]
return ' %s : %s %s ' % ( ' Subcommands ' if e . is_sub else ' Usage ' , e . synopsis , e . desc )
2019-05-12 11:30:13 +00:00
else :
2019-08-26 18:09:10 +00:00
return ' No such command ! %s ' % ' ' . join ( command )
2019-05-12 11:30:13 +00:00
2019-05-06 10:50:27 +00:00
def parse_command ( message , nick , irc ) :
def send ( m ) :
global irc_chan
irc . bot_response ( irc_chan , m )
2019-05-12 11:32:52 +00:00
def arg ( num , index = 1 ) :
2019-05-06 10:50:27 +00:00
nonlocal message
if type ( num ) == int :
num = [ num ]
if len ( message ) - index not in num :
2019-05-12 11:30:13 +00:00
send ( usage ( message [ : index ] ) )
2019-05-06 10:50:27 +00:00
return None
return message [ index : ]
2019-05-13 18:39:22 +00:00
def valid_choice ( c ) :
return re . fullmatch ( r ' \ d+(, \ d+)* ' , c )
2019-05-06 09:41:12 +00:00
events = gameloop . events
2019-05-06 10:50:27 +00:00
message = message . split ( )
if len ( message ) == 0 : return
c = message [ 0 ]
if c == ' !status ' :
2019-05-12 11:32:52 +00:00
if arg ( 0 ) is not None :
2019-05-06 10:50:27 +00:00
send_event ( ( events . status , ) )
elif c == ' !start ' :
2019-05-12 11:32:52 +00:00
args = arg ( [ 0 , 1 ] )
if args is not None :
if len ( args ) == 0 :
2019-05-06 11:29:34 +00:00
send_event ( ( events . start , nick ) )
else :
2019-05-12 11:32:52 +00:00
send_event ( ( events . start , nick , args [ 0 ] ) )
2019-05-06 10:50:27 +00:00
elif c == ' !ready ' :
2019-05-12 11:32:52 +00:00
if arg ( 0 ) is not None :
2019-05-06 10:50:27 +00:00
send_event ( ( events . ready , nick ) )
elif c == ' !unready ' :
2019-05-12 11:32:52 +00:00
if arg ( 0 ) is not None :
2019-05-06 10:50:27 +00:00
send_event ( ( events . unready , nick ) )
elif c == ' !kill ' :
2019-05-12 11:32:52 +00:00
if arg ( 0 ) is not None :
2019-05-06 10:50:27 +00:00
send_event ( ( events . kill , ) )
elif c == ' !join ' :
2019-05-10 18:46:39 +00:00
if len ( message ) > 1 :
send_event ( ( events . join , nick , ' ' . join ( message [ 1 : ] ) ) )
else :
2019-05-06 10:50:27 +00:00
send_event ( ( events . join , nick ) )
elif c == ' !leave ' :
2019-05-12 11:32:52 +00:00
if arg ( 0 ) is not None :
2019-05-06 10:50:27 +00:00
send_event ( ( events . leave , nick ) )
elif c == ' !players ' :
2019-05-12 11:32:52 +00:00
if arg ( 0 ) is not None :
2019-05-06 10:50:27 +00:00
send_event ( ( events . players , ) )
2019-05-07 09:05:59 +00:00
elif c == ' !kick ' :
2019-05-12 11:32:52 +00:00
args = arg ( 1 )
if args is not None :
kickee , = args
2019-05-07 09:05:59 +00:00
send_event ( ( events . kick , nick , kickee ) )
2019-05-06 10:50:27 +00:00
elif c == ' !deck ' :
if len ( message ) < 2 :
2019-05-12 11:30:13 +00:00
send ( usage ( ' !deck ' ) )
2019-05-06 10:50:27 +00:00
return
subc = message [ 1 ]
if subc == ' add ' :
2019-05-12 11:32:52 +00:00
args = arg ( 1 , 2 )
if args is not None :
code , = args
2019-05-06 10:50:27 +00:00
if code == ' random ' :
send_event ( ( events . deck_add_random , ) )
else :
send_event ( ( events . deck_add , code ) )
elif subc == ' remove ' :
2019-05-12 11:32:52 +00:00
args = arg ( 1 , 2 )
if args is not None :
code , = args
2019-05-06 10:50:27 +00:00
send_event ( ( events . deck_remove , code ) )
elif subc == ' list ' :
2019-05-12 11:32:52 +00:00
if arg ( 0 , 2 ) is not None :
2019-05-06 10:50:27 +00:00
send_event ( ( events . deck_list , ) )
2019-05-06 09:41:12 +00:00
2019-05-06 10:50:27 +00:00
else :
2019-05-12 11:30:13 +00:00
send ( usage ( ' !deck ' ) )
2019-05-06 10:50:27 +00:00
2019-05-10 16:25:55 +00:00
elif c == ' !bot ' :
if len ( message ) < 2 :
2019-05-12 11:30:13 +00:00
send ( usage ( ' !bot ' ) )
2019-05-10 16:25:55 +00:00
return
subc = message [ 1 ]
if subc == ' add ' :
2019-05-12 11:32:52 +00:00
args = arg ( [ 1 , 2 ] , 2 )
if args is not None :
if len ( args ) == 2 :
bot_type , name = args
2019-05-10 16:54:30 +00:00
else :
2019-05-12 11:32:52 +00:00
bot_type , = args
2019-05-10 16:54:30 +00:00
name = bot_type
2019-05-10 16:25:55 +00:00
if bot_type == ' rando ' :
send_event ( ( events . bot_add_rando , name ) )
else :
send ( ' Allowed bot types: rando ' )
elif subc == ' remove ' :
2019-05-12 11:32:52 +00:00
args = arg ( 1 , 2 )
if args is not None :
name , = args
2019-05-10 16:25:55 +00:00
send_event ( ( events . bot_remove , name ) )
else :
2019-05-12 11:30:13 +00:00
send ( usage ( ' !bot ' ) )
2019-05-10 16:25:55 +00:00
2019-05-06 10:50:27 +00:00
elif c == ' !limit ' :
2019-05-12 11:32:52 +00:00
args = arg ( [ 0 , 1 , 2 ] )
if args is None : return
2019-05-06 10:50:27 +00:00
2019-05-12 11:32:52 +00:00
if len ( args ) == 0 :
2019-05-06 10:50:27 +00:00
send_event ( ( events . limit , ) )
else :
2019-05-12 11:32:52 +00:00
num = args [ 0 ]
2019-05-06 10:50:27 +00:00
if not num . isdecimal ( ) :
2019-05-12 11:30:13 +00:00
send ( usage ( ' !limit ' ) )
2019-05-06 10:50:27 +00:00
return
num = int ( num )
2019-05-12 11:32:52 +00:00
if len ( args ) == 2 :
limit_type = args [ 1 ]
2019-05-06 10:50:27 +00:00
if limit_type == ' p ' or limit_type == ' points ' :
send_event ( ( events . limit , gameloop . limit_types . points , num ) )
elif limit_type == ' r ' or limit_type == ' rounds ' :
send_event ( ( events . limit , gameloop . limit_types . rounds , num ) )
else :
send ( ' Allowed limit types: p(oints), r(ounds) ' )
else :
send_event ( ( events . limit , gameloop . limit_types . points , num ) )
2019-10-11 16:11:02 +00:00
elif c == ' !card ' or c == ' !jape ' or all ( valid_choice ( i ) for i in message ) :
if c == ' !card ' or c == ' !jape ' :
2019-05-12 11:32:52 +00:00
args = message [ 1 : ]
2019-05-06 10:50:27 +00:00
else :
2019-05-12 11:32:52 +00:00
args = message
2019-05-06 10:50:27 +00:00
2019-05-13 18:39:22 +00:00
if not all ( valid_choice ( i ) for i in args ) :
2019-05-12 11:30:13 +00:00
send ( usage ( ' !card ' ) )
2019-05-06 10:50:27 +00:00
return
2019-05-13 18:39:22 +00:00
def pick ( c ) :
if c . isdecimal ( ) :
return c
else :
return random . choice ( c . split ( ' , ' ) )
choices = [ int ( pick ( i ) ) for i in args ]
2019-05-06 10:50:27 +00:00
send_event ( ( events . card , nick , choices ) )
elif c == ' !cards ' :
2019-05-12 11:32:52 +00:00
if arg ( 0 ) is not None :
2019-05-06 10:50:27 +00:00
send_event ( ( events . cards , nick ) )
2019-05-06 16:37:29 +00:00
elif c == ' !origins ' :
2019-05-12 11:32:52 +00:00
if arg ( 0 ) is not None :
2019-05-06 16:37:29 +00:00
send_event ( ( events . origins , nick ) )
2019-05-11 17:55:40 +00:00
elif c == ' !redeal ' :
2019-05-12 11:32:52 +00:00
if arg ( 0 ) is not None :
2019-05-11 17:55:40 +00:00
send_event ( ( events . redeal , nick ) )
2019-05-06 10:50:27 +00:00
elif c == ' !help ' :
2019-05-12 11:32:52 +00:00
args = arg ( [ 0 , 1 , 2 ] )
if args is not None :
send ( usage ( args ) )
2019-05-06 09:41:12 +00:00
2018-01-03 16:08:24 +00:00
# initialize(*, config)
2017-09-06 17:47:32 +00:00
# Called to initialize the IRC bot
# Runs before even logger is brought up, and blocks further bringup until it's done
2018-01-03 16:08:24 +00:00
# config is a configpatser.ConfigParser object containig contents of bot.conf
def initialize ( * , config ) :
2019-05-06 09:41:12 +00:00
global nickserv_pass , irc_chan
nickserv_pass = config [ ' nickserv ' ] [ ' password ' ]
irc_chan = config [ ' server ' ] [ ' channels ' ] . split ( ) [ 0 ] . encode ( )
2017-09-06 17:47:32 +00:00
2018-01-02 16:31:23 +00:00
# on_connect(*, irc)
# Called after IRC bot has connected and sent the USER/NICk commands but not yet attempted anything else
2018-06-14 07:50:30 +00:00
# Called for every reconnect
2018-01-02 16:31:23 +00:00
# Blocks the bot until it's done, including PING/PONG handling
2018-01-03 16:08:24 +00:00
# irc is the IRC API object
2018-01-02 16:31:23 +00:00
def on_connect ( * , irc ) :
2019-05-06 09:41:12 +00:00
global nickserv_pass
2019-05-06 12:10:54 +00:00
if nickserv_pass != ' ' :
irc . msg ( b ' nickserv ' , b ' IDENTIFY ' + nickserv_pass . encode ( ) )
time . sleep ( 30 ) # One day I will do this correctly. Today is not the day
2019-05-06 09:41:12 +00:00
stop_gameloop ( )
start_gameloop ( irc )
2018-01-02 16:31:23 +00:00
2018-01-20 14:54:13 +00:00
# on_quit(*, irc)
# Called just before IRC bot sends QUIT
# Blocks the bot until it's done, including PING/PONG handling
# irc is the IRC API object
def on_quit ( * , irc ) :
2019-05-06 09:41:12 +00:00
stop_gameloop ( )
2018-01-20 14:54:13 +00:00
2017-09-06 17:47:32 +00:00
# handle_message(*, prefix, message, nick, channel, irc)
# Called for PRIVMSGs.
# prefix is the prefix at the start of the message, without the leading ':'
# message is the contents of the message
# nick is who sent the message
# channel is where you should send the response (note: in queries nick == channel)
# irc is the IRC API object
2018-06-14 07:50:30 +00:00
# All strings are bytestrings
2017-09-06 17:47:32 +00:00
def handle_message ( * , prefix , message , nick , channel , irc ) :
2019-05-06 09:41:12 +00:00
global irc_chan
if channel == irc_chan :
2019-05-06 10:50:27 +00:00
parse_command ( message . decode ( ) , nick . decode ( ) , irc )
2017-09-06 17:47:32 +00:00
# handle_nonmessage(*, prefix, command, arguments, irc)
# Called for all other commands than PINGs and PRIVMSGs.
# prefix is the prefix at the start of the message, without the leading ':'
# command is the command or number code
# arguments is rest of the arguments of the command, represented as a list. ':'-arguments are handled automatically
2018-01-03 16:08:24 +00:00
# irc is the IRC API object
2018-06-14 07:50:30 +00:00
# All strings are bytestrings
2017-09-06 17:47:32 +00:00
def handle_nonmessage ( * , prefix , command , arguments , irc ) :
2019-05-06 10:50:27 +00:00
if command == b ' NICK ' :
old = prefix . split ( b ' ! ' ) [ 0 ] . decode ( )
new = arguments [ 0 ] . decode ( )
send_event ( ( gameloop . events . nick_change , old , new ) )
elif command == b ' PART ' or command == b ' QUIT ' :
nick = prefix . split ( b ' ! ' ) [ 0 ] . decode ( )
send_event ( ( gameloop . events . leave , nick ) )
elif command == b ' KICK ' :
nick = arguments [ 1 ] . decode ( )
send_event ( ( gameloop . events . leave , nick ) )