# Based on 2048 by Gabriele Cirulli - gabrielecirulli.github.io/2048 require "colorize" enum Action Up Down Left Right Escape Unknown end module Screen TILES = { 0 => {Colorize::ColorANSI::White, nil}, 2 => {Colorize::ColorANSI::Black, Colorize::ColorANSI::White}, 4 => {Colorize::ColorANSI::Blue, Colorize::ColorANSI::White}, 8 => {Colorize::ColorANSI::Black, Colorize::ColorANSI::Yellow}, 16 => {Colorize::ColorANSI::White, Colorize::ColorANSI::Red}, 32 => {Colorize::ColorANSI::Black, Colorize::ColorANSI::Red}, 64 => {Colorize::ColorANSI::White, Colorize::ColorANSI::Magenta}, 128 => {Colorize::ColorANSI::Red, Colorize::ColorANSI::Yellow}, 256 => {Colorize::ColorANSI::Magenta, Colorize::ColorANSI::Yellow}, 512 => {Colorize::ColorANSI::White, Colorize::ColorANSI::Yellow}, 1024 => {Colorize::ColorANSI::White, Colorize::ColorANSI::Yellow}, 2048 => {Colorize::ColorANSI::White, Colorize::ColorANSI::Yellow}, 4096 => {Colorize::ColorANSI::White, Colorize::ColorANSI::Black}, 8192 => {Colorize::ColorANSI::White, Colorize::ColorANSI::Black}, 16384 => {Colorize::ColorANSI::White, Colorize::ColorANSI::Black}, 32768 => {Colorize::ColorANSI::White, Colorize::ColorANSI::Black}, 65536 => {Colorize::ColorANSI::White, Colorize::ColorANSI::Black}, } def self.colorize_for(tile, &) fg_color, bg_color = TILES[tile] color = Colorize.with.fore(fg_color) color = color.back(bg_color) if bg_color color.surround do yield end end def self.clear print "\e[2J\e[1;1H" end def self.read_keypress STDIN.raw do |io| buffer = Bytes.new(3) bytes_read = io.read(buffer) return Action::Unknown if bytes_read == 0 input = String.new(buffer[0, bytes_read]) case input when "\e[A", "w" Action::Up when "\e[B", "s" Action::Down when "\e[C", "d" Action::Right when "\e[D", "a" Action::Left when "\e", "\u{3}", "q", "Q" Action::Escape else Action::Unknown end end end end class Drawer INNER_CELL_WIDTH = 16 INNER_CELL_HEIGHT = 6 def initialize @n = 0 @grid = [] of Array(String) @current_row = [] of String @content_line = false end def set_current_row(row) @current_row = row end def draw(grid) @grid = grid @n = @grid.size Screen.clear box end def box top_border (@n - 1).times do |row| tile row mid_border end tile @n - 1 bottom_border end def tile(row) set_current_row @grid[row] INNER_CELL_HEIGHT.times do |i| if i == (@n / 2) + 1 content_line else space_line end end set_current_row [] of String end def space_line line '│', " ", '│', '│' end def content_line @content_line = true space_line @content_line = false end def top_border line '┌', "─", '┬', '┐' end def mid_border line '├', "─", '┼', '┤' end def bottom_border line '└', "─", '┴', '┘' end def line(left, fill, inner, right) print left (@n - 1).times do |cell| cell_line fill, cell print inner end cell_line fill, @n - 1 puts right end def cell_line(fill, cell) content = @current_row.fetch(cell) { "empty" } tile_value = (content == "empty" ? 0 : (content.to_i? || 0)).to_i content = "" if !@content_line || content == "empty" fill_size = INNER_CELL_WIDTH // 2 fill_size -= content.size // 2 fill_size -= 2 print fill Screen.colorize_for(tile_value) do print fill*fill_size print content print fill*fill_size print fill if content.size % 2 == 0 end print fill end end class Game def initialize @drawer = Drawer.new @grid = [ [nil, nil, nil, nil] of Int32?, [nil, nil, nil, nil] of Int32?, [nil, nil, nil, nil] of Int32?, [nil, nil, nil, nil] of Int32?, ] insert_tile insert_tile end def run draw until won? || lost? if execute_action read_action insert_tile draw end end if won? end_game "You won!" elsif lost? end_game "You lost!" else raise "Game loop quit unexpectedly" end end def draw @drawer.draw drawable_grid end def drawable_grid @grid.map &.map(&.to_s) end def read_action Screen.read_keypress end def insert_tile value = rand > 0.8 ? 4 : 2 empty_cells = @grid.sum(&.count &.nil?) fill_cell = empty_cells > 1 ? rand(empty_cells - 1) + 1 : 1 empty_cell_count = 0 each_cell_with_index do |tile, row, col| empty_cell_count += 1 unless tile if empty_cell_count == fill_cell @grid[row][col] = value return end end end def each_cell_with_index(&) 0.upto(@grid.size - 1) do |row| 0.upto(@grid.size - 1) do |col| yield @grid[row][col], row, col end end end def execute_action(action) case action in .up?, .down?, .left?, .right? if can_move_in? action shift_grid action true else false end in .escape? end_game "Bye" in .unknown? false # ignore end end def shift_grid(direction) drow, dcol = offsets_for direction shift_tiles_to_empty_cells direction, drow, dcol merge_tiles direction, drow, dcol shift_tiles_to_empty_cells direction, drow, dcol end def shift_tiles_to_empty_cells(direction, drow, dcol) modified = true while modified modified = false movable_tiles(direction, drow, dcol) do |tile, row, col| unless @grid[row + drow][col + dcol] @grid[row + drow][col + dcol] = tile @grid[row][col] = nil modified = true end end end end def merge_tiles(direction, drow, dcol) movable_tiles(direction, drow, dcol) do |tile, row, col| if @grid[row + drow][col + dcol] == tile @grid[row][col] = nil @grid[row + drow][col + dcol] = tile*2 end end end def movable_tiles(direction, drow, dcol, &) max = @grid.size - 1 from_row, to_row, from_column, to_column = case direction when .up?, .left? {0, max, 0, max} when .down?, .right? {max, 0, max, 0} else raise ArgumentError.new "Unknown direction #{direction}" end from_row.to(to_row) do |row| from_column.to(to_column) do |col| tile = @grid[row][col] if tile && !to_border?(direction, row, col, drow, dcol) yield tile, row, col end end end end def can_move_in?(direction) drow, dcol = offsets_for direction movable_tiles(direction, drow, dcol) do |tile, row, col| target_tile = @grid[row + drow][col + dcol] return true if !target_tile || target_tile == tile end false end def offsets_for(direction) drow = dcol = 0 case direction when .up? drow = -1 when .down? drow = 1 when .left? dcol = -1 when .right? dcol = 1 else raise ArgumentError.new "Unknown direction #{direction}" end {drow, dcol} end def to_border?(direction, row, col, drow, dcol) case direction when .up? row + drow < 0 when .down? row + drow >= @grid.size when .left? col + dcol < 0 when .right? col + dcol >= @grid.size else false end end def won? @grid.any? &.any?(&.==(2048)) end def lost? !can_move? end def can_move? can_move_in?(Action::Up) || can_move_in?(Action::Down) || can_move_in?(Action::Left) || can_move_in?(Action::Right) end def end_game(msg) puts msg exit end end at_exit { STDIN.cooked! } Game.new.run