box-o-sand/conway/web.go

273 lines
5.5 KiB
Go
Raw Normal View History

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(GAME_OF_LIFE_INDEX_HTML)
if err != nil {
log.Fatal("Failed to decode internal index.html: ", err)
}
WebAssets.IndexHTML = idxHt
normCss, err := fromGzB64(NORMALIZE_CSS)
if err != nil {
log.Fatal("Failed to decode internal normalize.css: ", err)
}
WebAssets.NormalizeCSS = normCss
jqmin, err := fromGzB64(JQUERY_MIN_JS)
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
}