Search code examples
pythonhashconways-game-of-life

Game of LIfe Hashing Python


I'm trying to create a simple Game of Life by storing Cell objects into a set (have to use classes) and I reach the problem in which I cannot add the cell object into the Set because it is unhashable... is there any way around this? Thanks!

class Cell():
    def __init__(self, row, col):
        self.row = row
        self.col = col
    def getRow(self):
        return self.row
    def getCol(self):
        return self.col
    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

    def __ne__(self, other):
        return not self.__eq__(other)    

class SparseLifeGrid(set):
    # Creates a new infinite-sized game grid with all cells set to dead.
    def __init__(self):
        self.rowList = []
        self.colList = []
        self.cellSet = set()

    def __add__(self, cell):
        self.cellSet.add(cell)
        self.rowList.append(cell.getRow())     
        self.colList.append(cell.getCol())     

    def minRange(self):
        #Returns a 2-tuple (minrow, mincol) that contains the minimum
        #row index and the minimum
        #column index that is currently occupied by a live cell.
        #None is returned if there are no alive cells.        
        return (sorted(self.rowList)[0], sorted(self.rowList)[0])

    def maxRange(self):
        #Returns a 2-tuple (maxrow, maxcol) that contains the
        #maximum row index and the maximum
        #column index that is currently occupied by a live cell.
        #None is returned if there are no live cells.        
        return (sorted(self.rowList,reverse = True)[0],\
                sorted(self.colList,reverse = True)[0])

    def clearCell(self, row, col):
        #Clears the individual cell (row, col) and sets it to dead.
        #If the cell is already dead, no action is taken.
        for item in self:
            if item == Cell(row,col):
                self.remove(item)

    def setCell(self, row, col):
        #Sets the indicated cell (row, col) to be alive.
        #If the cell is already alive, no action is taken.
        self.__add__(Cell(row,col))

    def isLiveCell(self, row, col):
        #Returns a boolean value indicating if the given
        #cell (row, col) contains a live organism.
        return Cell(row,col) in self

    def numLiveNeighbors(self, row, col):
    #checks how many live adjacents (not diagonals I think) a cell has
        surround = 0
        if self.isLiveCell(row+1,col):
            surround += 1
        if self.isLiveCell(row-1,col):
            surround += 1
        if self.isLiveCell(row, col+1):
            surround += 1
        if self.isLiveCell(row, col-1):
            surround += 1
        return surround

G = SparseLifeGrid()
G.setCell(2,3)

Solution

  • You need to make your Cell instances immutable, then create a __hash__ method that always remains the same. If you can't use tuples directly, here's an alternative (that borrows only a little from tuple):

    class Cell:
        # each cell will have exactly two values, so no need for __dict__
        __slots__ = ["row", "col"]
    
        # set up values at construction time using super().__setitem__()
        def __init__(self, row, col):
            super().__setitem__("row", row)
            super().__setitem__("col", col)
            return self
    
        # a Cell is intended to be immutable, so __setattr__ always raises an error
        def __setattr__(self, name, value):
            if hasattr(self, name):
                raise AttributeError("{!r} object attribute {!r} is read-only"
                                     .format(self.__class__.__name__, name))
            else:
                raise AttributeError("{!r} object has no attribute {!r}"
                                     .format(self.__class__.__name__, name))
    
        # comparison operators
        def __eq__(self, other):
            return (isinstance(other, Cell) and
                    self.row == other.row and
                    self.col == other.col)
    
        def __ne__(self, other):
            return not self == other
    
        # hash function, with a value borrowed from a tuple
        def __hash__(self):
            return hash((self.row, self.col))
    

    This is rather a lot of work though, for something that is equivalent to:

    Cell = collections.namedtuple(["row", "col"])
    

    Your grid class also has some issues. For instance, you're overriding __add__, which is used for implementing the addition operator, but you don't return anything, so it won't work like it would be expected to. I suspect you mean to be overriding the add method (with no underscores) instead. However, if that's the case, you'll want to be sure you call super().add() with appropriate arguments if you want your grid to actually function as a set.

    Also, min(lst) should be much faster than sorted(lst)[0] (O(N) rather than O(N log N)).