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)
This commit is contained in:
Karma Riuk 2023-08-05 16:18:03 +02:00
parent e2b0b09636
commit fa4c13812d
7 changed files with 414 additions and 239 deletions

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

@ -0,0 +1,53 @@
package reader
import (
"fmt"
"maze-solver/maze"
"maze-solver/utils"
)
type StringsReader struct {
PathChar, WallChar byte
Lines *[]string
}
func (r *StringsReader) Read() (*maze.RawMaze, error) {
width, height := len((*r.Lines)[0]), len(*r.Lines)
ret := &maze.RawMaze{
Width: width,
Height: height,
Data: make([][]byte, height),
}
for i := 0; i < height; i++ {
ret.Data[i] = make([]byte, width/maze.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)/maze.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)*maze.CHUNK_SIZE, len(line))
for x, c := range line[i*maze.CHUNK_SIZE : end_index] {
if c == rune(r.PathChar) {
chunk |= 1 << (maze.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

@ -17,11 +17,13 @@ func (r TextReader) Read() (*maze.RawMaze, error) {
return nil, err
}
return &maze.RawMaze{
strings_reader := StringsReader{
PathChar: r.PathChar,
WallChar: r.WallChar,
Data: *lines,
}, nil
Lines: lines,
}
return strings_reader.Read()
}
func getLines(filename string) (*[]string, error) {

View File

@ -1,106 +1,108 @@
package reader
import (
"maze-solver/maze"
"maze-solver/utils"
"reflect"
// "maze-solver/maze"
// "maze-solver/utils"
// "reflect"
"testing"
)
func TestTextReadTrivial(t *testing.T) {
tests := []struct {
name string
filename string
pathChar byte
wallChar byte
expected *maze.RawMaze
}{
{
"Trivial",
"../../assets/trivial.txt",
' ',
'#',
&maze.RawMaze{
PathChar: ' ',
WallChar: '#',
Data: []string{
"## ##",
"# #",
"### #",
/*
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{
"### ###",
"### ###",
"# #",
"##### #",
"##### #",
{
"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{
"### ###",
"### ###",
"# #",
"#### ##",
"#### ##",
{
"Bigger Staggered",
"../../assets/trivial-bigger-staggered.txt",
' ',
'#',
&maze.RawMaze{
PathChar: ' ',
WallChar: '#',
Data: []string{
"### ###",
"### ###",
"# #",
"#### ##",
"#### ##",
},
},
},
},
{
"Normal",
"../../assets/normal.txt",
' ',
'#',
&maze.RawMaze{
PathChar: ' ',
WallChar: '#',
Data: []string{
"##### #####",
"# # #",
"##### ### #",
"# # #",
"# # ##### #",
"# # #",
"### ### # #",
"# # # #",
"# ####### #",
"# # #",
"##### #####",
{
"Normal",
"../../assets/normal.txt",
' ',
'#',
&maze.RawMaze{
PathChar: ' ',
WallChar: '#',
Data: []string{
"##### #####",
"# # #",
"##### ### #",
"# # #",
"# # ##### #",
"# # #",
"### ### # #",
"# # # #",
"# ####### #",
"# # #",
"##### #####",
},
},
},
},
}
for _, test := range tests {
reader := TextReader{
Filename: test.filename,
PathChar: test.pathChar,
WallChar: test.wallChar,
}
got, err := reader.Read()
utils.Check(err, "Couldn't read file %q", reader.Filename)
for _, test := range tests {
reader := TextReader{
Filename: test.filename,
PathChar: test.pathChar,
WallChar: test.wallChar,
}
if !reflect.DeepEqual(got, test.expected) {
t.Fatalf("%s: lexed mazes do not match\nGot: %v\nWant: %v", test.name, got, test.expected)
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)
}
}
}
*/
}

View File

@ -6,11 +6,6 @@ import (
"maze-solver/maze"
)
const (
WallChar = '#'
PathChar = ' '
)
func Parse(reader reader.Reader) (*maze.Maze, error) {
nodesByCoord := make(map[maze.Coordinates]*maze.Node)
ret := &maze.Maze{}
@ -20,55 +15,47 @@ func Parse(reader reader.Reader) (*maze.Maze, error) {
return nil, err
}
for y, line := range raw_maze.Data {
fmt.Println(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 = raw_maze.Data[y-1][x]
}
// Parse first line to get entrance
if y == 0 && char == PathChar {
coords := maze.Coordinates{X: x, Y: y}
node := maze.NewNode(coords)
ret.Nodes = append(ret.Nodes, node)
nodesByCoord[coords] = node
continue
}
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 y > 0 && char == PathChar &&
(left_char == WallChar && right_char == PathChar ||
left_char == PathChar && right_char == WallChar ||
above_char == PathChar && (left_char == PathChar || right_char == PathChar)) {
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.Data, node, &nodesByCoord, ret)
lookupNeighbourAbove(raw_maze, node, &nodesByCoord, ret)
ret.Nodes = append(ret.Nodes, node)
nodesByCoord[coords] = node
if left_char == PathChar && right_char == WallChar ||
above_char == PathChar && (left_char == PathChar || right_char == PathChar) {
lookupNeighbourLeft(&line, node, &nodesByCoord)
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, rune := range raw_maze.Data[len(raw_maze.Data)-1] {
char := byte(rune)
if char == PathChar {
coords := maze.Coordinates{X: x, Y: len(raw_maze.Data) - 1}
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.Data, node, &nodesByCoord, ret)
lookupNeighbourAbove(raw_maze, node, &nodesByCoord, ret)
ret.Nodes = append(ret.Nodes, node)
break
}
@ -77,7 +64,7 @@ func Parse(reader reader.Reader) (*maze.Maze, error) {
return ret, nil
}
func lookupNeighbourAbove(Data *[]string, node *maze.Node, nodesByCoord *map[maze.Coordinates]*maze.Node, m *maze.Maze) {
func lookupNeighbourAbove(raw_maze *maze.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}]
@ -87,15 +74,15 @@ func lookupNeighbourAbove(Data *[]string, node *maze.Node, nodesByCoord *map[maz
break
}
if y > 0 && (*Data)[y][node.Coords.X] == WallChar {
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(&(*Data)[y], new_node, nodesByCoord)
lookupNeighbourRight(&(*Data)[y], new_node, nodesByCoord)
lookupNeighbourLeft(raw_maze, new_node, nodesByCoord)
lookupNeighbourRight(raw_maze, new_node, nodesByCoord)
(*nodesByCoord)[coords] = new_node
m.Nodes = append(m.Nodes, new_node)
@ -107,9 +94,9 @@ func lookupNeighbourAbove(Data *[]string, node *maze.Node, nodesByCoord *map[maz
}
}
func lookupNeighbourLeft(line *string, node *maze.Node, nodesByCoord *map[maze.Coordinates]*maze.Node) {
func lookupNeighbourLeft(raw_maze *maze.RawMaze, node *maze.Node, nodesByCoord *map[maze.Coordinates]*maze.Node) {
for x := node.Coords.X - 1; x > 0; x-- {
if (*line)[x] == WallChar && x < node.Coords.X-1 {
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))
}
@ -122,9 +109,9 @@ func lookupNeighbourLeft(line *string, node *maze.Node, nodesByCoord *map[maze.C
}
}
func lookupNeighbourRight(line *string, node *maze.Node, nodesByCoord *map[maze.Coordinates]*maze.Node) {
for x := node.Coords.X + 1; x < len(*line); x++ {
if (*line)[x] == WallChar {
func lookupNeighbourRight(raw_maze *maze.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))
}

View File

@ -1,25 +1,24 @@
package maze
import (
"fmt"
"strings"
)
const CHUNK_SIZE = 8 // size of a byte
type RawMaze struct {
PathChar, WallChar byte
Data []string
Width, Height int
Data [][]byte
}
func (m *RawMaze) String() string {
var ret strings.Builder
ret.WriteString("{\n")
ret.WriteString(fmt.Sprintf("\tPathChar: %v,\n", m.PathChar))
ret.WriteString(fmt.Sprintf("\tWallChar: %v,\n", m.WallChar))
ret.WriteString("\tData: \n")
for _, line := range m.Data {
ret.WriteRune('\t')
ret.WriteRune('\t')
ret.WriteString(line)
ret.Write(line) // TODO: prolly should fix this to make it readable
ret.WriteRune('\n')
}
ret.WriteString("}")
@ -27,10 +26,16 @@ func (m *RawMaze) String() string {
return ret.String()
}
func (m *RawMaze) isPath(x int, y int) bool {
return m.Data[y][x] == m.PathChar
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 {
return m.Data[y][x] == m.WallChar
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
}

View File

@ -4,20 +4,18 @@ import "testing"
func TestRawMazeWall(t *testing.T) {
tests := []struct {
name string
pathChar byte
wallChar byte
data []string
expected [][]bool
name string
width, height int
data [][]byte
expected [][]bool
}{
{
"Trivial",
' ',
'#',
[]string{
"## ##",
"# #",
"### #",
5, 3,
[][]byte{
{0b_00100_000},
{0b_01110_000},
{0b_00010_000},
},
[][]bool{
{true, true, false, true, true},
@ -27,14 +25,13 @@ func TestRawMazeWall(t *testing.T) {
},
{
"Trivial Bigger",
' ',
'#',
[]string{
"### ###",
"### ###",
"# #",
"##### #",
"##### #",
7, 5,
[][]byte{
{0b_0001000_0},
{0b_0001000_0},
{0b_0111110_0},
{0b_0000010_0},
{0b_0000010_0},
},
[][]bool{
{true, true, true, false, true, true, true},
@ -46,14 +43,13 @@ func TestRawMazeWall(t *testing.T) {
},
{
"Bigger Staggered",
' ',
'#',
[]string{
"### ###",
"### ###",
"# #",
"#### ##",
"#### ##",
7, 5,
[][]byte{
{0b_0001000_0},
{0b_0001000_0},
{0b_0111110_0},
{0b_0000100_0},
{0b_0000100_0},
},
[][]bool{
{true, true, true, false, true, true, true},
@ -65,20 +61,19 @@ func TestRawMazeWall(t *testing.T) {
},
{
"Normal",
' ',
'#',
[]string{
"##### #####",
"# # #",
"##### ### #",
"# # #",
"# # ##### #",
"# # #",
"### ### # #",
"# # # #",
"# ####### #",
"# # #",
"##### #####",
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{
{true, true, true, true, true, false, true, true, true, true, true},
@ -98,15 +93,14 @@ func TestRawMazeWall(t *testing.T) {
for _, test := range tests {
rawMaze := RawMaze{
PathChar: test.pathChar,
WallChar: test.wallChar,
Data: test.data,
Width: test.width,
Height: test.height,
Data: test.data,
}
for y, row := range test.expected {
for x, expected := range row {
if rawMaze.isWall(x, y) != expected {
t.Fatalf("Wanted wall at (%v, %v), apparently it isn't", x, y)
if rawMaze.IsWall(x, y) != expected {
t.Fatalf("%s: Wanted wall at (%v, %v), apparently it isn't", test.name, x, y)
}
}
}
@ -115,20 +109,18 @@ func TestRawMazeWall(t *testing.T) {
func TestRawMazePath(t *testing.T) {
tests := []struct {
name string
pathChar byte
wallChar byte
data []string
expected [][]bool
name string
width, height int
data [][]byte
expected [][]bool
}{
{
"Trivial",
' ',
'#',
[]string{
"## ##",
"# #",
"### #",
5, 3,
[][]byte{
{0b_00100_000},
{0b_01110_000},
{0b_00010_000},
},
[][]bool{
{false, false, true, false, false},
@ -138,14 +130,13 @@ func TestRawMazePath(t *testing.T) {
},
{
"Trivial Bigger",
' ',
'#',
[]string{
"### ###",
"### ###",
"# #",
"##### #",
"##### #",
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},
@ -157,14 +148,13 @@ func TestRawMazePath(t *testing.T) {
},
{
"Bigger Staggered",
' ',
'#',
[]string{
"### ###",
"### ###",
"# #",
"#### ##",
"#### ##",
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},
@ -176,20 +166,19 @@ func TestRawMazePath(t *testing.T) {
},
{
"Normal",
' ',
'#',
[]string{
"##### #####",
"# # #",
"##### ### #",
"# # #",
"# # ##### #",
"# # #",
"### ### # #",
"# # # #",
"# ####### #",
"# # #",
"##### #####",
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},
@ -209,15 +198,15 @@ func TestRawMazePath(t *testing.T) {
for _, test := range tests {
rawMaze := RawMaze{
PathChar: test.pathChar,
WallChar: test.wallChar,
Data: test.data,
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("Wanted path at (%v, %v), apparently it isn't", x, y)
if rawMaze.IsPath(x, y) != expected {
t.Fatalf("%s: Wanted path at (%v, %v), apparently it isn't", test.name, x, y)
}
}
}