aoc_cli 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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +282 -0
- data/Rakefile +3 -0
- data/aoc_cli.gemspec +30 -0
- data/bin/aoc +4 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/aoc_cli.rb +22 -0
- data/lib/aoc_cli/commands.rb +215 -0
- data/lib/aoc_cli/database.rb +159 -0
- data/lib/aoc_cli/day.rb +153 -0
- data/lib/aoc_cli/db/reddit.db +0 -0
- data/lib/aoc_cli/errors.rb +171 -0
- data/lib/aoc_cli/files.rb +93 -0
- data/lib/aoc_cli/help.rb +53 -0
- data/lib/aoc_cli/interface.rb +137 -0
- data/lib/aoc_cli/paths.rb +93 -0
- data/lib/aoc_cli/solve.rb +109 -0
- data/lib/aoc_cli/tables.rb +138 -0
- data/lib/aoc_cli/tools.rb +91 -0
- data/lib/aoc_cli/version.rb +5 -0
- data/lib/aoc_cli/year.rb +98 -0
- metadata +157 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
module AocCli
|
2
|
+
module Files
|
3
|
+
class Config
|
4
|
+
require 'fileutils'
|
5
|
+
def initialize
|
6
|
+
Paths::Config.create
|
7
|
+
end
|
8
|
+
def def_acc
|
9
|
+
get_line(key:"default") || "main"
|
10
|
+
end
|
11
|
+
def is_set?(key:nil, val:nil)
|
12
|
+
read.split("\n").grep(/#{key}=>#{val}/).any?
|
13
|
+
end
|
14
|
+
def mod_line(key:, val:)
|
15
|
+
is_set?(key:key) ?
|
16
|
+
write(f:read.gsub(/(?<=^#{key}=>).*$/,
|
17
|
+
val.to_s)) :
|
18
|
+
write(f:"#{key}=>#{val}\n", m:"a")
|
19
|
+
end
|
20
|
+
def get_line(key:)
|
21
|
+
read.scan(/(?<=#{key}=>).*$/)&.first
|
22
|
+
end
|
23
|
+
def get_bool(key:)
|
24
|
+
get_line(key:key) == "true" ? true : false
|
25
|
+
end
|
26
|
+
protected
|
27
|
+
def set_line(key:, val:)
|
28
|
+
write(f:"#{key}=>#{val}\n", m:"a")
|
29
|
+
end
|
30
|
+
private
|
31
|
+
def read
|
32
|
+
File.read(Paths::Config.path)
|
33
|
+
end
|
34
|
+
def write(f:, m:"w")
|
35
|
+
File.write(Paths::Config.path, f, mode:m)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
class Cookie < Config
|
39
|
+
attr_reader :user
|
40
|
+
def initialize(u:)
|
41
|
+
@user = u
|
42
|
+
super()
|
43
|
+
end
|
44
|
+
def store(key:)
|
45
|
+
set_line(key:"cookie=>#{Validate.set_user(user)}",
|
46
|
+
val:Validate.set_key(key))
|
47
|
+
end
|
48
|
+
def key
|
49
|
+
get_line(key:"cookie=>#{user}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
class Metafile
|
53
|
+
def self.get(field)
|
54
|
+
read.scan(/(?<=#{field}=>).*$/)&.first&.chomp
|
55
|
+
end
|
56
|
+
def self.type
|
57
|
+
get("dir").to_sym
|
58
|
+
end
|
59
|
+
def self.add(hash:, path:".meta")
|
60
|
+
hash.map {|k, v| "#{k}=>#{v}\n"}
|
61
|
+
.each{|l| File.write(path, l, mode:"a")}
|
62
|
+
end
|
63
|
+
def self.part(d:)
|
64
|
+
JSON.parse(read(dir:root_dir)
|
65
|
+
.scan(/(?<=stars=>).*$/)&.first)[d.to_s]
|
66
|
+
.to_i + 1
|
67
|
+
end
|
68
|
+
private
|
69
|
+
def self.read(dir:".")
|
70
|
+
File.read("#{Validate.init(dir)}/.meta")
|
71
|
+
end
|
72
|
+
def self.root_dir
|
73
|
+
type == :ROOT ? "." : ".."
|
74
|
+
end
|
75
|
+
def self.year(u:, y:)
|
76
|
+
<<~meta
|
77
|
+
dir=>ROOT
|
78
|
+
user=>#{u}
|
79
|
+
year=>#{y}
|
80
|
+
meta
|
81
|
+
end
|
82
|
+
def self.day(u:, y:, d:)
|
83
|
+
<<~meta
|
84
|
+
dir=>DAY
|
85
|
+
user=>#{u}
|
86
|
+
year=>#{y}
|
87
|
+
day=>#{d}
|
88
|
+
part=>#{part(d:d)}
|
89
|
+
meta
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/aoc_cli/help.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
def title(title)
|
2
|
+
"#{title.bold}"
|
3
|
+
end
|
4
|
+
def flag(short, full)
|
5
|
+
str = " #{short.yellow.bold} [#{full.blue.italic}]"
|
6
|
+
full.length > 6 ?
|
7
|
+
str += "\t" :
|
8
|
+
str += "\t\t"
|
9
|
+
str
|
10
|
+
end
|
11
|
+
puts <<~help
|
12
|
+
Advent of Code - cli, version 0.1.0
|
13
|
+
#{"C. Welham Feb 2021".italic}
|
14
|
+
|
15
|
+
#{title("Usage")}
|
16
|
+
#{" aoc".bold + " -flag".italic.yellow + " value".bold.blue}
|
17
|
+
|
18
|
+
#{title("Setup")}
|
19
|
+
- Store session cookie keys to access AoC
|
20
|
+
#{flag("-k", "--key")}Store a session cookie to use the cli.
|
21
|
+
#{flag("-u", "--user")}Set alias for key (default: "main")
|
22
|
+
|
23
|
+
#{title("Year Directory")}
|
24
|
+
- Initialise a year directory to use aoc-cli.
|
25
|
+
- If no alias is passed, the default key is used
|
26
|
+
#{flag("-y","--init-year")}Initialise directory and fetch calendar
|
27
|
+
#{flag("-u","--user")}Specify alias for initialisation
|
28
|
+
#{flag("-d","--init-day")}Create day subdirectory, fetch puzzle and input
|
29
|
+
#{flag("-r", "--refresh")}Refresh calendar
|
30
|
+
|
31
|
+
#{title("Day Subdirectory")}
|
32
|
+
- These commands can be run from the day subdirectory
|
33
|
+
#{flag("-s","--solve")}Attempt puzzle
|
34
|
+
#{flag("-r","--refresh")}Refresh puzzle
|
35
|
+
#{flag("-a","--attempts")}Prints previous attempt table
|
36
|
+
#{flag("-p","--part")}Specify part (attempts)
|
37
|
+
#{flag("-R","--reddit")}Open Reddit solution megathread
|
38
|
+
#{flag("-b","--browser")}Open Reddit thread in browser
|
39
|
+
|
40
|
+
#{title("Manual Usage")}
|
41
|
+
- AocCli uses metadata so that these flags do not need to be entered.
|
42
|
+
- Command flags can be entered manually, but this is not recommended
|
43
|
+
#{flag("-u","--user")}Specify user
|
44
|
+
#{flag("-Y","--year")}Specify year
|
45
|
+
#{flag("-D","--day")}Specify day
|
46
|
+
#{flag("-p","--part")}Specify part
|
47
|
+
|
48
|
+
#{title("Configuration")}
|
49
|
+
- Pass with a value to update setting
|
50
|
+
- Pass without an argument to display current setting.
|
51
|
+
#{flag("-B","--browser")}Always open Reddit in browser (default: false)
|
52
|
+
#{flag("-U","--default")}Default key alias to use (default: "main")
|
53
|
+
help
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module AocCli
|
2
|
+
module Interface
|
3
|
+
class Query
|
4
|
+
def initialize
|
5
|
+
ARGV.size > 0 ?
|
6
|
+
run(opts:Opts.new.parse_args) :
|
7
|
+
puts(Help.print)
|
8
|
+
rescue StandardError => e
|
9
|
+
abort e.message
|
10
|
+
end
|
11
|
+
def run(opts:)
|
12
|
+
cmd = Object
|
13
|
+
.const_get("AocCli::Commands::#{opts.cmd}")
|
14
|
+
.new(opts.args)
|
15
|
+
.exec
|
16
|
+
cmd.respond if cmd.class.instance_methods.include?(:respond)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
class Help
|
20
|
+
def self.print
|
21
|
+
require_relative "help.rb"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
class Opts
|
25
|
+
attr_reader :cmd, :args
|
26
|
+
def initialize
|
27
|
+
@args = {}
|
28
|
+
end
|
29
|
+
def parse_args
|
30
|
+
while ARGV.size > 0
|
31
|
+
case ARGV.shift
|
32
|
+
when "-a", "--attempts"
|
33
|
+
@cmd = :AttemptsTable
|
34
|
+
when "-b", "--browser"
|
35
|
+
args[:browser] = true
|
36
|
+
when "-d", "--init-day"
|
37
|
+
@cmd = :DayInit
|
38
|
+
args[:day] = Validate.day(ARGV.shift.to_i)
|
39
|
+
when "-h", "--help"
|
40
|
+
exit Help.print
|
41
|
+
when "-k", "--key"
|
42
|
+
@cmd = :KeyStore
|
43
|
+
args[:key] = Validate.set_key(ARGV.shift)
|
44
|
+
when "-p", "--part"
|
45
|
+
args[:part] = Validate.part(ARGV.shift.to_i)
|
46
|
+
when "-r", "--refresh"
|
47
|
+
@cmd = :Refresh
|
48
|
+
when "-s", "--solve"
|
49
|
+
@cmd = :DaySolve
|
50
|
+
args[:ans] = Validate.ans(ARGV.shift)
|
51
|
+
when "-u", "--user"
|
52
|
+
args[:user] = ARGV.shift
|
53
|
+
when "-y", "--init-year"
|
54
|
+
@cmd = :YearInit
|
55
|
+
args[:year] = Validate.year(ARGV.shift.to_i)
|
56
|
+
when "-B", "--browser"
|
57
|
+
@cmd = :DefaultReddit
|
58
|
+
args[:value] = ARGV.shift
|
59
|
+
when "-D", "--day"
|
60
|
+
args[:day] = Validate.day(ARGV.shift.to_i)
|
61
|
+
when "-R", "--reddit"
|
62
|
+
@cmd = :OpenReddit
|
63
|
+
when "-S", "--stats"
|
64
|
+
@cmd = :StatsTable
|
65
|
+
when "-U", "--default-user"
|
66
|
+
@cmd = :DefaultUser
|
67
|
+
args[:user] = ARGV.shift
|
68
|
+
when "-Y", "--year"
|
69
|
+
args[:year] = Validate.year(ARGV.shift.to_i)
|
70
|
+
else raise E::FlagInv
|
71
|
+
end
|
72
|
+
end
|
73
|
+
raise E::NoCmd if cmd.nil?
|
74
|
+
self
|
75
|
+
end
|
76
|
+
end
|
77
|
+
class Validate
|
78
|
+
def self.user(user)
|
79
|
+
raise E::UserNil if user.nil?
|
80
|
+
raise E::UserInv.new(user) unless Files::Config
|
81
|
+
.new.is_set?(key:"cookie=>#{user}")
|
82
|
+
user
|
83
|
+
end
|
84
|
+
def self.set_user(user)
|
85
|
+
raise E::UserNil if user.nil?
|
86
|
+
raise E::UserDup if Files::Config.new
|
87
|
+
.is_set?(key:"cookie=>#{user}")
|
88
|
+
user
|
89
|
+
end
|
90
|
+
def self.year(year)
|
91
|
+
raise E::YearNil if year.nil?
|
92
|
+
raise E::YearInv.new(year) if year.to_i < 2015 ||
|
93
|
+
year.to_i > 2020
|
94
|
+
year
|
95
|
+
end
|
96
|
+
def self.day(day)
|
97
|
+
raise E::DayNil if day.nil? || day == 0
|
98
|
+
raise E::DayInv.new(day) if day.to_i < 1 ||
|
99
|
+
day.to_i > 25
|
100
|
+
day
|
101
|
+
end
|
102
|
+
def self.part(part)
|
103
|
+
raise E::PartNil if part.nil?
|
104
|
+
raise E::PuzzComp if part.to_i == 3
|
105
|
+
raise E::PartInv if part.to_i < 1 || part.to_i > 2
|
106
|
+
part
|
107
|
+
end
|
108
|
+
def self.set_key(key)
|
109
|
+
raise E::KeyNil if key.nil?
|
110
|
+
raise E::KeyDup if Files::Config
|
111
|
+
.new.is_set?(val:key)
|
112
|
+
key
|
113
|
+
end
|
114
|
+
def self.get_key(key)
|
115
|
+
raise E::KeyNil if key.nil?
|
116
|
+
key
|
117
|
+
end
|
118
|
+
def self.ans(ans)
|
119
|
+
raise E::AnsNil if ans.nil?
|
120
|
+
ans
|
121
|
+
end
|
122
|
+
def self.day_dir(day)
|
123
|
+
raise E::DayExist.new(day) if Dir.exist?(day)
|
124
|
+
day
|
125
|
+
end
|
126
|
+
def self.init(dir)
|
127
|
+
raise E::NotInit unless File.exist?("#{dir}/.meta")
|
128
|
+
dir
|
129
|
+
end
|
130
|
+
def self.not_init(dir:, year:)
|
131
|
+
raise E::AlrInit if File.exist?("#{dir}/.meta") &&
|
132
|
+
Metafile.get(:year) != year.to_s
|
133
|
+
dir
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module AocCli
|
2
|
+
module Paths
|
3
|
+
class Config
|
4
|
+
def self.create
|
5
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
6
|
+
File.write(path, "", mode:"a") unless File
|
7
|
+
.exist?(path)
|
8
|
+
end
|
9
|
+
def self.dir
|
10
|
+
"#{Dir.home}/.config/aoc-cli"
|
11
|
+
end
|
12
|
+
def self.path
|
13
|
+
"#{dir}/aoc.rc"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
class Day
|
17
|
+
attr_reader :user, :year, :day
|
18
|
+
def initialize(u:Metafile.get(:user),
|
19
|
+
y:Metafile.get(:year), d:)
|
20
|
+
@user = Validate.user(u)
|
21
|
+
@year = Validate.year(y)
|
22
|
+
@day = Validate.day(d)
|
23
|
+
end
|
24
|
+
def filename(f:)
|
25
|
+
case f.to_sym
|
26
|
+
when :Input then "input"
|
27
|
+
when :Puzzle then "#{day}.md"
|
28
|
+
when :meta then ".meta"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
def in_day?
|
32
|
+
Metafile.type == :DAY
|
33
|
+
end
|
34
|
+
def day_dir
|
35
|
+
day.to_i < 10 ? "0#{day}" : day.to_s
|
36
|
+
end
|
37
|
+
def local_dir
|
38
|
+
in_day? ? "." : "#{day_dir}"
|
39
|
+
end
|
40
|
+
def cache_dir
|
41
|
+
"#{Dir.home}/.cache/aoc-cli/"\
|
42
|
+
"#{user}/#{year}/#{day_dir}"
|
43
|
+
end
|
44
|
+
def local(f:)
|
45
|
+
"#{local_dir}/#{filename(f:f)}"
|
46
|
+
end
|
47
|
+
def cache_path(f:)
|
48
|
+
"#{cache_dir}/#{filename(f:f)}"
|
49
|
+
end
|
50
|
+
def cache_and_local(f:)
|
51
|
+
[cache_path(f:f), local(f:f)]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
class Year
|
55
|
+
attr_reader :user, :year
|
56
|
+
def initialize(u:Metafile.get(:user),
|
57
|
+
y:Metafile.get(:year))
|
58
|
+
@user = Validate.user(u)
|
59
|
+
@year = Validate.year(y)
|
60
|
+
end
|
61
|
+
def in_year?
|
62
|
+
File.exist?("./.meta") ?
|
63
|
+
Metafile.type == :ROOT : true
|
64
|
+
end
|
65
|
+
def year_dir
|
66
|
+
in_year? ? "." : ".."
|
67
|
+
end
|
68
|
+
def local(f:)
|
69
|
+
"#{Validate.not_init(dir:year_dir,
|
70
|
+
year:year)}/#{filename(f:f)}"
|
71
|
+
end
|
72
|
+
def filename(f:)
|
73
|
+
case f.to_sym
|
74
|
+
when :Stars then "#{year}.md"
|
75
|
+
when :meta then ".meta"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
class Database < Config
|
80
|
+
def self.dir
|
81
|
+
"#{super}/db"
|
82
|
+
end
|
83
|
+
def self.cfg(name)
|
84
|
+
FileUtils.mkdir_p(dir) unless Dir
|
85
|
+
.exist?(dir)
|
86
|
+
"#{dir}/#{name}.db"
|
87
|
+
end
|
88
|
+
def self.root(name)
|
89
|
+
"#{__dir__}/db/#{name}.db"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module AocCli
|
2
|
+
module Solve
|
3
|
+
CORRECT = "That's the right answer"
|
4
|
+
INCORRECT = "That's not the right answer"
|
5
|
+
WAIT = "You gave an answer too recently"
|
6
|
+
class Attempt
|
7
|
+
attr_reader :user, :year, :day, :part, :answer
|
8
|
+
def initialize(u:Metafile.get(:user),
|
9
|
+
y:Metafile.get(:year),
|
10
|
+
d:Metafile.get(:day),
|
11
|
+
p:Metafile.get(:part), a:)
|
12
|
+
@user = Validate.user(u)
|
13
|
+
@year = Validate.year(y)
|
14
|
+
@day = Validate.day(d)
|
15
|
+
@part = Validate.part(p)
|
16
|
+
@answer = Validate.ans(a)
|
17
|
+
end
|
18
|
+
def raw
|
19
|
+
@raw ||= Tools::Post
|
20
|
+
.new(u:user, y:year, d:day,
|
21
|
+
data:{level:part, answer:answer})
|
22
|
+
.plain
|
23
|
+
end
|
24
|
+
def check
|
25
|
+
case raw
|
26
|
+
when /#{CORRECT}/ then :Correct
|
27
|
+
when /#{INCORRECT}/ then :Incorrect
|
28
|
+
when /#{WAIT}/ then :Wait
|
29
|
+
end
|
30
|
+
end
|
31
|
+
def respond
|
32
|
+
Object
|
33
|
+
.const_get("AocCli::Solve::Respond::#{check}")
|
34
|
+
.new(attempt:self)
|
35
|
+
.respond
|
36
|
+
.react
|
37
|
+
end
|
38
|
+
end
|
39
|
+
module Respond
|
40
|
+
class Response
|
41
|
+
attr_reader :attempt
|
42
|
+
def initialize(attempt:)
|
43
|
+
@attempt = attempt
|
44
|
+
end
|
45
|
+
end
|
46
|
+
class Correct < Response
|
47
|
+
def react
|
48
|
+
log = Database::Log
|
49
|
+
.new(attempt:attempt)
|
50
|
+
.correct
|
51
|
+
Database::Stats::Complete
|
52
|
+
.new(n:log.count_attempts)
|
53
|
+
.update
|
54
|
+
Year.refresh
|
55
|
+
Day.refresh
|
56
|
+
Database::Stats::Init.new.init
|
57
|
+
end
|
58
|
+
def respond
|
59
|
+
response = "#{"Correct!".bold.green} "
|
60
|
+
response += case attempt.part
|
61
|
+
when "1" then next_part
|
62
|
+
when "2" then complete end
|
63
|
+
puts response
|
64
|
+
self
|
65
|
+
end
|
66
|
+
private
|
67
|
+
def next_part
|
68
|
+
"Downloading part two.."
|
69
|
+
end
|
70
|
+
def complete
|
71
|
+
"This day is now complete!".green
|
72
|
+
end
|
73
|
+
end
|
74
|
+
class Incorrect < Response
|
75
|
+
def react
|
76
|
+
Database::Log
|
77
|
+
.new(attempt:attempt)
|
78
|
+
.incorrect(high:high, low:low)
|
79
|
+
end
|
80
|
+
def respond
|
81
|
+
response = "#{"Incorrect".red.bold}: "\
|
82
|
+
"You guessed - #{attempt.answer.to_s.red}\n"
|
83
|
+
response += "This answer is too high\n" if high
|
84
|
+
response += "This answer is too low\n" if low
|
85
|
+
puts response
|
86
|
+
self
|
87
|
+
end
|
88
|
+
def high
|
89
|
+
/too high/.match?(attempt.raw)
|
90
|
+
end
|
91
|
+
def low
|
92
|
+
/too low/.match?(attempt.raw)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
class Wait < Response
|
96
|
+
def respond
|
97
|
+
response = "#{"Please wait".yellow.bold}: "\
|
98
|
+
"You have #{time.to_s} to wait"
|
99
|
+
puts response
|
100
|
+
self
|
101
|
+
end
|
102
|
+
def time
|
103
|
+
attempt.raw.scan(/(?:\d+m\s)?\d+s/).first.to_s
|
104
|
+
end
|
105
|
+
def react; end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|