Files
tic-tac-toe/plugins/games/battleship.go
Vishesh 'ironeagle' Bangotra f1e85a72dd feat(rules): add game-specific metadata attachment and unify match metadata initialization
Added AttachGameMetadata to GameRules interface

Implemented metadata setup for Battleship (phase + readiness flags)

Implemented no-op metadata hook for TicTacToe

Moved generic phase/ready metadata out of MatchInit

Added game/mode metadata to match state

Fixed json:"metadata" tag in MatchState
2025-12-03 22:02:24 +05:30

355 lines
8.7 KiB
Go

package games
import (
"fmt"
"encoding/json"
"localrepo/plugins/structs"
)
var Fleet = map[string]int{
"carrier": 5,
"battleship": 4,
"cruiser": 3,
"submarine": 3,
"destroyer": 2,
}
//
// 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 battleship
}
// ------------------------------
// Attach Game Metadata
// ------------------------------
func (b *BattleshipRules) AttachGameMetadata(state *structs.MatchState) {
state.Metadata["phase"] = "placement"
state.Metadata["p0_ready"] = false
state.Metadata["p1_ready"] = false
}
// ------------------------------
// ValidateMove
// payload.data = { "row": int, "col": int }
// ------------------------------
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)
shotKey := fmt.Sprintf("p%d_shots", playerIdx)
shotBoard := state.Boards[shotKey]
if shotBoard == nil {
return false
}
if !shotBoard.InBounds(r, c) {
return false
}
if !shotBoard.IsEmpty(r, c) { // already shot
return false
}
return true
}
// -----------------------------
// APPLY MOVE (MODE B — CLASSIC)
// -----------------------------
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))
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
}
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
// ------------------------------
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
}