539 lines
25 KiB
C#
539 lines
25 KiB
C#
// =============================================================================================
|
||
// R183 · 中国象棋规则引擎(服务端侧,纯逻辑、零依赖)
|
||
//
|
||
// 为什么需要它
|
||
// ------------
|
||
// R182 把「进 zone + 推对手单位」补上之后,客户端**棋盘出来了、棋子能选中**,但「下不了子」。
|
||
// 实测(logs/console-20260929-101956.log:1194+)给出精确病灶:
|
||
//
|
||
// -> rpc AskChineseChessMovePiece [0x040568ED] #128430095 24b
|
||
// ! [RPC4229938] unimplemented AskChineseChessMovePiece -> typed-default void 0b
|
||
// <- ret AskChineseChessMovePiece [0x040568ED] #128430095 e=0 0b
|
||
//
|
||
// 客户端 `ChineseChessMgr.MoveChess` 发完这条**只**等 `SyncChineseChessMove` 回推才落子
|
||
// (本地不乐观应用 —— `ChineseChessBoardBase.MoveChessTo` 只调 `Chart:MoveChess`,不翻回合),
|
||
// 而回合翻转的唯一来源是 `SyncGameGroundZoneTurnChange`(68479818)。
|
||
// ⇒ 服务端必须:① 回显玩家那一手;② 自己算黑方(AI)那一手;③ 两边都推 + 推回合。
|
||
//
|
||
// 为什么规则要「自己写」而不是照抄客户端 Lua
|
||
// ------------------------------------------
|
||
// ★★★ 客户端 `lua\lua\LX6\MiniGame\ChineseChess\ChineseChessChart.lua` 是 LuaJIT
|
||
// **反编译产物**,`==`/`~=` 全部反了(铁律 1),而且「吃子」分支被反编译器吞成了 `-- Nothing`:
|
||
//
|
||
// M.GetMovePoints_Che → if self.PointHasChess(self, newPoint) then
|
||
// ... if IsRedChess(a) ~= IsRedChess(b) then
|
||
// -- Nothing ← 真值这里应有 table.insert(result, newPoint)
|
||
// M.IsCheck → if shuaiPoint ~= -1 then return false end ← 真值是 == -1
|
||
//
|
||
// ⇒ **照抄必错**。本文件按标准中国象棋规则独立实现(走法几何 + 自将过滤 + 飞将),
|
||
// 并拿真实报文做对照:实测那一手 炮(10,5)→(7,5) 必须判合法(见 gym_tools/_chess_rules_check)。
|
||
//
|
||
// 坐标约定(★ 实测锚定,不是猜的)
|
||
// --------------------------------
|
||
// `gChineseChessTools.PointToCoord(point)` 返回的就是 (x, y):
|
||
// x = 列 ∈ [3, 11](9 路) y = 行 ∈ [3, 12](10 行)
|
||
// 锚点:`ChineseChessChart.InitChartBoard` 把红炮放在 `GetPointByPosition(10, 5)`,
|
||
// 而实测报文正是 from=(10,5) → to=(7,5)。两者对上 ⇒ 该约定成立。
|
||
// 红方在下(y 小),黑方在上(y 大);河界在 y=7 / y=8 之间。
|
||
//
|
||
// 棋子 id(0..31)
|
||
// ---------------
|
||
// 红 = 0..15,黑 = 16..31(`IsCheck` 里 `shuaiID = isRed and 0 or 16` 为证)。
|
||
// 同侧内按 `InitChartBoard` 的声明顺序分组:
|
||
// +0 = 帅/将 +1..2 = 仕/士 +3..4 = 相/象 +5..6 = 马
|
||
// +7..8 = 车 +9..10 = 炮 +11..15 = 兵/卒
|
||
// 初始摆位(与 InitChartBoard 逐条对齐,就是标准象棋):
|
||
// 红 帅(7,3) 仕(6,3)(8,3) 相(5,3)(9,3) 马(4,3)(10,3) 车(3,3)(11,3) 炮(4,5)(10,5) 兵(3,6)(5,6)(7,6)(9,6)(11,6)
|
||
// 黑 将(7,12) 士(6,12)(8,12) 象(5,12)(9,12) 马(4,12)(10,12) 车(3,12)(11,12) 炮(4,10)(10,10) 卒(3,9)(5,9)(7,9)(9,9)(11,9)
|
||
//
|
||
// 本文件**刻意不引用任何 Drmk 类型** —— 这样 gym_tools/_chess_rules_check 可以用
|
||
// `Compile Include` 直接把它编进来做离线断言(见 uxrpc-wire-verification §15.3 的手法)。
|
||
// =============================================================================================
|
||
|
||
using System;
|
||
using System.Collections.Generic;
|
||
|
||
namespace Ananta.Handlers.Game
|
||
{
|
||
/// <summary>棋子类型(同侧内的功能种类,与 chessId 的 %16 分段一一对应)。</summary>
|
||
internal enum ChessKind
|
||
{
|
||
King = 0, // 帅 / 将 id%16 == 0
|
||
Advisor = 1, // 仕 / 士 1..2
|
||
Elephant = 2, // 相 / 象 3..4
|
||
Horse = 3, // 马 5..6
|
||
Rook = 4, // 车 7..8
|
||
Cannon = 5, // 炮 9..10
|
||
Pawn = 6, // 兵 / 卒 11..15
|
||
}
|
||
|
||
/// <summary>一步棋(坐标即线上坐标,可直接写进 ChineseChessMove)。</summary>
|
||
internal readonly struct ChessMove
|
||
{
|
||
public readonly int FromId;
|
||
public readonly int FromX, FromY, ToX, ToY;
|
||
|
||
public ChessMove(int fromId, int fx, int fy, int tx, int ty)
|
||
{
|
||
FromId = fromId; FromX = fx; FromY = fy; ToX = tx; ToY = ty;
|
||
}
|
||
|
||
public override string ToString() => $"#{FromId} ({FromX},{FromY})->({ToX},{ToY})";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 9×10 的象棋盘 + 完整走法生成 + 将军/绝杀判定 + 一个 2 层极小极大 AI。
|
||
/// 非线程安全;一个 zone 一份,由 handler 持有在 session 里。
|
||
/// </summary>
|
||
internal sealed class ChineseChessBoard
|
||
{
|
||
public const int MinX = 3, MaxX = 11; // 9 路
|
||
public const int MinY = 3, MaxY = 12; // 10 行
|
||
public const int Empty = -1;
|
||
|
||
private const int Stride = 16;
|
||
|
||
private readonly int[] _at = new int[16 * Stride]; // 格子 → 棋子 id(index = y*16 + x)
|
||
private readonly int[] _pos = new int[32]; // 棋子 id → 格子(-1 = 不在盘上)
|
||
private readonly Random _rng;
|
||
|
||
/// <summary>轮到红方?(服务端镜像状态,仅用于日志/AI 选择)</summary>
|
||
public bool RedToMove;
|
||
|
||
/// <summary>本局已走过的着数(写进 ChineseChessMove.MoveIndex)。</summary>
|
||
public int MoveCount;
|
||
|
||
public ChineseChessBoard(int seed = 0)
|
||
{
|
||
_rng = seed == 0 ? new Random() : new Random(seed);
|
||
Init();
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────────
|
||
// 基础
|
||
// ─────────────────────────────────────────────────────────────────────────────────
|
||
|
||
public static bool IsRedId(int id) => id >= 0 && id < 16;
|
||
public static bool InBoard(int x, int y) => x >= MinX && x <= MaxX && y >= MinY && y <= MaxY;
|
||
|
||
public static ChessKind KindOf(int id)
|
||
{
|
||
var t = id & 15;
|
||
if (t == 0) return ChessKind.King;
|
||
if (t <= 2) return ChessKind.Advisor;
|
||
if (t <= 4) return ChessKind.Elephant;
|
||
if (t <= 6) return ChessKind.Horse;
|
||
if (t <= 8) return ChessKind.Rook;
|
||
if (t <= 10) return ChessKind.Cannon;
|
||
return ChessKind.Pawn;
|
||
}
|
||
|
||
/// <summary>红方九宫 x∈[6,8] y∈[3,5];黑方 x∈[6,8] y∈[10,12]。</summary>
|
||
public static bool InPalace(bool red, int x, int y) =>
|
||
x >= 6 && x <= 8 && (red ? y >= 3 && y <= 5 : y >= 10 && y <= 12);
|
||
|
||
/// <summary>是否还在本方半场(相/象不可过河)。红 y∈[3,7],黑 y∈[8,12]。</summary>
|
||
public static bool InOwnHalf(bool red, int y) => red ? y <= 7 : y >= 8;
|
||
|
||
/// <summary>兵/卒是否已过河。红过河 = y≥8;黑过河 = y≤7。</summary>
|
||
public static bool PawnCrossedRiver(bool red, int y) => red ? y >= 8 : y <= 7;
|
||
|
||
private static readonly (int, int)[] Dirs = { (1, 0), (-1, 0), (0, 1), (0, -1) };
|
||
|
||
public int At(int x, int y) => InBoard(x, y) ? _at[(y * Stride) + x] : Empty;
|
||
|
||
private void Put(int x, int y, int id)
|
||
{
|
||
if (InBoard(x, y)) _at[(y * Stride) + x] = id;
|
||
if (id >= 0 && id < 32) _pos[id] = InBoard(x, y) ? (y * Stride) + x : -1;
|
||
}
|
||
|
||
private void ClearAt(int x, int y)
|
||
{
|
||
if (!InBoard(x, y)) return;
|
||
var old = _at[(y * Stride) + x];
|
||
_at[(y * Stride) + x] = Empty;
|
||
if (old >= 0 && old < 32) _pos[old] = -1;
|
||
}
|
||
|
||
/// <summary>清空盘面(残局 / 自定义局面 / 回放重置用)。</summary>
|
||
public void Clear()
|
||
{
|
||
for (var i = 0; i < _at.Length; i++) _at[i] = Empty;
|
||
for (var i = 0; i < 32; i++) _pos[i] = -1;
|
||
MoveCount = 0;
|
||
}
|
||
|
||
/// <summary>直接把一枚子摆在某个点(残局 / 回放 / 摆位校验用)。</summary>
|
||
public void Place(int x, int y, int id)
|
||
{
|
||
if (!InBoard(x, y) || id < 0 || id >= 32) return;
|
||
ClearAt(x, y);
|
||
Put(x, y, id);
|
||
}
|
||
|
||
public void Init()
|
||
{
|
||
Clear();
|
||
RedToMove = true;
|
||
|
||
// 红 —— 与 ChineseChessChart.InitChartBoard 逐条对齐
|
||
Put(7, 3, 0); // 帅
|
||
Put(6, 3, 1); Put(8, 3, 2); // 仕 ×2
|
||
Put(5, 3, 3); Put(9, 3, 4); // 相 ×2
|
||
Put(4, 3, 5); Put(10, 3, 6); // 马 ×2
|
||
Put(3, 3, 7); Put(11, 3, 8); // 车 ×2
|
||
Put(4, 5, 9); Put(10, 5, 10); // 炮 ×2
|
||
Put(3, 6, 11); Put(5, 6, 12); Put(7, 6, 13); Put(9, 6, 14); Put(11, 6, 15); // 兵 ×5
|
||
|
||
// 黑
|
||
Put(7, 12, 16); // 将
|
||
Put(6, 12, 17); Put(8, 12, 18); // 士 ×2
|
||
Put(5, 12, 19); Put(9, 12, 20); // 象 ×2
|
||
Put(4, 12, 21); Put(10, 12, 22); // 马 ×2
|
||
Put(3, 12, 23); Put(11, 12, 24); // 车 ×2
|
||
Put(4, 10, 25); Put(10, 10, 26); // 炮 ×2
|
||
Put(3, 9, 27); Put(5, 9, 28); Put(7, 9, 29); Put(9, 9, 30); Put(11, 9, 31); // 卒 ×5
|
||
}
|
||
|
||
public bool TryLocate(int id, out int x, out int y)
|
||
{
|
||
var p = (id >= 0 && id < 32) ? _pos[id] : -1;
|
||
if (p < 0) { x = y = 0; return false; }
|
||
x = p % Stride; y = p / Stride;
|
||
return true;
|
||
}
|
||
|
||
public bool IsAlive(int id) => id >= 0 && id < 32 && _pos[id] >= 0;
|
||
|
||
private static int KingId(bool red) => red ? 0 : 16;
|
||
|
||
/// <summary>把 32 子的位置吐成一行(日志/断言用)。</summary>
|
||
public string Dump()
|
||
{
|
||
var parts = new List<string>();
|
||
for (var id = 0; id < 32; id++)
|
||
parts.Add(_pos[id] < 0 ? $"{id}=-" : $"{id}=({_pos[id] % Stride},{_pos[id] / Stride})");
|
||
return string.Join(" ", parts);
|
||
}
|
||
|
||
/// <summary>紧凑盘面串(棋盘 9×10,空格用 '.'),日志里一眼能看出错位。</summary>
|
||
public string Render()
|
||
{
|
||
var sb = new System.Text.StringBuilder();
|
||
for (var y = MinY; y <= MaxY; y++)
|
||
{
|
||
sb.Append(y - MinY).Append(' ');
|
||
for (var x = MinX; x <= MaxX; x++)
|
||
{
|
||
var id = At(x, y);
|
||
sb.Append(id == Empty ? '.' : Glyph(id).ToString());
|
||
sb.Append(' ');
|
||
}
|
||
sb.Append('\n');
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
|
||
private static char Glyph(int id)
|
||
{
|
||
var black = !IsRedId(id);
|
||
switch (KindOf(id))
|
||
{
|
||
case ChessKind.King: return black ? 'k' : 'K';
|
||
case ChessKind.Advisor: return black ? 'a' : 'A';
|
||
case ChessKind.Elephant: return black ? 'b' : 'B';
|
||
case ChessKind.Horse: return black ? 'n' : 'N';
|
||
case ChessKind.Rook: return black ? 'r' : 'R';
|
||
case ChessKind.Cannon: return black ? 'c' : 'C';
|
||
default: return black ? 'p' : 'P';
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────────
|
||
// 走法生成
|
||
// ─────────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 伪合法走法(几何 + 不吃己子 + 蹩腿/塞眼/炮架),**不**过滤「自将」。
|
||
/// ★ 王永远不会被「吃」——所以这里不把对方王的格子当落点;王的「输」由
|
||
/// 「无合法着法」(绝杀/困毙)表达。飞将的将军判定另见 <see cref="IsInCheck"/>。
|
||
/// </summary>
|
||
public List<(int X, int Y)> PseudoTargets(int id) => Targets(id, forAttack: false);
|
||
|
||
/// <summary>
|
||
/// 攻击集:与可落点唯一的差别是**包含对方王的格子**(用于将军判定)。
|
||
/// 把这两者分开是必须的 —— 混在一起会让「绝杀」永远判不出来(对方王总能"吃"回来)。
|
||
/// </summary>
|
||
private List<(int X, int Y)> Targets(int id, bool forAttack)
|
||
{
|
||
var res = new List<(int, int)>();
|
||
if (!TryLocate(id, out var x, out var y)) return res;
|
||
|
||
var red = IsRedId(id);
|
||
var kind = KindOf(id);
|
||
var enemyKing = KingId(!red);
|
||
|
||
void Add(int tx, int ty)
|
||
{
|
||
if (!InBoard(tx, ty)) return;
|
||
var t = At(tx, ty);
|
||
if (t != Empty)
|
||
{
|
||
if (IsRedId(t) == red) return; // 不吃己子
|
||
if (t == enemyKing && !forAttack) return; // 不吃王(攻击集里要留)
|
||
}
|
||
res.Add((tx, ty));
|
||
}
|
||
|
||
switch (kind)
|
||
{
|
||
case ChessKind.King:
|
||
Add(x + 1, y); Add(x - 1, y); Add(x, y + 1); Add(x, y - 1);
|
||
res.RemoveAll(p => !InPalace(red, p.Item1, p.Item2)); // 九宫限制
|
||
break;
|
||
|
||
case ChessKind.Advisor:
|
||
Add(x + 1, y + 1); Add(x + 1, y - 1); Add(x - 1, y + 1); Add(x - 1, y - 1);
|
||
res.RemoveAll(p => !InPalace(red, p.Item1, p.Item2));
|
||
break;
|
||
|
||
case ChessKind.Elephant:
|
||
for (var sx = -1; sx <= 1; sx += 2)
|
||
for (var sy = -1; sy <= 1; sy += 2)
|
||
{
|
||
var tx = x + (2 * sx); var ty = y + (2 * sy);
|
||
if (!InBoard(tx, ty)) continue;
|
||
if (!InOwnHalf(red, ty)) continue; // 象不过河
|
||
if (At(x + sx, y + sy) != Empty) continue; // 塞象眼
|
||
Add(tx, ty);
|
||
}
|
||
break;
|
||
|
||
case ChessKind.Horse:
|
||
for (var sx = -1; sx <= 1; sx += 2)
|
||
for (var sy = -1; sy <= 1; sy += 2)
|
||
{
|
||
// 蹩马腿:走 2 的那一轴上的相邻格必须为空
|
||
if (At(x + sx, y) == Empty) Add(x + (2 * sx), y + sy);
|
||
if (At(x, y + sy) == Empty) Add(x + sx, y + (2 * sy));
|
||
}
|
||
break;
|
||
|
||
case ChessKind.Rook:
|
||
foreach (var (dx, dy) in Dirs)
|
||
{
|
||
var tx = x + dx; var ty = y + dy;
|
||
while (InBoard(tx, ty))
|
||
{
|
||
var t = At(tx, ty);
|
||
if (t == Empty) res.Add((tx, ty));
|
||
else { if (IsRedId(t) != red) res.Add((tx, ty)); break; }
|
||
tx += dx; ty += dy;
|
||
}
|
||
}
|
||
break;
|
||
|
||
case ChessKind.Cannon:
|
||
foreach (var (dx, dy) in Dirs)
|
||
{
|
||
var tx = x + dx; var ty = y + dy;
|
||
// ① 未遇炮架前:空格可走
|
||
while (InBoard(tx, ty) && At(tx, ty) == Empty) { res.Add((tx, ty)); tx += dx; ty += dy; }
|
||
if (!InBoard(tx, ty)) continue;
|
||
// ② 越过炮架(该枚敌我皆可当架子):找架子之后的第一个子
|
||
tx += dx; ty += dy;
|
||
while (InBoard(tx, ty) && At(tx, ty) == Empty) { tx += dx; ty += dy; }
|
||
if (!InBoard(tx, ty)) continue;
|
||
var t2 = At(tx, ty);
|
||
if (IsRedId(t2) != red) res.Add((tx, ty)); // 隔一子吃敌子
|
||
}
|
||
break;
|
||
|
||
case ChessKind.Pawn:
|
||
Add(x, red ? y + 1 : y - 1); // 只能向前
|
||
if (PawnCrossedRiver(red, y)) { Add(x + 1, y); Add(x - 1, y); } // 过河可横走
|
||
break;
|
||
}
|
||
|
||
return res;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 某一方是否被将军。
|
||
/// ① 常规:敌方任何一枚子的**攻击集**覆盖我方王的格子;
|
||
/// ② 飞将:双方王同列且中间无子 —— 这一条必须显式写,
|
||
/// 因为「王」的攻击集里不含对方王(王不吃王)。
|
||
/// </summary>
|
||
public bool IsInCheck(bool red)
|
||
{
|
||
if (!TryLocate(KingId(red), out var kx, out var ky)) return false; // 王已不在盘上
|
||
|
||
// ② 飞将
|
||
if (TryLocate(KingId(!red), out var ex, out var ey) && ex == kx)
|
||
{
|
||
var clear = true;
|
||
for (var yy = Math.Min(ky, ey) + 1; yy <= Math.Max(ky, ey) - 1; yy++)
|
||
if (At(kx, yy) != Empty) { clear = false; break; }
|
||
if (clear) return true;
|
||
}
|
||
|
||
// ① 常规
|
||
for (var id = 0; id < 32; id++)
|
||
{
|
||
if (IsRedId(id) == red) continue; // 只看敌方
|
||
if (_pos[id] < 0) continue;
|
||
foreach (var (tx, ty) in Targets(id, forAttack: true))
|
||
if (tx == kx && ty == ky) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>走一步(不改回合、不校验合法性),返回被吃子的 id(无 = -1)。</summary>
|
||
public int ApplyMove(int id, int tx, int ty)
|
||
{
|
||
if (!TryLocate(id, out var x, out var y)) return Empty;
|
||
var captured = At(tx, ty);
|
||
ClearAt(x, y);
|
||
if (captured != Empty) ClearAt(tx, ty);
|
||
Put(tx, ty, id);
|
||
MoveCount++;
|
||
return captured;
|
||
}
|
||
|
||
/// <summary>撤销 ApplyMove。参数就是刚才那一步的原样。</summary>
|
||
public void UndoMove(int id, int fx, int fy, int tx, int ty, int captured)
|
||
{
|
||
ClearAt(tx, ty);
|
||
Put(fx, fy, id);
|
||
if (captured != Empty) Put(tx, ty, captured);
|
||
MoveCount--;
|
||
}
|
||
|
||
/// <summary>该子走这一步之后,自己是否被将军(= 送将 / 自将)。</summary>
|
||
public bool LeavesOwnKingInCheck(int id, int tx, int ty)
|
||
{
|
||
if (!TryLocate(id, out var fx, out var fy)) return true;
|
||
var captured = ApplyMove(id, tx, ty);
|
||
var bad = IsInCheck(IsRedId(id));
|
||
UndoMove(id, fx, fy, tx, ty, captured);
|
||
return bad;
|
||
}
|
||
|
||
/// <summary>某一方所有**合法**着法(已过滤自将)。</summary>
|
||
public List<ChessMove> LegalMoves(bool red)
|
||
{
|
||
var res = new List<ChessMove>();
|
||
for (var id = red ? 0 : 16; id < (red ? 16 : 32); id++)
|
||
{
|
||
if (_pos[id] < 0) continue;
|
||
if (!TryLocate(id, out var fx, out var fy)) continue;
|
||
foreach (var (tx, ty) in PseudoTargets(id))
|
||
if (!LeavesOwnKingInCheck(id, tx, ty))
|
||
res.Add(new ChessMove(id, fx, fy, tx, ty));
|
||
}
|
||
return res;
|
||
}
|
||
|
||
public bool HasAnyLegalMove(bool red) => LegalMoves(red).Count > 0;
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────────
|
||
// 评估 + AI
|
||
// ─────────────────────────────────────────────────────────────────────────────────
|
||
|
||
// 子力价值:与客户端 ChineseChessEnum.lua 的打分表同量级
|
||
// (帅10000 / 仕100 / 相100 / 马400 / 车900 / 炮450 / 兵100)
|
||
public static int ValueOf(int id)
|
||
{
|
||
switch (KindOf(id))
|
||
{
|
||
case ChessKind.King: return 10000;
|
||
case ChessKind.Advisor: return 100;
|
||
case ChessKind.Elephant: return 100;
|
||
case ChessKind.Horse: return 400;
|
||
case ChessKind.Rook: return 900;
|
||
case ChessKind.Cannon: return 450;
|
||
default: return 100; // Pawn
|
||
}
|
||
}
|
||
|
||
/// <summary>从黑方视角的静态子力差(正 = 黑优)。</summary>
|
||
public int EvalBlack()
|
||
{
|
||
var score = 0;
|
||
for (var id = 0; id < 32; id++)
|
||
{
|
||
if (_pos[id] < 0) continue;
|
||
score += IsRedId(id) ? -ValueOf(id) : ValueOf(id);
|
||
}
|
||
return score;
|
||
}
|
||
|
||
/// <summary>从某一方视角的静态分(正 = 该方占优)。</summary>
|
||
public int EvalFor(bool red) => red ? -EvalBlack() : EvalBlack();
|
||
|
||
/// <summary>黑方挑一步棋(= <see cref="PickMove"/> 的便捷壳,离线自测用)。</summary>
|
||
public ChessMove? PickBlackMove(byte difficulty) => PickMove(red: false, difficulty);
|
||
|
||
/// <summary>
|
||
/// 给<b>某一方</b>挑一步棋。difficulty = 0..4
|
||
/// (<c>UX.Game.ChineseChessAIDifficulty</c>: Beginner 0 / Easy 1 / Medium 2 / Hard 3 / Expert 4)。
|
||
/// Beginner/Easy 会有一小部分随机着,避免一开局就把玩家掐死。
|
||
///
|
||
/// ★ R183:原来只有 <see cref="PickBlackMove"/>。加了「玩家可以选择执黑」这条路之后,
|
||
/// 服务端还得替 AI 走**红方**(红先手,玩家执黑时开局是 AI 的),所以改成按颜色取。
|
||
/// </summary>
|
||
public ChessMove? PickMove(bool red, byte difficulty)
|
||
{
|
||
var mine = LegalMoves(red);
|
||
if (mine.Count == 0) return null; // 无着可走 = 已被将死/困毙
|
||
|
||
if (difficulty <= 1 && _rng.Next(100) < 20)
|
||
return mine[_rng.Next(mine.Count)];
|
||
|
||
ChessMove? best = null;
|
||
var bestScore = int.MinValue;
|
||
var myKing = KingId(red);
|
||
var enemy = !red;
|
||
|
||
foreach (var m in mine)
|
||
{
|
||
var captured = ApplyMove(m.FromId, m.ToX, m.ToY);
|
||
int score;
|
||
if (!IsAlive(myKing))
|
||
{
|
||
score = int.MinValue + 1; // 理论不可达(自将已被 LegalMoves 过滤)
|
||
}
|
||
else if (!IsAlive(KingId(enemy)))
|
||
{
|
||
score = 2_000_000; // 吃到对方王(同上,留作兜底)
|
||
}
|
||
else if (!HasAnyLegalMove(enemy))
|
||
{
|
||
score = 1_000_000; // 绝杀 / 困毙(象棋里困毙也算输)
|
||
}
|
||
else
|
||
{
|
||
// 第 2 层:对手会挑「对我最不利」的那一手(极小极大)
|
||
var worst = int.MaxValue;
|
||
foreach (var r in LegalMoves(enemy))
|
||
{
|
||
var rc = ApplyMove(r.FromId, r.ToX, r.ToY);
|
||
var v = EvalFor(red);
|
||
UndoMove(r.FromId, r.FromX, r.FromY, r.ToX, r.ToY, rc);
|
||
if (v < worst) worst = v;
|
||
}
|
||
score = worst;
|
||
if (captured != Empty) score += 5; // 同分时略偏好「吃到东西」
|
||
}
|
||
UndoMove(m.FromId, m.FromX, m.FromY, m.ToX, m.ToY, captured);
|
||
|
||
score += _rng.Next(-3, 4); // 轻微扰动,避免每局一模一样
|
||
if (score > bestScore) { bestScore = score; best = m; }
|
||
}
|
||
|
||
return best;
|
||
}
|
||
}
|
||
}
|