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
355 lines
8.7 KiB
Go
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
|
|
}
|