This commit is contained in:
parent
804d6aed2a
commit
ab1dd08225
7 changed files with 216 additions and 140 deletions
|
|
@ -33,22 +33,23 @@ const (
|
|||
|
||||
// ChessState represents the match state
|
||||
type ChessState struct {
|
||||
PositionID int `json:"positionId"`
|
||||
FEN string `json:"fen"`
|
||||
PGN string `json:"pgn"`
|
||||
WhiteID string `json:"whiteId"`
|
||||
BlackID string `json:"blackId"`
|
||||
WhiteName string `json:"whiteName"`
|
||||
BlackName string `json:"blackName"`
|
||||
WhiteColor string `json:"whiteColor"`
|
||||
BlackColor string `json:"blackColor"`
|
||||
Turn string `json:"turn"`
|
||||
GameOver bool `json:"gameOver"`
|
||||
Result string `json:"result,omitempty"`
|
||||
MoveHistory []string `json:"moveHistory"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
Spectators []string `json:"spectators"`
|
||||
PendingCancel bool `json:"pendingCancel,omitempty"`
|
||||
PositionID int `json:"positionId"`
|
||||
FEN string `json:"fen"`
|
||||
PGN string `json:"pgn"`
|
||||
WhiteID string `json:"whiteId"`
|
||||
BlackID string `json:"blackId"`
|
||||
WhiteName string `json:"whiteName"`
|
||||
BlackName string `json:"blackName"`
|
||||
WhiteColor string `json:"whiteColor"`
|
||||
BlackColor string `json:"blackColor"`
|
||||
Turn string `json:"turn"`
|
||||
GameOver bool `json:"gameOver"`
|
||||
Result string `json:"result,omitempty"`
|
||||
MoveHistory []string `json:"moveHistory"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
LastMoveAt int64 `json:"lastMoveAt"`
|
||||
Spectators []string `json:"spectators"`
|
||||
PendingCancel bool `json:"pendingCancel,omitempty"`
|
||||
}
|
||||
|
||||
// AppJwtClaims from the main app
|
||||
|
|
@ -427,6 +428,7 @@ func (m *ChessMatch) MatchJoin(ctx context.Context, logger runtime.Logger, db *s
|
|||
s.BlackID = userID
|
||||
s.BlackName = username
|
||||
s.BlackColor = userColor
|
||||
s.LastMoveAt = time.Now().Unix() // Game starts - white has 3 days to move
|
||||
logger.Info("%s joined as BLACK", username)
|
||||
|
||||
label := MatchLabel{
|
||||
|
|
@ -521,6 +523,46 @@ func (m *ChessMatch) MatchLoop(ctx context.Context, logger runtime.Logger, db *s
|
|||
}
|
||||
}
|
||||
|
||||
// Check for move timeout (3 days without a move = forfeit)
|
||||
if tick%60 == 0 && !s.GameOver && s.WhiteID != "" && s.BlackID != "" && s.LastMoveAt > 0 {
|
||||
now := time.Now().Unix()
|
||||
const moveTimeout = 3 * 24 * 60 * 60 // 3 days in seconds
|
||||
|
||||
if (now - s.LastMoveAt) > moveTimeout {
|
||||
// Current player (whose turn it is) forfeits
|
||||
if s.Turn == "w" {
|
||||
logger.Info("White player forfeit due to move timeout (3 days)")
|
||||
s.GameOver = true
|
||||
s.Result = "0-1"
|
||||
updateElo(ctx, nk, logger, s.BlackID, s.BlackName, s.WhiteID, s.WhiteName, false)
|
||||
} else {
|
||||
logger.Info("Black player forfeit due to move timeout (3 days)")
|
||||
s.GameOver = true
|
||||
s.Result = "1-0"
|
||||
updateElo(ctx, nk, logger, s.WhiteID, s.WhiteName, s.BlackID, s.BlackName, false)
|
||||
}
|
||||
|
||||
msg, _ := json.Marshal(map[string]interface{}{
|
||||
"result": s.Result,
|
||||
"reason": "timeout - no move in 3 days",
|
||||
})
|
||||
dispatcher.BroadcastMessage(OpCodeGameOver, msg, nil, nil, true)
|
||||
|
||||
label := MatchLabel{
|
||||
Game: "chess960",
|
||||
Status: "finished",
|
||||
Result: s.Result,
|
||||
White: s.WhiteName,
|
||||
WhiteID: s.WhiteID,
|
||||
Black: s.BlackName,
|
||||
BlackID: s.BlackID,
|
||||
PositionID: s.PositionID,
|
||||
}
|
||||
labelJSON, _ := json.Marshal(label)
|
||||
dispatcher.MatchLabelUpdate(string(labelJSON))
|
||||
}
|
||||
}
|
||||
|
||||
for _, message := range messages {
|
||||
playerID := message.GetUserId()
|
||||
|
||||
|
|
@ -559,6 +601,7 @@ func (m *ChessMatch) MatchLoop(ctx context.Context, logger runtime.Logger, db *s
|
|||
s.FEN = result.NewFEN
|
||||
s.Turn = result.Turn
|
||||
s.MoveHistory = append(s.MoveHistory, fmt.Sprintf("%s-%s", moveData.From, moveData.To))
|
||||
s.LastMoveAt = time.Now().Unix() // Reset move timer
|
||||
|
||||
// ALWAYS broadcast the move to all players/spectators first
|
||||
moveMsg, _ := json.Marshal(map[string]interface{}{
|
||||
|
|
@ -662,60 +705,11 @@ func (m *ChessMatch) MatchLeave(ctx context.Context, logger runtime.Logger, db *
|
|||
}
|
||||
}
|
||||
|
||||
// Player leaving
|
||||
// Player leaving (disconnect/refresh)
|
||||
if s.WhiteID == userID || s.BlackID == userID {
|
||||
logger.Info("Player %s left the match", username)
|
||||
|
||||
// If waiting for opponent, don't terminate immediately
|
||||
if s.WhiteID != "" && s.BlackID == "" && userID == s.WhiteID {
|
||||
logger.Info("White player disconnected from waiting challenge")
|
||||
return s
|
||||
}
|
||||
|
||||
// If game in progress, opponent wins by forfeit
|
||||
if !s.GameOver && s.WhiteID != "" && s.BlackID != "" {
|
||||
isWhiteLeaving := userID == s.WhiteID
|
||||
s.GameOver = true
|
||||
if isWhiteLeaving {
|
||||
s.Result = "0-1"
|
||||
} else {
|
||||
s.Result = "1-0"
|
||||
}
|
||||
|
||||
var winnerID, winnerName, loserName string
|
||||
if isWhiteLeaving {
|
||||
winnerID = s.BlackID
|
||||
winnerName = s.BlackName
|
||||
loserName = s.WhiteName
|
||||
} else {
|
||||
winnerID = s.WhiteID
|
||||
winnerName = s.WhiteName
|
||||
loserName = s.BlackName
|
||||
}
|
||||
updateElo(ctx, nk, logger, winnerID, winnerName, userID, loserName, false)
|
||||
|
||||
msg, _ := json.Marshal(map[string]interface{}{
|
||||
"result": s.Result,
|
||||
"reason": "forfeit",
|
||||
})
|
||||
dispatcher.BroadcastMessage(OpCodeGameOver, msg, nil, nil, true)
|
||||
|
||||
// Update match label to finished so it's not detected as active
|
||||
label := MatchLabel{
|
||||
Game: "chess960",
|
||||
Status: "finished",
|
||||
Result: s.Result,
|
||||
White: s.WhiteName,
|
||||
WhiteID: s.WhiteID,
|
||||
Black: s.BlackName,
|
||||
BlackID: s.BlackID,
|
||||
PositionID: s.PositionID,
|
||||
}
|
||||
labelJSON, _ := json.Marshal(label)
|
||||
dispatcher.MatchLabelUpdate(string(labelJSON))
|
||||
|
||||
logger.Info("Game forfeited: %s", s.Result)
|
||||
}
|
||||
logger.Info("Player %s disconnected from match (can rejoin anytime)", username)
|
||||
// Games persist indefinitely - player can rejoin anytime
|
||||
// No forfeit on disconnect - only manual resign ends the game
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1054,17 +1048,21 @@ func listChessMatchesRpc(ctx context.Context, logger runtime.Logger, db *sql.DB,
|
|||
}
|
||||
|
||||
userID, _ := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
|
||||
logger.Info("listChessMatches: found %d total matches, filtering by status=%s", len(matches), params.Status)
|
||||
|
||||
var result []map[string]interface{}
|
||||
for _, match := range matches {
|
||||
var label MatchLabel
|
||||
if err := json.Unmarshal([]byte(match.Label.Value), &label); err != nil {
|
||||
logger.Warn("listChessMatches: failed to parse label for match %s", match.MatchId)
|
||||
continue
|
||||
}
|
||||
if label.Game != "chess960" {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("listChessMatches: match %s has status=%s", match.MatchId, label.Status)
|
||||
|
||||
// Filter by status
|
||||
if params.Status != "" && params.Status != "all" && label.Status != params.Status {
|
||||
continue
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue