From 3a438cafa99616aa7a18217ec4ccb1a4231b1bef Mon Sep 17 00:00:00 2001 From: fredfishgames Date: Fri, 19 Jan 2018 15:55:42 +0000 Subject: [PATCH] zulip_bots: Migrate connect_four bot to new game_handler. --- tools/run-mypy | 8 +- .../bots/connect_four/connect_four.conf | 2 - .../bots/connect_four/connect_four.py | 35 +- .../bots/connect_four/controller.py | 48 +- .../zulip_bots/bots/connect_four/doc.md | 82 +-- .../bots/connect_four/game_adapter.py | 547 ------------------ .../bots/connect_four/test_connect_four.py | 211 +++---- 7 files changed, 138 insertions(+), 795 deletions(-) delete mode 100644 zulip_bots/zulip_bots/bots/connect_four/connect_four.conf delete mode 100644 zulip_bots/zulip_bots/bots/connect_four/game_adapter.py diff --git a/tools/run-mypy b/tools/run-mypy index 215994c..c6cbf5e 100755 --- a/tools/run-mypy +++ b/tools/run-mypy @@ -82,7 +82,13 @@ force_include = [ "zulip_bots/zulip_bots/bots/salesforce/salesforce.py", "zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py", "zulip_bots/zulip_bots/bots/idonethis/idonethis.py", - "zulip_bots/zulip_bots/bots/idonethis/test_idonethis.py" + "zulip_bots/zulip_bots/bots/idonethis/test_idonethis.py", + "zulip_bots/zulip_bots/bots/connect_four/connect_four.py", + "zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py", + "zulip_bots/zulip_bots/bots/tictactoe/tictactoe.py", + "zulip_bots/zulip_bots/bots/tictactoe/test_tictactoe.py", + "zulip_bots/zulip_bots/bots/game_handler_bot/game_handler_bot.py", + "zulip_bots/zulip_bots/bots/game_handler_bot/test_game_handler_bot.py", ] parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.") diff --git a/zulip_bots/zulip_bots/bots/connect_four/connect_four.conf b/zulip_bots/zulip_bots/bots/connect_four/connect_four.conf deleted file mode 100644 index 7f763a3..0000000 --- a/zulip_bots/zulip_bots/bots/connect_four/connect_four.conf +++ /dev/null @@ -1,2 +0,0 @@ -[connect_four] -superusers = ["user@example.com"] diff --git a/zulip_bots/zulip_bots/bots/connect_four/connect_four.py b/zulip_bots/zulip_bots/bots/connect_four/connect_four.py index fe6ae49..60e50ef 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/connect_four.py +++ b/zulip_bots/zulip_bots/bots/connect_four/connect_four.py @@ -1,10 +1,12 @@ -from zulip_bots.bots.connect_four.game_adapter import GameAdapter +from zulip_bots.game_handler import GameAdapter from zulip_bots.bots.connect_four.controller import ConnectFourModel +from typing import Any + class ConnectFourMessageHandler(object): tokens = [':blue_circle:', ':red_circle:'] - def parse_board(self, board): + def parse_board(self, board: Any) -> str: # Header for the top of the board board_str = ':one: :two: :three: :four: :five: :six: :seven:' @@ -20,19 +22,17 @@ class ConnectFourMessageHandler(object): return board_str - def get_player_color(self, turn): + def get_player_color(self, turn: int) -> str: return self.tokens[turn] - def alert_move_message(self, original_player, move_info): - column_number = move_info - return '**' + original_player + ' moved in column ' + str(column_number + 1) + '**.' + def alert_move_message(self, original_player: str, move_info: str) -> str: + column_number = move_info.replace('move ', '') + return original_player + ' moved in column ' + column_number - def confirm_move_message(self, move_info): - column_number = move_info - return 'You placed your token in column ' + str(column_number + 1) + '.' + def game_start_message(self) -> str: + return 'Type `move ` to place a token.\n\ +The first player to get 4 in a row wins!\n Good Luck!' - def invalid_move_message(self): - return 'Please specify a column between 1 and 7 with at least one open spot.' class ConnectFourBotHandler(GameAdapter): ''' @@ -42,7 +42,7 @@ class ConnectFourBotHandler(GameAdapter): Four ''' - def __init__(self): + def __init__(self) -> None: game_name = 'Connect Four' bot_name = 'connect_four' move_help_message = '* To make your move during a game, type\n' \ @@ -51,6 +51,15 @@ class ConnectFourBotHandler(GameAdapter): model = ConnectFourModel gameMessageHandler = ConnectFourMessageHandler - super(ConnectFourBotHandler, self).__init__(game_name, bot_name, move_help_message, move_regex, model, gameMessageHandler) + super(ConnectFourBotHandler, self).__init__( + game_name, + bot_name, + move_help_message, + move_regex, + model, + gameMessageHandler, + max_players=2 + ) + handler_class = ConnectFourBotHandler diff --git a/zulip_bots/zulip_bots/bots/connect_four/controller.py b/zulip_bots/zulip_bots/bots/connect_four/controller.py index ac07a6f..2ecabf5 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/controller.py +++ b/zulip_bots/zulip_bots/bots/connect_four/controller.py @@ -1,6 +1,8 @@ from copy import deepcopy from random import randint from functools import reduce +from zulip_bots.game_handler import BadMoveException + class ConnectFourModel(object): ''' @@ -8,18 +10,17 @@ class ConnectFourModel(object): Four logic for the Connect Four Bot ''' - blank_board = [ - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0]] + def __init__(self): + self.blank_board = [ + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0] + ] - current_board = blank_board - - def parse_move(self, move): - return int(move) - 1 + self.current_board = self.blank_board def update_board(self, board): self.current_board = deepcopy(board) @@ -42,12 +43,18 @@ class ConnectFourModel(object): return available_moves - def make_move(self, column_number, token_number): + def make_move(self, move, player_number, is_computer=False): + if player_number == 1: + token_number = -1 + if player_number == 0: + token_number = 1 finding_move = True row = 5 - column = column_number + column = int(move.replace('move ', '')) - 1 while finding_move: + if row < 0: + raise BadMoveException('Make sure your move is in a column with free space.') if self.current_board[row][column] == 0: self.current_board[row][column] = token_number finding_move = False @@ -56,7 +63,7 @@ class ConnectFourModel(object): return deepcopy(self.current_board) - def determine_game_over(self, first_player, second_player): + def determine_game_over(self, players): def get_horizontal_wins(board): horizontal_sum = 0 @@ -111,8 +118,9 @@ class ConnectFourModel(object): return 0 + first_player, second_player = players[0], players[1] # If all tokens in top row are filled (its a draw), product != 0 - top_row_multiple = reduce(lambda x, y: x*y, self.current_board[0]) + top_row_multiple = reduce(lambda x, y: x * y, self.current_board[0]) if top_row_multiple != 0: return 'draw' @@ -126,12 +134,4 @@ class ConnectFourModel(object): elif winner == -1: return second_player - return False - - def computer_move(self): - # @TODO: Make the computer more intelligent - # perhaps by implementing minimax - available_moves = deepcopy(self.available_moves()) - final_move = available_moves[randint(0, len(available_moves) - 1)] - - return final_move + return '' diff --git a/zulip_bots/zulip_bots/bots/connect_four/doc.md b/zulip_bots/zulip_bots/bots/connect_four/doc.md index 3525496..92e8a88 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/doc.md +++ b/zulip_bots/zulip_bots/bots/connect_four/doc.md @@ -2,84 +2,4 @@ The Connect Four bot is a Zulip bot that will allow users to play a game of Connect Four against either another user, -or the computer. All games are run within private messages -sent between the user(s) and the bot. - -Starting a new game with another user requires a simple command, -and the desired opponent's zulip-related email adress: - -``` -@ start game with user@example.com -``` - -Starting a game with the computer is even simpler: - -``` -@ start game with computer -``` - -**See Usage for a complete list of commands** - -*Due to design contraints, the Connect Four Bot -can only run a single game at a time* - -## Setup - -To set moderators for the bot, modify the connect_four.conf -file as shown: - -superusers = ["user@example.com", "user@example2.com", ...] - -Moderators can run ```force reset``` in case any user abuse the bot - -## Usage - -*All commands should be prefaced with* ```@``` - -1. ```help``` : provides the user with relevant -commands for first time users. - -2. ```status``` : due to design contraints, the -bot can only run a single game at a time. This command allows -the user to see the current status of the bot, including -whether or not the bot is running a game, if the bot is waiting -for a player to accept an invitation to play, as well as who -is currently using the bot. - -3. ```start game with user@example.com``` : provided -that the bot is not running a game, this command can be used to -invite another player to play a game of Connect Four with the user. -Note that the user must be specified with their email adress, not -their username. - -4. ```start game with computer``` : provided that the bot is not -running a game, this command will begin a single player game -between the user and a computer player. Note that the currently -implemented computer plays randomly. - -5. ```accept``` : a command that can only be run by an invited -player to accept an invitation to play Connect Four against -another user. - -6. ```decline``` : a command that can only be run by an invited -player to decline an invitation to play Connect Four against -another user. - -7. ```cancel game``` : a command that can only be run by the -inviter to withdraw their invitation to play. Especially -useful if a player does not respond to an invitation for a -long period of time. - -8. ```move ``` : during a game, a player may run -this command on their turn to place a token in the specified -column. - -9. ```quit``` : responds with a confirmation message that asks -the user to confirm they wish to forfeit the game. - -10. ```confirm quit``` : causes the user that runs this command -to forfeit the game. - -11. ```force reset``` : a command that can only be run by the bot -owner and moderators (see 'Usage' for specifying). Destroys any -game currently being run if users are abusing the bot +or the computer. diff --git a/zulip_bots/zulip_bots/bots/connect_four/game_adapter.py b/zulip_bots/zulip_bots/bots/connect_four/game_adapter.py deleted file mode 100644 index 15979df..0000000 --- a/zulip_bots/zulip_bots/bots/connect_four/game_adapter.py +++ /dev/null @@ -1,547 +0,0 @@ -import re -import json -from copy import deepcopy - -class InputVerification(object): - def __init__(self, move_regex, superusers): - self.move_regex = move_regex - self.verified_commands = { - 'waiting': ['start game with computer', 'start game with \w+@\w+\.\w+'], - 'inviting': [['withdraw invitation'], ['accept', 'decline']], - 'playing': [[move_regex, 'quit', 'confirm quit'], ['quit', 'confirm quit']] - } - self.all_valid_commands = ['help', 'status', 'start game with computer', 'start game with \w+@\w+\.\w+', - 'withdraw invitation', 'accept', 'decline', self.move_regex, 'quit', 'confirm quit', 'force reset'] - self.superusers = superusers - - verified_users = [] - - def permission_lacking_message(self, command): - return 'Sorry, but you can\'t run the command ```' + command + '```' - - def update_commands(self, turn): - self.verified_commands['playing'] = [['quit', 'confirm quit'], ['quit', 'confirm quit']] - self.verified_commands['playing'][turn].append(self.move_regex) - - def reset_commands(self): - self.verified_commands['playing'] = [[self.move_regex, 'quit', 'confirm quit'], ['quit', 'confirm quit']] - - def regex_match_in_array(self, command_array, command): - for command_regex in command_array: - if re.compile(command_regex).match(command.lower()): - return True - - return False - - def valid_command(self, command): - return self.regex_match_in_array(self.all_valid_commands, command) - - def verify_user(self, user): - return user in self.verified_users - - def verify_command(self, user, command, state): - if state != 'waiting': - command_array = self.verified_commands[state][self.verified_users.index(user)] - else: - command_array = self.verified_commands[state] - - return self.regex_match_in_array(command_array, command) - - def verify_superuser(self, user): - return user in self.superusers - -class StateManager(object): - def __init__(self, main_bot_handler): - self.users = None - self.state = '' - self.user_messages = [] - self.opponent_messages = [] - self.main_bot_handler = main_bot_handler - - # Updates to the main bot handler that all state managers must use - def basic_updates(self): - if self.users is not None: - self.main_bot_handler.inputVerification.verified_users = self.users - - if self.state: - self.main_bot_handler.state = self.state - - self.main_bot_handler.user_messages = self.user_messages - - self.main_bot_handler.opponent_messages = self.opponent_messages - - def reset_self(self): - self.users = None - self.user_messages = [] - self.opponent_messages = [] - self.state = '' - -class GameCreator(StateManager): - def __init__(self, main_bot_handler): - super(GameCreator, self).__init__(main_bot_handler) - self.gameHandler = None - self.invitationHandler = None - - def handle_message(self, content, sender): - if content == 'start game with computer': - self.users = [sender] - self.state = 'playing' - self.gameHandler = GameHandler(self.main_bot_handler, 'one_player', self.main_bot_handler.model()) - - self.user_messages.append('**You started a new game with the computer!**') - self.user_messages.append(self.main_bot_handler.gameMessageHandler.parse_board(self.main_bot_handler.model().blank_board)) - self.user_messages.append(self.gameHandler.your_turn_message()) - - elif re.compile('\w+@\w+\.\w+').search(content): - opponent = re.compile('(\w+@\w+\.\w+)').search(content).group(1) - - if opponent == sender: - self.user_messages.append('You can\'t play against yourself!') - self.update_main_bot_handler() - return - - self.users = [sender, opponent] - self.state = 'inviting' - self.gameHandler = GameHandler(self.main_bot_handler, 'two_player', self.main_bot_handler.model()) - self.invitationHandler = InvitationHandler(self.main_bot_handler) - - self.user_messages.append(self.invitationHandler.confirm_new_invitation(opponent)) - - self.opponent_messages.append(self.invitationHandler.alert_new_invitation(sender)) - - self.update_main_bot_handler() - - def update_main_bot_handler(self): - self.basic_updates() - - self.main_bot_handler.player_cache = self.users - - self.main_bot_handler.gameHandler = deepcopy(self.gameHandler) - - if self.invitationHandler: - self.main_bot_handler.invitationHandler = deepcopy(self.invitationHandler) - - self.reset_self() - -class GameHandler(StateManager): - def __init__(self, main_bot_handler, game_type, model, board = 'blank', turn = 0): - super(GameHandler, self).__init__(main_bot_handler) - self.game_type = game_type - self.turn = turn - self.game_ended = False - self.model = model - self.board = model.blank_board if board == 'blank' else board - self.model.update_board(board) - - def your_turn_message(self): - return '**It\'s your move!**\n' +\ - 'type ```move ``` to make your move\n\n' +\ - 'You are ' + self.main_bot_handler.gameMessageHandler.get_player_color(self.turn) - - def wait_turn_message(self, opponent): - return 'Waiting for ' + opponent + ' to move' - - def invalid_move_message(self): - return 'That\'s an invalid move. ' + self.main_bot_handler.gameMessageHandler.invalid_move_message() - - def append_game_over_messages(self, result): - if result == 'draw': - self.user_messages.append('**It\'s a draw!**') - self.opponent_messages.append('**It\'s a draw!**') - else: - if result != 'the Computer': - self.user_messages.append('**Congratulations, you win! :tada:**') - self.opponent_messages.append('Sorry, but ' + result + ' won :cry:') - else: - self.user_messages.append('Sorry, but ' + result + ' won :cry:') - - def get_player_token(self, sender): - player = self.main_bot_handler.inputVerification.verified_users.index(sender) - # This computation will return 1 for player 0, and -1 for player 1, as is expected - return (-2) * player + 1 - - def toggle_turn(self): - self.turn = (-1) * self.turn + 1 - - def end_game(self): - self.state = 'waiting' - self.game_ended = True - self.users = [] - - def handle_move(self, move_info, token_number, player_one, player_two, computer_play = False): - if not self.model.validate_move(move_info): - self.user_messages.append(self.invalid_move_message()) - return - - self.board = self.model.make_move(move_info, token_number) - - if not computer_play: - self.user_messages.append(self.main_bot_handler.gameMessageHandler.confirm_move_message(move_info)) - self.user_messages.append(self.main_bot_handler.gameMessageHandler.parse_board(self.model.current_board)) - - self.opponent_messages.append(self.main_bot_handler.gameMessageHandler.alert_move_message(self.sender, move_info)) - self.opponent_messages.append(self.main_bot_handler.gameMessageHandler.parse_board(self.model.current_board)) - - else: - self.user_messages.append(self.main_bot_handler.gameMessageHandler.alert_move_message('the Computer', move_info)) - self.user_messages.append(self.main_bot_handler.gameMessageHandler.parse_board(self.model.current_board)) - - game_over = self.model.determine_game_over(player_one, player_two) - - if game_over: - self.append_game_over_messages(game_over) - self.end_game() - - else: - self.toggle_turn() - - self.main_bot_handler.inputVerification.update_commands(self.turn) - - if not computer_play: - self.user_messages.append(self.wait_turn_message(self.opponent)) - - self.opponent_messages.append(self.your_turn_message()) - - else: - self.user_messages.append(self.your_turn_message()) - - def handle_message(self, content, sender): - self.sender = sender - move_regex = self.main_bot_handler.inputVerification.move_regex - - if self.game_type == 'two_player': - opponent_array = deepcopy(self.main_bot_handler.inputVerification.verified_users) - opponent_array.remove(sender) - self.opponent = opponent_array[0] - else: - self.opponent = 'the Computer' - - if content == 'quit': - self.user_messages.append('Are you sure you want to quit? You will forfeit the game!\n' + - 'Type ```confirm quit``` to forfeit.') - - elif content == 'confirm quit': - self.end_game() - - self.user_messages.append('**You have forfeit the game**\nSorry, but you lost :cry:') - - self.opponent_messages.append('**' + sender + ' has forfeit the game**\nCongratulations, you win! :tada:') - - elif re.compile(move_regex).match(content): - player_one = player_one = self.main_bot_handler.inputVerification.verified_users[0] - player_two = 'the Computer' if self.game_type == 'one_player' else self.main_bot_handler.inputVerification.verified_users[1] - - human_move = re.compile(move_regex).search(content).group(1) - human_move = self.model.parse_move(human_move) - human_token_number = self.get_player_token(sender) - - self.handle_move(human_move, human_token_number, player_one, player_two) - - if not self.game_ended and self.game_type == 'one_player': - computer_move = self.model.computer_move() - computer_token_number = -1 - - self.handle_move(computer_move, computer_token_number, player_one, player_two, computer_play = True) - - self.update_main_bot_handler() - - def update_main_bot_handler(self): - if self.game_type == 'one_player': - self.opponent_messages = [] - - self.basic_updates() - - if self.game_ended: - self.main_bot_handler.gameHandler = None - - self.reset_self() - -class InvitationHandler(StateManager): - def __init__(self, main_bot_handler): - super(InvitationHandler, self).__init__(main_bot_handler) - self.game_cancelled = False - self.gameHandler = object - self.game_name = main_bot_handler.game_name - - def confirm_new_invitation(self, opponent): - return 'You\'ve sent an invitation to play ' + self.game_name + ' with ' +\ - opponent + '. I\'ll let you know when they respond to the invitation' - - def alert_new_invitation(self, challenger): - # Since the first player invites, the challenger is always the first player - return '**' + challenger + ' has invited you to play a game of ' + self.game_name + '.**\n' +\ - 'Type ```accept``` to accept the game invitation\n' +\ - 'Type ```decline``` to decline the game invitation.' - - def handle_message(self, content, sender): - challenger = self.main_bot_handler.inputVerification.verified_users[0] - opponent = self.main_bot_handler.inputVerification.verified_users[1] - - if content.lower() == 'accept': - self.state = 'playing' - - self.user_messages.append('You accepted the invitation to play with ' + challenger) - self.user_messages.append(self.main_bot_handler.gameHandler.wait_turn_message(challenger)) - - self.opponent_messages.append('**' + opponent + ' has accepted your invitation to play**') - self.opponent_messages.append(self.main_bot_handler.gameMessageHandler.parse_board(self.main_bot_handler.model().blank_board)) - self.opponent_messages.append(self.main_bot_handler.gameHandler.your_turn_message()) - - elif content.lower() == 'decline': - self.state = 'waiting' - self.users = [] - self.gameHandler = None - - self.user_messages.append('You declined the invitation to play with ' + challenger) - - self.opponent_messages.append('**' + opponent + ' has declined your invitation to play**\n' + - 'Invite another player by typing ```start game with user@example.com```') - - elif content.lower() == 'withdraw invitation': - self.state = 'waiting' - self.users = [] - self.gameHandler = None - - self.user_messages.append('Your invitation to play ' + opponent + ' has been withdrawn') - - self.opponent_messages.append('**' + challenger + ' has withdrawn his invitation to play you**\n' + - 'Type ``` start game with ' + challenger + '``` if you would like to play them.') - - self.update_main_bot_handler() - - def update_main_bot_handler(self): - self.basic_updates() - - self.main_bot_handler.invitationHandler = None - - if self.gameHandler is None: - self.main_bot_handler.gameHandler = self.gameHandler - - self.reset_self() - -class GameAdapter(object): - ''' - Class that serves as a template to easily - create one and two player games - ''' - - def __init__(self, game_name, bot_name, move_help_message, move_regex, model, gameMessageHandler): - self.game_name = game_name - self.bot_name = bot_name - self.move_help_message = move_help_message - self.model = model - self.gameMessageHandler = gameMessageHandler() - self.inputVerification = InputVerification(move_regex, []) - - def get_stored_data(self): - return self.bot_handler.storage.get(self.bot_name) - - def update_data(self): - self.state = self.data['state'] - - if 'users' in self.data: - self.inputVerification.verified_users = self.data['users'] - else: - self.inputVerification.verified_users = [] - - if self.state == 'inviting': - self.invitationHandler = InvitationHandler(self) - self.gameHandler = GameHandler(self, self.data['game_type'], self.model()) - - elif self.state == 'playing': - self.gameHandler = GameHandler(self, self.data['game_type'], self.model(), - board = self.data['board'], turn = self.data['turn']) - self.inputVerification.update_commands(self.data['turn']) - - def put_stored_data(self): - self.data = {} - - self.data['state'] = self.state - - if self.inputVerification.verified_users: - self.data['users'] = self.inputVerification.verified_users - - if self.state == 'inviting': - self.data['game_type'] = self.gameHandler.game_type - - elif self.state == 'playing': - self.data['game_type'] = self.gameHandler.game_type - self.data['board'] = self.gameHandler.board - self.data['turn'] = self.gameHandler.turn - - self.bot_handler.storage.put(self.bot_name, self.data) - - # Stores the current state of the game. Either 'waiting 'inviting' or 'playing' - state = 'waiting' - - # Stores the users, in case one of the state managers modifies the verified users - player_cache = [] - - # Object-wide storage to the bot_handler to allow custom message-sending function - bot_handler = None - - invitationHandler = None - gameHandler = None - gameCreator = None - - user_messages = [] - opponent_messages = [] - - # Stores a compact version of all data the bot is managing - data = {'state': 'waiting'} - - def status_message(self): - prefix = '**' + self.game_name + ' Game Status**\n' +\ - '*If you suspect users are abusing the bot,' +\ - ' please alert the bot owner*\n\n' - - if self.state == 'playing': - if self.gameHandler.game_type == 'one_player': - message = 'The bot is currently running a single player game' +\ - ' for ' + self.inputVerification.verified_users[0] + '.' - - elif self.gameHandler.game_type == 'two_player': - message = 'The bot is currently running a two player game ' +\ - 'between ' + self.inputVerification.verified_users[0] +\ - ' and ' + self.inputVerification.verified_users[1] + '.' - - elif self.state == 'inviting': - message = self.inputVerification.verified_users[0] + '\'s' +\ - ' invitation to play ' + self.inputVerification.verified_users[1] +\ - ' is still pending. Wait for the game to finish to play a game.' - - elif self.state == 'waiting': - message = '**The bot is not running a game right now!**\n' + \ - 'Type ```start game with user@example.com``` ' +\ - 'to start a game with another user,\n' +\ - 'or type ```start game with computer``` ' +\ - 'to start a game with the computer' - - return prefix + message - - def help_message(self): - return '**' + self.game_name + ' Bot Help:**\n' + \ - '*Preface all commands with @bot-name*\n\n' + \ - '* To see the current status of the game, type\n' + \ - '```status```\n' + \ - '* To start a game against the computer, type\n' + \ - '```start game with computer```\n' +\ - '* To start a game against another player, type\n' + \ - '```start game with user@example.com```\n' + \ - '* To quit a game at any time, type\n' + \ - '```quit```\n' + \ - '* To withdraw an invitation, type\n' + \ - '```cancel game```\n' + \ - self.move_help_message - - def send_message(self, user, content): - self.bot_handler.send_message(dict( - type = 'private', - to = user, - content = content - )) - - # Sends messages returned from helper classes, where user, is the user who sent the bot the original messages - def send_message_arrays(self, user): - if self.opponent_messages: - opponent_array = deepcopy(self.player_cache) - opponent_array.remove(user) - opponent = opponent_array[0] - - for message in self.user_messages: - self.send_message(user, message) - - for message in self.opponent_messages: - self.send_message(opponent, message) - - self.user_messages = [] - self.opponent_messages = [] - - def parse_message(self, message): - content = message['content'].strip() - sender = message['sender_email'] - return (content, sender) - - def usage(self): - return ''' - Bot that allows users to play another user - or the computer in a game of ''' + self.game_name + ''' - - To see the entire list of commands, type - @bot-name help - ''' - - def initialize(self, bot_handler): - self.config_info = bot_handler.get_config_info('connect_four') - if self.config_info: - self.inputVerification.superusers = json.loads(self.config_info['superusers']) - self.gameCreator = GameCreator(self) - self.inputVerification.reset_commands() - - if not bot_handler.storage.contains(self.bot_name): - bot_handler.storage.put(self.bot_name, self.data) - - def force_reset(self, sender): - for user in self.inputVerification.verified_users: - self.send_message(user, 'A bot moderator determined you were abusing the bot, and quit your game.' - ' Please make sure you finish all your games in a timely fashion.') - - self.send_message(sender, 'The game has been force reset') - - self.data = data = {'state': 'waiting'} - self.update_data() - self.put_stored_data() - - def handle_message(self, message, bot_handler): - self.bot_handler = bot_handler - - self.data = self.get_stored_data() - self.update_data() - - self.player_cache = self.inputVerification.verified_users - content, sender = self.parse_message(message) - - if not self.inputVerification.valid_command(content.lower()): - self.send_message(sender, 'Sorry, but I couldn\'t understand your input.\n' - 'Type ```help``` to see a full list of commands.') - return - - elif self.inputVerification.verify_superuser(sender) and content.lower() == 'force reset': - self.force_reset(sender) - return - - elif content.lower() == 'help' or content == '': - self.send_message(sender, self.help_message()) - return - - elif content.lower() == 'status': - self.send_message(sender, self.status_message()) - return - - elif self.state == 'waiting': - if not self.inputVerification.verify_command(sender, content.lower(), 'waiting'): - self.send_message(sender, self.inputVerification.permission_lacking_message(content)) - - self.gameCreator.handle_message(content, sender) - - elif not self.inputVerification.verify_user(sender): - self.send_message(sender, 'Sorry, but other users are already using the bot.' - 'Type ```status``` to see the current status of the bot.') - return - - elif self.state == 'inviting': - if not self.inputVerification.verify_command(sender, content.lower(), 'inviting'): - self.send_message(sender, self.inputVerification.permission_lacking_message(content)) - return - - self.invitationHandler.handle_message(content, sender) - - elif self.state == 'playing': - if not self.inputVerification.verify_command(sender, content.lower(), 'playing'): - self.send_message(sender, self.inputVerification.permission_lacking_message(content)) - return - - self.gameHandler.handle_message(content, sender) - - self.send_message_arrays(sender) - self.put_stored_data() diff --git a/zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py b/zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py index 076fce3..b8eca1c 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py +++ b/zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py @@ -3,19 +3,27 @@ from zulip_bots.test_lib import BotTestCase from contextlib import contextmanager from unittest.mock import MagicMock from zulip_bots.bots.connect_four.connect_four import * +from typing import Dict, Any, List + class TestConnectFourBot(BotTestCase): bot_name = 'connect_four' - def make_request_message(self, content, user='foo@example.com'): + def make_request_message( + self, + content: str, + user: str='foo@example.com', + user_name: str='foo' + ) -> Dict[str, str]: message = dict( sender_email=user, content=content, + sender_full_name=user_name ) return message # Function that serves similar purpose to BotTestCase.verify_dialog, but allows for multiple responses to be handled - def verify_response(self, request, expected_response, response_number, data=None, computer_move=None, user = 'foo@example.com'): + def verify_response(self, request: str, expected_response: str, response_number: int, user: str='foo@example.com') -> None: ''' This function serves a similar purpose to BotTestCase.verify_dialog, but allows @@ -26,13 +34,6 @@ class TestConnectFourBot(BotTestCase): bot, bot_handler = self._get_handlers() message = self.make_request_message(request, user) bot_handler.reset_transcript() - stash = ConnectFourModel.computer_move - - if data: - bot.get_stored_data = MagicMock(return_value = data) - - if computer_move is not None: - ConnectFourModel.computer_move = MagicMock(return_value = computer_move) bot.handle_message(message, bot_handler) @@ -45,47 +46,53 @@ class TestConnectFourBot(BotTestCase): first_response = responses[response_number] self.assertEqual(expected_response, first_response['content']) - ConnectFourModel.computer_move = stash + def help_message(self) -> str: + return '''** Connect Four Bot Help:** +*Preface all commands with @**test-bot*** +* To start a game in a stream (*recommended*), type +`start game` +* To start a game against another player, type +`start game with @` +* To quit a game at any time, type +`quit` +* To end a game with a draw, type +`draw` +* To forfeit a game, type +`forfeit` +* To see the leaderboard, type +`leaderboard` +* To withdraw an invitation, type +`cancel game` +* To make your move during a game, type +```move ```''' - def help_message(self): - return '**Connect Four Bot Help:**\n' + \ - '*Preface all commands with @bot-name*\n\n' + \ - '* To see the current status of the game, type\n' + \ - '```status```\n' + \ - '* To start a game against the computer, type\n' + \ - '```start game with computer```\n' + \ - '* To start a game against another player, type\n' + \ - '```start game with user@example.com```\n' + \ - '* To quit a game at any time, type\n' + \ - '```quit```\n' + \ - '* To withdraw an invitation, type\n' + \ - '```cancel game```\n' +\ - '* To make your move during a game, type\n' + \ - '```move ```' + def test_static_responses(self) -> None: + self.verify_response('help', self.help_message(), 0) - def no_game_status(self): - return '**Connect Four Game Status**\n' + \ - '*If you suspect users are abusing the bot, please alert the bot owner*\n\n' +\ - '**The bot is not running a game right now!**\n' +\ - 'Type ```start game with user@example.com``` to start a game with another user,\n' +\ - 'or type ```start game with computer``` to start a game with the computer' - - def inviting_status(self): - return '**Connect Four Game Status**\n' +\ - '*If you suspect users are abusing the bot, please alert the bot owner*\n\n' +\ - 'foo@example.com\'s invitation to play foo2@example.com' +\ - ' is still pending. Wait for the game to finish to play a game.' - - def one_player_status(self): - return '**Connect Four Game Status**\n' +\ - '*If you suspect users are abusing the bot, please alert the bot owner*\n\n' +\ - 'The bot is currently running a single player game for foo@example.com.' - - def two_player_status(self): - return '**Connect Four Game Status**\n' +\ - '*If you suspect users are abusing the bot, please alert the bot owner*\n\n' +\ - 'The bot is currently running a two player game ' +\ - 'between foo@example.com and foo2@example.com.' + def test_game_message_handler_responses(self) -> None: + board = ':one: :two: :three: :four: :five: :six: :seven:\n\n' + '\ +:heavy_large_circle: :heavy_large_circle: :heavy_large_circle: :heavy_large_circle: \ +:heavy_large_circle: :heavy_large_circle: :heavy_large_circle: \n\n\ +:heavy_large_circle: :heavy_large_circle: :heavy_large_circle: :heavy_large_circle: \ +:heavy_large_circle: :heavy_large_circle: :heavy_large_circle: \n\n\ +:heavy_large_circle: :heavy_large_circle: :heavy_large_circle: :heavy_large_circle: \ +:heavy_large_circle: :heavy_large_circle: :heavy_large_circle: \n\n\ +:blue_circle: :red_circle: :heavy_large_circle: :heavy_large_circle: :heavy_large_circle: \ +:heavy_large_circle: :heavy_large_circle: \n\n\ +:blue_circle: :red_circle: :heavy_large_circle: :heavy_large_circle: :heavy_large_circle: \ +:heavy_large_circle: :heavy_large_circle: \n\n\ +:blue_circle: :red_circle: :heavy_large_circle: :heavy_large_circle: :heavy_large_circle: \ +:heavy_large_circle: :heavy_large_circle: ' + bot, bot_handler = self._get_handlers() + self.assertEqual(bot.gameMessageHandler.parse_board( + self.almost_win_board), board) + self.assertEqual( + bot.gameMessageHandler.get_player_color(1), ':red_circle:') + self.assertEqual(bot.gameMessageHandler.alert_move_message( + 'foo', 'move 6'), 'foo moved in column 6') + self.assertEqual(bot.gameMessageHandler.game_start_message( + ), 'Type `move ` to place a token.\n\ +The first player to get 4 in a row wins!\n Good Luck!') blank_board = [ [0, 0, 0, 0, 0, 0, 0], @@ -111,60 +118,12 @@ class TestConnectFourBot(BotTestCase): [0, 0, 0, 0, 0, 0, -1], [0, 0, 0, 0, 0, 0, 1]] - start_two_player_data = {'state': 'playing', 'game_type': 'two_player', 'board': blank_board, 'users': ['foo@example.com', 'foo2@example.com'], 'turn': 0} - start_one_player_data = {'state': 'playing', 'game_type': 'one_player', 'board': blank_board, 'users': ['foo@example.com'], 'turn': 0} - end_two_player_data = {'state': 'playing', 'game_type': 'two_player', 'board': almost_win_board, 'users': ['foo@example.com', 'foo2@example.com'], 'turn': 0} - end_one_player_data = {'state': 'playing', 'game_type': 'one_player', 'board': almost_win_board, 'users': ['foo@example.com'], 'turn': 0} - inviting_two_player_data = {'state': 'inviting', 'game_type': 'two_player', 'board': blank_board, 'users': ['foo@example.com', 'foo2@example.com'], 'turn': 0} - draw_data = {'state': 'playing', 'game_type': 'one_player', 'board': almost_draw_board, 'users': ['foo@example.com', 'foo2@example.com'], 'turn': 0} - - def test_static_messages(self): - self.verify_response('help', self.help_message(), 0) - self.verify_response('status', self.no_game_status(), 0) - self.verify_response('status', self.inviting_status(), 0, data=self.inviting_two_player_data) - self.verify_response('status', self.one_player_status(), 0, data=self.start_one_player_data) - self.verify_response('status', self.two_player_status(), 0, data=self.start_two_player_data) - - def test_start_game(self): - self.verify_response('start game with computer', '**You started a new game with the computer!**', 0) - self.verify_response('start game with user@example.com', 'You\'ve sent an invitation to play Connect Four with user@example.com. I\'ll let you know when they respond to the invitation', 0) - self.verify_response('start game with foo@example.com', 'You can\'t play against yourself!', 0) - - def test_invitation(self): - self.verify_response('accept', 'You accepted the invitation to play with foo@example.com', 0, data=self.inviting_two_player_data, user = 'foo2@example.com') - self.verify_response('decline', 'You declined the invitation to play with foo@example.com', 0, data=self.inviting_two_player_data, user = 'foo2@example.com') - self.verify_response('withdraw invitation', 'Your invitation to play foo2@example.com has been withdrawn', 0, data=self.inviting_two_player_data) - - def test_move(self): - self.verify_response('move 8', 'That\'s an invalid move. Please specify a column ' - 'between 1 and 7 with at least one open spot.', 0, data=self.start_two_player_data) - self.verify_response('move 1', 'You placed your token in column 1.', 0, data=self.start_two_player_data) - self.verify_response('move 1', '**the Computer moved in column 1**.', 3, data=self.start_one_player_data, computer_move=0) - - def test_game_over(self): - self.verify_response('move 1', '**Congratulations, you win! :tada:**', 2, data=self.end_two_player_data) - self.verify_response('move 3', 'Sorry, but the Computer won :cry:', 5, data=self.end_one_player_data, computer_move=1) - self.verify_response('move 7', '**It\'s a draw!**', 2, data = self.draw_data) - - def test_quit(self): - self.verify_response('quit', 'Are you sure you want to quit? You will forfeit the game!\n' - 'Type ```confirm quit``` to forfeit.', 0, data=self.start_two_player_data) - self.verify_response('confirm quit', '**You have forfeit the game**\nSorry, but you lost :cry:', 0, data=self.start_two_player_data) - - def test_force_reset(self): - with self.mock_config_info({'superusers': '["foo@example.com"]'}): - self.verify_response('force reset', 'The game has been force reset', 1, data=self.start_one_player_data) - - def test_privilege_check(self): - self.verify_response('move 4', 'Sorry, but you can\'t run the command ```move 4```', 0, data=self.inviting_two_player_data) - self.verify_response('start game with computer', 'Sorry, but other users are already using the bot.' - 'Type ```status``` to see the current status of the bot.', 0, data=self.inviting_two_player_data, user = 'foo3@example.com') - self.verify_response('quit', 'Sorry, but you can\'t run the command ```quit```', 0) - self.verify_response('accept', 'Sorry, but you can\'t run the command ```accept```', 0, data=self.end_two_player_data) - self.verify_response('force reset', 'Sorry, but you can\'t run the command ```force reset```', 0) - - def test_connect_four_logic(self): - def confirmAvailableMoves(good_moves, bad_moves, board): + def test_connect_four_logic(self) -> None: + def confirmAvailableMoves( + good_moves: List[int], + bad_moves: List[int], + board: List[List[int]] + ) -> None: connectFourModel.update_board(board) for move in good_moves: @@ -173,19 +132,26 @@ class TestConnectFourBot(BotTestCase): for move in bad_moves: self.assertFalse(connectFourModel.validate_move(move)) - def confirmMove(column_number, token_number, initial_board, final_board): + def confirmMove( + column_number: int, + token_number: int, + initial_board: List[List[int]], + final_board: List[List[int]] + ) -> None: connectFourModel.update_board(initial_board) - test_board = connectFourModel.make_move(column_number, token_number) + test_board = connectFourModel.make_move( + 'move ' + str(column_number), token_number) self.assertEqual(test_board, final_board) - def confirmGameOver(board, result): + def confirmGameOver(board: List[List[int]], result: str) -> None: connectFourModel.update_board(board) - game_over = connectFourModel.determine_game_over('first_player', 'second_player') + game_over = connectFourModel.determine_game_over( + ['first_player', 'second_player']) self.assertEqual(game_over, result) - def confirmWinStates(array): + def confirmWinStates(array: List[List[List[List[int]]]]) -> None: for board in array[0]: confirmGameOver(board, 'first_player') @@ -428,7 +394,8 @@ class TestConnectFourBot(BotTestCase): # Test Available Move Logic connectFourModel.update_board(blank_board) - self.assertEqual(connectFourModel.available_moves(), [0, 1, 2, 3, 4, 5, 6]) + self.assertEqual(connectFourModel.available_moves(), + [0, 1, 2, 3, 4, 5, 6]) connectFourModel.update_board(single_column_board) self.assertEqual(connectFourModel.available_moves(), [3]) @@ -437,7 +404,7 @@ class TestConnectFourBot(BotTestCase): self.assertEqual(connectFourModel.available_moves(), []) # Test Move Logic - confirmMove(0, 1, blank_board, + confirmMove(1, 0, blank_board, [[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], @@ -445,7 +412,7 @@ class TestConnectFourBot(BotTestCase): [0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0]]) - confirmMove(0, -1, blank_board, + confirmMove(1, 1, blank_board, [[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], @@ -453,7 +420,7 @@ class TestConnectFourBot(BotTestCase): [0, 0, 0, 0, 0, 0, 0], [-1, 0, 0, 0, 0, 0, 0]]) - confirmMove(0, 1, diagonal_board, + confirmMove(1, 0, diagonal_board, [[0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 1, 1, 1], @@ -461,7 +428,7 @@ class TestConnectFourBot(BotTestCase): [0, 0, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1]]) - confirmMove(1, 1, diagonal_board, + confirmMove(2, 0, diagonal_board, [[0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 1, 1, 1], @@ -469,7 +436,7 @@ class TestConnectFourBot(BotTestCase): [0, 1, 1, 1, 1, 1, 1], [0, 1, 1, 1, 1, 1, 1]]) - confirmMove(2, 1, diagonal_board, + confirmMove(3, 0, diagonal_board, [[0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 1, 1, 1], @@ -477,7 +444,7 @@ class TestConnectFourBot(BotTestCase): [0, 0, 1, 1, 1, 1, 1], [0, 1, 1, 1, 1, 1, 1]]) - confirmMove(3, 1, diagonal_board, + confirmMove(4, 0, diagonal_board, [[0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 1, 1], [0, 0, 0, 1, 1, 1, 1], @@ -485,7 +452,7 @@ class TestConnectFourBot(BotTestCase): [0, 0, 1, 1, 1, 1, 1], [0, 1, 1, 1, 1, 1, 1]]) - confirmMove(4, 1, diagonal_board, + confirmMove(5, 0, diagonal_board, [[0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 1, 1, 1], [0, 0, 0, 0, 1, 1, 1], @@ -493,7 +460,7 @@ class TestConnectFourBot(BotTestCase): [0, 0, 1, 1, 1, 1, 1], [0, 1, 1, 1, 1, 1, 1]]) - confirmMove(5, 1, diagonal_board, + confirmMove(6, 0, diagonal_board, [[0, 0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 1, 1, 1], @@ -502,7 +469,7 @@ class TestConnectFourBot(BotTestCase): [0, 1, 1, 1, 1, 1, 1]]) # Test Game Over Logic: - confirmGameOver(blank_board, False) + confirmGameOver(blank_board, '') confirmGameOver(full_board, 'draw') # Test Win States: @@ -510,13 +477,3 @@ class TestConnectFourBot(BotTestCase): confirmWinStates(vertical_win_boards) confirmWinStates(major_diagonal_win_boards) confirmWinStates(minor_diagonal_win_boards) - - # Test Computer Move: - connectFourModel.update_board(blank_board) - self.assertTrue(connectFourModel.computer_move() in [0, 1, 2, 3, 4, 5, 6]) - - connectFourModel.update_board(single_column_board) - self.assertEqual(connectFourModel.computer_move(), 3) - - connectFourModel.update_board(diagonal_board) - self.assertTrue(connectFourModel.computer_move() in [0, 1, 2, 3, 4, 5])