Skip to content

feat(uci): add adapter seam and convert globals to struct types#118

Draft
CorentinGS wants to merge 1 commit into
fix/annotation-structurefrom
feat/uci-adapter-seam
Draft

feat(uci): add adapter seam and convert globals to struct types#118
CorentinGS wants to merge 1 commit into
fix/annotation-structurefrom
feat/uci-adapter-seam

Conversation

@CorentinGS
Copy link
Copy Markdown
Owner

@CorentinGS CorentinGS commented Jun 3, 2026

Summary

This PR refactors the uci package around an adapter seam so engine communication is no longer hard-wired directly into Engine.

The main goals are:

  • separate UCI subprocess I/O from command parsing and engine state updates
  • make UCI behavior testable without launching a real engine binary
  • replace package-level command globals with concrete command struct types
  • keep Engine.Run(...) as the main execution entry point while routing commands through an Adapter

New API

New adapter interface:

type Adapter interface {
    Exchange(cmd Cmd) ([]string, error)
    Close() error
}

New subprocess adapter:

type SubprocessAdapter struct { ... }

func NewSubprocessAdapter(path string) (*SubprocessAdapter, error)
func (s *SubprocessAdapter) Exchange(cmd Cmd) ([]string, error)
func (s *SubprocessAdapter) Close() error
func (s *SubprocessAdapter) Pid() int

New engine constructor for tests/custom transports:

func NewWithAdapter(adapter Adapter, opts ...func(e *Engine)) *Engine

New fake adapter for tests:

type FakeAdapter struct {
    Responses map[string][]string
}

Example:

fake := &uci.FakeAdapter{
    Responses: map[string][]string{
        "uci": {"id name TestEngine", "uciok"},
        "go":  {"info depth 1 score cp 10 pv e2e4", "bestmove e2e4"},
    },
}

eng := uci.NewWithAdapter(fake)
defer eng.Close()

err := eng.Run(uci.CmdUCI{}, uci.CmdIsReady{}, uci.CmdGo{MoveTime: 100})

Command API Changes

Cmd now describes both command completion and response handling:

type Cmd interface {
    fmt.Stringer
    IsDone(line string) bool
    Handle(lines []string, e *Engine) error
    LockRequired() bool
}

Commands are now concrete struct types instead of package-level command values:

uci.CmdUCI{}
uci.CmdIsReady{}
uci.CmdUCINewGame{}
uci.CmdPonderHit{}
uci.CmdStop{}
uci.CmdQuit{}
uci.CmdEval{}

Existing struct commands remain struct values:

uci.CmdSetOption{Name: "Hash", Value: "128"}
uci.CmdPosition{Position: pos}
uci.CmdGo{MoveTime: time.Second}

Breaking Changes

This is a source-level breaking change for callers using the uci package.

Callers must update global command usage from value references to struct construction:

// before
eng.Run(uci.CmdUCI, uci.CmdIsReady, uci.CmdUCINewGame)

// after
eng.Run(uci.CmdUCI{}, uci.CmdIsReady{}, uci.CmdUCINewGame{})

Custom command implementations must update from:

ProcessResponse(e *Engine) error

to:

IsDone(line string) bool
Handle(lines []string, e *Engine) error
LockRequired() bool

Other compatibility notes:

  • Engine.New(path, opts...) still exists and still launches a subprocess engine.
  • Engine.Run(...) still accepts commands and executes them in order.
  • Engine.Close() now delegates shutdown to the adapter after sending CmdQuit{}.
  • Engine.Getpid() was removed from Engine; subprocess process IDs are now exposed on SubprocessAdapter.Pid().
  • CmdStop, CmdPonderHit, CmdQuit, and CmdSetOption are modeled as commands that do not require the engine mutex.

Implementation Notes

  • Engine now stores an Adapter instead of raw exec.Cmd, stdin pipe, and stdout pipe fields.
  • New(path, opts...) creates a SubprocessAdapter internally, then delegates to NewWithAdapter(...).
  • SubprocessAdapter.Exchange(...) writes the command string and reads response lines until cmd.IsDone(line) returns true.
  • Command response parsing moved from ProcessResponse methods into Handle(lines, e) implementations.
  • Debug logging now logs outbound commands and inbound adapter response lines.
  • FakeAdapter returns deterministic command responses for tests without requiring Stockfish or another UCI binary.

Tests

Added adapter-focused tests covering:

  • CmdUCI ID and option parsing through FakeAdapter
  • CmdGo best move and search info parsing
  • CmdEval parsing
  • MultiPV search info parsing
  • CmdIsReady
  • fire-and-forget commands such as ucinewgame, stop, and ponderhit
  • a full fake UCI game flow

Updated existing UCI engine tests to construct command struct values, for example uci.CmdUCI{} instead of uci.CmdUCI.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an Adapter seam for UCI I/O and refactors the command layer from global variables/functions into struct-based command types, enabling easier testing (including a new fake adapter) and decoupling the engine from subprocess specifics.

Changes:

  • Added an Adapter interface with a SubprocessAdapter implementation and a FakeAdapter for tests.
  • Refactored Engine to communicate via an injected adapter (NewWithAdapter) instead of directly owning pipes/process.
  • Redefined Cmd to use IsDone/Handle/LockRequired and converted previously-global commands into struct types.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
uci/fake.go Adds FakeAdapter to return canned responses keyed by command verb.
uci/engine.go Refactors Engine to use an injected Adapter; adds NewWithAdapter; updates command execution flow.
uci/engine_test.go Updates tests to use the new struct command types (CmdUCI{} etc.).
uci/cmd.go Redesigns the Cmd interface and rewrites built-in commands as struct types with adapter-driven response handling.
uci/adapter.go Introduces Adapter and SubprocessAdapter encapsulating subprocess + scan/write exchange.
uci/adapter_test.go Adds unit tests around FakeAdapter + adapter-driven command handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread uci/cmd.go
Comment on lines +100 to 102
func (CmdEval) IsDone(line string) bool {
return strings.HasPrefix(line, "Final evaluation")
}
Comment thread uci/cmd.go Outdated
}
func (CmdSetOption) IsDone(_ string) bool { return true }

func (CmdSetOption) LockRequired() bool { return false }
Comment thread uci/cmd.go Outdated

func (CmdQuit) IsDone(_ string) bool { return true }

func (CmdQuit) LockRequired() bool { return false }
Comment thread uci/engine.go
Comment on lines 102 to 105
func (e *Engine) Close() error {
if err := e.Run(CmdQuit); err != nil {
return err
}
if err := e.in.Close(); err != nil {
return err
}
if err := e.out.Close(); err != nil {
return err
}
return e.cmd.Process.Kill()
_ = e.Run(CmdQuit{})
return e.adapter.Close()
}
Comment thread uci/adapter_test.go Outdated
}
}

func Test_FakeAdapter_FireAndForgetPathrough(t *testing.T) {
@CorentinGS CorentinGS force-pushed the fix/annotation-structure branch from fdd76b9 to 60f222b Compare June 3, 2026 08:03
@CorentinGS CorentinGS force-pushed the feat/uci-adapter-seam branch from 13ec153 to 93c8d1f Compare June 3, 2026 08:03
@CorentinGS CorentinGS force-pushed the feat/uci-adapter-seam branch from 93c8d1f to 222d597 Compare June 3, 2026 08:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants