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! :)

asked Apr 11, 2017 at 9:14
\$\endgroup\$

2 Answers 2

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.

answered Aug 6, 2019 at 0:22
\$\endgroup\$
2
\$\begingroup\$

This is now long past, but having coded my own version, some comments on your code.

Generally speaking:

  • You can do simpler. In particular, there is no need for the winning_position and earlier_move variables, simply knowing the value of len(board) % 4 gives you all that is needed.
  • Whether computer or user played first should not change the algorithm beyond the first move either.

In details:

  • Don't use a for loop with rejection for:

     best_move = 0
     while best_move not in range(1,len(board)+1):
     best_move = random.randint(1,3)
    

when you can simply use min:

 best_move = random.randint(1,min(3,len(board))
  • All the comments by @ieatbagels are relevant.

I offer a simpler version which is based on the Rosetta code version (but in Rosetta initial condition is such that computer always wins)

 #!python3
 import random
 misere=1 # 0 for "normal game", 1 for "misere game"
 if misere: print("Py Nim, misere game: don't take last token")
 else: print("Py Nim, normal game: be the one to take last token")
 
 def get_tokens(tokens):
 take=0
 while (take < 1 or take > min(3,tokens)):
 take = int(input("How many tokens would you like to take? "))
 
 tokens = tokens - take
 print(f'You take {take} tokens.')
 print(f'{tokens} tokens remaining.\n')
 if tokens==misere:
 print ('You win!')
 return 0
 elif tokens==0:
 print('Strange way of losing, this was a misere game...')
 return 0
 else:
 return comp_turn(tokens)
 
 def comp_turn(tokens):
 take = (tokens-misere) % 4
 if take==0:
 take=random.randint(1,3)
 tokens = tokens - take
 print (f'Computer takes {take} tokens.')
 print (f'{tokens} tokens remaining.\n')
 if tokens==misere:
 print ('Computer wins!')
 return 0
 return tokens
 
 tokens = random.randint(12,16)
 print (f'We start with {tokens} tokens.')
 if (input("Should the computer play first? (y/n)").upper()=="Y"):
 tokens=comp_turn(tokens)
 while (tokens > 0):
 tokens=get_tokens(tokens)

If you use the information of how many tokens there are for answering whether computer plays first, you can always win.

answered May 3, 2021 at 8:27
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.