Compare commits

...

15 Commits

Author SHA1 Message Date
Karma Riuk
8b0fa4c1f9 Moved RawMaze to reader package since it is used
mostly there
2023-08-05 16:36:49 +02:00
Karma Riuk
0e42c0f15d Reader -> Reader+Parser refactoring: COMPLETE
Added a string reader too so that one can create a maze just with a slice of stings and RawMaze now has chunks of bytes to limit memory usage with big mazes (hopefully)
2023-08-05 16:21:56 +02:00
Karma Riuk
6481fe2665 Added min function to utils to get min between two
comparable types (how isn't it in the STL?)
2023-08-05 16:15:54 +02:00
Karma Riuk
ee9d439485 Added isWall and isPath to RawMaze with tests 2023-08-05 12:02:42 +02:00
Karma Riuk
130deb40d8 Reader -> Reader+Parser refactoring: main.go now
uses the new structure
2023-08-05 12:01:29 +02:00
Karma Riuk
c8e517f73c Moved RaMaze to its own file, cuz we gonna need
some more functions and it would just clutter maze.go
2023-08-05 11:29:53 +02:00
Karma Riuk
be688e6920 Reader -> Reader+Parser refactoring: added tests
for new reader
2023-08-05 11:21:32 +02:00
Karma Riuk
435ea54343 Added String() method to RawMaze for debugging
purposes
2023-08-05 11:20:53 +02:00
Karma Riuk
929c5b58a0 Reader -> Reader+Parser refactoring: moved the
reading of the lines to its own function
2023-08-05 11:03:42 +02:00
Karma Riuk
ab6f85b7b6 Reader -> Reader+Parser refacfotring: create the
parser package, moved the parsing aspect of reader
to parser (still have some naughty stuff like
WallChar and PathChar in parser but it'll be fixed
in next commit)
2023-08-05 10:38:46 +02:00
Karma Riuk
e04aad4b77 Start of refactoring for reader+parser: read all
the lines before starting to parse
2023-08-05 10:01:34 +02:00
Karma Riuk
345d9267ad moved instructions around to make it more
consistent and fixed tiny bug
2023-08-05 09:38:12 +02:00
Karma Riuk
ff28832f72 Added missing logic to text reader and tested it 2023-08-04 23:53:34 +02:00
Karma Riuk
f46af33702 Added a new test case for text reader 2023-08-04 23:06:37 +02:00
Karma Riuk
c2d16a4d20 Removed useless print statements 2023-08-04 23:05:16 +02:00
15 changed files with 1109 additions and 176 deletions

View File

@ -0,0 +1,5 @@
### ###
### ###
# #
#### ##
#### ##

View File

@ -0,0 +1,5 @@
### ###
### ###
# #
##### #
##### #

2
go.mod
View File

@ -1,3 +1,5 @@
module maze-solver module maze-solver
go 1.20 go 1.20
require golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI=
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=

41
io/reader/raw_maze.go Normal file
View File

@ -0,0 +1,41 @@
package reader
import (
"strings"
)
const CHUNK_SIZE = 8 // size of a byte
type RawMaze struct {
Width, Height int
Data [][]byte
}
func (m *RawMaze) String() string {
var ret strings.Builder
ret.WriteString("{\n")
ret.WriteString("\tData: \n")
for _, line := range m.Data {
ret.WriteRune('\t')
ret.WriteRune('\t')
ret.Write(line) // TODO: prolly should fix this to make it readable
ret.WriteRune('\n')
}
ret.WriteString("}")
return ret.String()
}
func (m *RawMaze) IsPath(x int, y int) bool {
chunk_index := x / CHUNK_SIZE
chunk_rest := x % CHUNK_SIZE
chunk := m.Data[y][chunk_index]
return chunk&(1<<(CHUNK_SIZE-1-chunk_rest)) != 0
}
func (m *RawMaze) IsWall(x int, y int) bool {
chunk_index := x / CHUNK_SIZE
chunk_rest := x % CHUNK_SIZE
chunk := m.Data[y][chunk_index]
return chunk&(1<<(CHUNK_SIZE-1-chunk_rest)) == 0
}

220
io/reader/raw_maze_test.go Normal file
View File

@ -0,0 +1,220 @@
package reader
import "testing"
func TestRawMazeWall(t *testing.T) {
tests := []struct {
name string
width, height int
pathChar, wallChar byte
data []string
expected [][]bool
}{
{
"Trivial",
5, 3,
' ', '#',
[]string{
"## ##",
"# #",
"### #",
},
[][]bool{
{true, true, false, true, true},
{true, false, false, false, true},
{true, true, true, false, true},
},
},
{
"Trivial Bigger",
7, 5,
' ', '#',
[]string{
"### ###",
"### ###",
"# #",
"##### #",
"##### #",
},
[][]bool{
{true, true, true, false, true, true, true},
{true, true, true, false, true, true, true},
{true, false, false, false, false, false, true},
{true, true, true, true, true, false, true},
{true, true, true, true, true, false, true},
},
},
{
"Bigger Staggered",
7, 5,
' ', '#',
[]string{
"### ###",
"### ###",
"# #",
"#### ##",
"#### ##",
},
[][]bool{
{true, true, true, false, true, true, true},
{true, true, true, false, true, true, true},
{true, false, false, false, false, false, true},
{true, true, true, true, false, true, true},
{true, true, true, true, false, true, true},
},
},
{
"Normal",
11, 11,
' ', '#',
[]string{
"##### #####",
"# # #",
"##### ### #",
"# # #",
"# # ##### #",
"# # #",
"### ### # #",
"# # # #",
"# ####### #",
"# # #",
"##### #####",
},
[][]bool{
{true, true, true, true, true, false, true, true, true, true, true},
{true, false, false, false, false, false, true, false, false, false, true},
{true, true, true, true, true, false, true, true, true, false, true},
{true, false, false, false, true, false, false, false, false, false, true},
{true, false, true, false, true, true, true, true, true, false, true},
{true, false, true, false, false, false, false, false, false, false, true},
{true, true, true, false, true, true, true, false, true, false, true},
{true, false, false, false, true, false, false, false, true, false, true},
{true, false, true, true, true, true, true, true, true, false, true},
{true, false, false, false, false, false, true, false, false, false, true},
{true, true, true, true, true, false, true, true, true, true, true},
},
},
}
for _, test := range tests {
reader := StringsReader{
PathChar: test.pathChar,
WallChar: test.wallChar,
Lines: &test.data,
}
rawMaze, _ := reader.Read()
for y, row := range test.expected {
for x, expected := range row {
if rawMaze.IsWall(x, y) != expected {
t.Fatalf("%s: Wanted wall at (%v, %v), apparently it isn't", test.name, x, y)
}
}
}
}
}
func TestRawMazePath(t *testing.T) {
tests := []struct {
name string
width, height int
data [][]byte
expected [][]bool
}{
{
"Trivial",
5, 3,
[][]byte{
{0b_00100_000},
{0b_01110_000},
{0b_00010_000},
},
[][]bool{
{false, false, true, false, false},
{false, true, true, true, false},
{false, false, false, true, false},
},
},
{
"Trivial Bigger",
7, 5,
[][]byte{
{0b_0001000_0},
{0b_0001000_0},
{0b_0111110_0},
{0b_0000010_0},
{0b_0000010_0},
},
[][]bool{
{false, false, false, true, false, false, false},
{false, false, false, true, false, false, false},
{false, true, true, true, true, true, false},
{false, false, false, false, false, true, false},
{false, false, false, false, false, true, false},
},
},
{
"Bigger Staggered",
7, 5,
[][]byte{
{0b_0001000_0},
{0b_0001000_0},
{0b_0111110_0},
{0b_0000100_0},
{0b_0000100_0},
},
[][]bool{
{false, false, false, true, false, false, false},
{false, false, false, true, false, false, false},
{false, true, true, true, true, true, false},
{false, false, false, false, true, false, false},
{false, false, false, false, true, false, false},
},
},
{
"Normal",
11, 11,
[][]byte{
{0b_00000100, 0b000_00000},
{0b_01111101, 0b110_00000},
{0b_00000100, 0b010_00000},
{0b_01110111, 0b110_00000},
{0b_01010000, 0b010_00000},
{0b_01011111, 0b110_00000},
{0b_00010001, 0b010_00000},
{0b_01110111, 0b010_00000},
{0b_01000000, 0b010_00000},
{0b_01111101, 0b110_00000},
{0b_00000100, 0b000_00000},
},
[][]bool{
{false, false, false, false, false, true, false, false, false, false, false},
{false, true, true, true, true, true, false, true, true, true, false},
{false, false, false, false, false, true, false, false, false, true, false},
{false, true, true, true, false, true, true, true, true, true, false},
{false, true, false, true, false, false, false, false, false, true, false},
{false, true, false, true, true, true, true, true, true, true, false},
{false, false, false, true, false, false, false, true, false, true, false},
{false, true, true, true, false, true, true, true, false, true, false},
{false, true, false, false, false, false, false, false, false, true, false},
{false, true, true, true, true, true, false, true, true, true, false},
{false, false, false, false, false, true, false, false, false, false, false},
},
},
}
for _, test := range tests {
rawMaze := RawMaze{
Width: test.width,
Height: test.height,
Data: test.data,
}
for y, row := range test.expected {
for x, expected := range row {
if rawMaze.IsPath(x, y) != expected {
t.Fatalf("%s: Wanted path at (%v, %v), apparently it isn't", test.name, x, y)
}
}
}
}
}

View File

@ -1,7 +1,5 @@
package reader package reader
import "maze-solver/maze"
type Reader interface { type Reader interface {
Read(filename string) (*maze.Maze, error) Read() (*RawMaze, error)
} }

52
io/reader/strings.go Normal file
View File

@ -0,0 +1,52 @@
package reader
import (
"fmt"
"maze-solver/utils"
)
type StringsReader struct {
PathChar, WallChar byte
Lines *[]string
}
func (r *StringsReader) Read() (*RawMaze, error) {
width, height := len((*r.Lines)[0]), len(*r.Lines)
ret := &RawMaze{
Width: width,
Height: height,
Data: make([][]byte, height),
}
for i := 0; i < height; i++ {
ret.Data[i] = make([]byte, width/CHUNK_SIZE+1)
}
for y, line := range *r.Lines {
r.processLine(line, &ret.Data[y])
}
return ret, nil
}
func (r *StringsReader) processLine(line string, dest *[]byte) {
n_chunks := len(line)/CHUNK_SIZE + 1
if len(*dest) != n_chunks {
panic(fmt.Sprintf("The row that should receive the chunks does not have the correct length (%v, want %v)", len(*dest), n_chunks))
}
for i := 0; i < n_chunks; i++ {
var chunk byte = 0 // all walls
end_index := utils.Min((i+1)*CHUNK_SIZE, len(line))
for x, c := range line[i*CHUNK_SIZE : end_index] {
if c == rune(r.PathChar) {
chunk |= 1 << (CHUNK_SIZE - 1 - x)
}
}
(*dest)[i] = chunk
}
}

137
io/reader/strings_test.go Normal file
View File

@ -0,0 +1,137 @@
package reader
import (
"testing"
)
func TestStringsReader(t *testing.T) {
tests := []struct {
name string
width, height int
pathChar byte
wallChar byte
lines []string
expected [][]byte
}{
{
"Trivial",
5, 3,
' ',
'#',
[]string{
"## ##",
"# #",
"### #",
},
[][]byte{
{0b_00100_000},
{0b_01110_000},
{0b_00010_000},
},
},
{
"Trivial Bigger",
7, 5,
' ',
'#',
[]string{
"### ###",
"### ###",
"# #",
"##### #",
"##### #",
},
[][]byte{
{0b_0001000_0},
{0b_0001000_0},
{0b_0111110_0},
{0b_0000010_0},
{0b_0000010_0},
},
},
{
"Bigger Staggered",
7, 5,
' ',
'#',
[]string{
"### ###",
"### ###",
"# #",
"#### ##",
"#### ##",
},
[][]byte{
{0b_0001000_0},
{0b_0001000_0},
{0b_0111110_0},
{0b_0000100_0},
{0b_0000100_0},
},
},
{
"Normal",
11, 11,
' ',
'#',
[]string{
"##### #####",
"# # #",
"##### ### #",
"# # #",
"# # ##### #",
"# # #",
"### ### # #",
"# # # #",
"# ####### #",
"# # #",
"##### #####",
},
[][]byte{
{0b_00000100, 0b000_00000},
{0b_01111101, 0b110_00000},
{0b_00000100, 0b010_00000},
{0b_01110111, 0b110_00000},
{0b_01010000, 0b010_00000},
{0b_01011111, 0b110_00000},
{0b_00010001, 0b010_00000},
{0b_01110111, 0b010_00000},
{0b_01000000, 0b010_00000},
{0b_01111101, 0b110_00000},
{0b_00000100, 0b000_00000},
},
},
}
for _, test := range tests {
reader := StringsReader{
PathChar: test.pathChar,
WallChar: test.wallChar,
Lines: &test.lines,
}
got, _ := reader.Read()
assertEqual(t, got.Width, test.width, "%s: width of raw maze don't match", test.name)
assertEqual(t, got.Height, test.height, "%s: height of raw maze don't match", test.name)
assertEqual(t, len(got.Data), len(test.expected), "%s: don't have the same number of rows", test.name)
for y, line_exp := range test.expected {
line_got := got.Data[y]
assertEqual(t, len(line_got), len(line_exp), "%s (line %v): don't have same number of chunks, %v, want %v", test.name, y)
for i, chunk_exp := range line_exp {
chunk_got := line_got[i]
if chunk_got != chunk_exp {
t.Fatalf("%s (line %v): chunk %v don't coincide, %08b, want %08b", test.name, y, i, chunk_got, chunk_exp)
}
}
}
}
}
func assertEqual[T comparable](t *testing.T, got T, want T, msg string, args ...any) {
args = append(args, got, want)
if got != want {
t.Fatalf(msg+"\nGot: %v, Want: %v", args...)
}
}

View File

@ -2,21 +2,31 @@ package reader
import ( import (
"bufio" "bufio"
"fmt"
"maze-solver/maze"
"os" "os"
) )
type TextReader struct { type TextReader struct {
Filename string
PathChar, WallChar byte PathChar, WallChar byte
} }
func (r *TextReader) Read(filename string) (*maze.Maze, error) { func (r TextReader) Read() (*RawMaze, error) {
nodesByCoord := make(map[maze.Coordinates]*maze.Node) lines, err := getLines(r.Filename)
if err != nil {
return nil, err
}
strings_reader := StringsReader{
PathChar: r.PathChar,
WallChar: r.WallChar,
Lines: lines,
}
return strings_reader.Read()
}
func getLines(filename string) (*[]string, error) {
var lines []string var lines []string
ret := &maze.Maze{}
if _, err := os.Stat(filename); err != nil { if _, err := os.Stat(filename); err != nil {
return nil, err return nil, err
} }
@ -25,101 +35,15 @@ func (r *TextReader) Read(filename string) (*maze.Maze, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer file.Close()
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
y := 0
var line string
for scanner.Scan() { for scanner.Scan() {
line = scanner.Text() line := scanner.Text()
if len(lines) == 0 {
lines = make([]string, 0, len(line))
}
for x := 1; x < len(line)-1; x++ {
char := line[x]
var left_char, right_char, above_char byte
if y > 0 {
left_char = line[x-1]
right_char = line[x+1]
above_char = lines[y-1][x]
}
// Parse first line to get entrance
if y == 0 && char == r.PathChar {
coords := maze.Coordinates{X: x, Y: y}
node := maze.NewNode(coords)
ret.Nodes = append(ret.Nodes, node)
nodesByCoord[coords] = node
continue
}
// Parse middle of the maze
if y > 0 && char == r.PathChar &&
(left_char == r.WallChar && right_char == r.PathChar ||
left_char == r.PathChar && right_char == r.WallChar ||
above_char == r.PathChar && (left_char == r.PathChar || right_char == r.PathChar)) {
coords := maze.Coordinates{X: x, Y: y}
node := maze.NewNode(coords)
ret.Nodes = append(ret.Nodes, node)
nodesByCoord[coords] = node
r.lookupNeighbourAbove(&lines, node, &nodesByCoord)
if left_char == r.PathChar && right_char == r.WallChar ||
above_char == r.PathChar && (left_char == r.PathChar || right_char == r.PathChar) {
r.lookupNeighbourLeft(&line, node, &nodesByCoord)
}
}
}
lines = append(lines, line) lines = append(lines, line)
y++
}
y--
// Parse last line to get exit
for x, rune := range line {
char := byte(rune)
if char == r.PathChar {
fmt.Printf("last line number: %v\n", y)
coords := maze.Coordinates{X: x, Y: y}
node := maze.NewNode(coords)
r.lookupNeighbourAbove(&lines, node, &nodesByCoord)
ret.Nodes = append(ret.Nodes, node)
break
}
} }
return ret, nil return &lines, nil
}
func (r *TextReader) lookupNeighbourAbove(lines *[]string, node *maze.Node, nodesByCoord *map[maze.Coordinates]*maze.Node) {
for y := node.Coords.Y - 1; y >= 0; y-- {
if (*lines)[y][node.Coords.X] == r.WallChar {
break
}
neighbour, ok := (*nodesByCoord)[maze.Coordinates{X: node.Coords.X, Y: y}]
if ok {
node.Up = neighbour
neighbour.Down = node
}
}
}
func (r *TextReader) lookupNeighbourLeft(line *string, node *maze.Node, nodesByCoord *map[maze.Coordinates]*maze.Node) {
for x := node.Coords.X - 1; x > 0; x-- {
if (*line)[x] == r.WallChar {
panic(fmt.Sprintf("Found no node before wall while looking to the left at neighbours of node %v", node))
}
neighbour, ok := (*nodesByCoord)[maze.Coordinates{X: x, Y: node.Coords.Y}]
if ok {
node.Left = neighbour
fmt.Printf("Setting left of %v to %v\n", node.Coords, neighbour.Coords)
neighbour.Right = node
break
}
}
} }

View File

@ -1,81 +1,108 @@
package reader package reader
import ( import (
"fmt" // "maze-solver/maze"
"maze-solver/maze" // "maze-solver/utils"
"maze-solver/utils" // "reflect"
"testing" "testing"
) )
func TestTextRead(t *testing.T) { func TestTextReadTrivial(t *testing.T) {
/* trivial.txt /*
## ## tests := []struct {
# # name string
### # filename string
pathChar byte
wallChar byte
expected *maze.RawMaze
}{
{
"Trivial",
"../../assets/trivial.txt",
' ',
'#',
&maze.RawMaze{
PathChar: ' ',
WallChar: '#',
Data: []string{
"## ##",
"# #",
"### #",
},
},
},
{
"Trivial Bigger",
"../../assets/trivial-bigger.txt",
' ',
'#',
&maze.RawMaze{
PathChar: ' ',
WallChar: '#',
Data: []string{
"### ###",
"### ###",
"# #",
"##### #",
"##### #",
},
},
},
{
"Bigger Staggered",
"../../assets/trivial-bigger-staggered.txt",
' ',
'#',
&maze.RawMaze{
PathChar: ' ',
WallChar: '#',
Data: []string{
"### ###",
"### ###",
"# #",
"#### ##",
"#### ##",
},
},
},
{
"Normal",
"../../assets/normal.txt",
' ',
'#',
&maze.RawMaze{
PathChar: ' ',
WallChar: '#',
Data: []string{
"##### #####",
"# # #",
"##### ### #",
"# # #",
"# # ##### #",
"# # #",
"### ### # #",
"# # # #",
"# ####### #",
"# # #",
"##### #####",
},
},
},
}
Nodes are for _, test := range tests {
##0## reader := TextReader{
#123# Filename: test.filename,
###4# PathChar: test.pathChar,
WallChar: test.wallChar,
}
got, err := reader.Read()
utils.Check(err, "Couldn't read file %q", reader.Filename)
if !reflect.DeepEqual(got, test.expected) {
t.Fatalf("%s: lexed mazes do not match\nGot: %v\nWant: %v", test.name, got, test.expected)
}
}
*/ */
nodes := make([]*maze.Node, 5)
nodes[0] = maze.NewNode(maze.Coordinates{X: 2, Y: 0})
nodes[1] = maze.NewNode(maze.Coordinates{X: 1, Y: 1})
nodes[2] = maze.NewNode(maze.Coordinates{X: 2, Y: 1})
nodes[3] = maze.NewNode(maze.Coordinates{X: 3, Y: 1})
nodes[4] = maze.NewNode(maze.Coordinates{X: 3, Y: 2})
nodes[0].Down = nodes[2]
nodes[1].Right = nodes[2]
nodes[2].Up = nodes[0]
nodes[2].Left = nodes[1]
nodes[2].Right = nodes[3]
nodes[3].Left = nodes[2]
nodes[3].Down = nodes[4]
nodes[4].Up = nodes[3]
reader := TextReader{
PathChar: ' ',
WallChar: '#',
}
filename := "../../assets/trivial.txt"
got, err := reader.Read(filename)
utils.Check(err, "Couldn't create maze from %q", filename)
if len(nodes) != len(got.Nodes) {
t.Fatalf("Didn't get the same size of nodes: %v, want %v", len(got.Nodes), len(nodes))
}
for i, got := range got.Nodes {
fmt.Println(i)
expected := nodes[i]
checkNode(t, i, got, expected, "")
checkNode(t, i, got.Left, expected.Left, "left")
checkNode(t, i, got.Right, expected.Right, "Right")
checkNode(t, i, got.Up, expected.Up, "Up")
checkNode(t, i, got.Down, expected.Down, "Down")
}
}
func checkNode(t *testing.T, i int, got *maze.Node, expected *maze.Node, side string) {
if expected == nil {
return
}
if got == nil {
t.Fatalf("No %s node of %v, want %v", side, i, expected.Coords)
}
if got.Coords != expected.Coords {
t.Fatalf("Coords %s node of %v: %v, but want %v", side, i, got.Coords, expected.Coords)
}
} }

View File

@ -3,21 +3,21 @@ package main
import ( import (
"maze-solver/io/reader" "maze-solver/io/reader"
"maze-solver/io/writer" "maze-solver/io/writer"
"maze-solver/maze/parser"
"maze-solver/solver" "maze-solver/solver"
"maze-solver/utils" "maze-solver/utils"
) )
func main() { func main() {
input := "filename"
output := "filename" output := "filename"
reader := &reader.TextReader{PathChar: ' ', WallChar: '#'} reader := &reader.TextReader{Filename: "filename", PathChar: ' ', WallChar: '#'}
writer := &writer.ImageWriter{} writer := &writer.ImageWriter{}
solver := &solver.Bfs{} solver := &solver.Bfs{}
maze, err := reader.Read(input) maze, err := parser.Parse(reader)
utils.Check(err, "Couldn't read maze from %q", input) utils.Check(err, "Couldn't read maze from %q", reader.Filename)
solved := solver.Solve(maze) solved := solver.Solve(maze)
err = writer.Write(output, solved) err = writer.Write(output, solved)

125
maze/parser/parser.go Normal file
View File

@ -0,0 +1,125 @@
package parser
import (
"fmt"
"maze-solver/io/reader"
"maze-solver/maze"
)
func Parse(reader reader.Reader) (*maze.Maze, error) {
nodesByCoord := make(map[maze.Coordinates]*maze.Node)
ret := &maze.Maze{}
raw_maze, err := reader.Read()
if err != nil {
return nil, err
}
y := 0
// Parse first line to get entrance
for x := 0; x < raw_maze.Width-1; x++ {
if raw_maze.IsPath(x, y) {
coords := maze.Coordinates{X: x, Y: y}
node := maze.NewNode(coords)
ret.Nodes = append(ret.Nodes, node)
nodesByCoord[coords] = node
break
}
}
for y = 1; y < raw_maze.Height-1; y++ {
for x := 1; x < raw_maze.Width-1; x++ {
// Parse middle of the maze
if raw_maze.IsPath(x, y) &&
(raw_maze.IsWall(x-1, y) && raw_maze.IsPath(x+1, y) ||
raw_maze.IsPath(x-1, y) && raw_maze.IsWall(x+1, y) ||
raw_maze.IsPath(x, y-1) && (raw_maze.IsPath(x-1, y) || raw_maze.IsPath(x+1, y))) {
coords := maze.Coordinates{X: x, Y: y}
node := maze.NewNode(coords)
lookupNeighbourAbove(raw_maze, node, &nodesByCoord, ret)
ret.Nodes = append(ret.Nodes, node)
nodesByCoord[coords] = node
if raw_maze.IsPath(x-1, y) && raw_maze.IsWall(x+1, y) ||
raw_maze.IsPath(x, y-1) && (raw_maze.IsPath(x-1, y) || raw_maze.IsPath(x+1, y)) {
lookupNeighbourLeft(raw_maze, node, &nodesByCoord)
}
}
}
}
// Parse last line to get exit
for x := 0; x < raw_maze.Width-1; x++ {
if raw_maze.IsPath(x, y) {
coords := maze.Coordinates{X: x, Y: y}
node := maze.NewNode(coords)
lookupNeighbourAbove(raw_maze, node, &nodesByCoord, ret)
ret.Nodes = append(ret.Nodes, node)
break
}
}
return ret, nil
}
func lookupNeighbourAbove(raw_maze *reader.RawMaze, node *maze.Node, nodesByCoord *map[maze.Coordinates]*maze.Node, m *maze.Maze) {
for y := node.Coords.Y - 1; y >= 0; y-- {
neighbour, ok := (*nodesByCoord)[maze.Coordinates{X: node.Coords.X, Y: y}]
if ok {
node.Up = neighbour
neighbour.Down = node
break
}
if y > 0 && raw_maze.IsWall(node.Coords.X, y) {
y++
if y == node.Coords.Y {
break
}
coords := maze.Coordinates{X: node.Coords.X, Y: y}
new_node := maze.NewNode(coords)
lookupNeighbourLeft(raw_maze, new_node, nodesByCoord)
lookupNeighbourRight(raw_maze, new_node, nodesByCoord)
(*nodesByCoord)[coords] = new_node
m.Nodes = append(m.Nodes, new_node)
node.Up = new_node
new_node.Down = node
break
}
}
}
func lookupNeighbourLeft(raw_maze *reader.RawMaze, node *maze.Node, nodesByCoord *map[maze.Coordinates]*maze.Node) {
for x := node.Coords.X - 1; x > 0; x-- {
if raw_maze.IsWall(x, node.Coords.Y) && x < node.Coords.X-1 {
panic(fmt.Sprintf("Found no node before wall while looking to the left at neighbours of node %v", node))
}
neighbour, ok := (*nodesByCoord)[maze.Coordinates{X: x, Y: node.Coords.Y}]
if ok {
node.Left = neighbour
neighbour.Right = node
break
}
}
}
func lookupNeighbourRight(raw_maze *reader.RawMaze, node *maze.Node, nodesByCoord *map[maze.Coordinates]*maze.Node) {
for x := node.Coords.X + 1; x < raw_maze.Width; x++ {
if raw_maze.IsWall(x, node.Coords.Y) {
panic(fmt.Sprintf("Found no node before wall while looking to the right at neighbours of node %v", node))
}
neighbour, ok := (*nodesByCoord)[maze.Coordinates{X: x, Y: node.Coords.Y}]
if ok {
node.Right = neighbour
neighbour.Left = node
break
}
}
}

384
maze/parser/parser_test.go Normal file
View File

@ -0,0 +1,384 @@
package parser
import (
"fmt"
"maze-solver/io/reader"
"maze-solver/maze"
"maze-solver/utils"
"testing"
)
func TestTextReadTrivial(t *testing.T) {
fmt.Println("---------- Trivial ----------")
/* trivial.txt
## ##
# #
### #
Nodes are
##0##
#123#
###4#
*/
nodes := make([]*maze.Node, 5)
nodes[0] = maze.NewNode(maze.Coordinates{X: 2, Y: 0})
nodes[1] = maze.NewNode(maze.Coordinates{X: 1, Y: 1})
nodes[2] = maze.NewNode(maze.Coordinates{X: 2, Y: 1})
nodes[3] = maze.NewNode(maze.Coordinates{X: 3, Y: 1})
nodes[4] = maze.NewNode(maze.Coordinates{X: 3, Y: 2})
nodes[0].Down = nodes[2]
nodes[1].Right = nodes[2]
nodes[2].Up = nodes[0]
nodes[2].Left = nodes[1]
nodes[2].Right = nodes[3]
nodes[3].Left = nodes[2]
nodes[3].Down = nodes[4]
nodes[4].Up = nodes[3]
reader := reader.TextReader{
Filename: "../../assets/trivial.txt",
PathChar: ' ',
WallChar: '#',
}
got, err := Parse(reader)
utils.Check(err, "Couldn't create maze from %q", reader.Filename)
if len(nodes) != len(got.Nodes) {
t.Fatalf("Didn't get the same size of nodes: %v, want %v", len(got.Nodes), len(nodes))
}
for i, got := range got.Nodes {
expected := nodes[i]
checkNode(t, i, got, expected, "")
checkNode(t, i, got.Left, expected.Left, "left")
checkNode(t, i, got.Right, expected.Right, "Right")
checkNode(t, i, got.Up, expected.Up, "Up")
checkNode(t, i, got.Down, expected.Down, "Down")
}
}
func TestTextReadTrivialBigger(t *testing.T) {
fmt.Println("---------- Trivial Bigger ----------")
/* trivial-bigger.txt
### ###
### ###
# #
##### #
##### #
Nodes are
###0###
### ###
#1 2 3#
##### #
#####4#
*/
nodes := make([]*maze.Node, 5)
nodes[0] = maze.NewNode(maze.Coordinates{X: 3, Y: 0})
nodes[1] = maze.NewNode(maze.Coordinates{X: 1, Y: 2})
nodes[2] = maze.NewNode(maze.Coordinates{X: 3, Y: 2})
nodes[3] = maze.NewNode(maze.Coordinates{X: 5, Y: 2})
nodes[4] = maze.NewNode(maze.Coordinates{X: 5, Y: 4})
nodes[0].Down = nodes[2]
nodes[1].Right = nodes[2]
nodes[2].Up = nodes[0]
nodes[2].Left = nodes[1]
nodes[2].Right = nodes[3]
nodes[3].Left = nodes[2]
nodes[3].Down = nodes[4]
nodes[4].Up = nodes[3]
reader := reader.TextReader{
Filename: "../../assets/trivial-bigger.txt",
PathChar: ' ',
WallChar: '#',
}
got, err := Parse(reader)
utils.Check(err, "Couldn't create maze from %q", reader.Filename)
if len(nodes) != len(got.Nodes) {
t.Fatalf("Didn't get the same size of nodes: %v, want %v", len(got.Nodes), len(nodes))
}
for _, node := range got.Nodes {
fmt.Println(node)
}
for i, got := range got.Nodes {
expected := nodes[i]
checkNode(t, i, got, expected, "")
checkNode(t, i, got.Left, expected.Left, "left")
checkNode(t, i, got.Right, expected.Right, "Right")
checkNode(t, i, got.Up, expected.Up, "Up")
checkNode(t, i, got.Down, expected.Down, "Down")
}
}
func TestTextReadTrivialBiggerStaggered(t *testing.T) {
fmt.Println("---------- Trivial Staggered ----------")
/* trivial-bigger-staggered.txt
### ###
### ###
# #
#### ##
#### ##
Nodes are
###0###
### ###
#1 243#
#### ##
####5##
*/
nodes := make([]*maze.Node, 6)
nodes[0] = maze.NewNode(maze.Coordinates{X: 3, Y: 0})
nodes[1] = maze.NewNode(maze.Coordinates{X: 1, Y: 2})
nodes[2] = maze.NewNode(maze.Coordinates{X: 3, Y: 2})
nodes[3] = maze.NewNode(maze.Coordinates{X: 5, Y: 2})
nodes[4] = maze.NewNode(maze.Coordinates{X: 4, Y: 2})
nodes[5] = maze.NewNode(maze.Coordinates{X: 4, Y: 4})
nodes[0].Down = nodes[2]
nodes[1].Right = nodes[2]
nodes[2].Up = nodes[0]
nodes[2].Left = nodes[1]
nodes[2].Right = nodes[4]
nodes[3].Left = nodes[4]
nodes[4].Down = nodes[5]
nodes[4].Left = nodes[2]
nodes[4].Right = nodes[3]
nodes[5].Up = nodes[4]
reader := reader.TextReader{
Filename: "../../assets/trivial-bigger-staggered.txt",
PathChar: ' ',
WallChar: '#',
}
got, err := Parse(reader)
utils.Check(err, "Couldn't create maze from %q", reader.Filename)
if len(nodes) != len(got.Nodes) {
t.Fatalf("Didn't get the same size of nodes: %v, want %v", len(got.Nodes), len(nodes))
}
for _, node := range got.Nodes {
fmt.Println(node)
}
for i, got := range got.Nodes {
expected := nodes[i]
checkNode(t, i, got, expected, "")
checkNode(t, i, got.Left, expected.Left, "left")
checkNode(t, i, got.Right, expected.Right, "Right")
checkNode(t, i, got.Up, expected.Up, "Up")
checkNode(t, i, got.Down, expected.Down, "Down")
}
}
func TestTextReadNormal(t *testing.T) {
return
fmt.Println("---------- Normal ----------")
/* trivial-bigger-staggered.txt
##### #####
# # #
##### ### #
# # #
# # ##### #
# # #
### ### # #
# # # #
# ####### #
# # #
##### #####
Nodes are
#####0#####
#1 2#3 4#
##### ### #
#5 6#7 8#
# # ##### #
#9#A G B#
### ### # #
#C D#E F# #
# ####### #
#H I#J K#
#####L#####
*/
// TODO: we are not detecting vertical dead-ends that go downwards
nodes := make([]*maze.Node, 22)
// ---- Node creation ----
coords := []struct{ x, y int }{
{5, 0}, // 0
{1, 1}, // 1
{5, 1}, // 2
{7, 1}, // 3
{9, 1}, // 4
{1, 3}, // 5
{3, 3}, // 6
{5, 3}, // 7
{9, 3}, // 8
{1, 5}, // 9
{3, 5}, // A (10)
{7, 5}, // G (16)
{9, 5}, // B (11)
{1, 7}, // C (12)
{3, 7}, // D (13)
{5, 7}, // E (14)
{7, 7}, // F (15)
{1, 9}, // H (17)
{5, 9}, // I (18)
{7, 9}, // J (19)
{9, 9}, // K (20)
{5, 10}, // L (21)
}
for i, coord := range coords {
nodes[i] = maze.NewNode(maze.Coordinates{X: coord.x, Y: coord.y})
}
// ---- Node linking ----
nodes[0].Down = nodes[2]
nodes[1].Right = nodes[2]
nodes[2].Up = nodes[0]
nodes[2].Left = nodes[1]
nodes[2].Down = nodes[7]
nodes[3].Right = nodes[4]
nodes[4].Left = nodes[3]
nodes[4].Down = nodes[8]
nodes[5].Right = nodes[6]
nodes[5].Down = nodes[9]
nodes[6].Left = nodes[5]
nodes[6].Down = nodes[10]
nodes[7].Right = nodes[8]
nodes[8].Up = nodes[4]
nodes[8].Left = nodes[7]
nodes[8].Down = nodes[11]
nodes[9].Up = nodes[5]
nodes[10].Up = nodes[6]
nodes[10].Right = nodes[16]
nodes[10].Down = nodes[13]
nodes[11].Up = nodes[6]
nodes[11].Right = nodes[16]
nodes[11].Down = nodes[20]
nodes[12].Right = nodes[13]
nodes[12].Down = nodes[17]
nodes[13].Up = nodes[10]
nodes[13].Left = nodes[16]
nodes[14].Right = nodes[15]
nodes[15].Up = nodes[16]
nodes[15].Left = nodes[14]
nodes[16].Left = nodes[10]
nodes[16].Right = nodes[11]
nodes[16].Down = nodes[15]
nodes[17].Up = nodes[12]
nodes[17].Right = nodes[18]
nodes[18].Left = nodes[17]
nodes[18].Down = nodes[21]
nodes[19].Right = nodes[20]
nodes[20].Up = nodes[11]
nodes[20].Left = nodes[19]
nodes[21].Up = nodes[20]
reader := reader.TextReader{
Filename: "../../assets/normal.txt",
PathChar: ' ',
WallChar: '#',
}
got, err := Parse(reader)
utils.Check(err, "Couldn't create maze from %q", reader.Filename)
if len(nodes) != len(got.Nodes) {
for i, node := range got.Nodes {
fmt.Printf("%v: %v\n", i, node)
}
t.Fatalf("Didn't get the same size of nodes: %v, want %v", len(got.Nodes), len(nodes))
}
for _, node := range got.Nodes {
fmt.Println(node)
}
for i, got := range got.Nodes {
expected := nodes[i]
checkNode(t, i, got, expected, "")
checkNode(t, i, got.Left, expected.Left, "left")
checkNode(t, i, got.Right, expected.Right, "Right")
checkNode(t, i, got.Up, expected.Up, "Up")
checkNode(t, i, got.Down, expected.Down, "Down")
}
}
func checkNode(t *testing.T, i int, got *maze.Node, expected *maze.Node, side string) {
if expected == nil {
return
}
if got == nil {
t.Fatalf("No %s node of %v, want %v", side, i, expected.Coords)
}
if got.Coords != expected.Coords {
t.Fatalf("Coords %s node of %v: %v, but want %v", side, i, got.Coords, expected.Coords)
}
}

View File

@ -1,6 +1,10 @@
package utils package utils
import "log" import (
"log"
"golang.org/x/exp/constraints"
)
func Check(err error, msg string, args ...any) { func Check(err error, msg string, args ...any) {
if err != nil { if err != nil {
@ -8,3 +12,10 @@ func Check(err error, msg string, args ...any) {
panic(err) panic(err)
} }
} }
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}