package main import ( "embed" "fmt" "html/template" "image/color" "image/png" "log" "net/http" "net/url" "strconv" "strings" "time" maze "tea.chunkbyte.com/kato/go-maze/maze" ) const ( 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 } //go:embed templates/index.html var templatesFS embed.FS var pageTmpl = template.Must(template.ParseFS(templatesFS, "templates/index.html")) type pageData struct { mazeParams MazeURL string Query string } func main() { 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 { http.Error(w, fmt.Sprintf("generate maze: %v", err), http.StatusBadRequest) return } options := maze.DefaultRenderOptions() 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} img, err := maze.RenderImage(grid, options) if err != nil { http.Error(w, fmt.Sprintf("render maze: %v", err), http.StatusBadRequest) return } 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 } }