ankit 0.0.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.
- data/bin/ankit +6 -0
- data/lib/ankit/add_command.rb +28 -0
- data/lib/ankit/card.rb +66 -0
- data/lib/ankit/card_happening_command.rb +31 -0
- data/lib/ankit/challenge_command.rb +444 -0
- data/lib/ankit/coming_command.rb +90 -0
- data/lib/ankit/command.rb +40 -0
- data/lib/ankit/event.rb +62 -0
- data/lib/ankit/event_traversing_command.rb +37 -0
- data/lib/ankit/fail_command.rb +14 -0
- data/lib/ankit/find_command.rb +38 -0
- data/lib/ankit/hello_command.rb +22 -0
- data/lib/ankit/list_command.rb +26 -0
- data/lib/ankit/name_command.rb +16 -0
- data/lib/ankit/pass_command.rb +9 -0
- data/lib/ankit/round_command.rb +31 -0
- data/lib/ankit/runtime.rb +151 -0
- data/lib/ankit/score_command.rb +24 -0
- data/lib/ankit/text_reading_command.rb +23 -0
- data/lib/ankit.rb +3 -0
- data/test/card_test.rb +92 -0
- data/test/command_test.rb +406 -0
- data/test/data/bye_card.card +2 -0
- data/test/data/hello_card.card +2 -0
- data/test/data/hello_config.rb +4 -0
- data/test/data/hello_repo/anemone.journal +2 -0
- data/test/data/hello_repo/baobab.journal +2 -0
- data/test/data/hello_repo/cards/foo/hello.card +2 -0
- data/test/data/hello_repo/cards/foo/this_is_not_a_card.txt +1 -0
- data/test/data/hello_repo/cards/foo/vanilla-please.card +2 -0
- data/test/data/hope.card +1 -0
- data/test/data/luck.card +1 -0
- data/test/data/number_repo/anemone.journal +0 -0
- data/test/data/number_repo/cards/eight.card +2 -0
- data/test/data/number_repo/cards/five.card +2 -0
- data/test/data/number_repo/cards/four.card +2 -0
- data/test/data/number_repo/cards/one.card +2 -0
- data/test/data/number_repo/cards/seven.card +2 -0
- data/test/data/number_repo/cards/six.card +2 -0
- data/test/data/number_repo/cards/three.card +2 -0
- data/test/data/number_repo/cards/two.card +2 -0
- data/test/data/vanilla_repo/anemone.journal +0 -0
- data/test/event_test.rb +29 -0
- data/test/helpers.rb +54 -0
- data/test/progress_test.rb +99 -0
- data/test/runtime_test.rb +58 -0
- metadata +138 -0
data/bin/ankit
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
require 'ankit/card'
|
3
|
+
require 'ankit/text_reading_command'
|
4
|
+
|
5
|
+
module Ankit
|
6
|
+
class AddCommand < TextReadingCommand
|
7
|
+
include CardNaming
|
8
|
+
available
|
9
|
+
define_options do |spec, options|
|
10
|
+
superclass.option_spec.call(spec, options)
|
11
|
+
spec.on("-d", "--dir DIR") { |d| options[:dir] = d }
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute()
|
15
|
+
validate_options
|
16
|
+
each_text do |text|
|
17
|
+
card = Card.parse(text)
|
18
|
+
# TODO: gaurd ovewrite
|
19
|
+
# TODO: guard out-of-path write
|
20
|
+
filename = to_card_path(dest_dir, card.name)
|
21
|
+
File.open(filename, "w") { |f| f.write(text) }
|
22
|
+
runtime.stdout.write("#{filename}\n")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def dest_dir; options[:dir] || runtime.config.card_paths[0]; end
|
27
|
+
end
|
28
|
+
end
|
data/lib/ankit/card.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Ankit
|
4
|
+
class Card
|
5
|
+
|
6
|
+
attr_reader :deckname, :source
|
7
|
+
|
8
|
+
def self.parse(text)
|
9
|
+
lines = text.split(/\r?\n/).select { |l| !/^\#/.match(l) and !/^\s*$/.match(l) }
|
10
|
+
return nil if lines.empty?
|
11
|
+
|
12
|
+
params = lines.inject({}) do |a, line|
|
13
|
+
case line
|
14
|
+
when /^(\w+)\:(.*)/
|
15
|
+
a[$1.downcase.to_sym] = $2.strip
|
16
|
+
end
|
17
|
+
a
|
18
|
+
end
|
19
|
+
|
20
|
+
self.new(params)
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(params)
|
24
|
+
@params = params
|
25
|
+
end
|
26
|
+
|
27
|
+
def original() @params[:o]; end
|
28
|
+
def translation() @params[:t]; end
|
29
|
+
|
30
|
+
def name
|
31
|
+
original.gsub(/\W+/, "-").gsub(/^\-/, "").gsub(/\-$/, "").downcase
|
32
|
+
end
|
33
|
+
|
34
|
+
def match?(text)
|
35
|
+
return :match if plain_original == text
|
36
|
+
return :wrong if text.empty?
|
37
|
+
|
38
|
+
hiddens = decorated_original { |m| "*"*m[1].size }.chars.to_a
|
39
|
+
inside_essentials = to_enum(:diff_from_original, text).find do |ch|
|
40
|
+
ch.action != "=" && hiddens[ch.old_position] == "*"
|
41
|
+
end
|
42
|
+
|
43
|
+
inside_essentials ? :wrong : :typo
|
44
|
+
end
|
45
|
+
|
46
|
+
def diff_from_original(text, &block)
|
47
|
+
changes = Diff::LCS.sdiff(text, plain_original)
|
48
|
+
changes.map do |ch|
|
49
|
+
block.call(ch)
|
50
|
+
end.join("")
|
51
|
+
end
|
52
|
+
|
53
|
+
def decorated_original(&block)
|
54
|
+
decoed = original.gsub(/\[(.*?)\]/) { |t| block.call(Regexp.last_match) }
|
55
|
+
decoed != original ? decoed : decoed.gsub(/^(.*)$/) { |t| block.call(Regexp.last_match) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def plain_original; decorated_original { |m| m[1] }; end
|
59
|
+
end
|
60
|
+
|
61
|
+
module CardNaming
|
62
|
+
def to_card_name(path) File.basename(path, ".card"); end
|
63
|
+
def to_card_path(dir, name) File.join(dir, "#{name}.card"); end
|
64
|
+
def card_wildcard_for(dir) File.join(dir, "*.card"); end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
require 'ankit/card'
|
3
|
+
require 'ankit/command'
|
4
|
+
require 'ankit/event'
|
5
|
+
require 'ankit/event_traversing_command'
|
6
|
+
require 'ankit/round_command'
|
7
|
+
require 'fileutils'
|
8
|
+
|
9
|
+
module Ankit
|
10
|
+
module CardHappening
|
11
|
+
include CardNaming, EventTraversing, EventFormatting, RoundCounting
|
12
|
+
|
13
|
+
def make_happen(method_name, card_name, round_proceeding=latest_round)
|
14
|
+
last = EventTraversing.find_latest_event_for(runtime, card_name) || Event.for_card(card_name, "vanilla", last_round)
|
15
|
+
head = last.send(method_name, Envelope.fresh(round_proceeding))
|
16
|
+
FileUtils.touch(runtime.config.primary_journal)
|
17
|
+
open(runtime.config.primary_journal, "a") { |f| f.write("#{head.to_json}\n") }
|
18
|
+
self.round_proceeded
|
19
|
+
head
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class CardHappeningCommand < Command
|
24
|
+
include EventFormatting, CardHappening
|
25
|
+
|
26
|
+
def execute()
|
27
|
+
head = make_happen(self.class::EVENT_HAPPENING, to_card_name(args[0]))
|
28
|
+
runtime.stdout.print("#{format_as_score(head)}\n")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,444 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'ankit/command'
|
4
|
+
require 'ankit/coming_command'
|
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'
|
11
|
+
|
12
|
+
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
|
+
class ChallengeCommand < Command
|
418
|
+
include Challenge, RoundCounting, Coming, Finding
|
419
|
+
include Challenge::Approaching
|
420
|
+
available
|
421
|
+
|
422
|
+
define_options do |spec, options|
|
423
|
+
spec.on("-l", "--limit N") { |n| options[:limit] = n.to_i }
|
424
|
+
end
|
425
|
+
|
426
|
+
DEFAULT_COUNT = 5
|
427
|
+
|
428
|
+
def session; @session ||= Challenge::Session.make(runtime); end
|
429
|
+
|
430
|
+
def execute()
|
431
|
+
Signal.trap("INT") do
|
432
|
+
STDERR.print("Quit.\n")
|
433
|
+
exit(0)
|
434
|
+
end
|
435
|
+
|
436
|
+
initial_state.keep_pumping_until { |state| state.over? }
|
437
|
+
Signal.trap("INT", "DEFAULT")
|
438
|
+
end
|
439
|
+
|
440
|
+
def coming_limit
|
441
|
+
options[:limit] or DEFAULT_COUNT
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
|
2
|
+
require 'ankit/command'
|
3
|
+
require 'ankit/event_traversing_command'
|
4
|
+
require 'ankit/list_command'
|
5
|
+
require 'ankit/round_command'
|
6
|
+
require 'ankit/event'
|
7
|
+
|
8
|
+
module Ankit
|
9
|
+
class ComingCommand < Command
|
10
|
+
include RoundCounting
|
11
|
+
|
12
|
+
available
|
13
|
+
define_options do |spec, options|
|
14
|
+
spec.on("-n", "--name") { options[:name] = true }
|
15
|
+
end
|
16
|
+
|
17
|
+
DEFAULT_COUNT = 6
|
18
|
+
|
19
|
+
def find_command; @find_command ||= FindCommand.new(runtime); end
|
20
|
+
|
21
|
+
def execute()
|
22
|
+
toprint = to_enum(options[:name] ? :each_coming_names : :each_coming_paths)
|
23
|
+
toprint.take(0 <= count ? count : name_to_events.size).each { |i| runtime.stdout.print("#{i}\n") }
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_coming_names(&block); each_coming_events { |e| block.call(e.name) }; end
|
27
|
+
|
28
|
+
def each_coming_paths(&block)
|
29
|
+
each_coming_events do |event|
|
30
|
+
found = self.find_command.path_for(event.name)
|
31
|
+
block.call(found) if found
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def each_coming_events(&block)
|
36
|
+
name_to_events.values.sort_by(&:next_round).each(&block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def each_existing_events(&block)
|
40
|
+
name_to_existing_events.values.sort_by(&:next_round).each(&block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def name_to_events
|
44
|
+
@name_to_events ||= compute_name_to_events
|
45
|
+
end
|
46
|
+
|
47
|
+
def name_to_existing_events
|
48
|
+
EventTraversingCommand.new(runtime).to_enum(:each_event).reduce({}) do |a, e|
|
49
|
+
existing = a[e.name]
|
50
|
+
a[e.name] = e if existing.nil? or existing.round < e.round
|
51
|
+
a
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def name_to_vanilla_events
|
56
|
+
vanilla_round = last_round
|
57
|
+
ListCommand.new(runtime).to_enum(:each_card_name).reduce({}) do |a, name|
|
58
|
+
a[name] = Event.for_card(name, "vanilla", Envelope.fresh(vanilla_round))
|
59
|
+
a
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def compute_name_to_events
|
66
|
+
existing = name_to_existing_events
|
67
|
+
vanilla = name_to_vanilla_events
|
68
|
+
vanilla.merge(existing)
|
69
|
+
end
|
70
|
+
|
71
|
+
def count
|
72
|
+
args.empty? ? DEFAULT_COUNT : args[0].to_i
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
module Coming
|
77
|
+
def self.coming_events(runtime)
|
78
|
+
ComingCommand.new(runtime).to_enum(:each_coming_events).to_a
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.coming_paths(runtime)
|
82
|
+
ComingCommand.new(runtime).to_enum(:each_coming_paths).to_a
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.existing_events(runtime)
|
86
|
+
ComingCommand.new(runtime).to_enum(:each_existing_events).to_a
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module Ankit
|
5
|
+
class Command
|
6
|
+
attr_reader :runtime, :options, :args
|
7
|
+
|
8
|
+
COMMANDS = []
|
9
|
+
|
10
|
+
def self.available
|
11
|
+
COMMANDS.push(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.by_name
|
15
|
+
COMMANDS.inject({}) do |a, cls|
|
16
|
+
name = /(.*)\:\:(\w+)Command/.match(cls.name).to_a[-1].downcase
|
17
|
+
a[name] = cls
|
18
|
+
a
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.define_options(&block) @option_spec = block end
|
23
|
+
def self.option_spec; @option_spec; end
|
24
|
+
|
25
|
+
def initialize(runtime, args=[])
|
26
|
+
@runtime = runtime
|
27
|
+
@options = {}
|
28
|
+
@args = OptionParser.new do |spec|
|
29
|
+
self.class.option_spec.call(spec, @options) if self.class.option_spec
|
30
|
+
end.parse(args)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class BadOptions < StandardError; end
|
35
|
+
|
36
|
+
class HelloCommand < Command
|
37
|
+
available
|
38
|
+
def execute; end
|
39
|
+
end
|
40
|
+
end
|