package conway import ( "bytes" "compress/gzip" "encoding/base64" "encoding/json" "errors" "fmt" "image/png" "io/ioutil" "log" "math/rand" "net/http" "net/url" "strconv" "time" ) type WebGameParams struct { Height int Width int Xmul int Ymul int } type WebGameState struct { State *GameState `json:"s"` Img string `json:"i"` } type webAssetStrings struct { IndexHTML []byte NormalizeCSS []byte JqueryMinJS []byte } var WebAssets webAssetStrings func init() { rand.Seed(time.Now().UTC().UnixNano()) idxHt, err := fromGzB64(gameOfLifeIndexHtml) if err != nil { log.Fatal("Failed to decode internal index.html: ", err) } WebAssets.IndexHTML = idxHt normCss, err := fromGzB64(normalizeCss) if err != nil { log.Fatal("Failed to decode internal normalize.css: ", err) } WebAssets.NormalizeCSS = normCss jqmin, err := fromGzB64(jqueryMinJs) if err != nil { log.Fatal("Failed to decode internal jquery.min.js: ", err) } WebAssets.JqueryMinJS = jqmin } func fromGzB64(input string) ([]byte, error) { decoded, err := base64.StdEncoding.DecodeString(input) if err != nil { return []byte(""), err } buf := bytes.NewBuffer(decoded) zReader, err := gzip.NewReader(buf) if err != nil { return []byte(""), err } defer zReader.Close() gunzipped, err := ioutil.ReadAll(zReader) if err != nil { return []byte(""), err } return gunzipped, nil } func NewWebGameParams(q url.Values) *WebGameParams { params := &WebGameParams{ Height: 50, Width: 50, Xmul: 10, Ymul: 10, } h := q.Get("h") if len(h) > 0 { if i, err := strconv.Atoi(h); err == nil { params.Height = i } } w := q.Get("w") if len(w) > 0 { if i, err := strconv.Atoi(w); err == nil { params.Width = i } } xm := q.Get("xm") if len(xm) > 0 { if i, err := strconv.Atoi(xm); err == nil { params.Xmul = i } } ym := q.Get("ym") if len(ym) > 0 { if i, err := strconv.Atoi(ym); err == nil { params.Ymul = i } } return params } func handle405(method, uri string, w http.ResponseWriter) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte(fmt.Sprintf("Nope. %v not allowed at %v\n", method, uri))) } func handle500(err error, w http.ResponseWriter) { log.Println("ERROR:", err) w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(fmt.Sprintf("We exploded!: %v\n", err))) } func HandleWebGameRoot(w http.ResponseWriter, req *http.Request) { if req.Method != "GET" { handle405(req.Method, req.RequestURI, w) return } w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusOK) w.Write(WebAssets.IndexHTML) return } func HandleWebGameStatic(w http.ResponseWriter, req *http.Request) { if req.Method != "GET" { handle405(req.Method, req.RequestURI, w) return } if req.URL.Path == "/static/normalize.css" { w.Header().Set("Content-Type", "text/css") w.WriteHeader(http.StatusOK) w.Write(WebAssets.NormalizeCSS) return } if req.URL.Path == "/static/jquery.min.js" { w.Header().Set("Content-Type", "text/javascript") w.WriteHeader(http.StatusOK) w.Write(WebAssets.JqueryMinJS) return } w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusNotFound) w.Write([]byte(fmt.Sprintf("Don't have: %v\n", req.URL.Path))) } func HandleWebGameState(w http.ResponseWriter, req *http.Request) { switch req.Method { case "GET": handleWebGameStateGET(w, req) return case "POST": handleWebGameStatePOST(w, req) return default: handle405(req.Method, req.RequestURI, w) return } } func handleWebGameStateGET(w http.ResponseWriter, req *http.Request) { q := req.URL.Query() params := NewWebGameParams(q) game := NewGameOfLife(params.Height, params.Width) err := game.ImportRandomState() if err != nil { handle500(err, w) return } sendWebGameStateJSONPayload(w, http.StatusOK, game, params) } func handleWebGameStatePOST(w http.ResponseWriter, req *http.Request) { q := req.URL.Query() params := NewWebGameParams(q) defer func() { r := recover() if r != nil { handle500(errors.New(fmt.Sprintf("OH NOES! %v", r)), w) } }() body, err := ioutil.ReadAll(req.Body) if err != nil { handle500(err, w) } webGameState := &WebGameState{} err = json.Unmarshal(body, webGameState) if err != nil { handle500(err, w) } height, width := webGameState.State.Height(), webGameState.State.Width() game := NewGameOfLife(height, width) game.ImportState(webGameState.State) game.EvaluateGeneration() sendWebGameStateJSONPayload(w, http.StatusCreated, game, params) } func sendWebGameStateJSONPayload(w http.ResponseWriter, status int, game *GameOfLife, params *WebGameParams) { img, err := game.Image(params.Xmul, params.Ymul) if err != nil { handle500(err, w) return } log.Println("Serving state image.") var pngBuf bytes.Buffer err = png.Encode(&pngBuf, img) if err != nil { handle500(err, w) } imgB64 := base64.StdEncoding.EncodeToString(pngBuf.Bytes()) webGameState := &WebGameState{State: game.State, Img: imgB64} wgsJson, err := json.Marshal(webGameState) if err != nil { handle500(err, w) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) w.Write(wgsJson) } func RunWebGame(address string, height, width, sleepMs int, mutate bool) (int, error) { http.HandleFunc("/", HandleWebGameRoot) http.HandleFunc("/state", HandleWebGameState) http.HandleFunc("/static/", HandleWebGameStatic) fmt.Printf("Serving on %v\n", address) err := http.ListenAndServe(address, nil) if err != nil { return 1, err } return 0, nil }