4
\$\begingroup\$

A prospective employer asked me for a code sample, but unfortunately this is the best project I've done, most of my other programs were very short (~100 lines). Should I send this in/try to refactor it? Or would it be best to send a shorter sample, or quickly write a new project?

#!/usr/bin/python

import curses
import letters # Letter scores and distributions
import libsquares # Locations of bonus squares
import random
import re
from optparse import OptionParser

# Command-line options
parser = OptionParser()
parser.add_option("-c", "--challenge", action="store_true", dest="challenge",
                  default=False, help="Enable challenges (disabled by default)")
parser.add_option("-d", "--dictionary", dest="dict", default="twl",
                  help="Choose dictionary (pick \"twl\" or \"sowpods\")")
(options, args) = parser.parse_args()

# Choose your dictionary
if options.dict == "sowpods":
   dictionary = open('sowpods.txt', 'r') # SOWPODS dictionary - International
else:
   dictionary = open('TWL06.txt', 'r')     #TWL dictionary - American

# --------------------------------------------
# Defines a "player" class
class Player:
    def __init__(player, name):
        player.name = name
        player.score = 0
        player.rack = []
        player.index = -1

# Defines a class for a square on the board
class Square:
    def __init__(square, x, y, multiplier):
        square.x = x
        square.y = y
        # Note that the multiplier gets set to 1 when a tile is placed
        # on the board
        square.multiplier = multiplier
        square.letter = ""
        square.age = -1

# Defines a class for a tile that hasn't yet been placed on the board
class Tile:
    def __init__(tile, x, y, letter):
        tile.x = x
        tile.y = y
        tile.letter = letter

# Defines a class for a turn.  A turn object gives you the player, the
# tiles used, the squares used on the board, the score of the move,
# and its validity.
class Turn:
    def __init__(turn, player, lettersUsed, squaresUsed, addedScore, validity):
        turn.player = player # The object "player."
        turn.lettersUsed = lettersUsed
        turn.squaresUsed = squaresUsed
        turn.addedScore = addedScore
        # Validity of a turn can be 0, 1, or 2.  0 means invalid.  1
        # means valid.  2 means if you challenge it, you won't lose
        # your turn, but the word won't be taken off the table either.
        turn.validity = validity

# -------------------------------------------
# Redraws the rack, which should happen every time a letter is placed.
def redrawRack(rack, stdscr):
    for i in range(7):
        stdscr.delch(19, 15 + i)
    for i in range(len(rack)):
        stdscr.addch(19, 15 + i, rack[i])

# Visually shuffle the rack.
def shuffleRack(rack, stdscr):
    random.shuffle(rack)
    redrawRack(rack, stdscr)

# Display score
def displayScore(players, numPlayers, stdscr):
    for i in range(70):
        stdscr.delch(2, 18)
    for i in range(numPlayers):
        stdscr.addstr(2, 18 + 9*i, str(players[i].score))

# Returns "1" if all words are proper Scrabble words, returns 0 if
# they are not.
def verifyWords(words):
    # First, prune word list so that no word appears more than once.
    # This is so the "index" function works correctly.
    prunedList = []
    for word in words:
        if word in prunedList: pass
        else: prunedList.append(word)

    # Each number in the list "check" is 0 or 1, and if it's 0, the
    # corresponding word is invalid, and if it's 1, the corresponding
    # word is valid.  Each word starts "invalid" and becomes valid
    # once it is found in the dictionary.
    check = [0] * len(prunedList)

    dictionary.seek(0) # Go to beginning of file

    for line in dictionary:
        for word in prunedList:
            expression = "^" + word + "\n" + "$"
            object = re.compile(expression)
            if object.match(line) != None:
                check[prunedList.index(word)] = 1

    if check == [1] * len(prunedList): return 1
    else: return 0

# Given a tile and the direction of a word that it's a part of, find
# the coordinates of the first letter of the word.  Also, the last
# letter of the word.  Note that seek only finds coordinates in one
# direction.
def seek(x0, y0, direction, squares):
    if direction == "horiz":
        xLeft = x0
        xRight = x0
        while xLeft > 0 and squares[(xLeft-1, y0)].letter != "":
            xLeft = xLeft - 1
        while xRight < 14 and squares[(xRight+1, y0)].letter != "":
            xRight = xRight + 1
        return (xLeft, xRight)

    elif direction == "vert":
        yUp = y0
        yDown = y0
        while yDown < 14 and squares[(x0, yDown+1)].letter != "":
            yDown = yDown + 1
        while yUp > 0 and squares[(x0, yUp-1)].letter != "":
            yUp = yUp - 1
        return (yUp, yDown)

    else: raise NameError('Unspecified direction')

# Given a board configuration, return the score.  The board
# configuration specifies the age of every tile, and it returns the
# score associated with all tiles of age 0.  (age 0 means it was
# placed on the square on that particular turn).  Also it finds all
# words generated by a bunch of new tiles.
def findAndScore(squares):
    score = 0
    wordList = []

    # First find which tiles are new
    newTiles = []
    for tuple in squares.keys():
        if squares[tuple].age == 0:
            newTiles.append(squares[tuple])

    # Since squares must form a word, we can take two squares and see
    # which coordinate they differ by, and that will give us the
    # direction of the word.  We can count the parallel word, and then
    # count all the perpendicular words.  If there is one square, the
    # direction can be horizontal.

    if len(newTiles) == 1:
        # Check to see what direction it's in, that is, where the old
        # tiles are.  We want to find the start and end of a proposed
        # word both horizontally and vertically, and if the start
        # equals the end, it's not a word.
        horizRange = seek(newTiles[0].x, newTiles[0].y, "horiz", squares)
        vertRange = seek(newTiles[0].x, newTiles[0].y, "vert", squares)
        if horizRange[0] != horizRange[1]:
            direction = "horiz"
        elif vertRange[0] != vertRange[1]:
            direction = "vert"
        else: return 3
    elif len(newTiles) > 1:
        if newTiles[0].x - newTiles[1].x == 0:
            direction = "vert"
        elif newTiles[0].y - newTiles[1].y == 0:
            direction = "horiz"
        else:
            raise NameError('Two squares do not form a line')
    else:
        raise NameError('len(newTiles) < 1')

    # Now we count the parallel word (we can only count this once, we
    # can't count it once for each letter, which is why we have to
    # count it separately).  Make sure to check that tiles form one
    # continuous word.

    # To find the start and end of the word, we use seek:

    wordRange = seek(newTiles[0].x, newTiles[0].y, direction, squares)
    word = ""
    wordScore = 0
    if direction == "horiz":
        # Check that tiles form one continuous word
        for tile in newTiles:
            if tile.x in range(wordRange[0], wordRange[1]+1): pass
            else: return 3
        y = newTiles[0].y

        wordBonus = 1 # Multiply by this at the end - DW, TW
        for x in range(wordRange[0], wordRange[1] + 1):
            # Find the word
            word = word + squares[(x, y)].letter
            # Score the word - count letters first, then once the word
            # is formed, multiply the DW and TW scores in.

            letterBonus = 1 # Accounts for DL, TL bonuses
            letterValue = letters.score[squares[(x, y)].letter]
            if squares[(x, y)].age == 0:
                bonus = squares[(x, y)].multiplier
                if bonus == "DW": wordBonus = 2*wordBonus
                elif bonus == "TW": wordBonus = 3*wordBonus
                elif bonus == "DL": letterBonus = 2*letterBonus
                elif bonus == "TL": letterBonus = 3*letterBonus
                else: pass
            wordScore = wordScore + letterValue*letterBonus
        wordScore = wordScore*wordBonus

    elif direction == "vert":
        # Check that tiles form one continuous word
        for tile in newTiles:
            if tile.y in range(wordRange[0], wordRange[1]+1): pass
            else: return 3
        x = newTiles[0].x

        wordBonus = 1 # Multiply by this at the end - DW, TW
        for y in range(wordRange[0], wordRange[1] + 1):
            # Find the word
            word = word + squares[(x, y)].letter
            # Score the word

            letterBonus = 1
            letterValue = letters.score[squares[(x, y)].letter]
            if squares[(x, y)].age == 0:
                bonus = squares[(x, y)].multiplier
                if bonus == "DW": wordBonus = 2*wordBonus
                elif bonus == "TW": wordBonus = 3*wordBonus
                elif bonus == "DL": letterBonus = 2*letterBonus
                elif bonus == "TL": letterBonus = 3*letterBonus
                else: pass
            wordScore = wordScore + letterValue*letterBonus
        wordScore = wordScore*wordBonus
    else: raise NameError('Wrong direction')
    wordList.append(word) # Add parallel word to list of words
    score = score + wordScore

    # Now count perpendicular words in the exact same way!
    for tile in newTiles:
        word = ""
        wordScore = 0
        if direction == "horiz":
            wordRange = seek(tile.x, tile.y, "vert", squares)
            x = tile.x

            wordBonus = 1
            for y in range(wordRange[0], wordRange[1] + 1):
                # Find the word
                word = word + squares[(x, y)].letter
                # Score the word

                letterBonus = 1
                letterValue = letters.score[squares[(x, y)].letter]
                if squares[(x, y)].age == 0:
                    bonus = squares[(x, y)].multiplier
                    if bonus == "DW": wordBonus = 2*wordBonus
                    elif bonus == "TW": wordBonus = 3*wordBonus
                    elif bonus == "DL": letterBonus = 2*letterBonus
                    elif bonus == "TL": letterBonus = 3*letterBonus
                    else: pass
                wordScore = wordScore + letterValue*letterBonus
            wordScore = wordScore*wordBonus

        elif direction == "vert":
            wordRange = seek(tile.x, tile.y, "horiz", squares)
            y = tile.y

            wordBonus = 1
            for x in range(wordRange[0], wordRange[1] + 1):
                # Find the word
                word = word + squares[(x, y)].letter
                # Score the word

                letterBonus = 1
                letterValue = letters.score[squares[(x, y)].letter]
                if squares[(x, y)].age == 0:
                    bonus = squares[(x, y)].multiplier
                    if bonus == "DW": wordBonus = 2*wordBonus
                    elif bonus == "TW": wordBonus = 3*wordBonus
                    elif bonus == "DL": letterBonus = 2*letterBonus
                    elif bonus == "TL": letterBonus = 3*letterBonus
                    else: pass
                wordScore = wordScore + letterValue*letterBonus
            wordScore = wordScore*wordBonus

        else: raise NameError('Wrong direction')
        if wordRange[0] != wordRange[1]:
            wordList.append(word) # Add perpendicular word to word list
            score = score + wordScore

    # Bingos
    if len(newTiles) == 7:
        score = score + 50

    # Now we have counted all the tiles and found all the words!
    return [wordList, score]

# Removes a turn.
def removeTurn(turn, players, numPlayers, squares, stdscr):
    # Reset score
    turn.player.score = turn.player.score - turn.addedScore
    displayScore(players, numPlayers, stdscr)
    # Reset squares
    for square in turn.squaresUsed:
        square.letter = ""
        if (square.x, square.y) in libsquares.bonuses.keys():
            color = libsquares.colors[square.multiplier]
            stdscr.addch(square.y, square.x, " ", curses.color_pair(color))
        else: stdscr.addch(square.y, square.x, ".", curses.color_pair(1))
    # Reset racks
    for letter in turn.lettersUsed:
        turn.player.rack.append(letter)
    redrawRack(turn.player.rack, stdscr)
    # Subtract from age of tiles
    for tuple in squares.keys():
        if squares[tuple].age > -1:
            squares[tuple].age = squares[tuple].age - 1

# Play the tiles.  Tiles are written as a tile object.
def playTiles(player, tiles, players, numPlayers, squares, stdscr):
    lettersUsed = []
    squaresUsed = []
    # See which letters and squares are being used
    for tile in tiles:
        lettersUsed.append(tile.letter)
        squaresUsed.append(squares[(tile.x, tile.y)])

    # Check that at least one letter has been used
    if len(tiles) < 1: return 3
    # Check that letters are not on top of other letters
    for tile in tiles:
        if squares[(tile.x, tile.y)].letter != "":
            return 3
    # Check that tiles are in a line
    if len(tiles) == 1: direction = "horiz"
    else:
        # If x coordinates are the same
        if tiles[0].x - tiles[1].x == 0:
            direction = "vert"
        # If y coordinates are the same
        elif tiles[0].y - tiles[1].y == 0:
            direction = "horiz"
        else:
            return 3
    if direction == "horiz":
        # Make sure y coordinates are the same
        for tile in tiles:
            if tile.y != tiles[0].y:
                return 3
    elif direction == "vert":
        # Make sure x coordinates are the same
        for tile in tiles:
            if tile.x != tiles[0].x:
                return 3
    else: raise NameError('Wrong direction')

    # Check that tiles form one connected word (done in
    # "findAndScore")

    # Check to see if it's the first move.
    firstMove = 1
    for tuple in squares.keys():
        if squares[tuple].age > -1: firstMove = 0

    # Check that tiles link to at least one old tile, unless it's the
    # first move.
    link = 0
    for tile in tiles:
        if tile.y + 1 <= 14:
            if squares[(tile.x, tile.y+1)].age > -1: link = 1
        if tile.y - 1 >= 0:
            if squares[(tile.x, tile.y-1)].age > -1: link = 1
        if tile.x + 1 <= 14:
            if squares[(tile.x+1, tile.y)].age > -1: link = 1
        if tile.x - 1 >= 0:
            if squares[(tile.x-1, tile.y)].age > -1: link = 1
    if link == 0 and firstMove == 0: return 3

    # If it's the first move, check to see that the tiles go through
    # the center.
    if firstMove == 1:
        goThroughCenter = 0
        for tile in tiles:
            if tile.x == 7 and tile.y == 7: goThroughCenter = 1
        if goThroughCenter == 0: return 3  # Comment to debug

    # Play word (age 0) and age other tiles
    for tuple in squares.keys():
        if squares[tuple].age > -1:
            squares[tuple].age = squares[tuple].age + 1
    for tile in tiles:
        squares[(tile.x, tile.y)].letter = tile.letter
        squares[(tile.x, tile.y)].age = 0

    # Score word
    wordsAndScore = findAndScore(squares)
    wordList = wordsAndScore[0]
    addedScore = wordsAndScore[1]

    player.score = player.score + addedScore

    # Display score
    displayScore(players, numPlayers, stdscr)

    # Create a turn object and check validity
    if verifyWords(wordList) == 1: validity = 1
    elif verifyWords(wordList) == 0: validity = 0
    else: raise NameError('verifyWords returns neither 1 nor 0')
    newturn = Turn(player, lettersUsed, squaresUsed, addedScore, validity)

    # Remove turn if word verification is on and the word is wrong
    if validity == 0 and options.challenge == False:
        removeTurn(newturn, players, numPlayers, squares, stdscr)
        return 4
    else:
        return newturn

# A player takes a turn.  Return a turn object and the "turnCounter"
# (something indicating who is going next).
def oneTurn(previousTurn, player, squares, stdscr, bag, players, numPlayers):
    # Announce whose turn it is
    stdscr.addnstr(17, 3, player.name + "'s turn", 70)

    # Give player a rack
    while len(player.rack) < 7 and len(bag) > 0:
        tile = random.choice(bag)
        player.rack.append(tile)
        bag.remove(tile)

    tiles = [] # Tiles the player will place on the board this turn

    # Display rack
    redrawRack(player.rack, stdscr)

    # Graphics stuff
    while 1:
        c = stdscr.getch()
        coords = stdscr.getyx()
        x = coords[1]; y = coords[0]

        # Arrow keys move the cursor (or hjkl)
        if c == curses.KEY_LEFT or c == 104:
            if x == 0: pass
            else: stdscr.move(y, x-1)
        elif c == curses.KEY_RIGHT or c == 108:
            if x == 79: pass
            else: stdscr.move(y, x+1)
        elif c == curses.KEY_UP or c == 107:
            if y == 0: pass
            else: stdscr.move(y-1, x)
        elif c == curses.KEY_DOWN or c == 106:
            if y == 23: pass
            else: stdscr.move(y+1, x)
        # yubn moves diagonally (nethack-style)
        elif c == 121:
            if x == 0 or y == 0: pass
            else: stdscr.move(y-1, x-1)
        elif c == 117:
            if x == 79 or y == 0: pass
            else: stdscr.move(y-1, x+1)
        elif c == 98:
            if x == 0 or y == 23: pass
            else: stdscr.move(y+1, x-1)
        elif c == 110:
            if x == 79 or y == 23: pass
            else: stdscr.move(y+1, x+1)

        elif c == 8 or c == 127: # Backspace - remove tile from board
            # Ensure that cursor is over a tile, and if so, find the
            # tile
            ourTile = 0
            for tile in tiles:
                if tile.x == x and tile.y == y: ourTile = tile

            if ourTile != 0:
                # Repaint square on board
                if (x, y) in libsquares.bonuses.keys():
                    color = libsquares.colors[squares[(x, y)].multiplier]
                    stdscr.addch(y, x, " ", curses.color_pair(color))
                else: stdscr.addch(y, x, ".", curses.color_pair(1))
                # Remove tile from tile list
                tiles.remove(ourTile)
                # Add tile to rack
                player.rack.append(ourTile.letter)
                redrawRack(player.rack, stdscr)
                stdscr.move(y, x)

        elif c == 32 or c == 115: # Space or "s" - shuffle tiles
            shuffleRack(player.rack, stdscr)
            stdscr.move(y, x)
        # Once a player presses Enter, that's like inputting a command.
        # Deal with commands accordingly.
        elif c == 10: # New line - submit data
            # Submit word
            if x < 15 and y < 15:
                a = playTiles(player, tiles, players, numPlayers, squares, stdscr)
                if a == 3: # User has made an error
                    turnObject = previousTurn
                    turnCounter = player
                    # Remove stuff from the board
                    for tile in tiles:
                        if (tile.x, tile.y) in libsquares.bonuses.keys():
                            color = libsquares.colors[squares[(tile.x, tile.y)].multiplier]
                            stdscr.addch(tile.y, tile.x, " ", \
                            curses.color_pair(color))
                        else: stdscr.addch(tile.y, tile.x, ".", \
                        curses.color_pair(1))
                        player.rack.append(tile.letter)
                    tiles = []
                    redrawRack(player.rack, stdscr)
                elif a == 4: # User has made an invalid word (we need
                # a separate case because if this has happened the rack
                # has already been removed)
                    turnObject = previousTurn
                    turnCounter = player
                    tiles = []
                else:
                    turnObject = a
                    if player.index < numPlayers - 1:
                        turnCounter = players[player.index + 1]
                    elif player.index == numPlayers - 1:
                        turnCounter = players[0]
                    else:
                        raise NameError('Unknown player index')
                break

            elif y == 21: # On the shuffle/recall/pass line
                # Shuffle rack
                if x >= 2 and x <= 8:
                    shuffleRack(player.rack, stdscr)
                    stdscr.move(21, 2)
                # Recall tiles
                elif x >= 11 and x <= 16:
                    for tile in tiles:
                        if (tile.x, tile.y) in libsquares.bonuses.keys():
                            color = libsquares.colors[squares[(tile.x, \
                            tile.y)].multiplier]
                            stdscr.addch(tile.y, tile.x, " ", \
                            curses.color_pair(color))
                        else: stdscr.addch(tile.y, tile.x, ".", \
                        curses.color_pair(1))
                        player.rack.append(tile.letter)
                    tiles = []
                    redrawRack(player.rack, stdscr)
                    stdscr.move(21, 11)
                # Exchange tiles
                elif x >= 19 and x <= 31:
                    if len(bag) >= 7:
                        # Skip your turn
                        if player.index == numPlayers - 1:
                            turnCounter = players[0]
                        else: turnCounter = players[player.index + 1]
                        turnObject = Turn(player, [], [], 0, 2)
                        stdscr.addstr(21, 34, "Exchange tiles:")
                        stdscr.addstr(22, 34, "(press Enter when done or Esc to cancel)")
                        exchange = []
                        while 1:
                            c = stdscr.getch()
                            if c == 10: # New line - submit exchanged tiles
                                # Now add tiles from the bag
                                while len(player.rack) < 7:
                                    tile = random.choice(bag)
                                    player.rack.append(tile)
                                    bag.remove(tile)
                                for tile in exchange:
                                    bag.append(tile)
                                # Now erase the exchange messages from
                                # the screen
                                for i in range(45):
                                    stdscr.addch(21, 34 + i, " ")
                                    stdscr.addch(22, 34 + i, " ")
                                break

                            elif c == 8 or c == 127: # Backspace - delete a tile
                                if len(exchange) > 0:
                                    stdscr.delch(21, 50 + len(exchange) - 1)
                                    tile = exchange.pop()
                                    player.rack.append(tile)
                            elif c == 27: # Esc (Escape) - don't exchange
                                turnCounter = player
                                turnObject = previousTurn
                                for tile in exchange:
                                    player.rack.append(tile)
                                exchange = []
                                for i in range(45):
                                    stdscr.addch(21, 34 + i, " ")
                                    stdscr.addch(22, 34 + i, " ")
                                break
                            else:
                                if player.rack.count(chr(c)) > 0:
                                    stdscr.addch(21, 50 + len(exchange), chr(c))
                                    exchange.append(chr(c))
                                    player.rack.remove(chr(c))
                            redrawRack(player.rack, stdscr)
                        break
                    else:
                        stdscr.addstr(21, 34, "Less than 7 tiles left in bag")
                    stdscr.move(21, 19)

            # Undo the last turn (so that it is once again the
            # previous player's turn)
            elif y == 5 and x >= 20 and x <= 23:
                removeTurn(previousTurn, players, numPlayers, squares, stdscr)
                turnCounter = previousTurn.player
                turnObject = Turn(previousTurn.player, [], [], 0, 2)
                stdscr.move(5, 20)
                break

            # Challenge the last turn
            elif y == 7 and x >= 20 and x <= 28:
                if previousTurn.validity == 0:
                # Player uses his turn to invalidate the other's move,
                # then takes another turn
                    removeTurn(previousTurn, players, numPlayers, squares, stdscr)
                    turnCounter = player
                    turnObject = Turn(player, [], [], 0, 2)
                elif previousTurn.validity == 1:
                    # Player doesn't get a turn if the challenged word
                    # is valid
                    if player.index == numPlayers - 1:
                        turnCounter = players[0]
                    else: turnCounter = players[player.index + 1]
                    turnObject = Turn(player, [], [], 0, 2)
                elif previousTurn.validity == 2:
                # Then just go on as usual, nobody loses his turn.
                    turnCounter = player
                    turnObject = Turn(player, [], [], 0, 2)
                else: raise NameError('Validity not 0, 1, or 2')
                stdscr.move(7, 20)
                break

            # Quit the game
            elif y == 9 and x >= 20 and x <= 23:
                turnObject = previousTurn
                turnCounter = 0
                stdscr.move(9, 20)
                break
        elif c == 113 and (x >= 15 or y >= 15): # the letter "q"
            turnObject = previousTurn
            turnCounter = 0
            stdscr.move(9, 20)
            break

        else: # If on the board, assume he is adding tiles
            if x < 15 and y < 15:
                # Make sure tile is in rack and you are adding to a
                # blank space
\$\endgroup\$
11
\$\begingroup\$

I have little experience with python, so I can't offer critique on the finer points of your code, just the big picture. That said, several things stick out to me.

First, you have some large chunks which are duplicated two, three, or even four times (see lines 198-215). Scour through the code and extract these into their own functions.

Second, several of your functions are massively too long: findAndScore(161 lines), playTiles(97 lines), oneTurn(228 lines). findAndScore, once you get rid of the previously mentioned duplication, could easily be broken into two functions: the finding and the scoring. oneTurn is too long simply because it contains all of the input handling. The section within the block starting at line 435 absolutely should be its own function, if not more since it clocks in at 144 lines.

Finally, and this is more observation than critique, your classes do nothing but operate as organized data structures. This isn't necessarily a wrong approach, but it would look good if you were to refactor your code to be a bit more object oriented, especially since this is intended as a code sample.

\$\endgroup\$
11
\$\begingroup\$
#!/usr/bin/python

import curses
import letters # Letter scores and distributions
import libsquares # Locations of bonus squares
import random
import re
from optparse import OptionParser

# Command-line options
parser = OptionParser()
parser.add_option("-c", "--challenge", action="store_true", dest="challenge",
                  default=False, help="Enable challenges (disabled by default)")
parser.add_option("-d", "--dictionary", dest="dict", default="twl",
                  help="Choose dictionary (pick \"twl\" or \"sowpods\")")
(options, args) = parser.parse_args()

I'd rather see this kind of thing done in a main function rather then at the global level.

# Choose your dictionary
if options.dict == "sowpods":
   dictionary = open('sowpods.txt', 'r') # SOWPODS dictionary - International
else:
   dictionary = open('TWL06.txt', 'r')     #TWL dictionary - American

I'm a little bothered by the fact that you simply adopt the other dictionary in an else clause rather then showing an error for an invalid dictionary.

# --------------------------------------------
# Defines a "player" class
class Player:
    def __init__(player, name):
        player.name = name
        player.score = 0
        player.rack = []
        player.index = -1

By convention, the first parameter to methods should be named self. This will work, but it may confused other python coders.

# Defines a class for a square on the board
class Square:
    def __init__(square, x, y, multiplier):
        square.x = x
        square.y = y
        # Note that the multiplier gets set to 1 when a tile is placed
        # on the board
        square.multiplier = multiplier
        square.letter = ""
        square.age = -1

# Defines a class for a tile that hasn't yet been placed on the board
class Tile:
    def __init__(tile, x, y, letter):
        tile.x = x
        tile.y = y
        tile.letter = letter

# Defines a class for a turn.  A turn object gives you the player, the
# tiles used, the squares used on the board, the score of the move,
# and its validity.
class Turn:
    def __init__(turn, player, lettersUsed, squaresUsed, addedScore, validity):

The python style guide recommends using lowercase_wdith_underscorse for parameter names

        turn.player = player # The object "player."
        turn.lettersUsed = lettersUsed
        turn.squaresUsed = squaresUsed
        turn.addedScore = addedScore
        # Validity of a turn can be 0, 1, or 2.  0 means invalid.  1
        # means valid.  2 means if you challenge it, you won't lose
        # your turn, but the word won't be taken off the table either.

Rather then using magic numbers, use some global constants

        turn.validity = validity

# -------------------------------------------
# Redraws the rack, which should happen every time a letter is placed.
def redrawRack(rack, stdscr):
    for i in range(7):
        stdscr.delch(19, 15 + i)
    for i in range(len(rack)):

Use for i, letter in enumerate(rack):

        stdscr.addch(19, 15 + i, rack[i])

# Visually shuffle the rack.
def shuffleRack(rack, stdscr):
    random.shuffle(rack)
    redrawRack(rack, stdscr)

# Display score
def displayScore(players, numPlayers, stdscr):
    for i in range(70):
        stdscr.delch(2, 18)

Deleting the same character repeatedly seems like a bad idea

    for i in range(numPlayers):
        stdscr.addstr(2, 18 + 9*i, str(players[i].score))

# Returns "1" if all words are proper Scrabble words, returns 0 if
# they are not.

Don't use 1 and 0, use True and False

def verifyWords(words):
    # First, prune word list so that no word appears more than once.
    # This is so the "index" function works correctly.
    prunedList = []
    for word in words:
        if word in prunedList: pass
        else: prunedList.append(word)

Instead of this use pruned = set(words) the set will take remove all the duplicates.

    # Each number in the list "check" is 0 or 1, and if it's 0, the
    # corresponding word is invalid, and if it's 1, the corresponding
    # word is valid.  Each word starts "invalid" and becomes valid
    # once it is found in the dictionary.
    check = [0] * len(prunedList)

    dictionary.seek(0) # Go to beginning of file

    for line in dictionary:
        for word in prunedList:
            expression = "^" + word + "\n" + "$"
            object = re.compile(expression)
            if object.match(line) != None:

That's rather a waste of a regular expression. All you really need to check is 'word == line[:-1]`.

                check[prunedList.index(word)] = 1

If you had used for index, word in enumerate(prunedList) above, you'd already have the index for this word.

Actually, the better approach would be the read the dictionary into a large set. Then you can easily check whether a word is in that dictionary

    if check == [1] * len(prunedList): return 1
    else: return 0

Use return all(check)

# Given a tile and the direction of a word that it's a part of, find
# the coordinates of the first letter of the word.  Also, the last
# letter of the word.  Note that seek only finds coordinates in one
# direction.
def seek(x0, y0, direction, squares):
    if direction == "horiz":

I'm not a fan of string parameters like this.

        xLeft = x0
        xRight = x0
        while xLeft > 0 and squares[(xLeft-1, y0)].letter != "":
            xLeft = xLeft - 1
        while xRight < 14 and squares[(xRight+1, y0)].letter != "":
            xRight = xRight + 1
        return (xLeft, xRight)

    elif direction == "vert":
        yUp = y0
        yDown = y0
        while yDown < 14 and squares[(x0, yDown+1)].letter != "":
            yDown = yDown + 1
        while yUp > 0 and squares[(x0, yUp-1)].letter != "":
            yUp = yUp - 1
        return (yUp, yDown)

I notice that the two halves of this function are very similiar. I'd look for a way to simplify them and combine them.

    else: raise NameError('Unspecified direction')

That's really not what the python NameError usually means, so I wouldn't reuse that class.

More general comments:

  1. You program is complex enough it should really split across multiple files
  2. You would do better to have more functionality in classes
  3. It would be a good idea to separate functions which update the UI from those that do logic.
\$\endgroup\$
5
\$\begingroup\$

This started out as a comment but it quickly became too long:

Here are a few tips, in no particular order

  • For you reading pleasure, The python style guide.

  • You use comments a lot. As a general rule, you should write code that is clear enough that you only have to use comments sparingly. Use descriptive variable names and write clear code. (For example, I shouldn't have to read through your code to make an educated guess as to the purpose of stdscr, or c). Additionally, you should include docstrings immediately after your function/class/method definitions.

  • You repeat yourself a few times in the above code. If you find yourself writing the same/similar code over and over again, put it in a loop, or put it in a separate function.

  • I'm not sure if your using Python 2.x or Python 3. If you're using 2.x, it's recommended that you use new-style classes that inherit from object: class Player(object): for compatability reasons. (If you're using Python 3, classes implicitly inherit from object).

  • oneTurn() is ridiculously long. You should break it up into separate functions.

  • As Danny Kirchmeier said, you're classes are just containers. I would consider one of two things. 1) Used namedtuples to eliminate the overhead of using classes for these. 2) Restructure your code so your classes do have some functionality.

\$\endgroup\$

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.