3

I have written a program that checks if a chess board is valid. In one part of my code I test, if the amounts of the pieces are correct.

count is dictionary, which is an inventory of the board I want to check. For example (b stands for black, w fo white):

count = {'bking': 1, 'wking': 1, 'bpawn': 3, 'bbishop': 1, 'wrook': 1, 'wqueen': 1}

the possible colors and pieces are available in lists:

colors = ['b', 'w']
pieces = ['queen', 'rook', 'knight', 'bishop', 'pawn']

I have the following ugly if-else statement with multiple conditions:

if count['bking'] == 1 and \
    count['wking'] == 1 and \
    count.get('bqueen', 0) <= 2 and \
    count.get('wqueen', 0) <= 2 and \
    count.get('bpawn', 0) <= 8 and \
    count.get('wpawn', 0) <= 8 and \
    count.get('brook', 0) <= 2 and \
    count.get('wrook', 0) <= 2 and \
    count.get('bknight', 0) <= 2 and \
    count.get('wknight', 0) <= 2 and \
    count.get('bbishop', 0) <= 2 and \
    count.get('wbishop', 0) <= 2 and \
    len(board) <= 32:
        return True
    else:
        return False

Is there a way to simplify this if-else structure with a nested for loop? I realized that the lines with the get() method are very repetitive. My idea was to make an outer for loop that iterates over the colors and an inner loop that iterates over the pieces. The first argument in the get() call is a concatenation of an item in the colors list with an item in the pieces list. Is there a way to do that?

Is there another way to make this if-else statement more pythonic?

This is my first attempt:

for c in colors:
   for p in pieces[:4]:
      if count.get(c + p, 0) <= 2:
   if count.get(c + pieces[-1], 0) <= 8:
      return = True
   else:
      return = False

But that does not work, I get a SyntaxError or an IndentationError.

My original code that seems to work is the following:

# chessDictValidator.py

def boardInventory(board):
    # Makes an inventory of the board to be evaluated.
    count = {}
    for value in board.values():
        count.setdefault(value, 0)
        count[value] += 1
    return count

def boardCounter(board):
    # Checks if amounts of pieces are valid.
    count = boardInventory(board)
    if count['bking'] == 1 and \
    count['wking'] == 1 and \
    count.get('bqueen', 0) <= 2 and \
    count.get('wqueen', 0) <= 2 and \
    count.get('bpawn', 0) <= 8 and \
    count.get('wpawn', 0) <= 8 and \
    count.get('brook', 0) <= 2 and \
    count.get('wrook', 0) <= 2 and \
    count.get('bknight', 0) <= 2 and \
    count.get('wknight', 0) <= 2 and \
    count.get('bbishop', 0) <= 2 and \
    count.get('wbishop', 0) <= 2 and \
    len(board) <= 32:
        return True
    else:
        return False

def fieldValidator(board):
    # Checks if the board contains valid fields.
    fieldOK = 0
    for key in board.keys():
        if key[0] in fieldInt and key[1] in fieldChar:
            fieldOK += 1
        else:
            return False
    if fieldOK == len(board):
        return True

def pieceValidator(board):
    # Checks if the pieces are spelled correctly.
    pieceOK = 0
    for value in board.values():
        if value[0] in pieceColor and value[1:] in pieces:
            pieceOK += 1
        else:
            return False
    if pieceOK == len(board):
        return True

def boardValidator(board):
    # Checks if the board is valid, depending on the tests above. Prints an error message when board is invalid.
    valid = 'This is a valid chess board!'
    invalid = 'Invalid chess board!'
    wrong = 'There is a wrong {0} in your board!'
    numPieces = 'There is something wrong with the allowed amount of pieces!'
    if fieldValidator(board) and pieceValidator(board) and boardCounter(board):
        print(valid)
    elif fieldValidator(board) == False:
        print(invalid, wrong.format('field'))
    elif pieceValidator(board) == False:
        print(invalid, wrong.format('piece'))
    elif boardCounter(board) == False:
        print(invalid, numPieces)

board = {
    '1b': 'bking',
    '6a': 'wqueen',
    '3f': 'brook',
    '4h': 'bknight',
    '3e': 'wking',
    '6d': 'wbishop',
    '2g': 'wbishop',
    '5c': 'bpawn',
    '8g': 'bpawn',
    '7b': 'bpawn',
}

fieldInt = ['1', '2', '3', '4', '5', '6', '7', '8']
fieldChar = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
pieceColor = ['b', 'w']
pieces = ['king', 'queen', 'knight', 'rook', 'bishop', 'pawn']

boardValidator(board)
aurumpurum
  • 932
  • 3
  • 11
  • 27
  • your idea 2 nested for loops is the best way to do it, It's what I think. I don't see why you didn't continue with that approach. – Albin Paul Dec 07 '20 at 21:52
  • Does https://stackoverflow.com/questions/29370317/compressing-multiple-conditions-in-python help? – Karl Knechtel Dec 07 '20 at 21:54
  • `len(board) <= 32` seems suspicious. There are 64 squares on a chess board. – Karl Knechtel Dec 07 '20 at 21:56
  • @KarlKnechtel in my code board represents the given fields and pieces of a given board, for example: board = {'1b': 'bking', '8c': 'bknight', '4a': 'wking', '6c': 'wqueen', '5d': 'brook', '4g': 'bbishop'}. Because every player has 16 pieces, the board dictionary must have less or equal 32 items. – aurumpurum Dec 07 '20 at 22:05

2 Answers2

3

First, I would recommend outsourcing this check to a dedicated function.

Second, you can use a dict with your expected max values, and use a comprehension with all() to perform the check more concisely.

max_counts = {
    'bking': 1,
    'bqueen': 1,
    'brook': 2,
    'bknight': 2,
    'bbishop': 2,
    'bpawn': 8,
    'wking': 1,
    'wqueen': 1,
    'wrook': 2,
    'wknight': 2,
    'wbishop': 2,
    'wpawn': 8
}

def board_is_valid(count, board):
    return len(board) <= 32 and all(
               count[piece] <= ct 
               for (piece, ct) in max_counts.items()
           )

If you want to be slightly less verbose with max_counts, you could try creating a dummy dict with the basic counts for king, queen, rook, etc., and make max_counts the result of adding two copies of that list, one with 'b' prefixed to each key and the other with 'w'. But I don't think thats necessary if this is the entire set of pieces.


Also consider that this may not be a foolproof method of validation for your chessboard. Pawns may promote into any other type of piece besides king, so having more than one queen or more than two rooks is technically possible.

Green Cloak Guy
  • 23,793
  • 4
  • 33
  • 53
  • Green Cloak Guy: thanks. You are right, good point. So that means, I have to raise the values in max_counts by 1 for the queens, rooks, bishops, and knights. This would be foolproof then, correct? – aurumpurum Dec 07 '20 at 22:17
  • Green Cloak Guy: looks elegant, but I have a problem. In my code, count is a dictionary with the played pieces of my board. When bqueen is gone, your function will lead to an IndexError...I will post my original code, so you can see what I mean. – aurumpurum Dec 07 '20 at 22:39
  • @aurumpurum Add another bit to the comprehension after the `for` clause: `if piece in count` – Green Cloak Guy Dec 07 '20 at 23:12
  • Green Cloak Guy: Thanks, your function seems to work! Just for interest: What's the name of the concept, you have used in your function board_is_valid()? You said it is a comprehension, but the order of the condition, the for clause and the the second condition seem to me in totally different order than I expected. Would like to learn more about that... – aurumpurum Dec 08 '20 at 09:25
  • 1
    It's a generator comprehension (like a list comprehension but without the square brackets) given as an argument to the function `all()`. The format of the comprehension conforms to the general format of comprehensions: `output for item in iterable if condition`. In this case, the output per item is just the result of `count[piece] <= ct`, which is either True or False; and since we're iterating through 2-tuples returned from `.items()`, we can unpack them into two variables `(piece, ct)`. – Green Cloak Guy Dec 08 '20 at 14:50
2

A simple way would be to encode the maxima for each category. The only special case is that there must be one king. Instead of having pieces as a list, make it a dict that maps the piece to the number of instances at the start of the game:

pieces = {'queen': 1, 'rook': 2, 'knight': 2, 'bishop': 2, 'pawn': 8, 'king': 1}

Second, by concatenating keys, you make your life more complicated. I would recommend making a nested dictionary, or a list with two dictionaries in it to contain black and white pieces separately:

count = {'b': {'king': 1, 'pawn': 3, 'bishop': 1},
         'w': {'king': 1, 'rook': 1, 'queen': 1}}

Checking is now much easier. First, verify that the keys of each dictionary in count is a subset of the allowed keys. You can do this using the fact that dict.keys returns a set-like object:

count['b'].keys() < pieces.keys()

Next, check that the values for corresponding keys are within the allowed range:

all(pieces[k] >= v for k, v in count['b'].items())

And finally, check that there is a king on the board:

count['b'].get('king') == 1

You should not use count['b']['king'] because that will raise a KeyError when there is no king on the board. It's much more consistent to return False, as you do for all other invalid boards.

Assuming you go with the structure of count I propose, here is how I would write the check:

def validate(count):
    def check_side(side):
        return side.keys() < pieces.keys() and all(pieces[k] >= v for k, v in side.items()) and side.get('king') == 1
    return all(check(v) for v in count.values())

The operators and and all are short-circuiting, so if the answer is False, this will return as early as possible.

Notice also that with this approach you no longer need colors. The information is stored in count.keys(). Redundancy of information is usually not a great idea.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • This code suggestions look elegant to me, Mad Physicist! Because the boards I want to validate are flattened dictionaries, I have to figure out how to convert them into nested dictionaries to make use of your solution. Is there a pythonic way to do that? I have only an ugly non-pythonic solution: https://gist.github.com/DataMower/ad6a7d7f789b1d95bc8b8c1709b90ed7 – aurumpurum Dec 08 '20 at 11:04
  • 1
    `for k, v in flattened.items(): nested[k[0]][k1:] = v` – Mad Physicist Dec 08 '20 at 11:08
  • @aurumpurum. Sounds like you ought to ask another question. Keep in mind that pythonic and unpythonic are extremely subjective – Mad Physicist Dec 08 '20 at 11:13
  • Ok, thanks. I tried out your code: Is there a typo in [k1:] or is this correct? It does not work for me: https://gist.github.com/DataMower/1add4f0a6d2e689cff1887b02f1574f3 – aurumpurum Dec 08 '20 at 11:39
  • Typo: `[k[1:]]` – Mad Physicist Dec 08 '20 at 11:40
  • I'm not going to go off-site to help. If you have another question, please ping me after you ask. I'll be happy to take a look – Mad Physicist Dec 08 '20 at 11:41
  • ok, here's my follow up question: https://stackoverflow.com/q/65198660/11030607 – aurumpurum Dec 08 '20 at 12:11
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/225682/discussion-between-aurumpurum-and-mad-physicist). – aurumpurum Dec 08 '20 at 18:52