17 Nisan 2023 Pazartesi

Python Tkinter ile Slant Oyunu Yazalım

 Selam , bu sıralar oyunlara taktım. Mantık oyunlarını ya oynamak ya da çözmek için kod denemeleri yapıp duruyorum. Bu oyunlardan biri de Slant oyunu. Bu oyun için de çözmek değil ama oynamak için kod yazdım. Bu yazımda sizlerle bu kodu  yazarken nasıl düşündüğümü ve hangi yollardan gittiğimi paylaşacağım.

Pek uzun bir program olmayacak, bu yüzden tek bir Python dosyası ile bitireceğiz. Ama yanında oyun haritaları olacağı için ayrı bir klasörde olsa daha iyi olacak. 

Öncelikle temel ihtiyacımız olan görsel yapıları bir oluşturalım. Öncelikle minimum bir uygulama .

import tkinter as tk

app = tk.Frame(None, width=500, height=500, bg="gray")

app.pack()
app.master.title("SLANT")
app.master.resizable(0, 0)
app.mainloop()

İlk önce Tkinter GUI kütüphanesini tk adıyla kodumuza dahil ediyoruz. Programın çalışacağı pencere app adında bir Tkinter.Frame nesnesi. En dışta olacağı için master değeri None olarak verildi. Genişlik ve yükseklik 500'er piksel yeterli. Arkaplan rengi de Gri olarak belirtiyoruz.

app.pack() metodu ile app nesnemizim ekranda görüntülenmesi için ekrana yerleştiriyoruz. app Frame nesnesini tanımlarken master değerini None vermiştik ama onu ekrana yerleştirdiğimizde otomatik olarak bir Windows uygulama penceresi içine yerleşecektir. Bu durumda app.master ile o uygulama penceresine ulaşıyoruz. İlk önce uygulama başlık değerini "SLANT" olarak veriyoruz, sonra uygulamanın yatay ve dikey olarak boyutlandırılmasını engelliyoruz. Son satır ise uygulamamızın kullanıcı ile etkileşimlerini takip edecek olan mainloop() metodunu çağırıyoruz. Artık penceremiz hem ekranda görünüyor, hem de bizim yapacağımız mouse , klavye vs etkileşimleri takip ediyor. mainloop() çağırmazsak ve programı çalıştırırsak sanki hiçbir şey yapmıyor gibi görünür ama aslında pencere falan gösterilip mili saniye içinde program biter ekrandan yok olur. mainloop() metodu ise sürekli bir döngü olarak çalışıyor. Ne zaman ki biz çarpı tuşuna tıklayıp ya da başka yollarla programdan çıkarsak bu döngüden dışarı çıkar.

Bu programı çalıştıralım




Python Tkinter ile Button ve Canvas Eklemek

Penceremizde yeni oyunu seçmek ve başlatmak için bir Tkinter.Button nesnesi ve bir de oyunu göstereceğimiz tablayı çizmek için Tkinter.Canvas nesensi ekleyelim. 

....
app = tk.Frame(None, width=500, height=500, bg="gray")

newButton = tk.Button(app, text="Yeni Oyun", bg="lime",
            font="Helvetica 14 bold")
newButton.place(x=30, y=10)

GameCanvas = tk.Canvas(app, bg="white", width=(5+2)*30,
            height=(5+2)*30)
GameCanvas.place(x=30, y=70)

app.pack()
....

newButton nesnemiz bir Tkinter.Button nesnesi, üzerinde "Yeni Oyun" yazıyor, rengi parlak yeşil ve yazı fontu biraz büyük ve koyu. master değerini app vererek bu butonun app penceresi içinde olduğunu belirtiyoruz. Bu sefer yerleştirme için place() metodu kullanıyoruz. place() metodu buton nesnemizi sabit bir yere yerleştirmek için kullanılan mutlak (absolute) yerleştirme komutu. 

Oyun tablasını çizeceğimiz Canvas nesensine ise GameCanvas adını verdik. İsmini büyük harf ile yazmamın sebebi bu nesneyi daha sonra başka metodlarım içinde bir global değişken olarak kullanacağım için. İlk harfi büyük isimli değişkenler bana global değişken ifade ediyor. GameCanvas yine app penceresi içinde arkaplan rengi beyaz. Boyutlarını (5+2)*30 şeklinde yazmamın sebebi 5x5 oyun tablasının nasıl görüneceğini test etmek için. Daha ilerledikçe oraya 5 sayısı yerine tablanın boyut bilgisini içeren bir Size adlı değişken gelecek. GameCanvas nesenmizi de place() metodu ile sabit bir noktaya yerleştiriyoruz.

Şimdi çalıştırırsak pencere şu hale geldi,




Python Tkinter.Canvas Üzerinde Dikdörtgen, Çizgi, Daire, Yazı Çizmek

Oyunu biraz incelediyseniz çözülmüş hali şuna benziyor:

Bir çerçeve, arkada ızgara çizgileri, birleşme noktalarında yuvarlaklar ve içlerinde sayılar. Izgaranın her hücresinde sağa ya da sola çapraz çizgiler. Hepsini tek tek yazacağız öncelikle çerçeve ile başlayalım. Başlarken tabla boyutunu da Size isimli bir global değişkene göre ayarlayalım.

import tkinter as tk

Size = 5

app = tk.Frame(None, width=500, height=500, bg="gray")

....

GameCanvas = tk.Canvas(app, bg="white", width=(Size+2)*30,
            height=(Size+2)*30)
GameCanvas.place(x=30, y=70)

....

Size değerini 5,7 ve 10 girerek ekranda boyutuna bakabilirsiniz. Bu 3 Size değeri ile çalışacağız. Şimdi çerçeve var sırada. Çerçeve ve diğer tüm verilere göre oyun tablası çizecek bir DrawGame() metodu tanımı yazalım.

import tkinter as tk

Size = 5

def DrawGame():
    global Size, GameCanvas

    GameCanvas.delete("all")
    GameCanvas.place(x=30, y=70, width=(Size+2)*30, height=(Size+2)*30)
    GameCanvas.create_rectangle(30, 30, Size*30+30, Size*30+30, width=3)

....

DrawGame()

app.pack()
app.master.title("SLANT")
app.master.resizable(0, 0)
app.mainloop()

DrawGame() metodumuzu program başlamadan hemen önce bir çağırıp görselimizin temellerini çizmesini sağlıyoruz. Global değişkenler Size ve GameCanvas kullanacağımızı deklare ediyoruz. İlk başta GameCanvas içinde olan tüm çizimleri (şimdilik yok ama ilerledikçe olacak) GameCanvas.delete("all") ile siliyoruz. Size değerimiz yeni oyun başlattığımızda kullanıcının seçeceği haritaya göre değişecek ve bu yeni Size değerinin etkili olması için place() metodunu kullanarak GameCanvas nesnesinin görseli tekrar yerleştirmesini sağlıyoruz. Son olarak create_rectangle() metodu ile GameCanvas içine ortalayacak şekilde bir kare çizdiriyoruz. Karenin üst-sol köşesi 30 piksel soldan 30 piksel yukardan içeride. Karenin genişliği ve yüksekliği Size*30 olacak ama create_rectangle() metodu bizden karenin bitiş noktası koordinatlarını beklediği için , başlangıçlarımız 30'ar piksel içeriden se bitişlerimiz de boyut artı 30'ar piksel olacak yani Size*30+30. Şimdi çalıştırıp Size = 5 için bir bakalım

İstediğimiz oldu. Şimdiden sonra fazla yer kaplamasın diye sadece Canvas resmini sizinle paylaşacağım, tüm pencereyi resimlemeye gerek yok. Sırada ızgara çizgilerimiz var. Bunu bir döngü içinde yaparız.

def DrawGame():
    global Size, GameCanvas

    GameCanvas.delete("all")
    GameCanvas.place(x=30, y=70, width=(Size+2)*30, height=(Size+2)*30)
    GameCanvas.create_rectangle(30, 30, Size*30+30, Size*30+30, width=3)

    for r in range(2, Size+1):
        GameCanvas.create_line(30, r*30, Size*30+30, r*30, width=1, fill="lightgray")
        GameCanvas.create_line(r*30, 30, r*30, Size*30+30, width=1, fill="lightgray")

30 içerden başladık, bir 30 da ilk çizgiye gitmek için olunca döngü 2'den başlıyor. Size 5 ise 4 tane çizgi var, yani 2,3,4,5 değerleri olacak bu amaçla döngümüz için range(2, Size+1) değerleri veriliyor. Hatırlarsak range() komutunda ilk parametredeki değer kullanıma dahil, ikinci parametre ise hariçte kalır. Size+1 yani örneğin 5 için 6 vererek döngüde r değerinin en fazla 5 olacağını belirtiyoruz. 

İlk çizgimiz yatay. Bu çizgilerin hepsinin x0 değeri 30 yani çerçevenin sol kenarından başlıyorlar. y0 değeri ilk başta 60 (2 x 30) ve her turda 30 artıyor. x1 çizginin bittiği yerin yatay değeri ve bu da tüm çizgilerde aynı ve Size*30+30 değerinde. y1 değeri çizgimiz yatay olduğu için y0 değeri ile aynı yani r*30

İkinci çizgi dikey olacak. Yani bu sefer x0 ve x1 değerleri aynı ve r*30 olacak. Çizgilerin başlangıcı yukarıdan sabit 30 piksel aşağıda yani y0 = 30. Bittiği yer y1 de Size*30+30 değerinde olacaktır. Şimdi çalıştırıp bakalım.

Izgara da tamam. Sırada çapraz çizgiler var. Bu çizgileri aslında bir array içinde tanımlanmış değerlere göre çizeceğiz. Ama şimdilik sabit 2 tane çizelim. Bu çizgileri içinde sayılar olan noktalardan önce çizmeliyiz ki noktalar çizgilerin üzerine gelsin. 

def DrawGame():
    global Size, GameCanvas

    GameCanvas.delete("all")
    GameCanvas.place(x=30, y=70, width=(Size+2)*30, height=(Size+2)*30)
    GameCanvas.create_rectangle(30, 30, Size*30+30, Size*30+30, width=3)

    for r in range(2, Size+1):
        GameCanvas.create_line(30, r*30, Size*30+30, r*30, width=1, fill="lightgray")
        GameCanvas.create_line(r*30, 30, r*30, Size*30+30, width=1, fill="lightgray")

    r, c = 1, 1
    GameCanvas.create_line(c*30+30, r*30+60, c*30+60, r*30+30, width=2)
    c = 2
    GameCanvas.create_line(c*30+60, r*30+60, c*30+30, r*30+30, width=2)

Izgaraların aralarında kalan kare boşlukları bir tablonun satır ve sütun index'leri ile hücre gibi ifade ettiğimizi düşünürsek sağa çizginin başladığı nokta hücrenin sol-alt köşesi bittiği nokta hücrenin sağ-üst köşesi olur. Satır ve sütun değerlerini sırayla r ve c değişkenlerine koyuyoruz (row ve column). x0 = c*30+30 , y0 = r*30+60 değerleri bize hücrenin sol alt köşesini verir. x1 = c*30+60 ve y1 = r*30+30 değerleri de bize hücrenin sağ-üst köşesinin koordinatlarını verir. width=2 ile çizgi kalınlığını 2 piksel olarak veriyoruz. Renk belirtilmediği için çizgilerimiz default siyah olacaktır. 

İkinci çizgimiz ise sola doğru çapraz olacak ve satır 1, sütun 2'de yer alacak. x0=c*30+60 ve y0=r*30+60 bizi hücrenin sağ-alt köşesine götürür. Çizgi buradan başlar , bittiği nokta ise x1=c*30+30 ve y1=r*30+30 hücrenin sol-üst köşesinin koordinatlarını verir. Çalıştırırsak

Sıra geldi bir nokta ve üzerindeki sayıyı nasıl yazacağımıza. Noktalarımız kenarlar üzerinde de olabileceği için ileride onlar için kullanacağımız değerlerde Size değerinden bir fazla sayıda satır ve sütun değerine sahip olabileceği şimdiden görünüyor. Şu yukarıdaki iki çapraz çizginin birleşme koordinatına bir nokta bilgisi ekleyelim. 

def DrawGame():
    global Size, GameCanvas

    ....

    r, c = 1, 1
    GameCanvas.create_line(c*30+30, r*30+60, c*30+60, r*30+30, width=2)
    c = 2
    GameCanvas.create_line(c*30+60, r*30+60, c*30+30, r*30+30, width=2)

    r, c = 1, 2
    GameCanvas.create_oval(c*30+21, r*30+21, c*30+39, r*30+39,
            width=1, fill="white")
    GameCanvas.create_text(c*30+30, r*30+30, text=str(2),
            font="Helvetica 10 bold")

create_oval() metodunda parametre olarak çizilecek dairenin sol-üst ve sağ-alt koordinatlarını veriyoruz. Tabi tam dairenin kenar çizgisi değil bunlar, daireyi sanki bir dikdörtgen içinde kenarlara teğet olarak çizilmiş gibi düşünüp o dörtgenin köşelerini parametre olarak veririz. x=c*30+30 ve y=r*30+30 tam olarak ızgara çizgilerin birleşme noktası olur. 9 piksel yarı çapında bir daire çizmek için bu koordinattan 9 yukarı ve 9 sola giderek daire kutusunun sol-üst koordinatını buluruz, yani x0=c*30+21 ve y0=r*30+21 olacak. Daire kutusunun sağ-alt köşesi için de x1=c*30+39 ve y1=r*30+39 olacaktır. width=1 ile daire çember çizgisini ince bir çizgi olarak kalınlığını veririz. Default olarak create_oval() metodu fill değeri None olup içi boş kalır. Ama bu durumda altında kalan çapraz çizgiler görüneceği için fill="white" değerini girerek daire içinin dolu beyaz olmasını sağlıyoruz.

create_text() metodu ise Canvas üzerine yazı yazar. Yazılarda tek bir koordinat verilir, bu da yazının yatay ve dikey olarak orta noktasıdır. Bu yüzden tam olarak ızgara kesişme noktasının koordinatını x=c*30+30 ve y=r*30+30 olarak veririz. text=str(2) değerini girerek oraya "2" yazmak istediğimizi belirttik. Aslında text="2" şeklinde yazabilirdik, ama ilerleyen kısımda bu değerleri integer sayılar olarak kullanacağımız aşikar olduğu için harita verilerinden oraya bir tamsayı değeri geleceği ve bunun da string'e dönüştürülmesi gerekeceği şimdiden aşikar.

Programı çalıştırırsak,



Tkinter.filedialog Kullanarak Seçilen Dosyadan Verileri Almak

Yukarıda en son nasıl yaparız diye örneğini hazırladığımız içinde sayı olan dairelerin bilgisi oyunumuzun temel verilerinden birini oluşturuyor. Oyun en başında oyuncuya bu noktalar veriliyor ve oyuncu kurallara uyarak tüm ızgarayı çapraz çizgilerle dolduruyor. Örnek bir oyuna başlama tablası şöyle görünür.

Bu noktaların bilgilerini bir array içinde toplayabiliriz. Mesela sol üt köşedeki sıfır değerli nokta için [0,0,0] değerleri verilirse ilk değer satır bilgisi, ikinci değer sütun bilgisi, üçüncü değer de üzerinde yazan sayı olur. Bu doğrultuda tabladaki 2 tane içinde 3 yazan dairenin bilgileri de [2,4,3] ve [3,4,3] olacaktır. Önce Map adında global bir değişken içinde yukarıdaki örnekteki noktaların değerlerini sabit olarak girelim ve Canvas üzerine çizilmelerini sağlayalım. Daha sonrasında bu değerleri kullanıcının seçtiği harita dosyasından okuyacağız. 

import tkinter as tk

Size = 5
Map = [
    [0,0,0], [0,2,0], [0,3,2], [1,0,1], [1,4,1], [2,0,1],
    [2,1,1], [2,3,2], [2,4,3], [3,2,2], [3,4,3], [3,5,0],
    [4,0,1], [4,3,1], [4,5,2], [5,1,1], [5,2,1]
]

def DrawGame():
    global Size, GameCanvas

    GameCanvas.delete("all")
    GameCanvas.place(x=30, y=70, width=(Size+2)*30, height=(Size+2)*30)
    GameCanvas.create_rectangle(30, 30, Size*30+30, Size*30+30, width=3)

    for r in range(2, Size+1):
        GameCanvas.create_line(30, r*30, Size*30+30, r*30, width=1, fill="lightgray")
        GameCanvas.create_line(r*30, 30, r*30, Size*30+30, width=1, fill="lightgray")

    r, c = 1, 1
    GameCanvas.create_line(c*30+30, r*30+60, c*30+60, r*30+30, width=2)
    c = 2
    GameCanvas.create_line(c*30+60, r*30+60, c*30+30, r*30+30, width=2)

    for p in Map:
        GameCanvas.create_oval(p[1]*30+21, p[0]*30+21, p[1]*30+39, p[0]*30+39,
            width=1, fill="white")
        GameCanvas.create_text(p[1]*30+30, p[0]*30+30, text=str(p[2]),
            font="Helvetica 10 bold")
....

 Program başında Map değişkenine örnek haritamızdaki değerleri verdik. DrawGame() metodumuzun sonunda daha önce sadece bir tane noktayı çizdiğimiz kodu artık bir döngü içine alıp Map içinde verilen tüm noktaları çizecek hale getirdik. 

Şimdi bu harita bilgilerini bir dosya içine koyalım ve kullanıcı Yeni Oyun butonuna tıklayınca seçtireceğimiz dosyadaki verilere göre tablayı çizelim. Dosya içinde yukarıdaki örnek bilgileri şöyle koyabiliriz.

0,0,0
0,2,0
0,3,2
1,0,1
1,4,1
2,0,1
2,1,1
2,3,2
2,4,3
3,2,2
3,4,3
3,5,0
4,0,1
4,3,1
4,5,2
5,1,1
5,2,1

Ben bir kısım harita dosyasını bir zip dosya olarak şurada sizinle paylaştım. Bu zip dosya içindeki maps klasörünü slant.py program dosyamızın olduğu klasör içine kopyalayalım.Yeni Oyun butonu tıklanınca newGame adında bir metod çağıralım.

....
newButton = tk.Button(app, text="Yeni Oyun", bg="lime",
            font="Helvetica 14 bold")
newButton.place(x=30, y=10)
newButton.bind("<Button-1>", newGame)
....

Bu metodun tanımında öncelikle kullanıcıdan bir harita dosyası seçmesini isteyeceğiz. Bunun için de ilk önce Tkinter.filedialog kütüphanesini import etmeliyiz.

import tkinter as tk
import tkinter.filedialog as fd

Size = 5
Map = []
....

Map global değişkenine denemek için verdiğimiz değerleri de sildik. Şimdi newGame() metodu tanımını yapalım.

....
def newGame(olay):
    global Map, Size
    fname = fd.askopenfilename(initialdir="./maps/",
        filetypes =[('Map Dosyaları', '*.map')])
    if fname:
        print(fname)


app = tk.Frame(None, width=500, height=500, bg="gray")
....

Map ve Size global değişkenlerini kullanmak mutlaka gerekecek, onları metoda dahil ettik. Tkinter.filedialog kütüphanesini fd adıyla programa dahil etmiştik. askopenfilename() metodu bize sistemden bir Dosya Aç penceresi getirir ve eğer bir dosyayı seçer ve butonuna tıklarsak bize dosyanın path bağlantısı ile birlikte tüm adını verir. Eğer dosya seçmeden pencere kapatılırsa metod None değeri döner. initialdir="./maps/" opsiyonu ile pencerenin Python programımız ile aynı klasörde bulunan maps alt klasöründe açılmasını sağlıyoruz. filetypes opsiyonunda ise tuple değerlerden oluşan bir array şeklinde izin verilen dosya filtrelemelerini tanımlarız. Burada tek tuple var o da ('Map Dosyaları', '*.map') filtresi. Bu bize uzantısı .map olan tüm dosyaları gösterir. Bu filtre ile açılan pencere şuna benzer.

Ama mesela sadece Kolay olan .map dosyaları veya sadece Orta zorluk olan .map dosyalarını da seçebiliriz.

    fname = fd.askopenfilename(initialdir="./maps/",
        filetypes =[('Map Dosyaları', '*.map'),
            ('Kolay Oyunlar', 'fKolay*.map'),
            ('Orta Zorluk', 'fOrta*.map')])

gibi. Çalıştırıp bir dosya seçtiğimizde konsola seçtiğimiz dosyanın tam adını yazacaktır.

Şimdi dosyanın içindeki bilgilerle Size ve Map değişkenlerimizin değerlerini verelim. Önce Size,

def newGame(olay):
    global Map, Size
    fname = fd.askopenfilename(initialdir="./maps/",
        filetypes =[('Map Dosyaları', '*.map'),
            ('Kolay Oyunlar', 'fKolay*.map'),
            ('Orta Zorluk', 'fOrta*.map')])
    if fname:
        if "5_5_" in fname:
            Size = 5
        if "7_7_" in fname:
            Size = 7
        if "10_10_" in fname:
            Size = 10

Dosya ismine göre Size değeri 5, 7 veya 10 olarak veriliyor. Map değerleri için dosyadaki her satırı bir array olarak alıp eklemeliyiz.

def newGame(olay):
    global Map, Size
    fname = fd.askopenfilename(initialdir="./maps/",
        filetypes =[('Map Dosyaları', '*.map'),
            ('Kolay Oyunlar', 'fKolay*.map'),
            ('Orta Zorluk', 'fOrta*.map')])
    if fname:
        if "5_5_" in fname:
            Size = 5
        if "7_7_" in fname:
            Size = 7
        if "10_10_" in fname:
            Size = 10
        Map.clear()
        for line in open(fname, "r").readlines():
            ars = line.split(",")
            arn = []
            for s in ars:
                arn.append(int(s))
            Map.append(arn)
        DrawGame()

Her şey bittikten sonra DrawGame() metodunu çağırarak en son verilere göre tablayı tekrar çizdiğimize dikkat edin. Öncelikle Map değişkeni içinde bir şeyler varsa Map.clear() metodu ile siliyoruz. open(fname, "r").readlines() bize dosyayı sadece okuma amacıyla açar ("r" parametresi) ve her satıra karşı bir string olarak bir array şeklinde dosyadaki bilgileri döner. Python Konsolda deneyip bakalım ne dönüyor.

PS G:\ujk_common\prg\python\Tkinter\blog\slant> python
Python 3.9.4 (tags/v3.9.4:1f2e308, Apr  6 2021, 13:40:21) [MSC v.1928
64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license"
for more information.
>>> fname = "./maps/fKolay5_5_01.map"
>>> open(fname, "r").readlines()
['0,0,0\n', '0,2,0\n', '0,3,2\n', '1,0,1\n', '1,4,1\n', '2,0,1\n',
'2,1,1\n', '2,3,2\n', '2,4,3\n', '3,2,2\n', '3,4,3\n', '3,5,0\n',
'4,0,1\n', '4,3,1\n', '4,5,2\n', '5,1,1\n', '5,2,1']
>>>

ars değişkenine o andaki satırı virgüllerinden split() ile ayırıp koyuyoruz. Mesela ilk satır için 

>>> ars = '0,0,0\n'.split(",")
>>> ars
['0', '0', '0\n']

Sonrasında bu array elemanlarını tek tek sayıya çevirip arn array değerini oluşturuyoruz. Elde ettiğimiz arn değerlerini de Map değişkenine yeni eleman olarak ekliyoruz.

Çalıştırırsak artık Yeni Oyun butonu bastıktan sonra seçtiğimiz dosyaya ait harita ekrana gelecektir. Aşağıda örnek haritalar görünüyor.

Görselde deneme için koyduğumuz 2 çapraz çizgi her görünümde sabit duruyor. Onların da bir veriye göre çizilmesi gerekiyor. Şimdi sıra onlara geldi. 



Tkinter Mouse Tıklamalara Göre Canvas'a Çizgiler Eklemek

Öncelikle çapraz çizgileri nasıl veri olarak ifade edeceğiz bir karar vermek lazım. Oyun tablamızı ızgara olarak çizdik, bu ızgaranın her hücresinde mutlaka bir çapraz çizgi olacak oyunu bitirebilmek için. Lines adında 2 boyutlu bir array yapmaya ve bu array içinde her satırdaki hücreler için de bir array içinde çizgilere karşılık sayılar kullanmaya karar verdim. Eğer hücrede çizgi yoksa değer sıfır, sağa çapraz çizgi varsa değer +1 sola çapraz çizgi varsa değer -1 olacak. Başlangıç için Lines değişkenine 5x5 oyun tablası için örnek değer girip DrawGame() metodu içinde buna uygun çapraz çizgilerin çizilmesini sağlayalım. 

import tkinter as tk
import tkinter.filedialog as fd

Size = 5
Map = []
Lines = [[0,0,0,0,0],[0,0,-1,0,0],[0,0,0,0,0],[0,1,0,0,0],[0,0,0,0,0]]
....

ve DrawGame() metoduna yapılan değişim. Daha önce deneme için yaptığımız çizimleri döngü içinde bu Lines array değerlerine göre yapacağız.

def DrawGame():
    global Size, GameCanvas, Lines

    .....

    for r in range(0, len(Lines)):
        for c in range(0, len(Lines[r])):
            if Lines[r][c] > 0 :  # 45 derce
                GameCanvas.create_line(c*30+30, r*30+60, c*30+60, r*30+30, width=2)
            elif Lines[r][c] < 0 :  # -45 derece
                GameCanvas.create_line(c*30+60, r*30+60, c*30+30, r*30+30, width=2)

    for p in Map:
        GameCanvas.create_oval(p[1]*30+21, p[0]*30+21, p[1]*30+39, p[0]*30+39,
            width=1, fill="white")
        GameCanvas.create_text(p[1]*30+30, p[0]*30+30, text=str(p[2]),
            font="Helvetica 10 bold")

Çapraz çizgileri Map içindeki noktaları koymadan önce çizeceğiz ki noktaların altında kalsın, bunu daha önce de söylemiştik. Şimdi çalıştırırsak örnek değerlere binaen başlangıçta tabla şöyle görünecektir.

Verilere göre çapraz çizgilerin çizilmesi işlemi tamam. Şimdi Canvas üzerinde mouse tıklamasına göre bu çizgilerin çizilmesi var. Bir hücre üzerinde sol tıklanınca sola çapraz çizgi, sağ tıklanınca sağa çapraz çizgi çizeceğiz. Eğer önceden aynı yönde çizgi varsa da sileceğiz. Önce örnek değerleri sıfırlayalım.

import tkinter as tk
import tkinter.filedialog as fd

Size = 5
Map = []
Lines = [[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]]
....

GameCanvas nesnesi üzerinde sağ ve sol mouse tıklamalarına karşılık gelen metod yönlendirmelerini yapalım.

....
GameCanvas = tk.Canvas(app, bg="white", width=(Size+2)*30,
            height=(Size+2)*30)
GameCanvas.bind("<Button-1>", LeftClick)
GameCanvas.bind("<Button-3>", RightClick)
GameCanvas.place(x=30, y=70)
....

bind() metodu bağlı olduğu nesneye yapılan etkileşimleri metodlara bağlar. <Button-1> sol mouse butonu tıklanması olayını ifade eder, <Button-3> ise sağ mouse butonu tıklanmasıdır. 2 nolu buton mouse tekerleğini bastırınca oluşan tıklama olur. Metodları bağlarken sadece isimlerinin yazıldığı sonuna parantez işareti konmadığına dikkat edelim. Bu metodları tanımlamadan program çalışmaz. Şimdilik sadece metodlarda bir şeyler yazalım.

....
def LeftClick(olay):
    print("Sol tık:", olay.x, olay.y)

def RightClick(olay):
    print("Sağ tık:", olay.x, olay.y)

def newGame(olay):
....

Metodların default parametresi olay bize gerçekleşen olay ile ilgili bilgileri veren bir nesnedir. olay.x tıklama yapılan x koordinatının Canvas içindeki yerini ve olay.y ise tıklama yapılan y koordinatının Canvas içindeki yerini verir. Deneyelim,

Verilen koordinatlar piksel cinsinden biz bunlara karşılık Lines değerlerine müdahale edemeyiz. Bu noktanın ızgaranın hangi satır ve sütununda olduğunu hesaplamalıyız. Bu hesabı yapan bir metod yazalım.

....
def GetRowCol(x, y):
    if y < 30 or x < 30:
        return -1, -1
    else:
        return int((y-30)/30), int((x-30)/30)

def LeftClick(olay):
....

x veya y değeri 30'dan küçükse çerçeve dışında tıklama olmuştur. Sonuçları integer değere dönüştürüyoruz, çünkü bölme sonrası noktalı sayı çıkar ve biz bunu array index olarak kullanamayız. 

Şimdi Tıklama metodlarına geri dönebiliriz.

def LeftClick(olay):
    global Lines, Size
    row, col = GetRowCol(olay.x, olay.y)
    if row >= 0 and col >= 0 and row < Size and col < Size:
        if Lines[row][col] == -1:
            Lines[row][col] = 0
        else:
            Lines[row][col] = -1
        DrawGame()

def RightClick(olay):
    global Lines, Size
    row, col = GetRowCol(olay.x, olay.y)
    if row >= 0 and col >= 0 and row < Size and col < Size:
        if Lines[row][col] == 1:
            Lines[row][col] = 0
        else:
            Lines[row][col] = 1
        DrawGame()

Öncelikle global olan Lines ve Size değerlerini kullanacağımızı deklare ediyoruz. Eğer tıklamanın gerçekleştiği hücre çerçeve içindeyse çizim işlemi gerçekleşecek demektir. Sol tık için eğer o anda hücrede -1 varsa zaten çizgi vardır , silmek için sıfır yazarız, -1 yoksa ne olursa olsun -1 yazarak sola çapraz çizgi olacağını belirtiyoruz. En sonda da DrawGame() metodunu çağırarak yaptığımız değişimin görsele gelmesini sağlıyoruz.

Fakat bir şey unuttuk. Oyunun en başında tüm çizgileri silmek gerekir. Bunu newGame() metodu içinde yapacağız.

def newGame(olay):
    global Map, Size
    ....
        for line in open(fname, "r").readlines():
            ars = line.split(",")
            arn = []
            for s in ars:
                arn.append(int(s))
            Map.append(arn)

        Lines.clear()
       
        for i in range(0, Size):
            arl = [0] * Size
            Lines.append(arl)

        DrawGame()

Öncelikle Lines içindeki tüm değerleri siliyoruz, sonrasında Size değerine göre içine tamamen sıfırlar ekleyerek boş tablayı elde ediyoruz. 

Artık oyunumuz oynamaya hazır çalıştırıp Yeni Oyun başlatıp oynayabiliriz.

Ama bir şey var tüm kontrolü bizim yapmamız gerekiyor. Çözüm doğru mu değil mi biz kendimiz yapıyoruz. 




Oyun Kurallarını Uygulamak

Oyunun bitmesi için tüm hücrelerde çapraz çizgi olması gerekiyor. Bunları çizerken 2 basit kural var:

  1. Her noktada içinde yazan sayı kadar çapraz çizgi ucu birleşebilir
  2. Çizilen çapraz çizgiler birleşip bir kapalı döngü oluşturmayacak

İlkinden başlayalım. Bir noktaya içindeki sayıdan fazla sayıda çapraz çizgi bağlandıysa dairenin arka plan rengini kırmızı yaparak oyuncuyu uyaralım. Bunu DrawGame() metodu içinde noktaları çizmeden yapalım ki noktayı çizmeden önce rengini karar verelim. Noktanın dört tarafındaki hücrelerdeki çizgileri sayalım. 

def DrawGame():
    global Size, GameCanvas, Lines
....

    for p in Map:
        oColor = "white"
        # bağlı olan fazlaysa kırmızı
        connNr = 0
        if p[0] < Size and p[1] < Size and Lines[p[0]][p[1]] == -1:
            connNr +=1
        if p[1] > 0 and p[0] < Size and Lines[p[0]][p[1]-1] == 1:
            connNr += 1
        if p[0] > 0 and p[1] < Size and Lines[p[0]-1][p[1]] == 1:
            connNr += 1
        if p[0] > 0 and p[1] > 0 and Lines[p[0]-1][p[1]-1] == -1:
            connNr += 1
        if connNr > p[2]:
            oColor = "red"
        GameCanvas.create_oval(p[1]*30+21, p[0]*30+21, p[1]*30+39, p[0]*30+39,
            width=1, fill=oColor)
        GameCanvas.create_text(p[1]*30+30, p[0]*30+30, text=str(p[2]),
            font="Helvetica 10 bold")

İlk karşılaştırma noktanın sağ altında sola çapraz çizgi varsa toplamı bir arttırıyor. İkincisi noktanın sol altında sağa çapraz varsa toplamı bir arttırıyor. Üçincü noktanın sağ üstünde sağa çapraz varsa toplamı bir arttırıyor. Sonuncu karşılaştırma ise noktanın sol üstündeki hücrede sola çapraz varsa toplamı bir arttırıyor. Toplam bağlı çizgi sayısı nokta üzerinde yazan rakamdan fazlaysa oColor değişkenine "red" değeri konuyor ve bu değer daha sonra noktaya ait daireyi çizerken fill özelliğine veriliyor. Deneyelim

Noktalar konusunda oyuncuyu bir konuda daha uyarabiliriz. Olası bağlanabilecek çizgi sayısı nokta içindeki sayıdan azsa boş hücreler tamamen dolsa da yeterli çizgi bağlama ihtimali kalmamış demektir. Bir kontrol de olası bağlantı sayısı üzerinden yapalım.

....
        if connNr > p[2]:
            oColor = "red"
        # olası bağlantı azsa kırmızı
        possNr = 0
        if p[0] < Size and p[1] < Size and Lines[p[0]][p[1]] == 0:
            possNr +=1
        if p[1] > 0 and p[0] < Size and Lines[p[0]][p[1]-1] == 0:
            possNr += 1
        if p[0] > 0 and p[1] < Size and Lines[p[0]-1][p[1]] == 0:
            possNr += 1
        if p[0] > 0 and p[1] > 0 and Lines[p[0]-1][p[1]-1] == 0:
            possNr += 1
        if possNr+connNr < p[2]:
            oColor = "red"
                       
        GameCanvas.create_oval(p[1]*30+21, p[0]*30+21, p[1]*30+39, p[0]*30+39,
            width=1, fill=oColor)
....

Daha önce mevcut bağlantı sayısını connNr değişkeninde toplamıştık. Bunun üzerine etraftaki boş hücrelerin sayısını eklersek olası toplam bağlantı sayısını buluruz. Bu sayı nokta üzerindeki sayıdan küçükse noktanın tamamlanma ihtimali kalmamış demektir. Deneyelim.


Çember Kontrolü

Burası çok karışık. Çapraz çizgiler birleşip bir kapalı döngü yapmamalı. Önce oturup bir plan yapalım.

Bu çember örneğinden yola çıkarsak aşağı yukarı kurallar belirlenir. Bir araba olup çapraz çizgilerde yol aldığımızı düşüneceğiz. Oyun tablasındaki tüm noktaları tarayacağız. Eğer sağ-aşağı yönde bir çizgi varsa o yönde gitmeye başlayacağız. Öyle ya!, bir kapalı döngü varsa mutlaka bir yerinde sağ-aşağı gidecek, bir yerden başlamak lazım. Devam etmeden buna göre daha karmaşık çemberlere de örnek verelim.

İşimiz kolay değil bunlar da kapalı birer döngü. Ben gezinmeli oyunlarda izlediğim yolu izleyeceğim. Her köşeye gelişte sola gidilebilir mi? diye bakarım. Gidemezsem düz gidiliyor mu? diye bakarım. Yine olmazsa sağa dönülebiliyor mu? ona bakarım. Olmadı geri dönerim. Tekrar plandaki örneğe dönelim.

[1,3,3] noktasına tararken gelince bakacaz sağ-aşağı bir çizgi var. Bu noktayı başlangıç olarak kaydedeceğiz. Sonra sağ-aşağı noktaya yani [2,4,3] noktasına geleceğiz. Burada sola dönülebiliyor mu, yani sol-üste doğru çizgi var mı, yok. Düz gidilebiliyor mu, yok. Sol-aşağı yönde çizgi var mı, var. Gideriz [3,3,0] noktasına. Sola dönülüyor mu, hayır. Düz gidiliyor mu, evet. Gideriz [4,2,0] noktasına. Sola dönülüyor mu, hayır. Düz gidilebiliyor mu, hayır. Sağa dönülebiliyor mu, evet. Gideriz [3,1,2] noktasına. Sola dönülüyor mu, hayır. Düz gidilebiliyor mu, hayır. Sağa dönülebiliyor mu, evet. Gideriz [2,2,2] noktasına. Sola dönülüyor mu, hayır. Düz gidilebiliyor mu, evet. Gideriz [1,3,3] noktasına. Burası başlangıç noktası. Bu noktaya çıktığımız açıdan geri gelmedik , yani sol-yukarı giderek gelmedik. Demek ki bir kapalı döngü yol çizdik. Burada kullanıcıya mesaj vermemiz lazım. En az bir çember oluştu. 

Bütün bunları adım adım programsal bir şekilde topladım. Önce yönlere bir sayı değer verdim.

Sola dönmek için yön değeri bir azaltılacak, sağa dönmek için bir arttırılacak. Düz gitmek için aynı kalacak. Şimdi az önce yaptığımız geziyi tek tek analiz edelim.

Satır satır inceledim ve şu ek kuralları yazdım.

Program döngüsünün bitiş koşulları ve tabla sınırlarına gelince yön değiştirmeler. 

Başlıyoruz. Önce kullanıcıya verilecek mesajı canvas üzerine yazmak için ilave yapalım. Çalışacağımız ter çapraz çizgiler çizildikten sonrası.

def DrawGame():
    ....

    for r in range(0, len(Lines)):
        for c in range(0, len(Lines[r])):
            if Lines[r][c] > 0 :  # 45 derce
                GameCanvas.create_line(c*30+30, r*30+60, c*30+60, r*30+30, width=2)
            elif Lines[r][c] < 0 :  # -45 derece
                GameCanvas.create_line(c*30+60, r*30+60, c*30+30, r*30+30, width=2)

        # Çember kontrolü
    for r in range(0, Size+1):      #nokta.row
        for c in range(0, Size+1):  #nokta.col
            yön = 1                 #sağ-aşağı
            kırmızı = False

            if r < Size and c < Size and Lines[r][c] == -1:
                # Burada döngü içinde yol varsa bitene kadar takip edilecek
                kırmızı = True # test için

                if kırmızı:
                    GameCanvas.create_text(Size*15, 15, fill="red",
                        text="ÇEMBER", font="Helvetica 16 bold")

    for p in Map:
....

kırmızı adında bir değişkene ilk önce False değer verdik, çember varsa bu değeri True yapacağız. Satır sütun tararken eğer o noktadan sağ aşağı bir çapraz çizgi varsa Lines[r][c] değeri -1 olacaktır. Bu noktadan kontrol başlayacak. Tabi ki tablanın sağ kenarı ve alt kenarındaki noktalardan sağ aşağı çizgi aramaya kalkarsak Lines array boyutlarının dışına taşarız, onu da garantiye aldık. Sonra diğer hiç bir şeyi yapmadan direk olarak test için kırmızı değerine True yazdık.

Tabla üzerinde herhangi bir yere sola çapraz çizgi eklediğimiz anda tabla üst tarafında ÇEMBER diye bir kırmızı yazı çıkacaktır. 

Sola çapraz çizgiyi silersek de ÇEMBER yazısı yok olacaktır. Şimdi tüm kuralları yazalım.

        # Çember kontrolü
    for r in range(0, Size+1):      #nokta.row
        for c in range(0, Size+1):  #nokta.col
            yön = 1                 #sağ-aşağı
            kırmızı = False
            bitti = False

            if r < Size and c < Size and Lines[r][c] == -1:
                nr, nc = r, c
                nr += 1
                nc += 1
                yön = 4
                while not bitti:
                    if yön == 4:
                        if nr > 0 and nc != Size\
                            and Lines[nr-1][nc] == 1:
                            nr -= 1
                            nc += 1
                            if nr == r and nc == c:
                                bitti = True
                                kırmızı = True
                            yön = 3
                        else:
                            yön = 1

                    elif yön == 1:
                        if  nr != Size and nc != Size\
                            and Lines[nr][nc] == -1:
                            nr += 1
                            nc += 1
                            if nr == r and nc == c:
                                bitti = True
                                kırmızı = True
                            yön = 4
                        else:
                            yön = 2

                    elif yön == 2:
                        if nr != Size and nc > 0\
                            and Lines[nr][nc-1] == 1:
                            nr += 1
                            nc -= 1
                            if nr == r and nc == c:
                                bitti = True
                                kırmızı = True
                            yön = 1
                        else:
                            yön = 3

                    elif yön == 3:
                        if nr > 0 and nc > 0\
                            and Lines[nr-1][nc-1] == -1:
                            nr -= 1
                            nc -= 1
                            if nr == r and nc == c:
                                bitti = True
                            yön = 2
                        else:
                            yön = 4
                           
                if kırmızı:
                    GameCanvas.create_text(Size*15, 15, fill="red",
                        text="ÇEMBER", font="Helvetica 16 bold")

Öncelikle bitti adında bir değişkene False değeri koyarak başlıyoruz. Bu değer True olduğunda gezintimiz bitmiş olacak ve çember olup olmadığı ortaya çıkmış olacak. İnceleyince yön değerinin üzerinde matematik işlem yapmaya gerek olmadığı ve koşullara göre direk değerler verildiğini görüyoruz. Bu durumda yön değerini sayı yerine daha açıklayıcı text değerler olarak da ifade edebiliriz. Mesela standart yön ifadeleri kuzeydoğu güneybatı kısaltılmışları KD ve GB gibi. 

        # Çember kontrolü
    for r in range(0, Size+1):      #nokta.row
        for c in range(0, Size+1):  #nokta.col
            yön = "GD"                 #sağ-aşağı
            kırmızı = False
            bitti = False

            if r < Size and c < Size and Lines[r][c] == -1:
                nr, nc = r, c
                nr += 1
                nc += 1
                yön = "KD"
                while not bitti:
                    if yön == "KD":
                        if nr > 0 and nc != Size\
                            and Lines[nr-1][nc] == 1:
                            nr -= 1
                            nc += 1
                            if nr == r and nc == c:
                                bitti = True
                                kırmızı = True
                            yön = "KB"
                        else:
                            yön = "GD"

                    elif yön == "GD":
                        if  nr != Size and nc != Size\
                            and Lines[nr][nc] == -1:
                            nr += 1
                            nc += 1
                            if nr == r and nc == c:
                                bitti = True
                                kırmızı = True
                            yön = "KD"
                        else:
                            yön = "GB"

                    elif yön == "GB":
                        if nr != Size and nc > 0\
                            and Lines[nr][nc-1] == 1:
                            nr += 1
                            nc -= 1
                            if nr == r and nc == c:
                                bitti = True
                                kırmızı = True
                            yön = "GD"
                        else:
                            yön = "KB"

                    elif yön == "KB":
                        if nr > 0 and nc > 0\
                            and Lines[nr-1][nc-1] == -1:
                            nr -= 1
                            nc -= 1
                            if nr == r and nc == c:
                                bitti = True
                            yön = "GB"
                        else:
                            yön = "KD"
                           
                if kırmızı:
                    GameCanvas.create_text(Size*15, 15, fill="red",
                        text="ÇEMBER", font="Helvetica 16 bold")

nr ve nc gezerken ulaştığımız noktanın satır sütun değerleri. Her yöne göre eğer o yönde çizgi varsa o yönde devam edip sola dönüyoruz, yoksa sağa dönüp çizgi var mı bakıyoruz. Eğer çizgi var ve sonraki noktaya geçtiysek ilk önce vardığımız nokta en başta çıktığımız r ve c noktasıysa döngü bitmiştir. Biterken sadece kuzeybatı yönünde giderek başa döndüysek geri giderek dönmüşüzdür, geri kalan tüm olasılıklarda bir çember oluşmuştur. Çalıştırıp çeşit çeşit çemberler oluşturup deneyebiliriz artık. 

Artık oyunumuzu kontrolleri de otomatik yapılan bir şekilde oynayabiliriz. Geriye tek şey kaldı oyunun sağlıklı bittiğini görüp oyuncuyu tebrik etmek.





OyunBitişinin Algılanması

Oyunun bittiğini anlamak için DrawGame() metodu başında oyunBitti adında bir değişkene True değer veririz ve eğer çizimi yaparken yanlış bir şey görürsek bu değeri False yaparız. Eğer metod sonuna kadar oyunBitti değeri True kalmayı başarmışsa oyun bitmiş demektir.

import tkinter as tk
import tkinter.filedialog as fd
from tkinter import messagebox  # Mesaj popup için

....

def DrawGame():
    global Size, GameCanvas, Lines
    oyunBitti = True

    ....

    for r in range(0, len(Lines)):
        for c in range(0, len(Lines[r])):
            if Lines[r][c] > 0 :  # 45 derce
                GameCanvas.create_line(c*30+30, r*30+60, c*30+60, r*30+30, width=2)
            elif Lines[r][c] < 0 :  # -45 derece
                GameCanvas.create_line(c*30+60, r*30+60, c*30+30, r*30+30, width=2)
            else:
                oyunBitti = False   # BOŞ HÜCRE VAR

        # Çember kontrolü
    for r in range(0, Size+1):      #nokta.row
        for c in range(0, Size+1):  #nokta.col

....         
                if kırmızı:
                    oyunBitti = False   # ÇEMBER VAR
                    GameCanvas.create_text(Size*15, 15, fill="red",
                        text="ÇEMBER", font="Helvetica 16 bold")

    for p in Map:
        oColor = "white"
        # bağlı olan fazlaysa kırmızı
....
        if connNr > p[2]:
            oColor = "red"
            oyunBitti = False   # FAZLA ÇİZGİ BAĞLI
        # olası bağlantı azsa kırmızı
....
        if possNr+connNr < p[2]:
            oColor = "red"
            oyunBitti = False   # OLASI ÇİZGİ AZ
                       
        GameCanvas.create_oval(p[1]*30+21, p[0]*30+21, p[1]*30+39, p[0]*30+39,
            width=1, fill=oColor)
        GameCanvas.create_text(p[1]*30+30, p[0]*30+30, text=str(p[2]),
            font="Helvetica 10 bold")

    if oyunBitti:
        messagebox.showinfo("BRAVO", "KAZANDINIZ")

....

Yaşasın!! Artık oyun oynayabiliriz. Sağlıklı bitince mesajla bizi tebrik edecektir.

Bir şu yukarıdaki butonun rengini halledemedim ya, ona yanıyorum. Dosya açma diyaloğu açılınca buton basılı kalıyor.

Oyun programımız bu kadar. Aslında bir buton daha ekleyim otomatik çözsün de istiyorum ama daha sonra inşallah.

Keyfimiz olup böyle eğlenceli projelerde tekrar buluşmak ümidiyle , kalın sağlıcakla..




Hiç yorum yok:

Yorum Gönder