10
\$\begingroup\$

I'm a Python novice and decided to give Game of Life a try in Python 3:

import os
import time
import curses

class Cell:
    newlabel = ''
    def __init__(self,row,col,label):
        self.row = row
        self.col = col
        self.label = label

    def updatecell(self,celllist, boardsize):
        liveneighbours = self.getliveneighbours(celllist, boardsize)
        if liveneighbours < 2 or liveneighbours > 3: #Kills cell if it has <2 or >3 neighbours
            self.newlabel = ' '
        elif liveneighbours == 2 and self.label == ' ': #Cell doesn't come alive if it is dead and only has 2 neighbours
            self.newlabel = ' '
        else: #Cell comes alive if it is already alive and has 2/3 neighbours or if it is dead and has 3 neighbours
            self.newlabel = 0

    def getliveneighbours(self,celllist,boardsize):
        count = 0
        for row in range(self.row-1,self.row+2):
            try:
                celllist[row]
            except:    #Handles vertical wrapping. If there's an error, it checks the wrapped cell
                if row < 0:
                    row += boardsize[0]
                else:
                    row -= boardsize[0]
            for col in range(self.col-1,self.col+2):
                try: #Handles horizontal wrapping. If there's an error, it checks the wrapped cell
                    celllist[row][col]
                except:
                    if col < 0:
                        col += boardsize[1]
                    else:
                        col -= boardsize[1]
                if not celllist[row][col].label:
                    count += 1
        if not self.label: #Subtracts the cell from the neighbours count
            return count-1
        else:
            return count

    def updatelabel(self): #Updates the cell's label after all cell updates have been processes
        self.label = self.newlabel


class Board:
    celllist = {} #Dict of rows
    def __init__(self, rows, columns):
        self.rows = rows
        self.columns = columns
        self.generate()

    def printboard(self): #Prints the board to the terminal using curses
        for num, row in self.celllist.items():
            line = ''
            for col, cell in enumerate(row):
                line += str(cell.label)
            terminal.addstr(num, 0, line)
        terminal.refresh()

    def generate(self): #Adds all the cells to the board
        for row in range(self.rows-1):
            self.celllist[row] = []
            for col in range(self.columns):
                self.celllist[row].append(Cell(row,col,' '))

    def updateboard(self): #Prompts each cell to update and then update their label
        for row in self.celllist:
            for cell in self.celllist[row]:
                cell.updatecell(self.celllist, (self.rows-1, self.columns))
        for row in self.celllist:
            for cell in self.celllist[row]:
                cell.updatelabel()

if __name__ == "__main__":
    terminal = curses.initscr() # Opens a curses window
    curses.noecho()
    curses.cbreak()
    terminal.nodelay(1) #Don't wait for user input later
    rows, columns = os.popen('stty size', 'r').read().split()
    board = Board(int(rows), int(columns))
    board.celllist[6][8].label = 0
    board.celllist[7][9].label = 0
    board.celllist[7][10].label = 0
    board.celllist[8][8].label = 0
    board.celllist[8][9].label = 0
    while 1:
        board.printboard()
        time.sleep(0.1)
        board.updateboard()
        char = terminal.getch()
        if char == 113: #Checks for ASCII Char code for q and then breaks loop
            break
    curses.endwin() #Closes curses window

I've written it to work based on your terminal size and I've made the sides wrap around because it was the only solution I could think of. Also, at the end, I've included a glider example as a test.

Questions:

  1. Is my implementation pythonic enough (particularly concerning ranges and my constant use of iteration)?

  2. Are the data structures that I've used for the celllist (arrays in a dict) suitable?

  3. Is curses a good way of displaying the game? Would pyGame be better?

  4. Is the overall code style good?

  5. To further develop this, is there a better algorithm I could implement or a better method (other than wrapping the sides) of displaying all the cells?

\$\endgroup\$
5
\$\begingroup\$
  1. Running your code through pycodestyle will highlight some issues to make the code more familiar to Pythonistas. Other than that:
    • while 1 should be written as while True (Explicit is better than implicit)
    • Rather than checking for a specific character to quit I would just assume that people know about Ctrl-c
    • Use as easily readable names as possible. updatecell, for example, could be update_cell or even update since it's implicit that it's the Cell that's being updated.
    • Usually if __name__ == "__main__": is followed by simply main() or sys.exit(main()) (probably not relevant in your case). This makes the code more testable. Aim to minimize the amount of top-level code in general.
  2. The board is really a matrix, so it would IMO be better to represent it using a list of lists or ideally a matrix class. A dict has no intrinsic order, so semantically it's the wrong thing to use in any case.
  3. Somebody else will have to answer this.
  4. This is too subjective to answer.
  5. Some suggestions:
    • Try to take into account the fact that the Game of Life board is infinite in all directions. Ideally your implementation should handle this, but you could also use it as a stop condition - when things get too close to the edge to be able to figure out the next step with certainty the program could stop.
    • You can avoid conversions between numbers and strings by using numbers or even booleans everywhere (for example stored in a value or state field rather than label). You can then write a separate method to display a Board, converting values to whatever pair of characters you want, and possibly with decorations.
    • Rather than updating Cells individually by saving their new value you can replace the entire Board with a new one at each step.
    • The time.sleep() currently does not take into account that the Board update time might change. This is unlikely to be a problem for a small board, but if you want completely smooth frames you'll want to either use threads or a tight loop which checks whether it's time to print the current Board yet.
\$\endgroup\$
  • \$\begingroup\$ Thanks! How would I do this: "Rather than updating Cells individually by saving their new value you can replace the entire Board with a new one at each step." ? \$\endgroup\$ – HydraulicSheep Apr 3 '18 at 5:02
  • 1
    \$\begingroup\$ You currently populate the newlabel field and then updatelabel once all the Cells are processed. You can avoid re-entering each Cell by creating the new Board based on the current Board, conceptually replacing newlabel with newboard. \$\endgroup\$ – l0b0 Apr 3 '18 at 6:18

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.