Files
Toolbox/dev_test_scripts/unit/test_bot_chess_referee.js
yangxiangyuan 3ba267b38a feat(server, thought-lab): 优化百炼API配置并新增机器人象棋实验室
- 优化服务端百炼API配置,将硬编码的API地址改为通过环境变量读取,支持自定义部署端点
- 新增机器人象棋思想实验室,包含完整对战逻辑、管理接口与前端页面,配套测试用例
- 优化思想实验室路由注册,新增公开路由绑定能力支持非浏览器客户端调用
2026-08-18 13:23:36 +08:00

203 lines
8.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// test_bot_chess_referee.js —— bot_chess 规则引擎与对局核心单测
// 运行:node dev_test_scripts/unit/test_bot_chess_referee.js
//
// 覆盖:FEN 解析/往返、走子校验(蹩马腿/塞象眼/炮架/将帅照面/送将)、
// 飞将直取、子力判和、以及 service 层建赛-开局-走子-非法拒绝的端到端。
// 测试使用真实 SQLite(bot_chess.db),跑完自动删除测试比赛。
// ============================================================
const assert = require('assert')
const r = require('../../src/server/thought_lab/labs/bot_chess/referee')
const service = require('../../src/server/thought_lab/labs/bot_chess/service')
const db = require('../../src/server/thought_lab/labs/bot_chess/db')
let passed = 0, failed = 0
const ok = (name) => { passed++; console.log(' ✔ ' + name) }
const bad = (name, e) => { failed++; console.log(' ✘ ' + name + ' —— ' + (e && e.message)) }
const t = async (name, fn) => {
try { await fn(); ok(name) } catch (e) { bad(name, e) }
}
const main = async () => {
// ---------- 1. FEN 往返 ----------
await t('FEN 解析 + 序列化往返一致', () => {
const { board, turn } = r.parseFen(r.START_FEN)
assert.strictEqual(turn, 'w')
assert.strictEqual(r.toFen(board, 'w').split(' ')[0], r.START_FEN.split(' ')[0])
})
await t('开局双方各 16 子(红 16 / 黑 16)', () => {
const { board } = r.parseFen(r.START_FEN)
const list = r.boardToPieceList(board)
assert.strictEqual(list.red.length, 16)
assert.strictEqual(list.black.length, 16)
})
// ---------- 2. 走子合法性 ----------
await t('标准开局:红炮二平五 h2-e2 合法', () => {
const { board } = r.parseFen(r.START_FEN)
const v = r.validateMove(board, 'red', { r: 2, c: 7 }, { r: 2, c: 4 })
assert.strictEqual(v.ok, true)
})
await t('标准开局:黑马 b9-c7 合法(无蹩马腿)', () => {
const { board } = r.parseFen(r.START_FEN)
const v = r.validateMove(board, 'black', { r: 9, c: 1 }, { r: 7, c: 2 })
assert.strictEqual(v.ok, true)
})
await t('蹩马腿:腿点被占 → BLOCKED_HORSE', () => {
const fen = '4k4/9/9/9/9/9/9/1N7/1p7/4K4 w - - 0 1'
const { board } = r.parseFen(fen) // 红马 b2 想跳 a0,腿点 b1 有黑卒 → 蹩马腿
const v = r.validateMove(board, 'red', { r: 2, c: 1 }, { r: 0, c: 0 })
assert.strictEqual(v.ok, false)
assert.strictEqual(v.code, r.E.BLOCKED_HORSE)
})
await t('马腿清空后同一着法合法', () => {
const fen = '3k5/9/9/9/9/9/9/1N7/9/4K4 w - - 0 1'
const { board } = r.parseFen(fen)
const v = r.validateMove(board, 'red', { r: 2, c: 1 }, { r: 0, c: 0 })
assert.strictEqual(v.ok, true)
})
await t('炮吃子必须有且仅有一个炮架', () => {
// 红炮 d2,红车 f2 作架,黑卒 h2 → 隔一子吃,合法
const withScreen = '3k5/9/9/9/9/9/9/3C1R1p1/9/4K4 w - - 0 1'
let { board } = r.parseFen(withScreen)
let v = r.validateMove(board, 'red', { r: 2, c: 3 }, { r: 2, c: 7 })
assert.strictEqual(v.ok, true)
// 无炮架直接吃 → CANNON_SCREEN
const noScreen = '3k5/9/9/9/9/9/9/3C3p1/9/4K4 w - - 0 1'
;({ board } = r.parseFen(noScreen))
v = r.validateMove(board, 'red', { r: 2, c: 3 }, { r: 2, c: 7 })
assert.strictEqual(v.ok, false)
assert.strictEqual(v.code, r.E.CANNON_SCREEN)
})
await t('送将:移开自家遮挡车 → SELF_CHECK', () => {
// 黑车 e9 打红帅 e0,红车 e1 挡着;红车横移 e1-f1 让出直线 → 送将
const fen = '4r4/9/9/9/9/9/9/9/4R4/4K4 w - - 0 1'
const { board } = r.parseFen(fen)
const v = r.validateMove(board, 'red', { r: 1, c: 4 }, { r: 1, c: 5 })
assert.strictEqual(v.ok, false)
assert.strictEqual(v.code, r.E.SELF_CHECK)
})
await t('将帅照面:走完双方帅将同列无遮挡 → FLYING_GENERAL', () => {
const fen = '4k4/9/9/9/9/9/9/9/9/4K4 w - - 0 1'
const { board } = r.parseFen(fen)
const v = r.validateMove(board, 'red', { r: 0, c: 4 }, { r: 1, c: 4 })
assert.strictEqual(v.ok, false)
assert.strictEqual(v.code, r.E.FLYING_GENERAL)
})
await t('飞将直取敌帅:合法且立即获胜(king_captured)', () => {
const fen = '4k4/9/9/9/9/9/9/9/9/4K4 w - - 0 1'
const { board } = r.parseFen(fen)
const v = r.validateMove(board, 'red', { r: 0, c: 4 }, { r: 9, c: 4 })
assert.strictEqual(v.ok, true)
if (v.ok) {
const after = r.applyMove(board, { r: 0, c: 4 }, { r: 9, c: 4 }).board
const res = r.resolveAfterMove(after, 'red')
assert.strictEqual(res.ended, true)
assert.strictEqual(res.kind, 'king_captured')
assert.strictEqual(res.winner, 'red')
}
})
await t('不能吃自己棋子 → CAPTURE_OWN', () => {
const { board } = r.parseFen(r.START_FEN)
const v = r.validateMove(board, 'red', { r: 0, c: 0 }, { r: 0, c: 1 })
assert.strictEqual(v.ok, false)
assert.strictEqual(v.code, r.E.CAPTURE_OWN)
})
// ---------- 3. 子力判和 ----------
await t('子力不足:双方只剩帅士相 → 和', () => {
const fen = '4k4/9/9/9/9/9/9/9/4K4/9 w - - 0 1'
const { board } = r.parseFen(fen)
assert.strictEqual(r.insufficientMaterial(board), true)
})
await t('存在车/马/炮/兵任一即非子力和', () => {
const { board } = r.parseFen(r.START_FEN)
assert.strictEqual(r.insufficientMaterial(board), false)
})
// ---------- 4. service 端到端(建赛 → 开局 → 走子 → 非法拒绝)----------
let cleanMatchId = 0
await t('service:创建比赛 + 开局 + 合法走子 + 非法拒绝', async () => {
const snap1 = service.createMatch({ title: '单测比赛', red_bot: '测试红', black_bot: '测试黑', best_of: 1 })
cleanMatchId = snap1.match.id
assert.strictEqual(snap1.match.status, 'created')
const started = service.startMatch(snap1.match.id)
assert.strictEqual(started.ok, true)
let res = service.submitMove(snap1.match.id, 'red', 'h2e2')
assert.strictEqual(res.ok, true)
assert.strictEqual(res.game_finished, false)
// 非法走子:黑方兵不能后退(b6-b5 不是黑方合理)→ 用明显非法:黑车 b9 斜走
res = service.submitMove(snap1.match.id, 'black', 'b9c8') // 车斜走 → 非法
assert.strictEqual(res.ok, false)
// 非法走子后局面不动:依旧黑手
const snap2 = service.buildSnapshot(snap1.match.id)
assert.strictEqual(snap2.game.turn, 'b')
// 轮次保护:红方连走两次被拒
res = service.submitMove(snap1.match.id, 'red', 'b0c2')
assert.strictEqual(res.ok, false)
assert.strictEqual(res.code, 'NOT_YOUR_TURN')
// 黑方合法走子
res = service.submitMove(snap1.match.id, 'black', 'h9g7')
assert.strictEqual(res.ok, true)
// 日志可导出
const md = service.buildMarkdownLog(snap1.match.id)
assert.ok(md.includes('h2-e2') || md.includes('红炮 h2-e2') || md.includes('比赛'))
})
await t('service:犯规累计到上限判负', async () => {
const snap = service.createMatch({ title: '犯规测试', red_bot: 'R', black_bot: 'B', best_of: 1 })
service.startMatch(snap.match.id)
let r2 = service.submitMove(snap.match.id, 'red', 'h2e2')
assert.strictEqual(r2.ok, true)
// 黑方连续 3 次犯规(超时/非法由监督器调用 applyFoul)
service.applyFoul(snap.match.id, 'black', '测试犯规1')
service.applyFoul(snap.match.id, 'black', '测试犯规2')
const r3 = service.applyFoul(snap.match.id, 'black', '测试犯规3')
assert.strictEqual(r3.ok, true)
const snap2 = service.buildSnapshot(snap.match.id)
assert.strictEqual(snap2.game.status, 'finished')
assert.strictEqual(snap2.game.winner, 'red')
db.deleteMatch(snap.match.id)
})
await t('service:终局后自动换先开下一局 / 结算', async () => {
// best_of=3:第 1 局红胜 → 红黑对调开第 2 局
const snap = service.createMatch({ title: '换先测试', red_bot: 'R', black_bot: 'B', best_of: 3 })
service.startMatch(snap.match.id)
// 快速杀棋:飞将直取(构造后手动写入局面不好走 service API,这里用 applyFoul 制造判负)
service.forfeitSide(snap.match.id, 'black', '测试判负')
const snap2 = service.buildSnapshot(snap.match.id)
assert.strictEqual(snap2.game.game_no, 2)
assert.strictEqual(snap2.game.status, 'playing')
assert.strictEqual(snap2.game.turn, 'w')
// 第 2 局黑方(原黑 bot)执红
assert.strictEqual(snap2.match.score_red, 1)
db.deleteMatch(snap.match.id)
})
if (cleanMatchId) db.deleteMatch(cleanMatchId)
console.log(`\n结果:${passed} 通过 / ${failed} 失败`)
process.exit(failed ? 1 : 0)
}
main().catch(e => { console.error('测试运行异常:', e); process.exit(1) })