Files
tic-tac-toe/plugins/games/battleship.go
Vishesh 'ironeagle' Bangotra d75dcd3c74 feat(battleship,tictactoe,engine): add multi-board support and keepTurn logic
### Core Engine
- Updated `GameRules.ApplyMove` to return `(changed, gameOver, winnerIdx, keepTurn)`
- Added keepTurn handling in `MatchLoop` to support Battleship mode B (classic rules)
- Removed old single-board handling from MatchState and MatchInit
- Cleaned go.mod by marking protobuf dependency as indirect

### Battleship
- Implemented board-based state tracking using MatchState.Boards:
  - `p0_ships`, `p0_shots`, `p1_ships`, `p1_shots`
- Removed legacy metadata-based ship/shot board encoding
- Rewrote ValidateMove to use structured boards
- Rewrote ApplyMove for classic Battleship rules (mode B):
  - Hits allow the attacker to keep their turn
  - Miss switches turn
  - Destroyed ship sections marked `X`
- Improved CheckGameOver using structured boards

### TicTacToe
- Updated ApplyMove signature to match new interface
- Ensured TicTacToe always returns `keepTurn = false`
- Updated code paths to use MatchState.Boards instead of Board

### Summary
This commit completes the migration from a single-board architecture to a
multi-board architecture across the engine, TicTacToe, and Battleship, enabling
support for more complex games and multiple modes such as Battleship Mode B.
2025-12-03 18:09:49 +05:30

199 lines
4.8 KiB
Go

package games
import (
"fmt"
"encoding/json"
"localrepo/plugins/structs"
)
//
// BATTLESHIP RULES IMPLEMENTATION
//
// NOTES:
// - 2 players
// - Each player has 2 boards:
// 1. Their own ship board (state.Board is not reused here)
// 2. Their "shots" board (hits/misses on opponent)
// - We store boards in Player.Metadata as JSON strings
// (simplest method without changing your structs).
//
// ShipBoard and ShotBoard are encoded inside Metadata:
//
// Metadata["ship_board"] = JSON string of [][]string
// Metadata["shot_board"] = JSON string of [][]string
//
// ------------------------------
// Helpers: encode/decode
// ------------------------------
func encodeBoard(b [][]string) string {
out := "["
for i, row := range b {
out += "["
for j, col := range row {
out += fmt.Sprintf("%q", col)
if j < len(row)-1 {
out += ","
}
}
out += "]"
if i < len(b)-1 {
out += ","
}
}
out += "]"
return out
}
func decodeBoard(s string) [][]string {
var out [][]string
// should never fail; safe fallback
_ = json.Unmarshal([]byte(s), &out)
return out
}
// ------------------------------
// BattleshipRules
// ------------------------------
type BattleshipRules struct{}
func (b *BattleshipRules) MaxPlayers() int { return 2 }
func (b *BattleshipRules) InitBoards(players []*structs.Player, cfg GameConfiguration) map[string]*structs.Board {
boards := make(map[string]*structs.Board)
// One ships board and one shots board per player
for _, p := range players {
pid := fmt.Sprintf("p%d", p.Index)
// Player's fleet board (ships placement)
boards[pid+"_ships"] = structs.NewBoard(cfg.Board.Rows, cfg.Board.Cols)
// Player's attack tracking board (shots fired at opponent)
boards[pid+"_shots"] = structs.NewBoard(cfg.Board.Rows, cfg.Board.Cols)
}
return boards
}
// ------------------------------
// Assign player boards
// ------------------------------
func (b *BattleshipRules) AssignPlayerSymbols(players []*structs.Player) {
// nothing needed for classic mode
}
// ------------------------------
// ValidateMove
// payload.data = { "row": int, "col": int }
// ------------------------------
func (b *BattleshipRules) ValidateMove(state *structs.MatchState, playerIdx int, payload MovePayload) bool {
rf, ok1 := payload.Data["row"].(float64)
cf, ok2 := payload.Data["col"].(float64)
if !ok1 || !ok2 {
return false
}
r := int(rf)
c := int(cf)
shotKey := fmt.Sprintf("p%d_shots", playerIdx)
shotBoard := state.Boards[shotKey]
if shotBoard == nil {
return false
}
if !shotBoard.InBounds(r, c) {
return false
}
// can't shoot same cell twice
if !shotBoard.IsEmpty(r, c) {
return false
}
return true
}
// -----------------------------
// APPLY MOVE (MODE B — CLASSIC)
// -----------------------------
func (b *BattleshipRules) ApplyMove(
state *structs.MatchState,
playerIdx int,
payload MovePayload,
) (bool, bool, int, bool) {
r := int(payload.Data["row"].(float64))
c := int(payload.Data["col"].(float64))
shotKey := fmt.Sprintf("p%d_shots", playerIdx)
shipKey := fmt.Sprintf("p%d_ships", 1-playerIdx)
shots := state.Boards[shotKey]
ships := state.Boards[shipKey]
hit := false
if ships.Get(r, c) == "S" {
// hit
hit = true
shots.Set(r, c, "H")
ships.Set(r, c, "X") // mark destroyed section
} else {
shots.Set(r, c, "M")
}
// check game over
over, winner := b.CheckGameOver(state)
// keepTurn = hit (classic rule)
return true, over, winner, hit
}
// ------------------------------
// CheckGameOver
// ------------------------------
func (b *BattleshipRules) CheckGameOver(state *structs.MatchState) (bool, int) {
for i := range state.Players {
shipKey := fmt.Sprintf("p%d_ships", i)
ships := state.Boards[shipKey]
alive := false
for r := 0; r < ships.Rows; r++ {
for c := 0; c < ships.Cols; c++ {
if ships.Get(r, c) == "S" {
alive = true
break
}
}
if alive {
break
}
}
if !alive {
// this player has no ships left → opponent wins
return true, 1 - i
}
}
return false, -1
}
// ------------------------------
// Forfeit Winner
// ------------------------------
func (b *BattleshipRules) ForfeitWinner(state *structs.MatchState, leaverIndex int) int {
// If player leaves, opponent automatically wins.
if leaverIndex == 0 {
return 1
}
if leaverIndex == 1 {
return 0
}
return -1
}