576 lines
18 KiB
Python
576 lines
18 KiB
Python
"""Mechanics is what makes everything moves and works. It stores the game
|
|
mechanisms as well as some functions for accessing the database.
|
|
"""
|
|
|
|
from collections import Counter
|
|
from math import sqrt
|
|
|
|
from zulip_bots.game_handler import BadMoveException
|
|
|
|
from . import constants, database, game_data, 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(topic_name, 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(topic_name, 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 {}. " \
|
|
"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(topic_name, 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 {}. Take mode: {}.
|
|
X taken: {}, O taken: {}.
|
|
""".format(data.get_phase(), 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(topic_name, 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(topic_name, 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 ({}, {}) -> ({}, {}) for {}.".format(
|
|
p1[0], p1[1], p2[0], p2[1], data.turn)
|
|
else:
|
|
raise BadMoveException("Failed: That's not a legal move. Please try again.")
|
|
|
|
def put_man(topic_name, v, h, merels_storage):
|
|
"""Puts a man into the specified cell in topic_name
|
|
|
|
: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(topic_name, 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 ({}, {}) for {}.".format(v, h, data.turn)
|
|
else:
|
|
raise BadMoveException("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(topic_name, 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 ({}, {}) for {}.".format(v, h, data.turn)
|
|
else:
|
|
raise BadMoveException("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(topic_name, 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(topic_name, 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(topic_name, 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(topic_name, 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(topic_name, 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(topic_name, 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
|