HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Unicode Chess PvP with Move Validation

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
withvalidationunicodechessmovepvp

Problem

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 ♖ ♘ ♗ ♕ ♔ ♗

Solution

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.

Code Snippets

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
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)
intermediates = list(takewhile(lambda x: x != end, (Point(start.x + x, start.y + y) for x, y in ps)))
return end in (Point(start.x + x, start.y + y) for x, y in deltas)
import math

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

Context

StackExchange Code Review Q#132582, answer score: 4

Revisions (0)

No revisions yet.