Tic Tac Toe check for win/tie in Python In the below GameOutcome class I'm checking for wins/ties in Tic Tac Toe. I'm pretty set on breaking the checks into 5 different functions because I'm specifically practicing making functions short and single-purposed. But I'm not at all set on my 5 if statements in the find_winner_or_tie method. Does somebody know a more elegant solution for the find_winner_or_tie method so it doesn't have 5 if statements in it? I sort of which I could just call each of the "check functions" in a loop but I don't know how to do this without hardcoding the names of all 5 of the "check functions".

class GameOutcome:

    def __init__(self):
        self.letter_dict = {'X': -1, 'O': 1, ' ': 0}
        self.state_of_game = None
        self.row_index_of_move = None
        self.column_index_of_move = None
        self.game_outcome = None

    def find_winner_or_tie(self, state_of_game, row_index_of_move, column_index_of_move):

        self.set_board_and_move(state_of_game, row_index_of_move, column_index_of_move)

        if self.check_row_containing_move_for_win():
            return self.game_outcome
        if self.check_column_containing_move_for_win():
            return self.game_outcome
        if self.check_main_diagonal_for_win_iff_it_contains_move():
            return self.game_outcome
        if self.check_off_diagonal_for_win_iff_it_contains_move():
            return self.game_outcome
        if self.check_for_tie():
            return self.game_outcome

        return ''

    def set_board_and_move(self, state_of_game, row_index_of_move, column_index_of_move):
        self.state_of_game = state_of_game
        self.row_index_of_move = row_index_of_move
        self.column_index_of_move = column_index_of_move

    def check_row_containing_move_for_win(self):
        total = 0
        for column in range(3):
            total = total + int(self.letter_dict[self.state_of_game.board[self.row_index_of_move][column]])
            if abs(total) == 3:
                winning_letter = self.state_of_game.board[self.row_index_of_move][self.column_index_of_move]
                self.game_outcome = winning_letter
                return True
        return False

    def check_column_containing_move_for_win(self):
        total = 0
        for row in range(3):
            total = total + int(self.letter_dict[self.state_of_game.board[row][self.column_index_of_move]])
            if abs(total) == 3:
                winning_letter = self.state_of_game.board[self.row_index_of_move][self.column_index_of_move]
                self.game_outcome = winning_letter
                return True
        return False

    def check_main_diagonal_for_win_iff_it_contains_move(self):
        if self.row_index_of_move == self.column_index_of_move:
            total = 0
            for diagonal_indexing in range(3):
                total = total + int(self.letter_dict[self.state_of_game.board[diagonal_indexing][diagonal_indexing]])
                if abs(total) == 3:
                    winning_letter = self.state_of_game.board[self.row_index_of_move][self.column_index_of_move]
                    self.game_outcome = winning_letter
                    return True
        return False

    def check_off_diagonal_for_win_iff_it_contains_move(self):
        if self.row_index_of_move + self.column_index_of_move == 2:
            total = 0
            for off_diagonal_indexing in range(3):
                total = total + int(self.letter_dict[self.state_of_game.board[off_diagonal_indexing][2 - off_diagonal_indexing]])
                if abs(total) == 3:
                    winning_letter = self.state_of_game.board[self.row_index_of_move][self.column_index_of_move]
                    self.game_outcome = winning_letter
                    return True
        return False

    def check_for_tie(self):
        if len(self.state_of_game.available_squares) == 0:
            self.game_outcome = 'Tie'
            return True
        return False
(Note: no code in this post has been run- beware errors.)

I have a couple of issues with your design, and with your review request.

First, you say that you are committed to using five different functions for the checking. That's unfortunate, since this problem is very amenable to being solved with iteration.

Next, you have a class GameOutcome that is being asked to identify a winner or tie. I disagree with this- this is something the game itself should do. The game outcome, IMO, would be a data class just collecting results. The name GameOutcome suggest that this class would have no knowledge of the sequence of moves, or the state of the game board.

With those out of the way, let's talk about your code.

Those names!

I hate your naming style. You have fallen into the trap of making excessively long names for no good purpose. I strongly suggest you review code written by other people (either here on CodeReview or look through github or the pypi repositories) to get a sense of how names should be chosen. 

Consider this line:

    if self.check_row_containing_move_for_win():


The method check_row_containing_move_for_win is an internal method. It is not intended for use by arbitrary external callers, it is only a method in order to package up its code behind a name. 

For an internal function, why not just call it _check_row. 

Similarly, you have an externally callable method named find_winner_or_tie. The declaration:

def find_winner_or_tie(self, state_of_game, row_index_of_move, column_index_of_move):


Here's a better approach:

def find_winner_or_tie(self, state, row, col):
    """ Check if either player has won, or if a tie has been forced. Return 'X' or 'O' 
        for a win, 'Tie' for a tie, and None when the game is not over.

        Parameters:
          state:  state of the game
          row:    row of the last move
          col:    column of the last move
    """


Providing in-depth explanation is the function of comments, not names. Your names should be as long as they need to be, but no longer.

Builtins any() and all()

You are writing code that checks for various conditions. Python supplies the any() and all() built-ins for situations like this. The trick is to learn to construct iterables in line, and that means using generator expressions.

A generator expression is an inside-out for loop in parentheses, like:

(abs(x) for x in somelist)


They resemble, and are related to, list, dictionary, and set comprehensions. In all cases there is an iteration keyword (for ... in ...) and some kind of leading expression. With a generator expression, you are constructing a generator in-line and storing it, evaluating it, or passing it as parameters.

How does this help you? Well, suppose you have a group of functions you want to evaluate for success. Say ... five of them? You might decide that you were going to return if any of those conditions were true. Well ...

def find_winner_or_tie(self, state, row, column):
    """ ... """
    self.set_board_and_move(state, row, column)

    checks = (self.check_row, self.check_column, self.check_diag_lr, self.check_diag_rl, self.check_tie)

    if any(check() for check in checks):   # If any check is true, we know the outcome
        return self.game_outcome

    return None


Less code, more data!

The thing about most games is that they are based on lots of data. And if you write your code correctly, you will find more and more data, with less and less code. That's the case here. Let's take a look at one of your check-functions:

def check_row_containing_move_for_win(self):
    total = 0
    for column in range(3):
        total = total + int(self.letter_dict[self.state_of_game.board[self.row_index_of_move][column]])
        if abs(total) == 3:
            winning_letter = self.state_of_game.board[self.row_index_of_move][self.column_index_of_move]
            self.game_outcome = winning_letter
            return True
    return False


Honestly, that's so long that it's unreadable. So let's refactor out the names:

def check_row(self):
    """Check row containing latest move for a win."""
    bias = self.letter_dict
    board = self.state_of_game.board
    row = self.row_index_of_move

    total = 0
    for column in range(3):
        total = total + bias[board[row][column]]

    if abs(total) == 3:
        self.game_outcome = board[row][0]
        return True
    return False


I think some people have already pointed out sum and some other options. But let's actually spell out what the row and column choices are:

ROWS = [ ((0, 0), (0, 1), (0, 2)),
         ((1, 0), (1, 1), (1, 2)),
         ((2, 0), (2, 1), (2, 2)),
]


Now, you could write code to generate that. In one line, even! But that's not important. What's important is that we know that there's a ROWS variable that can be indexed by row-number and which has a sequence of (row, col) tuples identifying the cells to look at.

Now, how does that help? Well, it helps because you can write a helper function that does the checking for you given a sequence of cells. Like this:

def check_cells(self, indices):
    """ Return True if all gameboard[][] cells specified by indices have same value,
        else False.
    """
    board = self.state_of_game.board
    values = [board[row][col] for row,col in indices]
    v0 = values[0]
    return all(v == v0 for v in values)


Then you can write the check_ functions in terms of that:

def check_row(self):
    """Check row containing latest move for a win."""
    indices = ROWS[self.row_index_of_move]
    return self.check_cells(indices)


You'll have to put in some validation on the diagonals, so you only call the check_cells if the move really was on a diagonal. The key here is that this uses the check_ functions to provide more data to the underlying check_cells, and so the bottom level code where the work is getting done is mainly data-driven.

Extra credit: detect forced ties

Your tie-detector only triggers at the last minute. But there are plenty of circumstances where a tie is inevitable. Why not code your tie-checker to recognize them?