7
\$\begingroup\$

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)
\$\endgroup\$
  • 2
    \$\begingroup\$ The idea and implementation looks pretty cool :) \$\endgroup\$ – Ave Jun 20 '16 at 23:24
  • 1
    \$\begingroup\$ You might find github.com/thomasahle/sunfish interesting. \$\endgroup\$ – Quentin Pradet Jun 21 '16 at 9:44
  • 1
    \$\begingroup\$ The board looks so awesome! \$\endgroup\$ – coderodde Jun 21 '16 at 12:07
4
\$\begingroup\$

Repeated logic

You have an is_empty helper function that you don't use in make_move validation. You also check that the user input fits into the board both in ask_chess_coordinate and in make_move. You can keep the validation in ask_chess_coordinate an remove it from make_move since it makes more sense to warn about such error this early.

Recursion

ask_chess_coordinate and human_player both use recursion to handle illegal moves/positions. But I don't see an interest to that as you’re not modifying their parameters. Using an explicit loop feels better here:

def ask_chess_coordinate(prompt):
    """
    Prompts the user for a square in chess coordinates and
    returns a `Point` object indicating such square.
    """
    while True:
        given = input(prompt)
        if not (given[0] in ALPHABET and given[1] in "12345678"):
            print("Invalid coordinates, [ex: b4, e6, a1, h8 ...]. Try again.")
        else:
            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`
    """
    while True:
        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))
        else:
            break

Unpacking

It is my personal taste, but I find unpacking sexier than indexing. You can use it in various places:

  • ask_chess_coordinates (even though it makes it a bit more verbose :/)

    def ask_chess_coordinate(prompt):
        """
        Prompts the user for a square in chess coordinates and
        returns a `Point` object indicating such square.
        """
        while True:
            try:
                x, y = input(prompt)
                y = 8 - int(y)
            except ValueError:
                print("Invalid format. Expecting a letter and a digit [ex: b4, e6, a1, h8 ...].")
            else:
                if x not in ALPHABET and y not in range(BOARD_SIZE):
                    print("Coordinates out of bounds. Try again.")
                else:
                    return Point(ALPHABET.index(x), y)
    
  • bishop_move:

    intermediates = list(takewhile(lambda x: x != end, (Point(start.x + x, start.y + y) for x, y in ps)))
    
  • legal_by_delta:

    return end in (Point(start.x + x, start.y + y) for x, y in deltas)
    

You get the point.

sign

import math

def sign(x):
    return int(math.copysign(1, x))

Can help you simplify some "delta" generation:

def bishop_move(start, end, board):
    """
    Can a bishop move from start to end?
    """
    delta_x = sign(end.x - start.x)
    delta_y = sign(end.y - start.y)
    ps = ((delta_x * i, delta_y * i) for i in range(1, BOARD_SIZE))

    intermediates = takewhile(end.__ne__, (Point(start.x + x, start.y + y) for x, y in ps))
    return bishop_move_ignoring_obstruction(start, end) and all(is_empty(s, board) for s in intermediates)

(I also changed the lambda to propose an alternative and removed converting intermediates to a list as you don't need it.)

def rook_move(start, end, board):
    """
    Can a rook move from start to end?

    Also checks if a piece blocks the path.
    """
    def r(a, b):
        direction = sign(b - a)
        return range(a + direction, b, direction)

    if start.x == end.x:
        intermediates = (Point(start.x, y) for y in r(start.y, end.y))
    if start.y == end.y:
        intermediates = (Point(x, start.y) for x in r(start.x, end.x))

    return rook_move_ignoring_obstruction(start, end) and all(is_empty(s, board) for s in intermediates)

By the way, your function had a bug. Try printing the list of intermediates positions instead of returning something and call it with rook_move(Point(3,4), Point(3, 1), None) ;)

TODO list

You should add pawn promotion to your list, probably before castling or en-passant, but after limiting moves for checks (because you need that to check for end of game).

Given the amount of functions that take the board as parameter, you may want to define a class instead. Or at least:

if __name__ == "__main__":
    interact_with_board(board.copy())

to easily restart games.

\$\endgroup\$
4
\$\begingroup\$

This is pretty cool! I like that you're using the unicode. I do have some points about readability/usability though.

You have DELTAS constants, but they're all defined within functions, instead I would make these constants global. That would mean they're all defined in the same place, and you could maybe even have a dictionary of DELTAS with keys for 'knight', 'king' etc. I also think you should store all those deltas as points rather than just pairs of co-ordinates that you need to turn into points.

Also named tuples are great and handy, but a custom class offers you more options. You could set up a Point class so that you could just sum them directly. By defining an __add__ method you can just sum two points with Point() + Point().

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

    def __repr__(self):
        return "Point(x={}, y={})".format(self.x, self.y)

By combining those options, you can make a much more simple legal_by_deltas function:

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 (start + delta for delta in deltas)

You could even expand Point further with an __eq__ function that can handle Point() == Point(), which would do away with the need for rook_move_ignoring_obstruction.

In general, I think you have too many little functions, it would be more readable to me to avoid abstracting all the time. is_empty doesn't make sense to me, when you could just use the test directly. Compare:

all(is_empty(s, board) for s in intermediates)

to

all(square not in board for square in intermediates)
\$\endgroup\$
  • 2
    \$\begingroup\$ It is even possible to define class Point(namedtuple('Point', 'x y')) to get the best of both worlds (__eq__ and __repr__ are already defined by namedtuple). \$\endgroup\$ – Mathias Ettinger Jun 21 '16 at 9:32
4
\$\begingroup\$

Since this mostly looks like it is good shape, I hate to suggest it, but I would strongly consider using a list of lists for your board. There are several advantages to this approach.

  1. The board size is fixed, so all moves would still be O(1) as they are with a dictionary (the main reason for dicts as opposed to lists)
  2. This allows the syntax board[row][col] which is arguably nicer.
  3. You get the ability to write board[row] and [board[i][col] for i in range(8)] for quite quick ways of obtaining a row or column of the board: something that I'd imagine would be useful for AI stuff.
  4. Lists have faster access as indexes are just memory jumps rather than a hash function and a memory jump

lastly, a suggestion for how to implement check: after a move is made, check if your king is in check, and if so say that the move is illegal. This is preferable to checking if the king is in check first because it will also prevent people from moving a piece that was in-between an enemy piece and their king. Overall, this is a really cool project, and half of the reason I'm making suggestions is to see what happens with it.

\$\endgroup\$
  • \$\begingroup\$ You may use ` [BACKQUOTE] to highlight code. Good answer, welcome to Codereview. \$\endgroup\$ – Caridorc Jun 24 '16 at 14:44
  • \$\begingroup\$ I also just realized that the last 3 lines of move_pawn are redundant. The function returns True no matter what happens with the if statement. \$\endgroup\$ – Oscar Smith Jun 25 '16 at 20:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.