16 Mart 2023 Perşembe

Python Tkinter Hashiwokakero Oyunu Yazalım

 Selam uzun bir yazı olacağını sandığım yeni bir yazı dizisiyle karşınızdayım. Japonların Hashiwokakero adını verdikleri bir mantık oyunu var. Çok severim. Ben bunu yaparım dedim. Önce C# ile yaptım ama çok uzun sürdü. Python ile yaparım dedim. Çok daha az kodla biteceğini tahmin ettim ve aynen öyle oldu. Bu yazı dizisinde oyunu yazarken gittiğim adımları sizinle paylaşmak istedim. Emin olduğum bir şey var, bir programlama dilini etkili kullanmak istiyorsan, o dil ile iş gören bir şey yapacaksın. 

Bütün yaygın kullanılan Python desktop GUI kütüphaneleri ile denemeler yapmaya başladım ve Python'un default GUI kütüphanesi olan Tkinter kütüphanesini kullanmaya karar verdim. 


Python Oyun Penceresi Taslağı

Öncelikle oyun penceresini görsel olarak bir hazırlayayım dedim. İlk önce bir klasör ve içinde hashiwokakero.py adında bir Python dosyası ile başladım. Tkinter standart yapısında bir pencere ile başlayalım.

import tkinter as tk

window = tk.Tk()
window.title("HASHIWOKAKERO")
window.geometry("800x650+200+10")

frame = tk.Frame(master=window, bg="gray", width=800, height=650)

frame.pack()

window.mainloop()

Standart Tkinter penceresi kodu. İlk satırda tkinter kütüphanesini programımıza tk adıyla dahil ediyoruz. Sonra window adını verdiğimiz uygulama penceresini tanımlıyoruz. window.title("HASHIWOKAKERO") satırı penceremizin üst bandında yazan etiketi belirliyor. Penceremize verdiğimiz "800x650+200+10" geometri değeri pencerenin 800'e 650 piksel (genişlik 800, yükseklik 650 piksel) boyutlarında olacağını, ekrana yerleştirilirken soldan 200 piksel ve yukarıdan 10 piksel içeriye sol üst köşesi gelecek şekilde yerleştirileceğini belirtiyor. 

Sonrasında pencereye yerleştireceğimiz elemanlar için (kontroller) frame adını verdiğimiz bir tkinter.Frame nesnesi tanımlıyoruz. Bu boş bir panel ve arka plan rengini gri yapıyoruz (bg="gray" opsiyonu). master=window opsiyonu ise frame nesnemizin window penceresi içine yerleştirileceğini belirtiyor. width=800 opsiyonu frame genişliğini, height=650 opsiyonu frame yüksekliğini piksel olarak belirliyor. master değerini window vermekle frame nesnemiz pencere içine yerleştirilmez, nasıl yerleşeceğine dair değişik komutlar var. Burada frame.pack() komutu ile frame nesnemizi window içine yerleştiriyoruz. Bu durumda window içine tek yerleştirilen frame olduğu için pencere frame'i tamamen saracaktır. 

Son satır ise tkinter penceresinin gösterilmesi ve kullanıcı etkileşimlerini (mouse tıklama , tuş basma vs) takip etmesi için sürekli dönmesini sağlayan window.mainloop() komutu (yani ana döngü başlasın). 

Bu kodu çalıştırdığımızda şöyle bir pencere açılır,

Pencereyi boyutlandırmaya kalkarsak frame nesnemizin pencere içine yerleştirilmiş ayrı bir nesne olduğunu görürüz. 

Görselimize 4 tane buton ekleyip çeşitli oyun zorluklarından birinde oyunu başlatması için kullanıcıya imkan verelim. Buton yazılarında kullanılacak font'u değiştireceğimiz için ilk önce tkinter.font kütüphanesini de programa eklemek zorundayız. En üste ekleyelim

import tkinter as tk
import tkinter.font as font

window = tk.Tk()
...

tkinter.font kütüphanesini kısaca font adıyla programa dahil ettik. Şimdi butonları ekleyebiliriz.

frame = tk.Frame(master=window, bg="gray", width=800, height=650)

bFont = font.Font(size=16, weight="bold")
bKolay10_10 = tk.Button(master=frame, text="10 x 10 Kolay",
    font=bFont, bg="lime")
bKolay10_10.place(x=25, y=25, width=160, height=40)

bOrta10_10 = tk.Button(master=frame, text="10 x 10 Orta",
    font=bFont, bg="yellow")
bOrta10_10.place(x=215, y=25, width=160, height=40)

bOrta15_15 = tk.Button(master=frame, text="15 x 15 Orta",
    font=bFont, bg="yellow")
bOrta15_15.place(x=405, y=25, width=160, height=40)

bZor15_15 = tk.Button(master=frame, text="15 x 15 Zor",
    font=bFont, bg="red")
bZor15_15.place(x=595, y=25, width=160, height=40)

frame.pack()

Butonlar master opsiyonu ile frame nesnesi içinde yerleşeceği belirtiliyor. En üstte bFont nesnesi ile butonlarda kullanılacak büyük ve koyu bir yazı belirliyoruz. size opsiyonu boyutunu , weight opsiyonu ise yazının koyuluk derecesini belirtiyor. Daha sonra butonları tanımlarken bu bFont nesnesini font opsiyonlarına değer olarak veriyoruz. Butonların text opsiyonu üzerlerinde yazmasını istediğimiz yazıyı belirtiyor. bg opsiyonu ise arka plan rengi (back ground).

Butonların boyutlandırmasını ise yerleştirirken yapıyoruz. Bu sefer pack() metodu değil place() metodu kullanarak butonları frame üzerinde istediğimiz koordinatlara yerleştiriyoruz. Aynı pencereyi ekrana yerleştirirken olduğu gibi eleman yerleştirirken de sol üst köşesinin nerede olacağını belirtiyoruz. x opsiyonu butonumuzun frame sol kenarından kaç piksel içeride olacağını , y opsiyonu da frame üst kenarından ne kadar aşağıda olacağını belirliyor. width opsiyonu butonun kaç piksel genişlik olarak yerleştirileceğini, height opsiyonu da kaç piksel yüksekliğe sahip olarak yerleştirileceğini belirliyor. 

Programı çalıştırıp görelim

Pencere üst tarafına yakışıklı 4 buton geldi. Butonlar şimdilik bir iş yapmıyor, birazdan iş yapmaya başlarlar. Ama önce bir tane de oyunumuzu oynayacağımız panel yerleştirelim pencereye.

...
gameFrame = tk.Frame(master=frame, width=32*10, height=32*10, bg="white")

gameFrame.place(x=25, y=95)

frame.pack()

Genişliği ve yüksekliği verirken 32x10 şeklinde matematik işlem yapmamın sebebi var. 10'a 10 ya da 15'e 15 oyun panellerimiz olacak. Panelde düşündüğüm her hücre 32 x 32 piksel olursa panel nasıl görünür diye kontrol ettim. 10'a 10 panelle pencere şöyle görünüyor,



Tkinter.Button Tıklama Olayının İşlenmesi

Öncelikle buraya kadar görseli yaptık ve bundan sonra çalışma mantığına geçeceğiz. Bu noktada kodu iki parçaya ayırmak istedim. Oyunun çalışması için yazacağım metodları Funcs.py adında ayrı bir dosyada toplamaya karar verdim. İlk başta bu dosyayı ana programda import etmem gerekiyor. En başa,

hashiwokakero.py
import tkinter as tk
import tkinter.font as font
import Funcs
...

Şimdi Funcs.py dosyamızı ekleyelim ve içine butonlarımız tıklanınca çalışmasını beklediğimiz metodları yazalım.

Funcs.py
def bKolay10_10_Click():
    print("10 x 10 Kolay")

def bOrta10_10_Click():
    print("10 x 10 Orta")

def bOrta15_15_Click():
    print("15 x 15 Orta")

def bZor15_15_Click():
    print("15 x 15 Zor")

Şimdilik sadece metodların çağrıldığını görelim yeter. Ana programda buton kodlarına bu metodları çağıran opsiyonları ekleyelim

hashiwokakero.py
bKolay10_10 = tk.Button(master=frame, text="10 x 10 Kolay",
    font=bFont, bg="lime", command=Funcs.bKolay10_10_Click)
...
bOrta10_10 = tk.Button(master=frame, text="10 x 10 Orta",
    font=bFont, bg="yellow", command=Funcs.bOrta10_10_Click)
...
bOrta15_15 = tk.Button(master=frame, text="15 x 15 Orta",
    font=bFont, bg="yellow", command=Funcs.bOrta15_15_Click)
...
bZor15_15 = tk.Button(master=frame, text="15 x 15 Zor",
    font=bFont, bg="red", command=Funcs.bZor15_15_Click)
...

command= opsiyonu ile kısa yoldan buton tıklanması karşılığı çağrılmak istenen metodun adını yazıyoruz. Metod ismini verirken parantezler konmuyor ona dikkat edelim sadece ismini veriyoruz. Eğer parantezleri koyarsak metod çağrılır ve metoddan dönen değer command opsiyonuna değer olarak verilir. Ki bu da işe yaramaz. Şimdi çalıştırıp butonlara tıklarsak konsolda mesajları göreceğiz.

Ekşın başladı. Şimdi kafayı kaldırıp ne yapacağımızı hayal etme zamanı. Programı tasarlarken bu noktada yanlış yola bir girerseniz, çok başınız ağrır. Düşündüm taşındım, oyuna başlarken çözülecek oyunun bilgilerini dosyalardan okuyarak tahtayı çizeyim diye karar verdim. Mesela şöyle bir tahta için 

Bir text dosya hazırlayayım içinde her noktanın satır , sütun numaraları ve içindeki sayı değerini her biri bir satırda virgülle ayrılmış olarak yazayım.

0,0,2
0,9,2
2,0,3
2,8,1
4,1,2
4,4,3
4,9,5
6,0,2
6,2,3
6,7,2
8,0,1
8,2,3
8,9,4
9,1,3
9,5,4
9,7,2

Satır=0 sütun=0 da içinde 2 yazan bir nokta. satır=6, sütun=7 de içinde 2 yazan bir nokta vs. Bu amaçla her zorluktan dosyalar hazırladım. Siz de dosyaları buradan indirebilirsiniz. Dosyaların hepsini maps klasörü içinde yerleştirirseniz benimle aynı yoldan gidersiniz.

İlk butonun çalışması ile başlayalım. Öncelikle map dosyası içinden bilgileri okuyup hafızada global bir array değişken içine saklayalım. Bu amaçla ReadMap() adında yeni bir metod tanımlamaya karar verdim. Nasılsa tüm butonlar aynı metodu çağıracak. Bilgileri saklamak için Map adında bir global array değişkeni tanımlayalım. 

Map = []  #[0]: Satır, [1]: Sütun, [2]: Sayı

def bKolay10_10_Click():
...

Ve şimdi verilen dosya ismindeki dosyayı açıp Map global değişkenine yazan ReadMap() metodu,

Funcs.py
# Verilen isimdeki dosyadan oyun tablasını okur
def ReadMap(fname):
    global Map
    Map.clear()
    f = open(fname, "r")
    for line in f.readlines():
        sayılar = line.split(",")
        Map.append([int(sayılar[0]), int(sayılar[1]), int(sayılar[2])])

İlk satırda global Map değişkenini kullanacağımızı deklare ediyoruz. Sonra Map array içinde herhangi bir eleman varsa hepsini siliyoruz. f = open(fname, "r") satırı ile map dosyası okuma amaçlı açılıyor. Dosyanın her satırı line değişkenine tek tek okunarak döngü yapıyoruz. Satırdaki yazıyı virgülleri kullanarak split() metodu ile ayırarak her bir sayıyı sayılar array içine alıyoruz. Mesela satırda "0,2,5" yazıyorsa sayılar[0] = "0" , sayılar[1] = "2" ve sayılar[3] = "5" olacaktır.

Tabi bu değerler string değerler. Ben bu değerleri programımda hep integer sayılar olarak kullanacağım için son satırda değerleri Map değişkenine eklerken string'den integer'a çeviriyorum. 

Şimdi buton metodumuzda bir örnekle çalıştıralım bunu ve görelim.

def bKolay10_10_Click():
    global Map
    fname = "maps/fKolay10_10_01.map"
    ReadMap(fname)
    print(Map)

Çalıştırırsak,

İstediğimiz oldu. Global Map değişkeninde bilgilerimiz yaşıyor. Sırada oyun tablasına bu noktaları çizmek var. Tüm çizim işlerini falan Funcs.py içindeki metodlarda yapmayı düşündüğüm için buraya da tkinter kütüphanesini import etmeliyim. Dosyanın en başına şunları ekleyelim. 

import tkinter as tk
import tkinter.font as font

Map = []  #[0]: Satır, [1]: Sütun, [2]: Sayı
...

Bu arada oyun tablası olarak kullandığım gameFrame panelini de buton metodlarına gönderip Funcs.py içine almam gerekiyor. Bu amaçla metodları parametre ile çağırmam gerekiyor. Önce buton metodu,

def bKolay10_10_Click(frame):
    global Map, Frame
    Frame = frame
    fname = "maps/fKolay10_10_01.map"
    ReadMap(fname)

Ana programdan gönderilen frame nesnesini Frame adında bir global değişkene atıyorum. Ancak hatırlarsak ana programda buton metodlarını çağırırken parantezleri kullanmamak gerekiyordu. Parametreli metod çağırabilmek için orada lambda yapısını kullanmak gerekir.

bKolay10_10 = tk.Button(master=frame, text="10 x 10 Kolay",
    font=bFont, bg="lime",                                      
    command=lambda: Funcs.bKolay10_10_Click(gameFrame))

Aslında bu lambda yapısını anlatmak için olayı önce şöyle hayal edelim,

def geçici():
    Funcs.bKolay10_10_Click(gameFrame)
bKolay10_10 = tk.Button(master=frame, text="10 x 10 Kolay",
    font=bFont, bg="lime",                                      
    command=geçici)

Önce parametresiz olan ama içinde parametreli olan buton metodumuzu çağıran geçici bir metod tanımlıyoruz ve sonra bu metodu butonun command= opsiyonuna rahatça yazabiliyoruz. lambda yapısı da arka planda aynen böyle bir şey yapıyor.

Sırada verilen verilerle oyun tablasını çizen DrawGrid() metodunun tanımlaması var.

# Oyun tablasını çizer
def DrawGrid(frame, size):
    global Size, Frame, Map
    Frame = frame
    Size = size
    for child in frame.winfo_children():
        child.destroy()
    c = tk.Canvas(master=frame, width=size*32, height=size*32, bg="white")
    c.pack()

Globale bir de Size değişkeni ekledik, burada da tablanın boyutunu saklayacağız. Metoda gelen frame ve size değerlerini sırayla Frame ve Size global değişkenlerine atarak başlıyoruz, lazım olacaklar ileride. Sonra frame.winfo_children() ile oyun tablası içindeki tüm kontrollerin listesini alıp hepsini yok ediyoruz. Bu metod her çağrıldığında o anki verilere göre oyun tablasını baştan çizecek. 

Sırada çizmeye başlamak var ilk önce frame içini tamamen kaplayacak bir Canvas nesnesi yerleştiriyoruz. Şimdi gelelim Map değişkeni içinde bulunan verilerle tabladaki noktaları çizmeye,

...
    c.pack()

    for el in Map: # noktaları çiz
        x1 = el[1] * 32
        y1 = el[0] * 32
        c.create_oval(x1+3,y1+3,x1+29,y1+29, outline="blue", fill="white", width=3)
        c.create_text(x1+16, y1+16, text=str(el[2]), fill="black",
font=('Helvetica 15 bold'))

Çizeceğimiz kutunun sol üst köşesini buluyoruz önce. el[1] içinde bildiğimiz gibi sütun değeri var, bunu 32 ile çarparsak frame sol kenarından kaç piksel içeride olacağımızı buluruz. el[0] içinde de satır değeri var, bunu da 32 ile çarparsak frame üst kenarından kaç piksel aşağıda olacağımızı buluruz. Tüm çalışmam boyunca x ile anılan değerlerin sütunlara y ile anılan değerlerin satırlara karşı gelmesi yüzünden sürekli kafam karıştı. 

Neyse , kutu içine önce bir yuvarlak çiziyoruz. 32'ye 32 piksel bir kutu içindeyiz ve her yandan 3 piksel boşluk kalan bir daire çiziyoruz. outline= opsiyonu ile daire dış çizgisini mavi yapıyoruz. fill= opsiyonu ile daire içinin rengini beyaz olarak belirliyoruz. width= opsiyonu ile de daire dış çizgi genişliğini 3 piksel yapıyoruz.

Sonrasında create_text() metodu ile daire ortasına gelecek şekilde sayıyı yazıyoruz. Burada yeni olan şey font= opsiyonuna nasıl değer verildiği. Buton yazılarında boşuna mı font nesnesi ürettik acaba? Bu şekil de yazılabiliyormuş.

Şimdi buton metodundan DrawGrid() metodumuzu çağıralım da çizdiğini görelim.

def bKolay10_10_Click(frame):
    global Map, Frame
    Frame = frame
    fname = "maps/fKolay10_10_01.map"
    ReadMap(fname)
    DrawGrid(frame, 10)

Çalıştıralım

Yaşasın, mutlu son. Şimdi diğer butonlar için de aynısını yapalım. Önce ana programdan command= opsiyonları,

bOrta10_10 = tk.Button(master=frame, text="10 x 10 Orta",
    font=bFont, bg="yellow",
    command=lambda: Funcs.bOrta10_10_Click(gameFrame))
bOrta10_10.place(x=215, y=25, width=160, height=40)

bOrta15_15 = tk.Button(master=frame, text="15 x 15 Orta",
    font=bFont, bg="yellow",
    command=lambda: Funcs.bOrta15_15_Click(gameFrame))
bOrta15_15.place(x=405, y=25, width=160, height=40)

bZor15_15 = tk.Button(master=frame, text="15 x 15 Zor",
    font=bFont, bg="red",
    command=lambda: Funcs.bZor15_15_Click(gameFrame))
bZor15_15.place(x=595, y=25, width=160, height=40)

Şimdi de buton metodları, tabi hepsi kendi dosyalarına göre.

def bOrta10_10_Click(frame):
    global Map, Frame
    Frame = frame
    fname = "maps/fOrta10_10_01.map"
    ReadMap(fname)
    DrawGrid(frame, 10)

def bOrta15_15_Click(frame):
    global Map, Frame
    Frame = frame
    fname = "maps/fOrta15_15_01.map"
    ReadMap(fname)
    DrawGrid(frame, 15)

def bZor15_15_Click(frame):
    global Map, Frame
    Frame = frame
    fname = "maps/fZor15_15_01.map"
    ReadMap(fname)
    DrawGrid(frame, 15)

Bütün butonları test edip oyun tablalarının çizildiğini görelim. 

Sırada noktalar arasındaki köprü çizgilerinin çizilmesi var. Yine plan zamanı. 





Tkinter.Canvas Üzerine Çizgilerin Çizilmesi

Oyun oynama şekli olarak bir yerde gördüğümü yapacağım. Noktalardan biri üzerinde mouse basar ve nokta dışına sürüklerseniz o yönde bir çizgi çiziyordu. Çok hızlı çalışan bir etkileşim olduğu için beğendim ve bunu uygulamaya karar verdim. Peki bu şekilde üreteceğim çizgileri veri olarak nasıl saklamalıyım. Her bir çizgiyi 5 elemandan oluşan bir array içine koymaya karar verdim. Elemanlar şu bilgileri saklayacak:

  • line[0] : Çizginin başladığı satır
  • line[1] : Çizginin başladığı sütun
  • line[2] : Çizginin hangi yöne çizileceği, 0: Kuzey, 1:Doğu, 2: Güney, 3: Batı
  • line[3] : Çizginin uzunluğu (satır ya da sütun olarak)
  • line[4] : Kaç çizgi var. Bildiğimiz üzere Hashiwokakero köprülerinde 1 ya da 2 çizgi olabilir sadece.

Örnek olarak 10 x 10 Kolay tablamıza bakalım ve burada çizgiler hayal edelim. 

  • 1 nolu çizgi satır 2 , sütun 1 den başlıyor. Satır ve sütun sayıları sıfırdan başlıyor unutmayalım. Sağa yani Doğu'ya gidiyor, 7 sütun uzunlukta ve tek çizgi. Bunun için saklanan veri [2, 1, 1, 7, 1] olacaktır.
  • 2 nolu çizgi satır 4 , sütun 3'ten başlıyor , Batı'ya doğru , 2 sütun uzunlukta ve 2 çizgili. Bunun veri olarak ifadesi de [4, 3, 3, 2, 2] olacaktır.
  • 3 nolu çizgi satır 7, sütun 7'den başlıyor, Güney'e doğru , 2 satır uzunlukta ve 2 çizgili. Bu da veri olarak [7, 7, 2, 2, 2] olacaktır.
  • 4 nolu çizgi satır 7, sütun 9'dan başlıyor , Kuzey'e doğru , 3 satır uzunlukta ve tek çizgili. Veri olarak karşılığı [7, 9, 0, 3, 1] olacaktır.

Bu bilgileri Lines adında global bir değişken içinde test verisi olarak saklayalım. Bu işi ReadMap metodu içinde yapalım. 

Map = []  #[0]: Satır, [1]: Sütun, [2]: Sayı
Lines = []  # [0]: Satır, [1]: Sütun, [2]: Yön,
            # [3]: Uzunluk, [4]: Çizgi sayısı

...
def ReadMap(fname):
    global Map, Lines
    Map.clear()
    Lines.clear()
    Lines.append([2,1,1,7,1])
    Lines.append([4,3,3,2,2])
    Lines.append([7,7,2,2,2])
    Lines.append([7,9,0,3,1])
    f = open(fname, "r")
...

Önce eskiden kalan bir veri olmasın diye clear() metodu ile array içindeki bilgileri sıfırlıyoruz. Daha sonra örnek çizgi bilgilerimizi append() metodu ile ekliyoruz. Sırada ReadMap() metodumuzdan sonra çağrılan DrawGrid() metodunda bu bilgilere göre çizgileri Canvas nesnesine ekleyecek kodlar var.

def DrawGrid(frame, size):
    global Size, Frame, Map, Lines
...
        c.create_text(x1+16, y1+16, text=str(el[2]), fill="black",
font=('Helvetica 15 bold'))
   
    for line in Lines:              #çizgi çizen döngü
        x1, x2, y1, y2 = 0, 0, 0, 0 # çizginin başlama ve bitiş koordinatları
        if line[2] == 0:            # Yön Kuzey
            x1 = line[1] * 32 + 16
            x2 = x1
            y1 = line[0] * 32 + 34
            y2 = y1 - line[3] * 32 - 3
        elif line[2] == 1:          # Yön Doğu
            x1 = line[1] * 32 - 1
            x2 = x1 + line[3] * 32 + 3
            y1 = line[0] * 32 + 16
            y2 = y1
        elif line[2] == 2:          # Yön Güney
            x1 = line[1] * 32 + 16
            x2 = x1
            y1 = line[0] * 32 - 1
            y2 = y1 + line[3] * 32 + 3
        elif line[2] == 3:          # Yön Batı
            x1 = line[1] * 32 + 34
            x2 = x1 - line[3] * 32 - 3
            y1 = line[0] * 32 + 16
            y2 = y1
       
        if line[4] == 1:  # tek çizgi
            c.create_line(x1, y1, x2, y2, width=3)

Önce en başta global Lines değişkenini kullanacağımızı ekliyoruz. Noktaların çizimi bittiği yerden bir döngü ile çizgilerin çizimini ekleyeceğiz. Çizginin başladığı koordinatlar soldan x1 kadar içerde ve yukarıdan y1 kadar içeride olacak. Benzer şekilde çizginin bittiği noktanın koordinatlarını da x2 ve y2 değişkenleri ile ifade edeceğiz. Karşılaştırmalarda ise yön bilgisine göre x1, x2, y1, y2 değerleri hesaplanıyor.

Örneğin Kuzeye giden çizgi için [7, 9, 0, 3, 1]9 verisi var.. Sütun sayısı çarpı 32 kutunun sol kenarı çizgi dikey olacağından kutunun ortasında olması için üstüne 16 ekliyoruz. x1 ve x2 değerleri dikey çizgi olacağından aynı olacaktır. Çizgi yukarı kenardan satır sayısı çarpı 32 aşağıdaki kutuda olacak ama bu değer kutunun üst kenarı, alt kenarından başlamak için bu değere bir 32 daha eklemek gerekir. Kodda 34 ekleniyor çünkü yuvarlağın kenarından başlaması için çizgiyi 2 piksel daha aşağıdan başlatmak lazım bu değer y1. Uzunluk 3 kutu olduğuna göre ve yukarı doğru gidileceği için y1 değerinden 3 çarpı 32 yukarı gideceğiz. Yine tam daire kenarından başlasın diye çizgiyi 3 piksel daha yukarıda bitiriyoruz. 

En altta sadece tek çizgi olanlar için kod yazdım çünkü 2 çizgi olunca bir takla daha atmak ve çizgileri tam ortadan değil aralarında açıklık bırakarak çizmek gerekiyor. 

İkinci tek çizgilik değer [2, 1, 1, 7, 1] değeri.. Doğuya giden bir çizgi. Çizginin başı sol taraftan sütun sayısı çarpı 32 kadar içerde olacak. Yine daire kenarına yapışsın diye değerden 1 çıkardım bu değer x1. Bu çıkarma ve eklemeler tabiki kodu çalıştırınca gördüğüm boşlukları yok etmek için sonradan kondu. x2 değeri ise x1 değerinden uzunluk çarpı 32 kadar sonra, çizginin bitiş noktası olacak. Ona da 3 ekleyerek daire kenarına yapışmasını sağladım.  Yatay çizgi olduğu için y değerleri aynı olacak. Üst kenardan satır sayısı çarpı 32 aşağıda kutunun üstü var ortasına gelmek için 16 daha eklemeliyiz.

Sadece tek çizgileri çizeceği iin programı bu haliyle çalıştırırsak 10 x 10 Kolay butonu tıklayınca şöyle görünecektir.

Sıra geldi çift çizgilere. Bu amaçla yazdığım kodu vereyim önce.

...
        if line[4] == 1:  # tek çizgi
            c.create_line(x1, y1, x2, y2, width=3)
        else: # çift çizgi
            xDelta, yDelta = 0, 0
            if line[2] == 0 or line[2] == 2: #dikey çizgi
                xDelta = 4
            else:                            #yatay çizgi
                yDelta = 4
            c.create_line(x1-xDelta, y1-yDelta, x2-xDelta, y2-yDelta, width=3) # çizgi 1
            c.create_line(x1+xDelta, y1+yDelta, x2+xDelta, y2+yDelta, width=3) # çizgi 2

xDelta yatay olarak sağa-sola kayma miktarı ve yDelta dikey olarak yukarı-aşağı kayma miktarı. Çizgi dikeyse yatay olarak kaymalar olacak, yataysa dikey olarak kaymalar olacak. Kodda görüldüğü üzere ortadan 4 piksel kaydırma yaptım. En son 2 satırda ise bu kayma değerlerini artı ve eksi alarak 2 çizgiyi tablaya ekledim. Şimdi çalıştırırsak çift çizgili olan köprüler de görünecektir.

Al sana bir mutlu son daha. Bir şeyler çıkmaya başladı. 





Tkinter.Canvas Üzerinde Mouse Hareketi Algılama

Şunu düşündüğümü söylemiştim: Diyelim bir noktadan sağa doğru çizgi çizmesini istiyorum. Noktanın üzerinde mousa basarım, basılı şekilde sürükler sağa doğru nokta dışında bir yerde bırakırım. Program sağa doğru bir çizgi çizmek istediğimi anlar. 

Bu eylemleri yaptığını nasıl anlarım? Mouse düğmesine basıldığı andaki koordinatları alırım, sonra da bıraktığı andaki koordinatları alırım. Bunları analiz ederek hareket edilmiş mi, edildiyse ne yöne gidilmiş hesaplayabilirim. Burada bizim için önemli 2 an var. Düğmeye basıldığı an ve düğmenin bırakıldığı an. Sol mouse düğmesine basılan an Tkinter olaylarında <Button-1> olayı olarak isimlendirilir, Düğmenin bırakıldığı an ise <ButtonRelease-1> olayıdır. Tüm Canvas çizimi üzerinde algılamayı yapacağımız için bu olayları Canvas nesnemize tanımlamamız gerekiyor. 

def DrawGrid(frame, size):
...
    c = tk.Canvas(master=frame, width=size*32, height=size*32, bg="white")
    c.bind("<Button-1>", pPressed)
    c.bind("<ButtonRelease-1>", pReleased)
    c.pack()
...

bind() metodu kontrollerimizi olay metodlarına bağlamak için kullanılır. pPressed() ve pReleased() metodlarımızı henüz tanımlamadık. Onlara da şimdilik sadece çalışmayı görmek için bir şeyler yazalım.

# Mouse butona basılınca
def pPressed(olay):
    print("Basıldı")

# Mouse butonu bırakılınca
def pReleased(olay):
    print("Bırakıldı")

bind() metodu ile bağlanan olay metodlarının bir parametre alması gerekiyor. Bu parametreye biz olay adını verdik çünkü bu parametre üzerinden bize gerçekleşen olay ile ilgili bilgiler geliyor. Bu kadarıyla test edersek , oyun tablası üzerinde mouse sol butonu tıkladığımızda "Basıldı", buton bırakılınca da "Bırakıldı" mesajları konsola yazılacaktır. Tabla dışında metodlar çalışmaz çünkü Canvas nesnesine bağlandılar. 

İlk işimiz butona basılan ve butonun bırakıldığı koordinatları kaydetmek. 

...
Lines = []  # [0]: Satır, [1]: Sütun, [2]: Yön,
            # [3]: Uzunluk, [4]: Çizgi sayısı
StartX = 0
StartY = 0
EndX = 0
EndY = 0
...

# Mouse butona basılınca
def pPressed(olay):
    global StartX, StartY
    StartX = olay.x
    StartY = olay.y

# Mouse butonu bırakılınca
def pReleased(olay):
    global EndX, EndY
    EndX = olay.x
    EndY = olay.y
    print("Basıldı:", StartX, StartY)
    print("Bırakıldı", EndX, EndY)

Global StartX, StartY, EndX, EndY değişkenlerine olayların gerçekleştiği anlardaki mouse koordinatlarını kaydediyoruz.Aslında End'lere gerek yoktu global olsun. Çünkü büyük ihtimal pReleased() metodu içinde işimiz bitecek ve dışarıda lazım olmayacak. Neyse devam edelim. olay parametresi ile sistemin bize gönderdiği bilgi işimize yarıyor. olay nesnesi x özelliği o anda mouse pointerin x koordinatını y özelliği de y kordinatını saklıyor. Çalıştırırsak konsola şuna benzer bir şeyler yazacak,

Gelen bilgiler piksel olarak değerler. Bizim pek işimize yaramaz , bunlara karşı gelen satır-sütun bilgileri bizim Map tablosu verileriyle çalışırken kullanabileceğimiz veriler. pReleased() metodumuza devam etmeden önce bize verilen piksel değerleri satır-sütun değerlere dönüştüren bir getRowCol() metodu tanımlayalım.

# verilen x,y pozisyonuna karşı gelen satır-sütun değerleri
def getRowCol(posX, posY):
    row = int(posY / 32)
    col = int(posX / 32)
    return row, col

Tamsayı olarak verilen piksel değerleri 32'ye bölerek elde ettiğimiz satır ve sütun değerlerini bir veri çifti olarak metoddan geri döndürüyoruz. 

Şimdi bir daha toplayalım. Olay noktalarını satır sütuna dönüştürüp, ilk önce başlangıç noktası oyun tablası üzerinde içinde sayı olan noktalardan biri mi? onu bileceğiz. Sonra da hareketin yönüne göre çizgi çizilmesi taleplerinde bulunacağız. Çizgi talep edilmesi yeterli değil, acaba başka bir çizgi ile kesişiyor mu? ya da çizeceğimiz yönde gidebileceğimiz başka bir nokta var mı? 

Biz pReleased() metodundan sadece çizgi çizilmesi taleplerini çıkaralım, ötesini başka metodlar içinde yaparız. 

def pReleased(olay):
    global EndX, EndY
    EndX = olay.x
    EndY = olay.y
    startRowCol = getRowCol(StartX, StartY)
    endRowCol = getRowCol(EndX, EndY)
    deltaX = endRowCol[1] - startRowCol[1]
    deltaY = endRowCol[0] - startRowCol[0]

    # başlangıç noktasını kontrol et
    p = [m for m in Map if m[1] == startRowCol[1] and m[0] == startRowCol[0]]
    if p:
        print(p)

deltaX içinde eylemin bittiği andaki sütun değerinden başladığı andaki sütun değerini çıkararak yatay olarak oyun tablasında kaç kutu hareket yapıldığını buluyoruz. Bu değer artı ise sağa doğru, eksi ise sola doğru gidilmiştir. deltaY içinde de dikey olarak kaç kutu hareket edildiğini buluyoruz. Bu değerde artı ise aşağıya , eksi ise yukarıya gidilmiştir. 

m for m in Map if ... satırı Python ile böyle kısacık ama başka dillerde aynı şey için baya bir takla atmak gerekiyor. Şunu demek oluyor, m değişkeni olsun ve bu m içine Map içindeki tüm nokta bilgileri teker teker alınsın ve eğer m[1] yani noktanın sütun değeri start kutusu sütun değeri ile aynıysa ve m[0] yani noktanın satır değeri start kutusu satır değeri ile aynıysa o nokta bilgisini dışarı ver. if sonrası verilen kritere uyan tüm noktalar bir array içinde p değişkenine yazılacaktır. Tabi ki tam bir eşitlik istediğimiz için eğer başlangıç noktasında Map içinde bir nokta varsa o noktayı içeren tek bir değer olan array dönecektir. if p: ise eğer sonuçta bir şey gelmişse yani başlangıç kutusunda bir nokta tanımlıysa altındaki bloğu çalıştıracaktır. 

Şimdi çalıştıralım ve görelim

Sadece mouse butonu tıkladığımızda altında bir nokta varsa değer gelecek, başka boş bir yerde tıklarsak değer gelmeyecektir. Şimdi hareketin ne yönde yapıldığına bakalım.

    # başlangıç noktasını kontrol et
    p = [m for m in Map if m[1] == startRowCol[1] and m[0] == startRowCol[0]]
    if p:
        if abs(deltaX) > abs(deltaY): #yatay çizgi istendi
            if deltaX > 0:
                print("Sağa")
            else:
                print("Sola")
        elif abs(deltaY) > abs(deltaX): #dikey çizgi istendi
            if deltaY > 0:
                print("Aşağıya")
            else:
                print("Yukarıya")

Eğer büyüklük olarak (mutlak değer) deltaX değeri deltaY değerinden büyükse yatay bir hareket yapılmış demektir. Eğer deltaY mutlak değeri deltaX mutlak değerinden büyükse dikey olarak hareket yapılmış demektir. Yatay ve deltaX sıfırdan büyükse sağa doğru hareket edilmiştir, küçükse sola doğru hareket edilmiştir. Dikey ve deltaY değeri sıfırdan büyükse aşağıya doğru hareket edilmiştir küçükse yukarıya doğru hareket edilmiştir. Çalıştırıp her yönü bir test edelim.

Buraya kadar geldiyseniz sizin de baya bu programı yapmaya niyetiniz var demektir, hem tebrik hem teşekkür ederim. 




Mouse Hareketlerine Göre Çizgi Eklenmesi

Çizgileri çizen rutinler bu uygulamanın en karmaşık rutinleri. Bu rutinler aslında çizgi çizmeyecek Lines array üzerindeki verilerle oynayacak, çizgileri zaten DrawGrid() metodu çiziyor. Verilerde gereken oynamaları yapıp DrawGrid() metodunu çağırırız olur biter. Dört yöne çizgi çizmek için 4 metod hazırlıyoruz

def LineToDown(row, col):
    global Lines, Map

def LineToLeft(row, col):
    global Lines, Map

def LineToRight(row, col):
    global Lines, Map

def LineToUp(row, col):
    global Lines, Map

İsimlerin harf sırasına göre sıraladım. Metodlar çizginin başlangıç noktası olan kutunun satır ve sütun numaralarını parametre olarak alıyor (sırasıyla row ve col). Dikkat edilmesi gereken nokta çizginin başladığı kutunun satır ve sütun değerleri, başlangıcın dayandığı Map noktasının değil. Şimdi istekleri bu metodlara yönlendirelim. 

def pReleased(olay):
    ...
    if p:
        if abs(deltaX) > abs(deltaY): #yatay çizgi istendi
            if deltaX > 0:
                LineToRight(startRowCol[0], startRowCol[1] + 1)
            else:
                LineToLeft(startRowCol[0], startRowCol[1] - 1)
        elif abs(deltaY) > abs(deltaX): #dikey çizgi istendi
            if deltaY > 0:
                LineToDown(startRowCol[0] + 1, startRowCol[1])
            else:
                LineToUp(startRowCol[0] - 1, startRowCol[1])

Sağa çizgi için row değeri Map noktasının satır değeri ama col değeri Map noktası sütun değerinden bir fazlası çünkü çizgi sağ tarafından başlayacak. Sola çizerken de sütun değerinin bir eksiği veriliyor çünkü çizgi soldaki kutudan başlayacak. Benzer şekilde aşağı çizerken Map noktası satır değerinin bir fazlası, yukarı çizerken bir eksiği veriliyor. 

En üstteki metoddan başlayalım, LineToDown(). Olasılıkların kolay olanından başlayalım. Eğer aynı yerde zaten bir çizgi varsa. Bunu deneyebilmek için Lines array test değerlerini şöyle değiştirelim.

def ReadMap(fname):
    global Map, Lines
    Map.clear()
    Lines.clear()
    Lines.append([1,0,2,1,1])
    Lines.append([3,9,0,3,1])
    f = open(fname, "r")
...

Çizgilerin ikisi de dikey ama biri aşağıya doğru diğeri yukarıya doğru çizilmiş. Bunların her ikisi de bizim için aynı şeyi ifade edecek. Orada bir çizgi var!. Çalıştırırsak tablamız şöyle görünecek,

Bir çizgi varsa ve tek çizgiyse çift çizgi olacak ama çift çizgiyse de oradan silinecek. LineToDown() metodumuzu şöyle düzenleyelim.

def LineToDown(row, col):
    global Lines, Map
    # aynı yönde çizgi var mı kontrol
    line = [i for i in Lines if (i[0] == row and i[1] == col and i[2] == 2) or\
        (i[0] == row + i[3] - 1 and i[1] == col and i[2] == 0)]
    if line:
        if line[0][4] == 1:
            line[0][4] = 2
        else:
            Lines.remove(line[0])

    DrawGrid(Frame, Size)

Yine yakışıklı bir i for i in Lines if... sorgu satırı. Çizgilerin içinden aynı row ve col değerlerinde başlangıç noktası olan ve aşağı doğru yönü olanları yani tam çizmek istenilen çizgi ile aynı veriye sahip olanları buluyor. Bir de yukarıya doğru olan ama bittiği nokta bizim başlangıç noktanız olan çizgileri buluyor. Bu tersine olan çizgileri nasıl buluyor, çizgi aynı sütunda olacak ve yönü yukarı olacak ama başlangıç noktası çizgi boyu kadar aşağıda olacak. Niye -1 var? Sağdaki çizgiye bakalım, row değerimiz 1 ve baktığımız çizginin uzunluğu 3 kutu. row ve uzunluğu toplarsak 4 ediyor ama bizim çizgi 3 değerli satırdan başlıyor. Şimdi programı çalıştırırsak örnekte soldaki ve sağdaki çizgiler için şu şekilde line değerleri oluşacaktır:

Aynı daha önce Map değerlerinde sorgularken olduğu gibi array içinde array olarak geliyor değerler. Bu yüzden çizgimizin değerleri line[0] elemanı içinde array olarak duruyor. Bu yüzden if line[0][4] == 1: şeklinde sorguluyoruz. Eğer 1 çizgi varsa onu 2 çizgi yapıyoruz , yok 2 çizgi varsa onu da Lines array içinden siliyoruz. Programı bu haliyle çalıştırırsak çizgilerin üstündeki noktalardan aşağı doğru mouse'u sürükleyip bıraktığımızda , ilkinde tek olan çizgi çift olacak ikinciye yaparsak çizgi yok olacaktır. Tabi ki biz burada Lines içinde çizgi değerleri ile oynuyoruz. Çizgilerin çizilmesi ya da silinmesi için DrawGrid() metodunu çağırmamız gerekir. Bu metodu çağırırken de daha önce globale attığımız değerleriyle Frame ve Size değişkenlerini kullanıyoruz.

Sırada yeni çizgi eklenmesi var. Eğer gitmek istediğimiz yönde çizgi yoksa yeni çizgi ekleyeceğiz. Ama bazı koşullar var. Öncelikle gitmek istediğimiz yönde gidilebilecek bir nokta var mı kontrol edip çizgiyi o noktaya kadar çizeceğiz. Ama bunu yaparken de yolumuz üzerinde başka çizgi ile kesişme olup olmadığını kontrol etmemiz gerekiyor. Bu işler baya bir sancılı olacak. Bir yerden başlamak lazım, sanki kesişme yokmuş gibi sadece karşıda bir nokta varsa oraya çizgi çizelim.

...
    if line:
        if line[0][4] == 1:
            line[0][4] = 2
        else:
            Lines.remove(line[0])
    else: # yeni çizgi ekle
        # aşağıda nokta var mı kontrol edelim
        p = [m for m in Map if m[1] == col and m[0] > row]
        p = sorted(p, key= lambda x: x[0])
        if p:
            Lines.append([row, col, 2, p[0][0] - row, 1])

    DrawGrid(Frame, Size)

Sütun değeri col değeri ile aynı olan ve satır değeri row değerinden büyük olan yani aşağıda aynı sütunda olan tüm noktaları m for m in Map if... satırı ile topluyoruz. sorted(p, key= lambda x: x[0]) komutu ile elde ettiğimiz array içinde array değerleri sıfırıncı eleman değerine göre sıralanır yani satır değerine göre. Böylece en başta bize en yakın nokta değerleri olacaktır, şimdi o noktaya çizgi ekleyeceğiz. row ve col zaten çizgimizin başlama koordinatları. Aşağı doğru gideceğimiz için yön değerini 2 giriyoruz. Çizgimizin uzunluğu karşı noktanın satır değerinden row değerini çıkarınca bulunuyor. En son da çizgi yeni eklendiği için çizgi adeti olarak 1 giriyoruz. 

Şimdi bunu çalıştırıp değişik noktalarda test edip deneyelim.

Gördüğümüz üzere çizgi tek olarak ekleniyor, bir daha istersek çift oluyor ve bir daha istersek siliniyor.

Sıra geldi kesişen çizgi var mı kontrol etmeye. Lines içinde bizim çizgimizi kesecek koordinatlarda başka çizgi var mı bakalım. Öncelikle test için bir yatay çizgi ekleyelim.

def ReadMap(fname):
    global Map, Lines
    Map.clear()
    Lines.clear()
    Lines.append([6,1,1,1,1])
    f = open(fname, "r")
...

Bu çizgiyle kesişen bir dikey çizgi çizmeye kalkarsak üstüne çizecektir.

Aşağıya doğru çizilecek bir çizgiyle kesişme ihtimali olan mevcut çizgileri şu sorgu ile buluruz.

...
        if p:
            # kesişmeleri kontrol et
            ls = [i for i in Lines if ((i[2] == 1 and i[1] <= col and\
                i[1]+i[3]-1 >= col) or\
                (i[2] == 3 and i[1] >= col and (i[1]-i[3]+1) <= col)) and\
                i[0] >= row and i[0] < p[0][0]]
            if not ls:
                Lines.append([row, col, 2, p[0][0] - row, 1])
...

Yatay kesişen çizgi sağdan sola olabilir ya da soldan sağa olabilir. Sorgu ifadesinin son satırı her iki durum için ortak:

i[0] >= row and i[0] < p[0][0]

Yani Kesişecek olan bir çizginin bulunduğu satır (i[0]) çizginin başladığı yukarıdaki kutunun satır değerinden (row) büyük ya da eşit olur. Ayrıca yine kesişecek çizginin bulunduğu satır, aşağıya doğru çizeceğimiz çizginin hedefindeki noktanın satır değerinden (p[0][0]) daha yukarıda olmalıdır, yani küçük olmalıdır.  Böylece mevcut kesişecek çizgilerin satır değerlerine ait kriterimizi belirledik. 

Sırada çizmek istediğimiz çizginin sütun değeri, kesişecek çizgilerin başlama ve bitiş sütun değerleri arasında olur ifadesi var. İlk karşılaştırma

(i[2] == 1 and i[1] <= col and\
                i[1]+i[3]-1 >= col)

Kesişecek çizginin yönü (i[2]) sağa doğru, yani 1 ise ve çizginin başladığı sütun (i[1]) çizeceğimiz çizginin sütunu olan col'dan küçük ya da en fazla eşit olmalı ve kesişen çizginin bittiği sütun (i[1]+i[3]-1) da col değerinden büyük olmalı ya da en azından eşit olmalı. 

İkinci karşılaştırma da sola doğru çizilmiş çizgiler içinde arıyor.

(i[2] == 3 and i[1] >= col and (i[1]-i[3]+1) <= col))

Kesişecek çizgi yönü (i[2]) sola doğru yani 3 olmalı ve kesişecek çizginin başladığı nokta sütun değeri (i[1]) çizeceğimiz çizgi sütun değeri olan col'dan büyük olmalı ya da en azından eşit olmalı ve çizginin bittiği sütun (i[1]-i[3]+1) değeri de col değerinden küçük ya da en fazla eşit olmalı.

En sonda da eğer kesişen çizgi sorgusundan hiç bir çizgi verisi gelmemişse yeni çizgiyi ekliyoruz.

Aşağıya doğru çizgi isteğini işleyen metod burada bitiyor. Şimdi diğer 3 yön için de benzer mantıklarla çalışan kodları veriyorum. Her birini tek tek inceleyin ve bir hata varsa bana bildirin lütfen. 

def LineToLeft(row, col):
    # aynı yönde çizgi var mı kontrol
    line = [i for i in Lines if (i[0] == row and i[1] == col and i[2] == 3) or\
        (i[0] == row and (i[1] + i[3] - 1) == col and i[2] == 1)]
    if line:
        if line[0][4] == 1:
            line[0][4] = 2
        else:
            Lines.remove(line[0])
    else: # yeni çizgi ekle
        # solda nokta var mı kontrol edelim
        p = [m for m in Map if m[1] < col and m[0] == row]
        p = sorted(p, key= lambda x: x[1])
        if p:
            # kesişmeleri kontrol et
            ls = [i for i in Lines if ((i[2] == 0 and i[0] >= row and\
                (i[0]-i[3]+1) <= row) or\
                (i[2] == 2 and i[0] <= row and (i[0]+i[3]-1) >= row)) and\
                i[1] <= col and i[1] > p[-1][1]]
            if not ls:
                Lines.append([row, col, 3, col - p[-1][1], 1])

Bu sola çizme metodu, sadece aşağı ve sola çizerek de oyunu oynayabilirsiniz. Baya bir ortaya çıkmaya başladı.

def LineToRight(row, col):
    # aynı yönde çizgi var mı kontrol
    line = [i for i in Lines if (i[0] == row and i[1] == col and i[2] == 1) or\
        (i[0] == row and (i[1] - i[3] + 1) == col and i[2] == 3)]
    if line:
        if line[0][4] == 1:
            line[0][4] = 2
        else:
            Lines.remove(line[0])
    else: # yeni çizgi ekle
        # sağda nokta var mı kontrol edelim
        p = [m for m in Map if m[1] > col and m[0] == row]
        p = sorted(p, key= lambda x: x[1])
        if p:
            # kesişmeleri kontrol et
            ls = [i for i in Lines if ((i[2] == 0 and i[0] >= row and\
                i[0]-i[3]+1 <= row) or\
                (i[2] == 2 and i[0] <= row and (i[0]+i[3]-1) >= row)) and\
                i[1] >= col and i[1] < p[0][1]]
            if not ls:
                Lines.append([row, col, 1, p[0][1] - col, 1])

    DrawGrid(Frame, Size)

Sağa doğru çizgi talebini işleyen metod ve son olarak

def LineToUp(row, col):
    global Lines, Map
    # aynı yönde çizgi var mı kontrol
    line = [i for i in Lines if (i[0] == row and i[1] == col and i[2] == 0) or\
        (i[0] == row - i[3] + 1 and i[1] == col and i[2] == 2)]
    if line:
        if line[0][4] == 1:
            line[0][4] = 2
        else:
            Lines.remove(line[0])
    else: # yeni çizgi ekle
        # yukarıda nokta var mı kontrol edelim
        p = [m for m in Map if m[1] == col and m[0] < row]
        p = sorted(p, key= lambda x: x[0])
        if p:
            # kesişmeleri kontrol et
            ls = [i for i in Lines if ((i[2] == 1 and i[1] <= col and\
                i[1]+i[3]-1 >= col) or\
                (i[2] == 3 and i[1] >= col and (i[1]-i[3]+1) <= col)) and\
                i[0] <= row and i[0] >= p[-1][0]]
            if not ls:
                Lines.append([row, col, 0, row - p[-1][0], 1])

    DrawGrid(Frame, Size)

Yukarıya doğru çizgi talebini işleyen metod. Hadi şu test verisini de silip oyunumuzu bir oynayalım.

def ReadMap(fname):
    global Map, Lines
    Map.clear()
    Lines.clear()
    #Lines.append([6,1,1,1,1])
...

10 x 10 Kolay butonu ile açtığımız tablanın çözülmüş hali şöyle

İyi de ben oyunun bittiğini tek tek kontrol mü edeceğim?




Hashiwokakero Oyunun Bittiğini Görmek

Oyun tablasında her bir noktayı çizerken o noktaya bağlı çizgi sayısını bulalım ve bağlı çizgi sayısı o nokta içinde yazan sayıya eşitse nokta içini yeşil renk yapalım. Bunu yapacağımız yer DrawGrid() metodu da , ben bu metod içinde verilen verilerle çizim yapılsın istiyorum bu kontroller falan dışında kalsın istiyorum. Bu durumda Map verilerine her nokta için bir de renk değeri eklemem gerekiyor. 

Başlangıçta bunları dosyadan okumama gerek yok en başta hepsi beyaz olacak. Bu yüzden her nokta için beyaz renk olacağını ReadMap() metodundaki döngü içinde yapsam olur.

# Verilen isimdeki dosyadan oyun tablasını okur
def ReadMap(fname):
    global Map, Lines
    Map.clear()
    Lines.clear()
    #Lines.append([6,1,1,1,1])
    f = open(fname, "r")
    for line in f.readlines():
        sayılar = line.split(",")
        Map.append([int(sayılar[0]), int(sayılar[1]), int(sayılar[2]), "white"])

Bu yeni veri bilgisine göre DrawGrid() metoduna da arka plan rengini değişken yapan modifikasyonun yapalım. 

def DrawGrid(frame, size):
    global Size, Frame, Map, Lines
    ....

    for el in Map: # noktaları çiz
        x1 = el[1] * 32
        y1 = el[0] * 32
        c.create_oval(x1+3,y1+3,x1+29,y1+29, outline="blue", fill=el[3], width=3)
        ....

Şimdi mesela ReadMap() içinde 

....
    for line in f.readlines():
        sayılar = line.split(",")
        Map.append([int(sayılar[0]), int(sayılar[1]), int(sayılar[2]), "lime"])

Yazarsak ilk başta tüm noktalar yeşil olarak çizilir. Neyse biz şimdi bir noktanın bağlı çizgilerinin sayısına göre yeşil mi beyaz mı olacağını belirleyen bir metod yazalım.

# verilen noktanın tamamlanmasını kontrol eder
def CheckCompleted(row, col, num):
    totalLines = 0 # noktaya bağlı çizgi toplamı
    clines = [i for i in Lines if\
        (i[0] == row and i[1] == col + 1 and i[2] == 1) or\
        (i[0] == row and i[1] == col + i[3] and i[2] == 3) or\
        (i[0] == row and i[1] == col - 1 and i[2] == 3) or\
        (i[0] == row and i[1] == col - i[3] and i[2] == 1) or\
        (i[1] == col and i[0] == row - 1 and i[2] == 0) or\
        (i[1] == col and i[0] == row - i[3] and i[2] == 2) or\
        (i[1] == col and i[0] == row + 1 and i[2] == 2) or\
        (i[1] == col and i[0] == row + i[3] and i[2] == 0)]
    for line in clines:
        totalLines += line[4]
    if totalLines == num:
        return True
    else:
        return False

Noktaya bağlı olabilecek 4 tane noktadan giden ve 4 tane de noktaya gelen olmak üzere 8 kriterde çizgi olabilir. Bunların hepsinin verilerini clines adında bir değişkende topluyoruz. Sonra da bu çizgilerde verilen çizgi sayısı bilgilerini toplayarak totallines değerini buluyoruz. Eğer toplam bağlı çizgi sayısı metoda gönderilen num sayısına eşitse metod True değer dönüyor, eşit değilse False değer dönüyor.

Bu metodu DrawGrid() metodu içinde noktaları çizerken her noktanın rengini belirlemekte kullanabiliriz. 

....
    for el in Map: # draw points
        if CheckCompleted(el[0], el[1], el[2]):
            el[3] = "lime"
        else:
            if el[3] == "lime":
                el[3] = "white"

        x1 = el[1] * 32
        y1 = el[0] * 32
        c.create_oval(x1+3,y1+3,x1+29,y1+29, outline="blue", fill=el[3], width=3)
        c.create_text(x1+16, y1+16, text=str(el[2]), fill="black",
                font=('Helvetica 15 bold'))
....

Artık oyunu deneyebiliriz. Aşağıda bitime çeyrek kala bir oyun tablası görünüyor.




Python PyGame ile Oyunu Seslendirmek

Bu oyunun biraz sese ihtiyacı var çok sessiz. Çizgi çizerken ya da silerken , oyuna başlarken veya bittiğinde sesler çıkarsın. Bu amaçla bazı sesleri kaydedip sounds adında bir klasör içine koydum. Bu ses dosyalarını şu bağlantıyı kullanarak indirebilirsiniz.

Python ile mp3 dosyası oynatmak için bir sürü kütüphane var ama ben en kolay ve sağlıklı olarak pygame.mixer kullanmaya karar verdim. Bu arada içimden bir ses "yahu madem pygame kullanacaktın niye oyunu pygame ile yazmadın" deyip duruyor. 

İlk önce Funcs.py dosyamızın en başında pygame kütüphanesini import edelim.

import tkinter as tk
import tkinter.font as font
from tkinter import filedialog
import pygame

Map = []  #[0]: Row, [1]: Column, [2]: Number
Lines = []  #[0]: Row, [1]: Column, [2]: Direction, [3]: Length, [4]: Line count
StartX = 0
StartY = 0
EndX = 0
EndY = 0
Size = 5

pygame.mixer.init()
....

Funcs.py ilk yüklendiğinde de pygame.mixer'i başlatıyoruz

İlk sesimiz oyun tablosunu yüklediğimizde olsun. ReadMap() metoduna ilave yapalım.

def ReadMap(fname):
    global Map, Lines
....
    pygame.mixer.music.load("sounds/start.mp3")
    pygame.mixer.music.play()

Yeni çizgi eklenirken ya da tek çizgi yerine çift çizgi yapılırken addline.mp3 dosyasını oynatacağız. 4 yönden sadece birinde eklenmesini burada göstereceğim. Diğer yönleri de siz toparlarsınız.

def LineToDown(row, col):
    ....
    if line:
        if line[0][4] == 1:
            line[0][4] = 2
            pygame.mixer.music.load("sounds/addline.mp3")
            pygame.mixer.music.play()
        else:
            Lines.remove(line[0])
    else: # yeni çizgi ekle
        ....
            if not ls:
                Lines.append([row, col, 2, p[0][0] - row, 1])
                pygame.mixer.music.load("sounds/addline.mp3")
                pygame.mixer.music.play()

    DrawGrid(Frame, Size)

Çizgi silinirken boom.mp3 dosyasını oynatalım patlasın gitsin. 

def LineToDown(row, col):
    ....
    if line:
        if line[0][4] == 1:
            ....
        else:
            Lines.remove(line[0])
            pygame.mixer.music.load("sounds/boom.mp3")
            pygame.mixer.music.play()
....

Geriye bir tek victory.mp3 dosyası kaldı. Tüm noktalar yeşil olduğunda da onu oynatacağız.

def DrawGrid(frame, size):
    global Size, Frame, Map, Lines
    finished = True
    ....
    for el in Map: # draw points
        if CheckCompleted(el[0], el[1], el[2]):
            el[3] = "lime"
        else:
            if el[3] == "lime":
                el[3] = "white"
            finished = False

DrawGrid() metodu başında finished adında bir değişkene önce True değeri veriyoruz. Ama sonra noktaları çizerken bir tane bile nokta yeşil değilse finished değeri False yapılıyor. Metodun en sonunda da bu değer hala True ise tüm noktalar yeşil olmuş demektir, sesi çalabiliriz.

....
            c.create_line(x1+xDelta, y1+yDelta, x2+xDelta, y2+yDelta, width=3) # çizgi 2

    if finished:
        pygame.mixer.music.load("sounds/victory.mp3")
        pygame.mixer.music.play()

Seslerimiz de tamam. Oyunumuzu sesli oynayabiliriz.




Oyun Tablasını Kullanıcıya Seçtirmek

Şimdiye kadar hep sabit dosya ismi verdik. Orada bir sürü tabla bilgisi var Butona basınca ilgili sınıfa ait dosyalardan kullanıcı istediğini seçerek başlasın dersek,

İlk önce Tkinter'in filedialog modülünü de programa dahil etmeliyiz. Fıncs.py dosyası en başa import satırımızı ekleyelim.

import tkinter as tk
import tkinter.font as font
import pygame
from tkinter import filedialog
....

Butona tıklanınca dosya açma penceresi aktif etmek için buton koduna şöyle ilave yapmalıyız.

def bKolay10_10_Click(frame):
    global Map, Frame
    Frame = frame
    # fname = "maps/fKolay10_10_01.map"
    fname = filedialog.askopenfilename(title="Oyun tablası seçiniz",\
        filetypes = [("Map files", "fKolay10_10_*.map")] )
    ReadMap(fname)
    DrawGrid(frame, 10)

def bOrta10_10_Click(frame):
    global Map, Frame
    Frame = frame
    # fname = "maps/fOrta10_10_01.map"
    fname = filedialog.askopenfilename(title="Oyun tablası seçiniz",\
        filetypes = [("Map files", "fOrta10_10_*.map")] )
    ReadMap(fname)
    DrawGrid(frame, 10)

def bOrta15_15_Click(frame):
    global Map, Frame
    Frame = frame
    # fname = "maps/fOrta15_15_01.map"
    fname = filedialog.askopenfilename(title="Oyun tablası seçiniz",\
        filetypes = [("Map files", "fOrta15_15_*.map")] )
    ReadMap(fname)
    DrawGrid(frame, 15)

def bZor15_15_Click(frame):
    global Map, Frame
    Frame = frame
    # fname = "maps/fZor15_15_01.map"
    fname = filedialog.askopenfilename(title="Oyun tablası seçiniz",\
        filetypes = [("Map files", "fZor15_15_*.map")] )
    ReadMap(fname)
    DrawGrid(frame, 15)

Bana bu kadarı yetti. Şimdi internetten oyunuma uygun haritaları bulup bilgilerini .map dosyalara yazıyorum sonra da oynuyorum.

Daha çok şey yapılabilir, süreler tutulabilir, rekor tabloları oluşturulabilir, kullanıcının tabloları veri olarak değil de görsel olarak girmesi sağlanabilir vs. Tüm bunlar sizin ufkunuza kalmış artık.

Bu yazı da şükürler olsun bitti. Birazcık bilgilerinize ekleme yapabildiysem ne mutlu bana. Sonraki yazılarda görüşmek üzere , kalın sağlıcakla..







Hiç yorum yok:

Yorum Gönder