diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/database.py b/zulip_bots/zulip_bots/bots/merels/libraries/database.py index 5fdb317..c3e96e4 100644 --- a/zulip_bots/zulip_bots/bots/merels/libraries/database.py +++ b/zulip_bots/zulip_bots/bots/merels/libraries/database.py @@ -14,7 +14,7 @@ from .constants import EMPTY_BOARD class MerelsStorage(): - def __init__(self, storage): + def __init__(self, topic_name, storage): """Instantiate storage field. The current database has this form: @@ -30,29 +30,6 @@ class MerelsStorage(): """ self.storage = storage - def create_new_game(self, topic_name): - """ Creates a new merels game if it doesn't exists yet. - - :param topic_name: Name of the topic - :return: True, if the game is successfully created, False if otherwise. - """ - - parameters = ("X", 0, 0, EMPTY_BOARD, "", 0) - - # Checks whether the game exists yet - # If it exists - - try: - if not self.storage.contains(topic_name) or self.storage.get( - topic_name) == "": - self.storage.put(topic_name, json.dumps(parameters)) - return True - else: - return False - except KeyError: - self.storage.put(topic_name, json.dumps(parameters)) - return True - def update_game(self, topic_name, turn, x_taken, o_taken, board, hill_uid, take_mode): """ Updates the current status of the game to the database. diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/game.py b/zulip_bots/zulip_bots/bots/merels/libraries/game.py index 76a01fd..ccd6f43 100644 --- a/zulip_bots/zulip_bots/bots/merels/libraries/game.py +++ b/zulip_bots/zulip_bots/bots/merels/libraries/game.py @@ -7,13 +7,13 @@ freely import another modules. import re +from zulip_bots.game_handler import BadMoveException + from . import database from . import mechanics - COMMAND_PATTERN = re.compile( "^(\\w*).*(\\d,\\d).*(\\d,\\d)|^(\\w+).*(\\d,\\d)") - def getInfo(): """ Gets the info on starting the game @@ -22,7 +22,6 @@ def getInfo(): return "To start a game, mention me and add `create`. A game will start " \ "in that topic. " - def getHelp(): """ Gets the help message @@ -30,8 +29,6 @@ def getHelp(): """ return """Commands: -create: Create a new game if it doesn't exist -reset: Reset a current game put (v,h): Put a man into the grid in phase 1 move (v,h) -> (v,h): Moves a man from one point to -> another point take (v,h): Take an opponent's man from the grid in phase 2/3 @@ -39,125 +36,111 @@ take (v,h): Take an opponent's man from the grid in phase 2/3 v: vertical position of grid h: horizontal position of grid""" - def unknown_command(): """Returns an unknown command info :return: A string containing info about available commands """ - return "Unknown command. Available commands: create, reset, help, " \ - "put (v,h), take (v,h), move (v,h) -> (v,h)" - + message = "Unknown command. Available commands: " \ + "put (v,h), take (v,h), move (v,h) -> (v,h)" + raise BadMoveException(message) +# def beat(message, topic_name, merels_storage): def beat(message, topic_name, merels_storage): """ This gets triggered every time a user send a message in any topic - :param message: User's message :param topic_name: User's current topic :param merels_storage: Merels' storage - :return: A response string to reply to that topic, if any. If not, it - returns an empty string + :return: a tuple of response string and message, non-empty string + we want to keep the turn of the same played, + an empty string otherwise. """ - - merels = database.MerelsStorage(merels_storage) - - if "create" in message.lower(): - return mechanics.create_room(topic_name, merels_storage) - - if "help" in message.lower(): - return getHelp() - - if "reset" in message.lower(): - if merels.get_game_data(topic_name) is not None: - return mechanics.reset_game(topic_name, merels_storage) - else: - return "No game created yet." - + merels = database.MerelsStorage(topic_name, merels_storage) match = COMMAND_PATTERN.match(message) + same_player_move = "" # message indicating move of the same player if match is None: return unknown_command() + if match.group(1) is not None and match.group( + 2) is not None and match.group(3) is not None: - # Matches when user types the command in format of: "command v,h -> v, - # h" or something similar that has three arguments + responses = "" + command = match.group(1) - if merels.get_game_data(topic_name) is not None: - if match.group(1) is not None and match.group( - 2) is not None and match.group(3) is not None: + if command.lower() == "move": + + p1 = [int(x) for x in match.group(2).split(",")] + p2 = [int(x) for x in match.group(3).split(",")] + + if mechanics.get_take_status(topic_name, merels_storage) == 1: + + raise BadMoveException("Take is required to proceed." + " Please try again.\n") + + responses += mechanics.move_man(topic_name, p1, p2, + merels_storage) + "\n" + no_moves = after_event_checkup(responses, topic_name, merels_storage) + + mechanics.update_hill_uid(topic_name, merels_storage) + + responses += mechanics.display_game(topic_name, merels_storage) + "\n" + + if no_moves != "": + same_player_move = no_moves + + else: + return unknown_command() + + if mechanics.get_take_status(topic_name, merels_storage) == 1: + same_player_move = "Take is required to proceed.\n" + return responses, same_player_move + + elif match.group(4) is not None and match.group(5) is not None: + command = match.group(4) + p1 = [int(x) for x in match.group(5).split(",")] + + # put 1,2 + if command == "put": responses = "" - command = match.group(1) - if command.lower() == "move": - p1 = [int(x) for x in match.group(2).split(",")] - p2 = [int(x) for x in match.group(3).split(",")] + if mechanics.get_take_status(topic_name, merels_storage) == 1: + raise BadMoveException("Take is required to proceed." + " Please try again.\n") + responses += mechanics.put_man(topic_name, p1[0], p1[1], + merels_storage) + "\n" + no_moves = after_event_checkup(responses, topic_name, merels_storage) - # Note that it doesn't have to be "move 1,1 -> 1,2". - # It can also just be "move 1,1 1,2" - if mechanics.get_take_status(topic_name, merels_storage) == 1: - responses += "Take is required to proceed. Please try " \ - "again.\n" - else: - responses += mechanics.move_man(topic_name, p1, p2, - merels_storage) + "\n" - responses += after_event_checkup(responses, topic_name, - merels_storage) + mechanics.update_hill_uid(topic_name, merels_storage) - mechanics.update_hill_uid(topic_name, merels_storage) - else: - responses += unknown_command() + responses += mechanics.display_game(topic_name, merels_storage) + "\n" - responses += mechanics.display_game(topic_name, + if no_moves != "": + same_player_move = no_moves + if mechanics.get_take_status(topic_name, merels_storage) == 1: + same_player_move = "Take is required to proceed.\n" + return responses, same_player_move + # take 5,3 + elif command == "take": + responses = "" + if mechanics.get_take_status(topic_name, merels_storage) == 1: + responses += mechanics.take_man(topic_name, p1[0], p1[1], merels_storage) + "\n" - return responses - elif match.group(4) is not None and match.group(5) is not None: - command = match.group(4) - p1 = [int(x) for x in match.group(5).split(",")] - - # put 1,2 - if command == "put": - responses = "" - - if mechanics.get_take_status(topic_name, merels_storage) == 1: - responses += "Take is required to proceed. Please try " \ - "again.\n" - else: - responses += mechanics.put_man(topic_name, p1[0], p1[1], - merels_storage) + "\n" - responses += after_event_checkup(responses, topic_name, - merels_storage) + if "Failed" in responses: + raise BadMoveException(responses) + mechanics.update_toggle_take_mode(topic_name, merels_storage) + no_moves = after_event_checkup(responses, topic_name, merels_storage) mechanics.update_hill_uid(topic_name, merels_storage) - responses += mechanics.display_game(topic_name, - merels_storage) + "\n" - return responses - # take 5,3 - elif command == "take": - responses = "" - if mechanics.get_take_status(topic_name, merels_storage) == 1: - responses += mechanics.take_man(topic_name, p1[0], p1[1], - merels_storage) + "\n" - if not ("Failed" in responses): - mechanics.update_toggle_take_mode(topic_name, - merels_storage) - mechanics.update_change_turn(topic_name, - merels_storage) - mechanics.update_hill_uid(topic_name, merels_storage) - responses += check_win(topic_name, merels_storage) - if not ("win" in responses.lower()): - responses += mechanics.display_game(topic_name, - merels_storage) \ - + "\n" + responses += mechanics.display_game(topic_name, merels_storage) + "\n" - return responses - else: - return "Taking is not possible." + if no_moves != "": + same_player_move = no_moves + return responses, same_player_move else: - return unknown_command() - else: - return "No game created yet. You cannot do any of the game commands." \ - " Create the game first." - + raise BadMoveException("Taking is not possible.") + else: + return unknown_command() def check_take_mode(response, topic_name, merels_storage): """This checks whether the previous action can result in a take mode for @@ -175,7 +158,6 @@ def check_take_mode(response, topic_name, merels_storage): else: mechanics.update_change_turn(topic_name, merels_storage) - def check_any_moves(topic_name, merels_storage): """Check whether the player can make any moves, if can't switch to another player @@ -191,7 +173,6 @@ def check_any_moves(topic_name, merels_storage): return "" - def after_event_checkup(response, topic_name, merels_storage): """After doing certain moves in the game, it will check for take mode availability and check for any possible moves @@ -205,7 +186,6 @@ def after_event_checkup(response, topic_name, merels_storage): check_take_mode(response, topic_name, merels_storage) return check_any_moves(topic_name, merels_storage) - def check_win(topic_name, merels_storage): """Checks whether the current grid has a winner, if it does, finish the game and remove it from the database @@ -214,7 +194,7 @@ def check_win(topic_name, merels_storage): :param merels_storage: Merels' storage :return: """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) win = mechanics.who_won(topic_name, merels_storage) if win != "None": diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/game_data.py b/zulip_bots/zulip_bots/bots/merels/libraries/game_data.py index 0e5ffa6..4a1dbe9 100644 --- a/zulip_bots/zulip_bots/bots/merels/libraries/game_data.py +++ b/zulip_bots/zulip_bots/bots/merels/libraries/game_data.py @@ -11,7 +11,7 @@ from .interface import construct_grid class GameData(): def __init__(self, game_data=( - 'test', 'X', 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', '', 0)): + 'merels', 'X', 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', '', 0)): self.topic_name = game_data[0] self.turn = game_data[1] self.x_taken = game_data[2] @@ -41,7 +41,7 @@ class GameData(): """ return construct_grid(self.board) - def get_x_piece_possesed_not_on_grid(self): + def get_x_piece_possessed_not_on_grid(self): """Gets the amount of X pieces that the player X still have, but not put yet on the grid @@ -49,7 +49,7 @@ class GameData(): """ return 9 - self.x_taken - mechanics.get_piece("X", self.grid()) - def get_o_piece_possesed_not_on_grid(self): + def get_o_piece_possessed_not_on_grid(self): """Gets the amount of X pieces that the player O still have, but not put yet on the grid @@ -64,8 +64,8 @@ class GameData(): :return: A phase number (1, 2, or 3) """ return mechanics.get_phase_number(self.grid(), self.turn, - self.get_x_piece_possesed_not_on_grid(), - self.get_o_piece_possesed_not_on_grid()) + self.get_x_piece_possessed_not_on_grid(), + self.get_o_piece_possessed_not_on_grid()) def switch_turn(self): """Switches turn between X and O diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/mechanics.py b/zulip_bots/zulip_bots/bots/merels/libraries/mechanics.py index 7339311..48b152d 100644 --- a/zulip_bots/zulip_bots/bots/merels/libraries/mechanics.py +++ b/zulip_bots/zulip_bots/bots/merels/libraries/mechanics.py @@ -10,7 +10,7 @@ from . import constants from . import database from . import game_data from . import interface - +from zulip_bots.game_handler import BadMoveException def is_in_grid(vertical_pos, horizontal_pos): """Checks whether the cell actually exists or not @@ -228,7 +228,7 @@ def who_won(topic_name, merels_storage): is winning """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) data = game_data.GameData(merels.get_game_data(topic_name)) if data.get_phase() > 1: @@ -275,7 +275,7 @@ def create_room(topic_name, merels_storage): :param merels_storage: Merels' storage :return: A response string """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) if merels.create_new_game(topic_name): response = "" @@ -297,7 +297,7 @@ def display_game(topic_name, merels_storage): :param merels_storage: Merels' storage :return: A response string """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) data = game_data.GameData(merels.get_game_data(topic_name)) @@ -309,9 +309,9 @@ def display_game(topic_name, merels_storage): take = "No" response += interface.graph_grid(data.grid()) + "\n" - response += """Phase {}, {}'s turn. Take mode: {}. + response += """Phase {}. Take mode: {}. X taken: {}, O taken: {}. - """.format(data.get_phase(), data.turn, take, data.x_taken, data.o_taken) + """.format(data.get_phase(), take, data.x_taken, data.o_taken) return response @@ -323,7 +323,7 @@ def reset_game(topic_name, merels_storage): :param merels_storage: Merels' storage :return: A response string """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) merels.remove_game(topic_name) return "Game removed.\n" + create_room(topic_name, @@ -339,7 +339,7 @@ def move_man(topic_name, p1, p2, merels_storage): :param merels_storage: Merels' storage :return: A response string """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) data = game_data.GameData(merels.get_game_data(topic_name)) # Get the grid @@ -361,8 +361,7 @@ def move_man(topic_name, p1, p2, merels_storage): return "Moved a man from ({0}, {1}) -> ({2}, {3}) for {4}.".format( p1[0], p1[1], p2[0], p2[1], data.turn) else: - return "Failed: That's not a legal move. Please try again." - + raise BadMoveException("Failed: That's not a legal move. Please try again.") def put_man(topic_name, v, h, merels_storage): """Puts a man into the specified cell in topic_name @@ -373,7 +372,7 @@ def put_man(topic_name, v, h, merels_storage): :param merels_storage: MerelsDatabase object :return: A response string """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) data = game_data.GameData(merels.get_game_data(topic_name)) # Get the grid @@ -393,7 +392,7 @@ def put_man(topic_name, v, h, merels_storage): data.take_mode) return "Put a man to ({0}, {1}) for {2}.".format(v, h, data.turn) else: - return "Failed: That's not a legal put. Please try again." + raise BadMoveException("Failed: That's not a legal put. Please try again.") def take_man(topic_name, v, h, merels_storage): @@ -405,7 +404,7 @@ def take_man(topic_name, v, h, merels_storage): :param merels_storage: Merels' storage :return: A response string """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) data = game_data.GameData(merels.get_game_data(topic_name)) # Get the grid @@ -431,7 +430,7 @@ def take_man(topic_name, v, h, merels_storage): data.take_mode) return "Taken a man from ({0}, {1}) for {2}.".format(v, h, data.turn) else: - return "Failed: That's not a legal take. Please try again." + raise BadMoveException("Failed: That's not a legal take. Please try again.") def update_hill_uid(topic_name, merels_storage): @@ -442,7 +441,7 @@ def update_hill_uid(topic_name, merels_storage): :return: None """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) data = game_data.GameData(merels.get_game_data(topic_name)) data.hill_uid = get_hills_numbers(data.grid()) @@ -460,7 +459,7 @@ def update_change_turn(topic_name, merels_storage): :return: None """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) data = game_data.GameData(merels.get_game_data(topic_name)) data.switch_turn() @@ -478,7 +477,7 @@ def update_toggle_take_mode(topic_name, merels_storage): :return: None """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) data = game_data.GameData(merels.get_game_data(topic_name)) data.toggle_take_mode() @@ -496,7 +495,7 @@ def get_take_status(topic_name, merels_storage): :return: 1 or 0 """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) data = game_data.GameData(merels.get_game_data(topic_name)) return data.take_mode @@ -528,7 +527,7 @@ def can_take_mode(topic_name, merels_storage): :return: True if this turn can trigger take mode, False if otherwise """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) data = game_data.GameData(merels.get_game_data(topic_name)) current_hill_uid = data.hill_uid @@ -570,7 +569,7 @@ def can_make_any_move(topic_name, merels_storage): :return: True if the player has a way, False if there isn't """ - merels = database.MerelsStorage(merels_storage) + merels = database.MerelsStorage(topic_name, merels_storage) data = game_data.GameData(merels.get_game_data(topic_name)) if data.get_phase() != 1: diff --git a/zulip_bots/zulip_bots/bots/merels/merels.py b/zulip_bots/zulip_bots/bots/merels/merels.py index 927a82f..4928aa0 100644 --- a/zulip_bots/zulip_bots/bots/merels/merels.py +++ b/zulip_bots/zulip_bots/bots/merels/merels.py @@ -1,42 +1,102 @@ -from zulip_bots.bots.merels.libraries import game +from typing import List, Any +from zulip_bots.bots.merels.libraries import ( + game, + mechanics, + database, + game_data +) +from zulip_bots.game_handler import GameAdapter, SamePlayerMove +class Storage(object): + data = {} -class MerelsBot(object): - """ - Simulate the merels game to the chat - """ + def __init__(self, topic_name): + self.data[topic_name] = '["X", 0, 0, "NNNNNNNNNNNNNNNNNNNNNNNN", "", 0]' - def __init__(self): - pass + def put(self, topic_name, value: str): + self.data[topic_name] = value - def usage(self): + def get(self, topic_name): + return self.data[topic_name] + +class MerelsModel(object): + + def __init__(self, board: Any=None) -> None: + self.topic = "merels" + self.storage = Storage(self.topic) + self.current_board = mechanics.display_game(self.topic, self.storage) + self.token = ['O', 'X'] + + def determine_game_over(self, players: List[str]) -> str: + if self.contains_winning_move(self.current_board): + return 'current turn' + return '' + + def contains_winning_move(self, board: Any) -> bool: + merels = database.MerelsStorage(self.topic, self.storage) + data = game_data.GameData(merels.get_game_data(self.topic)) + + if data.get_phase() > 1: + if (mechanics.get_piece("X", data.grid()) <= 2) or\ + (mechanics.get_piece("O", data.grid()) <= 2): + return True + return False + + def make_move(self, move: str, player_number: int, computer_move: bool=False) -> Any: + if self.storage.get(self.topic) == '["X", 0, 0, "NNNNNNNNNNNNNNNNNNNNNNNN", "", 0]': + self.storage.put( + self.topic, + '["{}", 0, 0, "NNNNNNNNNNNNNNNNNNNNNNNN", "", 0]'.format( + self.token[player_number] + )) + self.current_board, same_player_move = game.beat(move, self.topic, self.storage) + if same_player_move != "": + raise SamePlayerMove(same_player_move) + return self.current_board + +class MerelsMessageHandler(object): + tokens = [':o_button:', ':cross_mark_button:'] + + def parse_board(self, board: Any) -> str: + return board + + def get_player_color(self, turn: int) -> str: + return self.tokens[turn] + + def alert_move_message(self, original_player: str, move_info: str) -> str: + return original_player + " :" + + def game_start_message(self) -> str: + return game.getHelp() + +class MerelsHandler(GameAdapter): + ''' + You can play merels! Make sure your message starts with + "@mention-bot". + ''' + META = { + 'name': 'merels', + 'description': 'Lets you play merels against any player.', + } + + def usage(self) -> str: return game.getInfo() - def handle_message(self, message, bot_handler): - room_name = self.compose_room_name(message) - content = message['content'] + def __init__(self) -> None: + game_name = 'Merels' + bot_name = 'merels' + move_help_message = "" + move_regex = '.*' + model = MerelsModel + gameMessageHandler = MerelsMessageHandler + super(MerelsHandler, self).__init__( + game_name, + bot_name, + move_help_message, + move_regex, + model, + gameMessageHandler, + supports_computer=False + ) - response = game.beat(content, room_name, bot_handler.storage) - - bot_handler.send_reply(message, response) - - def compose_room_name(self, message): - room_name = "test" - if "type" in message: - if message['type'] == "stream": - if 'subject' in message: - realm = message['sender_realm_str'] - stream = message['display_recipient'] - topic = message['subject'] - room_name = "{}-{}-{}".format(realm, stream, topic) - else: - # type == "private" - realm = message['sender_realm_str'] - users_list = [recipient['email'] for recipient in message[ - 'display_recipient']] - users = "-".join(sorted(users_list)) - room_name = "{}-{}".format(realm, users) - return room_name - - -handler_class = MerelsBot +handler_class = MerelsHandler diff --git a/zulip_bots/zulip_bots/bots/merels/test_merels.py b/zulip_bots/zulip_bots/bots/merels/test_merels.py index 7becc9d..b7c5a1e 100644 --- a/zulip_bots/zulip_bots/bots/merels/test_merels.py +++ b/zulip_bots/zulip_bots/bots/merels/test_merels.py @@ -17,61 +17,12 @@ class TestFollowUpBot(zulip_bots.test_lib.BotTestCase): message = dict( content='magic', type='stream', + sender_email="boo@email.com", + sender_full_name="boo" ) res = self.get_response(message) self.assertEqual(res['content'], - 'Unknown command. Available commands: create, ' - 'reset, help, put (v,h), take (v,h), move (v,' - 'h) -> (v,h)') - - def test_help_command(self): - message = dict( - content='help', - type='stream', - ) - - res = self.get_response(message) - - self.assertEqual(res['content'], "Commands:\ncreate: Create a new " - "game if it doesn't exist\nreset: " - "Reset a current game\nput (v," - "h): Put a man into the grid in " - "phase 1\nmove (v,h) -> (v," - "h): Moves a man from one point to " - "-> another point\ntake (v,h): Take " - "an opponent's man from the grid in " - "phase 2/3\n\nv: vertical position " - "of grid\nh: horizontal position of " - "grid") - - def test_create_new_game(self): - message = dict( - content='create', - type='stream', - subject='test' - ) - - with mock.patch.object(zulip_bots.bots.merels.merels.MerelsBot, - 'compose_room_name', - return_value="test"): - res = self.get_response(message) - - self.assertEqual(res['content'], '''A room has been created in test. Starting game now. -` 0 1 2 3 4 5 6 - 0 [ ]---------------[ ]---------------[ ] - | | | - 1 | [ ]---------[ ]---------[ ] | - | | | | | - 2 | | [ ]---[ ]---[ ] | | - | | | | | | - 3 [ ]---[ ]---[ ] [ ]---[ ]---[ ] - | | | | | | - 4 | | [ ]---[ ]---[ ] | | - | | | | | - 5 | [ ]---------[ ]---------[ ] | - | | | - 6 [ ]---------------[ ]---------------[ ]` -Phase 1, X's turn. Take mode: No. -X taken: 0, O taken: 0.\n ''') + 'You are not in a game at the moment.' + ' Type `help` for help.')