Last active
May 31, 2024 13:43
-
-
Save itarato/8e4b34201a9a4054151c81e934d658eb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 [email protected](input) | |
break | |
end | |
@game.draw | |
sleep(0.016) | |
end | |
Util.end_raw_mode | |
print("GAME OVER\n") | |
end | |
end | |
Engine.new.loop |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://asciinema.org/a/UumspFYtvtJaXjBWjaRTxR33f