minesweeprb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/.travis.yml +6 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +134 -0
- data/LICENSE +21 -0
- data/README.md +62 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/minesweeprb +18 -0
- data/lib/minesweeprb.rb +8 -0
- data/lib/minesweeprb/cli.rb +35 -0
- data/lib/minesweeprb/command.rb +127 -0
- data/lib/minesweeprb/commands/.gitkeep +1 -0
- data/lib/minesweeprb/commands/play.rb +185 -0
- data/lib/minesweeprb/game.rb +236 -0
- data/lib/minesweeprb/templates/.gitkeep +1 -0
- data/lib/minesweeprb/templates/play/.gitkeep +1 -0
- data/lib/minesweeprb/version.rb +5 -0
- data/minesweeprb.gemspec +33 -0
- metadata +83 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c0a927b6f66d1e8859c03abc54b13a0cbbc493d6f49363ff64749e7492dca708
|
4
|
+
data.tar.gz: 19bcd6ea2b5eba19d15a130e801198380b7d75362bd7a30e4978ae9073eb92f7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5dd06487cef1c41d3ace0c74f2faa9d1b221f677f61605dff72b1c4f2bfd298c28a09d26eec4f3bc7a516584c7cb77d863902c1b75813c9ddf80ae232f2e4e7c
|
7
|
+
data.tar.gz: 4bba0e734f6140c4ef5e147e713d38ce874ede0b9a3b2c5b2bb3462a7cddc41555ea4aaf3c93ccaf758729293868341ace2ce12f3f0e685abed30118d8bcf5ff
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require: rubocop-rspec
|
2
|
+
|
3
|
+
Style/Documentation:
|
4
|
+
Enabled: false
|
5
|
+
|
6
|
+
Metrics/BlockLength:
|
7
|
+
ExcludedMethods: ['describe']
|
8
|
+
|
9
|
+
Style/TrailingCommaInArrayLiteral:
|
10
|
+
EnforcedStyleForMultiline: comma
|
11
|
+
|
12
|
+
Style/TrailingCommaInHashLiteral:
|
13
|
+
EnforcedStyleForMultiline: comma
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
minesweeprb (0.1.0)
|
5
|
+
tty (~> 0.10)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.3)
|
11
|
+
equatable (0.6.1)
|
12
|
+
kramdown (1.16.2)
|
13
|
+
necromancer (0.5.1)
|
14
|
+
pastel (0.7.3)
|
15
|
+
equatable (~> 0.6)
|
16
|
+
tty-color (~> 0.5)
|
17
|
+
rake (12.3.3)
|
18
|
+
rouge (3.15.0)
|
19
|
+
rspec (3.9.0)
|
20
|
+
rspec-core (~> 3.9.0)
|
21
|
+
rspec-expectations (~> 3.9.0)
|
22
|
+
rspec-mocks (~> 3.9.0)
|
23
|
+
rspec-core (3.9.1)
|
24
|
+
rspec-support (~> 3.9.1)
|
25
|
+
rspec-expectations (3.9.0)
|
26
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
27
|
+
rspec-support (~> 3.9.0)
|
28
|
+
rspec-mocks (3.9.1)
|
29
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
30
|
+
rspec-support (~> 3.9.0)
|
31
|
+
rspec-support (3.9.2)
|
32
|
+
strings (0.1.8)
|
33
|
+
strings-ansi (~> 0.1)
|
34
|
+
unicode-display_width (~> 1.5)
|
35
|
+
unicode_utils (~> 1.4)
|
36
|
+
strings-ansi (0.1.0)
|
37
|
+
thor (0.20.3)
|
38
|
+
tty (0.10.0)
|
39
|
+
bundler (~> 1.16, < 2.0)
|
40
|
+
equatable (~> 0.5)
|
41
|
+
pastel (~> 0.7.2)
|
42
|
+
thor (~> 0.20.0)
|
43
|
+
tty-box (~> 0.4.1)
|
44
|
+
tty-color (~> 0.5)
|
45
|
+
tty-command (~> 0.9.0)
|
46
|
+
tty-config (~> 0.3.2)
|
47
|
+
tty-cursor (~> 0.7)
|
48
|
+
tty-editor (~> 0.5)
|
49
|
+
tty-file (~> 0.8.0)
|
50
|
+
tty-font (~> 0.4.0)
|
51
|
+
tty-logger (~> 0.2.0)
|
52
|
+
tty-markdown (~> 0.6.0)
|
53
|
+
tty-pager (~> 0.12)
|
54
|
+
tty-pie (~> 0.3.0)
|
55
|
+
tty-platform (~> 0.2)
|
56
|
+
tty-progressbar (~> 0.17)
|
57
|
+
tty-prompt (~> 0.19)
|
58
|
+
tty-screen (~> 0.7)
|
59
|
+
tty-spinner (~> 0.9)
|
60
|
+
tty-table (~> 0.11.0)
|
61
|
+
tty-tree (~> 0.3)
|
62
|
+
tty-which (~> 0.4)
|
63
|
+
tty-box (0.4.1)
|
64
|
+
pastel (~> 0.7.2)
|
65
|
+
strings (~> 0.1.6)
|
66
|
+
tty-cursor (~> 0.7)
|
67
|
+
tty-color (0.5.1)
|
68
|
+
tty-command (0.9.0)
|
69
|
+
pastel (~> 0.7.0)
|
70
|
+
tty-config (0.3.2)
|
71
|
+
tty-cursor (0.7.1)
|
72
|
+
tty-editor (0.5.1)
|
73
|
+
tty-prompt (~> 0.19)
|
74
|
+
tty-which (~> 0.4)
|
75
|
+
tty-file (0.8.0)
|
76
|
+
diff-lcs (~> 1.3)
|
77
|
+
pastel (~> 0.7.2)
|
78
|
+
tty-prompt (~> 0.18)
|
79
|
+
tty-font (0.4.0)
|
80
|
+
tty-logger (0.2.0)
|
81
|
+
pastel (~> 0.7.0)
|
82
|
+
tty-markdown (0.6.0)
|
83
|
+
kramdown (~> 1.16.2)
|
84
|
+
pastel (~> 0.7.2)
|
85
|
+
rouge (~> 3.3)
|
86
|
+
strings (~> 0.1.4)
|
87
|
+
tty-color (~> 0.4)
|
88
|
+
tty-screen (~> 0.6)
|
89
|
+
tty-pager (0.12.1)
|
90
|
+
strings (~> 0.1.4)
|
91
|
+
tty-screen (~> 0.6)
|
92
|
+
tty-which (~> 0.4)
|
93
|
+
tty-pie (0.3.0)
|
94
|
+
pastel (~> 0.7.3)
|
95
|
+
tty-cursor (~> 0.7)
|
96
|
+
tty-platform (0.3.0)
|
97
|
+
tty-progressbar (0.17.0)
|
98
|
+
strings-ansi (~> 0.1.0)
|
99
|
+
tty-cursor (~> 0.7)
|
100
|
+
tty-screen (~> 0.7)
|
101
|
+
unicode-display_width (~> 1.6)
|
102
|
+
tty-prompt (0.20.0)
|
103
|
+
necromancer (~> 0.5.0)
|
104
|
+
pastel (~> 0.7.0)
|
105
|
+
tty-reader (~> 0.7.0)
|
106
|
+
tty-reader (0.7.0)
|
107
|
+
tty-cursor (~> 0.7)
|
108
|
+
tty-screen (~> 0.7)
|
109
|
+
wisper (~> 2.0.0)
|
110
|
+
tty-screen (0.7.1)
|
111
|
+
tty-spinner (0.9.3)
|
112
|
+
tty-cursor (~> 0.7)
|
113
|
+
tty-table (0.11.0)
|
114
|
+
equatable (~> 0.6)
|
115
|
+
necromancer (~> 0.5)
|
116
|
+
pastel (~> 0.7.2)
|
117
|
+
strings (~> 0.1.5)
|
118
|
+
tty-screen (~> 0.7)
|
119
|
+
tty-tree (0.4.0)
|
120
|
+
tty-which (0.4.2)
|
121
|
+
unicode-display_width (1.6.1)
|
122
|
+
unicode_utils (1.4.0)
|
123
|
+
wisper (2.0.1)
|
124
|
+
|
125
|
+
PLATFORMS
|
126
|
+
ruby
|
127
|
+
|
128
|
+
DEPENDENCIES
|
129
|
+
minesweeprb!
|
130
|
+
rake (~> 12.0)
|
131
|
+
rspec (~> 3.0)
|
132
|
+
|
133
|
+
BUNDLED WITH
|
134
|
+
1.17.3
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2020 scudco
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# Minesweeper
|
2
|
+
|
3
|
+
Use clues on the gameboard to deduce locations of mines. Correctly reveal all non-mine squares to win.
|
4
|
+
|
5
|
+
## Rules
|
6
|
+
A gameboard is composed of a number of squares laid out in a rectangle.
|
7
|
+
|
8
|
+
A square hold either a Clue or a Mine
|
9
|
+
|
10
|
+
### Clue Square
|
11
|
+
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.
|
12
|
+
|
13
|
+
For example, a Clue Square containing a "1" will have exactly one mine in one of the spaces that borders itself.
|
14
|
+
There is a mine in exactly one of the ? squares.
|
15
|
+
```
|
16
|
+
◼ ◼ ◼
|
17
|
+
◼ 1 ◼
|
18
|
+
◼ ◼ ◼
|
19
|
+
```
|
20
|
+
|
21
|
+
There are no mines surrounding an empty square. Note: Revealing an empty square will reveal all neighboring Clue Squares automatically. For example revealing the square where '▣' is placed (B2) could result in the gameboard looking something like:
|
22
|
+
```
|
23
|
+
◼ ◼ ◼ ◼ ◼ ◻ ◻ 1 ◼ ◼
|
24
|
+
◼ ▣ ◼ ◼ ◼ ◻ ◻ 1 ◼ ◼
|
25
|
+
◼ ◼ ◼ ◼ ◼ → 1 2 2 ◼ ◼
|
26
|
+
◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
|
27
|
+
◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
|
28
|
+
```
|
29
|
+
where '◻' is an empty Clue Square.
|
30
|
+
|
31
|
+
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.
|
32
|
+
```
|
33
|
+
3 ◼ ◼ 3 ⚑ ◼
|
34
|
+
◼ ◼ ◼ → ⚑ ⚑ ◼
|
35
|
+
◼ ◼ ◼ ◼ ◼ ◼
|
36
|
+
```
|
37
|
+
|
38
|
+
### Mine Square
|
39
|
+
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.
|
40
|
+
|
41
|
+
## Gameboard
|
42
|
+
A gameboard contains a Width, Height, and Number of Mines.
|
43
|
+
|
44
|
+
The first move is always safe which means a gameboard's Mines are not placed until the first square is revealed.
|
45
|
+
|
46
|
+
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)
|
47
|
+
|
48
|
+
## How To Play
|
49
|
+
Reveal squares you believe do not contain a Mine.
|
50
|
+
|
51
|
+
Your first move will never reveal a Mine.
|
52
|
+
|
53
|
+
If you reveal a square that contains a Mine, the game will end.
|
54
|
+
|
55
|
+
## How To Win
|
56
|
+
Reveal all Clue Squares without revealing a Mine.
|
57
|
+
|
58
|
+
### ASCII Reference
|
59
|
+
* Flags `⚑`
|
60
|
+
* Squares `◻ ◼ ▣`
|
61
|
+
* Numbers `➊ ➋ ➌ ➍ ➎ ➏ ➀ ➁ ➂ ➃ ➄ ➅`
|
62
|
+
* Mines `☀ ☼ ✺`
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'minesweeprb'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/minesweeprb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib_path = File.expand_path('../lib', __dir__)
|
5
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
6
|
+
require 'minesweeprb/cli'
|
7
|
+
|
8
|
+
Signal.trap('INT') do
|
9
|
+
warn("\n#{caller.join("\n")}: interrupted")
|
10
|
+
exit(1)
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
Minesweeprb::CLI.start
|
15
|
+
rescue Minesweeprb::CLI::Error => e
|
16
|
+
puts "ERROR: #{e.message}"
|
17
|
+
exit 1
|
18
|
+
end
|
data/lib/minesweeprb.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
|
5
|
+
module Minesweeprb
|
6
|
+
# Handle the application command line parsing
|
7
|
+
# and the dispatch to various command objects
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
class CLI < Thor
|
11
|
+
# Error raised by this runner
|
12
|
+
Error = Class.new(StandardError)
|
13
|
+
|
14
|
+
default_command 'play'
|
15
|
+
|
16
|
+
desc 'version', 'minesweeprb version'
|
17
|
+
def version
|
18
|
+
require_relative 'version'
|
19
|
+
puts "v#{Minesweeprb::VERSION}"
|
20
|
+
end
|
21
|
+
map %w[--version -v] => :version
|
22
|
+
|
23
|
+
desc 'play', 'Play Minesweeper'
|
24
|
+
method_option :help, aliases: '-h', type: :boolean,
|
25
|
+
desc: 'Display usage information'
|
26
|
+
def play(*)
|
27
|
+
if options[:help]
|
28
|
+
invoke :help, ['play']
|
29
|
+
else
|
30
|
+
require_relative 'commands/play'
|
31
|
+
Minesweeprb::Commands::Play.new(options).execute
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Minesweeprb
|
6
|
+
class Command
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :command, :run
|
10
|
+
|
11
|
+
# Execute this command
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
def execute(*)
|
15
|
+
raise(
|
16
|
+
NotImplementedError,
|
17
|
+
"#{self.class}##{__method__} must be implemented"
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
# The external commands runner
|
22
|
+
#
|
23
|
+
# @see http://www.rubydoc.info/gems/tty-command
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def command(**options)
|
27
|
+
require 'tty-command'
|
28
|
+
TTY::Command.new(options)
|
29
|
+
end
|
30
|
+
|
31
|
+
# The cursor movement
|
32
|
+
#
|
33
|
+
# @see http://www.rubydoc.info/gems/tty-cursor
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def cursor
|
37
|
+
require 'tty-cursor'
|
38
|
+
TTY::Cursor
|
39
|
+
end
|
40
|
+
|
41
|
+
# Open a file or text in the user's preferred editor
|
42
|
+
#
|
43
|
+
# @see http://www.rubydoc.info/gems/tty-editor
|
44
|
+
#
|
45
|
+
# @api public
|
46
|
+
def editor
|
47
|
+
require 'tty-editor'
|
48
|
+
TTY::Editor
|
49
|
+
end
|
50
|
+
|
51
|
+
# File manipulation utility methods
|
52
|
+
#
|
53
|
+
# @see http://www.rubydoc.info/gems/tty-file
|
54
|
+
#
|
55
|
+
# @api public
|
56
|
+
def generator
|
57
|
+
require 'tty-file'
|
58
|
+
TTY::File
|
59
|
+
end
|
60
|
+
|
61
|
+
# Terminal output paging
|
62
|
+
#
|
63
|
+
# @see http://www.rubydoc.info/gems/tty-pager
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
def pager(**options)
|
67
|
+
require 'tty-pager'
|
68
|
+
TTY::Pager.new(options)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Terminal platform and OS properties
|
72
|
+
#
|
73
|
+
# @see http://www.rubydoc.info/gems/tty-pager
|
74
|
+
#
|
75
|
+
# @api public
|
76
|
+
def platform
|
77
|
+
require 'tty-platform'
|
78
|
+
TTY::Platform.new
|
79
|
+
end
|
80
|
+
|
81
|
+
# The interactive prompt
|
82
|
+
#
|
83
|
+
# @see http://www.rubydoc.info/gems/tty-prompt
|
84
|
+
#
|
85
|
+
# @api public
|
86
|
+
def prompt(**options)
|
87
|
+
require 'tty-prompt'
|
88
|
+
TTY::Prompt.new(options)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Get terminal screen properties
|
92
|
+
#
|
93
|
+
# @see http://www.rubydoc.info/gems/tty-screen
|
94
|
+
#
|
95
|
+
# @api public
|
96
|
+
def screen
|
97
|
+
require 'tty-screen'
|
98
|
+
TTY::Screen
|
99
|
+
end
|
100
|
+
|
101
|
+
# The unix which utility
|
102
|
+
#
|
103
|
+
# @see http://www.rubydoc.info/gems/tty-which
|
104
|
+
#
|
105
|
+
# @api public
|
106
|
+
def which(*args)
|
107
|
+
require 'tty-which'
|
108
|
+
TTY::Which.which(*args)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Check if executable exists
|
112
|
+
#
|
113
|
+
# @see http://www.rubydoc.info/gems/tty-which
|
114
|
+
#
|
115
|
+
# @api public
|
116
|
+
def exec_exist?(*args)
|
117
|
+
require 'tty-which'
|
118
|
+
TTY::Which.exist?(*args)
|
119
|
+
end
|
120
|
+
|
121
|
+
def add_color(str, color)
|
122
|
+
return str if @options['no-color'] || color == :none
|
123
|
+
|
124
|
+
@pastel.decorate(str, color)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
#
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pastel'
|
4
|
+
require 'tty-reader'
|
5
|
+
require 'tty-screen'
|
6
|
+
|
7
|
+
require_relative '../command'
|
8
|
+
require_relative '../game'
|
9
|
+
|
10
|
+
module Minesweeprb
|
11
|
+
module Commands
|
12
|
+
class Play < Minesweeprb::Command
|
13
|
+
VIM_MAPPING = {
|
14
|
+
'k' => :up,
|
15
|
+
'j' => :down,
|
16
|
+
'h' => :left,
|
17
|
+
'l' => :right,
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
attr_reader :game, :pastel
|
21
|
+
|
22
|
+
def initialize(options)
|
23
|
+
@options = options
|
24
|
+
@pastel = Pastel.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def start_game(output)
|
28
|
+
height, width =
|
29
|
+
|
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
|
32
|
+
disabled = '(screen too small)' if too_big
|
33
|
+
size.merge(
|
34
|
+
value: index,
|
35
|
+
disabled: disabled
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
size = prompt(interrupt: -> { exit 1 }).select('Size:', sizes, cycle: true)
|
40
|
+
output.print cursor.hide
|
41
|
+
output.print cursor.up(1)
|
42
|
+
output.print cursor.clear_screen_down
|
43
|
+
output.puts
|
44
|
+
|
45
|
+
@game = Minesweeprb::Game.new(size)
|
46
|
+
|
47
|
+
print_gameboard(output)
|
48
|
+
end
|
49
|
+
|
50
|
+
def how_to_play
|
51
|
+
instructions = []
|
52
|
+
instructions << '(←↓↑→ or hjkl) Move' unless game.over?
|
53
|
+
instructions << '(f or ␣)Flag/Mark' if game.started?
|
54
|
+
instructions << '(↵)Reveal' unless game.over?
|
55
|
+
instructions << '(r)Restart'
|
56
|
+
instructions << '(q)Quit'
|
57
|
+
instructions.join(' ')
|
58
|
+
end
|
59
|
+
|
60
|
+
def execute(input: $stdin, output: $stdout)
|
61
|
+
start_game(output)
|
62
|
+
|
63
|
+
reader
|
64
|
+
.on(:keyescape) { exit }
|
65
|
+
.on(:keyalpha) do |event|
|
66
|
+
key = event.value.downcase
|
67
|
+
case key
|
68
|
+
when 'q' then exit
|
69
|
+
when 'r'
|
70
|
+
output.print cursor.clear_screen_down
|
71
|
+
start_game(output)
|
72
|
+
when 'f'
|
73
|
+
unless game.over?
|
74
|
+
game.cycle_flag
|
75
|
+
print_gameboard(output)
|
76
|
+
end
|
77
|
+
when 'j', 'k', 'h', 'l'
|
78
|
+
unless game.over?
|
79
|
+
game.move(VIM_MAPPING[key])
|
80
|
+
print_gameboard(output)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end.on(:keypress) do |event|
|
85
|
+
unless game.over?
|
86
|
+
case event.key.name
|
87
|
+
when :up, :down, :left, :right
|
88
|
+
game.move(event.key.name)
|
89
|
+
when :space
|
90
|
+
game.cycle_flag
|
91
|
+
when :return
|
92
|
+
game.reveal_active_square
|
93
|
+
end
|
94
|
+
|
95
|
+
print_gameboard(output)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
loop { reader.read_keypress }
|
100
|
+
end
|
101
|
+
|
102
|
+
def reader
|
103
|
+
@reader ||= TTY::Reader.new
|
104
|
+
end
|
105
|
+
|
106
|
+
def print_gameboard(output)
|
107
|
+
total_height = game.height + game.header.lines.length + 1
|
108
|
+
output.print cursor.clear_lines(total_height, :down)
|
109
|
+
output.print cursor.up(total_height - 2)
|
110
|
+
|
111
|
+
game.header.each_line do |line|
|
112
|
+
chars = line.chars.map do |char|
|
113
|
+
case char
|
114
|
+
when Game::WON_FACE then pastel.bright_yellow(char)
|
115
|
+
when Game::LOST_FACE then pastel.bright_red(char)
|
116
|
+
when Game::PLAYING_FACE then pastel.bright_cyan(char)
|
117
|
+
when Game::MINE then pastel.bright_red(char)
|
118
|
+
when Game::CLOCK then pastel.bright_cyan(char)
|
119
|
+
else
|
120
|
+
char
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
center(output, chars.join)
|
125
|
+
end
|
126
|
+
|
127
|
+
game.squares.each.with_index do |row, y|
|
128
|
+
line = row.map.with_index do |char, x|
|
129
|
+
char = case char
|
130
|
+
when Game::FLAG then pastel.bright_red(char)
|
131
|
+
when Game::MINE
|
132
|
+
if game.won?
|
133
|
+
pastel.bright_green(char)
|
134
|
+
elsif game.active_square == [x,y]
|
135
|
+
pastel.black.on_bright_red(char)
|
136
|
+
else
|
137
|
+
pastel.bright_red(char)
|
138
|
+
end
|
139
|
+
when Game::MARK then pastel.bright_magenta(char)
|
140
|
+
when Game::CLUES[0] then pastel.dim(char)
|
141
|
+
when Game::CLUES[1] then pastel.blue(char)
|
142
|
+
when Game::CLUES[2] then pastel.green(char)
|
143
|
+
when Game::CLUES[3] then pastel.red(char)
|
144
|
+
when Game::CLUES[4] then pastel.magenta(char)
|
145
|
+
when Game::CLUES[5] then pastel.black(char)
|
146
|
+
when Game::CLUES[6] then pastel.bright_red(char)
|
147
|
+
when Game::CLUES[7] then pastel.bright_white(char)
|
148
|
+
when Game::CLUES[8] then pastel.bright_cyan(char)
|
149
|
+
else
|
150
|
+
char
|
151
|
+
end
|
152
|
+
|
153
|
+
!game.over? && game.active_square == [x,y] ? pastel.inverse(char): char
|
154
|
+
end.join(' ')
|
155
|
+
|
156
|
+
center(output, line)
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
output.print cursor.clear_screen_down
|
161
|
+
output.puts
|
162
|
+
|
163
|
+
if game.won?
|
164
|
+
center(output, pastel.bright_green.bold('☻ YOU WON ☻'))
|
165
|
+
output.puts
|
166
|
+
center(output, how_to_play)
|
167
|
+
elsif game.lost?
|
168
|
+
center(output, pastel.bright_magenta.bold('☹ GAME OVER ☹'))
|
169
|
+
output.puts
|
170
|
+
center(output, how_to_play)
|
171
|
+
else
|
172
|
+
center(output, how_to_play)
|
173
|
+
output.print cursor.up(total_height + 2)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def center(output, line)
|
178
|
+
width = TTY::Screen.width
|
179
|
+
padding = (width - (pastel.strip(line.chomp.strip).length)) / 2
|
180
|
+
output.print(' ' * [padding, 0].max)
|
181
|
+
output.puts line
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pastel'
|
4
|
+
|
5
|
+
module Minesweeprb
|
6
|
+
class Game
|
7
|
+
DEFAULT_SIZE = 'Tiny'
|
8
|
+
DEFAULT_MINE_COUNT = 1
|
9
|
+
FLAG = '⚑'
|
10
|
+
SQUARE = '◼'
|
11
|
+
ACTIVE_SQUARE = '▣'
|
12
|
+
MARK = '⍰'
|
13
|
+
CLUES = '◻➊➋➌➍➎➏➐➑'.chars.freeze
|
14
|
+
CLOCK = '◷'
|
15
|
+
MINE = '☀'
|
16
|
+
WON_FACE = '☻'
|
17
|
+
LOST_FACE = '☹'
|
18
|
+
PLAYING_FACE = '☺'
|
19
|
+
SIZES = [
|
20
|
+
{
|
21
|
+
name: 'Tiny',
|
22
|
+
width: 5,
|
23
|
+
height: 5,
|
24
|
+
mines: 3,
|
25
|
+
},
|
26
|
+
{
|
27
|
+
name: 'Small',
|
28
|
+
width: 9,
|
29
|
+
height: 9,
|
30
|
+
mines: 10,
|
31
|
+
},
|
32
|
+
{
|
33
|
+
name: 'Medium',
|
34
|
+
width: 13,
|
35
|
+
height: 13,
|
36
|
+
mines: 15,
|
37
|
+
},
|
38
|
+
{
|
39
|
+
name: 'Large',
|
40
|
+
width: 17,
|
41
|
+
height: 17,
|
42
|
+
mines: 20,
|
43
|
+
},
|
44
|
+
{
|
45
|
+
name: 'Huge',
|
46
|
+
width: 21,
|
47
|
+
height: 21,
|
48
|
+
mines: 25,
|
49
|
+
},
|
50
|
+
].freeze
|
51
|
+
|
52
|
+
attr_reader :active_square,
|
53
|
+
:flagged_squares,
|
54
|
+
:marked_squares,
|
55
|
+
:mined_squares,
|
56
|
+
:pastel,
|
57
|
+
:revealed_squares,
|
58
|
+
:size,
|
59
|
+
:squares
|
60
|
+
|
61
|
+
def initialize(size)
|
62
|
+
@pastel = Pastel.new
|
63
|
+
@size = SIZES[size]
|
64
|
+
@active_square = center
|
65
|
+
@flagged_squares = []
|
66
|
+
@marked_squares = []
|
67
|
+
@mined_squares = []
|
68
|
+
@revealed_squares = {}
|
69
|
+
end
|
70
|
+
|
71
|
+
def mines
|
72
|
+
size[:mines] - flagged_squares.size
|
73
|
+
end
|
74
|
+
|
75
|
+
def width
|
76
|
+
size[:width]
|
77
|
+
end
|
78
|
+
|
79
|
+
def height
|
80
|
+
size[:height]
|
81
|
+
end
|
82
|
+
|
83
|
+
def center
|
84
|
+
[(width / 2).floor, (height / 2).floor]
|
85
|
+
end
|
86
|
+
|
87
|
+
def time
|
88
|
+
0
|
89
|
+
end
|
90
|
+
|
91
|
+
def move(direction)
|
92
|
+
return if over?
|
93
|
+
|
94
|
+
x, y = @active_square
|
95
|
+
|
96
|
+
case direction
|
97
|
+
when :up then y -= 1
|
98
|
+
when :down then y += 1
|
99
|
+
when :left then x -= 1
|
100
|
+
when :right then x += 1
|
101
|
+
end
|
102
|
+
|
103
|
+
x = x < 0 ? width - 1 : x
|
104
|
+
x = x > width - 1 ? 0 : x
|
105
|
+
y = y < 0 ? height - 1 : y
|
106
|
+
y = y > height - 1 ? 0 : y
|
107
|
+
|
108
|
+
@active_square = [x,y]
|
109
|
+
end
|
110
|
+
|
111
|
+
def face
|
112
|
+
if won?
|
113
|
+
WON_FACE
|
114
|
+
elsif lost?
|
115
|
+
LOST_FACE
|
116
|
+
else
|
117
|
+
PLAYING_FACE
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def header
|
122
|
+
"#{MINE} #{mines.to_s.rjust(3, '0')}" \
|
123
|
+
" #{face} " \
|
124
|
+
"#{CLOCK} #{time.to_s.rjust(3, '0')}"
|
125
|
+
end
|
126
|
+
|
127
|
+
def cycle_flag
|
128
|
+
return if over? || @revealed_squares.empty? || @revealed_squares.include?(active_square)
|
129
|
+
|
130
|
+
if flagged_squares.include?(active_square)
|
131
|
+
@flagged_squares -= [active_square]
|
132
|
+
@marked_squares += [active_square]
|
133
|
+
elsif marked_squares.include?(active_square)
|
134
|
+
@marked_squares -= [active_square]
|
135
|
+
elsif flagged_squares.length < size[:mines]
|
136
|
+
@flagged_squares += [active_square]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def reveal_active_square
|
141
|
+
return if over? || flagged_squares.include?(active_square)
|
142
|
+
|
143
|
+
reveal_square(active_square)
|
144
|
+
end
|
145
|
+
|
146
|
+
def squares
|
147
|
+
height.times.map do |y|
|
148
|
+
width.times.map do |x|
|
149
|
+
pos = [x,y]
|
150
|
+
|
151
|
+
if mined_squares.include?(pos) && (revealed_squares[pos] || over?)
|
152
|
+
MINE
|
153
|
+
elsif flagged_squares.include?(pos)
|
154
|
+
FLAG
|
155
|
+
elsif marked_squares.include?(pos)
|
156
|
+
MARK
|
157
|
+
elsif revealed_squares[pos]
|
158
|
+
CLUES[revealed_squares[pos]]
|
159
|
+
else
|
160
|
+
SQUARE
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def started?
|
167
|
+
!over? && revealed_squares.count > 0
|
168
|
+
end
|
169
|
+
|
170
|
+
def won?
|
171
|
+
!lost? && revealed_squares.count == width * height - size[:mines]
|
172
|
+
end
|
173
|
+
|
174
|
+
def lost?
|
175
|
+
(revealed_squares.keys & mined_squares).any?
|
176
|
+
end
|
177
|
+
|
178
|
+
def over?
|
179
|
+
won? || lost?
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def place_mines
|
185
|
+
size[:mines].times do
|
186
|
+
pos = random_square
|
187
|
+
pos = random_square while pos == active_square || mined_squares.include?(pos)
|
188
|
+
@mined_squares << pos
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def random_square
|
193
|
+
x = (1..width).to_a.sample - 1
|
194
|
+
y = (1..height).to_a.sample - 1
|
195
|
+
[x, y]
|
196
|
+
end
|
197
|
+
|
198
|
+
def reveal_square(square)
|
199
|
+
place_mines if revealed_squares.empty?
|
200
|
+
return if revealed_squares.keys.include?(square)
|
201
|
+
return lose! if mined_squares.include?(square)
|
202
|
+
|
203
|
+
value = square_value(square)
|
204
|
+
@revealed_squares[square] = value
|
205
|
+
neighbors(square).each { |neighbor| reveal_square(neighbor) } if value.zero?
|
206
|
+
end
|
207
|
+
|
208
|
+
def lose!
|
209
|
+
@mined_squares.each { |square| @revealed_squares[square] = -1 }
|
210
|
+
end
|
211
|
+
|
212
|
+
def square_value(square)
|
213
|
+
(neighbors(square) & mined_squares).size
|
214
|
+
end
|
215
|
+
|
216
|
+
def neighbors(square)
|
217
|
+
[
|
218
|
+
# top
|
219
|
+
[square[0] - 1, square[1] - 1],
|
220
|
+
[square[0] - 0, square[1] - 1],
|
221
|
+
[square[0] + 1, square[1] - 1],
|
222
|
+
|
223
|
+
# middle
|
224
|
+
[square[0] - 1, square[1] - 0],
|
225
|
+
[square[0] + 1, square[1] - 0],
|
226
|
+
|
227
|
+
# bottom
|
228
|
+
[square[0] - 1, square[1] + 1],
|
229
|
+
[square[0] - 0, square[1] + 1],
|
230
|
+
[square[0] + 1, square[1] + 1],
|
231
|
+
].select do |x,y|
|
232
|
+
(0...width).include?(x) && (0...height).include?(y)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
#
|
@@ -0,0 +1 @@
|
|
1
|
+
#
|
data/minesweeprb.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/minesweeprb/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'minesweeprb'
|
7
|
+
spec.license = 'MIT'
|
8
|
+
spec.version = Minesweeprb::VERSION
|
9
|
+
spec.authors = ['scudco']
|
10
|
+
spec.email = ['3806+scudco@users.noreply.github.com']
|
11
|
+
|
12
|
+
spec.summary = 'Terminal-based Minesweeper'
|
13
|
+
spec.homepage = 'https://github.com/scudco/minesweeper'
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
|
15
|
+
|
16
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
17
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
18
|
+
spec.metadata['changelog_uri'] = spec.homepage
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added
|
22
|
+
# into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
f.match(%r{^(test|spec|features)/})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
spec.bindir = 'exe'
|
29
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ['lib']
|
31
|
+
|
32
|
+
spec.add_runtime_dependency 'tty', '~> 0.10'
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: minesweeprb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- scudco
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-02-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: tty
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.10'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.10'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- 3806+scudco@users.noreply.github.com
|
30
|
+
executables:
|
31
|
+
- minesweeprb
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- ".gitignore"
|
36
|
+
- ".rspec"
|
37
|
+
- ".rubocop.yml"
|
38
|
+
- ".travis.yml"
|
39
|
+
- Gemfile
|
40
|
+
- Gemfile.lock
|
41
|
+
- LICENSE
|
42
|
+
- README.md
|
43
|
+
- Rakefile
|
44
|
+
- bin/console
|
45
|
+
- bin/setup
|
46
|
+
- exe/minesweeprb
|
47
|
+
- lib/minesweeprb.rb
|
48
|
+
- lib/minesweeprb/cli.rb
|
49
|
+
- lib/minesweeprb/command.rb
|
50
|
+
- lib/minesweeprb/commands/.gitkeep
|
51
|
+
- lib/minesweeprb/commands/play.rb
|
52
|
+
- lib/minesweeprb/game.rb
|
53
|
+
- lib/minesweeprb/templates/.gitkeep
|
54
|
+
- lib/minesweeprb/templates/play/.gitkeep
|
55
|
+
- lib/minesweeprb/version.rb
|
56
|
+
- minesweeprb.gemspec
|
57
|
+
homepage: https://github.com/scudco/minesweeper
|
58
|
+
licenses:
|
59
|
+
- MIT
|
60
|
+
metadata:
|
61
|
+
homepage_uri: https://github.com/scudco/minesweeper
|
62
|
+
source_code_uri: https://github.com/scudco/minesweeper
|
63
|
+
changelog_uri: https://github.com/scudco/minesweeper
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 2.3.0
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubygems_version: 3.1.2
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: Terminal-based Minesweeper
|
83
|
+
test_files: []
|