9
\$\begingroup\$

I tried creating the game of NIM in python. I researched a couple of videos on how to always win at NIM. Eventually, I found a method and i was able to implement it in my code. This is the result...

#---------------------
#      NIM GAME
#  Author : Akil Riaz
#---------------------

import random
import time

def welcome_message():
    print("""
----------------------------------------------------------------------------
                        WELCOME TO THE GAME OF NIM!
----------------------------------------------------------------------------
Rules: 1 - The user plays against the computer.
       2 - Only 1-3 sticks can be taken at a time.
       3 - The person who is left with the last stick is the loser.
       4 - Have FUN!
----------------------------------------------------------------------------
""")

def display_board(board):
    print("\n----------------------------------------------------------------------------")
    print("      ",*board, sep="   ")
    print("----------------------------------------------------------------------------\n")

def who_starts(question):
    choice = None
    while choice not in ("F", "S","f","s"):
        choice = input(question)
        if choice.upper() == "F":
            f_player = "USER"
            s_player = "USER"
        elif choice.upper() == "S":
            f_player = "CMP"
            s_player  = "CMP"
        else:
            print("\nInvalid choice. Please re-enter your choice.")

    return f_player, s_player

def users_pick(question,minStick,maxStick,userIn,board):
    print("\n--USER'S TURN--")
    while userIn not in range(minStick, maxStick) or userIn >= len(board):
        try:
            userIn = int(input(question))
            if userIn not in range(minStick, maxStick) or userIn >= len(board):
                print("\nYou can cannot choose that many sticks!")
                userIn = int(input(question))

        except Exception as e:
            print("\nAn error has occured.\nError: " + str(e) + "\nPlease re-enter your choice.")

    f_player = "CMP"

    return userIn, f_player

def update_board(board,move):
    valid_moves = (1,2)
    if len(board) > move:
        for i in  range(move):
            board.remove("/")
        return board
    elif len(board) <= move:
        if move in valid_moves:
            for i in range(move):
                board.remove("/")
            return board
        else:
            print("\n" + str(move) + " sticks cannot be taken.\nThe move you made was inavalid. Please  re-enter.")
            return board

def computers_move(board,userIn,s_player,winning_position,earlier_move):
    best_move = 1
    print("\n--COMPUTER'S TURN--")

    if s_player == "CMP" and winning_position == False:
        if len(board) % 4 ==1:
            best_move = 0
            while best_move not in range(1,len(board)+1):
                best_move = random.randint(1,3)
        else:
            if userIn + earlier_move < 4:
                best_move = 4 - (userIn + earlier_move)
                winning_position = True
            elif userIn + earlier_move > 4:
                best_move = 8 - (userIn + earlier_move)
                winning_position = True

    elif s_player == "USER" or winning_position == True:
        best_move = 4 - userIn

    earlier_move = best_move

    print("\nThe computer chooses to remove...")
    time.sleep(1)
    print(": " + str(best_move) + " stick(s).")
    for sticks in range(best_move):
        board.remove("/")

    f_player = "USER"
    return board, f_player, winning_position, earlier_move

def game_over(board,f_player):
    if len(board) == 1 and f_player == "USER":
        winner = "COMPUTER"
        loser = "USER"
    elif len(board) == 1 and f_player == "CMP":
        winner = "USER"
        loser = "COMPUTER"
    else:
        winner = None

    return winner, loser

def keepPlaying():
    while True:
        another_go = input("\nDo you want to play again?[Y/N]: ")
        if another_go in ("y","Y"):
            return True
        elif another_go in ("n","N"):
            return False
        else:
            print("\nInavlid choice. Please re-enter.")

def main():
    anotherGo = True
    welcome_message()
    while anotherGo == True:
        earlier_move = 0
        winning_position = False
        nimBoard = ["/","/","/","/","/","/","/","/","/","/","/","/","/","/","/","/","/"]
        winner = None
        userIn = 0
        print("----------------------------------------------------------------------------")
        display_board(nimBoard)
        print("\nWe begin with " + str(len(nimBoard)) + " sticks.")
        f_player, s_player = who_starts("\nDo you want to start first or second? [F for fisrt/S for second]: ")
        while len(nimBoard) != 1:
            if f_player == "USER":
                time.sleep(0.5)
                userIn,f_player = users_pick("\nEnter your choice [1, 2 or 3]: ", 1, 4, 20,nimBoard)
                nimBoard = update_board(nimBoard,userIn)
                display_board(nimBoard)
                print("\nThere are " + str(len(nimBoard)) + " stick(s) remaining.")
            else:
                time.sleep(1)
                nimBoard, f_player,winning_position, earlier_move = computers_move(nimBoard,userIn,s_player,winning_position,earlier_move)
                time.sleep(1)
                display_board(nimBoard)
                print("\nThere are " + str(len(nimBoard)) + " stick(s) remaining.")
        time.sleep(1)
        print("\nFinally...\nOnly one stick remains...")
        game_winner, game_loser = game_over(nimBoard,f_player)
        time.sleep(1)
        print("\nThe " + game_loser + " is left with the last stick and so he is the loser...")
        print("\nThe winner of the game is...\n: " + str(game_winner))
        anotherGo = keepPlaying()
        time.sleep(1)
    print("\nThank you for playing!")
    print("\n--GAME OVER--")

if __name__ ==  "__main__":
    main()

I did not follow the traditional method of having different rows/piles of sticks as i found that too challenging(although i will try that later on). The computer wins most of the time. But it is beatable if the user knows the strategy I have used.

But I still feel like there is room to improve. Is there anyway I can improve on this?

I would appreciate if you guys could give any simple efficiency improvements or logical improvements of any kind.


Just made a simple improvement to the program where the computer uses its earlier move to determine an even better move to beat the user... Thanks for the vote-ups as well! :)

\$\endgroup\$
2
\$\begingroup\$

General coding style and other useful tips

Tip 1 When using conditions, you don't need to type x == True, which means :

while anotherGo == True:

can be :

while anotherGo:

Tip 2 Don't use time.sleep for nothing. It could create confusion as to why you're using it when, I think, you're using it just so the program doesn't go too fast, but... why would you want that? :)

Tip 3 When you use if/else, you can move the common code to the end or the beginning of the conditions, this removes code duplication :

if f_player == "USER":
    time.sleep(0.5)
    userIn,f_player = users_pick("\nEnter your choice [1, 2 or 3]: ", 1, 4, 20,nimBoard)
    nimBoard = update_board(nimBoard,userIn)
    display_board(nimBoard)
    print("\nThere are " + str(len(nimBoard)) + " stick(s) remaining.")
else:
    time.sleep(1)
    nimBoard, f_player,winning_position, earlier_move = computers_move(nimBoard,userIn,s_player,winning_position,earlier_move)
    time.sleep(1)
    display_board(nimBoard)
    print("\nThere are " + str(len(nimBoard)) + " stick(s) remaining.")

Becomes :

if f_player == "USER":
    userIn,f_player = users_pick("\nEnter your choice [1, 2 or 3]: ", 1, 4, 20,nimBoard)
    nimBoard = update_board(nimBoard,userIn)

else:
    nimBoard, f_player,winning_position, earlier_move = computers_move(nimBoard,userIn,s_player,winning_position,earlier_move)

display_board(nimBoard)
print("\nThere are " + str(len(nimBoard)) + " stick(s) remaining.")

Tip 4 If all your prints start with a line break, you could consider creating an helper function so you don't have to retype it every time :

def printn(s):
    print("\n" + s)

Tip 5 Create constants for those "magic numbers". When I read :

users_pick("\nEnter your choice [1, 2 or 3]: ", 1, 4, 20,nimBoard)

Tip 6 You don't pay per character written, use meaningful variable names

Instead of having a variable named userIn, you could consider naming it userInput. It's... 3 more characters, but it's also much clearer what it does

I need to go check the function definition to see what 1,4,20 mean. If you had constants named MIN_STICK = 1, MAX_STICKS=4, USERIN=20.

Python tips

Tip 1 If you create an array with the same value multiple times, you can do :

# Notice how nice it is not to have to count to see the number of sticks
["/"] * 17

instead of :

["/","/","/","/","/","/","/","/","/","/","/","/","/","/","/","/","/"]

But I also think you don't need this array at all. You could simply keep the number of sticks available as an int and decrease it when necessary. This array is used to print the board, so you shouldn't mix the display logic with the game logic, it'll make code easier to maintain and to read.

Tip 2 Use only one naming style

You mix snake_case and camelCase in your code, which makes the code harder to read. Since you're using Python, you should stick to snake_case.

Code "cleanness"

In the users_pick method, you pass a userIn that's higher than the maximal accepted value, only so that your while loop works (I suspect). Don't do this, as a matter of fact, you shouldn't pass userIn at all as a parameter, because you completely ignore the input.


In users_pick again. Doing this : userIn not in range(minStick, maxStick) might look "great", but it also means every time you call this you generate a range, only to check if the value is in there, why not use userIn < minStick or userIn > maxStick that's much faster and more... logical.


I'm not sure if it's a copy/paste error, but in the who_starts method you set both f_player and s_player to the same values.

All in all, I think you should try to separate the display logic and the game logic in your code. You've done it pretty well regarding user inputs, but for example, you don't need to keep an array of / to see where the game is at.

\$\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.