minesweeprb 0.3.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8570b88e453c2b2831d7b4d155ece9b35191aa989b38c3df409cde3258af522
4
- data.tar.gz: b4d5c63637d268285cc2a0a08d9b3011b9ceed0aaeabfb36fb63004b90938f82
3
+ metadata.gz: 1a4af15df91c5d1f7b09134c5effe760ae2c16ab6cf1b568599cacd7ae0c648b
4
+ data.tar.gz: e8b3bc60828dfd5f93e526a0ec8bdcecba7c4b7adf811fab6efb9a1e66b8ad97
5
5
  SHA512:
6
- metadata.gz: 0250dbc2b118b525aad974cf1ab17001cff0484abdf4b2c49961d02e7f040a5f9dfdcb687473e3514f030899369ef650d61011cd68b9bf17c0ca0c36afa9ef7b
7
- data.tar.gz: ecdb3eacfb7288c3f8383ac6936d782947c7c1fb06fcc57c80dba1e9b404508ff51526bebec7a793beee00e6d2e55e469354d3cae95cfebf5afce23c7727f990
6
+ metadata.gz: 950340b2ad509214d3a29b9ce6bfb150afd0e5c5bd51bed672d6ad995a9960b447be27ab45d20bc45b41d0496bd53b2171ed527164d92b4e4d51ec9631e15b9c
7
+ data.tar.gz: 0f4eda9e575449989c208b4d32430591f697b63728b5c4a7697c48f5f8b46a0f32cc0d79348f8f7ab61d9293984f4382f6155f194f21c1bcb4899265ba621de2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ### 2020-02-08 0.4.0
2
+
3
+ * Add custom boards
4
+ * Fix gemspec
5
+
1
6
  ### 2020-02-08 0.3.0
2
7
 
3
8
  * Add mouse support
data/Gemfile.lock CHANGED
@@ -1,13 +1,15 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- minesweeprb (0.1.0)
4
+ minesweeprb (0.3.0)
5
+ curses (~> 1.3)
5
6
  timers (~> 4.3)
6
7
  tty (~> 0.10)
7
8
 
8
9
  GEM
9
10
  remote: https://rubygems.org/
10
11
  specs:
12
+ curses (1.3.2)
11
13
  diff-lcs (1.3)
12
14
  equatable (0.6.1)
13
15
  kramdown (1.16.2)
data/README.md CHANGED
@@ -10,7 +10,7 @@ Use clues on the gameboard to deduce locations of mines. Correctly reveal all no
10
10
  ## Install
11
11
 
12
12
  ```
13
- gem install minesweerb
13
+ gem install minesweeprb
14
14
  ```
15
15
 
16
16
  ## Run
@@ -22,13 +22,13 @@ minesweeprb
22
22
  ## Rules
23
23
  A gameboard is composed of a number of squares laid out in a rectangle.
24
24
 
25
- A square hold either a Clue or a Mine
25
+ A square holds either a Clue or a Mine
26
26
 
27
27
  ### Clue Square
28
28
  A Clue Square will contain a number representing the numbers of mines that border itself. If the Clue Square has no neighboring mines then it will be blank.
29
29
 
30
30
  For example, a Clue Square containing a "1" will have exactly one mine in one of the spaces that borders itself.
31
- There is a mine in exactly one of the ? squares.
31
+ There is a mine in exactly one of the squares.
32
32
  ```
33
33
  ◼ ◼ ◼
34
34
  ◼ 1 ◼
@@ -45,7 +45,7 @@ There are no mines surrounding an empty square. Note: Revealing an empty square
45
45
  ```
46
46
  where '◻' is an empty Clue Square.
47
47
 
48
- In the example below, there is a mine in exactly 3 of the ? squares. Because the "3" Clue Square only has three unrevealed spaces bordering itself, it is correct to assume that there is mine in each space.
48
+ In the example below, there is a mine in exactly 3 of the squares. Because the "3" Clue Square only has three unrevealed spaces bordering itself, it is correct to assume that there is mine in each space.
49
49
  ```
50
50
  3 ◼ ◼ 3 ⚑ ◼
51
51
  ◼ ◼ ◼ → ⚑ ⚑ ◼
@@ -53,14 +53,14 @@ In the example below, there is a mine in exactly 3 of the ? squares. Because the
53
53
  ```
54
54
 
55
55
  ### Mine Square
56
- Mine Squares should not be revealed. If you believe you have found the location of a Mine then you can mark that square to prevent accidentally revealing it.
56
+ Mine Squares should not be revealed. If you believe you have found the location of a Mine then you can flag that square to prevent accidentally revealing it.
57
57
 
58
58
  ## Gameboard
59
59
  A gameboard contains a Width, Height, and Number of Mines.
60
60
 
61
61
  The first move is always safe which means a gameboard's Mines are not placed until the first square is revealed.
62
62
 
63
- Since the first is always safe, a gameboard is only valid if the number of mines is less than the total number of squares. A valid gameboard must have more than one square. (i.e., 0 < # of Mines < Width * Height)
63
+ Since the first move is always safe, a gameboard is only valid if the number of mines is less than the total number of squares. A valid gameboard must have more than one square. (i.e., 0 < # of Mines < Width * Height)
64
64
 
65
65
  ## How To Play
66
66
  Reveal squares you believe do not contain a Mine.
@@ -80,10 +80,12 @@ Reveal all Clue Squares without revealing a Mine.
80
80
 
81
81
 
82
82
  ## TODO
83
- * Extract Gameboard
83
+ * ~Extract Gameboard~
84
84
  * Simplify logic
85
- * Repaint only what's necessary
86
- * Implement timer
85
+ * ~Repaint only what's necessary~
86
+ * Separate squares and timer into separate window?
87
+ * ~Implement timer~
87
88
  * Add Leaderboard
88
- * Add custom games (set width, height, and number of mines)
89
+ * ~Add custom games (set width, height, and number of mines)~
89
90
  * Add peek mode, undo, or lives to help users learn
91
+ * restarting a game brings back to prompt instead of generating a new board of the same dimensions
@@ -1,22 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'tty-reader'
4
3
  require 'tty-screen'
5
4
 
6
5
  require_relative '../command'
7
- require_relative '../gameboard'
6
+ require_relative '../../minesweeprb'
8
7
 
9
8
  module Minesweeprb
10
9
  module Commands
11
10
  class Play < Minesweeprb::Command
11
+ SIZES = [
12
+ # [ label, width, height, # of mines ]
13
+ ['Tiny', 5, 5, 3],
14
+ ['Small', 9, 9, 10],
15
+ ['Medium', 13, 13, 15],
16
+ ['Large', 17, 17, 20],
17
+ ['Huge', 21, 21, 25],
18
+ ].map { |options| GameTemplate.new(*options) }.freeze
19
+
12
20
  def initialize(options)
13
21
  @options = options
14
22
  end
15
23
 
16
24
  def execute(input: $stdin, output: $stdout)
17
- size = prompt_size(output)
18
- gameboard = Gameboard.new(size)
25
+ template = prompt_size(output)
26
+ template = prompt_custom(output) if template == :custom
19
27
 
28
+ game = Game.new(**template.to_h)
29
+ gameboard = Gameboard.new(game)
20
30
  begin
21
31
  gameboard.draw
22
32
  ensure
@@ -27,21 +37,63 @@ module Minesweeprb
27
37
  private
28
38
 
29
39
  def prompt_size(output)
30
- sizes = Game::SIZES.map.with_index do |size, index|
31
- too_big = size[:height] * 2 > TTY::Screen.height || size[:width] * 2 > TTY::Screen.width
40
+ options = SIZES.map do |option|
41
+ too_big = option.height > TTY::Screen.height || option.width * 2 - 1 > TTY::Screen.width
32
42
  disabled = '(screen too small)' if too_big
33
- size.merge(
34
- value: index,
35
- disabled: disabled
36
- )
43
+ {
44
+ disabled: disabled,
45
+ name: option.label,
46
+ value: option,
47
+ }
48
+ end
49
+
50
+ options << {
51
+ name: 'Custom',
52
+ value: :custom
53
+ }
54
+
55
+ prompt(interrupt: -> { exit 1 }).select('Size:', options, cycle: true)
56
+ end
57
+
58
+ def prompt_custom(output)
59
+ min_width = 1
60
+ max_width = TTY::Screen.width / 2 - 1
61
+ width = prompt.ask("Width (#{min_width}-#{max_width})") do |q|
62
+ q.required true
63
+ q.convert :int
64
+ q.validate do |val|
65
+ val =~ /\d+/ && (min_width..max_width).include?(val.to_i)
66
+ end
37
67
  end
38
68
 
39
- size = prompt(interrupt: -> { exit 1 }).select('Size:', sizes, cycle: true)
69
+ min_height = width == 1 ? 2 : 1
70
+ max_height = TTY::Screen.height - 10 # leave room for interface
71
+ height = prompt.ask("Height (#{min_height}-#{max_height})") do |q|
72
+ q.required true
73
+ q.convert :int
74
+ q.validate do |val|
75
+ val =~ /\d+/ && (min_height..max_height).include?(val.to_i)
76
+ end
77
+ end
78
+
79
+ min_mines = 1
80
+ max_mines = width * height - 1
81
+ mines = prompt.ask("Mines (#{min_mines}-#{max_mines})") do |q|
82
+ q.required true
83
+ q.convert :int
84
+ q.validate do |val|
85
+ val =~ /\d+/ && (min_mines..max_mines).include?(val.to_i)
86
+ end
87
+ end
88
+
89
+ GameTemplate.new('Custom', width, height, mines)
90
+ end
91
+
92
+ def clear_output(output)
40
93
  output.print cursor.hide
41
94
  output.print cursor.up(1)
42
95
  output.print cursor.clear_screen_down
43
96
  output.puts
44
- size
45
97
  end
46
98
  end
47
99
  end
@@ -2,8 +2,6 @@
2
2
 
3
3
  module Minesweeprb
4
4
  class Game
5
- DEFAULT_SIZE = 'Tiny'
6
- DEFAULT_MINE_COUNT = 1
7
5
  SPRITES = {
8
6
  clock: '◷',
9
7
  clues: '◻➊➋➌➍➎➏➐➑'.chars.freeze,
@@ -17,73 +15,36 @@ module Minesweeprb
17
15
  }.freeze
18
16
  WIN = "#{SPRITES[:win_face]} YOU WON #{SPRITES[:win_face]}"
19
17
  LOSE = "#{SPRITES[:lose_face]} GAME OVER #{SPRITES[:lose_face]}"
20
- SIZES = [
21
- {
22
- name: 'Tiny',
23
- width: 5,
24
- height: 5,
25
- mines: 3,
26
- },
27
- {
28
- name: 'Small',
29
- width: 9,
30
- height: 9,
31
- mines: 10,
32
- },
33
- {
34
- name: 'Medium',
35
- width: 13,
36
- height: 13,
37
- mines: 15,
38
- },
39
- {
40
- name: 'Large',
41
- width: 17,
42
- height: 17,
43
- mines: 20,
44
- },
45
- {
46
- name: 'Huge',
47
- width: 21,
48
- height: 21,
49
- mines: 25,
50
- },
51
- ].freeze
52
18
 
53
19
  attr_accessor :active_square
54
20
  attr_reader :flagged_squares,
21
+ :height,
55
22
  :marked_squares,
56
- :mined_squares,
23
+ :mines,
57
24
  :revealed_squares,
58
- :size,
59
- :squares,
60
- :start_time
25
+ :start_time,
26
+ :width
61
27
 
62
- def initialize(size)
63
- @size = SIZES[size]
28
+ def initialize(label:, width:, height:, mines:)
29
+ @width = width
30
+ @height = height
31
+ @mines = mines
64
32
  restart
65
33
  end
66
34
 
67
35
  def restart
68
36
  @active_square = center
69
- @flagged_squares = []
70
- @marked_squares = []
71
- @mined_squares = []
72
- @revealed_squares = {}
37
+ @flagged_squares = Set[]
38
+ @marked_squares = Set[]
39
+ @mined_squares = Set[]
40
+ @revealed_squares = Set[]
41
+ @grid = Array.new(height) { Array.new(width) }
73
42
  @start_time = nil
74
43
  @end_time = nil
75
44
  end
76
45
 
77
- def mines
78
- size[:mines] - flagged_squares.size
79
- end
80
-
81
- def width
82
- size[:width]
83
- end
84
-
85
- def height
86
- size[:height]
46
+ def remaining_mines
47
+ mines - flagged_squares.length
87
48
  end
88
49
 
89
50
  def center
@@ -97,13 +58,13 @@ module Minesweeprb
97
58
  def time
98
59
  return 0 unless start_time
99
60
 
100
- (end_time - start_time).round
61
+ end_time - start_time
101
62
  end
102
63
 
103
64
  def move(direction)
104
65
  return if over?
105
66
 
106
- x, y = @active_square
67
+ x, y = active_square
107
68
 
108
69
  case direction
109
70
  when :up then y -= 1
@@ -112,6 +73,11 @@ module Minesweeprb
112
73
  when :right then x += 1
113
74
  end
114
75
 
76
+ self.active_square = [x,y]
77
+ end
78
+
79
+ def active_square=(pos)
80
+ x, y = pos
115
81
  x = x < 0 ? width - 1 : x
116
82
  x = x > width - 1 ? 0 : x
117
83
  y = y < 0 ? height - 1 : y
@@ -131,20 +97,20 @@ module Minesweeprb
131
97
  end
132
98
 
133
99
  def header
134
- "#{SPRITES[:mine]} #{mines.to_s.rjust(3, '0')}" \
100
+ "#{SPRITES[:mine]} #{remaining_mines.to_s.rjust(3, '0')}" \
135
101
  " #{face} " \
136
- "#{SPRITES[:clock]} #{time.to_s.rjust(3, '0')}"
102
+ "#{SPRITES[:clock]} #{time.round.to_s.rjust(3, '0')}"
137
103
  end
138
104
 
139
105
  def cycle_flag
140
- return if over? || @revealed_squares.empty? || @revealed_squares.include?(active_square)
106
+ return if over? || revealed_squares.empty? || revealed_squares.include?(active_square)
141
107
 
142
108
  if flagged_squares.include?(active_square)
143
109
  @flagged_squares -= [active_square]
144
110
  @marked_squares += [active_square]
145
111
  elsif marked_squares.include?(active_square)
146
112
  @marked_squares -= [active_square]
147
- elsif flagged_squares.length < size[:mines]
113
+ elsif flagged_squares.length < mines
148
114
  @flagged_squares += [active_square]
149
115
  end
150
116
  end
@@ -152,22 +118,23 @@ module Minesweeprb
152
118
  def reveal_active_square
153
119
  return if over? || flagged_squares.include?(active_square)
154
120
 
155
- reveal_square(active_square)
121
+ x, y = active_square
122
+ reveal_square(x, y)
156
123
  @end_time = now if over?
157
124
  end
158
125
 
159
- def squares
126
+ def play_grid
160
127
  height.times.map do |y|
161
128
  width.times.map do |x|
162
- pos = [x,y]
129
+ square = [x,y]
163
130
 
164
- if mined_squares.include?(pos) && (revealed_squares[pos] || over?)
131
+ if @mined_squares.include?(square) && (revealed_squares.include?(square) || over?)
165
132
  SPRITES[:mine]
166
- elsif flagged_squares.include?(pos)
133
+ elsif revealed_squares.include?(square) || over?
134
+ SPRITES[:clues][@grid[y][x]]
135
+ elsif flagged_squares.include?(square)
167
136
  SPRITES[:flag]
168
- elsif revealed_squares[pos]
169
- SPRITES[:clues][revealed_squares[pos]]
170
- elsif marked_squares.include?(pos)
137
+ elsif marked_squares.include?(square)
171
138
  SPRITES[:mark]
172
139
  else
173
140
  SPRITES[:square]
@@ -181,11 +148,11 @@ module Minesweeprb
181
148
  end
182
149
 
183
150
  def won?
184
- !lost? && revealed_squares.count == width * height - size[:mines]
151
+ !lost? && revealed_squares.count == width * height - mines
185
152
  end
186
153
 
187
154
  def lost?
188
- (revealed_squares.keys & mined_squares).any?
155
+ (revealed_squares & @mined_squares).any?
189
156
  end
190
157
 
191
158
  def over?
@@ -204,13 +171,14 @@ module Minesweeprb
204
171
 
205
172
  def start_game
206
173
  place_mines
174
+ place_clues
207
175
  @start_time = now
208
176
  end
209
177
 
210
178
  def place_mines
211
- size[:mines].times do
179
+ mines.times do
212
180
  pos = random_square
213
- pos = random_square while pos == active_square || mined_squares.include?(pos)
181
+ pos = random_square while pos == active_square || @mined_squares.include?(pos)
214
182
  @mined_squares << pos
215
183
  end
216
184
  end
@@ -221,43 +189,54 @@ module Minesweeprb
221
189
  [x, y]
222
190
  end
223
191
 
224
- def reveal_square(square)
192
+ def place_clues
193
+ width.times do |x|
194
+ height.times do |y|
195
+ @grid[y][x] = square_value(x,y)
196
+ end
197
+ end
198
+ end
199
+
200
+ def square_value(x,y)
201
+ return if @mined_squares.include?([x,y])
202
+
203
+ (neighbors(x,y) & @mined_squares).length
204
+ end
205
+
206
+ def reveal_square(x,y)
207
+ square = [x,y]
225
208
  return if over? || flagged_squares.include?(active_square)
226
209
  start_game if revealed_squares.empty?
227
- return if revealed_squares.keys.include?(square)
228
- return lose! if mined_squares.include?(square)
210
+ return if revealed_squares.include?(square)
211
+ return lose! if @mined_squares.include?(square)
229
212
 
230
- value = square_value(square)
231
- @revealed_squares[square] = value
232
- neighbors(square).each { |neighbor| reveal_square(neighbor) } if value.zero?
213
+ @revealed_squares << [x,y]
214
+ value = @grid[y][x]
215
+ neighbors(x,y).each { |x,y| reveal_square(x,y) } if value == 0
233
216
  end
234
217
 
235
218
  def lose!
236
- @mined_squares.each { |square| @revealed_squares[square] = -1 }
237
- end
238
-
239
- def square_value(square)
240
- (neighbors(square) & mined_squares).size
219
+ @revealed_squares |= @mined_squares
241
220
  end
242
221
 
243
- def neighbors(square)
222
+ def neighbors(x,y)
244
223
  [
245
224
  # top
246
- [square[0] - 1, square[1] - 1],
247
- [square[0] - 0, square[1] - 1],
248
- [square[0] + 1, square[1] - 1],
225
+ [x - 1, y - 1],
226
+ [x - 0, y - 1],
227
+ [x + 1, y - 1],
249
228
 
250
- # middle
251
- [square[0] - 1, square[1] - 0],
252
- [square[0] + 1, square[1] - 0],
229
+ # sides
230
+ [x - 1, y - 0],
231
+ [x + 1, y - 0],
253
232
 
254
233
  # bottom
255
- [square[0] - 1, square[1] + 1],
256
- [square[0] - 0, square[1] + 1],
257
- [square[0] + 1, square[1] + 1],
234
+ [x - 1, y + 1],
235
+ [x - 0, y + 1],
236
+ [x + 1, y + 1],
258
237
  ].select do |x,y|
259
- (0...width).include?(x) && (0...height).include?(y)
260
- end
238
+ x.between?(0, width-1) && y.between?(0, height-1)
239
+ end.to_set
261
240
  end
262
241
  end
263
242
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minesweeprb
4
+ class GameTemplate < Struct.new(:label, :width, :height, :mines)
5
+ end
6
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'curses'
4
4
  require 'timers'
5
- require_relative './game'
6
5
 
7
6
  module Minesweeprb
8
7
  class Gameboard
@@ -48,20 +47,39 @@ module Minesweeprb
48
47
 
49
48
  COLOR_PAIRS = COLORS.keys.freeze
50
49
 
51
- attr_reader :game, :pastel, :window, :game_x, :game_y
50
+ attr_reader :game, :windows, :game_x, :game_y
52
51
 
53
- def initialize(size)
54
- @pastel = Pastel.new
55
- @game = Game.new(size)
56
- @timers = Timers::Group.new
57
- @game_timer = @timers.every(0.5) { paint }
52
+ def initialize(game)
53
+ @game = game
58
54
  setup_curses
59
- Thread.new { loop { @timers.wait } }
55
+ end
56
+
57
+ def w_header
58
+ windows[:header]
59
+ end
60
+
61
+ def w_grid
62
+ windows[:grid]
63
+ end
64
+
65
+ def w_status
66
+ windows[:status]
67
+ end
68
+
69
+ def w_instructions
70
+ windows[:instructions]
71
+ end
72
+
73
+ def w_debug
74
+ windows[:debug]
60
75
  end
61
76
 
62
77
  def draw
63
- paint
64
- draw if process_input(window.getch)
78
+ # paint_debug
79
+ Thread.new { loop { paint_header && sleep(0.5) } }
80
+
81
+ paint_grid
82
+ paint_grid while process_input(w_grid.getch)
65
83
  end
66
84
 
67
85
  def clear
@@ -71,21 +89,63 @@ module Minesweeprb
71
89
  private
72
90
 
73
91
  def setup_curses
74
- init_screen
92
+ screen = init_screen
75
93
  use_default_colors
76
94
  start_color
77
95
  curs_set(0)
78
96
  noecho
79
97
  self.ESCDELAY = 1;
80
98
  mousemask(BUTTON1_CLICKED|BUTTON2_CLICKED|BUTTON3_CLICKED|BUTTON4_CLICKED)
81
- @window = Window.new(0, 0, 0, 0)
82
- @window.keypad(true)
99
+
100
+ header = {
101
+ top: 1,
102
+ left: (screen.maxx - game.header.length) / 2,
103
+ cols: game.header.length,
104
+ rows: 1,
105
+ }
106
+ grid = {
107
+ left: (screen.maxx - (game.width * 2 - 1)) / 2,
108
+ top: header[:top] + header[:rows] + 1,
109
+ cols: game.width * 2 - 1, # leave room for spaces between squares
110
+ rows: game.height, # leave room for win/lose status and instructions
111
+ }
112
+ status = {
113
+ left: 0,
114
+ top: grid[:top] + grid[:rows] + 1,
115
+ cols: 0,
116
+ rows: 1,
117
+ }
118
+ instructions = {
119
+ left: 0,
120
+ top: status[:top] + status[:rows] + 1,
121
+ cols: 0,
122
+ rows: 1,
123
+ }
124
+ debug = {
125
+ left: 0,
126
+ top: screen.maxy - 1,
127
+ cols: 0,
128
+ rows: 1,
129
+ }
130
+
131
+ @windows = {}
132
+ @windows[:header] = build_window(**header)
133
+ @windows[:grid] = build_window(**grid)
134
+ @windows[:status] = build_window(**status)
135
+ @windows[:instructions] = build_window(**instructions)
136
+ @windows[:debug] = build_window(**debug)
137
+ @windows[:grid].keypad(true)
138
+
83
139
  COLOR_PAIRS.each.with_index do |char, index|
84
140
  fg, bg = COLORS[char]
85
141
  init_pair(index + 1, fg, bg || -1)
86
142
  end
87
143
  end
88
144
 
145
+ def build_window(rows:, cols:, top:, left:)
146
+ Window.new(rows, cols, top, left)
147
+ end
148
+
89
149
  def process_input(key)
90
150
  case key
91
151
  when KEY_MOUSE then process_mouse(getmouse)
@@ -100,17 +160,17 @@ module Minesweeprb
100
160
  end
101
161
 
102
162
  def process_mouse(m)
103
- top = game_y
104
- left = game_x
105
- bottom = game_y + game.height
106
- right = game_x + game.width * 2 - 1
163
+ top = w_grid.begy
164
+ left = w_grid.begx
165
+ bottom = top + game.height
166
+ right = left + game.width * 2 - 1
107
167
  on_board = (top..bottom).include?(m.y) &&
108
168
  (left..right).include?(m.x) &&
109
- (m.x - game_x).even?
169
+ (m.x - w_grid.begx).even?
110
170
 
111
171
  return if !on_board && !game.over?
112
172
 
113
- game.active_square = [(m.x - game_x) / 2, m.y - game_y]
173
+ game.active_square = [(m.x - w_grid.begx) / 2, m.y - w_grid.begy]
114
174
 
115
175
  case m.bstate
116
176
  when BUTTON1_CLICKED then game.reveal_active_square
@@ -118,70 +178,75 @@ module Minesweeprb
118
178
  end
119
179
  end
120
180
 
121
- def how_to_play
122
- instructions = []
123
- instructions << '(←↓↑→ or hjkl)Move' unless game.over?
124
- instructions << '(f or ␣)Flag/Mark' if game.started?
125
- instructions << '(↵)Reveal' unless game.over?
126
- instructions << '(r)Restart'
127
- instructions << '(q or ⎋)Quit'
128
- instructions.join(' ')
181
+ def paint_header
182
+ w_header.setpos(0,0)
183
+
184
+ game.header.chars.each do |char|
185
+ w_header.attron(color_for(char)) { w_header << char }
186
+ end
187
+
188
+ w_header.refresh
129
189
  end
130
190
 
131
- def paint
132
- window.clear
133
- window << "\n"
134
- game.header.center(window.maxx - 1).chars.each do |char|
135
- window.attron(color_for(char)) { window << char }
191
+ def paint_debug
192
+ COLORS.keys.each do |char|
193
+ w_debug.attron(color_for(char)) { w_debug << char.to_s }
136
194
  end
137
- clrtoeol
138
- window << "\n"
139
-
140
- # COLOR_PAIRS.each do |char|
141
- # window.attron(color_for(char)) { window << char }
142
- # end
143
- # clrtoeol
144
- # window << "\n"
145
-
146
- padding = (window.maxx - game.width * 2) / 2
147
- game.squares.each.with_index do |line, row|
148
- window << ' ' * padding
149
- @game_x, @game_y = window.curx, window.cury if row.zero?
195
+ w_debug.refresh
196
+ end
197
+
198
+ def paint_grid
199
+ w_grid.setpos(0,0)
200
+
201
+ game.play_grid.each.with_index do |line, row|
150
202
  line.each.with_index do |char, col|
203
+ w_grid.setpos(row, col * 2) if col < line.length
204
+
151
205
  if game.active_square == [col, row]
152
- window.attron(color_for(char) | A_REVERSE) { window << char }
206
+ w_grid.attron(color_for(char) | A_REVERSE) { w_grid << char }
153
207
  else
154
- window.attron(color_for(char)) { window << char }
208
+ w_grid.attron(color_for(char)) { w_grid << char }
155
209
  end
156
- window << ' ' if col < line.length
157
210
  end
158
- clrtoeol
159
- window << "\n"
160
211
  end
161
212
 
213
+ paint_status
214
+ paint_instructions
215
+
216
+ w_grid.refresh
217
+ w_status.refresh
218
+ w_instructions.refresh
219
+ end
220
+
221
+ def paint_status
162
222
  if game.over?
163
- window << "\n"
223
+ w_status.setpos(0,0)
164
224
  outcome = game.won? ? :win : :lose
165
- message = game.game_over_message.center(window.maxx - 1)
225
+ message = game.game_over_message.center(w_status.maxx - 1)
166
226
  message.chars.each do |char|
167
227
  char_color = color_for(char)
168
228
 
169
229
  if char_color.zero?
170
- window.attron(color_for(outcome)) { window << char }
230
+ w_status.attron(color_for(outcome)) { w_status << char }
171
231
  else
172
- window.attron(char_color) { window << char }
232
+ w_status.attron(char_color) { w_status << char }
173
233
  end
174
234
  end
175
- clrtoeol
176
- window << "\n"
235
+ else
236
+ w_status.clear
177
237
  end
238
+ end
178
239
 
179
- window << "\n"
180
- window << how_to_play.center(window.maxx - 1)
181
- clrtoeol
182
- window << "\n"
240
+ def paint_instructions
241
+ instructions = []
242
+ instructions << '(←↓↑→ or hjkl)Move' unless game.over?
243
+ instructions << '(f or ␣)Flag/Mark' if game.started?
244
+ instructions << '(↵)Reveal' unless game.over?
245
+ instructions << '(r)Restart'
246
+ instructions << '(q or ⎋)Quit'
183
247
 
184
- window.refresh
248
+ w_instructions.setpos(0,0)
249
+ w_instructions << instructions.join(' ').center(w_instructions.maxx - 1)
185
250
  end
186
251
 
187
252
  def color_for(char)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Minesweeprb
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/minesweeprb.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'minesweeprb/version'
4
+ require 'minesweeprb/game'
5
+ require 'minesweeprb/game_template'
6
+ require 'minesweeprb/gameboard'
4
7
 
5
8
  module Minesweeprb
6
9
  class Error < StandardError; end
data/minesweeprb.gemspec CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
29
29
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
30
  spec.require_paths = ['lib']
31
31
 
32
+ spec.add_runtime_dependency 'curses', '~> 1.3'
32
33
  spec.add_runtime_dependency 'timers', '~> 4.3'
33
34
  spec.add_runtime_dependency 'tty', '~> 0.10'
34
35
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minesweeprb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - scudco
@@ -10,6 +10,20 @@ bindir: exe
10
10
  cert_chain: []
11
11
  date: 2020-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: curses
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: timers
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -65,6 +79,7 @@ files:
65
79
  - lib/minesweeprb/commands/.gitkeep
66
80
  - lib/minesweeprb/commands/play.rb
67
81
  - lib/minesweeprb/game.rb
82
+ - lib/minesweeprb/game_template.rb
68
83
  - lib/minesweeprb/gameboard.rb
69
84
  - lib/minesweeprb/templates/.gitkeep
70
85
  - lib/minesweeprb/templates/play/.gitkeep