minesweeprb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []