minesweeprb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.0
6
+ before_install: gem install bundler -v 2.1.2
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in minesweeprb.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 12.0'
9
+ gem 'rspec', '~> 3.0'
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minesweeprb/version'
4
+
5
+ module Minesweeprb
6
+ class Error < StandardError; end
7
+ # Your code goes here...
8
+ end
@@ -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
+ #
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minesweeprb
4
+ VERSION = '0.1.0'
5
+ end
@@ -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: []