hexops / vecty

Vecty lets you build responsive and dynamic web frontends in Go using WebAssembly, competing with modern web frameworks like React & VueJS.
BSD 3-Clause "New" or "Revised" License
2.79k stars 144 forks source link

Add snake videogame example. #289

Closed soypat closed 2 years ago

soypat commented 2 years ago

I'd like to contribute a very basic Snake game example to the available examples.

```go package main import ( "fmt" "math/rand" "syscall/js" "time" "snek/svg" "github.com/hexops/vecty" "github.com/hexops/vecty/elem" "github.com/hexops/vecty/event" _ "github.com/hexops/vecty/event" ) const ( svgPix = 6 svgH = 20 svgW = 30 ) type direction int const ( dirNone direction = iota dirUp dirDown dirLeft dirRight ) type msg struct { dir direction } type Model struct { vecty.Core state gamestate snek []vec food vec msgs chan msg } func (m *Model) update(action msg) { var facing direction // Set current direction if non-keydown update. switch { case m.state == lose: //Do nothing after loss return case m.snek[0].x == m.snek[1].x && m.snek[0].y > m.snek[1].y: // snake head below body facing = dirDown case m.snek[0].x == m.snek[1].x && m.snek[0].y < m.snek[1].y: // snake head above body facing = dirUp case m.snek[0].y == m.snek[1].y && m.snek[0].x < m.snek[1].x: // snake head left of body facing = dirLeft case m.snek[0].y == m.snek[1].y && m.snek[0].x > m.snek[1].x: // snake head right of body facing = dirRight default: panic("impossible position") } if action.dir == dirNone { action.dir = facing } m.state = play head := m.snek[0] switch action.dir { // TODO(soypat): need to prevent going in opposite direction of facing dir. case dirDown: m.snek = append([]vec{{head.x, head.y + 1}}, m.snek...) case dirUp: m.snek = append([]vec{{head.x, head.y - 1}}, m.snek...) case dirLeft: m.snek = append([]vec{{head.x - 1, head.y}}, m.snek...) case dirRight: m.snek = append([]vec{{head.x + 1, head.y}}, m.snek...) } newHead := m.snek[0] for i, v := range m.snek[1:] { if v.x == newHead.x && v.y == newHead.y { log("lose bodypart/vec", i, v, "to head position", newHead) m.state = lose } } if newHead == m.food { m.food = vec{rand.Intn(svgW), rand.Intn(svgH)} log("new food at", m.food) } else { m.snek = m.snek[:len(m.snek)-1] } log(m.snek) vecty.Rerender(m) } func (m *Model) Render() vecty.ComponentOrHTML { s := &svg.SVG{ Height: svgH * svgPix, Width: svgW * svgPix, } food := svg.NewRect("blue", m.food.x*svgPix, m.food.y*svgPix, svgPix, svgPix) s.Add(food) for i := range m.snek { r := svg.NewRect("red", m.snek[i].x*svgPix, m.snek[i].y*svgPix, svgPix, svgPix) s.Add(r) } log(s) return elem.Body( vecty.If(m.state == play, elem.Paragraph( vecty.Markup(vecty.UnsafeHTML("hello")), )), vecty.If(m.state == lose, elem.Paragraph( vecty.Markup(vecty.UnsafeHTML("YOU LOST")), )), vecty.Markup( event.KeyUp(func(e *vecty.Event) { action := msg{} switch e.Value.Get("key").String() { case "ArrowUp": action.dir = dirUp case "ArrowDown": action.dir = dirDown case "ArrowLeft": action.dir = dirLeft case "ArrowRight": action.dir = dirRight default: log("do nothing, invalid keydown") return } log("update ready") m.msgs <- action }), ), elem.Div( s.Render(), ), ) } type vec struct { x, y int } type gamestate int const ( play gamestate = iota lose ) func main() { vecty.SetTitle("GopherJS • TodoMVC") p := &Model{ snek: []vec{ {svgW / 2, svgH / 2}, {svgW / 2, svgH/2 + 1}, {svgW / 2, svgH/2 + 2}, }, food: vec{svgW / 4, svgH / 4}, msgs: make(chan msg), } go func() { // Update every second. ticker := time.NewTicker(time.Second) for range ticker.C { p.msgs <- msg{} } }() vecty.RenderInto("body", p) for a := range p.msgs { p.update(a) } } func log(a ...interface{}) { js.Global().Get("console").Call("log", fmt.Sprint(a...)) } ``` ```go package svg import ( "syscall/js" "github.com/hexops/vecty" ) type SVG struct { vecty.Core Height, Width int things []Obj } func log(a ...interface{}) { js.Global().Get("console").Call("log", a...) } func (s *SVG) Render() vecty.ComponentOrHTML { var childs []vecty.MarkupOrChild childs = append(childs, vecty.Markup( vecty.Namespace("http://www.w3.org/2000/svg"), vecty.Attribute("width", s.Width), vecty.Attribute("height", s.Height), vecty.Attribute("style", "border: thick solid black"), )) for i := range s.things { childs = append(childs, s.things[i].HTML()) } return vecty.Tag("svg", childs...) } func (s *SVG) Add(objs ...Obj) { s.things = append(s.things, objs...) } func NewRect(color string, coordx, coordy, height, width int) Rect { return Rect{ color: color, coordx: coordx, coordy: coordy, width: width, height: height, } } type Obj interface { HTML() *vecty.HTML } type Rect struct { color string coordx, coordy int width, height int } func (o Rect) HTML() *vecty.HTML { return vecty.Tag("rect", vecty.Markup( vecty.Namespace("http://www.w3.org/2000/svg"), vecty.Attribute("x", o.coordx), vecty.Attribute("y", o.coordy), vecty.Attribute("width", o.width), vecty.Attribute("height", o.height), vecty.Attribute("style", "fill:"+o.color+";"), ), ) } ```
slimsag commented 2 years ago

Pretty cool! How about you publish this in a repository of your own somewhere, and we link to it from the README.md in the examples dir?

soypat commented 2 years ago

https://github.com/soypat/vecty-examples

Alright! Also notice: You should change wasmserve installation instructions to the new installation method for installing Go packages.

    go install github.com/hajimehoshi/wasmserve@latest
soypat commented 2 years ago

Shall I make the PR? With or without the installation instruction fix?

slimsag commented 2 years ago

A PR for both would be great!