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