到目前为止,象棋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

本文为原创文章,转载请注明出处,欢迎访问作者网站(和而不同)

发表评论

error: Content is protected !!