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:
@@ -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
|
||||
// ------------------------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user