51 Commits

Author SHA1 Message Date
4f79d6f1ed Removed useless print statements 2023-08-14 15:09:24 +02:00
346cfe9705 Finally written some good tests for the image writer 2023-08-14 15:08:27 +02:00
94af003adc Added some assets 2023-08-14 15:08:13 +02:00
aec655610a Added files to gitignore 2023-08-14 15:07:10 +02:00
2423645f3a Removed log statement 2023-08-14 14:27:07 +02:00
6e0a1032d1 Figured out that the implementation of turn left
was actually dfs lol
2023-08-13 21:31:47 +02:00
69afaed1bf fixed lil mistake eheh 2023-08-11 14:34:12 +02:00
908e8c14fb Commented out image writer tests cuz i gotta think
of a good way to do them :)
2023-08-11 14:31:51 +02:00
954d44085c Fixed workflows 2023-08-11 14:29:35 +02:00
8a6543cc1e Updated the github workflows 2023-08-11 14:27:24 +02:00
485efeebaf Does this one work? 2023-08-11 14:18:52 +02:00
f803dc7771 Fixed github workflow (hopefully) 2023-08-11 14:10:35 +02:00
46c42cb67d Refactored main.go to make the entry point clearer 2023-08-11 14:09:31 +02:00
32f7720069 Added github workflow for auto-generating
pre-resleases
2023-08-11 14:05:55 +02:00
3c7c181911 Fixed failing tests 2023-08-11 13:48:39 +02:00
a7dd3e1a81 Added argument parsing to run the solver correctly 2023-08-11 12:30:37 +02:00
5500007fb4 Added a solver: turn left 2023-08-10 19:39:16 +02:00
a80e2c9cc3 Added a logging system to show home much time it
took to do a certain part of the program
2023-08-10 19:38:43 +02:00
fb59c890ca Corrected some bugs for when it came to parsing
and writing mazes
2023-08-10 19:13:24 +02:00
18f37e65ed added new maze (15x15) for testing purposes 2023-08-10 19:11:15 +02:00
0f05998295 Changed type of maze in SolvedMaze to pointer, to
not copy the entire maze by value
2023-08-10 10:30:37 +02:00
99fc6ba48a renamed assets so that they are more consistent 2023-08-09 19:54:38 +02:00
1cfd92593f implemented image reader 2023-08-09 19:51:23 +02:00
e72e9e694a added the png version of the mazes used for
testing purposes (so that the reader has something to read)
2023-08-09 17:46:24 +02:00
41e665c169 Written the ImageWriter 2023-08-09 17:44:59 +02:00
acf8aff469 Moved the generation of the solved mazes into its
own file since they are needed for both ImageWriter and StringsWriter
2023-08-09 17:42:53 +02:00
fd17cb3526 Added description comments 2023-08-09 17:41:44 +02:00
4949e5fa21 Updated writer interface and wrote the strings
writer
2023-08-09 10:21:11 +02:00
4852aece8a Made the maze generation part of normal.txt a bit
shorter and more readable (or at least i hope so)
2023-08-09 10:17:00 +02:00
c77e3f514a Removed TODO commment that was done 2023-08-09 10:14:45 +02:00
f085efa2fe fixed name of file name in comment 2023-08-09 10:14:29 +02:00
92ba1b48e4 Forgot to put the width and height of the maze
when I parsed it, oops (and now it's tested)
2023-08-07 18:22:04 +02:00
58787dc4af moved assertEquals to utils so that other tests
can use it
2023-08-07 18:18:25 +02:00
3d4a2b9bfb Re-enabled text_test.go 2023-08-07 18:09:58 +02:00
b6dff509f9 Fixed parser 2023-08-07 17:43:35 +02:00
bfc370bdda Removed useless prints in parser_test 2023-08-07 17:43:01 +02:00
8b0fa4c1f9 Moved RawMaze to reader package since it is used
mostly there
2023-08-05 16:36:49 +02:00
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
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
ee9d439485 Added isWall and isPath to RawMaze with tests 2023-08-05 12:02:42 +02:00
130deb40d8 Reader -> Reader+Parser refactoring: main.go now
uses the new structure
2023-08-05 12:01:29 +02:00
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
be688e6920 Reader -> Reader+Parser refactoring: added tests
for new reader
2023-08-05 11:21:32 +02:00
435ea54343 Added String() method to RawMaze for debugging
purposes
2023-08-05 11:20:53 +02:00
929c5b58a0 Reader -> Reader+Parser refactoring: moved the
reading of the lines to its own function
2023-08-05 11:03:42 +02:00
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
e04aad4b77 Start of refactoring for reader+parser: read all
the lines before starting to parse
2023-08-05 10:01:34 +02:00
345d9267ad moved instructions around to make it more
consistent and fixed tiny bug
2023-08-05 09:38:12 +02:00
ff28832f72 Added missing logic to text reader and tested it 2023-08-04 23:53:34 +02:00
f46af33702 Added a new test case for text reader 2023-08-04 23:06:37 +02:00
c2d16a4d20 Removed useless print statements 2023-08-04 23:05:16 +02:00
42 changed files with 2673 additions and 188 deletions

View File

@ -0,0 +1,33 @@
---
name: "pre-release"
on:
push:
branches:
- "main"
jobs:
pre-release:
name: "Pre Release"
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.21"
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
- uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "latest"
prerelease: true
title: "Development Build"

31
.github/workflows/tagged-release.yml vendored Normal file
View File

@ -0,0 +1,31 @@
---
name: "tagged-release"
on:
push:
tags:
- "v*"
jobs:
tagged-release:
name: "Tagged Release"
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.21"
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
- uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false

2
.gitignore vendored
View File

@ -20,3 +20,5 @@
# Go workspace file # Go workspace file
go.work go.work
/Session.vim /Session.vim
/maze.png
/maze_sol.png

BIN
assets/normal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/normal2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

15
assets/normal2.txt Normal file
View File

@ -0,0 +1,15 @@
####### #######
# # # #
### # ##### # #
# # # #
# ########### #
# # # #
### # # ##### #
# # # # #
# ### ### # # #
# # # # #
# ### # #######
# # # # #
# # ### ### # #
# # # #
####### #######

BIN
assets/real.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
assets/solved/normal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

BIN
assets/solved/normal2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets/solved/real.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

BIN
assets/solved/trivial.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

View File

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

BIN
assets/trivial-bigger.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

View File

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

BIN
assets/trivial.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

14
go.mod
View File

@ -1,3 +1,15 @@
module maze-solver module maze-solver
go 1.20 go 1.21
require (
github.com/akamensky/argparse v1.4.0
github.com/mazznoer/colorgrad v0.9.1
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
)
require (
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mazznoer/csscolorparser v0.1.2 // indirect
golang.org/x/image v0.11.0
)

43
go.sum Normal file
View File

@ -0,0 +1,43 @@
github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc=
github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mazznoer/colorgrad v0.9.1 h1:MB80JYVndKWSMEM1beNqnuOowWGhoQc3DXWXkFp6JlM=
github.com/mazznoer/colorgrad v0.9.1/go.mod h1:WX2R9wt9B47+txJZVVpM9LY+LAGIdi4lTI5wIyreDH4=
github.com/mazznoer/csscolorparser v0.1.2 h1:/UBHuQg792ePmGFzTQAC9u+XbFr7/HzP/Gj70Phyz2A=
github.com/mazznoer/csscolorparser v0.1.2/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtdHtGfQ32xZ5PJQic=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
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=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -1,9 +1,76 @@
package reader package reader
import "maze-solver/maze" import (
"image"
"image/color"
"image/png"
"maze-solver/utils"
"os"
type ImageReader struct{} "golang.org/x/image/draw"
)
func (r *ImageReader) Read(filename string) (*maze.Maze, error) { type ImageReader struct {
return nil, nil Filename string
PathColor, WallColor color.Color
CellWidth, CellHeight int
}
func (r *ImageReader) Read() (*RawMaze, error) {
defer utils.Timer("Image reader", 3)()
image, err := r.getShrunkImage()
if err != nil {
return nil, err
}
width, height := image.Bounds().Max.X, image.Bounds().Max.Y
ret := &RawMaze{
Width: width,
Height: height,
Data: make([][]byte, height),
}
n_chunks := width/CHUNK_SIZE + 1
for i := 0; i < height; i++ {
ret.Data[i] = make([]byte, n_chunks)
}
for y := 0; y < height; y++ {
for i := 0; i < n_chunks; i++ {
var chunk byte = 0 // all walls
end_index := min((i+1)*CHUNK_SIZE, width)
for x := i * CHUNK_SIZE; x < end_index; x++ {
c := image.At(x, y)
if c == r.PathColor {
chunk |= 1 << (CHUNK_SIZE - 1 - (x - i*CHUNK_SIZE))
}
}
ret.Data[y][i] = chunk
}
}
return ret, nil
}
func (r *ImageReader) getShrunkImage() (*image.RGBA, error) {
input, err := os.Open(r.Filename)
if err != nil {
return nil, err
}
defer input.Close()
// Decode the image (from PNG to image.Image):
src, _ := png.Decode(input)
// Set the expected size that you want:
dst := image.NewRGBA(image.Rect(0, 0, src.Bounds().Max.X/r.CellWidth, src.Bounds().Max.Y/r.CellHeight))
// Resize:
draw.NearestNeighbor.Scale(dst, dst.Rect, src, src.Bounds(), draw.Over, nil)
return dst, nil
} }

136
io/reader/image_test.go Normal file
View File

@ -0,0 +1,136 @@
package reader
import (
"image/color"
"maze-solver/utils"
"testing"
)
func TestImageReader(t *testing.T) {
white := color.RGBA{255, 255, 255, 255}
black := color.RGBA{0, 0, 0, 255}
tests := []struct {
name string
width, height int
cellWidth, cellHeight int
pathColor, wallColor color.Color
filename string
expected [][]byte
}{
{
"Trivial",
5, 3,
40, 40,
white, black,
"../../assets/trivial.png",
[][]byte{
{0b_00100_000},
{0b_01110_000},
{0b_00010_000},
},
},
{
"Trivial Bigger",
7, 5,
40, 40,
white, black,
"../../assets/trivial-bigger.png",
[][]byte{
{0b_0001000_0},
{0b_0001000_0},
{0b_0111110_0},
{0b_0000010_0},
{0b_0000010_0},
},
},
{
"Bigger Staggered",
7, 5,
40, 40,
white, black,
"../../assets/trivial-bigger-staggered.png",
[][]byte{
{0b_0001000_0},
{0b_0001000_0},
{0b_0111110_0},
{0b_0000100_0},
{0b_0000100_0},
},
},
{
"Normal",
11, 11,
40, 40,
white, black,
"../../assets/normal.png",
[][]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},
},
},
{
"Normal2",
15, 15,
20, 20,
white, black,
"../../assets/normal2.png",
[][]byte{
{0b00000001, 0b0000000_0},
{0b01110111, 0b1111010_0},
{0b00010100, 0b0001010_0},
{0b01110111, 0b1101110_0},
{0b01000000, 0b0000010_0},
{0b01111101, 0b1111010_0},
{0b00010101, 0b0000010_0},
{0b01110111, 0b0111010_0},
{0b01000100, 0b0101010_0},
{0b01011101, 0b1101110_0},
{0b01000101, 0b0000000_0},
{0b01110101, 0b0111110_0},
{0b01010001, 0b0001010_0},
{0b01011111, 0b1111010_0},
{0b00000001, 0b0000000_0},
},
},
}
for _, test := range tests {
reader := ImageReader{
Filename: test.filename,
PathColor: test.pathColor,
WallColor: test.wallColor,
CellWidth: test.cellWidth,
CellHeight: test.cellHeight,
}
got, err := reader.Read()
if err != nil {
t.Fatalf("%s: got error while reading, got\n%v", test.filename, err)
}
utils.AssertEqual(t, got.Width, test.width, "%s: width of raw maze don't match", test.name)
utils.AssertEqual(t, got.Height, test.height, "%s: height of raw maze don't match", test.name)
utils.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]
utils.AssertEqual(t, len(line_got), len(line_exp), "%s (line %v): don't have same number of chunks", 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)
}
}
}
}
}

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,48 @@
package reader package reader
import "maze-solver/maze" import (
"fmt"
"image/color"
)
type Reader interface { type Reader interface {
Read(filename string) (*maze.Maze, error) Read() (*RawMaze, error)
}
type ReaderFactory struct {
Type string
Filename *string
PathChar, WallChar, SolutionChar *string
CellWidth, CellHeight *int
WallColor, PathColor color.Color
}
const (
_IMAGE = "image"
_TEXT = "text"
)
var TYPES = map[string]string{
".png": _IMAGE,
".txt": _TEXT,
}
func (f *ReaderFactory) Get() Reader {
switch f.Type {
case _TEXT:
return &TextReader{
Filename: *f.Filename,
PathChar: byte((*f.PathChar)[0]),
WallChar: byte((*f.WallChar)[0]),
}
case _IMAGE:
return &ImageReader{
Filename: *f.Filename,
CellWidth: *f.CellWidth,
CellHeight: *f.CellHeight,
WallColor: f.WallColor,
PathColor: f.PathColor,
}
}
panic(fmt.Sprintf("Unrecognized reader type %q", f.Type))
} }

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

@ -0,0 +1,53 @@
package reader
import (
"fmt"
"maze-solver/utils"
)
type StringsReader struct {
PathChar, WallChar byte
Lines *[]string
}
func (r *StringsReader) Read() (*RawMaze, error) {
defer utils.Timer("Strings Reader", 3)()
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
}
}

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

@ -0,0 +1,131 @@
package reader
import (
"maze-solver/utils"
"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()
utils.AssertEqual(t, got.Width, test.width, "%s: width of raw maze don't match", test.name)
utils.AssertEqual(t, got.Height, test.height, "%s: height of raw maze don't match", test.name)
utils.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]
utils.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)
}
}
}
}
}

View File

@ -2,21 +2,33 @@ package reader
import ( import (
"bufio" "bufio"
"fmt" "maze-solver/utils"
"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) defer utils.Timer("Text Reader", 3)()
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 +37,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,105 @@
package reader package reader
import ( import (
"fmt"
"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
Nodes are expected *RawMaze
##0## }{
#123# {
###4# "Trivial",
*/ "../../assets/trivial.txt",
nodes := make([]*maze.Node, 5) ' ',
'#',
nodes[0] = maze.NewNode(maze.Coordinates{X: 2, Y: 0}) &RawMaze{
Width: 5,
nodes[1] = maze.NewNode(maze.Coordinates{X: 1, Y: 1}) Height: 3,
nodes[2] = maze.NewNode(maze.Coordinates{X: 2, Y: 1}) Data: [][]byte{
nodes[3] = maze.NewNode(maze.Coordinates{X: 3, Y: 1}) {0b_00100_000},
{0b_01110_000},
nodes[4] = maze.NewNode(maze.Coordinates{X: 3, Y: 2}) {0b_00010_000},
},
nodes[0].Down = nodes[2] },
},
nodes[1].Right = nodes[2] {
"Trivial Bigger",
nodes[2].Up = nodes[0] "../../assets/trivial-bigger.txt",
nodes[2].Left = nodes[1] ' ',
nodes[2].Right = nodes[3] '#',
&RawMaze{
nodes[3].Left = nodes[2] Width: 7,
nodes[3].Down = nodes[4] Height: 5,
Data: [][]byte{
nodes[4].Up = nodes[3] {0b_0001000_0},
{0b_0001000_0},
reader := TextReader{ {0b_0111110_0},
PathChar: ' ', {0b_0000010_0},
WallChar: '#', {0b_0000010_0},
},
},
},
{
"Bigger Staggered",
"../../assets/trivial-bigger-staggered.txt",
' ',
'#',
&RawMaze{
Width: 7,
Height: 5,
Data: [][]byte{
{0b_0001000_0},
{0b_0001000_0},
{0b_0111110_0},
{0b_0000100_0},
{0b_0000100_0},
},
},
},
{
"Normal",
"../../assets/normal.txt",
' ',
'#',
&RawMaze{
Width: 11,
Height: 11,
Data: [][]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},
},
},
},
} }
filename := "../../assets/trivial.txt" for _, test := range tests {
got, err := reader.Read(filename) reader := TextReader{
utils.Check(err, "Couldn't create maze from %q", filename) Filename: test.filename,
PathChar: test.pathChar,
WallChar: test.wallChar,
}
if len(nodes) != len(got.Nodes) { got, err := reader.Read()
t.Fatalf("Didn't get the same size of nodes: %v, want %v", len(got.Nodes), len(nodes)) utils.Check(err, "Couldn't read file %q", reader.Filename)
}
for i, got := range got.Nodes { if !reflect.DeepEqual(got, test.expected) {
fmt.Println(i) t.Fatalf("%s: lexed mazes do not match\nGot: %v\nWant: %v", test.name, got, test.expected)
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,11 +1,123 @@
package writer package writer
import ( import (
"errors"
"image"
"image/color"
"image/draw"
"image/png"
"maze-solver/maze" "maze-solver/maze"
"maze-solver/utils"
"os"
"github.com/mazznoer/colorgrad"
) )
type ImageWriter struct{} type ImageWriter struct {
Filename string
Maze *maze.SolvedMaze
CellWidth, CellHeight int
WallColor, PathColor color.Color
SolutionGradient colorgrad.Gradient
img *image.RGBA
}
func (w *ImageWriter) Write(filename string, maze *maze.SolvedMaze) error { func (w *ImageWriter) Write() error {
defer utils.Timer("Image writer", 3)()
if w.Filename[len(w.Filename)-4:] != ".png" {
return errors.New("Filename of ImageWriter doesn't have .png extension. The only suppported image type is png")
}
w.img = image.NewRGBA(image.Rect(0, 0, w.Maze.Width*w.CellWidth, w.Maze.Height*w.CellHeight))
// Fill the image with walls
draw.Draw(w.img, w.img.Bounds(), &image.Uniform{w.WallColor}, image.Pt(0, 0), draw.Src)
// Fill in the paths
var x0, y0, width, height int
for _, node := range w.Maze.Nodes {
x0 = node.Coords.X * w.CellWidth
y0 = node.Coords.Y * w.CellHeight
if node.Right != nil {
width = (node.Right.Coords.X - node.Coords.X + 1) * w.CellWidth
height = w.CellHeight
w.draw(x0, y0, width, height, w.PathColor)
}
if node.Down != nil {
width = w.CellWidth
height = (node.Down.Coords.Y - node.Coords.Y + 1) * w.CellHeight
w.draw(x0, y0, width, height, w.PathColor)
}
}
// Fill in the solution
total_len := w.getSolutionLength()
colors := w.SolutionGradient.Colors(uint(total_len + 1))
c := 0
width, height = w.CellWidth, w.CellHeight
for i, from := range w.Maze.Solution[:len(w.Maze.Solution)-1] {
to := w.Maze.Solution[i+1]
if from.Coords.X == to.Coords.X {
// Fill verticallly
x0 = from.Coords.X * w.CellWidth
if from.Coords.Y < to.Coords.Y {
for y := from.Coords.Y; y < to.Coords.Y; y++ {
y0 = y * w.CellHeight
w.draw(x0, y0, width, height, colors[c])
c++
}
} else {
for y := from.Coords.Y; y > to.Coords.Y; y-- {
y0 = y * w.CellHeight
w.draw(x0, y0, width, height, colors[c])
c++
}
}
y0 = to.Coords.Y * w.CellHeight
w.draw(x0, y0, width, height, colors[c])
} else {
// Fill horizontally
y0 = from.Coords.Y * w.CellHeight
if from.Coords.X < to.Coords.X {
for x := from.Coords.X; x < to.Coords.X; x++ {
x0 = x * w.CellWidth
w.draw(x0, y0, width, height, colors[c])
c++
}
} else {
for x := from.Coords.X; x > to.Coords.X; x-- {
x0 = x * w.CellWidth
w.draw(x0, y0, width, height, colors[c])
c++
}
}
x0 = to.Coords.X * w.CellWidth
w.draw(x0, y0, width, height, colors[c])
}
}
f, err := os.Create(w.Filename)
if err != nil {
return err
}
png.Encode(f, w.img)
f.Close()
return nil return nil
} }
func (w *ImageWriter) getSolutionLength() int {
ret := 0
for i, node := range w.Maze.Solution[:len(w.Maze.Solution)-1] {
next := w.Maze.Solution[i+1]
ret += int(node.Coords.Distance(next.Coords))
}
return ret
}
func (w *ImageWriter) draw(x0, y0, width, height int, color color.Color) {
draw.Draw(w.img, image.Rect(x0, y0, x0+width, y0+height), &image.Uniform{color}, image.Pt(0, 0), draw.Src)
}

122
io/writer/image_test.go Normal file
View File

@ -0,0 +1,122 @@
package writer
import (
"bytes"
"image/color"
"io"
"maze-solver/maze"
"os"
"testing"
"github.com/mazznoer/colorgrad"
)
const (
OUT_DIR = "./out"
EXPECTED_DIR = "../../assets/solved"
)
func TestImageWriter(t *testing.T) {
if _, err := os.Stat(OUT_DIR); os.IsNotExist(err) {
os.Mkdir(OUT_DIR, 0700)
}
tests := []struct {
name string
filename string
m *maze.SolvedMaze
CellWidth, cellHeight int
pathColor, wallColor color.Color
gradient colorgrad.Gradient
}{
{
"Trivial",
"trivial.png",
trivial(),
20, 20,
color.White, color.Black,
colorgrad.Warm(),
},
{
"Trivial Bigger",
"trivial-bigger.png",
bigger(),
20, 20,
color.White, color.Black,
colorgrad.Warm(),
},
{
"Trivial Bigger Staggered",
"trivial-bigger-staggered.png",
bigger_staggered(),
20, 20,
color.White, color.Black,
colorgrad.Warm(),
},
{
"Normal",
"normal.png",
normal(),
20, 20,
color.White, color.Black,
colorgrad.Warm(),
},
}
for _, test := range tests {
writer := ImageWriter{
Filename: OUT_DIR + "/" + test.filename,
Maze: test.m,
CellWidth: test.CellWidth,
CellHeight: test.cellHeight,
WallColor: test.wallColor,
PathColor: test.pathColor,
SolutionGradient: test.gradient,
}
err := writer.Write()
if err != nil {
t.Fatalf("%s: couldn't write solution, got following error\n%v", test.name, err)
}
assertEqualFile(t, EXPECTED_DIR+"/"+test.filename, OUT_DIR+"/"+test.filename, test.name)
os.Remove(writer.Filename)
}
os.Remove(OUT_DIR)
}
const chunkSize = 64000
func assertEqualFile(t *testing.T, file1, file2, name string) {
f1, err := os.Open(file1)
if err != nil {
t.Fatal(err)
}
defer f1.Close()
f2, err := os.Open(file2)
if err != nil {
t.Fatal(err)
}
defer f2.Close()
for {
b1 := make([]byte, chunkSize)
_, err1 := f1.Read(b1)
b2 := make([]byte, chunkSize)
_, err2 := f2.Read(b2)
if err1 != nil || err2 != nil {
if err1 == io.EOF && err2 == io.EOF {
return
} else if err1 == io.EOF || err2 == io.EOF {
t.Fatalf("%s: files are not equal. Got %q, wanted %q", name, file1, file2)
}
}
if !bytes.Equal(b1, b2) {
t.Fatalf("%s: files are not equal. Got %q, wanted %q", name, file1, file2)
}
}
}

77
io/writer/strings.go Normal file
View File

@ -0,0 +1,77 @@
package writer
import (
"bytes"
"fmt"
"maze-solver/maze"
"maze-solver/utils"
)
type StringsWriter struct {
PathChar, WallChar byte
SolutionChar byte
Maze *maze.SolvedMaze
lines [][]byte
}
func (w *StringsWriter) Write() error {
defer utils.Timer("Strings writer", 3)()
w.lines = make([][]byte, w.Maze.Height)
// Fill the lines with walls
for y := 0; y < w.Maze.Height; y++ {
w.lines[y] = bytes.Repeat([]byte{w.WallChar}, w.Maze.Width)
}
// Fill in the paths
for _, node := range w.Maze.Nodes {
if node.Right != nil {
w.fillHorizontally(node.Coords, node.Right.Coords, w.PathChar)
}
if node.Down != nil {
w.fillVertically(node.Coords, node.Down.Coords, w.PathChar)
}
}
// Fill in the solution
for i := 0; i < len(w.Maze.Solution)-1; i++ {
current := w.Maze.Solution[i].Coords
next := w.Maze.Solution[i+1].Coords
if current.X == next.X {
w.fillVertically(current, next, w.SolutionChar)
} else {
w.fillHorizontally(current, next, w.SolutionChar)
}
}
return nil
}
func (w *StringsWriter) fillHorizontally(from maze.Coordinates, to maze.Coordinates, char byte) {
y := from.Y
if from.X > to.X {
from, to = to, from
}
for x := from.X; x <= to.X; x++ {
w.lines[y][x] = char
}
}
func (w *StringsWriter) fillVertically(from maze.Coordinates, to maze.Coordinates, char byte) {
x := from.X
if from.Y > to.Y {
from, to = to, from
}
for y := from.Y; y <= to.Y; y++ {
w.lines[y][x] = char
}
}
func (w *StringsWriter) GetLines() []string {
ret := make([]string, len(w.lines))
for i, line := range w.lines {
ret[i] = fmt.Sprint(string(line))
}
return ret
}

86
io/writer/strings_test.go Normal file
View File

@ -0,0 +1,86 @@
package writer
import (
"maze-solver/maze"
"maze-solver/utils"
"testing"
)
func TestStringsWriter(t *testing.T) {
tests := []struct {
name string
m *maze.SolvedMaze
pathChar, wallChar, solutionChar byte
expected []string
}{
{
"Trivial",
trivial(),
' ', '#', '.',
[]string{
"##.##",
"# ..#",
"###.#",
},
},
{
"Bigger",
bigger(),
'_', '~', '*',
[]string{
"~~~*~~~",
"~~~*~~~",
"~__***~",
"~~~~~*~",
"~~~~~*~",
},
},
{
"Bigger Staggered",
bigger_staggered(),
' ', '#', '.',
[]string{
"###.###",
"###.###",
"# .. #",
"####.##",
"####.##",
},
},
{
"Normal",
normal(),
' ', '#', '.',
[]string{
"#####.#####",
"# .# #",
"#####.### #",
"# #.....#",
"# # #####.#",
"# #.......#",
"###.### # #",
"#...# # #",
"#.####### #",
"#.....# #",
"#####.#####",
},
},
}
for _, test := range tests {
writer := StringsWriter{
PathChar: test.pathChar,
WallChar: test.wallChar,
SolutionChar: test.solutionChar,
Maze: test.m,
}
writer.Write()
got := writer.GetLines()
utils.AssertEqual(t, len(got), len(test.expected), "%s: different amount of lines.", test.name)
for i, line := range test.expected {
utils.AssertEqual(t, got[i], line, "%s, line %v: not what we expected.", test.name, i)
}
}
}

291
io/writer/utils.go Normal file
View File

@ -0,0 +1,291 @@
package writer
import "maze-solver/maze"
func trivial() *maze.SolvedMaze {
/* 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]
ret := &maze.SolvedMaze{
Maze: &maze.Maze{
Width: 5,
Height: 3,
Nodes: nodes,
},
Solution: []*maze.Node{
nodes[0], nodes[2], nodes[3], nodes[4],
},
}
return ret
}
func bigger() *maze.SolvedMaze {
/* 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]
ret := &maze.SolvedMaze{
Maze: &maze.Maze{
Width: 7,
Height: 5,
Nodes: nodes,
},
Solution: []*maze.Node{
nodes[0], nodes[2], nodes[3], nodes[4],
},
}
return ret
}
func bigger_staggered() *maze.SolvedMaze {
/* 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].Left = nodes[2]
nodes[4].Right = nodes[3]
nodes[4].Down = nodes[5]
nodes[5].Up = nodes[4]
ret := &maze.SolvedMaze{
Maze: &maze.Maze{
Width: 7,
Height: 5,
Nodes: nodes,
},
Solution: []*maze.Node{
nodes[0], nodes[2], nodes[4], nodes[5],
},
}
return ret
}
func normal() *maze.SolvedMaze {
/* normal.txt
##### #####
# # #
##### ### #
# # #
# # ##### #
# # #
### ### # #
# # # #
# ####### #
# # #
##### #####
Nodes are
#####0#####
#1 2#3 4#
##### ### #
#5 6#7 8#
# # ##### #
#9#A F B#
### ### # #
#C D#E G# #
# ####### #
#H I#J K#
#####L#####
*/
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)
{9, 5}, // B (11)
{1, 7}, // C (12)
{3, 7}, // D (13)
{5, 7}, // E (14)
{7, 5}, // F (15)
{7, 7}, // G (16)
{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 ----
// Vertical
links := []struct {
from, to int
}{
{0, 2},
{2, 7},
{4, 8},
{5, 9},
{6, 10},
{8, 11},
{10, 13},
{15, 16},
{11, 20},
{12, 17},
{18, 21},
}
for _, link := range links {
nodes[link.from].Down = nodes[link.to]
nodes[link.to].Up = nodes[link.from]
}
links = []struct {
from, to int
}{
{1, 2},
{3, 4},
{5, 6},
{7, 8},
{10, 15},
{15, 11},
{12, 13},
{14, 16},
{17, 18},
{19, 20},
}
for _, link := range links {
nodes[link.from].Right = nodes[link.to]
nodes[link.to].Left = nodes[link.from]
}
ret := &maze.SolvedMaze{
Maze: &maze.Maze{
Width: 11,
Height: 11,
Nodes: nodes,
},
Solution: []*maze.Node{
nodes[0],
nodes[2],
nodes[7],
nodes[8],
nodes[11],
nodes[15],
nodes[10],
nodes[13],
nodes[12],
nodes[17],
nodes[18],
nodes[21],
},
}
return ret
}

View File

@ -1,7 +1,46 @@
package writer package writer
import "maze-solver/maze" import (
"fmt"
"image/color"
"maze-solver/maze"
"github.com/mazznoer/colorgrad"
)
type Writer interface { type Writer interface {
Write(filename string, maze *maze.SolvedMaze) error Write() error
}
type WriterFactory struct {
Type string
Filename *string
PathChar, WallChar, SolutionChar *string
CellWidth, CellHeight *int
WallColor, PathColor color.Color
SolutionGradient colorgrad.Gradient
}
const (
_IMAGE = "image"
)
var TYPES = map[string]string{
".png": _IMAGE,
}
func (f *WriterFactory) Get(m *maze.SolvedMaze) Writer {
switch f.Type {
case _IMAGE:
return &ImageWriter{
Filename: *f.Filename,
Maze: m,
CellWidth: *f.CellWidth,
CellHeight: *f.CellHeight,
WallColor: f.WallColor,
PathColor: f.PathColor,
SolutionGradient: f.SolutionGradient,
}
}
panic(fmt.Sprintf("Unrecognized writer type %q", f.Type))
} }

156
main.go
View File

@ -1,25 +1,163 @@
package main package main
import ( import (
"errors"
"fmt"
"image/color"
"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"
"os"
"strings"
"github.com/akamensky/argparse"
"github.com/mazznoer/colorgrad"
) )
func main() { func main() {
input := "filename" readerFactory, writerFactory, solverFactory, ok := parse_arguments()
output := "filename"
reader := &reader.TextReader{PathChar: ' ', WallChar: '#'} if !ok {
writer := &writer.ImageWriter{} return
}
solver := &solver.Bfs{} defer utils.Timer("TOTAL", 1)()
reader := readerFactory.Get()
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")
solver := solverFactory.Get()
solved := solver.Solve(maze) solved := solver.Solve(maze)
err = writer.Write(output, solved)
utils.Check(err, "Couldn't write solved maze to %q", output) writer := writerFactory.Get(solved)
err = writer.Write()
utils.Check(err, "Couldn't write solved maze")
}
func parse_arguments() (*reader.ReaderFactory, *writer.WriterFactory, *solver.SolverFactory, bool) {
argparser := argparse.NewParser("maze-solver", "Solves the given maze (insane, right? who would've guessed?)")
var verboseLevel *int = argparser.FlagCounter("v", "verbose", &argparse.Options{
Help: `Verbose level of the solver
0: nothing printed to stdout
1: print the total time taken by the solver (time of the main() function)
2: prints the time the solving algorithm took to run
3: prints the time taken by each section (reader, solving algorithm, writer)`,
})
readerFactory := reader.ReaderFactory{}
writerFactory := writer.WriterFactory{}
solverFactory := solver.SolverFactory{}
readerFactory.Type = reader.TYPES[".png"]
readerFactory.Filename = argparser.String("i", "input", &argparse.Options{
Help: "Input file",
Default: "maze.png",
Validate: func(args []string) error {
var ok bool
extension := args[0][len(args[0])-4:]
readerFactory.Type, ok = reader.TYPES[extension]
if ok {
return nil
} else {
return errors.New(fmt.Sprintf("Filetype not recognized %q", extension))
}
},
})
writerFactory.Type = writer.TYPES[".png"]
writerFactory.Filename = argparser.String("o", "output", &argparse.Options{
Help: "Input file",
Default: "maze_sol.png",
Validate: func(args []string) error {
var ok bool
extension := args[0][len(args[0])-4:]
writerFactory.Type, ok = writer.TYPES[extension]
if ok {
return nil
} else {
return errors.New(fmt.Sprintf("Filetype not recognized %q", extension))
}
},
})
readerFactory.PathChar = argparser.String("", "path-char-in", &argparse.Options{
Help: "Character to represent the path in a input text file",
Default: " ",
Validate: func(args []string) error {
if len(args[0]) > 1 {
return errors.New("Character must a string of length 1")
}
return nil
},
})
readerFactory.WallChar = argparser.String("", "wall-char-in", &argparse.Options{
Help: "Character to represent the wall in a input text file",
Default: "#",
Validate: func(args []string) error {
if len(args[0]) > 1 {
return errors.New("Character must a string of length 1")
}
return nil
},
})
writerFactory.PathChar = argparser.String("", "path-char-out", &argparse.Options{
Help: "Character to represent the path in a output text file",
Default: " ",
Validate: func(args []string) error {
if len(args[0]) > 1 {
return errors.New("Character must a string of length 1")
}
return nil
},
})
writerFactory.WallChar = argparser.String("", "wall-char-out", &argparse.Options{
Help: "Character to represent the wall in a output text file",
Default: "#",
Validate: func(args []string) error {
if len(args[0]) > 1 {
return errors.New("Character must a string of length 1")
}
return nil
},
})
cellSizeIn := argparser.Int("", "cell-size-in", &argparse.Options{
Help: "Size of a cell (in pixels) for input file of image type",
Default: 20,
})
cellSizeOut := argparser.Int("", "cell-size-out", &argparse.Options{
Help: "Size of a cell (in pixels) for output file of image type",
Default: 20,
})
solverFactory.Type = argparser.Selector("a", "algo", solver.TYPES, &argparse.Options{
Help: fmt.Sprintf("Algorithm to solve the maze, avaiable options: %s", strings.Join(solver.TYPES, ", ")),
Default: solver.TYPES[0],
})
if err := argparser.Parse(os.Args); err != nil {
fmt.Println(argparser.Usage(err))
return nil, nil, nil, false
}
utils.VERBOSE_LEVEL = *verboseLevel
readerFactory.CellHeight, readerFactory.CellWidth = cellSizeIn, cellSizeIn
readerFactory.WallColor = color.RGBA{0, 0, 0, 255}
readerFactory.PathColor = color.RGBA{255, 255, 255, 255}
writerFactory.CellHeight, writerFactory.CellWidth = cellSizeOut, cellSizeOut
writerFactory.WallColor = color.RGBA{0, 0, 0, 255}
writerFactory.PathColor = color.RGBA{255, 255, 255, 255}
writerFactory.SolutionGradient = colorgrad.Warm()
return &readerFactory, &writerFactory, &solverFactory, true
} }

View File

@ -1,8 +1,31 @@
package maze package maze
import "math"
type Coordinates struct { type Coordinates struct {
X, Y int X, Y int
} }
func (c Coordinates) Distance(o Coordinates) float64 {
x, y := float64(o.X-c.X), float64(o.Y-c.Y)
if y == 0 {
if x < 0 {
return -x
}
return x
}
if x == 0 {
if y < 0 {
return -y
}
return y
}
return math.Sqrt(x*x + y*y)
}
type Node struct { type Node struct {
Coords Coordinates Coords Coordinates
Up, Down *Node Up, Down *Node
@ -20,11 +43,11 @@ func NewNode(coords Coordinates) *Node {
} }
type Maze struct { type Maze struct {
Width, Height uint Width, Height int
Nodes []*Node Nodes []*Node
} }
type SolvedMaze struct { type SolvedMaze struct {
Maze *Maze
Solution []*Node Solution []*Node
} }

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

@ -0,0 +1,144 @@
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)
raw_maze, err := reader.Read()
if err != nil {
return nil, err
}
ret := &maze.Maze{
Width: raw_maze.Width,
Height: raw_maze.Height,
Nodes: []*maze.Node{},
}
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 isCoordEligibleForNode(x, y, raw_maze) {
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 isCoordEligibleForNode(x int, y int, raw_maze *reader.RawMaze) bool {
return raw_maze.IsPath(x, y) &&
(raw_maze.IsWall(x-1, y) && raw_maze.IsPath(x+1, y) || // wall left, path right
raw_maze.IsPath(x-1, y) && raw_maze.IsWall(x+1, y) || // path left, wall right
raw_maze.IsPath(x, y-1) && (raw_maze.IsPath(x-1, y) || raw_maze.IsPath(x+1, y)) || // path above and not in vertical corridor
raw_maze.IsWall(x-1, y) && raw_maze.IsWall(x+1, y) && raw_maze.IsPath(x, y-1) && raw_maze.IsWall(x, y+1)) // wall to left, below, above and path above
}
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 {
return
}
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 (arrived at x=%v before hitting a wall)", node, x))
}
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) && x == node.Coords.X+1 {
return
}
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 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
}
}
}

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

@ -0,0 +1,550 @@
package parser
import (
"fmt"
"maze-solver/io/reader"
"maze-solver/maze"
"maze-solver/utils"
"testing"
)
func TestTextReadTrivial(t *testing.T) {
/* 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)
utils.AssertEqual(t, got.Width, 5, "Normal: width differ")
utils.AssertEqual(t, got.Height, 3, "Normal: height differ")
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) {
/* 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)
utils.AssertEqual(t, got.Width, 7, "Normal: width differ")
utils.AssertEqual(t, got.Height, 5, "Normal: height differ")
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 TestTextReadTrivialBiggerStaggered(t *testing.T) {
/* 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)
utils.AssertEqual(t, got.Width, 7, "Normal: width differ")
utils.AssertEqual(t, got.Height, 5, "Normal: height differ")
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 TestTextReadNormal(t *testing.T) {
/* normal.txt
##### #####
# # #
##### ### #
# # #
# # ##### #
# # #
### ### # #
# # # #
# ####### #
# # #
##### #####
Nodes are
#####0#####
#1 2#3 4#
##### ### #
#5 6#7 8#
# # ##### #
#9#A F B#
### ### # #
#C D#E G# #
# ####### #
#H I#J K#
#####L#####
*/
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)
{9, 5}, // B (11)
{1, 7}, // C (12)
{3, 7}, // D (13)
{5, 7}, // E (14)
{7, 5}, // F (15)
{7, 7}, // G (16)
{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 ----
// Vertical
links := []struct {
from, to int
}{
{0, 2},
{2, 7},
{4, 8},
{5, 9},
{6, 10},
{8, 11},
{10, 13},
{15, 16},
{11, 20},
{12, 17},
{18, 21},
}
for _, link := range links {
nodes[link.from].Down = nodes[link.to]
nodes[link.to].Up = nodes[link.from]
}
links = []struct {
from, to int
}{
{1, 2},
{3, 4},
{5, 6},
{7, 8},
{10, 15},
{15, 11},
{12, 13},
{14, 16},
{17, 18},
{19, 20},
}
for _, link := range links {
nodes[link.from].Right = nodes[link.to]
nodes[link.to].Left = nodes[link.from]
}
reader := &reader.TextReader{
Filename: "../../assets/normal.txt",
PathChar: ' ',
WallChar: '#',
}
got, err := Parse(reader)
utils.Check(err, "Couldn't create maze from %q", reader.Filename)
utils.AssertEqual(t, got.Width, 11, "Normal: width differ")
utils.AssertEqual(t, got.Height, 11, "Normal: height differ")
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 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 TestTextReadNormal2(t *testing.T) {
/* normal2.txt
####### #######
# # # #
### # ##### # #
# # # #
# ########### #
# # # #
### # # ##### #
# # # # #
# ### ### # # #
# # # # #
# ### # #######
# # # # #
# # ### ### # #
# # # #
####### #######
Nodes are
#######0#######
#1 2#3 4 5#B#
### # ##### # #
#6 7#8 9#A C#
# ########### #
#D I E#F G# #
### # # ##### #
#H J#K L#M N# #
# ### ### # # #
# #O P#Q R#S T#
# ### # #######
#U V#W# #X C Y#
# # ### ### # #
#Z#A B D#E#
#######F#######
*/
nodes := make([]*maze.Node, 42)
// ---- Node creation ----
coords := []struct{ x, y int }{
{7, 0}, // 0
{1, 1}, // 1
{3, 1}, // 2
{5, 1}, // 3
{7, 1}, // 4
{11, 1}, // 5
{1, 3}, // 6
{3, 3}, // 7
{5, 3}, // 8
{9, 3}, // 9
{11, 3}, // 10 (A)
{13, 1}, // 11 (B)
{13, 3}, // 12 (C)
{1, 5}, // 13 (D)
{5, 5}, // 14 (E)
{7, 5}, // 15 (F)
{11, 5}, // 16 (G)
{1, 7}, // 17 (H)
{3, 5}, // 18 (I)
{3, 7}, // 19 (J)
{5, 7}, // 20 (K)
{7, 7}, // 21 (L)
{9, 7}, // 22 (M)
{11, 7}, // 23 (N)
{3, 9}, // 24 (O)
{5, 9}, // 25 (P)
{7, 9}, // 26 (Q)
{9, 9}, // 27 (R)
{11, 9}, // 28 (S)
{13, 9}, // 29 (T)
{1, 11}, // 30 (U)
{3, 11}, // 31 (V)
{5, 11}, // 32 (W)
{9, 11}, // 33 (X)
{13, 11}, // 34 (Y)
{1, 13}, // 35 (Z)
{3, 13}, // 36 (AA)
{7, 13}, // 37 (AB)
{11, 11}, // 38 (AC)
{11, 13}, // 39 (AD)
{13, 13}, // 40 (AE)
{7, 14}, // 42 (AF)
}
for i, coord := range coords {
nodes[i] = maze.NewNode(maze.Coordinates{X: coord.x, Y: coord.y})
}
// ---- Node linking ----
// Vertical
links := []struct {
from, to int
}{
{0, 4},
{2, 7},
{3, 8},
{5, 10},
{11, 12},
{6, 13},
{12, 29},
{18, 19},
{14, 20},
{15, 21},
{17, 30},
{20, 25},
{22, 27},
{23, 28},
{25, 32},
{26, 37},
{30, 35},
{31, 36},
{38, 39},
{34, 40},
{37, 41},
}
for _, link := range links {
nodes[link.from].Down = nodes[link.to]
nodes[link.to].Up = nodes[link.from]
}
links = []struct {
from, to int
}{
{1, 2},
{3, 4},
{4, 5},
{6, 7},
{8, 9},
{10, 12},
{13, 18},
{18, 14},
{15, 16},
{17, 19},
{20, 21},
{22, 23},
{24, 25},
{26, 27},
{28, 29},
{30, 31},
{33, 38},
{38, 34},
{36, 37},
{37, 39},
}
for _, link := range links {
nodes[link.from].Right = nodes[link.to]
nodes[link.to].Left = nodes[link.from]
}
reader := &reader.TextReader{
Filename: "../../assets/normal2.txt",
PathChar: ' ',
WallChar: '#',
}
got, err := Parse(reader)
utils.Check(err, "Couldn't create maze from %q", reader.Filename)
utils.AssertEqual(t, got.Width, 15, "Normal 2: width differ")
utils.AssertEqual(t, got.Height, 15, "Normal 2: height differ")
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 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 && got != nil {
t.Fatalf("Somehow there is a node %s of %v, didn't want any", side, i)
}
if expected == nil && got == nil {
return
}
if expected != nil && 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)
}
}

63
solver/dfs.go Normal file
View File

@ -0,0 +1,63 @@
package solver
import (
"maze-solver/maze"
"maze-solver/utils"
)
type DFSSolver struct {
visited map[*maze.Node]bool
}
func (s *DFSSolver) Solve(m *maze.Maze) *maze.SolvedMaze {
defer utils.Timer("Turn left algorithm", 2)()
current, end := m.Nodes[0], m.Nodes[len(m.Nodes)-1]
s.visited = make(map[*maze.Node]bool, len(m.Nodes))
for _, node := range m.Nodes {
s.visited[node] = false
}
stack := make([]*maze.Node, 0, len(m.Nodes))
stack = append(stack, current)
for current != end {
s.visited[current] = true
left_visited, right_visited, up_visited, down_visited := s.wasVisited(current.Left), s.wasVisited(current.Right), s.wasVisited(current.Up), s.wasVisited(current.Down)
if left_visited && right_visited && up_visited && down_visited {
// dead end or no more visited nodes
stack = stack[:len(stack)-1]
current = stack[len(stack)-1]
} else {
if !left_visited {
current = current.Left
} else if !down_visited {
current = current.Down
} else if !right_visited {
current = current.Right
} else if !up_visited {
current = current.Up
}
stack = append(stack, current)
}
}
ret := &maze.SolvedMaze{
Maze: m,
Solution: stack,
}
return ret
}
func (s *DFSSolver) wasVisited(node *maze.Node) bool {
if node == nil {
return true
}
visited, _ := s.visited[node]
return visited
}

View File

@ -1,7 +1,30 @@
package solver package solver
import "maze-solver/maze" import (
"fmt"
"maze-solver/maze"
)
type Solver interface { type Solver interface {
Solve(*maze.Maze) *maze.SolvedMaze Solve(*maze.Maze) *maze.SolvedMaze
} }
type SolverFactory struct {
Type *string
}
const (
_TURN_LEFT = "turn-left"
)
var TYPES = []string{
_TURN_LEFT,
}
func (f *SolverFactory) Get() Solver {
switch *f.Type {
case _TURN_LEFT:
return &DFSSolver{}
}
panic(fmt.Sprintf("Unrecognized solver type %q", *f.Type))
}

View File

@ -1,6 +1,13 @@
package utils package utils
import "log" import (
"fmt"
"log"
"testing"
"time"
"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 +15,28 @@ 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
}
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...)
}
}
var VERBOSE_LEVEL int
func Timer(msg string, level int) func() {
start := time.Now()
return func() {
if level <= VERBOSE_LEVEL {
fmt.Printf("%-19s %12v\n", msg, time.Since(start))
}
}
}