tags: 應用, 開發隨筆
已完成章節索引
用Swift寫圍棋App-00序
用Swift寫圍棋App-01準備工作
用Swift寫圍棋App-02建立工程
用Swift寫圍棋App-03StoryBoard
用Swift寫圍棋App-04棋盤
用Swift寫圍棋App-05初識棋譜
用Swift寫圍棋App-06解析器初版
用Swift寫圍棋App-07解析器改進
用Swift寫圍棋App-08繪制每一手棋
用Swift寫圍棋App-09分片算法
用Swift寫圍棋App-10氣的算法
用Swift寫圍棋App-11算法改進
上次寫完氣的算法后,以為快接近尾聲了。在使用中卻發現有一些問題:
- 如果在下棋過程中,如果曾經有多手下在同一個位置,則計算不正確;
- 在回退時,被"提"的棋子顯示不正常;
- 在“打劫”的時候程序處理不正確;
對這個問題反復思考,我發現根本原因是計算過程中忽略了棋子的先后順序,分組和算氣的時候只是考慮了棋子的位置,而沒有考慮落子先后。我這才明白: 圍棋的過程是時間相關的,棋子的先后有著重要的作用。
具體說來有以下規則: - 每一步棋只是在當前的形勢下起作用; 而且只會影響對方的某一片棋的生死; (棋譜中不存在一手棋讓自己沒氣的情形)
- 歷史上已經死了棋不會復生; 它們也不會對分組或者任何一片棋起作用;
想清楚了這個問題,改進算法就不難了。
首先,我們需要在 Model也就是Move中記錄落子的先后:
class Move: NSObject {
...
var handNumber = 0 //the order of move
var handOfDead = -1 // in which step the move is caculated to be dead
...
然后,當前這一步只需要考慮對前一步結果的影響。因此,成了一個遞歸問題。其最初狀態是第一手棋,圍棋中第一手肯定是黑棋,它不管落在何處,這個棋自己就成了一個組,而且也是第一手那個狀態時的唯一一個組:
// the very beginning
if hand == 0 {
let group0 = MoveGroup()
group0.name = "B0"
group0.addMove(allMoves[0])
return [group0]
}
接下來,后面每一手依賴于前一手的結果:
var groups = playToHand(hand - 1)
當前手對分組的影響只會影響自己這一邊已經存在的組:
- 如果當前手和前面自己方的任何一組中的任何一個棋子相連,則當前手加入那一組;
- 如果當前手和多個組相連,則這些組需要合并為一個新的組;
- 如果當前手不和任何自己方的組相連,則需要建立一個新的組;
實現如下:
let groupsWithSameColor = groups.filter({$0.name.hasPrefix(type)})
var handled = false
lastMove.groupName = ""
for g in groupsWithSameColor {
for move in g.allMoves {
if move.isConnectedTo(lastMove) {
if lastMove.groupName == "" {
handled = true
g.addMove(lastMove)
} else {
// last move is already in a group, current group needs to be merged into that group
let groupToConnect = groupsWithSameColor.filter({$0.name == lastMove.groupName}).first!
g.mergeTo(groupToConnect)
}
break
}
}// end for move
}//end for g
分完組再計算每一個對方組的氣,判斷其死活:
// filter out empty groups
let liveGroups = groups.filter({$0.allMoves.count > 0 })
let allOccupied = occupiedLocations(allMoves.filter({$0.handNumber <= hand && $0.handOfDead == -1}))
// only need to check opposite party
let oppositeGroups = liveGroups.filter({!$0.name.hasPrefix(type)})
for grp in oppositeGroups{
let liberty = grp.calculateLiberty(allOccupied)
if liberty == 0 {
for move in grp.allMoves {
move.isDead = true
move.handOfDead = hand
}
grp.isDead = true
}
}
寫起來一氣呵成。完整的算法實現:
func playToHand(hand:Int)-> [MoveGroup]{
// the very beginning
if hand == 0 {
let group0 = MoveGroup()
group0.name = "B0"
group0.addMove(allMoves[0])
return [group0]
}
//the current step depends on the last status
var groups = playToHand(hand - 1)
let lastMove = allMoves[hand]
let type = String(lastMove.type.rawValue) //"B" or "W"
let groupsWithSameColor = groups.filter({$0.name.hasPrefix(type)})
var handled = false
lastMove.groupName = ""
for g in groupsWithSameColor {
for move in g.allMoves {
if move.isConnectedTo(lastMove) {
if lastMove.groupName == "" {
handled = true
g.addMove(lastMove)
} else {
// last move is already in a group, current group needs to be merged into that group
let groupToConnect = groupsWithSameColor.filter({$0.name == lastMove.groupName}).first!
g.mergeTo(groupToConnect)
}
break
}
}// end for move
}//end for g
if !handled {
let groupNew = MoveGroup()
groupNew.name = "\\(lastMove.type.rawValue)\\(lastMove.handNumber)"
groupNew.addMove(lastMove)
groups.append(groupNew)
}
// filter out empty groups
let liveGroups = groups.filter({$0.allMoves.count > 0 })
let allOccupied = occupiedLocations(allMoves.filter({$0.handNumber <= hand && $0.handOfDead == -1}))
// only need to check opposite party
let oppositeGroups = liveGroups.filter({!$0.name.hasPrefix(type)})
for grp in oppositeGroups{
let liberty = grp.calculateLiberty(allOccupied)
if liberty == 0 {
for move in grp.allMoves {
move.isDead = true
move.handOfDead = hand
}
grp.isDead = true
}
}
return liveGroups.filter({!$0.isDead })
}
效果如下:
screen.jpg
最新的代碼已經放到github:https://github.com/marknote/GoTao
玩起來挺有趣的,下到200手左右有驚喜 :)