2
\$\begingroup\$

I made a tic tac toe win checker that represents a 3 x 3 board as a list of list with variables X O B as 'X', 'O' and ' '. However the input can be any valid and invalid board of tic tac toe game. e.g.

board = [[X,X,X],
         [X,X,X],
         [O,O,B]]
board = [[X,O,X],
         [X,O,X],
         [O,O,B]]

I've created a checker that works perfect for ever possible board however it's very inefficient and long. I've tried to simplify it myself with limited succes. Any help making it more compact and effienct would be much appreciated.

#    'X wins''O wins''draw''no winner'
X,O,B = 'X', 'O', ' '
def tictactoe(board):
    N = 'no winner'
    D = 'draw'
    # row operation
    for i in range(0,3): 
        if (board[i][0]==board[i][1]==board[i][2]) and board[i][0] is not B:
            for k in range(0,3):
                if i!=k and (board[k][0]==board[k][1]==board[k][2]) and board[k][0] is not B:
                    if (board[0] == board[1] and not board[2][0]==board[2][1]==board[2][2] and board[2][0] is not B)\
                        or (board[1] == board[2] and not board[0][0]==board[0][1]==board[0][2] and board[0][0] is not B)\
                        or (board[0] == board[2] and not board[1][0]==board[1][1]==board[1][2] and board[1][0] is not B):
                        return(board[i][0]+" wins")
                    return D
            return(board[i][0]+" wins")
    # changes the columns to rows - needed for the column operation    
    boardc = [[board[0][0], board[1][0], board[2][0]],
          [board[0][1], board[1][1], board[2][1]],
          [board[0][2], board[1][2], board[2][2]]]
    # column operation
    for j in range(0,3):
        if (board[0][j]==board[1][j]==board[2][j]) and board[0][j] is not B:
            for l in range(0,3):
                if j!=l and (board[0][l]==board[1][l]==board[2][l]) and board[0][l] is not B: 
                    #does the same as above in the row operation but uses the rotated board
                    if (boardc[0] == boardc[1] and not boardc[2][0]==boardc[2][1]==boardc[2][2] and boardc[2][0] is not B)\
                        or (boardc[1] == boardc[2] and not boardc[0][0]==boardc[0][1]==boardc[0][2] and boardc[0][0] is not B)\
                        or (boardc[0] == boardc[2] and not boardc[1][0]==boardc[1][1]==boardc[1][2] and boardc[1][0] is not B):
                        return(board[0][j]+" wins")
                    return D
            return(board[0][j]+" wins")
    #diagonal winners
    if (board[0][0]==board[1][1]==board[2][2]) and board[0][0] is not B:
        return(board[0][0]+" wins")
    if (board[2][0]==board[1][1]==board[0][2]) and board[2][0] is not B:
        return(board[2][0]+" wins")
    #no winner - if there's no winner yet and there's empty spaces
    for a in range(0,3):
        for b in range(0,3):
            if board[a][b] == B:
                return N
    #where the came has finished and it's a draw 
    else: 
        return D

I tried to simplify the row and column operation that differenciates the double line win from the double line draw using for loops however it came out longer than the current solution.

I'm very new to coding and a friend told me of possible way to do it with arrays. Would this be possible?

Thank you very much and I'm sorry if this was the wrong place to ask this question.

\$\endgroup\$
5
\$\begingroup\$

The current code is repetitive and difficult to read. You are correct in sensing that your current code is "inefficient". More specifically, it it repetitive (checks for horizontal and vertical wins are similar), verbose (long, complex lines of conditional code), and therefore difficult to read and reason about.

Don't return presentation-oriented strings from algorithmically-oriented functions. Checking for tic-tac-toe wins is an algorithmic process: receive a grid, do a bunch of logic, and return a winner. Rather than returning the ultimate message that you might print for an end-user (e.g., "X wins" or "Draw"), functions like that should generally focus on returning data-centric answers. For example, one natural approach for a win-checking function is to return either the winning player or None. Leave the creation of user-facing messages to a different part of the program.

Transposing a matrix. With those preliminaries out of the way, let's start with your best idea, where you create boardc, which in your words "changes the columns to rows". The more technical term for this operation is transpose, and we could pull that operation out into a separate function. While we're at it, we could do the same for your horizontal-win checking logic:

def transpose(board):
    # Your code moved to a function.
    return [
        [board[0][0], board[1][0], board[2][0]],
        [board[0][1], board[1][1], board[2][1]],
        [board[0][2], board[1][2], board[2][2]],
    ]

def tictactoe_horiz(board):
    # Your code moved to a function.
    for i in range(0,3):
        if (board[i][0]==board[i][1]==board[i][2]) and board[i][0] is not B:
            for k in range(0,3):
                if i!=k and (board[k][0]==board[k][1]==board[k][2]) and board[k][0] is not B:
                    if (board[0] == board[1] and not board[2][0]==board[2][1]==board[2][2] and board[2][0] is not B)\
                        or (board[1] == board[2] and not board[0][0]==board[0][1]==board[0][2] and board[0][0] is not B)\
                        or (board[0] == board[2] and not board[1][0]==board[1][1]==board[1][2] and board[1][0] is not B):
                        return(board[i][0])
                    return None
            return(board[i][0])
    return None

We have made real progress. With those two functions in place, our tic-tac-toe function is much nicer. Also notice how this code is simpler because our row-checking function returns data (winner or None) rather than user-facing messages. That behavior allows us to take advantage of boolean short-circuiting to combine different checks easily.

def tictactoe(board):
    # Checks for horizontal and vertical wins.
    return (
        tictactoe_horiz(board) or
        tictactoe_horiz(transpose(board))
    )

Simplifying row-checking. So far so good, but the row-checking itself is pretty verbose and therefore requires quite careful reading to confirm its correctness. One key problem with your current approach is accessing the matrix value via list indexes rather than taking full advantage of Python's iteration capabilities. More specifically, if you have a matrix, you can iterate over the rows directly, without using list indexes, and the same thing applies to iterating over the cells within a row. For example, one might write the following code to check for a horizontal win. There are certainly more compact ways to do this, but this approach has the virtue of being super easy to understand.

def tictactoe_horiz(board):
    for row in board:
        if all(cell == X for cell in row):
            return X
        if all(cell == O for cell in row):
            return O
    return None

Simplifying diagonal-checking. One way to simplify diagonal checks is to build on the idea behind transpose() and figure out another data transformation that would allow use to reuse the row-checking logic. The details are explained here, but the short version is that the trick is to shift and pad the rows before doing the transposing. But taking that approach requires further adjustments: for example, we need to filter out the pad values before applying the row-checking function; and our transposing function would need to be generalized rather than using hard-coded indexes. A simpler approach that sticks closer to your current code is just to move the diagonal checks to their own function:

def tictactoe_diag(board):
    # Your code moved to a function.
    if (board[0][0]==board[1][1]==board[2][2]) and board[0][0] is not B:
        return board[0][0]
    if (board[2][0]==board[1][1]==board[0][2]) and board[2][0] is not B:
        return board[2][0]
    return None

def tictactoe(board):
    # Now checks for all types of wins.
    return (
        tictactoe_horiz(board) or
        tictactoe_horiz(transpose(board)) or
        tictactoe_diag(board)
    )

In partial defense of hard-coding: an alternative. As you know, your current code takes a very hard-coded approach to the board indexes. Much of your logic is devoted to examining specific cells in the matrix, via their indexes. What would happen if we fully embraced hard-coded indexes -- but in a more purposeful way by putting the hard-coded values in a data structure? The outcome is quite nice. We define a single constant, the checking logic is simple, and we write very little algorithmic code:

X, O, B = ('X', 'O', ' ')

WINNERS = (
    [(0, 0), (0, 1), (0, 2)], # Horizontal
    [(1, 0), (1, 1), (1, 2)],
    [(2, 0), (2, 1), (2, 2)],
    [(0, 0), (1, 0), (2, 0)], # Vertical
    [(0, 1), (1, 1), (2, 1)],
    [(0, 2), (1, 2), (2, 2)],
    [(0, 0), (1, 1), (2, 2)], # Diagonal
    [(0, 2), (1, 1), (2, 0)],
)

def tictactoe_alt(board):
    for indexes in WINNERS:
        row = [board[r][c] for r, c in indexes]
        if all(cell == X for cell in row):
            return X
        if all(cell == O for cell in row):
            return O
    return None
\$\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.