#!/usr/bin/env python3
"""
Simple two‑player chess in the terminal.
Pieces are shown with single‑letter abbreviations:
  P N B R Q K  (White)
  p n b r q k  (Black)
Empty squares are '.'.
Players type moves as: e2 e4   (source dest)
Optional promotion: e7 e8 q   (promote to queen)
"""

from copy import deepcopy

# Board representation: 8x8 list of strings.
# Row 0 = rank 8, Row 7 = rank 1, Column 0 = file a.
def init_board():
    b = [
        list("rnbqkbnr"),
        list("pppppppp"),
        list("........"),
        list("........"),
        list("........"),
        list("........"),
        list("PPPPPPPP"),
        list("RNBQKBNR"),
    ]
    return b

def print_board(board):
    print("  a b c d e f g h")
    for r in range(8):
        print(8 - r, end=' ')
        for c in range(8):
            print(board[r][c], end=' ')
        print(8 - r)
    print("  a b c d e f g h\n")

def algebraic_to_coord(sq):
    file = sq[0].lower()
    rank = int(sq[1])
    c = ord(file) - ord('a')
    r = 8 - rank
    return r, c

def coord_to_algebraic(r, c):
    return chr(ord('a') + c) + str(8 - r)

def opponent(color):
    return 'b' if color == 'w' else 'w'

def piece_color(piece):
    if piece.isupper():
        return 'w'
    if piece.islower():
        return 'b'
    return None

def is_inside(r, c):
    return 0 <= r < 8 and 0 <= c < 8

# Directions for sliding pieces
DIRS = {
    'N': [(-2, -1), (-2, 1), (-1, -2), (-1, 2), (1, -2), (1, 2), (2, -1), (2, 1)],
    'B': [(-1, -1), (-1, 1), (1, -1), (1, 1)],
    'R': [(-1, 0), (1, 0), (0, -1), (0, 1)],
    'Q': [(-1, -1), (-1, 1), (1, -1), (1, 1), (-1, 0), (1, 0), (0, -1), (0, 1)],
    'K': [(-1, -1), (-1, 1), (1, -1), (1, 1), (-1, 0), (1, 0), (0, -1), (0, 1)],
}

def square_attacked(board, r, c, by_color, en_passant_target):
    # pawn attacks
    pawn_dir = -1 if by_color == 'w' else 1
    for dc in (-1, 1):
        rp = r + pawn_dir
        cp = c + dc
        if is_inside(rp, cp):
            if board[rp][cp] == ('P' if by_color == 'w' else 'p'):
                return True
    # knight attacks
    for dr, dc in DIRS['N']:
        rp, cp = r + dr, c + dc
        if is_inside(rp, cp):
            p = board[rp][cp]
            if p.lower() == 'n' and piece_color(p) == by_color:
                return True
    # sliding attacks
    for piece_type in ('B', 'R', 'Q'):
        for dr, dc in DIRS[piece_type]:
            rp, cp = r + dr, c + dc
            while is_inside(rp, cp):
                p = board[rp][cp]
                if p != '.':
                    if piece_color(p) == by_color:
                        if p.upper() == piece_type or p.upper() == 'Q':
                            return True
                    break
                rp += dr
                cp += dc
    # king attacks
    for dr, dc in DIRS['K']:
        rp, cp = r + dr, c + dc
        if is_inside(rp, cp):
            p = board[rp][cp]
            if p.lower() == 'k' and piece_color(p) == by_color:
                return True
    return False

def find_king(board, color):
    target = 'K' if color == 'w' else 'k'
    for r in range(8):
        for c in range(8):
            if board[r][c] == target:
                return r, c
    return None

def in_check(board, color, en_passant_target):
    kr, kc = find_king(board, color)
    return square_attacked(board, kr, kc, opponent(color), en_passant_target)

def generate_moves(board, color, castling, en_passant_target):
    moves = []  # each move: (sr, sc, dr, dc, promotion_piece or None, special flag)
    pawn_dir = -1 if color == 'w' else 1
    start_rank = 6 if color == 'w' else 1
    promotion_rank = 0 if color == 'w' else 7

    for r in range(8):
        for c in range(8):
            piece = board[r][c]
            if piece == '.' or piece_color(piece) != color:
                continue
            p = piece.upper()
            if p == 'P':
                # single forward
                nr = r + pawn_dir
                if is_inside(nr, c) and board[nr][c] == '.':
                    if nr == promotion_rank:
                        for promo in ('Q', 'R', 'B', 'N'):
                            moves.append((r, c, nr, c, promo, None))
                    else:
                        moves.append((r, c, nr, c, None, None))
                    # double forward
                    if r == start_rank:
                        nr2 = r + 2 * pawn_dir
                        if board[nr2][c] == '.':
                            moves.append((r, c, nr2, c, None, None))
                # captures
                for dc in (-1, 1):
                    nc = c + dc
                    nr = r + pawn_dir
                    if not is_inside(nr, nc):
                        continue
                    target = board[nr][nc]
                    if target != '.' and piece_color(target) == opponent(color):
                        if nr == promotion_rank:
                            for promo in ('Q', 'R', 'B', 'N'):
                                moves.append((r, c, nr, nc, promo, None))
                        else:
                            moves.append((r, c, nr, nc, None, None))
                    # en passant
                    if (nr, nc) == en_passant_target:
                        moves.append((r, c, nr, nc, None, 'ep'))
            elif p == 'N':
                for dr, dc in DIRS['N']:
                    nr, nc = r + dr, c + dc
                    if not is_inside(nr, nc):
                        continue
                    target = board[nr][nc]
                    if target == '.' or piece_color(target) == opponent(color):
                        moves.append((r, c, nr, nc, None, None))
            elif p in ('B', 'R', 'Q'):
                dirs = DIRS[p]
                for dr, dc in dirs:
                    nr, nc = r + dr, c + dc
                    while is_inside(nr, nc):
                        target = board[nr][nc]
                        if target == '.':
                            moves.append((r, c, nr, nc, None, None))
                        else:
                            if piece_color(target) == opponent(color):
                                moves.append((r, c, nr, nc, None, None))
                            break
                        nr += dr
                        nc += dc
            elif p == 'K':
                for dr, dc in DIRS['K']:
                    nr, nc = r + dr, c + dc
                    if not is_inside(nr, nc):
                        continue
                    target = board[nr][nc]
                    if target == '.' or piece_color(target) == opponent(color):
                        moves.append((r, c, nr, nc, None, None))
                # castling
                if color == 'w':
                    rank = 7
                    king_side = castling['wK']
                    queen_side = castling['wQ']
                else:
                    rank = 0
                    king_side = castling['bK']
                    queen_side = castling['bQ']
                if king_side:
                    if board[rank][5] == '.' and board[rank][6] == '.':
                        if not in_check(board, color, en_passant_target):
                            if not square_attacked(board, rank, 5, opponent(color), en_passant_target) and \
                               not square_attacked(board, rank, 6, opponent(color), en_passant_target):
                                moves.append((r, c, rank, 6, None, 'O-O'))
                if queen_side:
                    if board[rank][1] == '.' and board[rank][2] == '.' and board[rank][3] == '.':
                        if not in_check(board, color, en_passant_target):
                            if not square_attacked(board, rank, 2, opponent(color), en_passant_target) and \
                               not square_attacked(board, rank, 3, opponent(color), en_passant_target):
                                moves.append((r, c, rank, 2, None, 'O-O-O'))
    return moves

def make_move(board, move, castling, en_passant_target):
    sr, sc, dr, dc, promo, flag = move
    piece = board[sr][sc]
    new_board = deepcopy(board)
    new_board[sr][sc] = '.'
    captured = new_board[dr][dc]
    # en passant capture
    if flag == 'ep':
        cap_r = sr
        cap_c = dc
        captured = new_board[cap_r][cap_c]
        new_board[cap_r][cap_c] = '.'
    # promotion
    if promo:
        new_piece = promo.upper() if piece.isupper() else promo.lower()
        new_board[dr][dc] = new_piece
    else:
        new_board[dr][dc] = piece
    # castling rook move
    if flag == 'O-O':
        rank = dr
        new_board[rank][5] = new_board[rank][7]  # rook to f
        new_board[rank][7] = '.'
    elif flag == 'O-O-O':
        rank = dr
        new_board[rank][3] = new_board[rank][0]  # rook to d
        new_board[rank][0] = '.'
    # update castling rights
    new_castling = castling.copy()
    if piece.upper() == 'K':
        if piece.isupper():
            new_castling['wK'] = new_castling['wQ'] = False
        else:
            new_castling['bK'] = new_castling['bQ'] = False
    if piece.upper() == 'R':
        if sr == 7 and sc == 0:
            new_castling['wQ'] = False
        if sr == 7 and sc == 7:
            new_castling['wK'] = False
        if sr == 0 and sc == 0:
            new_castling['bQ'] = False
        if sr == 0 and sc == 7:
            new_castling['bK'] = False
    if captured.upper() == 'R':
        if dr == 7 and dc == 0:
            new_castling['wQ'] = False
        if dr == 7 and dc == 7:
            new_castling['wK'] = False
        if dr == 0 and dc == 0:
            new_castling['bQ'] = False
        if dr == 0 and dc == 7:
            new_castling['bK'] = False
    # new en passant target
    new_ep = None
    if piece.upper() == 'P' and abs(dr - sr) == 2:
        new_ep = ((sr + dr)//2, sc)
    return new_board, new_castling, new_ep

def legal_moves(board, color, castling, en_passant_target):
    pseudo = generate_moves(board, color, castling, en_passant_target)
    legal = []
    for m in pseudo:
        nb, nc, ne = make_move(board, m, castling, en_passant_target)
        if not in_check(nb, color, ne):
            legal.append(m)
    return legal

def main():
    board = init_board()
    turn = 'w'   # w = white, b = black
    castling = {'wK': True, 'wQ': True, 'bK': True, 'bQ': True}
    en_passant = None
    move_number = 1
    while True:
        print_board(board)
        player = 'White' if turn == 'w' else 'Black'
        if in_check(board, turn, en_passant):
            print(f"{player} is in check.")
        legal = legal_moves(board, turn, castling, en_passant)
        if not legal:
            if in_check(board, turn, en_passant):
                print(f"Checkmate! {'White' if turn == 'b' else 'Black'} wins.")
            else:
                print("Stalemate! It's a draw.")
            break
        while True:
            try:
                user = input(f"{player} to move (e.g., e2 e4): ").strip()
                if not user:
                    continue
                parts = user.split()
                if len(parts) < 2:
                    raise ValueError
                src, dst = parts[0], parts[1]
                promo = parts[2].upper() if len(parts) >= 3 else None
                sr, sc = algebraic_to_coord(src)
                dr, dc = algebraic_to_coord(dst)
                candidate = None
                for m in legal:
                    if m[0]==sr and m[1]==sc and m[2]==dr and m[3]==dc:
                        if m[4] is None or (promo and m[4]==promo):
                            candidate = m
                            break
                if candidate is None:
                    print("Illegal move, try again.")
                    continue
                board, castling, en_passant = make_move(board, candidate, castling, en_passant)
                turn = opponent(turn)
                if turn == 'w':
                    move_number += 1
                break
            except (ValueError, IndexError):
                print("Bad input format, try again.")
                continue

if __name__ == "__main__":
    main()
