From 35c49b22b62e738e210696db83c1902048d89ae023793e894474560233800b97 Mon Sep 17 00:00:00 2001 From: Lixen Wraith Date: Thu, 13 Nov 2025 14:31:31 -0500 Subject: [PATCH] v0.7.1 client readline removed for cross-platform compatibility with wasm, client logic fix fixes and refactor --- cmd/chess-client/main.go | 95 ++++----- go.mod | 1 - go.sum | 7 - internal/client/api/client.go | 39 ++-- internal/client/{commands => command}/auth.go | 51 ++--- .../client/{commands => command}/debug.go | 17 +- internal/client/{commands => command}/game.go | 183 +++++++++--------- internal/client/command/pass_unix.go | 20 ++ internal/client/command/pass_wasm.go | 29 +++ .../client/{commands => command}/registry.go | 68 +++---- internal/client/display/board.go | 14 +- internal/client/display/colors.go | 34 ++++ internal/client/display/format.go | 4 +- internal/client/session/session.go | 4 +- internal/server/http/validator.go | 2 +- internal/server/processor/command.go | 6 +- internal/server/storage/game.go | 2 +- internal/server/storage/user.go | 2 +- 18 files changed, 306 insertions(+), 272 deletions(-) rename internal/client/{commands => command}/auth.go (71%) rename internal/client/{commands => command}/debug.go (80%) rename internal/client/{commands => command}/game.go (64%) create mode 100644 internal/client/command/pass_unix.go create mode 100644 internal/client/command/pass_wasm.go rename internal/client/{commands => command}/registry.go (65%) diff --git a/cmd/chess-client/main.go b/cmd/chess-client/main.go index c6a3eba..567fe96 100644 --- a/cmd/chess-client/main.go +++ b/cmd/chess-client/main.go @@ -3,17 +3,15 @@ package main import ( + "bufio" "fmt" - "io" "os" "strings" "chess/internal/client/api" - "chess/internal/client/commands" + "chess/internal/client/command" "chess/internal/client/display" "chess/internal/client/session" - - "github.com/chzyer/readline" ) func main() { @@ -23,44 +21,37 @@ func main() { Verbose: false, } - // Initialize readline - rl, err := readline.NewEx(&readline.Config{ - Prompt: display.Prompt("chess"), - HistoryFile: ".chess_history", - InterruptPrompt: "^C", - EOFPrompt: "exit", - }) - if err != nil { - fmt.Printf("%s%s%s\n", display.Red, err.Error(), display.Reset) - os.Exit(1) - } - defer rl.Close() + // Initialize simple input scanner + scanner := bufio.NewScanner(os.Stdin) - fmt.Printf("%sChess Debug Client%s\n", display.Cyan, display.Reset) - fmt.Printf("%sAPI: %s%s\n", display.Cyan, s.APIBaseURL, display.Reset) - fmt.Printf("Type 'help' for commands\n\n") + display.Println(display.Cyan, "Chess Debug Client") + display.Println(display.Cyan, "API: %s", s.APIBaseURL) + fmt.Println("Type 'help' for commands\n") - registry := commands.NewRegistry(s) + registry := command.NewRegistry(s) for { // Build enhanced prompt prompt := buildPrompt(s) - rl.SetPrompt(prompt) + fmt.Print(prompt) - line, err := rl.Readline() - if err == io.EOF { + // Read input + if !scanner.Scan() { + // EOF or error + if err := scanner.Err(); err != nil { + display.Println(display.Red, "\nError reading input: %s", err.Error()) + } break } - if err != nil { - continue - } - line = strings.TrimSpace(line) + line := strings.TrimSpace(scanner.Text()) if line == "" { continue } + // Check for exit commands if line == "exit" || line == "quit" || line == "x" { + display.Println(display.Cyan, "Goodbye!") break } @@ -77,63 +68,53 @@ func main() { } func buildPrompt(s *session.Session) string { - parts := []string{} - - // Base - base := "chess" + var b display.Builder + b.Add("", "chess") // Add user/game context if s.Username != "" { - parts = append(parts, fmt.Sprintf("%s%s%s", display.Magenta, s.Username, display.Reset)) - } - if s.Username != "" && s.CurrentGame != "" { - parts = append(parts, fmt.Sprintf("%s - %s", display.Yellow, display.Reset)) + b.Add("", " [").Add(display.Magenta, s.Username) + if s.CurrentGame != "" { + b.Add(display.Yellow, " - ") + } else { + b.Add("", "]") + } } + if s.CurrentGame != "" { - parts = append(parts, fmt.Sprintf("%s%s%s", display.White, s.CurrentGame[:8], display.Reset)) + if s.Username == "" { + b.Add("", " [") + } + b.Add(display.White, s.CurrentGame[:8]) + b.Add("", "]") } // Add player color if in game if s.CurrentGameState != nil && s.PlayerColor != "" { - colorText := "" if s.PlayerColor == "w" { - colorText = display.Blue + "White" + display.Reset + b.Add("", " ").Add(display.Blue, "White") } else { - colorText = display.Red + "Black" + display.Reset + b.Add("", " ").Add(display.Red, "Black") } - parts = append(parts, colorText) - } - - // Build first part - promptStr := base - if len(parts) > 0 { - promptStr += display.Yellow + " [" + display.Reset + strings.Join(parts, "") + display.Yellow + "]" } // Add game state if available if s.CurrentGameState != nil { - turnInfo := "" + turnInfo := " - Turn:" if s.CurrentGameState.Turn == "w" { - turnPlayer := "White" playerType := "h" if s.CurrentGameState.Players.White.Type == 2 { playerType = "c" } - turnInfo = fmt.Sprintf(" - Turn:%s(%s)", - fmt.Sprintf("%s%s%s", display.Blue, turnPlayer, display.Reset), - playerType) + b.Add("", turnInfo).Add(display.Blue, "White").Add("", fmt.Sprintf("(%s)", playerType)) } else { - turnPlayer := "Black" playerType := "h" if s.CurrentGameState.Players.Black.Type == 2 { playerType = "c" } - turnInfo = fmt.Sprintf(" - Turn:%s(%s)", - fmt.Sprintf("%s%s%s", display.Red, turnPlayer, display.Reset), - playerType) + b.Add("", turnInfo).Add(display.Red, "Black").Add("", fmt.Sprintf("(%s)", playerType)) } - promptStr += turnInfo } - return display.Prompt(promptStr) + return display.Prompt(b.String()) } \ No newline at end of file diff --git a/go.mod b/go.mod index b1203c7..2ae28ee 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module chess go 1.25.4 require ( - github.com/chzyer/readline v1.5.1 github.com/go-playground/validator/v10 v10.28.0 github.com/gofiber/fiber/v2 v2.52.9 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 234b84c..26504ee 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,5 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= -github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= -github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= -github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= -github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= -github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= @@ -58,7 +52,6 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/internal/client/api/client.go b/internal/client/api/client.go index 58bf0f0..79b297b 100644 --- a/internal/client/api/client.go +++ b/internal/client/api/client.go @@ -13,6 +13,8 @@ import ( "chess/internal/client/display" ) +const HttpTimeout = 30 * time.Second + type Client struct { BaseURL string AuthToken string @@ -24,7 +26,7 @@ func New(baseURL string) *Client { return &Client{ BaseURL: baseURL, HTTPClient: &http.Client{ - Timeout: 30 * time.Second, + Timeout: HttpTimeout, }, } } @@ -42,7 +44,7 @@ func (c *Client) SetToken(token string) { c.AuthToken = token } -func (c *Client) doRequest(method, path string, body interface{}, result interface{}) error { +func (c *Client) doRequest(method, path string, body any, result any) error { url := c.BaseURL + path // Prepare body @@ -72,23 +74,24 @@ func (c *Client) doRequest(method, path string, body interface{}, result interfa } // Display request - fmt.Printf("\n%s[API] %s %s%s\n", display.Blue, method, path, display.Reset) + display.Print(display.Blue, "\n[API] %s %s\n", method, path) if bodyStr != "" { if c.Verbose { // Display request body if verbose - var prettyBody interface{} + var prettyBody any json.Unmarshal([]byte(bodyStr), &prettyBody) prettyJSON, _ := json.MarshalIndent(prettyBody, "", " ") - fmt.Printf("%sRequest Body:%s\n%s\n", display.Cyan, display.Reset, string(prettyJSON)) + display.Println(display.Cyan, "Request Body:") + display.Println(display.Reset, string(prettyJSON)) } else { - fmt.Printf("%s%s%s\n", display.Blue, bodyStr, display.Reset) + display.Print(display.Blue, "%s\n", bodyStr) } } // Execute request resp, err := c.HTTPClient.Do(req) if err != nil { - fmt.Printf("%s[ERROR] %s%s\n", display.Red, err.Error(), display.Reset) + display.Print(display.Red, "[ERROR] %s\n", err.Error()) return err } defer resp.Body.Close() @@ -108,12 +111,14 @@ func (c *Client) doRequest(method, path string, body interface{}, result interfa // Display response body if verbose if c.Verbose && len(respBody) > 0 { - var prettyResp interface{} + var prettyResp any 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)) + display.Println(display.Cyan, "Response Body:") + display.Println(display.Reset, string(prettyJSON)) } else { - fmt.Printf("%sResponse:%s\n%s\n", display.Cyan, display.Reset, string(respBody)) + display.Println(display.Cyan, "Response:") + display.Println(display.Reset, string(respBody)) } } @@ -122,16 +127,16 @@ func (c *Client) doRequest(method, path string, body interface{}, result interfa 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) + display.Print(display.Red, "Error: %s\n", errResp.Error) if errResp.Code != "" { - fmt.Printf("%sCode: %s%s\n", display.Red, errResp.Code, display.Reset) + display.Print(display.Red, "Code: %s\n", errResp.Code) } if errResp.Details != "" { - fmt.Printf("%sDetails: %s%s\n", display.Red, errResp.Details, display.Reset) + display.Print(display.Red, "Details: %s\n", errResp.Details) } } } else if !c.Verbose { - fmt.Printf("%s%s%s\n", display.Red, string(respBody), display.Reset) + display.Println(display.Red, string(respBody)) } return fmt.Errorf("request failed with status %d", resp.StatusCode) } @@ -140,8 +145,8 @@ func (c *Client) doRequest(method, path string, body interface{}, result interfa 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) + display.Print(display.Red, "Response parse error: %s\n", err.Error()) + display.Print(display.Green, "Raw response: %s\n", string(respBody)) return err } } @@ -229,7 +234,7 @@ func (c *Client) GetCurrentUser() (*UserResponse, error) { // RawRequest performs a raw HTTP request for debugging purposes func (c *Client) RawRequest(method, path string, body string) error { - var bodyData interface{} + var bodyData any if body != "" { if err := json.Unmarshal([]byte(body), &bodyData); err != nil { // Try as raw string diff --git a/internal/client/commands/auth.go b/internal/client/command/auth.go similarity index 71% rename from internal/client/commands/auth.go rename to internal/client/command/auth.go index 0d1aa8e..f1a3497 100644 --- a/internal/client/commands/auth.go +++ b/internal/client/command/auth.go @@ -1,17 +1,14 @@ -// FILE: lixenwraith/chess/internal/client/commands/auth.go -package commands +// FILE: lixenwraith/chess/internal/client/command/auth.go +package command import ( "bufio" + "chess/internal/client/api" + "chess/internal/client/display" + "chess/internal/client/session" "fmt" "os" "strings" - "syscall" - - "chess/internal/client/api" - "chess/internal/client/display" - - "golang.org/x/term" ) func (r *Registry) registerAuthCommands() { @@ -56,21 +53,11 @@ func (r *Registry) registerAuthCommands() { }) } -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 { +func registerHandler(s *session.Session, args []string) error { scanner := bufio.NewScanner(os.Stdin) c := s.GetClient().(*api.Client) - fmt.Print(display.Yellow + "Username: " + display.Reset) + display.Print(display.Yellow, "Username: ") scanner.Scan() username := strings.TrimSpace(scanner.Text()) @@ -79,7 +66,7 @@ func registerHandler(s Session, args []string) error { return err } - fmt.Print(display.Yellow + "Email (optional): " + display.Reset) + display.Print(display.Yellow, "Email (optional): ") scanner.Scan() email := strings.TrimSpace(scanner.Text()) @@ -93,18 +80,18 @@ func registerHandler(s Session, args []string) error { s.SetUsername(resp.Username) c.SetToken(resp.Token) - fmt.Printf("%sRegistered successfully%s\n", display.Green, display.Reset) + display.Println(display.Green, "Registered successfully") fmt.Printf("User ID: %s\n", resp.UserID) fmt.Printf("Username: %s\n", resp.Username) return nil } -func loginHandler(s Session, args []string) error { +func loginHandler(s *session.Session, args []string) error { scanner := bufio.NewScanner(os.Stdin) c := s.GetClient().(*api.Client) - fmt.Print(display.Yellow + "Username or Email: " + display.Reset) + display.Print(display.Yellow, "Username or Email: ") scanner.Scan() identifier := strings.TrimSpace(scanner.Text()) @@ -123,27 +110,27 @@ func loginHandler(s Session, args []string) error { s.SetUsername(resp.Username) c.SetToken(resp.Token) - fmt.Printf("%sLogged in successfully%s\n", display.Green, display.Reset) + display.Println(display.Green, "Logged in successfully") fmt.Printf("User ID: %s\n", resp.UserID) fmt.Printf("Username: %s\n", resp.Username) return nil } -func logoutHandler(s Session, args []string) error { +func logoutHandler(s *session.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) + display.Println(display.Green, "Logged out") return nil } -func whoamiHandler(s Session, args []string) error { +func whoamiHandler(s *session.Session, args []string) error { if s.GetAuthToken() == "" { - fmt.Printf("%sNot authenticated%s\n", display.Yellow, display.Reset) + display.Println(display.Yellow, "Not authenticated") return nil } @@ -153,7 +140,7 @@ func whoamiHandler(s Session, args []string) error { return err } - fmt.Printf("%sCurrent User:%s\n", display.Cyan, display.Reset) + display.Println(display.Cyan, "Current User:") fmt.Printf(" User ID: %s\n", user.UserID) fmt.Printf(" Username: %s\n", user.Username) if user.Email != "" { @@ -167,14 +154,14 @@ func whoamiHandler(s Session, args []string) error { return nil } -func setUserHandler(s Session, args []string) error { +func setUserHandler(s *session.Session, args []string) error { if len(args) < 1 { return fmt.Errorf("usage: user ") } userID := args[0] s.SetCurrentUser(userID) - fmt.Printf("%sUser ID set to: %s%s\n", display.Cyan, userID, display.Reset) + display.Println(display.Cyan, "User ID set to: %s", userID) fmt.Println("Note: This doesn't authenticate, just sets the ID for display") return nil diff --git a/internal/client/commands/debug.go b/internal/client/command/debug.go similarity index 80% rename from internal/client/commands/debug.go rename to internal/client/command/debug.go index b3bfb84..837bccd 100644 --- a/internal/client/commands/debug.go +++ b/internal/client/command/debug.go @@ -1,5 +1,5 @@ -// FILE: lixenwraith/chess/internal/client/commands/debug.go -package commands +// FILE: lixenwraith/chess/internal/client/command/debug.go +package command import ( "fmt" @@ -10,6 +10,7 @@ import ( "chess/internal/client/api" "chess/internal/client/display" + "chess/internal/client/session" ) func (r *Registry) registerDebugCommands() { @@ -46,14 +47,14 @@ func (r *Registry) registerDebugCommands() { }) } -func healthHandler(s Session, args []string) error { +func healthHandler(s *session.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) + display.Println(display.Cyan, "%sServer Health:%s") fmt.Printf(" Status: %s\n", resp.Status) // Convert Unix timestamp to readable time t := time.Unix(resp.Time, 0) @@ -65,7 +66,7 @@ func healthHandler(s Session, args []string) error { return nil } -func urlHandler(s Session, args []string) error { +func urlHandler(s *session.Session, args []string) error { if len(args) == 0 { fmt.Printf("Current API URL: %s\n", s.GetAPIBaseURL()) return nil @@ -80,11 +81,11 @@ func urlHandler(s Session, args []string) error { c := s.GetClient().(*api.Client) c.SetBaseURL(url) - fmt.Printf("%sAPI URL set to: %s%s\n", display.Cyan, url, display.Reset) + display.Println(display.Cyan, "API URL set to: %s", url) return nil } -func rawRequestHandler(s Session, args []string) error { +func rawRequestHandler(s *session.Session, args []string) error { if len(args) < 2 { return fmt.Errorf("usage: raw [json-body]") } @@ -101,7 +102,7 @@ func rawRequestHandler(s Session, args []string) error { return c.RawRequest(method, path, body) } -func clearHandler(s Session, args []string) error { +func clearHandler(s *session.Session, args []string) error { cmd := exec.Command("clear") cmd.Stdout = os.Stdout return cmd.Run() diff --git a/internal/client/commands/game.go b/internal/client/command/game.go similarity index 64% rename from internal/client/commands/game.go rename to internal/client/command/game.go index 2eb4b5f..79d8f52 100644 --- a/internal/client/commands/game.go +++ b/internal/client/command/game.go @@ -1,5 +1,5 @@ -// FILE: lixenwraith/chess/internal/client/commands/game.go -package commands +// FILE: lixenwraith/chess/internal/client/command/game.go +package command import ( "bufio" @@ -11,6 +11,7 @@ import ( "chess/internal/client/api" "chess/internal/client/display" + "chess/internal/client/session" ) func (r *Registry) registerGameCommands() { @@ -87,14 +88,14 @@ func (r *Registry) registerGameCommands() { }) } -func newGameHandler(s Session, args []string) error { +func newGameHandler(s *session.Session, args []string) error { scanner := bufio.NewScanner(os.Stdin) - c := s.GetClient().(*api.Client) + c := s.Client - fmt.Println("\n" + display.Cyan + "Creating new game..." + display.Reset) + display.Println(display.Cyan, "\nCreating new game...") // White player - fmt.Print(display.Yellow + "White player type (h/c) [h]: " + display.Reset) + display.Print(display.Yellow, "White player type (h/c) [h]: ") scanner.Scan() whiteType := strings.ToLower(strings.TrimSpace(scanner.Text())) if whiteType == "" { @@ -105,7 +106,7 @@ func newGameHandler(s Session, args []string) error { if whiteType == "c" { white.Type = 2 - fmt.Print(display.Yellow + "Computer level (0-20) [10]: " + display.Reset) + display.Print(display.Yellow, "Computer level (0-20) [10]: ") scanner.Scan() levelStr := strings.TrimSpace(scanner.Text()) if levelStr == "" { @@ -115,7 +116,7 @@ func newGameHandler(s Session, args []string) error { white.Level = level } - fmt.Print(display.Yellow + "Search time (100-10000ms) [1000]: " + display.Reset) + display.Print(display.Yellow, "Search time (100-10000ms) [1000]: ") scanner.Scan() timeStr := strings.TrimSpace(scanner.Text()) if timeStr == "" { @@ -126,8 +127,8 @@ func newGameHandler(s Session, args []string) error { } } - // Black player - fmt.Print(display.Yellow + "Black player type (h/c) [h]: " + display.Reset) + // 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 == "" { @@ -138,7 +139,7 @@ func newGameHandler(s Session, args []string) error { if blackType == "c" { black.Type = 2 - fmt.Print(display.Yellow + "Computer level (0-20) [10]: " + display.Reset) + display.Print(display.Yellow, "Computer level (0-20) [10]: ") scanner.Scan() levelStr := strings.TrimSpace(scanner.Text()) if levelStr == "" { @@ -148,7 +149,7 @@ func newGameHandler(s Session, args []string) error { black.Level = level } - fmt.Print(display.Yellow + "Search time (100-10000ms) [1000]: " + display.Reset) + display.Print(display.Yellow, "Search time (100-10000ms) [1000]: ") scanner.Scan() timeStr := strings.TrimSpace(scanner.Text()) if timeStr == "" { @@ -160,7 +161,7 @@ func newGameHandler(s Session, args []string) error { } // Starting position - fmt.Print(display.Yellow + "Starting position (FEN) [default]: " + display.Reset) + display.Print(display.Yellow, "Starting position (FEN) [default]: ") scanner.Scan() fen := strings.TrimSpace(scanner.Text()) @@ -175,36 +176,31 @@ func newGameHandler(s Session, args []string) error { return err } - s.SetCurrentGame(resp.GameID) - s.SetLastMoveCount(len(resp.Moves)) - s.SetGameState(resp) + s.CurrentGame = resp.GameID + s.LastMoveCount = len(resp.Moves) + s.CurrentGameState = 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") + if s.CurrentUser != "" { + if resp.Players.White.ID == s.CurrentUser { + s.PlayerColor = "w" + } else if resp.Players.Black.ID == s.CurrentUser { + s.PlayerColor = "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) + display.Println(display.Green, "Game created: %s", resp.GameID) + display.Println(display.Cyan, "Current game set to: %s", resp.GameID) - // If white is computer, trigger first move + // If white is computer, inform user to trigger 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) - } + display.Println(display.Magenta, "\nWhite is computer. Use 'computer' or 'c' to trigger first move.") } return nil } -func joinGameHandler(s Session, args []string) error { +func joinGameHandler(s *session.Session, args []string) error { if len(args) < 1 { return fmt.Errorf("usage: join ") } @@ -239,74 +235,65 @@ func joinGameHandler(s Session, args []string) error { return nil } -func moveHandler(s Session, args []string) error { +func moveHandler(s *session.Session, args []string) error { if len(args) < 1 { return fmt.Errorf("usage: move ") } - gameID := s.GetCurrentGame() + gameID := s.CurrentGame if gameID == "" { return fmt.Errorf("no current game, use 'new' or 'join '") } move := args[0] - c := s.GetClient().(*api.Client) + c := s.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) + s.LastMoveCount = len(resp.Moves) + s.CurrentGameState = resp + display.Println(display.Green, "Move accepted") - // 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 - } + // Check if game ended + switch resp.State { + case "checkmate": + winner := "Black" + if resp.Turn == "b" { // Turn switches after move, so if black's turn after checkmate, white won + winner = "White" + } + display.Println(display.Green, "\nCHECKMATE! %s wins!", winner) + case "stalemate": + display.Println(display.Yellow, "\nSTALEMATE! Game drawn.") + case "draw": + display.Println(display.Yellow, "\nDRAW! Game drawn.") + case "ongoing": + // Check if computer needs to play + 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 - } - } + if computerPlayer != nil { + display.Println(display.Magenta, "\nComputer's turn. Use 'computer' or 'c' to trigger move.") } } return nil } -func computerMoveHandler(s Session, args []string) error { - gameID := s.GetCurrentGame() +func computerMoveHandler(s *session.Session, args []string) error { + gameID := s.CurrentGame if gameID == "" { return fmt.Errorf("no current game, use 'new' or 'join '") } - c := s.GetClient().(*api.Client) + c := s.Client resp, err := c.MakeMove(gameID, "cccc") if err != nil { @@ -314,34 +301,50 @@ func computerMoveHandler(s Session, args []string) error { } if resp.State == "pending" { - fmt.Printf("%sComputer is thinking...%s\n", display.Magenta, display.Reset) + display.Println(display.Magenta, "Computer is thinking...") // 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) + s.LastMoveCount = len(resp2.Moves) + s.CurrentGameState = resp2 if resp2.LastMove != nil { - fmt.Printf("%sComputer played: %s%s", display.Magenta, resp2.LastMove.Move, display.Reset) + display.Print(display.Magenta, "Computer played: %s", resp2.LastMove.Move) if resp2.LastMove.Depth > 0 { fmt.Printf(" (depth %d, score %d)", resp2.LastMove.Depth, resp2.LastMove.Score) } fmt.Println() } + + // Check if game ended after computer move + switch resp2.State { + case "checkmate": + winner := "Black" + if resp2.Turn == "b" { + winner = "White" + } + display.Println(display.Green, "\nCHECKMATE! %s wins!", winner) + case "stalemate": + display.Println(display.Yellow, "\nSTALEMATE! Game drawn.") + case "draw": + display.Println(display.Yellow, "\nDRAW! Game drawn.") + } + 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) + s.LastMoveCount = len(resp.Moves) + s.CurrentGameState = resp + display.Println(display.Green, "Move triggered") return nil } -func undoHandler(s Session, args []string) error { +func undoHandler(s *session.Session, args []string) error { gameID := s.GetCurrentGame() if gameID == "" { return fmt.Errorf("no current game, use 'new' or 'join '") @@ -363,11 +366,12 @@ func undoHandler(s Session, args []string) error { } s.SetLastMoveCount(len(resp.Moves)) - fmt.Printf("%sUndid %d move(s)%s\n", display.Green, count, display.Reset) + s.SetGameState(resp) + display.Println(display.Green, "Undid %d move(s)", count) return nil } -func showBoardHandler(s Session, args []string) error { +func showBoardHandler(s *session.Session, args []string) error { gameID := s.GetCurrentGame() if gameID == "" { return fmt.Errorf("no current game, use 'new' or 'join '") @@ -431,7 +435,7 @@ func showBoardHandler(s Session, args []string) error { return nil } -func gameStateHandler(s Session, args []string) error { +func gameStateHandler(s *session.Session, args []string) error { gameID := s.GetCurrentGame() if gameID == "" { return fmt.Errorf("no current game, use 'new' or 'join '") @@ -446,13 +450,13 @@ func gameStateHandler(s Session, args []string) error { s.SetLastMoveCount(len(resp.Moves)) // Pretty print JSON - fmt.Printf("%sGame State:%s\n", display.Cyan, display.Reset) + display.Println(display.Cyan, "Game State:") display.PrettyPrintJSON(resp) return nil } -func deleteGameHandler(s Session, args []string) error { +func deleteGameHandler(s *session.Session, args []string) error { gameID := s.GetCurrentGame() if len(args) > 0 { gameID = args[0] @@ -477,7 +481,7 @@ func deleteGameHandler(s Session, args []string) error { return nil } -func pollHandler(s Session, args []string) error { +func pollHandler(s *session.Session, args []string) error { gameID := s.GetCurrentGame() if gameID == "" { return fmt.Errorf("no current game, use 'new' or 'join '") @@ -486,9 +490,8 @@ func pollHandler(s Session, args []string) error { 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) + display.Println(display.Cyan, "Long-polling for updates (move count: %d)...", moveCount) + display.Println(display.Cyan, "This may take up to 25 seconds") resp, err := c.GetGameWithPoll(gameID, moveCount) if err != nil { @@ -499,12 +502,12 @@ func pollHandler(s Session, args []string) error { s.SetGameState(resp) if len(resp.Moves) > moveCount { - fmt.Printf("%sGame updated! New moves detected%s\n", display.Green, display.Reset) + display.Println(display.Green, "Game updated! New moves detected") 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) + display.Println(display.Yellow, "No updates (timeout)") } return nil diff --git a/internal/client/command/pass_unix.go b/internal/client/command/pass_unix.go new file mode 100644 index 0000000..a2e5387 --- /dev/null +++ b/internal/client/command/pass_unix.go @@ -0,0 +1,20 @@ +// FILE: internal/client/command/auth_unix.go +//go:build !js && !wasm + +package command + +import ( + "fmt" + + "golang.org/x/term" +) + +func readPassword(prompt string) (string, error) { + fmt.Print(prompt) + bytePassword, err := term.ReadPassword(0) // 0 is stdin + fmt.Println() + if err != nil { + return "", err + } + return string(bytePassword), nil +} \ No newline at end of file diff --git a/internal/client/command/pass_wasm.go b/internal/client/command/pass_wasm.go new file mode 100644 index 0000000..33f1ceb --- /dev/null +++ b/internal/client/command/pass_wasm.go @@ -0,0 +1,29 @@ +// FILE: lixenwraith/chess/internal/client/command/auth_wasm.go +//go:build js && wasm + +package command + +import ( + "bufio" + "fmt" + "os" + "strings" + + "chess/internal/client/display" +) + +func readPassword(prompt string) (string, error) { + display.Println(display.Red, "(warning: password visible in browser)") + display.Print(display.Yellow, prompt) + + // In WASM/browser, password masking must be handled by JavaScript/xterm.js + // This is fallback with visible input + scanner := bufio.NewScanner(os.Stdin) + if scanner.Scan() { + return strings.TrimSpace(scanner.Text()), nil + } + if err := scanner.Err(); err != nil { + return "", err + } + return "", fmt.Errorf("no input received") +} \ No newline at end of file diff --git a/internal/client/commands/registry.go b/internal/client/command/registry.go similarity index 65% rename from internal/client/commands/registry.go rename to internal/client/command/registry.go index 12884bc..2a0528e 100644 --- a/internal/client/commands/registry.go +++ b/internal/client/command/registry.go @@ -1,53 +1,33 @@ -// FILE: lixenwraith/chess/internal/client/commands/registry.go -package commands +// FILE: lixenwraith/chess/internal/client/command/registry.go +package command import ( "fmt" - "os" "strings" "chess/internal/client/api" "chess/internal/client/display" + "chess/internal/client/session" ) -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 + Handler func(*session.Session, []string) error } type Registry struct { - session Session + session *session.Session commands map[string]*Command } // Registry manages command registration and execution -func NewRegistry(session Session) *Registry { +func NewRegistry(s *session.Session) *Registry { r := &Registry{ - session: session, + session: s, commands: make(map[string]*Command), } @@ -65,7 +45,7 @@ func NewRegistry(session Session) *Registry { Handler: r.helpHandler, }) - // Exit command + // Exit command (handled in main loop, but registered for help display) r.Register(&Command{ Name: "exit", ShortName: "x", @@ -95,8 +75,8 @@ func (r *Registry) Execute(input string) { 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") + display.Println(display.Red, "Unknown command: %s", cmdName) + display.Println(display.Reset, "Type 'help' for available commands") return } @@ -106,27 +86,29 @@ func (r *Registry) Execute(input string) { } if err := cmd.Handler(r.session, args); err != nil { - fmt.Printf("%sError: %s%s\n", display.Red, err.Error(), display.Reset) + display.Println(display.Red, "Error: %s", err.Error()) } } -func (r *Registry) helpHandler(s Session, args []string) error { +func (r *Registry) helpHandler(s *session.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) + fmt.Println() + display.Print(display.Cyan, cmd.Name) + display.Println(display.Reset, " - %s", cmd.Description) if cmd.ShortName != "" { - fmt.Printf("Short form: %s%s%s\n", display.Cyan, cmd.ShortName, display.Reset) + display.Println(display.Cyan, "Short form: %s", cmd.ShortName) } fmt.Printf("Usage: %s\n", cmd.Usage) return nil } // Show all commands - fmt.Printf("\n%sAvailable Commands:%s\n\n", display.Cyan, display.Reset) + display.Println(display.Cyan, "\nAvailable Commands:\n") // Group commands type cmdInfo struct { @@ -165,7 +147,7 @@ func (r *Registry) helpHandler(s Session, args []string) error { } printCommandGroup := func(title string, cmds []cmdInfo) { - fmt.Printf("%s%s:%s\n", display.Yellow, title, display.Reset) + display.Println(display.Yellow, "%s:", title) for _, info := range cmds { if cmd, exists := r.commands[info.name]; exists { shortPart := "" @@ -175,20 +157,20 @@ func (r *Registry) helpHandler(s Session, args []string) error { fmt.Printf(" %s%-10s %s\n", shortPart, cmd.Name, cmd.Description) } } + fmt.Println() } printCommandGroup("Game Commands", gameCommands) - fmt.Println() printCommandGroup("Auth Commands", authCommands) - fmt.Println() printCommandGroup("Utility Commands", utilCommands) - fmt.Printf("\nType 'help ' for detailed usage\n") - fmt.Printf("Add '-v' to any command for verbose output\n") + display.Println(display.Reset, "Type 'help ' for detailed usage") + display.Println(display.Reset, "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) + +func exitHandler(s *session.Session, args []string) error { + // Exit is handled in main loop, this is just for consistency + display.Println(display.Cyan, "Goodbye!\n") return nil } \ No newline at end of file diff --git a/internal/client/display/board.go b/internal/client/display/board.go index 7f9ca80..8595ed1 100644 --- a/internal/client/display/board.go +++ b/internal/client/display/board.go @@ -22,23 +22,23 @@ func RenderBoard(asciiBoard string) { switch { case char >= 'a' && char <= 'h' && isRankLine: // File letters - Cyan - fmt.Printf("%s%c%s", Cyan, char, Reset) + Print(Cyan, "%c", char) case char >= 'A' && char <= 'Z': // White pieces - Blue - fmt.Printf("%s%c%s", Blue, char, Reset) + Print(Blue, "%c", char) case char >= 'a' && char <= 'z' && !isRankLine: // Black pieces - Red - fmt.Printf("%s%c%s", Red, char, Reset) + Print(Red, "%c", char) case char == '.': // Empty squares - fmt.Printf(".") + Print(White, ".") case char >= '1' && char <= '8': // Rank numbers - Cyan - fmt.Printf("%s%c%s", Cyan, char, Reset) + Print(Cyan, "%c", char) case char == ' ': - fmt.Printf(" ") + Print(Reset, " ") default: - fmt.Printf("%c", char) + Print(Reset, "%c", char) } } fmt.Println() diff --git a/internal/client/display/colors.go b/internal/client/display/colors.go index ee483e1..29c7b9f 100644 --- a/internal/client/display/colors.go +++ b/internal/client/display/colors.go @@ -1,6 +1,11 @@ // FILE: lixenwraith/chess/internal/client/display/colors.go package display +import ( + "fmt" + "strings" +) + // Terminal color codes const ( Reset = "\033[0m" @@ -13,6 +18,35 @@ const ( White = "\033[37m" ) +// C wraps text with color and reset codes +func C(color, text string) string { + return color + text + Reset +} + +// Print outputs colored text immediately +func Print(color, format string, args ...any) { + fmt.Printf(C(color, format), args...) +} + +// Println outputs colored text with newline +func Println(color, format string, args ...any) { + fmt.Println(C(color, fmt.Sprintf(format, args...))) +} + +// Build creates a multi-colored string +type Builder struct { + parts []string +} + +func (b *Builder) Add(color, text string) *Builder { + b.parts = append(b.parts, C(color, text)) + return b +} + +func (b *Builder) String() string { + return strings.Join(b.parts, "") +} + // Prompt returns a colored prompt string func Prompt(text string) string { return Yellow + text + Yellow + " > " + Reset diff --git a/internal/client/display/format.go b/internal/client/display/format.go index c6d124c..e90fb51 100644 --- a/internal/client/display/format.go +++ b/internal/client/display/format.go @@ -7,10 +7,10 @@ import ( ) // PrettyPrintJSON prints formatted JSON -func PrettyPrintJSON(v interface{}) { +func PrettyPrintJSON(v any) { data, err := json.MarshalIndent(v, "", " ") if err != nil { - fmt.Printf("%sError formatting JSON: %s%s\n", Red, err.Error(), Reset) + Print(Red, "Error formatting JSON: %s\n", err.Error()) return } fmt.Println(string(data)) diff --git a/internal/client/session/session.go b/internal/client/session/session.go index 61784b0..4a110b8 100644 --- a/internal/client/session/session.go +++ b/internal/client/session/session.go @@ -33,9 +33,9 @@ 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) GetClient() any { return s.Client } func (s *Session) IsVerbose() bool { return s.Verbose } -func (s *Session) SetGameState(game interface{}) { +func (s *Session) SetGameState(game any) { if g, ok := game.(*api.GameResponse); ok { s.CurrentGameState = g } diff --git a/internal/server/http/validator.go b/internal/server/http/validator.go index 6569402..3bb18b6 100644 --- a/internal/server/http/validator.go +++ b/internal/server/http/validator.go @@ -25,7 +25,7 @@ func validationMiddleware(c *fiber.Ctx) error { // Determine request type based on path path := c.Path() - var requestType interface{} + var requestType any switch { case strings.HasSuffix(path, "/games") && method == fiber.MethodPost: diff --git a/internal/server/processor/command.go b/internal/server/processor/command.go index 1113da0..ea16fee 100644 --- a/internal/server/processor/command.go +++ b/internal/server/processor/command.go @@ -22,15 +22,15 @@ const ( type Command struct { Type CommandType UserID string - GameID string // For game-specific commands - Args interface{} // Command-specific arguments + GameID string // For game-specific commands + Args any // Command-specific arguments } // ProcessorResponse wraps the response with metadata type ProcessorResponse struct { Success bool `json:"success"` Pending bool `json:"pending,omitempty"` // For async operations - Data interface{} `json:"data,omitempty"` + Data any `json:"data,omitempty"` Error *core.ErrorResponse `json:"error,omitempty"` } diff --git a/internal/server/storage/game.go b/internal/server/storage/game.go index 7cbd4ae..ae152ff 100644 --- a/internal/server/storage/game.go +++ b/internal/server/storage/game.go @@ -93,7 +93,7 @@ func (s *Store) QueryGames(gameID, playerID string) ([]GameRecord, error) { start_time_utc FROM games WHERE 1=1` - var args []interface{} + var args []any // Handle gameID filtering if gameID != "" && gameID != "*" { diff --git a/internal/server/storage/user.go b/internal/server/storage/user.go index db0a3a3..91784cf 100644 --- a/internal/server/storage/user.go +++ b/internal/server/storage/user.go @@ -45,7 +45,7 @@ func (s *Store) CreateUser(record UserRecord) error { func (s *Store) userExists(tx *sql.Tx, username, email string) (bool, error) { var count int query := `SELECT COUNT(*) FROM users WHERE username = ? COLLATE NOCASE` - args := []interface{}{username} + args := []any{username} if email != "" { query = `SELECT COUNT(*) FROM users WHERE username = ? COLLATE NOCASE OR email = ? COLLATE NOCASE`