# A game of NIM (Basic AI strategy)

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:

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:

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

## 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


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


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.

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