aurumpurum
aurumpurum

Reputation: 1082

Can I use a nested for loop for an if-else statement with multiple conditions in python?

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)

Upvotes: 3

Views: 189

Answers (2)

Mad Physicist
Mad Physicist

Reputation: 114548

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.

Upvotes: 2

Green Cloak Guy
Green Cloak Guy

Reputation: 24711

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.

Upvotes: 3

Related Questions