typing_trainer 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 +7 -0
- data/.gitignore +17 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +49 -0
- data/Rakefile +2 -0
- data/bin/typing_trainer +29 -0
- data/lib/typing_trainer.rb +32 -0
- data/lib/typing_trainer/game.rb +318 -0
- data/lib/typing_trainer/keyboard_layout.rb +7 -0
- data/lib/typing_trainer/keyboard_layout/qwerty.rb +37 -0
- data/lib/typing_trainer/level.rb +44 -0
- data/lib/typing_trainer/level_generator.rb +51 -0
- data/lib/typing_trainer/version.rb +3 -0
- data/screenshots/playing.png +0 -0
- data/spec/game_spec.rb +41 -0
- data/spec/level_generator_spec.rb +16 -0
- data/text/dvorak/home.txt +4 -0
- data/text/dvorak/numbers.txt +5 -0
- data/text/fox_and_crow.txt +8 -0
- data/text/hare_and_hound.txt +5 -0
- data/typing_trainer.gemspec +23 -0
- metadata +152 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5b3afbdba090c7a2a268af3a7329fa7e030819b33a2b0a201b65b62b7f453475
|
4
|
+
data.tar.gz: 0425d685fda5e33b89618aebc0f8ca2c52cc99e3f6e394520d7663cc91420240
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6ab033cdd3268b4ba6c51932e4c76a35a989e0313e0c0cc4488d18259a7549e86db6693299a302bf30a9459a749e7ee1c70f63bc8b4a5e1428fb091580d7dd9f
|
7
|
+
data.tar.gz: cd32d9e32af941911bc3d67a1404e1dfea2be0c198bce04347c7a81e6c9a4d646bddee8c7cd152d238c629542c53da2de3e24e99a31bd94ff07cce1af2d2455f
|
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.2
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Juan Germán Castañeda Echevarría
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# TypingTrainer
|
2
|
+
|
3
|
+
Learn to touch type from your command line!
|
4
|
+
|
5
|
+
## Gameplay screenshots
|
6
|
+
|
7
|
+

|
8
|
+
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
$ gem install 'typing_trainer'
|
13
|
+
|
14
|
+
## Starting the game
|
15
|
+
|
16
|
+
$ typing_trainer
|
17
|
+
|
18
|
+
To exit during game, use `Ctrl-C` or wait until asked if you want to continue playing.
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
$ typing_trainer --help
|
23
|
+
|
24
|
+
Usage: typing_trainer [options]
|
25
|
+
-l, --level LEVEL The starting level to play
|
26
|
+
-f, --file FILEPATH Path to file containing custom text to use
|
27
|
+
-a, --advanced Hide finger help (default show)
|
28
|
+
-d, --debug Show debugging messages (default hide)
|
29
|
+
|
30
|
+
## Contributing
|
31
|
+
|
32
|
+
1. Fork it
|
33
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
34
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
35
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
36
|
+
5. Create new Pull Request
|
37
|
+
|
38
|
+
## Architecture Overview
|
39
|
+
|
40
|
+
This project is comprised by the following elements:
|
41
|
+
|
42
|
+
1. `typing_trainer` - The binary to start the trainer
|
43
|
+
2. `TypingTrainer` - The high level runner
|
44
|
+
3. `TypingTrainer::Game` - Abstracts game mechanics and modes and screen
|
45
|
+
4. `TypingTrainer::LevelGenerator` - Generates levels based on a layout
|
46
|
+
5. `TypingTrainer::Level` - Represents a level: the text to use, instructions and settings
|
47
|
+
6. `TypingTrainer::KeyboardLayout` - Defines finger mappings and letter progressions
|
48
|
+
|
49
|
+
|
data/Rakefile
ADDED
data/bin/typing_trainer
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby -r rubygems
|
2
|
+
require 'typing_trainer'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
options = {}
|
6
|
+
|
7
|
+
OptionParser.new do |opts|
|
8
|
+
opts.accept(Symbol) do |string|
|
9
|
+
string.to_sym
|
10
|
+
end
|
11
|
+
|
12
|
+
opts.banner = 'Usage: typing_trainer [options]'
|
13
|
+
|
14
|
+
opts.on('-t', '--letter LETTER', String, "A single letter to practice")
|
15
|
+
|
16
|
+
opts.on('-l', '--level LEVEL', Integer, "The starting level to play")
|
17
|
+
|
18
|
+
opts.on('-y', '--layout LAYOUT', Symbol, "The layout to use for finger hints and level progression")
|
19
|
+
|
20
|
+
opts.on('-f', '--file FILEPATH', String, "Path to file containing custom text to use")
|
21
|
+
|
22
|
+
opts.on('-a', '--advanced', TrueClass, "Hide finger help (default show)")
|
23
|
+
|
24
|
+
opts.on('-d', '--debug', TrueClass, "Show debugging messages (default hide)")
|
25
|
+
end.parse!(into: options)
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
TypingTrainer.run(options)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "typing_trainer/version"
|
2
|
+
|
3
|
+
require "highline"
|
4
|
+
require "highline/system_extensions"
|
5
|
+
require "stty"
|
6
|
+
require "ffi"
|
7
|
+
require "termios"
|
8
|
+
require "typing_trainer/game"
|
9
|
+
|
10
|
+
module TypingTrainer
|
11
|
+
|
12
|
+
COLORS = HighLine::ColorScheme.new do |cs|
|
13
|
+
cs[:error] = [ :bold, :white, :on_red ]
|
14
|
+
cs[:correct] = [ :bold, :white, :on_green ]
|
15
|
+
end
|
16
|
+
|
17
|
+
HighLine::Style.new(:name=>:return, :builtin=>true, :code=>"\r")
|
18
|
+
HighLine::Style.new(:name=>:newline, :builtin=>true, :code=>"\n")
|
19
|
+
HighLine::Style.new(:name=>:up, :builtin=>true, :code=>"\e[A")
|
20
|
+
HighLine::Style.new(:name=>:down, :builtin=>true, :code=>"\e[B")
|
21
|
+
HighLine::Style.new(:name=>:forward, :builtin=>true, :code=>"\e[C")
|
22
|
+
HighLine::Style.new(:name=>:backward, :builtin=>true, :code=>"\e[D")
|
23
|
+
|
24
|
+
HighLine.color_scheme = COLORS
|
25
|
+
|
26
|
+
def self.run(options)
|
27
|
+
@game = Game.new(options)
|
28
|
+
|
29
|
+
@game.play!
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
require "typing_trainer/level_generator"
|
2
|
+
require "typing_trainer/level"
|
3
|
+
|
4
|
+
#
|
5
|
+
# Game
|
6
|
+
#
|
7
|
+
# Manages the game and internal state, checks if input is correct, prints information and handles
|
8
|
+
# screen cleanup
|
9
|
+
#
|
10
|
+
class TypingTrainer::Game
|
11
|
+
include HighLine::SystemExtensions
|
12
|
+
|
13
|
+
# Creates a new game
|
14
|
+
#
|
15
|
+
# @param [String] letter only words with that letter are shown and finger is shown.
|
16
|
+
# @param [Integer] level the level of the game, this is used to generate sentences. Overrides `letter`
|
17
|
+
# @param [String] file a file path to a text file for the game, overrides `level`
|
18
|
+
# @param [Array<String>] sentences the list of sentences to use for this game
|
19
|
+
# @param [Bool] advanced whether to show the finger hints or not
|
20
|
+
# @param [Symbol] layout the name of the layout to use. See TypingTrainer::KeyboardLayout
|
21
|
+
def initialize(level: nil, file: nil, sentences: nil, layout: :QWERTY, debug: false, advanced: false, letter: nil)
|
22
|
+
@layout = layout
|
23
|
+
@level_number = level
|
24
|
+
if !level && !letter
|
25
|
+
@level_number = 1
|
26
|
+
end
|
27
|
+
@file = file
|
28
|
+
@debug = debug
|
29
|
+
@advanced = advanced
|
30
|
+
@override_sentences = sentences
|
31
|
+
@introduce_letter = letter
|
32
|
+
@h = HighLine.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def play!
|
36
|
+
@total_errors = 0
|
37
|
+
@errors = {}
|
38
|
+
@level = generate_level
|
39
|
+
@sentences = get_sentences
|
40
|
+
prepare_screen
|
41
|
+
before = Time.now
|
42
|
+
@sentences.each do |sentence|
|
43
|
+
print_level_header
|
44
|
+
show_sentence(sentence)
|
45
|
+
clean_screen
|
46
|
+
end
|
47
|
+
show_result(before)
|
48
|
+
rescue SystemExit, Interrupt
|
49
|
+
reset_terminal
|
50
|
+
rescue Exception => e
|
51
|
+
reset_terminal
|
52
|
+
raise e
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def reset_terminal
|
58
|
+
clean_screen
|
59
|
+
@h.restore_mode
|
60
|
+
puts "Thank you for playing."
|
61
|
+
puts "Good bye!"
|
62
|
+
print :clear
|
63
|
+
print :erase_line
|
64
|
+
end
|
65
|
+
|
66
|
+
def generate_level
|
67
|
+
@level = if @override_sentences
|
68
|
+
TypingTrainer::Level.new(sentences: @override_sentences, layout: @layout)
|
69
|
+
elsif @file
|
70
|
+
TypingTrainer::Level.new(
|
71
|
+
sentences: open(@file).readlines.map {|l| l.strip!; l == "" ? nil : l;}.compact,
|
72
|
+
filename: @file,
|
73
|
+
layout: @layout
|
74
|
+
)
|
75
|
+
elsif @introduce_letter
|
76
|
+
TypingTrainer::LevelGenerator.generate(letters: [@introduce_letter], layout: @layout)
|
77
|
+
else
|
78
|
+
TypingTrainer::LevelGenerator.generate(level: @level_number, layout: @layout)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_sentences
|
83
|
+
@level.sentences
|
84
|
+
end
|
85
|
+
|
86
|
+
def get_character
|
87
|
+
@h.get_character
|
88
|
+
end
|
89
|
+
|
90
|
+
def show_sentence(sentence)
|
91
|
+
@cursor = 0
|
92
|
+
original = sentence
|
93
|
+
@typed = ""
|
94
|
+
|
95
|
+
print original
|
96
|
+
print :newline
|
97
|
+
print_hands(finger: @level.finger(original[@cursor])) if @introduce_letter
|
98
|
+
$stdout.flush
|
99
|
+
|
100
|
+
while c = get_character.chr do
|
101
|
+
# puts "CHAR: #{c}"
|
102
|
+
typed_error = false
|
103
|
+
case c
|
104
|
+
when "\x7F" # backspace
|
105
|
+
delete_char
|
106
|
+
print_hands(finger: @level.finger(original[@cursor]))
|
107
|
+
@cursor.times do
|
108
|
+
print :forward
|
109
|
+
end
|
110
|
+
next
|
111
|
+
when original[@cursor] # good
|
112
|
+
print c, :correct
|
113
|
+
else # error
|
114
|
+
log_error(c)
|
115
|
+
typed_error = true
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
@typed += c
|
120
|
+
@cursor += 1
|
121
|
+
finished = original == @typed
|
122
|
+
should_show_hands = !finished && original[@cursor] && (@introduce_letter || typed_error)
|
123
|
+
if (should_show_hands)
|
124
|
+
print_hands(finger: @level.finger(original[@cursor]))
|
125
|
+
@cursor.times do
|
126
|
+
print :forward
|
127
|
+
end
|
128
|
+
elsif !finished
|
129
|
+
hide_hands
|
130
|
+
@cursor.times do
|
131
|
+
print :forward
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
typed_error = false
|
136
|
+
|
137
|
+
if original == @typed
|
138
|
+
break
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Prints hands with finger hints
|
144
|
+
def print_hands(finger: nil)
|
145
|
+
print :newline
|
146
|
+
print HANDS
|
147
|
+
(HANDS.lines.count + 1).times do
|
148
|
+
print :up
|
149
|
+
end
|
150
|
+
print_finger(finger)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Erases the lines showing finger hints
|
154
|
+
def hide_hands
|
155
|
+
print :newline
|
156
|
+
(HANDS.lines.count + 1).times do
|
157
|
+
print :erase_line
|
158
|
+
puts
|
159
|
+
end
|
160
|
+
(HANDS.lines.count + 1).times do
|
161
|
+
print :up
|
162
|
+
end
|
163
|
+
print :up
|
164
|
+
end
|
165
|
+
|
166
|
+
# Finds which finger has to be highlighted and prints
|
167
|
+
# a white block inside it.
|
168
|
+
def print_finger(finger)
|
169
|
+
finger_character = '█'
|
170
|
+
finger_coords = {
|
171
|
+
LEFT_P: [2,1],
|
172
|
+
LEFT_R: [1,3],
|
173
|
+
LEFT_M: [1,5],
|
174
|
+
LEFT_I: [1,7],
|
175
|
+
LEFT_T: [4,10],
|
176
|
+
RIGHT_P: [2,28],
|
177
|
+
RIGHT_R: [1,26],
|
178
|
+
RIGHT_M: [1,24],
|
179
|
+
RIGHT_I: [1,22],
|
180
|
+
RIGHT_T: [4,19]
|
181
|
+
}
|
182
|
+
|
183
|
+
coords = finger_coords[finger]
|
184
|
+
if (coords)
|
185
|
+
print :down
|
186
|
+
coords[0].times { print :down }
|
187
|
+
coords[1].times { print :forward }
|
188
|
+
print finger_character
|
189
|
+
print :backward
|
190
|
+
coords[1].times { print :backward }
|
191
|
+
coords[0].times { print :up }
|
192
|
+
print :up
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Print results of last played game.
|
197
|
+
def show_result(started_at)
|
198
|
+
@h.restore_mode
|
199
|
+
total_chars = @sentences.join.size
|
200
|
+
accuracy = "#{((total_chars - @total_errors) * 100.0 / total_chars).round(2)}%"
|
201
|
+
elapsed_minutes = (Time.now - started_at)/60
|
202
|
+
total_words = @sentences.join.split(' ').size
|
203
|
+
# wpm = "#{total_words/elapsed_minutes.to_f} WPM"
|
204
|
+
ccpm = "#{((total_chars)/elapsed_minutes.to_f).floor/5} WPM / #{((total_chars)/elapsed_minutes.to_f).floor} CPM"
|
205
|
+
|
206
|
+
print "These are your results:\n", :bold
|
207
|
+
print "Total time: #{elapsed_minutes} minutes\n", :bold
|
208
|
+
print "Character count: #{total_chars}\n", :bold
|
209
|
+
print 'Speed: ', :blue
|
210
|
+
print ccpm, :bold
|
211
|
+
print ' '
|
212
|
+
print 'Accuracy: ', :green
|
213
|
+
print accuracy, :bold
|
214
|
+
print ' '
|
215
|
+
print 'Errors: ', :red
|
216
|
+
print "#{@total_errors}/#{total_chars}", :bold
|
217
|
+
if @h.ask("\n\nDo you want to play again? [y,n]", ["y", "n"]) == "y"
|
218
|
+
# The logic ahead allows us to support starting from a letter or a level indistictively and
|
219
|
+
# continue with the next levels and letters as expected
|
220
|
+
if @introduce_letter
|
221
|
+
@level_number = @level.next_level(@introduce_letter)
|
222
|
+
if @level_number == 1
|
223
|
+
# If they only know one letter, don't make them repeat it. Introduce one more instead
|
224
|
+
@introduce_letter = @level.next_letter(1)
|
225
|
+
else
|
226
|
+
@introduce_letter = nil
|
227
|
+
end
|
228
|
+
elsif @level_number
|
229
|
+
@introduce_letter = @level.next_letter(@level_number)
|
230
|
+
end
|
231
|
+
play!
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Our own print to override the system one.
|
236
|
+
# This uses HighLine to output special characters
|
237
|
+
# If the parameter is a symbol, it will use
|
238
|
+
# HighLine.Style() and use its code
|
239
|
+
def print(str_or_sym, color = :none)
|
240
|
+
s = case str_or_sym
|
241
|
+
when String
|
242
|
+
@h.color(str_or_sym, color)
|
243
|
+
when Symbol
|
244
|
+
HighLine.Style(str_or_sym).code
|
245
|
+
end
|
246
|
+
$stdout.print(s)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Ensures the screen has all the lines that the terminal has
|
250
|
+
# to move freely without needing to create more lines.
|
251
|
+
def prepare_screen
|
252
|
+
# Ensure we restore mode bedore setting to no_echo
|
253
|
+
# otherwise we lose context and cannot go back
|
254
|
+
@h.restore_mode rescue
|
255
|
+
print :erase_line
|
256
|
+
@h.output_rows.times do
|
257
|
+
print :newline
|
258
|
+
end
|
259
|
+
@h.output_rows.times do
|
260
|
+
print :up
|
261
|
+
end
|
262
|
+
@h.raw_no_echo_mode
|
263
|
+
end
|
264
|
+
|
265
|
+
# Uses the level information to print
|
266
|
+
# a header while a game is underway.
|
267
|
+
def print_level_header
|
268
|
+
print @level.header(@h.output_cols)
|
269
|
+
if @debug
|
270
|
+
puts({
|
271
|
+
layout: @layout,
|
272
|
+
level: @level_number,
|
273
|
+
letter: @introduce_letter,
|
274
|
+
advanced: @advanced,
|
275
|
+
cursor: @cursor
|
276
|
+
}).inspect
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# Uses a clear screen escape sequence
|
281
|
+
# and prepares the screen
|
282
|
+
def clean_screen
|
283
|
+
print "\e[2J"
|
284
|
+
prepare_screen
|
285
|
+
end
|
286
|
+
|
287
|
+
# Deletes a character in the input "bar"
|
288
|
+
def delete_char
|
289
|
+
print :backward
|
290
|
+
print :erase_char
|
291
|
+
@cursor -= 1 if @cursor > 0
|
292
|
+
@typed = @typed[0..-2] if @typed.size > 0
|
293
|
+
end
|
294
|
+
|
295
|
+
# Changes the color of the input "bar" to show
|
296
|
+
# the user didn't type the right character
|
297
|
+
def log_error(c)
|
298
|
+
print c, :error
|
299
|
+
@total_errors += 1
|
300
|
+
@errors[c] ||= 0
|
301
|
+
@errors[c] += 1
|
302
|
+
end
|
303
|
+
|
304
|
+
# ASCII Hands. We need to escape \ because it is
|
305
|
+
# a special character for strings.
|
306
|
+
HANDS = <<~HANDS
|
307
|
+
_.-._ _.-._
|
308
|
+
_| | | | | | | |_
|
309
|
+
| | | | | | | | | |
|
310
|
+
| | | | | /\\ /\\ | | | | |
|
311
|
+
| |/ / \\ \\| |
|
312
|
+
| / / \\ \\ |
|
313
|
+
| / \\ |
|
314
|
+
\\ / \\ /
|
315
|
+
| | | |
|
316
|
+
HANDS
|
317
|
+
|
318
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#require "typing_trainer/keyboard_layout"
|
2
|
+
|
3
|
+
module TypingTrainer::KeyboardLayout
|
4
|
+
class Qwerty
|
5
|
+
LETTER_PROGRESSION = "asdfjklghruvmbntyeiwoqpxz"
|
6
|
+
|
7
|
+
FINGER = {
|
8
|
+
'a' => :LEFT_P,
|
9
|
+
'b' => :LEFT_I,
|
10
|
+
'c' => :LEFT_M,
|
11
|
+
'd' => :LEFT_M,
|
12
|
+
'e' => :LEFT_M,
|
13
|
+
'f' => :LEFT_I,
|
14
|
+
'g' => :LEFT_I,
|
15
|
+
'h' => :RIGHT_I,
|
16
|
+
'i' => :RIGHT_M,
|
17
|
+
'j' => :RIGHT_I,
|
18
|
+
'k' => :RIGHT_M,
|
19
|
+
'l' => :RIGHT_R,
|
20
|
+
'm' => :RIGHT_I,
|
21
|
+
'n' => :RIGHT_I,
|
22
|
+
'o' => :RIGHT_R,
|
23
|
+
'p' => :RIGHT_P,
|
24
|
+
'q' => :LEFT_P,
|
25
|
+
'r' => :LEFT_I,
|
26
|
+
's' => :LEFT_R,
|
27
|
+
't' => :LEFT_I,
|
28
|
+
'u' => :RIGHT_I,
|
29
|
+
'v' => :LEFT_I,
|
30
|
+
'w' => :LEFT_R,
|
31
|
+
'x' => :LEFT_R,
|
32
|
+
'y' => :RIGHT_I,
|
33
|
+
'z' => :LEFT_P,
|
34
|
+
' ' => :LEFT_T
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "typing_trainer/keyboard_layout"
|
2
|
+
|
3
|
+
class TypingTrainer::Level
|
4
|
+
attr_reader :sentences
|
5
|
+
attr_reader :number
|
6
|
+
attr_reader :letters
|
7
|
+
|
8
|
+
def initialize(number: nil, letters: nil, sentences: [], filename: nil, layout: nil)
|
9
|
+
throw "Missing layout!" unless layout
|
10
|
+
throw "Missing sentences!" unless sentences.count > 0
|
11
|
+
@number = number
|
12
|
+
@letters = letters
|
13
|
+
@sentences = sentences
|
14
|
+
@layout_id = layout
|
15
|
+
layouts = TypingTrainer::KeyboardLayout::LAYOUTS
|
16
|
+
@layout = layouts[layout]
|
17
|
+
end
|
18
|
+
|
19
|
+
def header(width)
|
20
|
+
if @number
|
21
|
+
puts "Playing Level #{@number}"
|
22
|
+
puts "-"*width
|
23
|
+
puts
|
24
|
+
elsif @letters && @letters.size == 1
|
25
|
+
puts "New letter: #{@letters[0].upcase}"
|
26
|
+
puts "-"*width
|
27
|
+
puts
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def finger(letter)
|
32
|
+
@layout::FINGER[letter.downcase]
|
33
|
+
end
|
34
|
+
|
35
|
+
# What is the next level if you know up to this letter?
|
36
|
+
def next_level(letter)
|
37
|
+
@layout::LETTER_PROGRESSION.index(letter.downcase) + 1
|
38
|
+
end
|
39
|
+
|
40
|
+
# What is the next letter to learn after beating this level?
|
41
|
+
def next_letter(level)
|
42
|
+
@layout::LETTER_PROGRESSION[level]
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'contemporary_words'
|
2
|
+
require "typing_trainer/level"
|
3
|
+
require "typing_trainer/keyboard_layout"
|
4
|
+
|
5
|
+
class TypingTrainer::LevelGenerator
|
6
|
+
|
7
|
+
SENCENCES_PER_LEVEL = 3
|
8
|
+
WORDS_PER_SENTENCE = 8
|
9
|
+
|
10
|
+
def self.generate(level: nil, letters: nil, layout: nil)
|
11
|
+
throw "You need to specify a layout" unless layout
|
12
|
+
throw "No level or letters were provided to generate a Game level :(" unless level || letters
|
13
|
+
|
14
|
+
layouts = TypingTrainer::KeyboardLayout::LAYOUTS
|
15
|
+
|
16
|
+
layout_letters = layouts[layout]::LETTER_PROGRESSION
|
17
|
+
if letters
|
18
|
+
letters = letters.join('').downcase
|
19
|
+
end
|
20
|
+
letters ||= layout_letters.slice(0, level)
|
21
|
+
sentences = self.new(letters).generate_sentences(SENCENCES_PER_LEVEL)
|
22
|
+
TypingTrainer::Level.new(number: level, letters: letters, sentences: sentences, layout: layout)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(letters)
|
26
|
+
@letters = letters.split(//)
|
27
|
+
@level_pattern = /^[#{letters}]+$/
|
28
|
+
@words = initialize_words
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize_words
|
32
|
+
words = ContemporaryWords.all.filter { |word| @level_pattern.match?(word) }
|
33
|
+
while words.size < WORDS_PER_SENTENCE
|
34
|
+
words << self.generate_word
|
35
|
+
end
|
36
|
+
words
|
37
|
+
end
|
38
|
+
|
39
|
+
def generate_sentences(count)
|
40
|
+
sentences = []
|
41
|
+
count.times {
|
42
|
+
sentences << @words.sample(WORDS_PER_SENTENCE).join(' ') + ' '
|
43
|
+
}
|
44
|
+
sentences
|
45
|
+
end
|
46
|
+
|
47
|
+
def generate_word
|
48
|
+
(@letters * 10).sample(rand(5) + 1).join('')
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
Binary file
|
data/spec/game_spec.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'typing_trainer'
|
2
|
+
require 'typing_trainer/game'
|
3
|
+
|
4
|
+
describe TypingTrainer::Game, '#play!' do
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
game.stub(:prepare_screen)
|
8
|
+
game.stub(:show_result)
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:game) { TypingTrainer::Game.new(sentences: ["first", "second"], layout: :QWERTY) }
|
12
|
+
|
13
|
+
it "shows each sentence" do
|
14
|
+
game.should_receive(:show_sentence).exactly(2)
|
15
|
+
game.should_receive(:clean_screen).exactly(2)
|
16
|
+
game.should_receive(:show_result)
|
17
|
+
game.play!
|
18
|
+
end
|
19
|
+
|
20
|
+
it "reads the characters" do
|
21
|
+
game.should_receive(:get_character).and_return(*%w{f i r s t s e c o n d})
|
22
|
+
game.play!
|
23
|
+
end
|
24
|
+
|
25
|
+
it "handles delete" do
|
26
|
+
input = %w{f i r s x} + [[0x7F].pack('U')] + %w{t s e c o n d}
|
27
|
+
|
28
|
+
game.should_receive(:get_character).and_return(*input)
|
29
|
+
game.should_receive(:show_result)
|
30
|
+
game.play!
|
31
|
+
end
|
32
|
+
|
33
|
+
it "handles delete at beginning" do
|
34
|
+
input = [[0x7F].pack('U')]*3 + %w{f i r s t} + %w{s e c o n d}
|
35
|
+
|
36
|
+
game.should_receive(:get_character).and_return(*input)
|
37
|
+
game.should_receive(:show_result)
|
38
|
+
game.play!
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'typing_trainer'
|
2
|
+
require 'typing_trainer/level_generator'
|
3
|
+
|
4
|
+
describe TypingTrainer::LevelGenerator, '#generate' do
|
5
|
+
|
6
|
+
it "should generate 5 sentences per level" do
|
7
|
+
level = TypingTrainer::LevelGenerator.generate(level: 1, layout: :QWERTY);
|
8
|
+
level.sentences.size.should be 4
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should generate 8 words per sentence" do
|
12
|
+
level = TypingTrainer::LevelGenerator.generate(level: 1, layout: :QWERTY);
|
13
|
+
level.sentences[0].split(' ').size.should be 8
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
A Crow having stolen a bit of meat, perched in a tree and held it in her beak.
|
2
|
+
A Fox, seeing this, longed to possess the meat himself, and by a wily stratagem succeeded.
|
3
|
+
'How handsome is the Crow,' he exclaimed, in the beauty of her shape and in the fairness of her complexion!
|
4
|
+
Oh, if her voice were only equal to her beauty, she would deservedly be considered the Queen of Birds!'
|
5
|
+
This he said deceitfully but the Crow, anxious to refute the reflection cast upon her voice,
|
6
|
+
set up a loud caw and dropped the flesh.
|
7
|
+
The Fox quickly picked it up, and thus addressed the Crow:
|
8
|
+
'My good Crow, your voice is right enough, but your wit is wanting.'
|
@@ -0,0 +1,5 @@
|
|
1
|
+
A Hound started a Hare from his lair, but after a long run, gave up the chase.
|
2
|
+
A goat-herd seeing him stop, mocked him, saying
|
3
|
+
'The little one is the best runner of the two.'
|
4
|
+
The Hound replied, 'You do not see the difference between us:
|
5
|
+
I was only running for a dinner, but he for his life.'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/typing_trainer/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ['Juan Germán Castañeda Echevarría']
|
6
|
+
gem.email = ['juanger@gmail.com']
|
7
|
+
gem.description = %q{Ruby typing trainer}
|
8
|
+
gem.summary = %q{Improve your typing skills by completing the tutorial or using your own text or code!}
|
9
|
+
gem.homepage = ''
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = 'typing_trainer'
|
15
|
+
gem.require_paths = ['lib']
|
16
|
+
gem.version = TypingTrainer::VERSION
|
17
|
+
gem.add_dependency 'highline', '~> 1.7.3'
|
18
|
+
gem.add_dependency 'ffi-ncurses'
|
19
|
+
gem.add_dependency 'ruby-termios'
|
20
|
+
gem.add_dependency 'stty'
|
21
|
+
gem.add_dependency 'contemporary_words'
|
22
|
+
gem.add_development_dependency 'rspec'
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: typing_trainer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Juan Germán Castañeda Echevarría
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-03-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: highline
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.7.3
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.7.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ffi-ncurses
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: ruby-termios
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: stty
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: contemporary_words
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Ruby typing trainer
|
98
|
+
email:
|
99
|
+
- juanger@gmail.com
|
100
|
+
executables:
|
101
|
+
- typing_trainer
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- ".gitignore"
|
106
|
+
- ".ruby-version"
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- bin/typing_trainer
|
112
|
+
- lib/typing_trainer.rb
|
113
|
+
- lib/typing_trainer/game.rb
|
114
|
+
- lib/typing_trainer/keyboard_layout.rb
|
115
|
+
- lib/typing_trainer/keyboard_layout/qwerty.rb
|
116
|
+
- lib/typing_trainer/level.rb
|
117
|
+
- lib/typing_trainer/level_generator.rb
|
118
|
+
- lib/typing_trainer/version.rb
|
119
|
+
- screenshots/playing.png
|
120
|
+
- spec/game_spec.rb
|
121
|
+
- spec/level_generator_spec.rb
|
122
|
+
- text/dvorak/home.txt
|
123
|
+
- text/dvorak/numbers.txt
|
124
|
+
- text/fox_and_crow.txt
|
125
|
+
- text/hare_and_hound.txt
|
126
|
+
- typing_trainer.gemspec
|
127
|
+
homepage: ''
|
128
|
+
licenses: []
|
129
|
+
metadata: {}
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
require_paths:
|
133
|
+
- lib
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
requirements: []
|
145
|
+
rubygems_version: 3.0.3
|
146
|
+
signing_key:
|
147
|
+
specification_version: 4
|
148
|
+
summary: Improve your typing skills by completing the tutorial or using your own text
|
149
|
+
or code!
|
150
|
+
test_files:
|
151
|
+
- spec/game_spec.rb
|
152
|
+
- spec/level_generator_spec.rb
|