diff --git a/botcmd.py b/botcmd.py index 00e522f..d4e1530 100644 --- a/botcmd.py +++ b/botcmd.py @@ -218,6 +218,30 @@ def parse_command(message, nick, irc): else: send('Subcommands: !deck add | remove | list') + elif c == '!bot': + if len(message) < 2: + send('Subcommands: !bot add | remove') + return + + subc = message[1] + if subc == 'add': + arg = args(2, 2) + if arg is not None: + bot_type, name = arg + if bot_type == 'rando': + send_event((events.bot_add_rando, name)) + else: + send('Allowed bot types: rando') + + elif subc == 'remove': + arg = args(1, 2) + if arg is not None: + name, = arg + send_event((events.bot_remove, name)) + + else: + send('Subcommands: !bot add | remove') + elif c == '!limit': arg = args([0, 1, 2]) if arg is None: return @@ -287,6 +311,8 @@ def parse_command(message, nick, irc): send('Usage: [!card] ...') elif arg[0] == 'deck': send('Subcommands: !deck add | remove | list') + elif arg[0] == 'bot': + send('Subcommands: !bot add | remove') elif arg[0] == 'limit': send('Usage: !limit [ []]') else: @@ -301,6 +327,13 @@ def parse_command(message, nick, irc): send('Usage: !deck list') else: send('No such subcommand !%s %s' % (arg[0], arg[1])) + elif arg[0] == 'bot': + if arg[1] == 'add': + send('Usage: !bot add ') + elif arg[1] == 'remove': + send('Usage: !bot remove ') + else: + send('No such subcommand !%s %s' % (arg[0], arg[1])) else: send('No such subcommand !%s %s' % (arg[0], arg[1])) diff --git a/gameloop.py b/gameloop.py index 3bb8904..83a8e4d 100644 --- a/gameloop.py +++ b/gameloop.py @@ -4,11 +4,14 @@ from collections import namedtuple import cardcast_api -# TODO: rando -# TODO: https://dl.puckipedia.com/colondeck{,/cards} - 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, limit, card, cards, origins = range(19) + (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) = range(21) class limit_types(enum.Enum): points, rounds = range(2) @@ -35,6 +38,23 @@ class Player: def __hash__(self): return id(self) +class Rando: + def __init__(self, name): + self.nick = '<%s>' % name + + self.hand = [] + self.points = 0 + + def num_need_cards(self, num_blanks): + return num_blanks - len(self.hand) + self.hand.count(None) + + 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): @@ -48,6 +68,12 @@ def game(send, notice, voice, devoice, get_event): 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 @@ -68,10 +94,9 @@ def game(send, notice, voice, devoice, get_event): players[new] = player def list_players(): - nonlocal players + nonlocal players, bots - send(', '.join(sorted(players))) - # TODO: Add bots + send(', '.join(sorted(players) + sorted(i.nick for i in bots.values()))) def add_deck(code): nonlocal decks @@ -142,15 +167,13 @@ def game(send, notice, voice, devoice, get_event): return code def remove_deck(code): - nonlocal players, decks, round_call_card + nonlocal decks, round_call_card # Purge all the cards from the deck from the game - for player in players.values(): - for index, card in enumerate(player.hand): + 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.hand[index] = None - - # TODO: Remove from bots + player_bot.hand[index] = None if round_call_card is not None and round_call_card.deck.code == code: round_call_card = None @@ -213,7 +236,7 @@ def game(send, notice, voice, devoice, get_event): return ', '.join('%i: %s' % (index, i) for index, i in enumerate(hand_origins)) def common_handler(event, args): - nonlocal players, decks, limit + nonlocal players, bots, decks, limit if event == events.kill: send('Stopping game') @@ -279,6 +302,20 @@ def game(send, notice, voice, devoice, get_event): elif event == events.deck_list: list_decks() + elif event == events.bot_add_rando: + name, = args + if name not in bots: + bots[name] = Rando(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] @@ -310,11 +347,12 @@ def game(send, notice, voice, devoice, get_event): error('Unknown event type: %s' % event) def no_game(): - nonlocal players, decks, limit, round_number, round_call_card, czar, card_choices + 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 @@ -477,7 +515,7 @@ def game(send, notice, voice, devoice, get_event): return responses def setup_round(): - nonlocal players, round_call_card, czar, card_choices + nonlocal players, bots, round_call_card, czar, card_choices # Select a czar randomly, if we need to if czar not in players.values(): @@ -505,7 +543,11 @@ def game(send, notice, voice, devoice, get_event): if len(player.hand) < 10: need_responses += 10 - len(player.hand) need_responses += player.hand.count(None) - # TODO: Add bot hooks + + # See note above num_blanks in top_of_round() + num_blanks = len(round_call_card.text) - 1 + 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() @@ -527,7 +569,15 @@ def game(send, notice, voice, devoice, get_event): for index in range(10): if player.hand[index] is None: player.hand[index] = responses.pop() - # TODO: Add bot hooks + + # 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 @@ -584,15 +634,25 @@ def game(send, notice, voice, devoice, get_event): 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, round_number, round_call_card, czar, card_choices + 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)) - # TODO: Bot plays + # 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: @@ -633,10 +693,7 @@ def game(send, notice, voice, devoice, get_event): notice(nick, 'You\'ll get to choose next round') continue - # Round call card has N parts. Between each of - # those parts goes one response card. Therefore - # there should be N - 1 response cards - if len(choices) != len(round_call_card.text) - 1: + if len(choices) != num_blanks: notice(nick, 'Select %i card(s)' % (len(round_call_card.text) - 1)) continue @@ -659,7 +716,7 @@ def game(send, notice, voice, devoice, get_event): card_choices[player] = selected_cards if player in choosers: choosers.remove(player) - notice(nick, combine_cards(round_call_card.text, [player.hand[i].text for i in selected_cards])) + notice(nick, combine_played(round_call_card, player, selected_cards)) elif event == events.cards: nick, = args @@ -700,16 +757,15 @@ def game(send, notice, voice, devoice, get_event): send('Lost a card from player\'s hand, restarting round') return setup_round - for player in card_choices: + 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.hand: + if None in player_bot.hand: # Yes, restart round send('Lost a card from player\'s hand, restarting round') return setup_round - # TODO: Consider bots else: r = common_handler(event, args) @@ -724,9 +780,8 @@ def game(send, notice, voice, devoice, get_event): # Display the cards choosers = random.sample(card_choices.keys(), k = len(card_choices)) - for index, player in enumerate(choosers): - send('%i: %s' % (index, combine_cards(round_call_card.text, [player.hand[i].text for i in card_choices[player]]))) - # TODO: Consider bots + 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: @@ -761,14 +816,17 @@ def game(send, notice, voice, devoice, get_event): choice = choices[0] if 0 <= choice < len(choosers): - player = choosers[choice] - player.points += 1 + player_bot = choosers[choice] + player_bot.points += 1 - # Winner is Czar semantics - czar = player + # 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.nick, combine_cards(round_call_card.text, [player.hand[i].text for i in card_choices[player]]))) - # TODO: Consider bots + send('The winner is %s with: %s' % (player_bot.nick, combine_played(round_call_card, player_bot, card_choices[player_bot]))) break @@ -778,9 +836,8 @@ def game(send, notice, voice, devoice, get_event): elif len(choices) == 0: # Special case: award everyone a point # and randomize czar - for player in card_choices: - player.points += 1 - # TODO: Consider bots + for player_bot in card_choices: + player_bot.points += 1 # If we set czar to None, setup_round() # will handle ramdomizing it for us @@ -801,10 +858,9 @@ def game(send, notice, voice, devoice, get_event): else: answers_origins = [] - for index, player in enumerate(choosers): - answer_origins = [player.hand[i].deck.code for i in card_choices[player]] + 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))) - # TODO: Consider bots notice(nick, 'call: %s; %s' % (round_call_card.deck.code, '; '.join(answers_origins))) @@ -818,25 +874,23 @@ def game(send, notice, voice, devoice, get_event): return setup_round # Did it affect any response cards on this round? - for player in card_choices: - for index in card_choices[player]: - if player.hand[index] is None: + 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 - # TODO: Consider bots else: r = common_handler(event, args) if r is not None: return r points = [] - for player in players.values(): - if player in choosers: - points.append('%s: %i (%i)' % (player.nick, player.points, choosers.index(player))) + 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.nick, player.points)) - # TODO: Handle bots + points.append('%s: %i' % (player_bot.nick, player_bot.points)) send('Points: %s' % ' | '.join(points)) @@ -850,15 +904,13 @@ def game(send, notice, voice, devoice, get_event): return end_game elif limit.type == limit_types.points: - if max(i.points for i in players.values()) >= limit.number: + if max(i.points for i in players_bots()) >= limit.number: return end_game - # TODO: Handle bots # Remove the cards that were played this round from hands - for player in card_choices: - for index in card_choices[player]: - player.hand[index] = None - # TODO: Consider bots + 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 @@ -872,10 +924,9 @@ def game(send, notice, voice, devoice, get_event): def end_game(): nonlocal players - # TODO: Handle bots - max_score = max(i.points for i in players.values()) + max_score = max(i.points for i in players_bots()) - winners = [i for i in players.values() if i.points == max_score] + winners = [i for i in players_bots() if i.points == max_score] send('We have a winner! %s' % ', '.join(i.nick for i in winners)) @@ -885,6 +936,7 @@ def game(send, notice, voice, devoice, get_event): pass players = None + bots = None decks = None limit = None @@ -946,6 +998,12 @@ if __name__ == '__main__': 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':