From 6bdc0615082cbe95af450f17bef72a6969f38d485c683f3ba37ba2be914c611a Mon Sep 17 00:00:00 2001 From: Lixen Wraith Date: Thu, 13 Nov 2025 08:55:06 -0500 Subject: [PATCH] v0.7.0 cli client with readline added, directory structure updated --- .gitignore | 2 + Makefile | 159 ++++++ README.md | 58 +- cmd/chess-client/main.go | 139 +++++ cmd/{chessd => chess-server}/cli/cli.go | 4 +- cmd/{chessd => chess-server}/main.go | 16 +- cmd/{chessd => chess-server}/pid.go | 6 +- doc/architecture.md | 1 + doc/client.md | 291 ++++++++++ doc/development.md | 58 +- go.mod | 11 +- go.sum | 23 +- internal/client/api/client.go | 241 +++++++++ internal/client/api/types.go | 97 ++++ internal/client/commands/auth.go | 181 +++++++ internal/client/commands/debug.go | 108 ++++ internal/client/commands/game.go | 511 ++++++++++++++++++ internal/client/commands/registry.go | 194 +++++++ internal/client/display/board.go | 54 ++ internal/client/display/colors.go | 19 + internal/client/display/format.go | 17 + internal/client/session/session.go | 44 ++ internal/{ => server}/board/board.go | 7 +- internal/{ => server}/core/api.go | 2 +- internal/{ => server}/core/error.go | 2 +- internal/{ => server}/core/player.go | 2 +- internal/{ => server}/core/state.go | 2 +- internal/{ => server}/engine/engine.go | 2 +- internal/{ => server}/game/game.go | 8 +- internal/{ => server}/http/auth.go | 4 +- internal/{ => server}/http/handler.go | 9 +- internal/{ => server}/http/middleware.go | 5 +- internal/{ => server}/http/validator.go | 5 +- internal/{ => server}/processor/command.go | 4 +- internal/{ => server}/processor/processor.go | 14 +- internal/{ => server}/processor/queue.go | 8 +- internal/{ => server}/service/game.go | 8 +- internal/{ => server}/service/service.go | 6 +- internal/{ => server}/service/user.go | 4 +- internal/{ => server}/service/waiter.go | 4 +- internal/{ => server}/storage/game.go | 2 +- internal/{ => server}/storage/schema.go | 2 +- internal/{ => server}/storage/storage.go | 4 +- internal/{ => server}/storage/user.go | 2 +- internal/{ => server}/webserver/server.go | 1 + internal/{ => server}/webserver/web/app.js | 2 +- .../{ => server}/webserver/web/index.html | 2 +- internal/{ => server}/webserver/web/style.css | 2 +- test/run-test-server.sh | 32 +- test/test-api.sh | 4 +- test/test-db.sh | 32 +- test/test-longpoll.sh | 2 +- 52 files changed, 2260 insertions(+), 157 deletions(-) create mode 100644 Makefile create mode 100644 cmd/chess-client/main.go rename cmd/{chessd => chess-server}/cli/cli.go (99%) rename cmd/{chessd => chess-server}/main.go (93%) rename cmd/{chessd => chess-server}/pid.go (96%) create mode 100644 doc/client.md create mode 100644 internal/client/api/client.go create mode 100644 internal/client/api/types.go create mode 100644 internal/client/commands/auth.go create mode 100644 internal/client/commands/debug.go create mode 100644 internal/client/commands/game.go create mode 100644 internal/client/commands/registry.go create mode 100644 internal/client/display/board.go create mode 100644 internal/client/display/colors.go create mode 100644 internal/client/display/format.go create mode 100644 internal/client/session/session.go rename internal/{ => server}/board/board.go (92%) rename internal/{ => server}/core/api.go (96%) rename internal/{ => server}/core/error.go (87%) rename internal/{ => server}/core/player.go (96%) rename internal/{ => server}/core/state.go (91%) rename internal/{ => server}/engine/engine.go (98%) rename internal/{ => server}/game/game.go (94%) rename internal/{ => server}/http/auth.go (98%) rename internal/{ => server}/http/handler.go (98%) rename internal/{ => server}/http/middleware.go (94%) rename internal/{ => server}/http/validator.go (97%) rename internal/{ => server}/processor/command.go (94%) rename internal/{ => server}/processor/processor.go (98%) rename internal/{ => server}/processor/queue.go (95%) rename internal/{ => server}/service/game.go (96%) rename internal/{ => server}/service/service.go (92%) rename internal/{ => server}/service/user.go (97%) rename internal/{ => server}/service/waiter.go (96%) rename internal/{ => server}/storage/game.go (98%) rename internal/{ => server}/storage/schema.go (98%) rename internal/{ => server}/storage/storage.go (97%) rename internal/{ => server}/storage/user.go (98%) rename internal/{ => server}/webserver/server.go (97%) rename internal/{ => server}/webserver/web/app.js (99%) rename internal/{ => server}/webserver/web/index.html (98%) rename internal/{ => server}/webserver/web/style.css (99%) diff --git a/.gitignore b/.gitignore index f7e2abd..efbeb62 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ log bin db build.sh +.chess_history + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ad157f0 --- /dev/null +++ b/Makefile @@ -0,0 +1,159 @@ +# Makefile for chess server and client + +# Variables +BINARY_DIR := bin +SERVER_BINARY := $(BINARY_DIR)/chess-server +CLIENT_BINARY := $(BINARY_DIR)/chess-client +SERVER_SOURCE := ./cmd/chess-server +CLIENT_SOURCE := ./cmd/chess-client +GO := go +GOFLAGS := -trimpath +LDFLAGS := -s -w + +# Build info +GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") +BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S') +VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null || echo "dev") + +# Default target +.PHONY: all +all: build + +# Build both binaries +.PHONY: build +build: server client + +# Build server only +.PHONY: server +server: $(SERVER_BINARY) + +$(SERVER_BINARY): $(BINARY_DIR) + $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(SERVER_BINARY) $(SERVER_SOURCE) + @echo "Built server: $(SERVER_BINARY)" + +# Build client only +.PHONY: client +client: $(CLIENT_BINARY) + +$(CLIENT_BINARY): $(BINARY_DIR) + $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(CLIENT_BINARY) $(CLIENT_SOURCE) + @echo "Built client: $(CLIENT_BINARY)" + +# Create bin directory +$(BINARY_DIR): + @mkdir -p $(BINARY_DIR) + +# Run server with default settings +.PHONY: run-server +run-server: server + $(SERVER_BINARY) -api-port 8080 -dev -storage-path db/chess.db + +# Run server with web UI +.PHONY: run-server-web +run-server-web: server + $(SERVER_BINARY) -api-port 8080 -dev -storage-path db/chess.db -serve -web-port 9090 + +# Run client +.PHONY: run-client +run-client: client + $(CLIENT_BINARY) + +# Run tests (start server and run test scripts) +.PHONY: test +test: server + test/run-test-server.sh + +# Run individual test suites +.PHONY: test-api +test-api: + test/test-api.sh + +.PHONY: test-db +test-db: + test/test-db.sh + +.PHONY: test-longpoll +test-longpoll: + test/test-longpoll.sh + +# Database operations +.PHONY: db-init +db-init: server + $(SERVER_BINARY) db init -path db/chess.db + +.PHONY: db-clean +db-clean: + # ☣ DESTRUCTIVE: Removes database + rm -f db/chess.db db/chess.db-* + +# Development build (with race detector) +.PHONY: dev +dev: + $(GO) build -race -o $(SERVER_BINARY) $(SERVER_SOURCE) + $(GO) build -race -o $(CLIENT_BINARY) $(CLIENT_SOURCE) + @echo "Built with race detector enabled" + +# Clean build artifacts +.PHONY: clean +clean: + rm -f $(SERVER_BINARY) $(CLIENT_BINARY) + rm -rf $(BINARY_DIR) + @echo "Cleaned build artifacts" + +# Install dependencies +.PHONY: deps +deps: + $(GO) mod download + $(GO) mod verify + +# Update dependencies +.PHONY: deps-update +deps-update: + $(GO) get -u ./... + $(GO) mod tidy + +# Format code +.PHONY: fmt +fmt: + $(GO) fmt ./... + +# Run linter +.PHONY: lint +lint: + golangci-lint run ./... + +# Show help +.PHONY: help +help: + @echo "Chess Build System" + @echo "" + @echo "Build targets:" + @echo " make build Build both server and client" + @echo " make server Build server only" + @echo " make client Build client only" + @echo " make dev Build with race detector" + @echo "" + @echo "Run targets:" + @echo " make run-server Run server (port 8080, dev mode)" + @echo " make run-server-web Run server with web UI (ports 8080/9090)" + @echo " make run-client Run client" + @echo "" + @echo "Test targets:" + @echo " make test Run all tests" + @echo " make test-api Run API tests" + @echo " make test-db Run database tests" + @echo " make test-longpoll Run long-poll tests" + @echo "" + @echo "Database targets:" + @echo " make db-init Initialize database" + @echo " make db-clean Remove database (destructive)" + @echo "" + @echo "Maintenance:" + @echo " make clean Remove build artifacts" + @echo " make deps Download dependencies" + @echo " make deps-update Update dependencies" + @echo " make fmt Format code" + @echo " make lint Run linter" + +# Default make without arguments shows help +.DEFAULT_GOAL := help \ No newline at end of file diff --git a/README.md b/README.md index 2b8e772..b6bf7eb 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

Go Chess API

- Go + Go License

@@ -30,7 +30,7 @@ Go backend server providing a RESTful API for chess gameplay with user authentic ## Requirements -- Go 1.24+ +- Go 1.25+ - Stockfish chess engine (`stockfish` in PATH) - SQLite3 (for persistence features) @@ -44,24 +44,45 @@ pkg install stockfish ``` ## Quick Start + +### Using Make (Recommended) +```bash +git clone https://github.com/lixenwraith/chess +cd chess +make build + +# Standard mode with persistence and auth +make run-server + +# Or run with web UI +make run-server-web + +# Initialize database with user support +make db-init + +# Add users via CLI +./bin/chess-server db user add -path db/chess.db -username alice -password AlicePass123 +``` + +### Building Manually ```bash #git clone https://git.lixen.com/lixen/chess # Mirror git clone https://github.com/lixenwraith/chess cd chess -go build ./cmd/chessd +go build ./cmd/chess-server # Standard mode with persistence and auth -./chessd -storage-path chess.db +./chess-server -storage-path chess.db # Development mode with all features -./chessd -dev -storage-path chess.db -pid /tmp/chessd.pid -pid-lock -port 9090 +./chess-server -dev -storage-path chess.db -pid /tmp/chess-server.pid -pid-lock -port 9090 # Initialize database with user support -./chessd db init -path chess.db +./chess-server db init -path chess.db # Add users via CLI -./chessd db user add -path chess.db -username alice -password AlicePass123 -./chessd db user list -path chess.db +./chess-server db user add -path chess.db -username alice -password AlicePass123 +./chess-server db user list -path chess.db ``` Server listens on `http://localhost:8080`. See [API Reference](./doc/api.md) for endpoints including authentication. @@ -73,28 +94,28 @@ The chess server supports user accounts with secure authentication: ### Creating Users ```bash # Add user with password -./chessd db user add -path chess.db -username alice -email alice@example.com -password SecurePass123 +./chess-server db user add -path chess.db -username alice -email alice@example.com -password SecurePass123 # Interactive password prompt -./chessd db user add -path chess.db -username bob -interactive +./chess-server db user add -path chess.db -username bob -interactive # Import with existing hash -./chessd db user add -path chess.db -username charlie -hash '$argon2id$...' +./chess-server db user add -path chess.db -username charlie -hash '$argon2id$...' ``` ### Managing Users ```bash # List all users -./chessd db user list -path chess.db +./chess-server db user list -path chess.db # Update password -./chessd db user set-password -path chess.db -username alice -password NewPass456 +./chess-server db user set-password -path chess.db -username alice -password NewPass456 # Update email -./chessd db user set-email -path chess.db -username alice -email newemail@example.com +./chess-server db user set-email -path chess.db -username alice -email newemail@example.com # Delete user -./chessd db user delete -path chess.db -username alice +./chess-server db user delete -path chess.db -username alice ``` ## Web UI @@ -104,13 +125,13 @@ The chess server includes an embedded web UI for playing games through a browser ### Enabling Web UI ```bash # Start with web UI on default port 9090 -./chessd -serve +./chess-server -serve # Custom web UI port -./chessd -serve -web-port 3000 -web-host 0.0.0.0 +./chess-server -serve -web-port 3000 -web-host 0.0.0.0 # Full example with authentication enabled -./chessd -dev -serve -web-port 9090 -api-port 8080 -storage-path chess.db +./chess-server -dev -serve -web-port 9090 -api-port 8080 -storage-path chess.db ``` ### Features @@ -130,6 +151,7 @@ Access the UI at `http://localhost:9090` when server is running with `-serve` fl - [API Reference](./doc/api.md) - Endpoint specifications including auth - [Architecture](./doc/architecture.md) - System design with auth layer - [Development](./doc/development.md) - Build, test, and user management +- [Client Guide](./doc/client.md) - Interactive debugging client - [Stockfish Integration](./doc/stockfish.md) - Engine communication ## License diff --git a/cmd/chess-client/main.go b/cmd/chess-client/main.go new file mode 100644 index 0000000..c6a3eba --- /dev/null +++ b/cmd/chess-client/main.go @@ -0,0 +1,139 @@ +// FILE: lixenwraith/chess/cmd/chess-client/main.go +// Package main implements an interactive debugging client for the chess server API. +package main + +import ( + "fmt" + "io" + "os" + "strings" + + "chess/internal/client/api" + "chess/internal/client/commands" + "chess/internal/client/display" + "chess/internal/client/session" + + "github.com/chzyer/readline" +) + +func main() { + s := &session.Session{ + APIBaseURL: "http://localhost:8080", + Client: api.New("http://localhost:8080"), + 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() + + 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") + + registry := commands.NewRegistry(s) + + for { + // Build enhanced prompt + prompt := buildPrompt(s) + rl.SetPrompt(prompt) + + line, err := rl.Readline() + if err == io.EOF { + break + } + if err != nil { + continue + } + + line = strings.TrimSpace(line) + if line == "" { + continue + } + + if line == "exit" || line == "quit" || line == "x" { + break + } + + // Check for verbose flag + if strings.HasSuffix(line, " -v") { + s.Verbose = true + line = strings.TrimSuffix(line, " -v") + } else { + s.Verbose = false + } + + registry.Execute(line) + } +} + +func buildPrompt(s *session.Session) string { + parts := []string{} + + // Base + base := "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)) + } + if s.CurrentGame != "" { + parts = append(parts, fmt.Sprintf("%s%s%s", display.White, s.CurrentGame[:8], display.Reset)) + } + + // Add player color if in game + if s.CurrentGameState != nil && s.PlayerColor != "" { + colorText := "" + if s.PlayerColor == "w" { + colorText = display.Blue + "White" + display.Reset + } else { + colorText = display.Red + "Black" + display.Reset + } + 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 := "" + 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) + } 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) + } + promptStr += turnInfo + } + + return display.Prompt(promptStr) +} \ No newline at end of file diff --git a/cmd/chessd/cli/cli.go b/cmd/chess-server/cli/cli.go similarity index 99% rename from cmd/chessd/cli/cli.go rename to cmd/chess-server/cli/cli.go index 1bcf917..7ebf887 100644 --- a/cmd/chessd/cli/cli.go +++ b/cmd/chess-server/cli/cli.go @@ -1,4 +1,4 @@ -// FILE: cmd/chessd/cli/cli.go +// FILE: lixenwraith/chess/cmd/chess-server/cli/cli.go package cli import ( @@ -10,7 +10,7 @@ import ( "text/tabwriter" "time" - "chess/internal/storage" + "chess/internal/server/storage" "github.com/google/uuid" "github.com/lixenwraith/auth" diff --git a/cmd/chessd/main.go b/cmd/chess-server/main.go similarity index 93% rename from cmd/chessd/main.go rename to cmd/chess-server/main.go index 67f91fb..1293cd4 100644 --- a/cmd/chessd/main.go +++ b/cmd/chess-server/main.go @@ -1,4 +1,6 @@ -// FILE: cmd/chessd/main.go +// FILE: lixenwraith/chess/cmd/chess-server/main.go +// Package main implements the chess server application with RESTful API, +// user authentication, and optional web UI serving capabilities. package main import ( @@ -12,12 +14,12 @@ import ( "syscall" "time" - "chess/cmd/chessd/cli" - "chess/internal/http" - "chess/internal/processor" - "chess/internal/service" - "chess/internal/storage" - "chess/internal/webserver" + "chess/cmd/chess-server/cli" + "chess/internal/server/http" + "chess/internal/server/processor" + "chess/internal/server/service" + "chess/internal/server/storage" + "chess/internal/server/webserver" ) const ( diff --git a/cmd/chessd/pid.go b/cmd/chess-server/pid.go similarity index 96% rename from cmd/chessd/pid.go rename to cmd/chess-server/pid.go index 54b659b..fa15533 100644 --- a/cmd/chessd/pid.go +++ b/cmd/chess-server/pid.go @@ -1,4 +1,4 @@ -// FILE: cmd/chessd/pid.go +// FILE: lixenwraith/chess/cmd/chess-server/pid.go package main import ( @@ -10,8 +10,8 @@ import ( "syscall" ) -// managePIDFile creates and manages a PID file with optional locking. -// Returns a cleanup function that must be called on exit. +// managePIDFile creates and manages a PID file with optional locking +// Returns a cleanup function that must be called on exit func managePIDFile(path string, lock bool) (func(), error) { // Open/create PID file with exclusive create first attempt file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) diff --git a/doc/architecture.md b/doc/architecture.md index 8a290b5..2c68003 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -29,6 +29,7 @@ SQLite persistence with async writes for games, synchronous writes for authentic - **Board** (`internal/board`): FEN parsing and ASCII generation - **Core** (`internal/core`): Shared types, API models, error constants - **CLI** (`cmd/chessd/cli`): Database and user management commands +- **Client** (`cmd/chess-client`, `internal/client`): Interactive debugging client with command registry, session management, and colored terminal output ## Request Flow diff --git a/doc/client.md b/doc/client.md new file mode 100644 index 0000000..803c67a --- /dev/null +++ b/doc/client.md @@ -0,0 +1,291 @@ +# Chess Client Documentation + +The chess client is an interactive command-line debugging tool for the chess server API. It provides a rich terminal interface with colored output, command completion, and session management. + +## Features + +- Interactive shell with readline support and command history +- Comprehensive command set for game operations and debugging +- User authentication with JWT token management +- Colored board visualization and game state display +- Session persistence across commands +- Verbose mode for detailed API request/response inspection +- Long-polling support for real-time game updates + +## Building +```bash +go build ./cmd/chess-client +``` + +## Running +```bash +# Connect to default server (localhost:8080) +./chess-client + +# The client starts with an interactive prompt +chess > +``` + +### Prompt States + +The prompt dynamically updates to show context: +- `chess` - Base prompt +- `chess [username]` - Authenticated user +- `chess [username - gameId]` - User in active game +- `chess [username - gameId] White` - Shows player color +- Turn indicator with player type (h=human, c=computer) + +## Command Reference + +### Authentication Commands + +#### `register` / `r` +Register new user account. +``` +chess > register +Username: alice +Password: ******** +Email (optional): alice@example.com +``` + +#### `login` / `l` +Authenticate with existing account. +``` +chess > login +Username or Email: alice +Password: ******** +``` + +#### `logout` / `o` +Clear authentication token. +``` +chess > logout +``` + +#### `whoami` / `i` +Display current authenticated user. +``` +chess > whoami +``` + +#### `user` / `e` +Set user ID manually (for display only, doesn't authenticate). +``` +chess > user 550e8400-e29b-41d4-a716-446655440000 +``` + +### Game Commands + +#### `new` / `n` +Create new game interactively. +``` +chess > new +White player type (h/c) [h]: h +Black player type (h/c) [h]: c +Computer level (0-20) [10]: 15 +Search time (100-10000ms) [1000]: 2000 +Starting position (FEN) [default]: +``` + +#### `join` / `j` +Set current game context. +``` +chess > join a1b2c3d4-e5f6-7890-1234-567890abcdef +``` + +#### `move` / `m` +Make chess move in UCI notation. +``` +chess > move e2e4 +chess > move e7e5 +``` + +#### `computer` / `c` +Trigger computer move calculation. +``` +chess > computer +``` + +#### `undo` / `u` +Undo one or more moves. +``` +chess > undo # Undo last move +chess > undo 3 # Undo last 3 moves +``` + +#### `show` / `h` +Display board and game state with colored pieces. +``` +chess > show +``` + +#### `state` / `s` +Show raw game JSON response. +``` +chess > state +``` + +#### `delete` / `d` +Delete game from server. +``` +chess > delete # Delete current game +chess > delete # Delete specific game +``` + +#### `poll` / `p` +Long-poll for game updates (waits up to 25 seconds). +``` +chess > poll +``` + +### Debug Commands + +#### `health` / `.` +Check server health status. +``` +chess > health +``` + +#### `url` / `/` +Get or set API base URL. +``` +chess > url # Show current URL +chess > url http://localhost:9090 # Change server URL +``` + +#### `raw` / `:` +Send raw API request. +``` +chess > raw GET /api/v1/games/ +chess > raw POST /api/v1/games '{"white":{"type":1},"black":{"type":2}}' +``` + +#### `clear` / `-` +Clear terminal screen. +``` +chess > clear +``` + +#### `help` / `?` +Display available commands or specific command usage. +``` +chess > help # Show all commands +chess > help move # Show move command details +``` + +#### `exit` / `x` +Exit the client. +``` +chess > exit +``` + +### Verbose Mode + +Append `-v` flag to any command for detailed output: +``` +chess > move e2e4 -v +``` + +Shows full HTTP request/response with formatted JSON bodies. + +## Session Management + +The client maintains session state including: +- API base URL +- Current game ID +- Authentication token +- Username and user ID +- Last move count (for polling) +- Current game state +- Player color assignment + +Session state persists across commands within the same client instance. + +## Display Features + +### Colored Output +- **Blue**: White pieces, API requests, prompts +- **Red**: Black pieces, errors +- **Green**: Success messages +- **Yellow**: Warnings, input prompts +- **Magenta**: Computer moves, usernames +- **Cyan**: Information, file coordinates +- **White**: Game IDs + +### Board Visualization +ASCII board with colored pieces: +``` + a b c d e f g h +8 r n b q k b n r 8 +7 p p p p p p p p 7 +6 . . . . . . . . 6 +5 . . . . . . . . 5 +4 . . . . P . . . 4 +3 . . . . . . . . 3 +2 P P P P . P P P 2 +1 R N B Q K B N R 1 + a b c d e f g h +``` + +### Move History +Displayed in algebraic notation with move numbers: +``` +History: 1.e4 e5 2.Nf3 Nc6 3.Bb5 +``` + +## Workflows + +### Authenticated Game Creation +1. Register or login to obtain token +2. Create game (human players automatically associated with user) +3. Make moves with authentication context +4. Games persist with user association + +### Computer vs Computer Observation +1. Create game with both players as computer +2. Use polling to watch moves in real-time +3. Board updates automatically as engines calculate + +### Debug Server Testing +1. Set verbose mode for request inspection +2. Use raw command for custom API calls +3. Health endpoint for connectivity verification +4. URL switching for multi-server testing + +## Configuration + +### Environment +- History file: `.chess_history` in working directory +- Default API URL: `http://localhost:8080` +- Timeout: 30 seconds for HTTP requests + +### Limitations +- No move validation in client (server validates) +- No algebraic notation input (UCI format only) +- Single game context at a time +- No persistent authentication across restarts + +## Error Handling + +The client displays server errors with color coding: +- Red text for error messages +- Error codes for specific failures +- Details when available + +Common error codes: +- `GAME_NOT_FOUND` - Invalid game ID +- `INVALID_MOVE` - Illegal chess move +- `NOT_HUMAN_TURN` - Wrong player type +- `GAME_OVER` - Game already ended +- `RATE_LIMIT_EXCEEDED` - Too many requests + +## Development + +The client architecture follows a command pattern with: +- **Registry**: Central command dispatcher +- **Session**: State management interface +- **API Client**: HTTP communication layer +- **Display**: Terminal formatting utilities +- **Commands**: Modular command handlers + +Extensions can add new commands by registering handlers in the appropriate command group (game, auth, debug). \ No newline at end of file diff --git a/doc/development.md b/doc/development.md index 0167fa9..4fb7972 100644 --- a/doc/development.md +++ b/doc/development.md @@ -13,7 +13,8 @@ #git clone https://git.lixen.com/lixen/chess # Mirror git clone https://github.com/lixenwraith/chess cd chess -go build ./cmd/chessd +go build ./cmd/chess-server +go build ./cmd/chess-client ``` ## Running @@ -118,30 +119,39 @@ go build ./cmd/chessd ## Project Structure ``` chess/ -├── cmd/chessd/ -│ ├── main.go # Entry point with auth initialization -│ ├── pid.go # PID file management -│ └── cli/ # Database and user CLI +├── cmd/ +│ ├── chess-server/ # Server app +│ │ ├── main.go # Server entry point +│ │ ├── pid.go # PID file management +│ │ └── cli/ # Database and user CLI +│ └── chess-client/ # Client app +│ └── main.go # Interactive debugging client ├── internal/ -│ ├── board/ # FEN/ASCII operations -│ ├── core/ # Shared types and API models -│ ├── engine/ # Stockfish UCI wrapper -│ ├── game/ # Game state with player associations -│ ├── http/ # Fiber handlers and auth endpoints -│ │ ├── handler.go # Game endpoints -│ │ ├── auth.go # Authentication endpoints -│ │ └── middleware.go # JWT validation -│ ├── processor/ # Command processing with user context -│ ├── service/ # State and user management -│ │ ├── service.go # Core service -│ │ ├── game.go # Game operations -│ │ └── user.go # User and auth operations -│ └── storage/ # SQLite persistence -│ ├── storage.go # Async writer for games -│ ├── game.go # Game persistence -│ ├── user.go # User persistence (synchronous) -│ └── schema.go # Database schema -└── test/ # Test scripts +│ ├── client/ # Client components +│ │ ├── api/ # HTTP client for server API +│ │ ├── commands/ # Command registry and handlers +│ │ ├── display/ # Terminal output formatting +│ │ └── session/ # Session state management +│ └── server/ # Server components +│ ├── board/ # FEN/ASCII operations +│ ├── core/ # Shared types and API models +│ ├── engine/ # Stockfish UCI wrapper +│ ├── game/ # Game state with player associations +│ ├── http/ # Fiber handlers and auth endpoints +│ │ ├── handler.go # Game endpoints +│ │ ├── auth.go # Authentication endpoints +│ │ └── middleware.go # JWT validation +│ ├── processor/ # Command processing with user context +│ ├── service/ # State and user management +│ │ ├── service.go # Core service +│ │ ├── game.go # Game operations +│ │ └── user.go # User and auth operations +│ └── storage/ # SQLite persistence +│ ├── storage.go # Async writer for games +│ ├── game.go # Game persistence +│ ├── user.go # User persistence (synchronous) +│ └── schema.go # Database schema +└── test/ # Test scripts ``` ## Testing diff --git a/go.mod b/go.mod index aaa08a7..b1203c7 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,15 @@ module chess -go 1.25.3 +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 github.com/lixenwraith/auth v0.0.0-20251104131016-e5a810f4e226 github.com/mattn/go-sqlite3 v1.14.32 - golang.org/x/term v0.36.0 + golang.org/x/term v0.37.0 ) require ( @@ -28,7 +29,7 @@ require ( github.com/tinylib/msgp v1.5.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.68.0 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/crypto v0.44.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect ) diff --git a/go.sum b/go.sum index b870437..234b84c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ 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= @@ -50,14 +56,15 @@ github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFn github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/client/api/client.go b/internal/client/api/client.go new file mode 100644 index 0000000..58bf0f0 --- /dev/null +++ b/internal/client/api/client.go @@ -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) +} \ No newline at end of file diff --git a/internal/client/api/types.go b/internal/client/api/types.go new file mode 100644 index 0000000..1d230db --- /dev/null +++ b/internal/client/api/types.go @@ -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"` +} \ No newline at end of file diff --git a/internal/client/commands/auth.go b/internal/client/commands/auth.go new file mode 100644 index 0000000..0d1aa8e --- /dev/null +++ b/internal/client/commands/auth.go @@ -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 ", + 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 := 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 +} \ No newline at end of file diff --git a/internal/client/commands/debug.go b/internal/client/commands/debug.go new file mode 100644 index 0000000..b3bfb84 --- /dev/null +++ b/internal/client/commands/debug.go @@ -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 [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 [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() +} \ No newline at end of file diff --git a/internal/client/commands/game.go b/internal/client/commands/game.go new file mode 100644 index 0000000..2eb4b5f --- /dev/null +++ b/internal/client/commands/game.go @@ -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 ", + Handler: joinGameHandler, + }) + + r.Register(&Command{ + Name: "move", + ShortName: "m", + Description: "Make a move", + Usage: "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 := 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 ") + } + + gameID := s.GetCurrentGame() + if gameID == "" { + return fmt.Errorf("no current game, use 'new' or 'join '") + } + + 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 '") + } + + 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 '") + } + + 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 '") + } + + 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 '") + } + + 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 '") + } + + 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 +} \ No newline at end of file diff --git a/internal/client/commands/registry.go b/internal/client/commands/registry.go new file mode 100644 index 0000000..12884bc --- /dev/null +++ b/internal/client/commands/registry.go @@ -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 ' 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 +} \ No newline at end of file diff --git a/internal/client/display/board.go b/internal/client/display/board.go new file mode 100644 index 0000000..7f9ca80 --- /dev/null +++ b/internal/client/display/board.go @@ -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 +} \ No newline at end of file diff --git a/internal/client/display/colors.go b/internal/client/display/colors.go new file mode 100644 index 0000000..ee483e1 --- /dev/null +++ b/internal/client/display/colors.go @@ -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 +} \ No newline at end of file diff --git a/internal/client/display/format.go b/internal/client/display/format.go new file mode 100644 index 0000000..c6d124c --- /dev/null +++ b/internal/client/display/format.go @@ -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)) +} \ No newline at end of file diff --git a/internal/client/session/session.go b/internal/client/session/session.go new file mode 100644 index 0000000..61784b0 --- /dev/null +++ b/internal/client/session/session.go @@ -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 } \ No newline at end of file diff --git a/internal/board/board.go b/internal/server/board/board.go similarity index 92% rename from internal/board/board.go rename to internal/server/board/board.go index b9b38f5..b09a423 100644 --- a/internal/board/board.go +++ b/internal/server/board/board.go @@ -1,11 +1,11 @@ -// FILE: internal/board/board.go +// FILE: lixenwraith/chess/internal/server/board/board.go package board import ( "fmt" "strings" - "chess/internal/core" + "chess/internal/server/core" ) const ( @@ -21,6 +21,7 @@ type Board struct { fullmove int } +// FromFEN creates a Board from a FEN string with validation func ParseFEN(fen string) (*Board, error) { parts := strings.Fields(fen) if len(parts) != 6 { @@ -95,7 +96,7 @@ func (b *Board) ToASCII() string { sb.WriteString(fmt.Sprintf("%c ", piece)) } } - sb.WriteString(fmt.Sprintf(" %d\n", 8-r)) + sb.WriteString(fmt.Sprintf("%d\n", 8-r)) } sb.WriteString(" a b c d e f g h") diff --git a/internal/core/api.go b/internal/server/core/api.go similarity index 96% rename from internal/core/api.go rename to internal/server/core/api.go index eb3af41..bac9c97 100644 --- a/internal/core/api.go +++ b/internal/server/core/api.go @@ -1,4 +1,4 @@ -// FILE: internal/core/api.go +// FILE: lixenwraith/chess/internal/server/core/api.go package core // Request types diff --git a/internal/core/error.go b/internal/server/core/error.go similarity index 87% rename from internal/core/error.go rename to internal/server/core/error.go index bace4a5..1fa4ebb 100644 --- a/internal/core/error.go +++ b/internal/server/core/error.go @@ -1,4 +1,4 @@ -// FILE: internal/core/core.go +// FILE: lixenwraith/chess/internal/server/core/error.go package core // Error codes diff --git a/internal/core/player.go b/internal/server/core/player.go similarity index 96% rename from internal/core/player.go rename to internal/server/core/player.go index 1a20d7f..9c3aa22 100644 --- a/internal/core/player.go +++ b/internal/server/core/player.go @@ -1,4 +1,4 @@ -// FILE: internal/core/player.go +// FILE: lixenwraith/chess/internal/server/core/player.go package core import ( diff --git a/internal/core/state.go b/internal/server/core/state.go similarity index 91% rename from internal/core/state.go rename to internal/server/core/state.go index cd2081f..b7d2bac 100644 --- a/internal/core/state.go +++ b/internal/server/core/state.go @@ -1,4 +1,4 @@ -// FILE: internal/core/core.go +// FILE: lixenwraith/chess/internal/server/core/state.go package core type State int diff --git a/internal/engine/engine.go b/internal/server/engine/engine.go similarity index 98% rename from internal/engine/engine.go rename to internal/server/engine/engine.go index ff1df39..a3099d9 100644 --- a/internal/engine/engine.go +++ b/internal/server/engine/engine.go @@ -1,4 +1,4 @@ -// FILE: internal/engine/engine.go +// FILE: lixenwraith/chess/internal/server/engine/engine.go package engine import ( diff --git a/internal/game/game.go b/internal/server/game/game.go similarity index 94% rename from internal/game/game.go rename to internal/server/game/game.go index bbf6e2d..0cbc09e 100644 --- a/internal/game/game.go +++ b/internal/server/game/game.go @@ -1,11 +1,11 @@ -// FILE: internal/game/game.go +// FILE: lixenwraith/chess/internal/server/game/game.go package game import ( "fmt" - "chess/internal/board" - "chess/internal/core" + "chess/internal/server/board" + "chess/internal/server/core" ) type Snapshot struct { @@ -66,10 +66,12 @@ func (g *Game) LastResult() *MoveResult { return g.lastResult } +// CurrentSnapshot returns the latest game snapshot func (g *Game) CurrentSnapshot() Snapshot { return g.snapshots[len(g.snapshots)-1] } +// CurrentFEN returns the current position in FEN notation func (g *Game) CurrentFEN() string { return g.CurrentSnapshot().FEN } diff --git a/internal/http/auth.go b/internal/server/http/auth.go similarity index 98% rename from internal/http/auth.go rename to internal/server/http/auth.go index 9c7f09e..34c3a3f 100644 --- a/internal/http/auth.go +++ b/internal/server/http/auth.go @@ -1,4 +1,4 @@ -// FILE: internal/http/auth.go +// FILE: lixenwraith/chess/internal/server/http/auth.go package http import ( @@ -8,7 +8,7 @@ import ( "time" "unicode" - "chess/internal/core" + "chess/internal/server/core" "github.com/gofiber/fiber/v2" ) diff --git a/internal/http/handler.go b/internal/server/http/handler.go similarity index 98% rename from internal/http/handler.go rename to internal/server/http/handler.go index f59445d..b39c5de 100644 --- a/internal/http/handler.go +++ b/internal/server/http/handler.go @@ -1,4 +1,4 @@ -// FILE: internal/http/handler.go +// FILE: lixenwraith/chess/internal/server/http/handler.go package http import ( @@ -7,9 +7,9 @@ import ( "strings" "time" - "chess/internal/core" - "chess/internal/processor" - "chess/internal/service" + "chess/internal/server/core" + "chess/internal/server/processor" + "chess/internal/server/service" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" @@ -20,6 +20,7 @@ import ( const rateLimitRate = 10 // req/sec +// HTTPHandler handles HTTP requests and routes them to the processor type HTTPHandler struct { proc *processor.Processor svc *service.Service diff --git a/internal/http/middleware.go b/internal/server/http/middleware.go similarity index 94% rename from internal/http/middleware.go rename to internal/server/http/middleware.go index 92f35c8..559d7a5 100644 --- a/internal/http/middleware.go +++ b/internal/server/http/middleware.go @@ -1,11 +1,10 @@ -// FILE: internal/http/middleware.go +// FILE: lixenwraith/chess/internal/server/http/middleware.go package http import ( + "chess/internal/server/core" "strings" - "chess/internal/core" - "github.com/gofiber/fiber/v2" ) diff --git a/internal/http/validator.go b/internal/server/http/validator.go similarity index 97% rename from internal/http/validator.go rename to internal/server/http/validator.go index 659b248..6569402 100644 --- a/internal/http/validator.go +++ b/internal/server/http/validator.go @@ -1,13 +1,12 @@ -// FILE: internal/http/handler.go +// FILE: lixenwraith/chess/internal/server/http/handler.go package http import ( + "chess/internal/server/core" "fmt" "reflect" "strings" - "chess/internal/core" - "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/google/uuid" diff --git a/internal/processor/command.go b/internal/server/processor/command.go similarity index 94% rename from internal/processor/command.go rename to internal/server/processor/command.go index 89e993e..1113da0 100644 --- a/internal/processor/command.go +++ b/internal/server/processor/command.go @@ -1,8 +1,8 @@ -// FILE: internal/processor/command.go +// FILE: lixenwraith/chess/internal/server/processor/command.go package processor import ( - "chess/internal/core" + "chess/internal/server/core" ) // CommandType defines the type of command being executed diff --git a/internal/processor/processor.go b/internal/server/processor/processor.go similarity index 98% rename from internal/processor/processor.go rename to internal/server/processor/processor.go index 0ba7a10..2a1618c 100644 --- a/internal/processor/processor.go +++ b/internal/server/processor/processor.go @@ -1,4 +1,4 @@ -// FILE: internal/processor/processor.go +// FILE: lixenwraith/chess/internal/server/processor/processor.go package processor @@ -11,11 +11,11 @@ import ( "time" "unicode" - "chess/internal/board" - "chess/internal/core" - "chess/internal/engine" - "chess/internal/game" - "chess/internal/service" + "chess/internal/server/board" + "chess/internal/server/core" + "chess/internal/server/engine" + "chess/internal/server/game" + "chess/internal/server/service" ) const ( @@ -25,7 +25,7 @@ const ( // FEN validation regex var fenPattern = regexp.MustCompile(`^[rnbqkpRNBQKP1-8/]+ [wb] [KQkq-]+ [a-h1-8-]+ \d+ \d+$`) -// Processor orchestrates all game logic and engine interactions +// Processor handles command execution and coordinates between service and engine layers type Processor struct { svc *service.Service queue *EngineQueue diff --git a/internal/processor/queue.go b/internal/server/processor/queue.go similarity index 95% rename from internal/processor/queue.go rename to internal/server/processor/queue.go index d542d48..4534a19 100644 --- a/internal/processor/queue.go +++ b/internal/server/processor/queue.go @@ -1,4 +1,4 @@ -// FILE: internal/processor/queue.go +// FILE: lixenwraith/chess/internal/server/processor/queue.go package processor import ( @@ -7,11 +7,11 @@ import ( "sync" "time" - "chess/internal/core" - "chess/internal/engine" + "chess/internal/server/core" + "chess/internal/server/engine" ) -// EngineTask represents a computer move calculation request +// EngineTask contains computer move calculation request and response channel type EngineTask struct { GameID string FEN string diff --git a/internal/service/game.go b/internal/server/service/game.go similarity index 96% rename from internal/service/game.go rename to internal/server/service/game.go index c6985f4..1302bdf 100644 --- a/internal/service/game.go +++ b/internal/server/service/game.go @@ -1,13 +1,13 @@ -// FILE: internal/service/game.go +// FILE: lixenwraith/chess/internal/server/service/game.go package service import ( "fmt" "time" - "chess/internal/core" - "chess/internal/game" - "chess/internal/storage" + "chess/internal/server/core" + "chess/internal/server/game" + "chess/internal/server/storage" "github.com/google/uuid" ) diff --git a/internal/service/service.go b/internal/server/service/service.go similarity index 92% rename from internal/service/service.go rename to internal/server/service/service.go index 9e045c7..0f0c091 100644 --- a/internal/service/service.go +++ b/internal/server/service/service.go @@ -1,4 +1,4 @@ -// FILE: internal/service/service.go +// FILE: lixenwraith/chess/internal/server/service/service.go package service import ( @@ -8,8 +8,8 @@ import ( "sync" "time" - "chess/internal/game" - "chess/internal/storage" + "chess/internal/server/game" + "chess/internal/server/storage" ) // Service is a pure state manager for chess games with optional persistence diff --git a/internal/service/user.go b/internal/server/service/user.go similarity index 97% rename from internal/service/user.go rename to internal/server/service/user.go index f45ba15..ca258df 100644 --- a/internal/service/user.go +++ b/internal/server/service/user.go @@ -1,4 +1,4 @@ -// FILE: internal/service/user.go +// FILE: lixenwraith/chess/internal/server/service/user.go package service import ( @@ -6,7 +6,7 @@ import ( "strings" "time" - "chess/internal/storage" + "chess/internal/server/storage" "github.com/google/uuid" "github.com/lixenwraith/auth" diff --git a/internal/service/waiter.go b/internal/server/service/waiter.go similarity index 96% rename from internal/service/waiter.go rename to internal/server/service/waiter.go index cd533d7..0e40bcb 100644 --- a/internal/service/waiter.go +++ b/internal/server/service/waiter.go @@ -1,4 +1,4 @@ -// FILE: internal/service/waiter.go +// FILE: lixenwraith/chess/internal/server/service/waiter.go package service import ( @@ -16,7 +16,7 @@ const ( WaitChannelBuffer = 1 ) -// WaitRegistry manages long-polling clients waiting for game state changes +// WaitRegistry manages clients waiting for game state changes via long-polling type WaitRegistry struct { mu sync.RWMutex waiters map[string][]*WaitRequest // gameID → waiting clients diff --git a/internal/storage/game.go b/internal/server/storage/game.go similarity index 98% rename from internal/storage/game.go rename to internal/server/storage/game.go index 7a5ae5d..7cbd4ae 100644 --- a/internal/storage/game.go +++ b/internal/server/storage/game.go @@ -1,4 +1,4 @@ -// FILE: internal/storage/game.go +// FILE: lixenwraith/chess/internal/server/storage/game.go package storage import ( diff --git a/internal/storage/schema.go b/internal/server/storage/schema.go similarity index 98% rename from internal/storage/schema.go rename to internal/server/storage/schema.go index ff0507a..7a7e235 100644 --- a/internal/storage/schema.go +++ b/internal/server/storage/schema.go @@ -1,4 +1,4 @@ -// FILE: internal/storage/schema.go +// FILE: lixenwraith/chess/internal/server/storage/schema.go package storage import "time" diff --git a/internal/storage/storage.go b/internal/server/storage/storage.go similarity index 97% rename from internal/storage/storage.go rename to internal/server/storage/storage.go index 05b236a..cfd7f78 100644 --- a/internal/storage/storage.go +++ b/internal/server/storage/storage.go @@ -1,4 +1,4 @@ -// FILE: internal/storage/storage.go +// FILE: lixenwraith/chess/internal/server/storage/storage.go package storage import ( @@ -70,7 +70,7 @@ func NewStore(dataSourceName string, devMode bool) (*Store, error) { return s, nil } -// IsHealthy returns the current health status +// IsHealthy returns true if the storage is operational func (s *Store) IsHealthy() bool { return s.healthStatus.Load() } diff --git a/internal/storage/user.go b/internal/server/storage/user.go similarity index 98% rename from internal/storage/user.go rename to internal/server/storage/user.go index 47546e1..db0a3a3 100644 --- a/internal/storage/user.go +++ b/internal/server/storage/user.go @@ -1,4 +1,4 @@ -// FILE: internal/storage/user.go +// FILE: lixenwraith/chess/internal/server/storage/user.go package storage import ( diff --git a/internal/webserver/server.go b/internal/server/webserver/server.go similarity index 97% rename from internal/webserver/server.go rename to internal/server/webserver/server.go index 877b6e3..5ec4830 100644 --- a/internal/webserver/server.go +++ b/internal/server/webserver/server.go @@ -1,3 +1,4 @@ +// FILE: lixenwraith/chess/internal/server/webserver/server.go package webserver import ( diff --git a/internal/webserver/web/app.js b/internal/server/webserver/web/app.js similarity index 99% rename from internal/webserver/web/app.js rename to internal/server/webserver/web/app.js index 7202e27..d4d527f 100644 --- a/internal/webserver/web/app.js +++ b/internal/server/webserver/web/app.js @@ -1,4 +1,4 @@ -// FILE: internal/webserver/web/app.js +// FILE: lixenwraith/chess/internal/server/webserver/web/app.js // Game state management let gameState = { gameId: null, diff --git a/internal/webserver/web/index.html b/internal/server/webserver/web/index.html similarity index 98% rename from internal/webserver/web/index.html rename to internal/server/webserver/web/index.html index 33953a2..b1deb45 100644 --- a/internal/webserver/web/index.html +++ b/internal/server/webserver/web/index.html @@ -1,4 +1,4 @@ - + diff --git a/internal/webserver/web/style.css b/internal/server/webserver/web/style.css similarity index 99% rename from internal/webserver/web/style.css rename to internal/server/webserver/web/style.css index 731cde3..b217d29 100644 --- a/internal/webserver/web/style.css +++ b/internal/server/webserver/web/style.css @@ -1,4 +1,4 @@ -/* FILE: internal/webserver/web/style.css */ +/* FILE: lixenwraith/chess/internal/server/webserver/web/style.css */ * { margin: 0; padding: 0; diff --git a/test/run-test-server.sh b/test/run-test-server.sh index 3bbf5f5..7de5dcc 100755 --- a/test/run-test-server.sh +++ b/test/run-test-server.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# FILE: test/run-test-server.sh +# FILE: lixenwraith/chess/test/run-test-server.sh set -e # Configuration -CHESSD_EXEC=${1:-"./chessd"} +CHESS_SERVER_EXEC=${1:-"bin/chess-server"} TEST_DB="test.db" -PID_FILE="/tmp/chessd_test.pid" +PID_FILE="/tmp/chess-server_test.pid" API_PORT=${API_PORT:-8080} # Colors for output @@ -17,10 +17,10 @@ CYAN='\033[0;36m' NC='\033[0m' # Check executable -if [ ! -x "$CHESSD_EXEC" ]; then - echo -e "${RED}Error: chessd executable not found or not executable: $CHESSD_EXEC${NC}" - echo "Provide the path to chessd binary as first argument or place it in the current directory." - echo "Build the binary if not available: go build ./cmd/chessd" +if [ ! -x "$CHESS_SERVER_EXEC" ]; then + echo -e "${RED}Error: chess-server executable not found or not executable: $CHESS_SERVER_EXEC${NC}" + echo "Provide the path to chess-server binary as first argument or place it in the current directory." + echo "Build the binary if not available: go build ./cmd/chess-server" exit 1 fi @@ -32,7 +32,7 @@ cleanup() { if [ -f "$PID_FILE" ]; then PID=$(cat "$PID_FILE") if kill -0 "$PID" 2>/dev/null; then - echo "Stopping chessd server (PID: $PID)" + echo "Stopping chess-server server (PID: $PID)" kill "$PID" 2>/dev/null || true sleep 0.5 kill -9 "$PID" 2>/dev/null || true @@ -56,7 +56,7 @@ rm -f "$TEST_DB" "${TEST_DB}-wal" "${TEST_DB}-shm" "$PID_FILE" # Initialize database echo -e "${CYAN}Initializing test database...${NC}" -"$CHESSD_EXEC" db init -path "$TEST_DB" +"$CHESS_SERVER_EXEC" db init -path "$TEST_DB" if [ $? -ne 0 ]; then echo -e "${RED}Failed to initialize database${NC}" exit 1 @@ -64,14 +64,14 @@ fi # Add test users echo -e "${CYAN}Adding test users...${NC}" -"$CHESSD_EXEC" db user add -path "$TEST_DB" \ +"$CHESS_SERVER_EXEC" db user add -path "$TEST_DB" \ -username alice -email alice@test.com -password AlicePass123 if [ $? -ne 0 ]; then echo -e "${RED}Failed to create user alice${NC}" exit 1 fi -"$CHESSD_EXEC" db user add -path "$TEST_DB" \ +"$CHESS_SERVER_EXEC" db user add -path "$TEST_DB" \ -username bob -email bob@test.com -password BobSecure456 if [ $? -ne 0 ]; then echo -e "${RED}Failed to create user bob${NC}" @@ -84,15 +84,15 @@ echo " • bob / BobSecure456" # Start server echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}" -echo -e "${GREEN}║ Chess API Test Server with User Management ║${NC}" +echo -e "${GREEN}║ Chess API Test Server with User Management ║${NC}" echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}" echo "" echo "Configuration:" -echo " Executable: $CHESSD_EXEC" +echo " Executable: $CHESS_SERVER_EXEC" echo " Database: $TEST_DB" echo " Port: $API_PORT" echo " Mode: Development (WAL enabled, relaxed rate limits)" -echo " Purpose: Backend for chessd tests" +echo " Purpose: Backend for chess-server tests" echo " PID File: $PID_FILE" echo "" echo -e "${YELLOW}Instructions:${NC}" @@ -104,8 +104,8 @@ echo -e "${CYAN}───────────────────── echo "Starting server..." echo "" -# Start chessd in foreground with dev mode and storage -"$CHESSD_EXEC" \ +# Start chess-server in foreground with dev mode and storage +"$CHESS_SERVER_EXEC" \ -dev \ -storage-path "$TEST_DB" \ -api-port "$API_PORT" \ diff --git a/test/test-api.sh b/test/test-api.sh index cb50331..f144cee 100755 --- a/test/test-api.sh +++ b/test/test-api.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# FILE: test/test-api.sh +# FILE: lixenwraith/chess/test/test-api.sh # Chess API Robustness Test Suite # Tests the refactored chess API with security hardening @@ -134,7 +134,7 @@ echo "Server: $BASE_URL" echo "API Version: v1" echo -e "${MAGENTA} IMPORTANT: Server must be started with -dev flag for tests to pass!${NC}" echo -e "${MAGENTA} Start the server first: test/run-test-server.sh${NC}" -echo -e "${MAGENTA} Or directly after build: ./chessd -dev${NC}" +echo -e "${MAGENTA} Or directly after build: bin/chess-server -dev${NC}" echo "" echo "Starting comprehensive tests..." diff --git a/test/test-db.sh b/test/test-db.sh index 79e86fa..a870ee6 100755 --- a/test/test-db.sh +++ b/test/test-db.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# FILE: test/test-db.sh +# FILE: lixenwraith/chess/test/test-db.sh # Database & Authentication API Integration Test Suite # Tests user operations, authentication, and persistence via HTTP API @@ -10,7 +10,7 @@ BASE_URL="http://localhost:8080" API_URL="${BASE_URL}/api/v1" TEST_DB="test.db" -CHESSD_EXEC=${1:-"./chessd"} +CHESS_SERVER_EXEC=${1:-"bin/chess-server"} API_DELAY=${API_DELAY:-50} # Colors @@ -130,8 +130,8 @@ for cmd in jq sqlite3 curl; do done # Check executable exists -if [ ! -x "$CHESSD_EXEC" ]; then - echo -e "${RED}Error: chessd executable not found or not executable: $CHESSD_EXEC${NC}" +if [ ! -x "$CHESS_SERVER_EXEC" ]; then + echo -e "${RED}Error: chess-server executable not found or not executable: $CHESS_SERVER_EXEC${NC}" exit 1 fi @@ -145,7 +145,7 @@ fi # Start tests print_header "Database & User Management Test Suite" echo "Server: $BASE_URL" -echo "Executable: $CHESSD_EXEC" +echo "Executable: $CHESS_SERVER_EXEC" echo "Test Database (server-managed): $TEST_DB" echo "" @@ -154,11 +154,11 @@ print_header "SECTION 1: CLI User Operations" # ============================================================================== test_case "1.1: database initialization" -assert_command "$CHESSD_EXEC db init -path $TEST_DB" 0 "initialize database" +assert_command "$CHESS_SERVER_EXEC db init -path $TEST_DB" 0 "initialize database" # Create testuser1 first (not charlie) test_case "1.2: Add First User via CLI" -OUTPUT=$($CHESSD_EXEC db user add -path "$TEST_DB" -username "testuser1" \ +OUTPUT=$($CHESS_SERVER_EXEC db user add -path "$TEST_DB" -username "testuser1" \ -email "testuser1@test.com" -password "TestPass123" 2>&1) if echo "$OUTPUT" | grep -qi "User created successfully"; then echo -e "${GREEN} ✓ User created: testuser1${NC}" @@ -169,7 +169,7 @@ else fi test_case "1.3: Add Second User" -OUTPUT=$($CHESSD_EXEC db user add -path "$TEST_DB" -username "testuser2" \ +OUTPUT=$($CHESS_SERVER_EXEC db user add -path "$TEST_DB" -username "testuser2" \ -password "TestPass456" 2>&1) if echo "$OUTPUT" | grep -qi "User created successfully"; then echo -e "${GREEN} ✓ User created: testuser2${NC}" @@ -181,7 +181,7 @@ fi # Now test duplicate prevention with an existing user test_case "1.4: Duplicate Username Prevention" -assert_command "$CHESSD_EXEC db user add -path $TEST_DB -username testuser1 -password TestPass789" 1 \ +assert_command "$CHESS_SERVER_EXEC db user add -path $TEST_DB -username testuser1 -password TestPass789" 1 \ "Duplicate username rejected" test_case "1.5: Login with Case-Insensitive Username (ALICE)" @@ -197,11 +197,11 @@ else fi test_case "1.6: Update User Email" -assert_command "$CHESSD_EXEC db user set-email -path $TEST_DB -username testuser2 -email testuser2_updated@test.com" 0 \ +assert_command "$CHESS_SERVER_EXEC db user set-email -path $TEST_DB -username testuser2 -email testuser2_updated@test.com" 0 \ "Email update" test_case "1.7: Update User Password" -assert_command "$CHESSD_EXEC db user set-password -path $TEST_DB -username testuser2 -password NewPass789" 0 \ +assert_command "$CHESS_SERVER_EXEC db user set-password -path $TEST_DB -username testuser2 -password NewPass789" 0 \ "Password update" test_case "2.1: Health Check" @@ -349,7 +349,7 @@ print_header "SECTION 5: Password Operations" # Now TEST_PASS2_NEW is defined, this test should work test_case "5.1: Update User Password via CLI for 'bob'" -assert_command "$CHESSD_EXEC db user set-password -path $TEST_DB -username $TEST_USER2 -password $TEST_PASS2_NEW" 0 \ +assert_command "$CHESS_SERVER_EXEC db user set-password -path $TEST_DB -username $TEST_USER2 -password $TEST_PASS2_NEW" 0 \ "CLI password update for '$TEST_USER2'" test_case "5.2: Login with NEW Password for 'bob'" @@ -372,11 +372,11 @@ STATUS=$(api_request POST "$API_URL/auth/login" \ assert_status 401 "$STATUS" "Old password correctly rejected for '$TEST_USER2'" test_case "5.4: Add new user '$TEST_USER_CLI' via CLI for hash test" -assert_command "$CHESSD_EXEC db user add -path $TEST_DB -username $TEST_USER_CLI -password $TEST_PASS_CLI" 0 \ +assert_command "$CHESS_SERVER_EXEC db user add -path $TEST_DB -username $TEST_USER_CLI -password $TEST_PASS_CLI" 0 \ "Add user '$TEST_USER_CLI' for hash test" test_case "5.5: CLI rejects unsupported hash format" -assert_command "$CHESSD_EXEC db user set-hash -path $TEST_DB -username $TEST_USER_CLI -hash '$UNSUPPORTED_HASH'" 1 \ +assert_command "$CHESS_SERVER_EXEC db user set-hash -path $TEST_DB -username $TEST_USER_CLI -hash '$UNSUPPORTED_HASH'" 1 \ "Unsupported bcrypt hash rejected by CLI" # ============================================================================== @@ -419,11 +419,11 @@ fi test_case "6.3: Delete User" # First, get a user to delete -OUTPUT=$($CHESSD_EXEC db user add -path "$TEST_DB" -username "deleteme" \ +OUTPUT=$($CHESS_SERVER_EXEC db user add -path "$TEST_DB" -username "deleteme" \ -password "TempPass123" 2>&1) TEMP_ID=$(echo "$OUTPUT" | grep "ID:" | awk '{print $2}') -assert_command "$CHESSD_EXEC db user delete -path $TEST_DB -username deleteme" 0 \ +assert_command "$CHESS_SERVER_EXEC db user delete -path $TEST_DB -username deleteme" 0 \ "User deletion by username" # Verify deletion diff --git a/test/test-longpoll.sh b/test/test-longpoll.sh index 1b8e8a8..f2f81e0 100755 --- a/test/test-longpoll.sh +++ b/test/test-longpoll.sh @@ -1,5 +1,5 @@ #!/bin/bash -# test-longpoll.sh - Test long-polling functionality +# FILE: lixenwraith/chess/test/test-longpoll.sh set -e