12
\$\begingroup\$

Let me preface this by detailing some of my design philosophy. The intent is to view the board from the perspective of the active player. Player moves are also factored into the design. When determining move logic, all that needs to happen is comparing against '0'. For checking the whiteness or blackness of a piece, ord(c) % 265 should help by pulling the remainder and checking if it's in range(4,10) or range(10,16). Anyway, any optimizations and critique is welcome! I'm particularly interested if it involves numpy, scipy | bitwise operators.

chess.py

import numpy as np

chrs = {
    'b_checker': u'\u25FB',
    'b_pawn': u'\u265F',
    'b_rook': u'\u265C',
    'b_knight': u'\u265E',
    'b_bishop': u'\u265D',
    'b_king': u'\u265A',
    'b_queen': u'\u265B',
    'w_checker': u'\u25FC',
    'w_pawn': u'\u2659',
    'w_rook': u'\u2656',
    'w_knight': u'\u2658',
    'w_bishop': u'\u2657',
    'w_king': u'\u2654',
    'w_queen': u'\u2655'
}


def get_checkers():
    bw_row = [chrs['b_checker'], chrs['w_checker']]*4
    bw_checkers = []

    for i in range(8):
        bw_checkers.append(bw_row if i % 2 == 0 else bw_row[::-1])

    bw_checkers = np.array(bw_checkers)
    wb_checkers = bw_checkers[::-1]
    return {'W': wb_checkers, 'B': bw_checkers}


def get_board():

    def get_army(user):
        u = user.lower()
        guard = [chrs[u+'_rook'], chrs[u+'_knight'], chrs[u+'_bishop']]
        rear = guard + [chrs[u+'_king'], chrs[u+'_queen']] + guard[::-1]
        front = [chrs[u+'_pawn']]*8

        if user == 'B':
            return [rear, front]
        else:  # since white moves first
            return [front, rear]

    board = [squad for squad in get_army('B')]

    for _ in range(4):
        board.append(['0']*8)

    board += get_army('W')

    return np.array(board)


def print_board(board, checkers, user):
    chks = checkers[user]
    temp = board.copy() if user == 'W' else board.copy()[::-1]

    for i, row in enumerate(temp):
        for j, c in enumerate(row):
            print('', chks[i][j] if c == '0' else c, end='', flush=True)
        print()


if __name__ == "__main__":
    checkers = get_checkers()
    board = get_board()
    user = 'W'
    print_board(board, checkers, user)

Present Output:

White's Perspective

white

♜ ♞ ♝ ♚ ♛ ♝ ♞ ♜
♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
◼ ◻ ◼ ◻ ◼ ◻ ◼ ◻
◻ ◼ ◻ ◼ ◻ ◼ ◻ ◼
◼ ◻ ◼ ◻ ◼ ◻ ◼ ◻
◻ ◼ ◻ ◼ ◻ ◼ ◻ ◼
♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
♖ ♘ ♗ ♔ ♕ ♗ ♘ ♖

Black's Perspective

black

♖ ♘ ♗ ♔ ♕ ♗ ♘ ♖
♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
◻ ◼ ◻ ◼ ◻ ◼ ◻ ◼
◼ ◻ ◼ ◻ ◼ ◻ ◼ ◻
◻ ◼ ◻ ◼ ◻ ◼ ◻ ◼
◼ ◻ ◼ ◻ ◼ ◻ ◼ ◻
♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
♜ ♞ ♝ ♚ ♛ ♝ ♞ ♜
\$\endgroup\$
  • \$\begingroup\$ The board should be with a white square in the bottom right corner, and the white king on the right, so the black representation is correct, the white one is mirrored. en.wikipedia.org/wiki/Chess \$\endgroup\$ – Maarten Fabré Nov 4 '19 at 15:57
  • \$\begingroup\$ @MaartenFabré Ah, I see now. The L-R diagonal should be black, while the R-L diagonal should be white. A 180 rotation should yield the same checker state. \$\endgroup\$ – T145 Nov 4 '19 at 16:49
  • 2
    \$\begingroup\$ I recommend using background/foreground color ANSI escape codes for terminal emulators \$\endgroup\$ – Michał Krzysztof Feiler Nov 4 '19 at 22:13
  • 1
    \$\begingroup\$ @MichałKrzysztofFeiler You mean using something like this: stackoverflow.com/questions/12492810/… \$\endgroup\$ – T145 Nov 5 '19 at 1:15
  • \$\begingroup\$ @T145 yes that's what I meant \$\endgroup\$ – Michał Krzysztof Feiler Nov 5 '19 at 1:44
12
\$\begingroup\$

numpy

In this case there is no need to use. For an 8 by 8 board, filled with strings, there is no advantage to using it, apart from the possibility to index row and column at the same time

enums

You have a few properties wich would be best presented as an enum. The color and the type of the piece

import enum


class Color(enum.Enum):
    WHITE = 0
    BLACK = 1


class Piece(enum.Enum):
    EMPTY = enum.auto()
    PAWN = enum.auto()
    ROOK = enum.auto()
    KNIGHT = enum.auto()
    BISHOP = enum.auto()
    KING = enum.auto()
    QUEEN = enum.auto()

tuple

You have a chrs dictionary with keys that contain both the color and the piece. A better approach here would be to use tuples as keys

chrs = {
    (Color.WHITE, Piece.EMPTY): "\u25FB",
    (Color.WHITE, Piece.PAWN): "\u265F",
    (Color.WHITE, Piece.ROOK): "\u265C",
    (Color.WHITE, Piece.KNIGHT): "\u265E",
    (Color.WHITE, Piece.BISHOP): "\u265D",
    (Color.WHITE, Piece.KING): "\u265A",
    (Color.WHITE, Piece.QUEEN): "\u265B",
    (Color.BLACK, Piece.EMPTY): "\u25FC",
    (Color.BLACK, Piece.PAWN): "\u2659",
    (Color.BLACK, Piece.ROOK): "\u2656",
    (Color.BLACK, Piece.KNIGHT): "\u2658",
    (Color.BLACK, Piece.BISHOP): "\u2657",
    (Color.BLACK, Piece.KING): "\u2654",
    (Color.BLACK, Piece.QUEEN): "\u2655",
}

board

You keep a black and a white board. Better would be to keep one board, and just flip it at the time of presentation.

def board_begin():
    return (
        [
            [
                (Color.WHITE, Piece.ROOK),
                (Color.WHITE, Piece.KNIGHT),
                (Color.WHITE, Piece.BISHOP),
                (Color.WHITE, Piece.QUEEN),
                (Color.WHITE, Piece.KING),
                (Color.WHITE, Piece.BISHOP),
                (Color.WHITE, Piece.KNIGHT),
                (Color.WHITE, Piece.ROOK),
            ],
            [(Color.WHITE, Piece.PAWN) for _ in range(8)],
            *[[None] * 8 for _ in range(4)],
            [(Color.BLACK, Piece.PAWN) for _ in range(8)],
            [
                (Color.BLACK, Piece.ROOK),
                (Color.BLACK, Piece.KNIGHT),
                (Color.BLACK, Piece.BISHOP),
                (Color.BLACK, Piece.QUEEN),
                (Color.BLACK, Piece.KING),
                (Color.BLACK, Piece.BISHOP),
                (Color.BLACK, Piece.KNIGHT),
                (Color.BLACK, Piece.ROOK),
            ],
        ]
    )

Here I use None to represent an empty tile, and replace it by a white or black tile at the time of display.

flip board

If you use numpy to keep your board, you can np.flip, or a simple routine like this:

def flip(board):
    return [
        row[::-1] for row in reversed(board)
    ]

display the board

Here a simple routine, which takes a boolean flag on whether to flip it:

def display_board(board, flip_board=False):
    for i, row in enumerate(board if not flip_board else flip(board)):
        row_strings = [
            chrs.get(tile, chrs[(Color((i + j) % 2), Piece.EMPTY)])
            for j, tile in enumerate(row)
        ]
        print("".join(row_strings))

using dict.get to replace the empty tiles by the correct squares


board = board_begin()
[[(<Color.WHITE: 0>, <Piece.ROOK: 3>),
        (<Color.WHITE: 0>, <Piece.KNIGHT: 4>),
        (<Color.WHITE: 0>, <Piece.BISHOP: 5>),
        (<Color.WHITE: 0>, <Piece.QUEEN: 7>),
        (<Color.WHITE: 0>, <Piece.KING: 6>),
        (<Color.WHITE: 0>, <Piece.BISHOP: 5>),
        (<Color.WHITE: 0>, <Piece.KNIGHT: 4>),
        (<Color.WHITE: 0>, <Piece.ROOK: 3>)],
       [(<Color.WHITE: 0>, <Piece.PAWN: 2>),
        (<Color.WHITE: 0>, <Piece.PAWN: 2>),
        (<Color.WHITE: 0>, <Piece.PAWN: 2>),
        (<Color.WHITE: 0>, <Piece.PAWN: 2>),
        (<Color.WHITE: 0>, <Piece.PAWN: 2>),
        (<Color.WHITE: 0>, <Piece.PAWN: 2>),
        (<Color.WHITE: 0>, <Piece.PAWN: 2>),
        (<Color.WHITE: 0>, <Piece.PAWN: 2>)],
       [None, None, None, None, None, None, None, None],
       [None, None, None, None, None, None, None, None],
       [None, None, None, None, None, None, None, None],
       [None, None, None, None, None, None, None, None],
       [(<Color.BLACK: 1>, <Piece.PAWN: 2>),
        (<Color.BLACK: 1>, <Piece.PAWN: 2>),
        (<Color.BLACK: 1>, <Piece.PAWN: 2>),
        (<Color.BLACK: 1>, <Piece.PAWN: 2>),
        (<Color.BLACK: 1>, <Piece.PAWN: 2>),
        (<Color.BLACK: 1>, <Piece.PAWN: 2>),
        (<Color.BLACK: 1>, <Piece.PAWN: 2>),
        (<Color.BLACK: 1>, <Piece.PAWN: 2>)],
       [(<Color.BLACK: 1>, <Piece.ROOK: 3>),
        (<Color.BLACK: 1>, <Piece.KNIGHT: 4>),
        (<Color.BLACK: 1>, <Piece.BISHOP: 5>),
        (<Color.BLACK: 1>, <Piece.QUEEN: 7>),
        (<Color.BLACK: 1>, <Piece.KING: 6>),
        (<Color.BLACK: 1>, <Piece.BISHOP: 5>),
        (<Color.BLACK: 1>, <Piece.KNIGHT: 4>),
        (<Color.BLACK: 1>, <Piece.ROOK: 3>)]]
display_board(board, flip_board=False)
♜♞♝♛♚♝♞♜
♟♟♟♟♟♟♟♟
◻◼◻◼◻◼◻◼
◼◻◼◻◼◻◼◻
◻◼◻◼◻◼◻◼
◼◻◼◻◼◻◼◻
♙♙♙♙♙♙♙♙
♖♘♗♕♔♗♘♖
display_board(board, flip_board=True)
♖♘♗♔♕♗♘♖
♙♙♙♙♙♙♙♙
◻◼◻◼◻◼◻◼
◼◻◼◻◼◻◼◻
◻◼◻◼◻◼◻◼
◼◻◼◻◼◻◼◻
♟♟♟♟♟♟♟♟
♜♞♝♚♛♝♞♜

Board class

If you want to incorporate moves etc, it might pay to make a Class of the board, with it's own display, __getitem__ and move routines

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Just wanted to note that your output of the grid is presently wrong. When flipped, the squares should be also, not just the main units. \$\endgroup\$ – T145 Nov 4 '19 at 15:26
  • 1
    \$\begingroup\$ depends on what you mean with flip. Your code just mirrors it along an axis, but doesn't do a 180° rotation. If you look at a chess board, both black and white have a white square at their bottom right corner. For white the king is on the right, for black on the left, so I think my board is correct \$\endgroup\$ – Maarten Fabré Nov 4 '19 at 15:43
  • \$\begingroup\$ I've added some pictures that should help w/ the visual reference. :) \$\endgroup\$ – T145 Nov 4 '19 at 15:55
  • \$\begingroup\$ When I test displaying the board w/ flip=True, I get a TypeError: 'bool' object is not callable for some reason. \$\endgroup\$ – T145 Nov 4 '19 at 17:39
  • \$\begingroup\$ I renamed the flag to flip_board to solve this \$\endgroup\$ – Maarten Fabré Nov 4 '19 at 23:32
7
\$\begingroup\$

Piece dictionary

chrs is a very generic name. Since this is all about chess, you should be more specific by calling it something like PIECES or ELEMENTS. I chose to capitalize the name because you are using it as a module level constant, which according to PEP8 should have capitalized names.

Also instead of <color prefix>_<piece name>, it might be more elegant to have a "two-stage" dictionary, like

ELEMENTS = {
    'b': {
        'checker': u'\u25FB',
        'pawn': u'\u265F',
        'rook': u'\u265C',
        'knight': u'\u265E',
        'bishop': u'\u265D',
        'king': u'\u265A',
        'queen': u'\u265B',
    },
    'w': {
        'checker': u'\u25FC',
        'pawn': u'\u2659',
        'rook': u'\u2656',
        'knight': u'\u2658',
        'bishop': u'\u2657',
        'king': u'\u2654',
        'queen': u'\u2655'
    }
}

Using this approach would help you to get rid of all the string concatenation in order to access the correct elements of the dictionary. With this approach the access would be like ELEMENTS[<color prefix>][<piece name>].

Magic values

There are a few magic values like 'B', 'W', or '0'. They should be replaced with module level constants or an Enum. From my experience this helps to avoid typos and makes it easier to change those values.

Numpy

From the code shown in your question, I don't think Numpy is the right tool for the job here. Numpy can play its strengths mainly when applying uniform operations to larger "lumps" of numerical data. A 2-dimensional chessboard with 8x8 fields where you mainly perform operations at distinct locations is likely not a good match for this description. Since there is also quite some conversion between Python and Numpy data types, there is a good chance that this overhead will decrease the performance compared to plain Python code.

| improve this answer | |
\$\endgroup\$
6
\$\begingroup\$

Python-specific improvements:

  • creating bw_checkers (in get_checkers function). Instead of appending repeatedly to previously created empty list:

    for i in range(8):
        bw_checkers.append(bw_row if i % 2 == 0 else bw_row[::-1]) 
    

    use old-good list comprehension:

    bw_checkers = [bw_row if i % 2 == 0 else bw_row[::-1] for i in range(8)]
    
  • composing string keys in get_army() function. Formatted strings f"" give a better visual perception:

    ...
    guard = [chrs[f'{u}_rook'], chrs[f'{u}_knight'], chrs[f'{u}_bishop']]
    
  • get_board function.
    Creating the initial board with board = [squad for squad in get_army('B')] is redundantly, but essentially the same as board = get_army('B').
    Appending 4 rows of 8 0s with:

    for _ in range(4):
        board.append(['0'] * 8)
    

    is flexibly replaced with list multiplication:

    board += [['0'] * 8] * 4
    

    or board += [['0'] * 8 for _ in range(4)] - to avoid cross-mutability if those rows happen to be modified in further potential game

  • board.copy() (in print_board function) is redundant as the board argument itself is created with np.array(board) (as new array)

| improve this answer | |
\$\endgroup\$

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.