feat(battleship): add full placement/battle phases + metadata support + action-based moves

- Added Battleship Fleet definition (carrier, battleship, cruiser, submarine, destroyer)
- Implemented action-based MovePayload (`action: "place" | "shoot"`)
- Added placement and shot validation (ValidatePlacementMove, ValidateShotMove)
- Added ApplyPlacement and ApplyShot with correct ship placement + hit/miss logic
- Added pX_placed, pX_ready tracking and phase switching (placement → battle)
- Added Metadata field to MatchState (for phase/ready tracking)
- Updated MatchInit to initialize placement phase and readiness flags
- Updated MatchLoop to enforce turn order only during battle phase
- Added debug logging for state broadcasts
- Fixed protobuf dependency marking as indirect
This commit is contained in:
2025-12-03 21:00:38 +05:30
parent 10c7933aca
commit 0562d1e0c9
4 changed files with 165 additions and 10 deletions

View File

@@ -6,6 +6,14 @@ import (
"localrepo/plugins/structs" "localrepo/plugins/structs"
) )
var Fleet = map[string]int{
"carrier": 5,
"battleship": 4,
"cruiser": 3,
"submarine": 3,
"destroyer": 2,
}
// //
// BATTLESHIP RULES IMPLEMENTATION // BATTLESHIP RULES IMPLEMENTATION
// //
@@ -90,11 +98,39 @@ func (b *BattleshipRules) AssignPlayerSymbols(players []*structs.Player) {
// ------------------------------ // ------------------------------
func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool { func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
switch payload.Action {
case "place":
return b.ValidatePlacementMove(state, playerIdx, payload)
case "shoot":
return b.ValidateShotMove(state, playerIdx, payload)
default:
return false
}
}
func (b *BattleshipRules) ValidatePlacementMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
// Allow placement until player placed all ships
if state.Metadata["phase"] != "placement" {
return false
}
key := fmt.Sprintf("p%d_placed", playerIdx)
placed := 0
if state.Metadata[key] != nil {
placed = state.Metadata[key].(int)
}
return placed < len(Fleet)
}
func (b *BattleshipRules) ValidateShotMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
if state.Metadata["phase"] != "battle" {
return false
}
rf, ok1 := payload.Data["row"].(float64) rf, ok1 := payload.Data["row"].(float64)
cf, ok2 := payload.Data["col"].(float64) cf, ok2 := payload.Data["col"].(float64)
if !ok1 || !ok2 { if !ok1 || !ok2 {
return false return false
} }
r := int(rf) r := int(rf)
c := int(cf) c := int(cf)
@@ -108,8 +144,7 @@ func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int,
return false return false
} }
// can't shoot same cell twice if !shotBoard.IsEmpty(r, c) { // already shot
if !shotBoard.IsEmpty(r, c) {
return false return false
} }
@@ -119,11 +154,14 @@ func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int,
// ----------------------------- // -----------------------------
// APPLY MOVE (MODE B — CLASSIC) // APPLY MOVE (MODE B — CLASSIC)
// ----------------------------- // -----------------------------
func (b *BattleshipRules) ApplyMove( func (b *BattleshipRules) ApplyShot(
state *structs.MatchState, state *structs.MatchState,
playerIdx int, playerIdx int,
payload MovePayload, payload MovePayload,
) (bool, bool, int, bool) { ) (bool, bool, int, bool) {
if !b.bothPlayersReady(state) {
return false, false, -1, false
}
r := int(payload.Data["row"].(float64)) r := int(payload.Data["row"].(float64))
c := int(payload.Data["col"].(float64)) c := int(payload.Data["col"].(float64))
@@ -152,6 +190,115 @@ func (b *BattleshipRules) ApplyMove(
return true, over, winner, hit return true, over, winner, hit
} }
func (b *BattleshipRules) ApplyMove(
state *structs.MatchState,
playerIdx int,
payload MovePayload,
) (bool, bool, int, bool) {
switch payload.Action {
case "place":
return b.ApplyPlacement(state, playerIdx, payload)
case "shoot":
return b.ApplyShot(state, playerIdx, payload)
default:
return false, false, -1, false
}
}
func (b *BattleshipRules) ApplyPlacement(
state *structs.MatchState,
playerIdx int,
payload MovePayload,
) (bool, bool, int, bool) {
shipName, _ := payload.Data["ship"].(string)
rf, _ := payload.Data["row"].(float64)
cf, _ := payload.Data["col"].(float64)
dir, _ := payload.Data["dir"].(string)
r := int(rf)
c := int(cf)
size, ok := Fleet[shipName]
if !ok {
return false, false, -1, false // invalid ship name
}
shipKey := fmt.Sprintf("p%d_ships", playerIdx)
shipBoard := state.Boards[shipKey]
// Validate placement
if !b.validatePlacement(shipBoard, r, c, size, dir) {
return false, false, -1, false
}
// Place the ship
if dir == "h" {
for i := 0; i < size; i++ {
shipBoard.Set(r, c+i, "S")
}
} else { // vertical
for i := 0; i < size; i++ {
shipBoard.Set(r+i, c, "S")
}
}
// Track ships placed by player
placedCountKey := fmt.Sprintf("p%d_placed", playerIdx)
count := state.Metadata[placedCountKey]
if count == nil {
state.Metadata[placedCountKey] = 1
} else {
state.Metadata[placedCountKey] = count.(int) + 1
}
// If all 5 ships placed → ready
if state.Metadata[placedCountKey].(int) == len(Fleet) {
readyKey := fmt.Sprintf("p%d_ready", playerIdx)
state.Metadata[readyKey] = true
}
// Check if both players are ready
if b.bothPlayersReady(state) {
state.Metadata["phase"] = "battle"
}
return true, false, -1, false
}
func (b *BattleshipRules) validatePlacement(board *structs.Board, r, c, size int, dir string) bool {
rows, cols := board.Rows, board.Cols
if dir == "h" {
if c+size > cols {
return false
}
for i := 0; i < size; i++ {
if board.Get(r, c+i) != "" {
return false
}
}
} else {
if r+size > rows {
return false
}
for i := 0; i < size; i++ {
if board.Get(r+i, c) != "" {
return false
}
}
}
return true
}
func (b *BattleshipRules) bothPlayersReady(state *structs.MatchState) bool {
r0 := state.Metadata["p0_ready"]
r1 := state.Metadata["p1_ready"]
return r0 == true && r1 == true
}
// ------------------------------ // ------------------------------
// CheckGameOver // CheckGameOver
// ------------------------------ // ------------------------------

View File

@@ -6,7 +6,8 @@ import "localrepo/plugins/structs"
// It is intentionally untyped (map[string]interface{}) so each game // It is intentionally untyped (map[string]interface{}) so each game
// can define its own move structure (e.g., row/col, coordinate, action type, etc.) // can define its own move structure (e.g., row/col, coordinate, action type, etc.)
type MovePayload struct { type MovePayload struct {
Data map[string]interface{} `json:"data"` Action string `json:"action"` // "place" or "shoot"
Data map[string]interface{} `json:"data"`
} }
// GameRules defines a generic interface for match logic. // GameRules defines a generic interface for match logic.

View File

@@ -128,7 +128,11 @@ func (m *GenericMatch) MatchInit(
Turn: 0, Turn: 0,
Winner: -1, Winner: -1,
GameOver: false, GameOver: false,
Metadata: map[string]interface{}{},
} }
state.Metadata["phase"] = "placement"
state.Metadata["p0_ready"] = false
state.Metadata["p1_ready"] = false
label := fmt.Sprintf("%s:%s", m.GameName, m.Mode) label := fmt.Sprintf("%s:%s", m.GameName, m.Mode)
@@ -312,7 +316,8 @@ func (m *GenericMatch) MatchLoop(
// Turn enforcement — keep this here for turn-based games. If you want per-game control, // Turn enforcement — keep this here for turn-based games. If you want per-game control,
// move this check into the game's ApplyMove implementation or toggle via config. // move this check into the game's ApplyMove implementation or toggle via config.
if playerIdx != s.Turn { phase := s.Metadata["phase"]
if phase == "battle" && playerIdx != s.Turn {
logger.Warn("Move rejected: not player's turn (idx=%d turn=%d)", playerIdx, s.Turn) logger.Warn("Move rejected: not player's turn (idx=%d turn=%d)", playerIdx, s.Turn)
continue continue
} }
@@ -338,6 +343,7 @@ func (m *GenericMatch) MatchLoop(
if changed { if changed {
if data, err := json.Marshal(s); err == nil { if data, err := json.Marshal(s); err == nil {
logger.Info("Broadcasting state update (op=%d): %v", OpState, data)
if err := dispatcher.BroadcastMessage(OpState, data, nil, nil, true); err != nil { if err := dispatcher.BroadcastMessage(OpState, data, nil, nil, true); err != nil {
logger.Error("BroadcastMessage failed: %v", err) logger.Error("BroadcastMessage failed: %v", err)
} }

View File

@@ -2,9 +2,10 @@ package structs
// MatchState holds the full game session state. // MatchState holds the full game session state.
type MatchState struct { type MatchState struct {
Players []*Player `json:"players"` Players []*Player `json:"players"`
Boards map[string]*Board `json:"boards"` // Multiple named boards: Boards map[string]*Board `json:"boards"` // Multiple named boards:
Turn int `json:"turn"` // index in Players[] Turn int `json:"turn"` // index in Players[]
Winner int `json:"winner"` // -1 = none, >=0 = winner index Winner int `json:"winner"` // -1 = none, >=0 = winner index
GameOver bool `json:"game_over"` // true when the match ends GameOver bool `json:"game_over"` // true when the match ends
Metadata map[string]interface{} `json:metadata` // metadata
} }