A Simple Battleship Game I have just finished learning basic-intermediate Python subjects and wanted to test myself. This Battleship game is the third game i have written.

import random


def create_random_ship():
    return random.randint(0, 5), random.randint(0, 5)


def play_again():
    try_again = input("Wanna play again? &lt;Y&gt;es or &lt;N&gt;o? &gt;: ").lower()
    if try_again == "y":
        play_game()
    else:
        print("Goodbye!")
        return


print("Welcome to the Battleship game!"
      "\nYour main objective is to find and destroy all the hidden ships on map!\n")

print("""\nIntroductions:
\nYou have 10 ammo and there are 3 hidden ships on map.
In order to hit them, you have to enter specific numbers for that location. For example:
For the first row and first column, you have to write 1 and 1.
I wish you good fortune in wars to come!\n""")


def play_game():
    game_board = [["O", "O", "O", "O", "O"],
                  ["O", "O", "O", "O", "O"],
                  ["O", "O", "O", "O", "O"],
                  ["O", "O", "O", "O", "O"],
                  ["O", "O", "O", "O", "O"]]

    for i in game_board:
        print(*i)

    ship1 = create_random_ship()
    ship2 = create_random_ship()
    ship3 = create_random_ship()
    ships_left = 3
    ammo = 10

    while ammo:
        try:
            row = int(input("Enter a row number between 1-5 &gt;: "))
            column = int(input("Enter a column number between 1-5 &gt;: "))
        except ValueError:
            print("Only enter number!")
            continue

        if row not in range(1,6) or column not in range(1, 6):
            print("\nThe numbers must be between 1-5!")
            continue

        row = row - 1 # Reducing number to desired index.
        column = column - 1 # Reducing number to desired index.

        if game_board[row][column] == "-" or game_board[row][column] == "X":
            print("\nYou have already shoot that place!\n")
            continue
        elif (row, column) == ship1 or (row, column) == ship2 or (row, column) == ship3:
            print("\nBoom! You hit! A ship has exploded! You were granted a new ammo!\n")
            game_board[row][column] = "X"
            ships_left -= 1
            if ships_left == 0:
                print("My my, i didn't know you were a sharpshooter! Congratz, you won!")
                play_again()
        else:
            print("\nYou missed!\n")
            game_board[row][column] = "-"
            ammo -= 1

        for i in game_board:
            print(*i)

        print(f"Ammo left: {ammo} | Ships left: {ships_left}")

    play_again()


if __name__ == "__main__":
    play_game()


What do you think? How can I make it better? What are my mistakes?
Magic Numbers

Your biggest issue is too many magic numbers and hard-coded information.

If you added at the top of your script:

ROWS = 5
COLUMNS = 5


then you could have:

def create_random_ship():
    return random.randrange(ROWS), random.randrange(COLUMNS)


Game board can be created with:

game_board = [["O"] * COLUMNS for _ in range(ROWS)]


And you can ask for input from the user with:

row = int(input(f"Enter a row number between 1-{ROWS} &gt;: "))


Along the same vein, you could have:

print(f"""\nIntroductions:
\nYou have {INIT_AMMO} ammo and there are {INIT_ENEMY} hidden ships on map.
...""")


so your welcome instructions can keep up-to-date with any changes to your game initial conditions.

Instructions

You have a if __name__ == "__main__": guard to prevent the code from running if imported into another script, but your instruction are unconditionally printed.  You should move the instruction printing into an instructions() function, and call that only if appropriate.

Infinite Recursion

play_game() calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), which calls play_again(), which can call play_game(), ...

Traceback (most recent call last):
  [...omitted...]
RecursionError: maximum recursion depth exceeded


Don't do that.  Much simpler is to have play_again() return a True/False result, and loop based on the return value:

def play_many_games():

    instructions()

    play_game()

    while play_again():
        play_game()

    print("Goodbye!")

if __name__ == "__main__":
    play_many_games()


No more recursion.

In the comments, @Graipher suggests an alternate method to avoid the recursion:

while True:
    play_game()
    if not play_again():
        break


Yet another alternative introduces a loop condition variable, which is pre-set before the loop:

 playing = True
 while playing:
     play_game()
     playing = play_again()


All three approaches avoid the infinite recursion.

I Saw Three Ships...

ship1 = create_random_ship()
ship2 = create_random_ship()
ship3 = create_random_ship()

# ...

    elif (row, column) == ship1 or (row, column) == ship2 or (row, column) == ship3:


What are you going to do if you change this to four ships, or even 5 ships?

With computers, there are 3 important numbers: zero, one and many.  If you have more than one ship, you have "many" ships, and should put them in a container, such as a list():

ship_coordinates = []
for _ in range(ships_left):
    ship_coordinates.append(create_random_ship())


And when you want to see if a particular row/column matches any one of the ship coordinates in the container, you use the in function:

    elif (row, column) in ship_coordinates:


There are other container types you can use, such as a set() which has faster in performance.  However, it cannot add the same value more than once, and using it would result in slightly different behaviour to what you currently have.  You've mentioned that you have a bug where ships can have the same coordinates, and I am trying very hard not to accidentally fix that.  (Code Review is for reviewing working code only, not help debugging, so I am tiptoeing around the bug, so that you can fix it yourself.)