+100
cmd/one/main.go
+100
cmd/one/main.go
···
1
+
package main
2
+
3
+
import (
4
+
"fmt"
5
+
"strconv"
6
+
"strings"
7
+
8
+
"tangled.org/evan.jarrett.net/aoc2025/internal/puzzle"
9
+
)
10
+
11
+
type DayOne struct {
12
+
rotations []int
13
+
}
14
+
15
+
func (d *DayOne) ParseInput(input string) error {
16
+
for line := range strings.SplitSeq(strings.TrimSpace(input), "\n") {
17
+
line = strings.TrimSpace(line)
18
+
if line == "" {
19
+
continue
20
+
}
21
+
line = strings.Replace(strings.Replace(line, "L", "-", 1), "R", "", 1)
22
+
n, err := strconv.Atoi(line)
23
+
if err != nil {
24
+
return fmt.Errorf("invalid rotation: %s, %w", line, err)
25
+
}
26
+
d.rotations = append(d.rotations, n)
27
+
}
28
+
return nil
29
+
}
30
+
31
+
func (d *DayOne) Part1() (int, error) {
32
+
position := 50
33
+
zeroCount := 0
34
+
fmt.Printf("The dial starts by pointing at %d.\n", position)
35
+
36
+
for _, rot := range d.rotations {
37
+
newPosition, _, err := Rotate(position, rot)
38
+
if err != nil {
39
+
return 0, err
40
+
}
41
+
fmt.Printf("The dial is rotated %d to point at %d.\n", rot, newPosition)
42
+
position = newPosition
43
+
if position == 0 {
44
+
zeroCount++
45
+
}
46
+
}
47
+
48
+
fmt.Printf("Final position: %d\n", position)
49
+
return zeroCount, nil
50
+
}
51
+
52
+
func (d *DayOne) Part2() (int, error) {
53
+
position := 50
54
+
zeroCount := 0
55
+
fmt.Printf("The dial starts by pointing at %d.\n", position)
56
+
57
+
for _, rot := range d.rotations {
58
+
newPosition, passes, err := Rotate(position, rot)
59
+
if err != nil {
60
+
return 0, err
61
+
}
62
+
fmt.Printf("The dial is rotated %d to point at %d.", rot, newPosition)
63
+
if passes > 0 {
64
+
if newPosition != 0 || passes > 2 {
65
+
fmt.Printf(" during this rotation, it points at 0 %d times.", passes)
66
+
}
67
+
zeroCount += passes
68
+
}
69
+
fmt.Println()
70
+
fmt.Printf("count is now: %d\n", zeroCount)
71
+
position = newPosition
72
+
}
73
+
74
+
fmt.Printf("Final position: %d\n", position)
75
+
return zeroCount, nil
76
+
}
77
+
78
+
func Rotate(position int, amount int) (newPosition int, zeroCount int, err error) {
79
+
if position < 0 || position > 99 {
80
+
return 0, 0, fmt.Errorf("position must be between 0 and 99, got %d", position)
81
+
}
82
+
83
+
end := position + amount
84
+
newPosition = ((end % 100) + 100) % 100
85
+
86
+
if amount < 0 && end <= 0 {
87
+
zeroCount = -end / 100
88
+
if position != 0 {
89
+
zeroCount++
90
+
}
91
+
} else if amount > 0 && end >= 100 {
92
+
zeroCount = end / 100
93
+
}
94
+
95
+
return newPosition, zeroCount, nil
96
+
}
97
+
98
+
func main() {
99
+
puzzle.Run(1, &DayOne{})
100
+
}
+124
cmd/one/main_test.go
+124
cmd/one/main_test.go
···
1
+
package main
2
+
3
+
import (
4
+
"testing"
5
+
)
6
+
7
+
const testInput = `L68
8
+
L30
9
+
R48
10
+
L5
11
+
R60
12
+
L55
13
+
L1
14
+
L99
15
+
R14
16
+
L82`
17
+
18
+
func TestPart1(t *testing.T) {
19
+
d := &DayOne{}
20
+
if err := d.ParseInput(testInput); err != nil {
21
+
t.Fatalf("ParseInput failed: %v", err)
22
+
}
23
+
24
+
got, err := d.Part1()
25
+
if err != nil {
26
+
t.Fatalf("Part1 failed: %v", err)
27
+
}
28
+
29
+
want := 3
30
+
if got != want {
31
+
t.Errorf("Part1() = %d, want %d", got, want)
32
+
}
33
+
}
34
+
35
+
func TestPart2(t *testing.T) {
36
+
d := &DayOne{}
37
+
if err := d.ParseInput(testInput); err != nil {
38
+
t.Fatalf("ParseInput failed: %v", err)
39
+
}
40
+
41
+
got, err := d.Part2()
42
+
if err != nil {
43
+
t.Fatalf("Part2 failed: %v", err)
44
+
}
45
+
46
+
want := 6
47
+
if got != want {
48
+
t.Errorf("Part2() = %d, want %d", got, want)
49
+
}
50
+
}
51
+
52
+
func TestPart2LargeRotations(t *testing.T) {
53
+
tests := []struct {
54
+
input string
55
+
want int
56
+
}{
57
+
{"R555", 6},
58
+
{"L432", 4},
59
+
{"L555", 6},
60
+
}
61
+
62
+
for _, tt := range tests {
63
+
t.Run(tt.input, func(t *testing.T) {
64
+
d := &DayOne{}
65
+
if err := d.ParseInput(tt.input); err != nil {
66
+
t.Fatalf("ParseInput failed: %v", err)
67
+
}
68
+
69
+
got, err := d.Part2()
70
+
if err != nil {
71
+
t.Fatalf("Part2 failed: %v", err)
72
+
}
73
+
74
+
if got != tt.want {
75
+
t.Errorf("Part2(%s) = %d, want %d", tt.input, got, tt.want)
76
+
}
77
+
})
78
+
}
79
+
}
80
+
81
+
func TestRotate(t *testing.T) {
82
+
tests := []struct {
83
+
name string
84
+
position int
85
+
amount int
86
+
wantPos int
87
+
wantCount int
88
+
}{
89
+
// Starting at 0, no crossing
90
+
{"from 0, R50", 0, 50, 50, 0},
91
+
{"from 0, L50", 0, -50, 50, 0},
92
+
// Starting at 0, multiple rotations (end on 0, passed through once)
93
+
{"from 0, R200", 0, 200, 0, 2}, // pass 100, land 200
94
+
{"from 0, L200", 0, -200, 0, 2}, // pass -100, land -200
95
+
// Starting at 0, single rotation ending elsewhere
96
+
{"from 0, R150", 0, 150, 50, 1}, // pass 100
97
+
{"from 0, L150", 0, -150, 50, 1}, // pass -100
98
+
// Starting at 50, landing on 0
99
+
{"from 50, R50", 50, 50, 0, 1}, // land 100
100
+
{"from 50, L50", 50, -50, 0, 1}, // land 0
101
+
// Starting at 50, no crossing
102
+
{"from 50, R40", 50, 40, 90, 0},
103
+
{"from 50, L40", 50, -40, 10, 0},
104
+
// Starting at 50, large rotations (pass through multiple times)
105
+
{"from 50, R555", 50, 555, 5, 6}, // passes 100,200,300,400,500,600, lands on 5
106
+
{"from 50, L432", 50, -432, 18, 4}, // passes 0,-100,-200,-300, lands on 18
107
+
}
108
+
109
+
for _, tt := range tests {
110
+
t.Run(tt.name, func(t *testing.T) {
111
+
gotPos, gotCount, err := Rotate(tt.position, tt.amount)
112
+
if err != nil {
113
+
t.Fatalf("Rotate failed: %v", err)
114
+
}
115
+
116
+
if gotPos != tt.wantPos {
117
+
t.Errorf("Rotate(%d, %d) position = %d, want %d", tt.position, tt.amount, gotPos, tt.wantPos)
118
+
}
119
+
if gotCount != tt.wantCount {
120
+
t.Errorf("Rotate(%d, %d) count = %d, want %d", tt.position, tt.amount, gotCount, tt.wantCount)
121
+
}
122
+
})
123
+
}
124
+
}
+44
internal/input.go
+44
internal/input.go
···
1
+
package internal
2
+
3
+
import (
4
+
"fmt"
5
+
"io"
6
+
"net/http"
7
+
"os"
8
+
)
9
+
10
+
// GetInput fetches the Advent of Code input for the specified day.
11
+
// Requires AOC_SESSION environment variable to be set with your session cookie.
12
+
func GetInput(day int) (string, error) {
13
+
session := os.Getenv("AOC_SESSION")
14
+
if session == "" {
15
+
return "", fmt.Errorf("AOC_SESSION environment variable not set")
16
+
}
17
+
18
+
url := fmt.Sprintf("https://adventofcode.com/2025/day/%d/input", day)
19
+
20
+
req, err := http.NewRequest("GET", url, nil)
21
+
if err != nil {
22
+
return "", fmt.Errorf("failed to create request: %w", err)
23
+
}
24
+
25
+
req.AddCookie(&http.Cookie{Name: "session", Value: session})
26
+
27
+
client := &http.Client{}
28
+
resp, err := client.Do(req)
29
+
if err != nil {
30
+
return "", fmt.Errorf("failed to fetch input: %w", err)
31
+
}
32
+
defer resp.Body.Close()
33
+
34
+
if resp.StatusCode != http.StatusOK {
35
+
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
36
+
}
37
+
38
+
body, err := io.ReadAll(resp.Body)
39
+
if err != nil {
40
+
return "", fmt.Errorf("failed to read response body: %w", err)
41
+
}
42
+
43
+
return string(body), nil
44
+
}
+7
internal/puzzle/puzzle.go
+7
internal/puzzle/puzzle.go
+31
internal/puzzle/runner.go
+31
internal/puzzle/runner.go
···
1
+
package puzzle
2
+
3
+
import (
4
+
"fmt"
5
+
"log"
6
+
7
+
"tangled.org/evan.jarrett.net/aoc2025/internal"
8
+
)
9
+
10
+
func Run[T1 any, T2 any](day int, p Puzzle[T1, T2]) {
11
+
input, err := internal.GetInput(day)
12
+
if err != nil {
13
+
log.Fatalf("Failed to get input: %v", err)
14
+
}
15
+
16
+
if err := p.ParseInput(input); err != nil {
17
+
log.Fatalf("Failed to parse input: %v", err)
18
+
}
19
+
20
+
result1, err := p.Part1()
21
+
if err != nil {
22
+
log.Fatalf("Failed to solve part 1: %v", err)
23
+
}
24
+
fmt.Printf("Part 1: %v\n", result1)
25
+
26
+
result2, err := p.Part2()
27
+
if err != nil {
28
+
log.Fatalf("Failed to solve part 2: %v", err)
29
+
}
30
+
fmt.Printf("Part 2: %v\n", result2)
31
+
}
+77
newday.sh
+77
newday.sh
···
1
+
#!/bin/bash
2
+
3
+
set -e
4
+
5
+
if [ -z "$1" ]; then
6
+
echo "Usage: $0 <day>"
7
+
echo "Example: $0 5"
8
+
exit 1
9
+
fi
10
+
11
+
DAY=$1
12
+
13
+
if ! [[ "$DAY" =~ ^[0-9]+$ ]] || [ "$DAY" -lt 1 ] || [ "$DAY" -gt 12 ]; then
14
+
echo "Error: Day must be a number between 1 and 12"
15
+
exit 1
16
+
fi
17
+
18
+
declare -a WORDS=("" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine" "ten"
19
+
"eleven" "twelve")
20
+
21
+
DAY_WORD=${WORDS[$DAY]}
22
+
23
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+
CMD_DIR="$SCRIPT_DIR/cmd/$DAY_WORD"
25
+
26
+
# Capitalize first letter for struct name
27
+
DAY_WORD_CAP="$(echo "${DAY_WORD:0:1}" | tr '[:lower:]' '[:upper:]')${DAY_WORD:1}"
28
+
29
+
# Open browser to puzzle page
30
+
URL="https://adventofcode.com/2025/day/$DAY"
31
+
if command -v xdg-open &> /dev/null; then
32
+
xdg-open "$URL"
33
+
elif command -v open &> /dev/null; then
34
+
open "$URL"
35
+
elif command -v start &> /dev/null; then
36
+
start "$URL"
37
+
else
38
+
echo "Could not detect browser opener. Please visit: $URL"
39
+
fi
40
+
41
+
# Create directory and main.go
42
+
if [ -d "$CMD_DIR" ]; then
43
+
echo "Directory $CMD_DIR already exists"
44
+
else
45
+
mkdir -p "$CMD_DIR"
46
+
cat > "$CMD_DIR/main.go" << EOF
47
+
package main
48
+
49
+
import (
50
+
"tangled.org/evan.jarrett.net/aoc2025/internal/puzzle"
51
+
)
52
+
53
+
type Day${DAY_WORD_CAP} struct {
54
+
// parsed input fields
55
+
}
56
+
57
+
func (d *Day${DAY_WORD_CAP}) ParseInput(input string) error {
58
+
// TODO: parse input
59
+
return nil
60
+
}
61
+
62
+
func (d *Day${DAY_WORD_CAP}) Part1() (int, error) {
63
+
// TODO: solve part 1
64
+
return 0, nil
65
+
}
66
+
67
+
func (d *Day${DAY_WORD_CAP}) Part2() (int, error) {
68
+
// TODO: solve part 2
69
+
return 0, nil
70
+
}
71
+
72
+
func main() {
73
+
puzzle.Run($DAY, &Day${DAY_WORD_CAP}{})
74
+
}
75
+
EOF
76
+
echo "Created $CMD_DIR/main.go"
77
+
fi