棋类游戏对战的实现

  • 六洲棋
  • 五子棋
  • AI对战
  • 蓝牙对战
  • 在线对战

六洲棋

六洲棋,又称:泥棋、插方、来马、五福棋,中国民间传统棋类体育形式。源于民间,简便、通俗、易学,在民间广为流行,深受社会底层大众的喜爱。龙其在淮河流域的安徽省、河南省、江苏省、以及湖北省、山东省非常普及,并流传到中国各地,包括港、澳、台地区。起源于劳动人民生活,根植于民间大众之中,它简捷、明快,趣味性、竞技性强,是一项长期流行于民间,富有传统文化色彩的竞技项目。对于启迪智慧,休闲娱乐,增进交流非常有益。列安徽省第二批省级非物质文化遗产。
6*6纵横线组成,共三十六个棋点。每方十八枚棋子,以两色区分敌我。

规则

对弈过程分三阶段。(凤阳下法)放子:对弈双方依次将己子放入空棋点,将手上的棋子放完才开始走子。逼子:若无棋子被吃,使得棋子放满棋盘。则两人各选对方一枚敌子移出游戏。走子:由后手方开始轮流移动己棋,沿线直横线一格。吃子:无论是下子或走子阶段,只要己方棋子排成以下排列称为成城,就要吃掉一定数量的敌子,但不可吃掉已成城子的敌棋。在放子阶段,被吃的子先作记号,等走子阶段开始才一齐提取。
成六:六枚棋子以纵、横和斜3个方向连成直线(除了四条边的直线)。吃掉敌方三子。

斜五:连子的2头都靠棋盘边缘,吃掉敌方两子。

斜四:连子的2头都靠棋盘边缘,吃掉敌方一子。

斜三:连子的2头都靠棋盘边缘,吃掉敌方一子。

成方:四枚棋子组成一个紧邻相连的小正方形,吃掉敌方一子。

使对方只剩下三枚以下则获胜。因为是民间文化,各地稍有差异。

棋型的算法实现

“`swift
//是否形成斜子棋(三子棋,四子棋,五子棋,六子棋)
static func isXiZiChess(_ point:SWSPoint,_ chessArray: [[FlagType]]) -> LianZhuState?{
let type = chessArray[point.x][point.y]
let pointLeft = SWSPoint()
let pointRight = SWSPoint()
let ponitTop = SWSPoint()
let pointBottom = SWSPoint()

    // 东北方向var i = 0while point.x - i >= 0 && point.y + i <= 5 && chessArray[point.x - i][point.y + i] == type {pointLeft.x = point.x - ipointLeft.y = point.y + ii += 1}i = 0while point.x + i <= 5 && point.y - i >= 0 && chessArray[point.x + i][point.y - i] == type {pointRight.x = point.x + ipointRight.y = point.y - ii += 1}//西北方向i = 0while point.x - i >= 0 && point.y - i >= 0 && chessArray[point.x - i][point.y - i] == type {ponitTop.x = point.x - iponitTop.y = point.y - ii += 1}i = 0while point.x + i <= 5 && point.y + i <= 5 && chessArray[point.x + i][point.y + i] == type {pointBottom.x = point.x + ipointBottom.y = point.y + ii += 1}print(pointRight.x,pointRight.y,pointLeft.x,pointLeft.y,ponitTop.x,ponitTop.y,pointBottom.x,pointBottom.y)let arr = [3,2,1,0]for index in arr {func condition() -> Bool {if pointRight.x == 2+index && pointRight.y == 0 && pointLeft.x == 0 && pointLeft.y == 2+index {return true}if pointRight.x == 5  && pointRight.y == 3 - index && pointLeft.x == 3 - index && pointLeft.y == 5 {return true}if ponitTop.x == 0 && ponitTop.y == 3-index && pointBottom.x == 2+index && pointBottom.y == 5 {return true}if ponitTop.x == 3-index && ponitTop.y == 0 && pointBottom.x == 5 && pointBottom.y == 2+index {return true}return false}if condition() {switch index {case 0:return .threeChesscase 1:return .fourChesscase 2:return .fiveChesscase 3:return .sixChessdefault:()}}}return nil
}//是否形成方格棋
static func isCheckChess(_ point:SWSPoint,_ chessArray: [[FlagType]]) ->LianZhuState? {let type = chessArray[point.x][point.y]//左上if point.x - 1 >= 0 && point.y - 1 >= 0 && chessArray[point.x][point.y-1] == type &&chessArray[point.x-1][point.y] == type && chessArray[point.x-1][point.y-1] == type {return .checkChess}//左下if point.x - 1 >= 0 && point.y + 1 <= 5 && chessArray[point.x][point.y+1] == type &&chessArray[point.x-1][point.y] == type && chessArray[point.x-1][point.y+1] == type {return .checkChess}//右上if point.x + 1 <= 5 && point.y - 1 >= 0 && chessArray[point.x][point.y-1] == type &&chessArray[point.x+1][point.y] == type && chessArray[point.x+1][point.y-1] == type {return .checkChess}//右下if point.x + 1 <= 5 && point.y + 1 <= 5 && chessArray[point.x][point.y+1] == type &&chessArray[point.x+1][point.y] == type && chessArray[point.x+1][point.y+1] == type {return .checkChess}return nil
}

“`

小结

六洲棋,在我们老家被称为泥棋,小时候经常玩的一种棋,偶有回忆,因此实现下这个游戏,望能找到个棋友没事玩玩,这种棋,玩法多种,很有趣。

五子棋

五子棋五子棋是比较流行的棋类游戏了,玩法简单,基本上人人会玩,在此就不介绍游戏规则了。下面使用 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 = 0while point.x - i >= 0 && chessArray[point.x - i][point.y] == type {pointLeft.x = point.x - ii += 1}i = 0while point.x + i <= 14 && chessArray[point.x + i][point.y] == type {pointRight.x = point.x + ii += 1}if pointRight.x - pointLeft.x == 4 && (pointLeft.y != 15 || pointLeft.y != 0){return true}//南北方向i = 0while point.y - i >= 0 && chessArray[point.x][point.y-i] == type {pointTop.y = point.y - ii += 1}i = 0while point.y + i <= 14 && chessArray[point.x][point.y+i] == type {pointBottom.y = point.y + ii += 1}if pointBottom.y - pointTop.y == 4 && (pointTop.x != 15 || pointTop.x != 0) {return true}// 东北方向i = 0while point.x - i >= 0 && point.y + i <= 14 && chessArray[point.x - i][point.y + i] == type {pointLeft45.x = point.x - ipointLeft45.y = point.y + ii += 1}i = 0while point.x + i <= 14 && point.y - i >= 0 && chessArray[point.x + i][point.y - i] == type {pointRight45.x = point.x + ipointRight45.y = point.y - ii += 1}if pointLeft45.y - pointRight45.y == 4{return true}//西北方向i = 0while point.x - i >= 0 && point.y - i >= 0 && chessArray[point.x - i][point.y - i] == type {pointTop135.x = point.x - ipointTop135.y = point.y - ii += 1}i = 0while point.x + i <= 14 && point.y + i <= 14 && chessArray[point.x + i][point.y + i] == type {pointBottom135.x = point.x + ipointBottom135.y = point.y + ii += 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最不利的位置,以此类推下去直到到达我们定义的最大搜索深度。所以每一层轮流从子节点选择最大值-最小值-最大值-最小值…

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

“`swift
//活一、活二、活三、活四、连五、眠一,眠二、眠三、眠四
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_Fivelet minScore = -1*maxScorelet checkmateDeep = self.checkmateDeepvar 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 = minScorecount = 0ABcut = 0for i in 0..<points.count {let p = points[i]chessArray[p.x][p.y] = roleself.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 = scorebestPoint.removeAll()bestPoint.append((p.x,p.y))}chessArray[p.x][p.y] = .freeChessself.updateOneEffectScore(chessArray: chessArray, point: (p.x,p.y), AIScore: &AIScore, humanScore: &humanScore)}steps += 1total += countif 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 = 0var aiMax = 0var humMax = 0var best = minScorefor 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 += 1if 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] = roleself.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)) * deepDecreasechessArray[p.x][p.y] = .freeChessself.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 += 1return 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 = 2var result:(Int,Int,Int)?while i <= deep {if let test = humMax(deep: i) {result = testif TJFTool.greatOrEqualThan(a: Float(test.2), b: Float(live_Four)) {return test}}i += 2}if result == nil {var maxAiScore = 0for 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 次方,也就是说原来需要遍历X^Y个节点,现在只需要遍历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 + otersvar 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 inreturn 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 inreturn abs(b.2) > abs(a.2)}}

小结

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

蓝牙对战

MultipeerConnectivity框架的使用

MultipeerConnectivity通过WiFi、P2P WiFi以及蓝牙个人局域网进行通信的框架,从而无需联网手机间就能传递消息。其原理是通过广播作为服务器去发现附近的节点,每个节点都以设备名称为标识。

   myPeer = MCPeerID.init(displayName: UIDevice.current.name)session = MCSession.init(peer: myPeer!, securityIdentity: nil, encryptionPreference: .none)session?.delegate = self

MCSession的几个代理方法必须实现,否则无法建立连接

    //监听连接状态func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {switch state {case .notConnected:print("未连接")case .connecting:print("正在连接中")case .connected:print("连接成功")}}//发送Dada数据func sendData(_ messageVo: GPBMessage, successBlock:(()->())?,errorBlock:((NSError)->())?) {guard let session = session else {return}guard let data = NSDataTool.shareInstance().returnData(messageVo, messageId: 0) else {return}do {try session.send(data as Data , toPeers: session.connectedPeers, with: .reliable)}catch let error as NSError {errorBlock?(error)return}successBlock?()}//接收到的Data数据func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {// 解析出过来的data数据包NSDataTool.shareInstance().startParse(data) { (gpbMessage) inself.getMessageBlock?(gpbMessage)}}//接收到的流数据func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {print("streamName")}//接收到的文件类型数据func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {print("resourceName")}//接收到的文件类型数据,可将文件换路劲func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL, withError error: Error?) {}

我们通过MCAdvertiserAssistant(广播)开启搜索服务

    advertiser = MCAdvertiserAssistant.init(serviceType: serviceStr, discoveryInfo: nil, session: session!)//发出广播advertiser?.start()advertiser?.delegate = self

MCBrowserViewControllerDelegate代理方法

 /// 发出广播请求func advertiserAssistantWillPresentInvitation(_ advertiserAssistant: MCAdvertiserAssistant) {print("advertiserAssistantWillPresentInvitation")}/// 结束广播请求func advertiserAssistantDidDismissInvitation(_ advertiserAssistant: MCAdvertiserAssistant) {print("advertiserAssistantDidDismissInvitation")} 

设置蓝牙连接页面,显示效果如图所示:

  func setupBrowserVC() {guard let session = session else {return}browser = MCBrowserViewController.init(serviceType: serviceStr,  session: session)browser?.delegate = self}

实现MCBrowserViewControllerDelegate代理方法

 func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) {print("蓝牙连接完成")browser?.dismiss(animated: true, completion: { [weak self] inself?.browserBlock?()})}func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) {print("取消蓝牙连接")browser?.dismiss(animated: true, completion: nil)}

小结

使用蓝牙技术进行传输数据,尽管不需要连接网络服务,但是真实因为这样存在着许多安全隐患,为此我们引入Google Protobuf框架进行数据传输。下章会对该技术的运用进行详解。

protobuf在iOS中的运用

protocolbuffer(以下简称protobuf)是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、oc、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml和json 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。
我们重点介绍protobuf在iOS中的运用,官方文档

protobuf使用步骤

  • 定义.proto文件
  • 配置protobuf环境
  • 映射相应语言的文件
  • 导入第三方库protobuf

.proto文件的定义

该文件主要是用来作为你传递数据的数据结构的文档,然后通过终端命令生成我们相应语言的model类,导入项目中使用。
.proto的定义语法有官方文档自己学习,在此不过多介绍,在此一定要注意的是,一定要使用proto3来定义,proto2已经在很多第三方库中被淘汰使用(以前用的都是proto2,Proto3出来并不了解,报错信息一度让我怀疑人生)。定义文件类似下图所示:

配置protobuf环境

使用homebrew进行配置(如果没安装,自己谷歌安装)

  • brew install automake
  • brew install libtool
  • brew install protobuf
  • ln -s /usr/local/Cellar/protobuf/(上步中安装protobuf的版本号)/bin/protoc /usr/local/bin
  • git clone https://github.com/alexeyxo/protobuf-objc.git(oc版本)或者
    git clone https://github.com/alexeyxo/protobuf-swift.git(swift版本)
  • cd protobuf-objc
  • ./autogen.sh
  • ./configure CXXFLAGS=-I/usr/local/include LDFLAGS=-L/usr/local/lib
  • make install

映射相应语言的文件

  • cd 到.proto文件的路劲中
  • protoc –plugin=/usr/local/bin/protoc-gen-objc test.proto –objc_out=.
    此为生成oc类的命令,其中test.proto是自己生成的proto文件的名字。相应swift类的命令为:
    protoc –plugin=/usr/local/bin/protoc-gen-swift test.proto –swift_out=.
  • 将生成的文件导入项目中

导入第三方库protobuf

这里建议使用pod管理:pod ‘Protobuf’

Protobuf库的使用

一般就是将Data类型的数据映射成model和将model生成data类型数据两个方法,他们分别是
使用GPBMessage中的俩个方法


+ (instancetype)parseFromData:(NSData *)data error:(NSError **)errorPtr {return [self parseFromData:data extensionRegistry:nil error:errorPtr];
}- (nullable NSData *)data;

小结

使用protobuf传输还是存在安全问题和数据比较大时的耗能问题,于是我们想到了,在直播领域应用很普遍的RTMP协议。下章详细讲解,使用分包思想拆解数据包进行数据传输。

RTMP协议蓝牙传输数据

RTMP传统定义

rtmp协议中基本的数据单元被称为消息(message)结构一般为:

  • 时戳:4 byte,单位毫秒。超过最大值后会翻转。
  • 长度:消息负载的长度。
  • 类型ID:Type Id 一部分ID范围用于rtmp的控制信令。还有一部分可以供上层使用,rtmp只是透 传。这样可以方便的在rtmp上进行扩展。
  • 消息流ID:Message Stream ID,用于区分不同流的消息。

消息在网络中传输时,会被分割成很多小的消息块,进行传输,增加传输的效率,而这些消息块是由消息头+消息体组成,消息头就是制定的标识消息的协议,消息体就是所传输的消息内容。

RTMP在蓝牙中的定义

手机蓝牙传输数据,无法保证双方手机时间同步,因此刨除时间戳定义改为固定字符串,因此messageHeader定义为:

“`oc
struct message_header
{
uint32_t magic;//magic number, 0x98765432
uint32_t total;//包长度,从这一字段头算起
uint32_t msgid;//消息ID
uint32_t seqnum;//客户端使用,自增量
uint32_t version;//协议版本,目前为1

};

“`

将需要传输的数据添加message_header

  //GPBMEssage为protobuf库里的类,请参考上篇文章-(NSMutableData*)returnData:(GPBMessage*)req messageId:(int)messageId {NSString *header=[NSString stringWithFormat:@"98765432%08lx%08x%08lx00000001",(unsigned long)req.data.length+20,messageId,(unsigned long)++self.header_count];Byte bytes[40];int j=0;for(int i=0;i*2+1<header.length;i++){int int_ch;  /// 两位16进制数转化后的10进制数const char* hex_char=[[header substringWithRange:NSMakeRange(i*2, 2)] UTF8String];int_ch = (int)strtoul(hex_char, 0, 16);//        DLog(@"int_ch=%d",int_ch);bytes[j] = int_ch;  ///将转化后的数放入Byte数组里j++;}NSMutableData *data = [[NSMutableData alloc] init];[data appendBytes:bytes length:j];[data appendData:req.data];return data;
}

接受到数据后,需要把长度小于message_header长度的数据进行拼包,并解析message_header结构

  //解析数据message_header结构-(void)parseSocketReceiveData:(NSData*)data result:(void (^)(NSData*result ,int messageId,int hearderId))resultBlock finish:(void(^)())finishBlockMessage{if (_halfData.length>0) {[_halfData appendData:data];data=[_halfData copy];_halfData =[[NSMutableData alloc]init];}else{data=[data copy];}if (data.length<20) {[_halfData appendData:data];if (finishBlockMessage) {finishBlockMessage();}return;}Byte *testByte = (Byte*)[data bytes];int length=(int) ((testByte[4] & 0xFF<<24)| ((testByte[5] & 0xFF)<<16)| ((testByte[6] & 0xFF)<<8)| ((testByte[7] & 0xFF)));int messageId=(int) ((testByte[8] & 0xFF<<24)| ((testByte[9] & 0xFF)<<16)| ((testByte[10] & 0xFF)<<8)| ((testByte[11] & 0xFF)));int headerId=(int)((testByte[12] & 0xFF<<24)| ((testByte[13] & 0xFF)<<16)| ((testByte[14] & 0xFF)<<8)| ((testByte[15] & 0xFF)));if(length==data.length){if (resultBlock) {resultBlock([data subdataWithRange:NSMakeRange(20, length-20)],messageId,headerId);}if (finishBlockMessage) {finishBlockMessage();}}else if(length<data.length){if (resultBlock) {resultBlock([data subdataWithRange:NSMakeRange(20, length-20)],messageId,headerId);}[self parseSocketReceiveData:[data subdataWithRange:NSMakeRange(length, data.length-length)] result:resultBlock finish:            finishBlockMessage];}else{[_halfData appendData:data];if (finishBlockMessage) {finishBlockMessage();}}
}

小结

rtmp协议虽然加快了数据传输的效率,一定程度上的安全,但是并不是特别的安全,为避免攻击者攻击,一些安全措施还是有必要的,在这里不过多介绍,有兴趣自己调研。

在线对战

IM采用的是环信SDK,环信作为免费的socket服务,相对已经很好了,功能也挺全面,但是,如果作为严谨的功能开发,他所暴露出来的api是远远不够的,如传输的数据必须是它定好的结构,虽然有个自定义字典可以传输但是,这个字典也是仅仅限于几种数据类型(做主要的DATA类型不接受)。导入SDK官方文档

环信的主要用到的API

环信的主要用到的API需要实现的代理

    //在初始化是设置代理private override init() {super.init()EMClient.shared().add(self, delegateQueue: nil)EMClient.shared().chatManager.add(self, delegateQueue: nil)EMClient.shared().contactManager.add(self, delegateQueue: nil)EMClient.shared().groupManager.add(self, delegateQueue: nil)EMClient.shared().roomManager.add(self, delegateQueue: nil)}  //在对象释放时,释放代理对象deinit {EMClient.shared().removeDelegate(self)EMClient.shared().chatManager.remove(self)EMClient.shared().contactManager.removeDelegate(self)EMClient.shared().groupManager.removeDelegate(self)EMClient.shared().roomManager.remove(self)}

实现登录异常的代理:服务器断开,开启定时器定时重连(环信并没有给出重连的api,我发现调用环信的需要连接服务器的api,sdk会自动重连服务器,所以断开服务器,定时调用上传错误日志的api,机制吧。)

extension ChatHelpTool: EMClientDelegate{//主要处理断开服务器重连机制func connectionStateDidChange(_ aConnectionState: EMConnectionState) {networkState?(aConnectionState)switch aConnectionState {case EMConnectionConnected:print("服务器已经连上")if reconnectTimer != nil {reconnectTimer.invalidate()reconnectTimer = nil}case EMConnectionDisconnected:print("服务器已断开")if reconnectTimer != nil {reconnectTimer.invalidate()reconnectTimer = nil}DispatchQueue.global().async {self.reconnectTimer = Timer.weak_scheduledTimerWithTimeInterval(2, selector: { [weak self] inself?.reconnectNetwork()}, repeats: true)self.reconnectTimer.fire()RunLoop.current.add(self.reconnectTimer, forMode: RunLoopMode.defaultRunLoopMode)RunLoop.current.run()}default:()}}func autoLoginDidCompleteWithError(_ aError: EMError!) {if let error = aError {TJFTool.errorForCode(code: error.code)TJFTool.loginOutMessage(message: "自动登录失败,请重新登录。")}else {PAMBManager.sharedInstance.showBriefMessage(message: "自动登录成功")}}//异地登录func userAccountDidLoginFromOtherDevice() {TJFTool.loginOutMessage(message: "该账号在其他设备上登录,请重新登录。")}func userAccountDidRemoveFromServer() {TJFTool.loginOutMessage(message: "当前登录账号已经被从服务器端删除,请重新登录")}func userDidForbidByServer() {TJFTool.loginOutMessage(message: "服务被禁用,请重新登录")}
}

实现发送消息的方法:因为是自定义的数据结构,所以使用消息的扩展,自定义字典传递数据。

“`swift
//发送消息
extension ChatHelpTool {
// 定义消息model EMMessage
static func sendTextMessage(text:String,toUser:String,messageType:EMChatType,messageExt:[String:Any]?) ->EMMessage?{
let body = EMTextMessageBody.init(text: text)
let from = EMClient.shared().currentUsername
let message = EMMessage.init(conversationID: toUser, from: from, to: toUser, body: body, ext: messageExt)
message?.chatType = messageType
return message
}
//发送消息
static func senMessage(aMessage:EMMessage,progress aProgressBlock:(( _ progres: Int32)->())?,completion aCompletionBlock:((_ message:EMMessage?,_ error:EMError?)->())?) {

    DispatchQueue.global().async {EMClient.shared().chatManager.send(aMessage, progress: aProgressBlock,completion:aCompletionBlock)}}

}
“`
实现接收消息的代理

extension ChatHelpTool: EMChatManagerDelegate{//会话列表发生变化<EMConversation>func conversationListDidUpdate(_ aConversationList: [Any]!) {print("会话列表发生变化")}//收到消息func messagesDidReceive(_ aMessages: [Any]!) {aMessages.forEach { (message) inif let message = message as? EMMessage {if  let data = message.ext as? [String:Any] {let model = MessageModel.init(dictionary: data)if model.gameType == "1" {self.letterOfChallengeAction(["userName":message.from,"message":(model.challengeList?.message).noneNull,"chessType":model.chessType.noneNull])}else if model.gameType == "2" {var role:Role = .blackervar gameType:GameType = .LiuZhouChessif model.chessType == "1" {role = .whitergameType = .fiveInRowChess}TJFTool.pushToChessChatRoom(message.from,role,chessType: gameType)}else {self.buZiChessMessage?(message)}}}}}//收到已读回执func messagesDidRead(_ aMessages: [Any]!) {print("收到已读回执")}//收到消息送达回执func messagesDidDeliver(_ aMessages: [Any]!) {print("收到消息送达回执")aMessages.forEach { (message) inif let message = message as? EMMessage {if  let data = message.ext as? [String:Any] {let model = MessageModel.init(dictionary: data)if model.gameType == "3" {}}print(message.messageId)print(TJFTool.timeWithTimeInterVal(time: message.timestamp),TJFTool.timeWithTimeInterVal(time: message.localTime))}}}//消息状态发生变化func messageStatusDidChange(_ aMessage: EMMessage!, error aError: EMError!){print("消息状态发生变化")}}

小结

IM在没有服务器的情况下,使用第三方免费的最方便,但是同时并不能满足产品的需求,有机会,我会为大家分享一篇自定义socket服务器下的即时通信结构和逻辑的设定。

最后

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

ios 棋类游戏对战的实现相关推荐

  1. iOS简易蓝牙对战五子棋游戏设计思路之一——核心蓝牙通讯类的设计

    iOS简易蓝牙对战五子棋游戏设计思路之一--核心蓝牙通讯类的设计 一.引言 本系列博客将系统的介绍一款蓝牙对战五子棋的开发思路与过程,其中的核心部分有两个,一部分是蓝牙通讯中对战双方信息交互框架的设计 ...

  2. iOS简易蓝牙对战五子棋游戏设计思路之二——核心棋盘逻辑与胜负判定算法

    2019独角兽企业重金招聘Python工程师标准>>> iOS简易蓝牙对战五子棋游戏设计思路之二--核心棋盘逻辑与胜负判定算法 一.引言 上一篇博客我们介绍了在开发一款蓝牙对战五子棋 ...

  3. 好玩的java战旗游戏_这6款可玩性高的战棋类游戏你喜欢玩吗?快get起来!

    战棋类游戏属于策略类角色扮演游戏,俗话说就走格打仗,这类游戏有战略性强,可玩性高等特点深受广大玩家喜爱,下面小编就为大家介绍几款经典的战棋类游戏. 1.风色幻想系列:提到战棋类游戏风色幻想系列可以说是 ...

  4. 您玩过这十三款iOS策略游戏了吗?

    子曰:三人行,必有我师焉,择其善者而从之,其不善者而改之.是故,乃成此篇. 一.小小战争(Great Little War Game) "Great Little War Game" ...

  5. 您玩过这十三款iOS策略游戏了吗

    子曰:三人行,必有我师焉,择其善者而从之,其不善者而改之.是故,乃成此篇. 一.小小战争(Great Little War Game) "Great Little War Game" ...

  6. python棋类程序_python棋类游戏编写入门

    刚接触棋类游戏程序编写的朋友,往往比较迷惑,不知从何下手. 本文总结了棋类游戏的主程序流程.计算机走子策略.打分方式(以井字棋.黑白棋.五子棋为例),未使用minimax算法,比较简单,适合刚接触的朋 ...

  7. 【Unity连载】斗兽棋—棋类游戏开发演示(1)

    序言 "黄梅时节家家雨,青草池塘处处蛙.有约不来过夜半,闲敲棋子落灯花." "象棋终日乐悠悠,苦被严亲一旦丢.兵卒坠河皆不救,将军溺水一齐休.马行千里随波去,象入三川逐浪 ...

  8. 【Unity连载】斗兽棋-棋类游戏开发演示(2)

    第四章 游戏操作与指令 如同养育一个婴儿,父母总会一步步引领孩子成长,从蹒跚学步到来去如风:我们对游戏功能的开发,也无疑应当从走出第一步棋开始.现在,我们已经构建出了棋盘.棋子等基本的游戏逻辑对象:那 ...

  9. AlphaZero炼成最强通用棋类AI,DeepMind强化学习算法8小时完爆人类棋类游戏

    [新智元导读]或许"智能爆炸"不会发生,但永远不要低估人工智能的发展.推出最强围棋AI AlphaGo Zero不到50天,DeepMind又一次超越了他们自己,也刷新了世人对人工 ...

最新文章

  1. 用gdb调试mpi程序的一些心得
  2. VS 2017开发ASP.NET Core Web应用过程中发现的一个重大Bug
  3. PHP框架的ORM思想:O类的实例化 R数据表 M映射XML
  4. STM32连续采样_STM32 - 利用双缓冲实现实时曲线显示(续)
  5. No package ‘gtksourceview-4‘ found
  6. 单片机 STM32 HAL IO扩展 74HC595 例子代码
  7. python中函数调用
  8. STM8 低功耗设计
  9. 解决xshell中数字小键盘不能使用的问题
  10. 离散数学——容斥原理
  11. 走进MyBatis源码一探Spring扩展点「知识点多多」「扩展点实战系列」- 第449篇
  12. 聚(N-异丙基丙烯酰胺)PNIPAM离子微凝胶-聚合物后分散形成的纳米粒子-瑞禧
  13. 俩分钟认识Sketch的基础功能
  14. 摄像头镜头焦距与照射距离关系图
  15. vue全局更新以及孙组件调用祖组件(利用provide和inject)
  16. Android音频 —— 音量调节
  17. python_MOD13A3_NDVI 最大合成
  18. rancher坏了或删除,继续使用k8s集群
  19. 去看李记者,天津印象
  20. 网易云信携手美踏,打造偶像社交新范式

热门文章

  1. 立象条码打印机PPLB类单双标签打印
  2. 轧钢测径仪软件分析图 钢材质量直观体现
  3. Linux中vsftpd配置文件vsftpd.conf详解
  4. 2021-03-24----------------------120月均线--------绩优股指数和垃圾股指数
  5. 升压芯片很简单(三),FSB628升压芯片大串讲
  6. 66个求职应聘技巧性问答(四)
  7. COMSOL两相流(相场法)
  8. python html 制作pdf,python将html转成PDF的实现代码(包含中文) -电脑资料
  9. 国产申威处理器上如何部署FFTW3,实现FFT的高效运算
  10. ARIMA-GARCH模型对央行汇率的实证研究(R)