interactive bots: Create Merels bot.
This commit is contained in:
parent
a2557ccbe6
commit
b80a0cb297
3
zulip_bots/zulip_bots/bots/merels/.gitignore
vendored
Normal file
3
zulip_bots/zulip_bots/bots/merels/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
*.db
|
||||||
|
|
||||||
|
.idea/
|
48
zulip_bots/zulip_bots/bots/merels/libraries/constants.py
Normal file
48
zulip_bots/zulip_bots/bots/merels/libraries/constants.py
Normal file
|
@ -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"
|
106
zulip_bots/zulip_bots/bots/merels/libraries/database.py
Normal file
106
zulip_bots/zulip_bots/bots/merels/libraries/database.py
Normal file
|
@ -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
|
223
zulip_bots/zulip_bots/bots/merels/libraries/game.py
Normal file
223
zulip_bots/zulip_bots/bots/merels/libraries/game.py
Normal file
|
@ -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 ""
|
88
zulip_bots/zulip_bots/bots/merels/libraries/game_data.py
Normal file
88
zulip_bots/zulip_bots/bots/merels/libraries/game_data.py
Normal file
|
@ -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
|
104
zulip_bots/zulip_bots/bots/merels/libraries/interface.py
Normal file
104
zulip_bots/zulip_bots/bots/merels/libraries/interface.py
Normal file
|
@ -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
|
579
zulip_bots/zulip_bots/bots/merels/libraries/mechanics.py
Normal file
579
zulip_bots/zulip_bots/bots/merels/libraries/mechanics.py
Normal file
|
@ -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
|
42
zulip_bots/zulip_bots/bots/merels/merels.py
Normal file
42
zulip_bots/zulip_bots/bots/merels/merels.py
Normal file
|
@ -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
|
0
zulip_bots/zulip_bots/bots/merels/test/__init__.py
Normal file
0
zulip_bots/zulip_bots/bots/merels/test/__init__.py
Normal file
50
zulip_bots/zulip_bots/bots/merels/test/test_constants.py
Normal file
50
zulip_bots/zulip_bots/bots/merels/test/test_constants.py
Normal file
|
@ -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")
|
66
zulip_bots/zulip_bots/bots/merels/test/test_database.py
Normal file
66
zulip_bots/zulip_bots/bots/merels/test/test_database.py
Normal file
|
@ -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"))
|
117
zulip_bots/zulip_bots/bots/merels/test/test_game.py
Normal file
117
zulip_bots/zulip_bots/bots/merels/test/test_game.py
Normal file
|
@ -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))
|
78
zulip_bots/zulip_bots/bots/merels/test/test_interface.py
Normal file
78
zulip_bots/zulip_bots/bots/merels/test/test_interface.py
Normal file
|
@ -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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
186
zulip_bots/zulip_bots/bots/merels/test/test_mechanics.py
Normal file
186
zulip_bots/zulip_bots/bots/merels/test/test_mechanics.py
Normal file
|
@ -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)
|
77
zulip_bots/zulip_bots/bots/merels/test_merels.py
Normal file
77
zulip_bots/zulip_bots/bots/merels/test_merels.py
Normal file
|
@ -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 ''')
|
Loading…
Reference in a new issue