象棋和五子棋的AI开发(四)

四、五子棋AI算法

相较于象棋,五子棋的走法相当简单,也就是空白的地方都可以走。但五子棋的棋局判断,也就是估值函数比较复杂。另外,在一开始的时候,五子棋的可走步数是254、252、250,而象棋基本都是40多,所以五子棋每一层的运算量都会更大。后面我们会说到一些五子棋的优化方法。

4.1 估值核心

这是五子棋AI算法里面最复杂的一部分。五子棋的得分是基于线的,这些线可以是横的、竖的,也可以是斜的。为了计算方便,我们一开始就初始化了所有的这些线:

internal class Position
{
    public Position(int r, int c)
    {
        row = r;
        col = c;
    }

    public int row;
    public int col;
}

internal class Line
{
    public Line()
    {
        line = new List<Position>();
        score白 = 0;
        score黑 = 0;
    }

    public List<Position> line;
    public int score白;
    public int score黑;
}

LineList = new List<Line>();
//横
for (int i = 0; i < Row; i++)
{
    Line l = new Line();
    LineList.Add(l);
    for (int j = 0; j < Col; j++)
    {
        l.line.Add(new Position(i, j));
    }
}
//竖
for (int i = 0; i < Col; i++)
{
    Line l = new Line();
    LineList.Add(l);
    for (int j = 0; j < Row; j++)
    {
        l.line.Add(new Position(j, i));
    }
}
//右斜
for (int i = Row - 1; i >= 1 - Col; i--)
{
    Line l = new Line();
    LineList.Add(l);

    int r = i >= 0 ? i : 0;
    int c = i < 0 ? -i : 0;
    while (r < Row && c < Col)
    {
        l.line.Add(new Position(r, c));

        r++;
        c++;
    }
}
//左斜
for (int i = 0; i <= Row + Col - 2; i++)
{
    Line l = new Line();
    LineList.Add(l);

    int r = i < Col ? 0 : i - Col + 1;
    int c = i >= Col ? Col - 1 : i;
    while (r < Row && c >= 0)
    {
        l.line.Add(new Position(r, c));

        r++;
        c--;
    }
}

这样处理的话,我们后面寻找一行有多少个子不需要再分几种情况去处理了。

然后我们需要做的,就是分析一条线上,各种子排成的序列,应该值多少分。我们用_代表空,用O代表白子,用X代表黑子,如果一行是这样______XXOO____OXO___OOOX___XOOX__,那白子应该是多少分,黑子又是多少分呢?

我考虑了几种方案,有必要在这里说一下思路。一开始,我想统计出有多少种XOOO、_OOO、XOOOO、_OOOO这样的模式,累加作为分数。但我马上发现,如果是OOO__和OOO_O,或是OOO__O,或是OOO_X,它们的分值应该都是不一样的吧。

由于把不同颜色的棋子混在一起考虑比较复杂,我想先扫描一次整个串,然后分成同一颜色的段(空白属于所有颜色)。例如一开始的例子可以这样分:______XX、OO____O、X、O___OOO、X___X、OO、X__。这样对于每一段的分析就会简单一些。我考虑一个这样的问题:如果整个串是O_OOO_OOO,那应该怎么切分呢?是O_OOO + _OOO?还是O_O + OOO_OOO?我认为这里涉及到一个动态规划的问题。就是一个串,按不同的切分方法,能得到的最高分的组合。但是这样算下来的话,每一行需要的计算时间都会很长。我只能考虑使用贪婪算法。也就是说,先挑出最高分的序列,然后在剩下的序列里找出最高分的序列,直到剩下序列分数为0。

对于每一种序列,我对他们的分数安排是这样的:

计算一条线分数的核心代码如下:

private void CalcLineScore(Line l)
{
    Chess pre_chess = new Chess(ChessColor.无, ChessType.有);
    List<bool> list = new List<bool>();
    List<bool> last_space = new List<bool>();
    int list_type = 0;

    int score白 = 0;
    int score黑 = 0;
    for (int j = 0; j < l.line.Count; j++)
    {
        Position p = l.line[j];
        Chess c = board[p.row, p.col];
        if (c.type == ChessType.空)
        {
            list.Add(false);
            last_space.Add(false);
        }
        else
        {
            if (pre_chess.color == ChessColor.无)
            {
                list.Add(true);
                list_type = (c.color == ChessColor.白 ? 1 : 2);
                pre_chess = c;
            }
            else if (c.color == pre_chess.color)
            {
                list.Add(true);
            }
            else
            {
                if (list_type != 0)
                {
                    int value = GetSequenceValue(list);
                    if (list_type == 1)
                    {
                        score白 += value;
                    }
                    else
                    {
                        score黑 += value;
                    }
                }

                list.Clear();
                list.AddRange(last_space);
                list.Add(true);
                list_type = (c.color == ChessColor.白 ? 1 : 2);
                pre_chess = c;
            }

            last_space.Clear();
        }
    }

    if (list_type != 0)
    {
        int value = GetSequenceValue(list);
        if (list_type == 1)
        {
            score白 += value;
        }
        else
        {
            score黑 += value;
        }
    }

    l.score白 = score白;
    l.score黑 = score黑;
}

4.2 终局判断

五子棋的终局判断并不简单,需要把所有线都扫描一遍。因为终局判断调用频率极高,它的性能对于整个算法的性能影响是很大的。其实,终局只会由最终下棋所在的线决定,我们只需要计算四条线就可以了!

public override bool GameOver(Put[] pre_steps)
{
    if (pre_steps == null)
    {
        return false;
    }

    Put step = pre_steps[pre_steps.Length - 1];
    ChessColor c = board[step.ToRow, step.ToCol].color;

    int num = 1;
    int row = step.ToRow;
    int col = step.ToCol;
    while (row > 0)
    {
        row--;
        if (board[row, col].type != ChessType.空 && board[row, col].color == c)
        {
            num++;
            if (num >= 5)
            {
                wincolor = c;
                return true;
            }
        }
        else
        {
            break;
        }
    }
    row = step.ToRow;
    while (row < Row - 1)
    {
        row++;
        if (board[row, col].type != ChessType.空 && board[row, col].color == c)
        {
            num++;
            if (num >= 5)
            {
                wincolor = c;
                return true;
            }
        }
        else
        {
            break;
        }
    }

    num = 1;
    row = step.ToRow;
    col = step.ToCol;
    while (col > 0)
    {
        col--;
        if (board[row, col].type != ChessType.空 && board[row, col].color == c)
        {
            num++;
            if (num >= 5)
            {
                wincolor = c;
                return true;
            }
        }
        else
        {
            break;
        }
    }
    col = step.ToCol;
    while (col < Col - 1)
    {
        col++;
        if (board[row, col].type != ChessType.空 && board[row, col].color == c)
        {
            num++;
            if (num >= 5)
            {
                wincolor = c;
                return true;
            }
        }
        else
        {
            break;
        }
    }

    num = 1;
    row = step.ToRow;
    col = step.ToCol;
    while (row > 0 && col > 0)
    {
        row--;
        col--;
        if (board[row, col].type != ChessType.空 && board[row, col].color == c)
        {
            num++;
            if (num >= 5)
            {
                wincolor = c;
                return true;
            }
        }
        else
        {
            break;
        }
    }
    row = step.ToRow;
    col = step.ToCol;
    while (row < Row - 1 && col < Col - 1)
    {
        row++;
        col++;
        if (board[row, col].type != ChessType.空 && board[row, col].color == c)
        {
            num++;
            if (num >= 5)
            {
                wincolor = c;
                return true;
            }
        }
        else
        {
            break;
        }
    }

    num = 1;
    row = step.ToRow;
    col = step.ToCol;
    while (row > 0 && col < Col - 1)
    {
        row--;
        col++;
        if (board[row, col].type != ChessType.空 && board[row, col].color == c)
        {
            num++;
            if (num >= 5)
            {
                wincolor = c;
                return true;
            }
        }
        else
        {
            break;
        }
    }
    row = step.ToRow;
    col = step.ToCol;
    while (row < Row - 1 && col > 0)
    {
        row++;
        col--;
        if (board[row, col].type != ChessType.空 && board[row, col].color == c)
        {
            num++;
            if (num >= 5)
            {
                wincolor = c;
                return true;
            }
        }
        else
        {
            break;
        }
    }

    return false;
}

4.3 每层搜索数量的优化

五子棋一般使用15*15的棋盘,也就是说有225个格子。一开始,每一层的搜索数量是224、222、220,即使使用了历史启发,搜索的数量还是相当巨大。

其实我们可以用一个最小矩形来优化。假如敌方第一步把棋子下在了中心(7,7),那最优的下法会出现在哪里呢?一般就是在这棋子的四周。假如我们把所有棋子都圈在一个矩形里面,然后适当考虑一个阈值(这里我们定的是3),在这个阈值之外的棋子根本就不需要考虑。这样我们的计算速度能得到极大的提高。例如在只有一个棋(7,7)的情况下,我们的最小矩形是(L7,R7,T7,B7),加上阈值,就是(L4,R10,T4,B10),这样需要考虑的棋子只有48颗!

经过测试,使用最小矩形,算法速度能提高10倍以上。

 

理论暂说这些,完整的代码下载:https://download.csdn.net/download/lweiyue/10807906

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值