diff --git a/cave.go b/cave.go index 5116c7b..cfbff0a 100644 --- a/cave.go +++ b/cave.go @@ -17,7 +17,6 @@ import ( type OnSubmiter interface { OnSubmit(name string, form map[string]string) } - type OnClicker interface { OnClick(name string) } @@ -25,12 +24,13 @@ type Renderer interface { Render() string } -// MAYBE type OnMounter interface { - OnMount(req *http.Request) + OnMount(Session) } -type Form struct{} +type Session interface { + Render() +} type Cave struct { template *template.Template diff --git a/cave_test.go b/cave_test.go index d24b4a4..8e5bd09 100644 --- a/cave_test.go +++ b/cave_test.go @@ -82,7 +82,7 @@ func TestCaveBasic(t *testing.T) { func TestCaveDiff(t *testing.T) { tc := NewTestComponent() - ls, err := newLiveComponent(tc) + ls, err := newLiveComponent(tc, nil) if err != nil { t.Fatal(err) } diff --git a/examples/connect4/main.go b/examples/connect4/main.go index f8da59b..c28252e 100644 --- a/examples/connect4/main.go +++ b/examples/connect4/main.go @@ -3,6 +3,8 @@ package main import ( "fmt" "log" + "math/rand" + "strconv" "strings" "github.com/gin-gonic/gin" @@ -10,16 +12,19 @@ import ( ) func main() { + + gm := GameMaster{} + gm.startGameMaster() + r := gin.Default() cavern := cave.New() if err := cavern.AddTemplateFile("main", "layout.html"); err != nil { log.Fatal(err) } - cavern.AddComponent("main", NewConnect4) + cavern.AddComponent("main", gm.NewConnect4) r.Use(func(c *gin.Context) { - _, ok := c.Request.URL.Query()["cavews"] - if ok { + if _, ok := c.Request.URL.Query()["cavews"]; ok { cavern.ServeWS(c.Writer, c.Request) c.Abort() } @@ -30,22 +35,54 @@ func main() { panic(err) } }) - r.GET("/bundle.js", func(c *gin.Context) { - cavern.ServeJS(c.Writer, c.Request) - }) - r.GET("/bundle.js.map", func(c *gin.Context) { - cavern.ServeJS(c.Writer, c.Request) - }) + r.GET("/bundle.js", func(c *gin.Context) { cavern.ServeJS(c.Writer, c.Request) }) + r.GET("/bundle.js.map", func(c *gin.Context) { cavern.ServeJS(c.Writer, c.Request) }) log.Fatal(r.Run()) } -func NewConnect4() cave.Renderer { - return &Connect4{} +type GameMaster struct { + playerRequests chan *Connect4 +} + +func (gm *GameMaster) startGameMaster() { + gm.playerRequests = make(chan *Connect4) + go func() { + for { + first := <-gm.playerRequests + second := <-gm.playerRequests + fmt.Println("got two players", first, second) + first.Opponent = second + second.Opponent = first + side := rand.Intn(1) + first.Board = &BoardView{parent: first} + second.Board = &BoardView{parent: second} + if side == 1 { + first.Board.Side = CircleTypeBlack + second.Board.Side = CircleTypeRed + } else { + first.Board.Side = CircleTypeRed + second.Board.Side = CircleTypeBlack + } + game := &Game{} + first.Board.game = game + second.Board.game = game + first.session.Render() + second.session.Render() + } + }() +} +func (gm *GameMaster) NewConnect4() cave.Renderer { + return &Connect4{ + playerRequests: gm.playerRequests, + } } type Connect4 struct { - Username string - Board *Board + Username string + Board *BoardView + session cave.Session + Opponent *Connect4 + playerRequests chan *Connect4 } var ( @@ -53,10 +90,14 @@ var ( _ cave.Renderer = new(Connect4) ) +func (tda *Connect4) OnMount(session cave.Session) { + tda.session = session +} + func (tda *Connect4) OnSubmit(name string, form map[string]string) { if name == "username" { tda.Username = form["username"] - tda.Board = &Board{} + tda.playerRequests <- tda } } func (tda *Connect4) Render() string { @@ -65,7 +106,12 @@ func (tda *Connect4) Render() string {

Connect4

{{ if .Username -}}

username: {{.Username}}

- {{ render .Board }} + {{ if .Board }} + Playing against {{ .Opponent.Username }} + {{ render .Board }} + {{ else }} + Waiting for another player to join... + {{- end}} {{- end}} {{ if eq .Username "" }}
@@ -88,25 +134,29 @@ const ( CircleTypeBlack ) -type Board struct { - board [7][6]CircleType +type BoardView struct { + game *Game + Side CircleType + parent *Connect4 } var ( - _ cave.OnClicker = new(Board) + _ cave.OnClicker = new(BoardView) ) -func (board *Board) OnClick(name string) { - board.board[2][4] = CircleTypeBlack +func (board *BoardView) OnClick(name string) { + column, _ := strconv.Atoi(name) + _ = board.game.play(board.Side, column) + board.parent.Opponent.session.Render() } -func (board *Board) Render() string { +func (board *BoardView) Render() string { var sb strings.Builder sb.WriteString(`
`) for i := 0; i < 7; i++ { sb.WriteString(fmt.Sprintf(`
`, i)) - for j := 0; j < 6; j++ { - switch board.board[i][j] { + for j := 5; j >= 0; j-- { + switch board.game.board[i][j] { case CircleTypeRed: sb.WriteString(`
`) case CircleTypeBlack: @@ -120,3 +170,84 @@ func (board *Board) Render() string { sb.WriteString("
") return sb.String() } + +const ( + BoardWidth = 7 + BoardHeight = 6 +) + +type Game struct { + board [BoardWidth][BoardHeight]CircleType + winner CircleType +} + +func (g *Game) moveNumber() int { + var count int + for _, column := range g.board { + for _, square := range column { + if square != CircleTypeNone { + count++ + } + } + } + return count +} +func (g *Game) whosMove() CircleType { + if g.moveNumber()%2 == 0 { + return CircleTypeRed + } + return CircleTypeBlack +} + +func (g *Game) didTheyWin(circle CircleType) bool { + // Taken from: https://stackoverflow.com/a/38211417/1333724 + for j := 0; j < BoardHeight-3; j++ { + for i := 0; i < BoardWidth; i++ { + if g.board[i][j] == circle && g.board[i][j+1] == circle && g.board[i][j+2] == circle && g.board[i][j+3] == circle { + return true + } + } + } + for i := 0; i < BoardWidth-3; i++ { + for j := 0; j < BoardHeight; j++ { + if g.board[i][j] == circle && g.board[i+1][j] == circle && g.board[i+2][j] == circle && g.board[i+3][j] == circle { + return true + } + } + } + for i := 3; i < BoardWidth; i++ { + for j := 0; j < BoardHeight-3; j++ { + if g.board[i][j] == circle && g.board[i-1][j+1] == circle && g.board[i-2][j+2] == circle && g.board[i-3][j+3] == circle { + return true + } + } + } + for i := 3; i < BoardWidth; i++ { + for j := 3; j < BoardHeight; j++ { + if g.board[i][j] == circle && g.board[i-1][j-1] == circle && g.board[i-2][j-2] == circle && g.board[i-3][j-3] == circle { + return true + } + } + } + return false +} + +func (g *Game) play(circle CircleType, column int) (winner CircleType) { + if g.winner != CircleTypeNone { + return + } + if circle != g.whosMove() { + return + } + for i, square := range g.board[column] { + if square == CircleTypeNone { + g.board[column][i] = circle + if g.didTheyWin(circle) { + g.winner = circle + return circle + } + break + } + } + return CircleTypeNone +} diff --git a/examples/connect4/main_test.go b/examples/connect4/main_test.go new file mode 100644 index 0000000..5d18d42 --- /dev/null +++ b/examples/connect4/main_test.go @@ -0,0 +1,100 @@ +package main + +import ( + "testing" +) + +func TestGame_didTheyWin(t *testing.T) { + type args struct { + circle CircleType + column int + row int + } + tests := []struct { + name string + board [7][6]CircleType + args args + want bool + }{ + { + name: "no win", + board: [7][6]CircleType{}, + args: args{ + circle: CircleTypeRed, + column: 0, + row: 0, + }, + want: false, + }, + { + name: "across columns", + board: [7][6]CircleType{ + {CircleTypeRed}, + {CircleTypeRed}, + {CircleTypeRed}, + {CircleTypeRed}, + }, + args: args{ + circle: CircleTypeRed, + column: 0, + row: 0, + }, + want: true, + }, + { + name: "down a column", + board: [7][6]CircleType{ + {}, + {CircleTypeBlack, CircleTypeBlack, CircleTypeBlack, CircleTypeBlack}, + }, + args: args{ + circle: CircleTypeBlack, + column: 1, + row: 0, + }, + want: true, + }, + { + name: "diag1", + board: [7][6]CircleType{ + {}, + {CircleTypeBlack}, + {CircleTypeNone, CircleTypeBlack}, + {CircleTypeNone, CircleTypeNone, CircleTypeBlack}, + {CircleTypeNone, CircleTypeNone, CircleTypeNone, CircleTypeBlack}, + }, + args: args{ + circle: CircleTypeBlack, + column: 1, + row: 0, + }, + want: true, + }, + { + name: "diag2", + board: [7][6]CircleType{ + {}, + {CircleTypeNone, CircleTypeNone, CircleTypeNone, CircleTypeBlack}, + {CircleTypeNone, CircleTypeNone, CircleTypeBlack}, + {CircleTypeNone, CircleTypeBlack}, + {CircleTypeBlack}, + }, + args: args{ + circle: CircleTypeBlack, + column: 1, + row: 0, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := &Game{ + board: tt.board, + } + if got := g.didTheyWin(tt.args.circle, tt.args.column, tt.args.row); got != tt.want { + t.Errorf("Game.didTheyWin() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/messages/messages.go b/internal/messages/messages.go index 383f13a..8addcad 100644 --- a/internal/messages/messages.go +++ b/internal/messages/messages.go @@ -47,7 +47,7 @@ func (sm ServerMessage) Serialize() ([]byte, error) { } if sm.Data == nil { - return nil, nil + return marshal(onWire) } onWire = append(onWire, nil) // we must use our internal marshal so that html is not escaped @@ -57,7 +57,7 @@ func (sm ServerMessage) Serialize() ([]byte, error) { } if sm.ComponentID == "" { - return nil, nil + return marshal(onWire) } onWire = append(onWire, nil) onWire[2], err = json.Marshal(sm.ComponentID) diff --git a/websocket.go b/websocket.go index afe69b8..d907dbc 100644 --- a/websocket.go +++ b/websocket.go @@ -44,8 +44,9 @@ type componentRenderer struct { type liveComponent struct { renderer Renderer + conn *websocket.Conn tree *html.Node - + session Session // need to check if we've seen the subcomponent before // need to take an action from a server and direct it to the right subcomponent // need to select an existing index for rendering @@ -54,6 +55,31 @@ type liveComponent struct { counter int } +func newLiveComponent(r Renderer, session Session) (*liveComponent, error) { + var buf bytes.Buffer + lc, err := renderOnce(r, &buf) + if err != nil { + return nil, err + } + node, err := parseHTMLToNode(buf.String()) + if err != nil { + return nil, err + } + lc.tree = node + lc.session = session + // printNode(lc.tree, 0) + return lc, nil +} + +func renderOnce(r Renderer, w io.Writer) (*liveComponent, error) { + lc := liveComponent{ + renderer: r, + subcomponents: map[uintptr]componentRenderer{}, + idMap: map[int]uintptr{}, + } + return &lc, lc.render(r, w) +} + func (lc *liveComponent) render(renderer Renderer, w io.Writer) error { t := template.New("").Funcs(template.FuncMap{ "add": add, @@ -96,30 +122,6 @@ func (lc *liveComponent) renderFn(i interface{}) (interface{}, error) { return buf.String(), nil } -func renderOnce(r Renderer, w io.Writer) (*liveComponent, error) { - lc := liveComponent{ - renderer: r, - subcomponents: map[uintptr]componentRenderer{}, - idMap: map[int]uintptr{}, - } - return &lc, lc.render(r, w) -} - -func newLiveComponent(r Renderer) (*liveComponent, error) { - var buf bytes.Buffer - lc, err := renderOnce(r, &buf) - if err != nil { - return nil, err - } - node, err := parseHTMLToNode(buf.String()) - if err != nil { - return nil, err - } - lc.tree = node - // printNode(lc.tree, 0) - return lc, nil -} - func (lc *liveComponent) diff() ([]Patch, error) { old := lc.tree var buf bytes.Buffer @@ -139,14 +141,15 @@ func (wss *websocketSession) sendError(err error) { if err == nil { return } + spew.Dump(err) msg := messages.ServerMessage{Type: messages.ServerTypeError, Data: []string{err.Error()}} b, err := msg.Serialize() if err != nil { // TODO panic(err) } - // TODO: confirm this error is likely just a broken conn and nothing more we should be worried about + fmt.Println("sending error", string(b)) _ = wss.conn.WriteMessage(websocket.TextMessage, b) } @@ -203,13 +206,15 @@ func (wss *websocketSession) handleClientMessage(msg messages.ClientMessage) err } renderer := rendererFunc() var err error - wss.components[componentID], err = newLiveComponent(renderer) + var session Session = &scopedSession{wss: wss, componentID: componentID} + lc, err := newLiveComponent(renderer, session) + wss.components[componentID] = lc if err != nil { return err } onmounter, ok := renderer.(OnMounter) if ok { - onmounter.OnMount(wss.req) + onmounter.OnMount(session) return wss.reRenderComponent(componentID) } } @@ -252,6 +257,17 @@ func (wss *websocketSession) handleClientMessage(msg messages.ClientMessage) err return nil } +type scopedSession struct { + wss *websocketSession + componentID string +} + +func (ss *scopedSession) Render() { + if err := ss.wss.reRenderComponent(ss.componentID); err != nil { + ss.wss.sendError(err) + } +} + func (wss *websocketSession) reRenderComponent(componentID string) error { component, ok := wss.components[componentID] if !ok { @@ -274,5 +290,6 @@ func (wss *websocketSession) reRenderComponent(componentID string) error { if err != nil { return err } + fmt.Println("sending diff", string(b)) return wss.conn.WriteMessage(websocket.TextMessage, b) }