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,6 +6,7 @@ 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 {
|
||||
Action string `json:"action"` // "place" or "shoot"
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ type MatchState struct {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user