refactor: extract grid abstraction for reuse

This commit is contained in:
Stevan Freeborn
2025-12-07 21:25:34 -06:00
parent d185ffda50
commit 4ca54ebb63
9 changed files with 418 additions and 131 deletions
+60 -67
View File
@@ -1,104 +1,97 @@
package grid
import (
"fmt"
"github.com/StevanFreeborn/advent-of-code-2025/internal/move"
"github.com/StevanFreeborn/advent-of-code-2025/internal/position"
)
type Grid interface {
Positions() map[Position]string
Walk()
NumberOfRows() int
NumberOfColumns() int
InBounds(position.Position) bool
GetValueAt(position.Position) string
GetSameNeighborsOf(position.Position, []move.Move) []position.Position
}
type grid struct {
positions map[Position]string
numberOfRows int
numberOfColumns int
positions map[position.Position]string
}
func NewGrid(input []string) Grid {
func From(input []string) Grid {
numberOfRows := len(input)
numberOfColumns := len(input[0])
positions := map[Position]string{}
positions := map[position.Position]string{}
for row := range numberOfRows {
for column := range numberOfColumns {
value := string(input[row][column])
pos := NewPosition(row, column)
pos := position.From(row, column)
positions[pos] = value
}
}
return grid{
positions: positions,
numberOfRows: numberOfRows,
numberOfColumns: numberOfColumns,
positions: positions,
}
}
func (g grid) Positions() map[Position]string {
positions := map[Position]string{}
func (g grid) NumberOfRows() int {
return g.numberOfRows
}
for key, value := range g.positions {
newPosition := NewPosition(key.Row(), key.Column())
positions[newPosition] = value
func (g grid) NumberOfColumns() int {
return g.numberOfColumns
}
func (g grid) InBounds(pos position.Position) bool {
if pos.Row() < 0 || pos.Row() >= g.numberOfRows {
return false
}
return positions
}
type Position interface {
Column() int
Row() int
}
type position struct {
column int
row int
}
func NewPosition(row int, column int) Position {
return position{
row: row,
column: column,
if pos.Column() < 0 || pos.Column() >= g.numberOfColumns {
return false
}
return true
}
func (p position) Row() int {
return p.row
func (g grid) GetValueAt(pos position.Position) string {
return g.positions[pos]
}
func (p position) Column() int {
return p.column
}
func (g grid) GetSameNeighborsOf(pos position.Position, moves []move.Move) []position.Position {
similarNeighbors := []position.Position{}
originalValue := g.GetValueAt(pos)
type Move interface {
NumberOfRows() int
NumberOfColumns() int
}
fmt.Println("Moves to check:", moves)
for _, m := range moves {
postionRow := pos.Row()
positionColumn := pos.Column()
type move struct {
numberOfRows int
numberOfColumns int
}
fmt.Println("Checking move:", m, "from position:", pos)
fmt.Println("Move rows:", m.NumberOfRows(), "columns:", m.NumberOfColumns())
neighborRow := postionRow + m.NumberOfRows()
neighborColumn := positionColumn + m.NumberOfColumns()
neighborPos := position.From(neighborRow, neighborColumn)
func (m move) NumberOfRows() int {
return m.numberOfColumns
}
if g.InBounds(neighborPos) == false {
continue
}
func (m move) NumberOfColumns() int {
return m.numberOfColumns
}
neighborValue := g.GetValueAt(neighborPos)
var (
Up = move{numberOfRows: -1, numberOfColumns: 0}
Down = move{numberOfRows: 1, numberOfColumns: 0}
Right = move{numberOfRows: 0, numberOfColumns: 1}
Left = move{numberOfRows: 0, numberOfColumns: -1}
UpRight = move{numberOfRows: -1, numberOfColumns: 1}
UpLeft = move{numberOfRows: -1, numberOfColumns: -1}
DownRight = move{numberOfRows: 1, numberOfColumns: 1}
DownLeft = move{numberOfRows: 1, numberOfColumns: -1}
)
if neighborValue != originalValue {
continue
}
var AllMoves = []Move{
Up,
Down,
Right,
Left,
UpRight,
UpLeft,
DownRight,
DownLeft,
similarNeighbors = append(similarNeighbors, neighborPos)
}
return similarNeighbors
}
-10
View File
@@ -1,10 +0,0 @@
..........
..........
..........
..........
..........
..........
..........
..........
..........
..........
+151
View File
@@ -1 +1,152 @@
package grid_test
import (
"testing"
"github.com/StevanFreeborn/advent-of-code-2025/internal/grid"
"github.com/StevanFreeborn/advent-of-code-2025/internal/move"
"github.com/StevanFreeborn/advent-of-code-2025/internal/position"
)
func TestFrom(t *testing.T) {
expectedNumberOfRows := 3
expectedNumberOfColumns := 3
expectedPositions := map[position.Position]string{
position.From(0, 0): "@",
position.From(0, 1): ".",
position.From(0, 2): ".",
position.From(1, 0): ".",
position.From(1, 1): "@",
position.From(1, 2): ".",
position.From(2, 0): ".",
position.From(2, 1): ".",
position.From(2, 2): "@",
}
input := []string{
"@..",
".@.",
"..@",
}
g := grid.From(input)
if g.NumberOfRows() != expectedNumberOfRows {
t.Errorf("expected number of rows to be %d, got %d", expectedNumberOfRows, g.NumberOfRows())
}
if g.NumberOfColumns() != expectedNumberOfColumns {
t.Errorf("expected number of columns to be %d, got %d", expectedNumberOfColumns, g.NumberOfColumns())
}
for pos, expectedValue := range expectedPositions {
value := g.GetValueAt(pos)
if value != expectedValue {
t.Errorf("expected value at position (%d, %d) to be %s, got %s", pos.Row(), pos.Column(), expectedValue, value)
}
}
}
func TestInBounds(t *testing.T) {
tests := []struct {
pos position.Position
expected bool
}{
{position.From(0, 0), true},
{position.From(1, 1), true},
{position.From(2, 2), true},
{position.From(-1, 0), false},
{position.From(0, -1), false},
{position.From(3, 0), false},
{position.From(0, 3), false},
}
input := []string{
"@..",
".@.",
"..@",
}
g := grid.From(input)
for _, test := range tests {
result := g.InBounds(test.pos)
if result != test.expected {
t.Errorf("expected InBounds(%v) to be %v, got %v", test.pos, test.expected, result)
}
}
}
func TestGetValueAt(t *testing.T) {
tests := []struct {
pos position.Position
expected string
}{
{position.From(0, 0), "@"},
{position.From(0, 1), "."},
{position.From(1, 1), "@"},
{position.From(2, 2), "@"},
}
input := []string{
"@..",
".@.",
"..@",
}
g := grid.From(input)
for _, test := range tests {
result := g.GetValueAt(test.pos)
if result != test.expected {
t.Errorf("expected GetValueAt(%v) to be %s, got %s", test.pos, test.expected, result)
}
}
}
func TestGetSameNeighborsOf(t *testing.T) {
input := []string{
"@..",
".@.",
"..@",
}
g := grid.From(input)
tests := []struct {
pos position.Position
expected []position.Position
}{
{
position.From(1, 1),
[]position.Position{
position.From(0, 0),
position.From(2, 2),
},
},
{
position.From(0, 0),
[]position.Position{
position.From(1, 1),
},
},
}
for _, test := range tests {
result := g.GetSameNeighborsOf(test.pos, move.AllDirections)
if len(result) != len(test.expected) {
t.Errorf("expected GetSameNeighborsOf(%v) to return %d neighbors, got %d", test.pos, len(test.expected), len(result))
continue
}
for i, expectedPos := range test.expected {
if result[i] != expectedPos {
t.Errorf("expected neighbor at index %d to be %v, got %v", i, expectedPos, result[i])
}
}
}
}
+36
View File
@@ -0,0 +1,36 @@
// Package move provides types and functions to represent and manipulate moves on a 2D grid.
package move
type Move interface {
NumberOfRows() int
NumberOfColumns() int
}
type move struct {
numberOfRows int
numberOfColumns int
}
func (m move) NumberOfRows() int {
return m.numberOfRows
}
func (m move) NumberOfColumns() int {
return m.numberOfColumns
}
var (
Up = move{numberOfRows: -1, numberOfColumns: 0}
Down = move{numberOfRows: 1, numberOfColumns: 0}
Right = move{numberOfRows: 0, numberOfColumns: 1}
Left = move{numberOfRows: 0, numberOfColumns: -1}
cardinal = []Move{Up, Down, Right, Left}
UpRight = move{numberOfRows: -1, numberOfColumns: 1}
UpLeft = move{numberOfRows: -1, numberOfColumns: -1}
DownRight = move{numberOfRows: 1, numberOfColumns: 1}
DownLeft = move{numberOfRows: 1, numberOfColumns: -1}
ordinal = []Move{UpRight, UpLeft, DownRight, DownLeft}
AllDirections = append(cardinal, ordinal...)
)
+91
View File
@@ -0,0 +1,91 @@
package move_test
import (
"testing"
"github.com/StevanFreeborn/advent-of-code-2025/internal/move"
)
func TestNumberOfRows(t *testing.T) {
tests := []struct {
move move.Move
expected int
}{
{move.Up, -1},
{move.Down, 1},
{move.Right, 0},
{move.Left, 0},
{move.UpRight, -1},
{move.UpLeft, -1},
{move.DownRight, 1},
{move.DownLeft, 1},
}
for _, test := range tests {
if test.move.NumberOfRows() != test.expected {
t.Errorf("expected number of rows for move to be %d, got %d", test.expected, test.move.NumberOfRows())
}
}
}
func TestNumberOfColumns(t *testing.T) {
tests := []struct {
move move.Move
expected int
}{
{move.Up, 0},
{move.Down, 0},
{move.Right, 1},
{move.Left, -1},
{move.UpRight, 1},
{move.UpLeft, -1},
{move.DownRight, 1},
{move.DownLeft, -1},
}
for _, test := range tests {
if test.move.NumberOfColumns() != test.expected {
t.Errorf("expected number of columns for move to be %d, got %d", test.expected, test.move.NumberOfColumns())
}
}
}
func TestAllDirectionsLength(t *testing.T) {
expectedLength := 8
if len(move.AllDirections) != expectedLength {
t.Errorf("expected AllDirections length to be %d, got %d", expectedLength, len(move.AllDirections))
}
}
func TestDefinedMoves(t *testing.T) {
definedMoves := []struct {
name string
move move.Move
expectedRows int
expectedColumns int
}{
{"Up", move.Up, -1, 0},
{"Down", move.Down, 1, 0},
{"Right", move.Right, 0, 1},
{"Left", move.Left, 0, -1},
{"UpRight", move.UpRight, -1, 1},
{"UpLeft", move.UpLeft, -1, -1},
{"DownRight", move.DownRight, 1, 1},
{"DownLeft", move.DownLeft, 1, -1},
}
for _, dm := range definedMoves {
if dm.move == nil {
t.Errorf("expected move %s to be defined, got nil", dm.name)
}
if dm.move.NumberOfRows() != dm.expectedRows {
t.Errorf("expected move %s to have %d rows, got %d", dm.name, dm.expectedRows, dm.move.NumberOfRows())
}
if dm.move.NumberOfColumns() != dm.expectedColumns {
t.Errorf("expected move %s to have %d columns, got %d", dm.name, dm.expectedColumns, dm.move.NumberOfColumns())
}
}
}
+27
View File
@@ -0,0 +1,27 @@
// Package position provides types and functions to represent and manipulate positions and moves on a 2D grid.
package position
type Position interface {
Row() int
Column() int
}
type position struct {
row int
column int
}
func From(row int, column int) Position {
return position{
row: row,
column: column,
}
}
func (p position) Row() int {
return p.row
}
func (p position) Column() int {
return p.column
}
+22
View File
@@ -0,0 +1,22 @@
package position_test
import (
"testing"
"github.com/StevanFreeborn/advent-of-code-2025/internal/position"
)
func TestFrom(t *testing.T) {
row := 3
column := 5
pos := position.From(row, column)
if pos.Row() != row {
t.Errorf("expected row to be %d, got %d", row, pos.Row())
}
if pos.Column() != column {
t.Errorf("expected column to be %d, got %d", column, pos.Column())
}
}