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) # fqcode = fully qualified code = (namespace, code) Deck = namedtuple('Deck', ['fqcode', 'name', 'author', 'call_count', 'response_count', 'calls', 'responses']) Limit = namedtuple('Limit', ['type', 'number']) Card = namedtuple('Card', ['deck', 'text']) Namespace = namedtuple('Namespace', ['url', 'supports_random']) deck_namespaces = { 'bslsk05': Namespace('https://dl.puckipedia.com/', False), 'colondeck': Namespace('https://puck.moe/_/cards/', False), 'ahti': Namespace('https://ahti.space/cards/', True) } 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 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 self.hand[: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(namespace, code): nonlocal decks assert (namespace, code) not in decks base_url = deck_namespaces[namespace].url # 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[(namespace, code)] = Deck( fqcode = (namespace, code), name = name, author = author, call_count = call_count, response_count = response_count, calls = calls, responses = responses ) def get_random_deck_code(namespace): nonlocal remote_deck_count 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) return code def remove_deck(namespace, 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.fqcode == (namespace, code): player_bot.hand[index] = None if round_call_card is not None and round_call_card.deck.fqcode == (namespace, code): round_call_card = None del decks[(namespace, 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) namespace, code = deck.fqcode send('%s (%s %s, by %s, %s black, %s white)' % ( deck.name, namespace, code, deck.author, calls, responses )) def deck_add_handler(namespace, code): nonlocal decks 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') else: send('Unknown deck namespace %s. Try one of: %s' % (namespace, ', '.join(deck_namespaces.keys()))) def deck_add_random_handler(namespace): nonlocal decks if namespace in deck_namespaces: if deck_namespaces[namespace].supports_random: for _ in range(5): 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) else: send('Did not get a fresh random deck in 5 tries, bailing out') return 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()))) def get_hand_origins(player): hand_origins = [] for card in player.hand: if card is None: hand_origins.append('') else: hand_origins.append('%s %s' % card.deck.fqcode) 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: namespace, code = args deck_add_handler(namespace, code) elif event == events.deck_add_random: namespace, = args deck_add_random_handler(namespace) elif event == events.deck_remove: namespace, code = args if (namespace, code) in decks: errwrapper('Failure removing deck %s (%%s)' % code, remove_deck, namespace, code) else: send('No such deck %s %s' % (namespace, 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] send('Bot %s removed' % 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): if len(rest) == 0 or rest[0] == 'default': send('Adding the default CAH deck (ahti Base)') deck_add_handler('ahti', 'Base') elif rest[0] == 'offtopia-random': send('Adding the default CAH deck (ahti Base), offtopia injoke deck (bslsk05 offtopiadeck), :Deck (bslsk05 colondeck) and three random ahti decks') deck_add_handler('ahti', 'Base') deck_add_handler('bslsk05', 'offtopiadeck') deck_add_handler('colondeck', 'colondeck') for _ in range(3): deck_add_random_handler('ahti') elif rest[0] == 'offtopia': send('Adding the default CAH deck (ahti Base), offtopia injoke deck (bslsk05 offtopiadeck), and :Deck (bslsk05 colondeck)') deck_add_handler('ahti', 'Base') deck_add_handler('bslsk05', 'offtopiadeck') deck_add_handler('colondeck', 'colondeck') elif rest[0] != 'empty': send('Unknown preset %s' % rest[0]) return False return True 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) 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') return game_setup else: send('Stopping game') # If we don't do this, the partially started game state doesn't get cleared off correctly return no_game elif event == events.join: send('Start a game with !start []') 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, quitting 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 (needs at least two joined). Try inviting others and send !ready again once they\'ve joined.') 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 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 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 r.append(text[start:index]) elif ord(char) < 32 or ord(char) == 127: r.append('^' + chr(ord(i) ^ 64)) else: r.append(char) return (''.join(r), state) def to_formatting_state(from_state, to_state): if to_state == from_state: return '' elif to_state == IRCFormattingState(): return '\x0f' # ^O r = '' 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 , or following this r += '\x03%02i,%02i' % (to_state.fg_color, to_state.bg_color) 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 return r def sanitize(text, start_state): sanitized, state_after = handle_control_codes(text, start_state) return sanitized + to_formatting_state(state_after, start_state) 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) def combine_cards(call, responses, dereference_responses = True): # This function is really messy, in part due to the format used # # `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: # # Foo bar? _. # # is stored as # # ['Foo bar? ', '.'] # # So far good, take a part from `call.text`, then one response # (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 $ with the # corresponding response when you add the call part. It'd give # this algorith: # # 1. Go over call.text[index] and satisfy backrefrences # 1. Copy text verbatim until we hit a $ # 2. Add responses[num].text # 3. Repeat # 2. Add response[index].text # 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 # # Additionally, we have a special mode in case we are displaying # the round call card, where we do not look up the responses # pointed to by backreferences or blanks, but instead add the # $ 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). formatting_state = IRCFormattingState() no_formatting = IRCFormattingState() r = [] part_index = 0 index = 0 segment_start_index = 0 while part_index < len(call.text): call_part = call.text[part_index] 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): if dereference_responses: # Add response r.append(to_formatting_state(formatting_state, no_formatting)) r.append('[') r.append(sanitize(responses[part_index].text, no_formatting)) 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)) # 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) if dereference_responses: # Add the response this backreference refers to r.append(to_formatting_state(formatting_state, no_formatting)) r.append('[') r.append(sanitize(responses[backreference_index].text, no_formatting)) 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)) # Start new segment after this char segment_start_index = index else: # A backreference, but not a # valid one. Copy verbatim pass else: # Not a backreference pass r.append(to_formatting_state(formatting_state, no_formatting)) return ''.join(r) 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] # 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 send('Round %i. %s is czar. %s choose your cards' % (round_number, czar.nick, ', '.join(i.nick for i in choosers))) send('[%s]' % combine_cards(round_call_card, [None] * num_blanks, dereference_responses = False)) # 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 True: # 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(choosers) == 0: break if len(players) < 2: send('Not enough players to continue (needs at least two), quitting 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): selected_cards.append(player.hand[choice]) 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, 'You have chosen: ' + combine_cards(round_call_card, 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 %s ' % round_call_card.deck.fqcode) else: notice(nick, 'call: %s %s, %s' % (*round_call_card.deck.fqcode, 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(list(card_choices.keys()), k = len(card_choices)) for index, player_bot in enumerate(choosers): send('%i: %s' % (index, combine_cards(round_call_card, card_choices[player_bot]))) while True: if len(players) < 2: send('Not enough players to continue (needs at least two), quitting 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 who's still in the game # won, keep same czar if a bot # won, randomize otherwise if player_bot in players.values(): czar = player_bot elif player_bot in bots.values(): pass else: # setup_round() will # randomize czar = None send('The winner is %s with: %s' % (player_bot.nick, combine_cards(round_call_card, 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 %s' % round_call_card.deck.fqcode) else: answers_origins = [] for index, player_bot in enumerate(choosers): answer_origins = ['%s %s' % i.deck.fqcode for i in card_choices[player_bot]] answers_origins.append('%i: %s' % (index, ', '.join(answer_origins))) notice(nick, 'call: %s %s; %s' % (*round_call_card.deck.fqcode, '; '.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 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 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: 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 # 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 remote_deck_count = {} 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': namespace = input('namespace> ') code = input('code> ') return (events.deck_add, namespace, code) elif t == 'deck add random': namespace = input('namespace> ') return (events.deck_add_random, namespace) elif t == 'deck remove': namespace = input('namespace> ') code = input('code> ') return (events.deck_remove, namespace, 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)