2048-ruby 0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 363aba227984e0848f9b6a40a1eb04b5540c1c5572f061c6d2a61d0ba7a3829e
4
+ data.tar.gz: ca798754075c148bc7ff45cde0f6287c336a8a2dd7400399b7f323a0b9b9751a
5
+ SHA512:
6
+ metadata.gz: 8cd4332ea3a8ccdbc4636906c9648aee7419f3b1aa20a099355c601f7e98bc5765dbabc9898c93abe6d4a0931fa985397a3999609b45c3b50dca1c8f6dc8f783
7
+ data.tar.gz: 6fcfe4af73f38cade1f2831324a5dff732c43f9f945794cefafe1d1425dbbde3549cd1d1449f00d3735613411fa6d70a27c50cec584a3f60ee6bfe86abc58831
data/bin/2048 ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require 'game_2048'
6
+
7
+ game = Game2048::Game.new
8
+ game.run
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Game2048
4
+ ##
5
+ # Game
6
+ class Game
7
+ def initialize
8
+ @terminal = Terminal.new
9
+ @tiles = Tiles.new
10
+ @render = Render.new(@terminal, @tiles)
11
+ @running = false
12
+ end
13
+
14
+ def stop
15
+ @running = false
16
+ end
17
+
18
+ def run
19
+ @running = true
20
+ @terminal.raw_mode
21
+ @terminal.hide_cursor
22
+ @render.refresh
23
+
24
+ Kernel.trap('SIGWINCH') do
25
+ @render.refresh
26
+ end
27
+
28
+ while @running
29
+ @render.draw
30
+ key = @terminal.read
31
+ case key
32
+ when 'q'
33
+ stop
34
+ when 'r'
35
+ @tiles.reset
36
+ when '+'
37
+ @render.size += 1
38
+ @render.refresh
39
+ when '-'
40
+ @render.size -= 1
41
+ @render.refresh
42
+ end
43
+
44
+ next if @tiles.win? || @tiles.game_over?
45
+
46
+ case key
47
+ when :up
48
+ @tiles.move_up
49
+ when :down
50
+ @tiles.move_down
51
+ when :right
52
+ @tiles.move_right
53
+ when :left
54
+ @tiles.move_left
55
+ when 'u'
56
+ @tiles.undo
57
+ end
58
+ end
59
+
60
+ @terminal.cooked_mode
61
+ @terminal.erase_display
62
+ @terminal.show_cursor
63
+ @terminal.move_home
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Game2048
4
+ ##
5
+ # Render
6
+ class Render
7
+ SIZE = 3
8
+ HORIZONTAL = "\u2500"
9
+ VERTICAL = "\u2502"
10
+ LEFT_VERTICAL = "\u251c"
11
+ RIGHT_VERTICAL = "\u2524"
12
+ TOP_HORIZONTAL = "\u252c"
13
+ BOTTOM_HORIZONTAL = "\u2534"
14
+ TOP_LEFT = "\u250c"
15
+ TOP_RIGHT = "\u2510"
16
+ BOTTOM_LEFT = "\u2514"
17
+ BOTTOM_RIGHT = "\u2518"
18
+ HORIZONTAL_VERTICAL = "\u253c"
19
+ BORDER_COLOR = :yellow
20
+ TILE_COLOR = :cyan
21
+ STATUSBAR_COLOR = :blue
22
+ STATUSBAR_TEXT_COLOR = :white
23
+ GAMEOVER_COLOR = :red
24
+ WIN_COLOR = :green
25
+ BLANK = ' '
26
+
27
+ attr_reader :size
28
+
29
+ def initialize(terminal, tiles, **options)
30
+ @tiles = tiles
31
+ @terminal = terminal
32
+ @x = @y = 1
33
+ @rows = @cols = 0
34
+ @width = @height = 0
35
+ @size = options.fetch(:size, SIZE)
36
+ end
37
+
38
+ def size=(value)
39
+ n = Math.sqrt(Tiles::SIZE).to_i
40
+ width = value * 2 * n + n + 1
41
+ height = value * n + n + 1
42
+ return if value <= 1 || width > @cols || height > @rows
43
+
44
+ @size = value
45
+ end
46
+
47
+ def refresh
48
+ @terminal.erase_display
49
+ align
50
+ draw
51
+ end
52
+
53
+ def draw
54
+ draw_tiles
55
+ draw_statusbar
56
+ draw_gameover
57
+ end
58
+
59
+ def draw_statusbar
60
+ line = String.new
61
+ line << "#{@tiles.score} #{VERTICAL} "
62
+ line << "\u2190\u2193\u2191\u2192 Move #{VERTICAL} "
63
+ line << "u Undo #{VERTICAL} "
64
+ line << "r Reset #{VERTICAL} "
65
+ line << "+- Scale #{VERTICAL} "
66
+ line << 'q Quit'
67
+ x = @cols / 2 - line.length / 2 + 1
68
+ return if @cols < line.length
69
+
70
+ @terminal.move_to(1, @rows)
71
+ @terminal.bg_color(STATUSBAR_COLOR)
72
+ @terminal.write(BLANK * @cols)
73
+ @terminal.move_to(x, @rows)
74
+ @terminal.fg_color(STATUSBAR_TEXT_COLOR)
75
+ @terminal.write(line[0..(@cols - 1)])
76
+ @terminal.reset
77
+ end
78
+
79
+ def draw_gameover
80
+ y = @y + @height + 1
81
+ return if @rows <= y
82
+
83
+ @terminal.move_to(1, y)
84
+ @terminal.write(BLANK * @cols)
85
+ @terminal.reset
86
+
87
+ line = String.new
88
+ if @tiles.win?
89
+ line << 'Win!'
90
+ elsif @tiles.game_over?
91
+ line << 'Game over!'
92
+ end
93
+ x = @cols / 2 - line.length / 2 + 1
94
+ @terminal.move_to(x, y)
95
+ @tiles.win? ? @terminal.fg_color(WIN_COLOR) : @terminal.fg_color(GAMEOVER_COLOR)
96
+ @terminal.bold
97
+ @terminal.write(line[0..(@cols - 1)])
98
+ @terminal.reset
99
+ end
100
+
101
+ def draw_tiles
102
+ n = Math.sqrt(Tiles::SIZE).to_i
103
+ return if @cols <= @width || @rows <= @height
104
+
105
+ @terminal.move_to(@x, @y)
106
+ @terminal.fg_color(BORDER_COLOR)
107
+ @terminal.write(TOP_LEFT)
108
+ n.times do |i|
109
+ @terminal.write(HORIZONTAL * (@size * 2))
110
+ next if i == n - 1
111
+
112
+ @terminal.write(TOP_HORIZONTAL)
113
+ end
114
+ @terminal.write(TOP_RIGHT)
115
+
116
+ n.times do |i|
117
+ @size.times do |j|
118
+ @terminal.move_to(@x, @y + i * @size + i + j + 1)
119
+ @terminal.write(VERTICAL)
120
+ n.times do |k|
121
+ @terminal.reset
122
+ @terminal.write(BLANK * (@size * 2))
123
+ @terminal.reset
124
+ next if k == n - 1
125
+
126
+ @terminal.fg_color(BORDER_COLOR)
127
+ @terminal.write(VERTICAL)
128
+ end
129
+ @terminal.fg_color(BORDER_COLOR)
130
+ @terminal.write(VERTICAL)
131
+ end
132
+ next if i == n - 1
133
+
134
+ @terminal.move_to(@x, @y + i * (@size + 1) + @size + 1)
135
+ @terminal.write(LEFT_VERTICAL)
136
+ n.times do |j|
137
+ @terminal.write(HORIZONTAL * (@size * 2))
138
+ next if j == n - 1
139
+
140
+ @terminal.write(HORIZONTAL_VERTICAL)
141
+ end
142
+ @terminal.write(RIGHT_VERTICAL)
143
+ end
144
+
145
+ @terminal.move_to(@x, @y + @size * n + n)
146
+ @terminal.write(BOTTOM_LEFT)
147
+ n.times do |i|
148
+ @terminal.write(HORIZONTAL * (@size * 2))
149
+ next if i == n - 1
150
+
151
+ @terminal.write(BOTTOM_HORIZONTAL)
152
+ end
153
+ @terminal.write(BOTTOM_RIGHT)
154
+ @terminal.reset
155
+
156
+ @terminal.fg_color(TILE_COLOR)
157
+ @terminal.bold
158
+ @tiles.items.each.with_index do |tile, i|
159
+ t = tile.zero? ? '' : tile.to_s
160
+ x = i % n * (@size * 2 + 1) + @size - t.length / 2 + 1
161
+ y = i / n * (@size + 1) + @size / 2 + 1
162
+ @terminal.move_to(@x + x, @y + y)
163
+ @terminal.write(t)
164
+ end
165
+ @terminal.reset
166
+ end
167
+
168
+ def align
169
+ @rows, @cols = @terminal.display_size
170
+ n = Math.sqrt(Tiles::SIZE).to_i
171
+ @width = @size * 2 * n + n + 1
172
+ @height = @size * n + n + 1
173
+ @x = @cols / 2 - @width / 2
174
+ @y = @rows / 2 - @height / 2
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+
5
+ module Game2048
6
+ ##
7
+ # VT100 terminal
8
+ class Terminal
9
+ CSI = "\e["
10
+ CUU = 'A'
11
+ CUD = 'B'
12
+ CUF = 'C'
13
+ CUB = 'D'
14
+ CUP = 'H'
15
+ ED = 'J'
16
+ SGR = 'm'
17
+ CPR = '6n'
18
+ CPA = 'R'
19
+
20
+ HIDE_CURSOR = '?25l'
21
+ SHOW_CURSOR = '?25h'
22
+
23
+ RESET = 0
24
+ BOLD = 1
25
+ DIM = 2
26
+ BLINK = 5
27
+ REVERSE = 7
28
+ UNDERSCORE = 4
29
+ COLOR_FG = 30
30
+ COLOR_BG = 40
31
+
32
+ COLORS = {
33
+ black: 0,
34
+ red: 1,
35
+ green: 2,
36
+ yellow: 3,
37
+ blue: 4,
38
+ magenta: 5,
39
+ cyan: 6,
40
+ white: 7
41
+ }.freeze
42
+
43
+ KEY_MAP = {
44
+ CUU => :up,
45
+ CUD => :down,
46
+ CUF => :right,
47
+ CUB => :left
48
+ }.freeze
49
+
50
+ def initialize(input: $stdin, output: $stdout)
51
+ @input = input
52
+ @output = output
53
+ @sgr = []
54
+ end
55
+
56
+ def move_to(x, y)
57
+ @output.write("#{CSI}#{y};#{x}#{CUP}")
58
+ end
59
+
60
+ def move_home
61
+ move_to(1, 1)
62
+ end
63
+
64
+ def erase_display
65
+ @output.write("#{CSI}2#{ED}")
66
+ end
67
+
68
+ def fg_color(color)
69
+ color_exist?(color)
70
+ @sgr << COLORS[color] + COLOR_FG
71
+ end
72
+
73
+ def bg_color(color)
74
+ color_exist?(color)
75
+ @sgr << COLORS[color] + COLOR_BG
76
+ end
77
+
78
+ def bold
79
+ @sgr << BOLD
80
+ end
81
+
82
+ def underscore
83
+ @sgr << UNDERSCORE
84
+ end
85
+
86
+ def dim
87
+ @sgr << DIM
88
+ end
89
+
90
+ def blink
91
+ @sgr << BLINK
92
+ end
93
+
94
+ def reverse
95
+ @sgr << REVERSE
96
+ end
97
+
98
+ def reset
99
+ @sgr.clear
100
+ @output.write("#{CSI}#{RESET}#{SGR}")
101
+ end
102
+
103
+ def hide_cursor
104
+ @output.write("#{CSI}#{HIDE_CURSOR}")
105
+ end
106
+
107
+ def show_cursor
108
+ @output.write("#{CSI}#{SHOW_CURSOR}")
109
+ end
110
+
111
+ def display_size
112
+ move_to(999, 999)
113
+ @output.write("#{CSI}#{CPR}")
114
+ data = String.new
115
+ data << @input.readchar
116
+ data << @input.readchar
117
+ return if data != CSI
118
+
119
+ data.clear
120
+ while (char = @input.readchar) != CPA
121
+ data << char
122
+ end
123
+ data.split(';').map(&:to_i)
124
+ end
125
+
126
+ def write(text = '')
127
+ text = "#{CSI}#{@sgr.join(';')}#{SGR}#{text}" unless @sgr.empty?
128
+ @sgr.clear
129
+ @output.write(text)
130
+ end
131
+
132
+ def read
133
+ data = @input.readchar
134
+ if data == "\e"
135
+ data << @input.readchar
136
+ if data == CSI
137
+ data.clear
138
+ loop do
139
+ char = @input.readchar
140
+ data << char
141
+ break unless char.ord.between?(0x30, 0x39) || char == ';'
142
+ end
143
+
144
+ return KEY_MAP.fetch(data, :unknown)
145
+ end
146
+ end
147
+
148
+ data
149
+ end
150
+
151
+ def raw_mode
152
+ @input&.raw!
153
+ end
154
+
155
+ def cooked_mode
156
+ @input&.cooked!
157
+ end
158
+
159
+ private
160
+
161
+ def color_exist?(color)
162
+ raise TerminalError, "Unknown color #{color}" unless COLORS.key?(color)
163
+
164
+ true
165
+ end
166
+ end
167
+
168
+ class TerminalError < StandardError; end
169
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Game2048
6
+ ##
7
+ # Game
8
+ class Tiles
9
+ SIZE = 4**2
10
+ NO_TILE = 0
11
+ NEW_TILE_CHANCE = 90
12
+ NEW_TILE_2 = 2
13
+ NEW_TILE_4 = 4
14
+ WIN_SUM = 2048
15
+
16
+ attr_reader :items
17
+
18
+ def initialize(items = nil, auto_new_tile: true)
19
+ raise TilesError, "Tile map size must be #{SIZE}" if !items.nil? && items.length != SIZE
20
+
21
+ if items.nil?
22
+ reset
23
+ else
24
+ @items = items
25
+ end
26
+ @auto_new_tile = auto_new_tile
27
+ @items_prev = []
28
+ end
29
+
30
+ def score
31
+ @items.sum
32
+ end
33
+
34
+ def reset
35
+ @items = Array.new(SIZE, NO_TILE)
36
+ new_tile
37
+ end
38
+
39
+ def new_tile
40
+ items = []
41
+ @items.each.with_index { |tile, i| items << i if tile == NO_TILE }
42
+ return if items.empty?
43
+
44
+ @items[items.sample] = SecureRandom.rand(1..100) <= NEW_TILE_CHANCE ? NEW_TILE_2 : NEW_TILE_4
45
+ end
46
+
47
+ def game_over?
48
+ return false if @items.index(NO_TILE)
49
+
50
+ n = Math.sqrt(SIZE).to_i
51
+ @items.each.with_index do |item, i|
52
+ a = i + 1
53
+ return false if a < @items.length && !(a % n).zero? && item == @items[a]
54
+
55
+ b = i + n
56
+ return false if b < @items.length && item == @items[b]
57
+ end
58
+ true
59
+ end
60
+
61
+ def win?
62
+ items.count { |item| item >= WIN_SUM } >= 1
63
+ end
64
+
65
+ def undo
66
+ return if @items_prev.empty?
67
+
68
+ @items = @items_prev.dup
69
+ @items_prev.clear
70
+ end
71
+
72
+ def move_up
73
+ items = @items.dup
74
+ n = Math.sqrt(SIZE).to_i
75
+ n.times do |row|
76
+ move_zeroes(-n)
77
+ n.times do |column|
78
+ tile = row * n + column
79
+ tile_prev = tile - n
80
+ if !tile_prev.negative? && @items[tile_prev] == @items[tile]
81
+ @items[tile_prev] += @items[tile]
82
+ @items[tile] = NO_TILE
83
+ end
84
+ end
85
+ move_zeroes(-n)
86
+ end
87
+ return if items == @items
88
+
89
+ @items_prev = items
90
+ new_tile if @auto_new_tile
91
+ end
92
+
93
+ def move_down
94
+ items = @items.dup
95
+ n = Math.sqrt(SIZE).to_i
96
+ (n - 1).downto(0) do |row|
97
+ move_zeroes(n)
98
+ n.times do |column|
99
+ tile = row * n + column
100
+ tile_next = tile + n
101
+ if tile_next < @items.length && @items[tile_next] == @items[tile]
102
+ @items[tile_next] += @items[tile]
103
+ @items[tile] = NO_TILE
104
+ end
105
+ end
106
+ move_zeroes(n)
107
+ end
108
+ return if items == @items
109
+
110
+ @items_prev = items
111
+ new_tile if @auto_new_tile
112
+ end
113
+
114
+ def move_right
115
+ items = @items.dup
116
+ n = Math.sqrt(SIZE).to_i
117
+ n.times do |row|
118
+ move_zeroes(1)
119
+ (n - 1).downto(0) do |column|
120
+ tile = row * n + column
121
+ tile_next = tile + 1
122
+ if tile_next < row * n + n && @items[tile_next] == @items[tile]
123
+ @items[tile_next] += @items[tile]
124
+ @items[tile] = NO_TILE
125
+ end
126
+ end
127
+ move_zeroes(1)
128
+ end
129
+ return if items == @items
130
+
131
+ @items_prev = items
132
+ new_tile if @auto_new_tile
133
+ end
134
+
135
+ def move_left
136
+ items = @items.dup
137
+ n = Math.sqrt(SIZE).to_i
138
+ n.times do |row|
139
+ move_zeroes(-1)
140
+ n.times do |column|
141
+ tile = row * n + column
142
+ tile_prev = tile - 1
143
+ if tile_prev >= row * n && @items[tile_prev] == @items[tile]
144
+ @items[tile_prev] += @items[tile]
145
+ @items[tile] = NO_TILE
146
+ end
147
+ end
148
+ move_zeroes(-1)
149
+ end
150
+ return if items == @items
151
+
152
+ @items_prev = items
153
+ new_tile if @auto_new_tile
154
+ end
155
+
156
+ private
157
+
158
+ def move_zeroes(dir)
159
+ n = Math.sqrt(SIZE).to_i
160
+ n.times do |row|
161
+ n.times do |column|
162
+ tile = row * n + column
163
+ loop do
164
+ tile_other = tile + dir
165
+ if tile_other.negative? || tile_other >= @items.length || (dir == 1 && tile_other >= row * n + n) || (dir == -1 && tile_other < row * n) || @items[tile_other] != NO_TILE
166
+ break
167
+ end
168
+
169
+ @items[tile_other] = @items[tile]
170
+ @items[tile] = NO_TILE
171
+ tile = tile_other
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ class TilesError < StandardError; end
179
+ end
data/lib/game_2048.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'game_2048/terminal'
4
+ require 'game_2048/game'
5
+ require 'game_2048/tiles'
6
+ require 'game_2048/render'
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: 2048-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - anim
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-10-19 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: me@telpart.ru
15
+ executables:
16
+ - '2048'
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - bin/2048
21
+ - lib/game_2048.rb
22
+ - lib/game_2048/game.rb
23
+ - lib/game_2048/render.rb
24
+ - lib/game_2048/terminal.rb
25
+ - lib/game_2048/tiles.rb
26
+ homepage: https://github.com/animotto/2048-ruby
27
+ licenses:
28
+ - MIT
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '2.7'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 3.1.2
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: Classic game 2048 in your terminal
49
+ test_files: []