''' AI project by Bokhtiar Adil and Sadman Sakib ''' import os import sys import pygame as pg import time WINDOW_HEIGHT = 700 WINDOW_WIDTH = 1000 GAME_NAME = TITLE = "VIKINGS_CHESS" GAME_ICON = pg.image.load("images/viking_anime_style.jpg") MAIN_MENU_TOP_BUTTON_x = 400 MAIN_MENU_TOP_BUTTON_y = 400 BOARD_TOP = 200 BOARD_LEFT = 125 CELL_WIDTH = 50 CELL_HEIGHT = 50 PIECE_RADIUS = 20 VALID_MOVE_INDICATOR_RADIUS = 10 SETTINGS_TEXT_GAP_VERTICAL = 50 SETTINGS_TEXT_GAP_HORIZONTAL = 100 bg = (204, 102, 0) bg2 = (40, 40, 40) red = (255, 0, 0) black = (0, 0, 0) yellow = (255, 255, 1) golden = (255, 215, 0) white = (255, 255, 255) pink_fuchsia = (255, 0, 255) green_neon = (15, 255, 80) green_dark = (2, 48, 32) green_teal = (0, 128, 128) blue_indigo = (63, 0, 255) blue_zaffre = (8, 24, 168) ATTACKER_PIECE_COLOR = pink_fuchsia DEFENDER_PIECE_COLOR = green_teal KING_PIECE_COLOR = golden VALID_MOVE_INDICATOR_COLOR = green_neon BORDER_COLOR = blue_zaffre GAME_ICON_resized = pg.image.load("images/viking_anime_style.jpg") click_snd = os.path.join("sounds", "click_1.wav") move_snd_1 = os.path.join("sounds", "move_1.mp3") kill_snd_1 = os.path.join("sounds", "kill_1.mp3") win_snd_1 = os.path.join("sounds", "win_1.wav") lose_snd_1 = os.path.join("sounds", "lose_1.wav") clicked = False def write_text(text, screen, position, color, font, new_window=True): ''' This function writes the given text at the given position on given surface applying th e given color and font. Parameters ---------- text : string This string will be #printed. screen : a pygame display or surface The text wiil be written on this suface. position : a pair of values e.g. (x,y) The text wiil be written at this position. color : rgb coolor code e.g. (255,255,255) The text wiil be written in this color. font : a pygame font (pg.font.SysFont) The text wiil be written in this font. new_window : a boolean value, optional This parameter wiil determine whether the text wil be #printed in a new window or current window. If the former, all current text and graphics on this surface will be overwritten with background color. The default is True. Returns ------- None. ''' if new_window: screen.fill(bg2) txtobj = font.render(text, True, (255, 255, 255)) txtrect = txtobj.get_rect() txtrect.topleft = position screen.blit(txtobj, txtrect) class Custom_button: ''' This class holds the ncessary part of a custom button operation. ''' button_col = (26, 117, 255) hover_col = red click_col = (50, 150, 255) text_col = yellow def __init__(self, x, y, text, screen, font, width=200, height=70): self.x = x self.y = y self.text = text self.screen = screen self.font = font self.width = width self.height = height def draw_button(self): global clicked action = False # get mouse position pos = pg.mouse.get_pos() # create pg Rect object for the button button_rect = pg.Rect(self.x, self.y, self.width, self.height) # check mouseover and clicked conditions if button_rect.collidepoint(pos): if pg.mouse.get_pressed()[0] == 1: clicked = True pg.draw.rect(self.screen, self.click_col, button_rect) elif pg.mouse.get_pressed()[0] == 0 and clicked == True: clicked = False action = True else: pg.draw.rect(self.screen, self.hover_col, button_rect) else: pg.draw.rect(self.screen, self.button_col, button_rect) # add text to button text_img = self.font.render(self.text, True, self.text_col) text_len = text_img.get_width() self.screen.blit(text_img, (self.x + int(self.width / 2) - int(text_len / 2), self.y + 15)) return action class ChessBoard: ''' This class contains all properties of a chess board. Properties: 1. initial_pattern: this parameter holds the position of pieces at the start of the match. 2. rows: n(rows) on board 3. columns: n(columns) on board 4. cell_width: width of each cell on surface 5. cell_height: height of each cell on surface 6. screen: where the board will be #printed 7. restricted_cell: holds the (row, column) value of restricted cells Methods: 1. draw_empty_board(): this method draws an empty board with no piece on given surface 2. initiate_board_pieces(): this method initiates all the sprite instances of different types of pieces ''' def __init__(self, screen, board_size="large"): # board size large means 11x11, small measn 9x9 self.initial_pattern11 = ["x..aaaaa..x", ".....a.....", "...........", "a....d....a", "a...ddd...a", "aa.ddcdd.aa", "a...ddd...a", "a....d....a", "...........", ".....a.....", "x..aaaaa..x"] self.initial_pattern9 = ["...aaa...", "....a....", "....d....", "a...d...a", "aaddcddaa", "a...d...a", "....d....", "....a....", "...aaa..."] if board_size == "large": self.initial_pattern = self.initial_pattern11 else: self.initial_pattern = self.initial_pattern9 self.rows = len(self.initial_pattern) self.columns = len(self.initial_pattern[0]) self.cell_width = CELL_WIDTH self.cell_height = CELL_HEIGHT self.screen = screen self.restricted_cells = [(4,4)] def draw_empty_board(self): ''' This method draws an empty board with no piece on given surface Returns ------- None. ''' border_top = pg.Rect(BOARD_LEFT - 10, BOARD_TOP - 10, self.columns*CELL_WIDTH + 20, 10) pg.draw.rect(self.screen, BORDER_COLOR, border_top) border_down = pg.Rect(BOARD_LEFT - 10, BOARD_TOP + self.rows*CELL_HEIGHT, self.columns*CELL_WIDTH + 20, 10) pg.draw.rect(self.screen, BORDER_COLOR, border_down) border_left = pg.Rect(BOARD_LEFT - 10, BOARD_TOP - 10, 10, self.rows*CELL_HEIGHT + 10) pg.draw.rect(self.screen, BORDER_COLOR, border_left) border_right = pg.Rect(BOARD_LEFT+self.columns*CELL_WIDTH, BOARD_TOP - 10, 10, self.rows*CELL_HEIGHT + 10) pg.draw.rect(self.screen, BORDER_COLOR, border_right) color_flag = True for row in range(self.rows): write_text(str(row), self.screen, (BOARD_LEFT - 30, BOARD_TOP + row*CELL_HEIGHT + PIECE_RADIUS), (255, 255, 255), pg.font.SysFont("Arial", 15), False) write_text(str(row), self.screen, (BOARD_LEFT + row*CELL_WIDTH + PIECE_RADIUS, BOARD_TOP - 30), (255, 255, 255), pg.font.SysFont("Arial", 15), False) for column in range(self.columns): cell_rect = pg.Rect(BOARD_LEFT + column * self.cell_width, BOARD_TOP + row * self.cell_height, self.cell_width, self.cell_height) if (row == 0 or row == self.rows-1) and (column == 0 or column == self.columns-1): pg.draw.rect(self.screen, red, cell_rect) elif row == int(self.rows / 2) and column == int(self.columns / 2): pg.draw.rect(self.screen, blue_indigo, cell_rect) elif color_flag: pg.draw.rect(self.screen, white, cell_rect) else: pg.draw.rect(self.screen, black, cell_rect) color_flag = not color_flag def initiate_board_pieces(self): ''' This method initiates all the sprite instances of different types of pieces Returns ------- None. ''' att_cnt, def_cnt = 1, 1 # for more effective use, this dict maps piece ids and pieces -> {pid : piece} global piece_pid_map piece_pid_map = {} for row in range(self.rows): for column in range(self.columns): if self.initial_pattern[row][column] == 'a': pid = "a" + str(att_cnt) AttackerPiece(pid, row, column) att_cnt += 1 elif self.initial_pattern[row][column] == 'd': pid = "d" + str(def_cnt) DefenderPiece(pid, row, column) def_cnt += 1 elif self.initial_pattern[row][column] == 'c': pid = "k" KingPiece(pid, row, column) else: pass for piece in All_pieces: piece_pid_map[piece.pid] = piece class ChessPiece(pg.sprite.Sprite): ''' This class contains information about each piece. Properties: 1. pid: holds a unique id for currnet piece instance 2. row: holds the row index of current piece instance 3. column: holds the column index of current piece instance 4. center: center position of corresponding piece instance Methods: 1. update_piece_position(row, column): if the corresponding piece instance is moved, this method updates row and column value of that piece. ''' def __init__(self, pid, row, column): pg.sprite.Sprite.__init__(self, self.groups) self.pid = pid self.row, self.column = (row, column) self.center = (BOARD_LEFT + int(CELL_WIDTH / 2) + self.column*CELL_WIDTH, BOARD_TOP + int(CELL_HEIGHT / 2) + self.row*CELL_HEIGHT) def draw_piece(self, screen): ''' Draws a piece on board. Parameters ---------- screen : surface Returns ------- None. ''' pg.draw.circle(screen, self.color, self.center, PIECE_RADIUS) def update_piece_position(self, row, column): ''' This updates the position of all pieces on board. Parameters ---------- row : row number column : column number Returns ------- None. ''' self.row, self.column = (row, column) self.center = (BOARD_LEFT + int(CELL_WIDTH / 2) + self.column*CELL_WIDTH, BOARD_TOP + int(CELL_HEIGHT / 2) + self.row*CELL_HEIGHT) if (row, column) == (4,4) and self.ptype != "k": return # Block non-kings from castle if self.ptype == "k" and (self.row, self.column) != (4,4): if (row, column) == (4,4): return class AttackerPiece(ChessPiece): ''' This class holds information about attacker pieces. It's a child of ChessPiece class. Properties: 1. color: a rgb color code. e.g. (255,255,255) color of the attacker piece that will be drawn on board. 2. ptype: type of piece. values: i. "a" means attacker ii. "d" means defender ii. "k" means king. here it's "a". 3. permit_to_res_sp: a boolean value. tells whether the current piece is allowed on a restricted cell or not. here it's false. ''' def __init__(self, pid, row, column): ChessPiece.__init__(self, pid, row, column) pg.sprite.Sprite.__init__(self, self.groups) self.color = ATTACKER_PIECE_COLOR self.permit_to_res_sp = False self.ptype = "a" class DefenderPiece(ChessPiece): ''' This class holds information about defender pieces. It's a child of ChessPiece class. Properties: 1. color: a rgb color code. e.g. (255,255,255) color of the attacker piece that will be drawn on board. 2. ptype: type of piece. values: i. "a" means attacker ii. "d" means defender ii. "k" means king. here it's "d". 3. permit_to_res_sp: a boolean value. tells whether the current piece is allowed on a restricted cell or not. here it's false. ''' def __init__(self, pid, row, column): ChessPiece.__init__(self, pid, row, column) pg.sprite.Sprite.__init__(self, self.groups) self.color = DEFENDER_PIECE_COLOR self.permit_to_res_sp = False self.ptype = "d" class KingPiece(DefenderPiece): ''' This class holds information about attacker pieces. It's a child of DefenderPiece class. Properties: 1. color: a rgb color code. e.g. (255,255,255) color of the attacker piece that will be drawn on board. 2. ptype: type of piece. values: i. "a" means attacker ii. "d" means defender ii. "k" means king. here it's "k". 3. permit_to_res_sp: a boolean value. tells whether the current piece is allowed on a restricted cell or not. here it's true. ''' def __init__(self, pid, row, column): DefenderPiece.__init__(self, pid, row, column) pg.sprite.Sprite.__init__(self, self.groups) self.color = KING_PIECE_COLOR self.permit_to_res_sp = True self.ptype = "k" def match_specific_global_data(): ''' This function declares and initiates all sprite groups. Global Properties: 1. All_pieces: a srpite group containing all pieces. 2. Attacker_pieces: a srpite group containing all attacker pieces. 3. Defender_pieces: a srpite group containing all defender pieces. 4. King_pieces: a srpite group containing all king piece. Returns ------- None. ''' global All_pieces, Attacker_pieces, Defender_pieces, King_pieces All_pieces = pg.sprite.Group() Attacker_pieces = pg.sprite.Group() Defender_pieces = pg.sprite.Group() King_pieces = pg.sprite.Group() ChessPiece.groups = All_pieces AttackerPiece.groups = All_pieces, Attacker_pieces DefenderPiece.groups = All_pieces, Defender_pieces KingPiece.groups = All_pieces, Defender_pieces, King_pieces class Game_manager: ''' This class handles all the events within the game. Properties: 1. screen: a pygame display or surface. holds the current screen where the game is played on. 2. board: a ChessBoard object. this board is used in current game. 3. turn: a boolean value. default is True. this value decides whose turn it is - attackers' or defenders'. 4. king_escape: a boolean value. dafult is false. this variable tells whether the king is captured or not. 5. king_captured: a boolean value. default is false. this variable tells whether the king escaped or not. 6. all_attackers_killed: a boolean value. default is false. this variable tells if all attackers are killed or not. 7. finish: a boolean value. default is false. this variable tells whether a match finishing condition is reached or not. 8. already_selected: a ChessPiece object, or any of it's child class object. this varaible holds currenlty selected piece. 9. is_selected: a boolean value. default is false. this variable tells whether any piece is selected or not. 10. valid_moves: a list of pair. this list contains all the valid move indices- (row, column) of currently selected piece. 11. valid_moves_positions: a list of pair. this list contains all the valid move pixel positions- (x_pos, y_pos) of currently selected piece. 12. current_board_status: a list of lists. this holds current positions of all pieces i.e. current board pattern. 13. current_board_status_with_border: a list of lists. this holds current positions of all pieces i.e. current board pattern along with border index. (this is redundent I know, but, it's needed for avoiding complexity) 14. mode: 0 means p-vs-p, 1 means p-vs-ai this variable holds game mode. 15. last_move: pair of pairs of indecies - ((prev_row, prev_col), (curr_row, curr_col)) this variable holds the 'from' and 'to' of last move. 16. board_size: "large" means 11x11, "small" means 9x9, default is "large" this variable holds board sizes. Methods: 1. select_piece(selected_piece): to select a piece. 2. find_valid_moves(): finds valid moves of selected piece. 3. show_valid_moves(): draws the indicator of valid moves on board. 4. deselect(): deselects currently selected piece. 5. update_board_status(): updates board status after each move. 6. capture_check(): contains capture related logics. 7. king_capture_check(): contains caturing-king related logics. 8. escape_check(): contains king-escape related logics. 9. attackers_count_check(): counts currently unkilled attacker pieces. 10. match_finished(): performs necessary tasks when match ends. 11. mouse_click_analyzer(msx, msy): analyzes current mouse click action and performs necessary functionalites. 12. turn_msg(): displays info about whose turn it is. 13. ai_move_manager(piece, row, column): handles ai moves ''' def __init__(self, screen, board, mode, board_size="large"): self.screen = screen self.board = board self.turn = True self.king_escaped = False self.king_captured = False self.all_attackers_killed = False self.finish = False self.already_selected = None self.is_selected = False self.valid_moves = [] self.valid_moves_positions = [] self.current_board_status = [] self.current_board_status_with_border = [] self.mode = mode self.last_move = None self.board_size = board_size # initiating current_board_status and current_board_status_with_border. # initially board is in initial_pattern # appending top border row border = [] for column in range(self.board.columns + 2): border.append("=") self.current_board_status_with_border.append(border) # appending according to initial_pattern for row in self.board.initial_pattern: bordered_row = ["="] # to add a left border one_row = [] for column in row: one_row.append(column) bordered_row.append(column) self.current_board_status.append(one_row) bordered_row.append("=") # to add a right border self.current_board_status_with_border.append(bordered_row) # appending bottom border row self.current_board_status_with_border.append(border) def select_piece(self, selected_piece): ''' This method selects a piece. Parameters ---------- selected_piece : a ChessPiece or it's child class object. assigns this piece to already_selected variable. Returns ------- None. ''' self.is_selected = True self.already_selected = selected_piece self.find_valid_moves() def find_valid_moves(self): ''' This method finds valid moves of selected piece. Returns ------- None. ''' self.valid_moves = [] tempr = self.already_selected.row tempc = self.already_selected.column # finding valid moves in upwards direction tempr -= 1 while tempr >= 0: # stores current row and column thispos = self.current_board_status[tempr][tempc] # if finds any piece, no move left in this direction anymore if thispos == "a" or thispos == "d" or thispos == "k" or (thispos == "x" and self.already_selected.ptype != "k"): break else: # if selected piece is king, only one move per direction is allowed # if self.already_selected.ptype == "k": # if tempr < self.already_selected.row - 1 or tempr > self.already_selected.row + 1: # break # self.valid_moves.append((tempr, tempc)) # else: # "." means empty cell if thispos == ".": self.valid_moves.append((tempr, tempc)) tempr -= 1 tempr = self.already_selected.row tempc = self.already_selected.column # finding valid moves in downwards direction tempr += 1 while tempr < self.board.rows: # stores current row and column thispos = self.current_board_status[tempr][tempc] # if finds any piece, no move left in this direction anymore if thispos == "a" or thispos == "d" or thispos == "k" or (thispos == "x" and self.already_selected.ptype != "k"): break else: # if selected piece is king, only one move per direction is allowed # if self.already_selected.ptype == "k": # if tempr < self.already_selected.row - 1 or tempr > self.already_selected.row + 1: # break # self.valid_moves.append((tempr, tempc)) # else: # # "." means empty cell if thispos == ".": self.valid_moves.append((tempr, tempc)) tempr += 1 tempr = self.already_selected.row tempc = self.already_selected.column # finding valid moves in left direction tempc -= 1 while tempc >= 0: # stores current row and column thispos = self.current_board_status[tempr][tempc] # if finds any piece, no move left in this direction anymore if thispos == "a" or thispos == "d" or thispos == "k" or (thispos == "x" and self.already_selected.ptype != "k"): break else: # if selected piece is king, only one move per direction is allowed # if self.already_selected.ptype == "k": # if tempc < self.already_selected.column - 1 or tempc > self.already_selected.column + 1: # break # self.valid_moves.append((tempr, tempc)) # else: # "." means empty cell if thispos == ".": self.valid_moves.append((tempr, tempc)) tempc -= 1 tempr = self.already_selected.row tempc = self.already_selected.column # finding valid moves in right direction tempc += 1 while tempc < self.board.columns: # stores current row and column thispos = self.current_board_status[tempr][tempc] # if finds any piece, no move left in this direction anymore if thispos == "a" or thispos == "d" or thispos == "k" or (thispos == "x" and self.already_selected.ptype != "k"): break else: # if selected piece is king, only one move per direction is allowed # if self.already_selected.ptype == "k": # if tempc < self.already_selected.column - 1 or tempc > self.already_selected.column + 1: # break # self.valid_moves.append((tempr, tempc)) # else: # "." means empty cell if thispos == ".": self.valid_moves.append((tempr, tempc)) tempc += 1 # for each (row, column) index of each valid move, corresponding pixel position is stored for position in self.valid_moves: self.valid_moves_positions.append((BOARD_LEFT + int(CELL_WIDTH / 2) + position[1]*CELL_WIDTH, BOARD_TOP + int(CELL_HEIGHT / 2) + position[0]*CELL_HEIGHT)) def show_valid_moves(self): ''' This method draws the indicator of valid moves on board. Returns ------- None. ''' # iterating over valid moves positions and drawing them on board for index in self.valid_moves_positions: pg.draw.circle(self.screen, VALID_MOVE_INDICATOR_COLOR, index, VALID_MOVE_INDICATOR_RADIUS) def deselect(self): ''' This method deselects currently selected piece. Returns ------- None. ''' self.is_selected = False self.already_selected = None self.valid_moves = [] self.valid_moves_positions = [] def update_board_status(self): ''' This method updates board status after each move. Returns ------- None. ''' self.current_board_status = [] self.current_board_status_with_border = [] # adding top border row border = [] for column in range(self.board.columns + 2): border.append("=") self.current_board_status_with_border.append(border) # first setting all cells as empty cells, then making changes where necessary for row in range(self.board.rows): bordered_row = ["="] # left border one_row = [] for column in range(self.board.columns): one_row.append(".") bordered_row.append(".") # if row == 0 or row == self.board.rows - 1: # one_row[0] = "x" # one_row[self.board.columns-1] = "x" # bordered_row[1] = "x" # bordered_row[self.board.columns] = "x" self.current_board_status.append(one_row) bordered_row.append("=") # right border self.current_board_status_with_border.append(bordered_row) # adding bottom border self.current_board_status_with_border.append(border) # according to each piece's positions, updating corresponding (row, column) value for piece in All_pieces: self.current_board_status[piece.row][piece.column] = piece.ptype # adding an extra 1 because 0th row and 0th column is border self.current_board_status_with_border[piece.row + 1][piece.column+1] = piece.ptype # initial pattern set middle cell as empty cell. but, if it is actually an restricted cell. # if it doesn't contain king, it's marked as "x'. if self.current_board_status[int(self.board.rows/2)][int(self.board.columns/2)] != "k": self.current_board_status[int( self.board.rows/2)][int(self.board.columns/2)] = "x" self.current_board_status_with_border[int( self.board.rows/2)+1][int(self.board.columns/2)+1] = "x" def capture_check(self): ''' This method contains capture related logics. Returns ------- None. ''' # storing current piece's type and index ptype, prow, pcol = self.already_selected.ptype, self.already_selected.row + \ 1, self.already_selected.column+1 # indices of sorrounding one hop cells and two hops cells. sorroundings = [(prow, pcol+1), (prow, pcol-1), (prow-1, pcol), (prow+1, pcol)] two_hop_away = [(prow, pcol+2), (prow, pcol-2), (prow-2, pcol), (prow+2, pcol)] # iterating over each neighbour cells and finding out if the piece of this cell is captured or not for pos, item in enumerate(sorroundings): # currently selected cell's piece, if any opp = self.current_board_status_with_border[item[0]][item[1]] # if index is 1, which means it's right beside border, which means there's no two-hop cell in thi direction # it may overflow the list index, so it will be set as empty cell instead to avoid error try: opp2 = self.current_board_status_with_border[two_hop_away[pos] [0]][two_hop_away[pos][1]] except: opp2 = "." # if next cell is empty or has same type of piece or has border, no capturing is possible # if two hop cell is empty, then also no capturing is possible if ptype == opp or ptype == "x" or ptype == "=" or opp == "." or opp2 == ".": continue elif opp == "k": # king needs 4 enemies on 4 cardinal points to be captured. so, handled in another function. self.king_capture_check(item[0], item[1]) # #print(self.king_captured) elif ptype != opp: # neghbour cell's piece is of different type if (ptype == "a" and (opp2 == self.already_selected.ptype or opp2 == "x")) or \ (ptype in ["d","k"] and (opp2 == "a" or opp2 == "x")): # Capture logic for both attackers and defenders for piece in All_pieces: if piece.ptype == opp and piece.row == item[0]-1 and piece.column == item[1]-1: pg.mixer.Sound.play(pg.mixer.Sound(kill_snd_1)) piece.kill() self.update_board_status() break elif ptype != "a" and opp2 != "a" and opp2 != "=" and opp == "a": # d-a-d or k-a-d or d-a-k or d-a-res_cell or k-a-res_cell situation for piece in All_pieces: if piece.ptype == opp and piece.row == sorroundings[pos][0]-1 and piece.column == sorroundings[pos][1]-1: pg.mixer.Sound.play(pg.mixer.Sound(kill_snd_1)) piece.kill() self.update_board_status() break # if self.already_selected.ptype == "k": # for dx, dy in [(0,1),(0,-1),(1,0),(-1,0)]: # adj_piece = self.current_board_status_with_border[prow+dx][pcol+dy] # if adj_piece[0] == "d": # opposite_pos = self.current_board_status_with_border[prow-dx][pcol-dy] # if opposite_pos[0] == "a": # # Capture attacker between king and defender # for piece in All_pieces: # if piece.ptype == "a" and piece.row == (prow-dx)-1 and piece.column == (pcol-dy)-1: # pg.mixer.Sound.play(pg.mixer.Sound(kill_snd_1)) # piece.kill() # self.update_board_status() if self.king_captured: self.finish = True pg.mixer.Sound.play(pg.mixer.Sound(lose_snd_1)) def king_capture_check(self, kingr, kingc): ''' This method contains caturing-king related logics. Parameters ---------- kingr : integer row index of king piece. kingc : integer column index of king piece. Returns ------- None. ''' # store all four neighbor cells' pieces king_pos = (kingr-1, kingc-1) # Convert to board coordinates attackers = 0 castle_adjacent = False # Check if king is adjacent to castle if abs(king_pos[0]-4) + abs(king_pos[1]-4) == 1: castle_adjacent = True # Check four cardinal directions for dx, dy in [(0,1),(0,-1),(1,0),(-1,0)]: adj = self.current_board_status_with_border[kingr+dx][kingc+dy] if adj[0] == "a": attackers += 1 elif adj == "x" and (kingr+dx-1, kingc+dy-1) == (4,4): attackers += 1 # Castle counts as attacker for adjacent king # Determine capture conditions if king_pos == (4,4): # Center self.king_captured = attackers >= 4 elif castle_adjacent: # Next to castle self.king_captured = attackers >= 3 else: # Regular position self.king_captured = attackers >= 2 def escape_check(self): ''' This method checks if the king has escaped or not. Returns ------- None. ''' king = next(p for p in King_pieces) # Check all edge cells edge_cells = ( any(row[0] == 'k' for row in self.current_board_status) or # First column any(row[-1] == 'k' for row in self.current_board_status) or # Last column 'k' in self.current_board_status[0] or # First row 'k' in self.current_board_status[self.board.rows-1] # Last row ) if edge_cells: self.king_escaped = True self.finish = True pg.mixer.Sound.play(pg.mixer.Sound(win_snd_1)) def attackers_count_check(self): ''' This method checks if all attackers are killed or not. Returns ------- None. ''' # only way attackers would win is by capturing king, so it's not necessary to check defenders' count # Attacker_pieces sprite group holds all attackers if len(Attacker_pieces) == 0: self.all_attackers_killed = True self.finish = True pg.mixer.Sound.play(pg.mixer.Sound(win_snd_1)) def match_finished(self): ''' This method displays necessary messages when the match finishes. Returns ------- None. ''' consolas = pg.font.SysFont("consolas", 22) if self.king_captured: if self.mode == 0: write_text(">>> KING CAPTURED !! ATTACKERS WIN !!", self.screen, (20, BOARD_TOP - 80), white, consolas, False) else: write_text(">>> KING CAPTURED !! AI WINS !!", self.screen, (20, BOARD_TOP - 80), white, consolas, False) elif self.king_escaped: write_text(">>> KING ESCAPED !! DEFENDERS WIN !!", self.screen, (20, BOARD_TOP - 80), white, consolas, False) elif self.all_attackers_killed: write_text(">>> ALL ATTACKERS DEAD !! DEFENDERS WIN !!", self.screen, (20, BOARD_TOP - 80), white, consolas, False) else: pass def mouse_click_analyzer(self, msx, msy): ''' This method analyzes a mouse click event. This is the heart of Game_manager class. Parameters ---------- msx : integer the row index of mouse clicked position. msy : integer the column index of mouse clicked position. Returns ------- None. ''' # if no piece is selected before and the player selects a piece, the valid moves of that piece is displayed if not self.is_selected: for piece in All_pieces: # collidepoint was not working (dunno why...). so, instead, made a custom logic # if mouse click position is within a distant of radius of piece from the center of so, it means it is clicked # iterates over all pieces to find out which piece satiefies such condition if (msx >= piece.center[0] - PIECE_RADIUS) and (msx < piece.center[0] + PIECE_RADIUS): if (msy >= piece.center[1] - PIECE_RADIUS) and (msy < piece.center[1] + PIECE_RADIUS): if (piece.ptype == "a" and self.turn) or (piece.ptype != "a" and not self.turn): self.select_piece(piece) break elif (self.already_selected.ptype != "a" and self.turn) or (self.already_selected.ptype == "a" and not self.turn): # opponent piece is selected, so previously selected piece will be deselected self.deselect() else: # some piece was selected previously # gonna check multiple scenerioes serially; if any meets requirement, 'done' flag will stop checking more done = False for piece in All_pieces: if (msx >= piece.center[0] - PIECE_RADIUS) and (msx < piece.center[0] + PIECE_RADIUS): if (msy >= piece.center[1] - PIECE_RADIUS) and (msy < piece.center[1] + PIECE_RADIUS): done = True if piece == self.already_selected: # previously selected piece is selected again, so it will be deselected self.deselect() break else: # some other piece of same side is selected # so previous one will be deselected and current will be selected self.deselect() if (piece.ptype == "a" and self.turn) or (piece.ptype != "a" and not self.turn): self.select_piece(piece) break if not done: # a valid move was selected for previously selected piece, so it will move to that new cell position for ind, pos in enumerate(self.valid_moves_positions): if (msx >= pos[0] - PIECE_RADIUS) and (msx < pos[0] + PIECE_RADIUS): if (msy >= pos[1] - PIECE_RADIUS) and (msy < pos[1] + PIECE_RADIUS): # updating piece's position prev = (self.already_selected.row, self.already_selected.column) self.already_selected.update_piece_position( self.valid_moves[ind][0], self.valid_moves[ind][1]) curr = (self.already_selected.row, self.already_selected.column) self.last_move = (prev, curr) # updating board status self.update_board_status() # playing a sound effect pg.mixer.Sound.play(pg.mixer.Sound(move_snd_1)) # checking if any opponent piece was captured or not self.capture_check() # checking if selected piece is king or not # if it is, then checking if it's escaped or not if self.already_selected.ptype == "k": self.escape_check() # if it was defender's turn, checking if all of the attackers are captured or not if self.already_selected.ptype != "a": self.attackers_count_check() # altering turn; a to d or d to a self.turn = not self.turn done = True break self.deselect() def ai_move_manager(self, piece, row, column): ''' This function handles functionalities after AI chooses which piece to move Parameters ---------- piece : AI's choosen piece row : row index column : column index Returns ------- None. ''' # updating piece's position self.already_selected = piece prev = (self.already_selected.row, self.already_selected.column) self.already_selected.update_piece_position(row-1, column-1) curr = (row-1, column-1) self.last_move = (prev, curr) # updating board status self.update_board_status() # playing a sound effect pg.mixer.Sound.play(pg.mixer.Sound(move_snd_1)) # self.already_selected = self.ai_selected # checking if any opponent piece was captured or not self.capture_check() # checking if selected piece is king or not # if it is, then checking if it's escaped or not if self.already_selected.ptype == "k": self.escape_check() # if it was defender's turn, checking if all of the attackers are captured or not if self.already_selected.ptype != "a": self.attackers_count_check() # altering turn; a to d or d to a self.turn = not self.turn self.deselect() def turn_msg(self, game_started): ''' This method shows message saying whose turn it is now. Returns ------- None. ''' consolas = pg.font.SysFont("consolas", 22) if not game_started: if self.mode == 0: write_text(">>> Click 'New Game' to start a new game.", self.screen, (20, BOARD_TOP - 80), white, consolas, False) else: write_text(">>> Click 'New Game' to start a new game. AI is attacker and you are defender.", self.screen, (20, BOARD_TOP - 80), white, consolas, False) elif self.mode == 0 and self.turn: write_text(">>> Attacker's Turn", self.screen, (20, BOARD_TOP - 80), white, consolas, False) elif self.mode == 1 and self.turn: write_text(">>> AI is thinking...", self.screen, (20, BOARD_TOP - 80), white, consolas, False) else: write_text(">>> Defender's Turn", self.screen, (20, BOARD_TOP - 80), white, consolas, False) class AI_manager: def __init__(self, manager, screen): self.manager = manager self.screen = screen def move(self): ''' AI uses this function to move a piece. Returns ------- None. ''' current_board = [] rows = self.manager.board.rows columns = self.manager.board.columns self.rows = rows self.columns = columns # creating pattern such as # [['x', '.', '.', 'a1', 'a2', 'a3', 'a4', 'a5', '.', '.', 'x'], # ['.', '.', '.', '.', '.', 'a6', '.', '.', '.', '.', '.'], # ['.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.'], # ['a7', '.', '.', '.', '.', 'd1', '.', '.', '.', '.', 'a8'], # ['a9', '.', '.', '.', 'd2', 'd3', 'd4', '.', '.', '.', 'a10'], # ['a11', 'a12', '.', 'd5', 'd6', 'k', 'd7', 'd8', '.', 'a13', 'a14'], # ['a15', '.', '.', '.', 'd9', 'd10', 'd11', '.', '.', '.', 'a16'], # ['a17', '.', '.', '.', '.', 'd12', '.', '.', '.', '.', 'a18'], # ['.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.'], # ['.', '.', '.', '.', '.', 'a19', '.', '.', '.', '.', '.'], # ['x', '.', '.', 'a20', 'a21', 'a22', 'a23', 'a24', '.', '.', 'x']] current_board = [] border_row = [] for column in range(columns+2): border_row.append("=") #adding border current_board.append(border_row) for row in range(rows): one_row = ["="] for column in range(columns): one_row.append('.') one_row.append("=") #adding border current_board.append(one_row) current_board.append(border_row) for piece in All_pieces: current_board[piece.row+1][piece.column+1] = piece.pid #corresponding id mapping for all the pieces in board current_board[1][1] = current_board[1][rows] = current_board[rows][1] = current_board[rows][columns] = 'x' #creating these positions as restricted for all except king if current_board[int((self.rows+1)/2)][int((self.columns+1)/2)] != 'k': current_board[int((self.rows+1)/2)][int((self.columns+1)/2)] = 'x' # find all possible valid move and return -> list[piece, (pair of indices)] piece, best_move = self.find_best_move(current_board) row, col = best_move # perform the move self.manager.ai_move_manager(piece, row, col) def find_all_possible_valid_moves(self, board_status_at_this_state, fake_turn): ''' AI uses this fucntion to finds out all valid moves of all pieces of a type. Parameters ---------- board_status_at_this_state : a 2d matrix at any state of evaluation, ai feeds that state's board status here to calculate moves fake_turn : boolean True - attackers' turn, False - defenders' turn Returns ------- valid_moves : a list of pairs - [(str, (int, int))] (piece_pid, (row, column)) ''' valid_moves = [] piece_pos_this_state = {} for row_ind, row in enumerate(board_status_at_this_state): for col_ind, column in enumerate(row): if column != "." and column != "x" and column != "=": piece_pos_this_state[column] = (row_ind, col_ind) for each in piece_pos_this_state.keys(): piece = each[0] # find moves for a side only if it's their turn if (fake_turn and not piece[0] == "a") or (not fake_turn and piece[0] == "a"): continue tempr = piece_pos_this_state[each][0] tempc = piece_pos_this_state[each][1] # finding valid moves in upwards direction tempr -= 1 while tempr >= 0: # stores current row and column thispos = board_status_at_this_state[tempr][tempc][0] # if finds any piece, no move left in this direction anymore if thispos == "a" or thispos == "d" or thispos == "k" or thispos == "=" or (thispos == "x" and piece != "k"): break else: pass # this part is commented out because so far ai is only attacker and this part checks both 'a' or 'd' # # if selected piece is king, only one move per direction is allowed if piece == "k": if tempr < piece_pos_this_state[each][0] - 1 or tempr > piece_pos_this_state[each][0] + 1: break valid_moves.append( (piece_pid_map[each], (tempr, tempc))) else: # "." means empty cell if thispos == ".": valid_moves.append( (piece_pid_map[each], (tempr, tempc))) tempr -= 1 tempr = piece_pos_this_state[each][0] tempc = piece_pos_this_state[each][1] # finding valid moves in downwards direction tempr += 1 while tempr < self.manager.board.rows+2: # stores current row and column thispos = board_status_at_this_state[tempr][tempc][0] # if finds any piece, no move left in this direction anymore if thispos == "a" or thispos == "d" or thispos == "k" or thispos == "=" or (thispos == "x" and piece != "k"): break else: # # if selected piece is king, only one move per direction is allowed if piece == "k": if tempr < piece_pos_this_state[each][0] - 1 or tempr > piece_pos_this_state[each][0] + 1: break valid_moves.append( (piece_pid_map[each], (tempr, tempc))) else: # "." means empty cell if thispos == ".": valid_moves.append( (piece_pid_map[each], (tempr, tempc))) tempr += 1 tempr = piece_pos_this_state[each][0] tempc = piece_pos_this_state[each][1] # finding valid moves in left direction tempc -= 1 while tempc >= 0: # stores current row and column thispos = board_status_at_this_state[tempr][tempc][0] # if finds any piece, no move left in this direction anymore if thispos == "a" or thispos == "d" or thispos == "k" or thispos == "=" or (thispos == "x" and piece != "k"): break else: # # if selected piece is king, only one move per direction is allowed if piece == "k": if tempc < piece_pos_this_state[each][1] - 1 or tempc > piece_pos_this_state[each][1] + 1: break valid_moves.append( (piece_pid_map[each], (tempr, tempc))) else: # "." means empty cell if thispos == ".": valid_moves.append( (piece_pid_map[each], (tempr, tempc))) tempc -= 1 tempr = piece_pos_this_state[each][0] tempc = piece_pos_this_state[each][1] # finding valid moves in right direction tempc += 1 while tempc < self.manager.board.columns+2: # stores current row and column thispos = board_status_at_this_state[tempr][tempc][0] # if finds any piece, no move left in this direction anymore if thispos == "a" or thispos == "d" or thispos == "k" or thispos == "=" or (thispos == "x" and piece != "k"): break else: # # if selected piece is king, only one move per direction is allowed if piece == "k": if tempc < piece_pos_this_state[each][1] - 1 or tempc > piece_pos_this_state[each][1] + 1: break valid_moves.append( (piece_pid_map[each], (tempr, tempc))) else: # "." means empty cell if thispos == ".": valid_moves.append( (piece_pid_map[each], (tempr, tempc))) tempc += 1 return valid_moves def king_mobility(self, fake_board, r, c): ''' THis function checks how many cells can king move at current state Parameters ---------- fake_board : board status at that state r : row of king c : column of king Returns ------- score : number of cells king can move to ''' score = 0 i = c-1 while(i != '='): if fake_board[r][i] == '.' or fake_board[r][i] == 'x': score += 1 else: break i -= 1 i = c+1 while(i != '='): if fake_board[r][i] == '.' or fake_board[r][i] == 'x': score += 1 else: break i += 1 i = r-1 while(i != '='): if fake_board[i][c] == '.' or fake_board[i][c] == 'x': score += 1 else: break i -= 1 i = r+1 while(i != '='): if fake_board[i][c] == '.' or fake_board[i][c] == 'x': score += 1 else: break i += 1 return score def king_sorrounded(self, fake_board, r, c): ''' Finds out how many attacekrs are sorrounding king at current board state. Parameters ---------- fake_board : board status at that state r : row of king c : column of king Returns ------- score : number of sorrounding attackers. ''' score = 0 if fake_board[r][c+1][0] == 'a': score += 1 if fake_board[r][c-1][0] == 'a': score += 1 if fake_board[r-1][c][0] == 'a': score += 1 if fake_board[r+1][c][0] == 'a': score += 1 return score def evaluate(self, fake_board): ''' This function evaluates current board state using a predefined heuristic value. Heart of AI... Parameters ---------- fake_board : current board state. Returns ------- score : calculated cost/value of this state. ''' # heuristic values weight_pos = 5 # for 11x11 board ''' here king position is weighted depending on how close it is to escape points , the closer it gets the larger the points are ''' weight_king_pos_11 = [[10000, 10000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 10000, 10000], [10000, 500, 500, 500, 500, 500, 500, 500, 500, 500, 10000], [1000, 500, 200, 200, 200, 200, 200, 200, 200, 500, 1000], [1000, 500, 200, 50, 50, 50, 50, 50, 200, 500, 1000], [1000, 500, 200, 50, 10, 10, 10, 50, 200, 500, 1000], [1000, 500, 200, 50, 10, 0, 10, 50, 200, 500, 1000], [1000, 500, 200, 50, 10, 10, 10, 50, 200, 500, 1000], [1000, 500, 200, 50, 50, 50, 50, 50, 200, 500, 1000], [1000, 500, 200, 200, 200, 200, 200, 200, 200, 500, 1000], [10000, 500, 500, 500, 500, 500, 500, 500, 500, 500, 10000], [10000, 10000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 10000, 10000]] # for 9x9 board weight_king_pos_9 = [[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000], [10000, 500, 500, 500, 500, 500, 500, 500, 10000], [10000, 500, 150, 150, 150, 150, 150, 500, 10000], [10000, 500, 150, 30, 30, 30, 150, 500, 10000], [10000, 500, 150, 30, 0, 30, 150, 500, 10000], [10000, 500, 150, 30, 30, 30, 150, 500, 10000], [10000, 500, 150, 150, 150, 150, 150, 500, 10000], [10000, 500, 500, 500, 500, 500, 500, 500, 10000], [10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]] if self.manager.board_size == "large": weight_king_pos = weight_king_pos_11 weight_attacker = 12 # weight is given because inequal number of attacker and defender weight_defender = 24 weight_king_sorrounded = 50000 else: weight_king_pos = weight_king_pos_9 weight_attacker = 8 # weight is given because inequal number of attacker and defender weight_defender = 12 weight_king_sorrounded = 10000 #weight_king_sorrounded = 50000 attacker = 0 # attacker count defender = 0 # defender count score = 0 if self.fake_gameOver(fake_board) == 1: # if 1 then winner is attacker print("c") score += 10000000 #if attacker wins a large score is added as it will help to maximize the score return score elif self.fake_gameOver(fake_board) == 2: # if 1 then winner is defender score -= 10000000 #if attacker wins a large score is subtracted as it will help to maximize the score return score # finding number of attackers and defenders currently on board # searching king position for row_index, row in enumerate(fake_board): for col_index, col in enumerate(row): if(col == 'k'): r = row_index c = col_index elif(col[0] == 'a'): attacker += 1 elif(col[0] == 'd'): defender += 1 # making dynamic heuristic evaluation to prioritize on restricting movement of king when he is close to corner cells ''' this dynamic heuristic helps to make decision for ai when king is obviously escaping in next few turns. Then the attacker will prioritize to prevent the king from escaping above anything else ''' if r-3 <= 1 and c-3 <= 1: if fake_board[1][2][0] == 'a': score += 1000 if fake_board[2][1][0] == 'a': score += 1000 elif r-3 <= 1 and c+3 >=(self.columns): if fake_board[1][self.columns-1][0] == 'a': score += 1000 if fake_board[2][self.columns][0] == 'a': score += 1000 elif r+3 >= (self.rows) and c-3 <= 1: if fake_board[self.rows-1][1][0] == 'a': score += 1000 if fake_board[self.rows][2][0] == 'a': score += 1000 elif r+3 >=(self.rows) and c+3 >=(self.columns): if fake_board[self.rows][self.columns-1][0] == 'a': score += 1000 if fake_board[self.rows-1][self.columns][0] == 'a': score += 1000 score += (attacker*weight_attacker) score -= (defender*weight_defender) score -= (weight_pos*weight_king_pos[r-1][c-1]) score += (weight_king_sorrounded * self.king_sorrounded(fake_board, r, c)) return score def fake_move(self, fake_board, commited_move): ''' This function performs a fake move - AI's imaginative move in alpha-beta pruning Fake move actually determines the score if this move is executed, it is not any actual move Parameters ---------- fake_board : this state's board status commited_move : which and where to move - (piece.pid, (row, column)) Returns ------- current_board : board status after commiting that move diff : difference of number of uncaptured pieces on both sides ''' # fake board = current state fake board, commited move=the move to be executed # (piece, (where to)) current_board = [] for row in range(len(fake_board)): one_row = [] for column in range(len(fake_board[0])): one_row.append(".") current_board.append(one_row) for row_index, row in enumerate(fake_board): for col_index, column in enumerate(row): current_board[row_index][col_index] = column for row_index, row in enumerate(current_board): f = True for column_index, col in enumerate(row): if(commited_move[0].pid == col): current_board[row_index][column_index] = "." f = False break if not f: break # row = int((self.rows+1)/2) # column = int((self.columns+1)/2) if current_board[int((self.rows+1)/2)][int((self.columns+1)/2)] == ".": current_board[int((self.rows+1)/2)][int((self.columns+1)/2)] = 'x' current_board[commited_move[1][0]][commited_move[1] [1]] = commited_move[0].pid current_board, king_captured = self.fake_capture_check( current_board, commited_move) attacker = 0 defender = 0 for row_index, row in enumerate(current_board): for col_index, col in enumerate(row): if(col[0] == 'a'): attacker += 1 elif(col[0] == 'd'): defender += 1 if current_board[int((self.rows+1)/2)][int((self.columns+1)/2)] == ".": current_board[int((self.rows+1)/2)][int((self.columns+1)/2)] = 'x' return current_board, attacker-defender def minimax(self, fake_board, alpha, beta, max_depth, turn): ''' Implementation of minimax algorithm. Parameters ---------- fake_board : current fake state's board alpha : integer beta : integer max_depth : number of step to dive into the tree turn : True for attackers, False for defenders Returns ------- bestvalue: the best value evaluated ''' bestvalue = -10000000000 moves = self.find_all_possible_valid_moves( fake_board, turn) # True attacker ,False Defender if max_depth <= 0 or self.fake_gameOver(fake_board) == 1 or self.fake_gameOver(fake_board) == 2: return self.evaluate(fake_board) # fake board is copied into current board current_board = [] for row in range(len(fake_board)): one_row = [] for column in range(len(fake_board[0])): one_row.append(".") current_board.append(one_row) for row_index, row in enumerate(fake_board): for col_index, column in enumerate(row): current_board[row_index][col_index] = column # commit a move from valid moves list -> evaluate -> pick bestvalue -> alpha-beta computing if(turn == True): # attacker maximizer bestvalue = -1000000000000000000 for i in moves: tmp_fake_board, diff = self.fake_move(current_board, i) value = self.minimax(tmp_fake_board, alpha, beta, max_depth-1, False) #calling minimax function recursively for next possible moves bestvalue = max(value, bestvalue) alpha = max(alpha, bestvalue) if(beta <= alpha): # alpha beta pruning break else: # defender minimizer bestvalue = 1000000000000000000 for i in moves: tmp_fake_board, diff = self.fake_move(current_board, i) value = self.minimax(tmp_fake_board, alpha, beta, max_depth-1, True) bestvalue = min(value, bestvalue) beta = min(beta, bestvalue) # alpha beta pruning if(beta <= alpha): break return bestvalue def strategy(self, current_board): ''' Brain of AI... Parameters ---------- current_board : current state's board Returns ------- bestmove : best move for this state to be committed by AI ''' # value to calcaute the move with best minimax value bestvalue = -1000000000000000000 max_depth = 3 # True attacker,False Defender # moves = (piece_object,(row,col)) moves = self.find_all_possible_valid_moves(current_board, True) c = 0 diffs = {} for i in moves: # iterate all possible valid moves and their corersponding min max value c += 1 fake_board, diff = self.fake_move(current_board, i) value = self.minimax(fake_board, -1000000000000000000, 1000000000000000000, max_depth-1, False) print(value, i[1], diff) if(value > bestvalue): bestmove = i #pick the best move which gives the maximum score for AI bestvalue = value diffs[value] = diff elif(value == bestvalue and diff > diffs[value]): bestmove = i bestvalue = value diffs[value] = diff if(value == bestvalue and (i[1] == (1, 2) or i[1] == (2, 1) or i[1] == (1, self.columns-1) or i[1] == (2, self.columns) or i[1] == (self.rows-1, 1) or i[1] == (self.rows, 2) or i[1] == (self.rows-1, self.columns) or i[1] == (self.rows, self.columns-1))): bestmove = i return bestmove def find_best_move(self, current_board): ''' Calls algoritm. Parameters ---------- current_board : current state's board Returns ------- best_move : best move for this state to be committed by AI ''' best_move = self.strategy(current_board) return best_move def fake_gameOver(self, fake_board): ''' Check AI's minimax tree traversing has reached game over condition or not. Parameters ---------- fake_board : current fake state's board Returns ------- int 1 attacker win, 2 defender win, 3 none win ''' # 1 attacker win,2 defender win,3 none win if self.fake_king_capture_check(fake_board): return 1 elif self.fake_king_escape(fake_board) or self.fake_attacker_cnt(fake_board): return 2 else: return 3 def fake_capture_check(self, fake_board_with_border, move): ''' This method contains capture related logics at any fake state. Parameters ---------- fake_board_with_border : current fake state's board move : for which move the capture event might happen Returns ------- fake_board_with_border : current fake state's board king_captured : whether the king is captured or not - True or False ''' # storing current piece's type and index ptype, prow, pcol = move[0].pid[0], move[1][0], move[1][1] # indices of sorrounding one hop cells and two hops cells. sorroundings = [(prow, pcol+1), (prow, pcol-1), (prow-1, pcol), (prow+1, pcol)] two_hop_away = [(prow, pcol+2), (prow, pcol-2), (prow-2, pcol), (prow+2, pcol)] # iterating over each neighbour cells and finding out if the piece of this cell is captured or not for pos, item in enumerate(sorroundings): king_captured = False # currently selected cell's piece, if any opp = fake_board_with_border[item[0]][item[1]][0] # if index is 1, which means it's right beside border, which means there's no two-hop cell in thi direction # it may overflow the list index, so it will be set as empty cell instead to avoid error try: opp2 = fake_board_with_border[two_hop_away[pos] [0]][two_hop_away[pos][1]][0] except: opp2 = "." # if next cell is empty or has same type of piece or has border, no capturing is possible # if two hop cell is empty, then also no capturing is possible if ptype == opp or ptype == "x" or ptype == "=" or opp == "." or opp2 == ".": continue elif opp == "k": # king needs 4 enemies on 4 cardinal points to be captured. so, handled in another function. king_captured = self.fake_king_capture_check( fake_board_with_border) elif ptype != opp: # neghbour cell's piece is of different type if ptype == "a" and (ptype == opp2 or opp2 == "x"): # a-d-a or a-d-res_cell situation fake_board_with_border[item[0]][item[1]] = '.' elif ptype != "a" and opp2 != "a" and opp2 != "=" and opp == "a": # d-a-d or k-a-d or d-a-k or d-a-res_cell or k-a-res_cell situation fake_board_with_border[item[0]][item[1]] = '.' return fake_board_with_border, king_captured def fake_king_capture_check(self, fake_board_with_border): ''' This method contains caturing-king related logics. Parameters ---------- fake_board_with_border : current fake state's board Returns ------- bool True if captured, False if not. ''' # store all four neighbor cells' pieces for row_index, row in enumerate(fake_board_with_border): for col_index, col in enumerate(row): if col == "k": kingr = row_index kingc = col_index break front = fake_board_with_border[kingr][kingc+1][0] back = fake_board_with_border[kingr][kingc-1][0] up = fake_board_with_border[kingr-1][kingc][0] down = fake_board_with_border[kingr+1][kingc][0] # if all four sides has attackers or a 3-attackers-one-bordercell situation occurs, king is captured # all other possible combos are discarded if front == "x" or back == "x" or up == "x" or down == "x": return False elif front == "d" or back == "d" or up == "d" or down == "d": return False elif front == "." or back == "." or up == "." or down == ".": return False else: return True def fake_king_escape(self, fake_board): ''' Checks whether king has escaped in this fake state or not. Parameters ---------- fake_board : current fake state's board Returns ------- bool True if escaped, False if not. ''' r = self.manager.board.rows c = self.manager.board.columns if fake_board[1][1] == 'k' or fake_board[1][c] == 'k' or fake_board[r][1] == 'k' or fake_board[r][c] == 'k': return True def fake_attacker_cnt(self, fake_board): ''' Checks whether all attacekrs are captured in this fake state or not. Parameters ---------- fake_board : current fake state's board Returns ------- bool True if all are captured, False if not. ''' for row_index, row in enumerate(fake_board): for col_ind, col in enumerate(row): if col[0] == "a": return False return True def game_window(screen, mode): ''' This handles game. Parameters ---------- screen : surface on which surface the game will be played. mode : integer 0 means p vs p, 1 means p vs ai. Returns ------- None. ''' # intializing some needed instances match_specific_global_data() chessboard = ChessBoard(screen) chessboard.draw_empty_board() chessboard.initiate_board_pieces() manager = Game_manager(screen, chessboard, mode) if mode == 1: bot = AI_manager(manager, screen) tafle = True game_started = False while tafle: write_text("Play Vikings Chess", screen, (20, 20), (255, 255, 255), pg.font.SysFont("Arial", 40)) backbtn = Custom_button(750, 20, "Back", screen, pg.font.SysFont("Arial", 30)) write_text("Game Settings", screen, (WINDOW_WIDTH - 250, BOARD_TOP), (255, 255, 255), pg.font.SysFont("Arial", 25), False) write_text("Board Size:", screen, (WINDOW_WIDTH - 300, BOARD_TOP + SETTINGS_TEXT_GAP_VERTICAL + 10), (255, 255, 255), pg.font.SysFont("Arial", 20), False) size9by9btn = Custom_button(WINDOW_WIDTH - 300 + SETTINGS_TEXT_GAP_HORIZONTAL, BOARD_TOP + SETTINGS_TEXT_GAP_VERTICAL, "9x9", screen, pg.font.SysFont("Arial", 20), width=50, height=50) size11by11btn = Custom_button(WINDOW_WIDTH - 300 + SETTINGS_TEXT_GAP_HORIZONTAL*1.7, BOARD_TOP + SETTINGS_TEXT_GAP_VERTICAL, "11x11", screen, pg.font.SysFont("Arial", 20), width=50, height=50) backbtn = Custom_button(750, 20, "Back", screen, pg.font.SysFont("Arial", 30)) if game_started: txt = "Restart Game" else: txt = 'New Game' newgamebtn = Custom_button( 525, 20, txt, screen, pg.font.SysFont("Arial", 30)) if backbtn.draw_button(): pg.mixer.Sound.play(pg.mixer.Sound(click_snd)) main() if size9by9btn.draw_button(): pg.mixer.Sound.play(pg.mixer.Sound(click_snd)) game_started = False match_specific_global_data() chessboard = ChessBoard(screen, "small") chessboard.draw_empty_board() chessboard.initiate_board_pieces() manager = Game_manager(screen, chessboard, mode, "small") if mode == 1: bot = AI_manager(manager, screen) if size11by11btn.draw_button(): pg.mixer.Sound.play(pg.mixer.Sound(click_snd)) game_started = False match_specific_global_data() chessboard = ChessBoard(screen, "large") chessboard.draw_empty_board() chessboard.initiate_board_pieces() manager = Game_manager(screen, chessboard, mode, "large") if mode == 1: bot = AI_manager(manager, screen) if newgamebtn.draw_button(): last_board = manager.board_size game_started = True match_specific_global_data() chessboard = ChessBoard(screen, last_board) chessboard.draw_empty_board() chessboard.initiate_board_pieces() manager = Game_manager(screen, chessboard, mode, last_board) if mode == 1: bot = AI_manager(manager, screen) chessboard.draw_empty_board() for event in pg.event.get(): if event.type == pg.QUIT: pg.quit() if event.type == pg.KEYDOWN: if event.key == pg.K_ESCAPE: tafle = False if event.type == pg.MOUSEBUTTONDOWN and event.button == 1: msx, msy = pg.mouse.get_pos() if not manager.finish: if mode == 0: manager.mouse_click_analyzer(msx, msy) else: if manager.turn == False: manager.mouse_click_analyzer(msx, msy) chessboard.draw_empty_board() for piece in All_pieces: piece.draw_piece(screen) if manager.finish: manager.match_finished() else: manager.turn_msg(game_started) if manager.last_move is not None: pg.draw.circle(screen, red, (BOARD_LEFT+(manager.last_move[0][1]*CELL_WIDTH)+( CELL_WIDTH/2), BOARD_TOP+(manager.last_move[0][0]*CELL_HEIGHT)+(CELL_HEIGHT/2)), 5) pg.draw.circle(screen, white, (BOARD_LEFT+(manager.last_move[1][1]*CELL_WIDTH)+( CELL_WIDTH/2), BOARD_TOP+(manager.last_move[1][0]*CELL_HEIGHT)+(CELL_HEIGHT/2)), 5) pg.display.update() if game_started and mode == 1 and manager.turn and not manager.finish: chessboard.draw_empty_board() for piece in All_pieces: piece.draw_piece(screen) if manager.finish: manager.match_finished() else: manager.turn_msg(game_started) if manager.last_move is not None: pg.draw.circle(screen, red, (BOARD_LEFT+(manager.last_move[0][1]*CELL_WIDTH)+(CELL_WIDTH/2), BOARD_TOP+( manager.last_move[0][0]*CELL_HEIGHT)+(CELL_HEIGHT/2)), 5) pg.draw.circle(screen, white, (BOARD_LEFT+(manager.last_move[1][1]*CELL_WIDTH)+(CELL_WIDTH/2), BOARD_TOP+( manager.last_move[1][0]*CELL_HEIGHT)+(CELL_HEIGHT/2)), 5) pg.display.update() print("c") bot.move() for piece in All_pieces: piece.draw_piece(screen) manager.show_valid_moves() if manager.finish: manager.match_finished() else: manager.turn_msg(game_started) if manager.last_move is not None: pg.draw.circle(screen, red, (BOARD_LEFT+(manager.last_move[0][1]*CELL_WIDTH)+(CELL_WIDTH/2), BOARD_TOP+( manager.last_move[0][0]*CELL_HEIGHT)+(CELL_HEIGHT/2)), 5) pg.draw.circle(screen, white, (BOARD_LEFT+(manager.last_move[1][1]*CELL_WIDTH)+(CELL_WIDTH/2), BOARD_TOP+( manager.last_move[1][0]*CELL_HEIGHT)+(CELL_HEIGHT/2)), 5) pg.display.update() def rules(screen): tafle = True while tafle: write_text("Rules of Viking Chess", screen, (20, 20), (255, 255, 255), pg.font.SysFont("Arial", 40)) backbtn = Custom_button(750, 20, "Back", screen, pg.font.SysFont("Arial", 30)) if backbtn.draw_button(): pg.mixer.Sound.play(pg.mixer.Sound(click_snd)) main() msgs = [] msgs.append("> Turn based board game.") # msgs.append("> Two board sizes: 'large' - 11x11 and 'small' - 9x9.") msgs.append("> Center cell and four corner cells are called restricted cells.") msgs.append("> Excluding king, a-d count is 24-12 on large board and 16-8 on small board.") msgs.append("> All pieces except king can move any number of cells horizontally or vertically.") msgs.append("> King can move only one cell at a time.") msgs.append("> Only king can move to any of the restricted cells.") msgs.append("> Pieces, except king, can be captured by sandwitching them from both sides.") msgs.append("> Restricted cells can be used to sandwitch opponent.") msgs.append("> Only one opponent piece can be captured in single line with single move.") msgs.append("> Multiple pieces can be captured with a single move on cardinal points.") msgs.append("> To capture king, attackers need to sorround him on all four cardinal points.") msgs.append("> If king is captured, attackers win.") msgs.append("> If king escapes to any of the four corner cells, defenders win.") msgs.append("> If all attackers are captured, defenders win.") consolas = pg.font.SysFont("consolas", 20) cnt = 0 for msg in msgs: write_text(msg, screen, (20, BOARD_TOP - 80 + 40*cnt), white, consolas, False) cnt += 1 for event in pg.event.get(): if event.type == pg.QUIT: pg.quit() if event.type == pg.KEYDOWN: if event.key == pg.K_ESCAPE: tafle = False pg.display.update() def history(screen): tafle = True while tafle: write_text("History", screen, (20, 20), (255, 255, 255), pg.font.SysFont("Arial", 40)) backbtn = Custom_button(750, 20, "Back", screen, pg.font.SysFont("Arial", 30)) if backbtn.draw_button(): pg.mixer.Sound.play(pg.mixer.Sound(click_snd)) main() msgs = [] msgs.append("> Originated in Scandinavia.") msgs.append("> Developed from a Roman game called Ludus Latrunculorum.") msgs.append("> This game flourished until the arrival of chess.") msgs.append("> This game was revived back in nineteenth century.") consolas = pg.font.SysFont("consolas", 20) cnt = 0 for msg in msgs: write_text(msg, screen, (20, BOARD_TOP - 80 + 40*cnt), white, consolas, False) cnt += 1 for event in pg.event.get(): if event.type == pg.QUIT: pg.quit() if event.type == pg.KEYDOWN: if event.key == pg.K_ESCAPE: tafle = False pg.display.update() def main(): pg.init() screen = pg.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pg.display.set_caption(GAME_NAME) pg.display.set_icon(GAME_ICON) icon_rect = GAME_ICON_resized.get_rect( center=(500, MAIN_MENU_TOP_BUTTON_y-150)) game_on = True while game_on: for event in pg.event.get(): if event.type == pg.QUIT: game_on = False pg.quit() screen.fill(bg2) write_text("Welcome To Vikings Chess!", screen, (250, 20), (255, 255, 255), pg.font.SysFont("Arial", 50)) btn_font = pg.font.SysFont("Arial", 28) gamebtn_1 = Custom_button( MAIN_MENU_TOP_BUTTON_x - 110, MAIN_MENU_TOP_BUTTON_y, "Play vs Human", screen, btn_font) gamebtn_2 = Custom_button( MAIN_MENU_TOP_BUTTON_x + 110, MAIN_MENU_TOP_BUTTON_y, "Play vs AI", screen, btn_font) rulesbtn = Custom_button( MAIN_MENU_TOP_BUTTON_x, MAIN_MENU_TOP_BUTTON_y + 100, "Rules", screen, btn_font) # historybtn = Custom_button( # MAIN_MENU_TOP_BUTTON_x, MAIN_MENU_TOP_BUTTON_y + 200, "History", screen, btn_font) exitbtn = Custom_button( MAIN_MENU_TOP_BUTTON_x, MAIN_MENU_TOP_BUTTON_y + 200, "Exit", screen, btn_font) if gamebtn_1.draw_button(): pg.mixer.Sound.play(pg.mixer.Sound(click_snd)) game_window(screen, mode=0) if gamebtn_2.draw_button(): pg.mixer.Sound.play(pg.mixer.Sound(click_snd)) game_window(screen, mode=1) if rulesbtn.draw_button(): pg.mixer.Sound.play(pg.mixer.Sound(click_snd)) rules(screen) # if historybtn.draw_button(): # pg.mixer.Sound.play(pg.mixer.Sound(click_snd)) # history(screen) if exitbtn.draw_button(): pg.mixer.Sound.play(pg.mixer.Sound(click_snd)) game_on = False pg.quit() screen.blit(GAME_ICON_resized, (icon_rect)) pg.display.update() if __name__ == "__main__": main()