839 lines
22 KiB
Go
839 lines
22 KiB
Go
package command
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/lixenwraith/chess/internal/client/api"
|
|
"github.com/lixenwraith/chess/internal/client/display"
|
|
"github.com/lixenwraith/chess/internal/client/session"
|
|
)
|
|
|
|
func (r *Registry) registerGameCommands() {
|
|
r.Register(&Command{
|
|
Name: "new",
|
|
ShortName: "n",
|
|
Description: "Create a new game",
|
|
Usage: "new",
|
|
Handler: newGameHandler,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "join",
|
|
ShortName: "j",
|
|
Description: "Join/set current game ID",
|
|
Usage: "join <gameId>",
|
|
Handler: joinGameHandler,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "move",
|
|
ShortName: "m",
|
|
Description: "Make a move",
|
|
Usage: "move <uci-move> (e2e4; castling e1g1; promotion e7e8q, e7e8n)",
|
|
Handler: moveHandler,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "computer",
|
|
ShortName: "c",
|
|
Description: "Trigger computer move",
|
|
Usage: "computer",
|
|
Handler: computerMoveHandler,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "undo",
|
|
ShortName: "u",
|
|
Description: "Undo moves",
|
|
Usage: "undo [count]",
|
|
Handler: undoHandler,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "show",
|
|
ShortName: "h",
|
|
Description: "Show board and game state",
|
|
Usage: "show",
|
|
Handler: showBoardHandler,
|
|
Paged: true,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "state",
|
|
ShortName: "s",
|
|
Description: "Show raw game JSON",
|
|
Usage: "state",
|
|
Handler: gameStateHandler,
|
|
Paged: true,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "delete",
|
|
ShortName: "d",
|
|
Description: "Unload a live game (history retained)",
|
|
Usage: "delete [gameId]",
|
|
Handler: deleteGameHandler,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "resign",
|
|
ShortName: "",
|
|
Description: "Resign the current game",
|
|
Usage: "resign [w|b] (side optional when you play one human side)",
|
|
Handler: resignHandler,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "draw",
|
|
ShortName: "",
|
|
Description: "Offer, accept, or decline a draw",
|
|
Usage: "draw [offer|accept|decline] [w|b] (default: offer)",
|
|
Handler: drawHandler,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "games",
|
|
ShortName: "g",
|
|
Description: "List, or delete, your stored games (login required)",
|
|
Usage: "games [more] | games delete <#|gameId>",
|
|
Handler: gamesHandler,
|
|
Paged: true,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "pgn",
|
|
ShortName: "",
|
|
Description: "Print or save a stored game as PGN",
|
|
Usage: "pgn [gameId] [ply] [save <path>] (default: current or replayed game)",
|
|
Handler: pgnHandler,
|
|
Paged: true,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "import",
|
|
ShortName: "",
|
|
Description: "Store a PGN file as one of your games (login required)",
|
|
Usage: "import <path>",
|
|
Handler: importHandler,
|
|
})
|
|
|
|
r.Register(&Command{
|
|
Name: "poll",
|
|
ShortName: "p",
|
|
Description: "Long-poll for game updates",
|
|
Usage: "poll",
|
|
Handler: pollHandler,
|
|
})
|
|
}
|
|
|
|
func newGameHandler(s *session.Session, args []string) error {
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
c := s.Client
|
|
|
|
display.Println(display.Cyan, "\nCreating new game...")
|
|
|
|
// White player
|
|
display.Print(display.Yellow, "White player type (h/c) [h]: ")
|
|
scanner.Scan()
|
|
whiteType := strings.ToLower(strings.TrimSpace(scanner.Text()))
|
|
if whiteType == "" {
|
|
whiteType = "h"
|
|
}
|
|
|
|
white := api.PlayerConfig{Type: 1}
|
|
if whiteType == "c" {
|
|
white.Type = 2
|
|
|
|
display.Print(display.Yellow, "Computer level (0-20) [10]: ")
|
|
scanner.Scan()
|
|
levelStr := strings.TrimSpace(scanner.Text())
|
|
if levelStr == "" {
|
|
white.Level = 10
|
|
} else {
|
|
level, _ := strconv.Atoi(levelStr)
|
|
white.Level = level
|
|
}
|
|
|
|
display.Print(display.Yellow, "Search time (100-10000ms) [1000]: ")
|
|
scanner.Scan()
|
|
timeStr := strings.TrimSpace(scanner.Text())
|
|
if timeStr == "" {
|
|
white.SearchTime = 1000
|
|
} else {
|
|
searchTime, _ := strconv.Atoi(timeStr)
|
|
white.SearchTime = searchTime
|
|
}
|
|
}
|
|
|
|
// Black player (same pattern)
|
|
display.Print(display.Yellow, "Black player type (h/c) [h]: ")
|
|
scanner.Scan()
|
|
blackType := strings.ToLower(strings.TrimSpace(scanner.Text()))
|
|
if blackType == "" {
|
|
blackType = "h"
|
|
}
|
|
|
|
black := api.PlayerConfig{Type: 1}
|
|
if blackType == "c" {
|
|
black.Type = 2
|
|
|
|
display.Print(display.Yellow, "Computer level (0-20) [10]: ")
|
|
scanner.Scan()
|
|
levelStr := strings.TrimSpace(scanner.Text())
|
|
if levelStr == "" {
|
|
black.Level = 10
|
|
} else {
|
|
level, _ := strconv.Atoi(levelStr)
|
|
black.Level = level
|
|
}
|
|
|
|
display.Print(display.Yellow, "Search time (100-10000ms) [1000]: ")
|
|
scanner.Scan()
|
|
timeStr := strings.TrimSpace(scanner.Text())
|
|
if timeStr == "" {
|
|
black.SearchTime = 1000
|
|
} else {
|
|
searchTime, _ := strconv.Atoi(timeStr)
|
|
black.SearchTime = searchTime
|
|
}
|
|
}
|
|
|
|
// Starting position
|
|
display.Print(display.Yellow, "Starting position (FEN) [default]: ")
|
|
scanner.Scan()
|
|
fen := strings.TrimSpace(scanner.Text())
|
|
|
|
req := &api.CreateGameRequest{
|
|
White: white,
|
|
Black: black,
|
|
FEN: fen,
|
|
}
|
|
|
|
resp, err := c.CreateGame(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.CurrentGame = resp.GameID
|
|
s.LastMoveCount = len(resp.Moves)
|
|
s.CurrentGameState = resp
|
|
|
|
// Determine player color if authenticated
|
|
if s.CurrentUser != "" {
|
|
if resp.Players.White.ID == s.CurrentUser {
|
|
s.PlayerColor = "w"
|
|
} else if resp.Players.Black.ID == s.CurrentUser {
|
|
s.PlayerColor = "b"
|
|
}
|
|
}
|
|
|
|
display.Println(display.Green, "Game created: %s", resp.GameID)
|
|
display.Println(display.Cyan, "Current game set to: %s", resp.GameID)
|
|
|
|
// If white is computer, inform user to trigger move
|
|
if white.Type == 2 {
|
|
display.Println(display.Magenta, "\nWhite is computer. Use 'computer' or 'c' to trigger first move.")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func joinGameHandler(s *session.Session, args []string) error {
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("usage: join <gameId>")
|
|
}
|
|
|
|
gameID := args[0]
|
|
c := s.GetClient().(*api.Client)
|
|
|
|
// Verify game exists
|
|
resp, err := c.GetGame(gameID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.SetCurrentGame(gameID)
|
|
s.SetLastMoveCount(len(resp.Moves))
|
|
s.SetGameState(resp)
|
|
|
|
// Determine player color if authenticated
|
|
if s.GetCurrentUser() != "" {
|
|
if resp.Players.White.ID == s.GetCurrentUser() {
|
|
s.SetPlayerColor("w")
|
|
} else if resp.Players.Black.ID == s.GetCurrentUser() {
|
|
s.SetPlayerColor("b")
|
|
} else {
|
|
s.SetPlayerColor("")
|
|
}
|
|
}
|
|
|
|
display.Printf("%sJoined game: %s%s\n", display.Green, gameID, display.Reset)
|
|
display.Printf("Turn: %s | State: %s | Moves: %d\n", resp.Turn, resp.State, len(resp.Moves))
|
|
|
|
return nil
|
|
}
|
|
|
|
// moveHandler submits a human move. Terminal/error outcomes are reported via
|
|
// the shared printOutcome (no-op on "ongoing"/"pending"); the computer-turn
|
|
// hint stays local since it only applies after a successful human move.
|
|
func moveHandler(s *session.Session, args []string) error {
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("usage: move <uci-move> (promotion appends q, r, b, or n: e7e8q)")
|
|
}
|
|
|
|
gameID := s.CurrentGame
|
|
if gameID == "" {
|
|
return fmt.Errorf("no current game, use 'new' or 'join <gameId>'")
|
|
}
|
|
|
|
move := args[0]
|
|
c := s.Client
|
|
|
|
resp, err := c.MakeMove(gameID, move)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.LastMoveCount = len(resp.Moves)
|
|
s.CurrentGameState = resp
|
|
display.Println(display.Green, "Move accepted")
|
|
|
|
printOutcome(resp)
|
|
|
|
// Hint to trigger the computer if the game continues on a computer's turn
|
|
if resp.State == "ongoing" {
|
|
isComputerTurn := (resp.Turn == "w" && resp.Players.White.Type == 2) ||
|
|
(resp.Turn == "b" && resp.Players.Black.Type == 2)
|
|
if isComputerTurn {
|
|
display.Println(display.Magenta, "\nComputer's turn. Use 'computer' or 'c' to trigger move.")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// computerMoveHandler triggers a computer move and waits for the result via
|
|
// the server's long-poll. With the state-aware waiter, a single poll wakes on
|
|
// either the applied move (move count delta) or a state-only settle
|
|
// (mate-without-move, stuck) — no fixed-interval GET hammering, no hard cap
|
|
// below the server's max searchTime. Polls loop only if the wake races the
|
|
// pending window (e.g. queue wait), each round costing at most WaitTimeout.
|
|
func computerMoveHandler(s *session.Session, args []string) error {
|
|
gameID := s.CurrentGame
|
|
if gameID == "" {
|
|
return fmt.Errorf("no current game, use 'new' or 'join <gameId>'")
|
|
}
|
|
|
|
c := s.Client
|
|
|
|
// Baseline BEFORE triggering: the long-poll returns immediately if the
|
|
// move count already differs from this value.
|
|
baselineMoves := s.LastMoveCount
|
|
if s.CurrentGameState != nil {
|
|
baselineMoves = len(s.CurrentGameState.Moves)
|
|
}
|
|
|
|
resp, err := c.MakeMove(gameID, "cccc")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if resp.State != "pending" {
|
|
// Server resolved synchronously (shouldn't normally happen)
|
|
s.LastMoveCount = len(resp.Moves)
|
|
s.CurrentGameState = resp
|
|
display.Println(display.Green, "Move triggered")
|
|
printOutcome(resp)
|
|
return nil
|
|
}
|
|
|
|
display.Println(display.Magenta, "Computer is thinking...")
|
|
|
|
// Up to 3 long-poll rounds (~90s ceiling) covers max searchTime (10s)
|
|
// plus pathological queue wait, without hanging indefinitely.
|
|
const maxPolls = 3
|
|
var final *api.GameResponse
|
|
for i := 0; i < maxPolls; i++ {
|
|
polled, err := c.GetGameWithPoll(gameID, baselineMoves)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if polled.State != "pending" {
|
|
final = polled
|
|
break
|
|
}
|
|
// Woke on timeout while still pending; poll again.
|
|
}
|
|
if final == nil {
|
|
return fmt.Errorf("computer move still pending after %d poll rounds", maxPolls)
|
|
}
|
|
|
|
s.LastMoveCount = len(final.Moves)
|
|
s.CurrentGameState = final
|
|
|
|
// A move may legitimately be absent: mate-without-move detection or a
|
|
// stuck transition settle the state without applying anything.
|
|
if final.LastMove != nil && len(final.Moves) > baselineMoves {
|
|
display.Print(display.Magenta, "Computer played: %s", final.LastMove.Move)
|
|
if final.LastMove.Depth > 0 {
|
|
display.Printf(" (depth %d, score %d)", final.LastMove.Depth, final.LastMove.Score)
|
|
}
|
|
display.Line()
|
|
}
|
|
|
|
printOutcome(final)
|
|
return nil
|
|
}
|
|
|
|
// printOutcome reports terminal or error states using the server's State
|
|
// ("white wins", "draw", ...) and Termination ("checkmate", "resignation",
|
|
// "threefold_repetition", ...) values, and a pending draw offer.
|
|
func printOutcome(resp *api.GameResponse) {
|
|
switch resp.State {
|
|
case "white wins", "black wins":
|
|
winner := "White"
|
|
if resp.State == "black wins" {
|
|
winner = "Black"
|
|
}
|
|
if resp.Termination == "resignation" {
|
|
display.Println(display.Green, "\n%s wins by resignation.", winner)
|
|
} else {
|
|
display.Println(display.Green, "\nCHECKMATE! %s wins!", winner)
|
|
}
|
|
case "stalemate":
|
|
display.Println(display.Yellow, "\nSTALEMATE! Game drawn.")
|
|
case "draw":
|
|
display.Println(display.Yellow, "\nDRAW %s.", drawReason(resp.Termination))
|
|
case "stuck":
|
|
display.Println(display.Yellow, "\nEngine error — 'undo' to recover, or 'new'/'delete'.")
|
|
}
|
|
if resp.DrawOffer != "" {
|
|
offerer := "White"
|
|
if resp.DrawOffer == "b" {
|
|
offerer = "Black"
|
|
}
|
|
display.Println(display.Magenta, "%s offers a draw: 'draw accept' or 'draw decline'.", offerer)
|
|
}
|
|
}
|
|
|
|
// printConcession notes a resignation or agreed draw that play continued
|
|
// past; the server keeps it on record.
|
|
func printConcession(resp *api.GameResponse) {
|
|
c := resp.Concession
|
|
if c == nil || (c.Result == resp.State && c.Termination == resp.Termination) {
|
|
return
|
|
}
|
|
event := "A draw was agreed"
|
|
switch c.Result {
|
|
case "white wins":
|
|
event = "Black resigned"
|
|
case "black wins":
|
|
event = "White resigned"
|
|
}
|
|
display.Println(display.Yellow, "%s at ply %d; it stays on record while play continues.", event, c.Ply)
|
|
}
|
|
|
|
func drawReason(termination string) string {
|
|
switch termination {
|
|
case "insufficient_material":
|
|
return "by insufficient material"
|
|
case "threefold_repetition":
|
|
return "by threefold repetition"
|
|
case "fifty_move_rule":
|
|
return "by the fifty-move rule"
|
|
case "agreement":
|
|
return "by agreement"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// resignHandler resigns the current game for the given side, or the side the
|
|
// server infers (the one human side, or the one the user claimed).
|
|
func resignHandler(s *session.Session, args []string) error {
|
|
gameID := s.CurrentGame
|
|
if gameID == "" {
|
|
return fmt.Errorf("no current game, use 'new' or 'join <gameId>'")
|
|
}
|
|
color := ""
|
|
if len(args) > 0 {
|
|
color = args[0]
|
|
}
|
|
resp, err := s.Client.Resign(gameID, color)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.LastMoveCount = len(resp.Moves)
|
|
s.CurrentGameState = resp
|
|
printOutcome(resp)
|
|
return nil
|
|
}
|
|
|
|
// drawHandler offers (default), accepts, or declines a draw.
|
|
func drawHandler(s *session.Session, args []string) error {
|
|
gameID := s.CurrentGame
|
|
if gameID == "" {
|
|
return fmt.Errorf("no current game, use 'new' or 'join <gameId>'")
|
|
}
|
|
action, color := "offer", ""
|
|
for _, arg := range args {
|
|
switch arg {
|
|
case "offer", "accept", "decline":
|
|
action = arg
|
|
default:
|
|
color = arg
|
|
}
|
|
}
|
|
resp, err := s.Client.Draw(gameID, action, color)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.LastMoveCount = len(resp.Moves)
|
|
s.CurrentGameState = resp
|
|
switch resp.DrawOutcome {
|
|
case "offered":
|
|
display.Println(display.Green, "Draw offered; it stands until answered or the opponent moves.")
|
|
case "declined":
|
|
display.Println(display.Yellow, "Draw declined.")
|
|
}
|
|
printOutcome(resp)
|
|
return nil
|
|
}
|
|
|
|
func undoHandler(s *session.Session, args []string) error {
|
|
gameID := s.GetCurrentGame()
|
|
if gameID == "" {
|
|
return fmt.Errorf("no current game, use 'new' or 'join <gameId>'")
|
|
}
|
|
|
|
count := 1
|
|
if len(args) > 0 {
|
|
var err error
|
|
count, err = strconv.Atoi(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid count: %s", args[0])
|
|
}
|
|
}
|
|
|
|
c := s.GetClient().(*api.Client)
|
|
resp, err := c.UndoMoves(gameID, count)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.SetLastMoveCount(len(resp.Moves))
|
|
s.SetGameState(resp)
|
|
display.Println(display.Green, "Undid %d move(s)", count)
|
|
printConcession(resp)
|
|
return nil
|
|
}
|
|
|
|
func showBoardHandler(s *session.Session, args []string) error {
|
|
if s.Replay != nil {
|
|
return showReplayPosition(s)
|
|
}
|
|
gameID := s.GetCurrentGame()
|
|
if gameID == "" {
|
|
return fmt.Errorf("no current game, use 'new' or 'join <gameId>'")
|
|
}
|
|
|
|
c := s.GetClient().(*api.Client)
|
|
|
|
// Get full game state
|
|
game, err := c.GetGame(gameID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get ASCII board
|
|
board, err := c.GetBoard(gameID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.SetLastMoveCount(len(game.Moves))
|
|
s.SetGameState(game)
|
|
|
|
// Display board with colors
|
|
display.Line()
|
|
display.RenderBoard(board.Board)
|
|
|
|
// Display game info
|
|
display.Printf("\nFEN: %s\n", game.FEN)
|
|
display.Printf("Turn: %s | State: %s | Moves: %d\n",
|
|
display.ColorForTurn(game.Turn), game.State, len(game.Moves))
|
|
|
|
// Display move history
|
|
if len(game.Moves) > 0 {
|
|
display.Printf("\nHistory: ")
|
|
for i, move := range game.Moves {
|
|
if i > 0 {
|
|
display.Write(" ")
|
|
}
|
|
if i%2 == 0 {
|
|
display.Printf("%d.%s", (i/2)+1, move)
|
|
} else {
|
|
display.Printf(" %s", move)
|
|
}
|
|
}
|
|
display.Line()
|
|
}
|
|
|
|
// Display last move info
|
|
if game.LastMove != nil {
|
|
color := "White"
|
|
if game.LastMove.PlayerColor == "b" {
|
|
color = "Black"
|
|
}
|
|
display.Printf("Last move: %s by %s", game.LastMove.Move, color)
|
|
if game.LastMove.Depth > 0 {
|
|
display.Printf(" (depth %d, score %d)", game.LastMove.Depth, game.LastMove.Score)
|
|
}
|
|
display.Line()
|
|
}
|
|
printOutcome(game)
|
|
printConcession(game)
|
|
|
|
return nil
|
|
}
|
|
|
|
func gameStateHandler(s *session.Session, args []string) error {
|
|
gameID := s.GetCurrentGame()
|
|
if gameID == "" {
|
|
return fmt.Errorf("no current game, use 'new' or 'join <gameId>'")
|
|
}
|
|
|
|
c := s.GetClient().(*api.Client)
|
|
resp, err := c.GetGame(gameID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.SetLastMoveCount(len(resp.Moves))
|
|
|
|
// Pretty print JSON
|
|
display.Println(display.Cyan, "Game State:")
|
|
display.PrettyPrintJSON(resp)
|
|
|
|
return nil
|
|
}
|
|
|
|
func deleteGameHandler(s *session.Session, args []string) error {
|
|
gameID := s.GetCurrentGame()
|
|
if len(args) > 0 {
|
|
gameID = args[0]
|
|
}
|
|
|
|
if gameID == "" {
|
|
return fmt.Errorf("specify game ID or set current game")
|
|
}
|
|
|
|
c := s.GetClient().(*api.Client)
|
|
err := c.DeleteGame(gameID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if gameID == s.GetCurrentGame() {
|
|
s.SetCurrentGame("")
|
|
s.SetLastMoveCount(0)
|
|
}
|
|
|
|
display.Printf("%sLive game unloaded (history retained): %s%s\n", display.Green, gameID, display.Reset)
|
|
return nil
|
|
}
|
|
|
|
func pollHandler(s *session.Session, args []string) error {
|
|
gameID := s.GetCurrentGame()
|
|
if gameID == "" {
|
|
return fmt.Errorf("no current game, use 'new' or 'join <gameId>'")
|
|
}
|
|
|
|
c := s.GetClient().(*api.Client)
|
|
moveCount := s.GetLastMoveCount()
|
|
previous := s.CurrentGameState
|
|
|
|
display.Println(display.Cyan, "Long-polling for updates (move count: %d)...", moveCount)
|
|
display.Println(display.Cyan, "This may take up to 30 seconds")
|
|
|
|
// The server answers at once for a finished game, and wakes a poll for a
|
|
// move, an undo, a result, or a draw offer.
|
|
resp, err := c.GetGameWithPoll(gameID, moveCount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.SetLastMoveCount(len(resp.Moves))
|
|
s.SetGameState(resp)
|
|
|
|
switch {
|
|
case len(resp.Moves) > moveCount:
|
|
display.Println(display.Green, "Game updated! New moves detected")
|
|
if resp.LastMove != nil {
|
|
display.Printf("Last move: %s\n", resp.LastMove.Move)
|
|
}
|
|
case len(resp.Moves) < moveCount:
|
|
display.Println(display.Green, "Game updated: %d move(s) taken back", moveCount-len(resp.Moves))
|
|
case previous == nil || previous.GameID != resp.GameID ||
|
|
previous.State != resp.State || previous.DrawOffer != resp.DrawOffer:
|
|
display.Println(display.Green, "Game updated")
|
|
default:
|
|
display.Println(display.Yellow, "No updates (timeout)")
|
|
}
|
|
printOutcome(resp)
|
|
|
|
return nil
|
|
}
|
|
|
|
// gamesHandler lists the stored games of the signed-in user, 20 at a time;
|
|
// "games more" continues from the previous page.
|
|
func gamesHandler(s *session.Session, args []string) error {
|
|
if s.AuthToken == "" {
|
|
return fmt.Errorf("login required")
|
|
}
|
|
if len(args) > 0 && args[0] == "delete" {
|
|
return gamesDeleteHandler(s, args[1:])
|
|
}
|
|
cursor := ""
|
|
if len(args) > 0 && args[0] == "more" {
|
|
if s.GamesCursor == "" {
|
|
return fmt.Errorf("no further games; run 'games' to list from the newest")
|
|
}
|
|
cursor = s.GamesCursor
|
|
}
|
|
|
|
resp, err := s.Client.GetMyGames(20, cursor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.GamesCursor = resp.NextCursor
|
|
s.GamesPage = resp.Games
|
|
if len(resp.Games) == 0 {
|
|
display.Println(display.Yellow, "No stored games")
|
|
return nil
|
|
}
|
|
|
|
display.Printf("\n%3s %-36s %-20s %-20s %-7s %5s %s\n", "#", "Game ID", "White", "Black", "Result", "Plies", "Started (UTC)")
|
|
for i, g := range resp.Games {
|
|
display.Printf("%3d %-36s %-20s %-20s %-7s %5d %s\n", i+1, g.GameID,
|
|
playerLabel(g.Players.White), playerLabel(g.Players.Black), g.PGNResult, g.MoveCount,
|
|
g.StartTimeUTC.UTC().Format("2006-01-02 15:04"))
|
|
}
|
|
display.Println(display.Cyan, "\nReplay one: replay <#> or replay <gameId>")
|
|
if resp.NextCursor != "" {
|
|
display.Println(display.Cyan, "More games: games more")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// gamesDeleteHandler removes a game from the user's list by its number in
|
|
// the last listing or its ID. A game another player also claims is only
|
|
// released; one the user alone holds is deleted with its moves.
|
|
func gamesDeleteHandler(s *session.Session, args []string) error {
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("usage: games delete <#|gameId>")
|
|
}
|
|
gameID := args[0]
|
|
if index, err := strconv.Atoi(gameID); err == nil {
|
|
if index < 1 || index > len(s.GamesPage) {
|
|
return fmt.Errorf("index %d is outside the last listing (1-%d)", index, len(s.GamesPage))
|
|
}
|
|
gameID = s.GamesPage[index-1].GameID
|
|
}
|
|
resp, err := s.Client.RemoveMyGame(gameID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.Removed == "claim" {
|
|
display.Println(display.Yellow, "Released your side of %s; the other player keeps the game", gameID)
|
|
} else {
|
|
display.Println(display.Green, "Deleted game %s", gameID)
|
|
}
|
|
if s.Replay != nil && s.Replay.History.GameID == gameID {
|
|
s.Replay = nil
|
|
}
|
|
if s.CurrentGame == gameID && resp.Removed == "game" {
|
|
s.CurrentGame = ""
|
|
s.CurrentGameState = nil
|
|
s.LastMoveCount = 0
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// importHandler stores a PGN file (one game) under the user's account and
|
|
// makes it the replay.
|
|
func importHandler(s *session.Session, args []string) error {
|
|
if s.AuthToken == "" {
|
|
return fmt.Errorf("login required")
|
|
}
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("usage: import <path>")
|
|
}
|
|
text, err := readImportFile(args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := s.Client.ImportGame(text)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
display.Println(display.Green, "Imported game %s (%d plies, %s)", resp.GameID, resp.MoveCount, resp.PGNResult)
|
|
display.Println(display.Cyan, "Replay it: replay %s", resp.GameID)
|
|
return nil
|
|
}
|
|
|
|
func playerLabel(p api.PlayerInfo) string {
|
|
switch {
|
|
case p.Type == 2:
|
|
return fmt.Sprintf("Stockfish L%d", p.Level)
|
|
case p.Name != "":
|
|
return p.Name
|
|
}
|
|
return "anonymous"
|
|
}
|
|
|
|
// pgnHandler prints the stored game as PGN: the current game unless an ID is
|
|
// given, and every ply unless a count is given. During a replay the default
|
|
// is the replayed game up to the current ply. "save <path>" writes the
|
|
// export to a file instead of printing it.
|
|
func pgnHandler(s *session.Session, args []string) error {
|
|
path, args, err := pgnSaveArgs(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gameID, ply := s.CurrentGame, -1
|
|
if s.Replay != nil {
|
|
gameID = s.Replay.History.GameID
|
|
if !s.Replay.AtEnd() {
|
|
ply = s.Replay.Ply
|
|
}
|
|
}
|
|
for _, arg := range args {
|
|
if n, err := strconv.Atoi(arg); err == nil && n >= 0 {
|
|
ply = n
|
|
} else {
|
|
gameID = arg
|
|
}
|
|
}
|
|
if gameID == "" {
|
|
return fmt.Errorf("no current game, use 'join <gameId>' or 'pgn <gameId>'")
|
|
}
|
|
text, err := s.Client.GetGamePGN(gameID, ply)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if path != "" {
|
|
if err := savePGN(path, trimPGN(text)); err != nil {
|
|
return err
|
|
}
|
|
display.Println(display.Green, "Saved PGN of %s to %s", describePGNTarget(gameID, ply), path)
|
|
return nil
|
|
}
|
|
display.Line()
|
|
display.Write(text)
|
|
return nil
|
|
}
|