Add game of fifteen bot.
This commit is contained in:
parent
b8d4f0b869
commit
8ef9b70191
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…
Reference in a new issue