Golang 蒙特卡洛算法 在 五子棋中的实现

蒙特卡洛算法在五子棋中的实现

引言

蒙特卡洛算法是一种基于随机抽样的数值计算方法,在许多领域都有广泛的应用。在围棋(五子棋)这类游戏中,蒙特卡洛算法被用于寻找最佳的走法。本文将通过一段示例代码来介绍如何利用蒙特卡洛树搜索(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的表现。

相关推荐
半夏知半秋15 分钟前
lua debug相关方法详解
开发语言·学习·单元测试·lua
Andy01_19 分钟前
Java八股汇总【MySQL】
java·开发语言·mysql
pk_xz12345622 分钟前
R 和 Origin 完成细菌 OTU 表、土壤理化性质数据的微生物 Beta 多样性分析
算法·机器学习·r语言
Ning_.31 分钟前
力扣第116题:填充每个节点的下一个右侧节点指针 - C语言解法
c语言·算法·leetcode
小小unicorn37 分钟前
第二章:算法练习题2
算法
坊钰38 分钟前
【Java 数据结构】合并两个有序链表
java·开发语言·数据结构·学习·链表
抓住鼹鼠不撒手38 分钟前
力扣 429 场周赛-前两题
数据结构·算法·leetcode
神经网络的应用1 小时前
C++程序设计例题——第三章程序控制结构
c++·学习·算法
南宫生2 小时前
力扣-数据结构-3【算法学习day.74】
java·数据结构·学习·算法·leetcode
工业甲酰苯胺2 小时前
聊一聊 C#线程池 的线程动态注入
java·开发语言·c#