basher-basher 0.1.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0f7643a1d5341d906390a12466992ecbcd7d9e8e
4
+ data.tar.gz: 4b0a87dd94139d10934e4ba9dc10827f9ebceee7
5
+ SHA512:
6
+ metadata.gz: 6913dfa636e802e50daea0bcc31aa0939c0b4a6bbff54bd367d45246d13962969166a9eb0e3e21335f67b038f83d101c79970cd24063821da546c2f968bdad2f
7
+ data.tar.gz: 99c6d8205450f186cd32a018c671c5d4d97b1c3657c5c1db76543bcdf6eff74895ff6e647f34b94752e51b9b205c106e7dec0fcb60fca791b0d336bd7313b8a6
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.sublime*
11
+
12
+ /experiments
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in basher.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Stefan Rotariu
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,37 @@
1
+ # Basher
2
+
3
+ Basher is a small CLI game that tests your typing speed.
4
+
5
+ [![asciicast](https://asciinema.org/a/36151.png)](https://asciinema.org/a/36151)
6
+
7
+ ## Installation
8
+
9
+ ```shell
10
+ $ gem install basher-basher
11
+ ```
12
+
13
+ ## Requirements
14
+
15
+ * Ruby 2.3
16
+
17
+ ## Usage
18
+
19
+ ```shell
20
+ $ basher -h
21
+ ```
22
+
23
+ ## Development
24
+
25
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
+
27
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/shuriu/basher.
32
+
33
+
34
+ ## License
35
+
36
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
37
+
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :spec
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'basher/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "basher-basher"
8
+ spec.version = Basher::VERSION
9
+ spec.authors = ["shuriu"]
10
+ spec.email = ["stefan.rotariu@gmail.com"]
11
+
12
+ spec.summary = %q{Small CLI text game that tests your typing speed.}
13
+ spec.homepage = "https://github.com/shuriu/basher"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "bin"
18
+ spec.executables = ["basher"]
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.11"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "minitest", "~> 5.0"
24
+ spec.add_development_dependency "pry"
25
+ spec.add_development_dependency "binding_of_caller"
26
+ spec.add_development_dependency "pry-byebug"
27
+ spec.add_dependency "curtis", "~> 0.1.3"
28
+ spec.add_dependency "artii", "~> 2.1.1"
29
+ end
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+
4
+ require 'bundler/setup'
5
+ require 'basher'
6
+
7
+ options = {
8
+ debug: false
9
+ }
10
+
11
+ OptionParser.new do |o|
12
+ o.on('-d', '--debug', 'Show small debug bar at the top (kinda useless)') do
13
+ require 'pry'
14
+ require 'binding_of_caller'
15
+
16
+ options[:debug] = true
17
+ end
18
+
19
+ o.on('-v', '--version', 'Print current version') do
20
+ puts Basher::VERSION
21
+ exit
22
+ end
23
+
24
+ o.on('-h', '--help', 'Show this message') do
25
+ puts o
26
+ exit
27
+ end
28
+ end.parse!
29
+
30
+ trap('INT') { exit }
31
+ Basher.start(options)
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "basher"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ require "pry"
10
+ Pry.start
@@ -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
@@ -0,0 +1,21 @@
1
+ require 'curtis'
2
+ require 'basher/refinements/string'
3
+ require 'basher/version'
4
+ require 'basher/ui'
5
+ require 'basher/game'
6
+
7
+ module Basher
8
+ module_function
9
+
10
+ def start(**options)
11
+ Curtis.show do |screen|
12
+ game = Basher::Game.new(screen, options)
13
+
14
+ Curtis::Input.get do |key|
15
+ result = game.handle(key)
16
+ break if result == :quit
17
+ game.render
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,59 @@
1
+ module Basher
2
+ # Helper class that allows storing movement state inside a collection.
3
+ class Cursor
4
+ # Frozen reference of the word.
5
+ attr_reader :collection
6
+ # Position of the cursor inside #collection. Default is 0.
7
+ attr_reader :index
8
+
9
+ # Returns a Cursor instance for the given collection. The cursor's
10
+ # index is defaulted to 0.
11
+ def initialize(collection)
12
+ @collection = collection
13
+ @index = 0
14
+ end
15
+
16
+ # Gets the item at the cursor.
17
+ def item
18
+ collection[index]
19
+ end
20
+
21
+ # Gets the items before the cursor.
22
+ def previous
23
+ collection[0...index]
24
+ end
25
+
26
+ # Gets remaining items, including item at the cursor.
27
+ def remaining
28
+ collection[index..-1]
29
+ end
30
+
31
+ # Advance the cursor one step.
32
+ def advance!
33
+ @index += 1 unless finished?
34
+ item
35
+ end
36
+
37
+ # Rewind the cursor back to start, and return the item.
38
+ def rewind!
39
+ @index = start
40
+ item
41
+ end
42
+
43
+ # Returns true if the cursor is at the last index,
44
+ # or false otherwise.
45
+ def finished?
46
+ index == finish
47
+ end
48
+
49
+ private
50
+
51
+ def start
52
+ 0
53
+ end
54
+
55
+ def finish
56
+ collection.size
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,50 @@
1
+ module Basher
2
+ # Gives samples of random words as single instances or in batches.
3
+ # TODO: Extract constants to global configuration values.
4
+ # TODO: Allow words_list to be a global config option / value.
5
+ module Dictionary
6
+ module_function
7
+
8
+ # Where is the words list path on the system. It is also assumed
9
+ # that there is only one word per line.
10
+ WORDS_LIST_PATH = '/usr/share/dict/words'.freeze
11
+
12
+ # Filter out words with fewer characters.
13
+ MIN_SIZE = 3
14
+ # Filter out words with more characters.
15
+ MAX_SIZE = 15
16
+
17
+ # Small utility function that preloads the words_list.
18
+ def preload
19
+ !words_list.empty?
20
+ end
21
+
22
+ # Returns a random word from the words file.
23
+ # The size argument must be greater than MIN_SIZE.
24
+ #
25
+ # +size+ - the size of the returned word.
26
+ def random_word(size = MIN_SIZE)
27
+ # Return any word from the list if we supply size: nil
28
+ return words_list.sample unless size
29
+
30
+ grouped_words_list.fetch(size) do
31
+ fail "Size must be in #{MIN_SIZE}..#{MAX_SIZE}"
32
+ end.sample
33
+ end
34
+
35
+ # Group words in the list by size.
36
+ def grouped_words_list
37
+ @grouped_words_list ||= words_list.group_by(&:size)
38
+ end
39
+
40
+ # Returns an array of words, read from a file. Also caches the list.
41
+ def words_list
42
+ @words_list ||= File.open(WORDS_LIST_PATH, 'r') do |file|
43
+ file.each_line.lazy
44
+ .map { |word| word.chomp.downcase }
45
+ .select { |word| word =~ /\A[a-z]{#{MIN_SIZE},#{MAX_SIZE}}\z/ }
46
+ .to_a
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,234 @@
1
+ require 'basher/timer'
2
+ require 'basher/state'
3
+ require 'basher/handler'
4
+ require 'basher/dictionary'
5
+ require 'basher/cursor'
6
+ require 'basher/word'
7
+ require 'basher/level'
8
+
9
+ module Basher
10
+ class Game
11
+ include UI
12
+
13
+ attr_reader :base_view
14
+ attr_reader :timer
15
+ attr_reader :handler
16
+ attr_reader :state
17
+ attr_reader :debug
18
+
19
+ attr_reader :difficulty
20
+ attr_reader :level
21
+ attr_reader :misses
22
+ attr_reader :characters
23
+ attr_reader :words
24
+
25
+ private def setup_default_bindings
26
+ Handler.bind :resize do
27
+ resize_and_reposition
28
+ end
29
+
30
+ Handler.bind 'q' do
31
+ case state.current
32
+ when :paused then back_to_menu
33
+ when :menu then :quit
34
+ when :score then back_to_menu
35
+ end
36
+ end
37
+
38
+ Handler.bind :escape do
39
+ case state.current
40
+ when :in_game
41
+ pause_game
42
+ when :paused
43
+ back_to_game
44
+ when :score
45
+ back_to_menu
46
+ end
47
+ end
48
+
49
+ Handler.bind :enter do
50
+ case state.current
51
+ when :score
52
+ back_to_menu
53
+ end
54
+ end
55
+
56
+ Handler.bind 's' do
57
+ case state.current
58
+ when :menu
59
+ start_game
60
+ end
61
+ end
62
+ end
63
+
64
+ def initialize(base_view, state: :menu, debug: false, bindings: {})
65
+ @debug = debug
66
+
67
+ @base_view = base_view
68
+ base_view.refresh
69
+
70
+ @state = State.new(state)
71
+
72
+ setup_default_bindings
73
+ @handler = Handler.new(bindings)
74
+
75
+ transition_to @state.current
76
+ @timer = Timer.new
77
+ end
78
+
79
+ def handle(input)
80
+ debug_view.last_input = input if debugging?
81
+
82
+ if state.in_game? && handler.letter?(input)
83
+ execute_logic input
84
+ end
85
+
86
+ handler.invoke(input)
87
+ end
88
+
89
+ def execute_logic(char)
90
+ if char == word.char
91
+ next_letter!
92
+ else
93
+ @misses += 1
94
+ level.timer.advance(200)
95
+ end
96
+ end
97
+
98
+ def word
99
+ level.word
100
+ end
101
+
102
+ def next_letter!
103
+ @characters += 1
104
+ word.advance!
105
+
106
+ next_word! if word.finished?
107
+ end
108
+
109
+ def next_word!
110
+ @words += 1
111
+ level.advance!
112
+
113
+ next_level! if level.finished?
114
+ end
115
+
116
+ def next_level!
117
+ @difficulty += 1
118
+ level.finish if level
119
+
120
+ @level = Level.start(difficulty) do
121
+ stop_game
122
+ end
123
+ end
124
+
125
+ def accuracy
126
+ return 0 if total_presses.zero?
127
+ value = (total_presses - misses).to_f / total_presses * 100
128
+ value.round(2)
129
+ end
130
+
131
+ def words_per_minute
132
+ words * 60 / timer.total_elapsed_in_seconds
133
+ end
134
+ alias_method :wpm, :words_per_minute
135
+
136
+ def chars_per_minute
137
+ total_presses * 60 / timer.total_elapsed_in_seconds
138
+ end
139
+ alias_method :cpm, :chars_per_minute
140
+
141
+ def total_presses
142
+ characters + misses
143
+ end
144
+
145
+ def render
146
+ base_view.render
147
+ current_views.each(&:render)
148
+ end
149
+
150
+ def clear
151
+ base_view.clear
152
+ current_views.each(&:clear)
153
+ end
154
+
155
+ def refresh
156
+ base_view.refresh
157
+ current_views.each(&:refresh)
158
+ end
159
+
160
+ def resize_and_reposition
161
+ clear
162
+ views.each(&:will_resize!)
163
+ current_views.each(&:resize_and_reposition)
164
+ render
165
+ end
166
+
167
+ def transition_to(new_state)
168
+ before_transition
169
+ state.transition_to(new_state)
170
+ after_transition
171
+ end
172
+
173
+ private
174
+
175
+ def debugging?
176
+ @debug
177
+ end
178
+
179
+ def playing?
180
+ state.playing? && input.letter?
181
+ end
182
+
183
+ def before_transition
184
+ clear
185
+ refresh
186
+ end
187
+
188
+ def after_transition
189
+ views.each do |view|
190
+ view.resize_and_reposition if view.should_redraw
191
+ end
192
+
193
+ render
194
+ end
195
+
196
+ def pause_game
197
+ timer.stop
198
+ level.timer.stop
199
+ transition_to(:paused)
200
+ end
201
+
202
+ def back_to_menu
203
+ timer.reset
204
+ transition_to(:menu)
205
+ end
206
+
207
+ def back_to_game
208
+ timer.start
209
+ level.timer.start
210
+ transition_to(:in_game)
211
+ end
212
+
213
+ def start_game
214
+ transition_to(:loading)
215
+ Basher::Dictionary.preload
216
+ setup_game
217
+ transition_to(:in_game)
218
+ timer.start
219
+ end
220
+
221
+ def stop_game
222
+ timer.stop
223
+ transition_to(:score)
224
+ end
225
+
226
+ def setup_game
227
+ @difficulty = 0
228
+ @characters = 0
229
+ @misses = 0
230
+ @words = 0
231
+ next_level!
232
+ end
233
+ end
234
+ end