diff --git a/plugins/games/battleship.go b/plugins/games/battleship.go index 1c5c41b..e6cfc00 100644 --- a/plugins/games/battleship.go +++ b/plugins/games/battleship.go @@ -6,6 +6,14 @@ import ( "localrepo/plugins/structs" ) +var Fleet = map[string]int{ + "carrier": 5, + "battleship": 4, + "cruiser": 3, + "submarine": 3, + "destroyer": 2, +} + // // 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 { + 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) cf, ok2 := payload.Data["col"].(float64) if !ok1 || !ok2 { return false } + r := int(rf) c := int(cf) @@ -108,8 +144,7 @@ func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int, return false } - // can't shoot same cell twice - if !shotBoard.IsEmpty(r, c) { + if !shotBoard.IsEmpty(r, c) { // already shot return false } @@ -119,11 +154,14 @@ func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int, // ----------------------------- // APPLY MOVE (MODE B — CLASSIC) // ----------------------------- -func (b *BattleshipRules) ApplyMove( +func (b *BattleshipRules) ApplyShot( state *structs.MatchState, playerIdx int, payload MovePayload, ) (bool, bool, int, bool) { + if !b.bothPlayersReady(state) { + return false, false, -1, false + } r := int(payload.Data["row"].(float64)) c := int(payload.Data["col"].(float64)) @@ -152,6 +190,115 @@ func (b *BattleshipRules) ApplyMove( 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 // ------------------------------ diff --git a/plugins/games/rules.go b/plugins/games/rules.go index 351d32a..9ae9e96 100644 --- a/plugins/games/rules.go +++ b/plugins/games/rules.go @@ -6,7 +6,8 @@ import "localrepo/plugins/structs" // It is intentionally untyped (map[string]interface{}) so each game // can define its own move structure (e.g., row/col, coordinate, action type, etc.) 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. diff --git a/plugins/modules/match.go b/plugins/modules/match.go index f7be266..b833c5a 100644 --- a/plugins/modules/match.go +++ b/plugins/modules/match.go @@ -128,7 +128,11 @@ func (m *GenericMatch) MatchInit( Turn: 0, Winner: -1, 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) @@ -312,7 +316,8 @@ func (m *GenericMatch) MatchLoop( // 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. - 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) continue } @@ -338,6 +343,7 @@ func (m *GenericMatch) MatchLoop( if changed { 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 { logger.Error("BroadcastMessage failed: %v", err) } diff --git a/plugins/structs/match_state.go b/plugins/structs/match_state.go index 1ebd00e..2848d10 100644 --- a/plugins/structs/match_state.go +++ b/plugins/structs/match_state.go @@ -2,9 +2,10 @@ package structs // MatchState holds the full game session state. type MatchState struct { - Players []*Player `json:"players"` - Boards map[string]*Board `json:"boards"` // Multiple named boards: - Turn int `json:"turn"` // index in Players[] - Winner int `json:"winner"` // -1 = none, >=0 = winner index - GameOver bool `json:"game_over"` // true when the match ends + Players []*Player `json:"players"` + Boards map[string]*Board `json:"boards"` // Multiple named boards: + Turn int `json:"turn"` // index in Players[] + Winner int `json:"winner"` // -1 = none, >=0 = winner index + GameOver bool `json:"game_over"` // true when the match ends + Metadata map[string]interface{} `json:metadata` // metadata }