自制带得分和推荐走法的象棋视频

① 到东萍象棋网可以下载许多棋谱。我用程序下载了1万多个推荐的,grep -v (--invert-match)去掉了和棋的,还有6813个。bash+wget干的,python的连不通(要送user-agent?) 从.html提取字符串用的Python,不必用正则。有礼貌的都sleep; NUMBER need not be an integer. 

pu.txt 6813行,形如:

河北金环建设象棋队-申鹏-胜-山东省棋牌运动管理中心-张瑞峰=694712321927102226250010091972427967706289798070777362817343

用Python按走法长度升序,用了dict,长度做key和sorted(keys),setdefault(k, []).append(...)

② ffmpeg把一系列bmp压缩成mp4,几十步棋才几十到数百KB,KB, KB. /dev/shm是内存,不担心SSD磨损。编码速度贼快,一回车完事。总之这事可干,就是CPU风扇时不时会响。引擎是从象棋巫师ElephantEye的改来的,在Intel N100上每秒636万节点(只用了单核),中国象棋云库查询0.868 GNPS。Fritz 9 Chess Benchmarks, Ryzen 9 7950X, 16 x 5947, 54,204 Kn/s.

ee.py

from subprocess import Popen, PIPE

class EleEye(Popen):
  BM = 'bestmove'

  def __init__(m): Popen.__init__(m, 'eemod', stdin=PIPE, stdout=PIPE, text=True)

  def send(m, s): m.stdin.write(s + '\n'); m.stdin.flush()

  def recv(m):
    out = ''
    while True:
      s = m.stdout.readline()
      out += s.replace('info ', '')
      if s.find(m.BM) != -1: return out

  def get_score(m, f):
    m.send('position fen ' + f + '\ngo')
    n = -900; fx = fy = tx = ty = -1
    while True:
      s = m.stdout.readline()
      i = s.find('score ')
      if i != -1:
        i += 6
        j = s.find(' ', i)
        n = int(s[i:j]) # j=-1: ok
      i = s.find(m.BM)
      if i != -1:
        if i != 0: break # nobestmove
        # b0c2
        fx = ord(s[9]) - ord('a')
        tx = ord(s[11]) - ord('a')
        fy = ord('9') - ord(s[10])
        ty = ord('9') - ord(s[12])
        break
    if n > 900: n = 900
    elif n < -900: n = -900
    return n,fx,fy,tx,ty

ee = EleEye()
View Code

img.py

#! /usr/bin/python3

from PIL import Image as I, ImageDraw, ImageFont #, ImageFilter

def get_circle_img(i):
    img = I.new('RGBA', (144,144), (255,255,255,0)) # no LA
    d = ImageDraw.Draw(img)
    f = (255,255,255,255) if i else (0,0,0,255)
    d.ellipse((0,0,132,132), fill=f, outline=(68,68,68,255), width=8)
    return img.resize((36,36), I.LANCZOS)
    #return img.filter(ImageFilter.GaussianBlur(radius=1))

red = get_circle_img(1); hei = get_circle_img(0)

FNT = '/usr/local/share/fonts/汉/汉仪旗黑.ttf'
fnt = ImageFont.truetype(FNT, 20)
fnt2 = ImageFont.truetype(FNT, 14)

dic = {"r":"", "n":"", "c":"", "b":"", "B":"", "a":"", "A":"", "k":"", "K":"", "p":"", "P":""}
dic['R'] = dic['r']; dic['N'] = dic['n']; dic['C'] = dic['c']

jpg = I.open('brd.jpg')

def fen2img (f, nm, str=None):
    if nm.find('bmp') == -1:
        S = 40; L = 6; T = 4
        brd = I.new('L', (368,400), 255)
        bmp = False
    else:
        S = 40; L = 6; T = 32
        brd = I.new('L', (368,432), 255)
        bmp = True

    brd.paste(jpg, (L,T-4))
    id = ImageDraw.Draw(brd)
    #id.rectangle([(0, 408), (432, 800)], fill='blue')

    def draw(c, x, y):
     if c.isupper(): img = red; tc=0
     else: img = hei; tc=255
     brd.paste(img, (x,y), mask=img.split()[-1])
     id.text((x+6,y+4), dic[c], font=fnt, fill=tc)

    f = f.split('/')
    b = [[' '] * 9 for i in range(10)]
    for y in range(10):
     x = 0
     for i in range(len(f[y])):
         c = f[y][i]
         j = ord(c) - ord('0')
         if j >= 1 and j <= 9: x += j
         else:
             draw(c, L+x*S, T+y*S); b[y][x] = c; x += 1

    if str != None:
        bb = fnt2.getbbox(str)
        id.text(((brd.size[0]-bb[2]+bb[0])/2, 5), str, font=fnt2, fill=0)

    if not bmp: brd = brd.resize((736,800), I.NEAREST)
    brd.save(nm, compress_level=9) # smaller than jpeg

if __name__ == '__main__':
    import sys
    if 'cover' in sys.argv:
        brd = I.new('L', (1920,1080), 255)
        brd.save('cover.png', compress_level=9)
    else: fen2img(input().replace(' ', ''), '/t/fen.png')
View Code

pu.py

#! /usr/bin/python3
from ee import *
from img import *

def fen2brd ():
  global b
  a = f.split('/')
  for y in range(10):
   x = 0
   for i in range(len(a[y])):
    c = a[y][i]
    j = ord(c) - ord('0')
    if j >= 1 and j <= 9: x += j
    else: b[y][x] = c; x += 1

def brd2fen ():
  global f
  f = ''
  for y in range(10):
   n = 0
   for x in range(9):
    c = b[y][x]
    if c == ' ': n += 1
    else:
     if n: f += str(n)
     f += c; n = 0
   if n: f += str(n)
   if y != 9: f += '/'
  return f

def mv2str (fx, fy, tx, ty):
  if fx < 0: return None
  d = abs(fy - ty)
  c = b[fy][fx]; red = c < 'a'
  m = p[c]
  x = "九八七六五四三二一" if red else "123456789"
  m += x[fx]
  if fy == ty: m += "" + x[tx]
  else:
    m += "" if red == (fy > ty) else "退"
    if   c in "RCPK": m += "零一二三四五六七八九"[d]
    elif c in "rcpk": m += "0123456789"[d]
    else: m += x[tx]
  return m

p = {"r":"", "n":"", "c":"", "b":"", "B":"","a":"", "A":"", "k":"", "K":"", "p":"", "P":""}
p['R'] = p['r']; p['N'] = p['n']; p['C'] = p['c']
b = [[' '] * 9 for i in range(10)]
f = 'rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR'; fen2brd()

# 开局,go 1500 7分,300 4分,bm都是兵七进一。这个分是假设走了bm.
# 若实际帅五进一呢?应该用黑分如17。红走棋后,考虑黑方的最佳应对。
a = [4]; bms = ['兵七进一']; n = 1
s = input().split('=')[1]
for i in range(0, len(s), 4):
  # 处理(红)实际走法
  fx,fy,tx,ty = int(s[i]), int(s[i+1]), int(s[i+2]), int(s[i+3])
  m = mv2str(fx,fy,tx,ty)
  b[ty][tx] = b[fy][fx]; b[fy][fx] = ' ' # 不能放在mv2str前
  # 处理(黑)
  r,fx,fy,tx,ty = ee.get_score(brd2fen() + (' b' if n % 2 else ' w'))
  # brd2fen() modifies f
  a.append(r); bms.append(mv2str(fx,fy,tx,ty))
  ##
  bm = bms[-2]
  if bm and m != bm: m += '_' + bm
  if n % 2: print(f'{n // 2 + 1}. {m}', end=' ')
  else: print(m, flush=True)
  ##
  fen2img(f, f'/t/{n:02d}.bmp', m + ('' if n % 2 else '') + str(r))
  n += 1

exit()

fen2img(f, '/t/t.png')

import matplotlib.pyplot as plt
#import numpy as np
plt.figure(figsize=(8, 6), dpi=72)
x = a[::2]
plt.plot(x, color='#FF0000', linewidth=1)
plt.plot(a[1::2], color='#000000', linewidth=1)
plt.xticks(rotation=45)
n = len(x)
plt.xticks(ticks=list(range(n)), labels=list(range(1, n+1)))  # 下标+1作为刻度
#plt.xticks(ticks=np.arange(len(x)), labels=np.arange(1, len(x)+1))
#plt.axis('off')

plt.savefig('/t/score.png', bbox_inches='tight', pil_kwargs={'compress_level':9})
#plt.show()
View Code

pu

# pu.htm里无对手信息; move用3字符编码

if [[ $# -lt 1 ]]; then exit; fi

n=$1 # n=$(expr $1 + 10)
p=`sed -n ${n}p pu.txt`

echo $p | xclip -f -selection clipboard

if [[ $# -eq 1 ]]; then    exit; fi

echo 'Pressing Enter will rm -f /t/*'
read
rm -f /t/*

t=/t/t.txt
echo $p >$t
echo $p | pu.py | tee -a $t
echo

cd /t
v=t.mp4
ffmpeg -framerate 0.5 -i %02d.bmp -r 8 -vf format=gray,format=yuv420p -c:v h264 -b:v 100k -preset fast -loglevel 0 $v
rm -f *.bmp
#ls -lh $v | awk '{print $5}' >>$t
#cp ~/xq/cover.png .
cd - >/dev/null

#kwrite $t 2>/dev/null &
View Code

视频

分辨率为368x432,都是16的倍数。除了VLC播放时会截去下面一块,ffplay,手机等都没事。在海信A5这样的墨水屏手机上,视频播放器默认背景也是大白纸,一点都不闪,真是“纸上的东西动起来了”。

0e791cfbd5264a9086da32c0ff943aed~tplv-obj_720_1041

手抖拍糊了。黑边为手机实际边框。

man ffplay, While playing, q/Esc quit; p/Space pause

VLC 3.0.21 Vetinari, debian 6.1.0-18-amd64, Intel N100,播576x432的也截(4:3, 都是16的倍数)。未排除是硬件解码器的问题。

posted @ 2025-10-07 16:24  华容道专家  阅读(13)  评论(0)    收藏  举报