Skip to content

Instantly share code, notes, and snippets.

@cyang-el
Last active December 3, 2024 13:15
Show Gist options
  • Save cyang-el/f53d2c1e42a0bdf8c64e964404b5f2a4 to your computer and use it in GitHub Desktop.
Save cyang-el/f53d2c1e42a0bdf8c64e964404b5f2a4 to your computer and use it in GitHub Desktop.

Revisions

  1. cyang-el revised this gist Dec 3, 2024. No changes.
  2. cyang-el created this gist Dec 3, 2024.
    260 changes: 260 additions & 0 deletions 2048.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,260 @@
    # 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