v0.7.0 cli client with readline added, directory structure updated

This commit is contained in:
2025-11-13 08:55:06 -05:00
parent 52868af4ea
commit 6bdc061508
52 changed files with 2260 additions and 157 deletions

2
.gitignore vendored
View File

@ -4,3 +4,5 @@ log
bin
db
build.sh
.chess_history

159
Makefile Normal file
View 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

View File

@ -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
View 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)
}

View File

@ -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"

View File

@ -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 (

View File

@ -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)

View File

@ -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
View 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).

View File

@ -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,11 +119,20 @@ 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/
│ ├── 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

11
go.mod
View File

@ -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
View File

@ -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=

View 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)
}

View 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"`
}

View 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
}

View 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()
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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))
}

View 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 }

View File

@ -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")

View File

@ -1,4 +1,4 @@
// FILE: internal/core/api.go
// FILE: lixenwraith/chess/internal/server/core/api.go
package core
// Request types

View File

@ -1,4 +1,4 @@
// FILE: internal/core/core.go
// FILE: lixenwraith/chess/internal/server/core/error.go
package core
// Error codes

View File

@ -1,4 +1,4 @@
// FILE: internal/core/player.go
// FILE: lixenwraith/chess/internal/server/core/player.go
package core
import (

View File

@ -1,4 +1,4 @@
// FILE: internal/core/core.go
// FILE: lixenwraith/chess/internal/server/core/state.go
package core
type State int

View File

@ -1,4 +1,4 @@
// FILE: internal/engine/engine.go
// FILE: lixenwraith/chess/internal/server/engine/engine.go
package engine
import (

View File

@ -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
}

View File

@ -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"
)

View File

@ -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

View File

@ -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"
)

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"
)

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -1,4 +1,4 @@
// FILE: internal/storage/game.go
// FILE: lixenwraith/chess/internal/server/storage/game.go
package storage
import (

View File

@ -1,4 +1,4 @@
// FILE: internal/storage/schema.go
// FILE: lixenwraith/chess/internal/server/storage/schema.go
package storage
import "time"

View File

@ -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()
}

View File

@ -1,4 +1,4 @@
// FILE: internal/storage/user.go
// FILE: lixenwraith/chess/internal/server/storage/user.go
package storage
import (

View File

@ -1,3 +1,4 @@
// FILE: lixenwraith/chess/internal/server/webserver/server.go
package webserver
import (

View File

@ -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,

View File

@ -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>

View File

@ -1,4 +1,4 @@
/* FILE: internal/webserver/web/style.css */
/* FILE: lixenwraith/chess/internal/server/webserver/web/style.css */
* {
margin: 0;
padding: 0;

View File

@ -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}"
@ -88,11 +88,11 @@ echo -e "${GREEN}║ Chess API Test Server with User Management ║
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" \

View File

@ -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..."

View File

@ -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

View File

@ -1,5 +1,5 @@
#!/bin/bash
# test-longpoll.sh - Test long-polling functionality
# FILE: lixenwraith/chess/test/test-longpoll.sh
set -e