Kurt Peek
Kurt Peek

Reputation: 57581

How to make a function determining the winner of Tic-Tac-Toe more concise

I'm writing a Python script which is supposed to allow human and computer players to play Tic Tac Toe. To represent the board, I'm using a 3x3 Numpy array with 1 and 0 for the marks of the players (instead of "X" and "O"). I've written the following function to determine the winner:

import numpy as np

class Board():
    def __init__(self, grid = np.ones((3,3))*np.nan):
        self.grid = grid

    def winner(self):
        rows = [self.grid[i,:] for i in range(3)]
        cols = [self.grid[:,j] for j in range(3)]
        diag = [np.array([self.grid[i,i] for i in range(3)])]
        cross_diag = [np.array([self.grid[2-i,i] for i in range(3)])]

        lanes = np.concatenate((rows, cols, diag, cross_diag))

        if any([np.array_equal(lane, np.ones(3)) for lane in lanes]):
            return 1
        elif any([np.array_equal(lane, np.zeros(3)) for lane in lanes]):
            return 0

So for example, if I execute

board = Board()
board.grid = np.diag(np.ones(3))
print board.winner()

I get the result 1. What bothers me slightly is the repetition of the any statements. I would think there would be a more concise, DRY way of coding this. (I was thinking of a switch/case as in MATLAB but this doesn't exist in Python). Any suggestions?

Upvotes: 2

Views: 251

Answers (4)

Eric O. Lebigot
Eric O. Lebigot

Reputation: 94545

This can be done in two lines, starting from the board (grid): simple sums along columns, rows and the two main diagonals gives you a value of 0 or 3 depending on who is winning (or some intermediate values only if nobody is winning). You can thus calculate something like:

# Score along each column, row and both main diagonals:
scores = (grid.sum(axis=0).tolist() + grid.sum(axis=1).tolist()
          +[grid.trace(), np.flipud(grid).trace()])

# If there is no winner, None is declared the winner:
print "Winner:", 1 if 3 in scores else 0 if 0 in scores else None

where flipud() transforms the diagonal into the anti-diagonal (the diagonal at 90° from the main diagonal) by flipping the array horizontally, so that a simple trace() gives the total value along the anti-diagonal.

Upvotes: 0

Aguy
Aguy

Reputation: 8059

Another option is to check the sum of lanes.

    s = np.sum(lanes, axis=1)
    if 3 in s:
        return 1
    elif 0 in s:
        return 0

Upvotes: 2

Jean-François Fabre
Jean-François Fabre

Reputation: 140216

I have made a loop instead, and return only once, to conform with PEP8 and to be honest to my personal coding standards :)

enumerate in the correct order will yield 0,zeromatrix then 1,onematrix

rval = None
for i,m in enumerate([np.zeros(3),np.ones(3)]):
   if any([np.array_equal(lane, m) for lane in lanes]):
      rval = i; break
return rval

Upvotes: 1

Kurt Peek
Kurt Peek

Reputation: 57581

I found out one way, by using a Lambda function:

any_lane = lambda x: any([np.array_equal(lane, x) for lane in lanes])

if any_lane(np.ones(3)):
    return 1
elif any_lane(np.zeros(3)):
    return 0

This adds an extra line to the code but makes it more legible overall, I reckon.

Upvotes: 0

Related Questions