zulip_bots: Migrate tictactoe bot to new game_handler.

This commit is contained in:
fredfishgames 2018-01-19 15:55:51 +00:00 committed by showell
parent 3a438cafa9
commit 3cbb16722d
2 changed files with 185 additions and 271 deletions

View file

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

View file

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