@@ -31,17 +31,179 @@ var globalTestData: String? = null
3131 field = null
3232 }
3333
34- @ExperimentalTime
35- fun main () {
36- verbose = false
37- with (aocTerminal) {
38- println (red(" \n ~~~ Advent Of Code Runner ~~~\n " ))
39- val dayClasses = getAllDayClasses().sortedBy(::dayNumber)
40- val totalDuration = dayClasses.map { it.execute() }.reduceOrNull(Duration ::plus)
41- println (" \n Total runtime: ${red(" $totalDuration " )} " )
34+ // @ExperimentalTime
35+ // fun main() {
36+ // verbose = false
37+ // with(aocTerminal) {
38+ // println(red("\n~~~ Advent Of Code Runner ~~~\n"))
39+ // val dayClasses = getAllDayClasses().sortedBy(::dayNumber)
40+ // val totalDuration = dayClasses.map { it.execute() }.reduceOrNull(Duration::plus)
41+ // println("\nTotal runtime: ${red("$totalDuration")}")
42+ // }
43+ // }
44+
45+ class AdventOfCode (var session : String? = null ) {
46+
47+ private val web = AoCWebInterface (getSessionCookie())
48+
49+ fun sendToClipboard (a : Any? ): Boolean {
50+ if (a in listOf (null , 0 , - 1 , Day .NotYetImplemented )) return false
51+ return runCatching {
52+ val clipboard: Clipboard = Toolkit .getDefaultToolkit().systemClipboard
53+ val transferable: Transferable = StringSelection (a.toString())
54+ clipboard.setContents(transferable, null )
55+ }.isSuccess
56+ }
57+
58+ fun getPuzzleInput (aocPuzzle : AoCPuzzle ): List <String > {
59+ val cached = readInputFileOrNull(aocPuzzle)
60+ if (! cached.isNullOrEmpty()) return cached
61+
62+ return web.downloadInput(aocPuzzle).onSuccess {
63+ writeInputFile(aocPuzzle, it)
64+ }.getOrElse {
65+ listOf (" Unable to download ${aocPuzzle} : $it " )
66+ }
67+ }
68+
69+ fun submitAnswer (aocPuzzle : AoCPuzzle , part : Part , answer : String ): AoCWebInterface .Verdict =
70+ web.submitAnswer(aocPuzzle, part, answer)
71+
72+ private val logFormat = DateTimeFormatter .ofPattern(" HH:mm:ss" )
73+
74+ fun appendSubmitLog (aocPuzzle : AoCPuzzle , part : Part , answer : String , verdict : AoCWebInterface .Verdict ) {
75+ val now = LocalDateTime .now()
76+ val nowText = logFormat.format(now)
77+ val id = idFor(aocPuzzle, part)
78+ val text =
79+ " $nowText - $id - submitted \" $answer \" - ${if (verdict is AoCWebInterface .Verdict .Correct ) " OK" else " FAIL with ${verdict::class .simpleName} " } "
80+ appendSubmitLog(aocPuzzle, text)
81+ appendSubmitLog(aocPuzzle, verdict.text)
82+ if (verdict is AoCWebInterface .Verdict .WithWait ) {
83+ val locked = now + verdict.wait.toJavaDuration()
84+ appendSubmitLog(
85+ aocPuzzle,
86+ " $nowText - $id - LOCKED until ${DateTimeFormatter .ISO_DATE_TIME .format(locked)} "
87+ )
88+ }
89+ }
90+
91+ class PreviousSubmitted (
92+ private val locked : LocalDateTime ? ,
93+ private val answers : List <String >,
94+ private val log : List <String >,
95+ ) {
96+ operator fun contains (answer : String ) = answer in answers
97+ fun isNotEmpty () = answers.isNotEmpty()
98+
99+ override fun toString () = (
100+ listOf (
101+ brightMagenta(" Previously submitted answers were:" ),
102+ " ${log.size} attempts in total" .takeIf { log.size > 3 }
103+ ) +
104+ log.takeLast(3 ).map { it.highlight() } +
105+ lockInfo()
106+ )
107+ .filterNotNull()
108+ .joinToString(" \n " , postfix = " \n " )
109+
110+ private fun String.highlight () =
111+ split(" \" " , limit = 3 ).mapIndexed { index, s -> if (index == 1 ) brightMagenta(s) else s }.joinToString(" " )
112+
113+ private fun lockInfo () = locked?.let {
114+ if (isStillLocked)
115+ brightRed(" Locked until $it " )
116+ else
117+ yellow(" Had been locked, but is free again!" )
118+ }
119+
120+ private val isStillLocked get() = locked?.let { LocalDateTime .now() < it } == true
121+
122+ val waitSecondsOrNull
123+ get() = locked?.let {
124+ val now = LocalDateTime .now()
125+ (it.toEpochSecond(ZoneOffset .UTC ) - now.toEpochSecond(ZoneOffset .UTC ))
126+ }.takeIf { (it ? : 0 ) > 0 }
127+
128+ fun waitUntilFree () {
129+ isStillLocked || return
130+ with (aocTerminal) {
131+ do {
132+ cursor.move { startOfLine();clearLine() }
133+ print (brightRed(" Waiting $waitSecondsOrNull more seconds..." ))
134+ Thread .sleep(500 )
135+ } while (LocalDateTime .now() < locked!! )
136+ cursor.move { startOfLine(); clearLine() }
137+ println (" Fire!" )
138+ }
139+ }
140+ }
141+
142+ fun previouslySubmitted (aocPuzzle : AoCPuzzle , part : Part ): PreviousSubmitted =
143+ readSubmitLog(aocPuzzle)
144+ .filter { idFor(aocPuzzle, part) in it }
145+ .let { relevant ->
146+ val answers = relevant
147+ .filter { " submitted" in it }
148+ .mapNotNull { log ->
149+ log.split(" \" " ).getOrNull(1 )?.let { it to log }
150+ }
151+
152+ val locked = if (" LOCKED" in relevant.lastOrNull().orEmpty()) {
153+ val lock = relevant.last().substringAfter(" until " )
154+ LocalDateTime .from(DateTimeFormatter .ISO_DATE_TIME .parse(lock))
155+ } else null
156+
157+ PreviousSubmitted (locked, answers.map { it.first }, answers.map { it.second })
158+ }
159+
160+ private fun idFor (aocPuzzle : AoCPuzzle , part : Part ) =
161+ " ${aocPuzzle.year} day ${aocPuzzle.day} part $part "
162+
163+ private fun getSessionCookie () =
164+ this .session
165+ ? : System .getenv(" AOC_COOKIE" )
166+ ? : object {}.javaClass.getResource(" session-cookie" )
167+ ?.readText()
168+ ?.lines()
169+ ?.firstOrNull { it.isNotBlank() }
170+ ? : warn(" No session cookie in environment or file found, will not be able to talk to AoC server." )
171+
172+ private fun readInputFileOrNull (aocPuzzle : AoCPuzzle ): List <String >? {
173+ val file = File (fileNameFor(aocPuzzle))
174+ file.exists() || return null
175+ return file.readLines()
176+ }
177+
178+ private fun writeInputFile (aocPuzzle : AoCPuzzle , content : List <String >) {
179+ File (pathNameForYear(aocPuzzle)).mkdirs()
180+ File (fileNameFor(aocPuzzle)).writeText(content.joinToString(" \n " ))
181+ }
182+
183+ private fun readSubmitLog (aocPuzzle : AoCPuzzle ): List <String > {
184+ val file = File (submitLogFor(aocPuzzle))
185+ file.exists() || return emptyList()
186+ return file.readLines()
187+ }
188+
189+ private fun pathNameForYear (aocPuzzle : AoCPuzzle ): String {
190+ return " puzzles/${aocPuzzle.year} "
191+ }
192+
193+ private fun fileNameFor (aocPuzzle : AoCPuzzle ): String {
194+ return " ${pathNameForYear(aocPuzzle)} /day${" %02d" .format(aocPuzzle.day)} .txt"
195+ }
196+
197+ private fun submitLogFor (aocPuzzle : AoCPuzzle ): String {
198+ return " ${pathNameForYear(aocPuzzle)} /submit.log"
199+ }
200+
201+ private fun appendSubmitLog (aocPuzzle : AoCPuzzle , content : String ) {
202+ File (submitLogFor(aocPuzzle)).appendText(" \n $content " )
42203 }
43204}
44205
206+
45207fun getAllDayClasses (): Collection <Class <out Day >> =
46208 Reflections (" " ).getSubTypesOf(Day ::class .java).filter { it.simpleName.matches(Regex (" Day\\ d+" )) }
47209
@@ -97,8 +259,11 @@ data class TestData(val input: String, val expectedPart1: Any?, val expectedPart
97259 println (gray(" Checking part $part against $expectation ..." ))
98260 val actual = partFun()
99261 val match = actual == Day .NotYetImplemented || " $actual " == expectation
100- if (! match) {
101- aocTerminal.danger(" Checking of part $part failed" )
262+ if (match) {
263+ aocTerminal.success(" ✅ Test succeeded." )
264+ }
265+ else {
266+ aocTerminal.danger(" ❌ Checking of part $part failed" )
102267 println (" Expected: ${brightRed(expectation)} " )
103268 println (" Actual: ${brightRed(" $actual " )} " )
104269 println (yellow(" Check demo ${TextStyles .bold(" input" )} and demo ${TextStyles .bold(" expectation" )} !" ))
@@ -201,166 +366,6 @@ fun Any?.restrictWidth(minWidth: Int, maxWidth: Int) = with("$this") {
201366 }
202367}
203368
204- object AoC {
205-
206- private val web = AoCWebInterface (getSessionCookie())
207-
208- fun sendToClipboard (a : Any? ): Boolean {
209- if (a in listOf (null , 0 , - 1 , Day .NotYetImplemented )) return false
210- return runCatching {
211- val clipboard: Clipboard = Toolkit .getDefaultToolkit().systemClipboard
212- val transferable: Transferable = StringSelection (a.toString())
213- clipboard.setContents(transferable, null )
214- }.isSuccess
215- }
216-
217- fun getPuzzleInput (aocPuzzle : AoCPuzzle ): List <String > {
218- val cached = readInputFileOrNull(aocPuzzle)
219- if (! cached.isNullOrEmpty()) return cached
220-
221- return web.downloadInput(aocPuzzle).onSuccess {
222- writeInputFile(aocPuzzle, it)
223- }.getOrElse {
224- listOf (" Unable to download ${aocPuzzle} : $it " )
225- }
226- }
227-
228- fun submitAnswer (aocPuzzle : AoCPuzzle , part : Part , answer : String ): AoCWebInterface .Verdict =
229- web.submitAnswer(aocPuzzle, part, answer)
230-
231- private val logFormat = DateTimeFormatter .ofPattern(" HH:mm:ss" )
232-
233- fun appendSubmitLog (aocPuzzle : AoCPuzzle , part : Part , answer : String , verdict : AoCWebInterface .Verdict ) {
234- val now = LocalDateTime .now()
235- val nowText = logFormat.format(now)
236- val id = idFor(aocPuzzle, part)
237- val text =
238- " $nowText - $id - submitted \" $answer \" - ${if (verdict is AoCWebInterface .Verdict .Correct ) " OK" else " FAIL with ${verdict::class .simpleName} " } "
239- appendSubmitLog(aocPuzzle, text)
240- appendSubmitLog(aocPuzzle, verdict.text)
241- if (verdict is AoCWebInterface .Verdict .WithWait ) {
242- val locked = now + verdict.wait.toJavaDuration()
243- appendSubmitLog(
244- aocPuzzle,
245- " $nowText - $id - LOCKED until ${DateTimeFormatter .ISO_DATE_TIME .format(locked)} "
246- )
247- }
248- }
249-
250- class PreviousSubmitted (
251- private val locked : LocalDateTime ? ,
252- private val answers : List <String >,
253- private val log : List <String >,
254- ) {
255- operator fun contains (answer : String ) = answer in answers
256- fun isNotEmpty () = answers.isNotEmpty()
257-
258- override fun toString () = (
259- listOf (
260- brightMagenta(" Previously submitted answers were:" ),
261- " ${log.size} attempts in total" .takeIf { log.size > 3 }
262- ) +
263- log.takeLast(3 ).map { it.highlight() } +
264- lockInfo()
265- )
266- .filterNotNull()
267- .joinToString(" \n " , postfix = " \n " )
268-
269- private fun String.highlight () =
270- split(" \" " , limit = 3 ).mapIndexed { index, s -> if (index == 1 ) brightMagenta(s) else s }.joinToString(" " )
271-
272- private fun lockInfo () = locked?.let {
273- if (isStillLocked)
274- brightRed(" Locked until $it " )
275- else
276- yellow(" Had been locked, but is free again!" )
277- }
278-
279- private val isStillLocked get() = locked?.let { LocalDateTime .now() < it } == true
280-
281- val waitSecondsOrNull
282- get() = locked?.let {
283- val now = LocalDateTime .now()
284- (it.toEpochSecond(ZoneOffset .UTC ) - now.toEpochSecond(ZoneOffset .UTC ))
285- }.takeIf { (it ? : 0 ) > 0 }
286-
287- fun waitUntilFree () {
288- isStillLocked || return
289- with (aocTerminal) {
290- do {
291- cursor.move { startOfLine();clearLine() }
292- print (brightRed(" Waiting $waitSecondsOrNull more seconds..." ))
293- Thread .sleep(500 )
294- } while (LocalDateTime .now() < locked!! )
295- cursor.move { startOfLine(); clearLine() }
296- println (" Fire!" )
297- }
298- }
299- }
300-
301- fun previouslySubmitted (aocPuzzle : AoCPuzzle , part : Part ): PreviousSubmitted =
302- readSubmitLog(aocPuzzle)
303- .filter { idFor(aocPuzzle, part) in it }
304- .let { relevant ->
305- val answers = relevant
306- .filter { " submitted" in it }
307- .mapNotNull { log ->
308- log.split(" \" " ).getOrNull(1 )?.let { it to log }
309- }
310-
311- val locked = if (" LOCKED" in relevant.lastOrNull().orEmpty()) {
312- val lock = relevant.last().substringAfter(" until " )
313- LocalDateTime .from(DateTimeFormatter .ISO_DATE_TIME .parse(lock))
314- } else null
315-
316- PreviousSubmitted (locked, answers.map { it.first }, answers.map { it.second })
317- }
318-
319- private fun idFor (aocPuzzle : AoCPuzzle , part : Part ) =
320- " ${aocPuzzle.year} day ${aocPuzzle.day} part $part "
321-
322- private fun getSessionCookie () =
323- System .getenv(" AOC_COOKIE" )
324- ? : object {}.javaClass.getResource(" session-cookie" )
325- ?.readText()
326- ?.lines()
327- ?.firstOrNull { it.isNotBlank() }
328- ? : warn(" No session cookie in environment or file found, will not be able to talk to AoC server." )
329-
330- private fun readInputFileOrNull (aocPuzzle : AoCPuzzle ): List <String >? {
331- val file = File (fileNameFor(aocPuzzle))
332- file.exists() || return null
333- return file.readLines()
334- }
335-
336- private fun writeInputFile (aocPuzzle : AoCPuzzle , content : List <String >) {
337- File (pathNameForYear(aocPuzzle)).mkdirs()
338- File (fileNameFor(aocPuzzle)).writeText(content.joinToString(" \n " ))
339- }
340-
341- private fun readSubmitLog (aocPuzzle : AoCPuzzle ): List <String > {
342- val file = File (submitLogFor(aocPuzzle))
343- file.exists() || return emptyList()
344- return file.readLines()
345- }
346-
347- private fun pathNameForYear (aocPuzzle : AoCPuzzle ): String {
348- return " puzzles/${aocPuzzle.year} "
349- }
350-
351- private fun fileNameFor (aocPuzzle : AoCPuzzle ): String {
352- return " ${pathNameForYear(aocPuzzle)} /day${" %02d" .format(aocPuzzle.day)} .txt"
353- }
354-
355- private fun submitLogFor (aocPuzzle : AoCPuzzle ): String {
356- return " ${pathNameForYear(aocPuzzle)} /submit.log"
357- }
358-
359- private fun appendSubmitLog (aocPuzzle : AoCPuzzle , content : String ) {
360- File (submitLogFor(aocPuzzle)).appendText(" \n $content " )
361- }
362- }
363-
364369@Suppress(" NOTHING_TO_INLINE" )
365370inline fun useSystemProxies () {
366371 System .setProperty(" java.net.useSystemProxies" , " true" )
0 commit comments