zulip_bots: Migrate tictactoe bot to new game_handler.
This commit is contained in:
parent
3a438cafa9
commit
3cbb16722d
|
@ -1,107 +1,69 @@
|
||||||
from zulip_bots.test_lib import BotTestCase
|
from zulip_bots.test_lib import BotTestCase
|
||||||
|
from zulip_bots.game_handler import GameInstance
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
from typing import List, Tuple, Any
|
||||||
|
|
||||||
class TestTictactoeBot(BotTestCase):
|
|
||||||
|
class TestTicTacToeBot(BotTestCase):
|
||||||
bot_name = 'tictactoe'
|
bot_name = 'tictactoe'
|
||||||
|
|
||||||
def test_bot(self):
|
# FIXME: Add tests for computer moves
|
||||||
messages = [ # Template for message inputs to test, absent of message content
|
# FIXME: Add test lib for game_handler
|
||||||
{
|
|
||||||
'type': 'stream',
|
def test_static_responses(self) -> None:
|
||||||
'display_recipient': 'some stream',
|
model, message_handler = self._get_game_handlers()
|
||||||
'subject': 'some subject',
|
self.assertNotEqual(message_handler.get_player_color(0), None)
|
||||||
'sender_email': 'foo_sender@zulip.com',
|
self.assertNotEqual(message_handler.game_start_message(), None)
|
||||||
},
|
self.assertEqual(message_handler.alert_move_message(
|
||||||
{
|
'foo', 'move 3'), 'foo put a token at 3')
|
||||||
'type': 'private',
|
|
||||||
'sender_email': 'foo_sender@zulip.com',
|
def test_has_attributes(self) -> None:
|
||||||
},
|
model, message_handler = self._get_game_handlers()
|
||||||
]
|
self.assertTrue(hasattr(message_handler, 'parse_board') is not None)
|
||||||
private_response = {
|
self.assertTrue(
|
||||||
'type': 'private',
|
hasattr(message_handler, 'alert_move_message') is not None)
|
||||||
'to': 'foo_sender@zulip.com',
|
self.assertTrue(hasattr(model, 'current_board') is not None)
|
||||||
'subject': 'foo_sender@zulip.com', # FIXME Requiring this in bot is a bug?
|
self.assertTrue(hasattr(model, 'determine_game_over') is not None)
|
||||||
|
|
||||||
|
def test_parse_board(self) -> None:
|
||||||
|
board = [[0, 1, 0],
|
||||||
|
[0, 0, 0],
|
||||||
|
[0, 0, 2]]
|
||||||
|
response = ':one: :cross_mark_button: :three:\n\n' +\
|
||||||
|
':four: :five: :six:\n\n' +\
|
||||||
|
':seven: :eight: :o_button:\n\n'
|
||||||
|
self._test_parse_board(board, response)
|
||||||
|
|
||||||
|
def _test_parse_board(self, board: List[List[int]], expected_response: str) -> None:
|
||||||
|
model, message_handler = self._get_game_handlers()
|
||||||
|
response = message_handler.parse_board(board)
|
||||||
|
self.assertEqual(response, expected_response)
|
||||||
|
|
||||||
|
def _test_determine_game_over(self, board: List[List[int]], players: List[str], expected_response: str) -> None:
|
||||||
|
model, message_handler = self._get_game_handlers()
|
||||||
|
response = model.determine_game_over(players)
|
||||||
|
self.assertEqual(response, expected_response)
|
||||||
|
|
||||||
|
def add_user_to_cache(self, name: str, bot: Any=None) -> Any:
|
||||||
|
if bot is None:
|
||||||
|
bot, bot_handler = self._get_handlers()
|
||||||
|
message = {
|
||||||
|
'sender_email': '{}@example.com'.format(name),
|
||||||
|
'sender_full_name': '{}'.format(name)
|
||||||
}
|
}
|
||||||
|
bot.add_user_to_cache(message)
|
||||||
|
return bot
|
||||||
|
|
||||||
msg = dict(
|
def setup_game(self) -> None:
|
||||||
help = "*Help for Tic-Tac-Toe bot* \nThe bot responds to messages starting with @mention-bot.\n**@mention-bot new** will start a new game (but not if you're already in the middle of a game). You must type this first to start playing!\n**@mention-bot help** will return this help function.\n**@mention-bot quit** will quit from the current game.\n**@mention-bot <coordinate>** will make a move at the given coordinate.\nCoordinates are entered in a (row, column) format. Numbering is from top to bottom and left to right. \nHere are the coordinates of each position. (Parentheses and spaces are optional). \n(1, 1) (1, 2) (1, 3) \n(2, 1) (2, 2) (2, 3) \n(3, 1) (3, 2) (3, 3) \n",
|
bot = self.add_user_to_cache('foo')
|
||||||
didnt_understand = "Hmm, I didn't understand your input. Type **@tictactoe help** or **@ttt help** to see valid inputs.",
|
self.add_user_to_cache('baz', bot)
|
||||||
new_game = "Welcome to tic-tac-toe! You'll be x's and I'll be o's. Your move first!\nCoordinates are entered in a (row, column) format. Numbering is from top to bottom and left to right.\nHere are the coordinates of each position. (Parentheses and spaces are optional.) \n(1, 1) (1, 2) (1, 3) \n(2, 1) (2, 2) (2, 3) \n(3, 1) (3, 2) (3, 3) \n Your move would be one of these. To make a move, type @mention-bot followed by a space and the coordinate.",
|
instance = GameInstance(bot, False, 'test game', 'abc123', [
|
||||||
already_playing = "You're already playing a game! Type **@tictactoe help** or **@ttt help** to see valid inputs.",
|
'foo@example.com', 'baz@example.com'], 'test')
|
||||||
already_played_there = 'That space is already filled, sorry!',
|
bot.instances.update({'abc123': instance})
|
||||||
successful_quit = "You've successfully quit the game.",
|
instance.start()
|
||||||
after_1_1 = ("[ x _ _ ]\n[ _ _ _ ]\n[ _ _ _ ]\n"
|
return bot
|
||||||
"My turn:\n[ x _ _ ]\n[ _ o _ ]\n[ _ _ _ ]\n"
|
|
||||||
"Your turn! Enter a coordinate or type help."),
|
|
||||||
after_2_1 = ("[ x _ _ ]\n[ x o _ ]\n[ _ _ _ ]\n"
|
|
||||||
"My turn:\n[ x _ _ ]\n[ x o _ ]\n[ o _ _ ]\n"
|
|
||||||
"Your turn! Enter a coordinate or type help."),
|
|
||||||
after_1_3 = ("[ x _ x ]\n[ x o _ ]\n[ o _ _ ]\n"
|
|
||||||
"My turn:\n[ x o x ]\n[ x o _ ]\n[ o _ _ ]\n"
|
|
||||||
"Your turn! Enter a coordinate or type help."),
|
|
||||||
after_3_2 = ("[ x o x ]\n[ x o _ ]\n[ o x _ ]\n"
|
|
||||||
"My turn:\n[ x o x ]\n[ x o _ ]\n[ o x o ]\n"
|
|
||||||
"Your turn! Enter a coordinate or type help."),
|
|
||||||
after_2_3_draw = ("[ x o x ]\n[ x o x ]\n[ o x o ]\n"
|
|
||||||
"It's a draw! Neither of us was able to win."),
|
|
||||||
after_2_3_try_lose = ("[ x _ _ ]\n[ _ o x ]\n[ _ _ _ ]\n"
|
|
||||||
"My turn:\n[ x _ _ ]\n[ _ o x ]\n[ _ o _ ]\n"
|
|
||||||
"Your turn! Enter a coordinate or type help."),
|
|
||||||
after_2_1_lost = ("[ x _ _ ]\n[ x o x ]\n[ _ o _ ]\n"
|
|
||||||
"My turn:\n[ x o _ ]\n[ x o x ]\n[ _ o _ ]\n"
|
|
||||||
"Game over! I've won!"),
|
|
||||||
)
|
|
||||||
|
|
||||||
conversation = [
|
def _get_game_handlers(self) -> Tuple[Any, Any]:
|
||||||
# Empty message
|
bot, bot_handler = self._get_handlers()
|
||||||
("", msg['didnt_understand']),
|
return bot.model, bot.gameMessageHandler
|
||||||
# Non-command
|
|
||||||
("adboh", msg['didnt_understand']),
|
|
||||||
# Help command
|
|
||||||
("help", msg['help']),
|
|
||||||
# Command: quit not understood with no game
|
|
||||||
("quit", msg['didnt_understand']),
|
|
||||||
# Can quit if new game and have state
|
|
||||||
("new", msg['new_game']),
|
|
||||||
("quit", msg['successful_quit']),
|
|
||||||
# Quit not understood when no game FIXME improve response?
|
|
||||||
("quit", msg['didnt_understand']),
|
|
||||||
# New right after new just restarts
|
|
||||||
("new", msg['new_game']),
|
|
||||||
("new", msg['new_game']),
|
|
||||||
# Make a corner play
|
|
||||||
("(1,1)", msg['after_1_1']),
|
|
||||||
# New while playing doesn't just restart
|
|
||||||
("new", msg['already_playing']),
|
|
||||||
# User played in this location already
|
|
||||||
("(1,1)", msg['already_played_there']),
|
|
||||||
# ... and bot played here
|
|
||||||
("(2,2)", msg['already_played_there']),
|
|
||||||
("quit", msg['successful_quit']),
|
|
||||||
# Can't play without game FIXME improve response?
|
|
||||||
("(1,1)", msg['didnt_understand']),
|
|
||||||
("new", msg['new_game']),
|
|
||||||
# Value out of range FIXME improve response?
|
|
||||||
("(1,5)", msg['didnt_understand']),
|
|
||||||
# Value out of range FIXME improve response?
|
|
||||||
("0,1", msg['didnt_understand']),
|
|
||||||
# Sequence of moves to show valid input formats:
|
|
||||||
("1,1", msg['after_1_1']),
|
|
||||||
("2, 1", msg['after_2_1']),
|
|
||||||
("(1,3)", msg['after_1_3']),
|
|
||||||
("3,2", msg['after_3_2']),
|
|
||||||
("2,3", msg['after_2_3_draw']),
|
|
||||||
# Game already over; can't quit FIXME improve response?
|
|
||||||
("quit", msg['didnt_understand']),
|
|
||||||
("new", msg['new_game']),
|
|
||||||
("1,1", msg['after_1_1']),
|
|
||||||
("2,3", msg['after_2_3_try_lose']),
|
|
||||||
("2,1", msg['after_2_1_lost']),
|
|
||||||
# Game already over; can't quit FIXME improve response?
|
|
||||||
("quit", msg['didnt_understand']),
|
|
||||||
]
|
|
||||||
|
|
||||||
with patch('zulip_bots.bots.tictactoe.tictactoe.random.choice') as choice:
|
|
||||||
choice.return_value = [2, 2]
|
|
||||||
self.verify_dialog(conversation)
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import copy
|
import copy
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from typing import List
|
from typing import List, Any, Tuple
|
||||||
|
from zulip_bots.game_handler import GameAdapter, BadMoveException
|
||||||
|
|
||||||
# -------------------------------------
|
# -------------------------------------
|
||||||
|
|
||||||
State = List[List[str]]
|
State = List[List[str]]
|
||||||
|
|
||||||
class TicTacToeGame(object):
|
|
||||||
|
class TicTacToeModel(object):
|
||||||
smarter = True
|
smarter = True
|
||||||
# If smarter is True, the computer will do some extra thinking - it'll be harder for the user.
|
# If smarter is True, the computer will do some extra thinking - it'll be harder for the user.
|
||||||
|
|
||||||
|
@ -21,52 +23,45 @@ class TicTacToeGame(object):
|
||||||
[(0, 2), (1, 1), (2, 0)] # Diagonal 2
|
[(0, 2), (1, 1), (2, 0)] # Diagonal 2
|
||||||
]
|
]
|
||||||
|
|
||||||
initial_board = [["_", "_", "_"],
|
initial_board = [[0, 0, 0],
|
||||||
["_", "_", "_"],
|
[0, 0, 0],
|
||||||
["_", "_", "_"]]
|
[0, 0, 0]]
|
||||||
|
|
||||||
def __init__(self, board=None):
|
def __init__(self, board: Any=None) -> None:
|
||||||
if board is not None:
|
if board is not None:
|
||||||
self.board = board
|
self.current_board = board
|
||||||
else:
|
else:
|
||||||
self.board = copy.deepcopy(self.initial_board)
|
self.current_board = copy.deepcopy(self.initial_board)
|
||||||
|
|
||||||
def get_state(self) -> State:
|
def get_value(self, board: Any, position: Tuple[int, int]) -> int:
|
||||||
return self.board
|
|
||||||
|
|
||||||
def is_new_game(self) -> bool:
|
|
||||||
return self.board == self.initial_board
|
|
||||||
|
|
||||||
def display_row(self, row):
|
|
||||||
''' Takes the row passed in as a list and returns it as a string. '''
|
|
||||||
row_string = " ".join([e.strip() for e in row])
|
|
||||||
return("[ {} ]\n".format(row_string))
|
|
||||||
|
|
||||||
def display_board(self, board):
|
|
||||||
''' Takes the board as a nested list and returns a nice version for the user. '''
|
|
||||||
return "".join([self.display_row(r) for r in board])
|
|
||||||
|
|
||||||
def get_value(self, board, position):
|
|
||||||
return board[position[0]][position[1]]
|
return board[position[0]][position[1]]
|
||||||
|
|
||||||
def board_is_full(self, board):
|
def determine_game_over(self, players: List[str]) -> str:
|
||||||
|
if self.contains_winning_move(self.current_board):
|
||||||
|
return 'current turn'
|
||||||
|
if self.board_is_full(self.current_board):
|
||||||
|
return 'draw'
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def board_is_full(self, board: Any) -> bool:
|
||||||
''' Determines if the board is full or not. '''
|
''' Determines if the board is full or not. '''
|
||||||
for row in board:
|
for row in board:
|
||||||
for element in row:
|
for element in row:
|
||||||
if element == "_":
|
if element == 0:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def contains_winning_move(self, board): # Used for current board & trial computer board
|
# Used for current board & trial computer board
|
||||||
|
def contains_winning_move(self, board: Any) -> bool:
|
||||||
''' Returns true if all coordinates in a triplet have the same value in them (x or o) and no coordinates
|
''' Returns true if all coordinates in a triplet have the same value in them (x or o) and no coordinates
|
||||||
in the triplet are blank. '''
|
in the triplet are blank. '''
|
||||||
for triplet in self.triplets:
|
for triplet in self.triplets:
|
||||||
if (self.get_value(board, triplet[0]) == self.get_value(board, triplet[1]) ==
|
if (self.get_value(board, triplet[0]) == self.get_value(board, triplet[1]) ==
|
||||||
self.get_value(board, triplet[2]) != "_"):
|
self.get_value(board, triplet[2]) != 0):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_locations_of_char(self, board, char):
|
def get_locations_of_char(self, board: Any, char: int) -> List[List[int]]:
|
||||||
''' Gets the locations of the board that have char in them. '''
|
''' Gets the locations of the board that have char in them. '''
|
||||||
locations = []
|
locations = []
|
||||||
for row in range(3):
|
for row in range(3):
|
||||||
|
@ -75,83 +70,91 @@ class TicTacToeGame(object):
|
||||||
locations.append([row, col])
|
locations.append([row, col])
|
||||||
return locations
|
return locations
|
||||||
|
|
||||||
def two_blanks(self, triplet, board):
|
def two_blanks(self, triplet: List[Tuple[int, int]], board: Any) -> List[Tuple[int, int]]:
|
||||||
''' Determines which rows/columns/diagonals have two blank spaces and an 'o' already in them. It's more advantageous
|
''' Determines which rows/columns/diagonals have two blank spaces and an 2 already in them. It's more advantageous
|
||||||
for the computer to move there. This is used when the computer makes its move. '''
|
for the computer to move there. This is used when the computer makes its move. '''
|
||||||
|
|
||||||
o_found = False
|
o_found = False
|
||||||
for position in triplet:
|
for position in triplet:
|
||||||
if self.get_value(board, position) == "o":
|
if self.get_value(board, position) == 2:
|
||||||
o_found = True
|
o_found = True
|
||||||
break
|
break
|
||||||
|
|
||||||
blanks_list = []
|
blanks_list = []
|
||||||
if o_found:
|
if o_found:
|
||||||
for position in triplet:
|
for position in triplet:
|
||||||
if self.get_value(board, position) == "_":
|
if self.get_value(board, position) == 0:
|
||||||
blanks_list.append(position)
|
blanks_list.append(position)
|
||||||
|
|
||||||
if len(blanks_list) == 2:
|
if len(blanks_list) == 2:
|
||||||
return blanks_list
|
return blanks_list
|
||||||
|
return []
|
||||||
|
|
||||||
def computer_move(self, board):
|
def computer_move(self, board: Any, player_number: Any) -> Any:
|
||||||
''' The computer's logic for making its move. '''
|
''' The computer's logic for making its move. '''
|
||||||
my_board = copy.deepcopy(board) # First the board is copied; used later on
|
my_board = copy.deepcopy(
|
||||||
blank_locations = self.get_locations_of_char(my_board, "_")
|
board) # First the board is copied; used later on
|
||||||
x_locations = self.get_locations_of_char(board, "x") # Gets the locations that already have x's
|
blank_locations = self.get_locations_of_char(my_board, 0)
|
||||||
corner_locations = [[0, 0], [0, 2], [2, 0], [2, 2]] # List of the coordinates of the corners of the board
|
# Gets the locations that already have x's
|
||||||
edge_locations = [[1, 0], [0, 1], [1, 2], [2, 1]] # List of the coordinates of the edge spaces of the board
|
x_locations = self.get_locations_of_char(board, 1)
|
||||||
|
# List of the coordinates of the corners of the board
|
||||||
|
corner_locations = [[0, 0], [0, 2], [2, 0], [2, 2]]
|
||||||
|
# List of the coordinates of the edge spaces of the board
|
||||||
|
edge_locations = [[1, 0], [0, 1], [1, 2], [2, 1]]
|
||||||
|
|
||||||
if blank_locations == []: # If no empty spaces are left, the computer can't move anyway, so it just returns the board.
|
# If no empty spaces are left, the computer can't move anyway, so it just returns the board.
|
||||||
|
if blank_locations == []:
|
||||||
return board
|
return board
|
||||||
|
|
||||||
if len(x_locations) == 1: # This is special logic only used on the first move.
|
# This is special logic only used on the first move.
|
||||||
# If the user played first in the corner or edge, the computer should move in the center.
|
if len(x_locations) == 1:
|
||||||
|
# If the user played first in the corner or edge,
|
||||||
|
# the computer should move in the center.
|
||||||
if x_locations[0] in corner_locations or x_locations[0] in edge_locations:
|
if x_locations[0] in corner_locations or x_locations[0] in edge_locations:
|
||||||
board[1][1] = "o"
|
board[1][1] = 2
|
||||||
# If user played first in the center, the computer should move in the corner. It doesn't matter which corner.
|
# If user played first in the center, the computer should move in the corner. It doesn't matter which corner.
|
||||||
else:
|
else:
|
||||||
location = random.choice(corner_locations)
|
location = random.choice(corner_locations)
|
||||||
row = location[0]
|
row = location[0]
|
||||||
col = location[1]
|
col = location[1]
|
||||||
board[row][col] = "o"
|
board[row][col] = 2
|
||||||
return board
|
return board
|
||||||
|
|
||||||
# This logic is used on all other moves.
|
# This logic is used on all other moves.
|
||||||
# First I'll check if the computer can win in the next move. If so, that's where the computer will play.
|
# First I'll check if the computer can win in the next move. If so, that's where the computer will play.
|
||||||
# The check is done by replacing the blank locations with o's and seeing if the computer would win in each case.
|
# The check is done by replacing the blank locations with o's and seeing if the computer would win in each case.
|
||||||
for row, col in blank_locations:
|
for row, col in blank_locations:
|
||||||
my_board[row][col] = "o"
|
my_board[row][col] = 2
|
||||||
if self.contains_winning_move(my_board):
|
if self.contains_winning_move(my_board):
|
||||||
board[row][col] = "o"
|
board[row][col] = 2
|
||||||
return board
|
return board
|
||||||
else:
|
else:
|
||||||
my_board[row][col] = "_" # Revert if not winning
|
my_board[row][col] = 0 # Revert if not winning
|
||||||
|
|
||||||
# If the computer can't immediately win, it wants to make sure the user can't win in their next move, so it
|
# If the computer can't immediately win, it wants to make sure the user can't win in their next move, so it
|
||||||
# checks to see if the user needs to be blocked.
|
# checks to see if the user needs to be blocked.
|
||||||
# The check is done by replacing the blank locations with x's and seeing if the user would win in each case.
|
# The check is done by replacing the blank locations with x's and seeing if the user would win in each case.
|
||||||
for row, col in blank_locations:
|
for row, col in blank_locations:
|
||||||
my_board[row][col] = "x"
|
my_board[row][col] = 1
|
||||||
if self.contains_winning_move(my_board):
|
if self.contains_winning_move(my_board):
|
||||||
board[row][col] = "o"
|
board[row][col] = 2
|
||||||
return board
|
return board
|
||||||
else:
|
else:
|
||||||
my_board[row][col] = "_" # Revert if not winning
|
my_board[row][col] = 0 # Revert if not winning
|
||||||
|
|
||||||
# Assuming nobody will win in their next move, now I'll find the best place for the computer to win.
|
# Assuming nobody will win in their next move, now I'll find the best place for the computer to win.
|
||||||
for row, col in blank_locations:
|
for row, col in blank_locations:
|
||||||
if ('x' not in my_board[row] and my_board[0][col] != 'x' and my_board[1][col] !=
|
if (1 not in my_board[row] and my_board[0][col] != 1 and my_board[1][col] !=
|
||||||
'x' and my_board[2][col] != 'x'):
|
1 and my_board[2][col] != 1):
|
||||||
board[row][col] = 'o'
|
board[row][col] = 2
|
||||||
return board
|
return board
|
||||||
|
|
||||||
# If no move has been made, choose a random blank location. If smarter is True, the computer will choose a
|
# If no move has been made, choose a random blank location. If smarter is True, the computer will choose a
|
||||||
# random blank location from a set of better locations to play. These locations are determined by seeing if
|
# random blank location from a set of better locations to play. These locations are determined by seeing if
|
||||||
# there are two blanks and an 'o' in each row, column, and diagonal (done in two_blanks).
|
# there are two blanks and an 2 in each row, column, and diagonal (done in two_blanks).
|
||||||
# If smarter is False, all blank locations can be chosen.
|
# If smarter is False, all blank locations can be chosen.
|
||||||
if self.smarter:
|
if self.smarter:
|
||||||
blanks = []
|
blanks = [] # type: Any
|
||||||
for triplet in self.triplets:
|
for triplet in self.triplets:
|
||||||
result = self.two_blanks(triplet, board)
|
result = self.two_blanks(triplet, board)
|
||||||
if result:
|
if result:
|
||||||
|
@ -164,17 +167,17 @@ class TicTacToeGame(object):
|
||||||
location = random.choice(blank_list)
|
location = random.choice(blank_list)
|
||||||
row = location[0]
|
row = location[0]
|
||||||
col = location[1]
|
col = location[1]
|
||||||
board[row][col] = 'o'
|
board[row][col] = 2
|
||||||
return board
|
return board
|
||||||
|
|
||||||
else:
|
else:
|
||||||
location = random.choice(blank_locations)
|
location = random.choice(blank_locations)
|
||||||
row = location[0]
|
row = location[0]
|
||||||
col = location[1]
|
col = location[1]
|
||||||
board[row][col] = 'o'
|
board[row][col] = 2
|
||||||
return board
|
return board
|
||||||
|
|
||||||
def is_valid_move(self, move):
|
def is_valid_move(self, move: str) -> bool:
|
||||||
''' Checks the validity of the coordinate input passed in to make sure it's not out-of-bounds (ex. 5, 5) '''
|
''' Checks the validity of the coordinate input passed in to make sure it's not out-of-bounds (ex. 5, 5) '''
|
||||||
try:
|
try:
|
||||||
split_move = move.split(",")
|
split_move = move.split(",")
|
||||||
|
@ -187,78 +190,58 @@ class TicTacToeGame(object):
|
||||||
valid = False
|
valid = False
|
||||||
return valid
|
return valid
|
||||||
|
|
||||||
def tictactoe(self, move):
|
def make_move(self, move: str, player_number: int, computer_move: bool=False) -> Any:
|
||||||
board = self.board
|
if computer_move:
|
||||||
printed_boards = dict(after_player = "", after_computer = "")
|
return self.computer_move(self.current_board, player_number + 1)
|
||||||
|
move_coords_str = coords_from_command(move)
|
||||||
|
if not self.is_valid_move(move_coords_str):
|
||||||
|
raise BadMoveException('Make sure your move is from 0-9')
|
||||||
|
board = self.current_board
|
||||||
|
move_coords = move_coords_str.split(',')
|
||||||
|
# Subtraction must be done to convert to the right indices,
|
||||||
|
# since computers start numbering at 0.
|
||||||
|
row = (int(move_coords[1])) - 1
|
||||||
|
column = (int(move_coords[0])) - 1
|
||||||
|
if board[row][column] != 0:
|
||||||
|
raise BadMoveException('Make sure your space hasn\'t already been filled.')
|
||||||
|
board[row][column] = player_number + 1
|
||||||
|
return board
|
||||||
|
|
||||||
# Subtraction must be done to convert to the right indices, since computers start numbering at 0.
|
|
||||||
row = (int(move[0])) - 1
|
|
||||||
column = (int(move[-1])) - 1
|
|
||||||
|
|
||||||
if board[row][column] != "_":
|
class TicTacToeMessageHandler(object):
|
||||||
return ("filled", printed_boards)
|
tokens = [':cross_mark_button:', ':o_button:']
|
||||||
else:
|
|
||||||
board[row][column] = "x"
|
|
||||||
|
|
||||||
printed_boards['after_player'] = self.display_board(board)
|
def parse_row(self, row: Tuple[int, int], row_num: int) -> str:
|
||||||
|
''' Takes the row passed in as a list and returns it as a string. '''
|
||||||
|
row_chars = []
|
||||||
|
num_symbols = [':one:', ':two:', ':three:', ':four:', ':five:', ':six:', ':seven:', ':eight:', ':nine:']
|
||||||
|
for i, e in enumerate(row):
|
||||||
|
if e == 0:
|
||||||
|
row_chars.append(num_symbols[row_num * 3 + i])
|
||||||
|
else:
|
||||||
|
row_chars.append(self.get_player_color(e - 1))
|
||||||
|
row_string = ' '.join(row_chars)
|
||||||
|
return row_string + '\n\n'
|
||||||
|
|
||||||
# Check to see if the user won/drew after they made their move. If not, it's the computer's turn.
|
def parse_board(self, board: Any) -> str:
|
||||||
if self.contains_winning_move(board):
|
''' Takes the board as a nested list and returns a nice version for the user. '''
|
||||||
return ("player_win", printed_boards)
|
return "".join([self.parse_row(r, r_num) for r_num, r in enumerate(board)])
|
||||||
|
|
||||||
if self.board_is_full(board):
|
def get_player_color(self, turn: int) -> str:
|
||||||
return ("draw", printed_boards)
|
return self.tokens[turn]
|
||||||
|
|
||||||
self.computer_move(board)
|
def alert_move_message(self, original_player: str, move_info: str) -> str:
|
||||||
printed_boards['after_computer'] = self.display_board(board)
|
move_info = move_info.replace('move ', '')
|
||||||
|
return '{} put a token at {}'.format(original_player, move_info)
|
||||||
|
|
||||||
# Checks to see if the computer won after it makes its move. (The computer can't draw, so there's no point
|
def game_start_message(self) -> str:
|
||||||
# in checking.) If the computer didn't win, the user gets another turn.
|
return ("Welcome to tic-tac-toe!"
|
||||||
if self.contains_winning_move(board):
|
"To make a move, type @-mention `move <number>`")
|
||||||
return ("computer_win", printed_boards)
|
|
||||||
|
|
||||||
return ("next_turn", printed_boards)
|
|
||||||
|
|
||||||
# -------------------------------------
|
class ticTacToeHandler(GameAdapter):
|
||||||
long_help_text = ("*Help for Tic-Tac-Toe bot* \n"
|
|
||||||
"The bot responds to messages starting with @mention-bot.\n"
|
|
||||||
"**@mention-bot new** will start a new game (but not if you're "
|
|
||||||
"already in the middle of a game). You must type this first to start playing!\n"
|
|
||||||
"**@mention-bot help** will return this help function.\n"
|
|
||||||
"**@mention-bot quit** will quit from the current game.\n"
|
|
||||||
"**@mention-bot <coordinate>** will make a move at the given coordinate.\n"
|
|
||||||
"Coordinates are entered in a (row, column) format. Numbering is from "
|
|
||||||
"top to bottom and left to right. \n"
|
|
||||||
"Here are the coordinates of each position. (Parentheses and spaces are optional). \n"
|
|
||||||
"(1, 1) (1, 2) (1, 3) \n(2, 1) (2, 2) (2, 3) \n(3, 1) (3, 2) (3, 3) \n")
|
|
||||||
|
|
||||||
short_help_text = "Type **@tictactoe help** or **@ttt help** to see valid inputs."
|
|
||||||
|
|
||||||
new_game_text = ("Welcome to tic-tac-toe! You'll be x's and I'll be o's."
|
|
||||||
" Your move first!\n"
|
|
||||||
"Coordinates are entered in a (row, column) format. "
|
|
||||||
"Numbering is from top to bottom and left to right.\n"
|
|
||||||
"Here are the coordinates of each position. (Parentheses and spaces are optional.) \n"
|
|
||||||
"(1, 1) (1, 2) (1, 3) \n(2, 1) (2, 2) (2, 3) \n(3, 1) (3, 2) (3, 3) \n "
|
|
||||||
"Your move would be one of these. To make a move, type @mention-bot "
|
|
||||||
"followed by a space and the coordinate.")
|
|
||||||
quit_game_text = "You've successfully quit the game."
|
|
||||||
unknown_message_text = "Hmm, I didn't understand your input."
|
|
||||||
already_playing_text = "You're already playing a game!"
|
|
||||||
|
|
||||||
mid_move_text = "My turn:"
|
|
||||||
end_of_move_text = {
|
|
||||||
"filled": "That space is already filled, sorry!",
|
|
||||||
"next_turn": "Your turn! Enter a coordinate or type help.",
|
|
||||||
"computer_win": "Game over! I've won!",
|
|
||||||
"player_win": "Game over! You've won!",
|
|
||||||
"draw": "It's a draw! Neither of us was able to win.",
|
|
||||||
}
|
|
||||||
# -------------------------------------
|
|
||||||
class ticTacToeHandler(object):
|
|
||||||
'''
|
'''
|
||||||
You can play tic-tac-toe in a private message with
|
You can play tic-tac-toe! Make sure your message starts with
|
||||||
tic-tac-toe bot! Make sure your message starts with
|
|
||||||
"@mention-bot".
|
"@mention-bot".
|
||||||
'''
|
'''
|
||||||
META = {
|
META = {
|
||||||
|
@ -266,69 +249,38 @@ class ticTacToeHandler(object):
|
||||||
'description': 'Lets you play Tic-tac-toe against a computer.',
|
'description': 'Lets you play Tic-tac-toe against a computer.',
|
||||||
}
|
}
|
||||||
|
|
||||||
def usage(self):
|
def usage(self) -> str:
|
||||||
return '''
|
return '''
|
||||||
You can play tic-tac-toe with the computer now! Make sure your
|
You can play tic-tac-toe now! Make sure your
|
||||||
message starts with @mention-bot.
|
message starts with @mention-bot.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def handle_message(self, message, bot_handler):
|
def __init__(self) -> None:
|
||||||
command = message['content']
|
game_name = 'Tic Tac Toe'
|
||||||
original_sender = message['sender_email']
|
bot_name = 'tictactoe'
|
||||||
|
move_help_message = '* To move during a game, type\n`move <number>`'
|
||||||
|
move_regex = 'move \d$'
|
||||||
|
model = TicTacToeModel
|
||||||
|
gameMessageHandler = TicTacToeMessageHandler
|
||||||
|
super(ticTacToeHandler, self).__init__(
|
||||||
|
game_name,
|
||||||
|
bot_name,
|
||||||
|
move_help_message,
|
||||||
|
move_regex,
|
||||||
|
model,
|
||||||
|
gameMessageHandler,
|
||||||
|
supports_computer=True
|
||||||
|
)
|
||||||
|
|
||||||
storage = bot_handler.storage
|
|
||||||
if not storage.contains(original_sender):
|
|
||||||
storage.put(original_sender, None)
|
|
||||||
state = storage.get(original_sender)
|
|
||||||
user_game = TicTacToeGame(state) if state else None
|
|
||||||
|
|
||||||
move = None
|
def coords_from_command(cmd: str) -> str:
|
||||||
if command == 'new':
|
|
||||||
if not user_game:
|
|
||||||
user_game = TicTacToeGame()
|
|
||||||
move = "new"
|
|
||||||
if not user_game.is_new_game():
|
|
||||||
response = " ".join([already_playing_text, short_help_text])
|
|
||||||
else:
|
|
||||||
response = new_game_text
|
|
||||||
elif command == 'help':
|
|
||||||
response = long_help_text
|
|
||||||
elif (user_game) and user_game.is_valid_move(coords_from_command(command)):
|
|
||||||
move, printed_boards = user_game.tictactoe(coords_from_command(command))
|
|
||||||
mid_text = mid_move_text+"\n" if printed_boards['after_computer'] else ""
|
|
||||||
response = "".join([printed_boards['after_player'], mid_text,
|
|
||||||
printed_boards['after_computer'],
|
|
||||||
end_of_move_text[move]])
|
|
||||||
elif (user_game) and command == 'quit':
|
|
||||||
move = "quit"
|
|
||||||
response = quit_game_text
|
|
||||||
else:
|
|
||||||
response = " ".join([unknown_message_text, short_help_text])
|
|
||||||
|
|
||||||
if move is not None:
|
|
||||||
if any(reset_text in move for reset_text in ("win", "draw", "quit")):
|
|
||||||
storage.put(original_sender, None)
|
|
||||||
elif any(keep_text == move for keep_text in ("new", "next_turn")):
|
|
||||||
storage.put(original_sender, user_game.get_state())
|
|
||||||
else: # "filled" => no change, state remains the same
|
|
||||||
pass
|
|
||||||
|
|
||||||
bot_handler.send_message(dict(
|
|
||||||
type = 'private',
|
|
||||||
to = original_sender,
|
|
||||||
subject = message['sender_email'],
|
|
||||||
content = response,
|
|
||||||
))
|
|
||||||
|
|
||||||
def coords_from_command(cmd):
|
|
||||||
# This function translates the input command into a TicTacToeGame move.
|
# This function translates the input command into a TicTacToeGame move.
|
||||||
# It should return two indices, each one of (1,2,3), separated by a comma, eg. "3,2"
|
# It should return two indices, each one of (1,2,3), separated by a comma, eg. "3,2"
|
||||||
''' As there are various ways to input a coordinate (with/without parentheses, with/without spaces, etc.) the
|
''' As there are various ways to input a coordinate (with/without parentheses, with/without spaces, etc.) the
|
||||||
input is stripped to just the numbers before being used in the program. '''
|
input is stripped to just the numbers before being used in the program. '''
|
||||||
cmd = cmd.replace("(", "")
|
cmd_num = int(cmd.replace('move ', '')) - 1
|
||||||
cmd = cmd.replace(")", "")
|
cmd = '{},{}'.format((cmd_num % 3) + 1, (cmd_num // 3) + 1)
|
||||||
cmd = cmd.replace(" ", "")
|
|
||||||
cmd = cmd.strip()
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
handler_class = ticTacToeHandler
|
handler_class = ticTacToeHandler
|
||||||
|
|
Loading…
Reference in a new issue