├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── index.html ├── serve.go └── snake_wasm.go /.gitignore: -------------------------------------------------------------------------------- 1 | snake.wasm 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Brad Fitzpatrick 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | GOOS=js GOARCH=wasm go build -o snake.wasm . 3 | go run serve.go 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snake 2 | 3 | A simple Snake game in Go WebAssembly, loading each grid tile from a 4 | different IP address for fun, while setting up a new network: 5 | 6 | http://snake.126.49.198.in-addr.arpa/ 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bradfitz/snake 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /serve.go: -------------------------------------------------------------------------------- 1 | //go:build !wasm 2 | // +build !wasm 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | _ "embed" 9 | "image" 10 | "image/png" 11 | "log" 12 | "net/http" 13 | "os" 14 | "path/filepath" 15 | "runtime" 16 | "sync" 17 | "time" 18 | ) 19 | 20 | //go:embed index.html 21 | var indexHTML []byte 22 | 23 | //go:embed snake.wasm 24 | var snakeWASM []byte 25 | 26 | func main() { 27 | log.Printf("listening on :9090") 28 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 29 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 30 | w.Write(indexHTML) 31 | }) 32 | http.HandleFunc("/snake.wasm", func(w http.ResponseWriter, r *http.Request) { 33 | w.Header().Set("Content-Type", "application/wasm") 34 | w.Write(snakeWASM) 35 | }) 36 | http.HandleFunc("/wasm_exec.js", func(w http.ResponseWriter, r *http.Request) { 37 | f, err := os.Open(filepath.Join(runtime.GOROOT(), "misc/wasm/wasm_exec.js")) 38 | if err != nil { 39 | log.Print(err) 40 | http.Error(w, "can't find wasm_exec.js", 500) 41 | return 42 | } 43 | defer f.Close() 44 | var modTime time.Time 45 | if fi, err := f.Stat(); err == nil { 46 | modTime = fi.ModTime() 47 | } 48 | http.ServeContent(w, r, "wasm_exec.js", modTime, f) 49 | }) 50 | http.Handle("/apple.png", redPNG) 51 | http.Handle("/white.png", whitePNG) 52 | http.Handle("/black.png", blackPNG) 53 | err := http.ListenAndServe(":9090", nil) 54 | if err != nil { 55 | log.Fatal(err) 56 | return 57 | } 58 | } 59 | 60 | type lazyPNG struct { 61 | r, g, b uint8 62 | 63 | o sync.Once 64 | png []byte // png bytes 65 | } 66 | 67 | func (p *lazyPNG) gen() { 68 | im := image.NewNRGBA(image.Rect(0, 0, 20, 20)) 69 | for i := 0; i < len(im.Pix); i += 4 { 70 | im.Pix[i+0] = p.r 71 | im.Pix[i+1] = p.g 72 | im.Pix[i+2] = p.b 73 | im.Pix[i+3] = 0xff 74 | } 75 | var buf bytes.Buffer 76 | png.Encode(&buf, im) 77 | p.png = buf.Bytes() 78 | } 79 | 80 | func (p *lazyPNG) ServeHTTP(w http.ResponseWriter, r *http.Request) { 81 | p.o.Do(p.gen) 82 | w.Header().Set("Content-Type", "image/png") 83 | w.Header().Set("Expires", time.Now().Add(time.Hour).UTC().Format(http.TimeFormat)) 84 | w.Header().Set("Connection", "close") 85 | w.Write(p.png) 86 | } 87 | 88 | var ( 89 | blackPNG = &lazyPNG{r: 0, g: 0, b: 0} 90 | redPNG = &lazyPNG{r: 209, g: 8, b: 8} 91 | whitePNG = &lazyPNG{r: 255, g: 255, b: 255} 92 | ) 93 | -------------------------------------------------------------------------------- /snake_wasm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "container/list" 6 | "fmt" 7 | "log" 8 | "math/rand" 9 | "syscall/js" 10 | "time" 11 | ) 12 | 13 | type gridState byte 14 | 15 | const ( 16 | Empty gridState = 0 17 | Black gridState = 'B' 18 | Apple gridState = 'A' 19 | ) 20 | 21 | type coord struct { 22 | x, y uint8 23 | } 24 | 25 | type gameState struct { 26 | board [16][16]gridState 27 | trail *list.List // of coord 28 | dx, dy int8 29 | maxLen int 30 | speed time.Duration 31 | } 32 | 33 | func newGame() *gameState { 34 | st := &gameState{ 35 | maxLen: 4, 36 | trail: list.New(), 37 | speed: time.Second / 2, 38 | } 39 | st.board[0][0] = Black 40 | st.board[15][15] = Black 41 | st.ensnake(8, 8) 42 | st.dx = 1 43 | st.setApple() 44 | return st 45 | } 46 | 47 | func (st *gameState) handleClick(click coord) { 48 | cur := st.trail.Front().Value.(coord) 49 | switch { 50 | case st.dx == 0: // moving up or down, so turn left or right 51 | if click.x < cur.x { // go left 52 | st.dx, st.dy = -1, 0 53 | return 54 | } 55 | if click.x > cur.x { // go right 56 | st.dx, st.dy = 1, 0 57 | return 58 | } 59 | case st.dy == 0: // moving left or right, so turn up or down 60 | if click.y < cur.y { // go up 61 | st.dx, st.dy = 0, -1 62 | return 63 | } 64 | if click.y > cur.y { // go down 65 | st.dx, st.dy = 0, 1 66 | return 67 | } 68 | } 69 | } 70 | 71 | func (st *gameState) tick() (stillAlive bool) { 72 | cur := st.trail.Front().Value.(coord) 73 | nx, ny := int8(cur.x)+st.dx, int8(cur.y)+st.dy 74 | if nx < 0 || nx > 15 || ny < 0 || ny > 15 { 75 | log.Printf("oob; dead") 76 | return false 77 | } 78 | at := st.board[nx][ny] 79 | if at == Black { 80 | log.Printf("new (%v, %v) is black; dead", nx, ny) 81 | return false 82 | } 83 | // log.Printf("moved to (%v, %v)", nx, ny) 84 | if at == Apple { 85 | st.maxLen += 2 86 | st.speed -= 20 * time.Millisecond 87 | const min = 50 * time.Millisecond 88 | if st.speed < min { 89 | st.speed = min 90 | } 91 | log.Printf("nom; now speed %v, maxLen %v", st.speed, st.maxLen) 92 | } 93 | st.ensnake(uint8(nx), uint8(ny)) 94 | for st.trail.Len() > st.maxLen { 95 | back := st.trail.Remove(st.trail.Back()).(coord) 96 | x, y := back.x, back.y 97 | st.board[x][y] = Empty 98 | if e := doc.Call("getElementById", fmt.Sprintf("p%d", y*16+x)); !e.IsNull() { 99 | e.Set("src", fmt.Sprintf("http://198.49.126.%d/white.png", y*16+x)) 100 | } 101 | } 102 | if at == Apple { 103 | st.setApple() 104 | } 105 | return true 106 | } 107 | 108 | func (st *gameState) setApple() { 109 | for { 110 | x, y := uint8(rand.Intn(16)), uint8(rand.Intn(16)) 111 | if st.board[x][y] != Empty { 112 | continue 113 | } 114 | st.board[x][y] = Apple 115 | if e := doc.Call("getElementById", fmt.Sprintf("p%d", y*16+x)); !e.IsNull() { 116 | e.Set("src", fmt.Sprintf("http://198.49.126.%d/apple.png", y*16+x)) 117 | } 118 | return 119 | } 120 | } 121 | 122 | func (st *gameState) ensnake(x, y uint8) { 123 | st.trail.PushFront(coord{x, y}) 124 | st.board[x][y] = Black 125 | if e := doc.Call("getElementById", fmt.Sprintf("p%d", y*16+x)); !e.IsNull() { 126 | e.Set("src", fmt.Sprintf("http://198.49.126.%d/black.png", y*16+x)) 127 | } 128 | } 129 | 130 | func (st *gameState) initDOM() { 131 | board := doc.Call("getElementById", "board") 132 | var buf bytes.Buffer 133 | buf.WriteString("

snake/24

IP addresses are precious. Make the most of them. Each grid pixel below is served from a different IP.

(This might seem like a waste, but this port wasn't being used anyway?)

Instructions: use arrows (or WASD, or clicking/touching direction) to move the snake and eat them apples.

") 134 | oct := -1 135 | for y := 0; y < 16; y++ { 136 | for x := 0; x < 16; x++ { 137 | oct++ 138 | blocked := oct == 0 || oct == 255 139 | ip := oct 140 | if blocked { 141 | ip = 1 142 | } 143 | color := "white" 144 | if blocked { 145 | color = "black" 146 | } else { 147 | switch st.board[x][y] { 148 | case Black: 149 | color = "black" 150 | case Apple: 151 | color = "apple" 152 | } 153 | } 154 | fmt.Fprintf(&buf, "", oct, ip, color) 155 | } 156 | buf.WriteString("
\n") 157 | } 158 | buf.WriteString("
Brad Fitzpatrick, <@bradfitz> [discuss]") 159 | board.Set("innerHTML", buf.String()) 160 | 161 | for y := 0; y < 16; y++ { 162 | for x := 0; x < 16; x++ { 163 | oct := y*16 + x 164 | x, y := x, y 165 | doc.Call("getElementById", fmt.Sprintf("p%d", oct)).Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 166 | select { 167 | case clickc <- coord{uint8(x), uint8(y)}: 168 | //log.Printf("clicked (%v, %v)", x, y) 169 | default: 170 | log.Printf("clicked (%v, %v); ignored", x, y) 171 | } 172 | return true 173 | })) 174 | } 175 | } 176 | } 177 | 178 | var doc = js.Global().Get("document") 179 | 180 | var clickc = make(chan coord, 1) 181 | 182 | func main() { 183 | log.SetFlags(0) 184 | log.Printf("snake/24") 185 | rand.Seed(time.Now().UnixNano()) 186 | doc.Set("bgColor", "#cccccc") 187 | doc.Get("body").Set("innerHTML", `
`) 188 | 189 | st := newGame() 190 | st.initDOM() 191 | 192 | keyCode := make(chan string, 10) 193 | doc.Call("addEventListener", "keydown", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 194 | e := args[0] 195 | keyCode <- e.Get("code").String() 196 | return true 197 | })) 198 | 199 | timer := time.NewTimer(st.speed) 200 | for { 201 | select { 202 | case key := <-keyCode: 203 | switch key { 204 | case "KeyW", "ArrowUp": 205 | st.dx, st.dy = 0, -1 206 | case "KeyS", "ArrowDown": 207 | st.dx, st.dy = 0, 1 208 | case "KeyA", "ArrowLeft": 209 | st.dx, st.dy = -1, 0 210 | case "KeyD", "ArrowRight": 211 | st.dx, st.dy = 1, 0 212 | default: 213 | log.Printf("unknown key %q", key) 214 | continue 215 | } 216 | log.Printf("key %q, dx=%v, dy=%v", key, st.dx, st.dy) 217 | case click := <-clickc: 218 | st.handleClick(click) 219 | case <-timer.C: 220 | if !st.tick() { 221 | js.Global().Call("alert", "you lose") 222 | st = newGame() 223 | st.initDOM() 224 | } 225 | timer = time.NewTimer(st.speed) 226 | } 227 | } 228 | } 229 | --------------------------------------------------------------------------------