~bonbon/gmcts

f4368f9a11fb1e5e4d23b8cb9a0873b147c98012 — bonbon 1 year, 5 months ago 052df34
add Hash() as a required method on Game

The game state is now allowed to be uncomparable, as we use the hash,
assumed to be comparable, to map game states to nodes. This might seem,
like kicking the rock down the road, and it is to an extent, but this
allows the underlying game state to hold slices and maps.

This commit is harmless to current implemenations as the Hash function
can return the game state itself.
6 files changed, 26 insertions(+), 9 deletions(-)

M comparable_test.go
M mcts.go
M mcts_test.go
M models.go
M search.go
M tree.go
M comparable_test.go => comparable_test.go +4 -0
@@ 24,6 24,10 @@ func (n comparableState) IsTerminal() bool {
	return true
}

func (n comparableState) Hash() interface{} {
	return 0
}

func (n comparableState) Player() Player {
	return 0
}

M mcts.go => mcts.go +2 -2
@@ 54,11 54,11 @@ func (m *MCTS) SpawnCustomTree(explorationConst float64) *Tree {
	defer m.mutex.Unlock()

	t := &Tree{
		gameStates:       make(map[gameState]*node),
		gameStates:       make(map[gameHash]*node),
		explorationConst: explorationConst,
		randSource:       rand.New(rand.NewSource(m.seed)),
	}
	t.current = initializeNode(gameState{m.init, 0}, t)
	t.current = initializeNode(gameState{m.init, gameHash{m.init.Hash(), 0}}, t)

	m.seed++
	return t

M mcts_test.go => mcts_test.go +4 -0
@@ 42,6 42,10 @@ func (g tttGame) ApplyAction(a Action) (Game, error) {
	return tttGame{game}, err
}

func (g tttGame) Hash() interface{} {
	return g.game
}

func (g tttGame) Player() Player {
	return getPlayerID(g.game.Player())
}

M models.go => models.go +10 -1
@@ 26,6 26,10 @@ type Game interface {
	//and returns a new game state and an error for invalid actions
	ApplyAction(Action) (Game, error)

	//Hash returns a unique representation of the state.
	//Any return value must be comparable.
	Hash() interface{}

	//Player returns the player that can take the next action
	Player() Player



@@ 39,6 43,11 @@ type Game interface {

type gameState struct {
	Game
	gameHash
}

type gameHash struct {
	hash interface{}

	//This is to separate states that seemingly look the same,
	//but actually occur on different turn orders. Without this,


@@ 73,7 82,7 @@ type node struct {
//Tree represents a game state tree
type Tree struct {
	current          *node
	gameStates       map[gameState]*node
	gameStates       map[gameHash]*node
	explorationConst float64
	randSource       *rand.Rand
}

M search.go => search.go +3 -3
@@ 97,18 97,18 @@ func (n *node) expand() {
			panic(fmt.Sprintf("gmcts: Game returned an error when exploring the tree: %s", err))
		}

		newState := gameState{newGame, n.state.turn + 1}
		newState := gameState{newGame, gameHash{newGame.Hash(), n.state.turn + 1}}

		//If we already have a copy in cache, use that and update
		//this node and its parents
		if cachedNode, made := n.tree.gameStates[newState]; made {
		if cachedNode, made := n.tree.gameStates[newState.gameHash]; made {
			n.unvisitedChildren[i] = cachedNode
		} else {
			newNode := initializeNode(newState, n.tree)
			n.unvisitedChildren[i] = newNode

			//Save node for reuse
			n.tree.gameStates[newState] = newNode
			n.tree.gameStates[newState.gameHash] = newNode
		}
	}
}

M tree.go => tree.go +3 -3
@@ 61,9 61,9 @@ func (t Tree) Nodes() int {
//this tree searched through.
func (t Tree) MaxDepth() int {
	maxDepth := 0
	for state := range t.gameStates {
		if state.turn > maxDepth {
			maxDepth = state.turn
	for _, node := range t.gameStates {
		if node.state.turn > maxDepth {
			maxDepth = node.state.turn
		}
	}
	return maxDepth