用Go写一个中国象棋(十七)| 开局库
到目前为止,象棋AI基本上都完成了,但在和AI对弈的过程中会发现几个细节问题:
(1) 对于同一个局面,总是走固定的走法。
(2) 搜索算法是否能更优化一些。
(3) 有些杀棋局面会走出莫名其妙的走法。
开局库
开局库几乎是每个象棋程序必备的部件,它的好处是:
(1) 即使再笨的程序,开局库能使得它们在开局阶段看上去不那么业余。
(2) 通过随机选择走法,让开局灵活多变,增加对弈的趣味性。
打开define.go:
//BanValue 长将判负的分值,低于该值将不写入置换表
const BanValue = MateValue - 100
//WinValue 搜索出胜负的分值界限,超出此值就说明已经搜索出杀棋了
const WinValue = MateValue - 200
//RandomMask 随机性分值
const RandomMask = 7
//BookSize 开局库大小
const BookSize = 16384
BookItem
打开rule.go,增加:
//BookItem 开局库项结构
type BookItem struct {
dwLock uint32
wmv int
wvl int
}
dwLock 记录了局面 Zobrist 校验码中的 dwLock1。
wmv 是走法。
wvl 是权重(随机选择走法的几率,仅当两个相同的 dwLock 有不同的 wmv 时,wvl 的值才有意义)。
修改Search,增加开局库:
//Search 与搜索有关的全局变量
type Search struct {
mvResult int // 电脑走的棋
nHistoryTable [65536]int // 历史表
mvKillers [LimitDepth][2]int // 杀手走法表
hashTable [HashSize]*HashItem // 置换表
BookTable []*BookItem // 开局库
}
开局库方法
//loadBook 加载开局库
func (p *PositionStruct) loadBook() bool {
file, err := os.Open("./res/book.dat")
if err != nil {
fmt.Print(err)
return false
}
defer file.Close()
reader := bufio.NewReader(file)
if reader == nil {
return false
}
for {
line, _, err := reader.ReadLine()
if err != nil {
if err == io.EOF {
break
} else {
fmt.Print(err)
return false
}
}
tmpLine := string(line)
tmpResult := strings.Split(tmpLine, ",")
if len(tmpResult) == 3 {
tmpItem := &BookItem{}
tmpdwLock, err := strconv.ParseUint(tmpResult[0], 10, 32)
if err != nil {
fmt.Print(err)
continue
}
tmpItem.dwLock = uint32(tmpdwLock)
tmpwmv, err := strconv.ParseInt(tmpResult[1], 10, 32)
if err != nil {
fmt.Print(err)
continue
}
tmpItem.wmv = int(tmpwmv)
tmpwvl, err := strconv.ParseInt(tmpResult[2], 10, 32)
if err != nil {
fmt.Print(err)
continue
}
tmpItem.wvl = int(tmpwvl)
p.search.BookTable = append(p.search.BookTable, tmpItem)
}
}
return true
}
由于开局库是按照dwLock排序的,因此可以用二分查找。找到一项以后,把它前后dwLock相同的所有项都取出,从中随机选择一个wmv:
//searchBook 搜索开局库
func (p *PositionStruct) searchBook() int {
bkToSearch := &BookItem{}
mvs := make([]int, MaxGenMoves)
vls := make([]int, MaxGenMoves)
bookSize := len(p.search.BookTable)
//如果没有开局库,则立即返回
if bookSize <= 0 {
return 0
}
//搜索当前局面
bMirror := false
bkToSearch.dwLock = p.zobr.dwLock1
lpbk := sort.Search(bookSize, func(i int) bool {
return p.search.BookTable[i].dwLock >= bkToSearch.dwLock
})
//如果没有找到,那么搜索当前局面的镜像局面
if lpbk == bookSize || (lpbk < bookSize && p.search.BookTable[lpbk].dwLock != bkToSearch.dwLock) {
bMirror = true
posMirror := NewPositionStruct()
p.mirror(posMirror)
bkToSearch.dwLock = posMirror.zobr.dwLock1
lpbk = sort.Search(bookSize, func(i int) bool {
return p.search.BookTable[i].dwLock >= bkToSearch.dwLock
})
}
//如果镜像局面也没找到,则立即返回
if lpbk == bookSize || (lpbk < bookSize && p.search.BookTable[lpbk].dwLock != bkToSearch.dwLock) {
return 0
}
//如果找到,则向前查第一个开局库项
for lpbk >= 0 && p.search.BookTable[lpbk].dwLock == bkToSearch.dwLock {
lpbk--
}
lpbk++
//把走法和分值写入到"mvs"和"vls"数组中
vl, nBookMoves, mv := 0, 0, 0
for lpbk < bookSize && p.search.BookTable[lpbk].dwLock == bkToSearch.dwLock {
if bMirror {
mv = mirrorMove(p.search.BookTable[lpbk].wmv)
} else {
mv = p.search.BookTable[lpbk].wmv
}
if p.legalMove(mv) {
mvs[nBookMoves] = mv
vls[nBookMoves] = p.search.BookTable[lpbk].wvl
vl += vls[nBookMoves]
nBookMoves++
if nBookMoves == MaxGenMoves {
// 防止"book.dat"中含有异常数据
break
}
}
lpbk++
}
if vl == 0 {
// 防止"book.dat"中含有异常数据
return 0
}
//根据权重随机选择一个走法
vl = rand.Intn(vl)
i := 0
for i = 0; i < nBookMoves; i++ {
vl -= vls[i]
if vl < 0 {
break
}
}
return mvs[i]
}
mirror
为了压缩开局库的容量,所有对称的局面只用一项,所以当一个局面在BookTable中找不到时,还应该试一下它的对称局面是否在BookTable中。
//mirror 对局面镜像
func (p *PositionStruct) mirror(posMirror *PositionStruct) {
pc := 0
posMirror.clearBoard()
for sq := 0; sq < 256; sq++ {
pc = p.ucpcSquares[sq]
if pc != 0 {
posMirror.addPiece(mirrorSquare(sq), pc)
}
}
if p.sdPlayer == 1 {
posMirror.changeSide()
}
posMirror.setIrrev()
}
加载开局库
打开game.go,修改为:
//NewGame 创建象棋程序
func NewGame() bool {
game := &Game{
images: make(map[int]*ebiten.Image),
audios: make(map[int]*audio.Player),
singlePosition: NewPositionStruct(),
}
if game == nil || game.singlePosition == nil {
return false
}
var err error
//音效器
game.audioContext, err = audio.NewContext(48000)
if err != nil {
fmt.Print(err)
return false
}
//加载资源
if ok := game.loadResource(); !ok {
return false
}
//加载开局库
game.singlePosition.loadBook()
game.singlePosition.startup()
//设置窗口,接收信息
ebiten.SetWindowSize(BoardWidth, BoardHeight)
ebiten.SetWindowTitle("中国象棋")
if err := ebiten.RunGame(game); err != nil {
log.Fatal(err)
return false
}
return true
}
在下一节中,我们将学习如何如何优化算法?
0