重构: aiohttp 全异步 + chessdb 云库 + uv 管理
This commit is contained in:
commit
8d996b8c5f
13 files changed
+1726
No files matched your search
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""皮卡鱼异步封装(asyncio 子进程, 替代旧的同步实现)。"""
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
ENGINE_DIRS = [
|
||||
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'Pikafish-2026-09-06'),
|
||||
]
|
||||
NNUE_FILE = 'pikafish.nnue'
|
||||
|
||||
|
||||
def find_engine():
|
||||
"""按当前平台挑选引擎二进制: Windows 优先 .exe, 其次 Linux/Mac。"""
|
||||
import platform
|
||||
system = platform.system() # Windows / Linux / Darwin
|
||||
for base in ENGINE_DIRS:
|
||||
if not os.path.isdir(base):
|
||||
continue
|
||||
candidates = []
|
||||
for name in sorted(os.listdir(base)):
|
||||
path = os.path.join(base, name)
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
lower = name.lower()
|
||||
if 'pikafish' not in lower or lower.endswith(('.nnue', '.txt', '.md')):
|
||||
continue
|
||||
if system == 'Windows':
|
||||
ok = lower.endswith('.exe') and 'android' not in lower
|
||||
elif system == 'Darwin':
|
||||
ok = 'macos' in lower
|
||||
else:
|
||||
ok = 'linux' in lower and 'android' not in lower
|
||||
if ok:
|
||||
candidates.append(path)
|
||||
if candidates:
|
||||
return candidates[0], base
|
||||
return None, None
|
||||
|
||||
|
||||
class Pikafish:
|
||||
"""异步 UCI 引擎进程。非法局面会导致引擎退出, 调用方负责 restart()。"""
|
||||
|
||||
def __init__(self, threads=None, hash_mb=256, movetime=800, log=None):
|
||||
found, workdir = find_engine()
|
||||
if not found:
|
||||
raise RuntimeError('未找到皮卡鱼引擎二进制')
|
||||
self.path, self.workdir = found, workdir
|
||||
self.threads = threads or os.cpu_count() or 1
|
||||
self.hash_mb = hash_mb
|
||||
self.movetime = movetime
|
||||
self.log = log or (lambda m: None)
|
||||
self._proc = None
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def start(self):
|
||||
self._proc = await asyncio.create_subprocess_exec(
|
||||
self.path, cwd=self.workdir,
|
||||
stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.DEVNULL)
|
||||
await self._send('uci')
|
||||
await self._wait_for('uciok')
|
||||
nnue = os.path.join(self.workdir, NNUE_FILE)
|
||||
opts = [('Threads', str(self.threads)), ('Hash', str(self.hash_mb))]
|
||||
if os.path.isfile(nnue):
|
||||
opts.append(('EvalFile', NNUE_FILE))
|
||||
for k, v in opts:
|
||||
await self._send(f'setoption name {k} value {v}')
|
||||
await self._send('isready')
|
||||
await self._wait_for('readyok')
|
||||
await self._send('ucinewgame')
|
||||
self.log(f'引擎已启动: {os.path.basename(self.path)} '
|
||||
f'(threads={self.threads}, hash={self.hash_mb}MB)')
|
||||
return self
|
||||
|
||||
async def restart(self):
|
||||
await self.quit()
|
||||
await self.start()
|
||||
|
||||
async def _send(self, cmd):
|
||||
self._proc.stdin.write((cmd + '\n').encode())
|
||||
await self._proc.stdin.drain()
|
||||
|
||||
async def _readline(self):
|
||||
line = await self._proc.stdout.readline()
|
||||
if not line:
|
||||
raise RuntimeError('引擎进程已退出')
|
||||
text = line.decode(errors='replace').strip()
|
||||
self.log('<< ' + text)
|
||||
return text
|
||||
|
||||
async def _wait_for(self, token):
|
||||
while True:
|
||||
if token in await self._readline():
|
||||
return
|
||||
|
||||
async def bestmove(self, moves=None, fen=None, movetime=None, depth=None):
|
||||
"""返回 (move, score)。fen 优先于 moves; depth 优先于 movetime。"""
|
||||
async with self._lock:
|
||||
if fen:
|
||||
await self._send(f'position fen {fen}')
|
||||
else:
|
||||
pos = 'position startpos'
|
||||
if moves:
|
||||
pos += ' moves ' + ' '.join(moves)
|
||||
await self._send(pos)
|
||||
if depth:
|
||||
await self._send(f'go depth {depth}')
|
||||
else:
|
||||
await self._send(f'go movetime {movetime or self.movetime}')
|
||||
score = None
|
||||
while True:
|
||||
line = await self._readline()
|
||||
if line.startswith('info') and ' pv ' in line:
|
||||
i = line.find('score ')
|
||||
if i >= 0:
|
||||
parts = line[i:].split()
|
||||
if len(parts) >= 3:
|
||||
score = f'{parts[1]} {parts[2]}'
|
||||
if line.startswith('bestmove'):
|
||||
mv = line.split()[1]
|
||||
if mv in ('(none)', '(null)'):
|
||||
raise ValueError('该局面无可走之子')
|
||||
return mv, score
|
||||
|
||||
async def quit(self):
|
||||
if self._proc and self._proc.returncode is None:
|
||||
try:
|
||||
self._proc.stdin.write(b'quit\n')
|
||||
await self._proc.stdin.drain()
|
||||
await asyncio.wait_for(self._proc.wait(), timeout=5)
|
||||
except Exception:
|
||||
self._proc.kill()
|
||||
Reference in new issue
Block a user