Add game of fifteen bot.
This commit is contained in:
		
							parent
							
								
									b8d4f0b869
								
							
						
					
					
						commit
						8ef9b70191
					
				
					 4 changed files with 351 additions and 0 deletions
				
			
		
							
								
								
									
										0
									
								
								zulip_bots/zulip_bots/bots/gameoffifteen/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								zulip_bots/zulip_bots/bots/gameoffifteen/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										149
									
								
								zulip_bots/zulip_bots/bots/gameoffifteen/gameoffifteen.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								zulip_bots/zulip_bots/bots/gameoffifteen/gameoffifteen.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,149 @@ | ||||||
|  | import copy | ||||||
|  | import random | ||||||
|  | 
 | ||||||
|  | from typing import List, Any, Tuple, Dict | ||||||
|  | from zulip_bots.game_handler import GameAdapter, BadMoveException | ||||||
|  | 
 | ||||||
|  | class GameOfFifteenModel(object): | ||||||
|  | 
 | ||||||
|  |     final_board = [[0, 1, 2], | ||||||
|  |                    [3, 4, 5], | ||||||
|  |                    [6, 7, 8]] | ||||||
|  | 
 | ||||||
|  |     initial_board = [[8, 7, 6], | ||||||
|  |                      [5, 4, 3], | ||||||
|  |                      [2, 1, 0]] | ||||||
|  | 
 | ||||||
|  |     def __init__(self, board: Any=None) -> None: | ||||||
|  |         if board is not None: | ||||||
|  |             self.current_board = board | ||||||
|  |         else: | ||||||
|  |             self.current_board = copy.deepcopy(self.initial_board) | ||||||
|  | 
 | ||||||
|  |     def get_coordinates(self, board: List[List[int]]) -> Dict[int, Tuple[int, int]]: | ||||||
|  |         return { | ||||||
|  |             board[0][0]: (0, 0), | ||||||
|  |             board[0][1]: (0, 1), | ||||||
|  |             board[0][2]: (0, 2), | ||||||
|  |             board[1][0]: (1, 0), | ||||||
|  |             board[1][1]: (1, 1), | ||||||
|  |             board[1][2]: (1, 2), | ||||||
|  |             board[2][0]: (2, 0), | ||||||
|  |             board[2][1]: (2, 1), | ||||||
|  |             board[2][2]: (2, 2), | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     def determine_game_over(self, players: List[str]) -> str: | ||||||
|  |         if self.won(self.current_board): | ||||||
|  |             return 'current turn' | ||||||
|  |         return '' | ||||||
|  | 
 | ||||||
|  |     def won(self, board: Any) -> bool: | ||||||
|  |         for i in range(3): | ||||||
|  |             for j in range(3): | ||||||
|  |                 if (board[i][j] != self.final_board[i][j]): | ||||||
|  |                     return False | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     def validate_move(self, tile: int) -> bool: | ||||||
|  |         if tile < 1 or tile > 8: | ||||||
|  |             return False | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     def update_board(self, board): | ||||||
|  |         self.current_board = copy.deepcopy(board) | ||||||
|  | 
 | ||||||
|  |     def make_move(self, move: str, player_number: int, computer_move: bool=False) -> Any: | ||||||
|  |         board = self.current_board | ||||||
|  |         move = move.strip() | ||||||
|  |         move = move.split(' ') | ||||||
|  | 
 | ||||||
|  |         if '' in move: | ||||||
|  |             raise BadMoveException('You should enter space separated digits.') | ||||||
|  |         moves = len(move) | ||||||
|  |         for m in range(1, moves): | ||||||
|  |             tile = int(move[m]) | ||||||
|  |             coordinates = self.get_coordinates(board) | ||||||
|  |             if tile not in coordinates: | ||||||
|  |                 raise BadMoveException('You can only move tiles which exist in the board.') | ||||||
|  |             i, j = coordinates[tile] | ||||||
|  |             if (j-1) > -1 and board[i][j-1] == 0: | ||||||
|  |                 board[i][j-1] = tile | ||||||
|  |                 board[i][j] = 0 | ||||||
|  |             elif (i-1) > -1 and board[i-1][j] == 0: | ||||||
|  |                 board[i-1][j] = tile | ||||||
|  |                 board[i][j] = 0 | ||||||
|  |             elif (j+1) < 3 and board[i][j+1] == 0: | ||||||
|  |                 board[i][j+1] = tile | ||||||
|  |                 board[i][j] = 0 | ||||||
|  |             elif (i+1) < 3 and board[i+1][j] == 0: | ||||||
|  |                 board[i+1][j] = tile | ||||||
|  |                 board[i][j] = 0 | ||||||
|  |             else: | ||||||
|  |                 raise BadMoveException('You can only move tiles which are adjacent to :grey_question:.') | ||||||
|  |             if m == moves - 1: | ||||||
|  |                 return board | ||||||
|  | 
 | ||||||
|  | class GameOfFifteenMessageHandler(object): | ||||||
|  | 
 | ||||||
|  |     tiles = { | ||||||
|  |         '0': ':grey_question:', | ||||||
|  |         '1': ':one:', | ||||||
|  |         '2': ':two:', | ||||||
|  |         '3': ':three:', | ||||||
|  |         '4': ':four:', | ||||||
|  |         '5': ':five:', | ||||||
|  |         '6': ':six:', | ||||||
|  |         '7': ':seven:', | ||||||
|  |         '8': ':eight:', | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     def parse_board(self, board: Any) -> str: | ||||||
|  |         # Header for the top of the board | ||||||
|  |         board_str = '' | ||||||
|  | 
 | ||||||
|  |         for row in range(3): | ||||||
|  |             board_str += '\n\n' | ||||||
|  |             for column in range(3): | ||||||
|  |                 board_str += self.tiles[str(board[row][column])] | ||||||
|  |         return board_str | ||||||
|  | 
 | ||||||
|  |     def alert_move_message(self, original_player: str, move_info: str) -> str: | ||||||
|  |         tile = move_info.replace('move ', '') | ||||||
|  |         return original_player + ' moved ' + tile | ||||||
|  | 
 | ||||||
|  |     def game_start_message(self) -> str: | ||||||
|  |         return ("Welcome to Game of Fifteen!" | ||||||
|  |                 "To make a move, type @-mention `move <tile1> <tile2> ...`") | ||||||
|  | 
 | ||||||
|  | class GameOfFifteenBotHandler(GameAdapter): | ||||||
|  |     ''' | ||||||
|  |     Bot that uses the Game Adapter class | ||||||
|  |     to allow users to play Game of Fifteen | ||||||
|  |     ''' | ||||||
|  | 
 | ||||||
|  |     def __init__(self) -> None: | ||||||
|  |         game_name = 'Game of Fifteen' | ||||||
|  |         bot_name = 'Game of Fifteen' | ||||||
|  |         move_help_message = '* To make your move during a game, type\n' \ | ||||||
|  |                             '```move <tile1> <tile2> ...```' | ||||||
|  |         move_regex = 'move [\d{1}\s]+$' | ||||||
|  |         model = GameOfFifteenModel | ||||||
|  |         gameMessageHandler = GameOfFifteenMessageHandler | ||||||
|  |         rules = '''Arrange the board’s tiles from smallest to largest, left to right, | ||||||
|  |                   top to bottom, and tiles adjacent to :grey_question: can only be moved. | ||||||
|  |                   Final configuration will have :grey_question: in top left.''' | ||||||
|  | 
 | ||||||
|  |         super(GameOfFifteenBotHandler, self).__init__( | ||||||
|  |             game_name, | ||||||
|  |             bot_name, | ||||||
|  |             move_help_message, | ||||||
|  |             move_regex, | ||||||
|  |             model, | ||||||
|  |             gameMessageHandler, | ||||||
|  |             rules, | ||||||
|  |             min_players=1, | ||||||
|  |             max_players=1, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | handler_class = GameOfFifteenBotHandler | ||||||
							
								
								
									
										202
									
								
								zulip_bots/zulip_bots/bots/gameoffifteen/test_game_of_fifteen.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								zulip_bots/zulip_bots/bots/gameoffifteen/test_game_of_fifteen.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,202 @@ | ||||||
|  | from zulip_bots.test_lib import BotTestCase | ||||||
|  | 
 | ||||||
|  | from contextlib import contextmanager | ||||||
|  | from unittest.mock import MagicMock | ||||||
|  | from zulip_bots.bots.gameoffifteen.gameoffifteen import * | ||||||
|  | from zulip_bots.game_handler import BadMoveException | ||||||
|  | from typing import Dict, Any, List | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestGameOfFifteenBot(BotTestCase): | ||||||
|  |     bot_name = 'gameoffifteen' | ||||||
|  | 
 | ||||||
|  |     def make_request_message( | ||||||
|  |         self, | ||||||
|  |         content: str, | ||||||
|  |         user: str='foo@example.com', | ||||||
|  |         user_name: str='foo' | ||||||
|  |     ) -> Dict[str, str]: | ||||||
|  |         message = dict( | ||||||
|  |             sender_email=user, | ||||||
|  |             content=content, | ||||||
|  |             sender_full_name=user_name | ||||||
|  |         ) | ||||||
|  |         return message | ||||||
|  | 
 | ||||||
|  |     # Function that serves similar purpose to BotTestCase.verify_dialog, but allows for multiple responses to be handled | ||||||
|  |     def verify_response(self, request: str, expected_response: str, response_number: int, user: str='foo@example.com') -> None: | ||||||
|  |         ''' | ||||||
|  |         This function serves a similar purpose | ||||||
|  |         to BotTestCase.verify_dialog, but allows | ||||||
|  |         for multiple responses to be validated, | ||||||
|  |         and for mocking of the bot's internal data | ||||||
|  |         ''' | ||||||
|  | 
 | ||||||
|  |         bot, bot_handler = self._get_handlers() | ||||||
|  |         message = self.make_request_message(request, user) | ||||||
|  |         bot_handler.reset_transcript() | ||||||
|  | 
 | ||||||
|  |         bot.handle_message(message, bot_handler) | ||||||
|  | 
 | ||||||
|  |         responses = [ | ||||||
|  |             message | ||||||
|  |             for (method, message) | ||||||
|  |             in bot_handler.transcript | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  |         first_response = responses[response_number] | ||||||
|  |         self.assertEqual(expected_response, first_response['content']) | ||||||
|  | 
 | ||||||
|  |     def help_message(self) -> str: | ||||||
|  |         return '''** Game of Fifteen Bot Help:** | ||||||
|  | *Preface all commands with @**test-bot*** | ||||||
|  | * To start a game in a stream, type | ||||||
|  | `start game` | ||||||
|  | * To quit a game at any time, type | ||||||
|  | `quit` | ||||||
|  | * To see rules of this game, type | ||||||
|  | `rules` | ||||||
|  | * To make your move during a game, type | ||||||
|  | ```move <tile1> <tile2> ...```''' | ||||||
|  | 
 | ||||||
|  |     def test_static_responses(self) -> None: | ||||||
|  |         self.verify_response('help', self.help_message(), 0) | ||||||
|  | 
 | ||||||
|  |     def test_game_message_handler_responses(self) -> None: | ||||||
|  |         board = '\n\n:grey_question::one::two:\n\n:three::four::five:\n\n:six::seven::eight:' | ||||||
|  |         bot, bot_handler = self._get_handlers() | ||||||
|  |         self.assertEqual(bot.gameMessageHandler.parse_board( | ||||||
|  |             self.winning_board), board) | ||||||
|  |         self.assertEqual(bot.gameMessageHandler.alert_move_message( | ||||||
|  |             'foo', 'move 1'), 'foo moved 1') | ||||||
|  |         self.assertEqual(bot.gameMessageHandler.game_start_message( | ||||||
|  |         ), "Welcome to Game of Fifteen!" | ||||||
|  |            "To make a move, type @-mention `move <tile1> <tile2> ...`") | ||||||
|  | 
 | ||||||
|  |     winning_board = [[0, 1, 2], | ||||||
|  |                      [3, 4, 5], | ||||||
|  |                      [6, 7, 8]] | ||||||
|  | 
 | ||||||
|  |     def test_game_of_fifteen_logic(self) -> None: | ||||||
|  |         def confirmAvailableMoves( | ||||||
|  |             good_moves: List[int], | ||||||
|  |             bad_moves: List[int], | ||||||
|  |             board: List[List[int]] | ||||||
|  |         ) -> None: | ||||||
|  |             gameOfFifteenModel.update_board(board) | ||||||
|  |             for move in good_moves: | ||||||
|  |                 self.assertTrue(gameOfFifteenModel.validate_move(move)) | ||||||
|  | 
 | ||||||
|  |             for move in bad_moves: | ||||||
|  |                 self.assertFalse(gameOfFifteenModel.validate_move(move)) | ||||||
|  | 
 | ||||||
|  |         def confirmMove( | ||||||
|  |             tile: str, | ||||||
|  |             token_number: int, | ||||||
|  |             initial_board: List[List[int]], | ||||||
|  |             final_board: List[List[int]] | ||||||
|  |         ) -> None: | ||||||
|  |             gameOfFifteenModel.update_board(initial_board) | ||||||
|  |             test_board = gameOfFifteenModel.make_move( | ||||||
|  |                 'move ' + tile, token_number) | ||||||
|  | 
 | ||||||
|  |             self.assertEqual(test_board, final_board) | ||||||
|  | 
 | ||||||
|  |         def confirmGameOver(board: List[List[int]], result: str) -> None: | ||||||
|  |             gameOfFifteenModel.update_board(board) | ||||||
|  |             game_over = gameOfFifteenModel.determine_game_over( | ||||||
|  |                 ['first_player']) | ||||||
|  | 
 | ||||||
|  |             self.assertEqual(game_over, result) | ||||||
|  | 
 | ||||||
|  |         def confirm_coordinates(board: List[List[int]], result: Dict[int, Tuple[int, int]]) -> None: | ||||||
|  |             gameOfFifteenModel.update_board(board) | ||||||
|  |             coordinates = gameOfFifteenModel.get_coordinates(board) | ||||||
|  |             self.assertEqual(coordinates, result) | ||||||
|  | 
 | ||||||
|  |         gameOfFifteenModel = GameOfFifteenModel() | ||||||
|  | 
 | ||||||
|  |         # Basic Board setups | ||||||
|  |         initial_board = [[8, 7, 6], | ||||||
|  |                          [5, 4, 3], | ||||||
|  |                          [2, 1, 0]] | ||||||
|  | 
 | ||||||
|  |         sample_board = [[7, 6, 8], | ||||||
|  |                         [3, 0, 1], | ||||||
|  |                         [2, 4, 5]] | ||||||
|  | 
 | ||||||
|  |         winning_board = [[0, 1, 2], | ||||||
|  |                          [3, 4, 5], | ||||||
|  |                          [6, 7, 8]] | ||||||
|  | 
 | ||||||
|  |         # Test Move Validation Logic | ||||||
|  |         confirmAvailableMoves([1, 2, 3, 4, 5, 6, 7, 8], [0, 9, -1], initial_board) | ||||||
|  | 
 | ||||||
|  |         # Test Move Logic | ||||||
|  |         confirmMove('1', 0, initial_board, | ||||||
|  |                     [[8, 7, 6], | ||||||
|  |                      [5, 4, 3], | ||||||
|  |                      [2, 0, 1]]) | ||||||
|  | 
 | ||||||
|  |         confirmMove('1 2', 0, initial_board, | ||||||
|  |                     [[8, 7, 6], | ||||||
|  |                      [5, 4, 3], | ||||||
|  |                      [0, 2, 1]]) | ||||||
|  | 
 | ||||||
|  |         confirmMove('1 2 5', 0, initial_board, | ||||||
|  |                     [[8, 7, 6], | ||||||
|  |                      [0, 4, 3], | ||||||
|  |                      [5, 2, 1]]) | ||||||
|  | 
 | ||||||
|  |         confirmMove('1 2 5 4', 0, initial_board, | ||||||
|  |                     [[8, 7, 6], | ||||||
|  |                      [4, 0, 3], | ||||||
|  |                      [5, 2, 1]]) | ||||||
|  | 
 | ||||||
|  |         confirmMove('3', 0, sample_board, | ||||||
|  |                     [[7, 6, 8], | ||||||
|  |                      [0, 3, 1], | ||||||
|  |                      [2, 4, 5]]) | ||||||
|  | 
 | ||||||
|  |         confirmMove('3 7', 0, sample_board, | ||||||
|  |                     [[0, 6, 8], | ||||||
|  |                      [7, 3, 1], | ||||||
|  |                      [2, 4, 5]]) | ||||||
|  | 
 | ||||||
|  |         # Test coordinates logic: | ||||||
|  |         confirm_coordinates(initial_board, {8: (0, 0), | ||||||
|  |                                             7: (0, 1), | ||||||
|  |                                             6: (0, 2), | ||||||
|  |                                             5: (1, 0), | ||||||
|  |                                             4: (1, 1), | ||||||
|  |                                             3: (1, 2), | ||||||
|  |                                             2: (2, 0), | ||||||
|  |                                             1: (2, 1), | ||||||
|  |                                             0: (2, 2)}) | ||||||
|  | 
 | ||||||
|  |         # Test Game Over Logic: | ||||||
|  |         confirmGameOver(winning_board, 'current turn') | ||||||
|  |         confirmGameOver(sample_board, '') | ||||||
|  | 
 | ||||||
|  |     def test_invalid_moves(self) -> None: | ||||||
|  |         model = GameOfFifteenModel() | ||||||
|  |         move1 = 'move 2' | ||||||
|  |         move2 = 'move 5' | ||||||
|  |         move3 = 'move 23' | ||||||
|  |         move4 = 'move 0' | ||||||
|  |         move5 = 'move  1  2' | ||||||
|  |         initial_board = [[8, 7, 6], | ||||||
|  |                          [5, 4, 3], | ||||||
|  |                          [2, 1, 0]] | ||||||
|  | 
 | ||||||
|  |         model.update_board(initial_board) | ||||||
|  |         with self.assertRaises(BadMoveException): | ||||||
|  |             model.make_move(move1, player_number=0) | ||||||
|  |         with self.assertRaises(BadMoveException): | ||||||
|  |             model.make_move(move2, player_number=0) | ||||||
|  |         with self.assertRaises(BadMoveException): | ||||||
|  |             model.make_move(move3, player_number=0) | ||||||
|  |         with self.assertRaises(BadMoveException): | ||||||
|  |             model.make_move(move4, player_number=0) | ||||||
|  |         with self.assertRaises(BadMoveException): | ||||||
|  |             model.make_move(move5, player_number=0) | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Tarun Kumar
						Tarun Kumar