ankit 0.0.1 → 0.0.2
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.
- data/lib/ankit/card.rb +2 -1
- data/lib/ankit/challenge.rb +413 -0
- data/lib/ankit/challenge_command.rb +1 -411
- data/lib/ankit/command.rb +5 -2
- data/lib/ankit/runtime.rb +1 -1
- data/test/card_test.rb +11 -1
- data/test/command_test.rb +18 -10
- metadata +11 -10
data/lib/ankit/card.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
|
2
|
+
require 'diff/lcs'
|
2
3
|
|
3
4
|
module Ankit
|
4
5
|
class Card
|
@@ -33,7 +34,7 @@ module Ankit
|
|
33
34
|
|
34
35
|
def match?(text)
|
35
36
|
return :match if plain_original == text
|
36
|
-
return :wrong if text.
|
37
|
+
return :wrong if 1 < (text.length - plain_original.length).abs
|
37
38
|
|
38
39
|
hiddens = decorated_original { |m| "*"*m[1].size }.chars.to_a
|
39
40
|
inside_essentials = to_enum(:diff_from_original, text).find do |ch|
|
@@ -0,0 +1,413 @@
|
|
1
|
+
require 'ankit/coming_command'
|
2
|
+
require 'ankit/fail_command'
|
3
|
+
require 'ankit/find_command'
|
4
|
+
require 'ankit/pass_command'
|
5
|
+
require 'ankit/round_command'
|
6
|
+
require 'highline'
|
7
|
+
require 'diff/lcs'
|
8
|
+
|
9
|
+
module Ankit
|
10
|
+
|
11
|
+
class StylableText
|
12
|
+
def self.styled_text(text, type)
|
13
|
+
case type
|
14
|
+
when :hidden
|
15
|
+
text.gsub(/\w/, "*")
|
16
|
+
when :failed
|
17
|
+
HighLine.color(text, HighLine::RED_STYLE)
|
18
|
+
when :warn
|
19
|
+
HighLine.color(text, HighLine::YELLOW_STYLE)
|
20
|
+
when :passed
|
21
|
+
HighLine.color(text, HighLine::GREEN_STYLE)
|
22
|
+
when :pending
|
23
|
+
HighLine.color(text, HighLine::DARK)
|
24
|
+
when :correct
|
25
|
+
HighLine.color(text, HighLine::GREEN_STYLE)
|
26
|
+
when :wrong
|
27
|
+
HighLine.color(text, HighLine::RED_STYLE)
|
28
|
+
else
|
29
|
+
raise
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(text); @text = text; end
|
34
|
+
|
35
|
+
def decorated(type)
|
36
|
+
raise
|
37
|
+
decorated = @text.gsub(/\[(.*?)\]/) { |t|
|
38
|
+
p Regexp::last_match.offset(0)
|
39
|
+
self.class.styled_text($1, type)
|
40
|
+
}
|
41
|
+
decorated != @text ? decorated : self.class.styled_text(@text, type)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
class Card
|
47
|
+
def hidden_original; decorated_original{ |m| StylableText.styled_text(m[1], :hidden) }; end
|
48
|
+
def corrected_original_over(wrong)
|
49
|
+
diff_from_original(wrong) do |ch|
|
50
|
+
case ch.action
|
51
|
+
when "="
|
52
|
+
ch.new_element
|
53
|
+
when "!", "+"
|
54
|
+
StylableText.styled_text(ch.new_element, :correct)
|
55
|
+
when "-"
|
56
|
+
""
|
57
|
+
else
|
58
|
+
raise
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def hilight_against_original(wrong)
|
64
|
+
diff_from_original(wrong) do |ch|
|
65
|
+
case ch.action
|
66
|
+
when "="
|
67
|
+
ch.old_element
|
68
|
+
when "!", "-"
|
69
|
+
StylableText.styled_text(ch.old_element, :wrong)
|
70
|
+
when "+"
|
71
|
+
""
|
72
|
+
else
|
73
|
+
raise
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
module Challenge
|
80
|
+
class Slot < Struct.new(:path, :rating, :event)
|
81
|
+
def maturity; self.event ? self.event.maturity : 0; end
|
82
|
+
end
|
83
|
+
|
84
|
+
class Session < Struct.new(:runtime, :npassed, :nfailed, :mature_names)
|
85
|
+
def self.make(runtime)
|
86
|
+
self.new(runtime, 0, 0, [])
|
87
|
+
end
|
88
|
+
|
89
|
+
def summary_text
|
90
|
+
total = self.npassed + self.nfailed
|
91
|
+
return "" if 0 == total
|
92
|
+
"#{self.npassed}/#{total} = #{self.npassed.to_f/total.to_f}, #mature: #{mature_names.size}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class Progress
|
97
|
+
include CardHappening, CardNaming, RoundCounting
|
98
|
+
|
99
|
+
attr_reader :session, :slots, :index, :this_round
|
100
|
+
|
101
|
+
def initialize(session, slots)
|
102
|
+
@session, @slots, @index = session, slots, 0
|
103
|
+
@this_round = latest_round
|
104
|
+
end
|
105
|
+
|
106
|
+
def current_card
|
107
|
+
# XXX: might be better to cache
|
108
|
+
Card.parse(open(current_path, "r") { |f| f.read })
|
109
|
+
end
|
110
|
+
|
111
|
+
def round_delta
|
112
|
+
latest_round - this_round
|
113
|
+
end
|
114
|
+
|
115
|
+
def runtime; @session.runtime; end
|
116
|
+
def last_slot; @slots[@index-1]; end
|
117
|
+
def current_slot; @slots[@index]; end
|
118
|
+
def current_path; current_slot.path; end
|
119
|
+
def size; @slots.size; end
|
120
|
+
def over?; @slots.size <= @index; end
|
121
|
+
def npassed; @slots.count { |c| c.rating == :passed }; end
|
122
|
+
def nfailed; @slots.count { |c| c.rating == :failed }; end
|
123
|
+
|
124
|
+
def already_failed?; current_slot.rating == :failed; end
|
125
|
+
|
126
|
+
def attack
|
127
|
+
current_slot.rating = :attacking unless current_slot.rating
|
128
|
+
self
|
129
|
+
end
|
130
|
+
|
131
|
+
def fail
|
132
|
+
unless already_failed?
|
133
|
+
last_slot = current_slot
|
134
|
+
last_slot.rating = :failed
|
135
|
+
last_slot.event = make_happen(FailCommand::EVENT_HAPPENING, to_card_name(current_path), this_round)
|
136
|
+
end
|
137
|
+
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
def pass
|
142
|
+
unless already_failed?
|
143
|
+
last_slot = current_slot
|
144
|
+
last_slot.rating = :passed
|
145
|
+
last_slot.event = make_happen(PassCommand::EVENT_HAPPENING, to_card_name(current_path), this_round)
|
146
|
+
end
|
147
|
+
|
148
|
+
@index += 1
|
149
|
+
self
|
150
|
+
end
|
151
|
+
|
152
|
+
def indicator
|
153
|
+
@slots.inject("") do |a, i|
|
154
|
+
a += case i.rating
|
155
|
+
when :failed; "x"
|
156
|
+
when :passed; "o"
|
157
|
+
when :attacking; "*"
|
158
|
+
else; "-"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def styled_indicator
|
164
|
+
indicator.to_enum(:each_char).map do |i|
|
165
|
+
case i
|
166
|
+
when "x"; StylableText.styled_text(i, :failed)
|
167
|
+
when "o"; StylableText.styled_text(i, :passed)
|
168
|
+
when "-"; StylableText.styled_text(i, :pending)
|
169
|
+
else; i
|
170
|
+
end
|
171
|
+
end.join
|
172
|
+
end
|
173
|
+
|
174
|
+
def update_session
|
175
|
+
session.npassed += npassed
|
176
|
+
session.nfailed += nfailed
|
177
|
+
session.mature_names = (session.mature_names + slots.select{ |s| 1 < s.maturity }.map(&:path)).uniq
|
178
|
+
end
|
179
|
+
|
180
|
+
def maturities; slots.map(&:maturity); end
|
181
|
+
end
|
182
|
+
|
183
|
+
class State
|
184
|
+
attr_reader :progress, :last_answer
|
185
|
+
|
186
|
+
def initialize(progress, last_answer=nil)
|
187
|
+
@progress, @last_answer = progress, last_answer
|
188
|
+
end
|
189
|
+
|
190
|
+
def keep_pumping_until(&block)
|
191
|
+
state = self
|
192
|
+
until block.call(state)
|
193
|
+
state = state.pump
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# http://7ujm.net/etc/esc.html
|
198
|
+
def erase_last
|
199
|
+
runtime.stdout.print("\033[1A")
|
200
|
+
end
|
201
|
+
|
202
|
+
def clear_screen
|
203
|
+
runtime.stdout.print("\033[2J")
|
204
|
+
h = HighLine::SystemExtensions.terminal_size[0]
|
205
|
+
runtime.stdout.print("\033[#{h}A")
|
206
|
+
end
|
207
|
+
|
208
|
+
def say(msg, type=:progress)
|
209
|
+
line.say(message_for(msg, type))
|
210
|
+
end
|
211
|
+
|
212
|
+
def show_summary_status
|
213
|
+
line.say("Round #{progress.this_round}: #{progress.styled_indicator}")
|
214
|
+
end
|
215
|
+
|
216
|
+
def show_breaking_status
|
217
|
+
show_summary_status
|
218
|
+
line.say("Maturity: #{progress.maturities.map(&:to_s).join(',')}")
|
219
|
+
line.say("Session: #{progress.session.summary_text}")
|
220
|
+
line.say("next round will be +#{progress.round_delta}")
|
221
|
+
end
|
222
|
+
|
223
|
+
def show_header
|
224
|
+
show_summary_status
|
225
|
+
line.say("\n")
|
226
|
+
end
|
227
|
+
|
228
|
+
def show_and_ask_enter(msg, type)
|
229
|
+
line.ask(message_for(msg, type) + " ") { |q| q.readline = true }
|
230
|
+
end
|
231
|
+
|
232
|
+
def ask(msg="", type=:ask)
|
233
|
+
line.ask(message_for(msg, type)) { |q| q.readline = true }
|
234
|
+
end
|
235
|
+
|
236
|
+
def over?; false; end
|
237
|
+
def runtime; progress.runtime; end
|
238
|
+
def line; progress.runtime.line; end
|
239
|
+
def session; progress.session; end
|
240
|
+
|
241
|
+
private
|
242
|
+
|
243
|
+
def message_for(body, type)
|
244
|
+
case type
|
245
|
+
when :progress
|
246
|
+
" "
|
247
|
+
when :fail
|
248
|
+
StylableText.styled_text("FAIL: ", :failed)
|
249
|
+
when :typo
|
250
|
+
StylableText.styled_text("TYPO: ", :warn)
|
251
|
+
when :pass
|
252
|
+
StylableText.styled_text("PASS: ", :passed)
|
253
|
+
when :ask
|
254
|
+
" > "
|
255
|
+
when :hit_return
|
256
|
+
" < "
|
257
|
+
when :cont
|
258
|
+
" "
|
259
|
+
else
|
260
|
+
raise "Unknown header type:#{type}"
|
261
|
+
end + body
|
262
|
+
end
|
263
|
+
|
264
|
+
def ask_header; " > "; end
|
265
|
+
end
|
266
|
+
|
267
|
+
# XXX: test
|
268
|
+
class EditState < State
|
269
|
+
def pump
|
270
|
+
# XXX: makes configurable
|
271
|
+
system("vi " + progress.current_path)
|
272
|
+
QuestionState.new(progress)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
module CommandRecognizing
|
277
|
+
def may_pump_command(answered)
|
278
|
+
/^\/(\w+)/.match(answered) ? pump_command($1) : nil
|
279
|
+
end
|
280
|
+
|
281
|
+
def pump_command(command)
|
282
|
+
case command
|
283
|
+
when "e", "edit"
|
284
|
+
EditState.new(progress)
|
285
|
+
when "z", "zero"
|
286
|
+
QuestionState.new(progress)
|
287
|
+
else
|
288
|
+
raise
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
class QuestionState < State
|
294
|
+
include CommandRecognizing
|
295
|
+
|
296
|
+
def pump
|
297
|
+
progress.attack
|
298
|
+
clear_screen
|
299
|
+
show_header
|
300
|
+
card = progress.current_card
|
301
|
+
say("#{card.translation}")
|
302
|
+
say("#{card.hidden_original}", :cont)
|
303
|
+
answered = ask().strip
|
304
|
+
c = may_pump_command(answered)
|
305
|
+
return c if c
|
306
|
+
case card.match?(answered.strip)
|
307
|
+
when :match
|
308
|
+
PassedState.new(progress, answered)
|
309
|
+
when :wrong
|
310
|
+
FailedState.new(progress, answered)
|
311
|
+
when :typo
|
312
|
+
TypoState.new(progress, answered)
|
313
|
+
else
|
314
|
+
raise
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
class FailedStateBase < State
|
320
|
+
include CommandRecognizing
|
321
|
+
|
322
|
+
def pump
|
323
|
+
original = progress.current_card.corrected_original_over(last_answer)
|
324
|
+
typed = progress.current_card.hilight_against_original(last_answer)
|
325
|
+
erase_last
|
326
|
+
say("#{typed}", :ask)
|
327
|
+
say("#{original}", decoration_type)
|
328
|
+
answered = ask("", :hit_return)
|
329
|
+
c = may_pump_command(answered)
|
330
|
+
return c if c
|
331
|
+
mark_progress
|
332
|
+
QuestionState.new(progress)
|
333
|
+
end
|
334
|
+
|
335
|
+
end
|
336
|
+
|
337
|
+
class FailedState < FailedStateBase
|
338
|
+
def mark_progress
|
339
|
+
progress.fail
|
340
|
+
end
|
341
|
+
|
342
|
+
def decoration_type
|
343
|
+
:fail
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
class TypoState < FailedStateBase
|
348
|
+
def mark_progress
|
349
|
+
|
350
|
+
end
|
351
|
+
|
352
|
+
def decoration_type
|
353
|
+
:typo
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
class PassedState < State
|
358
|
+
include CommandRecognizing
|
359
|
+
|
360
|
+
def pump
|
361
|
+
progress.pass
|
362
|
+
last_maturity = progress.last_slot.event.maturity
|
363
|
+
progress.over? ? BreakingState.new(progress) : QuestionState.new(progress)
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
class BreakingState < State
|
368
|
+
def pump
|
369
|
+
progress.update_session
|
370
|
+
clear_screen
|
371
|
+
show_breaking_status
|
372
|
+
|
373
|
+
case ask_more
|
374
|
+
when :yes
|
375
|
+
initial_state
|
376
|
+
when :no
|
377
|
+
OverState.new(progress)
|
378
|
+
else
|
379
|
+
# TODO: handle help
|
380
|
+
self
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def ask_more
|
385
|
+
case line.ask("More(y/n/?) ").strip
|
386
|
+
when /^y/, ""
|
387
|
+
:yes
|
388
|
+
when /^n/
|
389
|
+
:no
|
390
|
+
else
|
391
|
+
:help
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
def coming_limit
|
396
|
+
progress.size
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
class OverState < State
|
401
|
+
def over?; true; end
|
402
|
+
end
|
403
|
+
|
404
|
+
module Approaching
|
405
|
+
def initial_state
|
406
|
+
slots = Coming.coming_paths(self.runtime).take(self.coming_limit).map { |path| Slot.new(path, nil) }
|
407
|
+
QuestionState.new(Progress.new(self.session, slots))
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
class BreakingState; include Approaching; end
|
412
|
+
end
|
413
|
+
end
|
@@ -1,419 +1,9 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
|
3
3
|
require 'ankit/command'
|
4
|
-
require 'ankit/
|
5
|
-
require 'ankit/fail_command'
|
6
|
-
require 'ankit/find_command'
|
7
|
-
require 'ankit/pass_command'
|
8
|
-
require 'ankit/round_command'
|
9
|
-
require 'highline'
|
10
|
-
require 'diff/lcs'
|
4
|
+
require 'ankit/challenge'
|
11
5
|
|
12
6
|
module Ankit
|
13
|
-
|
14
|
-
class StylableText
|
15
|
-
def self.styled_text(text, type)
|
16
|
-
case type
|
17
|
-
when :hidden
|
18
|
-
text.gsub(/\w/, "*")
|
19
|
-
when :failed
|
20
|
-
HighLine.color(text, HighLine::RED_STYLE)
|
21
|
-
when :warn
|
22
|
-
HighLine.color(text, HighLine::YELLOW_STYLE)
|
23
|
-
when :passed
|
24
|
-
HighLine.color(text, HighLine::GREEN_STYLE)
|
25
|
-
when :pending
|
26
|
-
HighLine.color(text, HighLine::DARK)
|
27
|
-
when :plus
|
28
|
-
HighLine.color(text, HighLine::RED_STYLE)
|
29
|
-
when :minus
|
30
|
-
HighLine.color(text, HighLine::REVERSE_STYLE)
|
31
|
-
else
|
32
|
-
raise
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def initialize(text); @text = text; end
|
37
|
-
|
38
|
-
def decorated(type)
|
39
|
-
raise
|
40
|
-
decorated = @text.gsub(/\[(.*?)\]/) { |t|
|
41
|
-
p Regexp::last_match.offset(0)
|
42
|
-
self.class.styled_text($1, type)
|
43
|
-
}
|
44
|
-
decorated != @text ? decorated : self.class.styled_text(@text, type)
|
45
|
-
end
|
46
|
-
|
47
|
-
def diff(orig)
|
48
|
-
return @text if @text.empty?
|
49
|
-
|
50
|
-
changes = Diff::LCS.sdiff(orig, @text)
|
51
|
-
changes.map do |ch|
|
52
|
-
case ch.action
|
53
|
-
when "="
|
54
|
-
ch.new_element
|
55
|
-
when "!"
|
56
|
-
self.class.styled_text(ch.new_element, :plus)
|
57
|
-
when "-"
|
58
|
-
self.class.styled_text(ch.old_element, :minus)
|
59
|
-
when "+"
|
60
|
-
self.class.styled_text(ch.new_element, :plus)
|
61
|
-
else
|
62
|
-
raise
|
63
|
-
end
|
64
|
-
end.join("")
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
class Card
|
69
|
-
def hidden_original; decorated_original{ |m| StylableText.styled_text(m[1], :hidden) }; end
|
70
|
-
def hilighted_diff_from_original(text)
|
71
|
-
diff_from_original(text) do |ch|
|
72
|
-
case ch.action
|
73
|
-
when "="
|
74
|
-
ch.new_element
|
75
|
-
when "!"
|
76
|
-
StylableText.styled_text(ch.new_element, :plus)
|
77
|
-
when "-"
|
78
|
-
StylableText.styled_text(ch.old_element, :minus)
|
79
|
-
when "+"
|
80
|
-
StylableText.styled_text(ch.new_element, :plus)
|
81
|
-
else
|
82
|
-
raise
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
module Challenge
|
89
|
-
class Slot < Struct.new(:path, :rating, :event)
|
90
|
-
def maturity; self.event ? self.event.maturity : 0; end
|
91
|
-
end
|
92
|
-
|
93
|
-
class Session < Struct.new(:runtime, :npassed, :nfailed, :mature_names)
|
94
|
-
def self.make(runtime)
|
95
|
-
self.new(runtime, 0, 0, [])
|
96
|
-
end
|
97
|
-
|
98
|
-
def summary_text
|
99
|
-
total = self.npassed + self.nfailed
|
100
|
-
return "" if 0 == total
|
101
|
-
"#{self.npassed}/#{total} = #{self.npassed.to_f/total.to_f}, #mature: #{mature_names.size}"
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
class Progress
|
106
|
-
include CardHappening, CardNaming, RoundCounting
|
107
|
-
|
108
|
-
attr_reader :session, :slots, :index, :this_round
|
109
|
-
|
110
|
-
def initialize(session, slots)
|
111
|
-
@session, @slots, @index = session, slots, 0
|
112
|
-
@this_round = latest_round
|
113
|
-
end
|
114
|
-
|
115
|
-
def current_card
|
116
|
-
# XXX: might be better to cache
|
117
|
-
Card.parse(open(current_path, "r") { |f| f.read })
|
118
|
-
end
|
119
|
-
|
120
|
-
def round_delta
|
121
|
-
latest_round - this_round
|
122
|
-
end
|
123
|
-
|
124
|
-
def runtime; @session.runtime; end
|
125
|
-
def last_slot; @slots[@index-1]; end
|
126
|
-
def current_slot; @slots[@index]; end
|
127
|
-
def current_path; current_slot.path; end
|
128
|
-
def size; @slots.size; end
|
129
|
-
def over?; @slots.size <= @index; end
|
130
|
-
def npassed; @slots.count { |c| c.rating == :passed }; end
|
131
|
-
def nfailed; @slots.count { |c| c.rating == :failed }; end
|
132
|
-
|
133
|
-
def already_failed?; current_slot.rating == :failed; end
|
134
|
-
|
135
|
-
def attack
|
136
|
-
current_slot.rating = :attacking unless current_slot.rating
|
137
|
-
self
|
138
|
-
end
|
139
|
-
|
140
|
-
def fail
|
141
|
-
unless already_failed?
|
142
|
-
last_slot = current_slot
|
143
|
-
last_slot.rating = :failed
|
144
|
-
last_slot.event = make_happen(FailCommand::EVENT_HAPPENING, to_card_name(current_path), this_round)
|
145
|
-
end
|
146
|
-
|
147
|
-
self
|
148
|
-
end
|
149
|
-
|
150
|
-
def pass
|
151
|
-
unless already_failed?
|
152
|
-
last_slot = current_slot
|
153
|
-
last_slot.rating = :passed
|
154
|
-
last_slot.event = make_happen(PassCommand::EVENT_HAPPENING, to_card_name(current_path), this_round)
|
155
|
-
end
|
156
|
-
|
157
|
-
@index += 1
|
158
|
-
self
|
159
|
-
end
|
160
|
-
|
161
|
-
def indicator
|
162
|
-
@slots.inject("") do |a, i|
|
163
|
-
a += case i.rating
|
164
|
-
when :failed; "x"
|
165
|
-
when :passed; "o"
|
166
|
-
when :attacking; "*"
|
167
|
-
else; "-"
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
def styled_indicator
|
173
|
-
indicator.to_enum(:each_char).map do |i|
|
174
|
-
case i
|
175
|
-
when "x"; StylableText.styled_text(i, :failed)
|
176
|
-
when "o"; StylableText.styled_text(i, :passed)
|
177
|
-
when "-"; StylableText.styled_text(i, :pending)
|
178
|
-
else; i
|
179
|
-
end
|
180
|
-
end.join
|
181
|
-
end
|
182
|
-
|
183
|
-
def update_session
|
184
|
-
session.npassed += npassed
|
185
|
-
session.nfailed += nfailed
|
186
|
-
session.mature_names = (session.mature_names + slots.select{ |s| 1 < s.maturity }.map(&:path)).uniq
|
187
|
-
end
|
188
|
-
|
189
|
-
def maturities; slots.map(&:maturity); end
|
190
|
-
end
|
191
|
-
|
192
|
-
class State
|
193
|
-
attr_reader :progress, :last_answer
|
194
|
-
|
195
|
-
def initialize(progress, last_answer=nil)
|
196
|
-
@progress, @last_answer = progress, last_answer
|
197
|
-
end
|
198
|
-
|
199
|
-
def keep_pumping_until(&block)
|
200
|
-
state = self
|
201
|
-
until block.call(state)
|
202
|
-
state = state.pump
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
def clear_screen
|
207
|
-
runtime.stdout.print("\033[2J")
|
208
|
-
h = HighLine::SystemExtensions.terminal_size[0]
|
209
|
-
runtime.stdout.print("\033[#{h}0A")
|
210
|
-
end
|
211
|
-
|
212
|
-
def say(msg, type=:progress)
|
213
|
-
line.say(message_for(msg, type))
|
214
|
-
end
|
215
|
-
|
216
|
-
def show_summary_status
|
217
|
-
line.say("Round #{progress.this_round}: #{progress.styled_indicator}")
|
218
|
-
end
|
219
|
-
|
220
|
-
def show_breaking_status
|
221
|
-
show_summary_status
|
222
|
-
line.say("Maturity: #{progress.maturities.map(&:to_s).join(',')}")
|
223
|
-
line.say("Session: #{progress.session.summary_text}")
|
224
|
-
line.say("next round will be +#{progress.round_delta}")
|
225
|
-
end
|
226
|
-
|
227
|
-
def show_header
|
228
|
-
show_summary_status
|
229
|
-
line.say("\n")
|
230
|
-
end
|
231
|
-
|
232
|
-
def show_and_ask_enter(msg, type)
|
233
|
-
line.ask(message_for(msg, type) + " ") { |q| q.readline = true }
|
234
|
-
end
|
235
|
-
|
236
|
-
def ask(msg="", type=:ask)
|
237
|
-
line.ask(message_for(msg, type)) { |q| q.readline = true }
|
238
|
-
end
|
239
|
-
|
240
|
-
def over?; false; end
|
241
|
-
def runtime; progress.runtime; end
|
242
|
-
def line; progress.runtime.line; end
|
243
|
-
def session; progress.session; end
|
244
|
-
|
245
|
-
private
|
246
|
-
|
247
|
-
def message_for(body, type)
|
248
|
-
case type
|
249
|
-
when :progress
|
250
|
-
" "
|
251
|
-
when :fail
|
252
|
-
StylableText.styled_text("FAIL: ", :failed)
|
253
|
-
when :typo
|
254
|
-
StylableText.styled_text("TYPO: ", :warn)
|
255
|
-
when :pass
|
256
|
-
StylableText.styled_text("PASS: ", :passed)
|
257
|
-
when :ask
|
258
|
-
" > "
|
259
|
-
when :hit_return
|
260
|
-
" < "
|
261
|
-
when :cont
|
262
|
-
" "
|
263
|
-
else
|
264
|
-
raise "Unknown header type:#{type}"
|
265
|
-
end + body
|
266
|
-
end
|
267
|
-
|
268
|
-
def ask_header; " > "; end
|
269
|
-
end
|
270
|
-
|
271
|
-
# XXX: test
|
272
|
-
class EditState < State
|
273
|
-
def pump
|
274
|
-
# XXX: makes configurable
|
275
|
-
system("vi " + progress.current_path)
|
276
|
-
QuestionState.new(progress)
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
|
-
module CommandRecognizing
|
281
|
-
def may_pump_command(answered)
|
282
|
-
/^\/(\w+)/.match(answered) ? pump_command($1) : nil
|
283
|
-
end
|
284
|
-
|
285
|
-
def pump_command(command)
|
286
|
-
case command
|
287
|
-
when "edit"
|
288
|
-
EditState.new(progress)
|
289
|
-
else
|
290
|
-
raise
|
291
|
-
end
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
class QuestionState < State
|
296
|
-
include CommandRecognizing
|
297
|
-
|
298
|
-
def pump
|
299
|
-
progress.attack
|
300
|
-
clear_screen
|
301
|
-
show_header
|
302
|
-
card = progress.current_card
|
303
|
-
say("#{card.translation}")
|
304
|
-
say("#{card.hidden_original}", :cont)
|
305
|
-
answered = ask().strip
|
306
|
-
c = may_pump_command(answered)
|
307
|
-
return c if c
|
308
|
-
case card.match?(answered.strip)
|
309
|
-
when :match
|
310
|
-
PassedState.new(progress, answered)
|
311
|
-
when :wrong
|
312
|
-
FailedState.new(progress, answered)
|
313
|
-
when :typo
|
314
|
-
TypoState.new(progress, answered)
|
315
|
-
else
|
316
|
-
raise
|
317
|
-
end
|
318
|
-
end
|
319
|
-
end
|
320
|
-
|
321
|
-
class FailedStateBase < State
|
322
|
-
include CommandRecognizing
|
323
|
-
|
324
|
-
def pump
|
325
|
-
diff_from_original = progress.current_card.hilighted_diff_from_original(last_answer)
|
326
|
-
say("#{diff_from_original}", decoration_type)
|
327
|
-
answered = ask("", :hit_return)
|
328
|
-
c = may_pump_command(answered)
|
329
|
-
return c if c
|
330
|
-
mark_progress
|
331
|
-
QuestionState.new(progress)
|
332
|
-
end
|
333
|
-
|
334
|
-
end
|
335
|
-
|
336
|
-
class FailedState < FailedStateBase
|
337
|
-
def mark_progress
|
338
|
-
progress.fail
|
339
|
-
end
|
340
|
-
|
341
|
-
def decoration_type
|
342
|
-
:fail
|
343
|
-
end
|
344
|
-
end
|
345
|
-
|
346
|
-
class TypoState < FailedStateBase
|
347
|
-
def mark_progress
|
348
|
-
|
349
|
-
end
|
350
|
-
|
351
|
-
def decoration_type
|
352
|
-
:typo
|
353
|
-
end
|
354
|
-
end
|
355
|
-
|
356
|
-
class PassedState < State
|
357
|
-
include CommandRecognizing
|
358
|
-
|
359
|
-
def pump
|
360
|
-
progress.pass
|
361
|
-
last_maturity = progress.last_slot.event.maturity
|
362
|
-
say("Maturity: #{last_maturity}", :pass)
|
363
|
-
answered = ask("", :hit_return)
|
364
|
-
c = may_pump_command(answered)
|
365
|
-
return c if c
|
366
|
-
progress.over? ? BreakingState.new(progress) : QuestionState.new(progress)
|
367
|
-
end
|
368
|
-
end
|
369
|
-
|
370
|
-
class BreakingState < State
|
371
|
-
def pump
|
372
|
-
progress.update_session
|
373
|
-
clear_screen
|
374
|
-
show_breaking_status
|
375
|
-
|
376
|
-
case ask_more
|
377
|
-
when :yes
|
378
|
-
initial_state
|
379
|
-
when :no
|
380
|
-
OverState.new(progress)
|
381
|
-
else
|
382
|
-
# TODO: handle help
|
383
|
-
self
|
384
|
-
end
|
385
|
-
end
|
386
|
-
|
387
|
-
def ask_more
|
388
|
-
case line.ask("More(y/n/?) ").strip
|
389
|
-
when /^y/, ""
|
390
|
-
:yes
|
391
|
-
when /^n/
|
392
|
-
:no
|
393
|
-
else
|
394
|
-
:help
|
395
|
-
end
|
396
|
-
end
|
397
|
-
|
398
|
-
def coming_limit
|
399
|
-
progress.size
|
400
|
-
end
|
401
|
-
end
|
402
|
-
|
403
|
-
class OverState < State
|
404
|
-
def over?; true; end
|
405
|
-
end
|
406
|
-
|
407
|
-
module Approaching
|
408
|
-
def initial_state
|
409
|
-
slots = Coming.coming_paths(self.runtime).take(self.coming_limit).map { |path| Slot.new(path, nil) }
|
410
|
-
QuestionState.new(Progress.new(self.session, slots))
|
411
|
-
end
|
412
|
-
end
|
413
|
-
|
414
|
-
class BreakingState; include Approaching; end
|
415
|
-
end
|
416
|
-
|
417
7
|
class ChallengeCommand < Command
|
418
8
|
include Challenge, RoundCounting, Coming, Finding
|
419
9
|
include Challenge::Approaching
|
data/lib/ankit/command.rb
CHANGED
@@ -11,10 +11,13 @@ module Ankit
|
|
11
11
|
COMMANDS.push(self)
|
12
12
|
end
|
13
13
|
|
14
|
+
def self.command_name
|
15
|
+
/(.*)\:\:(\w+)Command/.match(self.name).to_a[-1].downcase
|
16
|
+
end
|
17
|
+
|
14
18
|
def self.by_name
|
15
19
|
COMMANDS.inject({}) do |a, cls|
|
16
|
-
|
17
|
-
a[name] = cls
|
20
|
+
a[cls.command_name] = cls
|
18
21
|
a
|
19
22
|
end
|
20
23
|
end
|
data/lib/ankit/runtime.rb
CHANGED
@@ -102,7 +102,7 @@ module Ankit
|
|
102
102
|
splitted = self.split_subcommand(args)
|
103
103
|
r = self.setup(splitted[:global])
|
104
104
|
if splitted[:subcommand].empty?
|
105
|
-
|
105
|
+
r.dispatch([ChallengeCommand.command_name])
|
106
106
|
else
|
107
107
|
r.dispatch(splitted[:subcommand])
|
108
108
|
end
|
data/test/card_test.rb
CHANGED
@@ -86,7 +86,17 @@ T: Konichiwa, Genki?
|
|
86
86
|
assert_equal(:typo, target.match?("Hallo, World."))
|
87
87
|
assert_equal(:typo, target.match?("Halo, World."))
|
88
88
|
assert_equal(:typo, target.match?("Hello, World!"))
|
89
|
-
assert_equal(:
|
89
|
+
assert_equal(:wrong, target.match?("World."))
|
90
|
+
assert_equal(:wrong, target.match?("Hello,"))
|
90
91
|
assert_equal(:wrong, target.match?(""))
|
91
92
|
end
|
93
|
+
|
94
|
+
def test_diff
|
95
|
+
Card.new(o:"hello").corrected_original_over("helo") {}
|
96
|
+
Card.new(o:"helo").corrected_original_over("hello") {}
|
97
|
+
Card.new(o:"helxo").corrected_original_over("hello") {}
|
98
|
+
Card.new(o:"hello").hilight_against_original("helo") {}
|
99
|
+
Card.new(o:"helo").hilight_against_original("hello") {}
|
100
|
+
Card.new(o:"helxo").hilight_against_original("hello") {}
|
101
|
+
end
|
92
102
|
end
|
data/test/command_test.rb
CHANGED
@@ -312,6 +312,18 @@ class ChallengeTest < Test::Unit::TestCase
|
|
312
312
|
end
|
313
313
|
end
|
314
314
|
|
315
|
+
def test_question_to_fail_but_zero_it
|
316
|
+
with_runtime_on_temp_repo do |runtime|
|
317
|
+
actual = ChallengeCommand.new(runtime).initial_state
|
318
|
+
actual_next = enter_text_pump(actual, FIRST_WRONG_ANSWER)
|
319
|
+
assert_instance_of(Challenge::FailedState, actual_next)
|
320
|
+
actual_next = enter_text_pump(actual_next, "/zero")
|
321
|
+
assert_instance_of(Challenge::QuestionState, actual_next)
|
322
|
+
assert_equal(actual_next.progress.npassed, 0)
|
323
|
+
assert_equal(actual_next.progress.nfailed, 0)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
315
327
|
def test_question_to_typo
|
316
328
|
with_runtime_on_temp_repo do |runtime|
|
317
329
|
actual = ChallengeCommand.new(runtime).initial_state
|
@@ -323,7 +335,7 @@ class ChallengeTest < Test::Unit::TestCase
|
|
323
335
|
assert_equal(actual_next.progress.nfailed, 0)
|
324
336
|
actual_next = enter_text_pump(actual_next, FIRST_CORRECT_ANSWER)
|
325
337
|
assert_instance_of(Challenge::PassedState, actual_next)
|
326
|
-
|
338
|
+
actual_next.pump
|
327
339
|
assert_equal(actual_next.progress.npassed, 1)
|
328
340
|
assert_equal(actual_next.progress.nfailed, 0)
|
329
341
|
end
|
@@ -334,7 +346,7 @@ class ChallengeTest < Test::Unit::TestCase
|
|
334
346
|
actual = ChallengeCommand.new(runtime).initial_state
|
335
347
|
actual_next = enter_text_pump(actual, FIRST_CORRECT_ANSWER)
|
336
348
|
assert_instance_of(Challenge::PassedState, actual_next)
|
337
|
-
actual_next =
|
349
|
+
actual_next = actual_next.pump
|
338
350
|
assert_instance_of(Challenge::QuestionState, actual_next)
|
339
351
|
assert_equal(actual_next.progress.npassed, 1)
|
340
352
|
assert_equal(actual_next.progress.nfailed, 0)
|
@@ -344,11 +356,13 @@ class ChallengeTest < Test::Unit::TestCase
|
|
344
356
|
def pass_two(state)
|
345
357
|
state = enter_text_pump(state, FIRST_CORRECT_ANSWER)
|
346
358
|
assert_instance_of(Challenge::PassedState, state)
|
347
|
-
state =
|
359
|
+
state = state.pump
|
348
360
|
assert_instance_of(Challenge::QuestionState, state)
|
349
361
|
state = enter_text_pump(state, SECOND_CORRECT_ANSWER)
|
350
362
|
assert_instance_of(Challenge::PassedState, state)
|
351
|
-
|
363
|
+
state = state.pump
|
364
|
+
assert_equal(state.progress.npassed, 2)
|
365
|
+
state
|
352
366
|
end
|
353
367
|
|
354
368
|
def test_to_breaking
|
@@ -397,10 +411,4 @@ class StylableTextTest < Test::Unit::TestCase
|
|
397
411
|
assert_equal("*****, *****!",
|
398
412
|
Card.new(o: "Hello, World!").hidden_original)
|
399
413
|
end
|
400
|
-
|
401
|
-
def test_diff
|
402
|
-
StylableText.new("hello").diff("helo")
|
403
|
-
StylableText.new("helo").diff("hello")
|
404
|
-
StylableText.new("helxo").diff("hello")
|
405
|
-
end
|
406
414
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ankit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-03-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: highline
|
16
|
-
requirement: &
|
16
|
+
requirement: &20716680 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 1.6.11
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *20716680
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: diff-lcs
|
27
|
-
requirement: &
|
27
|
+
requirement: &20716160 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 1.1.3
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *20716160
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: json
|
38
|
-
requirement: &
|
38
|
+
requirement: &20715660 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 1.6.5
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *20715660
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: mocha
|
49
|
-
requirement: &
|
49
|
+
requirement: &20715080 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,7 +54,7 @@ dependencies:
|
|
54
54
|
version: 0.10.4
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *20715080
|
58
58
|
description: ! 'Ankit is a CLI and terminal based flashcard program to learn your
|
59
59
|
new language.
|
60
60
|
|
@@ -82,6 +82,7 @@ files:
|
|
82
82
|
- lib/ankit/name_command.rb
|
83
83
|
- lib/ankit/score_command.rb
|
84
84
|
- lib/ankit/card_happening_command.rb
|
85
|
+
- lib/ankit/challenge.rb
|
85
86
|
- lib/ankit/fail_command.rb
|
86
87
|
- lib/ankit/list_command.rb
|
87
88
|
- lib/ankit.rb
|