diff --git a/main.go b/main.go index 9427ef0..601bfe5 100644 --- a/main.go +++ b/main.go @@ -2,29 +2,217 @@ package main import ( "fmt" + "html/template" + "image/color" + "image/png" "log" + "net/http" + "net/url" + "strconv" + "strings" + "time" "test-maze/maze" ) const ( - sizeX = 21 - sizeY = 21 + defaultWidth = 41 + defaultHeight = 41 + defaultScale = 8 ) +type mazeParams struct { + Width int + Height int + Scale int + Seed uint64 + Highlight bool + + WallR uint8 + WallG uint8 + WallB uint8 + + PathR uint8 + PathG uint8 + PathB uint8 + + SolveR uint8 + SolveG uint8 + SolveB uint8 +} + +var pageTmpl = template.Must(template.ParseFiles("templates/index.html")) + +type pageData struct { + mazeParams + MazeURL string + Query string +} + func main() { - grid, err := maze.Generate(sizeX, sizeY) + http.HandleFunc("/", handleIndex) + http.HandleFunc("/maze.png", handleMazePNG) + + addr := ":8080" + log.Printf("listening on http://localhost%s", addr) + if err := http.ListenAndServe(addr, nil); err != nil { + log.Fatal(err) + } +} + +func handleIndex(w http.ResponseWriter, r *http.Request) { + params := parseParams(r.URL.Query()) + query := params.encode().Encode() + + data := pageData{ + mazeParams: params, + MazeURL: "/maze.png?" + query, + Query: query, + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := pageTmpl.Execute(w, data); err != nil { + http.Error(w, "template rendering failed", http.StatusInternalServerError) + } +} + +func handleMazePNG(w http.ResponseWriter, r *http.Request) { + params := parseParams(r.URL.Query()) + + grid, err := maze.GenerateWithSeed(params.Width, params.Height, params.Seed) if err != nil { - log.Fatalf("generate maze: %v", err) + http.Error(w, fmt.Sprintf("generate maze: %v", err), http.StatusBadRequest) + return } options := maze.DefaultRenderOptions() - options.HighlightPath = true - options.Scale = 5 + options.Scale = params.Scale + options.HighlightPath = params.Highlight + options.WallColor = color.RGBA{R: params.WallR, G: params.WallG, B: params.WallB, A: 255} + options.PathColor = color.RGBA{R: params.PathR, G: params.PathG, B: params.PathB, A: 255} + options.SolutionColor = color.RGBA{R: params.SolveR, G: params.SolveG, B: params.SolveB, A: 255} - if err := maze.SavePNG(grid, "maze.png", options); err != nil { - log.Fatalf("save maze image: %v", err) + img, err := maze.RenderImage(grid, options) + if err != nil { + http.Error(w, fmt.Sprintf("render maze: %v", err), http.StatusBadRequest) + return } - fmt.Println("Saved maze to maze.png") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Content-Type", "image/png") + if err := png.Encode(w, img); err != nil { + http.Error(w, "encoding png failed", http.StatusInternalServerError) + } +} + +func parseParams(q url.Values) mazeParams { + defaults := maze.DefaultRenderOptions() + defaultSeed := uint64(time.Now().UnixNano()) + out := mazeParams{ + Width: parseOddInt(q.Get("w"), defaultWidth, 3, 151), + Height: parseOddInt(q.Get("h"), defaultHeight, 3, 151), + Scale: parseInt(q.Get("scale"), defaultScale, 2, 24), + Seed: parseUint64(q.Get("seed"), defaultSeed), + Highlight: parseBool(q.Get("solve")), + WallR: defaults.WallColor.R, + WallG: defaults.WallColor.G, + WallB: defaults.WallColor.B, + PathR: defaults.PathColor.R, + PathG: defaults.PathColor.G, + PathB: defaults.PathColor.B, + SolveR: defaults.SolutionColor.R, + SolveG: defaults.SolutionColor.G, + SolveB: defaults.SolutionColor.B, + } + + out.WallR = parseUint8(q.Get("wall_r"), out.WallR) + out.WallG = parseUint8(q.Get("wall_g"), out.WallG) + out.WallB = parseUint8(q.Get("wall_b"), out.WallB) + out.PathR = parseUint8(q.Get("path_r"), out.PathR) + out.PathG = parseUint8(q.Get("path_g"), out.PathG) + out.PathB = parseUint8(q.Get("path_b"), out.PathB) + out.SolveR = parseUint8(q.Get("solve_r"), out.SolveR) + out.SolveG = parseUint8(q.Get("solve_g"), out.SolveG) + out.SolveB = parseUint8(q.Get("solve_b"), out.SolveB) + + return out +} + +func (p mazeParams) encode() url.Values { + v := url.Values{} + v.Set("w", strconv.Itoa(p.Width)) + v.Set("h", strconv.Itoa(p.Height)) + v.Set("scale", strconv.Itoa(p.Scale)) + v.Set("seed", strconv.FormatUint(p.Seed, 10)) + if p.Highlight { + v.Set("solve", "1") + } + v.Set("wall_r", strconv.Itoa(int(p.WallR))) + v.Set("wall_g", strconv.Itoa(int(p.WallG))) + v.Set("wall_b", strconv.Itoa(int(p.WallB))) + v.Set("path_r", strconv.Itoa(int(p.PathR))) + v.Set("path_g", strconv.Itoa(int(p.PathG))) + v.Set("path_b", strconv.Itoa(int(p.PathB))) + v.Set("solve_r", strconv.Itoa(int(p.SolveR))) + v.Set("solve_g", strconv.Itoa(int(p.SolveG))) + v.Set("solve_b", strconv.Itoa(int(p.SolveB))) + return v +} + +func parseInt(raw string, fallback, min, max int) int { + n, err := strconv.Atoi(raw) + if err != nil { + return fallback + } + if n < min { + return min + } + if n > max { + return max + } + return n +} + +func parseOddInt(raw string, fallback, min, max int) int { + n := parseInt(raw, fallback, min, max) + if n%2 == 0 { + if n == max { + n-- + } else { + n++ + } + } + return n +} + +func parseUint8(raw string, fallback uint8) uint8 { + n, err := strconv.Atoi(raw) + if err != nil { + return fallback + } + if n < 0 { + return 0 + } + if n > 255 { + return 255 + } + return uint8(n) +} + +func parseUint64(raw string, fallback uint64) uint64 { + n, err := strconv.ParseUint(raw, 10, 64) + if err != nil { + return fallback + } + return n +} + +func parseBool(raw string) bool { + raw = strings.TrimSpace(strings.ToLower(raw)) + switch raw { + case "1", "true", "t", "yes", "y", "on": + return true + default: + return false + } } diff --git a/maze/maze.go b/maze/maze.go index 7949a6e..825024b 100644 --- a/maze/maze.go +++ b/maze/maze.go @@ -26,6 +26,18 @@ var ( // The algorithm is randomized depth-first carving with one entrance on the top border // and one exit on the bottom border. func Generate(width, height int) (Grid, error) { + return generate(width, height, rand.IntN) +} + +// GenerateWithSeed creates a maze grid with deterministic randomness. +// +// The same width, height, and seed always produce the same maze. +func GenerateWithSeed(width, height int, seed uint64) (Grid, error) { + rng := rand.New(rand.NewPCG(seed, seed^0x9e3779b97f4a7c15)) + return generate(width, height, rng.IntN) +} + +func generate(width, height int, intN func(int) int) (Grid, error) { if width <= 0 || height <= 0 { return nil, ErrInvalidDimensions } @@ -51,7 +63,7 @@ func Generate(width, height int) (Grid, error) { for len(stackX) > 0 { last := len(stackX) - 1 x, y := stackX[last], stackY[last] - dirs := shuffledDirections() + dirs := shuffledDirections(intN) carved := false for _, d := range dirs { @@ -85,7 +97,7 @@ func Generate(width, height int) (Grid, error) { } } if len(topChoices) > 0 { - entranceX := topChoices[rand.IntN(len(topChoices))] + entranceX := topChoices[intN(len(topChoices))] grid[0][entranceX] = 1 } @@ -96,7 +108,7 @@ func Generate(width, height int) (Grid, error) { } } if len(bottomChoices) > 0 { - exitX := bottomChoices[rand.IntN(len(bottomChoices))] + exitX := bottomChoices[intN(len(bottomChoices))] grid[height-1][exitX] = 1 } @@ -227,10 +239,10 @@ func directionDelta(direction uint8) (dx, dy int) { } } -func shuffledDirections() [4]uint8 { +func shuffledDirections(intN func(int) int) [4]uint8 { dirs := [4]uint8{0, 1, 2, 3} for i := 3; i > 0; i-- { - j := rand.IntN(i + 1) + j := intN(i + 1) dirs[i], dirs[j] = dirs[j], dirs[i] } return dirs diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..0d3de7f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,123 @@ + + +
+ + +GET /maze.png?{{.Query}}