From f3eb78012eba709fdce8c8bc2494da23161941a8 Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Sun, 7 Jul 2024 16:40:42 -0600 Subject: [PATCH 01/19] Rebased with the new structure --- api/sessions_api.go | 340 +++++++++++++++++++++++++++++++++++ models/connection/message.go | 4 + models/connection/signal.go | 3 + 3 files changed, 347 insertions(+) create mode 100644 api/sessions_api.go diff --git a/api/sessions_api.go b/api/sessions_api.go new file mode 100644 index 0000000..03eced5 --- /dev/null +++ b/api/sessions_api.go @@ -0,0 +1,340 @@ +package api + +import ( + "encoding/json" + "log" + "time" + + "github.com/gorilla/websocket" + mb "github.com/saeidalz13/battleship-backend/models/battleship" + mc "github.com/saeidalz13/battleship-backend/models/connection" +) + +const ( + gracePeriod time.Duration = time.Minute * 2 +) + +type Session struct { + ID string + Conn *websocket.Conn + GameUuid string + Player *mb.Player + StopRetry chan struct{} + GameManager *GameManager + SessionManager *SessionManager + CreatedAt time.Time +} + +func NewSession(conn *websocket.Conn, sessionID string, gameManager *GameManager, sessionManager *SessionManager) *Session { + return &Session{ + ID: sessionID, + Conn: conn, + StopRetry: make(chan struct{}), + GameManager: gameManager, + SessionManager: sessionManager, + CreatedAt: time.Now(), + } +} + +func (s *Session) run() { + defer s.terminate() + +sessionLoop: + for { + // A WebSocket frame can be one of 6 types: text=1, binary=2, ping=9, pong=10, close=8 and continuation=0 + // https://www.rfc-editor.org/rfc/rfc6455.html#section-11.8 + _, payload, err := s.Conn.ReadMessage() + if err != nil { + if s.handleReadErr(err) == ConnLoopCodeBreak { + break sessionLoop + } + } + var signal mc.Signal + if err := json.Unmarshal(payload, &signal); err != nil { + log.Println("incoming msg does not contain 'code':", err) + resp := mc.NewMessage[mc.NoPayload](mc.CodeSignalAbsent) + resp.AddError("incoming req payload must contain 'code' field", "") + + if s.writeToConn(resp) == ConnLoopCodeBreak { + break sessionLoop + } + continue sessionLoop + } + + switch signal.Code { + case mc.CodeCreateGame: + req := NewRequest(s, payload) + resp := req.HandleCreateGame() + + if s.writeToConn(resp) == ConnLoopCodeBreak { + break sessionLoop + } + + case mc.CodeAttack: + req := NewRequest(s, payload) + // response will have the IsTurn as false field of attacker + resp, defender := req.HandleAttack() + + if s.writeToConn(resp) == ConnLoopCodeBreak { + break sessionLoop + } + if resp.Error != nil { + continue sessionLoop + } + + // defender turn is set to true + resp.Payload.IsTurn = true + s.notifyOtherSession(defender.SessionID, resp) + + if defender.MatchStatus == mb.PlayerMatchStatusLost { + respAttacker := mc.NewMessage[mc.RespEndGame](mc.CodeEndGame) + respAttacker.AddPayload(mc.RespEndGame{PlayerMatchStatus: mb.PlayerMatchStatusWon}) + if s.writeToConn(respAttacker) == ConnLoopCodeBreak { + break sessionLoop + } + + respDefender := mc.NewMessage[mc.RespEndGame](mc.CodeEndGame) + respDefender.AddPayload(mc.RespEndGame{PlayerMatchStatus: mb.PlayerMatchStatusLost}) + s.notifyOtherSession(defender.SessionID, respDefender) + } + + case mc.CodeReady: + req := NewRequest(s, payload) + resp, game := req.HandleReadyPlayer() + + if s.writeToConn(resp) == ConnLoopCodeBreak { + break sessionLoop + } + if resp.Error != nil { + continue sessionLoop + } + + if game.HostPlayer.IsReady && game.JoinPlayer.IsReady { + respStartGame := mc.NewMessage[mc.NoPayload](mc.CodeStartGame) + if s.writeToConn(respStartGame) == ConnLoopCodeBreak { + break sessionLoop + } + + otherPlayer := game.GetOtherPlayer(s.Player) + s.notifyOtherSession(otherPlayer.SessionID, respStartGame) + } + + case mc.CodeJoinGame: + req := NewRequest(s, payload) + resp, game := req.HandleJoinPlayer() + + if s.writeToConn(resp) == ConnLoopCodeBreak { + break sessionLoop + } + if resp.Error != nil { + break sessionLoop + } + + readyResp := mc.NewMessage[mc.NoPayload](mc.CodeSelectGrid) + if s.writeToConn(readyResp) == ConnLoopCodeBreak { + break sessionLoop + } + s.notifyOtherSession(game.HostPlayer.SessionID, readyResp) + + case mc.CodeRematchCall: + // 1. See if the game still exists + game, err := s.GameManager.FindGame(s.GameUuid) + if err != nil { + break sessionLoop + } + + if game.IsRematchAlreadyCalled() { + continue sessionLoop + } + + game.CallRematch() + + otherPlayer := game.GetOtherPlayer(s.Player) + if otherPlayer == nil { + break sessionLoop + } + + s.Player.IsTurn = true + // Notify the other player if they want a rematch + msg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCall) + s.notifyOtherSession(otherPlayer.SessionID, msg) + + case mc.CodeRematchCallAccepted: + // Send the rematch call acceptance to other player + game, err := s.GameManager.FindGame(s.GameUuid) + if err != nil { + break sessionLoop + } + + if err := game.Reset(); err != nil { + break sessionLoop + } + + // Notify the other player with their turn + msgOtherPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) + otherPlayer := game.GetOtherPlayer(s.Player) + if otherPlayer == nil { + break sessionLoop + } + msgOtherPlayer.AddPayload(mc.RespRematch{IsTurn: otherPlayer.IsTurn}) + s.notifyOtherSession(otherPlayer.SessionID, msgOtherPlayer) + + s.Player.IsTurn = false + msgPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) + msgPlayer.AddPayload(mc.RespRematch{IsTurn: s.Player.IsTurn}) + + // Notify the acceptor with their turn + if s.writeToConn(msgPlayer) == ConnLoopCodeBreak { + break sessionLoop + } + + case mc.CodeRematchCallRejected: + game, err := s.GameManager.FindGame(s.GameUuid) + if err != nil { + break sessionLoop + } + + // Notify the other player that no rematch is wanted now + msg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCallRejected) + otherPlayer := game.GetOtherPlayer(s.Player) + if otherPlayer != nil { + s.notifyOtherSession(otherPlayer.SessionID, msg) + } + + break sessionLoop + + case mc.CodePlayerInteraction: + game, err := s.GameManager.FindGame(s.GameUuid) + if err != nil { + break sessionLoop + } + otherPlayer := game.GetOtherPlayer(s.Player) + if otherPlayer != nil { + s.notifyOtherSession(otherPlayer.SessionID, payload) + } + + default: + respInvalidSignal := mc.NewMessage[mc.NoPayload](mc.CodeInvalidSignal) + respInvalidSignal.AddError("", "invalid code in the incoming payload") + if s.writeToConn(respInvalidSignal) == ConnLoopCodeBreak { + break sessionLoop + } + } + } +} + +// This is to send a message to the other session. +func (s *Session) notifyOtherSession(otherSessionId string, msg interface{}) { + s.SessionManager.CommunicationChan <- NewSessionMessage(s, otherSessionId, s.GameUuid, msg) +} + +// This will delete player from the game players map +// and delete the player session +func (s *Session) terminate() { + if s.Player != nil { + s.GameManager.DeletePlayerFromGame(s.GameUuid, s.Player.Uuid) + } + s.SessionManager.DeleteSession(s.ID) +} + +// Writes to the connection of that session. It also +// handles the abnormal or other types of errors of +// writing to a websocket connection. +func (s *Session) writeToConn(p interface{}) int { + switch WriteJSONWithRetry(s.Conn, p) { + case ConnLoopAbnormalClosureRetry: + switch s.handleAbnormalClosure() { + case ConnLoopCodeBreak: + return ConnLoopCodeBreak + + case ConnLoopCodeContinue: + } + case ConnLoopCodeBreak: + return ConnLoopCodeBreak + default: + } + + return ConnLoopCodeContinue +} + +// This function takes care of abnormal closures happening +// to either of the clients. This happens due to backgrounding +// in IOS clients or any other unexpected reasons for web apps. +func (s *Session) handleAbnormalClosure() int { + // This means there is no game and abnormal closure is happening + // which means this session is invalid and should end + game, err := s.GameManager.FindGame(s.GameUuid) + if err != nil { + return ConnLoopCodeBreak + } + + otherPlayer := game.GetOtherPlayer(s.Player) + if otherPlayer == nil { + return ConnLoopCodeBreak + } + + // Absence of otherPlayer session means this game is invalid + otherSession, err := s.SessionManager.FindSession(otherPlayer.SessionID) + if err != nil { + return ConnLoopCodeBreak + } + + if err := otherSession.Conn.WriteJSON(mc.NewMessage[mc.NoPayload](mc.CodeOtherPlayerGracePeriod)); err != nil { + // If other player connection is disrupted as well, then end the session + return ConnLoopCodeBreak + } + + log.Printf("starting grace period for %s\n", s.ID) + timer := time.NewTimer(gracePeriod) + + select { + case <-timer.C: + if otherSession != nil { + _ = otherSession.Conn.WriteJSON(mc.NewMessage[mc.NoPayload](mc.CodeOtherPlayerDisconnected)) + } + log.Printf("session terminated: %s\n", s.ID) + return ConnLoopCodeBreak + + case <-s.StopRetry: + if otherSession != nil { + _ = otherSession.Conn.WriteJSON(mc.NewMessage[mc.NoPayload](mc.CodeOtherPlayerReconnected)) + } + log.Printf("player reconnected, session: %s\n", s.ID) + return ConnLoopCodeContinue + } +} + +// Handles the errors that occurs when reading from +// ws connection. `ConnLoopCodeContinue` will results in +// terminating the session and removing `run` from stack +func (s *Session) handleReadErr(err error) int { + retries := 0 + + switch IdentifyWsConnErrAction(err) { + case ConnLoopAbnormalClosureRetry: + switch s.handleAbnormalClosure() { + case ConnLoopCodeBreak: + return ConnLoopCodeBreak + case ConnLoopCodeContinue: + } + + case ConnLoopCodeRetry: + if retries < maxWriteWsRetries { + retries++ + log.Printf("failed to read from ws conn [%s]; retrying... (retry no. %d)\n", s.Conn.RemoteAddr().String(), retries) + time.Sleep(time.Duration(retries*backOffFactor) * time.Second) + + } else { + return ConnLoopCodeBreak + + } + + case ConnLoopCodeBreak: + log.Printf("break ws conn loop [%s] due to: %s\n", s.Conn.RemoteAddr().String(), err) + return ConnLoopCodeBreak + + case ConnLoopCodeContinue: + } + + return ConnLoopCodeContinue +} diff --git a/models/connection/message.go b/models/connection/message.go index 8e279a2..42d5f7e 100644 --- a/models/connection/message.go +++ b/models/connection/message.go @@ -18,3 +18,7 @@ func (m *Message[T]) AddPayload(payload T) { func (m *Message[T]) AddError(errorDetails, message string) { m.Error = NewRespErr(errorDetails, message) } + +type PlayerInteraction struct { + Content string `json:"content"` +} diff --git a/models/connection/signal.go b/models/connection/signal.go index 1f9f6c4..0f65a90 100644 --- a/models/connection/signal.go +++ b/models/connection/signal.go @@ -28,6 +28,9 @@ const ( CodeRematchCallAccepted CodeRematchCallRejected CodeRematch + + // Players can send template texts and emojis to each other + CodePlayerInteraction ) type Signal struct { From 939119ac5a9be8b1f65fdefca346fc2a05d211c4 Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Mon, 8 Jul 2024 13:39:41 -0600 Subject: [PATCH 02/19] wrote unit tests for player interaction --- test/ws_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/ws_test.go b/test/ws_test.go index 619284d..d54abe7 100644 --- a/test/ws_test.go +++ b/test/ws_test.go @@ -281,6 +281,51 @@ func TestReadyGame(t *testing.T) { } } +func TestPlayerInteraction(t *testing.T) { + msg := mc.NewMessage[mc.PlayerInteraction](mc.CodePlayerInteraction) + + tests := []Test[mc.Message[mc.PlayerInteraction], mc.Message[mc.PlayerInteraction]]{ + { + name: "successful msg host to join", + expectedCode: mc.CodePlayerInteraction, + reqPayload: msg, + respPayload: mc.Message[mc.PlayerInteraction]{}, + conn: HostConn, + otherConn: JoinConn, + }, + { + name: "successful msg join to host", + expectedCode: mc.CodePlayerInteraction, + reqPayload: msg, + respPayload: mc.Message[mc.PlayerInteraction]{}, + conn: JoinConn, + otherConn: HostConn, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Client writes to its own connection + if err := test.conn.WriteJSON(test.reqPayload); err != nil { + t.Fatal(err) + } + + // Server writes it to ther other connection + if err := test.otherConn.ReadJSON(&test.respPayload); err != nil { + t.Fatal(err) + } + + if test.respPayload.Code != test.expectedCode { + t.Fatalf("expected status: %d\t got: %d", test.expectedCode, test.respPayload.Code) + } + + if !reflect.DeepEqual(test.reqPayload, test.respPayload) { + t.Fatalf("expected resp payload: %+v\n got: %+v", test.expectedRespPayload, test.respPayload) + } + }) + } +} + func TestAttack(t *testing.T) { tests := []Test[mc.Message[mc.ReqAttack], mc.Message[mc.RespAttack]]{ { @@ -754,6 +799,7 @@ func TestAttack(t *testing.T) { } if test.respPayload.Error != nil { + log.Printf("%+v\n", test.respPayload.Error) if test.respPayload.Error.ErrorDetails != test.expectedErr { t.Fatalf("expected error: %s\t got: %s", test.expectedErr, test.respPayload.Error.ErrorDetails) } From e835975f0666f5184086cb70de9ad66657122aa8 Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Mon, 8 Jul 2024 13:40:11 -0600 Subject: [PATCH 03/19] modified notifying the other player based on message type --- api/sessions_api.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/api/sessions_api.go b/api/sessions_api.go index 03eced5..91938fe 100644 --- a/api/sessions_api.go +++ b/api/sessions_api.go @@ -84,7 +84,7 @@ sessionLoop: // defender turn is set to true resp.Payload.IsTurn = true - s.notifyOtherSession(defender.SessionID, resp) + s.notifyOtherSession(defender.SessionID, resp, TypeSessionMessageJSON) if defender.MatchStatus == mb.PlayerMatchStatusLost { respAttacker := mc.NewMessage[mc.RespEndGame](mc.CodeEndGame) @@ -95,7 +95,7 @@ sessionLoop: respDefender := mc.NewMessage[mc.RespEndGame](mc.CodeEndGame) respDefender.AddPayload(mc.RespEndGame{PlayerMatchStatus: mb.PlayerMatchStatusLost}) - s.notifyOtherSession(defender.SessionID, respDefender) + s.notifyOtherSession(defender.SessionID, respDefender, TypeSessionMessageJSON) } case mc.CodeReady: @@ -116,7 +116,7 @@ sessionLoop: } otherPlayer := game.GetOtherPlayer(s.Player) - s.notifyOtherSession(otherPlayer.SessionID, respStartGame) + s.notifyOtherSession(otherPlayer.SessionID, respStartGame, TypeSessionMessageJSON) } case mc.CodeJoinGame: @@ -134,7 +134,7 @@ sessionLoop: if s.writeToConn(readyResp) == ConnLoopCodeBreak { break sessionLoop } - s.notifyOtherSession(game.HostPlayer.SessionID, readyResp) + s.notifyOtherSession(game.HostPlayer.SessionID, readyResp, TypeSessionMessageJSON) case mc.CodeRematchCall: // 1. See if the game still exists @@ -157,7 +157,7 @@ sessionLoop: s.Player.IsTurn = true // Notify the other player if they want a rematch msg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCall) - s.notifyOtherSession(otherPlayer.SessionID, msg) + s.notifyOtherSession(otherPlayer.SessionID, msg, TypeSessionMessageJSON) case mc.CodeRematchCallAccepted: // Send the rematch call acceptance to other player @@ -177,7 +177,7 @@ sessionLoop: break sessionLoop } msgOtherPlayer.AddPayload(mc.RespRematch{IsTurn: otherPlayer.IsTurn}) - s.notifyOtherSession(otherPlayer.SessionID, msgOtherPlayer) + s.notifyOtherSession(otherPlayer.SessionID, msgOtherPlayer, TypeSessionMessageJSON) s.Player.IsTurn = false msgPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) @@ -198,7 +198,7 @@ sessionLoop: msg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCallRejected) otherPlayer := game.GetOtherPlayer(s.Player) if otherPlayer != nil { - s.notifyOtherSession(otherPlayer.SessionID, msg) + s.notifyOtherSession(otherPlayer.SessionID, msg, TypeSessionMessageJSON) } break sessionLoop @@ -209,10 +209,13 @@ sessionLoop: break sessionLoop } otherPlayer := game.GetOtherPlayer(s.Player) - if otherPlayer != nil { - s.notifyOtherSession(otherPlayer.SessionID, payload) + if otherPlayer == nil { + break sessionLoop } + s.notifyOtherSession(otherPlayer.SessionID, payload, TypeSessionMessageBytes) + continue sessionLoop + default: respInvalidSignal := mc.NewMessage[mc.NoPayload](mc.CodeInvalidSignal) respInvalidSignal.AddError("", "invalid code in the incoming payload") @@ -224,8 +227,13 @@ sessionLoop: } // This is to send a message to the other session. -func (s *Session) notifyOtherSession(otherSessionId string, msg interface{}) { - s.SessionManager.CommunicationChan <- NewSessionMessage(s, otherSessionId, s.GameUuid, msg) +func (s *Session) notifyOtherSession(otherSessionId string, msg interface{}, payloadType int8) { + switch payloadType { + case TypeSessionMessageJSON: + s.SessionManager.CommunicationChan <- NewSessionMessageJSON(otherSessionId, s.GameUuid, msg) + case TypeSessionMessageBytes: + s.SessionManager.CommunicationChan <- NewSessionMessageBytes(otherSessionId, s.GameUuid, msg) + } } // This will delete player from the game players map From c19fbc12f7aff1fbbf27d307f9d30d83b772c47e Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Tue, 16 Jul 2024 10:36:14 -0600 Subject: [PATCH 04/19] deleted extra files --- api/session_communication_api.go | 31 ++++++++ api/session_manager_api.go | 129 +++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 api/session_communication_api.go create mode 100644 api/session_manager_api.go diff --git a/api/session_communication_api.go b/api/session_communication_api.go new file mode 100644 index 0000000..a9ca186 --- /dev/null +++ b/api/session_communication_api.go @@ -0,0 +1,31 @@ +package api + +const ( + TypeSessionMessageBytes int8 = iota + TypeSessionMessageJSON +) + +type SessionMessage struct { + PayloadType int8 + ReceiverID string + GameUuid string + Payload interface{} +} + +func NewSessionMessageJSON(receiverId string, gameUuid string, p interface{}) SessionMessage { + return SessionMessage{ + PayloadType: TypeSessionMessageJSON, + ReceiverID: receiverId, + GameUuid: gameUuid, + Payload: p, + } +} + +func NewSessionMessageBytes(receiverId string, gameUuid string, p interface{}) SessionMessage { + return SessionMessage{ + PayloadType: TypeSessionMessageBytes, + ReceiverID: receiverId, + GameUuid: gameUuid, + Payload: p, + } +} diff --git a/api/session_manager_api.go b/api/session_manager_api.go new file mode 100644 index 0000000..e07388c --- /dev/null +++ b/api/session_manager_api.go @@ -0,0 +1,129 @@ +package api + +import ( + "log" + "sync" + "time" + + "github.com/gorilla/websocket" + cerr "github.com/saeidalz13/battleship-backend/internal/error" +) + +const ( + // Assuming this capacity for the slice when + // we're cleaning up the sessions map. + assumedClosedConns = 5 + cleanupInterval time.Duration = time.Minute * 20 +) + +type SessionManager struct { + Sessions map[string]*Session + CommunicationChan chan SessionMessage + mu sync.RWMutex +} + +func NewSessionManager() *SessionManager { + return &SessionManager{ + Sessions: make(map[string]*Session), + CommunicationChan: make(chan SessionMessage), + } +} + +func (sm *SessionManager) FindSession(sessionId string) (*Session, error) { + sm.mu.RLock() + defer sm.mu.RUnlock() + + session, prs := sm.Sessions[sessionId] + if !prs { + return nil, cerr.ErrSessionNotFound(sessionId) + } + + if session == nil { + return nil, cerr.ErrSessionIsNil(sessionId) + } + + return session, nil +} + +func (sm *SessionManager) DeleteSession(sessionId string) { + sm.mu.Lock() + delete(sm.Sessions, sessionId) + log.Printf("session deleted: %s", sessionId) + sm.mu.Unlock() +} + +// Function to faciliate the communication between +// two sessions through a channel +func (sm *SessionManager) ManageCommunication() { + for { + msg := <-sm.CommunicationChan + + sm.mu.Lock() + receiverSession, prs := sm.Sessions[msg.ReceiverID] + if !prs { + // It should never be the case that the other session + // is not found. The sender session should terminate + // msg.SenderSession.terminate() + continue + } + + if receiverSession.GameUuid != msg.GameUuid { + panic("receiver session msg game is not the same as game uuid; this error should never happen") + } + + switch msg.PayloadType { + case TypeSessionMessageJSON: + switch WriteJSONWithRetry(receiverSession.Conn, msg.Payload) { + case ConnLoopAbnormalClosureRetry: + switch receiverSession.handleAbnormalClosure() { + case ConnLoopCodeBreak: + receiverSession.terminate() + + case ConnLoopCodeContinue: + } + + case ConnLoopCodeBreak: + receiverSession.terminate() + + case ConnLoopCodePassThrough: + } + + case TypeSessionMessageBytes: + msgByte, ok := msg.Payload.([]byte) + if ok { + receiverSession.Conn.WriteMessage(websocket.TextMessage, msgByte) + } + + default: + log.Println("invalid message type for intersession communication") + continue + } + + sm.mu.Unlock() + } +} + +// To ensure that there is no dangling connections, +// server session manager marks the connections with a +// lifetime of more than 20 mins as stale and deletes them. +func (sm *SessionManager) CleanUpPeriodically() { + for { + time.Sleep(cleanupInterval) + + sm.mu.Lock() + toDelete := make([]string, 0, assumedClosedConns) + + for ID, session := range sm.Sessions { + if time.Since(session.CreatedAt) > cleanupInterval { + toDelete = append(toDelete, ID) + } + } + + log.Println("Clean up sessions:") + for _, ID := range toDelete { + delete(sm.Sessions, ID) + log.Printf("removed: %s", ID) + } + sm.mu.Unlock() + } +} From 759db5d351c9b0ee5b48b5dc9263921d94d3c09f Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Tue, 16 Jul 2024 14:23:15 -0600 Subject: [PATCH 05/19] rebased with restructure and modified tests --- api/request_processor.go | 5 + api/session_communication_api.go | 31 --- api/session_manager_api.go | 129 ------------ api/sessions_api.go | 348 ------------------------------- test/ws_test.go | 2 +- 5 files changed, 6 insertions(+), 509 deletions(-) delete mode 100644 api/session_communication_api.go delete mode 100644 api/session_manager_api.go delete mode 100644 api/sessions_api.go diff --git a/api/request_processor.go b/api/request_processor.go index df3b4f9..4ca27fd 100644 --- a/api/request_processor.go +++ b/api/request_processor.go @@ -334,6 +334,11 @@ sessionLoop: rp.sessionManager.Communicate(sessionId, receiverSessionId, msg, mc.MessageTypeJSON) break sessionLoop + case mc.CodePlayerInteraction: + if err := rp.sessionManager.Communicate(sessionId, receiverSessionId, payload, mc.MessageTypeBytes); err != nil { + break sessionLoop + } + default: respInvalidSignal := mc.NewMessage[mc.NoPayload](mc.CodeInvalidSignal) respInvalidSignal.AddError("", "invalid code in the incoming payload") diff --git a/api/session_communication_api.go b/api/session_communication_api.go deleted file mode 100644 index a9ca186..0000000 --- a/api/session_communication_api.go +++ /dev/null @@ -1,31 +0,0 @@ -package api - -const ( - TypeSessionMessageBytes int8 = iota - TypeSessionMessageJSON -) - -type SessionMessage struct { - PayloadType int8 - ReceiverID string - GameUuid string - Payload interface{} -} - -func NewSessionMessageJSON(receiverId string, gameUuid string, p interface{}) SessionMessage { - return SessionMessage{ - PayloadType: TypeSessionMessageJSON, - ReceiverID: receiverId, - GameUuid: gameUuid, - Payload: p, - } -} - -func NewSessionMessageBytes(receiverId string, gameUuid string, p interface{}) SessionMessage { - return SessionMessage{ - PayloadType: TypeSessionMessageBytes, - ReceiverID: receiverId, - GameUuid: gameUuid, - Payload: p, - } -} diff --git a/api/session_manager_api.go b/api/session_manager_api.go deleted file mode 100644 index e07388c..0000000 --- a/api/session_manager_api.go +++ /dev/null @@ -1,129 +0,0 @@ -package api - -import ( - "log" - "sync" - "time" - - "github.com/gorilla/websocket" - cerr "github.com/saeidalz13/battleship-backend/internal/error" -) - -const ( - // Assuming this capacity for the slice when - // we're cleaning up the sessions map. - assumedClosedConns = 5 - cleanupInterval time.Duration = time.Minute * 20 -) - -type SessionManager struct { - Sessions map[string]*Session - CommunicationChan chan SessionMessage - mu sync.RWMutex -} - -func NewSessionManager() *SessionManager { - return &SessionManager{ - Sessions: make(map[string]*Session), - CommunicationChan: make(chan SessionMessage), - } -} - -func (sm *SessionManager) FindSession(sessionId string) (*Session, error) { - sm.mu.RLock() - defer sm.mu.RUnlock() - - session, prs := sm.Sessions[sessionId] - if !prs { - return nil, cerr.ErrSessionNotFound(sessionId) - } - - if session == nil { - return nil, cerr.ErrSessionIsNil(sessionId) - } - - return session, nil -} - -func (sm *SessionManager) DeleteSession(sessionId string) { - sm.mu.Lock() - delete(sm.Sessions, sessionId) - log.Printf("session deleted: %s", sessionId) - sm.mu.Unlock() -} - -// Function to faciliate the communication between -// two sessions through a channel -func (sm *SessionManager) ManageCommunication() { - for { - msg := <-sm.CommunicationChan - - sm.mu.Lock() - receiverSession, prs := sm.Sessions[msg.ReceiverID] - if !prs { - // It should never be the case that the other session - // is not found. The sender session should terminate - // msg.SenderSession.terminate() - continue - } - - if receiverSession.GameUuid != msg.GameUuid { - panic("receiver session msg game is not the same as game uuid; this error should never happen") - } - - switch msg.PayloadType { - case TypeSessionMessageJSON: - switch WriteJSONWithRetry(receiverSession.Conn, msg.Payload) { - case ConnLoopAbnormalClosureRetry: - switch receiverSession.handleAbnormalClosure() { - case ConnLoopCodeBreak: - receiverSession.terminate() - - case ConnLoopCodeContinue: - } - - case ConnLoopCodeBreak: - receiverSession.terminate() - - case ConnLoopCodePassThrough: - } - - case TypeSessionMessageBytes: - msgByte, ok := msg.Payload.([]byte) - if ok { - receiverSession.Conn.WriteMessage(websocket.TextMessage, msgByte) - } - - default: - log.Println("invalid message type for intersession communication") - continue - } - - sm.mu.Unlock() - } -} - -// To ensure that there is no dangling connections, -// server session manager marks the connections with a -// lifetime of more than 20 mins as stale and deletes them. -func (sm *SessionManager) CleanUpPeriodically() { - for { - time.Sleep(cleanupInterval) - - sm.mu.Lock() - toDelete := make([]string, 0, assumedClosedConns) - - for ID, session := range sm.Sessions { - if time.Since(session.CreatedAt) > cleanupInterval { - toDelete = append(toDelete, ID) - } - } - - log.Println("Clean up sessions:") - for _, ID := range toDelete { - delete(sm.Sessions, ID) - log.Printf("removed: %s", ID) - } - sm.mu.Unlock() - } -} diff --git a/api/sessions_api.go b/api/sessions_api.go deleted file mode 100644 index 91938fe..0000000 --- a/api/sessions_api.go +++ /dev/null @@ -1,348 +0,0 @@ -package api - -import ( - "encoding/json" - "log" - "time" - - "github.com/gorilla/websocket" - mb "github.com/saeidalz13/battleship-backend/models/battleship" - mc "github.com/saeidalz13/battleship-backend/models/connection" -) - -const ( - gracePeriod time.Duration = time.Minute * 2 -) - -type Session struct { - ID string - Conn *websocket.Conn - GameUuid string - Player *mb.Player - StopRetry chan struct{} - GameManager *GameManager - SessionManager *SessionManager - CreatedAt time.Time -} - -func NewSession(conn *websocket.Conn, sessionID string, gameManager *GameManager, sessionManager *SessionManager) *Session { - return &Session{ - ID: sessionID, - Conn: conn, - StopRetry: make(chan struct{}), - GameManager: gameManager, - SessionManager: sessionManager, - CreatedAt: time.Now(), - } -} - -func (s *Session) run() { - defer s.terminate() - -sessionLoop: - for { - // A WebSocket frame can be one of 6 types: text=1, binary=2, ping=9, pong=10, close=8 and continuation=0 - // https://www.rfc-editor.org/rfc/rfc6455.html#section-11.8 - _, payload, err := s.Conn.ReadMessage() - if err != nil { - if s.handleReadErr(err) == ConnLoopCodeBreak { - break sessionLoop - } - } - var signal mc.Signal - if err := json.Unmarshal(payload, &signal); err != nil { - log.Println("incoming msg does not contain 'code':", err) - resp := mc.NewMessage[mc.NoPayload](mc.CodeSignalAbsent) - resp.AddError("incoming req payload must contain 'code' field", "") - - if s.writeToConn(resp) == ConnLoopCodeBreak { - break sessionLoop - } - continue sessionLoop - } - - switch signal.Code { - case mc.CodeCreateGame: - req := NewRequest(s, payload) - resp := req.HandleCreateGame() - - if s.writeToConn(resp) == ConnLoopCodeBreak { - break sessionLoop - } - - case mc.CodeAttack: - req := NewRequest(s, payload) - // response will have the IsTurn as false field of attacker - resp, defender := req.HandleAttack() - - if s.writeToConn(resp) == ConnLoopCodeBreak { - break sessionLoop - } - if resp.Error != nil { - continue sessionLoop - } - - // defender turn is set to true - resp.Payload.IsTurn = true - s.notifyOtherSession(defender.SessionID, resp, TypeSessionMessageJSON) - - if defender.MatchStatus == mb.PlayerMatchStatusLost { - respAttacker := mc.NewMessage[mc.RespEndGame](mc.CodeEndGame) - respAttacker.AddPayload(mc.RespEndGame{PlayerMatchStatus: mb.PlayerMatchStatusWon}) - if s.writeToConn(respAttacker) == ConnLoopCodeBreak { - break sessionLoop - } - - respDefender := mc.NewMessage[mc.RespEndGame](mc.CodeEndGame) - respDefender.AddPayload(mc.RespEndGame{PlayerMatchStatus: mb.PlayerMatchStatusLost}) - s.notifyOtherSession(defender.SessionID, respDefender, TypeSessionMessageJSON) - } - - case mc.CodeReady: - req := NewRequest(s, payload) - resp, game := req.HandleReadyPlayer() - - if s.writeToConn(resp) == ConnLoopCodeBreak { - break sessionLoop - } - if resp.Error != nil { - continue sessionLoop - } - - if game.HostPlayer.IsReady && game.JoinPlayer.IsReady { - respStartGame := mc.NewMessage[mc.NoPayload](mc.CodeStartGame) - if s.writeToConn(respStartGame) == ConnLoopCodeBreak { - break sessionLoop - } - - otherPlayer := game.GetOtherPlayer(s.Player) - s.notifyOtherSession(otherPlayer.SessionID, respStartGame, TypeSessionMessageJSON) - } - - case mc.CodeJoinGame: - req := NewRequest(s, payload) - resp, game := req.HandleJoinPlayer() - - if s.writeToConn(resp) == ConnLoopCodeBreak { - break sessionLoop - } - if resp.Error != nil { - break sessionLoop - } - - readyResp := mc.NewMessage[mc.NoPayload](mc.CodeSelectGrid) - if s.writeToConn(readyResp) == ConnLoopCodeBreak { - break sessionLoop - } - s.notifyOtherSession(game.HostPlayer.SessionID, readyResp, TypeSessionMessageJSON) - - case mc.CodeRematchCall: - // 1. See if the game still exists - game, err := s.GameManager.FindGame(s.GameUuid) - if err != nil { - break sessionLoop - } - - if game.IsRematchAlreadyCalled() { - continue sessionLoop - } - - game.CallRematch() - - otherPlayer := game.GetOtherPlayer(s.Player) - if otherPlayer == nil { - break sessionLoop - } - - s.Player.IsTurn = true - // Notify the other player if they want a rematch - msg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCall) - s.notifyOtherSession(otherPlayer.SessionID, msg, TypeSessionMessageJSON) - - case mc.CodeRematchCallAccepted: - // Send the rematch call acceptance to other player - game, err := s.GameManager.FindGame(s.GameUuid) - if err != nil { - break sessionLoop - } - - if err := game.Reset(); err != nil { - break sessionLoop - } - - // Notify the other player with their turn - msgOtherPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) - otherPlayer := game.GetOtherPlayer(s.Player) - if otherPlayer == nil { - break sessionLoop - } - msgOtherPlayer.AddPayload(mc.RespRematch{IsTurn: otherPlayer.IsTurn}) - s.notifyOtherSession(otherPlayer.SessionID, msgOtherPlayer, TypeSessionMessageJSON) - - s.Player.IsTurn = false - msgPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) - msgPlayer.AddPayload(mc.RespRematch{IsTurn: s.Player.IsTurn}) - - // Notify the acceptor with their turn - if s.writeToConn(msgPlayer) == ConnLoopCodeBreak { - break sessionLoop - } - - case mc.CodeRematchCallRejected: - game, err := s.GameManager.FindGame(s.GameUuid) - if err != nil { - break sessionLoop - } - - // Notify the other player that no rematch is wanted now - msg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCallRejected) - otherPlayer := game.GetOtherPlayer(s.Player) - if otherPlayer != nil { - s.notifyOtherSession(otherPlayer.SessionID, msg, TypeSessionMessageJSON) - } - - break sessionLoop - - case mc.CodePlayerInteraction: - game, err := s.GameManager.FindGame(s.GameUuid) - if err != nil { - break sessionLoop - } - otherPlayer := game.GetOtherPlayer(s.Player) - if otherPlayer == nil { - break sessionLoop - } - - s.notifyOtherSession(otherPlayer.SessionID, payload, TypeSessionMessageBytes) - continue sessionLoop - - default: - respInvalidSignal := mc.NewMessage[mc.NoPayload](mc.CodeInvalidSignal) - respInvalidSignal.AddError("", "invalid code in the incoming payload") - if s.writeToConn(respInvalidSignal) == ConnLoopCodeBreak { - break sessionLoop - } - } - } -} - -// This is to send a message to the other session. -func (s *Session) notifyOtherSession(otherSessionId string, msg interface{}, payloadType int8) { - switch payloadType { - case TypeSessionMessageJSON: - s.SessionManager.CommunicationChan <- NewSessionMessageJSON(otherSessionId, s.GameUuid, msg) - case TypeSessionMessageBytes: - s.SessionManager.CommunicationChan <- NewSessionMessageBytes(otherSessionId, s.GameUuid, msg) - } -} - -// This will delete player from the game players map -// and delete the player session -func (s *Session) terminate() { - if s.Player != nil { - s.GameManager.DeletePlayerFromGame(s.GameUuid, s.Player.Uuid) - } - s.SessionManager.DeleteSession(s.ID) -} - -// Writes to the connection of that session. It also -// handles the abnormal or other types of errors of -// writing to a websocket connection. -func (s *Session) writeToConn(p interface{}) int { - switch WriteJSONWithRetry(s.Conn, p) { - case ConnLoopAbnormalClosureRetry: - switch s.handleAbnormalClosure() { - case ConnLoopCodeBreak: - return ConnLoopCodeBreak - - case ConnLoopCodeContinue: - } - case ConnLoopCodeBreak: - return ConnLoopCodeBreak - default: - } - - return ConnLoopCodeContinue -} - -// This function takes care of abnormal closures happening -// to either of the clients. This happens due to backgrounding -// in IOS clients or any other unexpected reasons for web apps. -func (s *Session) handleAbnormalClosure() int { - // This means there is no game and abnormal closure is happening - // which means this session is invalid and should end - game, err := s.GameManager.FindGame(s.GameUuid) - if err != nil { - return ConnLoopCodeBreak - } - - otherPlayer := game.GetOtherPlayer(s.Player) - if otherPlayer == nil { - return ConnLoopCodeBreak - } - - // Absence of otherPlayer session means this game is invalid - otherSession, err := s.SessionManager.FindSession(otherPlayer.SessionID) - if err != nil { - return ConnLoopCodeBreak - } - - if err := otherSession.Conn.WriteJSON(mc.NewMessage[mc.NoPayload](mc.CodeOtherPlayerGracePeriod)); err != nil { - // If other player connection is disrupted as well, then end the session - return ConnLoopCodeBreak - } - - log.Printf("starting grace period for %s\n", s.ID) - timer := time.NewTimer(gracePeriod) - - select { - case <-timer.C: - if otherSession != nil { - _ = otherSession.Conn.WriteJSON(mc.NewMessage[mc.NoPayload](mc.CodeOtherPlayerDisconnected)) - } - log.Printf("session terminated: %s\n", s.ID) - return ConnLoopCodeBreak - - case <-s.StopRetry: - if otherSession != nil { - _ = otherSession.Conn.WriteJSON(mc.NewMessage[mc.NoPayload](mc.CodeOtherPlayerReconnected)) - } - log.Printf("player reconnected, session: %s\n", s.ID) - return ConnLoopCodeContinue - } -} - -// Handles the errors that occurs when reading from -// ws connection. `ConnLoopCodeContinue` will results in -// terminating the session and removing `run` from stack -func (s *Session) handleReadErr(err error) int { - retries := 0 - - switch IdentifyWsConnErrAction(err) { - case ConnLoopAbnormalClosureRetry: - switch s.handleAbnormalClosure() { - case ConnLoopCodeBreak: - return ConnLoopCodeBreak - case ConnLoopCodeContinue: - } - - case ConnLoopCodeRetry: - if retries < maxWriteWsRetries { - retries++ - log.Printf("failed to read from ws conn [%s]; retrying... (retry no. %d)\n", s.Conn.RemoteAddr().String(), retries) - time.Sleep(time.Duration(retries*backOffFactor) * time.Second) - - } else { - return ConnLoopCodeBreak - - } - - case ConnLoopCodeBreak: - log.Printf("break ws conn loop [%s] due to: %s\n", s.Conn.RemoteAddr().String(), err) - return ConnLoopCodeBreak - - case ConnLoopCodeContinue: - } - - return ConnLoopCodeContinue -} diff --git a/test/ws_test.go b/test/ws_test.go index d54abe7..e9707ef 100644 --- a/test/ws_test.go +++ b/test/ws_test.go @@ -283,6 +283,7 @@ func TestReadyGame(t *testing.T) { func TestPlayerInteraction(t *testing.T) { msg := mc.NewMessage[mc.PlayerInteraction](mc.CodePlayerInteraction) + msg.AddPayload(mc.PlayerInteraction{Content: "salam!"}) tests := []Test[mc.Message[mc.PlayerInteraction], mc.Message[mc.PlayerInteraction]]{ { @@ -799,7 +800,6 @@ func TestAttack(t *testing.T) { } if test.respPayload.Error != nil { - log.Printf("%+v\n", test.respPayload.Error) if test.respPayload.Error.ErrorDetails != test.expectedErr { t.Fatalf("expected error: %s\t got: %s", test.expectedErr, test.respPayload.Error.ErrorDetails) } From cfe9eca25267f0282a3a8e9e3b0a120dca28381c Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Tue, 16 Jul 2024 18:06:57 -0600 Subject: [PATCH 06/19] implemented graceful shutdown for server --- cmd/main.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 0f767df..4341633 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,9 +1,13 @@ package main import ( + "context" "log" "net/http" "os" + "os/signal" + "syscall" + "time" "github.com/joho/godotenv" "github.com/saeidalz13/battleship-backend/api" @@ -42,6 +46,23 @@ func main() { mux := http.NewServeMux() mux.Handle("GET /battleship", requestProcessor) - log.Printf("Listening to port %s\n", port) - log.Fatalln(http.ListenAndServe("0.0.0.0:"+port, mux)) + s := &http.Server{ + Addr: ":" + port, + Handler: mux, + } + + go func() { + log.Printf("Listening to port %s\n", port) + log.Fatalln(s.ListenAndServe()) + }() + + sigChan := make(chan os.Signal, 1) + + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + sig := <-sigChan + log.Println("Server termination signal from OS, graceful shutdown\treason:", sig) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + s.Shutdown(ctx) } From 03debbc39b1de1eb755f7158f5e43304f7d32c8d Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Thu, 18 Jul 2024 04:55:30 -0600 Subject: [PATCH 07/19] modified Makefile --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index e4c015c..37a407c 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,10 @@ flyconsole: fly console --app $(APP) ## With games with titles -> like 10 wins => captain +flypdb: +# Connect to production db + fly pg connect -a battleship-db + # Websocket fly ws_staging: websocat wss://battleship-go-ios-staging.fly.dev/battleship \ No newline at end of file From fe7eeac34ffedd7e145b44490cdd3a2ed3c9c94c Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Thu, 18 Jul 2024 04:57:32 -0600 Subject: [PATCH 08/19] transfer grid codes to grid.go --- models/battleship/grid.go | 14 ++++++++++++++ models/battleship/player.go | 6 ------ models/battleship/ship.go | 8 -------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/models/battleship/grid.go b/models/battleship/grid.go index 1e6557f..506d9cb 100644 --- a/models/battleship/grid.go +++ b/models/battleship/grid.go @@ -1,5 +1,19 @@ package battleship +const ( + PositionStateAttackGridEmpty uint8 = iota + PositionStateAttackGridMiss + PositionStateAttackGridHit +) + +const ( + PositionStateDefenceGridEmpty uint8 = iota + PositionStateDefenceGridHit + PositionStateDefenceDestroyer + PositionStateDefenceCruiser + PositionStateDefenceBattleship +) + type Coordinates struct { X uint8 `json:"x"` Y uint8 `json:"y"` diff --git a/models/battleship/player.go b/models/battleship/player.go index 0891858..2326191 100644 --- a/models/battleship/player.go +++ b/models/battleship/player.go @@ -10,12 +10,6 @@ const ( PlayerMatchStatusWon ) -const ( - PositionStateAttackGridEmpty uint8 = iota - PositionStateAttackGridMiss - PositionStateAttackGridHit -) - type Player interface { SessionId() string Uuid() string diff --git a/models/battleship/ship.go b/models/battleship/ship.go index 5daa72b..b2b96fd 100644 --- a/models/battleship/ship.go +++ b/models/battleship/ship.go @@ -1,13 +1,5 @@ package battleship -const ( - PositionStateDefenceGridEmpty uint8 = iota - PositionStateDefenceGridHit - PositionStateDefenceDestroyer - PositionStateDefenceCruiser - PositionStateDefenceBattleship -) - const ( sunkenShipsToLose uint8 = 3 ) From 1ce721e5423ee199ebe8ec9cb770c3434251b974 Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Thu, 18 Jul 2024 06:15:04 -0600 Subject: [PATCH 09/19] adde mine logic to attack handler and rp --- api/handlers_api.go | 19 +++++++++++++++++-- api/request_processor.go | 7 +++---- models/battleship/grid.go | 7 +++++++ models/battleship/player.go | 11 +++++++++++ 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/api/handlers_api.go b/api/handlers_api.go index 7901990..75d2d6d 100644 --- a/api/handlers_api.go +++ b/api/handlers_api.go @@ -2,7 +2,6 @@ package api import ( "encoding/json" - "log" cerr "github.com/saeidalz13/battleship-backend/internal/error" mb "github.com/saeidalz13/battleship-backend/models/battleship" @@ -140,6 +139,23 @@ func (r Request) HandleAttack(game *mb.Game, attacker mb.Player, defender mb.Pla hostPlayer := game.HostPlayer() joinPlayer := game.JoinPlayer() + // TODO: Check for game mode for this section + if defender.DidAttackerHitMine(coordinates) { + attacker.SetMatchStatusToLost() + defender.SetMatchStatusToWon() + + resp.AddPayload(mc.RespAttack{ + X: coordinates.X, + Y: coordinates.Y, + PositionState: mb.PositionStateMine, + SunkenShipsHost: hostPlayer.SunkenShips(), + SunkenShipsJoin: joinPlayer.SunkenShips(), + IsTurn: attacker.IsTurn(), + }) + + return resp + } + if defender.IsAttackMiss(coordinates) { attacker.SetAttackGridToMiss(coordinates) @@ -178,7 +194,6 @@ func (r Request) HandleAttack(game *mb.Game, attacker mb.Player, defender mb.Pla } } - log.Println("attack complete") resp.Payload.SunkenShipsHost = hostPlayer.SunkenShips() resp.Payload.SunkenShipsJoin = joinPlayer.SunkenShips() return resp diff --git a/api/request_processor.go b/api/request_processor.go index 4ca27fd..1ad642b 100644 --- a/api/request_processor.go +++ b/api/request_processor.go @@ -279,17 +279,16 @@ sessionLoop: if err := rp.sessionManager.Communicate(sessionId, receiverSessionId, respMsg, mc.MessageTypeJSON); err != nil { break sessionLoop } - log.Println("attack resp sent to other") - if sessionPlayer.IsWinner() { + if sessionPlayer.IsMatchOver() { respAttacker := mc.NewMessage[mc.RespEndGame](mc.CodeEndGame) - respAttacker.AddPayload(mc.RespEndGame{PlayerMatchStatus: mb.PlayerMatchStatusWon}) + respAttacker.AddPayload(mc.RespEndGame{PlayerMatchStatus: sessionPlayer.MatchStatus()}) if err := rp.sessionManager.WriteToSessionConn(session, respAttacker, mc.MessageTypeJSON, receiverSessionId); err != nil { break sessionLoop } respDefender := mc.NewMessage[mc.RespEndGame](mc.CodeEndGame) - respDefender.AddPayload(mc.RespEndGame{PlayerMatchStatus: mb.PlayerMatchStatusLost}) + respDefender.AddPayload(mc.RespEndGame{PlayerMatchStatus: otherSessionPlayer.MatchStatus()}) if err := rp.sessionManager.Communicate(sessionId, receiverSessionId, respDefender, mc.MessageTypeJSON); err != nil { break sessionLoop } diff --git a/models/battleship/grid.go b/models/battleship/grid.go index 506d9cb..d834525 100644 --- a/models/battleship/grid.go +++ b/models/battleship/grid.go @@ -9,11 +9,18 @@ const ( const ( PositionStateDefenceGridEmpty uint8 = iota PositionStateDefenceGridHit + + // Ship codes in defence grid PositionStateDefenceDestroyer PositionStateDefenceCruiser PositionStateDefenceBattleship ) +// Chosse the max uint8 to make the +// mine code unique. Hitting this will +// cause player to lose +const PositionStateMine uint8 = 255 + type Coordinates struct { X uint8 `json:"x"` Y uint8 `json:"y"` diff --git a/models/battleship/player.go b/models/battleship/player.go index 2326191..e6618b1 100644 --- a/models/battleship/player.go +++ b/models/battleship/player.go @@ -17,6 +17,7 @@ type Player interface { AreAllShipsSunken() bool IsShipSunken(uint8) bool IsWinner() bool + IsMatchOver() bool IncrementShipHit(code uint8, coordinates Coordinates) SetAttackGrid(newGrid Grid) SetReady(newGrid Grid) @@ -45,6 +46,8 @@ type Player interface { IsReady() bool IsTurn() bool + + DidAttackerHitMine(coordinates Coordinates) bool } type BattleshipPlayer struct { @@ -146,10 +149,18 @@ func (bp *BattleshipPlayer) IncrementSunkenShips() { bp.sunkenShips++ } +func (bp *BattleshipPlayer) DidAttackerHitMine(coordinates Coordinates) bool { + return bp.defenceGrid[coordinates.X][coordinates.Y] == PositionStateMine +} + func (bp *BattleshipPlayer) IsWinner() bool { return bp.matchStatus == PlayerMatchStatusWon } +func (bp *BattleshipPlayer) IsMatchOver() bool { + return bp.matchStatus != PlayerMatchStatusUndefined +} + func (bp *BattleshipPlayer) PrepareForRematch(gridSize uint8) { bp.matchStatus = PlayerMatchStatusUndefined bp.isReady = false From 5cc49bc8d185d5cbc856d9c971e40bf712abffc7 Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Thu, 18 Jul 2024 06:31:30 -0600 Subject: [PATCH 10/19] added mode to Game struct and modified req and resp json --- api/handlers_api.go | 13 +++++++++---- internal/error/error.go | 8 ++++++-- models/battleship/game.go | 19 +++++++++++++++---- models/battleship/game_manager.go | 17 +++++++++++++---- models/connection/request_json.go | 5 +++-- models/connection/response_json.go | 13 +++++++------ 6 files changed, 53 insertions(+), 22 deletions(-) diff --git a/api/handlers_api.go b/api/handlers_api.go index 75d2d6d..1bbeef8 100644 --- a/api/handlers_api.go +++ b/api/handlers_api.go @@ -38,15 +38,15 @@ func NewRequest(payloads ...[]byte) Request { } func (r Request) HandleCreateGame(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespCreateGame]) { - var reqCreateGame mc.Message[mc.ReqCreateGame] + var req mc.Message[mc.ReqCreateGame] respMsg := mc.NewMessage[mc.RespCreateGame](mc.CodeCreateGame) - if err := json.Unmarshal(r.payload, &reqCreateGame); err != nil { + if err := json.Unmarshal(r.payload, &req); err != nil { respMsg.AddError(err.Error(), cerr.ConstErrInvalidPayload) return nil, nil, respMsg } - game, err := gm.CreateGame(reqCreateGame.Payload.GameDifficulty) + game, err := gm.CreateGame(req.Payload.GameDifficulty, req.Payload.GameMode) if err != nil { respMsg.AddError(err.Error(), cerr.ConstErrCreateGame) return nil, nil, respMsg @@ -77,7 +77,12 @@ func (r Request) HandleJoinPlayer(gm mb.GameManager, sessionId string) (*mb.Game joinPlayer := game.CreateJoinPlayer(sessionId) - respMsg.AddPayload(mc.RespJoinGame{GameUuid: game.Uuid(), PlayerUuid: joinPlayer.Uuid(), GameDifficulty: game.Difficulty()}) + respMsg.AddPayload(mc.RespJoinGame{ + GameUuid: game.Uuid(), + PlayerUuid: joinPlayer.Uuid(), + GameDifficulty: game.Difficulty(), + GameMode: game.Mode(), + }) return game, joinPlayer, respMsg } diff --git a/internal/error/error.go b/internal/error/error.go index 5bf5122..1afd643 100644 --- a/internal/error/error.go +++ b/internal/error/error.go @@ -48,8 +48,12 @@ func ErrValueNotGridInt() error { // Game Errors -func ErrInvalidGameDifficulty() error { - return fmt.Errorf("invalid difficulty") +func ErrInvalidGameDifficulty(difficulty uint8) error { + return fmt.Errorf("invalid difficulty: %d", difficulty) +} + +func ErrInvalidGameMode(mode uint8) error { + return fmt.Errorf("invalid game mode: %d", mode) } func ErrGameAleardyRecalled() error { diff --git a/models/battleship/game.go b/models/battleship/game.go index 860151a..288f1db 100644 --- a/models/battleship/game.go +++ b/models/battleship/game.go @@ -12,6 +12,11 @@ const ( GameDifficultyHard ) +const ( + GameModeDefault uint8 = iota + GameModeMine +) + const ( GridSizeEasy uint8 = 6 GridSizeNormal uint8 = 7 @@ -21,20 +26,22 @@ const ( ) type Game struct { - uuid string - hostPlayer *BattleshipPlayer - joinPlayer *BattleshipPlayer difficulty uint8 gridSize uint8 validUpperBound uint8 + mode uint8 rematchAlreadyRequested bool + uuid string + hostPlayer *BattleshipPlayer + joinPlayer *BattleshipPlayer mu sync.Mutex } -func newGame(difficulty uint8, uuid string) *Game { +func newGame(difficulty uint8, mode uint8, uuid string) *Game { game := &Game{ uuid: uuid, difficulty: difficulty, + mode: mode, } var newGridSize uint8 @@ -56,6 +63,10 @@ func (g *Game) Uuid() string { return g.uuid } +func (g *Game) Mode() uint8 { + return g.mode +} + func (g *Game) CreateHostPlayer(sessionId string) *BattleshipPlayer { g.hostPlayer = newPlayer(true, true, sessionId, g.gridSize) return g.hostPlayer diff --git a/models/battleship/game_manager.go b/models/battleship/game_manager.go index e869f03..2707a89 100644 --- a/models/battleship/game_manager.go +++ b/models/battleship/game_manager.go @@ -8,11 +8,12 @@ import ( ) type GameManager interface { - CreateGame(difficulty uint8) (*Game, error) + CreateGame(difficulty, mode uint8) (*Game, error) FetchGame(gameUuid string) (*Game, error) TerminateGame(gameUuid string) isDifficultyValid(uint8) bool + isModeValid(uint8) bool } type BattleshipGameManager struct { @@ -27,13 +28,17 @@ func NewBattleshipGameManager() *BattleshipGameManager { games: make(map[string]*Game, 10), } } -func (bgm *BattleshipGameManager) CreateGame(difficulty uint8) (*Game, error) { +func (bgm *BattleshipGameManager) CreateGame(difficulty, mode uint8) (*Game, error) { if !bgm.isDifficultyValid(difficulty) { - return nil, cerr.ErrInvalidGameDifficulty() + return nil, cerr.ErrInvalidGameDifficulty(difficulty) + } + + if !bgm.isModeValid(mode) { + return nil, cerr.ErrInvalidGameMode(mode) } gameUuid := uuid.NewString()[:6] - bgm.games[gameUuid] = newGame(difficulty, gameUuid) + bgm.games[gameUuid] = newGame(difficulty, mode, gameUuid) return bgm.games[gameUuid], nil } @@ -59,3 +64,7 @@ func (bgm *BattleshipGameManager) TerminateGame(gameUuid string) { func (bgm *BattleshipGameManager) isDifficultyValid(difficulty uint8) bool { return !(difficulty != GameDifficultyEasy && difficulty != GameDifficultyNormal && difficulty != GameDifficultyHard) } + +func (bgm *BattleshipGameManager) isModeValid(mode uint8) bool { + return mode == GameModeDefault || mode == GameModeMine +} diff --git a/models/connection/request_json.go b/models/connection/request_json.go index 7eccd9e..e65566f 100644 --- a/models/connection/request_json.go +++ b/models/connection/request_json.go @@ -6,6 +6,7 @@ import ( type ReqCreateGame struct { GameDifficulty uint8 `json:"game_difficulty"` + GameMode uint8 `json:"game_mode"` } type ReqReadyPlayer struct { @@ -21,6 +22,6 @@ type ReqJoinGame struct { type ReqAttack struct { GameUuid string `json:"game_uuid"` PlayerUuid string `json:"player_uuid"` - X uint8 `json:"x"` - Y uint8 `json:"y"` + X uint8 `json:"x"` + Y uint8 `json:"y"` } diff --git a/models/connection/response_json.go b/models/connection/response_json.go index 1748990..31089d2 100644 --- a/models/connection/response_json.go +++ b/models/connection/response_json.go @@ -7,7 +7,8 @@ import ( type RespJoinGame struct { GameUuid string `json:"game_uuid"` PlayerUuid string `json:"player_uuid"` - GameDifficulty uint8 `json:"game_difficulty"` + GameDifficulty uint8 `json:"game_difficulty"` + GameMode uint8 `json:"game_mode"` } type RespCreateGame struct { @@ -16,12 +17,12 @@ type RespCreateGame struct { } type RespAttack struct { - X uint8 `json:"x"` - Y uint8 `json:"y"` - PositionState uint8 `json:"position_state"` + X uint8 `json:"x"` + Y uint8 `json:"y"` + PositionState uint8 `json:"position_state"` IsTurn bool `json:"is_turn"` - SunkenShipsHost uint8 `json:"sunken_ships_host"` - SunkenShipsJoin uint8 `json:"sunken_ships_join"` + SunkenShipsHost uint8 `json:"sunken_ships_host"` + SunkenShipsJoin uint8 `json:"sunken_ships_join"` DefenderSunkenShipsCoords []mb.Coordinates `json:"defender_sunken_ships_coords,omitempty"` } From 13aa477f63577a2568ae4988ecd82a36ef01568d Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Thu, 18 Jul 2024 10:21:25 -0600 Subject: [PATCH 11/19] changed test filename --- test/{ws_test.go => ws_defaultgame_test.go} | 1 + 1 file changed, 1 insertion(+) rename test/{ws_test.go => ws_defaultgame_test.go} (99%) diff --git a/test/ws_test.go b/test/ws_defaultgame_test.go similarity index 99% rename from test/ws_test.go rename to test/ws_defaultgame_test.go index e9707ef..ce71aa3 100644 --- a/test/ws_test.go +++ b/test/ws_defaultgame_test.go @@ -70,6 +70,7 @@ func TestCreateGame(t *testing.T) { expectedCode: mc.CodeCreateGame, reqPayload: mc.Message[mc.ReqCreateGame]{Code: mc.CodeCreateGame, Payload: mc.ReqCreateGame{ GameDifficulty: mb.GameDifficultyEasy, + GameMode: mb.GameModeDefault, }}, respPayload: mc.NewMessage[mc.RespCreateGame](mc.CodeCreateGame), conn: HostConn, From 7a548ed95d0e324fdb7ceaf82bb62e8da170e3fd Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Thu, 18 Jul 2024 12:13:01 -0600 Subject: [PATCH 12/19] added methods to plant mine for defender --- api/handlers_api.go | 237 ----------------------------- models/battleship/game.go | 4 + models/battleship/player.go | 22 +++ models/connection/response_json.go | 4 + 4 files changed, 30 insertions(+), 237 deletions(-) delete mode 100644 api/handlers_api.go diff --git a/api/handlers_api.go b/api/handlers_api.go deleted file mode 100644 index 1bbeef8..0000000 --- a/api/handlers_api.go +++ /dev/null @@ -1,237 +0,0 @@ -package api - -import ( - "encoding/json" - - cerr "github.com/saeidalz13/battleship-backend/internal/error" - mb "github.com/saeidalz13/battleship-backend/models/battleship" - mc "github.com/saeidalz13/battleship-backend/models/connection" -) - -type RequestHandler interface { - HandleCreateGame(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespCreateGame]) - HandleReadyPlayer(gm mb.GameManager, sessionGame *mb.Game, sessionPlayer mb.Player) mc.Message[mc.NoPayload] - HandleJoinPlayer(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespJoinGame]) - HandleAttack(*mb.Game, mb.Player, mb.Player, mb.GameManager) mc.Message[mc.RespAttack] - HandleCallRematch(bgm mb.GameManager, sessionGame *mb.Game) (mc.Message[mc.NoPayload], error) - HandleAcceptRematchCall(bgm mb.GameManager, sessionGame *mb.Game, sessionPlayer, otherSessionPlayer mb.Player) (mc.Message[mc.RespRematch], mc.Message[mc.RespRematch], error) -} - -// Every incoming valid request will have this structure -// The request then is handled in line with WsRequestHandler interface -type Request struct { - payload []byte -} - -// This tells the compiler that WsRequest struct must be of type of WsRequestHandler -var _ RequestHandler = (*Request)(nil) - -func NewRequest(payloads ...[]byte) Request { - if len(payloads) > 1 { - panic("request cannot accept more than one payload") - } - r := Request{} - if len(payloads) == 1 { - r.payload = payloads[0] - } - return r -} - -func (r Request) HandleCreateGame(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespCreateGame]) { - var req mc.Message[mc.ReqCreateGame] - respMsg := mc.NewMessage[mc.RespCreateGame](mc.CodeCreateGame) - - if err := json.Unmarshal(r.payload, &req); err != nil { - respMsg.AddError(err.Error(), cerr.ConstErrInvalidPayload) - return nil, nil, respMsg - } - - game, err := gm.CreateGame(req.Payload.GameDifficulty, req.Payload.GameMode) - if err != nil { - respMsg.AddError(err.Error(), cerr.ConstErrCreateGame) - return nil, nil, respMsg - } - - hostPlayer := game.CreateHostPlayer(sessionId) - - respMsg.AddPayload(mc.RespCreateGame{GameUuid: game.Uuid(), HostUuid: hostPlayer.Uuid()}) - return game, hostPlayer, respMsg -} - -// Join user sends the game uuid and if this game exists, -// a new join player is created and added to the database -func (r Request) HandleJoinPlayer(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespJoinGame]) { - var joinGameReq mc.Message[mc.ReqJoinGame] - respMsg := mc.NewMessage[mc.RespJoinGame](mc.CodeJoinGame) - - if err := json.Unmarshal(r.payload, &joinGameReq); err != nil { - respMsg.AddError(err.Error(), cerr.ConstErrInvalidPayload) - return nil, nil, respMsg - } - - game, err := gm.FetchGame(joinGameReq.Payload.GameUuid) - if err != nil { - respMsg.AddError(err.Error(), cerr.ConstErrJoin) - return nil, nil, respMsg - } - - joinPlayer := game.CreateJoinPlayer(sessionId) - - respMsg.AddPayload(mc.RespJoinGame{ - GameUuid: game.Uuid(), - PlayerUuid: joinPlayer.Uuid(), - GameDifficulty: game.Difficulty(), - GameMode: game.Mode(), - }) - return game, joinPlayer, respMsg -} - -// User will choose the configurations of ships on defence grid. -// Then the grid is sent to backend and adjustment happens accordingly. -func (r Request) HandleReadyPlayer(bgm mb.GameManager, game *mb.Game, sessionPlayer mb.Player) mc.Message[mc.NoPayload] { - var readyPlayerReq mc.Message[mc.ReqReadyPlayer] - resp := mc.NewMessage[mc.NoPayload](mc.CodeReady) - - if err := json.Unmarshal(r.payload, &readyPlayerReq); err != nil { - resp.AddError(err.Error(), cerr.ConstErrInvalidPayload) - return resp - } - - // Check to see if rows and cols are equal to game's grid size - if err := game.SetPlayerReadyForGame(sessionPlayer, readyPlayerReq.Payload.DefenceGrid); err != nil { - resp.AddError(err.Error(), cerr.ConstErrReady) - return resp - } - - return resp -} - -// Handle the attack logic for the incoming request -func (r Request) HandleAttack(game *mb.Game, attacker mb.Player, defender mb.Player, gm mb.GameManager) mc.Message[mc.RespAttack] { - var reqAttack mc.Message[mc.ReqAttack] - resp := mc.NewMessage[mc.RespAttack](mc.CodeAttack) - - if err := json.Unmarshal(r.payload, &reqAttack); err != nil { - resp.AddError(err.Error(), cerr.ConstErrInvalidPayload) - return resp - } - - coordinates := mb.NewCoordinates(reqAttack.Payload.X, reqAttack.Payload.Y) - if !game.AreAttackCoordinatesValid(coordinates) { - resp.AddError(cerr.ErrXorYOutOfGridBound(coordinates.X, coordinates.Y).Error(), cerr.ConstErrAttack) - return resp - } - - // Attack validity check - if !attacker.IsTurn() { - resp.AddError(cerr.ErrNotTurnForAttacker(attacker.Uuid()).Error(), cerr.ConstErrAttack) - return resp - } - - if !attacker.IsAttackGridEmptyInCoordinates(coordinates) { - resp.AddError(cerr.ErrAttackPositionAlreadyFilled(coordinates.X, coordinates.Y).Error(), cerr.ConstErrAttack) - return resp - } - - if defender.IsDefenceGridAlreadyHitInCoordinates(coordinates) { - resp.AddError(cerr.ErrDefenceGridPositionAlreadyHit(coordinates.X, coordinates.Y).Error(), cerr.ConstErrAttack) - return resp - } - - attacker.SetTurnFalse() - defender.SetTurnTrue() - - hostPlayer := game.HostPlayer() - joinPlayer := game.JoinPlayer() - - // TODO: Check for game mode for this section - if defender.DidAttackerHitMine(coordinates) { - attacker.SetMatchStatusToLost() - defender.SetMatchStatusToWon() - - resp.AddPayload(mc.RespAttack{ - X: coordinates.X, - Y: coordinates.Y, - PositionState: mb.PositionStateMine, - SunkenShipsHost: hostPlayer.SunkenShips(), - SunkenShipsJoin: joinPlayer.SunkenShips(), - IsTurn: attacker.IsTurn(), - }) - - return resp - } - - if defender.IsAttackMiss(coordinates) { - attacker.SetAttackGridToMiss(coordinates) - - resp.AddPayload(mc.RespAttack{ - X: coordinates.X, - Y: coordinates.Y, - PositionState: mb.PositionStateAttackGridMiss, - SunkenShipsHost: hostPlayer.SunkenShips(), - SunkenShipsJoin: joinPlayer.SunkenShips(), - IsTurn: attacker.IsTurn(), - }) - return resp - } - - shipCode := defender.ShipCode(coordinates) - defender.IncrementShipHit(shipCode, coordinates) - attacker.SetAttackGridToHit(coordinates) - - // Initialize the response payload - resp.AddPayload(mc.RespAttack{ - X: coordinates.X, - Y: coordinates.Y, - PositionState: mb.PositionStateAttackGridHit, - IsTurn: attacker.IsTurn(), - }) - - // Check if the attack caused the ship to sink - if defender.IsShipSunken(shipCode) { - defender.IncrementSunkenShips() - resp.Payload.DefenderSunkenShipsCoords = defender.ShipHitCoordinates(shipCode) - - // Check if this sunken ship was the last one and the attacker is lost - if defender.AreAllShipsSunken() { - defender.SetMatchStatusToLost() - attacker.SetMatchStatusToWon() - } - } - - resp.Payload.SunkenShipsHost = hostPlayer.SunkenShips() - resp.Payload.SunkenShipsJoin = joinPlayer.SunkenShips() - return resp -} - -func (r Request) HandleCallRematch(bgm mb.GameManager, game *mb.Game) (mc.Message[mc.NoPayload], error) { - respMsg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCall) - - if game.IsRematchAlreadyCalled() { - return respMsg, cerr.ErrGameAleardyRecalled() - } - - game.CallRematchForGame() - return respMsg, nil -} - -func (r Request) HandleAcceptRematchCall( - bgm mb.GameManager, - game *mb.Game, - sessionPlayer, otherSessionPlayer mb.Player, -) (mc.Message[mc.RespRematch], mc.Message[mc.RespRematch], error) { - - if err := game.ResetRematchForGame(); err != nil { - return mc.NewMessage[mc.RespRematch](mc.CodeRematch), mc.NewMessage[mc.RespRematch](mc.CodeRematch), err - } - - sessionPlayer.SetTurnFalse() - msgPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) - msgPlayer.AddPayload(mc.RespRematch{IsTurn: sessionPlayer.IsTurn()}) - - otherSessionPlayer.SetTurnTrue() - msgOtherPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) - msgOtherPlayer.AddPayload(mc.RespRematch{IsTurn: otherSessionPlayer.IsTurn()}) - - return msgPlayer, msgOtherPlayer, nil -} diff --git a/models/battleship/game.go b/models/battleship/game.go index 288f1db..bb90257 100644 --- a/models/battleship/game.go +++ b/models/battleship/game.go @@ -144,3 +144,7 @@ func (g *Game) SetPlayerReadyForGame(player Player, selectedGrid Grid) error { return nil } + +func (g *Game) IsModeMine() bool { + return g.mode == GameModeMine +} diff --git a/models/battleship/player.go b/models/battleship/player.go index e6618b1..5cd4a13 100644 --- a/models/battleship/player.go +++ b/models/battleship/player.go @@ -1,6 +1,8 @@ package battleship import ( + "math/rand" + "github.com/google/uuid" ) @@ -48,6 +50,7 @@ type Player interface { IsTurn() bool DidAttackerHitMine(coordinates Coordinates) bool + PlantMineInDefenceGrid() Coordinates } type BattleshipPlayer struct { @@ -198,4 +201,23 @@ func (bp *BattleshipPlayer) SunkenShips() uint8 { return bp.sunkenShips } +func (bp *BattleshipPlayer) PlantMineInDefenceGrid() Coordinates { + rand.New(rand.NewSource(0)) + + emptyCoordinates := make([]Coordinates, len(bp.defenceGrid)) + for x := range bp.defenceGrid { + for y := range bp.defenceGrid { + if bp.defenceGrid[x][y] == PositionStateDefenceGridEmpty { + emptyCoordinates = append(emptyCoordinates, NewCoordinates(uint8(x), uint8(y))) + } + } + } + + // rmp stands for random mine position + rmp := emptyCoordinates[rand.Intn(len(emptyCoordinates))] + bp.defenceGrid[rmp.X][rmp.Y] = PositionStateMine + + return rmp +} + var _ Player = (*BattleshipPlayer)(nil) diff --git a/models/connection/response_json.go b/models/connection/response_json.go index 31089d2..933cc82 100644 --- a/models/connection/response_json.go +++ b/models/connection/response_json.go @@ -26,6 +26,10 @@ type RespAttack struct { DefenderSunkenShipsCoords []mb.Coordinates `json:"defender_sunken_ships_coords,omitempty"` } +type RespReady struct { + MinePosition mb.Coordinates `json:"mine_position"` +} + type RespSessionId struct { SessionID string `json:"session_id"` } From 7b43b9c28a071265bab7a2ef67b2d127475181ea Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Thu, 18 Jul 2024 12:13:30 -0600 Subject: [PATCH 13/19] changed filename --- api/handlers.go | 242 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 api/handlers.go diff --git a/api/handlers.go b/api/handlers.go new file mode 100644 index 0000000..e91f725 --- /dev/null +++ b/api/handlers.go @@ -0,0 +1,242 @@ +package api + +import ( + "encoding/json" + + cerr "github.com/saeidalz13/battleship-backend/internal/error" + mb "github.com/saeidalz13/battleship-backend/models/battleship" + mc "github.com/saeidalz13/battleship-backend/models/connection" +) + +type RequestHandler interface { + HandleCreateGame(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespCreateGame]) + HandleReadyPlayer(gm mb.GameManager, sessionGame *mb.Game, sessionPlayer mb.Player) mc.Message[mc.RespReady] + HandleJoinPlayer(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespJoinGame]) + HandleAttack(*mb.Game, mb.Player, mb.Player, mb.GameManager) mc.Message[mc.RespAttack] + HandleCallRematch(bgm mb.GameManager, sessionGame *mb.Game) (mc.Message[mc.NoPayload], error) + HandleAcceptRematchCall(bgm mb.GameManager, sessionGame *mb.Game, sessionPlayer, otherSessionPlayer mb.Player) (mc.Message[mc.RespRematch], mc.Message[mc.RespRematch], error) +} + +// Every incoming valid request will have this structure +// The request then is handled in line with WsRequestHandler interface +type Request struct { + payload []byte +} + +// This tells the compiler that WsRequest struct must be of type of WsRequestHandler +var _ RequestHandler = (*Request)(nil) + +func NewRequest(payloads ...[]byte) Request { + if len(payloads) > 1 { + panic("request cannot accept more than one payload") + } + r := Request{} + if len(payloads) == 1 { + r.payload = payloads[0] + } + return r +} + +func (r Request) HandleCreateGame(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespCreateGame]) { + var req mc.Message[mc.ReqCreateGame] + respMsg := mc.NewMessage[mc.RespCreateGame](mc.CodeCreateGame) + + if err := json.Unmarshal(r.payload, &req); err != nil { + respMsg.AddError(err.Error(), cerr.ConstErrInvalidPayload) + return nil, nil, respMsg + } + + game, err := gm.CreateGame(req.Payload.GameDifficulty, req.Payload.GameMode) + if err != nil { + respMsg.AddError(err.Error(), cerr.ConstErrCreateGame) + return nil, nil, respMsg + } + + hostPlayer := game.CreateHostPlayer(sessionId) + + respMsg.AddPayload(mc.RespCreateGame{GameUuid: game.Uuid(), HostUuid: hostPlayer.Uuid()}) + return game, hostPlayer, respMsg +} + +// Join user sends the game uuid and if this game exists, +// a new join player is created and added to the database +func (r Request) HandleJoinPlayer(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespJoinGame]) { + var joinGameReq mc.Message[mc.ReqJoinGame] + respMsg := mc.NewMessage[mc.RespJoinGame](mc.CodeJoinGame) + + if err := json.Unmarshal(r.payload, &joinGameReq); err != nil { + respMsg.AddError(err.Error(), cerr.ConstErrInvalidPayload) + return nil, nil, respMsg + } + + game, err := gm.FetchGame(joinGameReq.Payload.GameUuid) + if err != nil { + respMsg.AddError(err.Error(), cerr.ConstErrJoin) + return nil, nil, respMsg + } + + joinPlayer := game.CreateJoinPlayer(sessionId) + + respMsg.AddPayload(mc.RespJoinGame{ + GameUuid: game.Uuid(), + PlayerUuid: joinPlayer.Uuid(), + GameDifficulty: game.Difficulty(), + GameMode: game.Mode(), + }) + return game, joinPlayer, respMsg +} + +// User will choose the configurations of ships on defence grid. +// Then the grid is sent to backend and adjustment happens accordingly. +func (r Request) HandleReadyPlayer(bgm mb.GameManager, game *mb.Game, sessionPlayer mb.Player) mc.Message[mc.RespReady] { + var readyPlayerReq mc.Message[mc.ReqReadyPlayer] + resp := mc.NewMessage[mc.RespReady](mc.CodeReady) + + if err := json.Unmarshal(r.payload, &readyPlayerReq); err != nil { + resp.AddError(err.Error(), cerr.ConstErrInvalidPayload) + return resp + } + + // Check to see if rows and cols are equal to game's grid size + if err := game.SetPlayerReadyForGame(sessionPlayer, readyPlayerReq.Payload.DefenceGrid); err != nil { + resp.AddError(err.Error(), cerr.ConstErrReady) + return resp + } + + if game.IsModeMine() { + mineCoordinates := sessionPlayer.PlantMineInDefenceGrid() + resp.AddPayload(mc.RespReady{MinePosition: mineCoordinates}) + } + + return resp +} + +// Handle the attack logic for the incoming request +func (r Request) HandleAttack(game *mb.Game, attacker mb.Player, defender mb.Player, gm mb.GameManager) mc.Message[mc.RespAttack] { + var reqAttack mc.Message[mc.ReqAttack] + resp := mc.NewMessage[mc.RespAttack](mc.CodeAttack) + + if err := json.Unmarshal(r.payload, &reqAttack); err != nil { + resp.AddError(err.Error(), cerr.ConstErrInvalidPayload) + return resp + } + + coordinates := mb.NewCoordinates(reqAttack.Payload.X, reqAttack.Payload.Y) + if !game.AreAttackCoordinatesValid(coordinates) { + resp.AddError(cerr.ErrXorYOutOfGridBound(coordinates.X, coordinates.Y).Error(), cerr.ConstErrAttack) + return resp + } + + // Attack validity check + if !attacker.IsTurn() { + resp.AddError(cerr.ErrNotTurnForAttacker(attacker.Uuid()).Error(), cerr.ConstErrAttack) + return resp + } + + if !attacker.IsAttackGridEmptyInCoordinates(coordinates) { + resp.AddError(cerr.ErrAttackPositionAlreadyFilled(coordinates.X, coordinates.Y).Error(), cerr.ConstErrAttack) + return resp + } + + if defender.IsDefenceGridAlreadyHitInCoordinates(coordinates) { + resp.AddError(cerr.ErrDefenceGridPositionAlreadyHit(coordinates.X, coordinates.Y).Error(), cerr.ConstErrAttack) + return resp + } + + attacker.SetTurnFalse() + defender.SetTurnTrue() + + hostPlayer := game.HostPlayer() + joinPlayer := game.JoinPlayer() + + // TODO: Check for game mode for this section + if defender.DidAttackerHitMine(coordinates) { + attacker.SetMatchStatusToLost() + defender.SetMatchStatusToWon() + + resp.AddPayload(mc.RespAttack{ + X: coordinates.X, + Y: coordinates.Y, + PositionState: mb.PositionStateMine, + SunkenShipsHost: hostPlayer.SunkenShips(), + SunkenShipsJoin: joinPlayer.SunkenShips(), + IsTurn: attacker.IsTurn(), + }) + + return resp + } + + if defender.IsAttackMiss(coordinates) { + attacker.SetAttackGridToMiss(coordinates) + + resp.AddPayload(mc.RespAttack{ + X: coordinates.X, + Y: coordinates.Y, + PositionState: mb.PositionStateAttackGridMiss, + SunkenShipsHost: hostPlayer.SunkenShips(), + SunkenShipsJoin: joinPlayer.SunkenShips(), + IsTurn: attacker.IsTurn(), + }) + return resp + } + + shipCode := defender.ShipCode(coordinates) + defender.IncrementShipHit(shipCode, coordinates) + attacker.SetAttackGridToHit(coordinates) + + // Initialize the response payload + resp.AddPayload(mc.RespAttack{ + X: coordinates.X, + Y: coordinates.Y, + PositionState: mb.PositionStateAttackGridHit, + IsTurn: attacker.IsTurn(), + }) + + // Check if the attack caused the ship to sink + if defender.IsShipSunken(shipCode) { + defender.IncrementSunkenShips() + resp.Payload.DefenderSunkenShipsCoords = defender.ShipHitCoordinates(shipCode) + + // Check if this sunken ship was the last one and the attacker is lost + if defender.AreAllShipsSunken() { + defender.SetMatchStatusToLost() + attacker.SetMatchStatusToWon() + } + } + + resp.Payload.SunkenShipsHost = hostPlayer.SunkenShips() + resp.Payload.SunkenShipsJoin = joinPlayer.SunkenShips() + return resp +} + +func (r Request) HandleCallRematch(bgm mb.GameManager, game *mb.Game) (mc.Message[mc.NoPayload], error) { + respMsg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCall) + + if game.IsRematchAlreadyCalled() { + return respMsg, cerr.ErrGameAleardyRecalled() + } + + game.CallRematchForGame() + return respMsg, nil +} + +func (r Request) HandleAcceptRematchCall( + bgm mb.GameManager, + game *mb.Game, + sessionPlayer, otherSessionPlayer mb.Player, +) (mc.Message[mc.RespRematch], mc.Message[mc.RespRematch], error) { + + if err := game.ResetRematchForGame(); err != nil { + return mc.NewMessage[mc.RespRematch](mc.CodeRematch), mc.NewMessage[mc.RespRematch](mc.CodeRematch), err + } + + sessionPlayer.SetTurnFalse() + msgPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) + msgPlayer.AddPayload(mc.RespRematch{IsTurn: sessionPlayer.IsTurn()}) + + otherSessionPlayer.SetTurnTrue() + msgOtherPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) + msgOtherPlayer.AddPayload(mc.RespRematch{IsTurn: otherSessionPlayer.IsTurn()}) + + return msgPlayer, msgOtherPlayer, nil +} From 616b136d75bf88197e22c558c9c3f224a1d0f761 Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Thu, 18 Jul 2024 13:37:06 -0600 Subject: [PATCH 14/19] Revert "changed filename" This reverts commit 7b43b9c28a071265bab7a2ef67b2d127475181ea. --- api/handlers.go | 242 ------------------------------------------------ 1 file changed, 242 deletions(-) delete mode 100644 api/handlers.go diff --git a/api/handlers.go b/api/handlers.go deleted file mode 100644 index e91f725..0000000 --- a/api/handlers.go +++ /dev/null @@ -1,242 +0,0 @@ -package api - -import ( - "encoding/json" - - cerr "github.com/saeidalz13/battleship-backend/internal/error" - mb "github.com/saeidalz13/battleship-backend/models/battleship" - mc "github.com/saeidalz13/battleship-backend/models/connection" -) - -type RequestHandler interface { - HandleCreateGame(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespCreateGame]) - HandleReadyPlayer(gm mb.GameManager, sessionGame *mb.Game, sessionPlayer mb.Player) mc.Message[mc.RespReady] - HandleJoinPlayer(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespJoinGame]) - HandleAttack(*mb.Game, mb.Player, mb.Player, mb.GameManager) mc.Message[mc.RespAttack] - HandleCallRematch(bgm mb.GameManager, sessionGame *mb.Game) (mc.Message[mc.NoPayload], error) - HandleAcceptRematchCall(bgm mb.GameManager, sessionGame *mb.Game, sessionPlayer, otherSessionPlayer mb.Player) (mc.Message[mc.RespRematch], mc.Message[mc.RespRematch], error) -} - -// Every incoming valid request will have this structure -// The request then is handled in line with WsRequestHandler interface -type Request struct { - payload []byte -} - -// This tells the compiler that WsRequest struct must be of type of WsRequestHandler -var _ RequestHandler = (*Request)(nil) - -func NewRequest(payloads ...[]byte) Request { - if len(payloads) > 1 { - panic("request cannot accept more than one payload") - } - r := Request{} - if len(payloads) == 1 { - r.payload = payloads[0] - } - return r -} - -func (r Request) HandleCreateGame(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespCreateGame]) { - var req mc.Message[mc.ReqCreateGame] - respMsg := mc.NewMessage[mc.RespCreateGame](mc.CodeCreateGame) - - if err := json.Unmarshal(r.payload, &req); err != nil { - respMsg.AddError(err.Error(), cerr.ConstErrInvalidPayload) - return nil, nil, respMsg - } - - game, err := gm.CreateGame(req.Payload.GameDifficulty, req.Payload.GameMode) - if err != nil { - respMsg.AddError(err.Error(), cerr.ConstErrCreateGame) - return nil, nil, respMsg - } - - hostPlayer := game.CreateHostPlayer(sessionId) - - respMsg.AddPayload(mc.RespCreateGame{GameUuid: game.Uuid(), HostUuid: hostPlayer.Uuid()}) - return game, hostPlayer, respMsg -} - -// Join user sends the game uuid and if this game exists, -// a new join player is created and added to the database -func (r Request) HandleJoinPlayer(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespJoinGame]) { - var joinGameReq mc.Message[mc.ReqJoinGame] - respMsg := mc.NewMessage[mc.RespJoinGame](mc.CodeJoinGame) - - if err := json.Unmarshal(r.payload, &joinGameReq); err != nil { - respMsg.AddError(err.Error(), cerr.ConstErrInvalidPayload) - return nil, nil, respMsg - } - - game, err := gm.FetchGame(joinGameReq.Payload.GameUuid) - if err != nil { - respMsg.AddError(err.Error(), cerr.ConstErrJoin) - return nil, nil, respMsg - } - - joinPlayer := game.CreateJoinPlayer(sessionId) - - respMsg.AddPayload(mc.RespJoinGame{ - GameUuid: game.Uuid(), - PlayerUuid: joinPlayer.Uuid(), - GameDifficulty: game.Difficulty(), - GameMode: game.Mode(), - }) - return game, joinPlayer, respMsg -} - -// User will choose the configurations of ships on defence grid. -// Then the grid is sent to backend and adjustment happens accordingly. -func (r Request) HandleReadyPlayer(bgm mb.GameManager, game *mb.Game, sessionPlayer mb.Player) mc.Message[mc.RespReady] { - var readyPlayerReq mc.Message[mc.ReqReadyPlayer] - resp := mc.NewMessage[mc.RespReady](mc.CodeReady) - - if err := json.Unmarshal(r.payload, &readyPlayerReq); err != nil { - resp.AddError(err.Error(), cerr.ConstErrInvalidPayload) - return resp - } - - // Check to see if rows and cols are equal to game's grid size - if err := game.SetPlayerReadyForGame(sessionPlayer, readyPlayerReq.Payload.DefenceGrid); err != nil { - resp.AddError(err.Error(), cerr.ConstErrReady) - return resp - } - - if game.IsModeMine() { - mineCoordinates := sessionPlayer.PlantMineInDefenceGrid() - resp.AddPayload(mc.RespReady{MinePosition: mineCoordinates}) - } - - return resp -} - -// Handle the attack logic for the incoming request -func (r Request) HandleAttack(game *mb.Game, attacker mb.Player, defender mb.Player, gm mb.GameManager) mc.Message[mc.RespAttack] { - var reqAttack mc.Message[mc.ReqAttack] - resp := mc.NewMessage[mc.RespAttack](mc.CodeAttack) - - if err := json.Unmarshal(r.payload, &reqAttack); err != nil { - resp.AddError(err.Error(), cerr.ConstErrInvalidPayload) - return resp - } - - coordinates := mb.NewCoordinates(reqAttack.Payload.X, reqAttack.Payload.Y) - if !game.AreAttackCoordinatesValid(coordinates) { - resp.AddError(cerr.ErrXorYOutOfGridBound(coordinates.X, coordinates.Y).Error(), cerr.ConstErrAttack) - return resp - } - - // Attack validity check - if !attacker.IsTurn() { - resp.AddError(cerr.ErrNotTurnForAttacker(attacker.Uuid()).Error(), cerr.ConstErrAttack) - return resp - } - - if !attacker.IsAttackGridEmptyInCoordinates(coordinates) { - resp.AddError(cerr.ErrAttackPositionAlreadyFilled(coordinates.X, coordinates.Y).Error(), cerr.ConstErrAttack) - return resp - } - - if defender.IsDefenceGridAlreadyHitInCoordinates(coordinates) { - resp.AddError(cerr.ErrDefenceGridPositionAlreadyHit(coordinates.X, coordinates.Y).Error(), cerr.ConstErrAttack) - return resp - } - - attacker.SetTurnFalse() - defender.SetTurnTrue() - - hostPlayer := game.HostPlayer() - joinPlayer := game.JoinPlayer() - - // TODO: Check for game mode for this section - if defender.DidAttackerHitMine(coordinates) { - attacker.SetMatchStatusToLost() - defender.SetMatchStatusToWon() - - resp.AddPayload(mc.RespAttack{ - X: coordinates.X, - Y: coordinates.Y, - PositionState: mb.PositionStateMine, - SunkenShipsHost: hostPlayer.SunkenShips(), - SunkenShipsJoin: joinPlayer.SunkenShips(), - IsTurn: attacker.IsTurn(), - }) - - return resp - } - - if defender.IsAttackMiss(coordinates) { - attacker.SetAttackGridToMiss(coordinates) - - resp.AddPayload(mc.RespAttack{ - X: coordinates.X, - Y: coordinates.Y, - PositionState: mb.PositionStateAttackGridMiss, - SunkenShipsHost: hostPlayer.SunkenShips(), - SunkenShipsJoin: joinPlayer.SunkenShips(), - IsTurn: attacker.IsTurn(), - }) - return resp - } - - shipCode := defender.ShipCode(coordinates) - defender.IncrementShipHit(shipCode, coordinates) - attacker.SetAttackGridToHit(coordinates) - - // Initialize the response payload - resp.AddPayload(mc.RespAttack{ - X: coordinates.X, - Y: coordinates.Y, - PositionState: mb.PositionStateAttackGridHit, - IsTurn: attacker.IsTurn(), - }) - - // Check if the attack caused the ship to sink - if defender.IsShipSunken(shipCode) { - defender.IncrementSunkenShips() - resp.Payload.DefenderSunkenShipsCoords = defender.ShipHitCoordinates(shipCode) - - // Check if this sunken ship was the last one and the attacker is lost - if defender.AreAllShipsSunken() { - defender.SetMatchStatusToLost() - attacker.SetMatchStatusToWon() - } - } - - resp.Payload.SunkenShipsHost = hostPlayer.SunkenShips() - resp.Payload.SunkenShipsJoin = joinPlayer.SunkenShips() - return resp -} - -func (r Request) HandleCallRematch(bgm mb.GameManager, game *mb.Game) (mc.Message[mc.NoPayload], error) { - respMsg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCall) - - if game.IsRematchAlreadyCalled() { - return respMsg, cerr.ErrGameAleardyRecalled() - } - - game.CallRematchForGame() - return respMsg, nil -} - -func (r Request) HandleAcceptRematchCall( - bgm mb.GameManager, - game *mb.Game, - sessionPlayer, otherSessionPlayer mb.Player, -) (mc.Message[mc.RespRematch], mc.Message[mc.RespRematch], error) { - - if err := game.ResetRematchForGame(); err != nil { - return mc.NewMessage[mc.RespRematch](mc.CodeRematch), mc.NewMessage[mc.RespRematch](mc.CodeRematch), err - } - - sessionPlayer.SetTurnFalse() - msgPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) - msgPlayer.AddPayload(mc.RespRematch{IsTurn: sessionPlayer.IsTurn()}) - - otherSessionPlayer.SetTurnTrue() - msgOtherPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) - msgOtherPlayer.AddPayload(mc.RespRematch{IsTurn: otherSessionPlayer.IsTurn()}) - - return msgPlayer, msgOtherPlayer, nil -} From 89eafac60186d4295851083defc97f4cc6c6a92c Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Thu, 18 Jul 2024 13:39:28 -0600 Subject: [PATCH 15/19] Revert "Revert "changed filename"" This reverts commit 616b136d75bf88197e22c558c9c3f224a1d0f761. --- api/handlers.go | 242 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 api/handlers.go diff --git a/api/handlers.go b/api/handlers.go new file mode 100644 index 0000000..e91f725 --- /dev/null +++ b/api/handlers.go @@ -0,0 +1,242 @@ +package api + +import ( + "encoding/json" + + cerr "github.com/saeidalz13/battleship-backend/internal/error" + mb "github.com/saeidalz13/battleship-backend/models/battleship" + mc "github.com/saeidalz13/battleship-backend/models/connection" +) + +type RequestHandler interface { + HandleCreateGame(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespCreateGame]) + HandleReadyPlayer(gm mb.GameManager, sessionGame *mb.Game, sessionPlayer mb.Player) mc.Message[mc.RespReady] + HandleJoinPlayer(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespJoinGame]) + HandleAttack(*mb.Game, mb.Player, mb.Player, mb.GameManager) mc.Message[mc.RespAttack] + HandleCallRematch(bgm mb.GameManager, sessionGame *mb.Game) (mc.Message[mc.NoPayload], error) + HandleAcceptRematchCall(bgm mb.GameManager, sessionGame *mb.Game, sessionPlayer, otherSessionPlayer mb.Player) (mc.Message[mc.RespRematch], mc.Message[mc.RespRematch], error) +} + +// Every incoming valid request will have this structure +// The request then is handled in line with WsRequestHandler interface +type Request struct { + payload []byte +} + +// This tells the compiler that WsRequest struct must be of type of WsRequestHandler +var _ RequestHandler = (*Request)(nil) + +func NewRequest(payloads ...[]byte) Request { + if len(payloads) > 1 { + panic("request cannot accept more than one payload") + } + r := Request{} + if len(payloads) == 1 { + r.payload = payloads[0] + } + return r +} + +func (r Request) HandleCreateGame(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespCreateGame]) { + var req mc.Message[mc.ReqCreateGame] + respMsg := mc.NewMessage[mc.RespCreateGame](mc.CodeCreateGame) + + if err := json.Unmarshal(r.payload, &req); err != nil { + respMsg.AddError(err.Error(), cerr.ConstErrInvalidPayload) + return nil, nil, respMsg + } + + game, err := gm.CreateGame(req.Payload.GameDifficulty, req.Payload.GameMode) + if err != nil { + respMsg.AddError(err.Error(), cerr.ConstErrCreateGame) + return nil, nil, respMsg + } + + hostPlayer := game.CreateHostPlayer(sessionId) + + respMsg.AddPayload(mc.RespCreateGame{GameUuid: game.Uuid(), HostUuid: hostPlayer.Uuid()}) + return game, hostPlayer, respMsg +} + +// Join user sends the game uuid and if this game exists, +// a new join player is created and added to the database +func (r Request) HandleJoinPlayer(gm mb.GameManager, sessionId string) (*mb.Game, mb.Player, mc.Message[mc.RespJoinGame]) { + var joinGameReq mc.Message[mc.ReqJoinGame] + respMsg := mc.NewMessage[mc.RespJoinGame](mc.CodeJoinGame) + + if err := json.Unmarshal(r.payload, &joinGameReq); err != nil { + respMsg.AddError(err.Error(), cerr.ConstErrInvalidPayload) + return nil, nil, respMsg + } + + game, err := gm.FetchGame(joinGameReq.Payload.GameUuid) + if err != nil { + respMsg.AddError(err.Error(), cerr.ConstErrJoin) + return nil, nil, respMsg + } + + joinPlayer := game.CreateJoinPlayer(sessionId) + + respMsg.AddPayload(mc.RespJoinGame{ + GameUuid: game.Uuid(), + PlayerUuid: joinPlayer.Uuid(), + GameDifficulty: game.Difficulty(), + GameMode: game.Mode(), + }) + return game, joinPlayer, respMsg +} + +// User will choose the configurations of ships on defence grid. +// Then the grid is sent to backend and adjustment happens accordingly. +func (r Request) HandleReadyPlayer(bgm mb.GameManager, game *mb.Game, sessionPlayer mb.Player) mc.Message[mc.RespReady] { + var readyPlayerReq mc.Message[mc.ReqReadyPlayer] + resp := mc.NewMessage[mc.RespReady](mc.CodeReady) + + if err := json.Unmarshal(r.payload, &readyPlayerReq); err != nil { + resp.AddError(err.Error(), cerr.ConstErrInvalidPayload) + return resp + } + + // Check to see if rows and cols are equal to game's grid size + if err := game.SetPlayerReadyForGame(sessionPlayer, readyPlayerReq.Payload.DefenceGrid); err != nil { + resp.AddError(err.Error(), cerr.ConstErrReady) + return resp + } + + if game.IsModeMine() { + mineCoordinates := sessionPlayer.PlantMineInDefenceGrid() + resp.AddPayload(mc.RespReady{MinePosition: mineCoordinates}) + } + + return resp +} + +// Handle the attack logic for the incoming request +func (r Request) HandleAttack(game *mb.Game, attacker mb.Player, defender mb.Player, gm mb.GameManager) mc.Message[mc.RespAttack] { + var reqAttack mc.Message[mc.ReqAttack] + resp := mc.NewMessage[mc.RespAttack](mc.CodeAttack) + + if err := json.Unmarshal(r.payload, &reqAttack); err != nil { + resp.AddError(err.Error(), cerr.ConstErrInvalidPayload) + return resp + } + + coordinates := mb.NewCoordinates(reqAttack.Payload.X, reqAttack.Payload.Y) + if !game.AreAttackCoordinatesValid(coordinates) { + resp.AddError(cerr.ErrXorYOutOfGridBound(coordinates.X, coordinates.Y).Error(), cerr.ConstErrAttack) + return resp + } + + // Attack validity check + if !attacker.IsTurn() { + resp.AddError(cerr.ErrNotTurnForAttacker(attacker.Uuid()).Error(), cerr.ConstErrAttack) + return resp + } + + if !attacker.IsAttackGridEmptyInCoordinates(coordinates) { + resp.AddError(cerr.ErrAttackPositionAlreadyFilled(coordinates.X, coordinates.Y).Error(), cerr.ConstErrAttack) + return resp + } + + if defender.IsDefenceGridAlreadyHitInCoordinates(coordinates) { + resp.AddError(cerr.ErrDefenceGridPositionAlreadyHit(coordinates.X, coordinates.Y).Error(), cerr.ConstErrAttack) + return resp + } + + attacker.SetTurnFalse() + defender.SetTurnTrue() + + hostPlayer := game.HostPlayer() + joinPlayer := game.JoinPlayer() + + // TODO: Check for game mode for this section + if defender.DidAttackerHitMine(coordinates) { + attacker.SetMatchStatusToLost() + defender.SetMatchStatusToWon() + + resp.AddPayload(mc.RespAttack{ + X: coordinates.X, + Y: coordinates.Y, + PositionState: mb.PositionStateMine, + SunkenShipsHost: hostPlayer.SunkenShips(), + SunkenShipsJoin: joinPlayer.SunkenShips(), + IsTurn: attacker.IsTurn(), + }) + + return resp + } + + if defender.IsAttackMiss(coordinates) { + attacker.SetAttackGridToMiss(coordinates) + + resp.AddPayload(mc.RespAttack{ + X: coordinates.X, + Y: coordinates.Y, + PositionState: mb.PositionStateAttackGridMiss, + SunkenShipsHost: hostPlayer.SunkenShips(), + SunkenShipsJoin: joinPlayer.SunkenShips(), + IsTurn: attacker.IsTurn(), + }) + return resp + } + + shipCode := defender.ShipCode(coordinates) + defender.IncrementShipHit(shipCode, coordinates) + attacker.SetAttackGridToHit(coordinates) + + // Initialize the response payload + resp.AddPayload(mc.RespAttack{ + X: coordinates.X, + Y: coordinates.Y, + PositionState: mb.PositionStateAttackGridHit, + IsTurn: attacker.IsTurn(), + }) + + // Check if the attack caused the ship to sink + if defender.IsShipSunken(shipCode) { + defender.IncrementSunkenShips() + resp.Payload.DefenderSunkenShipsCoords = defender.ShipHitCoordinates(shipCode) + + // Check if this sunken ship was the last one and the attacker is lost + if defender.AreAllShipsSunken() { + defender.SetMatchStatusToLost() + attacker.SetMatchStatusToWon() + } + } + + resp.Payload.SunkenShipsHost = hostPlayer.SunkenShips() + resp.Payload.SunkenShipsJoin = joinPlayer.SunkenShips() + return resp +} + +func (r Request) HandleCallRematch(bgm mb.GameManager, game *mb.Game) (mc.Message[mc.NoPayload], error) { + respMsg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCall) + + if game.IsRematchAlreadyCalled() { + return respMsg, cerr.ErrGameAleardyRecalled() + } + + game.CallRematchForGame() + return respMsg, nil +} + +func (r Request) HandleAcceptRematchCall( + bgm mb.GameManager, + game *mb.Game, + sessionPlayer, otherSessionPlayer mb.Player, +) (mc.Message[mc.RespRematch], mc.Message[mc.RespRematch], error) { + + if err := game.ResetRematchForGame(); err != nil { + return mc.NewMessage[mc.RespRematch](mc.CodeRematch), mc.NewMessage[mc.RespRematch](mc.CodeRematch), err + } + + sessionPlayer.SetTurnFalse() + msgPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) + msgPlayer.AddPayload(mc.RespRematch{IsTurn: sessionPlayer.IsTurn()}) + + otherSessionPlayer.SetTurnTrue() + msgOtherPlayer := mc.NewMessage[mc.RespRematch](mc.CodeRematch) + msgOtherPlayer.AddPayload(mc.RespRematch{IsTurn: otherSessionPlayer.IsTurn()}) + + return msgPlayer, msgOtherPlayer, nil +} From eb4abd12659e2ed35b0b098b80a4da48821abee7 Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Thu, 18 Jul 2024 13:50:15 -0600 Subject: [PATCH 16/19] checkpoint unit tests --- Makefile | 7 +- test/main_test.go | 30 +++---- test/ws_defaultgame_test.go | 157 +++++++++++++++++++----------------- 3 files changed, 100 insertions(+), 94 deletions(-) diff --git a/Makefile b/Makefile index 37a407c..0954b77 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ migrate_down: test: go test -v ./test -count=1 + # Fly # Log flogs_prod: @@ -32,16 +33,16 @@ flogs_prod: flogs_staging: fly logs -a battleship-go-ios-staging - +# Deploy depstage: fly deploy --app battleship-go-ios-staging --dockerfile Dockerfile.staging +# Console flyconsole: fly console --app $(APP) -## With games with titles -> like 10 wins => captain flypdb: -# Connect to production db +# Connect to production db fly pg connect -a battleship-db # Websocket diff --git a/test/main_test.go b/test/main_test.go index d1a80b3..9a16f05 100644 --- a/test/main_test.go +++ b/test/main_test.go @@ -24,14 +24,14 @@ const ( ) var ( - HostConn *websocket.Conn - JoinConn *websocket.Conn - testGame *mb.Game - testGameUuid string + hostConn *websocket.Conn + joinConn *websocket.Conn + testHostPlayer *mb.BattleshipPlayer testJoinPlayer *mb.BattleshipPlayer - HostSessionID string - JoinSessionID string + + hostSessionID string + joinSessionID string testRp api.RequestProcessor dialer = websocket.Dialer{ HandshakeTimeout: 10 * time.Second, @@ -87,26 +87,26 @@ func TestMain(m *testing.M) { log.Println(err) os.Exit(1) } - HostConn = c + hostConn = c // Read host session ID var respSessionId mc.Message[mc.RespSessionId] - _ = HostConn.ReadJSON(&respSessionId) - HostSessionID = respSessionId.Payload.SessionID + _ = hostConn.ReadJSON(&respSessionId) + hostSessionID = respSessionId.Payload.SessionID c2, _, err := dialer.Dial(testWsUrl, nil) if err != nil { log.Println(err) os.Exit(1) } - JoinConn = c2 + joinConn = c2 // Read Join sessoin ID - _ = JoinConn.ReadJSON(&respSessionId) - JoinSessionID = respSessionId.Payload.SessionID + _ = joinConn.ReadJSON(&respSessionId) + joinSessionID = respSessionId.Payload.SessionID - log.Println("Host session ID:", HostSessionID) - log.Println("Join session ID:", JoinSessionID) - log.Printf("host: %s\tjoin: %s", HostConn.LocalAddr().String(), JoinConn.LocalAddr().String()) + log.Println("Host session ID:", hostSessionID) + log.Println("Join session ID:", joinSessionID) + log.Printf("host: %s\tjoin: %s", hostConn.LocalAddr().String(), joinConn.LocalAddr().String()) os.Exit(m.Run()) } diff --git a/test/ws_defaultgame_test.go b/test/ws_defaultgame_test.go index ce71aa3..fd652db 100644 --- a/test/ws_defaultgame_test.go +++ b/test/ws_defaultgame_test.go @@ -14,6 +14,11 @@ import ( "github.com/sqlc-dev/pqtype" ) +var ( + testGame *mb.Game + testGameUuid string +) + type Test[T, K any] struct { name string @@ -35,14 +40,14 @@ func TestInvalidCode(t *testing.T) { expectedCode: mc.CodeInvalidSignal, reqPayload: mc.NewMessage[mc.NoPayload](255), respPayload: mc.NewMessage[mc.NoPayload](mc.CodeInvalidSignal), - conn: HostConn, + conn: hostConn, }, { name: "random invalid code join", expectedCode: mc.CodeInvalidSignal, reqPayload: mc.NewMessage[mc.NoPayload](200), respPayload: mc.NewMessage[mc.NoPayload](mc.CodeInvalidSignal), - conn: JoinConn, + conn: joinConn, }, } @@ -66,14 +71,14 @@ func TestInvalidCode(t *testing.T) { func TestCreateGame(t *testing.T) { tests := []Test[mc.Message[mc.ReqCreateGame], mc.Message[mc.RespCreateGame]]{ { - name: "create game valid code", + name: "create default game code", expectedCode: mc.CodeCreateGame, reqPayload: mc.Message[mc.ReqCreateGame]{Code: mc.CodeCreateGame, Payload: mc.ReqCreateGame{ GameDifficulty: mb.GameDifficultyEasy, GameMode: mb.GameModeDefault, }}, respPayload: mc.NewMessage[mc.RespCreateGame](mc.CodeCreateGame), - conn: HostConn, + conn: hostConn, }, } @@ -137,7 +142,7 @@ func TestJoinPlayer(t *testing.T) { expectedCode: mc.CodeJoinGame, reqPayload: mc.Message[mc.ReqJoinGame]{Code: mc.CodeJoinGame, Payload: mc.ReqJoinGame{GameUuid: testGameUuid}}, respPayload: mc.NewMessage[mc.RespJoinGame](mc.CodeJoinGame), - conn: JoinConn, + conn: joinConn, }, // Any invalid join request will close the connection // { @@ -171,7 +176,7 @@ func TestJoinPlayer(t *testing.T) { // we have to read it so it frees up the queue for the next steps of host read // when join player is added, a select grid code is sent to both players var respSelectGridJoin mc.Message[mc.NoPayload] - if err := JoinConn.ReadJSON(&respSelectGridJoin); err != nil { + if err := joinConn.ReadJSON(&respSelectGridJoin); err != nil { t.Fatal(err) } if respSelectGridJoin.Error != nil { @@ -179,7 +184,7 @@ func TestJoinPlayer(t *testing.T) { } var respSelectGridHost mc.Message[mc.NoPayload] - if err := HostConn.ReadJSON(&respSelectGridHost); err != nil { + if err := hostConn.ReadJSON(&respSelectGridHost); err != nil { t.Fatal(err) } if respSelectGridHost.Error != nil { @@ -210,7 +215,7 @@ func TestReadyGame(t *testing.T) { {0, 0, 0, 0, 0, 0}, } - tests := []Test[mc.Message[mc.ReqReadyPlayer], mc.Message[mc.NoPayload]]{ + tests := []Test[mc.Message[mc.ReqReadyPlayer], mc.Message[mc.RespReady]]{ { name: "set defence grid ready host", expectedCode: mc.CodeReady, @@ -222,8 +227,8 @@ func TestReadyGame(t *testing.T) { PlayerUuid: testHostPlayer.Uuid(), }, }, - respPayload: mc.Message[mc.NoPayload]{}, - conn: HostConn, + respPayload: mc.Message[mc.RespReady]{}, + conn: hostConn, }, { name: "set defence grid ready join", @@ -236,8 +241,8 @@ func TestReadyGame(t *testing.T) { PlayerUuid: testJoinPlayer.Uuid(), }, }, - respPayload: mc.Message[mc.NoPayload]{}, - conn: JoinConn, + respPayload: mc.Message[mc.RespReady]{}, + conn: joinConn, }, } @@ -268,13 +273,13 @@ func TestReadyGame(t *testing.T) { // Host var respStartGameHost mc.Message[mc.NoPayload] - if err := HostConn.ReadJSON(&respStartGameHost); err != nil { + if err := hostConn.ReadJSON(&respStartGameHost); err != nil { t.Fatal(err) } // Join var respStartGameJoin mc.Message[mc.NoPayload] - if err := JoinConn.ReadJSON(&respStartGameJoin); err != nil { + if err := joinConn.ReadJSON(&respStartGameJoin); err != nil { t.Fatal(err) } } @@ -292,16 +297,16 @@ func TestPlayerInteraction(t *testing.T) { expectedCode: mc.CodePlayerInteraction, reqPayload: msg, respPayload: mc.Message[mc.PlayerInteraction]{}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, { name: "successful msg join to host", expectedCode: mc.CodePlayerInteraction, reqPayload: msg, respPayload: mc.Message[mc.PlayerInteraction]{}, - conn: JoinConn, - otherConn: HostConn, + conn: joinConn, + otherConn: hostConn, }, } @@ -348,8 +353,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 0, }}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, { @@ -370,8 +375,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 0, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinConn, + otherConn: hostConn, }, { @@ -396,8 +401,8 @@ func TestAttack(t *testing.T) { {X: 0, Y: 2}, }, }}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, { @@ -418,8 +423,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinConn, + otherConn: hostConn, }, { @@ -434,8 +439,8 @@ func TestAttack(t *testing.T) { }}, respPayload: mc.Message[mc.RespAttack]{}, expectedRespPayload: mc.Message[mc.RespAttack]{Code: mc.CodeAttack}, - conn: JoinConn, - otherConn: HostConn, + conn: joinConn, + otherConn: hostConn, }, { @@ -450,8 +455,8 @@ func TestAttack(t *testing.T) { }}, respPayload: mc.Message[mc.RespAttack]{}, expectedRespPayload: mc.Message[mc.RespAttack]{Code: mc.CodeAttack}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, { @@ -466,8 +471,8 @@ func TestAttack(t *testing.T) { }}, respPayload: mc.Message[mc.RespAttack]{}, expectedRespPayload: mc.Message[mc.RespAttack]{Code: mc.CodeAttack}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, { @@ -482,8 +487,8 @@ func TestAttack(t *testing.T) { }}, respPayload: mc.Message[mc.RespAttack]{}, expectedRespPayload: mc.Message[mc.RespAttack]{Code: mc.CodeAttack}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, /* @@ -507,8 +512,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, { name: "successful miss attack valid payload join", @@ -528,8 +533,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinConn, + otherConn: hostConn, }, { @@ -550,8 +555,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, { name: "successful miss attack valid payload join", @@ -571,8 +576,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinConn, + otherConn: hostConn, }, { @@ -598,8 +603,8 @@ func TestAttack(t *testing.T) { {X: 3, Y: 0}, }, }}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, { name: "successful miss attack valid payload join", @@ -619,8 +624,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinConn, + otherConn: hostConn, }, /* @@ -644,8 +649,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, { name: "successful miss attack valid payload join", @@ -665,8 +670,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinConn, + otherConn: hostConn, }, { @@ -687,8 +692,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, { name: "successful miss attack valid payload join", @@ -708,8 +713,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinConn, + otherConn: hostConn, }, { @@ -730,8 +735,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, { name: "successful miss attack valid payload join", @@ -751,8 +756,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinConn, + otherConn: hostConn, }, // Final attack that sinks the last ship @@ -780,8 +785,8 @@ func TestAttack(t *testing.T) { {X: 4, Y: 4}, }, }}, - conn: HostConn, - otherConn: JoinConn, + conn: hostConn, + otherConn: joinConn, }, } @@ -821,10 +826,10 @@ func TestAttack(t *testing.T) { if test.name == "final hit attack valid battleship payload host 4 and sunk battleship" { // When the game ends, both players receive this message var endGameResp mc.Message[mc.RespEndGame] - if err := HostConn.ReadJSON(&endGameResp); err != nil { + if err := hostConn.ReadJSON(&endGameResp); err != nil { t.Fatal(err) } - if err := JoinConn.ReadJSON(&endGameResp); err != nil { + if err := joinConn.ReadJSON(&endGameResp); err != nil { t.Fatal(err) } } @@ -836,7 +841,7 @@ func TestAttack(t *testing.T) { func TestRematchAcceptance(t *testing.T) { // Host client sends a rematch call msg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCall) - if err := HostConn.WriteJSON(msg); err != nil { + if err := hostConn.WriteJSON(msg); err != nil { t.Fatal(err) } @@ -862,24 +867,24 @@ func TestRematchAcceptance(t *testing.T) { // Join client receives this rematch call var rematchCall mc.Message[mc.NoPayload] - if err := JoinConn.ReadJSON(&rematchCall); err != nil { + if err := joinConn.ReadJSON(&rematchCall); err != nil { t.Fatal(err) } // Join client sends this msg to server msg = mc.NewMessage[mc.NoPayload](mc.CodeRematchCallAccepted) - if err := JoinConn.WriteJSON(msg); err != nil { + if err := joinConn.WriteJSON(msg); err != nil { t.Fatal(err) } // Host client reads this rematch response including their turn var rematchHost mc.Message[mc.RespRematch] - if err := HostConn.ReadJSON(&rematchHost); err != nil { + if err := hostConn.ReadJSON(&rematchHost); err != nil { t.Fatal(err) } // Join client reads this rematch response including their turn var rematchJoin mc.Message[mc.RespRematch] - if err := JoinConn.ReadJSON(&rematchJoin); err != nil { + if err := joinConn.ReadJSON(&rematchJoin); err != nil { t.Fatal(err) } @@ -911,40 +916,40 @@ func TestRematchAcceptance(t *testing.T) { func TestRematchRejection(t *testing.T) { // Host client sends a rematch call msg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCall) - if err := HostConn.WriteJSON(msg); err != nil { + if err := hostConn.WriteJSON(msg); err != nil { t.Fatal(err) } // Join client receives this call and responds with no var rematchCall mc.Message[mc.NoPayload] - if err := JoinConn.ReadJSON(&rematchCall); err != nil { + if err := joinConn.ReadJSON(&rematchCall); err != nil { t.Fatal(err) } msg = mc.NewMessage[mc.NoPayload](mc.CodeRematchCallRejected) - if err := JoinConn.WriteJSON(msg); err != nil { + if err := joinConn.WriteJSON(msg); err != nil { t.Fatal(err) } // Host client reads this acceptance var rematchCallRejected mc.Message[mc.NoPayload] - if err := HostConn.ReadJSON(&rematchCallRejected); err != nil { + if err := hostConn.ReadJSON(&rematchCallRejected); err != nil { t.Fatal(err) } - hostSession, err := testSessionManager.FindSession(HostSessionID) + hostSession, err := testSessionManager.FindSession(hostSessionID) if err == nil { // This line will be done by IOS client testSessionManager.TerminateSession(hostSession.Id()) } - _, err = testSessionManager.FindSession(HostSessionID) - if err.Error() != cerr.ErrSessionNotFound(HostSessionID).Error() { + _, err = testSessionManager.FindSession(hostSessionID) + if err.Error() != cerr.ErrSessionNotFound(hostSessionID).Error() { t.Fatal("session for host player must not exist in session maps") } - _, err = testSessionManager.FindSession(JoinSessionID) - if err.Error() != cerr.ErrSessionNotFound(JoinSessionID).Error() { + _, err = testSessionManager.FindSession(joinSessionID) + if err.Error() != cerr.ErrSessionNotFound(joinSessionID).Error() { t.Fatal("session for join player must not exist in session maps") } } From 1ae287432f5a44360c07973a133d54b2f7b12e0b Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Fri, 19 Jul 2024 09:41:44 -0600 Subject: [PATCH 17/19] check point for unit tests --- Makefile | 4 +- api/handlers.go | 3 +- test/main_test.go | 56 +++++++++--- test/ws_defaultgame_test.go | 168 ++++++++++++++++-------------------- 4 files changed, 122 insertions(+), 109 deletions(-) diff --git a/Makefile b/Makefile index 0954b77..8ae3dbf 100644 --- a/Makefile +++ b/Makefile @@ -22,8 +22,8 @@ migrate_down: # Test test: - go test -v ./test -count=1 - +# go test -v ./test -count=1 -run Default + go test -v ./test -count=1 -run Mine # Fly # Log diff --git a/api/handlers.go b/api/handlers.go index e91f725..9b384e9 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -149,8 +149,7 @@ func (r Request) HandleAttack(game *mb.Game, attacker mb.Player, defender mb.Pla hostPlayer := game.HostPlayer() joinPlayer := game.JoinPlayer() - // TODO: Check for game mode for this section - if defender.DidAttackerHitMine(coordinates) { + if game.IsModeMine() && defender.DidAttackerHitMine(coordinates) { attacker.SetMatchStatusToLost() defender.SetMatchStatusToWon() diff --git a/test/main_test.go b/test/main_test.go index 9a16f05..8f6db2b 100644 --- a/test/main_test.go +++ b/test/main_test.go @@ -24,22 +24,44 @@ const ( ) var ( - hostConn *websocket.Conn - joinConn *websocket.Conn + hostClientConn *websocket.Conn + joinClientConn *websocket.Conn testHostPlayer *mb.BattleshipPlayer testJoinPlayer *mb.BattleshipPlayer - - hostSessionID string - joinSessionID string - testRp api.RequestProcessor - dialer = websocket.Dialer{ + + hostSessionID string + joinSessionID string + + hostSession *mc.Session + joinSession *mc.Session + + testRp api.RequestProcessor + dialer = websocket.Dialer{ HandshakeTimeout: 10 * time.Second, } testMock sqlmock.Sqlmock testGameManager *mb.BattleshipGameManager testSessionManager *mc.BattleshipSessionManager testQuerier sqlc.Querier + + defenceGridHost = mb.Grid{ + {0, mb.PositionStateDefenceDestroyer, mb.PositionStateDefenceDestroyer, 0, 0, 0}, + {mb.PositionStateDefenceCruiser, 0, 0, mb.PositionStateDefenceBattleship, 0, 0}, + {mb.PositionStateDefenceCruiser, 0, 0, mb.PositionStateDefenceBattleship, 0, 0}, + {mb.PositionStateDefenceCruiser, 0, 0, mb.PositionStateDefenceBattleship, 0, 0}, + {0, 0, 0, mb.PositionStateDefenceBattleship, 0, 0}, + {0, 0, 0, 0, 0, 0}, + } + + defenceGridJoin = mb.Grid{ + {0, mb.PositionStateDefenceDestroyer, mb.PositionStateDefenceDestroyer, 0, 0, 0}, + {mb.PositionStateDefenceCruiser, 0, 0, 0, mb.PositionStateDefenceBattleship, 0}, + {mb.PositionStateDefenceCruiser, 0, 0, 0, mb.PositionStateDefenceBattleship, 0}, + {mb.PositionStateDefenceCruiser, 0, 0, 0, mb.PositionStateDefenceBattleship, 0}, + {0, 0, 0, 0, mb.PositionStateDefenceBattleship, 0}, + {0, 0, 0, 0, 0, 0}, + } ) func TestMain(m *testing.M) { @@ -87,26 +109,36 @@ func TestMain(m *testing.M) { log.Println(err) os.Exit(1) } - hostConn = c + hostClientConn = c // Read host session ID var respSessionId mc.Message[mc.RespSessionId] - _ = hostConn.ReadJSON(&respSessionId) + _ = hostClientConn.ReadJSON(&respSessionId) hostSessionID = respSessionId.Payload.SessionID + hs, err := testSessionManager.FindSession(hostSessionID) + if err != nil { + log.Fatalln(err) + } + hostSession = hs c2, _, err := dialer.Dial(testWsUrl, nil) if err != nil { log.Println(err) os.Exit(1) } - joinConn = c2 + joinClientConn = c2 // Read Join sessoin ID - _ = joinConn.ReadJSON(&respSessionId) + _ = joinClientConn.ReadJSON(&respSessionId) joinSessionID = respSessionId.Payload.SessionID + js, err := testSessionManager.FindSession(joinSessionID) + if err != nil { + log.Fatalln(err) + } + joinSession = js log.Println("Host session ID:", hostSessionID) log.Println("Join session ID:", joinSessionID) - log.Printf("host: %s\tjoin: %s", hostConn.LocalAddr().String(), joinConn.LocalAddr().String()) + log.Printf("host: %s\tjoin: %s", hostClientConn.LocalAddr().String(), joinClientConn.LocalAddr().String()) os.Exit(m.Run()) } diff --git a/test/ws_defaultgame_test.go b/test/ws_defaultgame_test.go index fd652db..2f16677 100644 --- a/test/ws_defaultgame_test.go +++ b/test/ws_defaultgame_test.go @@ -33,21 +33,21 @@ type Test[T, K any] struct { otherConn *websocket.Conn // Used for attack when we need to know defender } -func TestInvalidCode(t *testing.T) { +func TestInvalidCodDefault(t *testing.T) { tests := []Test[mc.Message[mc.NoPayload], mc.Message[mc.NoPayload]]{ { name: "random invalid code host", expectedCode: mc.CodeInvalidSignal, reqPayload: mc.NewMessage[mc.NoPayload](255), respPayload: mc.NewMessage[mc.NoPayload](mc.CodeInvalidSignal), - conn: hostConn, + conn: hostClientConn, }, { name: "random invalid code join", expectedCode: mc.CodeInvalidSignal, reqPayload: mc.NewMessage[mc.NoPayload](200), respPayload: mc.NewMessage[mc.NoPayload](mc.CodeInvalidSignal), - conn: joinConn, + conn: joinClientConn, }, } @@ -68,7 +68,7 @@ func TestInvalidCode(t *testing.T) { } } -func TestCreateGame(t *testing.T) { +func TestCreateGameDefault(t *testing.T) { tests := []Test[mc.Message[mc.ReqCreateGame], mc.Message[mc.RespCreateGame]]{ { name: "create default game code", @@ -78,7 +78,7 @@ func TestCreateGame(t *testing.T) { GameMode: mb.GameModeDefault, }}, respPayload: mc.NewMessage[mc.RespCreateGame](mc.CodeCreateGame), - conn: hostConn, + conn: hostClientConn, }, } @@ -135,14 +135,14 @@ func TestCreateGame(t *testing.T) { } } -func TestJoinPlayer(t *testing.T) { +func TestJoinPlayerDefault(t *testing.T) { tests := []Test[mc.Message[mc.ReqJoinGame], mc.Message[mc.RespJoinGame]]{ { name: "valid game uuid", expectedCode: mc.CodeJoinGame, reqPayload: mc.Message[mc.ReqJoinGame]{Code: mc.CodeJoinGame, Payload: mc.ReqJoinGame{GameUuid: testGameUuid}}, respPayload: mc.NewMessage[mc.RespJoinGame](mc.CodeJoinGame), - conn: joinConn, + conn: joinClientConn, }, // Any invalid join request will close the connection // { @@ -176,7 +176,7 @@ func TestJoinPlayer(t *testing.T) { // we have to read it so it frees up the queue for the next steps of host read // when join player is added, a select grid code is sent to both players var respSelectGridJoin mc.Message[mc.NoPayload] - if err := joinConn.ReadJSON(&respSelectGridJoin); err != nil { + if err := joinClientConn.ReadJSON(&respSelectGridJoin); err != nil { t.Fatal(err) } if respSelectGridJoin.Error != nil { @@ -184,7 +184,7 @@ func TestJoinPlayer(t *testing.T) { } var respSelectGridHost mc.Message[mc.NoPayload] - if err := hostConn.ReadJSON(&respSelectGridHost); err != nil { + if err := hostClientConn.ReadJSON(&respSelectGridHost); err != nil { t.Fatal(err) } if respSelectGridHost.Error != nil { @@ -196,25 +196,7 @@ func TestJoinPlayer(t *testing.T) { } } -func TestReadyGame(t *testing.T) { - defenceGridHost := mb.Grid{ - {0, mb.PositionStateDefenceDestroyer, mb.PositionStateDefenceDestroyer, 0, 0, 0}, - {mb.PositionStateDefenceCruiser, 0, 0, mb.PositionStateDefenceBattleship, 0, 0}, - {mb.PositionStateDefenceCruiser, 0, 0, mb.PositionStateDefenceBattleship, 0, 0}, - {mb.PositionStateDefenceCruiser, 0, 0, mb.PositionStateDefenceBattleship, 0, 0}, - {0, 0, 0, mb.PositionStateDefenceBattleship, 0, 0}, - {0, 0, 0, 0, 0, 0}, - } - - defenceGridJoin := mb.Grid{ - {0, mb.PositionStateDefenceDestroyer, mb.PositionStateDefenceDestroyer, 0, 0, 0}, - {mb.PositionStateDefenceCruiser, 0, 0, 0, mb.PositionStateDefenceBattleship, 0}, - {mb.PositionStateDefenceCruiser, 0, 0, 0, mb.PositionStateDefenceBattleship, 0}, - {mb.PositionStateDefenceCruiser, 0, 0, 0, mb.PositionStateDefenceBattleship, 0}, - {0, 0, 0, 0, mb.PositionStateDefenceBattleship, 0}, - {0, 0, 0, 0, 0, 0}, - } - +func TestReadyGameDefault(t *testing.T) { tests := []Test[mc.Message[mc.ReqReadyPlayer], mc.Message[mc.RespReady]]{ { name: "set defence grid ready host", @@ -228,7 +210,7 @@ func TestReadyGame(t *testing.T) { }, }, respPayload: mc.Message[mc.RespReady]{}, - conn: hostConn, + conn: hostClientConn, }, { name: "set defence grid ready join", @@ -242,7 +224,7 @@ func TestReadyGame(t *testing.T) { }, }, respPayload: mc.Message[mc.RespReady]{}, - conn: joinConn, + conn: joinClientConn, }, } @@ -273,13 +255,13 @@ func TestReadyGame(t *testing.T) { // Host var respStartGameHost mc.Message[mc.NoPayload] - if err := hostConn.ReadJSON(&respStartGameHost); err != nil { + if err := hostClientConn.ReadJSON(&respStartGameHost); err != nil { t.Fatal(err) } // Join var respStartGameJoin mc.Message[mc.NoPayload] - if err := joinConn.ReadJSON(&respStartGameJoin); err != nil { + if err := joinClientConn.ReadJSON(&respStartGameJoin); err != nil { t.Fatal(err) } } @@ -287,7 +269,7 @@ func TestReadyGame(t *testing.T) { } } -func TestPlayerInteraction(t *testing.T) { +func TestPlayerInteractionDefault(t *testing.T) { msg := mc.NewMessage[mc.PlayerInteraction](mc.CodePlayerInteraction) msg.AddPayload(mc.PlayerInteraction{Content: "salam!"}) @@ -297,16 +279,16 @@ func TestPlayerInteraction(t *testing.T) { expectedCode: mc.CodePlayerInteraction, reqPayload: msg, respPayload: mc.Message[mc.PlayerInteraction]{}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { name: "successful msg join to host", expectedCode: mc.CodePlayerInteraction, reqPayload: msg, respPayload: mc.Message[mc.PlayerInteraction]{}, - conn: joinConn, - otherConn: hostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, } @@ -333,7 +315,7 @@ func TestPlayerInteraction(t *testing.T) { } } -func TestAttack(t *testing.T) { +func TestAttackDefault(t *testing.T) { tests := []Test[mc.Message[mc.ReqAttack], mc.Message[mc.RespAttack]]{ { name: "successful hit attack destroyer valid payload host 1", @@ -353,8 +335,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 0, }}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { @@ -375,8 +357,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 0, }}, - conn: joinConn, - otherConn: hostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -401,8 +383,8 @@ func TestAttack(t *testing.T) { {X: 0, Y: 2}, }, }}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { @@ -423,8 +405,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: joinConn, - otherConn: hostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -439,8 +421,8 @@ func TestAttack(t *testing.T) { }}, respPayload: mc.Message[mc.RespAttack]{}, expectedRespPayload: mc.Message[mc.RespAttack]{Code: mc.CodeAttack}, - conn: joinConn, - otherConn: hostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -455,8 +437,8 @@ func TestAttack(t *testing.T) { }}, respPayload: mc.Message[mc.RespAttack]{}, expectedRespPayload: mc.Message[mc.RespAttack]{Code: mc.CodeAttack}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { @@ -471,8 +453,8 @@ func TestAttack(t *testing.T) { }}, respPayload: mc.Message[mc.RespAttack]{}, expectedRespPayload: mc.Message[mc.RespAttack]{Code: mc.CodeAttack}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { @@ -487,8 +469,8 @@ func TestAttack(t *testing.T) { }}, respPayload: mc.Message[mc.RespAttack]{}, expectedRespPayload: mc.Message[mc.RespAttack]{Code: mc.CodeAttack}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, /* @@ -512,8 +494,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { name: "successful miss attack valid payload join", @@ -533,8 +515,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: joinConn, - otherConn: hostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -555,8 +537,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { name: "successful miss attack valid payload join", @@ -576,8 +558,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: joinConn, - otherConn: hostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -603,8 +585,8 @@ func TestAttack(t *testing.T) { {X: 3, Y: 0}, }, }}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { name: "successful miss attack valid payload join", @@ -624,8 +606,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: joinConn, - otherConn: hostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, /* @@ -649,8 +631,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { name: "successful miss attack valid payload join", @@ -670,8 +652,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: joinConn, - otherConn: hostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -692,8 +674,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { name: "successful miss attack valid payload join", @@ -713,8 +695,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: joinConn, - otherConn: hostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -735,8 +717,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { name: "successful miss attack valid payload join", @@ -756,8 +738,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: joinConn, - otherConn: hostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, // Final attack that sinks the last ship @@ -785,8 +767,8 @@ func TestAttack(t *testing.T) { {X: 4, Y: 4}, }, }}, - conn: hostConn, - otherConn: joinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, } @@ -826,10 +808,10 @@ func TestAttack(t *testing.T) { if test.name == "final hit attack valid battleship payload host 4 and sunk battleship" { // When the game ends, both players receive this message var endGameResp mc.Message[mc.RespEndGame] - if err := hostConn.ReadJSON(&endGameResp); err != nil { + if err := hostClientConn.ReadJSON(&endGameResp); err != nil { t.Fatal(err) } - if err := joinConn.ReadJSON(&endGameResp); err != nil { + if err := joinClientConn.ReadJSON(&endGameResp); err != nil { t.Fatal(err) } } @@ -838,10 +820,10 @@ func TestAttack(t *testing.T) { } } -func TestRematchAcceptance(t *testing.T) { +func TestRematchAcceptanceDefault(t *testing.T) { // Host client sends a rematch call msg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCall) - if err := hostConn.WriteJSON(msg); err != nil { + if err := hostClientConn.WriteJSON(msg); err != nil { t.Fatal(err) } @@ -867,24 +849,24 @@ func TestRematchAcceptance(t *testing.T) { // Join client receives this rematch call var rematchCall mc.Message[mc.NoPayload] - if err := joinConn.ReadJSON(&rematchCall); err != nil { + if err := joinClientConn.ReadJSON(&rematchCall); err != nil { t.Fatal(err) } // Join client sends this msg to server msg = mc.NewMessage[mc.NoPayload](mc.CodeRematchCallAccepted) - if err := joinConn.WriteJSON(msg); err != nil { + if err := joinClientConn.WriteJSON(msg); err != nil { t.Fatal(err) } // Host client reads this rematch response including their turn var rematchHost mc.Message[mc.RespRematch] - if err := hostConn.ReadJSON(&rematchHost); err != nil { + if err := hostClientConn.ReadJSON(&rematchHost); err != nil { t.Fatal(err) } // Join client reads this rematch response including their turn var rematchJoin mc.Message[mc.RespRematch] - if err := joinConn.ReadJSON(&rematchJoin); err != nil { + if err := joinClientConn.ReadJSON(&rematchJoin); err != nil { t.Fatal(err) } @@ -913,27 +895,27 @@ func TestRematchAcceptance(t *testing.T) { // } } -func TestRematchRejection(t *testing.T) { +func TestRematchRejectionDefault(t *testing.T) { // Host client sends a rematch call msg := mc.NewMessage[mc.NoPayload](mc.CodeRematchCall) - if err := hostConn.WriteJSON(msg); err != nil { + if err := hostClientConn.WriteJSON(msg); err != nil { t.Fatal(err) } // Join client receives this call and responds with no var rematchCall mc.Message[mc.NoPayload] - if err := joinConn.ReadJSON(&rematchCall); err != nil { + if err := joinClientConn.ReadJSON(&rematchCall); err != nil { t.Fatal(err) } msg = mc.NewMessage[mc.NoPayload](mc.CodeRematchCallRejected) - if err := joinConn.WriteJSON(msg); err != nil { + if err := joinClientConn.WriteJSON(msg); err != nil { t.Fatal(err) } // Host client reads this acceptance var rematchCallRejected mc.Message[mc.NoPayload] - if err := hostConn.ReadJSON(&rematchCallRejected); err != nil { + if err := hostClientConn.ReadJSON(&rematchCallRejected); err != nil { t.Fatal(err) } From bc74f15581e065464cbb27bd3f537517bc59b0cc Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Fri, 19 Jul 2024 09:42:26 -0600 Subject: [PATCH 18/19] unit tests for mine game --- test/ws_minegame_test.go | 114 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 test/ws_minegame_test.go diff --git a/test/ws_minegame_test.go b/test/ws_minegame_test.go new file mode 100644 index 0000000..03baf22 --- /dev/null +++ b/test/ws_minegame_test.go @@ -0,0 +1,114 @@ +package test + +import ( + "log" + + mb "github.com/saeidalz13/battleship-backend/models/battleship" + mc "github.com/saeidalz13/battleship-backend/models/connection" + + "testing" +) + +func TestGameMine(t *testing.T) { + /* + Create Mine Game + */ + reqCreateGame := mc.Message[mc.ReqCreateGame]{Code: mc.CodeCreateGame, Payload: mc.ReqCreateGame{ + GameDifficulty: mb.GameDifficultyEasy, + GameMode: mb.GameModeMine, + }} + respCreateGame := mc.NewMessage[mc.RespCreateGame](mc.CodeCreateGame) + + if err := hostClientConn.WriteJSON(reqCreateGame); err != nil { + t.Fatal(err) + } + if err := hostClientConn.ReadJSON(&respCreateGame); err != nil { + t.Fatal(err) + } + gameUuid := respCreateGame.Payload.GameUuid + game, err := testGameManager.FetchGame(gameUuid) + if err != nil { + t.Fatal(err) + } + hostPlayer := game.FetchPlayer(true) + + /* + Join Mine Game + */ + reqJoinGame := mc.NewMessage[mc.ReqJoinGame](mc.CodeJoinGame) + reqJoinGame.AddPayload(mc.ReqJoinGame{GameUuid: gameUuid}) + + if err := joinClientConn.WriteJSON(reqJoinGame); err != nil { + t.Fatal(err) + } + + respJoinGame := mc.NewMessage[mc.RespJoinGame](mc.CodeJoinGame) + if err := joinClientConn.ReadJSON(&respJoinGame); err != nil { + t.Fatal(err) + } + + joinPlayer := game.FetchPlayer(false) + log.Printf("join player resp %+v\n\n", respJoinGame) + + var respSelectGridJoin mc.Message[mc.NoPayload] + if err := joinClientConn.ReadJSON(&respSelectGridJoin); err != nil { + t.Fatal(err) + } + if respSelectGridJoin.Error != nil { + t.Fatalf("failed to receive select ready message for join: %s", respSelectGridJoin.Error.ErrorDetails) + } + + var respSelectGridHost mc.Message[mc.NoPayload] + if err := hostClientConn.ReadJSON(&respSelectGridHost); err != nil { + t.Fatal(err) + } + if respSelectGridHost.Error != nil { + t.Fatalf("failed to receive select ready message for host: %s", respSelectGridHost.Error.ErrorDetails) + } + + /* + Ready Mine Game + */ + reqReadyHost := mc.NewMessage[mc.ReqReadyPlayer](mc.CodeReady) + reqReadyHost.AddPayload(mc.ReqReadyPlayer{ + GameUuid: gameUuid, + PlayerUuid: hostPlayer.Uuid(), + DefenceGrid: defenceGridHost, + }) + + if err := hostClientConn.WriteJSON(reqReadyHost); err != nil { + t.Fatal(err) + } + + var respReadyHost mc.Message[mc.RespReady] + if err := hostClientConn.ReadJSON(&respReadyHost); err != nil { + t.Fatal(err) + } + + reqReadyJoin := mc.NewMessage[mc.ReqReadyPlayer](mc.CodeReady) + reqReadyJoin.AddPayload(mc.ReqReadyPlayer{ + GameUuid: gameUuid, + PlayerUuid: joinPlayer.Uuid(), + DefenceGrid: defenceGridJoin, + }) + + if err := joinClientConn.WriteJSON(reqReadyJoin); err != nil { + t.Fatal(err) + } + + var respReadyJoin mc.Message[mc.RespReady] + if err := joinClientConn.ReadJSON(&respReadyJoin); err != nil { + t.Fatal(err) + } + + // Free up both host and join client connections from StartGame response + var respStartGameHost mc.Message[mc.NoPayload] + if err := hostClientConn.ReadJSON(&respStartGameHost); err != nil { + t.Fatal(err) + } + + var respStartGameJoin mc.Message[mc.NoPayload] + if err := joinClientConn.ReadJSON(&respStartGameJoin); err != nil { + t.Fatal(err) + } +} From 68125565aea83a086f8d44ea99e37e190097b13f Mon Sep 17 00:00:00 2001 From: Saeid Alizadeh Date: Fri, 19 Jul 2024 10:08:03 -0600 Subject: [PATCH 19/19] finished mine game unit tests; modified Makefile test statement --- Makefile | 2 +- test/ws_minegame_test.go | 105 +++++++++++++++++++++++++++++++-------- 2 files changed, 86 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 8ae3dbf..193ea42 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ migrate_down: # Test test: -# go test -v ./test -count=1 -run Default + go test -v ./test -count=1 -run Default go test -v ./test -count=1 -run Mine # Fly diff --git a/test/ws_minegame_test.go b/test/ws_minegame_test.go index 03baf22..dd1ad08 100644 --- a/test/ws_minegame_test.go +++ b/test/ws_minegame_test.go @@ -2,6 +2,8 @@ package test import ( "log" + "reflect" + "time" mb "github.com/saeidalz13/battleship-backend/models/battleship" mc "github.com/saeidalz13/battleship-backend/models/connection" @@ -75,40 +77,103 @@ func TestGameMine(t *testing.T) { PlayerUuid: hostPlayer.Uuid(), DefenceGrid: defenceGridHost, }) - - if err := hostClientConn.WriteJSON(reqReadyHost); err != nil { - t.Fatal(err) - } - + if err := hostClientConn.WriteJSON(reqReadyHost); err != nil { + t.Fatal(err) + } var respReadyHost mc.Message[mc.RespReady] if err := hostClientConn.ReadJSON(&respReadyHost); err != nil { t.Fatal(err) } - reqReadyJoin := mc.NewMessage[mc.ReqReadyPlayer](mc.CodeReady) + reqReadyJoin := mc.NewMessage[mc.ReqReadyPlayer](mc.CodeReady) reqReadyJoin.AddPayload(mc.ReqReadyPlayer{ GameUuid: gameUuid, PlayerUuid: joinPlayer.Uuid(), DefenceGrid: defenceGridJoin, }) - - if err := joinClientConn.WriteJSON(reqReadyJoin); err != nil { - t.Fatal(err) - } - + if err := joinClientConn.WriteJSON(reqReadyJoin); err != nil { + t.Fatal(err) + } var respReadyJoin mc.Message[mc.RespReady] if err := joinClientConn.ReadJSON(&respReadyJoin); err != nil { t.Fatal(err) } - // Free up both host and join client connections from StartGame response - var respStartGameHost mc.Message[mc.NoPayload] - if err := hostClientConn.ReadJSON(&respStartGameHost); err != nil { - t.Fatal(err) - } + joinMineCoordinates := respReadyJoin.Payload.MinePosition - var respStartGameJoin mc.Message[mc.NoPayload] - if err := joinClientConn.ReadJSON(&respStartGameJoin); err != nil { - t.Fatal(err) - } + // Free up both host and join client connections from StartGame response + var respStartGameHost mc.Message[mc.NoPayload] + if err := hostClientConn.ReadJSON(&respStartGameHost); err != nil { + t.Fatal(err) + } + + var respStartGameJoin mc.Message[mc.NoPayload] + if err := joinClientConn.ReadJSON(&respStartGameJoin); err != nil { + t.Fatal(err) + } + + // We know the position of mine of join, so + // we attack the exact position and host + // should lose upon this move + reqAttackHost := mc.NewMessage[mc.ReqAttack](mc.CodeAttack) + reqAttackHost.AddPayload(mc.ReqAttack{ + GameUuid: gameUuid, + PlayerUuid: hostPlayer.Uuid(), + X: joinMineCoordinates.X, + Y: joinMineCoordinates.Y, + }) + + if err := hostClientConn.WriteJSON(reqAttackHost); err != nil { + t.Fatal(err) + } + + var respAttackPayload mc.Message[mc.RespAttack] + if err := hostClientConn.ReadJSON(&respAttackPayload); err != nil { + t.Fatal(err) + } + + done := make(chan bool) + timer := time.NewTimer(time.Second * 5) + var endGameResp mc.Message[mc.RespEndGame] + go func() { + _ = hostClientConn.ReadJSON(&endGameResp) + _ = joinClientConn.ReadJSON(&endGameResp) + done <- true + }() + + select { + case <-timer.C: + t.Fatal("game should have ended and this should not have blocked") + case <-done: + // pass + } + + expectedRespAttackHost := mc.NewMessage[mc.RespAttack](mc.CodeAttack) + expectedRespAttackHost.AddPayload(mc.RespAttack{ + X: joinMineCoordinates.X, + Y: joinMineCoordinates.Y, + PositionState: mb.PositionStateMine, + IsTurn: hostPlayer.IsTurn(), + SunkenShipsHost: 0, + SunkenShipsJoin: 0, + DefenderSunkenShipsCoords: nil, + }) + + if !reflect.DeepEqual(expectedRespAttackHost, respAttackPayload) { + t.Fatalf("expected resp attack:\n%+v\n\ngot:\n%+v", expectedRespAttackHost, respAttackPayload) + } + + if hostPlayer.MatchStatus() != mb.PlayerMatchStatusLost { + t.Fatal("host match status must be lost now") + } + if joinPlayer.MatchStatus() != mb.PlayerMatchStatusWon { + t.Fatal("join match status must be won now") + } + + if !hostPlayer.IsMatchOver() { + t.Fatal("match status should be over for host") + } + if !joinPlayer.IsMatchOver() { + t.Fatal("match status should be over for join") + } }