minehunter 0.1.0

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.
@@ -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: []