Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
42 changes: 33 additions & 9 deletions api/handlers_api.go → api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions api/request_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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")
Expand Down
25 changes: 23 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
}
8 changes: 6 additions & 2 deletions internal/error/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 19 additions & 4 deletions models/battleship/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ const (
GameDifficultyHard
)

const (
GameModeDefault uint8 = iota
GameModeMine
)

const (
GridSizeEasy uint8 = 6
GridSizeNormal uint8 = 7
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -133,3 +144,7 @@ func (g *Game) SetPlayerReadyForGame(player Player, selectedGrid Grid) error {

return nil
}

func (g *Game) IsModeMine() bool {
return g.mode == GameModeMine
}
17 changes: 13 additions & 4 deletions models/battleship/game_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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
}
21 changes: 21 additions & 0 deletions models/battleship/grid.go
Original file line number Diff line number Diff line change
@@ -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"`
Expand Down
Loading