Skip to content

Instantly share code, notes, and snippets.

@itarato
Last active January 27, 2024 15:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save itarato/8e4b34201a9a4054151c81e934d658eb to your computer and use it in GitHub Desktop.
Save itarato/8e4b34201a9a4054151c81e934d658eb to your computer and use it in GitHub Desktop.
# Terminal Tetris
#
# Usage:
#
# ```bash
# ruby tetris.rb <WIDTH> <HEIGHT> <SPEED>
# ```
#
# Control:
# - a: left
# - d: right
# - n: rotate left
# - m: rotate right
# - q: quit
module Util
class << self
def darwin?
return @darwin if defined?(@darwin)
@darwin = !!(RUBY_PLATFORM =~ /darwin/)
end
def linux?
return @linux if defined?(@linux)
@linux = !!(RUBY_PLATFORM =~ /linux/)
end
def clear_terminal
if linux?
system("clear")
elsif darwin?
print("\x1b[2J")
else
print("System #{RUBY_PLATFORM} not supported\n")
exit
end
end
def start_raw_mode
system('stty', 'raw', '-echo')
end
def end_raw_mode
system('stty', '-raw', 'echo')
end
def read_char
STDIN.getc
rescue
nil
end
end
end
class Game
COLOR_FREE = -1
COLORS = [31, 32, 33, 34, 35, 36, 37, 91, 92, 93, 94, 95, 96, 97]
REMOVAL_CLOCK_MOD = 30
PIXEL_WIDTH = 2
ELEMS = [
[[0, 0], [0, 1], [0, 2], [0, 3]],
[[0, 0], [0, 1], [0, 2], [1, 2]],
[[0, 0], [0, 1], [1, 0], [1, 1]],
[[0, 0], [1, 0], [1, 1], [2, 1]],
[[1, 0], [2, 0], [0, 1], [1, 1]],
[[0, 0], [1, 0], [2, 0], [1, 1]],
]
def initialize(gridw, gridh, speed)
@gridw = gridw
@gridh = gridh
@speed = speed
@map = @gridh.times.map { [COLOR_FREE] * @gridw }
@drop_ticker = 0
@removal_ticker = REMOVAL_CLOCK_MOD
@removals = []
@elem_dims = ELEMS.map do |coords|
maxx = 0
maxy = 0
coords.each do |x, y|
maxx = x if x > maxx
maxy = y if y > maxy
end
[maxx, maxy]
end
pick_new_elem
end
def update(input)
unpaint_current_elem
rotate(-1) if input == 'n'
rotate(+1) if input == 'm'
move_x(-1) if input == 'a'
move_x(+1) if input == 'd'
if removal?
@removal_ticker += 1
clear_removals if !removal?
end
@drop_ticker += 1 if !removal?
@drop_ticker += 20 if input == 's' && !removal?
if @drop_ticker >= @speed
@drop_ticker = 0
if !drop_elem
paint_current_elem
pick_new_elem
@removal_ticker = 0 if !(@removals = check_lines).empty?
if !coord_free?(current_elem_real_coords)
return false
end
end
end
paint_current_elem
true
end
def draw
Util.clear_terminal
print("+#{ '-' * @gridw * PIXEL_WIDTH}+\r\n")
@map.each_with_index do |row, y|
line = row.map do |color|
token = removal? && @removals.include?(y) && (@removal_ticker / 4) % 2 == 0 ? '▯' : '█'
(color == -1 ? " " : "\x1B[#{COLORS[color]}m#{token}\x1B[0m") * 2
end.join
print("|#{line}|\r\n")
end
print("+#{ '-' * @gridw * PIXEL_WIDTH}+\r\n")
end
private
def removal?
@removal_ticker < REMOVAL_CLOCK_MOD
end
def clear_removals
offs = 0
@removals.reverse.each do |remove_y|
realy = remove_y + offs
realy.downto(1) do |y|
@map[y] = @map[y - 1]
end
@map[0] = [COLOR_FREE] * @gridw
offs += 1
end
@removals = []
end
def check_lines
@gridh.times.select do |y|
@map[y].all? { |color| color != COLOR_FREE }
end
end
def drop_elem
@curr_y += 1
if !coord_free?(current_elem_real_coords)
@curr_y -= 1
false
else
true
end
end
def pick_new_elem
@curr_idx = rand(ELEMS.size)
@curr_x = (@gridw - @elem_dims[@curr_idx][0]) / 2
@curr_y = 0
@curr_rot = 0
@curr_color = rand(COLORS.size)
end
def move_x(offs)
@curr_x += offs
@curr_x -= offs if !coord_free?(current_elem_real_coords)
end
def rotate(offs)
@curr_rot += offs
@curr_rot -= offs if !coord_free?(current_elem_real_coords)
end
def unpaint_current_elem
current_elem_real_coords.each do |x, y|
@map[y][x] = COLOR_FREE
end
end
def paint_current_elem
current_elem_real_coords.each do |x, y|
@map[y][x] = @curr_color
end
end
def coord_free?(coords)
coords.all? do |x, y|
x >= 0 && x < @gridw && y >= 0 && y < @gridh && @map[y][x] == COLOR_FREE
end
end
def current_elem_real_coords
get_real_coords(ELEMS[@curr_idx])
end
def get_real_coords(coords)
coords.map do |xoffs, yoffs|
xshift, yshift = @elem_dims[@curr_idx]
xshift /= 2
yshift /= 2
xoffs -= xshift
yoffs -= yshift
xoffs, yoffs = case @curr_rot % 4
when 0 then [xoffs, yoffs]
when 1 then [yoffs, -xoffs]
when 2 then [-xoffs, -yoffs]
when 3 then [-yoffs, xoffs]
else raise("Invalid rotation")
end
xoffs += xshift
yoffs += yshift
x = @curr_x + xoffs
y = @curr_y + yoffs
[x, y]
end
end
end
class Engine
def initialize
gridw = [(ARGV[0] || 10).to_i, 5].max
gridh = [(ARGV[1] || 20).to_i, 5].max
speed = [(ARGV[2] || 40).to_i, 5].max
@game = Game.new(gridw, gridh, speed)
STDIN.timeout = 0
end
def loop
Util.start_raw_mode
while true
input = Util.read_char
break if input == 'q'
if !@game.update(input)
break
end
@game.draw
sleep(0.016)
end
Util.end_raw_mode
print("GAME OVER\n")
end
end
Engine.new.loop
@itarato
Copy link
Author

itarato commented Jan 27, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment