aoc_cli 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|