Python İle Basit ASCII Asteroit Oyunu

Özgün – Dev

Bu yazıda programlamaya giriş dersimde verilen python ile ascii asteroit içerikli oyun yapma ödevimde izlediğim adımları paylaşacağım.

İstenen basit oyunda, başlangıçta oyuncudan alınan aşağıdaki değişkenleri kullanarak oyununun oluşturulması isteniyor.

x: asteroit topluluğunun uzunluğu,
y: asteroit topluluğunun genişliği,
g: uzay gemisinin asteroitlere olan uzaklığı*

Uzay gemisinin her turda alabileceği bazı aksiyonlar var, bunlar:

left: gemi bir birim sola hareket eder
right: gemi bir birim sağa hareket eder
fire: gemi kendi hizasında bulunan asteroiti yok eder
exit: oyun o anki hâliyle biter

Dikkat edilmesi gereken diğer noktalar*:

Varsayımlar

  • x >= 0, y > 0, g >= 0
  • x, y, ve g değişkenleri her zaman integer.
  • Oyuncunun verdiği talimatlar case-insensitive olarak çalışmalı.
  • Import kullanılmamalı.
  • Eğer y tek ise uzay gemisi asteroitlerin ortasında, çift ise ortanın sol tarafında başlamalı.
  • Belirtilen aksiyonlar dışında bir aksiyon girildiğinde başka hiçbir şey olmadan tur yine de ilerlemeli ve oyun aynen olduğu gibi tekrar yazdırılmalı.
    Zafer ve Yenilgi Koşulu
  • Her tur sayısı 5'in katına ulaştığında asteroit topluluğu uzay gemisine bir satır yaklaşmalı.
  • Asteroitler ve uzay gemisi aynı satıra gelirse oyuncu yenilir.
  • Bütün asteroitler yok edilirse oyuncu kazanır ve skoru yazdırılır.

*Oyunun bu yazıdaki versiyonu istenen kurallarla birebir olacak şekilde yapılmıştır ve belki gelecek yazılarda bulunabilecek yaratıcı geliştirmeler içermemektedir.

Başlangıç Kodu

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

# DO_NOT_EDIT_ANYTHING_ABOVE_THIS_LINE

# DO_NOT_EDIT_ANYTHING_BELOW_THIS_LINE

Lets destroy some asteroits

İlk işim, tabii ki diğer pek çok işimde olduğu gibi, direkt olarak kodla alakası olmayan hazırlıklar yapmak idi. Fonksiyonlar ve asıl çalışan kısım için iki farklı yorum bloğu kondurmak buna bir örnek:

# -----------------------------------------------------------
# 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,  # asteroits Position: empty lines before the asteroits
    "board": create_board(x, y, g, sp, 0),  # Initial asteroit lists
}

Bu blokları uzay gemisinin yıldızlara göre olan pozisyonunu belirlemek için y değişkeni üzerinden sp (uzay gemisi önündeki boşluk sayısı) değişkenini tanımlayan bir if else bloğu takip ediyor. Daha sonra ise oyunun güncel durumunu her an gösterebilecek bir game dictionary'si oluşturuyoruz.

State, oyunun içinde bulunduğu durumu, ct, güncel turu, score, yok edilen asteroit sayısını, ap, asteroit bulutu öncesindeki boşluk satır sayısını ve board, oyun tahtasında bulunan bütün içeriklerin toplamını ifade ediyor. Board key'inin karşısında bulunan create_board fonksiyonu aslında başka yerde kullanılmayan ve kaldırılabilecek bir fonksiyon fakat ilk denemelerde create_board işlevinden ziyade her an tahtayı yazdırılabilecek bir fonksiyon olarak kurgulandığı için yer almaya devam ediyor.

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

İlk fonksiyon kendisine verilen ilk parametredeki genişlik kadar bir for döngüsü içinde ikinci parametrede belirtilen içerik ile bir liste oluşturmakta. Bu döngü içinde eğer bir üçüncü parametre tanımlı ise spde belirtilen boşluk kadar sonrasında listeye uzay gemisini dahil ediyor.

İkinci fonksiyon ise x, y, g, sp ve ap değişkenleriyle, asteroitler öncesi boşluk, asteroitler, asteroitler sonrası boşluk ve uzay gemisi bölümleri olmak üzere her bir kısıma ilk fonksiyonu kullanarak bir liste oluşturuyor ve en başta tanımlanan board listesine ekliyor.

# 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

Daha sonra girilen değerlerden x veya y'nin sıfır olması halinde oyunun hemen bitmesi için kazanma koşulunu kontrol ediyoruz. Bunu da player_won adlı fonksiyon içerisinde board içerisinde bulunan listelerin herhangi birinde * işareti kalıp kalmadığını kontrol ederek yapabiliriz. Eğer bir adet dahi asteroit bulunduğunda oyun devam edeceği için break ile for döngüsünden bütün listeleri kontrol etmeden çıkabiliriz.

Promt için print_prompt_of fonksiyonu ile oyun boyunca kullanılabilecek mesajların hepsini bir dictionary içinde toplayarak daha sonra kolaylıkla erişebiliriz. İlk parametrede belirtilen state'in key olarak karşılık geldiği mesaj aynen yazdırılıyor. Eğer mesaj içinde skor gerekli olacaksa ikinci parametre olarak gönderilebilir.

Oyun tahtasının yazdırılabilmesi için tahtadaki listelerin string'e dönüştürmek bir yöntem olabilir. Bunu print_board_with_divider fonksiyonu ile convert_to_string_from fonksiyonlarını birleştirerek yapabiliriz. İkincisi parametre olarak verilen listenin her bir elemanını out string'ine ekleyerek dönüştürme yapıyor.

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

Artık oyunun tekrar eden kısmını yapabiliriz. Bu noktada game state'i while içinde değiştirilmediği sürece while döngüsü devam edecek ve her seferinde oyun tahtasının güncel durumunu print_board_with_divider_and_prompt fonksiyonu ile oyuncuya sunup action_is içinde küçük harflerle yeni aksiyonu alacağız.

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

Eğer aksiyon sağ veya sol ise gemiyi hareket ettirmeli fakat sınırlardan dışarı çıkarmamalıyız. Uzay gemisi board içindeki en son listede bulunduğu için board[-1] index'i ile bu listeye ulaşabiliriz. Bu listeyle birlikte gemi öncesindeki boşluk olan sp'yi de değiştireceğimizden move_spaceship_to fonksiyonundan hem yeni listeyi hem de güncel boşluk sayısını dönürmeliyiz. Bu fonksiyona parametre olarak, aksiyonu, tahtanın en son listesini ve sağ sınırı belirlemek için asteroitlerin genişliği olan y'yi gönderiyoruz.

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

Eğer aksiyon left ise ve uzay gemisinin güncel index'i 0 ise bir değişiklik yapmadan aynen listeyi ve boşluk sayısını veriyoruz.

Eğer aksiyon left ise ve uzay gemisinin index'i 1 ise gerekli kaydırmayı yaptıktan sonra boşluk sayısı sıfır olacağından liste ve 0 olarak dönüyoruz.

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

Bu iki durum haricinde herhangi bir left aksiyonunda hem kaydırmayı yapıyoruz hem de boşluk sayısını 1 azaltıyoruz.

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

Eğer aksiyon right ise ve index yıldız genişliğinin sonunda bulunuyorsa bir değişiklik yapmadan dönüyoruz. Bu durum dışında kaydırmayı yapıp boşluğu 1 arttırabiliriz.

    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 Aksiyonu

 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)

Hedefin ateş edildiği esnadaki hizada bir yıldız olması gerektiğinden son listenin içinde uzay gemisinin index'ini alarak başlayabiliriz. Daha sonra son listeden başlayarak ilk listeye kadar aynı hizada bulunan içeriklerin arasında bir asteroit olup olmadığını kontrol ediyoruz. Bulunması halinde kaçıncı listede bulunduğunu x, hiza bilgisini ise y olarak bir target dictionary'si içinde dönüyoruz. Eğer aynı hizada bir asteroit kalmamış ise ayırt etmek adına x'i manuel olarak -1 olarak atıyoruz.

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}

Daha sonra fire_missiles_to fonksiyonuna bu koordinatları, tahtayı ve değişme ihtimali olan skor, x ve g değişkenlerini gönderiyoruz. Bu değişkende öncelikle uzay gemisinin hemen bir önceki satırından (len(in_board) - 2) başlamak üzere hedefe (coordinates["x"]) varana kadar ateş sahnelerinin görülebileceği sahneleri yazdırıyoruz.

Eğer x -1 değilse de ateş animasyonundan sonra asteroitin bulunduğu koordinatlardan * işaretini kaldırıyoruz. Bu işlem sonrasında skoru arttırmanın yanısıra bu asteroitin bulunduğu liste içinde başka bir asteroit kalmamış ise asteroit bulutunun yüksekliğini azaltıp gemiyle arasında bulunan boşluğu arttırıyoruz ve güncel board, skor, yükseklik ve uzaklık değişkenlerini döndürüyoruz.

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 Aksiyonu

Exit aksiyonu seçilmesi halinde state'i güncelleyerek tahtanın son hali ve skoru yazdırarak break ile while döngüsünden çıkıp oyunu o noktada sonlandırabiliriz.

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

Zafer Kontrolü ve Tur Artışı

Daha önce uyguladığımız zafer kontrolünün aynısını while içinde bu noktada tekrar uyguluyoruz. Bu sefer farklı olarak oyunun kazanılması halinde aynı exit aksiyonunda olduğu gibi break ile oyunu bu noktada sonlandırabiliriz.

Yine bu noktada henüz oyunu sonlandıran bir şey yaşanmadığı takdirde artık gelecek turu değerlendirmeye başlayabileceğimizden tur sayısını 1 arttırıyoruz.

# 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

Yenilgi Kontrolü ve Asteroitleri Yaklaştırmak

# 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

Her gelecek tur beşin katı olduğunda asteroitler ile uzay gemisi arasındaki boşluk olan g'yi bir eksiltiyoruz. Eğer g -1'e ulaşırsa bu asteroitler ve uzay gemisinin aynı çizgide olduğunu göstereceğinden yenilgi promptu ile oyunu sonlandırıyoruz.

G'nin 0 ve 1 olduğu koşullarda board'un sondan ikinci indexinde bulunan listeyi, diğer koşullarda index - g listeyi board'dan kaldırıyoruz ve daha sonra asteroitler üzerindeki boşluğu boş bir liste oluşturarak arttırıyoruz.

Kod Tahtası

Aşağıda oyunun rastgele inputlar ile başlatılmış bir versiyonu bulunuyor. Burada kod üzerinde değişiklikler yaparak deneme yapabilirsiniz. Yapmayıp sadece oynaya da bilirsiniz. Fakat böyle çok eğlenceli değil.

Öneriler ve Düzeltmeler

Kodda eksik ya da geliştirilebilir bulduğunuz kısımlar için bu yazıya github hesabınız ile aşağıdan yorum yapabilir veya [email protected] adresinden bildiremezsiniz. Çünkü böyle bir mail hesabı yok.