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