gameboard.rb
require 'Qt'
require './piece.rb'
$gravity = 9.8
FPS = 30
# overloaded Qt functions are camelcase
class Gameboard < Qt::Widget
slots 'do_physics()','highlight_animation()'
signals 'mouseMove(int, int)','pointsChanged(int)','highlightedPointsChanged(int)','info(int,int)','done(int,bool)'
attr_reader :points
Size = Struct.new(:width,:height)
Column = Struct.new(:column,:px,:x_vel,:falling)
def initialize parent, gameboard_width, gameboard_height, piece_size, seed, piece_type
super(parent)
setMouseTracking true
setPalette(Qt::Palette.new( Qt::Color.new(20,30,30)))
setAutoFillBackground(true)
setFocus Qt::OtherFocusReason
@last_mouse_pos = []
@last_over_piece = [] # last piece the mouse was over
@points = 0
@grid_width = gameboard_width
@grid_height = gameboard_height
@last_column = @grid_width-1
@piece_size = Size.new(piece_size,piece_size)
@width_px = @piece_size.width*@grid_width
@height_px = @piece_size.height*@grid_height
resize(@width_px, @height_px)
@grid = []
@grid_height.times{ @grid << Array.new(@grid_width) } # make 2 dimensional grid, accessed as @grid[y][x]
@highlighted_pieces = Array.new
Kernel.srand(seed)
#Kernel.srand(235) #for testing, use the same grid
generate_grid piece_type
@highlight_timer = Qt::Timer.new self
@highlight_timer.setInterval(1000/FPS)
@highlight_timer.connect(SIGNAL :timeout) do
@highlighted_pieces.each do |piece|
piece.advance
end
end
@falling_pieces = []
@physics_timer = Qt::Timer.new
@physics_timer.setInterval(1000/FPS)
connect(@physics_timer, SIGNAL('timeout()'), self, SLOT('do_physics()'))
@falling_columns = []
#@timer = Qt::Time.new timer for testing
@fall_stop_cnt = 0
@falling = false
@falling_sideways = false
@deleting = false
end
def below piece
for i in (piece.y+1)...@grid_height
next if @grid[i][piece.x].nil?
return @grid[i][piece.x]
end
nil
end
def column_to_left col
(col-1).downto(0) do |i|
return i if not column_empty? i
end
nil
end
def do_physics
damping = 0.5
sideways_damping = 0.5
gravity = 2
# do the integration
@falling_pieces.each do |piece|
next unless piece.falling
piece.y_vel += gravity
piece.py += piece.y_vel
end
# do collision detection
@falling_pieces.each do |piece|
next unless piece.falling
y1 = piece.y*piece.height + piece.py
y2 = y1+piece.height
# do collision detection
# can only collide with piece above or below it
if piece.y_vel > 0 #moving down
below = below piece # could do caching for some speed up
if below.nil? #bottom row there's no pieces below
if y2 >= @grid_height*@piece_size.height
piece.py -= y2-(@grid_height*@piece_size.height)
if piece.y_vel < gravity
piece.y_vel = 0
piece.falling = false
@fall_stop_cnt += 1
end
piece.y_vel *= -1*damping
end
elsif y2 > (below.y*below.height+below.py) #collision happened
piece.py -= y2-(below.y*below.height+below.py)
if piece.y_vel < gravity and not below.falling
piece.y_vel = 0
piece.falling = false
@fall_stop_cnt += 1
end
piece.y_vel *= -1*damping
end
end
end
#move pieces
@falling_pieces.each do |piece|
next unless piece.falling
x = piece.x*piece.width
y = piece.y*piece.height + piece.py
piece.move(x,y)
end
#do "falling" columns
@falling_columns.each do |col|
col.x_vel += gravity
col.px -= col.x_vel
end
@falling_columns.each do |col|
lcolumn = column_to_left col.column
if lcolumn.nil?
overlap = col.column*@piece_size.width+col.px
if col.column*@piece_size.width+col.px < 0 #collision with edge of gameboard
col.px -= overlap #overlap negative
if col.x_vel < gravity
col.falling = false
end
col.x_vel *= -1*sideways_damping
end
else
adjustment = 0
ind = @falling_columns.index{|col| col.column == lcolumn}
unless ind.nil?
adjustment = @falling_columns[ind].px #adjustment is if column to the left is also moving
end
lbound = (lcolumn+1)*@piece_size.width + adjustment
overlap = lbound - (col.column*@piece_size.width+col.px)
if overlap > 0 #collision happened
col.px += overlap
if col.x_vel < gravity
col.falling = false
end
col.x_vel *= -1*sideways_damping
end
end
end
@falling_columns.each do |col|
x = col.column*@piece_size.width + col.px
for i in 0...@grid_height
piece = @grid[i][col.column]
unless piece.nil?
piece.move(x, piece.pos.y)
end
end
end
vert_falling = @falling_pieces.length != @fall_stop_cnt
horiz_falling = @falling_columns.select{|c| c.falling == true}.length > 0
if not vert_falling and not horiz_falling
# update @grid and pieces
update_grid_after_fall
@physics_timer.stop
@fall_stop_cnt = 0
@falling_pieces.clear
@falling = false
@falling_sideways = false
@falling_columns.clear
#highlight pieces under the mouse
lx = @last_mouse_pos[0]/@piece_size.width
ly = @last_mouse_pos[1]/@piece_size.height
last_piece = @grid[ly][lx] unless (ly >= @grid_height) or (lx>=@grid_width)
dehighlight_pieces
highlight_pieces last_piece unless last_piece.nil?
if done?
cleared = @grid.flatten.reject{|spot| spot.nil?}.length == 0
emit done(@points,cleared)
end
end
end
def update_grid_after_fall
for x in (0...@grid_width)
(@grid_height-1).downto(1) do |y|
next unless @grid[y][x].nil?
(y-1).downto(0) do |i|
next if @grid[i][x].nil?
@grid[y][x] = @grid[i][x]
@grid[i][x] = nil
@grid[y][x].y = y
break
end
end
end
@falling_pieces.each do |piece|
piece.py = 0
# piece.move(piece.x*@piece_size.width,piece.y*@piece_size.height)
end
@falling_columns.each do |col|
for i in 0...@grid_height
next if @grid[i][col.column].nil?
piece = @grid[i][col.column]
@grid[i][col.column] = nil
piece.x -= (col.px/@piece_size.width).round.abs
@grid[i][piece.x] = piece
end
end
end
def grid_check
for x in (0...@grid_width)
for y in (0...@grid_height)
next if @grid[y][x].nil?
return false if @grid[y][x].x != x or @grid[y][x].y != y
end
end
true
end
def resizeEvent event
"puts gb resize"
end
def keyPressEvent event
if event.key == Qt::Key_Z and event.modifiers == Qt::ControlModifier
puts "undo"
else
event.ignore #pass to mainwindow
end
end
def paintEvent event
painter = Qt::Painter.new self
draw_background painter
painter.end
end
def leaveEvent event
unless @deleting #or @falling # this is because when you hide the widgets it genereates a leave event
# and clears the @highlighted_pieces array
# de-hightlight pieces
dehighlight_pieces
end
end
def mousePressEvent event
return if @falling or @falling_sideways
delete_and_fall_pieces
update
end
def mouseMoveEvent event
# handle the event processing ourselves because their seems to be a glitch in using the
# qwidget's enter and leave events
@last_mouse_pos = [event.x,event.y]
return if @falling or @falling_sideways
local_x = event.x/@piece_size.width
local_y = event.y/@piece_size.height
return if local_x >= @grid_width or local_y>=@grid_height
if (@last_over_piece[0] != local_x) or (@last_over_piece[1] != local_y)
#equivalent to a leave event
@last_over_piece = [local_x,local_y]
unless @deleting #or @falling # this is because when you hide the widgets it genereates a leave event
# and clears the @highlighted_pieces array
# de-hightlight pieces
dehighlight_pieces
end
unless @grid[local_y][local_x].nil?
if not @grid[local_y][local_x].highlighted #don't rerun highlight method if already highlighted
highlight_pieces @grid[local_y][local_x] # pass the starting piece to it.
end
end
end
end
def draw_background painter
# draw a grid for the background
painter.save
pen = Qt::Pen.new(Qt::Brush.new(Qt::Color.new 120,120,120),1,Qt::DashLine)
painter.setPen pen
for x in (1...@grid_width)
painter.drawLine( x*(@piece_size.width), 5, x*(@piece_size.width), @height_px -5)
end
for y in (1..@grid_height-1)
painter.drawLine 5, y*(@piece_size.height), @width_px-5, y*(@piece_size.height)
end
painter.restore
end
def move_widgets
(0...@grid_width).each do |x| # next_piece = nil
(0...@grid_height).each do |y|
piece = @grid[y][x]
unless piece.nil?
piece.move(x*(piece.width),y*piece.height)
end
end
end
end
def window_resize width, height
for x in 0...@grid_width
for y in 0...@grid_height
unless @grid[y][x].nil?
@grid[y][x].width = @piece_size.width
@grid[y][x].height = @piece_size.height
@grid[y][x].resize(@piece_size.width,@piece_size.height)
end
end
end
move_widgets
end
def highlight_pieces piece
@highlighted_pieces << piece
piece.highlighted = true
#recursive flood fill algorithm
walk piece
if @highlighted_pieces.length > 1
@highlighted_pieces.each{ |piece| piece.update }
val = (@highlighted_pieces.length-1)**2
@highlight_timer.start
emit highlightedPointsChanged(val)
else
@highlight_timer.stop
@highlighted_pieces[0].dehighlight
@highlighted_pieces.clear
end
end
def walk piece
x = piece.x
y = piece.y
# down
unless y+1 == @grid_height or @grid[y+1][x].nil? or @grid[y+1][x].highlighted
next_piece = @grid[y+1][x]
if piece.color == next_piece.color
next_piece.highlight
@highlighted_pieces << next_piece
walk next_piece
end
end
# left
unless x-1 < 0 or @grid[y][x-1].nil? or @grid[y][x-1].highlighted
next_piece = @grid[y][x-1]
if piece.color == next_piece.color
next_piece.highlight
@highlighted_pieces << next_piece
walk next_piece
end
end
# up
unless y-1 < 0 or @grid[y-1][x].nil? or @grid[y-1][x].highlighted
next_piece = @grid[y-1][x]
if piece.color == next_piece.color
next_piece.highlight
@highlighted_pieces << next_piece
walk next_piece
end
end
# right
unless x+1 == @grid_width or @grid[y][x+1].nil? or @grid[y][x+1].highlighted
next_piece = @grid[y][x+1]
if piece.color == next_piece.color
next_piece.highlight
@highlighted_pieces << next_piece
walk next_piece
end
end
end
def done?
for x in (0...@grid_width)
for y in (0...@grid_height)
unless @grid[y][x].nil?
unless @grid[y][x+1].nil?
return false if @grid[y][x].color == @grid[y][x+1].color #right
end
next if y == @grid_height-1
unless @grid[y+1][x].nil?
return false if @grid[y][x].color == @grid[y+1][x].color #below
end
end
end
end
return true
end
def dehighlight_pieces
@highlighted_pieces.each do |piece|
piece.dehighlight
piece.update
end
@highlight_timer.stop
@highlighted_pieces.clear
end
def generate_grid piece_type
(0...@grid_width).each do |x|
(0...(@grid_height)).each do |y|
piece = Piece.new self, @piece_size, x, y, piece_type
@grid[y][x] = piece
end
end
end
def delete_and_fall_pieces
return if @highlighted_pieces.length == 1
@deleting = true
@points = @points + @highlighted_pieces.length**2
emit pointsChanged(@points)
emit highlightedPointsChanged(0)
#delete pieces
@highlighted_pieces.each do |piece|
@grid[piece.y][piece.x] = nil
piece.hide
end
#fall pieces
(0...@grid_width).each do |x|
(@grid_height-1).downto(1) do |y|
if @grid[y][x].nil?
#find next one
(y-1).downto(0) do |i|
unless @grid[i][x].nil?
@falling_pieces << @grid[i][x]
@falling = true
@grid[i][x].falling = true
end
end
break
end
end
end
for a in (0...@grid_width)
#find first empty column
next unless column_empty? a
for b in (a+1)...@grid_width
unless column_empty? b
@falling_columns << Column.new(b,0,0,true)
@falling_sideways = true
end
end
break
end
unless @falling_pieces.empty? and @falling_columns.empty?
@physics_timer.start
@falling = true
else
if done?
cleared = @grid.flatten.reject{|spot| spot.nil?}.length == 0
emit done(@points,cleared)
end
end
@highlighted_pieces.clear
@deleting = false
end
def column_empty? col
for i in (0...@grid_height)
return false if not @grid[i][col].nil?
end
true
end
end