蒙特卡洛算法在五子棋中的实现
引言
蒙特卡洛算法是一种基于随机抽样的数值计算方法,在许多领域都有广泛的应用。在围棋(五子棋)这类游戏中,蒙特卡洛算法被用于寻找最佳的走法。本文将通过一段示例代码来介绍如何利用蒙特卡洛树搜索(MCTS)算法来实现一个简单的五子棋AI。
五子棋游戏概述
五子棋是一种两人对弈的策略型棋类游戏,双方轮流落子,目标是首先在棋盘上形成连续的五个同色棋子的一方获胜。本示例中的五子棋游戏大小为15×15,并且默认中间位置已放置了一个"裁判"棋子。
蒙特卡洛树搜索(MCTS)
蒙特卡洛树搜索是一种概率性的搜索算法,常用于解决具有不确定性和信息不完全的游戏问题。它通过模拟大量的随机游戏过程来评估不同走法的好坏,从而选择最有可能带来胜利的走法。
代码解析
下面我们将逐步解析上述代码中的关键部分。
游戏状态表示
GoBang
结构体定义了游戏的状态,包括棋盘的大小、棋盘上的布局以及当前玩家等信息。
go
type GoBang struct {
size int
board [][]int
player int
}
其中,board
是一个二维数组,用来存储棋盘上每个位置的状态(0表示空位,1表示先手玩家的棋子,2表示后手玩家的棋子)。player
表示当前应走的玩家。
蒙特卡洛树节点
McTreeNode
结构体定义了蒙特卡洛树中的一个节点。
go
type McTreeNode struct {
parent *McTreeNode
children []*McTreeNode
score float64
visitCount float64
untriedActions []any
nodeState TreeState
}
每个节点包含指向父节点的指针、一个子节点列表、得分、访问次数、未尝试的动作列表以及与该节点相关联的游戏状态。
搜索函数
Search
函数实现了蒙特卡洛树搜索的核心逻辑。
go
func Search(simulate any, state TreeState, discount ...float64) TreeState {
// ...
}
此函数接受一个模拟参数 simulate
和当前的游戏状态 state
,并返回一个新的游戏状态。模拟参数可以是一个整数、一个时间间隔或者一个布尔函数,用于控制模拟的终止条件。
树策略
treePolicy
方法实现了树策略的选择逻辑,即如何从当前节点扩展树结构,以及如何选择下一个要探索的节点。
go
func (cur *McTreeNode) treePolicy(discountParamC float64) *McTreeNode {
// ...
}
最佳子节点选择
chooseBestChild
方法用于选择最佳子节点,这是基于UCB1公式来完成的。
go
func (cur *McTreeNode) chooseBestChild(c float64) *McTreeNode {
// ...
}
反向传播
backPropagate
方法负责将模拟的结果反向传播至根节点,以便更新各个节点的得分和访问次数。
go
func (cur *McTreeNode) backPropagate(result float64) {
// ...
}
完整代码示例
go
package main
import (
"fmt"
"math"
"math/rand"
"strings"
"time"
)
type GoBang struct {
size int
board [][]int
player int
}
func NewGoBang(size int) *GoBang {
w := &GoBang{
size: size,
board: make([][]int, size),
player: 1,
}
for i := 0; i < size; i++ {
w.board[i] = make([]int, size)
}
size /= 2
// 默认中间落子
w.board[size][size] = 2
return w
}
func main() {
var (
x, y int
board = NewGoBang(15)
)
board.Print()
for board.IsTerminal() == 0 {
if board.player == 1 {
board = Search(time.Second*10, board).(*GoBang)
} else if board.player == 2 {
for {
fmt.Print("已执棋,请输入坐标: ")
_, _ = fmt.Scanln(&x, &y)
x--
y--
if x < 0 || y < 0 || x >= board.size || y >= board.size {
fmt.Println("超出棋盘范围")
} else if board.board[x][y] > 0 {
fmt.Println("已有棋子")
} else {
board.board[x][y] = 2
board.player = 1
break
}
}
}
board.Print()
if board.IsTerminal() != 0 {
fmt.Printf("Game Over: %d\n", board.IsTerminal())
return
}
}
}
func (w *GoBang) Print() {
var (
str strings.Builder
num = func(n int) {
a, b := n/10, n%10
if a > 0 {
str.WriteByte(byte(a + '0'))
} else {
str.WriteByte(' ') // 1位数前面加空格
}
str.WriteByte(byte(b + '0'))
}
)
str.WriteString(" ")
for i := 1; i <= w.size; i++ {
str.WriteByte(' ')
num(i)
}
str.WriteByte('\n')
for i := 0; i < w.size; i++ {
str.WriteString(" ")
for j := 0; j < w.size; j++ {
str.WriteString(" __")
}
str.WriteByte('\n')
num(i + 1)
str.WriteByte(' ')
for j := 0; j < w.size; j++ {
str.WriteByte('|')
switch w.board[i][j] {
case 0:
str.WriteByte(' ')
case 1:
str.WriteByte('O')
case 2:
str.WriteByte('X')
}
str.WriteByte(' ')
}
str.WriteString("|\n")
}
str.WriteString(" ")
for i := 0; i < w.size; i++ {
str.WriteString(" __")
}
fmt.Println(str.String())
}
func (w *GoBang) IsTerminal() int {
full := -1 // 没有空位且都没赢
for i := 0; i < w.size; i++ {
for j := 0; j < w.size; j++ {
if wc := w.board[i][j]; wc == 0 {
full = 0 // 还有空位,没结束
} else {
// 向右
cnt, x, y := 1, 0, j+1
for ; y < w.size && w.board[i][y] == wc; y++ {
cnt++
}
if cnt >= 5 {
return wc
}
// 向下
cnt, x = 1, i+1
for ; x < w.size && w.board[x][j] == wc; x++ {
cnt++
}
if cnt >= 5 {
return wc
}
// 向右下
cnt, x, y = 1, i+1, j+1
for ; x < w.size && y < w.size && w.board[x][y] == wc; x, y = x+1, y+1 {
cnt++
}
if cnt >= 5 {
return wc
}
// 向左下
cnt, x, y = 1, i+1, j-1
for ; x < w.size && y >= 0 && w.board[x][y] == wc; x, y = x+1, y-1 {
cnt++
}
if cnt >= 5 {
return wc
}
}
}
}
return full
}
func (w *GoBang) Result(state int) float64 {
switch state {
case -1:
return 0 // 都没赢且没空位
case 1:
return -1 // 先手赢了
case 2:
return +1 // 后手赢了
default:
return 0 // 都没赢且有空位
}
}
func (w *GoBang) GetActions() (res []any) {
// 将棋子周围2格范围的空位加到结果中
m := map[[2]int]struct{}{}
for i := 0; i < w.size; i++ {
for j := 0; j < w.size; j++ {
// 跳过空位和己方棋子
if w.board[i][j] == 0 || w.board[i][j] == w.player {
continue
}
x0, x1, y0, y1 := i-2, i+2, j-2, j+2
for ii := x0; ii <= x1; ii++ {
for jj := y0; jj <= y1; jj++ {
if ii >= 0 && jj >= 0 && ii < w.size && jj < w.size &&
w.board[ii][jj] == 0 {
p := [2]int{ii, jj}
_, ok := m[p]
if !ok {
res = append(res, p)
m[p] = struct{}{}
}
}
}
}
}
}
return
}
func (w *GoBang) ActionPolicy(action []any) any {
// 随机选一个动作(todo 替换为根据评分选取动作)
return action[rand.Intn(len(action))]
}
func (w *GoBang) Action(action any) TreeState {
// 初始化
wn := &GoBang{
size: w.size,
board: make([][]int, w.size),
player: 3 - w.player,
}
for i := 0; i < w.size; i++ {
wn.board[i] = make([]int, w.size)
for j := 0; j < w.size; j++ {
wn.board[i][j] = w.board[i][j]
}
}
// 新增落子
ac := action.([2]int)
wn.board[ac[0]][ac[1]] = w.player
return wn
}
// MonteCarloTree
type (
TreeState interface {
IsTerminal() int
Result(int) float64
GetActions() []any
ActionPolicy([]any) any
Action(any) TreeState
}
McTreeNode struct {
parent *McTreeNode
children []*McTreeNode
score float64
visitCount float64
untriedActions []any
nodeState TreeState
}
)
func Search(simulate any, state TreeState, discount ...float64) TreeState {
var (
root = &McTreeNode{nodeState: state}
leaf *McTreeNode
dp = 1.4
)
if len(discount) > 0 {
dp = discount[0]
}
var loop func() bool
switch s := simulate.(type) {
case int:
loop = func() bool {
s-- // 模拟指定次数后退出
return s >= 0
}
case time.Duration:
// 超过时间后退出
ts := time.Now().Add(s)
loop = func() bool { return time.Now().Before(ts) }
case func() bool:
loop = s
default:
panic(simulate)
}
for loop() {
leaf = root.treePolicy(dp)
result, curState := 0, leaf.nodeState
for {
// 重复该过程,直到结束(找位 选位 填写)
if result = curState.IsTerminal(); result != 0 {
break
}
all := curState.GetActions()
one := curState.ActionPolicy(all)
curState = curState.Action(one)
}
// 根据结束状态计算结果,将该结果反向传播
leaf.backPropagate(curState.Result(result))
}
return root.chooseBestChild(dp).nodeState // 选择最优子节点
}
func (cur *McTreeNode) chooseBestChild(c float64) *McTreeNode {
var (
idx = 0
maxValue = -math.MaxFloat64
childValue float64
)
for i, child := range cur.children {
childValue = (child.score / child.visitCount) +
c*math.Sqrt(math.Log(cur.visitCount)/child.visitCount)
if childValue > maxValue {
maxValue = childValue
idx = i
}
}
return cur.children[idx]
}
func (cur *McTreeNode) backPropagate(result float64) {
// 反向传播,增加访问次数,更新分数
nodeCursor := cur
for nodeCursor.parent != nil {
nodeCursor.score += result
nodeCursor.visitCount++
nodeCursor = nodeCursor.parent
}
nodeCursor.visitCount++
}
func (cur *McTreeNode) expand() *McTreeNode {
// 按顺序添加,并移除
res := cur.untriedActions[0]
cur.untriedActions = cur.untriedActions[1:]
child := &McTreeNode{
parent: cur,
nodeState: cur.nodeState.Action(res),
}
cur.children = append(cur.children, child)
return child
}
func (cur *McTreeNode) treePolicy(discountParamC float64) *McTreeNode {
nodeCursor := cur
for nodeCursor.nodeState.IsTerminal() == 0 {
if nodeCursor.untriedActions == nil {
nodeCursor.untriedActions = nodeCursor.nodeState.GetActions()
}
if len(nodeCursor.untriedActions) > 0 {
return nodeCursor.expand()
}
nodeCursor = nodeCursor.chooseBestChild(discountParamC)
}
return nodeCursor
}
结论
通过上述可以看到蒙特卡洛树搜索算法是如何在五子棋游戏中的实现。这种方法虽然简单但在实践中却非常有效,尤其是在处理复杂的决策问题时。此外通过调整算法中的参数(如模拟次数、探索因子等),还可以进一步优化AI的表现。