v0.7.0 cli client with readline added, directory structure updated
This commit is contained in:
241
internal/client/api/client.go
Normal file
241
internal/client/api/client.go
Normal file
@ -0,0 +1,241 @@
|
||||
// FILE: lixenwraith/chess/internal/api/client.go
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"chess/internal/client/display"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
AuthToken string
|
||||
HTTPClient *http.Client
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func New(baseURL string) *Client {
|
||||
return &Client{
|
||||
BaseURL: baseURL,
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SetVerbose(v bool) {
|
||||
c.Verbose = v
|
||||
}
|
||||
|
||||
// SetBaseURL updates the API base URL for the client
|
||||
func (c *Client) SetBaseURL(url string) {
|
||||
c.BaseURL = strings.TrimRight(url, "/")
|
||||
}
|
||||
|
||||
func (c *Client) SetToken(token string) {
|
||||
c.AuthToken = token
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(method, path string, body interface{}, result interface{}) error {
|
||||
url := c.BaseURL + path
|
||||
|
||||
// Prepare body
|
||||
var bodyReader io.Reader
|
||||
var bodyStr string
|
||||
if body != nil {
|
||||
jsonData, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bodyReader = bytes.NewReader(jsonData)
|
||||
bodyStr = string(jsonData)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest(method, url, bodyReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set headers
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if c.AuthToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.AuthToken)
|
||||
}
|
||||
|
||||
// Display request
|
||||
fmt.Printf("\n%s[API] %s %s%s\n", display.Blue, method, path, display.Reset)
|
||||
if bodyStr != "" {
|
||||
if c.Verbose {
|
||||
// Display request body if verbose
|
||||
var prettyBody interface{}
|
||||
json.Unmarshal([]byte(bodyStr), &prettyBody)
|
||||
prettyJSON, _ := json.MarshalIndent(prettyBody, "", " ")
|
||||
fmt.Printf("%sRequest Body:%s\n%s\n", display.Cyan, display.Reset, string(prettyJSON))
|
||||
} else {
|
||||
fmt.Printf("%s%s%s\n", display.Blue, bodyStr, display.Reset)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute request
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("%s[ERROR] %s%s\n", display.Red, err.Error(), display.Reset)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read response
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Display response
|
||||
statusColor := display.Green
|
||||
if resp.StatusCode >= 400 {
|
||||
statusColor = display.Red
|
||||
}
|
||||
fmt.Printf("%s[%d %s]%s\n", statusColor, resp.StatusCode, http.StatusText(resp.StatusCode), display.Reset)
|
||||
|
||||
// Display response body if verbose
|
||||
if c.Verbose && len(respBody) > 0 {
|
||||
var prettyResp interface{}
|
||||
if err := json.Unmarshal(respBody, &prettyResp); err == nil {
|
||||
prettyJSON, _ := json.MarshalIndent(prettyResp, "", " ")
|
||||
fmt.Printf("%sResponse Body:%s\n%s\n", display.Cyan, display.Reset, string(prettyJSON))
|
||||
} else {
|
||||
fmt.Printf("%sResponse:%s\n%s\n", display.Cyan, display.Reset, string(respBody))
|
||||
}
|
||||
}
|
||||
|
||||
// Parse error response
|
||||
if resp.StatusCode >= 400 {
|
||||
var errResp ErrorResponse
|
||||
if err := json.Unmarshal(respBody, &errResp); err == nil {
|
||||
if !c.Verbose {
|
||||
fmt.Printf("%sError: %s%s\n", display.Red, errResp.Error, display.Reset)
|
||||
if errResp.Code != "" {
|
||||
fmt.Printf("%sCode: %s%s\n", display.Red, errResp.Code, display.Reset)
|
||||
}
|
||||
if errResp.Details != "" {
|
||||
fmt.Printf("%sDetails: %s%s\n", display.Red, errResp.Details, display.Reset)
|
||||
}
|
||||
}
|
||||
} else if !c.Verbose {
|
||||
fmt.Printf("%s%s%s\n", display.Red, string(respBody), display.Reset)
|
||||
}
|
||||
return fmt.Errorf("request failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Parse success response
|
||||
if result != nil && len(respBody) > 0 {
|
||||
if err := json.Unmarshal(respBody, result); err != nil {
|
||||
// For debug, show raw response if parsing fails
|
||||
fmt.Printf("%sResponse parse error: %s%s\n", display.Red, err.Error(), display.Reset)
|
||||
fmt.Printf("%sRaw response: %s%s\n", display.Green, string(respBody), display.Reset)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// API Methods
|
||||
|
||||
func (c *Client) Health() (*HealthResponse, error) {
|
||||
var resp HealthResponse
|
||||
err := c.doRequest("GET", "/health", nil, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func (c *Client) CreateGame(req *CreateGameRequest) (*GameResponse, error) {
|
||||
var resp GameResponse
|
||||
err := c.doRequest("POST", "/api/v1/games", req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func (c *Client) GetGame(gameID string) (*GameResponse, error) {
|
||||
var resp GameResponse
|
||||
err := c.doRequest("GET", "/api/v1/games/"+gameID, nil, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func (c *Client) GetGameWithPoll(gameID string, moveCount int) (*GameResponse, error) {
|
||||
var resp GameResponse
|
||||
path := fmt.Sprintf("/api/v1/games/%s?wait=true&moveCount=%d", gameID, moveCount)
|
||||
err := c.doRequest("GET", path, nil, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func (c *Client) DeleteGame(gameID string) error {
|
||||
return c.doRequest("DELETE", "/api/v1/games/"+gameID, nil, nil)
|
||||
}
|
||||
|
||||
func (c *Client) MakeMove(gameID string, move string) (*GameResponse, error) {
|
||||
req := &MoveRequest{Move: move}
|
||||
var resp GameResponse
|
||||
err := c.doRequest("POST", "/api/v1/games/"+gameID+"/moves", req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func (c *Client) UndoMoves(gameID string, count int) (*GameResponse, error) {
|
||||
req := &UndoRequest{Count: count}
|
||||
var resp GameResponse
|
||||
err := c.doRequest("POST", "/api/v1/games/"+gameID+"/undo", req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func (c *Client) GetBoard(gameID string) (*BoardResponse, error) {
|
||||
var resp BoardResponse
|
||||
err := c.doRequest("GET", "/api/v1/games/"+gameID+"/board", nil, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func (c *Client) Register(username, password, email string) (*AuthResponse, error) {
|
||||
req := &RegisterRequest{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Email: email,
|
||||
}
|
||||
var resp AuthResponse
|
||||
err := c.doRequest("POST", "/api/v1/auth/register", req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func (c *Client) Login(identifier, password string) (*AuthResponse, error) {
|
||||
req := &LoginRequest{
|
||||
Identifier: identifier,
|
||||
Password: password,
|
||||
}
|
||||
var resp AuthResponse
|
||||
err := c.doRequest("POST", "/api/v1/auth/login", req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func (c *Client) GetCurrentUser() (*UserResponse, error) {
|
||||
var resp UserResponse
|
||||
err := c.doRequest("GET", "/api/v1/auth/me", nil, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
// RawRequest performs a raw HTTP request for debugging purposes
|
||||
func (c *Client) RawRequest(method, path string, body string) error {
|
||||
var bodyData interface{}
|
||||
if body != "" {
|
||||
if err := json.Unmarshal([]byte(body), &bodyData); err != nil {
|
||||
// Try as raw string
|
||||
bodyData = body
|
||||
}
|
||||
}
|
||||
|
||||
return c.doRequest(method, path, bodyData, nil)
|
||||
}
|
||||
97
internal/client/api/types.go
Normal file
97
internal/client/api/types.go
Normal file
@ -0,0 +1,97 @@
|
||||
// FILE: lixenwraith/chess/internal/client/api/types.go
|
||||
package api
|
||||
|
||||
import "time"
|
||||
|
||||
// Request types
|
||||
type CreateGameRequest struct {
|
||||
White PlayerConfig `json:"white"`
|
||||
Black PlayerConfig `json:"black"`
|
||||
FEN string `json:"fen,omitempty"`
|
||||
}
|
||||
|
||||
type PlayerConfig struct {
|
||||
Type int `json:"type"` // 1=human, 2=computer
|
||||
Level int `json:"level,omitempty"`
|
||||
SearchTime int `json:"searchTime,omitempty"`
|
||||
}
|
||||
|
||||
type MoveRequest struct {
|
||||
Move string `json:"move"`
|
||||
}
|
||||
|
||||
type UndoRequest struct {
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Identifier string `json:"identifier"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// Response types
|
||||
type GameResponse struct {
|
||||
GameID string `json:"gameId"`
|
||||
FEN string `json:"fen"`
|
||||
Turn string `json:"turn"`
|
||||
State string `json:"state"`
|
||||
Moves []string `json:"moves"`
|
||||
Players PlayersResponse `json:"players"`
|
||||
LastMove *MoveInfo `json:"lastMove,omitempty"`
|
||||
}
|
||||
|
||||
type PlayersResponse struct {
|
||||
White PlayerInfo `json:"white"`
|
||||
Black PlayerInfo `json:"black"`
|
||||
}
|
||||
|
||||
type PlayerInfo struct {
|
||||
ID string `json:"id"`
|
||||
Type int `json:"type"`
|
||||
Level int `json:"level,omitempty"`
|
||||
SearchTime int `json:"searchTime,omitempty"`
|
||||
}
|
||||
|
||||
type MoveInfo struct {
|
||||
Move string `json:"move"`
|
||||
PlayerColor string `json:"playerColor"`
|
||||
Score int `json:"score,omitempty"`
|
||||
Depth int `json:"depth,omitempty"`
|
||||
}
|
||||
|
||||
type BoardResponse struct {
|
||||
FEN string `json:"fen"`
|
||||
Board string `json:"board"`
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
UserID string `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
UserID string `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
LastLogin *time.Time `json:"lastLoginAt,omitempty"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Code string `json:"code"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
Time int64 `json:"time"`
|
||||
Storage string `json:"storage,omitempty"`
|
||||
}
|
||||
181
internal/client/commands/auth.go
Normal file
181
internal/client/commands/auth.go
Normal file
@ -0,0 +1,181 @@
|
||||
// FILE: lixenwraith/chess/internal/client/commands/auth.go
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"chess/internal/client/api"
|
||||
"chess/internal/client/display"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func (r *Registry) registerAuthCommands() {
|
||||
r.Register(&Command{
|
||||
Name: "register",
|
||||
ShortName: "r",
|
||||
Description: "Register a new user",
|
||||
Usage: "register",
|
||||
Handler: registerHandler,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
Name: "login",
|
||||
ShortName: "l",
|
||||
Description: "Login with credentials",
|
||||
Usage: "login",
|
||||
Handler: loginHandler,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
Name: "logout",
|
||||
ShortName: "o",
|
||||
Description: "Clear authentication",
|
||||
Usage: "logout",
|
||||
Handler: logoutHandler,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
Name: "whoami",
|
||||
ShortName: "i",
|
||||
Description: "Show current user",
|
||||
Usage: "whoami",
|
||||
Handler: whoamiHandler,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
Name: "user",
|
||||
ShortName: "e",
|
||||
Description: "Set user ID manually",
|
||||
Usage: "user <userId>",
|
||||
Handler: setUserHandler,
|
||||
})
|
||||
}
|
||||
|
||||
func readPassword(prompt string) (string, error) {
|
||||
fmt.Print(prompt)
|
||||
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytePassword), nil
|
||||
}
|
||||
|
||||
func registerHandler(s Session, args []string) error {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
c := s.GetClient().(*api.Client)
|
||||
|
||||
fmt.Print(display.Yellow + "Username: " + display.Reset)
|
||||
scanner.Scan()
|
||||
username := strings.TrimSpace(scanner.Text())
|
||||
|
||||
password, err := readPassword(display.Yellow + "Password: " + display.Reset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Print(display.Yellow + "Email (optional): " + display.Reset)
|
||||
scanner.Scan()
|
||||
email := strings.TrimSpace(scanner.Text())
|
||||
|
||||
resp, err := c.Register(username, password, email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.SetAuthToken(resp.Token)
|
||||
s.SetCurrentUser(resp.UserID)
|
||||
s.SetUsername(resp.Username)
|
||||
c.SetToken(resp.Token)
|
||||
|
||||
fmt.Printf("%sRegistered successfully%s\n", display.Green, display.Reset)
|
||||
fmt.Printf("User ID: %s\n", resp.UserID)
|
||||
fmt.Printf("Username: %s\n", resp.Username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loginHandler(s Session, args []string) error {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
c := s.GetClient().(*api.Client)
|
||||
|
||||
fmt.Print(display.Yellow + "Username or Email: " + display.Reset)
|
||||
scanner.Scan()
|
||||
identifier := strings.TrimSpace(scanner.Text())
|
||||
|
||||
password, err := readPassword(display.Yellow + "Password: " + display.Reset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.Login(identifier, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.SetAuthToken(resp.Token)
|
||||
s.SetCurrentUser(resp.UserID)
|
||||
s.SetUsername(resp.Username)
|
||||
c.SetToken(resp.Token)
|
||||
|
||||
fmt.Printf("%sLogged in successfully%s\n", display.Green, display.Reset)
|
||||
fmt.Printf("User ID: %s\n", resp.UserID)
|
||||
fmt.Printf("Username: %s\n", resp.Username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func logoutHandler(s Session, args []string) error {
|
||||
s.SetAuthToken("")
|
||||
s.SetCurrentUser("")
|
||||
s.SetUsername("")
|
||||
c := s.GetClient().(*api.Client)
|
||||
c.SetToken("")
|
||||
|
||||
fmt.Printf("%sLogged out%s\n", display.Green, display.Reset)
|
||||
return nil
|
||||
}
|
||||
|
||||
func whoamiHandler(s Session, args []string) error {
|
||||
if s.GetAuthToken() == "" {
|
||||
fmt.Printf("%sNot authenticated%s\n", display.Yellow, display.Reset)
|
||||
return nil
|
||||
}
|
||||
|
||||
c := s.GetClient().(*api.Client)
|
||||
user, err := c.GetCurrentUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%sCurrent User:%s\n", display.Cyan, display.Reset)
|
||||
fmt.Printf(" User ID: %s\n", user.UserID)
|
||||
fmt.Printf(" Username: %s\n", user.Username)
|
||||
if user.Email != "" {
|
||||
fmt.Printf(" Email: %s\n", user.Email)
|
||||
}
|
||||
fmt.Printf(" Created: %s\n", user.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||
if user.LastLogin != nil {
|
||||
fmt.Printf(" Last Login: %s\n", user.LastLogin.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setUserHandler(s Session, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("usage: user <userId>")
|
||||
}
|
||||
|
||||
userID := args[0]
|
||||
s.SetCurrentUser(userID)
|
||||
fmt.Printf("%sUser ID set to: %s%s\n", display.Cyan, userID, display.Reset)
|
||||
fmt.Println("Note: This doesn't authenticate, just sets the ID for display")
|
||||
|
||||
return nil
|
||||
}
|
||||
108
internal/client/commands/debug.go
Normal file
108
internal/client/commands/debug.go
Normal file
@ -0,0 +1,108 @@
|
||||
// FILE: lixenwraith/chess/internal/client/commands/debug.go
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"chess/internal/client/api"
|
||||
"chess/internal/client/display"
|
||||
)
|
||||
|
||||
func (r *Registry) registerDebugCommands() {
|
||||
r.Register(&Command{
|
||||
Name: "health",
|
||||
ShortName: ".",
|
||||
Description: "Check server health",
|
||||
Usage: "health",
|
||||
Handler: healthHandler,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
Name: "url",
|
||||
ShortName: "/",
|
||||
Description: "Set API base URL",
|
||||
Usage: "url [apiUrl]",
|
||||
Handler: urlHandler,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
Name: "raw",
|
||||
ShortName: ":",
|
||||
Description: "Send raw API request",
|
||||
Usage: "raw <method> <path> [json-body]",
|
||||
Handler: rawRequestHandler,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
Name: "clear",
|
||||
ShortName: "-",
|
||||
Description: "Clear screen",
|
||||
Usage: "clear",
|
||||
Handler: clearHandler,
|
||||
})
|
||||
}
|
||||
|
||||
func healthHandler(s Session, args []string) error {
|
||||
c := s.GetClient().(*api.Client)
|
||||
resp, err := c.Health()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%sServer Health:%s\n", display.Cyan, display.Reset)
|
||||
fmt.Printf(" Status: %s\n", resp.Status)
|
||||
// Convert Unix timestamp to readable time
|
||||
t := time.Unix(resp.Time, 0)
|
||||
fmt.Printf(" Time: %s\n", t.Format("2006-01-02 15:04:05"))
|
||||
if resp.Storage != "" {
|
||||
fmt.Printf(" Storage: %s\n", resp.Storage)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func urlHandler(s Session, args []string) error {
|
||||
if len(args) == 0 {
|
||||
fmt.Printf("Current API URL: %s\n", s.GetAPIBaseURL())
|
||||
return nil
|
||||
}
|
||||
|
||||
url := args[0]
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
url = "http://" + url
|
||||
}
|
||||
|
||||
s.SetAPIBaseURL(url)
|
||||
c := s.GetClient().(*api.Client)
|
||||
c.SetBaseURL(url)
|
||||
|
||||
fmt.Printf("%sAPI URL set to: %s%s\n", display.Cyan, url, display.Reset)
|
||||
return nil
|
||||
}
|
||||
|
||||
func rawRequestHandler(s Session, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: raw <method> <path> [json-body]")
|
||||
}
|
||||
|
||||
method := strings.ToUpper(args[0])
|
||||
path := args[1]
|
||||
|
||||
body := ""
|
||||
if len(args) > 2 {
|
||||
body = strings.Join(args[2:], " ")
|
||||
}
|
||||
|
||||
c := s.GetClient().(*api.Client)
|
||||
return c.RawRequest(method, path, body)
|
||||
}
|
||||
|
||||
func clearHandler(s Session, args []string) error {
|
||||
cmd := exec.Command("clear")
|
||||
cmd.Stdout = os.Stdout
|
||||
return cmd.Run()
|
||||
}
|
||||
511
internal/client/commands/game.go
Normal file
511
internal/client/commands/game.go
Normal file
@ -0,0 +1,511 @@
|
||||
// FILE: lixenwraith/chess/internal/client/commands/game.go
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"chess/internal/client/api"
|
||||
"chess/internal/client/display"
|
||||
)
|
||||
|
||||
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>",
|
||||
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,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
Name: "state",
|
||||
ShortName: "s",
|
||||
Description: "Show raw game JSON",
|
||||
Usage: "state",
|
||||
Handler: gameStateHandler,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
Name: "delete",
|
||||
ShortName: "d",
|
||||
Description: "Delete a game",
|
||||
Usage: "delete [gameId]",
|
||||
Handler: deleteGameHandler,
|
||||
})
|
||||
|
||||
r.Register(&Command{
|
||||
Name: "poll",
|
||||
ShortName: "p",
|
||||
Description: "Long-poll for game updates",
|
||||
Usage: "poll",
|
||||
Handler: pollHandler,
|
||||
})
|
||||
}
|
||||
|
||||
func newGameHandler(s Session, args []string) error {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
c := s.GetClient().(*api.Client)
|
||||
|
||||
fmt.Println("\n" + display.Cyan + "Creating new game..." + display.Reset)
|
||||
|
||||
// White player
|
||||
fmt.Print(display.Yellow + "White player type (h/c) [h]: " + display.Reset)
|
||||
scanner.Scan()
|
||||
whiteType := strings.ToLower(strings.TrimSpace(scanner.Text()))
|
||||
if whiteType == "" {
|
||||
whiteType = "h"
|
||||
}
|
||||
|
||||
white := api.PlayerConfig{Type: 1}
|
||||
if whiteType == "c" {
|
||||
white.Type = 2
|
||||
|
||||
fmt.Print(display.Yellow + "Computer level (0-20) [10]: " + display.Reset)
|
||||
scanner.Scan()
|
||||
levelStr := strings.TrimSpace(scanner.Text())
|
||||
if levelStr == "" {
|
||||
white.Level = 10
|
||||
} else {
|
||||
level, _ := strconv.Atoi(levelStr)
|
||||
white.Level = level
|
||||
}
|
||||
|
||||
fmt.Print(display.Yellow + "Search time (100-10000ms) [1000]: " + display.Reset)
|
||||
scanner.Scan()
|
||||
timeStr := strings.TrimSpace(scanner.Text())
|
||||
if timeStr == "" {
|
||||
white.SearchTime = 1000
|
||||
} else {
|
||||
searchTime, _ := strconv.Atoi(timeStr)
|
||||
white.SearchTime = searchTime
|
||||
}
|
||||
}
|
||||
|
||||
// Black player
|
||||
fmt.Print(display.Yellow + "Black player type (h/c) [h]: " + display.Reset)
|
||||
scanner.Scan()
|
||||
blackType := strings.ToLower(strings.TrimSpace(scanner.Text()))
|
||||
if blackType == "" {
|
||||
blackType = "h"
|
||||
}
|
||||
|
||||
black := api.PlayerConfig{Type: 1}
|
||||
if blackType == "c" {
|
||||
black.Type = 2
|
||||
|
||||
fmt.Print(display.Yellow + "Computer level (0-20) [10]: " + display.Reset)
|
||||
scanner.Scan()
|
||||
levelStr := strings.TrimSpace(scanner.Text())
|
||||
if levelStr == "" {
|
||||
black.Level = 10
|
||||
} else {
|
||||
level, _ := strconv.Atoi(levelStr)
|
||||
black.Level = level
|
||||
}
|
||||
|
||||
fmt.Print(display.Yellow + "Search time (100-10000ms) [1000]: " + display.Reset)
|
||||
scanner.Scan()
|
||||
timeStr := strings.TrimSpace(scanner.Text())
|
||||
if timeStr == "" {
|
||||
black.SearchTime = 1000
|
||||
} else {
|
||||
searchTime, _ := strconv.Atoi(timeStr)
|
||||
black.SearchTime = searchTime
|
||||
}
|
||||
}
|
||||
|
||||
// Starting position
|
||||
fmt.Print(display.Yellow + "Starting position (FEN) [default]: " + display.Reset)
|
||||
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.SetCurrentGame(resp.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")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("%sGame created: %s%s\n", display.Green, resp.GameID, display.Reset)
|
||||
fmt.Printf("%sCurrent game set to: %s%s\n", display.Cyan, resp.GameID, display.Reset)
|
||||
|
||||
// If white is computer, trigger first move
|
||||
if white.Type == 2 {
|
||||
fmt.Printf("\n%sTriggering white computer move...%s\n", display.Magenta, display.Reset)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, err = c.MakeMove(resp.GameID, "cccc")
|
||||
if err != nil {
|
||||
fmt.Printf("%sFailed to trigger computer move: %s%s\n", display.Red, err.Error(), display.Reset)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func joinGameHandler(s 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("")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("%sJoined game: %s%s\n", display.Green, gameID, display.Reset)
|
||||
fmt.Printf("Turn: %s | State: %s | Moves: %d\n", resp.Turn, resp.State, len(resp.Moves))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func moveHandler(s Session, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("usage: move <uci-move>")
|
||||
}
|
||||
|
||||
gameID := s.GetCurrentGame()
|
||||
if gameID == "" {
|
||||
return fmt.Errorf("no current game, use 'new' or 'join <gameId>'")
|
||||
}
|
||||
|
||||
move := args[0]
|
||||
c := s.GetClient().(*api.Client)
|
||||
|
||||
resp, err := c.MakeMove(gameID, move)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.SetLastMoveCount(len(resp.Moves))
|
||||
s.SetGameState(resp)
|
||||
fmt.Printf("%sMove accepted%s\n", display.Green, display.Reset)
|
||||
|
||||
// Check if computer should move
|
||||
currentTurn := resp.Turn
|
||||
var computerPlayer *api.PlayerInfo
|
||||
if currentTurn == "w" && resp.Players.White.Type == 2 {
|
||||
computerPlayer = &resp.Players.White
|
||||
} else if currentTurn == "b" && resp.Players.Black.Type == 2 {
|
||||
computerPlayer = &resp.Players.Black
|
||||
}
|
||||
|
||||
if computerPlayer != nil && resp.State == "ongoing" {
|
||||
fmt.Printf("\n%sComputer's turn, triggering move...%s\n", display.Magenta, display.Reset)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
resp2, err := c.MakeMove(gameID, "cccc")
|
||||
if err != nil {
|
||||
fmt.Printf("%sFailed to trigger computer move: %s%s\n", display.Red, err.Error(), display.Reset)
|
||||
} else if resp2.State == "pending" {
|
||||
fmt.Printf("%sComputer is thinking...%s\n", display.Magenta, display.Reset)
|
||||
// Wait for completion
|
||||
for i := 0; i < 50; i++ {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
resp3, err := c.GetGame(gameID)
|
||||
if err == nil && resp3.State != "pending" {
|
||||
s.SetLastMoveCount(len(resp3.Moves))
|
||||
if resp3.LastMove != nil {
|
||||
fmt.Printf("%sComputer played: %s%s", display.Magenta, resp3.LastMove.Move, display.Reset)
|
||||
if resp3.LastMove.Depth > 0 {
|
||||
fmt.Printf(" (depth %d, score %d)", resp3.LastMove.Depth, resp3.LastMove.Score)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func computerMoveHandler(s 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.MakeMove(gameID, "cccc")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.State == "pending" {
|
||||
fmt.Printf("%sComputer is thinking...%s\n", display.Magenta, display.Reset)
|
||||
|
||||
// Poll for completion
|
||||
for i := 0; i < 50; i++ {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
resp2, err := c.GetGame(gameID)
|
||||
if err == nil && resp2.State != "pending" {
|
||||
s.SetLastMoveCount(len(resp2.Moves))
|
||||
s.SetGameState(resp2)
|
||||
if resp2.LastMove != nil {
|
||||
fmt.Printf("%sComputer played: %s%s", display.Magenta, resp2.LastMove.Move, display.Reset)
|
||||
if resp2.LastMove.Depth > 0 {
|
||||
fmt.Printf(" (depth %d, score %d)", resp2.LastMove.Depth, resp2.LastMove.Score)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("timeout waiting for computer move")
|
||||
}
|
||||
|
||||
s.SetLastMoveCount(len(resp.Moves))
|
||||
fmt.Printf("%sMove triggered%s\n", display.Green, display.Reset)
|
||||
return nil
|
||||
}
|
||||
|
||||
func undoHandler(s 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))
|
||||
fmt.Printf("%sUndid %d move(s)%s\n", display.Green, count, display.Reset)
|
||||
return nil
|
||||
}
|
||||
|
||||
func showBoardHandler(s 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)
|
||||
|
||||
// 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
|
||||
fmt.Println()
|
||||
display.RenderBoard(board.Board)
|
||||
|
||||
// Display game info
|
||||
fmt.Printf("\nFEN: %s\n", game.FEN)
|
||||
fmt.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 {
|
||||
fmt.Printf("\nHistory: ")
|
||||
for i, move := range game.Moves {
|
||||
if i > 0 {
|
||||
fmt.Print(" ")
|
||||
}
|
||||
if i%2 == 0 {
|
||||
fmt.Printf("%d.%s", (i/2)+1, move)
|
||||
} else {
|
||||
fmt.Printf(" %s", move)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Display last move info
|
||||
if game.LastMove != nil {
|
||||
color := "White"
|
||||
if game.LastMove.PlayerColor == "b" {
|
||||
color = "Black"
|
||||
}
|
||||
fmt.Printf("Last move: %s by %s", game.LastMove.Move, color)
|
||||
if game.LastMove.Depth > 0 {
|
||||
fmt.Printf(" (depth %d, score %d)", game.LastMove.Depth, game.LastMove.Score)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gameStateHandler(s 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
|
||||
fmt.Printf("%sGame State:%s\n", display.Cyan, display.Reset)
|
||||
display.PrettyPrintJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteGameHandler(s 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)
|
||||
}
|
||||
|
||||
fmt.Printf("%sGame deleted: %s%s\n", display.Green, gameID, display.Reset)
|
||||
return nil
|
||||
}
|
||||
|
||||
func pollHandler(s 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()
|
||||
|
||||
fmt.Printf("%sLong-polling for updates (move count: %d)...%s\n",
|
||||
display.Cyan, moveCount, display.Reset)
|
||||
fmt.Printf("%sThis may take up to 25 seconds%s\n", display.Cyan, display.Reset)
|
||||
|
||||
resp, err := c.GetGameWithPoll(gameID, moveCount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.SetLastMoveCount(len(resp.Moves))
|
||||
s.SetGameState(resp)
|
||||
|
||||
if len(resp.Moves) > moveCount {
|
||||
fmt.Printf("%sGame updated! New moves detected%s\n", display.Green, display.Reset)
|
||||
if resp.LastMove != nil {
|
||||
fmt.Printf("Last move: %s\n", resp.LastMove.Move)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("%sNo updates (timeout)%s\n", display.Yellow, display.Reset)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
194
internal/client/commands/registry.go
Normal file
194
internal/client/commands/registry.go
Normal file
@ -0,0 +1,194 @@
|
||||
// FILE: lixenwraith/chess/internal/client/commands/registry.go
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"chess/internal/client/api"
|
||||
"chess/internal/client/display"
|
||||
)
|
||||
|
||||
type Session interface {
|
||||
GetAPIBaseURL() string
|
||||
SetAPIBaseURL(string)
|
||||
GetCurrentGame() string
|
||||
SetCurrentGame(string)
|
||||
GetCurrentUser() string
|
||||
SetCurrentUser(string)
|
||||
GetAuthToken() string
|
||||
SetAuthToken(string)
|
||||
GetUsername() string
|
||||
SetUsername(string)
|
||||
GetLastMoveCount() int
|
||||
SetLastMoveCount(int)
|
||||
GetClient() interface{}
|
||||
IsVerbose() bool
|
||||
SetGameState(interface{})
|
||||
SetPlayerColor(string)
|
||||
GetPlayerColor() string
|
||||
}
|
||||
|
||||
// Command defines a client command with its handler
|
||||
type Command struct {
|
||||
Name string
|
||||
ShortName string
|
||||
Description string
|
||||
Usage string
|
||||
Handler func(Session, []string) error
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
session Session
|
||||
commands map[string]*Command
|
||||
}
|
||||
|
||||
// Registry manages command registration and execution
|
||||
func NewRegistry(session Session) *Registry {
|
||||
r := &Registry{
|
||||
session: session,
|
||||
commands: make(map[string]*Command),
|
||||
}
|
||||
|
||||
// Register all commands
|
||||
r.registerGameCommands()
|
||||
r.registerAuthCommands()
|
||||
r.registerDebugCommands()
|
||||
|
||||
// Help command
|
||||
r.Register(&Command{
|
||||
Name: "help",
|
||||
ShortName: "?",
|
||||
Description: "Show available commands",
|
||||
Usage: "help [command]",
|
||||
Handler: r.helpHandler,
|
||||
})
|
||||
|
||||
// Exit command
|
||||
r.Register(&Command{
|
||||
Name: "exit",
|
||||
ShortName: "x",
|
||||
Description: "Exit the client",
|
||||
Usage: "exit",
|
||||
Handler: exitHandler,
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Registry) Register(cmd *Command) {
|
||||
r.commands[cmd.Name] = cmd
|
||||
if cmd.ShortName != "" {
|
||||
r.commands[cmd.ShortName] = cmd
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) Execute(input string) {
|
||||
parts := strings.Fields(input)
|
||||
if len(parts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cmdName := parts[0]
|
||||
args := parts[1:]
|
||||
|
||||
cmd, exists := r.commands[cmdName]
|
||||
if !exists {
|
||||
fmt.Printf("%sUnknown command: %s%s\n", display.Red, cmdName, display.Reset)
|
||||
fmt.Printf("Type 'help' for available commands\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Set verbose mode in client if session supports it
|
||||
if cl, ok := r.session.GetClient().(*api.Client); ok {
|
||||
cl.SetVerbose(r.session.IsVerbose())
|
||||
}
|
||||
|
||||
if err := cmd.Handler(r.session, args); err != nil {
|
||||
fmt.Printf("%sError: %s%s\n", display.Red, err.Error(), display.Reset)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) helpHandler(s Session, args []string) error {
|
||||
if len(args) > 0 {
|
||||
// Show help for specific command
|
||||
cmd, exists := r.commands[args[0]]
|
||||
if !exists {
|
||||
return fmt.Errorf("unknown command: %s", args[0])
|
||||
}
|
||||
fmt.Printf("\n%s%s%s - %s\n", display.Cyan, cmd.Name, display.Reset, cmd.Description)
|
||||
if cmd.ShortName != "" {
|
||||
fmt.Printf("Short form: %s%s%s\n", display.Cyan, cmd.ShortName, display.Reset)
|
||||
}
|
||||
fmt.Printf("Usage: %s\n", cmd.Usage)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Show all commands
|
||||
fmt.Printf("\n%sAvailable Commands:%s\n\n", display.Cyan, display.Reset)
|
||||
|
||||
// Group commands
|
||||
type cmdInfo struct {
|
||||
name string
|
||||
shortName string
|
||||
desc string
|
||||
}
|
||||
|
||||
gameCommands := []cmdInfo{
|
||||
{"new", "n", ""},
|
||||
{"join", "j", ""},
|
||||
{"move", "m", ""},
|
||||
{"computer", "c", ""},
|
||||
{"undo", "u", ""},
|
||||
{"show", "h", ""},
|
||||
{"state", "s", ""},
|
||||
{"delete", "d", ""},
|
||||
{"poll", "p", ""},
|
||||
}
|
||||
|
||||
authCommands := []cmdInfo{
|
||||
{"register", "r", ""},
|
||||
{"login", "l", ""},
|
||||
{"logout", "o", ""},
|
||||
{"whoami", "i", ""},
|
||||
{"user", "e", ""},
|
||||
}
|
||||
|
||||
utilCommands := []cmdInfo{
|
||||
{"health", ".", ""},
|
||||
{"url", "/", ""},
|
||||
{"raw", ":", ""},
|
||||
{"clear", "-", ""},
|
||||
{"help", "?", ""},
|
||||
{"exit", "x", ""},
|
||||
}
|
||||
|
||||
printCommandGroup := func(title string, cmds []cmdInfo) {
|
||||
fmt.Printf("%s%s:%s\n", display.Yellow, title, display.Reset)
|
||||
for _, info := range cmds {
|
||||
if cmd, exists := r.commands[info.name]; exists {
|
||||
shortPart := ""
|
||||
if info.shortName != "" {
|
||||
shortPart = fmt.Sprintf("[%s%s%s] ", display.Cyan, info.shortName, display.Reset)
|
||||
}
|
||||
fmt.Printf(" %s%-10s %s\n", shortPart, cmd.Name, cmd.Description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printCommandGroup("Game Commands", gameCommands)
|
||||
fmt.Println()
|
||||
printCommandGroup("Auth Commands", authCommands)
|
||||
fmt.Println()
|
||||
printCommandGroup("Utility Commands", utilCommands)
|
||||
|
||||
fmt.Printf("\nType 'help <command>' for detailed usage\n")
|
||||
fmt.Printf("Add '-v' to any command for verbose output\n")
|
||||
return nil
|
||||
}
|
||||
func exitHandler(s Session, args []string) error {
|
||||
fmt.Printf("%sGoodbye!%s\n", display.Cyan, display.Reset)
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
54
internal/client/display/board.go
Normal file
54
internal/client/display/board.go
Normal file
@ -0,0 +1,54 @@
|
||||
// FILE: lixenwraith/chess/internal/client/display/board.go
|
||||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RenderBoard renders an ASCII board with colored pieces
|
||||
func RenderBoard(asciiBoard string) {
|
||||
lines := strings.Split(asciiBoard, "\n")
|
||||
|
||||
for i, line := range lines {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
isRankLine := (i == 0) || (i == 9)
|
||||
|
||||
// Process each character
|
||||
for _, char := range line {
|
||||
switch {
|
||||
case char >= 'a' && char <= 'h' && isRankLine:
|
||||
// File letters - Cyan
|
||||
fmt.Printf("%s%c%s", Cyan, char, Reset)
|
||||
case char >= 'A' && char <= 'Z':
|
||||
// White pieces - Blue
|
||||
fmt.Printf("%s%c%s", Blue, char, Reset)
|
||||
case char >= 'a' && char <= 'z' && !isRankLine:
|
||||
// Black pieces - Red
|
||||
fmt.Printf("%s%c%s", Red, char, Reset)
|
||||
case char == '.':
|
||||
// Empty squares
|
||||
fmt.Printf(".")
|
||||
case char >= '1' && char <= '8':
|
||||
// Rank numbers - Cyan
|
||||
fmt.Printf("%s%c%s", Cyan, char, Reset)
|
||||
case char == ' ':
|
||||
fmt.Printf(" ")
|
||||
default:
|
||||
fmt.Printf("%c", char)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
// ColorForTurn returns colored turn indicator
|
||||
func ColorForTurn(turn string) string {
|
||||
if turn == "w" {
|
||||
return Blue + "White" + Reset
|
||||
}
|
||||
return Red + "Black" + Reset
|
||||
}
|
||||
19
internal/client/display/colors.go
Normal file
19
internal/client/display/colors.go
Normal file
@ -0,0 +1,19 @@
|
||||
// FILE: lixenwraith/chess/internal/client/display/colors.go
|
||||
package display
|
||||
|
||||
// Terminal color codes
|
||||
const (
|
||||
Reset = "\033[0m"
|
||||
Red = "\033[31m"
|
||||
Green = "\033[32m"
|
||||
Yellow = "\033[33m"
|
||||
Blue = "\033[34m"
|
||||
Magenta = "\033[35m"
|
||||
Cyan = "\033[36m"
|
||||
White = "\033[37m"
|
||||
)
|
||||
|
||||
// Prompt returns a colored prompt string
|
||||
func Prompt(text string) string {
|
||||
return Yellow + text + Yellow + " > " + Reset
|
||||
}
|
||||
17
internal/client/display/format.go
Normal file
17
internal/client/display/format.go
Normal file
@ -0,0 +1,17 @@
|
||||
// FILE: lixenwraith/chess/internal/client/display/format.go
|
||||
package display
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// PrettyPrintJSON prints formatted JSON
|
||||
func PrettyPrintJSON(v interface{}) {
|
||||
data, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("%sError formatting JSON: %s%s\n", Red, err.Error(), Reset)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
44
internal/client/session/session.go
Normal file
44
internal/client/session/session.go
Normal file
@ -0,0 +1,44 @@
|
||||
// FILE: lixenwraith/chess/internal/client/session/session.go
|
||||
package session
|
||||
|
||||
import (
|
||||
"chess/internal/client/api"
|
||||
)
|
||||
|
||||
// Session maintains client state and configuration
|
||||
type Session struct {
|
||||
APIBaseURL string
|
||||
CurrentGame string
|
||||
CurrentUser string
|
||||
AuthToken string
|
||||
Username string
|
||||
LastMoveCount int
|
||||
Client *api.Client
|
||||
Verbose bool
|
||||
// Game state for prompt
|
||||
CurrentGameState *api.GameResponse
|
||||
PlayerColor string // "w", "b", or ""
|
||||
}
|
||||
|
||||
// Session interface implementation
|
||||
func (s *Session) GetAPIBaseURL() string { return s.APIBaseURL }
|
||||
func (s *Session) SetAPIBaseURL(url string) { s.APIBaseURL = url }
|
||||
func (s *Session) GetCurrentGame() string { return s.CurrentGame }
|
||||
func (s *Session) SetCurrentGame(id string) { s.CurrentGame = id }
|
||||
func (s *Session) GetCurrentUser() string { return s.CurrentUser }
|
||||
func (s *Session) SetCurrentUser(id string) { s.CurrentUser = id }
|
||||
func (s *Session) GetAuthToken() string { return s.AuthToken }
|
||||
func (s *Session) SetAuthToken(token string) { s.AuthToken = token }
|
||||
func (s *Session) GetUsername() string { return s.Username }
|
||||
func (s *Session) SetUsername(name string) { s.Username = name }
|
||||
func (s *Session) GetLastMoveCount() int { return s.LastMoveCount }
|
||||
func (s *Session) SetLastMoveCount(count int) { s.LastMoveCount = count }
|
||||
func (s *Session) GetClient() interface{} { return s.Client }
|
||||
func (s *Session) IsVerbose() bool { return s.Verbose }
|
||||
func (s *Session) SetGameState(game interface{}) {
|
||||
if g, ok := game.(*api.GameResponse); ok {
|
||||
s.CurrentGameState = g
|
||||
}
|
||||
}
|
||||
func (s *Session) SetPlayerColor(color string) { s.PlayerColor = color }
|
||||
func (s *Session) GetPlayerColor() string { return s.PlayerColor }
|
||||
Reference in New Issue
Block a user