4
\$\begingroup\$

I have made my first game in Python.

My program can determine when there is a winner, however I am unsure what to do if the board is full and there is NO winner (a cats game).

import cTurtle
import random
import sys

def DrawTicTacToeBoard(t):
    #Vertical Lines of board
    t.pensize(3)
    t.up()
    t.goto(.5,-.5)
    t.down()
    t.goto(.5,2.5)
    t.up()
    t.goto(1.5,-.5)
    t.down()
    t.goto(1.5,2.5)
    #Horizontal Lines of board
    t.up()
    t.goto(-.5,.5)
    t.down()
    t.goto(2.5,.5)
    t.up()
    t.goto(-.5,1.5)
    t.down()
    t.goto(2.5,1.5)

def flipPlayer(currentPlayer):
    if currentPlayer==0:
        currentPlayer=1
    else:
        currentPlayer=0
    return currentPlayer

def drawWhiteBox(t,x,y):
    t.up()
    t.goto(x-.25,y+.25)
    t.down()
    t.color("white")
    t.begin_fill()
    for i in range(4):
        t.forward(.8)
        t.right(90)
    t.end_fill()
    t.color("black")

def drawMarker(t,currentPlayer,markerList,move,boardLoL):
    by=int(move[0:1])
    bx=int(move[2:])
    y=by+(.22)
    x=bx-(.15)
    drawWhiteBox(t,x,y)
    t.up()
    t.goto(x,y)
    boardLoL[by][bx]=markerList[currentPlayer]
    t.write(markerList[currentPlayer],move=False,align="left",font=("Arial",48,"normal"))

def getValidMove(validMoveList,markerList,currentPlayer,boardLoL):
    move = ""
    while move not in validMoveList:
        move = input("Enter location to place your marker " + markerList[currentPlayer] + ", or EXIT to end game => ")
        if move=="EXIT":
            sys.exit()
        if move in validMoveList:
            by=int(move[0])
            bx=int(move[2:])
            if boardLoL[by][bx]=="":
                return move
            else:
                print("Location already occupied")
                move=""

def gameWon(b):
    for row in range(3):
        if b[row][0]==b[row][1] and b[row][1]==b[row][2] and b[row][0]!="":
            print("Way to go",b[row][0])
            return b[row][0]
    for column in range(3):
        if b[0][column]==b[1][column] and b[1][column]==b[2][column] and b[0][column]!="":
            print("Way to go",b[column][0])
            return b[0][column]
    for diag in range(2):
        if b[0][0]==b[1][1] and b[1][1]==b[2][2] and b[0][0]!="":
            print("Way to go",b[0][0])
            return b[0][0]
        if b[2][0]==b[1][1] and b[1][1]==b[0][2] and b[2][0]!="":
            print("Way to go",b[2][0])
            return[2][0]



    return ""

def boardIsFull(boardLoL):
    return False




def main():
    boardLoL=[["","",""],["","",""],["","",""]]
    markerList=["O","X"]
    validMoveList=["0,0","0,1","0,2","1,0","1,1","1,2","2,0","2,1","2,2"]
    joe=cTurtle.Turtle()
    joe.setWorldCoordinates(-2,3,4,-1)
    joe.ht()
    currentPlayer=random.randint(0,1)
    DrawTicTacToeBoard(joe)
    move = ""
    while boardIsFull(boardLoL)==False and gameWon(boardLoL)=="" and move !="EXIT":
        move = getValidMove(validMoveList,markerList,currentPlayer,boardLoL)
        drawMarker(joe,currentPlayer,markerList,move,boardLoL)
        currentPlayer=flipPlayer(currentPlayer)
    gameWon(boardLoL)
    boardIsFull(boardLoL)

main()
\$\endgroup\$
1
\$\begingroup\$
import cTurtle
import random
import sys

Excellent! Many people use wildcard imports, (from <module> import *), but you didn't. You even alphabetized your imports. Keep up the good work.

PEP 8, the Python style guide, has this to say in the Function Names section:

Function names should be lowercase, with words separated by underscores as necessary to improve readability.

mixedCase is allowed only in contexts where that's already the prevailing style (e.g. threading.py), to retain backwards compatibility.

That means that DrawTicTacToeBoard would be draw_tic_tac_toe_board, for example.

Your spacing does not comply with PEP 8. Specifically:

Always surround these binary operators with a single space on either side: assignment (=), augmented assignment (+=,-= etc), comparisons (==,<,>,!=,<>,<=,>=,in,not in,is,is not), Booleans (and,or,not).

#Vertical Lines of board
t.pensize(3)
t.up()
t.goto(.5,-.5)
t.down()
t.goto(.5,2.5)
t.up()
t.goto(1.5,-.5)
t.down()
t.goto(1.5,2.5)

I realize that you want to save characters, but -.5 is a little hard to read. It almost looks like the - is one argument and 5 is another argument. The extra 0 between the two would greatly improve readability. Speaking of saving characters, you could use a for loop ... probably. I haven't come up with anything that I really like, though. You should also consider a different name for your argument. Perhaps turtle would be more descriptive.

def flipPlayer(currentPlayer):
    if currentPlayer==0:
        currentPlayer=1
    else:
        currentPlayer=0
    return currentPlayer

This function is included in two rules mentioned above, but I mention it here because there is a much easier way to do it:

def flip_player(current):
    return (current + 1) % 2

Read more about the modulus operator and when to use it.

for i in range(4):
    t.forward(.8)
    t.right(90)

Again, you should be using a more descriptive name, but I brought up the for loop because you use i as your variable name. There isn't anything wrong with using it, but in cases where the variable is not used, it is common practice in Python to use _ as the variable name. It just makes it a little more obvious that it is an unused variable.

move = input("Enter location to place your marker " + markerList[currentPlayer] + ", or EXIT to end game => ")

That line is 110 characters not including indentation. PEP 8 says:

Limit all lines to a maximum of 79 characters.

Perhaps it would be better to define a template and format it in the loop:

template = "Enter location to place your marker {}, or EXIT to end game => "
while move not in valid_move_list:
    move = input(template.format(marker_list[current_player]))
    ...

I would also change how you check your conditions in the while loop. Here is what I would do:

while True:
    move = input(template.format(marker_list[current_player]))
    if move == "EXIT":
        sys.exit()
    elif move in valid_move_list:
        y, x = map(int, move.split())
        if not boardLoL[y][x]:
            return move
        else:
            print("Location already occupied")
    else:
        print("Invalid move")

There is no need for while to be checking anything since you account for all cases inside. I also added an else: so that the user would know why he was being asked again for input.

def gameWon(b):
    for row in range(3):
        if b[row][0]==b[row][1] and b[row][1]==b[row][2] and b[row][0]!="":
            print("Way to go",b[row][0])
            return b[row][0]
    for column in range(3):
        if b[0][column]==b[1][column] and b[1][column]==b[2][column] and b[0][column]!="":  
            print("Way to go",b[column][0])
            return b[0][column]
    for diag in range(2):
        if b[0][0]==b[1][1] and b[1][1]==b[2][2] and b[0][0]!="":
            print("Way to go",b[0][0])
            return b[0][0] 
        if b[2][0]==b[1][1] and b[1][1]==b[0][2] and b[2][0]!="":
            print("Way to go",b[2][0])
            return[2][0]

You can simplify that to this:

def game_won(board):
    for row in board:
        if len(set(row)) == 1:
            return row[0]
    for column in zip(*board):
        if len(set(column)) == 1:
            return column[0]
    for diag in range(2):
        ...

I would do all of the printing outside of the function. In your main() function, it would be like this:

print(game_won(boardLoL))

instead of:

game_won(boardLol)

I'm not crazy about the diagonal searching, but I can't think of anything better at the moment. Maybe it's because I've been awake for ... yawn

return[2][0]

I don't think that's what you meant. Perhaps, return b[2][0]

def boardIsFull(boardLol):
    return False

Haven't added it yet, huh? Maybe this:

def board_is_full(board):
    return all(all(row) for row in board)

Yeah, nifty. You see, all(row) makes sure that each item in row is a Truthy value. Any non-empty string is Truthy, so if any of the cells in that row is empty, all(row) will return False. Since I am running all() on each of those all() calls, it returns False if any cell in the board is empty, but True otherwise.

while boardIsFull(boardLoL)==False and gameWon(boardLoL)=="" and move !="EXIT":

Any condition is checking a boolean. That is, if ...: is seeing if ... is True. Using if ... == True: is redundant because you could have just used if ...:. Consequently, if ... == False: is redundant because you could have used if not ...: You can simplify that line like this:

while not board_is_full(boardLoL) and not game_won(boardLoL) and move != "EXIT":

That line is one character too long for PEP 8. I would suggest changing boardLoL to board in all places. What is LoL doing there anyway? Are you laughing as you type?

I hope I haven't sounded too harsh, but it's been a late night.

\$\endgroup\$
1
\$\begingroup\$

Most of your program is quite good. It is well organized and easy to understand.

gameWon()

This is the buggiest function.

In one of the diagonal checks in gameWon(), you wrote:

return[2][0]

… which returns 2 (treating [2] as a single-element list).

The for diag in range(2) loop is pointless: it just executes the same checks twice, for no reason.

This function has a possible side-effect of printing "Way to go X". That's a bad design, since it means that you have to be careful to call it exactly once, if there is a winner. Unfortunately, you don't take that precaution, so you'll end up printing "Way to go X" twice when X wins.

Python supports double-ended comparisons, so you can write if b[0][0] == b[1][1] == b[2][2].

drawMarker()

This function does more than drawing: it also updates the board. You should either remove the board-updating code, or rename the function to executeMove(). I recommend the latter.

The move should not be represented as a string. If getValidMove(), which already parsed the string to validate the move, returned a tuple of integers (y, x), then you wouldn't have to repeat that work here.

DrawTicTacToeBoard()

This function is a bit tedious, and could benefit from a general routine to draw a line segment.

getValidMove()

The loop structure is awkward. See the suggested solution below.

flipPlayer()

I would just write currentPlayer = not currentPlayer, and eliminate the function altogether. (Note that this takes advantage of the fact that 0/False and 1/True are somewhat interchangeable. Some purists might object.)

Miscellaneous

The code doesn't follow PEP 8 conventions. Notably:

  • Function names and variable names should be lowercase_with_underscores.
  • There should be a space before and after = and ==. There should be a space after every comma.
  • The few blank lines near the end of gameWon() are awkward.

Avoid extreme Hungarian notation with names like boardLoL, markerList, and validMoveList. It would be fine to write board, MARKERS, and VALID_MOVES, respectively.

When a player wins, the game exits immediately, after showing the winning board for a split second. It would be much more satisfying to wait until the window is closed.

Suggested solution

import cTurtle
import random

def draw_board(t):
    LINES = [
        ((+0.5, -0.5), (+0.5, +2.5)),   # vertical
        ((+1.5, -0.5), (+1.5, +2.5)),   # vertical
        ((-0.5, +0.5), (+2.5, +0.5)),   # horizontal
        ((-0.5, +1.5), (+2.5, +1.5)),   # horizontal
    ]
    t.pensize(3)
    for (start, end) in LINES:
        t.up()
        t.goto(*start)
        t.down()
        t.goto(*end)

def execute_move(t, board, marker, move):
    def draw_white_box(t, move):
        y, x = move
        t.up()
        t.goto(x - 0.4, y + 0.4)
        t.down()
        t.color('white')
        t.begin_fill()
        for _ in range(4):
            t.forward(0.8)
            t.right(90)
        t.end_fill()
        t.color('black')
    y, x = move
    board[y][x] = marker
    draw_white_box(t, move)
    t.up()
    t.goto(x - 0.15, y + 0.22)
    t.write(marker, move=False, align="left", font=("Arial",48,"normal"))

def get_valid_move(board, marker):
    while True:
        try:
            move = input("Enter location to place your marker {0} or EXIT to end game => ".format(marker))
            if move == "EXIT":
                return None
            y, x = map(int, move.split(',', 1))
            if x >= 0 and y >= 0 and board[y][x] == '':
                return y, x
            else:
                print("Location already occupied")
        except (EOFError, KeyboardInterrupt):
            return None
        except (ValueError, IndexError):
            pass

def winner(b):
    for row in range(3):
        if '' != b[row][0] == b[row][1] == b[row][2]:
            return b[row][0]
    for column in range(3):
        if '' != b[0][column] == b[1][column] == b[2][column]:
            return b[0][column]
    if '' != b[0][0] == b[1][1] == b[2][2]:
        return b[1][1]
    if '' != b[2][0] == b[1][1] == b[0][2]:
        return b[1][1]
    return ""

def is_full(b):
    return all(b[y][x] != '' for x in [0, 1, 2] for y in [0, 1, 2])

def main():
    MARKERS = ['O', 'X']
    board = [["","",""], ["","",""], ["","",""]]
    current_player = random.randint(0,1)

    joe = cTurtle.Turtle()
    joe.setWorldCoordinates(-2, 3, 4, -1)
    joe.ht()
    draw_board(joe)

    while not winner(board):
        current_player = not current_player
        move = get_valid_move(board, MARKERS[current_player])
        if not move:
            break
        execute_move(joe, board, MARKERS[current_player], move)
        if is_full(board):
            break
    else:
        print("Way to go, {0}!".format(MARKERS[current_player]))
        input("Press the ENTER key to exit")

main()
\$\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.