Serving random state images, now need to tackle state transfer
This commit is contained in:
parent
8b536de45c
commit
6818d0ee01
@ -1,8 +1,14 @@
|
|||||||
package conway_test
|
package conway_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/png"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -104,7 +110,24 @@ func TestGameOfLifeCanDisplayItselfAsAString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGameOfLifeCanDisplayItselfAsAnImage(t *testing.T) {
|
func TestGameOfLifeCanDisplayItselfAsAnImage(t *testing.T) {
|
||||||
t.Fail()
|
game := newTestGameOfLife()
|
||||||
|
|
||||||
|
img, err := game.Image(1, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds := img.Bounds()
|
||||||
|
if bounds.Max.Y < 16 {
|
||||||
|
t.Errorf("image.Max.Y < 16!: %d", bounds.Max.Y)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if bounds.Max.X < 16 {
|
||||||
|
t.Errorf("image.Max.X < 16!: %d", bounds.Max.X)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewGameOfLifeHasCorrectDimensions(t *testing.T) {
|
func TestNewGameOfLifeHasCorrectDimensions(t *testing.T) {
|
||||||
@ -416,3 +439,128 @@ func TestGameStateCoordsAreNeverOutOfBounds(t *testing.T) {
|
|||||||
t.Errorf("(%d, %d) != %d: %d", oobX, oobY, newVal, cell.Value)
|
t.Errorf("(%d, %d) != %d: %d", oobX, oobY, newVal, cell.Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testResponseWriter struct {
|
||||||
|
Status int
|
||||||
|
Headers http.Header
|
||||||
|
Body []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *testResponseWriter) WriteHeader(status int) {
|
||||||
|
w.Status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *testResponseWriter) Header() http.Header {
|
||||||
|
return w.Headers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *testResponseWriter) Write(bytes []byte) (int, error) {
|
||||||
|
w.Body = append(w.Body, bytes[:]...)
|
||||||
|
return len(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestResponseWriter() *testResponseWriter {
|
||||||
|
return &testResponseWriter{
|
||||||
|
Body: []byte(""),
|
||||||
|
Headers: make(map[string][]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleWebGameRootReturnsIndexPageForGET(t *testing.T) {
|
||||||
|
req, err := http.NewRequest("GET", "http://localhost:9775/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w := newTestResponseWriter()
|
||||||
|
HandleWebGameRoot(w, req)
|
||||||
|
|
||||||
|
if w.Status != http.StatusOK {
|
||||||
|
t.Fail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
matched, err := regexp.MatchString("<h1>Conway's Game of Life</h1>", string(w.Body))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matched {
|
||||||
|
t.Fail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleWebGameRootReturns405ForNonGET(t *testing.T) {
|
||||||
|
req, err := http.NewRequest("OPTIONS", "http://localhost:9775/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w := newTestResponseWriter()
|
||||||
|
HandleWebGameRoot(w, req)
|
||||||
|
|
||||||
|
if w.Status != http.StatusMethodNotAllowed {
|
||||||
|
t.Fail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
matched, err := regexp.MatchString("Nope", string(w.Body))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matched {
|
||||||
|
t.Fail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleWebGameStateForGET(t *testing.T) {
|
||||||
|
req, err := http.NewRequest("GET", "http://localhost:9775/state.png?random=1", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w := newTestResponseWriter()
|
||||||
|
HandleWebGameState(w, req)
|
||||||
|
|
||||||
|
if w.Status != http.StatusOK {
|
||||||
|
t.Errorf("status != %d: %d", http.StatusOK, w.Status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
webState := &WebGameState{}
|
||||||
|
err = json.Unmarshal(w.Body, webState)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imgB64 := []byte(webState.Img)
|
||||||
|
imgBytes := make([]byte, len(imgB64))
|
||||||
|
|
||||||
|
_, err = base64.StdEncoding.Decode(imgBytes, imgB64)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := png.Decode(bytes.NewBuffer(imgBytes))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds := img.Bounds()
|
||||||
|
|
||||||
|
if bounds.Max.Y < 80 || bounds.Max.X < 80 {
|
||||||
|
t.Errorf("Image has incorrect bounds: %+v", bounds)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -11,10 +11,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
height = flag.Int("height", 40, "Game height")
|
height = flag.Int("height", 40, "Game height")
|
||||||
width = flag.Int("width", 80, "Game width")
|
width = flag.Int("width", 80, "Game width")
|
||||||
mutate = flag.Bool("mutate", false, "Mutate every other generation")
|
mutate = flag.Bool("mutate", false, "Mutate every other generation")
|
||||||
web = flag.Bool("web", false, "Run server for web-based game")
|
web = flag.Bool("web", false, "Run a web-based game.")
|
||||||
|
addr = flag.String("addr", ":9775",
|
||||||
|
"Address for server of web-based game (ignored if running a console game.)")
|
||||||
sleepMs = flag.Int("sleep.ms", 200,
|
sleepMs = flag.Int("sleep.ms", 200,
|
||||||
"Millisecond sleep interval per generation")
|
"Millisecond sleep interval per generation")
|
||||||
)
|
)
|
||||||
@ -28,7 +30,7 @@ func main() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if *web {
|
if *web {
|
||||||
retCode, err = RunWebGame(*height, *width, *sleepMs, *mutate)
|
retCode, err = RunWebGame(*addr, *height, *width, *sleepMs, *mutate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
|
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,7 @@ func (game *GameOfLife) String() string {
|
|||||||
return fmt.Sprintf("%s\n", game.State)
|
return fmt.Sprintf("%s\n", game.State)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *GameOfLife) Image(xMult, yMult int) (*image.Gray16, error) {
|
func (game *GameOfLife) Image(xMult, yMult int) (*image.Gray, error) {
|
||||||
return game.State.Image(xMult, yMult)
|
return game.State.Image(xMult, yMult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
@ -16,18 +18,18 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type GameStateCell struct {
|
type GameStateCell struct {
|
||||||
Value int
|
Value int `json:"value"`
|
||||||
X int
|
X int `json:"x"`
|
||||||
Y int
|
Y int `json:"y"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GameStateRow struct {
|
type GameStateRow struct {
|
||||||
Y int
|
Y int `json:"y"`
|
||||||
Cells []*GameStateCell
|
Cells []*GameStateCell `json:"cells"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GameState struct {
|
type GameState struct {
|
||||||
Rows []*GameStateRow
|
Rows []*GameStateRow `json:"rows"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGameState(height, width int) *GameState {
|
func NewGameState(height, width int) *GameState {
|
||||||
@ -175,6 +177,29 @@ func (state *GameState) String() string {
|
|||||||
return strings.Join(rows, "\n")
|
return strings.Join(rows, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *GameState) Image(xMult, yMult int) (*image.Gray16, error) {
|
func (state *GameState) Image(xMult, yMult int) (*image.Gray, error) {
|
||||||
return nil, errors.New("Not implemented!")
|
img := image.NewGray(image.Rect(0, 0, state.Height()*xMult, state.Width()*yMult))
|
||||||
|
draw.Draw(img, img.Bounds(), &image.Uniform{color.Gray16{0xff}},
|
||||||
|
image.ZP, draw.Src)
|
||||||
|
|
||||||
|
cells, err := state.Cells()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
black := &image.Uniform{color.Black}
|
||||||
|
white := &image.Uniform{color.White}
|
||||||
|
|
||||||
|
for cell := range cells {
|
||||||
|
color := white
|
||||||
|
if cell.Value == 1 {
|
||||||
|
color = black
|
||||||
|
}
|
||||||
|
|
||||||
|
square := image.Rect(cell.X*xMult, cell.Y*yMult,
|
||||||
|
(cell.X*xMult)+xMult, (cell.Y*yMult)+yMult)
|
||||||
|
draw.Draw(img, square, color, image.ZP, draw.Src)
|
||||||
|
}
|
||||||
|
|
||||||
|
return img, nil
|
||||||
}
|
}
|
||||||
|
209
conway/web.go
209
conway/web.go
@ -1,14 +1,215 @@
|
|||||||
package conway
|
package conway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"image/png"
|
||||||
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
type WebGameParams struct {
|
||||||
rand.Seed(time.Now().UTC().UnixNano())
|
Random bool
|
||||||
|
Height int
|
||||||
|
Width int
|
||||||
|
Xmul int
|
||||||
|
Ymul int
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunWebGame(height, width, sleepMs int, mutate bool) (int, error) {
|
type WebGameState struct {
|
||||||
return -1, nil
|
State *GameState `json:"state"`
|
||||||
|
Img string `json:"img"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var jQueryMinJs = []byte("")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rand.Seed(time.Now().UTC().UnixNano())
|
||||||
|
jqmin, err := base64.StdEncoding.DecodeString(JQUERY_MIN_JS)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to decode internal jquery.min.js")
|
||||||
|
}
|
||||||
|
jQueryMinJs = append(jQueryMinJs, jqmin...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebGameParams(q url.Values) *WebGameParams {
|
||||||
|
params := &WebGameParams{
|
||||||
|
Height: 50,
|
||||||
|
Width: 50,
|
||||||
|
Xmul: 10,
|
||||||
|
Ymul: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(q.Get("random")) > 0 {
|
||||||
|
params.Random = true
|
||||||
|
}
|
||||||
|
|
||||||
|
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([]byte(GAME_OF_LIFE_INDEX_HTML))
|
||||||
|
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([]byte(NORMALIZE_CSS))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.URL.Path == "/static/jquery.min.js" {
|
||||||
|
w.Header().Set("Content-Type", "text/javascript")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(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) {
|
||||||
|
if req.Method != "GET" {
|
||||||
|
handle405(req.Method, req.RequestURI, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
params := NewWebGameParams(q)
|
||||||
|
|
||||||
|
if params.Random {
|
||||||
|
game := NewGameOfLife(params.Height, params.Width)
|
||||||
|
err := game.ImportRandomState()
|
||||||
|
if err != nil {
|
||||||
|
handle500(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := game.Image(params.Xmul, params.Ymul)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handle500(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Serving random 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(http.StatusOK)
|
||||||
|
w.Write(wgsJson)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawState := q.Get("s")
|
||||||
|
if len(rawState) < 1 {
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte("Missing query param \"s\".\n"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//state := &GameState{}
|
||||||
|
//err := json.Unmarshal([]byte(rawState), state)
|
||||||
|
//if err != nil {
|
||||||
|
//w.Header().Set("Content-Type", "text/plain")
|
||||||
|
//w.WriteHeader(http.StatusBadRequest)
|
||||||
|
//w.Write([]byte(fmt.Sprintf("Invalid query param \"s\": %v\n", err)))
|
||||||
|
//return
|
||||||
|
//}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"state":null,"img":""}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
1849
conway/web_assets.go
Normal file
1849
conway/web_assets.go
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user