二、基类
这是两种棋子算法共有的部分。
public abstract class IGame<T>
{
/// <summary>
/// 伪无限大
/// </summary>
protected const int MAX_VALUE = 100000000;
/// <summary>
/// 伪无限小
/// </summary>
protected const int MIN_VALUE = -100000000;
protected int _row;
/// <summary>
/// 获得行数
/// </summary>
public int Row
{
get
{
return _row;
}
}
protected int _col;
/// <summary>
/// 获得列数
/// </summary>
public int Col
{
get
{
return _col;
}
}
protected ChessColor wincolor;
/// <summary>
/// 胜利一方,必须在GameOver为true时调用
/// </summary>
public ChessColor WinColor
{
get
{
return wincolor;
}
}
protected Chess[,] board;
/// <summary>
/// 刚刚移动的棋子颜色
/// </summary>
public ChessColor MoveColor;
/// <summary>
/// 剩余棋子数
/// </summary>
public int RemainChess;
/// <summary>
/// 算法深度
/// </summary>
protected int AlgorithmDepth;
/// <summary>
/// 最优走步
/// </summary>
protected T BestStep;
/// <summary>
/// 历史走步分数
/// </summary>
protected Dictionary<int, int> history_score;
/// <summary>
/// 获得棋子
/// </summary>
public Chess Chess(int row, int col)
{
return board[row, col];
}
/// <summary>
/// 初始化
/// </summary>
public abstract void Init();
/// <summary>
/// 是否游戏结束
/// </summary>
/// <param name="pre_steps">历史走步</param>
public abstract bool GameOver(T[] pre_steps);
/// <summary>
/// 移动一步
/// </summary>
/// <returns>是否移动成功</returns>
public abstract bool Move(T step);
/// <summary>
/// 产生所有可以移动的步
/// </summary>
/// <param name="c">要移动的颜色</param>
/// <returns>所以可移动走步列表</returns>
protected abstract List<T> GenerateAllMove(ChessColor c);
#region 算法
/// <summary>
/// 计算最优走步
/// </summary>
/// <param name="c">要移动的颜色</param>
/// <param name="max_depth">最深深度</param>
/// <returns>最优走步</returns>
public T CalcBestMove(ChessColor c, int max_depth, bool clear_history)
{
if (clear_history)
{
history_score = new Dictionary<int, int>();
}
else
{
if (history_score == null)
{
history_score = new Dictionary<int, int>();
}
}
AlgorithmDepth = max_depth;
AlphaBeta(c, AlgorithmDepth, MIN_VALUE, MAX_VALUE, null);
return BestStep;
}
/// <summary>
/// Alpha-Beta剪枝算法
/// </summary>
/// <param name="c">棋子颜色</param>
/// <param name="depth">深度</param>
/// <param name="alpha">alpha</param>
/// <param name="beta">beta</param>
/// <param name="pre_steps">历史走步</param>
/// <returns>当前节点的最高值</returns>
protected int AlphaBeta(ChessColor c, int depth, int alpha, int beta, T[] pre_steps)
{
if (depth == 0 || GameOver(pre_steps))
{
return Evaluation(c, pre_steps);
}
List<T> steps = GenerateAllMove(c);
SortMove(steps);
T current_best_step = default(T);
foreach (T step in steps)
{
object obj = MakeMove(step);
T[] new_steps = null;
if (pre_steps == null)
{
new_steps = new T[1];
}
else
{
new_steps = new T[pre_steps.Length + 1];
Array.Copy(pre_steps, new_steps, pre_steps.Length);
}
new_steps[new_steps.Length - 1] = step;
int score = -AlphaBeta(SwitchColor(c), depth - 1, -beta, -alpha, new_steps);
UnmakeMove(step, obj);
if (score > alpha)
{
alpha = score;
if (depth == AlgorithmDepth)
{
BestStep = step;
}
current_best_step = step;
if (alpha >= beta)
{
break;
}
}
}
if (current_best_step != null)
{
int from_chess = GetStepNum(current_best_step);
if (history_score.ContainsKey(from_chess))
{
history_score[from_chess] += (1 << depth);
}
else
{
history_score.Add(from_chess, (1 << depth));
}
}
return alpha;
}
/// <summary>
/// 进走步进行排序
/// </summary>
protected abstract void SortMove(List<T> steps);
/// <summary>
/// 估值
/// </summary>
/// <param name="c">要估值的颜色</param>
/// <param name="pre_steps">历史走步</param>
/// <returns>分数</returns>
protected abstract int Evaluation(ChessColor c, T[] pre_steps);
/// <summary>
/// 虚拟走一步
/// </summary>
protected abstract object MakeMove(T step);
/// <summary>
/// 撤销走一步
/// </summary>
protected abstract void UnmakeMove(T step, object obj);
/// <summary>
/// 相反颜色
/// </summary>
protected abstract ChessColor SwitchColor(ChessColor c);
/// <summary>
/// 把走步转成一个数
/// </summary>
protected abstract int GetStepNum(T step);
#endregion
}
在基类里面,或者需要我们注意的只有CalcBestMove和AlphaBeta两个函数。这两个函数在《PC游戏编程(人机博弈)》一书中都有详细讲解,我这里只说明不一样的地方。
在说明历史启发这一算法时,《PC游戏编程(人机博弈)》认为只需要标记起始位置和结束位置。但我认为起始位置是什么棋子也是有必要记录的。历史记录存放在一个字典里面,在象棋里面,键值的计算如下:
protected override int GetStepNum(Step step)
{
return step.ToRow * 100000 + step.ToCol * 10000 + step.FromRow * 1000 + step.FromCol * 100 + (int)board[step.FromRow, step.FromCol].color * 10 + (int)board[step.FromRow, step.FromCol].type;
}
而在五子棋里面,键值计算如下:
protected override int GetStepNum(Put step)
{
return step.ToRow * 1000 + step.ToCol * 10 + (int)step.Color;
}
我们可以看到,象棋的键值可能性明显更多,而且随着游戏的进展,某些移动位置的优势参考价值已不大。所以在象棋里面,每一次计算最优走步,历史启发数据都全部清空。
但五子棋就不一样了。五子棋的键值明显只有225种,而且越到后面会越少。所以我们在整个游戏里面,都不清空五子棋的历史启发数据。
在AlphaBeta函数里面,我加入了对整个走步过程的记录,这主要在打印路线和五子棋估值的时候使用。