Main Purpose
This script allows two players to play chess on a virtual chessboard printed on the screen by making use of the Unicode chess characters.
Visual appearence
The chessboard looks like this:
8 ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
7 ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
6
5
4
3
2 ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
1 ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
a b c d e f g h
User interaction
Moves are performed by typing in the start position of the piece in chess notation, [ENTER], and the end position.
For example (starting from the start position):
Start? e2
End? e4
Results in:
8 ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
7 ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
6
5
4 ♙
3
2 ♙ ♙ ♙ ♙ ♙ ♙ ♙
1 ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
a b c d e f g h
Legality checks
This programme also performs many checks to ensure that moves are legal.
It checks:
- If start and end position are both inside the board.
- If a player tries to move an opponents piece.
- If at the given start there is no piece.
- If a piece is being moved unlawfully. # TO DO support castling and en-passant
- If the end location is already occupied by a same colour piece.
- #TO DO: Limit options if the king is in check
AI extension possibility
The main loop loops between function objects to allow a possibility
of extension, introducing AI should be as easy as
replacing a player_turn
with an ai_turn
Board data storage
The board is represented as a dict {Point : piece}, the code
board[Point(x, y)]
returns the piece at position (x, y)
.
Empty squares are not even present in the dictionary as keys.
"""
-- Main Purpose
This script allows two players to play chess on a virtual chessboard
printed on the screen by making use of the Unicode chess characters.
-- Visual appearence
The chessboard looks like this:
8 ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
7 ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
6
5
4
3
2 ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
1 ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
a b c d e f g h
-- User interaction
Moves are performed by typing in the start position of
the piece in chess notation, [ENTER], and the end position.
For example (starting from the start position):
Start? e2
End? e4
Results in:
8 ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
7 ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
6
5
4 ♙
3
2 ♙ ♙ ♙ ♙ ♙ ♙ ♙
1 ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
a b c d e f g h
-- Legality checks
This programme also performs many checks to ensure that moves
are legal.
It checks:
- If start and end position are both inside the board.
- If a player tries to move an opponents piece.
- If at the given start there is no piece.
- If a piece is being moved unlawfully. # TO DO support castling and en-passant
- If the end location is already occupied by a same colour piece.
- #TO DO: Limit options if the king is in check
-- AI extension possibility
The main loop loops between function objects to allow a possibility
of extension, introducing AI should be as easy as
replacing a `player_turn` with an `ai_turn`
-- Board data storage
The board is represented as a dict {Point : piece}, the code
`board[Point(x, y)]` returns the piece at position `(x, y)`.
Empty squares are not even present in the dictionary as keys.
"""
from collections import namedtuple
from itertools import cycle, takewhile
from operator import add, sub
ALPHABET = "abcdefgh"
BOARD_SIZE = 8
Point = namedtuple('Point', ['x', 'y'])
# Board is a dict {Point : piece}.
board = {
Point(0, 6) : "♙",
Point(1, 6) : "♙",
Point(2, 6) : "♙",
Point(3, 6) : "♙",
Point(4, 6) : "♙",
Point(5, 6) : "♙",
Point(6, 6) : "♙",
Point(7, 6) : "♙",
Point(0, 7) : "♖",
Point(1, 7) : "♘",
Point(2, 7) : "♗",
Point(3, 7) : "♕",
Point(4, 7) : "♔",
Point(5, 7) : "♗",
Point(6, 7) : "♘",
Point(7, 7) : "♖",
Point(0, 1) : "♟",
Point(1, 1) : "♟",
Point(2, 1) : "♟",
Point(3, 1) : "♟",
Point(4, 1) : "♟",
Point(5, 1) : "♟",
Point(6, 1) : "♟",
Point(7, 1) : "♟",
Point(0, 0) : "♜",
Point(1, 0) : "♞",
Point(2, 0) : "♝",
Point(3, 0) : "♛",
Point(4, 0) : "♚",
Point(5, 0) : "♝",
Point(6, 0) : "♞",
Point(7, 0) : "♜",
}
def legal_by_deltas(start, end, deltas):
"""
Given `start` and `end` position of a piece that moves by fixed (x, y) deltas,
returns if the `end` is reachable legally.
"""
return end in (Point(start.x + delta.x, start.y + delta.y)
for delta in (Point(p[0], p[1]) for p in deltas))
def knight_jump(start, end, _):
"""
Can a knight jump from start to end?
The board is unused as the knight jumps on pieces in the middle
of its path and the end square is already checked in `make_move`.
"""
KNIGHT_DELTAS = ( (1, 2), (2, 1), (1, -2), (2, -1), (-1, -2), (-2, -1), (-1, 2), (-2, 1) )
return legal_by_deltas(start, end, KNIGHT_DELTAS)
def king_step(start, end, _):
# TO DO: Castling.
"""
Can a king step from start to end?
The board is unused as the king moving one square only
cannot have pieces in the middle
of its path and the end square is already checked in `make_move`.
"""
KING_DELTAS =( (1, -1), (-1, 1), (0, 1), (1, 0), (-1, 0), (0, -1), (1, 1), (-1, -1) )
return legal_by_deltas(start, end, KING_DELTAS)
def rook_move_ignoring_obstruction(start, end):
return start.x == end.x or start.y == end.y
def rook_move(start, end, board):
"""
Can a rook move from start to end?
Also checks if a piece blocks the path.
"""
r = lambda a, b: range(a, b) if a < b else reversed(range(a, b))
if start.x == end.x:
intermediates = (Point(start.x, y) for y in r((start.y + 1), end.y))
if start.y == end.y:
intermediates = (Point(x, start.y) for x in r((start.x + 1), end.x))
return rook_move_ignoring_obstruction(start, end) and all(is_empty(s, board) for s in intermediates)
def bishop_move_ignoring_obstruction(start, end):
delta_x = end.x - start.x
delta_y = end.y - start.y
return abs(delta_x) == abs(delta_y)
def bishop_move(start, end, board):
"""
Can a bishop move from start to end?
"""
delta_x = end.x - start.x
delta_y = end.y - start.y
if delta_x > 0 and delta_y > 0:
ps = ((1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7))
if delta_x > 0 and delta_y < 0:
ps = ((1, -1), (2, -2), (3, -3), (4, -4), (5, -5), (6, -6), (7, -7))
if delta_x < 0 and delta_y > 0:
ps = ((-1, 1), (-2, 2), (-3, 3), (-4, 4), (-5, 5), (-6, 6), (-7, 7))
if delta_x < 0 and delta_y < 0:
ps = ((-1, -1), (-2, -2), (-3, -3), (-4, -4), (-5, -5), (-6, -6), (-7, -7))
intermediates = list(takewhile(lambda x: x != end, (Point(start.x + p[0], start.y + p[1]) for p in ps)))
return bishop_move_ignoring_obstruction(start, end) and all(is_empty(s, board) for s in intermediates)
def is_empty(square, board):
"""
Is the given `square` (Point object) empty on the board?
Being the board a dictionary where the full squares are the keys,
`square in board` tells us if the square is full.
No false values are allowed, all pieces strings are True.
"""
return square not in board
def is_piece_of_color(square, board, color):
return (not is_empty(square, board)) and is_white(board[square]) == color
def pawn_move(start, end, board, color):
"""
Can a pawn move from `start` to `end`?
Note that this function requires the colour of the pawn,
as the pawns are the only piece that cannot go back.
"""
# To-do en-passant
op = sub if color else add
start_y = 6 if color else 1
one_away = Point(start.x, op(start.y, 1))
two_away = Point(start.x, op(start.y, 2))
if end.x == start.x: # Normal : not capturing
if end == one_away:
return True
if start.y == start_y: # Never moved
return end in (one_away, two_away)
if end.x not in (start.x + 1, start.x, start.x - 1): # No more than one step diagonally
return False
# Capturing
one_away_right = Point(one_away.x + 1, one_away.y)
one_away_left = Point(one_away.x - 1, one_away.y)
if is_piece_of_color(end, board, not color) and end in (one_away_right, one_away_left):
return True
return True
def white_pawn_move(start, end, board):
return pawn_move(start, end, board, True)
def black_pawn_move(start, end, board):
return pawn_move(start, end, board, False)
def queen_move(start, end, board):
return rook_move(start, end, board) or bishop_move(start, end, board)
# En-passant and castling validations to be perfected later being more complex.
# Validation for black and white is equal for all but pawns,
# as pawns cannot go back and are asymmetrical.
PIECE_TO_MOVE_VALIDATION = {
"♙" : white_pawn_move,
"♘" : knight_jump,
"♗" : bishop_move,
"♕" : queen_move,
"♔" : king_step,
"♖" : rook_move,
"♟" : black_pawn_move,
"♞" : knight_jump,
"♝" : bishop_move,
"♛" : queen_move,
"♚" : king_step,
"♜" : rook_move
}
def print_board(board):
"""
Prints the given `board : dict{Point:piece}` in a human readable format
and adds notation letters and numbers to the side to aid the user in
inputting their moves.
See __doc__ at the top, section `Visual appearence` to see an example output.
"""
for y in range(BOARD_SIZE):
print(BOARD_SIZE - y, end=" ")
for x in range(BOARD_SIZE):
print(board[Point(x, y)] if Point(x, y) in board else " ", end=" ")
print("\n",end="")
print(" " + ' '.join(ALPHABET) + "\n")
def is_white(piece):
""" Is the given piece white? """
return piece in "♙♘♗♖♕♔"
def make_move(board, start, end, turn):
"""
Performs the validations listed in the main __doc__
section `Legality checks` and actuates the move.
The board is mutated in place.
"""
if start.x not in range(BOARD_SIZE) or start.y not in range(BOARD_SIZE):
raise ValueError("The starting square is not inside the board.")
if end.x not in range(BOARD_SIZE) or end.y not in range(BOARD_SIZE):
raise ValueError("The destination square is not inside the board.")
if start not in board:
raise ValueError("There is no piece to be moved at the given starting location.")
if is_white(board[start]) != turn:
raise ValueError("The current player is attempting to move an opponent's piece.")
if not is_valid_move(board[start], start, end, board):
raise ValueError("The {} does not move in this manner (Or there is a piece blocking its path).".format(board[start]))
if end in board and is_white(board[start]) == is_white(board[end]):
raise ValueError("The destination square is already occupied by a same color piece.")
board[end] = board[start]
del board[start]
def is_valid_move(piece, start, end, board=board):
""" Can the given piece move this way? """
return PIECE_TO_MOVE_VALIDATION[piece](start, end, board)
def ask_chess_coordinate(prompt):
"""
Prompts the user for a square in chess coordinates and
returns a `Point` object indicating such square.
"""
given = input(prompt)
if not (given[0] in ALPHABET and given[1] in "12345678"):
print("Invalid coordinates, [ex: b4, e6, a1, h8 ...]. Try again.")
return ask_chess_coordinate(prompt)
return Point(ALPHABET.index(given[0]), 8 - int(given[1]))
def human_player(board, turn):
"""
Prompts a human player to make a move.
Also shows him the board to inform him about the
current game state and validates the move as
detailed in the main __doc__ section `Legality checks`
"""
print("{}'s Turn.\n".format("White" if turn else "Black"))
print_board(board)
start = ask_chess_coordinate("Start? ")
end = ask_chess_coordinate("End? ")
print("\n\n")
try:
make_move(board, start, end, turn)
except ValueError as e:
print("Invalid move: {}".format(e))
human_player(board, turn)
def interact_with_board(board):
"""
Allows the players to play a game.
"""
for turn, player in cycle(((True, human_player), (False, human_player))):
player(board, turn)
if __name__ == "__main__":
interact_with_board(board)