1. 程式人生 > 其它 >瀏覽器裡的中國象棋: HTML5 canvas, JS, python

瀏覽器裡的中國象棋: HTML5 canvas, JS, python

介面程式很短。引擎不是我寫的,棋力不是很強——但我寫不出來,正在學GNU chess的原始碼。全部檔案:https://files.cnblogs.com/files/blogs/714801/ccib.zip

引擎是可以換的,如象棋旋風官方網站--中國象棋第一AI智慧引擎 (ccyclone.com)旋風專業版.zip (41.45 MB)

ELEEYE.EXE 87KB,BOOK.DAT 95KB ……

# Universal Chinese Chess Protocol(UCCI)是象棋介面和引擎間的通訊協議。國際象棋有UCI.
# 引擎是個.exe,它和介面通過stdin和stdout通訊。
# 介面向引擎傳送“指令”,引擎向介面傳送“反饋”。指令和反饋以“行”為單位(以'\n'結束)。
# 別忘了重新整理緩衝區,如fflush().
# 引擎有引導、空閒和思考三種狀態。
#  引導狀態: 介面用ucci指令讓引擎進入空閒狀態; 引擎輸出ucciok作為初始化結束的標誌。
#  空閒狀態: 引擎接收思考(go)和退出(quit)指令。
#  思考狀態: 引擎收到go指令後進入思考狀態,輸出bestmove或nobestmove。
# 介面用position指令把局面告訴引擎。如:
# ucci
#   id name ElephantEye 3.1
#   option usemillisec type check default true
#   ucciok
# position fen rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1
# go time 3
#   info depth 0 score 1473 pv b2e2
#   bestmove g3g4 ponder h7g7
from subprocess import Popen, PIPE
class EleEye(Popen):
	def __init__(m):
		wd='ElephantEye/'; Popen.__init__(m,[wd+'ELEEYE.EXE',],cwd=wd,stdin=PIPE,stdout=PIPE)
	def send(m, s): m.stdin.write(s.encode() + b'\n'); m.stdin.flush()
	def recv(m, p):
		p = p.encode(); out = b''
		while True:
			s = m.stdout.readline(); print(s.decode(), end='')
			out += s
			if s.find(p) != -1: return out

print('Staring engine...')
ee = EleEye()
ee.send('ucci'); print(ee.recv('ucciok').decode())

from http.server import *
from threading import *
import re
import urllib
class HTTPReqHandler(SimpleHTTPRequestHandler):
	def __init__(m, r, c, s): super().__init__(r, c, s, directory='www')
	def do_GET(m):
		path = m.requestline.split(' ')[1]
		if not path.startswith('/ucci'): return super().do_GET()
		param = re.split('[\?\<]', urllib.parse.unquote(path))
		m.send_response(200)
		m.send_header('Content-type', 'text/html')
		m.end_headers()
		ee.send(param[2]); print(param[2])
		if param[1] != 'none': m.wfile.write(ee.recv(param[1]))
	def do_HEAD(m): super().do_HEAD()
	def do_POST(m): super().do_POST()

def httpd_thread():
	port = 8000
	svr_addr = ('', port)
	httpd = ThreadingHTTPServer(svr_addr, HTTPReqHandler)
	print('Listening at', port)
	httpd.serve_forever()

Thread(daemon=1, target=httpd_thread).start()
while input(): pass

HTML:

<html><meta charset="gbk"><title>CCIB</title><style>
/* https://www.w3school.com.cn/cssref/css_selectors.asp */
* { font:12pt 'Segoe UI' }
#brd_canvas { cursor:hand }
h6 { color:green; margin:1em }
/* InfrawView裡在畫素上按住滑鼠左鍵,標題欄顯示顏色 */
/* red/black button */
.rb {font:bold 24pt '楷體'; padding:8px; background:#E0C088; cursor:hand; box-shadow:0 5px 8px 0 rgba(0,0,0,0.25) }
input, #fenbak { font:10pt mono; width:850px; padding:6px; border:dotted 1px }
</style>
<body>
<div style="position:absolute; left:80px; top:8px">
	<canvas id="brd_canvas" width="407" height="454"></canvas>
</div>
<div style="position:absolute; left: 500px" id="panel">
	<h6>Chinese Chess In Browser<br>引擎:ElephantEye by Morning Yellow</h6>
	<ul>
	<li>你可以連續走紅棋或黑棋。</li>
	<li>點選紅棋走子或黑棋走子,電腦走。</li>
	<li>可通過FEN設定局面。</li>
	<li>在棋盤上修改局面:<br>① 無規則、可亂走、能"吃"自己子<br>② 先點空白處再點棋子可拿掉它<br>
	③ 先點棋子再點棋盤外可拿掉它<br>④ 複製貼上FEN可備份</li>
	</ul>
	<p><button class="rb" style="color:#AC0000" onclick="move('w')">紅棋走子</button></p>
	<p><button class="rb" style="color:black" onclick="move('b')">黑棋走子</button></p>
</div>
<div style="position:absolute; left:8px; top:480px">
	<button style="font:11pt mono" onclick="fromFEN(), draw_all()">應用</button> FEN:<br>
	<input type="text" id="fen" value="rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR" style="margin-top:2px"></input><br>
	<textarea id="fenbak" value="FEN備份區" rows="6" style="position:relative; top:-1px"></textarea>
</div>
<div style="position:absolute; left:900px; top:0px; bottom:0px; border-left:solid #efe 1px">
<p id="ucciout" style="font:10pt mono; margin:8px"></p>
</div>
<script src="ccib.js"></script>
</body></html>

JS:

document.addEventListener('mousemove', function(e){ // mx,my記錄滑鼠指標位置(塊座標)
	mx = Math.floor((e.x - 80 - 4) / 41); my = Math.floor((e.y - 8 - 7) / 41)
})
sx = sy = -1 // 選擇的位置
ctx = brd_canvas.getContext('2d')
img_wood = new Image(); img_wood.src = 'img/WOOD.gif'
img_wood.onload = function (){ // 得這麼幹
	ctx.drawImage(img_wood, 0, 0)
	img_brd = get_2d_ary(10, 9)	// 存放10x9個棋盤切片圖片
	for(let y = 0; y < 10; y++) for(let x = 0; x < 10; x++)
		// 直接開啟test.html,報The canvas has been tainted by cross-origin data
		// 開啟http://127.0.0.1:8000/test.html則此問題
		img_brd[y][x] = ctx.getImageData(4 + x * 41, 7 + y * 41, 41, 41)

	img_pieces = {} // 獲取棋子圖片; 小寫表示黑方,大寫表示紅方
	let str = 'rnbakcp'; for(let c of str) {
		var i = new Image; i.src = 'img/B' + c + '.gif'; img_pieces[c] = i
				i = new Image; i.src = 'img/R' + c + '.gif'; img_pieces[c.toUpperCase()] = i
	}
	img_sel = new Image; img_sel.src = 'img/OOS.gif'
	
	fromFEN()
	setTimeout(draw_all, 100)	// 棋子圖片載入完再draw
}

function draw_all(){ for(let y=0; y<10; y++) for(let x=0; x<9; x++) draw(x,y) }

function draw(x, y){
	var	px = 4 + x * 41, py = 7 + y * 41	// pixel
	var	c = brd[y][x]
	if(c == ' ') ctx.putImageData(img_brd[y][x], px, py)
	else draw_img(img_pieces[c], px, py)
	if(x == sx && y == sy) draw_img(img_sel, px, py)
}

function draw_img(img, x, y){ // 得這麼幹
	var i = new Image; i.src = img.src; i.onload = function()
	{ ctx.drawImage(i, x, y); i = null }
}

document.addEventListener('mousedown', function(e){
	if(e.which != 1) return // Not left button
	var out = mx < 0 || mx >= 9 || my < 0 || my >= 10
	if(sx >= 0 && (sx != mx || sy != my)){
			if(!out) brd[my][mx] = brd[sy][sx], draw(mx, my)
			brd[sy][sx]=' '; var X=sx, Y=sy; sx=sy=-1; 
			draw(X, Y)
			toFEN()
	}
	else if(!out) draw(sx = mx, sy = my)
})

function fromFEN(){
	try{
		brd = get_2d_ary(10, 9)
		f = fen.value.split('/')
		var	x, y, i
		for(y = 0; y < 10; y++){
			x = 0
			for(i = 0; i < f[y].length; i++){
				var	c = f[y][i]
				if(c >= '1' && c <= '9') x += c - '0'
				else brd[y][x++] = c
			}
		}
	} catch(e){}
}

function toFEN(){
	let f = ''
	for(let y = 0; y < 10; y++){
		let	n = 0
		for(let x = 0; x < 9; x++){
			let c = brd[y][x]
			if(c == ' ') ++n
			else{
				if(n) f += n
				f += c; n = 0
			}
		}
		if(n) f += n
		if(y != 9) f += '/'
	}
	return fen.value = f
}

function ajax(req, cb){
	let ax = window.XMLHttpRequest ?	new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
	ax.onreadystatechange = function(){
		if(ax.readyState != 4 || ax.status != 200) return
		cb(ax.responseText)
	}
	ax.open('GET', '/ucci?' + req, true); ax.send()
}

function move(who){
	ajax('none<position fen ' + toFEN() + ' ' + who + ' - - 0 1', function(){
		ajax('bestmove<go time 5000', function(s){
			ucciout.innerText = s
			let	i = s.indexOf('\nbestmove')
			if(i == -1){ alert('No best move'); return }
			let	t = 'a0'
			fx = s.charCodeAt(i+10) - t.charCodeAt(0)
			tx = s.charCodeAt(i+12) - t.charCodeAt(0)
			fy = 9 - (s.charCodeAt(i+11) - t.charCodeAt(1))
			ty = 9 - (s.charCodeAt(i+13) - t.charCodeAt(1))
			console.log(fx, fy, tx, ty)
			brd[ty][tx] = brd[fy][fx]; brd[fy][fx] = ' '; draw_all(); toFEN()
		})
	})
}

function get_2d_ary(m, n) {
	var	a = new Array()
	for(;m>0;m--){
		var	r = new Array(), i
		for(i = 0; i < n; i++) r.push(0)
		a.push(r)
	}
	return a
}