CMU 15-112: Fundamentals of Programming and Computer Science
2d List Case Studies


Note: except for Othello (which is optional), these Case Studies may appear on quizzes, midterms, or the final exam.

You should understand their top-down design, and be prepared to explain what each function or method does in general, and also be prepared to write any function or method from scratch.

Do not attempt to memorize this code. That is not practical and in any case not helpful. Instead, try to understand the code. You would be well-served if you wrote these from scratch, referring back to the sample solutions as needed.
  1. Word Search
  2. Word Search Redux
  3. Connect4
  4. Othello (optional)


  1. Word Search
    # wordSearch1.py
    
    def wordSearch(board, word):
        (rows, cols) = (len(board), len(board[0]))
        for row in range(rows):
            for col in range(cols):
                result = wordSearchFromCell(board, word, row, col)
                if (result != None):
                    return result
        return None
    
    def wordSearchFromCell(board, word, startRow, startCol):
        for drow in [-1, 0, +1]:
            for dcol in [-1, 0, +1]:
                if (drow, dcol) != (0, 0):
                    result = wordSearchFromCellInDirection(board, word,
                                                           startRow, startCol,
                                                           drow, dcol)
                    if (result != None):
                        return result
        return None
    
    def wordSearchFromCellInDirection(board, word, startRow, startCol, drow, dcol):
        (rows, cols) = (len(board), len(board[0]))
        dirNames = [ ["up-left"  ,   "up", "up-right"],
                     ["left"     ,   ""  , "right"   ],
                     ["down-left", "down", "down-right" ] ]
        for i in range(len(word)):
            row = startRow + i*drow
            col = startCol + i*dcol
            if ((row < 0) or (row >= rows) or
                (col < 0) or (col >= cols) or
                (board[row][col] != word[i])):
                return None
        return (word, (startRow, startCol), dirNames[drow+1][dcol+1])
    
    def testWordSearch():
        board = [ [ 'd', 'o', 'g' ],
                  [ 't', 'a', 'c' ],
                  [ 'o', 'a', 't' ],
                  [ 'u', 'r', 'k' ],
                ]
        print(wordSearch(board, "dog")) # ('dog', (0, 0), 'right')
        print(wordSearch(board, "cat")) # ('cat', (1, 2), 'left')
        print(wordSearch(board, "tad")) # ('tad', (2, 2), 'up-left')
        print(wordSearch(board, "cow")) # None
    
    testWordSearch()

  2. Word Search Redux
    # wordSearch2.py
    # This time with a slightly different handling of directions
    
    def wordSearch(board, word):
        (rows, cols) = (len(board), len(board[0]))
        for row in range(rows):
            for col in range(cols):
                result = wordSearchFromCell(board, word, row, col)
                if (result != None):
                    return result
        return None
    
    def wordSearchFromCell(board, word, startRow, startCol):
        possibleDirections = 8 # 3^2 - 1
        for dir in range(possibleDirections):
            result = wordSearchFromCellInDirection(board, word,
                                                   startRow, startCol, dir)
            if (result != None):
                return result
        return None
    
    def wordSearchFromCellInDirection(board, word, startRow, startCol, dir):
        (rows, cols) = (len(board), len(board[0]))
        dirs = [ (-1, -1), (-1, 0), (-1, +1),
                 ( 0, -1),          ( 0, +1),
                 (+1, -1), (+1, 0), (+1, +1) ]
        dirNames = [ "up-left"  ,   "up", "up-right",
                     "left"     ,         "right",
                     "down-left", "down", "down-right" ]
        (drow,dcol) = dirs[dir]    
        for i in range(len(word)):
            row = startRow + i*drow
            col = startCol + i*dcol
            if ((row < 0) or (row >= rows) or
                (col < 0) or (col >= cols) or
                (board[row][col] != word[i])):
                return None
        return (word, (startRow, startCol), dirNames[dir])
    
    def testWordSearch():
        board = [ [ 'd', 'o', 'g' ],
                  [ 't', 'a', 'c' ],
                  [ 'o', 'a', 't' ],
                  [ 'u', 'r', 'k' ],
                ]
        print(wordSearch(board, "dog")) # ('dog', (0, 0), 'right')
        print(wordSearch(board, "cat")) # ('cat', (1, 2), 'left')
        print(wordSearch(board, "tad")) # ('tad', (2, 2), 'up-left')
        print(wordSearch(board, "cow")) # None
    
    testWordSearch()

  3. Connect4
    # connect4.py
    
    # A simple game of connect4 with a text interface
    # based on the wordSearch code written in class.
    
    def playConnect4():
        rows = 6
        cols = 7
        board = makeBoard(rows, cols)
        player = 'X'
        moveCount = 0
        printBoard(board)
        while (moveCount < rows*cols):
            moveCol = getMoveCol(board, player)
            moveRow = getMoveRow(board, moveCol)
            board[moveRow][moveCol] = player
            printBoard(board)
            if checkForWin(board, player):
                print(f'*** Player {player} Wins!!! ***')
                return
            moveCount += 1
            player = 'O' if (player == 'X') else 'X'
        print('*** Tie Game!!! ***')
    
    def makeBoard(rows, cols):
        return [ (['-'] * cols) for row in range(rows) ]
    
    def printBoard(board):
        rows = len(board)
        cols = len(board[0])
        print()
        # first print the column headers
        print('    ', end='')
        for col in range(cols):
            print(str(col+1).center(3), ' ', end='')
        print()
        # now print the board
        for row in range(rows):
            print('    ', end='')
            for col in range(cols):
                print(board[row][col].center(3), ' ', end='')
            print()
    
    def getMoveCol(board, player):
        cols = len(board[0])
        while True:
            response = input(f"Enter player {player}'s move (column number) --> ")
            try:
                moveCol = int(response)-1  # -1 since user sees cols starting at 1
                if ((moveCol < 0) or (moveCol >= cols)):
                    print(f'Columns must be between 1 and {cols}.', end='')
                elif (board[0][moveCol] != '-'):
                    print('That column is full! ', end='')
                else:
                    return moveCol
            except:
                # they did not even enter an integer!
                print('Columns must be integer values! ', end='')
            print('Please try again.')
    
    def getMoveRow(board, moveCol):
        # find first open row from bottom
        rows = len(board)
        for moveRow in range(rows-1, -1, -1):
            if (board[moveRow][moveCol] == '-'):
                return moveRow
        # should never get here!
        assert(False)
    
    def checkForWin(board, player):
        winningWord = player * 4
        return (wordSearch(board, winningWord) != None) # that was easy!
    
    ##############################################
    # taken from wordSearch.py
    ##############################################
    
    def wordSearch(board, word):
        (rows, cols) = (len(board), len(board[0]))
        for row in range(rows):
            for col in range(cols):
                result = wordSearchFromCell(board, word, row, col)
                if (result != None):
                    return result
        return None
    
    def wordSearchFromCell(board, word, startRow, startCol):
        for drow in [-1, 0, +1]:
            for dcol in [-1, 0, +1]:
                if (drow, dcol) != (0, 0):
                    result = wordSearchFromCellInDirection(board, word,
                                                           startRow, startCol,
                                                           drow, dcol)
                    if (result != None):
                        return result
        return None
    
    def wordSearchFromCellInDirection(board, word, startRow, startCol, drow, dcol):
        (rows, cols) = (len(board), len(board[0]))
        dirNames = [ ['up-left'  ,   'up', 'up-right'],
                     ['left'     ,   ''  , 'right'   ],
                     ['down-left', 'down', 'down-right' ] ]
        for i in range(len(word)):
            row = startRow + i*drow
            col = startCol + i*dcol
            if ((row < 0) or (row >= rows) or
                (col < 0) or (col >= cols) or
                (board[row][col] != word[i])):
                return None
        return (word, (startRow, startCol), dirNames[drow+1][dcol+1])
    
    playConnect4()

  4. Othello (optional)
    # othello.py
    # this is optional but interesting!
    
    def make2dList(rows, cols):
        a=[]
        for row in range(rows): a += [[0]*cols]
        return a
    
    def hasMove(board, player):
        (rows, cols) = (len(board), len(board[0]))
        for row in range(rows):
            for col in range(cols):
                if (hasMoveFromCell(board, player, row, col)):
                    return True
        return False
    
    def hasMoveFromCell(board, player, startRow, startCol):
        (rows, cols) = (len(board), len(board[0]))
        if (board[startRow][startCol] != 0):
            return False
        for dir in range(8):
            if (hasMoveFromCellInDirection(board, player, startRow, startCol, dir)):
                return True
        return False
    
    def hasMoveFromCellInDirection(board, player, startRow, startCol, dir):
        (rows, cols) = (len(board), len(board[0]))
        dirs = [ (-1, -1), (-1, 0), (-1, +1),
                 ( 0, -1),          ( 0, +1),
                 (+1, -1), (+1, 0), (+1, +1) ]
        (drow,dcol) = dirs[dir]
        i = 1
        while True:
            row = startRow + i*drow
            col = startCol + i*dcol
            if ((row < 0) or (row >= rows) or (col < 0) or (col >= cols)):
                return False
            elif (board[row][col] == 0):
                # no blanks allowed in a sandwich!
                return False
            elif (board[row][col] == player):
                # we found the other side of the 'sandwich'
                break
            else:
                # we found more 'meat' in the sandwich
                i += 1
        return (i > 1)
    
    def makeMove(board, player, startRow, startCol):
        # assumes the player has a legal move from this cell
        (rows, cols) = (len(board), len(board[0]))
        for dir in range(8):
            if (hasMoveFromCellInDirection(board, player, startRow, startCol, dir)):
                makeMoveInDirection(board, player, startRow, startCol, dir)
        board[startRow][startCol] = player
    
    def makeMoveInDirection(board, player, startRow, startCol, dir):
        (rows, cols) = (len(board), len(board[0]))
        dirs = [ (-1, -1), (-1, 0), (-1, +1),
                 ( 0, -1),          ( 0, +1),
                 (+1, -1), (+1, 0), (+1, +1) ]
        (drow,dcol) = dirs[dir]
        i = 1
        while True:
            row = startRow + i*drow
            col = startCol + i*dcol
            if (board[row][col] == player):
                # we found the other side of the 'sandwich'
                break
            else:
                # we found more 'meat' in the sandwich, so flip it!
                board[row][col] = player
                i += 1
    
    def getPlayerLabel(player):
        labels = ['-', 'X', 'O']
        return labels[player]
    
    def printColLabels(board):
        (rows, cols) = (len(board), len(board[0]))
        print('   ', end='') # skip row label
        for col in range(cols): print(chr(ord('A')+col),' ', end='')
        print()
    
    def printBoard(board):
        (rows, cols) = (len(board), len(board[0]))
        printColLabels(board)
        for row in range(rows):
            print(f'{row+1:2} ', end='')
            for col in range(cols):
                print(getPlayerLabel(board[row][col]), ' ', end='')
            print(f'{row+1:2}')
        printColLabels(board)
    
    def isLegalMove(board, player, row, col):
        (rows, cols) = (len(board), len(board[0]))
        if ((row < 0) or (row >= rows) or (col < 0) or (col >= cols)): return False
        return hasMoveFromCell(board, player, row, col)
    
    def getMove(board, player):
        print('\n**************************')
        printBoard(board)
        while True:
            prompt = 'Enter move for player ' + getPlayerLabel(player) + ': '
            move = input(prompt).upper()
            # move is something like 'A3'
            if ((len(move) != 2) or
                (not move[0].isalpha()) or
                (not move[1].isdigit())):
                print('Wrong format!  Enter something like A3 or D5.')
            else:
                col = ord(move[0]) - ord('A')
                row = int(move[1])-1
                if (not isLegalMove(board, player, row, col)):
                    print('That is not a legal move!  Try again.')
                else:
                    return (row, col)            
    
    def playOthello(rows, cols):
        # create initial board
        board = make2dList(rows, cols)
        board[rows//2][cols//2] = board[rows//2-1][cols//2-1] = 1
        board[rows//2-1][cols//2] = board[rows//2][cols//2-1] = 2
        (currentPlayer, otherPlayer) = (1, 2)
        # and play until the game is over
        while True:
            if (hasMove(board, currentPlayer) == False):
                if (hasMove(board, otherPlayer)):
                    print('No legal move!  PASS!')
                    (currentPlayer, otherPlayer) = (otherPlayer, currentPlayer)
                else:
                    print('No more legal moves for either player!  Game over!')
                    break
            (row, col) = getMove(board, currentPlayer)
            makeMove(board, currentPlayer, row, col)
            (currentPlayer, otherPlayer) = (otherPlayer, currentPlayer)
        print('Goodbye!')
    
    playOthello(8,8)