Simple ASCII Asteroid Game in Python

In this article, I will share the steps I followed in a homework of creating a simple asteroid game in python.

In the simple game requested, the player initially should be asked to enter the following variables:

x: the length of the asteroid cluster,
y: the width of the asteroid cluster,
g: the space between the spaceship and cluster*

There are some actions that the spaceship can take on each turn, these are:

left: ship moves one unit left
right: ship moves one unit right
fire: the ship destroys the asteroid in its line
exit: the game ends as it is

Other points to note:

Assumptions

  • x >= 0, y > 0, g >= 0
  • The variables x, y, and g are always integer.
  • Player's instructions should work as case-insensitive.
  • Imports should not be used.
  • If y is odd, the spaceship should start in the middle of the asteroids, and if it is even, it should start on the left side of the middle.
  • When an action other than the specified actions is entered, with nothing else, the turn should still end and the game should be reprinted exactly as it was next turn.

Victory and Defeat Condition

  • Each time the number of turns reaches a multiple of 5, the asteroid cluster should approach the spacecraft one line.
  • If the asteroids and the spaceship is on the same line, the player is defeated.
  • If all asteroids are destroyed, the player wins and the score is printed.

Initial Code

x = int(input())
y = int(input())
g = int(input())

# DO_NOT_EDIT_ANYTHING_ABOVE_THIS_LINE

# DO_NOT_EDIT_ANYTHING_BELOW_THIS_LINE

Code Board

Below is the final version of the game started with random inputs. Here you can experiment by making changes to the code.

Let's destroy some asteroids

My first job, of course, was to make preparations that were not directly related to the code. An example of this is to put two different comment blocks for the functions and the actual working part:

# -----------------------------------------------------------
# Functions Base
#
# (C) 412013 Spacehunters Base, Nova Mela, Sector XVII
# Released under Shadow Proclamation (SP)
# For inquiries teleport to @@4CD°D24'12.2″N 2z°10~'26.5″E@@
# -----------------------------------------------------------

# -----------------------------------------------------------
# Operations
#
# (C) 412013 Spacehunters Base, Nova Mela, Sector XVII
# Released under Shadow Proclamation (SP)
# For inquiries teleport to @@4CD°D24'12.2″N 2z°10~'26.5″E@@
# -----------------------------------------------------------

# Define initial game settings
if y % 2 == 0:
sp = int(y / 2) - 1
else:
sp = y // 2
game = {
"state": 2,  # Initial State
"ct": 0,  # Initial Turn
"score": 0,  # Initial Score
"sp": sp,  # Spaceship Position: spaces before the spaceship
"ap": 0,  # Asteroids Position: empty lines before the asteroits
"board": create_board(x, y, g, sp, 0),  # Initial asteroit lists
}

These blocks are followed by an if else block that defines the variable sp (number of spaces in front of the spaceship) over the variable y to determine the position of the spaceship relative to the asteroids. Then we create a game dictionary that can show the current state of the game at any time.

State is the state of the game, ct is the current turn, score is the number of asteroids destroyed, ap is the number of blank lines before the asteroid cluster, and board is the sum of all the content on the game board. The create_board function, is actually not used elsewhere and can be removed, it exists because it was designed as a function that would help to print the board, but I did not implement it that way later.

# -----------------------------------------------------------
# Functions Base
#
# (C) 412013 Spacehunters Base, Nova Mela, Sector XVII
# Released under Shadow Proclamation (SP)
# For inquiries teleport to @@4CD°D24'12.2″N 2z°10~'26.5″E@@
# -----------------------------------------------------------


def create_new_list_of(width, content, sp=None):
    new_list = []
    for i in range(width):
        if sp is not None and i == sp:
            new_list.append("@")
        else:
            new_list.append(content)
    return new_list


def create_board(height, width, distance, sp, ap):
    """
    Creates the board of the game.

    :param height: of asteroits
    :type height: int
    :param width: of asteroits
    :type width: int
    :param distance: current distance to asteroits >= 0
    :type distance: int
    :param sp: spaces before the spaceship
    :type sp: int
    :param ap: empty lines before the asteroits
    :type ap: int
    """
    board = []   # define an empty list
    for each_line in range(ap):
        board.append(create_new_list_of(width, " "))
    for each_line in range(height):
        board.append(create_new_list_of(width, "*"))
    for each_line in range(distance):
        board.append(create_new_list_of(width, " "))
    board.append(create_new_list_of(width, " ", sp))
    return board

The first function creates a list with the content specified in the second parameter in a for loop as wide as the first parameter given to it. In this loop, if a third parameter is defined, it includes the spaceship in the list with the space specified in sp.

The second function creates a list with the variables x, y, g, sp and ap, using the first function for each part, including the space before asteroids, asteroids, space after asteroids and spaceship, and adds it to the board list defined at the beginning.

# Define initial game settings
if y % 2 == 0:
    sp = int(y / 2) - 1
else:
    sp = y // 2
game = {
    "state": 2,  # Initial State
    "ct": 0,  # Initial Turn
    "score": 0,  # Initial Score
    "sp": sp,  # Spaceship Position: spaces before the spaceship
    "ap": 0,  # asteroits Position: empty lines before the asteroits
    "board": create_board(x, y, g, sp, 0),  # Initial asteroit lists
}

# Check if there are zero inputs
if player_won(game["board"]):
    game["state"] = 1    # Update game state as game won
    print_state_with_score(game["board"], game["state"], game["score"])
def player_won(board):
    """ Checks if there are any more asteroits left in the board. """
    won = True
    for each_list in board:
        if "*" in each_list:
            won = False
            break
    return won


def print_state_with_score(board, state, score):
    print_prompt_of(state)
    print_board_with_divider(board)
    print_prompt_of(-1, score)


def print_prompt_of(state=2, score=0):
    """
    Prints the prompt of specified state.

    -1 = exit or score
    0 = game over
    1 = success
    2 = next

    :param state: state indicator
    :type state: int
    :param score: current score
    :type score: int
    """
    messages = {
        -1: "YOUR SCORE: " + str(score),
        0: "GAME OVER",
        1: "YOU WON!",
        2: "Choose your action!"
    }
    print(messages[state])


def print_board_with_divider(board):
    for each_block in board:
        print(convert_to_string_from(each_block))
    print_divider()


def print_divider():
    print(72 * "-")


def convert_to_string_from(x_list):
    """ Converts list to string and returns. """
    out = ""
    for i in x_list:
        out += i
    return out

Then we check the win condition so that if x or y of the entered values are zero, the game will end immediately. We can do this by checking whether there is a * sign in any of the lists in the board in the function called player_won. If even one asteroid is found, the game will continue, so we can exit the for loop with break without checking all the lists.

With the print_prompt_of function, we can use all the messages that can be used throughout the game and access them later on easily. If the score parameter is sent, message includes current score.

# Define initial game settings
if y % 2 == 0:
    sp = int(y / 2) - 1
else:
    sp = y // 2
game = {
    "state": 2,  # Initial State
    "ct": 0,  # Initial Turn
    "score": 0,  # Initial Score
    "sp": sp,  # Spaceship Position: spaces before the spaceship
    "ap": 0,  # asteroits Position: empty lines before the asteroits
    "board": create_board(x, y, g, sp, 0),  # Initial asteroit lists
}

# Check if there are zero inputs
if player_won(game["board"]):
    game["state"] = 1
    print_state_with_score(game["board"], game["state"], game["score"])

# Loop till state changes
while game['state'] == 2:
    print_board_with_divider_and_prompt(game["board"], game["state"])
    action_is = input().lower()
def print_board_with_divider_and_prompt(board, state, score=0):
    print_board_with_divider(board)
    print_prompt_of(state, score)

Now we can build the repetitive part of the game. At this point, as long as the game state is not changed, the while loop will continue, and we will present the current state of the game board to the player with the print_board_with_divider_and_prompt function and take the new action in lowercase in action_is.

# Loop till state changes
while game['state'] == 2:
    print_board_with_divider_and_prompt(game["board"], game["state"])
    action_is = input().lower()

    if action_is in ("right", "left"):
        game["board"][-1], game["sp"] = move_spaceship_to(action_is, game["board"][-1], y)

    if action_is == 'fire':
        target = find_target(game["board"])
        game["board"], game["score"], x, g = fire_missiles_to(target, game["board"], game["score"], x, g)

    if action_is == 'exit':
        game['state'] = -1
        print_board_with_divider_and_prompt(game["board"], game['state'], game["score"])
        break

    # Check win condition
    if player_won(game["board"]):
        game["state"] = 1
        print_state_with_score(game["board"], game["state"], game["score"])
        break

    # Increase turn after each iteration
    game["ct"] += 1
def move_spaceship_to(direction, lst, inside_boundary):
    index = lst.index("@")
    if direction == 'left':
        if index == 0:
            return lst, 0
        if index == 1:
            lst[index - 1], lst[index] = "@", " "
            return lst, 0
        lst[index - 1], lst[index] = "@", " "
        return lst, index - 1
    if direction == 'right':
        if index == inside_boundary - 1:
            return lst, inside_boundary - 1
        lst[index + 1], lst[index] = "@", " "
        return lst, index + 1


def find_target(in_board):
    align = in_board[-1].index("@")
    for i in range(len(in_board) - 1, -1, -1):
        if in_board[i][align] == "*":
            return {"x": i, "y": align}
    return {"x": -1, "y": align}


def fire_missiles_to(coordinates, in_board, score, length, distance):
    for frame in range(len(in_board) - 2, coordinates["x"], -1):
        in_board[frame][coordinates["y"]] = "|"
        print_board_with_divider(in_board)
        in_board[frame][coordinates["y"]] = " "

    if coordinates["x"] != -1:
        in_board[coordinates["x"]][coordinates["y"]] = " "
        if "*" not in in_board[coordinates["x"]]:
            distance += 1
            length -= 1
        score += 1
    return in_board, score, length, distance

Right - Left Action

If the action is right or left, we should move the ship but not push it out of bounds. Since the spaceship is in the last list in the board, we can reach this list with the index of -1. Since we will change the sp space before the ship with this list, we must return both the new list and the current number of spaces from the move_spaceship_to function. As a parameter to this function, we pass y, the width of the asteroids, to determine the action, the latest list of the board, and the right border.

 if action_is in ("right", "left"):
        game["board"][-1], game["sp"] = move_spaceship_to(action_is, game["board"][-1], y)

If the action is left and the current index of the spaceship is 0, we give the same list and the number of spaces without making any changes.

If the action is left and the index of the spaceship is 1, we return the list and 0 as the number of spaces will be zero.

 if index == 1:
            lst[index - 1], lst[index] = "@", " "
            return lst, 0

Except for these two cases, in any left action, we both move the spaceship and reduce the number of spaces by 1.

lst[index - 1], lst[index] = "@", " "
        return lst, index - 1

If the action is right and the index is at the end of the star width, we return without any changes. Except for this case, we can move the spaceship and increase the space by 1.

    if direction == 'right':
        if index == inside_boundary - 1:    # Because index starts from 0, boundary - 1
            return lst, inside_boundary - 1
        lst[index + 1], lst[index] = "@", " "
        return lst, index + 1

Fire Action

 if action_is == 'fire':
        target = find_target(game["board"])
        game["board"], game["score"], x, g = fire_missiles_to(target, game["board"], game["score"], x, g)

Since the target should have a asteroid in the same vertical line as when it was fired, we can start by getting the index of the spaceship in the last list. We then check for an asteroid among the contents that line up from the last list to the first. If found, we return the number of lists as x and the alignment information as y in a target dictionary. If there is no asteroid in line, we manually set x to -1 to distinguish it.

def find_target(in_board):
    align = in_board[-1].index("@")
    for i in range(len(in_board) - 1, -1, -1):
        if in_board[i][align] == "*":
            return {"x": i, "y": align}
    return {"x": -1, "y": align}

Then we send these coordinates, the board, and the variable score, x, and g to the fire_missiles_to function. In this variable, we first print scenes where fire frames can be seen, starting from the spaceship's previous row (len(in_board) - 2), until we reach the target (coordinates["x"]).

If x is not -1, we remove the * sign from the coordinates where the asteroid is located after the fire animation. In addition to increasing the score after this process, if there is no other asteroid in the list where this asteroid is located, we decrease the height of the asteroid cluster and increase the space between them and the ship and return the current board, score, height and distance variables.

def fire_missiles_to(coordinates, in_board, score, length, distance):
    for frame in range(len(in_board) - 2, coordinates["x"], -1):
        in_board[frame][coordinates["y"]] = "|"
        print_board_with_divider(in_board)
        in_board[frame][coordinates["y"]] = " "

    if coordinates["x"] != -1:
        in_board[coordinates["x"]][coordinates["y"]] = " "
        if "*" not in in_board[coordinates["x"]]:
            distance += 1
            length -= 1
        score += 1
    return in_board, score, length, distance

Exit Action

If the exit action is selected, we can exit the while loop with break and end the game at that point by updating the state and printing the final state of the board and the score.

    if action_is == 'exit':
        game['state'] = -1
        print_board_with_divider_and_prompt(game["board"], game['state'], game["score"])
        break

Win Condition and Turn Check

We're reusing the same victory control we used earlier. If the game is won, we can end the game at this point with break, just like in the exit action.

Again, at this point, we increase the number of turns by 1, since we can now start to evaluate the next turn.

# Check win condition
    if player_won(game["board"]):
        game["state"] = 1
        print_state_with_score(game["board"], game["state"], game["score"])
        break

    # Increase turn after each iteration
    game["ct"] += 1

Lose Condition and Moving Asteroids

# Move asteroits
    if game["ct"] % 5 == 0:
        g -= 1
        # Check lose condition
        if g == -1:
            game["state"] = 0
            print_state_with_score(game["board"], game["state"], game["score"])
        else:
            if g == 1:
                game["board"].pop(len(game["board"]) - (g + 1))
            elif g == 0:
                game["board"].pop(len(game["board"]) - (g + 2))
            else:
                game["board"].pop(len(game["board"]) - g)
            game["ap"] += 1
            game["board"].insert(0, create_new_list_of(y, " "))  # increase space before asteroits

When the next turn is a multiple of five, we subtract g, the space between the asteroids and the spacecraft, by one. If g reaches -1, we end the game with a defeat prompt, as this will show that the asteroids and the spaceship are on the same line.

We remove the list in the second-to-last index of the board under the conditions where G is 0 and 1, and the index - g list in other conditions, and then we increase the space on the asteroids by creating an empty list.

Suggestions and Bugs

If there is anything you find missing or can be improved, you can let me know at [email protected]. Maybe you cannot. I am not sure. Because there is no such account.