2048-ruby 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []