396b3c0185
Since leave-event was hooked up into ppl quiting and parting from the channel as well as doing !leave, the message telling ppl were not in the game would get triggered unintentionally.
1173 lines
29 KiB
Python
1173 lines
29 KiB
Python
import enum
|
|
import random
|
|
from collections import namedtuple
|
|
|
|
import cardcast_api
|
|
|
|
class events(enum.Enum):
|
|
(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,
|
|
card, cards, origins, redeal) = range(22)
|
|
|
|
class limit_types(enum.Enum):
|
|
points, rounds = range(2)
|
|
|
|
Deck = namedtuple('Deck', ['code', 'name', 'author', 'call_count', 'response_count', 'calls', 'responses'])
|
|
|
|
Limit = namedtuple('Limit', ['type', 'number'])
|
|
|
|
Card = namedtuple('Card', ['deck', 'text'])
|
|
|
|
class Player:
|
|
def __init__(self, nick):
|
|
self.nick = nick
|
|
|
|
self.hand = []
|
|
self.points = 0
|
|
self.message = None
|
|
|
|
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)
|
|
|
|
class Rando:
|
|
def __init__(self, name):
|
|
self.nick = '<%s>' % name
|
|
|
|
self.hand = []
|
|
self.points = 0
|
|
self.message = None
|
|
|
|
def num_need_cards(self, num_blanks):
|
|
return max(num_blanks - len(self.hand) + self.hand.count(None), 0)
|
|
|
|
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):
|
|
return list(range(num_blanks))
|
|
|
|
class Error: pass
|
|
|
|
def game(send, notice, voice, devoice, get_event):
|
|
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
|
|
|
|
def players_bots():
|
|
nonlocal players, bots
|
|
|
|
yield from players.values()
|
|
yield from bots.values()
|
|
|
|
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():
|
|
nonlocal players, bots
|
|
|
|
send(', '.join(sorted(players) + sorted(i.nick for i in bots.values())))
|
|
|
|
def add_deck(code):
|
|
nonlocal decks
|
|
assert code not in decks
|
|
|
|
# Colondeck lives elsewhere
|
|
if code == 'colondeck':
|
|
base_url = 'https://dl.puckipedia.com/'
|
|
else:
|
|
base_url = None
|
|
|
|
# First get info for the deck we're adding
|
|
info = cardcast_api.info(code, base_url = base_url)
|
|
|
|
# Extract the information we want to keep of the deck
|
|
name = info['name']
|
|
author = info['author']['username']
|
|
|
|
# Get cards
|
|
calls, responses = cardcast_api.cards(code, base_url = base_url)
|
|
|
|
call_count = len(calls)
|
|
response_count = len(responses)
|
|
|
|
# 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
|
|
|
|
# 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] + '…'
|
|
|
|
# Add a new deck to list of decks
|
|
decks[code] = Deck(
|
|
code = code,
|
|
name = name,
|
|
author = author,
|
|
call_count = call_count,
|
|
response_count = response_count,
|
|
calls = calls,
|
|
responses = responses
|
|
)
|
|
|
|
def get_random_deck_code():
|
|
nonlocal cardcast_deck_count
|
|
|
|
# Provide the count on subsequent calls
|
|
# First time around cardcast_deck_count will be None, so it
|
|
# gets requested from Cardcast, like if we didn't pass the
|
|
# `count` parameter
|
|
# This will update cardcast_deck_count for each call
|
|
# unnecessarily, but I think it simplifies the code and is not
|
|
# too bad
|
|
code, cardcast_deck_count = cardcast_api.random_code(count = cardcast_deck_count)
|
|
|
|
return code
|
|
|
|
def remove_deck(code):
|
|
nonlocal decks, round_call_card
|
|
|
|
# Purge all the cards from the deck from the game
|
|
for player_bot in players_bots():
|
|
for index, card in enumerate(player_bot.hand):
|
|
if card is not None and card.deck.code == code:
|
|
player_bot.hand[index] = None
|
|
|
|
if round_call_card is not None and round_call_card.deck.code == code:
|
|
round_call_card = None
|
|
|
|
del decks[code]
|
|
|
|
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)
|
|
|
|
send('%s (%s, by %s, %s black, %s white)' % (
|
|
deck.name,
|
|
deck.code,
|
|
deck.author,
|
|
calls,
|
|
responses
|
|
))
|
|
|
|
def deck_add_handler(code):
|
|
nonlocal decks
|
|
|
|
if code not in decks:
|
|
errwrapper('Failure adding deck: %s (%%s)' % code, add_deck, code)
|
|
else:
|
|
send('Deck already added')
|
|
|
|
def deck_add_random_handler():
|
|
nonlocal decks
|
|
|
|
# Let's hope this never bites us in the butt
|
|
while True:
|
|
code = errwrapper('Failure getting random code for a deck. (%s)', get_random_deck_code)
|
|
if code is Error: return
|
|
if code not in decks: break
|
|
send('That was weird, got %s randomly but it was already added' % code)
|
|
errwrapper('Failure adding deck: %s (%%s)' % code, add_deck, code)
|
|
send('Added deck %s (%s)' % (decks[code].name, code))
|
|
|
|
def get_hand_origins(player):
|
|
hand_origins = []
|
|
|
|
for card in player.hand:
|
|
if card is None:
|
|
hand_origins.append('<empty>')
|
|
else:
|
|
hand_origins.append(card.deck.code)
|
|
|
|
return ', '.join('%i: %s' % (index, i) for index, i in enumerate(hand_origins))
|
|
|
|
def common_handler(event, args):
|
|
nonlocal players, bots, decks, limit
|
|
|
|
if event == events.kill:
|
|
send('Stopping game')
|
|
return no_game
|
|
|
|
elif event == events.quit:
|
|
return quit
|
|
|
|
elif event == events.nick_change:
|
|
old, new = args
|
|
if old in players:
|
|
change_player_nick(old, new)
|
|
|
|
elif event == events.join:
|
|
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))
|
|
else:
|
|
nick, = args
|
|
|
|
if nick not in players:
|
|
add_player(nick)
|
|
voice(nick)
|
|
|
|
players[nick].message = None
|
|
send('%s has joined' % nick)
|
|
|
|
elif event == events.leave:
|
|
nick, = args
|
|
if nick not in players:
|
|
# Ignore those not in the game
|
|
pass
|
|
|
|
elif errwrapper('Could not remove player %s (%%s)' % nick, remove_player, nick) is not Error:
|
|
devoice(nick)
|
|
send('%s has left the game' % nick)
|
|
|
|
elif event == events.players:
|
|
list_players()
|
|
|
|
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)
|
|
send('%s has been removed from the game' % kickee)
|
|
|
|
elif event == events.deck_add:
|
|
code, = args
|
|
deck_add_handler(code)
|
|
|
|
elif event == events.deck_add_random:
|
|
deck_add_random_handler()
|
|
|
|
elif event == events.deck_remove:
|
|
code, = args
|
|
if code in decks:
|
|
errwrapper('Failure removing deck %s (%%s)' % code, remove_deck, code)
|
|
else:
|
|
send('No such deck %s' % code)
|
|
|
|
elif event == events.deck_list:
|
|
list_decks()
|
|
|
|
elif event == events.bot_add_rando:
|
|
name, = args
|
|
if name not in bots:
|
|
bots[name] = Rando(name)
|
|
send('Bot %s added' % name)
|
|
else:
|
|
send('Bot named %s already exists' % name)
|
|
|
|
elif event == events.bot_remove:
|
|
name, = args
|
|
if name in bots:
|
|
del bots[name]
|
|
else:
|
|
send('No such bot %s' % name)
|
|
|
|
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))
|
|
|
|
elif event == events.origins:
|
|
nick, = args
|
|
|
|
if nick in players:
|
|
origins = get_hand_origins(players[nick])
|
|
if origins != '':
|
|
notice(nick, origins)
|
|
|
|
elif event == events.card or event == events.cards or event == events.redeal:
|
|
# Ignore card commands if no cards are available yet
|
|
pass
|
|
|
|
elif event == events.ready or event == events.unready:
|
|
# Ignore readiness commands by default
|
|
pass
|
|
|
|
else:
|
|
error('Unknown event type: %s' % event)
|
|
|
|
def start_game(rest):
|
|
expert = False
|
|
if len(rest) == 0 or rest[0] == 'default':
|
|
send('Adding the default CAH deck (A5DCM)')
|
|
|
|
deck_add_handler('A5DCM')
|
|
|
|
elif rest[0] == 'offtopia':
|
|
send('Adding the default CAH deck (A5DCM), offtopia injoke deck (PXWKC), :Deck (colondeck) and three random decks')
|
|
|
|
deck_add_handler('A5DCM')
|
|
deck_add_handler('PXWKC')
|
|
deck_add_handler('colondeck')
|
|
|
|
deck_add_random_handler()
|
|
deck_add_random_handler()
|
|
deck_add_random_handler()
|
|
|
|
elif rest[0] == 'offtopia-norandom':
|
|
send('Adding the default CAH deck (A5DCM), offtopia injoke deck (PXWKC), and :Deck (colondeck)')
|
|
|
|
deck_add_handler('A5DCM')
|
|
deck_add_handler('PXWKC')
|
|
deck_add_handler('colondeck')
|
|
|
|
elif rest[0] == 'empty':
|
|
pass
|
|
|
|
elif rest[0] == 'expert':
|
|
expert = True
|
|
|
|
else:
|
|
send('Unknown preset %s' % rest[0])
|
|
|
|
if not expert:
|
|
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')
|
|
|
|
def no_game():
|
|
nonlocal players, bots, decks, limit, round_number, round_call_card, czar, card_choices
|
|
if players is not None:
|
|
devoice(players)
|
|
|
|
players = {}
|
|
bots = {}
|
|
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:
|
|
nick, *rest = args
|
|
|
|
add_player(nick)
|
|
voice(nick)
|
|
|
|
send('%s started a game, !join to join!' % nick)
|
|
|
|
start_game(rest)
|
|
|
|
return game_setup
|
|
|
|
elif event == events.join:
|
|
nick = args[0]
|
|
|
|
send('Started game, !join to join!')
|
|
|
|
common_handler(event, args)
|
|
|
|
start_game([])
|
|
|
|
return game_setup
|
|
|
|
elif event == events.quit:
|
|
return quit
|
|
|
|
else:
|
|
pass
|
|
|
|
def game_setup():
|
|
nonlocal players
|
|
|
|
players_ready = set()
|
|
|
|
while True:
|
|
if len(players) == 0:
|
|
send('Lost all players, quiting game setup')
|
|
return no_game
|
|
|
|
players_ready = set(i for i in players_ready if i in players.values())
|
|
players_unready = [i for i in players.values() if i not in players_ready]
|
|
if len(players_unready) == 0: break
|
|
|
|
event, *args = get_event()
|
|
|
|
if event == events.status:
|
|
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))
|
|
|
|
elif event == events.start:
|
|
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')
|
|
|
|
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)
|
|
|
|
else:
|
|
r = common_handler(event, args)
|
|
if r is not None: return r
|
|
|
|
if len(players) < 2:
|
|
send('Not enough players')
|
|
return game_setup
|
|
else:
|
|
return setup_round
|
|
|
|
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))
|
|
return Card(deck, deck.calls.pop(index))
|
|
|
|
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))
|
|
responses.append(Card(deck, deck.responses.pop(index)))
|
|
|
|
# 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():
|
|
nonlocal players, bots, round_call_card, czar, card_choices
|
|
|
|
# Select a czar randomly, if we need to
|
|
if czar not in players.values():
|
|
czar = random.choice(list(players.values()))
|
|
|
|
# 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()
|
|
|
|
# See note above num_blanks in top_of_round()
|
|
num_blanks = len(round_call_card.text) - 1
|
|
|
|
# Find out how many response cards we need
|
|
hand_size = 9 + num_blanks
|
|
need_responses = 0
|
|
for player in players.values():
|
|
# Don't deal cards to the czar this round
|
|
if player is czar: continue
|
|
|
|
need_responses += max(hand_size - len(player.hand) + player.hand.count(None), 0)
|
|
|
|
for bot in bots.values():
|
|
need_responses += bot.num_need_cards(num_blanks)
|
|
|
|
# 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():
|
|
# We skipped the czar in the counts, so skip here too
|
|
if player is czar: continue
|
|
|
|
# Move the cards outside of the current hand size into
|
|
# the hand
|
|
overflow = [i for i in player.hand[hand_size:] if i is not None]
|
|
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:
|
|
player.hand.append(responses.pop())
|
|
|
|
for index in range(hand_size):
|
|
if player.hand[index] is None:
|
|
player.hand[index] = responses.pop()
|
|
|
|
# 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)
|
|
|
|
return top_of_round
|
|
|
|
def sanitize(text):
|
|
return ''.join(i if ord(i) >= 32 and ord(i) != 127 else '^' + chr(ord(i) ^ 64) for i in text)
|
|
|
|
def send_cards(nick):
|
|
nonlocal players
|
|
|
|
cards = ' | '.join('%i: [%s]' % (index, sanitize(card.text)) for index, card in enumerate(players[nick].hand))
|
|
|
|
notice(nick, cards)
|
|
|
|
def combine_cards(call, responses):
|
|
def handle_call_part(call_part):
|
|
nonlocal responses
|
|
|
|
r = []
|
|
after_dollar = False
|
|
for char in call_part:
|
|
if after_dollar and ord('0') <= ord(char) <= ord('9'):
|
|
# Handle $0 .. $9
|
|
# Hopefully we won't run into more backreferences
|
|
# in one card
|
|
index = int(char)
|
|
|
|
if 0 <= index < len(responses):
|
|
r.append('[' + responses[index] + ']')
|
|
else:
|
|
# Not valid backreference, copy verbatim
|
|
r.append('$' + char)
|
|
|
|
after_dollar = False
|
|
|
|
elif after_dollar:
|
|
# Wasn't a backreference, copy verbatim
|
|
r.append('$' + char)
|
|
|
|
after_dollar = False
|
|
|
|
elif char == '$':
|
|
after_dollar = True
|
|
|
|
else:
|
|
r.append(char)
|
|
|
|
return sanitize(''.join(r))
|
|
|
|
combined = [handle_call_part(call[0])]
|
|
|
|
for i in range(len(call) - 1):
|
|
combined.append('[' + sanitize(responses[i]) + ']')
|
|
combined.append(handle_call_part(call[i + 1]))
|
|
|
|
return ''.join(combined)
|
|
|
|
def combine_played(call, player_bot, selected_cards):
|
|
return combine_cards(call.text, [player_bot.hand[i].text for i in selected_cards])
|
|
|
|
def top_of_round():
|
|
nonlocal players, bots, round_number, round_call_card, czar, card_choices
|
|
|
|
choosers = [i for i in players.values() if i is not czar]
|
|
|
|
send('Round %i. %s is czar. %s choose your cards' % (round_number, czar.nick, ', '.join(i.nick for i in choosers)))
|
|
send('[%s]' % '_'.join(sanitize(part) for part in round_call_card.text))
|
|
|
|
# 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
|
|
|
|
# Have bots choose first
|
|
for bot in bots.values():
|
|
card_choices[bot] = bot.play(num_blanks)
|
|
|
|
for nick in players:
|
|
if players[nick] is not czar:
|
|
send_cards(nick)
|
|
|
|
while len(choosers) > 0:
|
|
# Make sure that if a chooser leaves, they won't be waited on
|
|
choosers = [i for i in choosers if i in players.values()]
|
|
|
|
if len(players) < 2:
|
|
send('Not enough players to continue, quiting game')
|
|
return no_game
|
|
|
|
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
|
|
elif player not in choosers and player not in card_choices:
|
|
notice(nick, 'You\'ll get to choose next round')
|
|
continue
|
|
|
|
if len(choices) != num_blanks:
|
|
notice(nick, 'Select %i card(s)' % (len(round_call_card.text) - 1))
|
|
continue
|
|
|
|
selected_cards = []
|
|
for choice in choices:
|
|
if 0 <= choice < len(player.hand):
|
|
if choice not in selected_cards:
|
|
selected_cards.append(choice)
|
|
else:
|
|
notice(nick, 'Can\'t play the same card twice')
|
|
break
|
|
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
|
|
if player in choosers:
|
|
choosers.remove(player)
|
|
notice(nick, combine_played(round_call_card, player, selected_cards))
|
|
|
|
elif event == events.cards:
|
|
nick, = args
|
|
|
|
if nick not in players:
|
|
# Ignore those not in the game
|
|
continue
|
|
|
|
player = players[nick]
|
|
|
|
if player in choosers or player in card_choices:
|
|
send_cards(nick)
|
|
else:
|
|
notice(nick, 'You can\'t choose now')
|
|
|
|
elif event == events.origins:
|
|
nick, = args
|
|
|
|
if nick not in players:
|
|
notice(nick, 'call: %s' % round_call_card.deck.code)
|
|
|
|
else:
|
|
notice(nick, 'call: %s, %s' % (round_call_card.deck.code, get_hand_origins(players[nick])))
|
|
|
|
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')
|
|
|
|
elif event == events.deck_remove:
|
|
common_handler(event, args)
|
|
|
|
# Did we lose our call card?
|
|
if round_call_card is None:
|
|
# Yes, restart round
|
|
send('Lost the black card, restarting round')
|
|
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
|
|
send('Lost a card from player\'s hand, restarting round')
|
|
return setup_round
|
|
|
|
for player_bot in card_choices:
|
|
# 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
|
|
if None in player_bot.hand:
|
|
# Yes, restart round
|
|
send('Lost a card from player\'s hand, restarting round')
|
|
return setup_round
|
|
|
|
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
|
|
choosers = random.sample(card_choices.keys(), k = len(card_choices))
|
|
for index, player_bot in enumerate(choosers):
|
|
send('%i: %s' % (index, combine_played(round_call_card, player_bot, card_choices[player_bot])))
|
|
|
|
while True:
|
|
if len(players) < 2:
|
|
send('Not enough players to continue, quiting game')
|
|
return no_game
|
|
|
|
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
|
|
|
|
if len(choices) == 1:
|
|
choice = choices[0]
|
|
|
|
if 0 <= choice < len(choosers):
|
|
player_bot = choosers[choice]
|
|
player_bot.points += 1
|
|
|
|
# Winner is Czar semantics if a
|
|
# player won, random otherwise
|
|
if player_bot in players.values():
|
|
czar = player_bot
|
|
else:
|
|
czar = None
|
|
|
|
send('The winner is %s with: %s' % (player_bot.nick, combine_played(round_call_card, player_bot, card_choices[player_bot])))
|
|
|
|
break
|
|
|
|
else:
|
|
notice(nick, '%i not in range' % choice)
|
|
|
|
elif len(choices) == 0:
|
|
# Special case: award everyone a point
|
|
# and randomize czar
|
|
for player_bot in card_choices:
|
|
player_bot.points += 1
|
|
|
|
# If we set czar to None, setup_round()
|
|
# will handle ramdomizing it for us
|
|
czar = None
|
|
|
|
send('Everyone is a winner!')
|
|
|
|
break
|
|
|
|
else:
|
|
notice(nick, 'Select one or zero choices')
|
|
|
|
elif event == events.origins:
|
|
nick, = args
|
|
|
|
if nick not in players:
|
|
notice(nick, 'call: %s' % round_call_card.deck.code)
|
|
|
|
else:
|
|
answers_origins = []
|
|
for index, player_bot in enumerate(choosers):
|
|
answer_origins = [player_bot.hand[i].deck.code for i in card_choices[player_bot]]
|
|
answers_origins.append('%i: %s' % (index, ', '.join(answer_origins)))
|
|
|
|
notice(nick, 'call: %s; %s' % (round_call_card.deck.code, '; '.join(answers_origins)))
|
|
|
|
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 card_choices:
|
|
send('Lost a card played this round, restarting round')
|
|
|
|
return setup_round
|
|
|
|
else:
|
|
notice(nick, 'New hand will be dealt next round')
|
|
|
|
elif event == events.deck_remove:
|
|
common_handler(event, args)
|
|
|
|
# Did we lose our call card?
|
|
if round_call_card is None:
|
|
# Yes, restart round
|
|
send('Lost the black card, restarting round')
|
|
return setup_round
|
|
|
|
# Did it affect any response cards on this round?
|
|
for player_bot in card_choices:
|
|
for index in card_choices[player_bot]:
|
|
if player_bot.hand[index] is None:
|
|
# Yes, restart round
|
|
send('Lost a card played this round, restarting round')
|
|
return setup_round
|
|
|
|
else:
|
|
r = common_handler(event, args)
|
|
if r is not None: return r
|
|
|
|
points = []
|
|
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)))
|
|
else:
|
|
points.append('%s: %i' % (player_bot.nick, player_bot.points))
|
|
|
|
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:
|
|
if max(i.points for i in players_bots()) >= limit.number:
|
|
return end_game
|
|
|
|
# Remove the cards that were played this round from hands
|
|
for player_bot in card_choices:
|
|
for index in card_choices[player_bot]:
|
|
player_bot.hand[index] = None
|
|
|
|
# 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
|
|
|
|
max_score = max(i.points for i in players_bots())
|
|
|
|
winners = [i for i in players_bots() if i.points == max_score]
|
|
|
|
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))
|
|
|
|
return no_game
|
|
|
|
def quit():
|
|
pass
|
|
|
|
players = None
|
|
bots = None
|
|
decks = None
|
|
limit = None
|
|
|
|
round_number = None
|
|
round_call_card = None
|
|
czar = None
|
|
card_choices = None
|
|
|
|
cardcast_deck_count = None
|
|
|
|
state = no_game
|
|
while state != quit:
|
|
state = state()
|
|
|
|
if __name__ == '__main__':
|
|
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)
|
|
elif t == 'start_preset':
|
|
nick = input('nick> ')
|
|
preset = input('preset> ')
|
|
return (events.start, nick, preset)
|
|
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)
|
|
elif t == 'join_message':
|
|
nick = input('nick> ')
|
|
message = input('message> ')
|
|
return (events.join, nick, message)
|
|
elif t == 'leave':
|
|
nick = input('nick> ')
|
|
return (events.leave, nick)
|
|
elif t == 'players':
|
|
return (events.players,)
|
|
elif t == 'kick':
|
|
kicker = input('kicker> ')
|
|
kickee = input('kickee> ')
|
|
return (events.kick, kicker, kickee)
|
|
elif t == 'deck add':
|
|
code = input('code> ')
|
|
return (events.deck_add, code)
|
|
elif t == 'deck add random':
|
|
return (events.deck_add_random,)
|
|
elif t == 'deck remove':
|
|
code = input('code> ')
|
|
return (events.deck_remove, code)
|
|
elif t == 'deck list':
|
|
return (events.deck_list,)
|
|
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)
|
|
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)
|
|
elif t == 'origins':
|
|
nick = input('nick> ')
|
|
return (events.origins, nick)
|
|
elif t == 'redeal':
|
|
nick = input('nick> ')
|
|
return (events.redeal, nick)
|
|
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)
|