minehunter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,384 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "field"
4
+
5
+ module Minehunter
6
+ # A grid with fields representation
7
+ #
8
+ # @api private
9
+ class Grid
10
+ # Track the number of flags remaining
11
+ #
12
+ # @return [Integer]
13
+ #
14
+ # @api public
15
+ attr_reader :flags_remaining
16
+
17
+ # Track the number of unmined fields remaining
18
+ #
19
+ # @return [Integer]
20
+ #
21
+ # @api public
22
+ attr_reader :unmined_fields_remaining
23
+
24
+ # Create a Grid instance
25
+ #
26
+ # @param [Integer] width
27
+ # the number of columns
28
+ # @param [Integer] height
29
+ # the number of rows
30
+ # @param [Integer] mines_limit
31
+ # the total number of mines
32
+ #
33
+ # @api public
34
+ def initialize(width: nil, height: nil, mines_limit: nil)
35
+ if mines_limit >= width * height
36
+ raise Error, "cannot have more mines than available fields"
37
+ end
38
+
39
+ @width = width
40
+ @height = height
41
+ @mines_limit = mines_limit
42
+ @fields = []
43
+
44
+ reset
45
+ end
46
+
47
+ # Reset all fields to defaults
48
+ #
49
+ # @api public
50
+ def reset
51
+ (@width * @height).times do |i|
52
+ @fields[i] = Field.new
53
+ end
54
+ @unmined_fields_remaining = @width * @height - @mines_limit
55
+ @flags_remaining = @mines_limit
56
+ end
57
+
58
+ # Check whether or not the grid is cleared
59
+ #
60
+ # @return [Boolean]
61
+ #
62
+ # @api public
63
+ def cleared?
64
+ @unmined_fields_remaining.zero?
65
+ end
66
+
67
+ # All fields with mines
68
+ #
69
+ # @return [Array<Field>]
70
+ #
71
+ # @api public
72
+ def mines
73
+ @fields.select(&:mine?)
74
+ end
75
+
76
+ # Move up on the grid
77
+ #
78
+ # @return [Integer]
79
+ #
80
+ # @api public
81
+ def move_up(y)
82
+ y.zero? ? @height - 1 : y - 1
83
+ end
84
+
85
+ # Move down on the grid
86
+ #
87
+ # @return [Integer]
88
+ #
89
+ # @api public
90
+ def move_down(y)
91
+ y == @height - 1 ? 0 : y + 1
92
+ end
93
+
94
+ # Move left on the grid
95
+ #
96
+ # @return [Integer]
97
+ #
98
+ # @api public
99
+ def move_left(x)
100
+ x.zero? ? @width - 1 : x - 1
101
+ end
102
+
103
+ # Move right on the grid
104
+ #
105
+ # @return [Integer]
106
+ #
107
+ # @api public
108
+ def move_right(x)
109
+ x == @width - 1 ? 0 : x + 1
110
+ end
111
+
112
+ # Find field index at a given position
113
+ #
114
+ # @param [Integer] x
115
+ # the x coordinate
116
+ # @param [Integer] y
117
+ # the y coordinate
118
+ #
119
+ # @return [Integer]
120
+ #
121
+ # @api public
122
+ def at(x, y)
123
+ y * @width + x
124
+ end
125
+
126
+ # Find a field at a given position
127
+ #
128
+ # @param [Integer] x
129
+ # the x coordinate
130
+ # @param [Integer] y
131
+ # the y coordinate
132
+ #
133
+ # @return [Field]
134
+ #
135
+ # @api public
136
+ def field_at(x, y)
137
+ @fields[at(x, y)]
138
+ end
139
+
140
+ # Set a mine at a given position
141
+ #
142
+ # @param [Integer] x
143
+ # the x coordinate
144
+ # @param [Integer] y
145
+ # the y coordinate
146
+ #
147
+ # @api public
148
+ def mine(x, y)
149
+ field_at(x, y).mine!
150
+ end
151
+
152
+ # Add or remove a flag at a given position
153
+ #
154
+ # @param [Integer] x
155
+ # the x coordinate
156
+ # @param [Integer] y
157
+ # the y coordinate
158
+ #
159
+ # @api public
160
+ def flag(x, y)
161
+ field = field_at(x, y)
162
+ return unless field.cover?
163
+
164
+ @flags_remaining += field.flag? ? 1 : -1
165
+ field.flag
166
+ end
167
+
168
+ # Check whether or not there is a flag at a given position
169
+ #
170
+ # @param [Integer] x
171
+ # the x coordinate
172
+ # @param [Integer] y
173
+ # the y coordinate
174
+ #
175
+ # @return [Boolean]
176
+ #
177
+ # @api public
178
+ def flag?(x, y)
179
+ field_at(x, y).flag?
180
+ end
181
+
182
+ # Fill grid with mines skipping the current position and nearby fields
183
+ #
184
+ # @param [Integer] x
185
+ # the x coordinate
186
+ # @param [Integer] y
187
+ # the y coordinate
188
+ # @param [Proc] randomiser
189
+ # the mine position randomiser
190
+ #
191
+ # @api public
192
+ def fill_with_mines(x, y, randomiser: DEFAULT_RANDOMISER)
193
+ limit = @mines_limit
194
+ while limit > 0
195
+ mine_x = randomiser[@width]
196
+ mine_y = randomiser[@height]
197
+ next if mine_x == x && mine_y == y
198
+ next if fields_next_to(x, y).include?([mine_x, mine_y])
199
+
200
+ field = field_at(mine_x, mine_y)
201
+ next if field.mine?
202
+
203
+ field.mine!
204
+ limit -= 1
205
+ end
206
+ end
207
+
208
+ # Enumerate fields next to a given position
209
+ #
210
+ # @param [Integer] x
211
+ # the x coordinate
212
+ # @param [Integer] y
213
+ # the y coordinate
214
+ #
215
+ # @return [Enumerator]
216
+ # the coordinates for nearby fields
217
+ #
218
+ # @api public
219
+ def fields_next_to(x, y)
220
+ return to_enum(:fields_next_to, x, y) unless block_given?
221
+
222
+ -1.upto(1) do |offset_x|
223
+ -1.upto(1) do |offset_y|
224
+ close_x = x + offset_x
225
+ close_y = y + offset_y
226
+
227
+ next if close_x == x && close_y == y
228
+ next unless within?(close_x, close_y)
229
+
230
+ yield(close_x, close_y)
231
+ end
232
+ end
233
+ end
234
+
235
+ # Check whether coordinates are within the grid
236
+ #
237
+ # return [Boolean]
238
+ #
239
+ # @api public
240
+ def within?(x, y)
241
+ x >= 0 && x < @width && y >= 0 && y < @height
242
+ end
243
+
244
+ # Total number of mines next to a given position
245
+ #
246
+ # @param [Integer] x
247
+ # the x coordinate
248
+ # @param [Integer] y
249
+ # the y coordinate
250
+ #
251
+ # @return [Integer]
252
+ #
253
+ # @api public
254
+ def count_mines_next_to(x, y)
255
+ fields_next_to(x, y).reduce(0) do |acc, cords|
256
+ acc + (field_at(*cords).mine? ? 1 : 0)
257
+ end
258
+ end
259
+
260
+ # Total number of flags next to a given position
261
+ #
262
+ # @param [Integer] x
263
+ # the x coordinate
264
+ # @param [Integer] y
265
+ # the y coordinate
266
+ #
267
+ # @return [Integer]
268
+ #
269
+ # @api public
270
+ def count_flags_next_to(x, y)
271
+ fields_next_to(x, y).reduce(0) do |acc, cords|
272
+ acc + (field_at(*cords).flag? ? 1 : 0)
273
+ end
274
+ end
275
+
276
+ # Uncover fields surrounding the position
277
+ #
278
+ # @param [Integer] x
279
+ # the x coordinate
280
+ # @param [Integer] y
281
+ # the y coordinate
282
+ #
283
+ # @return [Boolean]
284
+ # whether or not uncovered a mine
285
+ #
286
+ # @api public
287
+ def uncover(x, y)
288
+ field = field_at(x, y)
289
+
290
+ if field.mine?
291
+ field.uncover
292
+ uncover_mines
293
+ return true
294
+ end
295
+
296
+ return uncover_around(x, y) unless field.cover?
297
+
298
+ mine_count = count_mines_next_to(x, y)
299
+ field.mine_count = mine_count
300
+ flag(x, y) if field.flag?
301
+ field.uncover
302
+ @unmined_fields_remaining -= 1
303
+
304
+ if mine_count.zero?
305
+ fields_next_to(x, y) do |close_x, close_y|
306
+ close_field = field_at(close_x, close_y)
307
+ next if !close_field.cover? || close_field.mine?
308
+
309
+ uncover(close_x, close_y)
310
+ end
311
+ end
312
+ false
313
+ end
314
+
315
+ # Uncover fields around numbered field matching flags count
316
+ #
317
+ # @param [Integer] x
318
+ # the x coordinate
319
+ # @param [Integer] y
320
+ # the y coordinate
321
+ #
322
+ # @return [Boolean]
323
+ # whether or not uncovered a mine
324
+ #
325
+ # @api public
326
+ def uncover_around(x, y)
327
+ field = field_at(x, y)
328
+ uncovered_mine = false
329
+
330
+ if count_flags_next_to(x, y) != field.mine_count
331
+ return uncovered_mine
332
+ end
333
+
334
+ fields_next_to(x, y) do |close_x, close_y|
335
+ close_field = field_at(close_x, close_y)
336
+ next if !close_field.cover? || close_field.flag?
337
+
338
+ uncover(close_x, close_y)
339
+
340
+ uncovered_mine = true if close_field.mine?
341
+ end
342
+
343
+ uncovered_mine
344
+ end
345
+
346
+ # Uncover all mines without a flag
347
+ #
348
+ # @api public
349
+ def uncover_mines
350
+ @fields.each do |field|
351
+ if field.mine? && !field.flag? || field.flag? && !field.mine?
352
+ field.wrong if field.flag?
353
+ field.uncover
354
+ end
355
+ end
356
+ end
357
+
358
+ # Render grid
359
+ #
360
+ # @return [String]
361
+ #
362
+ # @api public
363
+ def render(x, y, decorator: DEFAULT_DECORATOR)
364
+ out = []
365
+
366
+ @height.times do |field_y|
367
+ @width.times do |field_x|
368
+ field = field_at(field_x, field_y)
369
+ rendered_field = field.render(decorator: decorator)
370
+
371
+ if field_x == x && field_y == y && decorator
372
+ bg_color = field.mine? && !field.cover? ? :on_red : :on_green
373
+ rendered_field = decorator[rendered_field, bg_color]
374
+ end
375
+
376
+ out << rendered_field
377
+ end
378
+ out << "\n"
379
+ end
380
+
381
+ out.join
382
+ end
383
+ end # Grid
384
+ end # Minehunter
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minehunter
4
+ # An intro screen content
5
+ #
6
+ # @api private
7
+ class Intro
8
+ INTRO = [
9
+ " ,-*",
10
+ " (_) Minehunter",
11
+ "",
12
+ "Movement",
13
+ " [↑] [w]",
14
+ " [←][↓][→] [a][s][d]",
15
+ "",
16
+ "Actions",
17
+ " Toggle Flag f",
18
+ " Uncover space",
19
+ " Restart r",
20
+ " Quit q",
21
+ "",
22
+ "Press any key to start!"
23
+ ].freeze
24
+
25
+ # The maximum intro screen content width
26
+ #
27
+ # @return [Integer]
28
+ #
29
+ # @api public
30
+ def self.width
31
+ @width ||= INTRO.max_by(&:length).size
32
+ end
33
+
34
+ # The intro screen content height
35
+ #
36
+ # @return [Integer]
37
+ #
38
+ # @api public
39
+ def self.height
40
+ @height ||= INTRO.size
41
+ end
42
+
43
+ # Render intro screen content
44
+ #
45
+ # @return [String]
46
+ #
47
+ # @api public
48
+ def self.render
49
+ INTRO.join("\n")
50
+ end
51
+ end # SplashScreen
52
+ end # Minehunter
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minehunter
4
+ VERSION = "0.1.0"
5
+ end # Minehunter
data/lib/minehunter.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "minehunter/cli"
4
+
5
+ module Minehunter
6
+ class Error < StandardError; end
7
+
8
+ # Apply no styling
9
+ DEFAULT_DECORATOR = ->(str, *_colors) { str }
10
+
11
+ # Random number generator
12
+ GENERATOR = Random.new
13
+
14
+ # Generate random number less than max
15
+ DEFAULT_RANDOMISER = ->(max) { GENERATOR.rand(max) }
16
+
17
+ # Start the game
18
+ #
19
+ # @api public
20
+ def self.run
21
+ CLI.new.run
22
+ end
23
+ end # Minehunter
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minehunter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Piotr Murach
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-10-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pastel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.8.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.8.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: tty-box
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.7.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.7.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-cursor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.7.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.7.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: tty-option
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.2.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.2.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: tty-reader
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.9.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.9.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: tty-screen
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.8.1
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.8.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ description: Terminal mine hunting game.
126
+ email:
127
+ - piotr@piotrmurach.com
128
+ executables:
129
+ - minehunter
130
+ - minehunt
131
+ extensions: []
132
+ extra_rdoc_files:
133
+ - README.md
134
+ - CHANGELOG.md
135
+ - LICENSE.txt
136
+ files:
137
+ - CHANGELOG.md
138
+ - LICENSE.txt
139
+ - README.md
140
+ - exe/minehunt
141
+ - exe/minehunter
142
+ - lib/minehunter.rb
143
+ - lib/minehunter/cli.rb
144
+ - lib/minehunter/field.rb
145
+ - lib/minehunter/game.rb
146
+ - lib/minehunter/grid.rb
147
+ - lib/minehunter/intro.rb
148
+ - lib/minehunter/version.rb
149
+ homepage: https://github.com/piotrmurach/minehunter
150
+ licenses:
151
+ - AGPL-3.0
152
+ metadata:
153
+ allowed_push_host: https://rubygems.org
154
+ bug_tracker_uri: https://github.com/piotrmurach/minehunter/issues
155
+ changelog_uri: https://github.com/piotrmurach/minehunter/blob/master/CHANGELOG.md
156
+ documentation_uri: https://www.rubydoc.info/gems/minehunter
157
+ homepage_uri: https://github.com/piotrmurach/minehunter
158
+ source_code_uri: https://github.com/piotrmurach/minehunter
159
+ post_install_message:
160
+ rdoc_options: []
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: 2.0.0
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ requirements: []
174
+ rubygems_version: 3.1.2
175
+ signing_key:
176
+ specification_version: 4
177
+ summary: Terminal mine hunting game.
178
+ test_files: []