2019-05-05 20:39:50 +00:00
import enum
import random
from collections import namedtuple
import cardcast_api
class events ( enum . Enum ) :
2019-05-10 16:25:55 +00:00
( quit , nick_change ,
status , start , ready , unready , kill ,
join , leave , players , kick ,
deck_add , deck_add_random , deck_remove , deck_list ,
bot_add_rando , bot_remove ,
limit ,
2019-05-11 17:55:40 +00:00
card , cards , origins , redeal ) = range ( 22 )
2019-05-05 20:39:50 +00:00
class limit_types ( enum . Enum ) :
points , rounds = range ( 2 )
2020-04-09 19:08:16 +00:00
# fqcode = fully qualified code = (namespace, code)
Deck = namedtuple ( ' Deck ' , [ ' fqcode ' , ' name ' , ' author ' , ' call_count ' , ' response_count ' , ' calls ' , ' responses ' ] )
2019-05-05 20:39:50 +00:00
Limit = namedtuple ( ' Limit ' , [ ' type ' , ' number ' ] )
2019-05-06 15:56:26 +00:00
Card = namedtuple ( ' Card ' , [ ' deck ' , ' text ' ] )
2020-04-09 19:08:16 +00:00
Namespace = namedtuple ( ' Namespace ' , [ ' url ' , ' supports_random ' ] )
deck_namespaces = {
2022-03-23 18:41:40 +00:00
' colondeck ' : Namespace ( ' https://puck.moe/_/cards/ ' , False ) ,
2020-06-02 23:07:13 +00:00
' ahti ' : Namespace ( ' https://ahti.space/cards/ ' , True )
2020-04-09 19:08:16 +00:00
}
2019-05-15 17:50:02 +00:00
class IRCFormattingState :
def __init__ ( self ) :
# 99 is the "client default colour"
self . fg_color = 99
self . bg_color = 99
self . bold = False
self . italic = False
self . underline = False
self . reverse = False
def __eq__ ( self , other ) :
return (
self . fg_color == other . fg_color and
self . bg_color == other . bg_color and
self . bold == other . bold and
self . italic == other . italic and
self . underline == other . underline and
self . reverse == other . reverse
)
def copy ( self ) :
new = IRCFormattingState ( )
new . fg_color = self . fg_color
new . bg_color = self . bg_color
new . bold = self . bold
new . italic = self . italic
new . underline = self . underline
new . reverse = self . reverse
return new
2019-05-05 20:39:50 +00:00
class Player :
def __init__ ( self , nick ) :
self . nick = nick
self . hand = [ ]
self . points = 0
2019-05-10 18:46:39 +00:00
self . message = None
2019-05-05 20:39:50 +00:00
def __repr__ ( self ) :
if __name__ == ' __main__ ' :
return ' Player( %s ) ' % repr ( self . nick )
else :
return ' %s .Player( %s ) ' % ( __name__ , repr ( self . nick ) )
def __hash__ ( self ) :
return id ( self )
2019-05-10 16:25:55 +00:00
class Rando :
def __init__ ( self , name ) :
self . nick = ' < %s > ' % name
self . hand = [ ]
self . points = 0
2019-05-10 18:46:39 +00:00
self . message = None
2019-05-10 16:25:55 +00:00
def num_need_cards ( self , num_blanks ) :
2019-05-11 08:08:31 +00:00
return max ( num_blanks - len ( self . hand ) + self . hand . count ( None ) , 0 )
2019-05-10 16:25:55 +00:00
def give_cards ( self , cards ) :
self . hand . extend ( cards )
self . hand = [ i for i in self . hand if i is not None ]
def play ( self , num_blanks ) :
2019-05-21 15:23:38 +00:00
return self . hand [ : num_blanks ]
2019-05-10 16:25:55 +00:00
2019-05-06 09:41:12 +00:00
class Error : pass
2019-05-05 20:39:50 +00:00
2019-05-06 13:00:55 +00:00
def game ( send , notice , voice , devoice , get_event ) :
2019-05-05 20:39:50 +00:00
def error ( message ) :
send ( ' Error: %s ' % message )
def errwrapper ( message , f , * args , * * kwargs ) :
try :
return f ( * args , * * kwargs )
except Exception as err :
error ( message % ( ' %s , %s ' % ( type ( err ) , err ) ) )
return Error
2019-05-10 16:25:55 +00:00
def players_bots ( ) :
nonlocal players , bots
yield from players . values ( )
yield from bots . values ( )
2019-05-05 20:39:50 +00:00
def add_player ( nick ) :
nonlocal players
assert nick not in players
players [ nick ] = Player ( nick )
def remove_player ( nick ) :
nonlocal players
del players [ nick ]
def change_player_nick ( old , new ) :
nonlocal players
player = players [ old ]
del players [ old ]
player . nick = new
players [ new ] = player
def list_players ( ) :
2019-05-10 16:25:55 +00:00
nonlocal players , bots
2019-05-05 20:39:50 +00:00
2019-05-10 16:25:55 +00:00
send ( ' , ' . join ( sorted ( players ) + sorted ( i . nick for i in bots . values ( ) ) ) )
2019-05-05 20:39:50 +00:00
2020-04-09 19:08:16 +00:00
def add_deck ( namespace , code ) :
2019-05-05 20:39:50 +00:00
nonlocal decks
2020-04-09 19:08:16 +00:00
assert ( namespace , code ) not in decks
2019-05-05 20:39:50 +00:00
2020-04-09 19:08:16 +00:00
base_url = deck_namespaces [ namespace ] . url
2019-05-10 15:01:05 +00:00
2019-05-05 20:39:50 +00:00
# First get info for the deck we're adding
2019-05-10 15:01:05 +00:00
info = cardcast_api . info ( code , base_url = base_url )
2019-05-05 20:39:50 +00:00
# Extract the information we want to keep of the deck
name = info [ ' name ' ]
author = info [ ' author ' ] [ ' username ' ]
# Get cards
2019-05-10 15:01:05 +00:00
calls , responses = cardcast_api . cards ( code , base_url = base_url )
call_count = len ( calls )
response_count = len ( responses )
2019-05-05 20:39:50 +00:00
2019-05-06 12:17:33 +00:00
# Preprocess calls so that ___ becomes only one _
# _ are indicated by splitting the card at that point, e.g.
# ['foo ', '.'] is "foo _."
# Two blanks a row will thus be ['foo ', '', '.']
# We can't just remove every single '', since it can be valid
# at the start and the end of a card
for i in range ( len ( calls ) ) :
call = [ ]
for index , part in enumerate ( calls [ i ] ) :
if index == 0 or index == len ( calls [ i ] ) - 1 :
# Always pass these ones through
call . append ( part )
elif part == ' ' :
# Remove '' in the middle
continue
else :
call . append ( part )
calls [ i ] = call
2019-05-12 16:29:50 +00:00
# Preprocess calls so that they are cut short if they're >200 chars
for i in range ( len ( calls ) ) :
call = [ ]
combined_length = 0
for index , part in enumerate ( calls [ i ] ) :
if combined_length + len ( part ) > 200 :
part = part [ : 200 - combined_length ] + ' … '
call . append ( part )
combined_length + = len ( part ) + 1
calls [ i ] = call
# Preprocess responses so that they are at max. 160 chars
for i in range ( len ( responses ) ) :
if len ( responses [ i ] ) > 160 :
responses [ i ] = responses [ i ] [ : 159 ] + ' … '
2019-05-05 20:39:50 +00:00
# Add a new deck to list of decks
2020-04-09 19:08:16 +00:00
decks [ ( namespace , code ) ] = Deck (
fqcode = ( namespace , code ) ,
2019-05-05 20:39:50 +00:00
name = name ,
author = author ,
call_count = call_count ,
response_count = response_count ,
calls = calls ,
responses = responses
)
2020-04-09 19:08:16 +00:00
def get_random_deck_code ( namespace ) :
nonlocal remote_deck_count
2019-05-05 20:39:50 +00:00
2020-04-09 19:08:16 +00:00
base_url = deck_namespaces [ namespace ] . url
# Keep track of how many cards there are on the remote, so that we don't
# need to keep rerequesting that
if namespace not in remote_deck_count :
code , remote_deck_count [ namespace ] = cardcast_api . random_code ( base_url = base_url )
else :
code , remote_deck_count [ namespace ] = cardcast_api . random_code ( count = remote_deck_count [ namespace ] , base_url = base_url )
2019-05-05 20:39:50 +00:00
return code
2020-04-09 19:08:16 +00:00
def remove_deck ( namespace , code ) :
2019-05-10 16:25:55 +00:00
nonlocal decks , round_call_card
2019-05-06 16:37:29 +00:00
# Purge all the cards from the deck from the game
2019-05-10 16:25:55 +00:00
for player_bot in players_bots ( ) :
for index , card in enumerate ( player_bot . hand ) :
2020-04-09 19:08:16 +00:00
if card is not None and card . deck . fqcode == ( namespace , code ) :
2019-05-10 16:25:55 +00:00
player_bot . hand [ index ] = None
2019-05-10 15:01:05 +00:00
2020-04-09 19:08:16 +00:00
if round_call_card is not None and round_call_card . deck . fqcode == ( namespace , code ) :
2019-05-06 16:37:29 +00:00
round_call_card = None
2019-05-05 20:39:50 +00:00
2020-04-09 19:08:16 +00:00
del decks [ ( namespace , code ) ]
2019-05-05 20:39:50 +00:00
def list_decks ( ) :
nonlocal decks
if len ( decks ) == 0 :
send ( ' No decks ' )
return
for deck in decks . values ( ) :
call_count = deck . call_count
calls_left = len ( deck . calls )
calls = str ( call_count ) if call_count == calls_left else ' %i / %i ' % ( calls_left , call_count )
response_count = deck . response_count
responses_left = len ( deck . responses )
responses = str ( response_count ) if response_count == responses_left else ' %i / %i ' % ( responses_left , response_count )
2020-04-09 19:08:16 +00:00
namespace , code = deck . fqcode
send ( ' %s ( %s %s , by %s , %s black, %s white) ' % (
2019-05-05 20:39:50 +00:00
deck . name ,
2020-04-09 19:08:16 +00:00
namespace ,
code ,
2019-05-05 20:39:50 +00:00
deck . author ,
calls ,
responses
) )
2020-04-09 19:08:16 +00:00
def deck_add_handler ( namespace , code ) :
2019-05-06 11:29:34 +00:00
nonlocal decks
2020-04-09 19:08:16 +00:00
if namespace in deck_namespaces :
if ( namespace , code ) not in decks :
errwrapper ( ' Failure adding deck: %s %s ( %% s) ' % ( namespace , code ) , add_deck , namespace , code )
else :
send ( ' Deck already added ' )
2019-05-06 11:29:34 +00:00
else :
2020-04-09 19:08:16 +00:00
send ( ' Unknown deck namespace %s . Try one of: %s ' % ( namespace , ' , ' . join ( deck_namespaces . keys ( ) ) ) )
2019-05-06 11:29:34 +00:00
2020-04-09 19:08:16 +00:00
def deck_add_random_handler ( namespace ) :
2019-05-06 11:29:34 +00:00
nonlocal decks
2020-04-09 19:08:16 +00:00
if namespace in deck_namespaces :
if deck_namespaces [ namespace ] . supports_random :
2020-06-17 00:37:50 +00:00
for _ in range ( 5 ) :
2020-04-09 19:08:16 +00:00
code = errwrapper ( ' Failure getting random code for a deck. ( %s ) ' , get_random_deck_code , namespace )
if code is Error : return
if ( namespace , code ) not in decks : break
send ( ' That was weird, got %s randomly but it was already added ' % code )
2020-06-17 00:37:50 +00:00
else :
send ( ' Did not get a fresh random deck in 5 tries, bailing out ' )
return
2020-04-09 19:08:16 +00:00
errwrapper ( ' Failure adding deck: %s %s ( %% s) ' % ( namespace , code ) , add_deck , namespace , code )
send ( ' Added deck %s ( %s %s ) ' % ( decks [ ( namespace , code ) ] . name , namespace , code ) )
else :
send ( ' Namespace %s does \' t support adding a random deck. Try one of: %s ' % ( namespace , ' , ' . join ( namespace for namespace in deck_namespaces . keys ( ) if deck_namespaces [ namespace ] . supports_random ) ) )
else :
send ( ' Unknown deck namespace %s . Try one of: %s ' % ( namespace , ' , ' . join ( deck_namespaces . keys ( ) ) ) )
2019-05-06 11:29:34 +00:00
2019-05-06 16:37:29 +00:00
def get_hand_origins ( player ) :
hand_origins = [ ]
for card in player . hand :
if card is None :
hand_origins . append ( ' <empty> ' )
else :
2020-04-09 19:08:16 +00:00
hand_origins . append ( ' %s %s ' % card . deck . fqcode )
2019-05-06 16:37:29 +00:00
return ' , ' . join ( ' %i : %s ' % ( index , i ) for index , i in enumerate ( hand_origins ) )
2019-05-05 20:39:50 +00:00
def common_handler ( event , args ) :
2019-05-10 16:25:55 +00:00
nonlocal players , bots , decks , limit
2019-05-05 20:39:50 +00:00
if event == events . kill :
send ( ' Stopping game ' )
return no_game
elif event == events . quit :
return quit
elif event == events . nick_change :
old , new = args
2019-05-06 10:48:04 +00:00
if old in players :
change_player_nick ( old , new )
2019-05-05 20:39:50 +00:00
elif event == events . join :
2019-05-10 18:46:39 +00:00
if len ( args ) == 2 :
nick , message = args
if nick not in players :
add_player ( nick )
voice ( nick )
players [ nick ] . message = message
send ( ' %s has joined %s ' % ( nick , message ) )
2019-05-05 20:39:50 +00:00
else :
2019-05-10 18:46:39 +00:00
nick , = args
if nick not in players :
add_player ( nick )
voice ( nick )
players [ nick ] . message = None
send ( ' %s has joined ' % nick )
2019-05-05 20:39:50 +00:00
elif event == events . leave :
nick , = args
if nick not in players :
2019-05-14 19:00:06 +00:00
# Ignore those not in the game
pass
2019-05-05 20:39:50 +00:00
elif errwrapper ( ' Could not remove player %s ( %% s) ' % nick , remove_player , nick ) is not Error :
2019-05-06 13:00:55 +00:00
devoice ( nick )
2019-05-05 20:39:50 +00:00
send ( ' %s has left the game ' % nick )
elif event == events . players :
list_players ( )
2019-05-07 09:05:59 +00:00
elif event == events . kick :
kicker , kickee = args
if kicker not in players :
# Ignore those not in the game
pass
elif kickee not in players :
send ( ' No such player %s ' % kickee )
elif errwrapper ( ' Could not remove player %s ( %% s) ' % kickee , remove_player , kickee ) is not Error :
devoice ( kickee )
2019-05-10 16:26:16 +00:00
send ( ' %s has been removed from the game ' % kickee )
2019-05-07 09:05:59 +00:00
2019-05-05 20:39:50 +00:00
elif event == events . deck_add :
2020-04-09 19:08:16 +00:00
namespace , code = args
deck_add_handler ( namespace , code )
2019-05-05 20:39:50 +00:00
elif event == events . deck_add_random :
2020-04-09 19:08:16 +00:00
namespace , = args
deck_add_random_handler ( namespace )
2019-05-05 20:39:50 +00:00
elif event == events . deck_remove :
2020-04-09 19:08:16 +00:00
namespace , code = args
if ( namespace , code ) in decks :
errwrapper ( ' Failure removing deck %s ( %% s) ' % code , remove_deck , namespace , code )
2019-05-05 20:39:50 +00:00
else :
2020-04-09 19:08:16 +00:00
send ( ' No such deck %s %s ' % ( namespace , code ) )
2019-05-05 20:39:50 +00:00
elif event == events . deck_list :
list_decks ( )
2019-05-10 16:25:55 +00:00
elif event == events . bot_add_rando :
name , = args
if name not in bots :
bots [ name ] = Rando ( name )
2019-05-10 16:34:51 +00:00
send ( ' Bot %s added ' % name )
2019-05-10 16:25:55 +00:00
else :
send ( ' Bot named %s already exists ' % name )
elif event == events . bot_remove :
name , = args
if name in bots :
del bots [ name ]
2019-06-01 19:39:51 +00:00
send ( ' Bot %s removed ' % name )
2019-05-10 16:25:55 +00:00
else :
send ( ' No such bot %s ' % name )
2019-05-05 20:39:50 +00:00
elif event == events . limit :
if len ( args ) == 0 :
limit_type = { limit_types . rounds : ' rounds ' , limit_types . points : ' points ' } [ limit . type ]
send ( ' Limit is %i %s ' % ( limit . number , limit_type ) )
else :
limit_type , number = args
limit = Limit ( limit_type , number )
limit_type = { limit_types . rounds : ' rounds ' , limit_types . points : ' points ' } [ limit . type ]
send ( ' Limit set to %i %s ' % ( limit . number , limit_type ) )
2019-05-06 16:37:29 +00:00
elif event == events . origins :
nick , = args
if nick in players :
origins = get_hand_origins ( players [ nick ] )
if origins != ' ' :
notice ( nick , origins )
2019-05-11 17:55:40 +00:00
elif event == events . card or event == events . cards or event == events . redeal :
# Ignore card commands if no cards are available yet
2019-05-05 20:39:50 +00:00
pass
2019-05-06 08:32:11 +00:00
elif event == events . ready or event == events . unready :
# Ignore readiness commands by default
2019-05-06 08:00:39 +00:00
pass
2019-05-05 20:39:50 +00:00
else :
error ( ' Unknown event type: %s ' % event )
2019-05-10 19:01:22 +00:00
def start_game ( rest ) :
if len ( rest ) == 0 or rest [ 0 ] == ' default ' :
2020-06-02 23:07:13 +00:00
send ( ' Adding the default CAH deck (ahti Base) ' )
2019-05-10 19:01:22 +00:00
2020-06-02 23:07:13 +00:00
deck_add_handler ( ' ahti ' , ' Base ' )
2019-05-10 19:01:22 +00:00
2019-06-06 18:43:01 +00:00
elif rest [ 0 ] == ' offtopia-random ' :
2024-07-10 19:31:46 +00:00
send ( ' Adding the default CAH deck (ahti Base), offtopia injoke deck (ahti offtopiadeck), :Deck (colondeck colondeck) and three random ahti decks ' )
2019-05-10 19:01:22 +00:00
2020-06-02 23:07:13 +00:00
deck_add_handler ( ' ahti ' , ' Base ' )
2024-07-10 19:31:46 +00:00
deck_add_handler ( ' ahti ' , ' offtopiadeck ' )
2022-03-23 18:41:40 +00:00
deck_add_handler ( ' colondeck ' , ' colondeck ' )
2019-05-10 19:01:22 +00:00
2020-04-09 19:08:16 +00:00
for _ in range ( 3 ) :
2020-06-02 23:07:13 +00:00
deck_add_random_handler ( ' ahti ' )
2019-05-10 19:01:22 +00:00
2019-06-06 18:43:01 +00:00
elif rest [ 0 ] == ' offtopia ' :
2024-07-10 19:31:46 +00:00
send ( ' Adding the default CAH deck (ahti Base), offtopia injoke deck (ahti offtopiadeck), and :Deck (colondeck colondeck) ' )
2019-05-11 17:35:27 +00:00
2020-06-02 23:07:13 +00:00
deck_add_handler ( ' ahti ' , ' Base ' )
2024-07-10 19:31:46 +00:00
deck_add_handler ( ' ahti ' , ' offtopiadeck ' )
2022-03-23 18:41:40 +00:00
deck_add_handler ( ' colondeck ' , ' colondeck ' )
2019-05-11 17:35:27 +00:00
2019-08-26 16:34:08 +00:00
elif rest [ 0 ] != ' empty ' :
2019-05-10 19:01:22 +00:00
send ( ' Unknown preset %s ' % rest [ 0 ] )
2019-08-26 16:34:08 +00:00
return False
2019-05-10 19:01:22 +00:00
2019-08-26 16:34:08 +00:00
return True
2019-05-10 19:01:22 +00:00
2019-05-05 20:39:50 +00:00
def no_game ( ) :
2019-05-10 16:25:55 +00:00
nonlocal players , bots , decks , limit , round_number , round_call_card , czar , card_choices
2019-05-06 13:00:55 +00:00
if players is not None :
devoice ( players )
2019-05-05 20:39:50 +00:00
players = { }
2019-05-10 16:25:55 +00:00
bots = { }
2019-05-05 20:39:50 +00:00
decks = { }
limit = Limit ( limit_types . points , 5 )
round_number = 1
round_call_card = None
czar = None
card_choices = None
while True :
event , * args = get_event ( )
if event == events . status :
send ( ' Idle ' )
elif event == events . start :
2019-05-06 11:29:34 +00:00
nick , * rest = args
2019-05-05 20:39:50 +00:00
add_player ( nick )
2019-05-06 13:00:55 +00:00
voice ( nick )
2019-05-05 20:39:50 +00:00
send ( ' %s started a game, !join to join! ' % nick )
2019-08-26 16:34:08 +00:00
if start_game ( rest ) :
limit_type = { limit_types . rounds : ' rounds ' , limit_types . points : ' points ' } [ limit . type ]
send ( ' Limit is %i %s , change with !limit ' % ( limit . number , limit_type ) )
send ( ' Once you are ready to start the game, everyone send !ready ' )
2019-05-06 11:29:34 +00:00
2019-08-26 16:34:08 +00:00
return game_setup
else :
send ( ' Stopping game ' )
2019-12-11 20:13:06 +00:00
# If we don't do this, the partially started game state doesn't get cleared off correctly
return no_game
2019-05-06 11:29:34 +00:00
2019-05-10 19:01:22 +00:00
elif event == events . join :
2019-06-09 18:52:23 +00:00
send ( ' Start a game with !start [<preset>] ' )
2019-05-05 20:39:50 +00:00
elif event == events . quit :
return quit
else :
2019-05-07 08:42:40 +00:00
pass
2019-05-05 20:39:50 +00:00
def game_setup ( ) :
nonlocal players
2019-05-06 08:32:11 +00:00
players_ready = set ( )
2019-05-05 20:39:50 +00:00
while True :
if len ( players ) == 0 :
2019-05-19 15:05:39 +00:00
send ( ' Lost all players, quitting game setup ' )
2019-05-05 20:39:50 +00:00
return no_game
2019-05-06 15:59:07 +00:00
players_ready = set ( i for i in players_ready if i in players . values ( ) )
2019-05-06 10:48:04 +00:00
players_unready = [ i for i in players . values ( ) if i not in players_ready ]
if len ( players_unready ) == 0 : break
2019-05-05 20:39:50 +00:00
event , * args = get_event ( )
if event == events . status :
2019-05-06 08:32:11 +00:00
if len ( players_ready ) == 0 :
send ( ' Game setup ' )
else :
send ( ' Game setup, waiting for %s to be ready ' % ' , ' . join ( i . nick for i in players_unready ) )
2019-05-05 20:39:50 +00:00
elif event == events . start :
2019-05-09 07:34:38 +00:00
if len ( args ) == 1 :
break
else :
send ( ' Can \' t apply presets once the game setup has started. Here !start begins the game without waiting for !ready ' )
2019-05-06 08:32:11 +00:00
elif event == events . ready :
nick , = args
# Ignore if not in the game
if nick not in players :
continue
player = players [ nick ]
if player not in players_ready :
players_ready . add ( player )
elif event == events . unready :
nick , = args
# Ignore if not in the game
if nick not in players :
continue
player = players [ nick ]
if player in players_ready :
players_ready . remove ( player )
2019-05-05 20:39:50 +00:00
else :
r = common_handler ( event , args )
if r is not None : return r
2019-05-06 08:32:11 +00:00
if len ( players ) < 2 :
2019-05-19 14:26:21 +00:00
send ( ' Not enough players (needs at least two joined). Try inviting others and send !ready again once they \' ve joined. ' )
2019-05-06 08:32:11 +00:00
return game_setup
else :
return setup_round
2019-05-05 20:39:50 +00:00
def total_calls ( ) :
nonlocal decks
return sum ( len ( deck . calls ) for deck in decks . values ( ) )
def total_responses ( ) :
nonlocal decks
return sum ( len ( deck . responses ) for deck in decks . values ( ) )
def deal_call ( ) :
nonlocal decks
deck_objs = list ( decks . values ( ) )
while True :
deck = random . choice ( deck_objs )
if len ( deck . calls ) != 0 : break
# See comment about mutation in deal_responses()
index = random . randrange ( len ( deck . calls ) )
2019-05-06 15:56:26 +00:00
return Card ( deck , deck . calls . pop ( index ) )
2019-05-05 20:39:50 +00:00
def deal_responses ( need_responses ) :
nonlocal decks
responses = [ ]
deck_objs = list ( decks . values ( ) )
for i in range ( need_responses ) :
while True :
deck = random . choice ( deck_objs )
if len ( deck . responses ) != 0 : break
# We generate an index and pop that, since that makes
# it easier to mutate the list in place
index = random . randrange ( len ( deck . responses ) )
2019-05-06 15:56:26 +00:00
responses . append ( Card ( deck , deck . responses . pop ( index ) ) )
2019-05-05 20:39:50 +00:00
# Shuffle the responses at the end, as otherwise the first
# cards are more likely to have come from small decks than
# the last cards
random . shuffle ( responses )
return responses
def setup_round ( ) :
2019-05-10 16:25:55 +00:00
nonlocal players , bots , round_call_card , czar , card_choices
2019-05-05 20:39:50 +00:00
2019-05-06 08:07:26 +00:00
# Select a czar randomly, if we need to
if czar not in players . values ( ) :
czar = random . choice ( list ( players . values ( ) ) )
2019-05-05 20:39:50 +00:00
# Clear out previous round's cards
card_choices = { }
# Check that we have a call card for next round, should we need one
if round_call_card is None :
available_calls = total_calls ( )
if available_calls == 0 :
send ( ' Need a black card, none available. Add decks and continue with !start ' )
return game_setup
# Select call card for the next round
round_call_card = deal_call ( )
2019-05-12 16:15:11 +00:00
# See note above num_blanks in top_of_round()
num_blanks = len ( round_call_card . text ) - 1
2019-05-05 20:39:50 +00:00
# Find out how many response cards we need
2019-05-12 16:15:11 +00:00
hand_size = 9 + num_blanks
2019-05-05 20:39:50 +00:00
need_responses = 0
for player in players . values ( ) :
2019-05-06 08:07:26 +00:00
# Don't deal cards to the czar this round
if player is czar : continue
2019-05-12 16:15:11 +00:00
need_responses + = max ( hand_size - len ( player . hand ) + player . hand . count ( None ) , 0 )
2019-05-10 16:25:55 +00:00
for bot in bots . values ( ) :
need_responses + = bot . num_need_cards ( num_blanks )
2019-05-05 20:39:50 +00:00
# If we don't have enough, kick back to setup
available_responses = total_responses ( )
if available_responses < need_responses :
send ( ' Need %i white cards, only %i available. Add decks and continue with !start ' % ( need_responses , available_responses ) )
return game_setup
# Get the cards
responses = deal_responses ( need_responses )
# Add responses to players' inventories
for player in players . values ( ) :
2019-05-06 08:07:26 +00:00
# We skipped the czar in the counts, so skip here too
if player is czar : continue
2019-05-12 16:15:11 +00:00
# Move the cards outside of the current hand size into
# the hand
2019-05-13 07:38:04 +00:00
overflow = [ i for i in player . hand [ hand_size : ] if i is not None ]
2019-05-12 16:15:11 +00:00
player . hand = player . hand [ : hand_size ]
for index in range ( len ( player . hand ) ) :
if len ( overflow ) == 0 :
break
if player . hand [ index ] is None :
# .pop(0) instead of .pop() since we
# want to keep the same order
player . hand [ index ] = overflow . pop ( 0 )
# Do we still have some overflow cards we couldn't fit
# into the hand? If so, just stick them at the end and
# we'll just have an oversized hand this round
player . hand . extend ( overflow )
# Fill any remaining empty spots with dealt cards
while len ( player . hand ) < hand_size :
2019-05-05 20:39:50 +00:00
player . hand . append ( responses . pop ( ) )
2019-05-12 16:15:11 +00:00
for index in range ( hand_size ) :
2019-05-05 20:39:50 +00:00
if player . hand [ index ] is None :
player . hand [ index ] = responses . pop ( )
2019-05-10 16:25:55 +00:00
# Give cards to bots
for bot in bots . values ( ) :
needed = bot . num_need_cards ( num_blanks )
fed = responses [ : needed ]
responses = responses [ needed : ]
bot . give_cards ( fed )
2019-05-05 20:39:50 +00:00
return top_of_round
2019-05-15 17:50:02 +00:00
def handle_control_codes ( text , start_state ) :
state = start_state . copy ( )
r = [ ]
index = 0
while index < len ( text ) :
char = text [ index ]
index + = 1
if char == ' \x02 ' :
# ^B - bold
state . bold = not state . bold
r . append ( char )
elif char == ' \x1d ' :
# ^] - italic
state . italic = not state . italic
r . append ( char )
elif char == ' \x1f ' :
# ^_ - underline
state . underline = not state . underline
r . append ( char )
elif char == ' \x16 ' :
# ^V - reverse video mode
state . reverse = not state . reverse
r . append ( char )
elif char == ' \x0f ' :
# ^O - disable all formatting
state = IRCFormattingState ( )
r . append ( char )
elif char == ' \x03 ' :
# ^C - colour
start = index - 1
# Find the foreground colour
# It can be at max 2 digits forwards
# We use <= here since we are comparing against
# the end point of slice, which'll be one more
# than the index of the last cell in the slice
if index + 2 < = len ( text ) and text [ index : index + 2 ] . isdigit ( ) :
state . fg_color = int ( text [ index : index + 2 ] )
index + = 2
elif index + 1 < = len ( text ) and text [ index : index + 1 ] . isdigit ( ) :
state . fg_color = int ( text [ index : index + 1 ] )
index + = 1
else :
# Not a valid colour code after all
r . append ( ' ^C ' )
continue
2019-05-05 20:39:50 +00:00
2019-05-15 17:50:02 +00:00
r . append ( text [ start : index ] )
# Do we have a background colour?
if index < len ( text ) and text [ index ] == ' , ' :
# Maybe
start = index
index + = 1
# Find the bg colour
# Details are the same as above
if index + 2 < = len ( text ) and text [ index : index + 2 ] . isdigit ( ) :
state . bg_color = int ( text [ index : index + 2 ] )
index + = 2
elif index + 1 < = len ( text ) and text [ index : index + 1 ] . isdigit ( ) :
state . bg_color = int ( text [ index : index + 1 ] )
index + = 1
else :
# No bg colour after all
index - = 1
2019-05-05 20:39:50 +00:00
2019-05-15 17:50:02 +00:00
r . append ( text [ start : index ] )
2019-05-05 20:39:50 +00:00
2019-05-15 17:50:02 +00:00
elif ord ( char ) < 32 or ord ( char ) == 127 :
r . append ( ' ^ ' + chr ( ord ( i ) ^ 64 ) )
2019-05-05 20:39:50 +00:00
2019-05-15 17:50:02 +00:00
else :
r . append ( char )
2019-05-06 12:11:10 +00:00
2019-05-15 17:50:02 +00:00
return ( ' ' . join ( r ) , state )
2019-05-06 12:11:10 +00:00
2019-05-15 17:50:02 +00:00
def to_formatting_state ( from_state , to_state ) :
if to_state == from_state :
return ' '
elif to_state == IRCFormattingState ( ) :
return ' \x0f ' # ^O
2019-05-06 12:11:10 +00:00
2019-05-15 17:50:02 +00:00
r = ' '
2019-05-06 12:11:10 +00:00
2019-05-15 17:50:02 +00:00
if to_state . fg_color != from_state . fg_color or to_state . bg_color != from_state . bg_color :
# Always use the full form, if for no other reason than
# to not screw up ,<num> or <num> following this
r + = ' \x03 %02i , %02i ' % ( to_state . fg_color , to_state . bg_color )
2019-05-06 12:11:10 +00:00
2019-05-15 17:50:02 +00:00
if to_state . bold != from_state . bold :
r + = ' \x02 ' # ^B
if to_state . italic != from_state . italic :
r + = ' \x1d ' # ^]
if to_state . underline != from_state . underline :
r + = ' \x1f ' # ^_
if to_state . reverse != from_state . reverse :
r + = ' \x16 ' # ^V
2019-05-06 12:11:10 +00:00
2019-05-15 17:50:02 +00:00
return r
2019-05-06 12:11:10 +00:00
2019-05-15 17:50:02 +00:00
def sanitize ( text , start_state ) :
sanitized , state_after = handle_control_codes ( text , start_state )
return sanitized + to_formatting_state ( state_after , start_state )
2019-05-06 12:11:10 +00:00
2019-05-15 17:50:02 +00:00
def send_cards ( nick ) :
nonlocal players
no_formatting = IRCFormattingState ( )
cards = ' | ' . join ( ' %i : [ %s ] ' % ( index , sanitize ( card . text , no_formatting ) ) for index , card in enumerate ( players [ nick ] . hand ) )
notice ( nick , cards )
2019-05-21 14:29:00 +00:00
def combine_cards ( call , responses , dereference_responses = True ) :
2019-05-15 17:50:02 +00:00
# This function is really messy, in part due to the format used
#
2019-05-21 15:23:38 +00:00
# `call` is a Card object, with `call.text` being a list of
# strings, and here I'll refer to each of those strings as
# "part". This division into parts indicates the locations of
# the blanks. For example:
2019-05-15 17:50:02 +00:00
#
# Foo bar? _.
#
# is stored as
#
# ['Foo bar? ', '.']
#
2019-05-21 15:23:38 +00:00
# So far good, take a part from `call.text`, then one response
2019-05-15 17:50:02 +00:00
# (assuming we still have one remaining) and repeat.
#
# However, CardsAgainstIRC extended the simple system of blanks
# with backreferences that are of the form /\$[0-9]/. Since
# Cardcast doesn't know about them, they are stored like any
# other text. E.g.
#
# Foo _? Bar $0.
# ['Foo ', '? Bar $0.']
#
# You could add backreference support to the earlier algorithm
# by going over each part and replacing each $<num> with the
# corresponding response when you add the call part. It'd give
# this algorith:
#
2019-05-21 15:23:38 +00:00
# 1. Go over call.text[index] and satisfy backrefrences
2019-05-15 17:50:02 +00:00
# 1. Copy text verbatim until we hit a $<num>
2019-05-21 15:23:38 +00:00
# 2. Add responses[num].text
2019-05-15 17:50:02 +00:00
# 3. Repeat
2019-05-21 15:23:38 +00:00
# 2. Add response[index].text
2019-05-15 17:50:02 +00:00
# 3. Repeat
#
# This was how I first implemented it. However, dealing with
# backreferences has more to do with dealing a blank than it
# does with copying the rest of the part For this reason, this
# function deals # with the concept of a "segment". Where parts
# are delineated by blanks, segments are delineated by both
# blanks and backreferences. To reuse the example from earlier:
#
# Foo _? Bar $0.
# ^^^^ ^^^^^^ ^
# 1111 222222 3
#
# Because the storage format is still split by part instead of
# by segment, this function has to manually split each part into
# its segments. That's why we walk both part by part and char by
# char.
#
# Our algorithm is
# 1. Find either a blank, end of card, or a backreference,
# whichever comes first
# blank:
# 1. Copy the segment before this verbatim
# 2. Add next response
# end:
# 1. Copy the segment before this verbatim
# backreference:
# 1. Copy the segment before this verbatim
# 2. Add the response pointed to by the
# backreference
# 2. Repeat
2019-05-19 15:02:42 +00:00
#
# Additionally, we have a special mode in case we are displaying
# the round call card, where we do not look up the responses
2019-05-21 14:29:00 +00:00
# pointed to by backreferences or blanks, but instead add the
# $<num> or _. In this mode `responses` still has to be correct
# length in order to recognize valid backreferences and to
# distinguish between a blank and end of a card (this could
# admitedly use some work).
2019-05-15 17:50:02 +00:00
formatting_state = IRCFormattingState ( )
no_formatting = IRCFormattingState ( )
r = [ ]
part_index = 0
index = 0
segment_start_index = 0
2019-05-21 15:23:38 +00:00
while part_index < len ( call . text ) :
call_part = call . text [ part_index ]
2019-05-15 17:50:02 +00:00
if index > = len ( call_part ) :
# We've reached a blank or the end of the card
# Copy the previous segment fully to `r`
call_segment , formatting_state = handle_control_codes ( call_part [ segment_start_index : ] , formatting_state )
r . append ( call_segment )
# If there is still one, add the response coming
# after that segment
if part_index < len ( responses ) :
2019-05-21 14:29:00 +00:00
if dereference_responses :
# Add response
r . append ( to_formatting_state ( formatting_state , no_formatting ) )
r . append ( ' [ ' )
2019-05-21 15:23:38 +00:00
r . append ( sanitize ( responses [ part_index ] . text , no_formatting ) )
2019-05-21 14:29:00 +00:00
r . append ( ' ] ' )
r . append ( to_formatting_state ( no_formatting , formatting_state ) )
else :
# Add the blank itself (useful for displaying the call card)
r . append ( to_formatting_state ( formatting_state , no_formatting ) )
r . append ( ' _ ' )
r . append ( to_formatting_state ( no_formatting , formatting_state ) )
2019-05-15 17:50:02 +00:00
# Start on a new part as well as a new segment
part_index + = 1
index = 0
segment_start_index = 0
continue
char = call_part [ index ]
index + = 1
if char == ' $ ' :
if index < len ( call_part ) and ord ( ' 0 ' ) < = ord ( call_part [ index ] ) < = ord ( ' 9 ' ) :
# Handle $0 .. $9
# Hopefully we won't run into more
# backreferences in one card
backreference_index = int ( call_part [ index ] )
index + = 1
if 0 < = backreference_index < len ( responses ) :
# Copy the previous segment fully to `r`
call_segment , formatting_state = handle_control_codes ( call_part [ segment_start_index : index - 2 ] , formatting_state )
r . append ( call_segment )
2019-05-19 15:02:42 +00:00
2019-05-21 14:29:00 +00:00
if dereference_responses :
2019-05-19 15:02:42 +00:00
# Add the response this backreference refers to
r . append ( to_formatting_state ( formatting_state , no_formatting ) )
r . append ( ' [ ' )
2019-05-21 15:23:38 +00:00
r . append ( sanitize ( responses [ backreference_index ] . text , no_formatting ) )
2019-05-19 15:02:42 +00:00
r . append ( ' ] ' )
r . append ( to_formatting_state ( no_formatting , formatting_state ) )
else :
# Add the backreference itself (useful for displaying the call card)
r . append ( to_formatting_state ( formatting_state , no_formatting ) )
r . append ( ' $ %i ' % backreference_index )
r . append ( to_formatting_state ( no_formatting , formatting_state ) )
2019-05-15 17:50:02 +00:00
# Start new segment after this char
segment_start_index = index
2019-05-06 12:11:10 +00:00
2019-05-15 17:50:02 +00:00
else :
# A backreference, but not a
# valid one. Copy verbatim
2019-05-21 15:23:38 +00:00
pass
2019-05-15 17:50:02 +00:00
else :
# Not a backreference
2019-05-21 15:23:38 +00:00
pass
2019-05-05 20:39:50 +00:00
2019-05-15 17:50:02 +00:00
r . append ( to_formatting_state ( formatting_state , no_formatting ) )
2019-05-05 20:39:50 +00:00
2019-05-15 17:50:02 +00:00
return ' ' . join ( r )
2019-05-05 20:39:50 +00:00
def top_of_round ( ) :
2019-05-10 16:25:55 +00:00
nonlocal players , bots , round_number , round_call_card , czar , card_choices
2019-05-05 20:39:50 +00:00
choosers = [ i for i in players . values ( ) if i is not czar ]
2019-05-10 16:25:55 +00:00
# Round call card has N parts. Between each of those parts
# goes one response card. Therefore there should be N - 1
# response cards
num_blanks = len ( round_call_card . text ) - 1
2019-05-15 17:50:02 +00:00
send ( ' Round %i . %s is czar. %s choose your cards ' % ( round_number , czar . nick , ' , ' . join ( i . nick for i in choosers ) ) )
2019-05-21 15:23:38 +00:00
send ( ' [ %s ] ' % combine_cards ( round_call_card , [ None ] * num_blanks , dereference_responses = False ) )
2019-05-15 17:50:02 +00:00
2019-05-10 16:25:55 +00:00
# Have bots choose first
for bot in bots . values ( ) :
card_choices [ bot ] = bot . play ( num_blanks )
2019-05-10 15:01:05 +00:00
2019-05-05 20:39:50 +00:00
for nick in players :
if players [ nick ] is not czar :
send_cards ( nick )
2019-07-20 19:23:57 +00:00
while True :
2019-05-05 20:39:50 +00:00
# Make sure that if a chooser leaves, they won't be waited on
choosers = [ i for i in choosers if i in players . values ( ) ]
2019-07-20 19:23:57 +00:00
if len ( choosers ) == 0 :
break
2019-05-05 20:39:50 +00:00
if len ( players ) < 2 :
2019-05-19 15:05:39 +00:00
send ( ' Not enough players to continue (needs at least two), quitting game ' )
2019-05-06 18:01:54 +00:00
return no_game
2019-05-05 20:39:50 +00:00
if czar not in players . values ( ) :
send ( ' Czar left the game, restarting round ' )
return setup_round
event , * args = get_event ( )
if event == events . status :
send ( ' Waiting for %s to choose ' % ' , ' . join ( i . nick for i in choosers ) )
elif event == events . start :
send ( ' Game already in progress ' )
elif event == events . card :
nick , choices = args
# Ignore those not in the game
if nick not in players :
continue
player = players [ nick ]
if player is czar :
notice ( nick , ' Czar can \' t choose now ' )
continue
2019-05-06 08:04:32 +00:00
elif player not in choosers and player not in card_choices :
2019-05-05 20:39:50 +00:00
notice ( nick , ' You \' ll get to choose next round ' )
continue
2019-05-10 16:25:55 +00:00
if len ( choices ) != num_blanks :
2019-05-06 15:56:26 +00:00
notice ( nick , ' Select %i card(s) ' % ( len ( round_call_card . text ) - 1 ) )
2019-05-06 11:57:04 +00:00
continue
2019-05-05 20:39:50 +00:00
selected_cards = [ ]
for choice in choices :
if 0 < = choice < len ( player . hand ) :
2020-07-12 22:27:58 +00:00
selected_cards . append ( player . hand [ choice ] )
2019-05-05 20:39:50 +00:00
else :
notice ( nick , ' %i not in your hand ' % choice )
break
if len ( selected_cards ) != len ( choices ) :
# Failed to use some choice
continue
card_choices [ player ] = selected_cards
2019-05-06 08:04:32 +00:00
if player in choosers :
choosers . remove ( player )
2020-07-14 17:50:10 +00:00
notice ( nick , ' You have chosen: ' + combine_cards ( round_call_card , selected_cards ) )
2019-05-05 20:39:50 +00:00
2019-05-06 08:00:39 +00:00
elif event == events . cards :
nick , = args
if nick not in players :
# Ignore those not in the game
continue
player = players [ nick ]
2019-05-06 08:04:32 +00:00
if player in choosers or player in card_choices :
2019-05-06 08:00:39 +00:00
send_cards ( nick )
else :
notice ( nick , ' You can \' t choose now ' )
2019-05-06 16:37:29 +00:00
elif event == events . origins :
nick , = args
if nick not in players :
2020-04-09 19:08:16 +00:00
notice ( nick , ' call: %s %s ' % round_call_card . deck . fqcode )
2019-05-06 16:37:29 +00:00
else :
2020-04-09 19:08:16 +00:00
notice ( nick , ' call: %s %s , %s ' % ( * round_call_card . deck . fqcode , get_hand_origins ( players [ nick ] ) ) )
2019-05-06 16:37:29 +00:00
2019-05-11 17:55:40 +00:00
elif event == events . redeal :
nick , = args
if nick not in players :
# Ignore those not in the game
continue
player = players [ nick ]
for index in range ( len ( player . hand ) ) :
player . hand [ index ] = None
if player in choosers or player in card_choices :
send ( ' Dealing out a new hand to %s , restarting round ' % nick )
return setup_round
else :
notice ( nick , ' New hand will be dealt next round ' )
2019-05-06 16:37:29 +00:00
elif event == events . deck_remove :
common_handler ( event , args )
# Did we lose our call card?
if round_call_card is None :
# Yes, restart round
2019-05-07 08:48:50 +00:00
send ( ' Lost the black card, restarting round ' )
2019-05-06 16:37:29 +00:00
return setup_round
# Did it remove a card from someone voting this round?
for player in choosers :
if None in player . hand :
# Yes, restart round
2019-05-07 08:48:50 +00:00
send ( ' Lost a card from player \' s hand, restarting round ' )
2019-05-06 16:37:29 +00:00
return setup_round
2019-05-10 16:25:55 +00:00
for player_bot in card_choices :
2019-05-06 16:37:29 +00:00
# We are checking all cards here, not
# just the ones chosen. This is because
# a player may change their selection,
# in which case we might hit a None
2019-05-10 16:25:55 +00:00
if None in player_bot . hand :
2019-05-06 16:37:29 +00:00
# Yes, restart round
2019-05-07 08:48:50 +00:00
send ( ' Lost a card from player \' s hand, restarting round ' )
2019-05-06 16:37:29 +00:00
return setup_round
2019-05-05 20:39:50 +00:00
else :
r = common_handler ( event , args )
if r is not None : return r
return bottom_of_round
def bottom_of_round ( ) :
nonlocal players , round_call_card , czar , card_choices
send ( ' Everyone has chosen. %s , now \' s your time to choose. ' % czar . nick )
# Display the cards
2024-01-14 18:24:29 +00:00
choosers = random . sample ( list ( card_choices . keys ( ) ) , k = len ( card_choices ) )
2019-05-10 16:25:55 +00:00
for index , player_bot in enumerate ( choosers ) :
2019-05-21 15:23:38 +00:00
send ( ' %i : %s ' % ( index , combine_cards ( round_call_card , card_choices [ player_bot ] ) ) )
2019-05-05 20:39:50 +00:00
while True :
if len ( players ) < 2 :
2019-05-19 15:05:39 +00:00
send ( ' Not enough players to continue (needs at least two), quitting game ' )
2019-05-06 18:01:54 +00:00
return no_game
2019-05-05 20:39:50 +00:00
if czar not in players . values ( ) :
send ( ' Czar left the game, restarting round ' )
return setup_round
event , * args = get_event ( )
if event == events . status :
send ( ' Waiting for czar %s to choose ' % czar . nick )
elif event == events . start :
send ( ' Game already in progress ' )
elif event == events . card :
nick , choices = args
# Ignore those not in the game
if nick not in players :
continue
player = players [ nick ]
if player is not czar :
notice ( nick , ' Only the czar can choose now ' )
continue
2019-05-06 17:58:40 +00:00
if len ( choices ) == 1 :
choice = choices [ 0 ]
if 0 < = choice < len ( choosers ) :
2019-05-10 16:25:55 +00:00
player_bot = choosers [ choice ]
player_bot . points + = 1
2019-05-06 17:58:40 +00:00
2019-05-10 16:25:55 +00:00
# Winner is Czar semantics if a
2020-06-23 01:50:20 +00:00
# player who's still in the game
# won, keep same czar if a bot
# won, randomize otherwise
2019-05-10 16:25:55 +00:00
if player_bot in players . values ( ) :
czar = player_bot
2020-06-23 01:50:20 +00:00
elif player_bot in bots . values ( ) :
pass
else :
# setup_round() will
# randomize
czar = None
2019-05-06 17:58:40 +00:00
2019-05-21 15:23:38 +00:00
send ( ' The winner is %s with: %s ' % ( player_bot . nick , combine_cards ( round_call_card , card_choices [ player_bot ] ) ) )
2019-05-06 17:58:40 +00:00
break
else :
notice ( nick , ' %i not in range ' % choice )
2019-05-05 20:39:50 +00:00
2019-05-06 17:58:40 +00:00
elif len ( choices ) == 0 :
# Special case: award everyone a point
# and randomize czar
2019-05-10 16:25:55 +00:00
for player_bot in card_choices :
player_bot . points + = 1
2019-05-05 20:39:50 +00:00
2019-05-06 17:58:40 +00:00
# If we set czar to None, setup_round()
# will handle ramdomizing it for us
czar = None
2019-05-05 20:39:50 +00:00
2019-05-06 17:58:40 +00:00
send ( ' Everyone is a winner! ' )
2019-05-05 20:39:50 +00:00
break
else :
2019-05-06 17:58:40 +00:00
notice ( nick , ' Select one or zero choices ' )
2019-05-05 20:39:50 +00:00
2019-05-06 16:37:29 +00:00
elif event == events . origins :
nick , = args
if nick not in players :
2020-04-09 19:08:16 +00:00
notice ( nick , ' call: %s %s ' % round_call_card . deck . fqcode )
2019-05-06 16:37:29 +00:00
else :
answers_origins = [ ]
2019-05-10 16:25:55 +00:00
for index , player_bot in enumerate ( choosers ) :
2020-04-09 19:08:16 +00:00
answer_origins = [ ' %s %s ' % i . deck . fqcode for i in card_choices [ player_bot ] ]
2019-05-06 16:37:29 +00:00
answers_origins . append ( ' %i : %s ' % ( index , ' , ' . join ( answer_origins ) ) )
2020-04-09 19:08:16 +00:00
notice ( nick , ' call: %s %s ; %s ' % ( * round_call_card . deck . fqcode , ' ; ' . join ( answers_origins ) ) )
2019-05-06 16:37:29 +00:00
2019-05-11 17:55:40 +00:00
elif event == events . redeal :
nick , = args
if nick not in players :
# Ignore those not in the game
continue
player = players [ nick ]
for index in range ( len ( player . hand ) ) :
player . hand [ index ] = None
2019-05-21 15:23:38 +00:00
notice ( nick , ' New hand will be dealt next round ' )
2019-05-11 17:55:40 +00:00
2019-05-06 16:37:29 +00:00
elif event == events . deck_remove :
common_handler ( event , args )
# Did we lose our call card?
if round_call_card is None :
# Yes, restart round
2019-05-07 08:48:50 +00:00
send ( ' Lost the black card, restarting round ' )
2019-05-06 16:37:29 +00:00
return setup_round
2019-05-05 20:39:50 +00:00
else :
r = common_handler ( event , args )
if r is not None : return r
points = [ ]
2019-05-10 16:25:55 +00:00
for player_bot in players_bots ( ) :
if player_bot in choosers :
points . append ( ' %s : %i ( %i ) ' % ( player_bot . nick , player_bot . points , choosers . index ( player_bot ) ) )
2019-05-05 20:39:50 +00:00
else :
2019-05-10 16:25:55 +00:00
points . append ( ' %s : %i ' % ( player_bot . nick , player_bot . points ) )
2019-05-05 20:39:50 +00:00
send ( ' Points: %s ' % ' | ' . join ( points ) )
return teardown_round
def teardown_round ( ) :
nonlocal players , limit , round_number , round_call_card , card_choices
if limit . type == limit_types . rounds :
if round_number > = limit . number :
return end_game
elif limit . type == limit_types . points :
2019-05-10 16:25:55 +00:00
if max ( i . points for i in players_bots ( ) ) > = limit . number :
2019-05-05 20:39:50 +00:00
return end_game
# Remove the cards that were played this round from hands
2019-05-10 16:25:55 +00:00
for player_bot in card_choices :
2019-05-21 15:23:38 +00:00
played = card_choices [ player_bot ]
for card in card_choices [ player_bot ] :
for index , hand_card in enumerate ( player_bot . hand ) :
if hand_card is card :
player_bot . hand [ index ] = None
break
2019-05-05 20:39:50 +00:00
# Increase the number of the round and clear the call card
# These are not done in setup_round() since we might want to
# restart a round in case the czar leaves
round_number + = 1
round_call_card = None
return setup_round
def end_game ( ) :
nonlocal players
2019-05-10 16:25:55 +00:00
max_score = max ( i . points for i in players_bots ( ) )
2019-05-05 20:39:50 +00:00
2019-05-10 16:25:55 +00:00
winners = [ i for i in players_bots ( ) if i . points == max_score ]
2019-05-05 20:39:50 +00:00
2019-05-10 18:46:39 +00:00
if len ( winners ) == 1 :
winner , = winners
if winner . message is not None :
send ( ' We have a winner! %s won %s ' % ( winner . nick , winner . message ) )
else :
send ( ' We have a winner! %s ' % winner . nick )
else :
send ( ' We have the winners! %s ' % ' , ' . join ( i . nick for i in winners ) )
2019-05-05 20:39:50 +00:00
return no_game
def quit ( ) :
pass
players = None
2019-05-10 16:25:55 +00:00
bots = None
2019-05-05 20:39:50 +00:00
decks = None
limit = None
round_number = None
round_call_card = None
czar = None
card_choices = None
2020-04-09 19:08:16 +00:00
remote_deck_count = { }
2019-05-05 20:39:50 +00:00
state = no_game
while state != quit :
state = state ( )
if __name__ == ' __main__ ' :
2019-05-06 13:00:55 +00:00
def get_event ( ) :
while True :
try :
t = input ( ' > ' )
except EOFError :
return ( events . quit , )
if t == ' nick ' :
old = input ( ' old> ' )
new = input ( ' new> ' )
return ( events . nick_change , old , new )
elif t == ' start ' :
nick = input ( ' nick> ' )
return ( events . start , nick )
2019-05-10 18:13:03 +00:00
elif t == ' start_preset ' :
nick = input ( ' nick> ' )
preset = input ( ' preset> ' )
return ( events . start , nick , preset )
2019-05-06 13:00:55 +00:00
elif t == ' ready ' :
nick = input ( ' nick> ' )
return ( events . ready , nick )
elif t == ' unready ' :
nick = input ( ' nick> ' )
return ( events . unready , nick )
elif t == ' status ' :
return ( events . status , )
elif t == ' kill ' :
return ( events . kill , )
elif t == ' join ' :
nick = input ( ' nick> ' )
return ( events . join , nick )
2019-05-10 18:46:39 +00:00
elif t == ' join_message ' :
nick = input ( ' nick> ' )
message = input ( ' message> ' )
return ( events . join , nick , message )
2019-05-06 13:00:55 +00:00
elif t == ' leave ' :
nick = input ( ' nick> ' )
return ( events . leave , nick )
elif t == ' players ' :
return ( events . players , )
2019-05-07 09:05:59 +00:00
elif t == ' kick ' :
kicker = input ( ' kicker> ' )
kickee = input ( ' kickee> ' )
return ( events . kick , kicker , kickee )
2019-05-06 13:00:55 +00:00
elif t == ' deck add ' :
2020-04-09 19:08:16 +00:00
namespace = input ( ' namespace> ' )
2019-05-06 13:00:55 +00:00
code = input ( ' code> ' )
2020-04-09 19:08:16 +00:00
return ( events . deck_add , namespace , code )
2019-05-06 13:00:55 +00:00
elif t == ' deck add random ' :
2020-04-09 19:08:16 +00:00
namespace = input ( ' namespace> ' )
return ( events . deck_add_random , namespace )
2019-05-06 13:00:55 +00:00
elif t == ' deck remove ' :
2020-04-09 19:08:16 +00:00
namespace = input ( ' namespace> ' )
2019-05-06 13:00:55 +00:00
code = input ( ' code> ' )
2020-04-09 19:08:16 +00:00
return ( events . deck_remove , namespace , code )
2019-05-06 13:00:55 +00:00
elif t == ' deck list ' :
return ( events . deck_list , )
2019-05-10 16:25:55 +00:00
elif t == ' bot add rando ' :
name = input ( ' name> ' )
return ( events . bot_add_rando , name )
elif t == ' bot remove ' :
name = input ( ' name> ' )
return ( events . bot_remove , name )
2019-05-06 13:00:55 +00:00
elif t == ' limit ' :
return ( events . limit , )
elif t == ' limit_set ' :
limit_type = { ' r ' : limit_types . rounds , ' p ' : limit_types . points } [ input ( ' type (p/r)> ' ) ]
number = int ( input ( ' limit> ' ) )
return ( events . limit , limit_type , number )
elif t == ' card ' :
nick = input ( ' nick> ' )
choice = [ int ( i ) for i in input ( ' choice> ' ) . split ( ) ]
return ( events . card , nick , choice )
elif t == ' cards ' :
nick = input ( ' nick> ' )
return ( events . cards , nick )
2019-05-06 16:37:29 +00:00
elif t == ' origins ' :
nick = input ( ' nick> ' )
return ( events . origins , nick )
2019-05-11 17:55:40 +00:00
elif t == ' redeal ' :
nick = input ( ' nick> ' )
return ( events . redeal , nick )
2019-05-06 13:00:55 +00:00
else :
print ( ' ? ' )
def send ( text ) :
print ( text )
def notice ( nick , text ) :
print ( ' \t ' , nick , text )
def nop ( * args , * * kwargs ) : pass
game ( send , notice , nop , nop , get_event )