diff --git a/Makefile b/Makefile index 900f647..a235a7d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ -GOFILES=$(wildcard *.go) +GAMEFILES=$(wildcard internal/game/*.go) +COORDFILEs=$(wildcard internal/coordinator/*.go) PROG=snengame #set variables GIT_COMMIT := $(shell git rev-list -1 HEAD) @@ -8,11 +9,17 @@ endif $(PROG): $(GOFILES) go build -ldflags "-X main.GitCommit=$(GIT_COMMIT)" +client: $(GAMEFILES) $(wildcard cmd/client/*.go) + go build -ldflags "-X main.GitCommit=$(GIT_COMMIT)" ./cmd/client +server: $(GAMEFILES) $(wildcard cmd/server/*.go) + go build -ldflags "-X main.GitCommit=$(GIT_COMMIT)" ./cmd/server +engine: $(GAMEFILES) + go build -ldflags "-X main.GitCommit=$(GIT_COMMIT)" ./cmd/engine clean: - rm -f snengame -run: snengame - ./snengame + rm -f client server engine +run: engine + ./engine install: $(SNENGAME) install -d $(DESTDIR)$(PREFIX)/bin install -m 755 $(PROG) $(DESTDIR)$(PREFIX)/bin diff --git a/cmd/client/main.go b/cmd/client/main.go new file mode 100644 index 0000000..b8c0cb3 --- /dev/null +++ b/cmd/client/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "strings" + "time" + + "git.saintnet.tech/stryan/snengame/internal/coordinator" + "github.com/google/uuid" + "github.com/gorilla/websocket" +) + +var done chan interface{} +var interrupt chan os.Signal + +func receiveHandler(connection *websocket.Conn) { + defer close(done) + for { + var resp coordinator.SessionCommandResult + err := connection.ReadJSON(&resp) + if err != nil { + log.Println("Error in receive:", err) + return + } + log.Printf("Received: %s\n", resp) + } +} + +func main() { + done = make(chan interface{}) // Channel to indicate that the receiverHandler is done + interrupt = make(chan os.Signal) // Channel to listen for interrupt signal to terminate gracefully + cmd := make(chan coordinator.SessionCommand) + + signal.Notify(interrupt, os.Interrupt) // Notify the interrupt channel for SIGINT + + socketUrl := "ws://localhost:7636" + "/ws" + conn, _, err := websocket.DefaultDialer.Dial(socketUrl, nil) + id := uuid.New() + + if err != nil { + log.Fatal("Error connecting to Websocket Server:", err) + } + defer conn.Close() + go receiveHandler(conn) + go GetCommand(id, cmd) + + // Our main loop for the client + // We send our relevant packets here + for { + var c coordinator.SessionCommand + select { + case c = <-cmd: + // Send an echo packet every second + err := conn.WriteJSON(c) + if err != nil { + log.Println("Error during writing to websocket:", err) + return + } + + case <-interrupt: + // We received a SIGINT (Ctrl + C). Terminate gracefully... + log.Println("Received SIGINT interrupt signal. Closing all pending connections") + + // Close our websocket connection + err := conn.WriteJSON(coordinator.SessionCommand{ + ID: id, + Command: coordinator.SessionCmdLeave, + }) + if err != nil { + log.Println("Error during closing websocket:", err) + return + } + + select { + case <-done: + log.Println("Receiver Channel Closed! Exiting....") + case <-time.After(time.Duration(1) * time.Second): + log.Println("Timeout in closing receiving channel. Exiting....") + } + return + } + } +} + +func GetCommand(uid uuid.UUID, resp chan coordinator.SessionCommand) { + for { + var cmd string + var t int + fmt.Print("> ") + _, err := fmt.Scanf("%d", &t) + if err != nil { + log.Println(err) + } + if t == -1 { + panic("quitting") + } + _, err = fmt.Scanf("%s", &cmd) + if err != nil { + log.Println(err) + } + cmd = strings.TrimSpace(cmd) + switch t { + case 0: + //session + switch coordinator.SessionCmd(cmd) { + case coordinator.SessionCmdQuery: + resp <- coordinator.SessionCommand{ + ID: uid, + Command: coordinator.SessionCmdQuery, + } + case coordinator.SessionCmdJoin: + resp <- coordinator.SessionCommand{ + ID: uid, + Command: coordinator.SessionCmdJoin, + } + case coordinator.SessionCmdLeave: + resp <- coordinator.SessionCommand{ + ID: uid, + Command: coordinator.SessionCmdLeave, + } + default: + break + } + } + } +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..e0fc562 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,9 @@ +package main + +import "git.saintnet.tech/stryan/snengame/internal/coordinator" + +func main() { + c := coordinator.NewCoordinator() + c.Start() + coordinator.Serve(c) +} diff --git a/go.mod b/go.mod index 2dca0b3..b721802 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module git.saintnet.tech/stryan/snengame go 1.16 -require github.com/gorilla/websocket v1.4.2 +require ( + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/websocket v1.4.2 +) + +replace git.saintnet.tech/stryan/snengame/internal => ./internal diff --git a/go.sum b/go.sum index 85efffd..e8df9a1 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/internal/coordinator/coordinator.go b/internal/coordinator/coordinator.go new file mode 100644 index 0000000..58a8710 --- /dev/null +++ b/internal/coordinator/coordinator.go @@ -0,0 +1,83 @@ +package coordinator + +import ( + "fmt" + "log" + "time" + + "git.saintnet.tech/stryan/snengame/internal/game" + "github.com/google/uuid" +) + +type Coordinator struct { + Match *Session + PlayerQueueChan chan uuid.UUID + CallbackChan map[uuid.UUID]chan bool +} + +func NewCoordinator() *Coordinator { + return &Coordinator{ + Match: NewSession(), + PlayerQueueChan: make(chan uuid.UUID), + CallbackChan: make(map[uuid.UUID]chan bool), + } +} + +func (c *Coordinator) Start() { + go func() { + m := NewSession() + var p1, p2 uuid.UUID + p1 = <-c.PlayerQueueChan + fmt.Println("p1 join") + p2 = <-c.PlayerQueueChan + fmt.Println("p2 join") + c.Match = m + m.Game = game.NewGame() + c.CallbackChan[p1] <- true + c.CallbackChan[p2] <- true + }() + go func() { + time.Sleep(5) + if c.Match.Game == nil { + log.Println("clearing old match") + c.Match = nil + } + }() + +} + +func (c *Coordinator) Coordinate(cmd *SessionCommand) *SessionCommandResult { + switch cmd.Command { + case SessionCmdQuery: + c.CallbackChan[cmd.ID] = make(chan bool) + c.PlayerQueueChan <- cmd.ID + <-c.CallbackChan[cmd.ID] + return &SessionCommandResult{ + ID: cmd.ID, + Result: SessionRespFound, + } + case SessionCmdJoin: + resp := c.Match.Join(cmd.ID) + return &SessionCommandResult{ + ID: cmd.ID, + Result: resp, + } + case SessionCmdLeave: + c.Match.Leave(cmd.ID) + return &SessionCommandResult{ + ID: cmd.ID, + Result: SessionRespLeft, + } + case SessionCmdPlay: + resp := c.Match.Play(cmd.ID, cmd.GameCommand) + return &SessionCommandResult{ + ID: cmd.ID, + Result: SessionRespPlayed, + GameResult: resp, + } + } + return &SessionCommandResult{ + ID: cmd.ID, + Result: SessionRespError, + } +} diff --git a/cmd/server/server.go b/internal/coordinator/server.go similarity index 76% rename from cmd/server/server.go rename to internal/coordinator/server.go index dd26875..81e293c 100644 --- a/cmd/server/server.go +++ b/internal/coordinator/server.go @@ -1,4 +1,4 @@ -package engine +package coordinator import ( "log" @@ -9,9 +9,9 @@ import ( var upgrader = websocket.Upgrader{} // use default options -func serve() { +func Serve(c *Coordinator) { http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { - serveWs(w, r) + serveWs(c, w, r) }) err := http.ListenAndServe(":7636", nil) if err != nil { @@ -19,7 +19,7 @@ func serve() { } } -func serveWs(w http.ResponseWriter, r *http.Request) { +func serveWs(c *Coordinator, w http.ResponseWriter, r *http.Request) { // Upgrade our raw HTTP connection to a websocket based one conn, err := upgrader.Upgrade(w, r, nil) if err != nil { @@ -30,18 +30,21 @@ func serveWs(w http.ResponseWriter, r *http.Request) { // The event loop for { - var cmd Command - var resp CommandResult + var cmd SessionCommand err := conn.ReadJSON(&cmd) if err != nil { log.Println("Error during message reading:", err) break } log.Printf("Received: %s", cmd) + resp := c.Coordinate(&cmd) err = conn.WriteJSON(resp) if err != nil { log.Println("Error during message writing:", err) break } + if resp.Result == SessionRespLeft { + break + } } } diff --git a/internal/coordinator/session.go b/internal/coordinator/session.go new file mode 100644 index 0000000..cd2c333 --- /dev/null +++ b/internal/coordinator/session.go @@ -0,0 +1,98 @@ +package coordinator + +import ( + "fmt" + + "git.saintnet.tech/stryan/snengame/internal/game" + "github.com/google/uuid" +) + +type SessionCmd string +type SessionResp string + +const ( + SessionCmdQuery SessionCmd = "query" + SessionCmdJoin = "join" + SessionCmdLeave = "leave" + SessionCmdPlay = "play" +) + +const ( + SessionRespFound SessionResp = "found" + SessionRespJoined1 = "joined p1" + SessionRespJoined2 = "joined p2" + SessionRespJoinError = "join error" + SessionRespLeft = "left" + SessionRespPlayed = "played" + SessionRespError = "generic error" +) + +type Session struct { + p1 uuid.UUID + p2 uuid.UUID + pMap map[uuid.UUID]int + Game *game.Game +} + +func NewSession() *Session { + return &Session{ + p1: uuid.Nil, + p2: uuid.Nil, + pMap: make(map[uuid.UUID]int), + Game: nil, + } +} + +func (s *Session) Join(id uuid.UUID) SessionResp { + if s.p1 == uuid.Nil { + s.p1 = id + s.pMap[id] = game.SentinalID + return SessionRespJoined1 + } else if s.p2 == uuid.Nil { + s.p2 = id + s.pMap[id] = game.ScourgeID + return SessionRespJoined2 + } else { + return SessionRespJoinError + } +} + +func (s *Session) Leave(id uuid.UUID) { + if id == s.p1 { + s.p1 = uuid.Nil + } else if id == s.p2 { + s.p2 = uuid.Nil + } + if s.p1 == uuid.Nil && s.p2 == uuid.Nil { + s.Game = nil + } else if s.Game.Status != game.StatusDraw || s.Game.Status != game.StatusScourgeWin || s.Game.Status != game.StatusSentinalWin { + s.Game.Status = game.StatusStop + } +} + +func (s *Session) Play(id uuid.UUID, cmd *game.Command) *game.CommandResult { + if s.pMap[id] != cmd.PlayerID { + return nil + } + return s.Game.Parse(cmd) +} + +type SessionCommand struct { + ID uuid.UUID `json:"player_id"` + Command SessionCmd `json:"command"` + GameCommand *game.Command `json:"game_command"` +} + +func (s *SessionCommand) String() string { + return fmt.Sprintf("%v %v\n%v\n", s.ID, s.Command, s.GameCommand) +} + +type SessionCommandResult struct { + ID uuid.UUID `json:"player_id"` + Result SessionResp `json:"result"` + GameResult *game.CommandResult `json:"game_result"` +} + +func (s *SessionCommandResult) String() string { + return fmt.Sprintf("%v %v\n%v\n", s.ID, s.Result, s.GameResult) +}