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 +4 -4
- data/CHANGELOG.md +5 -0
- data/Gemfile.lock +3 -1
- data/README.md +12 -10
- data/lib/minesweeprb/commands/play.rb +64 -12
- data/lib/minesweeprb/game.rb +74 -95
- data/lib/minesweeprb/game_template.rb +6 -0
- data/lib/minesweeprb/gameboard.rb +126 -61
- data/lib/minesweeprb/version.rb +1 -1
- data/lib/minesweeprb.rb +3 -0
- data/minesweeprb.gemspec +1 -0
- metadata +16 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a4af15df91c5d1f7b09134c5effe760ae2c16ab6cf1b568599cacd7ae0c648b
|
4
|
+
data.tar.gz: e8b3bc60828dfd5f93e526a0ec8bdcecba7c4b7adf811fab6efb9a1e66b8ad97
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 950340b2ad509214d3a29b9ce6bfb150afd0e5c5bd51bed672d6ad995a9960b447be27ab45d20bc45b41d0496bd53b2171ed527164d92b4e4d51ec9631e15b9c
|
7
|
+
data.tar.gz: 0f4eda9e575449989c208b4d32430591f697b63728b5c4a7697c48f5f8b46a0f32cc0d79348f8f7ab61d9293984f4382f6155f194f21c1bcb4899265ba621de2
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
minesweeprb (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
|
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
|
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
|
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
|
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
|
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
|
-
*
|
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 '
|
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
|
-
|
18
|
-
|
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
|
-
|
31
|
-
too_big =
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
data/lib/minesweeprb/game.rb
CHANGED
@@ -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
|
-
:
|
23
|
+
:mines,
|
57
24
|
:revealed_squares,
|
58
|
-
:
|
59
|
-
:
|
60
|
-
:start_time
|
25
|
+
:start_time,
|
26
|
+
:width
|
61
27
|
|
62
|
-
def initialize(
|
63
|
-
@
|
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
|
78
|
-
|
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
|
-
|
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 =
|
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]} #{
|
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? ||
|
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 <
|
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
|
-
|
121
|
+
x, y = active_square
|
122
|
+
reveal_square(x, y)
|
156
123
|
@end_time = now if over?
|
157
124
|
end
|
158
125
|
|
159
|
-
def
|
126
|
+
def play_grid
|
160
127
|
height.times.map do |y|
|
161
128
|
width.times.map do |x|
|
162
|
-
|
129
|
+
square = [x,y]
|
163
130
|
|
164
|
-
if mined_squares.include?(
|
131
|
+
if @mined_squares.include?(square) && (revealed_squares.include?(square) || over?)
|
165
132
|
SPRITES[:mine]
|
166
|
-
elsif
|
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
|
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 -
|
151
|
+
!lost? && revealed_squares.count == width * height - mines
|
185
152
|
end
|
186
153
|
|
187
154
|
def lost?
|
188
|
-
(revealed_squares
|
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
|
-
|
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
|
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.
|
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
|
-
|
231
|
-
@
|
232
|
-
neighbors(
|
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
|
-
@
|
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(
|
222
|
+
def neighbors(x,y)
|
244
223
|
[
|
245
224
|
# top
|
246
|
-
[
|
247
|
-
[
|
248
|
-
[
|
225
|
+
[x - 1, y - 1],
|
226
|
+
[x - 0, y - 1],
|
227
|
+
[x + 1, y - 1],
|
249
228
|
|
250
|
-
#
|
251
|
-
[
|
252
|
-
[
|
229
|
+
# sides
|
230
|
+
[x - 1, y - 0],
|
231
|
+
[x + 1, y - 0],
|
253
232
|
|
254
233
|
# bottom
|
255
|
-
[
|
256
|
-
[
|
257
|
-
[
|
234
|
+
[x - 1, y + 1],
|
235
|
+
[x - 0, y + 1],
|
236
|
+
[x + 1, y + 1],
|
258
237
|
].select do |x,y|
|
259
|
-
(0
|
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
|
@@ -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, :
|
50
|
+
attr_reader :game, :windows, :game_x, :game_y
|
52
51
|
|
53
|
-
def initialize(
|
54
|
-
@
|
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
|
-
|
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
|
-
|
64
|
-
|
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
|
-
|
82
|
-
|
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 =
|
104
|
-
left =
|
105
|
-
bottom =
|
106
|
-
right =
|
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 -
|
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 -
|
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
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
132
|
-
|
133
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
206
|
+
w_grid.attron(color_for(char) | A_REVERSE) { w_grid << char }
|
153
207
|
else
|
154
|
-
|
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
|
-
|
223
|
+
w_status.setpos(0,0)
|
164
224
|
outcome = game.won? ? :win : :lose
|
165
|
-
message = game.game_over_message.center(
|
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
|
-
|
230
|
+
w_status.attron(color_for(outcome)) { w_status << char }
|
171
231
|
else
|
172
|
-
|
232
|
+
w_status.attron(char_color) { w_status << char }
|
173
233
|
end
|
174
234
|
end
|
175
|
-
|
176
|
-
|
235
|
+
else
|
236
|
+
w_status.clear
|
177
237
|
end
|
238
|
+
end
|
178
239
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
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)
|
data/lib/minesweeprb/version.rb
CHANGED
data/lib/minesweeprb.rb
CHANGED
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.
|
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
|