8 Nisan 2025 Salı

WxRuby3 ile Masaüstü Uygulama Geliştirmek 6

https://ujk-ujk.blogspot.com/2025/04/wxruby3-ile-masaustu-uygulama.html
İçindekiler +


Selam, WxRuby ile masaüstü uygulama geliştirme yazı dizimizin son bölümünde bir Tetris oyunu yazacağız nasipse. Yavaş yavaş sindire sindire adım adım uygulamamızı oluşturacağız. 

Haydi başlayalım.



Tetris Oyunu

Öncelikle bir uygulama iskeleti oluşturarak başlayalım. Küçük kibar bir frame ile başlayalım.

tetris.rb

require "wx"

class Tetris < Wx::Frame
 
  def initialize(parent)
    super(parent, size: [180,380],
      style: Wx::DEFAULT_FRAME_STYLE ^ Wx::RESIZE_BORDER ^ Wx::MAXIMIZE_BOX)
    init_UI
  end

  def init_UI
    set_title "Tetris"
    centre

    @statusbar = create_status_bar
    @statusbar.set_status_text '0'
  end
end

Wx::App.run {
  Tetris.new(nil).show
}


Dikey, uzunlamasına bir pencere, default pencere stilinden boyutlandırılabilirlik ve maximize kutusu çıkarılmış. Temizlenen satırların sayısını durum çubuğuna yazacağız, şimdilik sıfır yazıyoruz. 

Oyun tablamızı bir Wx::Panel nesnesi olarak penceremize ekleyeceğiz, ama içinde çok fazla kod olacağını düşündüğüm için ona ayrı bir sınıf tanımı yazalım. 

class Board < Wx::Panel
  BoardWidth = 10
  BoardHeight = 22
  Speed = 300
  ID_TIMER = 1

  def initialize(*args)
    super(*args)
    init_board
  end
 
  def init_board

  end
end


Board sınıfımızı Wx::Panel'den türettik. BoardWidth oyun alanımızın kaç kare genişliği olacağı , yani bir satırda 10 göz olacak. BoardHeight değeri de alanımızın yüksekliği kaç göz olacağı, yani 22 göz yükseklik olacak. Speed değeri zamanlayıcının 300 ms'de bir kaydırma yapacağını belirtiyor. Bir de Timer nesnemiz olacak, onun da ID değerini 1 olarak belirleriz.

Şimdi bu Board nesnesinden bir tane penceremize ekleyelim.

class Tetris < Wx::Frame
 
  def initialize(parent)
    super(parent, size: [180,380],
      style: Wx::DEFAULT_FRAME_STYLE ^ Wx::RESIZE_BORDER ^ Wx::MAXIMIZE_BOX)
    init_UI
  end

  def init_UI
    set_title "Tetris"
    centre

    @statusbar = create_status_bar
    @statusbar.set_status_text '0'
    @board = Board.new self
    @board.set_focus
  end
end


set_focus ile panele odaklanıyoruz ki tuş basılmalarını algılayarak oyunun senaryolarını çalıştıralım. 




Parçalar neye benziyor?

Bu noktada biraz düşünelim. Board nesnemizi boyayarak hücrelerin içlerini mevcut tetris parçasına göre renklendirmek istiyoruz. Şekillerin biçimine odaklanmak yerine o gözde hangi parçanın bir gözü var ona odaklanmak daha iyi olacak. Çünkü silinmeler yaşandıkça parçaların bir kısmı silinebilir. Bu yüzden parçalar yukarıdan aşağı inerken bütün olsa da yerine yerleştiğinde onu hücrelerine ayırarak yerleştirmek mantıklı olacak. İlk önce olası şekiller hakkında bir taslak yapalım.




Üzerlerine numaralar yazdım. İşte hücreleri boyarken ne renk olacağını içine bu ilgili sayıyı koyarak renklendirme elde edeceğiz. 

Şimdi oyun tablamızı doldurmaya başlayalım.

class Board < Wx::Panel
....
  def init_board
    @timer = Wx::Timer.new self, ID_TIMER
    @board = [0,1,2,3,4,5,6,7]

    evt_paint :on_paint
  end

  def on_paint(e)

  end
end


İlk adımda bir Timer nesnesi üretiyoruz, bu zamanlayıcı parçaları hareket ettirirken kullanacağımız zamanlayıcı. @board oluşum değişkeninde tablamızdaki hücrelerin renkleri ve yukarıdaki tasarıma göre verilecek. Şimdilik başlangıçta boş olması gereken listeye deneme amacıyla her rengi koydum.

Şimdi on_paint metodu içinde sayfa ilk boyanırken bu renkleri o hücrelere koyacağız. @board değişkeni içinde sol alt köşeden başlamak üzere renk değerleri olacak. Sol alt köşe sıfır index, hemen sağı bir index, iki index olarak gidecek. 10 tane olunca bir üst sıraya gidilecek. 

  def on_paint(e)
    paint do |dc|
      size = get_client_size
      board_top = size.height - BoardHeight * square_height
     
      (0...BoardHeight).each do |i|
        (0...BoardWidth).each do |j|
          shape = shape_at j, BoardHeight - i - 1
          if shape && shape != 0
            draw_square dc,
                0 + j * square_width,
                board_top + i * square_height, shape
          end
        end
      end
    end
  end


Burada bir tek hücrenin boyutlarını belirleyen square_width ve square_height metodları var, onları bir tanımlayalım.

  def square_height
    return client_size.height / BoardHeight
  end

  def square_width
    return client_size.width / BoardWidth
  end


Yüksekliği tamsayı olarak 22'ye genişliği de 10'a bölerek piksel olarak bir hücrenin genişlik ve yüksekliklerini buluyoruz. 

Sırada verilen sütun ve satırdaki hücrenin içinde hangi şekil renginin olduğunu bulan shape_at metodu var.

  # sütun, satır
  def shape_at(col, row)
    return @board[row * BoardWidth + col]
  end


@board array değerlerinden verilen sütun ve satır numaralarına karşı gelen değer okunuyor ve geri dönülüyor, yani 0..7 arası bir değer. Bu shape_at daha birçok yerde kullanılacağı için bu tek satıra bir metod yazıp kodun daha anlaşılabilir olmasını sağladık. 

Daha draw_square metodu var ama şimdilik kodu biraz anlayalım sonra draw_square metoduna geçelim.

      size = get_client_size
      board_top = size.height - BoardHeight * square_height

En aşağıdan başlıyor ya hücrelerimizin index değerleri, bu yüzden çizime başlamak için çizim alanının sol üst köşe koordinatı lazım. Bu koordinatın x değeri sıfır (sol kenar), y değeri de board_top değerimiz olacak , aslında square_height değerini tamsayı bölmesi ile hesapladığımıza göre bu rakam sıfır ila square_height arasında küçük bir düzeltme değeri olacaktır. Yani panelin en üst kenarına yakın bir değer.

      (0...BoardHeight).each do |i|
        (0...BoardWidth).each do |j|

Buraya göre i değişkeninde satır numarası (0'dan 21 dahile kadar bir değer) ve j değişkeninde sütun numarası ( 0'dan 9 dahile kadar bir değer) olacak, yani tüm çizim alanını hücre hücre tarıyoruz. 

          shape = shape_at j, BoardHeight - i - 1

Buna bakarsak ilk şeklimiz shape_at(0, 21) , yani @board[210] yani sol üst köşedeki hücrenin renk kodu. 

          if shape && shape != 0
            draw_square dc,
                0 + j * square_width,
                board_top + i * square_height, shape

O hücreye çizim yapabilmemiz için iki koşul var birincisi o hücrede bir şekil kodu gelmiş olması, ki düşünürsek örnek verdiğimiz @board değişkeninde index 210 değeri yok ve isteyince nil döner, bu durumda çizim yapmaya gerek yok. İkinci koşulsa şekil kodunun sıfırdan farklı olması, bu durumda da çizime gerek yok, bu şekil kodunun sıfır olması kozunu hücreleri silerken falan kullanabiliriz. 

draw_square metoduna verilen ilk argüman çizim yaptığımız dc nesnesi. İkinci argüman sol kenara göre o hücrenin x koordinatı, bunu belirtirken 0 + j * square_width şeklinde yazdım ki kendime de sıfırdan itibaren diye hatırlatayım. Üçüncü argüman hücrenin y koordinatı, o da board_top + i * square_height değerinde. Son argüman ise şekil kodu , yani renk numarası. Gelelim metoda

  def draw_square(dc, x, y, shape)
    colors = ['#000000', '#CC6666', '#66CC66', '#6666CC',
              '#CCCC66', '#CC66CC', '#66CCCC', '#DAAA00']

    light = ['#000000', '#F89FAB', '#79FC79', '#7979FC',
              '#FCFC79', '#FC79FC', '#79FCFC', '#FCC600']

    dark = ['#000000', '#803C3B', '#3B803B', '#3B3B80',
              '#80803B', '#803B80', '#3B8080', '#806200']

    pen = Wx::Pen.new light[shape]
    pen.cap = Wx::CAP_PROJECTING
    dc.pen = pen

    dc.draw_line x, y + square_height - 1, x, y
    dc.draw_line x, y, x + square_width - 1, y

    darkpen = Wx::Pen.new dark[shape]
    darkpen.cap = Wx::CAP_PROJECTING
    dc.pen = darkpen

    dc.draw_line x + 1, y + square_height - 1,
      x + square_width - 1, y + square_height - 1
    dc.draw_line x + square_width - 1,
      y + square_height - 1, x + square_width - 1, y + 1

    dc.pen = Wx::TRANSPARENT_PEN
    dc.brush = Wx::Brush.new colors[shape]
    dc.draw_rectangle x + 1, y + 1,
      square_width - 2, square_height - 2
  end

Burada verilen hücre içine 3 boyutlu gibi görünen bir boyama yapıyoruz. colors array'inde hücre içinin rengi şekil koduna göre indexli olarak girilmiş. Şekiller taslağımızdaki numaralandırmaya bakarsanız oradaki numaralara karşı gelen renk kodlarını görürsünüz. light array'inde ışığa bakan kenarların çizgi rengi kodlarını, dark array'inde de gölge kalan kenar çizgileri renk kodunu görürüz. 

    pen = Wx::Pen.new light[shape]
    pen.cap = Wx::CAP_PROJECTING
    dc.pen = pen

    dc.draw_line x, y + square_height - 1, x, y
    dc.draw_line x, y, x + square_width - 1, y

Sol kenara bir çizgi ve üst kenara bir çizgiyi light array'indeki renk koduna göre çiziyoruz. 

    darkpen = Wx::Pen.new dark[shape]
    darkpen.cap = Wx::CAP_PROJECTING
    dc.pen = darkpen

    dc.draw_line x + 1, y + square_height - 1,
      x + square_width - 1, y + square_height - 1
    dc.draw_line x + square_width - 1,
      y + square_height - 1, x + square_width - 1, y + 1

Alt kenara ve sağ kenara birer çizgiyi dark array'indeki renk koduna göre çiziyoruz.

    dc.pen = Wx::TRANSPARENT_PEN
    dc.brush = Wx::Brush.new colors[shape]
    dc.draw_rectangle x + 1, y + 1,
      square_width - 2, square_height - 2

Kenar çizgisi olmayan bir dikdörtgeni, kenar çizgilerinin içinde kalan alana colors array'inde verilen renk koduyla çiziyoruz. 

Programımız burada bizim girdiğimiz örnek şekil kodlarıyla bir şeyler çiziyor olmalı. Önce şu ana kadarki kodumuzu bir toptan görelim ve programımızı çalıştırıp, bakalım ne çiziyor. 

tetris.rb

require "wx"

class Tetris < Wx::Frame
 
  def initialize(parent)
    super(parent, size: [180,380],
      style: Wx::DEFAULT_FRAME_STYLE ^ Wx::RESIZE_BORDER ^ Wx::MAXIMIZE_BOX)
    init_UI
  end

  def init_UI
    set_title "Tetris"
    centre

    @statusbar = create_status_bar
    @statusbar.set_status_text '0'
    @board = Board.new self
    @board.set_focus
  end
end

class Board < Wx::Panel
  BoardWidth = 10
  BoardHeight = 22
  Speed = 300
  ID_TIMER = 1

  def initialize(*args)
    super(*args)
    init_board
  end
 
  def init_board
    @timer = Wx::Timer.new self, ID_TIMER
    @board = [0,1,2,3,4,5,6,7]

    evt_paint :on_paint
  end

  def draw_square(dc, x, y, shape)
    colors = ['#000000', '#CC6666', '#66CC66', '#6666CC',
              '#CCCC66', '#CC66CC', '#66CCCC', '#DAAA00']

    light = ['#000000', '#F89FAB', '#79FC79', '#7979FC',
              '#FCFC79', '#FC79FC', '#79FCFC', '#FCC600']

    dark = ['#000000', '#803C3B', '#3B803B', '#3B3B80',
              '#80803B', '#803B80', '#3B8080', '#806200']

    pen = Wx::Pen.new light[shape]
    pen.cap = Wx::CAP_PROJECTING
    dc.pen = pen

    dc.draw_line x, y + square_height - 1, x, y
    dc.draw_line x, y, x + square_width - 1, y

    darkpen = Wx::Pen.new dark[shape]
    darkpen.cap = Wx::CAP_PROJECTING
    dc.pen = darkpen

    dc.draw_line x + 1, y + square_height - 1,
      x + square_width - 1, y + square_height - 1
    dc.draw_line x + square_width - 1,
      y + square_height - 1, x + square_width - 1, y + 1

    dc.pen = Wx::TRANSPARENT_PEN
    dc.brush = Wx::Brush.new colors[shape]
    dc.draw_rectangle x + 1, y + 1,
      square_width - 2, square_height - 2
  end

  def on_paint(e)
    paint do |dc|
      size = get_client_size
      board_top = size.height - BoardHeight * square_height
     
      (0...BoardHeight).each do |i|
        (0...BoardWidth).each do |j|
          shape = shape_at j, BoardHeight - i - 1
          if shape && shape != 0
            draw_square dc,
                0 + j * square_width,
                board_top + i * square_height, shape
          end
        end
      end
    end
  end

  # sütun, satır
  def shape_at(col, row)
    return @board[row * BoardWidth + col]
  end

  def square_height
    return client_size.height / BoardHeight
  end

  def square_width
    return client_size.width / BoardWidth
  end
end

Wx::App.run {
  Tetris.new(nil).show
}

ve çıktısı




Şekiller sınıfı

Boyama işi bitti, şimdi şekillerimizi bir nesne olarak üreteceğiz ve yukarıdan aşağı kayacaklar. Bunun için ilk önce şekillerimizi ve davranışlarını belirleyeceğimiz bir Shape sınıfı tanımlayacağız. Önce bütün kodu vereceğim , sonra adım adım açıklamaları yaparız. 

class Shape
  @@coords_table = [
    [[0, 0], [0, 0], [0, 0], [0, 0]],
    [[0, -1], [0, 0], [1, 0], [1, 1]],
    [[0, -1], [0, 0], [-1, 0], [-1, 1]],
    [[0, -1], [0, 0], [0, 1], [0, 2]],
    [[-1, 0], [0, 0], [1, 0], [0, 1]],
    [[0, 0], [1, 0], [0, 1], [1, 1]],
    [[-1, -1], [0, -1], [0, 0], [0, 1]],
    [[1, -1], [0, -1], [0, 0], [0, 1]]
  ]

  def initialize
    @coords = [[0, 0], [0, 0], [0, 0], [0, 0]]
    @piece_shape = 0

    set_shape(0)
  end

  def shape
    @piece_shape
  end

  def set_shape(shape)
    table = @@coords_table[shape]
    (0...4).each do |i|
      (0...2).each do |j|
        @coords[i][j] = table[i][j]
      end
    end
    @piece_shape = shape
  end

  def set_random_shape
    set_shape rand(1..7)
  end

  def x(index)
    @coords[index][0]
  end

  def y(index)
    @coords[index][1]
  end

  def setX(index, x)
    @coords[index][0] = x
  end

  def setY(index, y)
    @coords[index][1] = y
  end

  def minX
    m = @coords[0][0]
    (0...4).each do |i|
      m = [m, @coords[i][0]].min
    end
    return m
  end

  def maxX
    m = @coords[0][0]
    (0...4).each do |i|
      m = [m, @coords[i][0]].max
    end

    return m
  end

  def minY
    m = @coords[0][1]
    (0...4).each do |i|
      m = [m, @coords[i][1]].min
    end

    return m
  end

  def maxY
    m = @coords[0][1]
    (0...4).each do |i|
      m = [m, @coords[i][1]].max
    end

    return m
  end

  def rotated_left
    if @piece_shape == 5  # kare şekil
      return self
    end

    result = Shape.new
    result.set_shape @piece_shape

    (0...4).each do |i|
      result.setX(i, y(i))
      result.setY(i, -x(i))
    end

    return result
  end

  def rotated_right

    if @piece_shape == 5  # kare şekil
      return self
    end

    result = Shape.new
    result.set_shape @piece_shape

    (0...4).each do |i|
      result.setX(i, -y(i))
      result.setY(i, x(i))
    end

    return result
  end
end


Burada @@coords_table değişkeni içinde taslağımızda bulunan tüm şekillerin 4 hücreden oluşan koordinatları bulunuyor. Taslağımıza karşı gelen değerleri bir inceleyelim.


Bu tablo şekillerin ilk defa ekrana gelişini gösteriyor, daha sonra döndürdükçe şekle ait bu dörtlü koordinatları değiştirdiğimizde dönüşleri gerçekleştireceğiz. 

  def initialize
    @coords = [[0, 0], [0, 0], [0, 0], [0, 0]]
    @piece_shape = 0

    set_shape(0)
  end

@coords oluşum değişkeninde o şeklin koordinatları saklanacak, şekli döndürdükçe mesela, bo array elemanlarını değiştirerek dönmesini sağlayacağız. @piece_shape oluşum değişkeninde ise o şeklin tasarımda karşı gelen index sayısı var. 

  def set_shape(shape)
    table = @@coords_table[shape]
    (0...4).each do |i|
      (0...2).each do |j|
        @coords[i][j] = table[i][j]
      end
    end
    @piece_shape = shape
  end

set_shape metodu ise bir şekle ilk değerlerini veriyor. @coords ve @piece_shape oluşum değişkenlerinin değerlerini istenen şekle göre veriyor. Aslında buna bakılırsa initialize metodundaki ilk iki satıra gerek kalmıyor gibi. Ama en azından @coords array'i için ilk değeri vermek gerekli. 

  def shape
    @piece_shape
  end

shape metodu dışarıdan @piece_shape oluşum değişkenini okumak için kullanılıyor. 

  def set_random_shape
    set_shape rand(1..7)
  end

set_random_shape metodu rastgele 7 şekilden birini seçmek için kullanılıyor. 

  def x(index)
    @coords[index][0]
  end

  def y(index)
    @coords[index][1]
  end

Bu iki metod nesne koordinatlarının index'inci hücresinin x ve y değerlerini veriyor. Her hücrenin pozisyonunu okurken kullanacağız.

  def setX(index, x)
    @coords[index][0] = x
  end

  def setY(index, y)
    @coords[index][1] = y
  end

Bu iki metod da nesne koordinatlarının index'inci hücresinin x ve y değerlerini değiştiriyor. Hücreleri kaydırırken kullanacağız.

  def minX
    m = @coords[0][0]
    (0...4).each do |i|
      m = [m, @coords[i][0]].min
    end
    return m
  end

  def maxX
    m = @coords[0][0]
    (0...4).each do |i|
      m = [m, @coords[i][0]].max
    end

    return m
  end

minX değeri şeklin hücrelerinin en küçük x değerine sahip olan hücresinin x değeri, sol kenara dayandı mı, bu metodla kontrol edeceğiz. 

maxX değeri şeklin hücrelerinin en büyük x değerine sahip olan hücresinin değeri, sağ kenara dayandı mı, bu metodla kontrol edeceğiz. 

  def minY
    m = @coords[0][1]
    (0...4).each do |i|
      m = [m, @coords[i][1]].min
    end

    return m
  end

  def maxY
    m = @coords[0][1]
    (0...4).each do |i|
      m = [m, @coords[i][1]].max
    end

    return m
  end

minY ve maxY de şeklin en alt ve en üst hücrelerinin y pozisyonlarını okumak için kullanılıyor. 

  def rotated_left
    if @piece_shape == 5  # kare şekil
      return self
    end

    result = Shape.new
    result.set_shape @piece_shape

    (0...4).each do |i|
      result.setX(i, y(i))
      result.setY(i, -x(i))
    end

    return result
  end

Bu şekli sola döndürme rutini, daha doğrusu şeklin orijinalinin sola dönmüş halini veren rutin , şekil kare şekilse sorun yok dönmüşü de aynı olur. Diğer şekiller içim önce şeklin bir kopyasını oluşturuyoruz. Sonra her hücre için x yerine y değerini, y yerine de -x değerini koyuyoruz. Mesela 1 nolu şekil 

[[0, -1], [0, 0], [1, 0], [1, 1]]

iken (yani dikey Z)

[[-1, 0], [0, 0], [0, -1], [1, -1]]

oluyor (yani yatay Z)

rotated_right metodu da tam tersi yani orjinal şeklin sağa dönmüş halini şekil nesnesi olarak bize verecektir.  

Şimdi oyun tahtamızın (Board nesnesi) init_board metoduna dönelim ve birkaç ilave yapalım. 

  def init_board
    @timer = Wx::Timer.new self, ID_TIMER
    @cur_piece = Shape.new
    @next_piece = Shape.new
    @curX = 0
    @curY = 0
    @num_lines_removed = 0
    @board = []

    @is_started = false
    @is_paused = false

    evt_paint :on_paint
  end


@cur_piece şu anda (oyun çalışırkenki aktif anda) tahtada hareket halinde olan şekil olacak, şimdilik boş bir şekil olarak başlattık. @next_piece ise sırada bir sonra gelecek olan şekil, onu da boş bir şekil nesnesi olarak başlattık. @curX ve @curY ise aktif şeklin referans alacağı pozisyon, yani tablada hareket eden şekil çizilirken @coords değerleri bunlara eklenecek ve hücreleri çizilecek. @num_lines_removed ise oyun devam ederken tamamını doldurup silinmesini sağladığımız satır sayısı, bunu skor olarak kullanıyoruz. @is_started  ve @is_paused değişkenleri ise oyun çalışırken kullandığımız iki değişken. Bir de @board değişkeninde örnek olarak girdiğimiz hücre renk kodlarını kaldırdık. 

Sıra geldi oyuna start vermeye bu amaçla Board nesnemize start adında bir metod ekleyeceğiz. Öncelikle ana pencere rutinimize bu start metodunu çağıran bir satır ekleyelim. 

  def init_UI
    set_title "Tetris"
    centre

    @statusbar = create_status_bar
    @statusbar.set_status_text '0'
    @board = Board.new self
    @board.set_focus
    @board.start
  end

Gelelim Board sınıfı start metoduna.

  def start
    if @is_paused
      return
    end

    @is_started = true
    @num_lines_removed = 0
    clear_board

    new_piece
    @timer.start Speed
  end

clear_board metodu oyun tablasını temizliyor. 

  def clear_board
    (0...BoardHeight * BoardWidth).each do |i|
        @board.append 0
    end
  end

Sadece şu anda boş olan @board array içini görünmeyen hücre renk kodu olan sıfır ile dolduruyoruz. Bundan sonra on_paint olay metodu tetiklenirse tüm tabla boş çizilecektir , daha doğrusu hücre renk kodu sıfır olduğu için hücreler boyanmayacaktır. 

new_piece metodu ise yeni bir şekli üretiyor. 

  def new_piece
    @cur_piece = @next_piece.dup
    statusbar = parent.status_bar
    @next_piece.set_random_shape

    @curX = BoardWidth / 2 + 1
    @curY = BoardHeight - 1 + @cur_piece.minY

    if not try_move(@cur_piece, @curX, @curY)
      @cur_piece.set_shape 0
      @timer.stop
      @is_started = false
      statusbar.status_text = 'Game over'
    end
  end

Bu new_piece metodunu oyun ilerlerken tablaya yeni bir parça eklerken çağırıp duracağız. Öncelikle sonraki parçayı kopyalayarak şimdiki parça yapıyor. Sonraki parçaya rastgele bir şekil seçtirerek hazırlıyor. Start vermeden önce de bu sonraki şekle rastgele bir seçim yapsak iyi olacak, tablayı kurarken boş şekil üretmiştik.

  def init_board
    @timer = Wx::Timer.new self, ID_TIMER
    @cur_piece = Shape.new
    @next_piece = Shape.new
    @next_piece.set_random_shape
    @curX = 0
    .....

@curX ve @curY yani hareket etmekte olan şeklin koordinat merkezini tablanın üst ortasına getiriyoruz, bunu yaparken şeklim en altta kalan sırası tablanın en üst satırına gelecek şekilde ayarlıyoruz. 

try_move metodu verilen parçayı verilen koordinata götürmeye uğraşacak ve eğer başarılı olursa parçayı o koordinata kaydıracak ve true değer dönecek. Başarısız olursa , yani sağa sola dayanırsa ya da aşağı inerken başka parçaya ya da zemine dayandıysa parça verilen koordinata kaydırılamayarak false değer döner. Burada yeni parça üretirken eğer tablaya koyamıyorsa demek ki tabla doludur ve oyun bitmiş demektir. 

  def try_move(new_piece, newX, newY)
    (0...4).each do |i|
      x = newX + new_piece.x(i)
      y = newY - new_piece.y(i)

      if x < 0 or x >= BoardWidth or y < 0 or y >= BoardHeight
        return false
      end

      if shape_at(x, y) != 0
        return false
      end
    end

    @cur_piece = new_piece
    @curX = newX
    @curY = newY
    refresh

    return true
  end

Şeklin her bir hücresinin hedefte kayacağı yere bakılıyor, eğer orası tablanın sınırları dışındaysa ya da orada başka bir renk kodu varsa false değer döner. Sorun yoksa @cur_piece@curX ve @curY değerleri yenilenip on_paint olay metodu tetiklenip tabla boyansın diye refresh metodu çağrılıyor. 

Parça ekrana eklendikten sonra aşağı kaymaya devam etmesi için @timer için bir olay işleme rutini eklemeliyiz. Öncelikle init_board metoduna olay işleme metodu çağrısını ekleyelim.

  def init_board
    @timer = Wx::Timer.new self, ID_TIMER
    @cur_piece = Shape.new
    @next_piece = Shape.new
    @next_piece.set_random_shape
    @curX = 0
    @curY = 0
    @num_lines_removed = 0
    @board = []

    @is_started = false
    @is_paused = false

    evt_paint :on_paint
    evt_timer ID_TIMER, :on_timer
  end

Sonra da on_timer olay işleme metodunu ekleyelim.

  def on_timer(event)
    if event.id == ID_TIMER
      if @is_waiting_after_line
        @is_waiting_after_line = false
        new_piece
      else
        one_line_down
      end
    else
      event.skip
    end
  end

@is_waiting_after_line oluşum değişkeninde önceki parça bitmiş sonrakine geçilmesi gerektiği bilgisi olacak. Öncelikle Board nesnesi üretirken bu değişkenin ilk değerini vermeliyiz. 

  def init_board
    @timer = Wx::Timer.new self, ID_TIMER
    @is_waiting_after_line = false
    @cur_piece = Shape.new
....
  end

one_line_down metodu aktif parçayı bir hücre aşağı kaydıracak.

  def one_line_down
    if not try_move @cur_piece, @curX, @curY - 1
      piece_dropped
    end
  end

Öncelikle aktif parçanın y ekseni değerini bir azaltmaya çalışacak, başarılı olursa sorun yok parça aşağı kaymış olacak. Kaydırma başarısız olursa parça bir yere dayanmış demektir, yani aşağı gidecek yeri kalmamıştır ve piece_dropped metodu çağrılarak hem o parça @board array'inde yerleştirilecek , hem de yeni parçaya geçilecek.

Bu arada programı şu anda çalıştırırsak parçanın en aşağıya düşene kadar hareketini görmemiz gerekiyor, ama on_paint olay işleme metodunda bu parça için bir ilave yapmadığımız için görünmez, ve bir süre çalıştıktan sonra program piece_dropped metodunu bulamadığı için hata verecektir. Yani aslında parça kayıyor, en alta geliyor ama göremiyoruz. on_paint metoduna geri dönüp ilavemizi yapalım.

  def on_paint(e)
    paint do |dc|
      size = get_client_size
      board_top = size.height - BoardHeight * square_height
     
      (0...BoardHeight).each do |i|
        (0...BoardWidth).each do |j|
          shape = shape_at j, BoardHeight - i - 1
          if shape && shape != 0
            draw_square dc,
                0 + j * square_width,
                board_top + i * square_height, shape
          end
        end
      end

      if @cur_piece.shape != 0
        (0...4).each do |i|
          x = @curX + @cur_piece.x(i)
          y = @curY - @cur_piece.y(i)

          draw_square dc, 0 + x * square_width,
            board_top + (BoardHeight - y - 1) * square_height,
            @cur_piece.shape
        end
      end
    end
  end

Eğer aktif şekil boş şekil değilse her hücresini aktif koordinata göre boyuyoruz. Şimdi çalıştırırsak parçanın hareket edip aşağı kenara kadar ulaştıktan sonra  piece_dropped metodu bulunamadı der.

Gelelim metoda.

  def piece_dropped
    (0...4).each do |i|
      x = @curX + @cur_piece.x(i)
      y = @curY - @cur_piece.y(i)
      set_shape_at x, y, @cur_piece.shape
    end

    remove_full_lines

    if not @is_waiting_after_line
      new_piece
    end
  end

Burada set_shape_at metodu @board array'inde verilen hücreye o şekil kodunu koyar. 

  def set_shape_at(x, y, shape)
    @board[(y * BoardWidth) + x] = shape
  end

remove_full_lines metodu hareketli parça en alta indiğinde oluşmuş olan bütünü dolu satırları silecek.

  def remove_full_lines
    num_full_lines = 0

    statusbar = parent.status_bar

    rows_to_remove = []

    (0...BoardHeight).each do |i|
      n = 0
      (0...BoardWidth).each do |j|
        if not shape_at(j, i) == 0
          n = n + 1
        end

        if n == 10
          rows_to_remove.append(i)
        end
      end
    end

    rows_to_remove.reverse!

    rows_to_remove.each do |m|
      (m...BoardHeight).each do |k|
        (0...BoardWidth).each do |l|
          set_shape_at l, k, shape_at(l, k + 1)
        end
      end

      num_full_lines = num_full_lines + rows_to_remove.length

      if num_full_lines > 0
        @num_lines_removed = @num_lines_removed + num_full_lines
        statusbar.status_text = @num_lines_removed.to_s
        @is_waiting_after_line = true
        @cur_piece.set_shape 0
        refresh
      end
    end
  end

Bu metod biraz uzun ama yaptığı iş karmaşık değil. Önce rows_to_remove adında bir array içinde silinecek bütünü dolu satırların numaralarını topluyoruz. Sonra her bir bütünü dolu satırın üzerindeki tüm satırları bir satır aşağı kaydırarak bütünü dolu o satırı yok ediyoruz. num_full_lines değişkeninde silinecek kalan satırların sayısı var. @num_lines_removed değişkeninde ise şimdiye kadar silinmiş olan toplam satır sayısı var ve bunu skor değeri olarak durum çubuğuna yazıyoruz. Çoklu satır silmelerinde de skor katlayarak artıyor. 

Programı çalıştırınca bir şey dikkatimi çekti, şekillerin renk kodlarında bir kayma var, kendi renginde değil önceki şeklin renginde çıkıyor. Demek yeni şekil belirlenirken bir eksiğimiz var. Olay new_piece metodu en başında kopuyor, dup ile nesneyi kopyalıyoruz da o nesnenin @coords değerleri kopyalanmıyor.

  def new_piece
    @cur_piece = @next_piece.dup

yerine 

  def new_piece
    @cur_piece.set_shape @next_piece.shape
....

yaparsak daha iyi , bu nesne kopyalamalar her zaman dert çıkarır zaten.

Bir de Game Over yazarken en son skoru da yazsak fena olmayacak.

  def new_piece
    ....
      statusbar.status_text = "Score: #{@num_lines_removed} - Game over"
    end
  end


Bu noktada programın son halini bir versem iyi olacak 

tetris.rb

require "wx"

class Tetris < Wx::Frame
 
  def initialize(parent)
    super(parent, size: [180,380],
      style: Wx::DEFAULT_FRAME_STYLE ^ Wx::RESIZE_BORDER ^ Wx::MAXIMIZE_BOX)
    init_UI
  end

  def init_UI
    set_title "Tetris"
    centre

    @statusbar = create_status_bar
    @statusbar.set_status_text '0'
    @board = Board.new self
    @board.set_focus
    @board.start
  end
end

class Board < Wx::Panel
  BoardWidth = 10
  BoardHeight = 22
  Speed = 300
  ID_TIMER = 1

  def initialize(*args)
    super(*args)
    init_board
  end
 
  def init_board
    @timer = Wx::Timer.new self, ID_TIMER
    @is_waiting_after_line = false
    @cur_piece = Shape.new
    @next_piece = Shape.new
    @next_piece.set_random_shape
    @curX = 0
    @curY = 0
    @num_lines_removed = 0
    @board = []

    @is_started = false
    @is_paused = false

    evt_paint :on_paint
    evt_timer ID_TIMER, :on_timer
  end

  def clear_board
    (0...BoardHeight * BoardWidth).each do |i|
        @board.append 0
    end
  end

  def draw_square(dc, x, y, shape)
    colors = ['#000000', '#CC6666', '#66CC66', '#6666CC',
              '#CCCC66', '#CC66CC', '#66CCCC', '#DAAA00']

    light = ['#000000', '#F89FAB', '#79FC79', '#7979FC',
              '#FCFC79', '#FC79FC', '#79FCFC', '#FCC600']

    dark = ['#000000', '#803C3B', '#3B803B', '#3B3B80',
              '#80803B', '#803B80', '#3B8080', '#806200']

    pen = Wx::Pen.new light[shape]
    pen.cap = Wx::CAP_PROJECTING
    dc.pen = pen

    dc.draw_line x, y + square_height - 1, x, y
    dc.draw_line x, y, x + square_width - 1, y

    darkpen = Wx::Pen.new dark[shape]
    darkpen.cap = Wx::CAP_PROJECTING
    dc.pen = darkpen

    dc.draw_line x + 1, y + square_height - 1,
      x + square_width - 1, y + square_height - 1
    dc.draw_line x + square_width - 1,
      y + square_height - 1, x + square_width - 1, y + 1

    dc.pen = Wx::TRANSPARENT_PEN
    dc.brush = Wx::Brush.new colors[shape]
    dc.draw_rectangle x + 1, y + 1,
      square_width - 2, square_height - 2
  end

  def new_piece
    @cur_piece.set_shape @next_piece.shape
    statusbar = parent.status_bar
    @next_piece.set_random_shape

    @curX = BoardWidth / 2 + 1
    @curY = BoardHeight - 1 + @cur_piece.minY

    if not try_move(@cur_piece, @curX, @curY)
      @cur_piece.set_shape 0
      @timer.stop
      @is_started = false
      statusbar.status_text = "Score: #{@num_lines_removed} - Game over"
    end
  end

  def on_paint(e)
    paint do |dc|
      size = get_client_size
      board_top = size.height - BoardHeight * square_height
     
      (0...BoardHeight).each do |i|
        (0...BoardWidth).each do |j|
          shape = shape_at j, BoardHeight - i - 1
          if shape && shape != 0
            draw_square dc,
                0 + j * square_width,
                board_top + i * square_height, shape
          end
        end
      end

      if @cur_piece.shape != 0
        (0...4).each do |i|
          x = @curX + @cur_piece.x(i)
          y = @curY - @cur_piece.y(i)

          draw_square dc, 0 + x * square_width,
            board_top + (BoardHeight - y - 1) * square_height,
            @cur_piece.shape
        end
      end
    end
  end

  def on_timer(event)
    if event.id == ID_TIMER
      if @is_waiting_after_line
        @is_waiting_after_line = false
        new_piece
      else
        one_line_down
      end
    else
      event.skip
    end
  end

  def one_line_down
    if not try_move @cur_piece, @curX, @curY - 1
      piece_dropped
    end
  end

  def piece_dropped
    (0...4).each do |i|
      x = @curX + @cur_piece.x(i)
      y = @curY - @cur_piece.y(i)
      set_shape_at x, y, @cur_piece.shape
    end

    remove_full_lines

    if not @is_waiting_after_line
      new_piece
    end
  end

  def remove_full_lines
    num_full_lines = 0

    statusbar = parent.status_bar

    rows_to_remove = []

    (0...BoardHeight).each do |i|
      n = 0
      (0...BoardWidth).each do |j|
        if not shape_at(j, i) == 0
          n = n + 1
        end

        if n == 10
          rows_to_remove.append(i)
        end
      end
    end

    rows_to_remove.reverse!

    rows_to_remove.each do |m|
      (m...BoardHeight).each do |k|
        (0...BoardWidth).each do |l|
          set_shape_at l, k, shape_at(l, k + 1)
        end
      end

      num_full_lines = num_full_lines + rows_to_remove.length

      if num_full_lines > 0
        @num_lines_removed = @num_lines_removed + num_full_lines
        statusbar.status_text = @num_lines_removed.to_s
        @is_waiting_after_line = true
        @cur_piece.set_shape 0
        refresh
      end
    end
  end

  def set_shape_at(x, y, shape)
    @board[(y * BoardWidth) + x] = shape
  end

  # sütun, satır
  def shape_at(col, row)
    return @board[row * BoardWidth + col]
  end

  def square_height
    return client_size.height / BoardHeight
  end

  def square_width
    return client_size.width / BoardWidth
  end

  def start
    if @is_paused
      return
    end

    @is_started = true
    #@is_waiting_after_line = false
    @num_lines_removed = 0
    clear_board

    new_piece
    @timer.start Speed
  end

  def try_move(new_piece, newX, newY)
    (0...4).each do |i|
      x = newX + new_piece.x(i)
      y = newY - new_piece.y(i)

      if x < 0 or x >= BoardWidth or y < 0 or y >= BoardHeight
        return false
      end

      if shape_at(x, y) != 0
        return false
      end
    end

    @cur_piece = new_piece
    @curX = newX
    @curY = newY
    refresh

    return true
  end
end

class Shape
  @@coords_table = [
    [[0, 0], [0, 0], [0, 0], [0, 0]],
    [[0, -1], [0, 0], [1, 0], [1, 1]],
    [[0, -1], [0, 0], [-1, 0], [-1, 1]],
    [[0, -1], [0, 0], [0, 1], [0, 2]],
    [[-1, 0], [0, 0], [1, 0], [0, 1]],
    [[0, 0], [1, 0], [0, 1], [1, 1]],
    [[-1, -1], [0, -1], [0, 0], [0, 1]],
    [[1, -1], [0, -1], [0, 0], [0, 1]]
  ]

  def initialize
    @coords = [[0, 0], [0, 0], [0, 0], [0, 0]]
    @piece_shape = 0

    set_shape(0)
  end

  def shape
    @piece_shape
  end

  def set_shape(shape)
    table = @@coords_table[shape]
    (0...4).each do |i|
      (0...2).each do |j|
        @coords[i][j] = table[i][j]
      end
    end
    @piece_shape = shape
  end

  def set_random_shape
    set_shape rand(1..7)
  end

  def x(index)
    @coords[index][0]
  end

  def y(index)
    @coords[index][1]
  end

  def setX(index, x)
    @coords[index][0] = x
  end

  def setY(index, y)
    @coords[index][1] = y
  end

  def minX
    m = @coords[0][0]
    (0...4).each do |i|
      m = [m, @coords[i][0]].min
    end
    return m
  end

  def maxX
    m = @coords[0][0]
    (0...4).each do |i|
      m = [m, @coords[i][0]].max
    end

    return m
  end

  def minY
    m = @coords[0][1]
    (0...4).each do |i|
      m = [m, @coords[i][1]].min
    end

    return m
  end

  def maxY
    m = @coords[0][1]
    (0...4).each do |i|
      m = [m, @coords[i][1]].max
    end

    return m
  end

  def rotated_left
    if @piece_shape == 5  # kare şekil
      return self
    end

    result = Shape.new
    result.set_shape @piece_shape

    (0...4).each do |i|
      result.setX(i, y(i))
      result.setY(i, -x(i))
    end

    return result
  end

  def rotated_right

    if @piece_shape == 5  # kare şekil
      return self
    end

    result = Shape.new
    result.set_shape @piece_shape

    (0...4).each do |i|
      result.setX(i, -y(i))
      result.setY(i, x(i))
    end

    return result
  end
end

Wx::App.run {
  Tetris.new(nil).show
}



Tuş olayları

Parçalar düşmeye başladı, şimdi tuşlara basınca parçalara olacak işlemlere geldik. Öncelikle Olay metodunu çağırmakla başlayalım.

  def init_board
   .....

    evt_paint :on_paint
    evt_timer ID_TIMER, :on_timer
    evt_char_hook :on_key_down
  end

Ve on_key_down olay işleme metodu tanımı.

  def on_key_down(event)
    if not @is_started or @cur_piece.shape == 0
      event.skip
      return
    end

    keycode = event.key_code

    if keycode == 'P'.ord or keycode == 'p'.ord
      pause
      return
    end

    if @is_paused
      return
    elsif keycode == Wx::K_LEFT
      try_move @cur_piece, @curX - 1, @curY
    elsif keycode == Wx::K_RIGHT
      try_move @cur_piece, @curX + 1, @curY
    elsif keycode == Wx::K_DOWN
      try_move @cur_piece.rotated_right, @curX, @curY
    elsif keycode == Wx::K_UP
      try_move @cur_piece.rotated_left, @curX, @curY
    elsif keycode == Wx::K_SPACE
      drop_down
    elsif keycode == 'D'.ord or keycode == 'd'.ord
      one_line_down
    else
      event.skip
    end
  end

Burada ilk eklememiz gereken pause metodu olacak. 

  def pause
    if not @is_started
      return
    end

    @is_paused = not @is_paused
    statusbar = self.parent.status_bar

    if @is_paused
      @timer.stop
      statusbar.status_text = 'paused'
    else
      @timer.start Speed
      statusbar.status_text = @num_lines_removed.to_s
    end

    refresh
  end

P ya da p harfi gelince çağrılan pause metodumuz @timer zamanlayıcısını durdurarak hareketi engelliyor. Bir daha  P ya da p basılınca tekrar devam ediyor.

Sol ve sağ ok tuşları parçayı sola ya da sağa kaydırıyor, yukarı ve aşağı ok tuşları ise döndürme yapıyor, metod çağrıları mevcut olduğu için şu anda programı çalıştırsak sağlıklı iş yaparlar. 

Aralık çubuğuna basınca çağrılan drop_down metodu parçayı düşürecek.

  def drop_down
    newY = @curY

    while newY > 0
      if not try_move(@cur_piece, @curX, newY - 1)
        break
      end
      newY -= 1
    end

    piece_dropped
  end

Parçayı aşağıda bir şeye dayanana kadar zamanlayıcıyı beklemeden kaydırıyor ve piece_dropped metodunu çağırarak yeni parçaya geçişi sağlıyor. 

Burada denem yapınca bir şey dikkatimi çekti, bir bütün satır oluşturunca Game Over oluyor. Sebebi remove_full_lines metodunda satırları aşağı kaydırırken en üstten nil değeri girmesi, bunu engellemek için en üstte nil gelince şekil kodu sıfır olsun diye 

  def remove_full_lines
    ....

    rows_to_remove.each do |m|
      (m...BoardHeight).each do |k|
        (0...BoardWidth).each do |l|
          set_shape_at l, k, shape_at(l, k + 1)||0

olması gerekir.

Şimdi oyunumuz çalışıyor fakat boyama işleri çok vakit aldığı için biraz tekleye tekleye gidiyor. İsterseniz draw_square içinde kenar çizgileri çizen satırları yorum içine alarak hızlandırma yapabilirsiniz. 

size son halini bir daha vereyim , bu yazıyı da burada bitireyim. 

tetris.rb

require "wx"

class Tetris < Wx::Frame
 
  def initialize(parent)
    super(parent, size: [180,380],
      style: Wx::DEFAULT_FRAME_STYLE ^ Wx::RESIZE_BORDER ^ Wx::MAXIMIZE_BOX)
    init_UI
  end

  def init_UI
    set_title "Tetris"
    centre

    @statusbar = create_status_bar
    @statusbar.set_status_text '0'
    @board = Board.new self
    @board.set_focus
    @board.start
  end
end

class Board < Wx::Panel
  BoardWidth = 10
  BoardHeight = 22
  Speed = 300
  ID_TIMER = 1

  def initialize(*args)
    super(*args)
    init_board
  end
 
  def init_board
    @timer = Wx::Timer.new self, ID_TIMER
    @is_waiting_after_line = false
    @cur_piece = Shape.new
    @next_piece = Shape.new
    @next_piece.set_random_shape
    @curX = 0
    @curY = 0
    @num_lines_removed = 0
    @board = []

    @is_started = false
    @is_paused = false

    evt_paint :on_paint
    evt_timer ID_TIMER, :on_timer
    evt_char_hook :on_key_down
  end

  def clear_board
    (0...BoardHeight * BoardWidth).each do |i|
        @board.append 0
    end
  end

  def draw_square(dc, x, y, shape)
    colors = ['#000000', '#CC6666', '#66CC66', '#6666CC',
              '#CCCC66', '#CC66CC', '#66CCCC', '#DAAA00']

    light = ['#000000', '#F89FAB', '#79FC79', '#7979FC',
              '#FCFC79', '#FC79FC', '#79FCFC', '#FCC600']

    dark = ['#000000', '#803C3B', '#3B803B', '#3B3B80',
              '#80803B', '#803B80', '#3B8080', '#806200']
=begin
    pen = Wx::Pen.new light[shape]
    pen.cap = Wx::CAP_PROJECTING
    dc.pen = pen

    dc.draw_line x, y + square_height - 1, x, y
    dc.draw_line x, y, x + square_width - 1, y

    darkpen = Wx::Pen.new dark[shape]
    darkpen.cap = Wx::CAP_PROJECTING
    dc.pen = darkpen

    dc.draw_line x + 1, y + square_height - 1,
      x + square_width - 1, y + square_height - 1
    dc.draw_line x + square_width - 1,
      y + square_height - 1, x + square_width - 1, y + 1
=end
    dc.pen = Wx::TRANSPARENT_PEN
    dc.brush = Wx::Brush.new colors[shape]
    dc.draw_rectangle x + 1, y + 1,
      square_width - 2, square_height - 2
  end

  def drop_down
    newY = @curY

    while newY > 0
      if not try_move(@cur_piece, @curX, newY - 1)
        break
      end
      newY -= 1
    end

    piece_dropped
  end

  def new_piece
    @cur_piece.set_shape @next_piece.shape
    statusbar = parent.status_bar
    @next_piece.set_random_shape

    @curX = BoardWidth / 2 + 1
    @curY = BoardHeight - 1 + @cur_piece.minY

    if not try_move(@cur_piece, @curX, @curY)
      @cur_piece.set_shape 0
      @timer.stop
      @is_started = false
      statusbar.status_text = "Score: #{@num_lines_removed} - Game over"
    end
  end

  def on_key_down(event)
    if not @is_started or @cur_piece.shape == 0
      event.skip
      return
    end

    keycode = event.key_code

    if keycode == 'P'.ord or keycode == 'p'.ord
      pause
      return
    end

    if @is_paused
      return
    elsif keycode == Wx::K_LEFT
      try_move @cur_piece, @curX - 1, @curY
    elsif keycode == Wx::K_RIGHT
      try_move @cur_piece, @curX + 1, @curY
    elsif keycode == Wx::K_DOWN
      try_move @cur_piece.rotated_right, @curX, @curY
    elsif keycode == Wx::K_UP
      try_move @cur_piece.rotated_left, @curX, @curY
    elsif keycode == Wx::K_SPACE
      drop_down
    elsif keycode == 'D'.ord or keycode == 'd'.ord
      one_line_down
    else
      event.skip
    end
  end

  def on_paint(e)
    paint do |dc|
      size = get_client_size
      board_top = size.height - BoardHeight * square_height
     
      (0...BoardHeight).each do |i|
        (0...BoardWidth).each do |j|
          shape = shape_at j, BoardHeight - i - 1
          if shape && shape != 0
            draw_square dc,
                0 + j * square_width,
                board_top + i * square_height, shape
          end
        end
      end

      if @cur_piece.shape != 0
        (0...4).each do |i|
          x = @curX + @cur_piece.x(i)
          y = @curY - @cur_piece.y(i)

          draw_square dc, 0 + x * square_width,
            board_top + (BoardHeight - y - 1) * square_height,
            @cur_piece.shape
        end
      end
    end
  end

  def on_timer(event)
    if event.id == ID_TIMER
      if @is_waiting_after_line
        @is_waiting_after_line = false
        new_piece
      else
        one_line_down
      end
    else
      event.skip
    end
  end

  def one_line_down
    if not try_move @cur_piece, @curX, @curY - 1
      piece_dropped
    end
  end

  def pause
    if not @is_started
      return
    end

    @is_paused = not @is_paused
    statusbar = self.parent.status_bar

    if @is_paused
      @timer.stop
      statusbar.status_text = 'paused'
    else
      @timer.start Speed
      statusbar.status_text = @num_lines_removed.to_s
    end

    refresh
  end

  def piece_dropped
    (0...4).each do |i|
      x = @curX + @cur_piece.x(i)
      y = @curY - @cur_piece.y(i)
      set_shape_at x, y, @cur_piece.shape
    end

    remove_full_lines

    if not @is_waiting_after_line
      new_piece
    end
  end

  def remove_full_lines
    num_full_lines = 0

    statusbar = parent.status_bar

    rows_to_remove = []

    (0...BoardHeight).each do |i|
      n = 0
      (0...BoardWidth).each do |j|
        if not shape_at(j, i) == 0
          n = n + 1
        end

        if n == 10
          rows_to_remove.append(i)
        end
      end
    end

    rows_to_remove.reverse!

    rows_to_remove.each do |m|
      (m...BoardHeight).each do |k|
        (0...BoardWidth).each do |l|
          set_shape_at l, k, shape_at(l, k + 1)||0
        end
      end

      num_full_lines = num_full_lines + rows_to_remove.length

      if num_full_lines > 0
        @num_lines_removed = @num_lines_removed + num_full_lines
        statusbar.status_text = @num_lines_removed.to_s
        @is_waiting_after_line = true
        @cur_piece.set_shape 0
        refresh
      end
    end
  end

  def set_shape_at(x, y, shape)
    @board[(y * BoardWidth) + x] = shape
  end

  # sütun, satır
  def shape_at(col, row)
    return @board[row * BoardWidth + col]
  end

  def square_height
    return client_size.height / BoardHeight
  end

  def square_width
    return client_size.width / BoardWidth
  end

  def start
    if @is_paused
      return
    end

    @is_started = true
    #@is_waiting_after_line = false
    @num_lines_removed = 0
    clear_board

    new_piece
    @timer.start Speed
  end

  def try_move(new_piece, newX, newY)
    (0...4).each do |i|
      x = newX + new_piece.x(i)
      y = newY - new_piece.y(i)

      if x < 0 or x >= BoardWidth or y < 0 or y >= BoardHeight
        return false
      end

      if shape_at(x, y) != 0
        return false
      end
    end

    @cur_piece = new_piece
    @curX = newX
    @curY = newY
    refresh

    return true
  end
end

class Shape
  @@coords_table = [
    [[0, 0], [0, 0], [0, 0], [0, 0]],
    [[0, -1], [0, 0], [1, 0], [1, 1]],
    [[0, -1], [0, 0], [-1, 0], [-1, 1]],
    [[0, -1], [0, 0], [0, 1], [0, 2]],
    [[-1, 0], [0, 0], [1, 0], [0, 1]],
    [[0, 0], [1, 0], [0, 1], [1, 1]],
    [[-1, -1], [0, -1], [0, 0], [0, 1]],
    [[1, -1], [0, -1], [0, 0], [0, 1]]
  ]

  def initialize
    @coords = [[0, 0], [0, 0], [0, 0], [0, 0]]
    @piece_shape = 0

    set_shape(0)
  end

  def shape
    @piece_shape
  end

  def set_shape(shape)
    table = @@coords_table[shape]
    (0...4).each do |i|
      (0...2).each do |j|
        @coords[i][j] = table[i][j]
      end
    end
    @piece_shape = shape
  end

  def set_random_shape
    set_shape rand(1..7)
  end

  def x(index)
    @coords[index][0]
  end

  def y(index)
    @coords[index][1]
  end

  def setX(index, x)
    @coords[index][0] = x
  end

  def setY(index, y)
    @coords[index][1] = y
  end

  def minX
    m = @coords[0][0]
    (0...4).each do |i|
      m = [m, @coords[i][0]].min
    end
    return m
  end

  def maxX
    m = @coords[0][0]
    (0...4).each do |i|
      m = [m, @coords[i][0]].max
    end

    return m
  end

  def minY
    m = @coords[0][1]
    (0...4).each do |i|
      m = [m, @coords[i][1]].min
    end

    return m
  end

  def maxY
    m = @coords[0][1]
    (0...4).each do |i|
      m = [m, @coords[i][1]].max
    end

    return m
  end

  def rotated_left
    if @piece_shape == 5  # kare şekil
      return self
    end

    result = Shape.new
    result.set_shape @piece_shape

    (0...4).each do |i|
      result.setX(i, y(i))
      result.setY(i, -x(i))
    end

    return result
  end

  def rotated_right

    if @piece_shape == 5  # kare şekil
      return self
    end

    result = Shape.new
    result.set_shape @piece_shape

    (0...4).each do |i|
      result.setX(i, -y(i))
      result.setY(i, x(i))
    end

    return result
  end
end

Wx::App.run {
  Tetris.new(nil).show
}


Yani diyeceğim odur ki Ruby ile masaüstü program yazmak isterseniz WxRuby3 ile kolayca yapabilirsiniz. Kendinize iyi bakın, güzel günlerde görüşmek dileğiyle , şimdilik kalın sağlıcakla..





 


Hiç yorum yok:

Yorum Gönder