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