HTTP Server just to serve the madness
This commit is contained in:
206
main.go
206
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
|
||||
}
|
||||
}
|
||||
|
||||
22
maze/maze.go
22
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
|
||||
|
||||
123
templates/index.html
Normal file
123
templates/index.html
Normal file
@@ -0,0 +1,123 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Maze Generator</title>
|
||||
<style>
|
||||
body { font-family: "Segoe UI", Tahoma, sans-serif; margin: 24px; background: #f4f6f8; color: #222; }
|
||||
.layout { display: grid; grid-template-columns: 320px 1fr; gap: 20px; align-items: start; }
|
||||
.panel { background: #fff; border: 1px solid #d8dee5; border-radius: 10px; padding: 16px; }
|
||||
label { display: block; margin: 10px 0 6px; font-size: 14px; }
|
||||
input[type="range"] { width: 100%; }
|
||||
.row { display: grid; grid-template-columns: 1fr 60px; align-items: center; gap: 8px; }
|
||||
.rgb { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
|
||||
.swatch { width: 100%; height: 34px; border-radius: 8px; border: 1px solid #ccc; }
|
||||
.submit-btn { margin-top: 14px; width: 100%; padding: 10px; border-radius: 8px; border: 0; background: #1b5fc6; color: #fff; font-weight: 600; }
|
||||
#shuffle_seed { margin: 0; width: 100%; padding: 8px; border-radius: 8px; border: 1px solid #c8d0da; background: #f7f9fb; color: #222; font-weight: 600; }
|
||||
img { max-width: 100%; border: 1px solid #d8dee5; border-radius: 10px; background: #fff; }
|
||||
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Maze Generator</h1>
|
||||
<div class="layout">
|
||||
<form class="panel" method="get" action="/">
|
||||
<h3>Maze</h3>
|
||||
<label>Width</label>
|
||||
<div class="row"><input id="w" type="range" min="3" max="151" step="2" name="w" value="{{.Width}}"><output id="w_out">{{.Width}}</output></div>
|
||||
|
||||
<label>Height</label>
|
||||
<div class="row"><input id="h" type="range" min="3" max="151" step="2" name="h" value="{{.Height}}"><output id="h_out">{{.Height}}</output></div>
|
||||
|
||||
<label>Scale (px per cell)</label>
|
||||
<div class="row"><input id="scale" type="range" min="2" max="24" step="1" name="scale" value="{{.Scale}}"><output id="scale_out">{{.Scale}}</output></div>
|
||||
|
||||
<label>Seed</label>
|
||||
<div class="row"><input id="seed" type="number" min="0" max="18446744073709551615" step="1" name="seed" value="{{.Seed}}"><button id="shuffle_seed" type="button">Random</button></div>
|
||||
|
||||
<label><input type="checkbox" name="solve" value="1" {{if .Highlight}}checked{{end}}> Highlight solution path</label>
|
||||
|
||||
<h3>Wall Color</h3>
|
||||
<div class="rgb">
|
||||
<div><label>R</label><input type="range" min="0" max="255" name="wall_r" value="{{.WallR}}"></div>
|
||||
<div><label>G</label><input type="range" min="0" max="255" name="wall_g" value="{{.WallG}}"></div>
|
||||
<div><label>B</label><input type="range" min="0" max="255" name="wall_b" value="{{.WallB}}"></div>
|
||||
</div>
|
||||
<div class="swatch" style="background: rgb({{.WallR}}, {{.WallG}}, {{.WallB}});"></div>
|
||||
|
||||
<h3>Path Color</h3>
|
||||
<div class="rgb">
|
||||
<div><label>R</label><input type="range" min="0" max="255" name="path_r" value="{{.PathR}}"></div>
|
||||
<div><label>G</label><input type="range" min="0" max="255" name="path_g" value="{{.PathG}}"></div>
|
||||
<div><label>B</label><input type="range" min="0" max="255" name="path_b" value="{{.PathB}}"></div>
|
||||
</div>
|
||||
<div class="swatch" style="background: rgb({{.PathR}}, {{.PathG}}, {{.PathB}});"></div>
|
||||
|
||||
<h3>Solution Color</h3>
|
||||
<div class="rgb">
|
||||
<div><label>R</label><input type="range" min="0" max="255" name="solve_r" value="{{.SolveR}}"></div>
|
||||
<div><label>G</label><input type="range" min="0" max="255" name="solve_g" value="{{.SolveG}}"></div>
|
||||
<div><label>B</label><input type="range" min="0" max="255" name="solve_b" value="{{.SolveB}}"></div>
|
||||
</div>
|
||||
<div class="swatch" style="background: rgb({{.SolveR}}, {{.SolveG}}, {{.SolveB}});"></div>
|
||||
|
||||
<button class="submit-btn" type="submit">Generate</button>
|
||||
</form>
|
||||
|
||||
<div class="panel">
|
||||
<img id="maze_img" src="{{.MazeURL}}" alt="Generated maze">
|
||||
<p><code id="maze_get">GET /maze.png?{{.Query}}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var form = document.querySelector("form");
|
||||
var img = document.getElementById("maze_img");
|
||||
var getCode = document.getElementById("maze_get");
|
||||
var seedInput = document.getElementById("seed");
|
||||
var randomSeedButton = document.getElementById("shuffle_seed");
|
||||
|
||||
[["w","w_out"],["h","h_out"],["scale","scale_out"]].forEach(function(pair) {
|
||||
var input = document.getElementById(pair[0]);
|
||||
var out = document.getElementById(pair[1]);
|
||||
if (!input || !out) return;
|
||||
input.addEventListener("input", function() { out.textContent = input.value; });
|
||||
});
|
||||
|
||||
function buildQuery() {
|
||||
return new URLSearchParams(new FormData(form)).toString();
|
||||
}
|
||||
|
||||
function refreshMaze() {
|
||||
var query = buildQuery();
|
||||
img.src = "/maze.png?" + query;
|
||||
getCode.textContent = "GET /maze.png?" + query;
|
||||
history.replaceState(null, "", "/?" + query);
|
||||
}
|
||||
|
||||
form.querySelectorAll("input").forEach(function(el) {
|
||||
el.addEventListener("input", refreshMaze);
|
||||
el.addEventListener("change", refreshMaze);
|
||||
});
|
||||
|
||||
function randomUint64String() {
|
||||
if (typeof BigInt !== "function") {
|
||||
return String(Date.now());
|
||||
}
|
||||
if (window.crypto && window.crypto.getRandomValues) {
|
||||
var values = new Uint32Array(2);
|
||||
window.crypto.getRandomValues(values);
|
||||
return ((BigInt(values[0]) << 32n) | BigInt(values[1])).toString();
|
||||
}
|
||||
var hi = Math.floor(Math.random() * 4294967296);
|
||||
var lo = Math.floor(Math.random() * 4294967296);
|
||||
return (BigInt(hi) << 32n | BigInt(lo)).toString();
|
||||
}
|
||||
|
||||
randomSeedButton.addEventListener("click", function() {
|
||||
seedInput.value = randomUint64String();
|
||||
refreshMaze();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user