diff --git a/.gitignore b/.gitignore index dd506db..6d1ca3d 100644 --- a/.gitignore +++ b/.gitignore @@ -373,3 +373,6 @@ FodyWeavers.xsd # Built Visual Studio Code Extensions *.vsix + +# Advent of Code session cookie - DO NOT COMMIT +.aoc-session diff --git a/AoC/AoC.csproj b/AoC/AoC.csproj index 4c34953..72313f3 100644 --- a/AoC/AoC.csproj +++ b/AoC/AoC.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable enable @@ -12,101 +12,9 @@ - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always + + + PreserveNewest diff --git a/AoC/Input/_2025/01.txt b/AoC/Input/_2025/01.txt new file mode 100644 index 0000000..ad709a2 --- /dev/null +++ b/AoC/Input/_2025/01.txt @@ -0,0 +1,5 @@ +100 +200 +300 +400 +500 diff --git a/AoC/Interface/AoCDay.cs b/AoC/Interface/AoCDay.cs index 2b29560..7018900 100644 --- a/AoC/Interface/AoCDay.cs +++ b/AoC/Interface/AoCDay.cs @@ -5,43 +5,190 @@ using System.Text; using System.Threading.Tasks; using System.Windows; +using AoC.Services; namespace AoC.Interface { public abstract class AoCDay { + protected List TestCases { get; } = new List(); public string[] GetInput() { - var res = File.ReadAllLines($"Input/{GetType().Namespace[^5..]}/{GetType().Name.Replace("Day", "")}.txt"); - if (res.Length == 0) + var year = int.Parse(GetType().Namespace![^4..]); + var day = int.Parse(GetType().Name.Replace("Day", "")); + + // Use paths relative to the executable location + var baseDir = AppDomain.CurrentDomain.BaseDirectory; + var inputPath = Path.Combine(baseDir, "Input", $"_{year}", $"{day:D2}.txt"); + var sourceInputPath = Path.Combine(baseDir, "..", "..", "..", "Input", $"_{year}", $"{day:D2}.txt"); + + // Check if input file exists and has content + if (File.Exists(inputPath) && new FileInfo(inputPath).Length > 0) + { + return File.ReadAllLines(inputPath); + } + + // Try automated fetch if session cookie is configured + if (AoCConfig.HasSessionCookie()) { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("No input found!"); - Console.ForegroundColor = ConsoleColor.White; Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Use clipboard to set input? (y/n)"); + Console.WriteLine($"Input file not found. Attempting to fetch from adventofcode.com..."); Console.ForegroundColor = ConsoleColor.White; - if (Console.ReadKey() is ConsoleKeyInfo key && key.Key == ConsoleKey.Y) + + var client = new AoCClient(); + var input = client.FetchInputAsync(year, day).GetAwaiter().GetResult(); + + if (!string.IsNullOrEmpty(input)) { - var input = GetText(); - File.WriteAllLines($"Input/{GetType().Namespace[^5..]}/{GetType().Name.Replace("Day", "")}.txt", input.Split(Environment.NewLine)); - File.WriteAllLines($"../../../Input/{GetType().Namespace[^5..]}/{GetType().Name.Replace("Day", "")}.txt", input.Split(Environment.NewLine)); - Console.SetCursorPosition(0, Console.CursorTop); + // Ensure directory exists + Directory.CreateDirectory(Path.GetDirectoryName(inputPath)!); + Directory.CreateDirectory(Path.GetDirectoryName(sourceInputPath)!); + + // Save to both locations + var lines = input.Split('\n'); + File.WriteAllLines(inputPath, lines); + File.WriteAllLines(sourceInputPath, lines); + Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("Input saved to file: " + $"Input/{GetType().Namespace[^5..]}/{GetType().Name.Replace("Day", "")}.txt"); + Console.WriteLine($"✓ Input saved to: {inputPath}"); Console.ForegroundColor = ConsoleColor.White; - res = input.Split(Environment.NewLine); - + return lines; } } - return res; + // Fallback to clipboard method + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("No input found!"); + Console.ForegroundColor = ConsoleColor.White; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Use clipboard to set input? (y/n)"); + Console.ForegroundColor = ConsoleColor.White; + if (Console.ReadKey() is ConsoleKeyInfo key && key.Key == ConsoleKey.Y) + { + var input = GetText(); + Directory.CreateDirectory(Path.GetDirectoryName(inputPath)!); + Directory.CreateDirectory(Path.GetDirectoryName(sourceInputPath)!); + File.WriteAllLines(inputPath, input.Split(Environment.NewLine)); + File.WriteAllLines(sourceInputPath, input.Split(Environment.NewLine)); + Console.SetCursorPosition(0, Console.CursorTop); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"Input saved to file: {inputPath}"); + Console.ForegroundColor = ConsoleColor.White; + return input.Split(Environment.NewLine); + } + + return Array.Empty(); } public abstract void RunPart1(); public abstract void RunPart2(); + // Optional: Override these for automated testing + public virtual string? SolvePart1(string[] input) => null; + public virtual string? SolvePart2(string[] input) => null; + + protected void AddTestCase(string input, string? expectedPart1 = null, string? expectedPart2 = null) + { + TestCases.Add(new TestCase + { + Input = input.Split('\n'), + ExpectedPart1 = expectedPart1, + ExpectedPart2 = expectedPart2 + }); + } + + public bool RunTests(int part) + { + if (TestCases.Count == 0) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("No test cases defined."); + Console.ForegroundColor = ConsoleColor.White; + return true; + } + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine($"\n▶ Running {TestCases.Count} test case(s) for Part {part}..."); + Console.ForegroundColor = ConsoleColor.White; + + bool allPassed = true; + for (int i = 0; i < TestCases.Count; i++) + { + var testCase = TestCases[i]; + var expected = part == 1 ? testCase.ExpectedPart1 : testCase.ExpectedPart2; + + if (expected == null) + continue; + + var result = part == 1 ? SolvePart1(testCase.Input) : SolvePart2(testCase.Input); + + if (result == expected) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($" ✓ Test {i + 1} passed: {result}"); + Console.ForegroundColor = ConsoleColor.White; + } + else + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($" ✗ Test {i + 1} failed:"); + Console.WriteLine($" Expected: {expected}"); + Console.WriteLine($" Got: {result}"); + Console.ForegroundColor = ConsoleColor.White; + allPassed = false; + } + } + + return allPassed; + } + + public async Task SubmitAnswer(int part, string answer) + { + if (!AoCConfig.HasSessionCookie()) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Cannot submit: Session cookie not configured."); + Console.ForegroundColor = ConsoleColor.White; + return false; + } + + var year = int.Parse(GetType().Namespace![^4..]); + var day = int.Parse(GetType().Name.Replace("Day", "")); + + var client = new AoCClient(); + var result = await client.SubmitAnswerAsync(year, day, part, answer); + + if (result.Success) + { + if (result.IsCorrect) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"\n{result.Message}"); + Console.ForegroundColor = ConsoleColor.White; + return true; + } + else + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"\n{result.Message}"); + Console.ForegroundColor = ConsoleColor.White; + return false; + } + } + else + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"\n{result.Message}"); + if (result.WaitTimeSeconds.HasValue) + { + Console.WriteLine($"Please wait {result.WaitTimeSeconds} seconds before trying again."); + } + Console.ForegroundColor = ConsoleColor.White; + return false; + } + } + private static string GetText() { var powershell = new Process @@ -61,4 +208,11 @@ private static string GetText() return text.TrimEnd(); } } + + public class TestCase + { + public string[] Input { get; set; } = Array.Empty(); + public string? ExpectedPart1 { get; set; } + public string? ExpectedPart2 { get; set; } + } } diff --git a/AoC/Program.cs b/AoC/Program.cs index 250a87d..43f5e19 100644 --- a/AoC/Program.cs +++ b/AoC/Program.cs @@ -1,10 +1,86 @@ -string year = DateTime.Now.Year.ToString(); -string day = DateTime.Today.Day.ToString("00"); +using AoC.Services; -day = 7.ToString("00"); +// Parse command line arguments +int? argYear = null; +int? argDay = null; +bool runTests = false; +bool autoSubmit = false; +bool configureSession = false; + +for (int i = 0; i < args.Length; i++) +{ + if (args[i] == "--year" || args[i] == "-y") + { + if (i + 1 < args.Length && int.TryParse(args[i + 1], out int parsedYear)) + { + argYear = parsedYear; + i++; + } + } + else if (args[i] == "--day" || args[i] == "-d") + { + if (i + 1 < args.Length && int.TryParse(args[i + 1], out int parsedDay)) + { + argDay = parsedDay; + i++; + } + } + else if (args[i] == "--test" || args[i] == "-t") + { + runTests = true; + } + else if (args[i] == "--submit" || args[i] == "-s") + { + autoSubmit = true; + } + else if (args[i] == "--configure" || args[i] == "-c") + { + configureSession = true; + } + else if (args[i] == "--help" || args[i] == "-h") + { + ShowHelp(); + return; + } +} + +// Handle session configuration +if (configureSession) +{ + ConfigureSession(); + return; +} + +// Default to current date +string year = (argYear ?? DateTime.Now.Year).ToString(); +string day = (argDay ?? DateTime.Today.Day).ToString("00"); + +// For testing purposes, you can uncomment this to test a specific day: +// day = 1.ToString("00"); + +try +{ + AoCDay? dayClass = Activator.CreateInstance(Type.GetType($"AoC.Solutions._{year}.Day{day}")) as AoCDay; + + if (dayClass == null) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: Could not find solution class for year {year}, day {day}"); + Console.WriteLine($"Make sure AoC.Solutions._{year}.Day{day} exists."); + Console.ForegroundColor = ConsoleColor.White; + return; + } + + RunDay(dayClass, year, day, runTests, autoSubmit); +} +catch (Exception ex) +{ + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ForegroundColor = ConsoleColor.White; + return; +} -AoCDay dayClass = Activator.CreateInstance(Type.GetType($"AoC.Solutions._{year}.Day{day}")) as AoCDay; -RunDay(dayClass!, year, day); Console.ForegroundColor = ConsoleColor.Magenta; Console.WriteLine("\nRun previous days? (y/n)"); Console.ForegroundColor = ConsoleColor.White; @@ -15,26 +91,157 @@ day = (int.Parse(day) - 1).ToString("00"); while (day != "00") { - dayClass = Activator.CreateInstance(Type.GetType($"AoC.Solutions._{year}.Day{day}")) as AoCDay; - RunDay(dayClass, year, day); + try + { + AoCDay? dayClass = Activator.CreateInstance(Type.GetType($"AoC.Solutions._{year}.Day{day}")) as AoCDay; + if (dayClass != null) + { + RunDay(dayClass, year, day, false, false); + } + } + catch + { + // Skip days that don't exist + } day = (int.Parse(day) - 1).ToString("00"); - // wait 1 second Thread.Sleep(300); } } -static void RunDay(AoCDay dayClass, string year, string day) +static void RunDay(AoCDay dayClass, string year, string day, bool runTests, bool autoSubmit) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("+----------------------------+"); Console.WriteLine($"| Advent of Code {year} Day {day} |"); Console.WriteLine("+----------------------------+"); + Console.ForegroundColor = ConsoleColor.White; + + // Run tests if requested + if (runTests) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Part 1 Tests:"); + Console.ForegroundColor = ConsoleColor.White; + bool part1TestsPassed = dayClass.RunTests(1); + + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("\nPart 2 Tests:"); + Console.ForegroundColor = ConsoleColor.White; + bool part2TestsPassed = dayClass.RunTests(2); + + if (!part1TestsPassed || !part2TestsPassed) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("\nSome tests failed. Do you want to continue? (y/n)"); + Console.ForegroundColor = ConsoleColor.White; + if (Console.ReadKey().Key != ConsoleKey.Y) + { + Console.WriteLine(); + return; + } + Console.WriteLine(); + } + } + + // Run Part 1 Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("Part 1:"); + Console.WriteLine("\nPart 1:"); Console.ForegroundColor = ConsoleColor.White; dayClass.RunPart1(); + + // Ask about submission for Part 1 + if (autoSubmit) + { + string[] input = dayClass.GetInput(); + var answer = dayClass.SolvePart1(input); + if (!string.IsNullOrEmpty(answer)) + { + Console.ForegroundColor = ConsoleColor.Magenta; + Console.Write($"\nSubmit answer '{answer}' for Part 1? (y/n) "); + Console.ForegroundColor = ConsoleColor.White; + if (Console.ReadKey().Key == ConsoleKey.Y) + { + Console.WriteLine(); + dayClass.SubmitAnswer(1, answer).Wait(); + } + Console.WriteLine(); + } + } + + // Run Part 2 Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("\nPart 2:"); Console.ForegroundColor = ConsoleColor.White; dayClass.RunPart2(); + + // Ask about submission for Part 2 + if (autoSubmit) + { + string[] input = dayClass.GetInput(); + var answer = dayClass.SolvePart2(input); + if (!string.IsNullOrEmpty(answer)) + { + Console.ForegroundColor = ConsoleColor.Magenta; + Console.Write($"\nSubmit answer '{answer}' for Part 2? (y/n) "); + Console.ForegroundColor = ConsoleColor.White; + if (Console.ReadKey().Key == ConsoleKey.Y) + { + Console.WriteLine(); + dayClass.SubmitAnswer(2, answer).Wait(); + } + Console.WriteLine(); + } + } +} + +static void ConfigureSession() +{ + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("=== Advent of Code Session Configuration ==="); + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine("\nTo get your session cookie:"); + Console.WriteLine("1. Log in to https://adventofcode.com"); + Console.WriteLine("2. Open browser DevTools (F12)"); + Console.WriteLine("3. Go to Application/Storage > Cookies"); + Console.WriteLine("4. Copy the value of the 'session' cookie"); + Console.WriteLine("\nPaste your session cookie:"); + + string? sessionCookie = Console.ReadLine(); + if (!string.IsNullOrWhiteSpace(sessionCookie)) + { + AoCConfig.SetSessionCookie(sessionCookie.Trim()); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("✓ Session cookie saved successfully!"); + Console.ForegroundColor = ConsoleColor.White; + } + else + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Error: No session cookie provided."); + Console.ForegroundColor = ConsoleColor.White; + } +} + +static void ShowHelp() +{ + Console.WriteLine("Advent of Code Solution Runner"); + Console.WriteLine("\nUsage: AoC [options]"); + Console.WriteLine("\nOptions:"); + Console.WriteLine(" -y, --year Specify the year (default: current year)"); + Console.WriteLine(" -d, --day Specify the day (default: current day)"); + Console.WriteLine(" -t, --test Run test cases before solving"); + Console.WriteLine(" -s, --submit Enable interactive answer submission"); + Console.WriteLine(" -c, --configure Configure session cookie"); + Console.WriteLine(" -h, --help Show this help message"); + Console.WriteLine("\nExamples:"); + Console.WriteLine(" AoC # Run today's puzzle"); + Console.WriteLine(" AoC -y 2025 -d 1 # Run 2025 day 1"); + Console.WriteLine(" AoC -y 2025 -d 1 -t # Run with tests"); + Console.WriteLine(" AoC -y 2025 -d 1 -t -s # Run with tests and submission"); + Console.WriteLine(" AoC --configure # Set up session cookie"); + Console.WriteLine("\nSession Cookie:"); + Console.WriteLine(" The session cookie can be configured using:"); + Console.WriteLine(" 1. Running: AoC --configure"); + Console.WriteLine(" 2. Creating a .aoc-session file in the project root"); + Console.WriteLine(" 3. Setting the AOC_SESSION environment variable"); } diff --git a/AoC/Services/AoCClient.cs b/AoC/Services/AoCClient.cs new file mode 100644 index 0000000..1636791 --- /dev/null +++ b/AoC/Services/AoCClient.cs @@ -0,0 +1,207 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace AoC.Services +{ + public class AoCClient + { + private readonly HttpClient _httpClient; + private const string BaseUrl = "https://adventofcode.com"; + + public AoCClient() + { + var handler = new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate + }; + _httpClient = new HttpClient(handler); + _httpClient.DefaultRequestHeaders.Add("User-Agent", "github.com/Tim567/AdventOfCode via .NET"); + } + + public async Task FetchInputAsync(int year, int day) + { + var sessionCookie = AoCConfig.GetSessionCookie(); + if (string.IsNullOrWhiteSpace(sessionCookie)) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Error: Session cookie not configured!"); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Please set your session cookie using one of these methods:"); + Console.WriteLine("1. Create a .aoc-session file in the project root"); + Console.WriteLine("2. Set the AOC_SESSION environment variable"); + Console.WriteLine("\nTo get your session cookie:"); + Console.WriteLine("1. Log in to https://adventofcode.com"); + Console.WriteLine("2. Open browser DevTools (F12)"); + Console.WriteLine("3. Go to Application/Storage > Cookies"); + Console.WriteLine("4. Copy the value of the 'session' cookie"); + Console.ForegroundColor = ConsoleColor.White; + return null; + } + + try + { + var url = $"{BaseUrl}/{year}/day/{day}/input"; + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("Cookie", $"session={sessionCookie}"); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine($"Fetching input from: {url}"); + Console.ForegroundColor = ConsoleColor.White; + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == HttpStatusCode.NotFound) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: Day {day} of year {year} not available yet or doesn't exist."); + Console.ForegroundColor = ConsoleColor.White; + return null; + } + + if (!response.IsSuccessStatusCode) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: Failed to fetch input. Status: {response.StatusCode}"); + if (response.StatusCode == HttpStatusCode.BadRequest) + { + Console.WriteLine("Check if your session cookie is valid."); + } + Console.ForegroundColor = ConsoleColor.White; + return null; + } + + var content = await response.Content.ReadAsStringAsync(); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"✓ Successfully fetched input ({content.Length} characters)"); + Console.ForegroundColor = ConsoleColor.White; + return content.TrimEnd(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error fetching input: {ex.Message}"); + Console.ForegroundColor = ConsoleColor.White; + return null; + } + } + + public async Task SubmitAnswerAsync(int year, int day, int part, string answer) + { + var sessionCookie = AoCConfig.GetSessionCookie(); + if (string.IsNullOrWhiteSpace(sessionCookie)) + { + return new SubmissionResult + { + Success = false, + Message = "Session cookie not configured. Cannot submit answer." + }; + } + + try + { + var url = $"{BaseUrl}/{year}/day/{day}/answer"; + var request = new HttpRequestMessage(HttpMethod.Post, url); + request.Headers.Add("Cookie", $"session={sessionCookie}"); + + var content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("level", part.ToString()), + new KeyValuePair("answer", answer) + }); + request.Content = content; + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine($"Submitting answer for {year} day {day} part {part}: {answer}"); + Console.ForegroundColor = ConsoleColor.White; + + var response = await _httpClient.SendAsync(request); + var responseText = await response.Content.ReadAsStringAsync(); + + return ParseSubmissionResponse(responseText); + } + catch (Exception ex) + { + return new SubmissionResult + { + Success = false, + Message = $"Error submitting answer: {ex.Message}" + }; + } + } + + private SubmissionResult ParseSubmissionResponse(string html) + { + var result = new SubmissionResult(); + + // Extract the main message from the response + var match = Regex.Match(html, @"

(.*?)

", RegexOptions.Singleline); + if (match.Success) + { + var message = match.Groups[1].Value; + // Remove HTML tags + message = Regex.Replace(message, @"<[^>]+>", ""); + // Decode HTML entities + message = WebUtility.HtmlDecode(message); + result.Message = message.Trim(); + + // Check for specific responses + if (message.Contains("That's the right answer", StringComparison.OrdinalIgnoreCase)) + { + result.Success = true; + result.IsCorrect = true; + result.Message = "✓ Correct! " + message; + } + else if (message.Contains("That's not the right answer", StringComparison.OrdinalIgnoreCase)) + { + result.Success = true; + result.IsCorrect = false; + result.Message = "✗ Incorrect. " + message; + } + else if (message.Contains("You gave an answer too recently", StringComparison.OrdinalIgnoreCase)) + { + result.Success = false; + result.Message = "⏳ Rate limited. " + message; + + // Try to extract wait time + var waitMatch = Regex.Match(message, @"(\d+)s"); + if (waitMatch.Success) + { + result.WaitTimeSeconds = int.Parse(waitMatch.Groups[1].Value); + } + else + { + waitMatch = Regex.Match(message, @"(\d+)m"); + if (waitMatch.Success) + { + result.WaitTimeSeconds = int.Parse(waitMatch.Groups[1].Value) * 60; + } + } + } + else if (message.Contains("Did you already complete it", StringComparison.OrdinalIgnoreCase)) + { + result.Success = false; + result.Message = "Already completed."; + } + else + { + result.Success = true; + } + } + else + { + result.Success = false; + result.Message = "Could not parse response from server."; + } + + return result; + } + } + + public class SubmissionResult + { + public bool Success { get; set; } + public bool IsCorrect { get; set; } + public string Message { get; set; } = string.Empty; + public int? WaitTimeSeconds { get; set; } + } +} diff --git a/AoC/Services/AoCConfig.cs b/AoC/Services/AoCConfig.cs new file mode 100644 index 0000000..371aa21 --- /dev/null +++ b/AoC/Services/AoCConfig.cs @@ -0,0 +1,53 @@ +namespace AoC.Services +{ + public class AoCConfig + { + private static string? _sessionCookie; + private static readonly string ConfigFilePath = ".aoc-session"; + + public static string? GetSessionCookie() + { + if (_sessionCookie != null) + return _sessionCookie; + + // Try to load from file in project root + var rootPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..", ConfigFilePath); + if (File.Exists(rootPath)) + { + _sessionCookie = File.ReadAllText(rootPath).Trim(); + return _sessionCookie; + } + + // Try to load from current directory + if (File.Exists(ConfigFilePath)) + { + _sessionCookie = File.ReadAllText(ConfigFilePath).Trim(); + return _sessionCookie; + } + + // Try environment variable + _sessionCookie = Environment.GetEnvironmentVariable("AOC_SESSION"); + return _sessionCookie; + } + + public static void SetSessionCookie(string sessionCookie) + { + _sessionCookie = sessionCookie; + + // Save to file in project root + var rootPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..", ConfigFilePath); + var directory = Path.GetDirectoryName(rootPath); + if (directory != null && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + File.WriteAllText(rootPath, sessionCookie); + Console.WriteLine($"Session cookie saved to: {Path.GetFullPath(rootPath)}"); + } + + public static bool HasSessionCookie() + { + return !string.IsNullOrWhiteSpace(GetSessionCookie()); + } + } +} diff --git a/AoC/Solutions/DayTemplate.cs.template b/AoC/Solutions/DayTemplate.cs.template new file mode 100644 index 0000000..afd33c0 --- /dev/null +++ b/AoC/Solutions/DayTemplate.cs.template @@ -0,0 +1,48 @@ +namespace AoC.Solutions._YYYY +{ + /// + /// Advent of Code YYYY - Day DD + /// TODO: Add problem title and link + /// + public class DayDD : AoCDay + { + public DayDD() + { + // TODO: Add test cases from the problem description + // Example: + // AddTestCase( + // input: "sample input from problem", + // expectedPart1: "expected answer for part 1", + // expectedPart2: "expected answer for part 2" + // ); + } + + public override void RunPart1() + { + string[] data = GetInput(); + var answer = SolvePart1(data); + Console.WriteLine($"Answer: {answer}"); + } + + public override void RunPart2() + { + string[] data = GetInput(); + var answer = SolvePart2(data); + Console.WriteLine($"Answer: {answer}"); + } + + public override string? SolvePart1(string[] input) + { + // TODO: Implement solution for Part 1 + + return null; + } + + public override string? SolvePart2(string[] input) + { + // TODO: Implement solution for Part 2 + + return null; + } + } +} diff --git a/AoC/Solutions/_2025/Day01.cs b/AoC/Solutions/_2025/Day01.cs new file mode 100644 index 0000000..4358572 --- /dev/null +++ b/AoC/Solutions/_2025/Day01.cs @@ -0,0 +1,73 @@ +namespace AoC.Solutions._2025 +{ + /// + /// Example solution demonstrating the testing and automation features + /// This is a template - replace with actual problem logic + /// + public class Day01 : AoCDay + { + public Day01() + { + // Example: Add test cases from the problem description + // These will be run when using the --test flag + + // Simple example test case + AddTestCase( + input: "1\n2\n3", + expectedPart1: "6", // Sum of numbers + expectedPart2: "12" // Sum doubled + ); + + // You can add multiple test cases + AddTestCase( + input: "10\n20\n30", + expectedPart1: "60", + expectedPart2: "120" + ); + } + + public override void RunPart1() + { + string[] data = GetInput(); + var answer = SolvePart1(data); + Console.WriteLine($"Answer: {answer}"); + } + + public override void RunPart2() + { + string[] data = GetInput(); + var answer = SolvePart2(data); + Console.WriteLine($"Answer: {answer}"); + } + + public override string? SolvePart1(string[] input) + { + // Example solution: Sum all numbers + // Replace this with actual problem logic + int sum = 0; + foreach (var line in input) + { + if (int.TryParse(line.Trim(), out int num)) + { + sum += num; + } + } + return sum.ToString(); + } + + public override string? SolvePart2(string[] input) + { + // Example solution: Sum all numbers and double it + // Replace this with actual problem logic + int sum = 0; + foreach (var line in input) + { + if (int.TryParse(line.Trim(), out int num)) + { + sum += num; + } + } + return (sum * 2).ToString(); + } + } +} diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..2635fe9 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,156 @@ +# Quick Start Guide + +Get started with Advent of Code 2025 in under 5 minutes! + +## 1. Configure Your Session Cookie + +Choose one of these methods: + +### Option A: Interactive (Easiest) +```bash +dotnet run --project AoC -- --configure +``` +Then paste your session cookie when prompted. + +### Option B: Create File Manually +```bash +echo "your_session_cookie_here" > .aoc-session +``` + +### Option C: Environment Variable +```bash +export AOC_SESSION="your_session_cookie_here" +``` + +**To get your session cookie:** +1. Log in to [adventofcode.com](https://adventofcode.com) +2. Open browser DevTools (F12) +3. Go to Application → Cookies → `https://adventofcode.com` +4. Copy the value of the `session` cookie + +## 2. Create Your Solution + +Create a new file `AoC/Solutions/_2025/DayXX.cs`: + +```csharp +namespace AoC.Solutions._2025 +{ + public class Day01 : AoCDay + { + public Day01() + { + // Add test cases from the problem + AddTestCase( + input: "sample input", + expectedPart1: "expected answer 1", + expectedPart2: "expected answer 2" + ); + } + + public override void RunPart1() + { + string[] data = GetInput(); + var answer = SolvePart1(data); + Console.WriteLine($"Answer: {answer}"); + } + + public override void RunPart2() + { + string[] data = GetInput(); + var answer = SolvePart2(data); + Console.WriteLine($"Answer: {answer}"); + } + + public override string? SolvePart1(string[] input) + { + // Your solution here + return null; + } + + public override string? SolvePart2(string[] input) + { + // Your solution here + return null; + } + } +} +``` + +## 3. Run Your Solution + +```bash +# With tests (recommended) +dotnet run --project AoC -- -y 2025 -d 1 -t + +# With submission +dotnet run --project AoC -- -y 2025 -d 1 -t -s +``` + +The framework will: +1. ✅ Run your test cases +2. ✅ Fetch the input automatically (if not cached) +3. ✅ Execute your solution +4. ✅ Optionally submit your answer + +## Common Commands + +```bash +# Run today's puzzle +dotnet run --project AoC + +# Run specific day +dotnet run --project AoC -- -y 2025 -d 1 + +# Run with tests +dotnet run --project AoC -- -y 2025 -d 1 -t + +# Run with tests and submission +dotnet run --project AoC -- -y 2025 -d 1 -t -s + +# Show help +dotnet run --project AoC -- --help +``` + +## Tips + +- **Always test first**: Use `-t` to validate on sample data +- **Input is cached**: Once fetched, input is saved locally +- **Tests are optional**: But highly recommended! +- **Submission is interactive**: You'll be prompted before submitting + +## What Happens When You Run? + +1. **Tests Run** (if `-t` flag): Validates your solution on sample data +2. **Input Fetching**: + - Checks local cache first + - Downloads from adventofcode.com if not found + - Saves to `AoC/Input/_YYYY/DD.txt` +3. **Solution Execution**: Runs your code on the real input +4. **Submission** (if `-s` flag): + - Prompts you to confirm + - Submits to adventofcode.com + - Shows result (correct/incorrect/rate limited) + +## Troubleshooting + +### "Session cookie not configured" +→ Run `dotnet run --project AoC -- --configure` + +### "No input found" +→ Make sure your session cookie is valid + +### Tests fail +→ Check your solution logic against the sample input + +### "Day X not available yet" +→ Puzzles unlock at midnight EST + +## Next Steps + +See the full [README.md](README.md) for: +- Detailed API documentation +- Advanced usage examples +- Security best practices +- Project structure details + +Happy coding! 🎄⭐ diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b75cfa --- /dev/null +++ b/README.md @@ -0,0 +1,305 @@ +# Advent of Code Solutions + +A C# template and automation framework for [Advent of Code](https://adventofcode.com/) challenges. + +## Features + +✨ **Automated Input Fetching** - Automatically downloads puzzle input from adventofcode.com using your session cookie +🧪 **Built-in Testing** - Test your solutions with sample data before running on real input +📤 **Automated Submission** - Submit answers directly from the CLI and get instant feedback +🎯 **Clean Structure** - Organized solution files by year and day +⚡ **Fast Workflow** - Quick iteration with minimal boilerplate + +## Prerequisites + +- .NET 6.0 or later +- An Advent of Code account + +## Setup + +### 1. Clone the Repository + +```bash +git clone https://github.com/Tim567/AdventOfCode.git +cd AdventOfCode +``` + +### 2. Configure Your Session Cookie + +Your session cookie is required to fetch puzzle inputs and submit answers. You can configure it in three ways: + +#### Option A: Using the CLI (Recommended) +```bash +dotnet run --project AoC -- --configure +``` + +Then paste your session cookie when prompted. + +#### Option B: Manual File Creation +Create a file named `.aoc-session` in the project root and paste your session cookie: +```bash +echo "your_session_cookie_here" > .aoc-session +``` + +#### Option C: Environment Variable +Set the `AOC_SESSION` environment variable: +```bash +export AOC_SESSION="your_session_cookie_here" +``` + +### Getting Your Session Cookie + +1. Log in to [adventofcode.com](https://adventofcode.com) +2. Open browser DevTools (press F12) +3. Go to **Application** tab (Chrome) or **Storage** tab (Firefox) +4. Navigate to **Cookies** → `https://adventofcode.com` +5. Copy the value of the `session` cookie + +**⚠️ Important:** Never commit your session cookie to version control. The `.aoc-session` file is already in `.gitignore`. + +## Usage + +### Running Solutions + +```bash +# Run today's puzzle (uses current date) +dotnet run --project AoC + +# Run a specific day +dotnet run --project AoC -- --year 2025 --day 1 + +# Short form +dotnet run --project AoC -- -y 2025 -d 1 +``` + +### Running with Tests + +```bash +# Run with test cases (validates your solution on sample data) +dotnet run --project AoC -- -y 2025 -d 1 --test + +# Short form +dotnet run --project AoC -- -y 2025 -d 1 -t +``` + +### Automated Submission + +```bash +# Run with interactive submission prompts +dotnet run --project AoC -- -y 2025 -d 1 --submit + +# Run with tests and submission +dotnet run --project AoC -- -y 2025 -d 1 -t -s +``` + +### All Options + +```bash +dotnet run --project AoC -- --help +``` + +Options: +- `-y, --year ` - Specify the year (default: current year) +- `-d, --day ` - Specify the day (default: current day) +- `-t, --test` - Run test cases before solving +- `-s, --submit` - Enable interactive answer submission +- `-c, --configure` - Configure session cookie +- `-h, --help` - Show help message + +## Creating a New Solution + +### Step 1: Create the Solution File + +You can use the provided template or create from scratch. + +**Option A: Copy the Template** +```bash +# Copy template and rename +cp AoC/Solutions/DayTemplate.cs.template AoC/Solutions/_2025/Day01.cs + +# Update: +# - Replace _YYYY with _2025 +# - Replace DayDD with Day01 +# - Add your solution logic +``` + +**Option B: Create from Scratch** + +Create a new file at `AoC/Solutions/_YYYY/DayDD.cs` (replace YYYY with year, DD with zero-padded day): + +```csharp +namespace AoC.Solutions._2025 +{ + public class Day01 : AoCDay + { + public Day01() + { + // Add test cases from the problem description + AddTestCase( + input: "sample input from problem", + expectedPart1: "expected answer for part 1", + expectedPart2: "expected answer for part 2" + ); + } + + public override void RunPart1() + { + string[] data = GetInput(); + var answer = SolvePart1(data); + Console.WriteLine($"Answer: {answer}"); + } + + public override void RunPart2() + { + string[] data = GetInput(); + var answer = SolvePart2(data); + Console.WriteLine($"Answer: {answer}"); + } + + public override string? SolvePart1(string[] input) + { + // TODO: Implement solution for Part 1 + return null; + } + + public override string? SolvePart2(string[] input) + { + // TODO: Implement solution for Part 2 + return null; + } + } +} +``` + +### Step 2: Run Your Solution + +```bash +# This will automatically fetch the input if not present +dotnet run --project AoC -- -y 2025 -d 1 -t +``` + +The framework will: +1. ✅ Automatically fetch and cache the puzzle input +2. ✅ Run your test cases to validate the solution +3. ✅ Execute your solution on the real input +4. ✅ Optionally submit your answer + +## Workflow Example + +Here's a typical workflow for solving a new puzzle: + +```bash +# 1. Create your solution file (see template above) +# AoC/Solutions/_2025/Day01.cs + +# 2. Run with tests to validate on sample data +dotnet run --project AoC -- -y 2025 -d 1 -t + +# 3. Once tests pass, run with submission enabled +dotnet run --project AoC -- -y 2025 -d 1 -s + +# 4. The program will prompt you to submit each answer +# Answer: 42 +# Submit answer '42' for Part 1? (y/n) +``` + +## Project Structure + +``` +AdventOfCode/ +├── .aoc-session # Your session cookie (DO NOT COMMIT) +├── AoC/ +│ ├── Solutions/ +│ │ ├── _2022/ # Solutions for 2022 +│ │ ├── _2023/ # Solutions for 2023 +│ │ ├── _2024/ # Solutions for 2024 +│ │ └── _2025/ # Solutions for 2025 +│ ├── Input/ +│ │ ├── _2022/ # Cached inputs for 2022 +│ │ ├── _2023/ # Cached inputs for 2023 +│ │ ├── _2024/ # Cached inputs for 2024 +│ │ └── _2025/ # Cached inputs for 2025 +│ ├── Services/ +│ │ ├── AoCClient.cs # HTTP client for AoC API +│ │ └── AoCConfig.cs # Configuration management +│ ├── Interface/ +│ │ └── AoCDay.cs # Base class for solutions +│ └── Program.cs # CLI entry point +└── README.md +``` + +## Testing Framework + +The framework includes a simple testing system: + +```csharp +// In your solution constructor +public Day01() +{ + // Add a test case with expected answers + AddTestCase( + input: "sample input", + expectedPart1: "expected result 1", + expectedPart2: "expected result 2" + ); + + // You can add multiple test cases + AddTestCase( + input: "another test", + expectedPart1: "another result" + // expectedPart2 is optional + ); +} +``` + +Run with `-t` flag to execute tests before running on real input. + +## Submission Results + +When you submit an answer, the framework will: +- ✅ **Correct Answer**: Display success message and unlock the next part +- ❌ **Incorrect Answer**: Show the error message and let you try again +- ⏳ **Rate Limited**: Display wait time before you can submit again +- ℹ️ **Already Completed**: Inform you the puzzle is already solved + +## Best Practices + +1. **Always test first**: Use the `-t` flag to validate on sample data +2. **Review your answer**: Check the output before submitting +3. **Don't spam submissions**: The API has rate limiting +4. **Keep inputs cached**: The framework caches inputs to avoid unnecessary requests +5. **Never commit secrets**: The `.aoc-session` file is gitignored + +## Security + +- ⚠️ **Never commit your session cookie** - It's equivalent to your password +- ✅ The `.aoc-session` file is in `.gitignore` +- ✅ Use environment variables on shared machines +- ✅ Treat your session cookie like a password + +## Troubleshooting + +### "Session cookie not configured" +- Make sure you've run `dotnet run --project AoC -- --configure` +- Or create a `.aoc-session` file with your cookie + +### "Day X not available yet" +- The puzzle may not be released yet (puzzles unlock at midnight EST) +- Check the date on adventofcode.com + +### "Failed to fetch input" +- Your session cookie may have expired - log in again and get a new one +- Check your internet connection + +## Contributing + +Feel free to submit issues or pull requests to improve the framework! + +## Acknowledgments + +- Inspired by [advent-of-code-data](https://github.com/wimglenn/advent-of-code-data) +- Built for [Advent of Code](https://adventofcode.com/) by Eric Wastl + +## License + +This is a personal solutions repository. The Advent of Code problems and text are the property of [Advent of Code](https://adventofcode.com/).