zulip_bots: Migrate connect_four bot to new game_handler.

This commit is contained in:
fredfishgames 2018-01-19 15:55:42 +00:00 committed by showell
parent ec5be8fc7e
commit 3a438cafa9
7 changed files with 138 additions and 795 deletions

View file

@ -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.")

View file

@ -1,2 +0,0 @@
[connect_four]
superusers = ["user@example.com"]

View file

@ -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 <column>` 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

View file

@ -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 = [
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]]
[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,6 +118,7 @@ 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])
@ -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 ''

View file

@ -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:
```
@<bot-name> start game with user@example.com
```
Starting a game with the computer is even simpler:
```
@<bot-name> 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* ```@<bot-name>```
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 <column-number>``` : 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.

View file

@ -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 <column-number>``` 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()

View file

@ -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 @<player-name>`
* 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 <column-number>```'''
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 <column-number>```'
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 <column>` 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])