Pythonプログラミングで

チェスを作る

GitHub へ

解説ページへ

目次

config.py

                    
#! /usr/bin/env python3
# config.py
# programmed by Saito-Saito-Saito
# explained on https://Saito-Saito-Saito.github.io/chess
# last updated: 15 August 2020



from logging import getLogger, StreamHandler, FileHandler, DEBUG, INFO, WARNING, ERROR, CRITICAL, Formatter


### LOGGER SETTNGS
DEFAULT_LOG_ADDRESS = 'log.txt'
DEFAULT_FORMAT = Formatter('%(asctime)s - %(levelname)s - logger:%(name)s - %(filename)s - L%(lineno)d - %(funcName)s - %(message)s')


def setLogger(name='default', *, level=INFO, fhandler=None, shandler=None, fhandler_level=DEBUG, shandler_level=CRITICAL, filemode='w', filename=DEFAULT_LOG_ADDRESS, fhandler_format=DEFAULT_FORMAT, shandler_format=DEFAULT_FORMAT):
    logger = getLogger(name)
    logger.setLevel(level)

    fhandler = fhandler or FileHandler(filename, mode=filemode)
    fhandler.setLevel(fhandler_level)
    fhandler.setFormatter(fhandler_format)
    logger.addHandler(fhandler)

    shandler = shandler or StreamHandler()
    shandler.setLevel(shandler_level)
    shandler.setFormatter(shandler_format)
    logger.addHandler(shandler)

    return logger



# record files
MAINRECADDRESS = 'mainrecord.txt'
SUBRECADDRESS = 'subrecord.txt'

# board size
SIZE = 8
# for if switches
OVERSIZE = SIZE * SIZE

# for index
FILE = 0
RANK = 1
a, b, c, d, e, f, g, h = 1, 2, 3, 4, 5, 6, 7, 8

WHITE = 1
BLACK = -1

EMPTY = 0
P = PAWN = 1
R = ROOK = 2
N = KNIGHT = 3
B = BISHOP = 4
Q = QUEEN = 5
K = KING = 6
                    
                

fundam.py

                    
#! /usr/bin/env python3
# fundam.py
# programmed by Saito-Saito-Saito
# explained on https://Saito-Saito-Saito.github.io/chess
# last updated: 15 August 2020


import config

# positive or negative (returning 1, -1, or 0)
def PosNeg(subject):
    if subject > 0:
        return 1
    elif subject < 0:
        return - 1
    else:
        return 0


# whether the index is in the board (bool)
def InSize(subject):
    if 0 <= subject < config.SIZE:
        return True
    else:
        return False


if __name__=="__main__":
    try:
        print(PosNeg(int(input('Enter a posnegee '))))
    except:
        print('INVALID INPUT')
    try:
        print(InSize(int(input('Enter an InSizee '))))
    except:
        print('INVALID INPUT')
                    
                

board.py

                    
#! /usr/bin/env python3
# board.py
# programmed by Saito-Saito-Saito
# explained on https://Saito-Saito-Saito.github.io/chess
# last updated: 15 August 2020


import copy
import re
import sys

from config import *
import fundam
import IO


local_logger = setLogger(__name__)



class Board:    
    def __init__(self, *, board=[], target=[OVERSIZE, OVERSIZE], castl_k=[WHITE, BLACK], castl_q=[WHITE, BLACK], player=WHITE, turn=1, s='', logger=local_logger):
        # NOTE: when copying any list, you have to use copy.deepcopy
        if len(board) == SIZE:
            self.board = copy.deepcopy(board)
        else:
            # [0][0] is white's R, [0][1] is white's P, ...
            self.board = [
                [R, P, 0, 0, 0, 0, -P, -R],
                [N, P, 0, 0, 0, 0, -P, -N],
                [B, P, 0, 0, 0, 0, -P, -B],
                [Q, P, 0, 0, 0, 0, -P, -Q],
                [K, P, 0, 0, 0, 0, -P, -K],
                [B, P, 0, 0, 0, 0, -P, -B],
                [N, P, 0, 0, 0, 0, -P, -N],
                [R, P, 0, 0, 0, 0, -P, -R]
            ]
        self.ep_target = copy.deepcopy(target) # for en passan
        self.castl_k = copy.deepcopy(castl_k)   # for castling
        self.castl_q = copy.deepcopy(castl_q)   # for castling
        self.turn = turn  # starts from 1
        self.player = player
        self.s = s
        self.logger = logger
                        

    def print(self, *, turnmode=True, reverse=False):
        # cf. boardprint.py
        start = [SIZE - 1, 0]  # WHITESIDE: start[0] BLACKSIDE: start[1]
        stop = [-1, SIZE]  # WHITESIDE: stop[0] BLACKSIDE: stop[1]
        step = [-1, +1] # WHITESIDE: step[0] BLACKSIDE: step[1]
        switch = bool(turnmode and (self.player == BLACK))
        if reverse:
            switch = not switch
            
        print('\n')
        if switch:
            print('\t    h   g   f   e   d   c   b   a')
        else:
            print('\t    a   b   c   d   e   f   g   h')
        print('\t   -------------------------------')
        for rank in range(start[switch], stop[switch], step[switch]):    # down to less
            print('\t{} |'.format(rank + 1), end='')
            for file in range(start[not switch], stop[not switch], step[not switch]):
                print(' {} |'.format(IO.ToggleType(self.board[file][rank])), end='')
            print(' {}'.format(rank + 1))
            print('\t   -------------------------------')
        if switch:
            print('\t    h   g   f   e   d   c   b   a')
        else:
            print('\t    a   b   c   d   e   f   g   h')
        print('\n')
    

    def motionjudge(self, frFILE, frRANK, toFILE, toRANK, promote=EMPTY, logger=None):
        # logger setting
        logger = logger or self.logger
        
        # inside / out of the board
        if not (fundam.InSize(frFILE) and fundam.InSize(frRANK) and fundam.InSize(toFILE) and fundam.InSize(toRANK)):
            logger.debug('OUT OF THE BOARD')
            return False

        player = fundam.PosNeg(self.board[frFILE][frRANK])
        piece = abs(self.board[frFILE][frRANK])
        
        # moving to the square where there is own piece
        if fundam.PosNeg(self.board[toFILE][toRANK]) == player:
            logger.debug('MOVING TO OWN SQUARE')
            return False

        # there is no piece at Fr
        if piece == EMPTY:
            logger.debug('MOVING EMPTY')
            return False

        # PAWN
        elif piece == PAWN:
            # not promoting at the edge
            if (toRANK == 8 - 1 or toRANK == 1 - 1) and promote not in [R, N, B, Q]:
                logger.info('NECESSARY TO PROMOTE')
                return False
            # normal motion (one step forward); the same FILE, appropriate RANK, TO = EMPTY
            # NOTE: if player is WHITE (=1), the rank number has to increase. vice versa
            if frFILE == toFILE and toRANK - frRANK == player and self.board[toFILE][toRANK] == EMPTY:
                return True
            # normal capturing; next FILE, appropriate RANK, TO = opponent
            if abs(toFILE - frFILE) == 1 and toRANK - frRANK == player and fundam.PosNeg(self.board[toFILE][toRANK]) == -player:
                return True
            # first two steps; adequate frRANK the same FILE, appropriate RANK, passing squares are EMPTY
            if ((player == WHITE and frRANK == 2 - 1) or (player == BLACK and frRANK == 7 - 1)) and frFILE == toFILE and toRANK - frRANK == 2 * player and self.board[frFILE][frRANK + player] == self.board[toFILE][toRANK] == EMPTY:
                return True
            # en passant; FR - ep_target, TO - ep_target, TO = EMPTY
            if abs(self.ep_target[FILE] - frFILE) == 1 and frRANK == self.ep_target[RANK] and toFILE == self.ep_target[FILE] and toRANK - self.ep_target[RANK] == player and self.board[toFILE][toRANK] == EMPTY:
                return True
            # all other pawn moves are invalid
            logger.debug('INVALID MOTION of PAWN')
            return False

        # ROOK
        elif piece == ROOK:
            # invalid motion; not moving on the same file/rank
            if frFILE != toFILE and frRANK != toRANK:
                logger.debug('INVALID MOTION of ROOK')
                return False
            # else, necessary to check whether there is an obstacle in the way

        # KNIGHT
        elif piece == KNIGHT:
            # valid motion
            if (abs(toFILE - frFILE) == 1 and abs(toRANK - frRANK) == 2) or (abs(toFILE - frFILE) == 2 and abs(toRANK - frRANK) == 1):
                return True
            # all other motions are invalid
            logger.debug('INVALID MOTION of KNIGHT')
            return False

        # BISHOP
        elif piece == BISHOP:
            # invalid motion; not moving on the same diagonal
            if abs(toFILE - frFILE) != abs(toRANK - frRANK):
                logger.debug('INVALID MOTION of BISHOP')
                return False
            # else, necessary to check an obstacle in the way

        # QUEEN
        elif piece == QUEEN:
            # invalid motion (cf, B/R)
            if frFILE != toFILE and frRANK != toRANK and abs(toFILE - frFILE) != abs(toRANK - frRANK):
                logger.debug('INVALID MOTION of QUEEN')
                return False
            # else, necessary to check an obstacle in the way

        # KING
        elif piece == KING:
            # normal motion (one step)
            if abs(toFILE - frFILE) <= 1 and abs(toRANK - frRANK) <= 1:
                logger.debug('KING NORMAL')
                return True
            # preparing for castling; setting rank
            if player == WHITE:
                rank = 1 - 1
            elif player == BLACK:
                rank = 8 - 1
            else:
                logger.error('UNEXPECTED PLAYER VALUE in motionjudge')
                print('SYSTEM ERROR')
                sys.exit('SYSTEM ERROR')
            # Q-side; adequate fr and to, all passing squares are EMPTY
            if player in self.castl_q and frFILE == e - 1 and frRANK == rank and toFILE == c - 1 and toRANK == rank and self.board[b - 1][rank] == self.board[c - 1][rank] == self.board[d - 1][rank] == EMPTY:
                # K must not be checked while castling
                for ran in range(SIZE):
                    for fil in range(SIZE):
                        if fundam.PosNeg(self.board[fil][ran]) == -player and (self.motionjudge(fil, ran, e - 1, rank, Q) or self.motionjudge(fil, ran, d - 1, rank, Q) or self.motionjudge(fil, ran, c - 1, rank, Q)):
                            logger.info('CHECKED IN THE WAY')
                            return False
                logger.debug('KING Q-side')
                return True
            # K-side; adequate fr and to, all passing squares are EMPTY
            if player in self.castl_k and frFILE == e - 1 and frRANK == rank and toFILE == g - 1 and toRANK == rank and self.board[f - 1][rank] == self.board[g - 1][rank] == EMPTY:
                # K must be checked while castling
                for ran in range(SIZE):
                    for fil in range(SIZE):
                        if fundam.PosNeg(self.board[fil][ran]) == -player and (self.motionjudge(fil, ran, e - 1, rank, Q) or self.motionjudge(fil, ran, d - 1, rank, Q) or self.motionjudge(fil, ran, c - 1, rank, Q)):
                            logger.info('CHECKED IN THE WAY')
                            return False
                logger.debug('KING K-side')
                return True
            # all other King's moves are invalid
            logger.debug('INVALID MOTION of KING')
            return False

        # other piece values are invalid
        else:
            logger.error('UNEXPECTED VALUE of PIECE in motionjudge')
            print('SYSTEM ERROR')
            sys.exit('SYSTEM ERROR')

        # whether there is an obstacle in the wauy of R/B/Q
        direction = [fundam.PosNeg(toFILE - frFILE), fundam.PosNeg(toRANK - frRANK)]
        focused = [frFILE + direction[FILE], frRANK + direction[RANK]]  # focused square
        while focused[FILE] != toFILE or focused[RANK] != toRANK:   # while not reaching TO
            # out of the board
            if not (fundam.InSize(focused[0]) and fundam.InSize(focused[1])):
                logger.warning('')
                break
            # if there is a piece on the way
            if self.board[focused[FILE]][focused[RANK]] != EMPTY:
                logger.debug('THERE IS AN OBSTACLE in the way')
                return False
            # controlling parameters 
            focused[FILE] += direction[FILE]
            focused[RANK] += direction[RANK]
        # there is nothing in the wauy
        return True

    
    def move(self, frFILE, frRANK, toFILE, toRANK, promote=EMPTY, logger=None):
        # logger setup
        logger = logger or self.logger
        
        ### INVALID MOTON
        if self.motionjudge(frFILE, frRANK, toFILE, toRANK, promote) == False:
            return False
        
        ### NOT OWN PIECE
        if fundam.PosNeg(self.board[frFILE][frRANK]) != self.player:
            logger.debug('MOVING OPPONENT PIECE OR EMPTY')
            return False

        piece = abs(self.board[frFILE][frRANK])

        ### SPECIAL EVENTS
        # castling
        if piece == KING and abs(toFILE - frFILE) > 1:
            # preparing the rank
            if self.player == WHITE:
                rank = 1 - 1
            elif self.player == BLACK:
                rank = 8 - 1
            else:
                logger.error('UNEXPECTED VALUE of PLAYER in move')
                print('SYSTEM ERROR')
                sys.exit('SYSTEM ERROR')
            # moving rook
            if toFILE == c - 1:
                self.board[d - 1][rank] = self.player * ROOK
                self.board[a - 1][rank] = EMPTY
            elif toFILE == g - 1:
                self.board[f - 1][rank] = self.player * ROOK
                self.board[h - 1][rank] = EMPTY
            else:
                logger.error('UNEXPECTED VALUE of toFILE in move')
                return False
        # en passant; pawn moves diagonal and TO is EMPTY
        if piece == PAWN and frFILE != toFILE and self.board[toFILE][toRANK] == EMPTY:
            # capturing opponent's pawn
            self.board[self.ep_target[FILE]][self.ep_target[RANK]] = EMPTY
        # promotion; changing the moving piece into promote
        if piece == PAWN and (toRANK == 8 - 1 or toRANK == 1 - 1):
            self.board[frFILE][frRANK] = self.player * promote
        
        ### MOVING OWN PIECE
        self.board[toFILE][toRANK] = self.board[frFILE][frRANK]
        self.board[frFILE][frRANK] = EMPTY
        
        ### PARAMETERS CONTROL
        # for e.p.
        if piece == PAWN and abs(toRANK - frRANK) > 1:
            self.ep_target = [toFILE, toRANK]
        else:
            self.ep_target = [OVERSIZE, OVERSIZE]
        # for castling q-side
        if self.player in self.castl_q and (piece == KING or (piece == ROOK and frFILE == a - 1)):
            self.castl_q.remove(self.player)
        # for castling k-side
        if self.player in self.castl_k and (piece == KING or (piece == ROOK and frFILE == h - 1)):
            self.castl_k.remove(self.player)
        
        ### RETURN AS SUCCEEDED
        logger.info('SUCCESSFULLY MOVED')
        return True


    def king_place(self, searcher):
        # searching for the searcher's king
        for fil in range(SIZE):
            if searcher * KING in self.board[fil]:
                return [fil, self.board[fil].index(searcher * KING)]
        # there is no king
        return EMPTY
            

    def checkcounter(self, checkee, logger=None):
        # logger setup
        logger = logger or self.logger
        
        #if there is no king, impossible to check
        TO = self.king_place(checkee)
        try:
            toFILE = TO[FILE]
            toRANK = TO[RANK]
        except:
            logger.info('THERE IS NO KING ON THE BOARD')
            return False

        # searching all the squares, count up the checking pieces
        count = 0
        for frFILE in range(SIZE):
            for frRANK in range(SIZE):
                # pawn might capture the king by promoting, so do not forget promote=Q or something
                if fundam.PosNeg(self.board[frFILE][frRANK]) == -checkee and self.motionjudge(frFILE, frRANK, toFILE, toRANK, Q):
                    logger.info('CHECK: {}, {} -> {}, {}'.format(frFILE, frRANK, toFILE, toRANK))
                    count += 1
        # if checkee is not checked, return 0
        return count


    def checkmatejudge(self, matee, logger=None):
        # logger setup
        logger = logger or self.logger
        
        # if not checked, it's not checkmate
        if self.checkcounter(matee) in [False, 0]:
            logger.debug('NOT CHECKED')
            return False
        
        # searching all the moves matee can
        for frFILE in range(SIZE):
            for frRANK in range(SIZE):
                if fundam.PosNeg(self.board[frFILE][frRANK]) == matee:
                    # searching all TO the piece can reach
                    for toFILE in range(SIZE):
                        for toRANK in range(SIZE):
                            # cloning board
                            local_board = Board(board=self.board, target=self.ep_target, castl_k=self.castl_k, castl_q=self.castl_q, player=matee)
                            # moving the local board and count up check
                            if local_board.move(frFILE, frRANK, toFILE, toRANK, Q) and local_board.checkcounter(matee) == 0:
                                logger.info('THERE IS {}, {} -> {}, {}'.format(frFILE,frRANK,toFILE,toRANK))
                                return False
                    logger.debug('"FR = {}, {}" was unavailable'.format(frFILE, frRANK))

        # completing the loop, there is no way to flee
        return True

    
    def stalematejudge(self, matee, logger=None):
        # logger setup
        logger = logger or self.logger
        
        # if checked, it's not stalemate
        if self.checkcounter(matee) not in [0, False]:
            logger.debug('CHECKED')
            return False

        # searching all the moves matee can
        for frFILE in range(SIZE):
            for frRANK in range(SIZE):
                if fundam.PosNeg(self.board[frFILE][frRANK]) == matee:
                    # searching all TO the piece can reach
                    for toFILE in range(SIZE):
                        for toRANK in range(SIZE):
                            # cloning board
                            local_board = Board(board=self.board, target=self.ep_target, castl_k=self.castl_k, castl_q=self.castl_q, player=matee)
                            # moving the local board and count up check
                            if local_board.move(frFILE, frRANK, toFILE, toRANK, Q) and local_board.checkcounter(matee) == 0:
                                logger.info('THERE IS {}, {} -> {}, {}'.format(frFILE,frRANK,toFILE,toRANK))
                                return False
                    logger.debug('"FR = {}, {}" was unavailable'.format(frFILE, frRANK))
        # completing the loop, there is no way to avoid check when moving
        logger.info('STALEMATE. {} cannot move'.format(self.player))
        return True
    

    def s_analyze(self, logger=None):
        # logger setup
        logger = logger or self.logger
        
        # removing spaces
        self.s = self.s.replace(' ', '').replace('!', '').replace('?', '')

        # avoiding bugs
        if len(self.s) == 0:
            logger.debug('len(s) == 0')
            return False

        # the pattern of the normal format
        match = re.match(r'^[PRNBQK]?[a-h]?[1-8]?[x]?[a-h][1-8](=[RNBQ]|e.p.)?[\++#]?$', self.s)

        # normal format
        if match:
            line = match.group()
            logger.info('line = {}'.format(line))

            # what piece is moving
            if line[0] in ['P', 'R', 'N', 'B', 'Q', 'K']:
                piece = IO.ToggleType(line[0])
                # deleting the info of piece because we do not use it any more
                line = line.lstrip(line[0]) 
            else:
                piece = PAWN
            logger.info('PIECE == {}'.format(piece))

            # written info of what rank the piece comes from; frRANK starts from 0
            if line[0].isdecimal():
                frFILE = OVERSIZE
                frRANK = IO.ToggleType(line[0]) - 1
                # deleting the number so that the sentence seems simpler
                line = line.lstrip(line[0])
            # written info of what file the piece comes from; frFILE starts from 0
            elif ord('a') <= ord(line[0]) <= ord('h') and ord('a') <= ord(line[1]) <= ord('x'):
                frFILE = IO.ToggleType(line[0]) - 1
                frRANK = OVERSIZE
                # deleting only the first character of line
                line = line[1:]
            # nothing is written about where the piece comes from
            else:
                frFILE = OVERSIZE
                frRANK = OVERSIZE
            logger.info('FR = {}, {}'.format(frFILE, frRANK))

            # whether the piece has captured one of the opponent's pieces
            if line[0] == 'x':
                CAPTURED = True
                line = line.lstrip(line[0])
            else:
                CAPTURED = False

            # where the piece goes to; toFILE and toRANK starts from 0
            toFILE = IO.ToggleType(line[0]) - 1
            toRANK = IO.ToggleType(line[1]) - 1
            logger.info('TO = {}, {}'.format(toFILE, toRANK))

            # promotion
            if '=' in line:
                promote = IO.ToggleType(line[line.index('=') + 1])
            else:
                promote = EMPTY
            logger.info('promote = {}'.format(promote))

            # raising up all the available candidates
            candidates = []
            for fil in range(SIZE):
                # when frFILE is written
                if fundam.InSize(frFILE) and frFILE != fil:
                    continue

                for ran in range(SIZE):
                    # when frRANK is written
                    if fundam.InSize(frRANK) and frRANK != ran:
                        continue

                    # piece
                    if self.board[fil][ran] != self.player * piece:
                        continue

                    # available motion
                    if self.motionjudge(fil, ran, toFILE, toRANK, promote) == False:
                        continue

                    candidates.append([fil, ran])
            logger.info('candidates = {}'.format(candidates))

            # checking all the candidates
            for reference in range(len(candidates)):
                # copying and moving the board
                local_board = Board(board=self.board, target=self.ep_target, castl_k=self.castl_k, castl_q=self.castl_q, player=self.player, turn=self.turn, s=self.s)
                local_board.move(candidates[reference][FILE], candidates[reference][RANK], toFILE, toRANK, promote)

                # capture; searching for the opponent's piece that has disappeared
                if CAPTURED or 'e.p.' in line:
                    # normal capturing
                    if fundam.PosNeg(self.board[toFILE][toRANK]) == -self.player:
                        pass
                    # en passan to Q-side
                    elif fundam.InSize(toRANK - 1) and fundam.PosNeg(self.board[toFILE][toRANK - 1]) == -self.player and fundam.PosNeg(local_board.board[toFILE][toRANK - 1]) == EMPTY:
                        pass
                    # en passan to K-side
                    elif fundam.InSize(toRANK + 1) and fundam.PosNeg(self.board[toFILE][toRANK + 1]) == -self.player and fundam.PosNeg(local_board.board[toFILE][toRANK + 1]) == EMPTY:
                        pass
                    # here no piece can capture a piece
                    else:
                        logger.info('{} does not capture any piece'.format(candidates[reference]))
                        del candidates[reference]
                        reference -= 1  # back to the for loop's head, reference increases
                        continue
                
                # check
                if line.count('+') > local_board.checkcounter(-self.player):
                    logger.info('{} is short of the number of check'.format(candidates[reference]))
                    del candidates[reference]
                    reference -= 1  # back to the for loop's head, reference increases
                    continue

                # checkmate
                if '#' in line and local_board.checkmatejudge(-self.player) == False:
                    logger.info('{} does not checkmate'.format(candidates[reference]))
                    del candidates[reference]
                    reference -= 1  # back to the for loop's head, reference increases
                    continue

                # en passant
                if 'e.p.' in line and self.board[toFILE][toRANK] != EMPTY:
                    logger.info('{} does not en passant'.format(candidates[reference]))
                    del candidates[reference]
                    reference -= 1  # back to the for loop's head, reference increases
                    continue

            # normal return
            if len(candidates) == 1:
                logger.info('NORMALLY RETURNED')
                return [candidates[0][FILE], candidates[0][RANK], toFILE, toRANK, promote]
            # when some candidates are available
            elif len(candidates) > 1:
                logger.warning('THERE IS ANOTHER MOVE')
                return [candidates[0][FILE], candidates[0][RANK], toFILE, toRANK, promote]
            # no candidates are available
            else:
                logger.info('THERE IS NO MOVE')
                return False

        # in case the format does not match
        else:
            # game set; take note that player themselves cannot win by inputting these codes
            if self.s == '1/2-1/2':
                logger.info('DRAW GAME')
                return EMPTY
            elif self.s == '1-0' and self.player == BLACK:
                logger.info('WHITE WINS')
                return WHITE
            elif self.s == '0-1' and self.player == WHITE:
                logger.info('BLACK WINS')
                return BLACK
            
            # check whether it represents castling
            # rank setting
            if self.player == WHITE:
                rank = 1 - 1
            elif self.player == BLACK:
                rank = 8 - 1
            else:
                logger.error('UNEXPECTED PLAYER VALUE in s_analyze')
                print('SYSTEM ERROR')
                sys.exit('SYSTEM ERROR')
            # Q-side
            if self.s in ['O-O-O', 'o-o-o', '0-0-0'] and self.board[e - 1][rank] == self.player * KING:
                logger.info('format is {}, castl is {}'.format(self.s, self.castl_q))
                return [e - 1, rank, c - 1, rank, EMPTY]
            # K-side
            elif self.s in ['O-O', 'o-o', '0-0'] and self.board[e - 1][rank] == self.player * KING:
                logger.info('format is {}, castl is {}'.format(self.s, self.castl_k))
                return [e - 1, rank, g - 1, rank, EMPTY]
            
            # invalid format
            else:
                logger.debug('INVALID FORMAT')
                return False


    def record(self, address, logger=None):
        # logger setup
        logger = logger or self.logger
        
        # removing spaces, !, ?
        self.s = self.s.replace(' ', '').replace('!', '').replace('?', '')

        # avoiding bugs
        if len(self.s) == 0:
            logger.debug('len(s) == 0')
            return False

        # normal pattern
        match = re.match(r'^[PRNBQK]?[a-h]?[1-8]?[x]?[a-h][1-8](=[RNBQ]|e.p.)?[\++#]?$', self.s)
        # normal pattern matched
        if match:
            s_record = match.group()
        # resign
        elif self.s in ['1-0', '0-1', '1/2-1/2']:
            s_record = self.s
        # castling
        elif self.s in ['O-O-O', 'O-O', 'o-o-o', 'o-o', '0-0-0', '0-0']:
            s_record = self.s.replace('o', 'O').replace('0', 'O')
        # invalid format
        else:
            logger.info('OUT OF FORMAT in record')
            return False
        
        # open the recording file
        f = open(address, 'a')

        # WHITE WINS (BLACK DIDN'T MOVE)
        if s_record == '1-0':
            f.write('1-0')
        # BLACK WINS (WHITE DIDN'T MOVE)
        elif s_record == '0-1':
            f.write('{}\t0-1'.format(self.turn))
        # writing on WHITE side
        elif self.player == WHITE:
            f.write('{}\t'.format(self.turn) + s_record.ljust(12))
        # writing on BLACK side
        elif self.player == BLACK:
            f.write(s_record.ljust(12) + '\n')
        else:
            logger.error('UNEXPECTED VALUE of PLAYER in record')
            print('SYSTEM ERROR')
            sys.exit('SYSTEM ERROR')
        
        f.close()

        # return as succeeded
        return True


    def tracefile(self, destination_turn, destination_player, isrecwrite=True, logger=None):
        # logger setup
        logger = logger or self.logger
        
        # back to the first
        if destination_turn == 1 and destination_player == WHITE:
            local_board = Board()
            return local_board

        # preparing (initializing) the sub file; all the local moves are recorded on the sub file
        open(SUBRECADDRESS, 'w').close()
        # reading the main file
        f = open(MAINRECADDRESS, 'r')
        # deleting first and last spaces
        line = f.read()
        line = line.strip()
        f.close()
        logger.info('line is "{}"'.format(line))

        # local Board
        local_board = Board()
        
        # detectong each letter in line
        for letter in line:
            # when you come to the end of a sentence
            if letter in [' ', '\t', '\n', ',', '.']:
                logger.info('local_s is {}'.format(local_board.s))
                motion = local_board.s_analyze()
                # normal motion
                if type(motion) is list:
                    local_board.move(*motion)
                    local_board.record(SUBRECADDRESS)  # all the local moves are recorded on the sub file
                    if local_board.player == BLACK:
                        local_board.turn += 1
                    local_board.player *= -1
                    # destination
                    if local_board.turn == destination_turn and local_board.player == destination_player:
                        logger.info('trace succeeded')
                        if isrecwrite:
                            # copying the file
                            f = open(MAINRECADDRESS, 'w')
                            g = open(SUBRECADDRESS, 'r')
                            f.write(g.read())
                            f.close()
                            g.close()
                        return local_board
                # game set
                elif type(motion) is int:
                    print('GAME SET')
                    if isrecwrite:
                        # copying the record
                        f = open(MAINRECADDRESS, 'w')
                        g = open(SUBRECADDRESS, 'r')
                        f.write(g.read())
                        f.close()
                        g.close()
                    return motion
                # initializing the local_s
                local_board.s = ''
            # the sentence does not end yet
            else:
                local_board.s = ''.join([local_board.s, letter])
                logger.debug('local_s = {}'.format(local_board.s))

        # last one local_s; the same as in the for loop
        logger.info('local_s is {}'.format(local_board.s))
        motion = local_board.s_analyze()
        if type(motion) is list:
            local_board.move(*motion)
            local_board.record(SUBRECADDRESS)
            if local_board.player == BLACK:
                local_board.turn += 1
            local_board.player *= -1
            # reaching destination
            if local_board.turn == destination_turn and local_board.player == destination_player:
                logger.info('trace succeeded')
                if isrecwrite:
                    f = open(MAINRECADDRESS, 'w')
                    g = open(SUBRECADDRESS, 'r')
                    f.write(g.read())
                    f.close()
                    g.close()
                return local_board
        elif type(motion) is int:
            if isrecwrite:
                f = open(MAINRECADDRESS, 'w')
                g = open(SUBRECADDRESS, 'r')
                f.write(g.read())
                f.close()
                g.close()
            return motion

        # reaching here, you cannot trace
        logger.warning('FAILED TO BACK')
        return self


if __name__ == "__main__":
    local_board = Board()
    local_board.print()
    local_board.move(c - 1, 2 - 1, c - 1, 4 - 1)
    local_board.player *= -1
    local_board.print(turnmode=True)
    local_board.move(c - 1, 7 - 1, c - 1, 6 - 1)
    local_board.player *= -1
    local_board.print(turnmode=True)
    print(local_board.king_place(WHITE))
    print(local_board.king_place(BLACK))
                    
                

IO.py

                    
#! /usr/bin/env python3
# IO.py
# programmed by Saito-Saito-Saito
# explained on https://Saito-Saito-Saito.github.io/chess
# last updated: 04 March 2021


from config import *

local_logger = setLogger(__name__)


def ToggleType(target, logger=local_logger):
    # piece ID -> piece letter
    if type(target) is int:
        if target == EMPTY:
            return ' '
        elif target == P * BLACK:
            return '♙'
        elif target == R * BLACK:
            return '♖'
        elif target == N * BLACK:
            return '♘'
        elif target == B * BLACK:
            return '♗'
        elif target == Q * BLACK:
            return '♕'
        elif target == K * BLACK:
            return '♔'
        elif target == P * WHITE:
            return '♟'
        elif target == R * WHITE:
            return '♜'
        elif target == N * WHITE:
            return '♞'
        elif target == B * WHITE:
            return '♝'
        elif target == Q * WHITE:
            return '♛'
        elif target == K * WHITE:
            return '♚'
        # invalid target value
        else:
            logger.error('UNEXPECTED INPUT VALUE of A PIECE into IO.ToggleType')
            return False

    # str -> int
    elif type(target) is str:
        # a str number -> int
        if target.isdecimal():
            return int(target)
        # file id
        elif ord('a') <= ord(target) <= ord('h'):
            return ord(target) - ord('a') + 1
        # the kind of piece -> piece no.
        elif target == 'P':
            return P
        elif target == 'R':
            return R
        elif target == 'N':
            return N
        elif target == 'B':
            return B
        elif target == 'Q':
            return Q
        elif target == 'K':
            return K
        # invalid character
        else:
            logger.error('UNEXPECTED INPUT into IO.ToggleType')
            return False

    # unexpected type
    else:
        logger.error('UNEXPECTED INPUT TYPE into IO.ToggleType')
        return False


# for help in the playmode
def instruction():
    print('''
    棋譜の書き方には何通りかありますが、ここでは FIDE (The International Chess Federalation) 公認の standard algebraic notation と呼ばれる記法を使用します。
                
-- 盤面
盤上のマスを一つに絞るのに、数学でやるような「座標」を活用します。横長の行 (rank) は白番から見て下から順に 1, 2, ..., 8 と数え、縦長の列 (file) は白番から見て左から順に a, b, ..., h と番号を振ります。
                
    a   b   c   d   e   f   g   h
   -------------------------------
8 | a8| b8| c8| d8| e8| f8| g8| h8| 8
   -------------------------------
7 | a7| b7| c7| d7| e7| f7| g7| h7| 7
   -------------------------------
6 | a6| b6| c6| d6| e6| f6| g6| h6| 6
   -------------------------------
5 | a5| b5| c5| d5| e5| f5| g5| h5| 5
   -------------------------------
4 | a4| b4| c4| d4| e4| f4| g4| h4| 4
   -------------------------------
3 | a3| b3| c3| d3| e3| f3| g3| h3| 3
   -------------------------------
2 | a2| b2| c2| d2| e2| f2| g2| h2| 2
   -------------------------------
1 | a1| b1| c1| d1| e1| f1| g1| h1| 1
   -------------------------------
    a   b   c   d   e   f   g   h
                
黒番からみる場合にはこれが 180º 開店した形になります。


-- 駒の動き
各駒の名前は表のように割り振っていきます。

    P - ポーン
    R - ルーク
    N - ナイト
    B - ビショップ
    Q - クイーン
    K - キング

まず駒の名前を書いて、その後にどのマスへ移動したかを記録します。

例)
    Bc4 - ビショップが c4 のマスに動いた
    Nf3 - ナイトが f3 のマスに動いた
    Qc7 - クイーンが c7 のマスに動いた

ポーンはしょっちゅう動かすので、棋譜を書くときは基本的に省略します。

例)
    e4 - ポーンが e4 のマスに動いた
    g6 - ポーンが g6 のマスに動いた
    
ポーンが盤面の端まできてプロモーションしたときは、マスを表す2文字に続けて = (成り上がった後の駒の名前) の形で表します。

例)
    b8=Q - ポーンが b8 にきてクイーンにプロモーションした
    h1=N - ポーンが h1 にきてナイトにプロモーションした

相手の駒を取ったときは x を駒の名前と行き先のマスの間に入れて駒を取ったことを明示します。

例)
    Rxf5 - ルークが相手の駒をとって f5 のマスに移動した
    Kxd2 - キングが相手の駒をとって d2 のマスに移動した

ポーンが相手の駒を取ったときは、ポーンが元々いた列 (file) を表すアルファベットを先頭につけ、続けて x を、さらに移動先のマスを並べます。

例)
    gxf6 - g 列のポーンが相手の駒を取って f6 のマスに移動した
    exd5 - e 列のポーンが相手の駒を取って d5 のマスに移動した

ポーンがアンパッサンして相手の駒を取った場合、駒を取ったポーンの移動先を記録します。アンパッサンしたことを明示するために 'e.p.' をつけたりもしますが、必ずつけなければいけないものではありません。

例)
    exd6 - ポーンがアンパッサンして d5 にある相手のポーンをとり d6 のマスへ移動した
    gxh6 e.p. - ポーンがアンパッサンして h5 にある相手のポーンをとり h6 のマスへ移動した
    
駒を動かして相手にチェックをかけたときは、後ろに + をつけます。ダブルチェックの場合は ++ とする流儀もありますが、1つでも十分です。チェックメイトのときは後ろに # をつけます。

例)
    Ba3+ - ビショップが a3 のマスに移動してチェックとなった
    Qxh7# - クイーンが相手の駒を取って h7 のマスに移動しチェックメイトとなった
    f3+ - ポーンが f3 のマスに移動してチェックとなった
    
これまでご紹介した書き方だけだと、駒の動きを一つに絞れないことがあります。そんなときはどのマスにあった駒を動かしたか明示するために、移動先のマスの前に移動元の列 (file) を表すアルファベットを加えてあげます。

例)
    Rad1 - 元々 a 列にいたルークが d1 のマスに移動した
    Nbxd2 - 元々 b 列にいたナイトが相手の駒を取って d2 のマスに移動した
    Rfe1+ - 元々 f 列にいたルークが e1 のマスに移動してチェックとなった

元々同じ列にいた場合は、元いた行 (rank) を表す数字を先ほどと同じ位置に明示してあげます。

例)
    R7e4 - 元々 7 行にいたルークが e4 のマスに移動した
    N1xc3 - 元々 1 行にいたナイトが相手の駒を取って c3 のマスに移動した

キャスリングはここまであげた場合とは全く異なる書き方で記録します。クイーンサイドにキャスリングしたときは O-O と、キングサイドにキャスリングしたときは O-O-O と記録します。

記録者が感じたことをメモするときに下のような記号を使うこともあります。

    ! - 妙手
    !! - 非常に妙手
    ? - 疑問な手
    ?? - ひどい手
    !? - 面白い手
    ?! - 疑わしい手
    
参照:www.chessstrategyonline.comcontent/tutorials/basic-chess-concepts-chess-notation

    Did you get it?
    Read the whole passage and press enter to next
    ''')
    input()


if __name__=="__main__":
    try:
        print(ToggleType(input('enter a toffled str: ')))
    except:
        print('INVALID INPUT')
    try:
        print(ToggleType(int(input('Enter a toggled int: '))))
    except:
        print('INVALID INPUT')
    input('ENTER TO INSTRUCT')
    instruction()
                    
                

playmode.py

                    
#! /usr/bin/env python3
# playmode.py
# programmed by Saito-Saito-Saito
# explained on https://Saito-Saito-Saito.github.io/chess
# last updated: 15 August 2020


import sys

from config import *
import board
import IO

local_logger = setLogger(__name__)


def playmode(turnmode=True, logger=None):
    # logger setup
    logger = logger or local_logger
    
    # new file preparation
    record = open(MAINRECADDRESS, 'w')
    record.close()
    record = open(SUBRECADDRESS, 'w')
    record.close()

    # initializing the board
    main_board = board.Board()
    main_board.print(turnmode=turnmode)

    while True:
        ### GAME SET JUDGE
        # king captured
        if main_board.king_place(main_board.player) == False:
            winner = -main_board.player
            break
        # checkmate
        if main_board.checkmatejudge(main_board.player):
            print('CHECKMATE')
            winner = -main_board.player
            break
        # stalemate
        if main_board.stalematejudge(main_board.player):
            print('STALEMATE')
            winner = EMPTY  # stalemate is draw
            break

        ### PLAYER INTRODUCTION
        if main_board.player == WHITE:
            print('WHITE (X to resign / H to help / Z to back) >>> ', end='')
        elif main_board.player == BLACK:
            print('BLACK (X to resign / H to help / Z to back) >>> ', end='')
        else:
            logger.error('UNEXPECTED VALUE of PLAYER in while loop')
            print('SYSTEM ERROR')
            sys.exit('SYSTEM ERROR')

        ### INPUT ANALYSIS
        # inputting and deleting all spaces, replacing 'o' into 'O'
        main_board.s = input().replace(' ', '').replace('o', 'O')
        # help code
        if main_board.s in ['H', 'h']:
            IO.instruction()
            main_board.print(turnmode=turnmode, reverse=False)
            continue
        # resign code
        if main_board.s in ['X', 'x']:
            winner = -main_board.player
            break
        # back code
        if main_board.s in ['Z', 'z']:
            # necessary for the opponent to allow the player to back
            if main_board.player == WHITE:
                print('Do you agree, BLACK (y/n)? >>> ', end='')
            elif main_board.player == BLACK:
                print('Do you agree, WHITE (y/n) >>> ', end='')
            else:
                logger.error('UNEXPECTED VALUE of PLAYER in the while loop')
                sys.exit('SYSTEM ERROR')
            # in case rejected
            if input() not in ['y', 'Y', 'Yes', 'YES', 'yes']:
                continue
            # in case allowed
            new_board = main_board.tracefile(main_board.turn - 1, main_board.player, isrecwrite=True)
            # unavailable to back
            if new_board == main_board:
                logger.warning('IMPOSSIBLE TO BACK')
                print('SORRY, NOW WE CANNOT BACK THE BOARD')
            # available to back
            else:
                main_board = new_board
                main_board.print(turnmode=turnmode)
            continue
        # motion detection
        motion = main_board.s_analyze()
        # game set (resign)
        if type(motion) is int:
            if motion == EMPTY:
                # necessary to agree to end the game
                if main_board.player == WHITE:
                    print('Do you agree, BLACK (y/n)? >>>', end=' ')
                elif main_board.player == BLACK:
                    print('Do you agree, WHITE (y/n)? >>>', end=' ')
                else:
                    logger.error('UNEXPECTED VALUE of PLAYER in the while loop')
                    print('SYSTEM ERROR')
                    sys.exit('SYSTEM ERROR')
                # when agreed
                if input() in ['y', 'Y']:
                    winner = EMPTY
                    break
                # when rejected
                else:
                    main_board.print(turnmode=turnmode, reverse=False)
                    continue
            elif motion == WHITE == -main_board.player:
                winner = WHITE
                break
            elif motion == BLACK == -main_board.player:
                winner = BLACK
                break
            else:
                print('IVNALID INPUT')
                continue
        # invalid input (here, valid motion is conducted)
        if motion == False or main_board.move(*motion) == False:
            print('INVALID INPUT/MOTION')
            continue

        # recording the move
        main_board.record(MAINRECADDRESS)

        # turn count
        if main_board.player == BLACK:
            main_board.turn += 1

        # player change
        main_board.player *= -1
        
        # board output
        main_board.print(turnmode=turnmode)



    print('\nGAME SET')
    if winner == EMPTY:
        print('1/2 - 1/2\tDRAW')
        # for record
        main_board.s = '1/2-1/2 '
        main_board.record(MAINRECADDRESS)
    elif winner == WHITE:
        print('1 - 0\tWHITE WINS')
        # after white's move, 1-0 is written where black's move is written
        main_board.s = '1-0 '
        main_board.record(MAINRECADDRESS)
    elif winner == BLACK:
        print('0 - 1\tBLACK WINS')
        # after white's move, 1-0 is written where black's move is written
        main_board.s = '0-1 '
        main_board.record(MAINRECADDRESS)
    else:
        logger.error('UNEXPECTED VALUE of PLAYER out of the loop')
        print('SYSTEM ERROR')
        sys.exit('SYSTEM ERROR')


    # record output
    if input('\nDo you want the record (y/n)? >>> ') in ['y', 'Y', 'yes', 'YES', 'Yes']:
        record = open(MAINRECADDRESS, 'r')
        print('\n------------------------------------')
        print(record.read())
        print('------------------------------------')
        record.close()


    print('\nGAME OVER\n')


if __name__ == "__main__":
    playmode()
                    
                

readmode.py

                    
#! /usr/bin/env python3
# readmode.py
# programmed by Saito-Saito-Saito
# explained on https://Saito-Saito-Saito.github.io/chess
# last updated: 15 August 2020


import sys

from config import *
import board

local_logger = setLogger(__name__)


def readmode(turnmode=False, reverse=False, logger=None):
    # logger setup
    logger = logger or local_logger
    
    # initializing the board
    main_board = board.Board()
    main_board.print()
    print('ENTER TO START')
    input()
    
    while True:
        # displaying turn number and player
        if main_board.player == WHITE:
            print('\n{}\tWHITE'.format(main_board.turn), end='\t')
            new_board = main_board.tracefile(main_board.turn, BLACK, False)
        elif main_board.player == BLACK:
            print('\n{}\tBLACK'.format(main_board.turn), end='\t')
            new_board = main_board.tracefile(main_board.turn + 1, WHITE, False)
        else:
            logger.error('UNEXPECTED VALUE of PLAYER in readmode')
            print('SYSTEM ERROR')
            sys.exit('SYSTEM ERROR')

        if type(new_board) is int:
            if new_board == EMPTY:
                print('1/2-1/2\n\nDRAW')
                return
            elif new_board == WHITE:
                print('1-0\n\nWHITE WINS')
                return
            elif new_board == BLACK:
                print('0-1\n\nBLACK WINS')
                return
            else:
                logger.error('UNEXPECTED VALUE of new_board in readmode')
                print('SYSTEM ERROR')
                sys.exit('SYSTEM ERROR')
        # moving a piece
        else:
            main_board = new_board
            print(main_board.s)
            main_board.print(turnmode=turnmode, reverse=reverse)

        # exit code
        print('ENTER TO NEXT / X TO QUIT ', end='')
        if input() in ['X', 'x']:
            print('QUITTED')
            return


if __name__=="__main__":
    readmode()
                    
                

main.py

                    
#! /usr/bin/env python3
# main.py 
# programmed by Saito-Saito-Saito
# explained on https://Saito-Saito-Saito.github.io/chess
# last updated: 15 August 2020

import playmode
import readmode

settings = {'turnmode_play': True, 'turnmode_read': False, 'reverse_read': False}

def resetting():
    ON_OFF = ['OFF', 'ON']

    while True:
        print('''
    PLAY MODE
        BOARD ROTATION IN BLACK'S TURN: {}

    READ MODE
        BOARD ROTATION IN BLACK'S TURN: {}

        '''.format(ON_OFF[settings['turnmode_play']], ON_OFF[settings['turnmode_read']]))
        command = input("ENTER A COMMAND (P to change playmode / R to change readmode / X to exit) >>> ")
        if command in ['P', 'p', 'PLAY', 'play', 'Play', 'PLAYMODE', 'PlayMode', 'Playmode', 'playmode', 'PLAY MODE', 'Play Mode', 'Play mode', 'play mode']:
            print('''
            BOARD ROTATION IN BLACK'S TURN IN PLAY MODE: {}
            '''.format(ON_OFF[settings['turnmode_play']]))
            while True:
                command = input("ENTER A COMMAND (ON / OFF / EXIT) >>> ")
                if command in ['ON', 'on', 'On']:
                    settings['turnmode_play'] = True
                    break
                elif command in ['OFF', 'Off', 'off']:
                    settings['turnmode_play'] = False
                    break
                elif command in ['EXIT', 'Exit', 'exit', 'X', 'Ex', 'EX', 'ex']:
                    break
        elif command in ['R', 'r', 'READ', 'Read', 'read', 'READMODE', 'ReadMode', 'Readmode', 'readmode', 'READ MODE', 'Read Mode', 'Read mode', 'read mode']:
            print('''
            BOARD ROTATION IN BLACK'S TURN IN READ MODE: {}
            '''.format(ON_OFF[settings['turnmode_read']]))
            while True:
                command = input("ENTER A COMMAND (ON / OFF / EXIT) >>> ")
                if command in ['ON', 'on', 'On']:
                    settings['turnmode_read'] = True
                    settings['reverse_read'] = True
                    break
                elif command in ['OFF', 'Off', 'off']:
                    settings['turnmode_read'] = False
                    settings['reverse_read'] = False
                    break
                elif command in ['EXIT', 'Exit', 'exit', 'X', 'Ex', 'EX', 'ex']:
                    break
        elif command in ['X', 'x', 'Exit', 'EXIT', 'exit', 'EX', 'Ex', 'ex']:
            return



if __name__ == "__main__":
    while True:
        print("\n\nWELCOME TO CHESS\n")
        command = input("ENTER A COMMAND (P to PLAYMODE / R to READMODE / S to SETTINGS / X to EXIT) >>> ")
        if command in ['P', 'p', 'PLAY', 'play', 'Play', 'PLAYMODE', 'PlayMode', 'Playmode', 'playmode', 'PLAY MODE', 'Play Mode', 'Play mode', 'play mode']:
            playmode.playmode(settings['turnmode_play'])
            break

        elif command in ['R', 'r', 'READ', 'Read', 'read', 'READMODE', 'ReadMode', 'Readmode', 'readmode', 'READ MODE', 'Read Mode', 'Read mode', 'read mode']:
            readmode.readmode(settings['turnmode_read'], settings['reverse_read'])
            break

        elif command in ['S', 's', 'SETTINGS', 'settings', 'Settings', 'SETTING', 'Setting', 'setting']:
            resetting()
            
        elif command in ['X', 'x', 'Exit', 'EXIT', 'exit', 'EX', 'Ex', 'ex']:
            break
                    
                

print.py

                    
#! usr/bin/env/ Python3
# print.py
# coded by Saito-Saito-Saito
# explained on https://Saito-Saito-Saito.github.io/chess
# last updated: 15 August 2020
# NOTE: This code is only for explaining, so is not neccessary to run.

from board import *

class BoardPrint(Board):
    def PrintWhiteSide(self):
        print('\n')
        print('\t    a   b   c   d   e   f   g   h')
        print('\t   -------------------------------')
        for rank in range(SIZE - 1, -1, -1):  # down to less
            print('\t{} |'.format(rank + 1), end='')
            for file in range(SIZE):
                print(' {} |'.format(IO.ToggleType(self.board[file][rank])), end='')
            print(' {}'.format(rank + 1))
            print('\t   -------------------------------')
        print('\t    a   b   c   d   e   f   g   h')
        print('\n')
    

if __name__ == "__main__":
    test_board = BoardPrint()
    test_board.print()