interactive bots: Create connect four bot.

This commit is contained in:
sColin16 2017-12-16 20:12:57 +00:00 committed by Eeshan Garg
parent bcca5230a3
commit 35829218b7
5 changed files with 1265 additions and 0 deletions

View file

@ -0,0 +1,535 @@
# @TODO: place bot owner name in config file, allow bot owner to run special commands
import re
from copy import deepcopy
from zulip_bots.bots.connect_four.controller import ConnectFourModel
class InputVerification(object):
verified_users = []
all_valid_commands = ['help', 'status', 'start game with computer', 'start game with \w+@\w+\.\w+',
'withdraw invitation', 'accept', 'decline', 'move \d$', 'quit', 'confirm quit']
# Every command that can be run, in states requiring user verification, by each player
verified_commands = {
'waiting': ['start game with computer', 'start game with \w+@\w+\.\w+'],
'inviting': [['withdraw invitation'], ['accept', 'decline']],
'playing': [['move \d$', 'quit', 'confirm quit'], ['quit', 'confirm quit']]
}
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'][-1 * turn + 1].remove('move \d$')
self.verified_commands['playing'][turn].append('move \d$')
def reset_commands(self):
self.verified_commands['playing'] = [['move \d$', '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)
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.user_messages.append('**You started a new game with the computer!**')
self.user_messages.append(self.gameHandler.parse_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.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, board = ConnectFourModel().blank_board, turn = 0):
super(GameHandler, self).__init__(main_bot_handler)
self.game_type = game_type
self.board = board
self.turn = turn
self.game_ended = False
self.connectFourModel = ConnectFourModel()
self.connectFourModel.update_board(board)
self.tokens = [':blue_circle:', ':red_circle:']
def parse_board(self):
# Header for the top of the board
board_str = ':one: :two: :three: :four: :five: :six: :seven:'
for row in range(0, 6):
board_str += '\n\n'
for column in range(0, 7):
if self.board[row][column] == 0:
board_str += ':heavy_large_circle: '
elif self.board[row][column] == 1:
board_str += ':blue_circle: '
elif self.board[row][column] == -1:
board_str += ':red_circle: '
return board_str
def your_turn_message(self):
return '**It\'s your move!**\n' +\
'type ```move <column-number>``` to make your move\n\n' +\
'You are ' + self.tokens[self.turn]
def wait_turn_message(self, opponent):
return 'Waiting for ' + opponent + ' to move'
def alert_move_message(self, original_player, column_number):
return '**' + original_player + ' moved in column ' + str(column_number + 1) + '**.'
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, column_number, token_number, player_one, player_two, computer_play = False):
if not self.connectFourModel.validate_move(column_number):
self.user_messages.append('That\'s an invalid move. Please specify a column' +
' with at least one blank space, between 1 and 7')
return
self.board = self.connectFourModel.make_move(column_number, token_number)
if not computer_play:
self.user_messages.append('You placed your token in column ' + str(column_number + 1) + '.')
self.user_messages.append(self.parse_board())
self.opponent_messages.append(self.alert_move_message(self.sender, column_number))
self.opponent_messages.append(self.parse_board())
else:
self.user_messages.append(self.alert_move_message('the Computer', column_number))
self.user_messages.append(self.parse_board())
game_over = self.connectFourModel.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
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 \d$').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 = int(re.compile('move (\d)$').search(content).group(1)) - 1
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.connectFourModel.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
def confirm_new_invitation(self, opponent):
return 'You\'ve sent an invitation to play Connect Four 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 Connect Four.**\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.gameHandler.parse_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 ConnectFourBotHandler(object):
'''
Bot that allows users to player another user
or the computer in a game of Connect Four
'''
def get_stored_data(self):
# return self.data # Uncomment and rerun bot to reset data if users are abusing the bot
return self.bot_handler.storage.get('connect_four')
def update_data(self):
self.state = self.data['state']
if 'users' in self.data:
self.inputVerification.verified_users = self.data['users']
if self.state == 'inviting':
self.invitationHandler = InvitationHandler(self)
self.gameHandler = GameHandler(self, self.data['game_type'])
elif self.state == 'playing':
self.gameHandler = GameHandler(self, self.data['game_type'], board = self.data['board'], turn = 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('connect_four', 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
inputVerification = InputVerification()
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 = '**Connect Four 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 '**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 make your move during a game, type\n' + \
'```move <column-number>```\n' + \
'* To quit a game at any time, type\n' + \
'```quit```\n' + \
'* To withdraw an invitation, type\n' + \
'```cancel game```'
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 Connect Four.
To see the entire list of commands, type
@bot-name help
'''
def initialize(self, bot_handler):
self.gameCreator = GameCreator(self)
self.inputVerification.reset_commands()
if not bot_handler.storage.contains('connect_four'):
bot_handler.storage.put('connect_four', self.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
# Messages that can be sent regardless of state or user
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()
handler_class = ConnectFourBotHandler

View file

@ -0,0 +1,134 @@
from copy import deepcopy
from random import randint
from functools import reduce
class ConnectFourModel(object):
'''
Object that manages running the Connect
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]]
current_board = blank_board
def update_board(self, board):
self.current_board = deepcopy(board)
def validate_move(self, column_number):
if column_number < 0 or column_number > 6:
return False
row = 0
column = column_number
return self.current_board[row][column] == 0
def available_moves(self):
available_moves = []
row = 0
for column in range(0, 7):
if self.current_board[row][column] == 0:
available_moves.append(column)
return available_moves
def make_move(self, column_number, token_number):
finding_move = True
row = 5
column = column_number
while finding_move:
if self.current_board[row][column] == 0:
self.current_board[row][column] = token_number
finding_move = False
row -= 1
return deepcopy(self.current_board)
def determine_game_over(self, first_player, second_player):
def get_horizontal_wins(board):
horizontal_sum = 0
for row in range(0, 6):
for column in range(0, 4):
horizontal_sum = board[row][column] + board[row][column + 1] + \
board[row][column + 2] + board[row][column + 3]
if horizontal_sum == -4:
return -1
elif horizontal_sum == 4:
return 1
return 0
def get_vertical_wins(board):
vertical_sum = 0
for row in range(0, 3):
for column in range(0, 7):
vertical_sum = board[row][column] + board[row + 1][column] + \
board[row + 2][column] + board[row + 3][column]
if vertical_sum == -4:
return -1
elif vertical_sum == 4:
return 1
return 0
def get_diagonal_wins(board):
major_diagonal_sum = 0
minor_diagonal_sum = 0
# Major Diagonl Sum
for row in range(0, 3):
for column in range(0, 4):
major_diagonal_sum = board[row][column] + board[row + 1][column + 1] + \
board[row + 2][column + 2] + board[row + 3][column + 3]
if major_diagonal_sum == -4:
return -1
elif major_diagonal_sum == 4:
return 1
# Minor Diagonal Sum
for row in range(3, 6):
for column in range(0, 4):
minor_diagonal_sum = board[row][column] + board[row - 1][column + 1] + \
board[row - 2][column + 2] + board[row - 3][column + 3]
if minor_diagonal_sum == -4:
return -1
elif minor_diagonal_sum == 4:
return 1
return 0
# 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])
if top_row_multiple != 0:
return 'draw'
winner = get_horizontal_wins(self.current_board) + \
get_vertical_wins(self.current_board) + \
get_diagonal_wins(self.current_board)
if winner == 1:
return first_player
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

View file

@ -0,0 +1,77 @@
# Connect Four Bot
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
The Connect Four Bot does not require a config file or API key.
It can be used without setup.
## 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. ```withdraw invitation``` : 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.

View file

@ -0,0 +1,519 @@
from zulip_bots.test_lib import BotTestCase
from contextlib import contextmanager
from mock import MagicMock
from zulip_bots.bots.connect_four.connect_four import *
class TestConnectFourBot(BotTestCase):
bot_name = 'connect_four'
def make_request_message(self, content, user='foo@example.com'):
message = dict(
sender_email=user,
content=content,
)
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'):
'''
This function serves a similar purpose
to BotTestCase.verify_dialog, but allows
for multiple responses to be validated,
and for mocking of the bot's internal data
'''
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)
responses = [
message
for (method, message)
in bot_handler.transcript
]
first_response = responses[response_number]
self.assertEqual(expected_response, first_response['content'])
ConnectFourModel.computer_move = stash
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 make your move during a game, type\n' + \
'```move <column-number>```\n' + \
'* To quit a game at any time, type\n' + \
'```quit```\n' + \
'* To withdraw an invitation, type\n' + \
'```cancel game```'
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.'
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]]
almost_win_board = [
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[1, -1, 0, 0, 0, 0, 0],
[1, -1, 0, 0, 0, 0, 0],
[1, -1, 0, 0, 0, 0, 0]]
almost_draw_board = [
[1, -1, 1, -1, 1, -1, 0],
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, 1],
[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' +
' with at least one blank space, between 1 and 7', 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)
def test_confirm_quit(self):
self.verify_response('confirm quit', '**You have forfeit the game**\nSorry, but you lost :cry:', 0, data=self.start_two_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)
def test_connect_four_logic(self):
def confirmAvailableMoves(good_moves, bad_moves, board):
connectFourModel.update_board(board)
for move in good_moves:
self.assertTrue(connectFourModel.validate_move(move))
for move in bad_moves:
self.assertFalse(connectFourModel.validate_move(move))
def confirmMove(column_number, token_number, initial_board, final_board):
connectFourModel.update_board(initial_board)
test_board = connectFourModel.make_move(column_number, token_number)
self.assertEqual(test_board, final_board)
def confirmGameOver(board, result):
connectFourModel.update_board(board)
game_over = connectFourModel.determine_game_over('first_player', 'second_player')
self.assertEqual(game_over, result)
def confirmWinStates(array):
for board in array[0]:
confirmGameOver(board, 'first_player')
for board in array[1]:
confirmGameOver(board, 'second_player')
connectFourModel = ConnectFourModel()
# Basic Board setups
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]]
full_board = [
[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1]]
single_column_board = [
[1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1]]
diagonal_board = [
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1]]
# Winning Board Setups
# Each array if consists of two arrays:
# The first stores win states for '1'
# The second stores win state for '-1'
# Note these are not necessarily valid board states
# for simplicity (random -1 and 1s could be added)
horizontal_win_boards = [
[
[[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],
[1, 1, 1, 1, 0, 0, 0]],
[[0, 0, 0, 1, 1, 1, 1],
[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],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 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, 0],
[-1, -1, -1, -1, 0, 0, 0]],
[[0, 0, 0, -1, -1, -1, -1],
[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],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, -1, -1, -1, -1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]]
]
]
vertical_win_boards = [
[
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]]
],
[
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]]
]
]
major_diagonal_win_boards = [
[
[[1, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 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, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 1]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0]]
],
[
[[-1, 0, 0, 0, 0, 0, 0],
[0, -1, 0, 0, 0, 0, 0],
[0, 0, -1, 0, 0, 0, 0],
[0, 0, 0, -1, 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, -1, 0, 0, 0],
[0, 0, 0, 0, -1, 0, 0],
[0, 0, 0, 0, 0, -1, 0],
[0, 0, 0, 0, 0, 0, -1]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, -1, 0, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, -1, 0, 0],
[0, 0, 0, 0, 0, -1, 0],
[0, 0, 0, 0, 0, 0, 0]]
]
]
minor_diagonal_win_boards = [
[
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 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, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 1, 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, -1, 0, 0, 0],
[0, 0, -1, 0, 0, 0, 0],
[0, -1, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, -1, 0],
[0, 0, 0, 0, -1, 0, 0],
[0, 0, 0, -1, 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, -1, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, -1, 0, 0, 0, 0],
[0, -1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]]
]
]
# Test Move Validation Logic
confirmAvailableMoves([0, 1, 2, 3, 4, 5, 6], [-1, 7], blank_board)
confirmAvailableMoves([3], [0, 1, 2, 4, 5, 6], single_column_board)
confirmAvailableMoves([0, 1, 2, 3, 4, 5], [6], diagonal_board)
# Test Available Move Logic
connectFourModel.update_board(blank_board)
self.assertEqual(connectFourModel.available_moves(), [0, 1, 2, 3, 4, 5, 6])
connectFourModel.update_board(single_column_board)
self.assertEqual(connectFourModel.available_moves(), [3])
connectFourModel.update_board(full_board)
self.assertEqual(connectFourModel.available_moves(), [])
# Test Move Logic
confirmMove(0, 1, 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],
[1, 0, 0, 0, 0, 0, 0]])
confirmMove(0, -1, 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],
[-1, 0, 0, 0, 0, 0, 0]])
confirmMove(0, 1, diagonal_board,
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1]])
confirmMove(1, 1, diagonal_board,
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1]])
confirmMove(2, 1, diagonal_board,
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1]])
confirmMove(3, 1, diagonal_board,
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1]])
confirmMove(4, 1, diagonal_board,
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1]])
confirmMove(5, 1, diagonal_board,
[[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1]])
# Test Game Over Logic:
confirmGameOver(blank_board, False)
confirmGameOver(full_board, 'draw')
# Test Win States:
confirmWinStates(horizontal_win_boards)
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])