From b80a0cb297178ccb98757e82f617b76148408f08 Mon Sep 17 00:00:00 2001 From: Privisus Date: Fri, 29 Dec 2017 13:39:22 +0700 Subject: [PATCH] interactive bots: Create Merels bot. --- zulip_bots/zulip_bots/bots/merels/.gitignore | 3 + .../bots/merels/libraries/__init__.py | 0 .../bots/merels/libraries/constants.py | 48 ++ .../bots/merels/libraries/database.py | 106 ++++ .../zulip_bots/bots/merels/libraries/game.py | 223 +++++++ .../bots/merels/libraries/game_data.py | 88 +++ .../bots/merels/libraries/interface.py | 104 ++++ .../bots/merels/libraries/mechanics.py | 579 ++++++++++++++++++ zulip_bots/zulip_bots/bots/merels/merels.py | 42 ++ .../zulip_bots/bots/merels/test/__init__.py | 0 .../bots/merels/test/test_constants.py | 50 ++ .../bots/merels/test/test_database.py | 66 ++ .../zulip_bots/bots/merels/test/test_game.py | 117 ++++ .../bots/merels/test/test_interface.py | 78 +++ .../bots/merels/test/test_mechanics.py | 186 ++++++ .../zulip_bots/bots/merels/test_merels.py | 77 +++ 16 files changed, 1767 insertions(+) create mode 100644 zulip_bots/zulip_bots/bots/merels/.gitignore create mode 100644 zulip_bots/zulip_bots/bots/merels/libraries/__init__.py create mode 100644 zulip_bots/zulip_bots/bots/merels/libraries/constants.py create mode 100644 zulip_bots/zulip_bots/bots/merels/libraries/database.py create mode 100644 zulip_bots/zulip_bots/bots/merels/libraries/game.py create mode 100644 zulip_bots/zulip_bots/bots/merels/libraries/game_data.py create mode 100644 zulip_bots/zulip_bots/bots/merels/libraries/interface.py create mode 100644 zulip_bots/zulip_bots/bots/merels/libraries/mechanics.py create mode 100644 zulip_bots/zulip_bots/bots/merels/merels.py create mode 100644 zulip_bots/zulip_bots/bots/merels/test/__init__.py create mode 100644 zulip_bots/zulip_bots/bots/merels/test/test_constants.py create mode 100644 zulip_bots/zulip_bots/bots/merels/test/test_database.py create mode 100644 zulip_bots/zulip_bots/bots/merels/test/test_game.py create mode 100644 zulip_bots/zulip_bots/bots/merels/test/test_interface.py create mode 100644 zulip_bots/zulip_bots/bots/merels/test/test_mechanics.py create mode 100644 zulip_bots/zulip_bots/bots/merels/test_merels.py diff --git a/zulip_bots/zulip_bots/bots/merels/.gitignore b/zulip_bots/zulip_bots/bots/merels/.gitignore new file mode 100644 index 0000000..3053d71 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/.gitignore @@ -0,0 +1,3 @@ +*.db + +.idea/ diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/__init__.py b/zulip_bots/zulip_bots/bots/merels/libraries/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/constants.py b/zulip_bots/zulip_bots/bots/merels/libraries/constants.py new file mode 100644 index 0000000..3936bcf --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/libraries/constants.py @@ -0,0 +1,48 @@ +"""Provide some constants that are crucial for running the game +""" + +# Do NOT scramble these. This is written such that it starts from top left +# to bottom right. +ALLOWED_MOVES = ([0, 0], [0, 3], [0, 6], + [1, 1], [1, 3], [1, 5], + [2, 2], [2, 3], [2, 4], + [3, 0], [3, 1], [3, 2], [3, 4], [3, 5], [3, 6], + [4, 2], [4, 3], [4, 4], + [5, 1], [5, 3], [5, 5], + [6, 0], [6, 3], [6, 6]) + +AM = ALLOWED_MOVES + +# Do NOT scramble these, This is written such that it starts from horizontal +# to vertical, top to bottom, left to right. +HILLS = ([AM[0], AM[1], AM[2]], + [AM[3], AM[4], AM[5]], + [AM[6], AM[7], AM[8]], + [AM[9], AM[10], AM[11]], + [AM[12], AM[13], AM[14]], + [AM[15], AM[16], AM[17]], + [AM[18], AM[19], AM[20]], + [AM[21], AM[22], AM[23]], + [AM[0], AM[9], AM[21]], + [AM[3], AM[10], AM[18]], + [AM[6], AM[11], AM[15]], + [AM[1], AM[4], AM[7]], + [AM[16], AM[19], AM[22]], + [AM[8], AM[12], AM[17]], + [AM[5], AM[13], AM[20]], + [AM[2], AM[14], AM[23]], + ) + +OUTER_SQUARE = ([0, 0], [0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [0, 6], + [1, 0], [2, 0], [3, 0], [4, 0], [5, 0], [6, 0], + [6, 0], [6, 1], [6, 2], [6, 3], [6, 4], [6, 5], [6, 6], + [0, 6], [1, 6], [2, 6], [3, 6], [4, 6], [5, 6]) + +MIDDLE_SQUARE = ([1, 1], [1, 2], [1, 3], [1, 4], [1, 5], + [2, 1], [3, 1], [4, 1], [5, 1], + [5, 1], [5, 2], [5, 3], [5, 4], [5, 5], + [1, 5], [2, 5], [3, 5], [4, 5]) + +INNER_SQUARE = ([2, 2], [2, 3], [2, 4], [3, 2], [3, 4], [4, 2], [4, 3], [4, 4]) + +EMPTY_BOARD = "NNNNNNNNNNNNNNNNNNNNNNNN" diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/database.py b/zulip_bots/zulip_bots/bots/merels/libraries/database.py new file mode 100644 index 0000000..5fdb317 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/libraries/database.py @@ -0,0 +1,106 @@ +"""This module is used for managing, storing, and operating certain +functions to said data. Almost every action of the data is wrapped with the +connection opening and closing. This module is supplied with a default name +for the database. If the user is not satisfied with the name, the user can +change it with their own database name for convenience. + +Essentially, this database is used for storing static matches that hasn't +finished yet so any matches that are finished will be removed. +""" + +import json + +from .constants import EMPTY_BOARD + + +class MerelsStorage(): + def __init__(self, storage): + """Instantiate storage field. + + The current database has this form: + TOPIC_NAME (UNIQUE) + +----> TURN + +----> X_TAKEN + +----> O_TAKEN + +----> BOARD + +----> HILL_UID + +----> TAKE_MODE + + :param name: Name of the storage + """ + 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. + + :param topic_name: The name of the topic + :param turn: "X" or "O" + :param x_taken: How many X's are taken from the board during the + gameplay by O + :param o_taken: How many O's are taken from the board during the + gameplay by X + :param board: A compact representation of the grid + :param hill_uid: Unique hill id + :param take_mode: Whether the game is in take mode, which "turn" has + to take a piece + :return: None + """ + + parameters = ( + turn, x_taken, o_taken, board, hill_uid, take_mode) + + self.storage.put(topic_name, json.dumps(parameters)) + + def remove_game(self, topic_name): + """ Removes the game from the database by setting it to an empty + string. An empty string marks an empty match. + + :param topic_name: The name of the topic + :return: None + """ + + self.storage.put(topic_name, "") + + def get_game_data(self, topic_name): + """Gets the game data + + :param topic_name: The name of the topic + :return: A tuple containing the data + """ + + try: + select = json.loads(self.storage.get(topic_name)) + except (json.decoder.JSONDecodeError, KeyError): + select = "" + + if select == "": + return None + else: + res = (topic_name, select[0], select[1], select[2], select[3], + select[4], select[5]) + + return res diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/game.py b/zulip_bots/zulip_bots/bots/merels/libraries/game.py new file mode 100644 index 0000000..76a01fd --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/libraries/game.py @@ -0,0 +1,223 @@ +"""This is the main component of the game, the core of the game that squishes +everything together and make the game work. Usually user can just import this +module and use the beat() function and everything will be fine, but will there +be any certain things that can't be accomplished that way, the user may also +freely import another modules. +""" + +import re + +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 + + :return: Info on how to start the game + """ + return "To start a game, mention me and add `create`. A game will start " \ + "in that topic. " + + +def getHelp(): + """ Gets the help message + + :return: Help message + """ + + 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 + +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)" + + +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 + """ + + 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." + + match = COMMAND_PATTERN.match(message) + + if match is None: + return unknown_command() + + # Matches when user types the command in format of: "command v,h -> v, + # h" or something similar that has three arguments + + 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: + 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(",")] + + # 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) + else: + responses += unknown_command() + + responses += mechanics.display_game(topic_name, + 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) + + 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" + + return responses + else: + return "Taking is not possible." + else: + return unknown_command() + else: + return "No game created yet. You cannot do any of the game commands." \ + " Create the game first." + + +def check_take_mode(response, topic_name, merels_storage): + """This checks whether the previous action can result in a take mode for + current player. This assumes that the previous action is successful and not + failed. + + :param response: A response string + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: None + """ + if not ("Failed" in response): + if mechanics.can_take_mode(topic_name, merels_storage): + mechanics.update_toggle_take_mode(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 + + :param topic_name: Topic name + :param merels_storage: MerelsDatabase object + :return: A response string + """ + if not mechanics.can_make_any_move(topic_name, merels_storage): + mechanics.update_change_turn(topic_name, merels_storage) + return "Cannot make any move on the grid. Switching to " \ + "previous player.\n" + + 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 + + :param response: Current response string. This is useful for checking + any failed previous commands + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: A response string + """ + 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 + + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: + """ + merels = database.MerelsStorage(merels_storage) + + win = mechanics.who_won(topic_name, merels_storage) + if win != "None": + merels.remove_game(topic_name) + return "{0} wins the game!".format(win) + return "" diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/game_data.py b/zulip_bots/zulip_bots/bots/merels/libraries/game_data.py new file mode 100644 index 0000000..0e5ffa6 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/libraries/game_data.py @@ -0,0 +1,88 @@ +"""This serves as a bridge between the database and the other modules. + +In a nutshell, this module parses a tuple from database then translates it +into a more convenient naming for easier access. It also adds certain +functions that are useful for the function of the game. +""" + +from . import mechanics +from .interface import construct_grid + + +class GameData(): + def __init__(self, game_data=( + 'test', 'X', 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', '', 0)): + self.topic_name = game_data[0] + self.turn = game_data[1] + self.x_taken = game_data[2] + self.o_taken = game_data[3] + self.board = game_data[4] + self.hill_uid = game_data[5] + self.take_mode = game_data[6] + + def __len__(self): + return len(self.construct()) + + def construct(self): + """Constructs a tuple based on existing records + + :return: A tuple containing all the game records + """ + + res = ( + self.topic_name, self.turn, self.x_taken, self.o_taken, self.board, + self.hill_uid, self.take_mode) + return res + + def grid(self): + """Returns the grid + + :return: A 2-dimensional 7x7 list (the grid) + """ + return construct_grid(self.board) + + def get_x_piece_possesed_not_on_grid(self): + """Gets the amount of X pieces that the player X still have, but not + put yet on the grid + + :return: Amount of pieces that X has, but not on grid + """ + return 9 - self.x_taken - mechanics.get_piece("X", self.grid()) + + def get_o_piece_possesed_not_on_grid(self): + """Gets the amount of X pieces that the player O still have, but not + put yet on the grid + + :return: Amount of pieces that O has, but not on grid + """ + + return 9 - self.o_taken - mechanics.get_piece("O", self.grid()) + + def get_phase(self): + """Gets the phase number for the current game + + :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()) + + def switch_turn(self): + """Switches turn between X and O + + :return: None + """ + if self.turn == "X": + self.turn = "O" + else: + self.turn = "X" + + def toggle_take_mode(self): + """Toggles take mode + + :return: None + """ + if self.take_mode == 0: + self.take_mode = 1 + else: + self.take_mode = 0 diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/interface.py b/zulip_bots/zulip_bots/bots/merels/libraries/interface.py new file mode 100644 index 0000000..55a031e --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/libraries/interface.py @@ -0,0 +1,104 @@ +"""Interface helps the game displaying and maintaining the board. This is +where the grid can get translated into a board, which is easier to manage +in the database, and board translated to grid, which is easier to manage in +the mechanics. + +The display heavily depends on the availability of monospaced font. This is +why the graph_grid() is wrapped in `` (it is expected for the user to +provide a Markdown support) +""" + +from . import constants + + +def draw_grid(grid): + """Draws a board from a grid + + :param grid: a 2-dimensional 7x7 list + :return: None + """ + print(graph_grid(grid)) + + +def graph_grid(grid): + """Creates a nice grid display, something like this: + + 0 1 2 3 4 5 6 + 0 [ ]---------------[ ]---------------[ ] + | | | + 1 | [ ]---------[ ]---------[ ] | + | | | | | + 2 | | [ ]---[ ]---[ ] | | + | | | | | | + 3 [ ]---[ ]---[ ] [ ]---[ ]---[ ] + | | | | | | + 4 | | [ ]---[ ]---[ ] | | + | | | | | + 5 | [ ]---------[ ]---------[ ] | + | | | + 6 [ ]---------------[ ]---------------[ ] + + :param grid: a 2-dimensional 7x7 list. + :return: A nicer display of the grid + """ + + return '''` 0 1 2 3 4 5 6 + 0 [{}]---------------[{}]---------------[{}] + | | | + 1 | [{}]---------[{}]---------[{}] | + | | | | | + 2 | | [{}]---[{}]---[{}] | | + | | | | | | + 3 [{}]---[{}]---[{}] [{}]---[{}]---[{}] + | | | | | | + 4 | | [{}]---[{}]---[{}] | | + | | | | | + 5 | [{}]---------[{}]---------[{}] | + | | | + 6 [{}]---------------[{}]---------------[{}]`'''.format( + grid[0][0], grid[0][3], grid[0][6], + grid[1][1], grid[1][3], grid[1][5], + grid[2][2], grid[2][3], grid[2][4], + grid[3][0], grid[3][1], grid[3][2], grid[3][4], grid[3][5], grid[3][6], + grid[4][2], grid[4][3], grid[4][4], + grid[5][1], grid[5][3], grid[5][5], + grid[6][0], grid[6][3], grid[6][6]) + + +def construct_grid(board): + """Constructs the original grid from the database + + :param board: A compact representation of the grid (example: + "NONXONXONXONXONNOXNXNNOX") + + :return: A grid + """ + + grid = [[" " for _ in range(7)] for _ in range(7)] + + for k, cell in enumerate(board): + if cell == "O" or cell == "X": + grid[constants.ALLOWED_MOVES[k][0]][ + constants.ALLOWED_MOVES[k][1]] = cell + + return grid + + +def construct_board(grid): + """Constructs a board from a grid + + :param grid: A 2-dimensional 7x7 list + + :return: A board. Board is a compact representation of the grid + """ + + board = "" + + for cell_location in constants.ALLOWED_MOVES: + cell_content = grid[cell_location[0]][cell_location[1]] + if cell_content == "X" or cell_content == "O": + board += cell_content + else: + board += "N" + + return board diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/mechanics.py b/zulip_bots/zulip_bots/bots/merels/libraries/mechanics.py new file mode 100644 index 0000000..7339311 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/libraries/mechanics.py @@ -0,0 +1,579 @@ +"""Mechanics is what makes everything moves and works. It stores the game +mechanisms as well as some functions for accessing the database. +""" + +from math import sqrt + +from collections import Counter + +from . import constants +from . import database +from . import game_data +from . import interface + + +def is_in_grid(vertical_pos, horizontal_pos): + """Checks whether the cell actually exists or not + + :param vertical_pos: Vertical position of the man, in int + :param horizontal_pos: Horizontal position of the man, in int + + :return: + True, if it exists (meaning: in the grid) + False, if it doesn't exist (meaning: out of grid) + """ + + return [vertical_pos, horizontal_pos] in constants.ALLOWED_MOVES + + +def is_empty(vertical_pos, horizontal_pos, grid): + """Checks whether the current cell is empty + + :param vertical_pos: Vertical position of the man, in int + :param horizontal_pos: Horizontal position of the man, in int + :param grid: A 2-dimensional 7x7 list + + :return: + True, if it is empty + False, if it is not empty + """ + + return grid[vertical_pos][horizontal_pos] == " " + + +def is_jump(vpos_before, hpos_before, vpos_after, hpos_after): + """Checks whether the move is considered jumping + + :param vpos_before: Vertical cell location before jumping + :param hpos_before: Horizontal cell location before jumping + :param vpos_after: Vertical cell location after jumping + :param hpos_after: Horizontal cell location after jumping + + :return: + True, if it is jumping + False, if it is not jumping + """ + + distance = sqrt( + (vpos_after - vpos_before) ** 2 + (hpos_after - hpos_before) ** 2) + + # If the man is in outer square, the distance must be 3 or 1 + if [vpos_before, hpos_before] in constants.OUTER_SQUARE: + return not (distance == 3 or distance == 1) + + # If the man is in middle square, the distance must be 2 or 1 + if [vpos_before, hpos_before] in constants.MIDDLE_SQUARE: + return not (distance == 2 or distance == 1) + + # If the man is in inner square, the distance must be only 1 + if [vpos_before, hpos_before] in constants.INNER_SQUARE: + return not (distance == 1) + + +def get_hills_numbers(grid): + """Checks for hills, if it exists, get its relative position based on + constants.py + + :param grid: A 7x7 2 dimensional grid + + :return: A string, containing the relative position of hills based on + constants.py + """ + + relative_hills = "" + for k, hill in enumerate(constants.HILLS): + v1, h1 = hill[0][0], hill[0][1] + v2, h2 = hill[1][0], hill[1][1] + v3, h3 = hill[2][0], hill[2][1] + if all(x == "O" for x in + (grid[v1][h1], grid[v2][h2], grid[v3][h3])) or all( + x == "X" for x in (grid[v1][h1], grid[v2][h2], grid[v3][h3])): + relative_hills += str(k) + + return relative_hills + + +def move_man_legal(v1, h1, v2, h2, grid): + """Moves a man into a specified cell, assuming it is a legal move + + :param v1: Vertical position of cell + :param h1: Horizontal position of cell + :param v2: Vertical position of cell + :param h2: Horizontal version of cell + :param grid: A 2-dimensional 7x7 list + :return: None, since grid is mutable + """ + + grid[v2][h2] = grid[v1][h1] + grid[v1][h1] = " " + + +def put_man_legal(turn, v, h, grid): + """Puts a man into specified cell, assuming it is a legal move + + :param turn: "X" or "O" + :param v: Vertical position of cell + :param h: Horizontal position of cell + :param grid: A 2-dimensional 7x7 grid + :return: None, since grid is mutable + """ + + grid[v][h] = turn + + +def take_man_legal(v, h, grid): + """Takes an opponent's man from a specified cell. + + :param v: Vertical position of the cell + :param h: Horizontal position of the cell + :param grid: A 2-dimensional 7x7 list + :return: None, since grid is mutable + """ + + grid[v][h] = " " + + +def is_legal_move(v1, h1, v2, h2, turn, phase, grid): + """Determines whether the current move is legal or not + + :param v1: Vertical position of man + :param h1: Horizontal position of man + :param v2: Vertical position of man + :param h2: Horizontal position of man + :param turn: "X" or "O" + :param phase: Current phase of the game + :param grid: A 2-dimensional 7x7 list + :return: True if it is legal, False it is not legal + """ + + if phase == 1: + return False # Place all the pieces first before moving one + + if phase == 3 and get_piece(turn, grid) == 3: + return is_in_grid(v2, h2) and is_empty(v2, h2, grid) and is_own_piece( + v1, h1, turn, grid) + + return is_in_grid(v2, h2) and is_empty(v2, h2, grid) and ( + not is_jump(v1, h1, v2, h2)) and is_own_piece(v1, + h1, + turn, + grid) + + +def is_own_piece(v, h, turn, grid): + """Check if the player is using the correct piece + + :param v: Vertical position of man + :param h: Horizontal position of man + :param turn: "X" or "O" + :param grid: A 2-dimensional 7x7 list + :return: True, if the player is using their own piece, False if otherwise. + """ + + return grid[v][h] == turn + + +def is_legal_put(v, h, grid, phase_number): + """Determines whether putting the man in specified cell location is legal + or not + + :param v: Vertical position of man + :param h: Horizontal position of man + :param grid: A 2-dimensional 7x7 list + :param phase_number: 1, 2, or 3 + :return: True if it is legal, False if it is not legal + """ + return is_in_grid(v, h) and is_empty(v, h, grid) and phase_number == 1 + + +def is_legal_take(v, h, turn, grid, take_mode): + """Determines whether taking a man in that cell is legal or not + + :param v: Vertical position of man + :param h: Horizontal position of man + :param turn: "X" or "O" + :param grid: A 2-dimensional 7x7 list + :param take_mode: 1 or 0 + :return: True if it is legal, False if it is not legal + """ + + return is_in_grid(v, h) and not is_empty(v, h, grid) and not is_own_piece( + v, h, turn, grid) and take_mode == 1 + + +def get_piece(turn, grid): + """Counts the current piece on the grid + + :param turn: "X" or "O" + :param grid: A 2-dimensional 7x7 list + :return: Number of pieces of "turn" on the grid + """ + + grid_combined = [] + + for row in grid: + grid_combined += row + + counter = Counter(tuple(grid_combined)) + + return counter[turn] + + +def who_won(topic_name, merels_storage): + """Who won the game? If there was any at that moment + + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: "None", if there is no one, "X" if X is winning, "O" if O + is winning + """ + + merels = database.MerelsStorage(merels_storage) + data = game_data.GameData(merels.get_game_data(topic_name)) + + if data.get_phase() > 1: + if get_piece("X", data.grid()) <= 2: + return "O" + + if get_piece("O", data.grid()) <= 2: + return "X" + + return "None" + + +def get_phase_number(grid, turn, x_pieces_possessed_not_on_grid, + o_pieces_possessed_not_on_grid): + """Updates current game phase + + :param grid: A 2-dimensional 7x7 list + :param turn: "X" or "O" + :param x_pieces_possessed_not_on_grid: Amount of man that X currently have, + but not placed yet + :param o_pieces_possessed_not_on_grid: Amount of man that O currently have, + but not placed yet + :return: Phase number. 1 is "placing pieces", 2 is "moving pieces", and 3 + is "flying" + """ + + if x_pieces_possessed_not_on_grid != 0 or o_pieces_possessed_not_on_grid \ + != 0: + # Placing pieces + return 1 + else: + if get_piece("X", grid) <= 3 or get_piece("O", grid) <= 3: + # Flying + return 3 + else: + # Moving pieces + return 2 + + +def create_room(topic_name, merels_storage): + """Creates a game in current topic + + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: A response string + """ + merels = database.MerelsStorage(merels_storage) + + if merels.create_new_game(topic_name): + response = "" + response += "A room has been created in {0}. Starting game now.\n". \ + format(topic_name) + response += display_game(topic_name, merels_storage) + + return response + else: + return "Failed: Cannot create an already existing game in {0}. " \ + "Please finish the game first.".format(topic_name) + + +def display_game(topic_name, merels_storage): + """Displays the current layout of the game, with additional info such as + phase number and turn. + + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: A response string + """ + merels = database.MerelsStorage(merels_storage) + + data = game_data.GameData(merels.get_game_data(topic_name)) + + response = "" + + if data.take_mode == 1: + take = "Yes" + else: + take = "No" + + response += interface.graph_grid(data.grid()) + "\n" + response += """Phase {}, {}'s turn. Take mode: {}. +X taken: {}, O taken: {}. + """.format(data.get_phase(), data.turn, take, data.x_taken, data.o_taken) + + return response + + +def reset_game(topic_name, merels_storage): + """Resets the game in current topic + + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: A response string + """ + merels = database.MerelsStorage(merels_storage) + + merels.remove_game(topic_name) + return "Game removed.\n" + create_room(topic_name, + merels_storage) + "Game reset.\n" + + +def move_man(topic_name, p1, p2, merels_storage): + """Moves the current man in topic_name from p1 to p2 + + :param topic_name: Topic name + :param p1: First cell location + :param p2: Second cell location + :param merels_storage: Merels' storage + :return: A response string + """ + merels = database.MerelsStorage(merels_storage) + data = game_data.GameData(merels.get_game_data(topic_name)) + + # Get the grid + grid = data.grid() + + # Check legal move + if is_legal_move(p1[0], p1[1], p2[0], p2[1], data.turn, data.get_phase(), + data.grid()): + # Move the man + move_man_legal(p1[0], p1[1], p2[0], p2[1], grid) + # Construct the board back from updated grid + board = interface.construct_board(grid) + # Insert/update the current board + data.board = board + # Update the game data + merels.update_game(data.topic_name, data.turn, data.x_taken, + data.o_taken, data.board, data.hill_uid, + data.take_mode) + 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." + + +def put_man(topic_name, v, h, merels_storage): + """Puts a man into the specified cell in topic_name + + :param topic_name: Topic name + :param v: Vertical position of cell + :param h: Horizontal position of cell + :param merels_storage: MerelsDatabase object + :return: A response string + """ + merels = database.MerelsStorage(merels_storage) + data = game_data.GameData(merels.get_game_data(topic_name)) + + # Get the grid + grid = data.grid() + + # Check legal put + if is_legal_put(v, h, grid, data.get_phase()): + # Put the man + put_man_legal(data.turn, v, h, grid) + # Construct the board back from updated grid + board = interface.construct_board(grid) + # Insert/update form current board + data.board = board + # Update the game data + merels.update_game(data.topic_name, data.turn, data.x_taken, + data.o_taken, data.board, data.hill_uid, + 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." + + +def take_man(topic_name, v, h, merels_storage): + """Takes a man from the grid + + :param topic_name: Topic name + :param v: Vertical position of cell + :param h: Horizontal position of cell + :param merels_storage: Merels' storage + :return: A response string + """ + merels = database.MerelsStorage(merels_storage) + data = game_data.GameData(merels.get_game_data(topic_name)) + + # Get the grid + grid = data.grid() + + # Check legal put + if is_legal_take(v, h, data.turn, grid, data.take_mode): + # Take the man + take_man_legal(v, h, grid) + + if data.turn == "X": + data.o_taken += 1 + else: + data.x_taken += 1 + + # Construct the board back from updated grid + board = interface.construct_board(grid) + # Insert/update form current board + data.board = board + # Update the game data + merels.update_game(data.topic_name, data.turn, data.x_taken, + data.o_taken, data.board, data.hill_uid, + 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." + + +def update_hill_uid(topic_name, merels_storage): + """Updates the hill_uid then store it to the database + + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: None + """ + + merels = database.MerelsStorage(merels_storage) + data = game_data.GameData(merels.get_game_data(topic_name)) + + data.hill_uid = get_hills_numbers(data.grid()) + + merels.update_game(data.topic_name, data.turn, data.x_taken, + data.o_taken, data.board, data.hill_uid, + data.take_mode) + + +def update_change_turn(topic_name, merels_storage): + """Changes the turn of player, from X to O and from O to X + + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: None + """ + + merels = database.MerelsStorage(merels_storage) + data = game_data.GameData(merels.get_game_data(topic_name)) + + data.switch_turn() + + merels.update_game(data.topic_name, data.turn, data.x_taken, + data.o_taken, data.board, data.hill_uid, + data.take_mode) + + +def update_toggle_take_mode(topic_name, merels_storage): + """Toggle take_mode from 0 to 1 and from 1 to 0 + + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: None + """ + + merels = database.MerelsStorage(merels_storage) + data = game_data.GameData(merels.get_game_data(topic_name)) + + data.toggle_take_mode() + + merels.update_game(data.topic_name, data.turn, data.x_taken, + data.o_taken, data.board, data.hill_uid, + data.take_mode) + + +def get_take_status(topic_name, merels_storage): + """Gets the take status + + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: 1 or 0 + """ + + merels = database.MerelsStorage(merels_storage) + data = game_data.GameData(merels.get_game_data(topic_name)) + + return data.take_mode + + +def can_take_mode(topic_name, merels_storage): + """Check if current turn can trigger take mode. + + This process can be thought of as seeing the differences between previous + hill_uid and current hill_uid. + + Previous hill_uid can be obtained before updating the hill_uid, and current + hill_uid can be obtained after updating the grid. + + If the differences and length decreases after, then it is not possible that + he player forms a new hill. + + If the differences or length increases, it is possible that the player that + makes the move forms a new hill. This + essentially triggers the take mode, as the player must take one opponent's + piece from the grid. + + Essentially, how this works is by checking an updated, but not fully + finished complete database. So the differences between hill_uid can be + seen. + + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: True if this turn can trigger take mode, False if otherwise + """ + + merels = database.MerelsStorage(merels_storage) + data = game_data.GameData(merels.get_game_data(topic_name)) + + current_hill_uid = data.hill_uid + + updated_grid = data.grid() + + updated_hill_uid = get_hills_numbers(updated_grid) + + if current_hill_uid != updated_hill_uid and len(updated_hill_uid) >= len( + current_hill_uid): + return True + else: + return False + + +def check_moves(turn, grid): + """Checks whether the player can make any moves from specified grid and turn + + :param turn: "X" or "O" + :param grid: A 2-dimensional 7x7 list + :return: True, if there is any, False if otherwise + """ + for hill in constants.HILLS: + for k in range(0, 2): + g1 = grid[hill[k][0]][hill[k][1]] + g2 = grid[hill[k + 1][0]][hill[k + 1][1]] + if (g1 == " " and g2 == turn) or (g2 == " " and g1 == turn): + return True + + return False + + +def can_make_any_move(topic_name, merels_storage): + """Checks whether the player can actually make a move. If it is phase 1, + don't check it and return True instead + + :param topic_name: Topic name + :param merels_storage: Merels' storage + :return: True if the player has a way, False if there isn't + """ + + merels = database.MerelsStorage(merels_storage) + data = game_data.GameData(merels.get_game_data(topic_name)) + + if data.get_phase() != 1: + return check_moves(data.turn, data.grid()) + + return True diff --git a/zulip_bots/zulip_bots/bots/merels/merels.py b/zulip_bots/zulip_bots/bots/merels/merels.py new file mode 100644 index 0000000..927a82f --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/merels.py @@ -0,0 +1,42 @@ +from zulip_bots.bots.merels.libraries import game + + +class MerelsBot(object): + """ + Simulate the merels game to the chat + """ + + def __init__(self): + pass + + def usage(self): + return game.getInfo() + + def handle_message(self, message, bot_handler): + room_name = self.compose_room_name(message) + content = message['content'] + + 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 diff --git a/zulip_bots/zulip_bots/bots/merels/test/__init__.py b/zulip_bots/zulip_bots/bots/merels/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zulip_bots/zulip_bots/bots/merels/test/test_constants.py b/zulip_bots/zulip_bots/bots/merels/test/test_constants.py new file mode 100644 index 0000000..80ab8c7 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/test/test_constants.py @@ -0,0 +1,50 @@ +import unittest + +from libraries import constants + + +class CheckIntegrity(unittest.TestCase): + + def test_grid_layout_integrity(self): + grid_layout = ([0, 0], [0, 3], [0, 6], + [1, 1], [1, 3], [1, 5], + [2, 2], [2, 3], [2, 4], + [3, 0], [3, 1], [3, 2], [3, 4], [3, 5], [3, 6], + [4, 2], [4, 3], [4, 4], + [5, 1], [5, 3], [5, 5], + [6, 0], [6, 3], [6, 6]) + + self.assertEqual(constants.ALLOWED_MOVES, grid_layout, + "Incorrect grid layout.") + + def test_relative_hills_integrity(self): + grid_layout = ([0, 0], [0, 3], [0, 6], + [1, 1], [1, 3], [1, 5], + [2, 2], [2, 3], [2, 4], + [3, 0], [3, 1], [3, 2], [3, 4], [3, 5], [3, 6], + [4, 2], [4, 3], [4, 4], + [5, 1], [5, 3], [5, 5], + [6, 0], [6, 3], [6, 6]) + + AM = grid_layout + + relative_hills = ([AM[0], AM[1], AM[2]], + [AM[3], AM[4], AM[5]], + [AM[6], AM[7], AM[8]], + [AM[9], AM[10], AM[11]], + [AM[12], AM[13], AM[14]], + [AM[15], AM[16], AM[17]], + [AM[18], AM[19], AM[20]], + [AM[21], AM[22], AM[23]], + [AM[0], AM[9], AM[21]], + [AM[3], AM[10], AM[18]], + [AM[6], AM[11], AM[15]], + [AM[1], AM[4], AM[7]], + [AM[16], AM[19], AM[22]], + [AM[8], AM[12], AM[17]], + [AM[5], AM[13], AM[20]], + [AM[2], AM[14], AM[23]], + ) + + self.assertEqual(constants.HILLS, relative_hills, + "Incorrect relative hills arrangement") diff --git a/zulip_bots/zulip_bots/bots/merels/test/test_database.py b/zulip_bots/zulip_bots/bots/merels/test/test_database.py new file mode 100644 index 0000000..ea092b1 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/test/test_database.py @@ -0,0 +1,66 @@ +import unittest + +from libraries import database +from libraries import game_data +from zulip_bots.simple_lib import SimpleStorage + + +class DatabaseTest(unittest.TestCase): + def setUp(self): + self.storage = SimpleStorage() + self.merels = database.MerelsStorage(self.storage) + + def test_create_duplicate_game(self): + self.merels.create_new_game("test") + + self.assertEqual(self.merels.create_new_game("test"), False) + + def test_obtain_gamedata(self): + self.merels.create_new_game("test") + + res = self.merels.get_game_data("test") + self.assertTupleEqual(res, ( + 'test', 'X', 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', "", 0)) + self.assertEqual(len(res), 7) + + def test_obtain_nonexisting_gamedata(self): + res = self.merels.get_game_data("test") + + self.assertEqual(res, None) + + def test_game_session(self): + self.merels.create_new_game("test") + + self.merels.update_game("test", "O", 5, 4, "XXXXOOOOONNNNNNNNNNNNNNN", + "", + 0) + + self.merels.create_new_game("test2") + + self.assertTrue(self.storage.contains("test"), self.storage.contains( + "test2")) + + self.assertEqual( + game_data.GameData(self.merels.get_game_data("test")).board, + "XXXXOOOOONNNNNNNNNNNNNNN") + + def test_no_duplicates(self): + self.merels.create_new_game("test") + self.merels.update_game("test", "X", 0, 0, "XXXNNNOOOXXXNNNOOOXXXNNN", + "", 1) + self.merels.create_new_game("test") + self.merels.create_new_game("test") + self.merels.create_new_game("test") + self.merels.create_new_game("test") + self.merels.create_new_game("test") + self.merels.create_new_game("test") + self.merels.create_new_game("test") + + self.assertEqual(game_data.GameData(self.merels.get_game_data( + "test")).board, "XXXNNNOOOXXXNNNOOOXXXNNN") + + def test_remove_game(self): + self.merels.create_new_game("test") + self.merels.remove_game("test") + + self.assertTrue(self.merels.create_new_game("test")) diff --git a/zulip_bots/zulip_bots/bots/merels/test/test_game.py b/zulip_bots/zulip_bots/bots/merels/test/test_game.py new file mode 100644 index 0000000..de4dcf7 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/test/test_game.py @@ -0,0 +1,117 @@ +import unittest + +from libraries import game +from libraries import database + +from zulip_bots.simple_lib import SimpleStorage + + +class GameTest(unittest.TestCase): + def setUp(self): + self.storage = SimpleStorage() + self.topic_name = "test" + + def test_reset_game_output(self): + game.beat("create", self.topic_name, self.storage) + self.assertTrue("reset" in game.beat("reset", self.topic_name, + self.storage)) + + def test_reset_no_game_output(self): + self.assertTrue("No game created yet" in game.beat("reset", + self.topic_name, + self.storage)) + + def test_command_when_no_game_created_output(self): + self.assertTrue("cannot do any of the game commands" in game.beat( + "put 0,0", self.topic_name, self.storage)) + + def test_put_piece_output(self): + game.beat("create", self.topic_name, self.storage) + self.assertTrue("Put a man" in game.beat("put 0,0", self.topic_name, + self.storage)) + + def test_not_possible_put_piece_output(self): + game.beat("create", self.topic_name, self.storage) + self.assertTrue("Failed" in game.beat("put 0,1", self.topic_name, + self.storage)) + + def test_take_before_put_output(self): + merels = database.MerelsStorage(self.storage) + game.beat("create", self.topic_name, self.storage) + merels.update_game(self.topic_name, "X", 0, 0, + "XXXNNNOOOXXXNNNOOOXXXNNN", "", 1) + self.assertTrue("Take is required", game.beat("put 1,1", + self.topic_name, + self.storage)) + + def test_move_piece_output(self): + merels = database.MerelsStorage(self.storage) + game.beat("create", self.topic_name, self.storage) + merels.update_game(self.topic_name, "X", 0, 0, + "XXXNNNOOOXXXNNNOOOXXXOOO", "", 0) + self.assertTrue("Moved a man" in game.beat("move 0,3 1,3", + self.topic_name, + self.storage)) + + def test_not_possible_move_piece_output(self): + merels = database.MerelsStorage(self.storage) + game.beat("create", self.topic_name, self.storage) + merels.update_game(self.topic_name, "X", 0, 0, + "XXXNNNOOOXXXNNNOOOXXXOOO", "", 0) + self.assertTrue("Failed" in game.beat("move 0,3 1,2", + self.topic_name, + self.storage)) + + def test_cannot_make_any_move_output(self): + merels = database.MerelsStorage(self.storage) + game.beat("create", self.topic_name, self.storage) + merels.update_game(self.topic_name, "X", 3, 4, + "OOXOXNOXNNOXNNNNNNXNNXNN", "", 0) + self.assertTrue("Switching" in game.beat("move 6,0 3,0", + self.topic_name, + self.storage)) + + def test_take_before_move_output(self): + merels = database.MerelsStorage(self.storage) + game.beat("create", self.topic_name, self.storage) + merels.update_game(self.topic_name, "X", 6, 6, + "XXXNNNOOONNNNNNNNNNNNNNN", "", 1) + self.assertTrue("Take is required", game.beat("move 0,1 1,3", + self.topic_name, + self.storage)) + + def test_unknown_command(self): + merels = database.MerelsStorage(self.storage) + game.beat("create", self.topic_name, self.storage) + merels.update_game(self.topic_name, "X", 6, 6, + "XXXNNNOOONNNNNNNNNNNNNNN", "", 1) + self.assertTrue("Unknown command", game.beat("magic 2,2", + self.topic_name, + self.storage)) + + def test_take_piece_output(self): + merels = database.MerelsStorage(self.storage) + game.beat("create", self.topic_name, self.storage) + merels.update_game(self.topic_name, "X", 0, 0, + "XXXNNNOOOXXXNNNOOOXXXOOO", "", 1) + self.assertTrue("Taken a man" in game.beat("take 2,2", + self.topic_name, + self.storage)) + + def test_not_possible_take_piece_output(self): + merels = database.MerelsStorage(self.storage) + game.beat("create", self.topic_name, self.storage) + merels.update_game(self.topic_name, "X", 6, 6, + "XXXNNNOOOXXXNNNOOOXXXOOO", "", 0) + self.assertTrue("Taking is not possible" in game.beat("take 2,2", + self.topic_name, + self.storage)) + + def test_win_output(self): + merels = database.MerelsStorage(self.storage) + game.beat("create", self.topic_name, self.storage) + merels.update_game(self.topic_name, "X", 6, 6, + "XXXNNNOOONNNNNNNNNNNNNNN", "", 1) + self.assertTrue("wins the game!", game.beat("take 2,2", + self.topic_name, + self.storage)) diff --git a/zulip_bots/zulip_bots/bots/merels/test/test_interface.py b/zulip_bots/zulip_bots/bots/merels/test/test_interface.py new file mode 100644 index 0000000..a8e571d --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/test/test_interface.py @@ -0,0 +1,78 @@ +import unittest + +from libraries import interface + + +class BoardLayoutTest(unittest.TestCase): + + def test_empty_layout_arrangement(self): + grid = interface.construct_grid("NNNNNNNNNNNNNNNNNNNNNNNN") + self.assertEqual(interface.graph_grid(grid), '''` 0 1 2 3 4 5 6 + 0 [ ]---------------[ ]---------------[ ] + | | | + 1 | [ ]---------[ ]---------[ ] | + | | | | | + 2 | | [ ]---[ ]---[ ] | | + | | | | | | + 3 [ ]---[ ]---[ ] [ ]---[ ]---[ ] + | | | | | | + 4 | | [ ]---[ ]---[ ] | | + | | | | | + 5 | [ ]---------[ ]---------[ ] | + | | | + 6 [ ]---------------[ ]---------------[ ]`''') + + def test_full_layout_arragement(self): + grid = interface.construct_grid("NXONXONXONXONXONXONXONXO") + self.assertEqual(interface.graph_grid(grid), '''` 0 1 2 3 4 5 6 + 0 [ ]---------------[X]---------------[O] + | | | + 1 | [ ]---------[X]---------[O] | + | | | | | + 2 | | [ ]---[X]---[O] | | + | | | | | | + 3 [ ]---[X]---[O] [ ]---[X]---[O] + | | | | | | + 4 | | [ ]---[X]---[O] | | + | | | | | + 5 | [ ]---------[X]---------[O] | + | | | + 6 [ ]---------------[X]---------------[O]`''') + + def test_illegal_character_arrangement(self): + grid = interface.construct_grid("ABCDABCDABCDABCDABCDXXOO") + self.assertEqual(interface.graph_grid(grid), '''` 0 1 2 3 4 5 6 + 0 [ ]---------------[ ]---------------[ ] + | | | + 1 | [ ]---------[ ]---------[ ] | + | | | | | + 2 | | [ ]---[ ]---[ ] | | + | | | | | | + 3 [ ]---[ ]---[ ] [ ]---[ ]---[ ] + | | | | | | + 4 | | [ ]---[ ]---[ ] | | + | | | | | + 5 | [ ]---------[ ]---------[X] | + | | | + 6 [X]---------------[O]---------------[O]`''') + + +class ParsingTest(unittest.TestCase): + + def test_consistent_parse(self): + boards = ["NNNNOOOOXXXXNNNNOOOOXXXX", + "NOXNXOXNOXNOXOXOXNOXONON", + "OOONXNOXNONXONOXNXNNONOX", + "NNNNNNNNNNNNNNNNNNNNNNNN", + "OOOOOOOOOOOOOOOOOOOOOOOO", + "XXXXXXXXXXXXXXXXXXXXXXXX"] + + for board in boards: + self.assertEqual(board, interface.construct_board( + interface.construct_grid( + interface.construct_board( + interface.construct_grid(board) + ) + ) + ) + ) diff --git a/zulip_bots/zulip_bots/bots/merels/test/test_mechanics.py b/zulip_bots/zulip_bots/bots/merels/test/test_mechanics.py new file mode 100644 index 0000000..13daf3f --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/test/test_mechanics.py @@ -0,0 +1,186 @@ +import unittest + +from libraries import database +from libraries import game_data +from libraries import interface +from libraries import mechanics +from zulip_bots.simple_lib import SimpleStorage + + +class GridTest(unittest.TestCase): + + def test_out_of_grid(self): + points = [[v, h] for h in range(7) for v in range(7)] + expected_outcomes = [True, False, False, True, False, False, True, + False, True, False, True, False, True, False, + False, False, True, True, True, False, False, + True, True, True, False, True, True, True, + False, False, True, True, True, False, False, + False, True, False, True, False, True, False, + True, False, False, True, False, False, True] + + test_outcomes = [mechanics.is_in_grid(point[0], point[1]) for point in + points] + + self.assertListEqual(test_outcomes, expected_outcomes) + + def test_jump_and_grids(self): + points = [[0, 0, 1, 1], [1, 1, 2, 2], [2, 2, 3, 3], [0, 0, 0, 2], + [0, 0, 2, 2], [6, 6, 5, 4]] + expected_outcomes = [True, True, True, True, True, True] + + test_outcomes = [ + mechanics.is_jump(point[0], point[1], point[2], point[3]) for point + in points] + + self.assertListEqual(test_outcomes, expected_outcomes) + + def test_jump_special_cases(self): + points = [[0, 0, 0, 3], [0, 0, 3, 0], [6, 0, 6, 3], [4, 2, 6, 2], + [4, 3, 3, 4], [4, 3, 2, 2], + [0, 0, 0, 6], [0, 0, 1, 1], [0, 0, 2, 2], [3, 0, 3, 1], + [3, 0, 3, 2], [3, 1, 3, 0], [3, 1, 3, 2]] + + expected_outcomes = [False, False, False, True, True, True, True, True, + True, False, True, False, False] + + test_outcomes = [ + mechanics.is_jump(point[0], point[1], point[2], point[3]) for point + in points] + + self.assertListEqual(test_outcomes, expected_outcomes) + + def test_not_populated_move(self): + grid = interface.construct_grid("XXXNNNOOOXXXNNNOOOXXXNNN") + + moves = [[0, 0, 1, 1], [0, 3, 1, 3], [5, 1, 5, 3], [0, 0, 0, 3], + [0, 0, 3, 0]] + + expected_outcomes = [True, True, False, False, False] + + test_outcomes = [mechanics.is_empty(move[2], move[3], grid) for move in + moves] + + self.assertListEqual(test_outcomes, expected_outcomes) + + def test_legal_move(self): + grid = interface.construct_grid("XXXNNNOOONNNNNNOOONNNNNN") + + presets = [[0, 0, 0, 3, "X", 1], [0, 0, 0, 6, "X", 2], + [0, 0, 3, 6, "X", 3], [0, 0, 2, 2, "X", 3]] + + expected_outcomes = [False, False, True, False] + + test_outcomes = [ + mechanics.is_legal_move(preset[0], preset[1], preset[2], preset[3], + preset[4], preset[5], grid) + for preset in presets] + + self.assertListEqual(test_outcomes, expected_outcomes) + + def test_legal_put(self): + grid = interface.construct_grid("XXXNNNOOOXXXNNNOOOXXXNNN") + + presets = [[0, 0, 1], [0, 3, 2], [0, 6, 3], [1, 1, 2], [1, 3, 1], + [1, 6, 1], [1, 5, 1]] + + expected_outcomes = [False, False, False, False, True, False, True] + + test_outcomes = [ + mechanics.is_legal_put(preset[0], preset[1], grid, preset[2]) for + preset in presets] + + self.assertListEqual(test_outcomes, expected_outcomes) + + def test_legal_take(self): + grid = interface.construct_grid("XXXNNNOOOXXXNNNOOOXXXNNN") + + presets = [[0, 0, "X", 1], [0, 1, "X", 1], [0, 0, "O", 1], + [0, 0, "O", 0], [0, 1, "O", 1], [2, 2, "X", 1], + [2, 3, "X", 1], [2, 4, "O", 1]] + + expected_outcomes = [False, False, True, False, False, True, True, + False] + + test_outcomes = [ + mechanics.is_legal_take(preset[0], preset[1], preset[2], grid, + preset[3]) for preset in + presets] + + self.assertListEqual(test_outcomes, expected_outcomes) + + def test_own_piece(self): + grid = interface.construct_grid("XXXNNNOOOXXXNNNOOOXXXNNN") + + presets = [[0, 0, "X"], [0, 0, "O"], [0, 6, "X"], [0, 6, "O"], + [1, 1, "X"], [1, 1, "O"]] + + expected_outcomes = [True, False, True, False, False, False] + + test_outcomes = [ + mechanics.is_own_piece(preset[0], preset[1], preset[2], grid) for + preset in presets] + + self.assertListEqual(test_outcomes, expected_outcomes) + + def test_can_make_any_move(self): + grid = interface.construct_grid("NONNNNNNNNNNNNNNNNNNNNXN") + + self.assertEqual(mechanics.check_moves("O", grid), True) + self.assertEqual(mechanics.check_moves("X", grid), True) + + grid = interface.construct_grid("XXXXXXOXXXXXXXXXXXXXXXNX") + + self.assertEqual(mechanics.check_moves("O", grid), False) + self.assertEqual(mechanics.check_moves("X", grid), True) + + grid = interface.construct_grid("NXNNNNNNNNNNNNNNNNNNNNNN") + + self.assertEqual(mechanics.check_moves("O", grid), False) + self.assertEqual(mechanics.check_moves("X", grid), True) + + +class HillsTest(unittest.TestCase): + def test_unchanged_hills(self): + grid = interface.construct_grid("XXXNNNOOOXXXXNNOOOXXXNNN") + + hills_uid = "02356" + + mechanics.move_man_legal(3, 4, 3, 5, grid) + + updated_hills_uid = mechanics.get_hills_numbers(grid) + + self.assertEqual(updated_hills_uid, hills_uid) + + def test_no_diagonal_hills(self): + grid = interface.construct_grid("XXXNNXOONXXXXNNOOOXXXNNN") + + hills_uid = "0356" + + mechanics.move_man_legal(3, 4, 2, 4, grid) + + updated_hills_uid = mechanics.get_hills_numbers(grid) + + self.assertEqual(updated_hills_uid, hills_uid) + + +class PhaseTest(unittest.TestCase): + def test_new_game_phase(self): + storage = SimpleStorage() + merels = database.MerelsStorage(storage) + merels.create_new_game("test") + + res = game_data.GameData(merels.get_game_data("test")) + self.assertEqual(res.get_phase(), 1) + + merels.update_game(res.topic_name, "O", 5, 4, + "XXXXNNNOOOOONNNNNNNNNNNN", "03", 0) + res = game_data.GameData(merels.get_game_data("test")) + self.assertEqual(res.board, "XXXXNNNOOOOONNNNNNNNNNNN") + self.assertEqual(res.get_phase(), 2) + + merels.update_game(res.topic_name, "X", 6, 4, + "XXXNNNNOOOOONNNNNNNNNNNN", "03", 0) + res = game_data.GameData(merels.get_game_data("test")) + self.assertEqual(res.board, "XXXNNNNOOOOONNNNNNNNNNNN") + self.assertEqual(res.get_phase(), 3) diff --git a/zulip_bots/zulip_bots/bots/merels/test_merels.py b/zulip_bots/zulip_bots/bots/merels/test_merels.py new file mode 100644 index 0000000..7becc9d --- /dev/null +++ b/zulip_bots/zulip_bots/bots/merels/test_merels.py @@ -0,0 +1,77 @@ +""" +Most of the testing for the actual game are done in test_database + +This is only to really verify the output of the chat +""" + +from unittest import mock + +import zulip_bots.bots.merels.merels +import zulip_bots.test_lib + + +class TestFollowUpBot(zulip_bots.test_lib.BotTestCase): + bot_name = "merels" + + def test_no_command(self): + message = dict( + content='magic', + type='stream', + ) + + 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 ''')