Ruby Tetris (Expert 23 Solution)

James Edward Gray II <james-AUi9nNu29NfWNcQ1/[email protected]>
Newsgroups gmane.comp.lang.perl.qotw.discuss
Message-ID <[email protected]>
Since different language solutions are growing more common, I'll submit 
my Ruby solution.

I've been learning Ruby these past few weeks, so it's still probably 
rough around the edges.  That would be my fault, not Ruby's, which is a 
fun language that I think is worth a look.

My solution only implements the first milestone, because that was a lot 
of work and I'm tired now.  <laughs>

I built it with Ruby 1.8.2 preview 2, but I BELIEVE it will run on 
1.6.x (standard in the mentioned Mac OS X testing environment).  All 
you would have to do is:

% ruby unix_term_tetris.rb

WARNING:  As the name suggests, the interface I built only runs on 
Unix.  It would be trivial to convert it to Windows, but since I'm not 
a Windows guy it's someone else's triviality.  <laughs>

My solution uses two files, which should be placed in the same 
directory.

Enjoy.

James Edward Gray II

===  unix_term_tetris.rb  ===

#!/usr/bin/ruby -w

require "io/wait"

require "tetris"

system "stty raw -echo"

game = Tetris::Game.new

system "clear"
puts game.draw

game.start

loop do
	if game.run
		system "clear"
		puts game.draw
	end
	
	break if game.over?
	
	if STDIN.ready?
		case STDIN.getc
			when ?q, ?Q, ?\C-c
				break
			when ?z, ?Z
				game.move_left
			when ?x, ?X
				game.move_right
			when ?/, ??
				game.move_drop
			when ?', ?"
				game.move_rotate
		end
		system "clear"
		puts game.draw
	end
end

END { system "stty -raw echo" }

__END__

===  tetris.rb  ===

#!/usr/bin/ruby

module Tetris
	class Block
		@@blocks = [ [ ["#", "#"],
					   ["#", "#"] ],
					 [ [" ", "@", "@"],
					   ["@", "@", " "] ],
					 [ ["%", "%", " "],
					   [" ", "%", "%"] ],
					 [ ["*", "*", "*"],
					   ["*", " ", " "] ],
					 [ ["&", "&", "&"],
					   [" ", " ", "&"] ],
					 [ ["$", "$", "$"],
					   [" ", "$", " "] ],
					 [ ["+", "+", "+", "+"] ] ]
					
		attr_reader :x, :y

		def initialize(x = nil, y = nil, type = rand(7))
			@x = if x.nil?
					 if type == 6
						 3
					 else
						 4
					end
				 else
					 x
				 end
			@y = if x.nil?
					 if type == 6
						 0
					 else
						 -1
					end
				 else
					 y
				 end
			@dir = 1
			
			@type = type
			@block = @@blocks[@type]
		end
		
		def drop
			@y += 1
		end
		
		def move_left
			@x -= 1
		end
		
		def move_right
			@x += 1
		end
		
		def rotate
			copy = [ ]
			@block[0].size.times { copy.push((" " * @block.size).split "") }
			@block.each_with_index do |row, y|
				row.each_with_index do |square, x|
					copy[x][copy[0].size - 1 - y] = square
				end
			end
			@block = copy
			@dir = if @dir == 4 then 1 else @dir + 1 end
			
			case @type
				when 6
					case @dir
						when 1, 3
							@x -= 2
							@y += 2
						when 2, 4
							@x += 2
							@y -= 2
					end
				when 1, 2
					case @dir
						when 1, 3
							@x -= 1
							@y += 1
						when 2, 4
							@x += 1
							@y -= 1
					end
				when 3, 4, 5
					case @dir
						when 1
							@x -= 1
							@y += 1
						when 2
							@y -= 1
						when 4
							@x += 1
					end
			end
		end
		
		def to_a
			return Marshal.load(Marshal.dump(@block))
		end
		
		def to_s
			str = ""
			@block.each do |row|
				str += row.join() + "\r\n"
			end
			return str
		end
	end

	class Board
		attr_reader :line_count, :touch_count
		
		def initialize(block = Block.new, next_block = Block.new)
			@board = [ ]
			1.upto 20 do
				row = (" " * 10).split ""
				@board << row
			end
			
			@block = block
			@next_block = next_block
			
			@line_count = 0
			@touch_count = 0
		end
		
		def block=(block)
			@board = to_a
			
			@line_count = 0
			0.upto @board.size - 1 do |i|
				unless @board[i].include? " "
					@board.delete_at i
					@board.unshift((" " * 10).split "")
					@line_count += 1
				end
			end
			
			@touch_count = 0
			count_block = @block.to_a
			count_block.each_with_index do |row, i|
				next if @block.y + i < 0
				row.each_with_index do |square, j|
					if row[j] != " "
						if j == 0 and ( @block.x - 1 == -1 or
						   @board[@block.y + i][@block.x - 1] != " " )
							@touch_count += 1
						end
						if j == row.size - 1 and ( @block.x + 1 + j >= 10 or
						   @board[@block.y + i][@block.x + 1 + j] != " " )
							@touch_count += 1
						end
						if i == 0 and ( @block.y - 1 == -1 or
						   @board[@block.y - 1][@block.x + j] != " " )
							@touch_count += 1
						end
						if i == count_block.size - 1 and
						   ( @block.y + 1 + i >= 20 or
						   @board[@block.y + 1 + i][@block.x + j] != " " )
							@touch_count += 1
						end
					end
				end
			end

			@block = @next_block
			@next_block = block
		end

		def can_drop?
			block = @block.to_a
			block.each_with_index do |row, i|
				next if @block.y + 1 + i < 0
				return false if @block.y + 1 + i > 19
				row.each_with_index do |square, j|
					if row[j] != " " and
					   @board[@block.y + 1 + i][@block.x + j] != " "
						return false
					end
				end
			end
			
			return true
		end

		def can_left?
			block = @block.to_a
			block.each_with_index do |row, i|
				next if @block.y + i < 0
				row.each_with_index do |square, j|
					return false if @block.x - 1 + j < 0
					if row[j] != " " and
					   @board[@block.y + i][@block.x - 1 + j] != " "
						return false
					end
				end
			end
			
			return true
		end

		def can_right?
			block = @block.to_a
			block.each_with_index do |row, i|
				next if @block.y + i < 0
				row.each_with_index do |square, j|
					return false if @block.x + 1 + j >= 10
					if row[j] != " " and
					   @board[@block.y + i][@block.x + 1 + j] != " "
						return false
					end
				end
			end
			
			return true
		end

		def can_rotate?
			begin
				@block.rotate
				block = @block.to_a
				block.each_with_index do |row, i|
					next if @block.y + i < 0
					return false if @block.y + i > 19
					row.each_with_index do |square, j|
						return false if @block.x + j < 0
						return false if @block.x + j >= 10
						if row[j] != " " and
						   @board[@block.y + i][@block.x + j] != " "
							return false
						end
					end
				end
				
				return true
			ensure
				3.times { @block.rotate }
			end
		end

		def drop
			if can_drop?
				@block.drop
				return true
			else
				return false
			end
		end
		
		def full?
			if @board[0].join("") !~ /^ {10}$/
				return true
			else
				return false
			end
		end
		
		def move_left
			if can_left?
				@block.move_left
				return true
			else
				return false
			end
		end
		
		def move_right
			if can_right?
				@block.move_right
				return true
			else
				return false
			end
		end
		
		def rotate
			if can_rotate?
				@block.rotate
				return true
			else
				return false
			end
		end
		
		def to_a
			copy = Marshal.load(Marshal.dump(@board))
			
			block = @block.to_a
			block.each_with_index do |row, i|
				next if @block.y + i < 0
				row.each_with_index do |square, j|
					next if row[j] == " "
					copy[@block.y + i][@block.x + j] = square
				end
			end
			
			return copy
		end
		
		def to_s(level, lines, score)
			str = ""
			to_a.each do |row|
				str += "|" + row.join() + "|\r\n"
			end
			str += ("-" * 12) + "\r\n"
			
			details = [ "Level #{level}", "Lines #{lines}", "Score #{score}",
						"", "Next", "" ]
			@next_block.to_a.each do |row|
				details.push(row.join(""))
			end
			details.push("")
			
			display = str.split(/\r\n/).zip(details)
			display.collect! do |e|
				if e[1].nil?
					e[0] + "\r\n"
				else
					e[0] + "  " + e[1] + "\r\n"
				end
			end
			
			return display.join("");
		end
	end
	
	class Game
		attr_reader :board
		
		def initialize
			@board = Board.new
			
			@piece_floating = false
		
			@level = 1
			@lines = 0
			@score = 0
		end
		
		def draw
			return @board.to_s(@level, @lines, @score)
		end
		
		def move_drop
			@board.drop
		end
		
		def move_left
			@board.move_left
		end
		
		def move_right
			@board.move_right
		end
		
		def move_rotate
			@board.rotate
		end
		
		def over?
			return @board.full?
		end
		
		def run
			if (@last_drop + (1 / 1.3 ** (@level - 1))) - Time.now <= 0
				unless @board.drop
					if @piece_floating
						@board.block = Block.new
						@lines += @board.line_count
						@score += @board.touch_count
						@score += @board.line_count * 10 * @level
						@level += 1 if @lines > 0 and @lines % 10 == 0
						@piece_floating = false
					else
						@piece_floating = true
					end
				end
				@last_drop = Time.now
				return true
			end
			return false
		end
		
		def start
			@last_drop = Time.now
		end
	end
end

__END__
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.