Last active
December 3, 2024 13:15
-
-
Save cyang-el/f53d2c1e42a0bdf8c64e964404b5f2a4 to your computer and use it in GitHub Desktop.
a from scrach terminal 2048 game in Ruby :) asciinema: https://asciinema.org/a/693144
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
# frozen_string_literal: true | |
require 'io/console' | |
SIZE = 5 | |
def run | |
$stdin.echo = false | |
board = init_board | |
render!(board) | |
loop do | |
render!(update(board)) | |
end | |
ensure | |
$stdin.echo = true | |
end | |
def init_board | |
board = [] | |
4.times { board << [nil, nil, nil, nil] } # 4 x 4 game | |
srand(Time.now.to_i) | |
rand(0..3) | |
pos1 = rand(0..3) | |
pos2 = (pos1 + 2) % 4 | |
board[pos1][pos1] = 2 | |
board[pos2][pos2] = 2 | |
board | |
end | |
def update(board) | |
key_in = $stdin.getch | |
# ^ blocking | |
exit if key_in.eql?('q') | |
return board unless %w[h j k l].include? key_in | |
update_board(key_in, board) | |
end | |
def update_board(key_in, board) | |
handler = { | |
'j' => method(:down!), | |
'k' => method(:up!), | |
'h' => method(:left!), | |
'l' => method(:right!) | |
}[key_in] | |
raise RuntimeError if handler.nil? | |
handler.call(board) | |
if win?(board) | |
raise 'congrats, you win!!!!!' | |
elsif fail?(board) | |
raise 'oh no, you lost :(' | |
else | |
add_2!(board) | |
end | |
board | |
end | |
def up!(board) | |
move_along_x!(board, true) | |
end | |
def down!(board) | |
move_along_x!(board, false) | |
end | |
def right!(board) | |
move_along_y!(board, false) | |
end | |
def left!(board) | |
move_along_y!(board, true) | |
end | |
def win?(board) | |
board.any? { |row| row.any? { |n| n.eql? 2048 } } | |
end | |
def fail?(board) | |
board.all? { |row| row.all? { |n| !n.nil? } } | |
end | |
def add_2!(board) | |
nil_coords = [] | |
board.each_with_index do |row, y| | |
row.each_with_index do |val, x| | |
nil_coords << [x, y] if val.nil? | |
end | |
end | |
ty, tx = nil_coords.sample | |
board[ty][tx] = 2 | |
end | |
def column_at(board, x) | |
0.upto(3).each.map { |y| board[y][x] }.filter { |v| !v.nil? } | |
end | |
def collect(line) | |
0.upto(line.length - 1).each do |i| | |
if line[i].eql?(line[i + 1]) | |
line[i] = line[i] + line[i + 1] | |
line[i + 1] = 0 | |
end | |
end | |
line.filter(&:positive?) | |
end | |
def fold_column_at(board, x, is_up) | |
column = is_up ? column_at(board, x) : column_at(board, x).reverse | |
column = collect(column) | |
is_up ? column : column.reverse | |
end | |
def move_along_x!(board, is_up) | |
# move | |
0.upto(3).each do |x| | |
column = fold_column_at(board, x, is_up) | |
if is_up | |
column.fill(nil, column.length...4) | |
else | |
column = Array.new(4 - column.length) + column | |
end | |
0.upto(3).each { |y| board[y][x] = column[y] } | |
end | |
end | |
def fold_row_at(board, y, is_left) | |
row = board[y].filter { |v| !v.nil? } | |
row = is_left ? row : row.reverse | |
row = collect(row) | |
is_left ? row : row.reverse | |
end | |
def move_along_y!(board, is_left) | |
0.upto(3).each do |y| | |
row = fold_row_at(board, y, is_left) | |
if is_left | |
row.fill(nil, row.length...4) | |
else | |
row = Array.new(4 - row.length) + row | |
end | |
board[y] = row | |
end | |
end | |
def map_bg_color(val) | |
case val | |
when nil | |
"\e[42m" # Green | |
when 2 | |
"\e[105m" # Bright Magenta | |
when 4 | |
"\e[96m" # Bright Cyan | |
when 8 | |
"\e[43m" # Yellow | |
when 16 | |
"\e[104m" # Bright Blue | |
when 32 | |
"\e[45m" # Magenta | |
when 64 | |
"\e[46m" # Cyan | |
when 128 | |
"\e[101m" # Bright Red | |
when 256 | |
"\e[102m" # Bright Green | |
when 512 | |
"\e[103m" # Bright Yellow | |
when 1024 | |
"\e[44m" # Blue | |
when 2048 | |
"\e[41m" # Red | |
else | |
raise ArgumentError, 'only nil or 2048 nums can be colored' | |
end | |
end | |
def color_txt(txt, bg_color) | |
raise ArgumentError if txt.nil? | |
reset = "\e[0m" | |
"#{bg_color}#{txt}#{reset}" | |
end | |
def pos_block(pos, size = SIZE) | |
pos_start = pos * size | |
pos_end = pos_start + size - 1 | |
[pos_start, pos_end] | |
end | |
def pos_val_in_row(val, xhead, xtail) | |
txt_len = val.to_s.length | |
center = (xhead + xtail) / 2 | |
center - txt_len / 2 | |
end | |
def center_row(ytop, ybottom) | |
(ytop + ybottom) / 2 | |
end | |
def place_block!(pos_x, pos_y, val, color, canvas) | |
ytop, ybottom = pos_block(pos_y) | |
xhead, xtail = pos_block(pos_x) | |
val = val.nil? ? ' ' : val | |
(ytop..ybottom).each do |y| | |
(xhead..xtail).each do |x| | |
canvas[y][x] = color_txt(' ', color) | |
end | |
end | |
place_txt!(val, xhead, xtail, | |
ytop, ybottom, color, canvas) | |
end | |
def place_txt!(val, xhead, xtail, | |
ytop, ybottom, color, canvas) | |
return if val.nil? | |
xtxt = pos_val_in_row val, xhead, xtail | |
ytxt = center_row ytop, ybottom | |
(0...val.to_s.length).each do |idx| | |
canvas[ytxt][xtxt + idx] = color_txt(val.to_s[idx], color) | |
end | |
end | |
def place_board!(board, canvas) | |
board.each_with_index do |row, y| | |
row.each_with_index do |val, x| | |
color = map_bg_color(val) | |
place_block! x, y, val, color, canvas | |
end | |
end | |
end | |
def a_clean_canvas(size = SIZE) | |
# 4 x 4 game | |
len = size * 4 | |
Array.new(len) { Array.new(len, nil) } | |
end | |
def print_canvas!(canvas) | |
canvas.each { |row| puts row.join } | |
end | |
def render_board(board) | |
canvas = a_clean_canvas | |
place_board! board, canvas | |
print_canvas! canvas | |
end | |
def render!(board) | |
print "\033[2J" # clear screen | |
print "\033[H" # cursor to the top left | |
render_board board | |
print "\n" | |
end | |
run if __FILE__ == $PROGRAM_NAME |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment