6
\$\begingroup\$

I am currently looking for a language to program a game for my FYP which is to program the game "The game of nim". I am currently looking at pygame for this program, I hope someone can give me suggestions on any other language. I did made a simple python program which use console to show the rock and stuff, but I need to make an UI for it.

import random
from tokenize import Name

def main():
    StoneList=[]
    StonePile=random.randint(2,5)
    StoneQuantity=random.randint(1,9)
    Name=input("Enter Your name:")
    print("Hello ",Name,"! Welcome to the Game of Nim")
    player=Name
    Board(StoneList,StonePile,StoneQuantity,player)

def Board(StoneList,StonePile,StoneQuantity,player):
    #Print out the board
    for i in range(0,StonePile):
        StoneQuantity=random.randint(1,8)
        print("Pile {}:---{} Stones on this pile-----{}".format(i+1,StoneQuantity,'O'*StoneQuantity))
        StoneList.append(StoneQuantity)
    print("** 'O' represent stone **")
    
    CalculateNim(StoneList,StonePile,player)


def Get_Input(StoneList,StonePile,player):
    while True:
            stones=input(player+",How many stones you want to remove? ")
            piles=input("Which pile you want to remove from? ")

            if(stones and piles)and(stones.isdigit()) and (piles.isdigit()):
                if(int(stones)>0) and (int(piles)>0) and (int(piles)<=len(StoneList)):
                    if(int(stones)<=StoneList[int(piles)-1]):
                        if(int(stones)!=0) and (int(piles)!=0):
                            break
            
            print("Invalid input, please try again")
    
    StoneList[int(piles)-1]-=int(stones)
    
    proceed(StoneList,StonePile,player)
    CalculateNim(StoneList,StonePile,player)

def proceed(StoneList,StonePile,player):
    print("Current Board:")
    for i in range(0,StonePile):
        print("Pile {}:---{} Stones on this pile-----{}".format(i+1,StoneList[i],"O"*StoneList[i]))
    print("** 'O' represent stone **")
    if(StoneList==[0]*len(StoneList)):
        print("Game Over, Bot Won. Thanks for playing!")
        exit()

def CalculateNim(StoneList,StonePile,player):
    nim=0
    for i in StoneList:
        nim=nim^i
    if(nim==0):
        #If Nim-Sum=0, Player start first
        Get_Input(StoneList,StonePile,player)
    else:
        #If Nim-Sum is not equal to 0, Bot start first
        BotTurn(StoneList,StonePile,player)


def BotTurn(StoneList,StonePile,player):
    print("BOT's TURN")
    nim=0
    for i in StoneList:
        nim=nim^i
    
    counter=0
    for i in StoneList:
        if((nim^i)<i):
            print("Bot take {} stone(s) from Pile {}".format(StoneList[counter]-(nim^i),counter+1))
            StoneList[counter]=(nim^i)

            break
        counter=counter+1  
    input("Press Enter to continue...")
    proceed(StoneList,StonePile,player)
    CalculateNim(StoneList,StonePile,player)


        
        
    
main()
\$\endgroup\$
1
  • \$\begingroup\$ If you're looking for a new language to learn, maybe you should write it in Nim \$\endgroup\$
    – IMSoP
    Mar 30 at 12:54

3 Answers 3

3
\$\begingroup\$
    Name = input(...)

Pep-8 asks that you use the identifier name, downcased. Similarly for stone_list, stone_pile, stone_quantity, def get_list() and the like.

You are the programmer, you make the rules -- be decisive. Are we talking about a "name" or a "player" here? Either identifier would do nicely. Pick one.

    Board(StoneList, StonePile, StoneQuantity, player)

def Board(StoneList, StonePile, StoneQuantity, player):

Wow, I did not see that one coming.

Having read the final line of main, I believed we were instantiating a Board object and then I was about to draw a noun / verb distinction, asking you not to get carried away with lots of actions within a constructor, preferring e.g. Board(...).play(). But then the next line tells me that, "surprise!", it's a function instead. Which brings us back to the topic of choosing appropriate names. Use a capital noun for a Class, and a lower verb for a function such as this one. Consider def display_board(), for example.

    stone_pile = random.randint(2, 5)

That is slightly verbose, and we make several such calls. Prefer from random import randint so you can shorten it. That way the Gentle Reader's train of thought will remain firmly fixed on the idea you're trying to express, without being distracted by "Bond, James Bond, Integer Bond" redundancies.

    CalculateNim(...)

OIC, this function doesn't do just a single thing. We could call it "display and play", but probably better to make it the caller's responsibility to invoke calculate_nim(...).

Ok, that's as much as I'm going to read through.


This code appears to achieve some of its design goals. I would not be willing to delegate or accept maintenance tasks for this codebase in its current form.

You have started your journey and you're keen to learn, that's good. I encourage you to read other people's code and reflect on the choices they made. There are diverse ways to get the machine to compute the same thing, but some ways make it easier for others to understand and maintain working source code. I encourage you to keep writing code and then go back to critique your own code. What did I write yesterday? Is it clear to me today? How can I improve it today? What might I have focused on yesterday so there's no need for refactoring today? I wish you luck.

\$\endgroup\$
3
\$\begingroup\$

As mentioned in the other answers PEP-8 is the standard style recommendation for Python. I will try to avoid repeating what they've said.

I would recommend that you look into getting a linter such as Flake8 or Pylint which check your code against PEP-8 and issue useful warnings.

Unused import

You have the line:

from tokenize import Name

And never use it, was this debugging? From somewhere else? It can go, especially as you immediately shadow it (make it inaccessible by overwriting its name) within main

Name=input("Enter Your name:")

If I subsequently tried to run (if tokenize.Name was callable as a function/constructor)

bob = Name("Hello")

It would fail because Name is now the string I entered.

Avoid shadowing things as it very easily leads to difficult to debug problems.

Consistency

Again as mentioned by others, consistency of naming, casing and others is useful and a linter will highlight these, but you should also be consistent with other parts of your style. You have both:

    print("Hello ",Name,"! Welcome to the Game of Nim")

and

        print("Pile {}:---{} Stones on this pile-----{}".format(i+1,StoneQuantity,'O'*StoneQuantity))

and

stones = input(player+", How many stones you want to remove? ")

Be consistent within your code and it becomes easier to follow, debug and update.

The more modern, pythonic way to do these would be f-strings:

print(f"Pile {i+1}:---{StoneQuantity} Stones on this pile-----{'O'*StoneQuantity}")

DRY (Don't repeat yourself)

You have parts of your code which do (moreorless) the same thing:

def Board(StoneList,StonePile,StoneQuantity,player):
    #Print out the board
    for i in range(0,StonePile):
        StoneQuantity=random.randint(1,8)  # N.B. the StoneQuantity passed in 
                                           # Is completely useless and overwritten
        print("Pile {}:---{} Stones on this pile-----{}".format(i+1,StoneQuantity,'O'*StoneQuantity))
        StoneList.append(StoneQuantity)
    print("** 'O' represent stone **")

...

def proceed(StoneList,StonePile,player):
    print("Current Board:")
    for i in range(0,StonePile):
        print("Pile {}:---{} Stones on this pile-----{}".format(i+1,StoneList[i],"O"*StoneList[i]))
    print("** 'O' represent stone **")
    ...

Turn this into a function and use it everywhere:

def print_board(stone_list: List[int]):
    print("Current Board:")
    for ind, cnt in enumerate(stone_list, 1):  # Python can iterate directly over the list
        print(f"Pile {ind}:---{cnt} Stones on this pile-----{'O'*cnt}")
    print("** 'O' represent stone **")

which looks much nicer, and if we want to reformat how we print things, we only have to change it once.

Returns

Use returns and in your functions to handle passing things around, rather than diving deeper and deeper into functions. This means your main would look something more like:


def main():
    n_stone_piles = random.randint(2, 5)
    player = input("Enter Your name:")
    print(f"Hello {player}! Welcome to the Game of Nim")

    stone_list = build_board(n_stone_piles)

    while sum(stone_list) > 0: # While stones exist
        print_board(stone_list)
        stone_list = get_input(stone_list, n_stone_piles, player)
        print_board(stone_list)
        stone_list = calculate_nim(stone_list, n_stone_piles)

    print("Game Over, Bot Won. Thanks for playing!")

And, for example, your build_board might be:

def build_board(n_piles: int, max_stones: int = 8) -> List[int]:
    return [random.randint(1, max_stones) for _ in range(n_piles)]

Much simpler, no infinite recursion, much easier to debug.

\$\endgroup\$
3
\$\begingroup\$

PEP8 Style (reference)

  • standardize naming
    • lowercase_with_underscores (ex: CalculateNim to calulate_nim, you seem to use multiple styles, this comment is to pick one and stick with it, the general standard being lowercase_with_underscores)

I would recommend wrapping everything in a class so that:

  • you don't have to pass variables around
  • integrating with GUI later will be easier

I would rename a few variables to be more descriptive and follow PEP8

The __init__ will mimic current Board method

class Nim:

    def __init__(self, player):
        self.player = player
        self.num_piles = random.randint(2, 5)
        self.init_piles()

    def init_piles(self):
        self.stones_list = []
        for _ in range(0, self.num_piles):
            self.stones_list.append(random.randint(1, 8))

Board = Nim(input("Enter Your name: "))

Create a utility function to get number input, so that the error message can be custom, and get_input can be simplified

  • prompt: what prompted to user
  • max: max number allowed
  • allowed: mainly for piles prompt, list of piles with stones left
    ...
    def get_number_input(self, prompt, max, allowed=[]):
        num = ''
        while True:
            num = input(prompt)
            if not num.isdigit():
                print("Not a Number, please try again")
            elif int(num) <= 0 or int(num) > max:
                print("Out of Range, please try again")
            elif len(allowed) > 0 and int(num) not in allowed:
                print("No stones in pile, please try again")
            else:
                break
        return int(num)

    def get_input(self):
        pile_pick = self.get_number_input(
          "Which pile you want to remove from? ", len(self.stones_list),
          [i + 1 for i, s in enumerate(self.stones_list) if s > 0])
        num_stones = self.get_number_input(
          "{}, How many stones you want to remove from pile {}? ".format(
            self.player, pile_pick), self.stones_list[pile_pick - 1])
    
        self.stones_list[pile_pick - 1] -= num_stones

To run the game use something like:

class Board:
    ...
    def start(self):
        print("Hello {}! Welcome to the Game of Nim".format(self.player))
        while self.proceed():
            self.calculate_nim()
    def proceed(self):
        self.print()
        if (self.stones_list == [0] * len(self.stones_list)):
            print("Game Over, Bot Won. Thanks for playing!")
            return False
        return True
...
Board.start()

In the future, if/when integrating into GUI this can be modified/integrated in the GUI loop

while running:
    Board.calculate_nim()
    running = Board.proceed()

Whole code:

import random

class Nim:

    def __init__(self, player):
        self.player = player
        self.num_piles = random.randint(2, 5)
        self.init_piles()

    def init_piles(self):
        self.stones_list = []
        for _ in range(0, self.num_piles):
            self.stones_list.append(random.randint(1, 8))

    def print(self):
        print("Current Board:")
        for i in range(0, self.num_piles):
            print("Pile {}:---{} Stones on this pile-----{}".format(
                i + 1, self.stones_list[i], "O" * self.stones_list[i]))
        print("** 'O' represent stone **")

    def start(self):
        print("Hello {}! Welcome to the Game of Nim".format(self.player))
        while self.proceed():
            self.calculate_nim()

    def calculate_nim(self):
        nim = 0
        for i in self.stones_list:
            nim = nim ^ i
        if nim == 0:
            #If Nim-Sum=0, Player start first
            self.get_input()
        else:
            #If Nim-Sum is not equal to 0, Bot start first
            self.bot_turn()

    def get_number_input(self, prompt, max, allowed=[]):
        num = ''
        while True:
            num = input(prompt)
            if not num.isdigit():
                print("Not a Number, please try again")
            elif int(num) <= 0 or int(num) > max:
                print("Out of Range, please try again")
            elif len(allowed) > 0 and int(num) not in allowed:
                print("No stones in pile, please try again")
            else:
                break
        return int(num)

    def get_input(self):
        pile_pick = self.get_number_input(
            "Which pile you want to remove from? ", len(self.stones_list),
            [i + 1 for i, s in enumerate(self.stones_list) if s > 0])
        num_stones = self.get_number_input(
            "{}, How many stones you want to remove from pile {}? ".format(
            self.player, pile_pick), self.stones_list[pile_pick - 1])

        self.stones_list[pile_pick - 1] -= num_stones

    def bot_turn(self):
        print("BOT's TURN")
        nim = 0
        for i in self.stones_list:
            nim = nim ^ i
    
        counter = 0
        for i in self.stones_list:
            if ((nim ^ i) < i):
                print("Bot take {} stone(s) from Pile {}".format(
                  self.stones_list[counter] - (nim ^ i), counter + 1))
                self.stones_list[counter] = (nim ^ i)
    
                break
            counter = counter + 1
      
    def proceed(self):
        self.print()
        if (self.stones_list == [0] * len(self.stones_list)):
            print("Game Over, Bot Won. Thanks for playing!")
            return False
        return True

Board = Nim(input("Enter Your name: "))
Board.start()

\$\endgroup\$
4
  • 3
    \$\begingroup\$ PEP-8 suggests 4 space indentation. It's hard to tell what code belongs in which block with 2 spaces. You can use black to autoformat. \$\endgroup\$
    – ggorlen
    Mar 30 at 0:45
  • \$\begingroup\$ I would actually recommend having 2 classes -- one for the game logic, and one for the UI logic (CLI, here) -- and put the validation that inputs are correct in the game logic. This allows having unit-tests, as well as reusing the logic through another UI layer. \$\endgroup\$ Mar 30 at 9:51
  • \$\begingroup\$ @ggorlen I'm aware of spacing, transferring from my ide to here was not as smooth as I'd have liked. \$\endgroup\$
    – depperm
    Mar 30 at 10:27
  • \$\begingroup\$ No problem, but you can still edit the post. \$\endgroup\$
    – ggorlen
    Mar 30 at 15:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.