Untitled

 avatar
unknown
python
a year ago
19 kB
8
Indexable
import itertools
import os
import json

from gen_config import options as GAMEMODE_OPTIONS

DIM = 8

class Gamemode: # allows reference to gamemode e.g. Gamemode.default
    default = "regular"
    king_of_the_hill = "king of the hill"

class Position:
    def __init__(self, x:int, y:int, gamestate):
        self.x = x
        self.y = y
        self.piece_type = gamestate.get_piece(self)
    
    @property
    def pos(self) -> tuple:
        return self.x, self.y
    
    def __str__(self):
        return str(self.pos)
    
    def __eq__(self, other):
        if isinstance(other, Position) and self.pos == other.pos:
            return True
        return False


class Move:
    def __init__(self, start:Position, end:Position, gamestate):
        #// self.startx, self.starty = start
        #// self.endx, self.endy = end
        self.start, self.end = start, end
        self.start_piece = gamestate.get_piece(start)
        self.end_piece = gamestate.get_piece(end)
        #// self.id = int(str(self.startx)+str(self.starty)+str(self.endx)+str(self.endy))
    
    #// def __eq__(self, other) -> bool:
    #//     if isinstance(other, Move) and self.id == other.id:
    #//         return True
    #//     return False
    __eq__ = lambda self, other: True if isinstance(other, Move) and self.id == other.id else False # overrides the default __eq__ and compares the ids 
                                                                                                    # of the 2 Move objects
    
    #// def __str__(self) -> str:
    #//     return str([self.start.piece_type, self.start.x, self.start.y, self.end.x, self.end.y, self.end.piece_type])
    __str__ = lambda self: str([self.start.x, self.start.y, self.start.piece_type, self.end.x, self.end.y, self.end.piece_type])

    @property
    def id(self) -> str: return str(self.start.x)+str(self.start.y)+str(self.end.x) + str(self.end.y)

    @property
    def capturing_move(self)->bool:
        inverse = {"w":"b", "b":"w", "-":"-"}
        if self.start_piece[0] == inverse.get(self.end_piece[0]):
            return True
        return False


class State:
    #NOTE: b = black, w = white
    #NOTE: P = pawn, R = rook, N = knight, B = bishop, Q = queen, K = king
    #NOTE w- and b- are placeholder can used for en passant
    piece_codes:tuple = ("--", "b-", "w-", "wP", "wR", "wN", "wB", "wQ", "wK", "bP", "bR", "bN", "bB", "bQ", "bK")
    blank:str = "--"


    def __init__(self, custom_board_flag=False,
                gamemode=GAMEMODE_OPTIONS[0], 
                show_all_valid_moves_flag=False, 
                timer_min=0,
                timer_sec=0,
                resolution=400,
            ):
        self.board = [["--" for i in range(8)] for j in range(8)]
        fg:bool = False
        if custom_board_flag:
            fg = self.load_board(True)
        if not fg:
            self.board = [
                ["bR", "bN", "bB", "bK", "bQ", "bB", "bN", "bR"],
                ["bP", "bP", "bP", "bP", "bP", "bP", "bP", "bP"],
                ["--", "--", "--", "--", "--", "--", "--", "--"],
                ["--", "--", "--", "--", "--", "--", "--", "--"],
                ["--", "--", "--", "--", "--", "--", "--", "--"],
                ["--", "--", "--", "--", "--", "--", "--", "--"],
                ["wP", "wP", "wP", "wP", "wP", "wP", "wP", "wP"],
                ["wR", "wN", "wB", "wK", "wQ", "wB", "wN", "wR"]
            ]
        self.validate_board()

        self.white_move:bool = True
        self.move_log:list = []
        self.moves:list = []
        self._temp_moves:list = []

        #from config file
        self.custom_board_flag = custom_board_flag
        self.gamemode = gamemode
        self.show_all_valid_moves_flag = show_all_valid_moves_flag
        self.timer_min = timer_min
        self.timer_sec = timer_sec
        self.resolution = resolution

        self.checkmate = False

        #! Asumes only 1 king of each colour on board
        self.white_king_pos = self.get_white_king_pos() # tuple of Position objs
        self.black_king_pos = self.get_black_king_pos() # tuple of Position objs

        #! this line MUST go last in init
        self.moves = self.get_valid_moves() # list of all possible moves (ex checks) for 1 move

    def load_board(self, flag:bool, filename=os.path.join("boards", "board.json")) -> bool:
        if flag:
            try:
                with open(filename, "r") as f:
                    data = json.loads(f.read())
                    self.board = data
                    return True
            except FileNotFoundError:
                print("failed to load custom board layout, using default")
                # return False
            except ValueError:
                print("board invalid, using default")
                # return False
            except IndexError:
                print("board invalid, using default")
            return False

    def validate_board(self)->None:
        for y in range(DIM):
            for x in range(DIM):
                if self.get_piece(Position(x,y,self)) not in self.piece_codes:
                    raise ValueError(f"\"{self.get_piece(Position(x,y,self))}\" is not a valid piece code")

    @property
    def moves_ids(self)->list:
        return [self.moves[i].id for i in range(len(self.moves))]

    def get_white_king_pos(self)->tuple:
        arr:list = []
        # for y_idx, y in enumerate(self.board):
        #     for x_idx, x in enumerate(y):
        for y_idx in range(DIM):
            for x_idx in range(DIM):
                if self.get_piece(Position(x_idx, y_idx, self)) == "wK":
                    return Position(x_idx, y_idx, self)

    def get_black_king_pos(self)->Position:
        pos:Position
        for y_idx, y in enumerate(self.board):
            for x_idx, x in enumerate(y):
                if self.get_piece(Position(x_idx, y_idx, self)) == "bK":
                    return Position(x_idx, y_idx, self)
        
    def update_king_pos(self)->None:
        self.white_king_pos = self.get_white_king_pos()
        self.black_king_pos = self.get_black_king_pos()

    def undo_prev_move(self) -> None:
        if len(self.move_log) > 0:
            x:Move=self.move_log.pop()
            self.set_piece(x.start, x.start.piece_type)
            self.set_piece(x.end, x.end.piece_type)
            self.white_move = not self.white_move
            self.update_king_pos()
            # self.moves = self.get_valid_moves()

    def winner(self) -> str:
        if self.checkmate:
            return "White" if not self.white_move else "Black"

    # def get_piece(self, pos_obj: Position):
    #     return self.board[pos_obj.y][pos_obj.x]
    get_piece = lambda self, pos_obj: self.board[pos_obj.y][pos_obj.x]

    def set_piece(self, pos_obj:Position, change_to:str) -> None:
        if pos_obj.piece_type in self.piece_codes:
            self.board[pos_obj.y][pos_obj.x] = change_to
        else:
            raise ValueError("set_piece given invalid piece type")

    # Unused
    #// def is_move_valid(self, move_obj:Move) -> bool:
    #//     # check if player is trying to capture their own colour
    #//     is_own_colour = lambda self, move_obj: False if move_obj.start.piece_type[0] == move_obj.end.piece_type[0] else True
    #//     # for i in self.get_all_valid_moves_ex_checks():
    #//     #     print(i)
    #//     return is_own_colour(self, move_obj) and (move_obj in self.get_all_valid_moves_ex_checks())

    
    def move_piece(self, move_obj:Move, arr:list) -> None:
        #// start, end = move_obj.start, move_obj.end
        #// piece_type, captured_type = self.get_piece(start), self.get_piece(end) 
        if move_obj in arr:
            # print(move_obj)
            self.set_piece(move_obj.end, move_obj.start.piece_type) # replace enemy/black space with payers piece
            self.set_piece(move_obj.start, State.blank) # set moved piece to empty
            self.white_move = not self.white_move
            self.move_log.append(move_obj)
            self.update_king_pos()
            # self.moves = self.get_valid_moves()

    def move_piece_by_id(self, id)->None:
        x1,y1,x2,y2 = id
        self.move_piece(Move(Position(int(x1), int(y1), self), Position(int(x2), int(y2), self), self), self.moves)

    def get_valid_moves(self) -> list:
        copy_white_move = self.white_move
        self.moves.clear()
        # generate all possible moves
        arr:list=[]
        arr = self.get_all_valid_moves_ex_checks(arr) # appends moves to arr
        #! FIXED --------------------------------------------------------------------------------------------------
            #// arr.append(Move(Position(0,0,self), Position(5,5,self), self))  
            # bad fix, the last elements is handled incorrectly
            # append random move to the end of the list
            # then pop it off on line the 2nd last line of the function

            #// arr.pop(); return arr     
            # NOTE uncommenting this line removes the king in check check
            # NOTE also that when uncommented checkmate doesnt work and the game will only end when no piece can make a move
        #! FIXED --------------------------------------------------------------------------------------------------

        # for each move make move
        self.white_move = not self.white_move
        for i in range(len(arr)-1, -1, -1): # looping backward as removing from list, prevents shifting indexes
            
            self.move_piece(arr[i], arr)
            # self.white_move = not self.white_move
            if self.is_checked(): # generates oppenents moves
                arr.remove(arr[i]) # remove invalid moves
            self.undo_prev_move() # undo temporary moves
            self.white_move = not copy_white_move # make sure white_move var is set correctly
        self.white_move = copy_white_move
        #// arr.pop()
        return arr

    def is_checked(self) -> bool:
        self.update_king_pos()
        if self.is_attacked_position(self.white_king_pos) or self.is_attacked_position(self.black_king_pos):
            return True
        #// else:
        #//     if self.is_attacked_position(self.black_king_pos):
        #//         return True
        #// self.white_move = not self.white_move
        return False
    
    def is_attacked_position(self, pos:Position)->bool:
        self.white_move = not self.white_move # to generate oppenents move
        opponents_moves = self.get_all_valid_moves_ex_checks(list())
        self.white_move = not self.white_move # switch back
        op_mov:Move
        for op_mov in opponents_moves:
            #// print(op_mov.end)
            if op_mov.end == pos:
                return True
        return False

            

    def get_all_valid_moves_ex_checks(self, arr:list) -> None:
        #// move_arr:list = []
        arr.clear()
        for y in range(DIM):
            for x in range(DIM):
                p = Position(x, y, self).piece_type
                piece_colour, piece_type = p
                #// print(piece_colour+piece_type)
                if (piece_colour == "w" and self.white_move) or (piece_colour == "b" and not self.white_move):
                    # checking the Piece type and generating the correct move set for it
                    match piece_type: # match statement added in python 3.10
                        case "-": # null char, used for en passand
                            continue
                        case "P":
                            self.get_pawn_moves(x, y, arr)
                        case "R":
                            self.get_rook_moves(x, y, arr)
                        case "N":
                            self.get_knight_moves(x, y, arr)
                        case "B":
                            self.get_bishop_moves(x, y, arr)
                        case "Q":
                            self.get_queen_moves(x, y, arr)
                        case "K":
                            self.get_king_moves(x, y, arr)
                        case default: # piece not valid
                            raise ValueError(f"invalid piece: \"{piece_type}\"")
        return arr

    # helper function
    def generic_piece_move(self, x:int, y:int, matrix:tuple, arr:list, distance:int=7, no_jumps=True)->None:
        # var no_jumps is technically unused, but allow easy modification so im leaving it in

        distance+=1 # add to include the square the piece is on, to make more intuitive
        if not (0<distance<=8): # range check
            raise ValueError(f"distance is out of range and cant be {distance}")
        for i in matrix:
            if not isinstance(i, tuple):
                raise ValueError(f"{i} must be tuple")
    
        enemy_type:str = "b" if self.white_move else "w"
        m:tuple; i:int; endx:int; endy:int
        for m in matrix:
            for i in range(1, distance):
                endx, endy = x + m[0] * i, y + m[1] * i # perform vector move on piece and 
                #increment vector magnitude based on piece max distance (normally 1 or 8)

                if 0<=endx<8 and 0<=endy<8: #range check
                    target_piece = self.get_piece(Position(endx, endy, self))
                    if target_piece == State.blank:
                        arr.append(Move(Position(x,y,self), Position(endx, endy,self), self))
                    elif target_piece[0]==enemy_type:
                        arr.append(Move(Position(x,y,self), Position(endx, endy,self), self))
                        if no_jumps:
                            break # prevent piece from jumping pieces
                    else: # cant take white piece
                        if no_jumps:
                            break
                        else:
                            continue

    # // def get_pawn_moves(self, x:int, y:int): #//NOTE: have to do pawns taking on diagonals
    # //     piece_infront: list = []
    # //     if self.white_move and Position(x, y ,self).piece_type == "wP":
    # //         piece_infront.append(Position(x, y-1, self))
    # //         if y == 6:
    # //             piece_infront.append(Position(x, y-2, self))
    # //         # if self.get_piece(piece_infront) == State.blank:
    # //         #     self.moves.append(Move(Position(x, y, self), piece_infront, self))
    # //     else:
    # //         piece_infront.append(Position(x, y+1, self))
    # //         if y == 1:
    # //             piece_infront.append(Position(x, y+2, self))
    # //     for i in piece_infront:
    # //         if i.piece_type == State.blank:
    # //             self.moves.append(Move(Position(x, y, self), i, self))

    #         // for i in self.moves:
    #         //     print(i)

    def get_pawn_moves(self, x:int, y:int, arr:list) -> None: # moves appended to arr
        # NOTE should refactor this code :)
        diagonals = ((1,1), (-1, 1), (1,-1), (-1,-1))
        if self.white_move:
            try:
                if self.get_piece(Position(x,y-1, self)) == State.blank:
                    arr.append(Move(Position(x,y,self), Position(x,y-1,self), self))
                    if self.get_piece(Position(x,y-2, self)) == State.blank and y == 6:
                        arr.append(Move(Position(x,y,self), Position(x,y-2,self), self))
            except IndexError:
                pass
            # check white diagonals
            for i in range(2,4):
                try:
                    a,b = x + diagonals[i][0],y + diagonals[i][1]
                    if a >= 0 and b >= 0:
                        if self.get_piece(Position(a, b, self))[0] == "b":
                            arr.append(Move(Position(x,y,self), Position(a, b, self), self))
                except IndexError:
                    pass

        else:
            try:
                if self.get_piece(Position(x,y+1, self)) == State.blank:
                    arr.append(Move(Position(x,y,self), Position(x,y+1,self), self))
                    if self.get_piece(Position(x,y+2, self)) == State.blank and y == 1:
                        arr.append(Move(Position(x,y,self), Position(x,y+2,self), self))
            except IndexError:
                pass
        # checks blacks diagonals
            for i in range(2):
                try:
                    a,b = x + diagonals[i][0],y + diagonals[i][1]
                    if a >= 0 and b >= 0:
                        if self.get_piece(Position(a, b, self))[0] == "w":
                            arr.append(Move(Position(x,y,self), Position(a, b, self), self))
                except IndexError:
                    pass

    def get_rook_moves(self, x:int, y:int, arr:list) -> None: # moves appended to arr
        matrix = ((1, 0), (-1, 0), (0, 1), (0, -1))
        self.generic_piece_move(x, y, matrix, arr)

    def get_bishop_moves(self, x:int, y:int, arr:list) -> None: # moves appended to arr
        matrix:tuple = ((1, 1), (-1, -1), (-1, 1), (1, -1))
        self.generic_piece_move(x, y, matrix, arr)
    
    def get_knight_moves(self, x:int, y:int, arr:list) -> None: # moves appended to arr
        matrix = ((2,1), (2,-1), (-2,1), (-2,-1), (1,-2), (1,2), (-1, -2), (-1, 2))
        self.generic_piece_move(x,y,matrix,arr, distance=1, no_jumps=False)     #NOTE: no_jumps doesnt change to behavour in this instance as
                                                                                # it only has a dist of 1 so technically makes no jumps in the
                                                                                # code, due to the matrix moving the knight by +-2 and +-1 in
                                                                                # the x or y.
                                                                                #
                                                                                # a "jump" is defined by the number of times the matrix
                                                                                # is applied to the piece

    def get_king_moves(self, x:int, y:int, arr:list) -> None: # moves appended to arr
        iterations = [1, 0, -1]
        test = list(itertools.permutations(iterations, 2))
        test.append((1,1)); test.append((-1,-1))
        self.generic_piece_move(x, y, tuple(test), arr, distance=1)

    def get_queen_moves(self, x:int, y:int, arr:list) -> None: # moves appended to arr
        iterations = [1, 0, -1]
        test = list(itertools.permutations(iterations, 2)) # gets all permutations but not repeats e.g. (1,1) etc
        test.append((1,1)); test.append((-1,-1)) # add missed permutations
        self.generic_piece_move(x, y, tuple(test), arr)
Editor is loading...
Leave a Comment