3ba267b38a
- 优化服务端百炼API配置,将硬编码的API地址改为通过环境变量读取,支持自定义部署端点 - 新增机器人象棋思想实验室,包含完整对战逻辑、管理接口与前端页面,配套测试用例 - 优化思想实验室路由注册,新增公开路由绑定能力支持非浏览器客户端调用
203 lines
8.7 KiB
JavaScript
203 lines
8.7 KiB
JavaScript
// ============================================================
|
||
// 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) }) |