stockshark 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +104 -0
- data/Rakefile +6 -0
- data/bin/console +8 -0
- data/bin/setup +6 -0
- data/lib/stockshark/configuration.rb +25 -0
- data/lib/stockshark/crashed_error.rb +4 -0
- data/lib/stockshark/engine.rb +124 -0
- data/lib/stockshark/engine_not_found_error.rb +5 -0
- data/lib/stockshark/error.rb +6 -0
- data/lib/stockshark/process.rb +81 -0
- data/lib/stockshark/timeout_error.rb +7 -0
- data/lib/stockshark/uci_protocol.rb +53 -0
- data/lib/stockshark/version.rb +3 -0
- data/lib/stockshark.rb +20 -0
- data/stockshark.gemspec +28 -0
- metadata +68 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8dcf3f23451283338c0ab09bcf73a1d218b40864ec1238950d65ac40698aab17
|
|
4
|
+
data.tar.gz: ac7a4ca6772c90c1f989a4f4fa42d756896570c6bc2ff55495a2bb724a0869f4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8a03cbc0c0e94c066a22278f369f132414020a18f04bb2dddc39259a2b773c4f352f2ef75c6680dc20448255a121b5a3777ab6b20bbae6254520cefaf8cb534e
|
|
7
|
+
data.tar.gz: d1329f806ead3a46f7afc6f6270c9c53785797e7487faca0ab5b9b643cb230e3056c802ddb456d8d967cfbd5bf9963c71f6bfa57efb745ba274a1a48032eb559
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.4.8
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Phil
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Stockshark
|
|
2
|
+
|
|
3
|
+
A small, dependency-free Ruby wrapper for talking to a UCI-speaking chess
|
|
4
|
+
engine (Stockfish, or anything else that implements the protocol) over
|
|
5
|
+
stdin/stdout/stderr pipes: start it, hand it a FEN, get back a structured
|
|
6
|
+
evaluation (centipawns or mate score, best move, principal variation, depth
|
|
7
|
+
reached). No chess rules engine, no board representation, no opinions about
|
|
8
|
+
perspective or move classification — just the UCI protocol, cleanly
|
|
9
|
+
wrapped.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
bundle add stockshark
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or add it to your Gemfile directly:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "stockshark"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
engine = Stockshark::Engine.new
|
|
27
|
+
result = engine.analyze(fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", depth: 18)
|
|
28
|
+
|
|
29
|
+
result.best_move # => "e2e4"
|
|
30
|
+
result.depth # => 18
|
|
31
|
+
result.lines.first.pv # => ["e2e4", "e7e5"]
|
|
32
|
+
engine.quit
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Pass `movetime_ms:` instead of `depth:` for a time-bounded search, and
|
|
36
|
+
`multipv:` to get more than one ranked line back (`result.lines` is sorted
|
|
37
|
+
by rank, so `lines.first` is always the primary line).
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
configuration = Stockshark::Configuration.new(threads: 4, hash_mb: 128)
|
|
43
|
+
engine = Stockshark::Engine.new(configuration)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Every option also has an environment variable default (see
|
|
47
|
+
`lib/stockshark/configuration.rb`) — deliberately named `STOCKFISH_*`, not
|
|
48
|
+
`STOCKSHARK_*`, since they configure the actual engine binary being
|
|
49
|
+
launched rather than this library:
|
|
50
|
+
|
|
51
|
+
- **`STOCKFISH_PATH`** (default `stockfish`) — path to the engine binary,
|
|
52
|
+
resolved via `$PATH` if not absolute.
|
|
53
|
+
- **`STOCKFISH_THREADS`** (default `1`) — UCI `Threads`.
|
|
54
|
+
- **`STOCKFISH_HASH`** (default `16`) — UCI `Hash`, transposition table
|
|
55
|
+
size in MB.
|
|
56
|
+
- **`STOCKFISH_MAX_ANALYZE_MS`** (default `30000`) — how long the engine
|
|
57
|
+
is given to respond before `Stockshark::TimeoutError` is raised.
|
|
58
|
+
|
|
59
|
+
## Errors
|
|
60
|
+
|
|
61
|
+
All raised errors descend from `Stockshark::Error`:
|
|
62
|
+
|
|
63
|
+
- `Stockshark::EngineNotFoundError` — the configured binary doesn't exist,
|
|
64
|
+
isn't executable, or couldn't be spawned.
|
|
65
|
+
- `Stockshark::TimeoutError` — the engine didn't respond within the
|
|
66
|
+
configured deadline.
|
|
67
|
+
- `Stockshark::CrashedError` — the engine process exited mid-conversation.
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
|
72
|
+
Then:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
bundle exec rspec # fake-engine suite, no binary required
|
|
76
|
+
bundle exec rspec --tag stockfish # also runs the real-engine integration spec
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The `--tag stockfish` run needs an actual Stockfish binary on `$PATH` (or
|
|
80
|
+
pointed to via `STOCKFISH_PATH`) — it's skipped automatically otherwise
|
|
81
|
+
(see `spec/support/stockfish_availability.rb`). Install one with:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
brew install stockfish # macOS
|
|
85
|
+
apt install stockfish # Debian/Ubuntu
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
or download a build from [stockfishchess.org/download](https://stockfishchess.org/download).
|
|
89
|
+
|
|
90
|
+
Run `bin/console` for an interactive prompt with the gem already loaded.
|
|
91
|
+
|
|
92
|
+
To install this gem onto your local machine, run `bundle exec rake
|
|
93
|
+
install`. To release a new version, bump the version in
|
|
94
|
+
`lib/stockshark/version.rb`, then run `bundle exec rake release`, which
|
|
95
|
+
tags the version, pushes the commit and tag, and pushes the `.gem` file to
|
|
96
|
+
rubygems.org.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT — see `LICENSE.txt`.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
Built by [TrueFlux](https://trueflux.agency).
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Stockshark
|
|
2
|
+
# Environment-driven configuration — nothing here is ever a hardcoded,
|
|
3
|
+
# machine-specific path. Every value has a sensible default so the
|
|
4
|
+
# library works out of the box locally, but production is expected to
|
|
5
|
+
# set these explicitly (particularly STOCKFISH_PATH, since "stockfish"
|
|
6
|
+
# resolving via $PATH isn't guaranteed on every deploy target). Env var
|
|
7
|
+
# names stay STOCKFISH_* (not STOCKSHARK_*) deliberately — they configure
|
|
8
|
+
# the actual Stockfish (or Stockfish-compatible) binary being launched,
|
|
9
|
+
# which is a separate thing from what this gem happens to be called.
|
|
10
|
+
class Configuration
|
|
11
|
+
attr_accessor :path, :threads, :hash_mb, :max_analyze_ms
|
|
12
|
+
|
|
13
|
+
def initialize(
|
|
14
|
+
path: ENV.fetch("STOCKFISH_PATH", "stockfish"),
|
|
15
|
+
threads: Integer(ENV.fetch("STOCKFISH_THREADS", 1)),
|
|
16
|
+
hash_mb: Integer(ENV.fetch("STOCKFISH_HASH", 16)),
|
|
17
|
+
max_analyze_ms: Integer(ENV.fetch("STOCKFISH_MAX_ANALYZE_MS", 30_000))
|
|
18
|
+
)
|
|
19
|
+
@path = path
|
|
20
|
+
@threads = threads
|
|
21
|
+
@hash_mb = hash_mb
|
|
22
|
+
@max_analyze_ms = max_analyze_ms
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
module Stockshark
|
|
2
|
+
# The public façade for talking to a UCI engine — the only class most
|
|
3
|
+
# callers should ever need to touch. Hides process management (Process)
|
|
4
|
+
# and protocol parsing (UciProtocol) entirely: callers only ever see
|
|
5
|
+
# "give me a FEN and a depth/movetime, get back an evaluation."
|
|
6
|
+
class Engine
|
|
7
|
+
# One multipv rank's worth of a search result: its own score (exactly
|
|
8
|
+
# one of score_cp/score_mate is ever set, matching UCI itself — an
|
|
9
|
+
# engine never reports both for the same line) and principal variation.
|
|
10
|
+
Line = Struct.new(:multipv, :score_cp, :score_mate, :pv, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
# The full result of one #analyze call: the move the engine would
|
|
13
|
+
# actually play, the deepest ply reached, and one Line per multipv
|
|
14
|
+
# rank requested (ranks are 1-indexed and sorted, so lines.first is
|
|
15
|
+
# always the primary/best line). Raw and side-to-move-relative —
|
|
16
|
+
# unopinionated about which color that was.
|
|
17
|
+
AnalysisResult = Struct.new(:best_move, :depth, :lines, keyword_init: true)
|
|
18
|
+
|
|
19
|
+
attr_reader :name
|
|
20
|
+
|
|
21
|
+
def initialize(configuration = Stockshark::Configuration.new, process: nil)
|
|
22
|
+
@configuration = configuration
|
|
23
|
+
@process = process || Stockshark::Process.new(configuration.path)
|
|
24
|
+
@multipv = 1
|
|
25
|
+
handshake
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def analyze(fen:, depth: nil, movetime_ms: nil, multipv: 1)
|
|
29
|
+
raise ArgumentError, "must give depth or movetime_ms" unless depth || movetime_ms
|
|
30
|
+
|
|
31
|
+
set_multipv(multipv)
|
|
32
|
+
send_line("position fen #{fen}")
|
|
33
|
+
send_line(depth ? "go depth #{depth}" : "go movetime #{movetime_ms}")
|
|
34
|
+
|
|
35
|
+
collect_result(search_timeout(movetime_ms))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Safe to call more than once, and safe to call even if the engine was
|
|
39
|
+
# never fully brought up (e.g. handshake itself failed).
|
|
40
|
+
def quit
|
|
41
|
+
@process.stop
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def handshake
|
|
47
|
+
send_line("uci")
|
|
48
|
+
await("uciok") { |message| @name = Stockshark::UciProtocol.parse_id(message)&.fetch(:name) || @name }
|
|
49
|
+
|
|
50
|
+
send_line("setoption name Threads value #{@configuration.threads}")
|
|
51
|
+
send_line("setoption name Hash value #{@configuration.hash_mb}")
|
|
52
|
+
send_line("isready")
|
|
53
|
+
await("readyok")
|
|
54
|
+
|
|
55
|
+
send_line("ucinewgame")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def set_multipv(multipv)
|
|
59
|
+
return if multipv == @multipv
|
|
60
|
+
|
|
61
|
+
send_line("setoption name MultiPV value #{multipv}")
|
|
62
|
+
@multipv = multipv
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def send_line(line)
|
|
66
|
+
@process.write_line(line)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Reads lines (via the configured handshake timeout) until `stop_line`
|
|
70
|
+
# is seen, yielding every line along the way so the caller can pick up
|
|
71
|
+
# anything useful (like "id name ...") that appears before it.
|
|
72
|
+
def await(stop_line)
|
|
73
|
+
deadline = Time.now + (@configuration.max_analyze_ms / 1000.0)
|
|
74
|
+
loop do
|
|
75
|
+
message = read(deadline)
|
|
76
|
+
yield message if block_given?
|
|
77
|
+
return if message == stop_line
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def collect_result(timeout)
|
|
82
|
+
deadline = Time.now + timeout
|
|
83
|
+
latest_by_rank = {}
|
|
84
|
+
max_depth = 0
|
|
85
|
+
|
|
86
|
+
loop do
|
|
87
|
+
message = read(deadline)
|
|
88
|
+
|
|
89
|
+
if (info = Stockshark::UciProtocol.parse_info(message))
|
|
90
|
+
latest_by_rank[info[:multipv]] = info
|
|
91
|
+
max_depth = [ max_depth, info[:depth] ].max
|
|
92
|
+
elsif (best = Stockshark::UciProtocol.parse_bestmove(message))
|
|
93
|
+
lines = latest_by_rank.sort.map do |_, info|
|
|
94
|
+
Line.new(multipv: info[:multipv], score_cp: info[:score_cp], score_mate: info[:score_mate], pv: info[:pv])
|
|
95
|
+
end
|
|
96
|
+
return AnalysisResult.new(best_move: best[:best_move], depth: max_depth, lines: lines)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Blocks until a line arrives on stdout or `deadline` passes, skipping
|
|
102
|
+
# (but not erroring on) stderr activity — Process already drains it
|
|
103
|
+
# continuously in the background, this just isn't interested in its
|
|
104
|
+
# content, only in not being blocked by it.
|
|
105
|
+
def read(deadline)
|
|
106
|
+
loop do
|
|
107
|
+
remaining = deadline - Time.now
|
|
108
|
+
raise Stockshark::TimeoutError, "engine did not respond in time" if remaining <= 0
|
|
109
|
+
raise Stockshark::CrashedError, "engine process exited unexpectedly" unless @process.alive?
|
|
110
|
+
|
|
111
|
+
entry = @process.read_line(timeout: remaining)
|
|
112
|
+
next if entry.nil?
|
|
113
|
+
next if entry[:stream] == :stderr
|
|
114
|
+
|
|
115
|
+
return entry[:line]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def search_timeout(movetime_ms)
|
|
120
|
+
base = movetime_ms ? movetime_ms / 1000.0 : 0
|
|
121
|
+
base + (@configuration.max_analyze_ms / 1000.0)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
module Stockshark
|
|
4
|
+
# Raw OS process lifecycle for a UCI-speaking engine — spawns it, writes
|
|
5
|
+
# lines to its stdin, and continuously drains stdout *and* stderr from
|
|
6
|
+
# dedicated reader threads so neither pipe can ever fill up and block the
|
|
7
|
+
# child. Draining stderr matters here specifically because the engine
|
|
8
|
+
# path is operator-configurable: nothing guarantees the binary behind it
|
|
9
|
+
# is a "pure" build that never writes to stderr, and if it did while
|
|
10
|
+
# nobody was reading that pipe, the child would block writing to it and
|
|
11
|
+
# every read on stdout would hang right along with it.
|
|
12
|
+
#
|
|
13
|
+
# Zero chess/UCI knowledge on purpose — Engine owns the protocol, this
|
|
14
|
+
# only owns "a subprocess and its pipes."
|
|
15
|
+
class Process
|
|
16
|
+
def initialize(path)
|
|
17
|
+
@stdin, stdout, stderr, @wait_thread = Open3.popen3(path)
|
|
18
|
+
@queue = Queue.new
|
|
19
|
+
@readers = [ reader_thread(stdout, :stdout), reader_thread(stderr, :stderr) ]
|
|
20
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
21
|
+
raise Stockshark::EngineNotFoundError, "could not start engine at #{path.inspect}: #{e.message}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def write_line(line)
|
|
25
|
+
raise Stockshark::CrashedError, "engine process is not running" unless alive?
|
|
26
|
+
|
|
27
|
+
@stdin.puts(line)
|
|
28
|
+
rescue Errno::EPIPE, IOError => e
|
|
29
|
+
raise Stockshark::CrashedError, "could not write to engine: #{e.message}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Pops the next available line from either stream, tagged with which
|
|
33
|
+
# one it came from, waiting at most `timeout` seconds. Returns nil on
|
|
34
|
+
# timeout — never raises Timeout::Error, deliberately: cancellation
|
|
35
|
+
# here is just "give up waiting on the queue," not "interrupt whatever
|
|
36
|
+
# is actually blocking," which is what makes Ruby's Timeout.timeout
|
|
37
|
+
# unsafe to wrap around real IO.
|
|
38
|
+
def read_line(timeout:)
|
|
39
|
+
@queue.pop(timeout: timeout)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def alive?
|
|
43
|
+
@wait_thread.alive?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Safe to call more than once, and safe to call after the process has
|
|
47
|
+
# already died on its own.
|
|
48
|
+
def stop
|
|
49
|
+
return unless alive?
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
@stdin.puts("quit")
|
|
53
|
+
rescue Errno::EPIPE, IOError
|
|
54
|
+
# already gone — nothing left to say goodbye to.
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
unless @wait_thread.join(0.5)
|
|
58
|
+
::Process.kill("KILL", @wait_thread.pid)
|
|
59
|
+
@wait_thread.join
|
|
60
|
+
end
|
|
61
|
+
ensure
|
|
62
|
+
@readers.each(&:kill)
|
|
63
|
+
@stdin.close unless @stdin.closed?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def reader_thread(io, tag)
|
|
69
|
+
Thread.new do
|
|
70
|
+
loop do
|
|
71
|
+
line = io.gets
|
|
72
|
+
break if line.nil?
|
|
73
|
+
|
|
74
|
+
@queue << { stream: tag, line: line.chomp }
|
|
75
|
+
end
|
|
76
|
+
rescue IOError
|
|
77
|
+
# stream closed out from under us during shutdown — fine.
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
module Stockshark
|
|
2
|
+
# The engine didn't respond within the configured deadline — either a
|
|
3
|
+
# genuinely wedged search, or a process that's alive but not speaking
|
|
4
|
+
# UCI back (e.g. an unexpected build writing diagnostics to stderr and
|
|
5
|
+
# never reaching readyok/bestmove on stdout).
|
|
6
|
+
class TimeoutError < Error; end
|
|
7
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Stockshark
|
|
2
|
+
# Pure parsing of UCI protocol text into structured Ruby data. No IO, no
|
|
3
|
+
# process — every method here is a plain string in, hash/nil out, which
|
|
4
|
+
# is what makes it trivially unit-testable without ever spawning a real
|
|
5
|
+
# engine. Scores are returned exactly as the engine reports them:
|
|
6
|
+
# relative to whichever side is to move in the position that was
|
|
7
|
+
# analyzed. Flipping that to a fixed perspective is a game-aware decision
|
|
8
|
+
# this module deliberately knows nothing about — that's up to whatever
|
|
9
|
+
# calls this gem.
|
|
10
|
+
module UciProtocol
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# "id name Stockfish 16" => { name: "Stockfish 16" }
|
|
14
|
+
# "id author ..." and any other id line => nil (not what we need).
|
|
15
|
+
def parse_id(line)
|
|
16
|
+
match = line.match(/\Aid name (?<name>.+)\z/)
|
|
17
|
+
return nil unless match
|
|
18
|
+
|
|
19
|
+
{ name: match[:name] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# "bestmove e2e4 ponder e7e5" => { best_move: "e2e4", ponder: "e7e5" }
|
|
23
|
+
def parse_bestmove(line)
|
|
24
|
+
match = line.match(/\Abestmove (?<best_move>\S+)(?: ponder (?<ponder>\S+))?/)
|
|
25
|
+
return nil unless match
|
|
26
|
+
|
|
27
|
+
{ best_move: match[:best_move], ponder: match[:ponder] }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# "info depth 20 seldepth 28 multipv 1 score cp 34 nodes 12345 pv e2e4 e7e5 g1f3"
|
|
31
|
+
# => { depth: 20, multipv: 1, score_cp: 34, score_mate: nil, pv: ["e2e4", "e7e5", "g1f3"] }
|
|
32
|
+
# "info depth 12 multipv 2 score mate -3 pv ... "
|
|
33
|
+
# => { depth: 12, multipv: 2, score_cp: nil, score_mate: -3, pv: [...] }
|
|
34
|
+
# Returns nil for info lines that don't carry a scored principal
|
|
35
|
+
# variation (plain "info string ..." diagnostics, "info currmove ...",
|
|
36
|
+
# etc.) — callers should just skip those.
|
|
37
|
+
def parse_info(line)
|
|
38
|
+
return nil unless line.start_with?("info ") && line.include?(" pv ")
|
|
39
|
+
|
|
40
|
+
depth = line[/\bdepth (\d+)/, 1]
|
|
41
|
+
pv = line[/ pv (.+)\z/, 1]
|
|
42
|
+
return nil unless depth && pv
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
depth: depth.to_i,
|
|
46
|
+
multipv: (line[/\bmultipv (\d+)/, 1] || "1").to_i,
|
|
47
|
+
score_cp: line[/\bscore cp (-?\d+)/, 1]&.to_i,
|
|
48
|
+
score_mate: line[/\bscore mate (-?\d+)/, 1]&.to_i,
|
|
49
|
+
pv: pv.split(" ")
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/stockshark.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require_relative "stockshark/version"
|
|
2
|
+
require_relative "stockshark/error"
|
|
3
|
+
require_relative "stockshark/engine_not_found_error"
|
|
4
|
+
require_relative "stockshark/timeout_error"
|
|
5
|
+
require_relative "stockshark/crashed_error"
|
|
6
|
+
require_relative "stockshark/configuration"
|
|
7
|
+
require_relative "stockshark/process"
|
|
8
|
+
require_relative "stockshark/uci_protocol"
|
|
9
|
+
require_relative "stockshark/engine"
|
|
10
|
+
|
|
11
|
+
# Stockshark::Engine talks to a UCI chess engine (Stockfish, or anything
|
|
12
|
+
# else that speaks the protocol) over stdin/stdout/stderr pipes. Plain Ruby,
|
|
13
|
+
# stdlib only (Open3) — no chess rules engine, no board representation, no
|
|
14
|
+
# opinions about evaluation perspective or move classification, so it stays
|
|
15
|
+
# usable by anything that just needs "FEN in, evaluation out" regardless of
|
|
16
|
+
# what it's built on top of. No autoloader here (unlike when this lived
|
|
17
|
+
# inside a Rails app under Zeitwerk), so every file is required explicitly
|
|
18
|
+
# above, in dependency order.
|
|
19
|
+
module Stockshark
|
|
20
|
+
end
|
data/stockshark.gemspec
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require_relative 'lib/stockshark/version'
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = 'stockshark'
|
|
5
|
+
spec.version = Stockshark::VERSION
|
|
6
|
+
spec.authors = ['Phil Brockwell']
|
|
7
|
+
spec.email = ['phil@trueflux.agency']
|
|
8
|
+
|
|
9
|
+
spec.summary = 'A small, dependency-free Ruby wrapper for talking to a UCI chess engine.'
|
|
10
|
+
spec.description = <<~DESC
|
|
11
|
+
Stockshark drives any UCI-speaking chess engine (Stockfish, or anything
|
|
12
|
+
else that implements the protocol) over stdin/stdout/stderr pipes: start
|
|
13
|
+
it, hand it a FEN, get back a structured evaluation (centipawns or mate
|
|
14
|
+
score, best move, principal variation, depth reached). No chess rules
|
|
15
|
+
engine, no board representation, no opinions about perspective or move
|
|
16
|
+
classification — just the UCI protocol, cleanly wrapped.
|
|
17
|
+
DESC
|
|
18
|
+
spec.homepage = 'https://github.com/TrueFlux/stockshark'
|
|
19
|
+
spec.license = 'MIT'
|
|
20
|
+
spec.required_ruby_version = '>= 3.2.0'
|
|
21
|
+
|
|
22
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
23
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
24
|
+
|
|
25
|
+
spec.files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") }
|
|
26
|
+
.reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: stockshark
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Phil Brockwell
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
Stockshark drives any UCI-speaking chess engine (Stockfish, or anything
|
|
14
|
+
else that implements the protocol) over stdin/stdout/stderr pipes: start
|
|
15
|
+
it, hand it a FEN, get back a structured evaluation (centipawns or mate
|
|
16
|
+
score, best move, principal variation, depth reached). No chess rules
|
|
17
|
+
engine, no board representation, no opinions about perspective or move
|
|
18
|
+
classification — just the UCI protocol, cleanly wrapped.
|
|
19
|
+
email:
|
|
20
|
+
- phil@trueflux.agency
|
|
21
|
+
executables: []
|
|
22
|
+
extensions: []
|
|
23
|
+
extra_rdoc_files: []
|
|
24
|
+
files:
|
|
25
|
+
- ".gitignore"
|
|
26
|
+
- ".rspec"
|
|
27
|
+
- ".ruby-version"
|
|
28
|
+
- Gemfile
|
|
29
|
+
- LICENSE.txt
|
|
30
|
+
- README.md
|
|
31
|
+
- Rakefile
|
|
32
|
+
- bin/console
|
|
33
|
+
- bin/setup
|
|
34
|
+
- lib/stockshark.rb
|
|
35
|
+
- lib/stockshark/configuration.rb
|
|
36
|
+
- lib/stockshark/crashed_error.rb
|
|
37
|
+
- lib/stockshark/engine.rb
|
|
38
|
+
- lib/stockshark/engine_not_found_error.rb
|
|
39
|
+
- lib/stockshark/error.rb
|
|
40
|
+
- lib/stockshark/process.rb
|
|
41
|
+
- lib/stockshark/timeout_error.rb
|
|
42
|
+
- lib/stockshark/uci_protocol.rb
|
|
43
|
+
- lib/stockshark/version.rb
|
|
44
|
+
- stockshark.gemspec
|
|
45
|
+
homepage: https://github.com/TrueFlux/stockshark
|
|
46
|
+
licenses:
|
|
47
|
+
- MIT
|
|
48
|
+
metadata:
|
|
49
|
+
homepage_uri: https://github.com/TrueFlux/stockshark
|
|
50
|
+
source_code_uri: https://github.com/TrueFlux/stockshark
|
|
51
|
+
rdoc_options: []
|
|
52
|
+
require_paths:
|
|
53
|
+
- lib
|
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: 3.2.0
|
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '0'
|
|
64
|
+
requirements: []
|
|
65
|
+
rubygems_version: 3.6.9
|
|
66
|
+
specification_version: 4
|
|
67
|
+
summary: A small, dependency-free Ruby wrapper for talking to a UCI chess engine.
|
|
68
|
+
test_files: []
|