五子棋AI算法的实现

五子棋

五子棋五子棋是比较流行的棋类游戏了,玩法简单,基本上人人会玩,在此就不介绍游戏规则了。下面使用 swift实现五子棋这个游戏,主要实现AI算法,包括极大值极小值算法,深度搜索算法,估值函数,Alpha Beta 剪枝算法等等。

 //横向五子连珠(除去四边线的五子连珠)
    static func isFiveChess(_ point:SWSPoint,_ chessArray: [[FlagType]]) -> Bool {
        let type = chessArray[point.x][point.y]
        let pointLeft = SWSPoint()
        let pointRight = SWSPoint()
        let pointTop = SWSPoint()
        let pointBottom = SWSPoint()
        let pointLeft45 = SWSPoint()
        let pointRight45 = SWSPoint()
        let pointTop135  = SWSPoint()
        let pointBottom135 = SWSPoint()
        //东西方向
        var i = 0
        while point.x - i >= 0 && chessArray[point.x - i][point.y] == type {
            pointLeft.x = point.x - i
            i += 1
        }
        i = 0
        while point.x + i <= 14 && chessArray[point.x + i][point.y] == type {
            pointRight.x = point.x + i
            i += 1
        }
        
        if pointRight.x - pointLeft.x == 4 && (pointLeft.y != 15 || pointLeft.y != 0){
            return true
        }
        //南北方向
        i = 0
        while point.y - i >= 0 && chessArray[point.x][point.y-i] == type {
            pointTop.y = point.y - i
            i += 1
        }
        i = 0
        while point.y + i <= 14 && chessArray[point.x][point.y+i] == type {
            pointBottom.y = point.y + i
            i += 1
        }
        if pointBottom.y - pointTop.y == 4 && (pointTop.x != 15 || pointTop.x != 0) {
            return true
        }
        
        // 东北方向
         i = 0
        while point.x - i >= 0 && point.y + i <= 14 && chessArray[point.x - i][point.y + i] == type {
            pointLeft45.x = point.x - i
            pointLeft45.y = point.y + i
            i += 1
        }
        i = 0
        while point.x + i <= 14 && point.y - i >= 0 && chessArray[point.x + i][point.y - i] == type {
            pointRight45.x = point.x + i
            pointRight45.y = point.y - i
            i += 1
        }
        
        if pointLeft45.y - pointRight45.y == 4{
            return true
        }
        
        //西北方向
        i = 0
        while point.x - i >= 0 && point.y - i >= 0 && chessArray[point.x - i][point.y - i] == type {
            pointTop135.x = point.x - i
            pointTop135.y = point.y - i
            i += 1
        }
        i = 0
        while point.x + i <= 14 && point.y + i <= 14 && chessArray[point.x + i][point.y + i] == type {
            pointBottom135.x = point.x + i
            pointBottom135.y = point.y + i
            i += 1
        }
        if pointBottom135.y - pointTop135.y == 4{
            return true
        }
        
        return false
    }

demo中实现了五子棋的AI、同机、蓝牙、在线对战,下面重点介绍AI对战。

五子棋的AI算法实现

2017年互联网最火的技术毫无疑问就是AI了,在此尝试写了个算法来和人脑来pk。五子棋属于零和游戏:一方胜利代表另一方失败,而零和游戏的代表算法就是极大值极小值搜索算法。

极大值极小值搜索算法

A、B二人对弈,A先走,A始终选择使局面对自己最有利的位置,然后B根据A的选择,在剩下的位置中选择对A最不利的位置,以此类推下去直到到达我们定义的最大搜索深度。所以每一层轮流从子节点选择最大值-最小值-最大值-最小值...

这里写图片描述

我们如何知道哪个位置最有利和最不利呢?在此我们引入一套评估函数,来对棋盘上每个位置进行分数评估

 //活一、活二、活三、活四、连五、眠一,眠二、眠三、眠四
 enum FiveChessType:Int {
    case liveOne = 0
    case liveTwo
    case liveThree
    case liveFour
    case liveFive
    case sleepOne
    case sleepTwo
    case sleepThree
    case sleepFour
    case unknown
    var score:Int  {
        switch self {
        case .unknown:
            return un_known
        case .sleepOne:
            return sleep_One
        case .liveOne,.sleepTwo:
            return live_One
        case .liveTwo,.sleepThree:
            return live_Two
        case .liveThree:
            return live_Three
        case .sleepFour:
            return sleep_Four
        case .liveFour:
            return live_Four
        case .liveFive:
            return live_Five
            
        }
    }
    
}
let live_Five = 1000000
let live_Four = 100000
let sleep_Four = 10000
let live_Three = 1000
let live_Two = 100
let sleep_Three = 100
let live_One = 10
let sleep_Two = 10
let sleep_One = 1
let un_known = 0

在使用极大值极小值进行深度搜索时,遍历节点是指数增长的,如果不进行算法优化,将会导致电脑计算时间过长,影响下棋体验,所以这里引入 Alpha Beta 剪枝原理。

Alpha Beta 剪枝原理

AlphaBeta剪枝算法是一个搜索算法旨在减少在其搜索树中,被极大极小算法评估的节点数。
Alpha-Beta只能用递归来实现。这个思想是在搜索中传递两个值,第一个值是Alpha,即搜索到的最好值,任何比它更小的值就没用了,因为策略就是知道Alpha的值,任何小于或等于Alpha的值都不会有所提高。
第二个值是Beta,即对于对手来说最坏的值。这是对手所能承受的最坏的结果,因为我们知道在对手看来,他总是会找到一个对策不比Beta更坏的。如果搜索过程中返回Beta或比Beta更好的值,那就够好的了,走棋的一方就没有机会使用这种策略了。
在搜索着法时,每个搜索过的着法都返回跟Alpha和Beta有关的值,它们之间的关系非常重要,或许意味着搜索可以停止并返回。
如果某个着法的结果小于或等于Alpha,那么它就是很差的着法,因此可以抛弃。因为我前面说过,在这个策略中,局面对走棋的一方来说是以Alpha为评价的。
如果某个着法的结果大于或等于Beta,那么整个节点就作废了,因为对手不希望走到这个局面,而它有别的着法可以避免到达这个局面。因此如果我们找到的评价大于或等于Beta,就证明了这个结点是不会发生的,因此剩下的合理着法没有必要再搜索。
如果某个着法的结果大于Alpha但小于Beta,那么这个着法就是走棋一方可以考虑走的,除非以后有所变化。因此Alpha会不断增加以反映新的情况。有时候可能一个合理着法也不超过Alpha,这在实战中是经常发生的,此时这种局面是不予考虑的,因此为了避免这样的局面,我们必须在博弈树的上一个层局面选择另外一个着法。链接

c代码实现原理

int AlphaBeta(int depth, int alpha, int beta) 
{
    if (depth == 0) 
    {
        return Evaluate();
    }
    GenerateLegalMoves();
    while (MovesLeft()) 
    {
        MakeNextMove();
        val = -AlphaBeta(depth - 1, -beta, -alpha);
        UnmakeMove();
        if (val >= beta) 
        {
            return beta;
        }
        if (val > alpha) 
        {
            alpha = val;
        }
    }
    return alpha;
} 

实际在代码中的运用,代码比较复杂请结合项目理解。项目地址

static func getAIPoint(chessArray:inout[[FlagType]],role:FlagType,AIScore:inout [[Int]],humanScore:inout [[Int]],deep:Int) ->(Int,Int,Int)? {
        
        let maxScore = 10*live_Five
        let minScore = -1*maxScore
        let checkmateDeep = self.checkmateDeep
       var total=0, //总节点数
        steps=0,  //总步数
        count = 0,  //每次思考的节点数
        ABcut = 0 //AB剪枝次数
        
        
        func humMax(deep:Int)->(Int,Int,Int)? {
            let points = self.getFiveChessType(chessArray: chessArray, AIScore: &AIScore, humanScore: &humanScore)
            var bestPoint:[(Int,Int)] = []
            var best = minScore
            count = 0
            ABcut = 0
         
            for i in 0..<points.count {
                let p = points[i]
                chessArray[p.x][p.y] = role
                self.updateOneEffectScore(chessArray: chessArray, point: (p.x,p.y), AIScore: &AIScore, humanScore: &humanScore)
                var score = -aiMaxS(deep: deep-1, alpha: -maxScore, beta: -best, role: self.reverseRole(role: role))
                if p.x < 3 || p.x > 11 || p.y < 3 || p.y > 11 {
                    score = score/2
                }
                if TJFTool.equal(a: Float(score), b: Float(best)){
                    bestPoint.append((p.x,p.y))
                }
                if TJFTool.greatThan(a: Float(score), b: Float(best)){
                    best = score
                    bestPoint.removeAll()
                    bestPoint.append((p.x,p.y))
                }
                chessArray[p.x][p.y] = .freeChess
                self.updateOneEffectScore(chessArray: chessArray, point: (p.x,p.y), AIScore: &AIScore, humanScore: &humanScore)
                
            }
            steps += 1
            total += count
            if bestPoint.count > 0 {
                let num = arc4random()%UInt32(bestPoint.count)
                return (bestPoint[Int(num)].0,bestPoint[Int(num)].1,best)
            }
            return nil
           
        }
        
        func aiMaxS(deep:Int,alpha:Int,beta:Int,role:FlagType) -> Int{
            var score = 0
            var aiMax = 0
            var humMax = 0
            var best = minScore
            for i in 0..<15{
                for j in 0..<15{
                    if chessArray[i][j] == .freeChess{
                        aiMax = max(AIScore[i][j], aiMax)
                        humMax = max(humanScore[i][j], humMax)
                    }
                }
            }
            score = (role == .blackChess ? 1 : -1) * (aiMax-humMax)
            count += 1
            if deep <= 0 || TJFTool.greatOrEqualThan(a: Float(score), b: Float(live_Five)){
                return score
            }
            let points =  self.getFiveChessType(chessArray: chessArray, AIScore: &AIScore, humanScore: &humanScore)
            for i in 0..<points.count{
                let p = points[i]
                chessArray[p.x][p.y] = role
                self.updateOneEffectScore(chessArray: chessArray, point: (p.x,p.y), AIScore: &AIScore, humanScore: &humanScore)
                let some = -aiMaxS(deep: deep-1, alpha: -beta, beta: -1 * ( best > alpha ? best : alpha), role: self.reverseRole(role: role)) * deepDecrease
                chessArray[p.x][p.y] = .freeChess
                self.updateOneEffectScore(chessArray: chessArray, point: (p.x,p.y), AIScore: &AIScore, humanScore: &humanScore)
                if TJFTool.greatThan(a: Float(some), b: Float(best)) {
                    best = some
                }
                //在这里进行ab 剪枝
                if TJFTool.greatOrEqualThan(a: Float(some), b: Float(beta)){
                    ABcut += 1
                    return some
                }
            }
            
            if (deep == 2 || deep == 3 || deep == 4) && TJFTool.littleThan(a: Float(best), b: Float(sleep_Four)) && TJFTool.greatThan(a: Float(best), b: -(Float)(sleep_Four)){
                
                if let result = self.checkmateDeeping(chessArray: &chessArray, role: role, AIScore: &AIScore, humanScore: &humanScore, deep: checkmateDeep) {
                   return Int(Double(result[0].2) * pow(0.8, Double(result.count)) * (role == .blackChess ? 1:-1))
                }
            }
            return best
        }
        
        var i = 2
        var result:(Int,Int,Int)?
        while i <= deep {
            if let test = humMax(deep: i) {
                result = test
                if TJFTool.greatOrEqualThan(a: Float(test.2), b: Float(live_Four)) {
                    return test
                }
            }
            i += 2
        }
        if result == nil {
            var maxAiScore = 0
            for i in 0..<15{
                for j in 0..<15 {
                    if chessArray[i][j] == .freeChess && maxAiScore < AIScore[i][j] {
                        maxAiScore = AIScore[i][j]
                        result = (i,j,maxAiScore)
                    }
                }
            }
        }
        
        return result
    }

经过Alpha Beta剪枝后,优化效果应该达到 1/2 次方,也就是说原来需要遍历XY个节点,现在只需要遍历X(Y/2)个节点,相比之前已经有了极大的提升。
不过即使经过了Alpha Beta 剪枝,思考层数也只能达到四层,也就是一个不怎么会玩五子棋的普通玩家的水平。而且每增加一层,所需要的时间或者说计算的节点数量是指数级增加的。所以目前的代码想计算到第六层是很困难的。
我们的时间复杂度是一个指数函数 X^Y,其中底数X是每一层节点的子节点数,Y 是思考的层数。我们的剪枝算法能剪掉很多不用的分支,相当于减少了 Y,那么下一步我们需要减少 X,如果能把 X 减少一半,那么四层平均思考的时间能降低到 0.5^4 = 0.06 倍,也就是能从10秒降低到1秒以内。
如何减少X呢?我们知道五子棋中,成五、活四、双三、双眠四、眠四活三是必杀棋,于是我们遇到后就不用再往下搜索了。代码如下:

static func getFiveChessType(chessArray:[[FlagType]],AIScore:inout [[Int]],humanScore:inout [[Int]]) ->[(x:Int,y:Int)]{
        var twos:[(Int,Int)] = []
        var threes:[(Int,Int)] = []
        var doubleThrees:[(Int,Int)] = []
        var sleepFours:[(Int,Int)] = []
        var fours:[(Int,Int)] = []
        var fives:[(Int,Int)] = []
        var oters:[(Int,Int)] = []
        for i in 0..<15{
            for j in 0..<15{
                if chessArray[i][j] == .freeChess && self.effectivePoint(chessArray: chessArray, point: (x: i, y: j)) {
                    let aiScore = AIScore[i][j]
                    let humScore = humanScore[i][j]
                    if aiScore>=live_Five {
                        return[(i,j)]
                    }else if humScore >= live_Five {
                        fives.append((i,j))
                    }else if aiScore >= live_Four {
                        fours.insert((i,j), at: 0)
                    }else if humScore >= live_Four {
                        fours.append((i,j))
                    }else if aiScore >= sleep_Four{
                        sleepFours.insert((i,j), at: 0)
                    }else if humScore >= sleep_Four{
                        sleepFours.append((i,j))
                    }else if aiScore >= 2*live_Three{
                        doubleThrees.insert((i,j), at: 0)
                    }else if humScore >= 2*live_Three{
                        doubleThrees.append((i,j))
                    }else if aiScore >= live_Three {
                        threes.insert((i,j), at: 0)
                    }else if humScore >= live_Three {
                        threes.append((i, j))
                    }else if aiScore >= live_Two{
                        twos.insert((i,j), at: 0)
                    }else if humScore >= live_Two{
                        twos.append((i,j))
                    }else {
                        oters.append((i,j))
                    }
                }
            }
        }
        
        if fives.count > 0 {
            return [fives[0]]
        }
        if fours.count > 0 {
            return fours
        }
        if sleepFours.count > 0{
            return [sleepFours[0]]
        }
        if doubleThrees.count > 0{
            return doubleThrees + threes
        }
        let result = threes + twos + oters
        var realy:[(Int,Int)] = []
        if result.count > limitNum {
            realy += result.prefix(limitNum)
            return realy
        }
        return result
    }

五子棋是一种进攻优势的棋,依靠连续不断地活三或者冲四进攻,最后很容易会形成必杀棋,所以在进行深度搜索时,我们另开一种连续进攻的搜索,如果,电脑可以依靠连续进攻获得胜利,我们可以直接走这条路劲。这条路劲,其实也是极大值极小值搜索算法的一种,只不过是只考虑活三冲四这两种棋型,指数的底数较小,搜索的节点比较少,因此是效率很高的算法。代码如下:

//有限考虑ai成五
 static func findMaxScore(chessArray:[[FlagType]],role:FlagType,aiScore:[[Int]],humanScore:[[Int]],score:Int)->[(Int,Int,Int)]{
        var result:[(Int,Int,Int)] = []
        for i in 0..<15{
            for j in 0..<15{
                if chessArray[i][j] == .freeChess {
                    if self.effectivePoint(chessArray: chessArray, point: (i,j),chessCount: 1) {
                        let score1 =  role == .blackChess ?  aiScore[i][j] : humanScore[i][j]
                        if score1 >= live_Five {
                            return [(i,j,score1)]
                        }
                        if score1 >= score {
                            result.append((i,j,score1))
                            
                        }
                    }
                }
            }
        }
      return  result.sorted { (a, b) -> Bool in
            return b.2 > a.2
        }
        
    }
    //考虑活三,冲四
    static func findEnemyMaxScore(chessArray:[[FlagType]],role:FlagType,aiScore:[[Int]],humanScore:[[Int]],score:Int)->[(Int,Int,Int)]{
        var result:[(Int,Int,Int)] = []
        var fours:[(Int,Int,Int)] = []
        var fives:[(Int,Int,Int)] = []
        for i in 0..<15{
            for j in 0..<15{
                if chessArray[i][j] == .freeChess {
                    if  self.effectivePoint(chessArray: chessArray, point: (i,j),chessCount: 1) {
                        let score1 =  role == .blackChess ?  aiScore[i][j] : humanScore[i][j]
                        let score2 = role == .blackChess ?  humanScore[i][j] : aiScore[i][j]
                        if score1 >= live_Five {
                            return [(i,j,-score1)]
                        }
                        if score1 >= live_Four {
                            fours.insert((i,j,-score1), at: 0)
                            continue
                        }
                        if score2 >= live_Five {
                         fives.append((i,j,score2))
                            continue
                        }
                        if score2 >= live_Four{
                            fours.append((i,j,score2))
                            continue
                        }
                        if score1 > score || score2 > score {
                            result.append((i,j,score1))
                        }
                    }
                }
            }
        }
        if fives.count > 0 {
            return [fives[0]]
        }
        if fours.count > 0 {
            return [fours[0]]
        }
      return  result.sorted { (a, b) -> Bool in
            return abs(b.2) > abs(a.2)
        }
    }

小结

本次编写的AI还是比较强的,我胜利的机会很少,但还是存在赢的时候,因此AI算法还存在漏洞,主要表现在评分标准不准确和搜索深度不够问题上,如何优化评分标准和搜索算法,是实现AI无敌的关键工作。
另外,在增加搜索深度的同时,遍历的节点指数增长,计算时间增长,可以结合哈希算法,保存每次的棋盘评分,一定程度上提高计算时间,这也只是治标不治本的做法。

最后

代码具体实现地址
代码中具体实现了两个棋类游戏(有时间会持续添加游戏种类),包括在线对战,人机对战(算法不错哦),蓝牙对战。
代码编写不易,喜欢的请点赞,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容