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