From 105b0ab0dc9b9c65b1433471ab21bf1ad5d444a9 Mon Sep 17 00:00:00 2001 From: Maydha K Date: Thu, 5 Jan 2017 05:06:50 +0000 Subject: [PATCH] interactive bots: Create tic-tac-toe bot. --- contrib_bots/lib/tictactoe-bot.py | 331 +++++++++++++++++++++++++++++ contrib_bots/lib/tictactoe/docs.md | 34 +++ 2 files changed, 365 insertions(+) create mode 100644 contrib_bots/lib/tictactoe-bot.py create mode 100644 contrib_bots/lib/tictactoe/docs.md diff --git a/contrib_bots/lib/tictactoe-bot.py b/contrib_bots/lib/tictactoe-bot.py new file mode 100644 index 0000000..ffaef89 --- /dev/null +++ b/contrib_bots/lib/tictactoe-bot.py @@ -0,0 +1,331 @@ + +from __future__ import absolute_import +from __future__ import print_function +import copy +import random +from six.moves import range + +initial_board = [["_", "_", "_"], + ["_", "_", "_"], + ["_", "_", "_"]] + +mode = 'r' # default, can change for debugging to 'p' +def output_mode(string_to_print, mode): + if mode == "p": + print(string_to_print) + elif mode == "r": + return string_to_print + +# ------------------------------------- +class TicTacToeGame(object): + smarter = True + # If smarter is True, the computer will do some extra thinking - it'll be harder for the user. + + triplets = [[(0, 0), (0, 1), (0, 2)], # Row 1 + [(1, 0), (1, 1), (1, 2)], # Row 2 + [(2, 0), (2, 1), (2, 2)], # Row 3 + [(0, 0), (1, 0), (2, 0)], # Column 1 + [(0, 1), (1, 1), (2, 1)], # Column 2 + [(0, 2), (1, 2), (2, 2)], # Column 3 + [(0, 0), (1, 1), (2, 2)], # Diagonal 1 + [(0, 2), (1, 1), (2, 0)] # Diagonal 2 + ] + + positions = "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 @tictactoe or @ttt " \ + "followed by a space and the coordinate." + + detailed_help_message = "*Help for Tic-Tac-Toe bot* \n" \ + "The bot responds to messages starting with @tictactoe or @ttt.\n" \ + "**@tictactoe new** (or **@ttt 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" \ + "**@tictactoe help** (or **@ttt help**) will return this help function.\n" \ + "**@tictactoe quit** (or **@ttt quit**) will quit from the current game.\n" \ + "**@tictactoe ** (or **@ttt **) 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" + + def __init__(self, board): + self.board = 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]] + + def board_is_full(self, board): + ''' Determines if the board is full or not. ''' + full = False + board_state = "" + for row in board: + for element in row: + if element == "_": + board_state += "_" + if "_" not in board_state: + full = True + return full + + def win_conditions(self, board, triplets): + ''' 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. ''' + won = False + for triplet in triplets: + if (self.get_value(board, triplet[0]) == self.get_value(board, triplet[1]) == + self.get_value(board, triplet[2]) != "_"): + won = True + break + return won + + def get_locations_of_char(self, board, char): + ''' Gets the locations of the board that have char in them. ''' + locations = [] + for row in range(3): + for col in range(3): + if board[row][col] == char: + locations.append([row, col]) + return locations + + def two_blanks(self, triplet, board): + ''' Determines which rows/columns/diagonals have two blank spaces and an 'o' already in them. It's more advantageous + for the computer to move there. This is used when the computer makes its move. ''' + + o_found = False + for position in triplet: + if self.get_value(board, position) == "o": + o_found = True + break + + blanks_list = [] + if o_found: + for position in triplet: + if self.get_value(board, position) == "_": + blanks_list.append(position) + + if len(blanks_list) == 2: + return blanks_list + + def computer_move(self, board): + ''' The computer's logic for making its move. ''' + my_board = copy.deepcopy(board) # First the board is copied; used later on + blank_locations = self.get_locations_of_char(my_board, "_") + x_locations = self.get_locations_of_char(board, "x") # Gets the locations that already have x's + corner_locations = [[0, 0], [0, 2], [2, 0], [2, 2]] # List of the coordinates of the corners of the board + edge_locations = [[1, 0], [0, 1], [1, 2], [2, 1]] # List of the coordinates of the edge spaces of the board + + if blank_locations == []: # If no empty spaces are left, the computer can't move anyway, so it just returns the board. + return board + + if len(x_locations) == 1: # 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 x_locations[0] in corner_locations or x_locations[0] in edge_locations: + board[1][1] = "o" + # If user played first in the center, the computer should move in the corner. It doesn't matter which corner. + else: + location = random.choice(corner_locations) + row = location[0] + col = location[1] + board[row][col] = "o" + return board + + # 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. + # 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: + my_board[row][col] = "o" + if self.win_conditions(my_board, self.triplets) == True: + board[row][col] = "o" + return board + else: + my_board[row][col] = "_" # 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 + # 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. + for row, col in blank_locations: + my_board[row][col] = "x" + if self.win_conditions(my_board, self.triplets): + board[row][col] = "o" + return board + else: + my_board[row][col] = "_" # Revert if not winning + + # 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: + if ('x' not in my_board[row] and my_board[0][col] != 'x' and my_board[1][col] != + 'x' and my_board[2][col] != 'x'): + board[row][col] = 'o' + return board + + # 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 + # there are two blanks and an 'o' in each row, column, and diagonal (done in two_blanks). + # If smarter is False, all blank locations can be chosen. + if self.smarter == True: + blanks = [] + for triplet in self.triplets: + result = self.two_blanks(triplet, board) + if result: + blanks = blanks + result + blank_set = set(blanks) + blank_list = list(blank_set) + if blank_list == []: + location = random.choice(blank_locations) + else: + location = random.choice(blank_list) + row = location[0] + col = location[1] + board[row][col] = 'o' + return board + + else: + location = random.choice(blank_locations) + row = location[0] + col = location[1] + board[row][col] = 'o' + return board + + def check_validity(self, move): + ''' Checks the validity of the coordinate input passed in to make sure it's not out-of-bounds (ex. 5, 5) ''' + try: + split_move = move.split(",") + row = split_move[0].strip() + col = split_move[1].strip() + valid = False + if row == "1" or row == "2" or row == "3": + if col == "1" or col == "2" or col == "3": + valid = True + except IndexError: + valid = False + return valid + + def sanitize_move(self, move): + ''' 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. ''' + move = move.replace("(", "") + move = move.replace(")", "") + move = move.strip() + return move + + def tictactoe(self, board, input_string): + return_string = "" + move = self.sanitize_move(input_string) + + # 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] != "_": + return_string += output_mode("That space is already filled, sorry!", mode) + return return_string + else: + board[row][column] = "x" + + return_string += self.display_board(board) + + # Check to see if the user won/drew after they made their move. If not, it's the computer's turn. + if self.win_conditions(board, self.triplets) == True: + return_string += output_mode("Game over! You've won!", mode) + return return_string + + if self.board_is_full(board) == True: + return_string += output_mode("It's a draw! Neither of us was able to win.", mode) + return return_string + + return_string += output_mode("My turn:\n", mode) + self.computer_move(board) + return_string += self.display_board(board) + + # Checks to see if the computer won after it makes its move. (The computer can't draw, so there's no point + # in checking.) If the computer didn't win, the user gets another turn. + if self.win_conditions(board, self.triplets) == True: + return_string += output_mode("Game over! I've won!", mode) + return return_string + + return_string += output_mode("Your turn! Enter a coordinate or type help.", mode) + return return_string + +# ------------------------------------- +flat_initial = sum(initial_board, []) +def first_time(board): + flat = sum(board, []) + return flat == flat_initial + +class ticTacToeHandler(object): + ''' + You can play tic-tac-toe in a private message with + tic-tac-toe bot! Make sure your message starts with + "@tictactoe or @ttt". + ''' + + def usage(self): + return ''' + You can play tic-tac-toe with the computer now! Make sure your + message starts with @tictactoe or @ttt. + ''' + + def triage_message(self, message, client): + # return True if we want to (possibly) response to this message + original_content = message['content'] + is_tictactoe = (original_content.startswith('@tictactoe') or + original_content.startswith('@ttt')) + return is_tictactoe + + def handle_message(self, message, client, state_handler): + original_content = message['content'] + command_list = original_content.split()[1:] + command = "" + for val in command_list: + command += val + original_sender = message['sender_email'] + + mydict = state_handler.get_state() + if not mydict: + state_handler.set_state({}) + mydict = state_handler.get_state() + + user_game = mydict.get(original_sender) + if (not user_game) and command == "new": + user_game = TicTacToeGame(copy.deepcopy(initial_board)) + mydict[original_sender] = user_game + + if command == 'new': + if user_game and not first_time(user_game.board): + return_content = "You're already playing a game! Type **@tictactoe help** or **@ttt help** to see valid inputs." + else: + return_content = "Welcome to tic-tac-toe! You'll be x's and I'll be o's. Your move first!\n" + return_content += TicTacToeGame.positions + elif command == 'help': + return_content = TicTacToeGame.detailed_help_message + elif (user_game) and TicTacToeGame.check_validity(user_game, TicTacToeGame.sanitize_move(user_game, command)) == True: + user_board = user_game.board + return_content = TicTacToeGame.tictactoe(user_game, user_board, command) + elif (user_game) and command == 'quit': + del mydict[original_sender] + return_content = "You've successfully quit the game." + else: + return_content = "Hmm, I didn't understand your input. Type **@tictactoe help** or **@ttt help** to see valid inputs." + + if "Game over" in return_content or "draw" in return_content: + del mydict[original_sender] + + state_handler.set_state(mydict) + + client.send_message(dict( + type = 'private', + to = original_sender, + subject = message['sender_email'], + content = return_content, + )) + +handler_class = ticTacToeHandler diff --git a/contrib_bots/lib/tictactoe/docs.md b/contrib_bots/lib/tictactoe/docs.md new file mode 100644 index 0000000..e5d8162 --- /dev/null +++ b/contrib_bots/lib/tictactoe/docs.md @@ -0,0 +1,34 @@ +# About Tic-Tac-Toe Bot + +This bot allows you to play tic-tac-toe in a private message with the bot. +Multiple games can simultaneously be played by different users, each playing +against the computer. + +The bot only responds to messages starting with **@tictactoe** or **@ttt**. + +### Commands +**@tictactoe new** (or **@ttt new**) will start a new game (but not if you are +already playing a game.) You must type this first to start playing! + +**@tictactoe help** (or **@ttt help**) will return a help function with valid +commands and coordinates. + +**@tictactoe quit** (or **@ttt quit**) will quit from the current game. + +**@tictactoe ** (or **@ttt **) will make a move at the +entered coordinate. For example, **@ttt 1,1** . After this, the bot will make +its move, or declare the game over if the user or bot has won. + +Coordinates are entered in a (row, column) format. Numbering is from top to +bottom and left to right. +Here are the coordinates of each position. When entering coordinates, parentheses +and spaces are optional. + +(1, 1) | (1, 2) | (1, 3) + +(2, 1) | (2, 2) | (2, 3) + +(3, 1) | (3, 2) | (3, 3) + +Invalid commands will result in an "I don't understand" response from the bot, +with a suggestion to type **@tictactoe help** (or **@ttt help**).