interactive bots: Create tic-tac-toe bot.
This commit is contained in:
		
							parent
							
								
									75a9101f30
								
							
						
					
					
						commit
						105b0ab0dc
					
				
					 2 changed files with 365 additions and 0 deletions
				
			
		
							
								
								
									
										331
									
								
								contrib_bots/lib/tictactoe-bot.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								contrib_bots/lib/tictactoe-bot.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <coordinate>** (or **@ttt <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" | ||||
| 
 | ||||
|     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 | ||||
							
								
								
									
										34
									
								
								contrib_bots/lib/tictactoe/docs.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								contrib_bots/lib/tictactoe/docs.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <coordinate>** (or **@ttt <coordinate>**) 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**). | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Maydha K
						Maydha K