diff --git a/Makefile b/Makefile index e4c015c..193ea42 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +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 @@ -32,13 +33,17 @@ 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 + fly pg connect -a battleship-db # Websocket fly ws_staging: diff --git a/api/handlers_api.go b/api/handlers.go similarity index 86% rename from api/handlers_api.go rename to api/handlers.go index 7901990..9b384e9 100644 --- a/api/handlers_api.go +++ b/api/handlers.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" @@ -11,7 +10,7 @@ import ( 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] + 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) @@ -39,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 @@ -78,15 +77,20 @@ 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 } // 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] { +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.NoPayload](mc.CodeReady) + resp := mc.NewMessage[mc.RespReady](mc.CodeReady) if err := json.Unmarshal(r.payload, &readyPlayerReq); err != nil { resp.AddError(err.Error(), cerr.ConstErrInvalidPayload) @@ -99,6 +103,11 @@ func (r Request) HandleReadyPlayer(bgm mb.GameManager, game *mb.Game, sessionPla return resp } + if game.IsModeMine() { + mineCoordinates := sessionPlayer.PlantMineInDefenceGrid() + resp.AddPayload(mc.RespReady{MinePosition: mineCoordinates}) + } + return resp } @@ -140,6 +149,22 @@ func (r Request) HandleAttack(game *mb.Game, attacker mb.Player, defender mb.Pla hostPlayer := game.HostPlayer() joinPlayer := game.JoinPlayer() + if game.IsModeMine() && 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 +203,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 df3b4f9..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 } @@ -334,6 +333,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/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) } 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..bb90257 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 @@ -133,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/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/battleship/grid.go b/models/battleship/grid.go index 1e6557f..d834525 100644 --- a/models/battleship/grid.go +++ b/models/battleship/grid.go @@ -1,5 +1,26 @@ package battleship +const ( + PositionStateAttackGridEmpty uint8 = iota + PositionStateAttackGridMiss + PositionStateAttackGridHit +) + +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 0891858..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" ) @@ -10,12 +12,6 @@ const ( PlayerMatchStatusWon ) -const ( - PositionStateAttackGridEmpty uint8 = iota - PositionStateAttackGridMiss - PositionStateAttackGridHit -) - type Player interface { SessionId() string Uuid() string @@ -23,6 +19,7 @@ type Player interface { AreAllShipsSunken() bool IsShipSunken(uint8) bool IsWinner() bool + IsMatchOver() bool IncrementShipHit(code uint8, coordinates Coordinates) SetAttackGrid(newGrid Grid) SetReady(newGrid Grid) @@ -51,6 +48,9 @@ type Player interface { IsReady() bool IsTurn() bool + + DidAttackerHitMine(coordinates Coordinates) bool + PlantMineInDefenceGrid() Coordinates } type BattleshipPlayer struct { @@ -152,10 +152,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 @@ -193,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/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 ) 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/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..933cc82 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,15 +17,19 @@ 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"` } +type RespReady struct { + MinePosition mb.Coordinates `json:"mine_position"` +} + type RespSessionId struct { SessionID string `json:"session_id"` } 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 { diff --git a/test/main_test.go b/test/main_test.go index d1a80b3..8f6db2b 100644 --- a/test/main_test.go +++ b/test/main_test.go @@ -24,22 +24,44 @@ const ( ) var ( - HostConn *websocket.Conn - JoinConn *websocket.Conn - testGame *mb.Game - testGameUuid string + 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) - HostSessionID = respSessionId.Payload.SessionID + _ = 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) - JoinSessionID = respSessionId.Payload.SessionID + _ = 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.Println("Host session ID:", hostSessionID) + log.Println("Join session ID:", joinSessionID) + log.Printf("host: %s\tjoin: %s", hostClientConn.LocalAddr().String(), joinClientConn.LocalAddr().String()) os.Exit(m.Run()) } diff --git a/test/ws_test.go b/test/ws_defaultgame_test.go similarity index 83% rename from test/ws_test.go rename to test/ws_defaultgame_test.go index 619284d..2f16677 100644 --- a/test/ws_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 @@ -28,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, }, } @@ -63,16 +68,17 @@ 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 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: hostClientConn, }, } @@ -129,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 // { @@ -170,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 { @@ -178,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 { @@ -190,26 +196,8 @@ 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}, - } - - tests := []Test[mc.Message[mc.ReqReadyPlayer], mc.Message[mc.NoPayload]]{ +func TestReadyGameDefault(t *testing.T) { + tests := []Test[mc.Message[mc.ReqReadyPlayer], mc.Message[mc.RespReady]]{ { name: "set defence grid ready host", expectedCode: mc.CodeReady, @@ -221,8 +209,8 @@ func TestReadyGame(t *testing.T) { PlayerUuid: testHostPlayer.Uuid(), }, }, - respPayload: mc.Message[mc.NoPayload]{}, - conn: HostConn, + respPayload: mc.Message[mc.RespReady]{}, + conn: hostClientConn, }, { name: "set defence grid ready join", @@ -235,8 +223,8 @@ func TestReadyGame(t *testing.T) { PlayerUuid: testJoinPlayer.Uuid(), }, }, - respPayload: mc.Message[mc.NoPayload]{}, - conn: JoinConn, + respPayload: mc.Message[mc.RespReady]{}, + conn: joinClientConn, }, } @@ -267,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) } } @@ -281,7 +269,53 @@ func TestReadyGame(t *testing.T) { } } -func TestAttack(t *testing.T) { +func TestPlayerInteractionDefault(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]]{ + { + name: "successful msg host to join", + expectedCode: mc.CodePlayerInteraction, + reqPayload: msg, + respPayload: mc.Message[mc.PlayerInteraction]{}, + conn: hostClientConn, + otherConn: joinClientConn, + }, + { + name: "successful msg join to host", + expectedCode: mc.CodePlayerInteraction, + reqPayload: msg, + respPayload: mc.Message[mc.PlayerInteraction]{}, + conn: joinClientConn, + otherConn: hostClientConn, + }, + } + + 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 TestAttackDefault(t *testing.T) { tests := []Test[mc.Message[mc.ReqAttack], mc.Message[mc.RespAttack]]{ { name: "successful hit attack destroyer valid payload host 1", @@ -301,8 +335,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 0, }}, - conn: HostConn, - otherConn: JoinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { @@ -323,8 +357,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 0, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -349,8 +383,8 @@ func TestAttack(t *testing.T) { {X: 0, Y: 2}, }, }}, - conn: HostConn, - otherConn: JoinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, { @@ -371,8 +405,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -387,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, }, { @@ -403,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, }, { @@ -419,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, }, { @@ -435,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, }, /* @@ -460,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", @@ -481,8 +515,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -503,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", @@ -524,8 +558,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 1, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -551,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", @@ -572,8 +606,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, /* @@ -597,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", @@ -618,8 +652,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -640,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", @@ -661,8 +695,8 @@ func TestAttack(t *testing.T) { SunkenShipsHost: 0, SunkenShipsJoin: 2, }}, - conn: JoinConn, - otherConn: HostConn, + conn: joinClientConn, + otherConn: hostClientConn, }, { @@ -683,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", @@ -704,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 @@ -733,8 +767,8 @@ func TestAttack(t *testing.T) { {X: 4, Y: 4}, }, }}, - conn: HostConn, - otherConn: JoinConn, + conn: hostClientConn, + otherConn: joinClientConn, }, } @@ -774,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) } } @@ -786,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) } @@ -815,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) } @@ -861,43 +895,43 @@ 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) } - 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") } } diff --git a/test/ws_minegame_test.go b/test/ws_minegame_test.go new file mode 100644 index 0000000..dd1ad08 --- /dev/null +++ b/test/ws_minegame_test.go @@ -0,0 +1,179 @@ +package test + +import ( + "log" + "reflect" + "time" + + 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) + } + + joinMineCoordinates := respReadyJoin.Payload.MinePosition + + // 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") + } +}