Class Negamax<GS, M, D>

For deterministic zero-sum 2 player games with alternating turns and full game knowledge. Can be configured to do depth based, time based and deepening searches, with a number of tuning options.

For an example implementation, see the mancala game and required callback function and the usage.

For configuring the search options, see the NegamaxOpts page.

Usage

To search the game tree from a game state involves creating a root Node, creating the Negamax tree with the root, attaching callbacks and then evaluating.

Setting up

import * as mx from minimaxer
// Gamestate defined elsewhere
const gamestate = new Gamestate():
// root node and tree
const root = new mx.Node(mx.NodeType.ROOT, gamestate.clone(), 0, 0, mx.NodeAim.MAX);
const negamax = new mx.Negamax(root);
// callbacks (defined elsewhere)
negamax.CreateChildNode = createChildCallback;
negamax.GetMoves = getMovesCallback;
negamax.EvaluateNode = evaluateNodeCallback;

The 3rd argument 0 is the move used to get to this node but the value does not matter as it is the root node. The 4th is the data, not used here, so set to 0.

The 5th argument must be either MAX or MIN, depending on whether the player taking the next turn is trying to minimise or maximise the score.

Evaluating / searching

Search options must be configured as required, then the evaluate function can be called. See NegamaxOpts for details on the options.

// continuing from above ^^
negamax.opts.method = mx.SearchMethod.TIME;
negamax.opts.timeout = 100 //miliseconds
negamax.opts.optimal = true
const result = negamax.evaluate();

The result is of type SearchResult and the best move and value can be acquired from it, as well as other information about the search process.

Implementing callbacks

In the following implementations, the (made-up) concrete types of the 3 generic types are:

  • GS = Gamestate
  • M = number
  • D = number (always 0, unused)

GetMoves

The GetMoves callback returns a reference to a list of moves. It may have to generate this list if not done during the CreateChildNode callback.

import * as mx from minimaxer

const getMovesCallback: mx.GetMovesFunc<Gamestate, number[], number> = (node): Array<number[]> => {
node.gamestate.generate_moves();
return node.gamestate.moves;
};

The moves list and contained moves are never modified by the search, and must not be modified in any of the other callbacks.

CreateChildNode

The CreateChildNode callback as a few impnodeortant responsibilites:

  • Clones the gamestate of the parent node (important so parent gamestate is not modified)
  • Applies the given move to the cloned gamestate to create the next gamestate
  • Checks for game end condition,
  • Returns a node with the correct NodeType.

Passing a NodeAim argument to the child node constructor is not required and will be ignored by the negamax algorithm. The actual NodeAim is dervied internally from the root NodeAim.

import * as mx from minimaxer

const createChildCallback: mx.CreateChildNodeFunc<Gamestate, Array<number>, number> = (node, move) => {
// First create a clone of the gamestate
const new_gamestate = node.gamestate.clone();
// Apply the move
new_gamestate.playMove(move);
// Return a new node with correct node type
if (new_gamestate.check_end()) {
return new mx.Node(mx.NodeType.LEAF, new_gamestate, move, 0);
} else {
return new mx.Node(mx.NodeType.INNER, new_gamestate, move, 0);
}
};

The return type (Node<Gamestate, number, number>) is implied by the function type annotation.

If using the Node.data property, the CreateChildNode callback is also responsible for passing that data to the child node, with whatever modification/copying of data is required.

EvaluateNode

This callback receives a Node and evaluates its relative value based on the gamestate and any extra data included on the node.

This is highly game dependant, but may look something like this:

import * as mx from minimaxer

const evaluateGamestateCallback: mx.EvaluateNodeFunc<Gamestate, number[], number> = (node): number => {
if (node.gamestate.end){
if (node.gamestate.winner == player0){
return Infinity;
} else {
return -Infinity;
}
} else {
return complex_evaluation_function(node.gamestate);
}
};

This function should not modify the node.

depthCallback

This callback is optional and a little different to the other 3. It is called on every depth of search when using the DEEPENING or TIME based search methods.

It passes the full tree (Negamax class instance) and the SearchResult for that depth as arguments. It is most useful for printing out the current state of the search during the iterative deepening process

const negamax = new Negamax(*args*);

negamax.depthCallback = (tree, result) => {
console.log(result);
}

Doing any long processes in this function will hold up the search, and it cannot update the DOM before the search has finished.

Advanced usage

Removing the GetMoves callback

The GetMoves callback can be omitted if the root node is initialised with an array of moves and if the CreateChildNode callback assigns a list of moves to the created child.

The may be useful if the game state already has a list of moves in the process of creating the child node, either from playing the previous move or by checking for an end condition.

Do not do this if extra computation is required to create a list of moves.

const root = new mx.Node(mx.NodeType.ROOT, gamestate.clone(), 0, 0, mx.NodeAim.MAX, gamestate.moves);
import * as mx from minimaxer

const createChildCallback: mx.CreateChildNodeFunc<Gamestate, Array<number>, number> = (node, move) => {
...
if (new_gamestate.check_end()) {
return new minimax.Node(minimax.NodeType.LEAF, new_gamestate, move, 0, 0, new_gamestate.moves);
} else {
return new minimax.Node(minimax.NodeType.INNER, new_gamestate, move, 0, 0, new_gamestate.moves);
}
};

Removing the EvaluateNode callback

The EvaluateNode callback can be removed if the node value is directly assigned during the CreateChildNode callback. This requires first creating the node, then setting the value property value.

This should only be done if the evaluation is very quick e.g the difference between 2 scores on the gamestate.

Using the Node.data property

The data property is attached to the root Node and can then be passed through the tree via the CreateChildNode callback.

It is most useful for containing evaluation specific information that does not belong on the gamestate. An example is constant evaluation options that can be passed to the evaluation function. Otherwise these would have to be global variables.

It also does not have to be constant and could contain variables to help speed up the evaulation for complex games, by using information from previous turns.

The example below shows how a contant "options" object could be used:

// Class to change evaluation function
class EvalOpts {
opt1 = false,
opt2 = 4
}

// Modified evaluation function that uses node.data
const evaluateGamestateCallback: mx.EvaluateNodeFunc<Gamestate, number[], EvalOpts> = (node): number => {
if (node.gamestate.end){
if (node.gamestate.winner == player0){
return Infinity;
} else {
return -Infinity;
}
} else {
if (node.data.opt1){
return complex_evaluation_function(node.gamestate);
} else {
return node.data.opt2 * simple_evaluation_function(node.gamestate);
}
}
};

// Modifed create child function that passes data to child
const createChildCallback: mx.CreateChildNodeFunc<Gamestate, Array<number>, number> = (node, move) => {
// First create a clone of the gamestate
const new_gamestate = node.gamestate.clone();
// Apply the move
new_gamestate.playMove(move);
// Return a new node with correct node type
if (new_gamestate.check_end()) {
return new mx.Node(mx.NodeType.LEAF, new_gamestate, move, node.gamestate.data);
} else {
return new mx.Node(mx.NodeType.INNER, new_gamestate, move, node.gamestate.data);
}
};


const gamestate = new Gamestate():
// root node and tree that ises EvalOpts as D type
const root = new mx.Node(mx.NodeType.ROOT, gamestate.clone(), 0, new EvalOpts(), mx.NodeAim.MAX);
const negamax = new mx.Negamax(root);
// callbacks (defined elsewhere)
negamax.CreateChildNode = createChildCallback;
negamax.GetMoves = getMovesCallback;
negamax.EvaluateNode = evaluateNodeCallback;

Type Parameters

  • GS

    The object representing the state of the game

  • M

    The object representing a move in the game

  • D

    Extra data used in evaluation not suitable for storing in the gamestate

Hierarchy

  • SearchTree<GS, M, D>
    • Negamax

Constructors

Properties

opts: NegamaxOpts = ...

Search options.

expired: boolean = false

Tracks whether a searched has expired due to time

expireTime: number = 0

Time the search will expire. 0 disables expiry

fullDepth: boolean = true

Flags that full depth has not been reached when set to false

presortEnable: boolean = false

Enables presort of children internally

outcomes: number = 0

Number of leaf or depth = 0 reached during current call

nodeLimitExceeded: boolean = false

Flag set if node limit has been reached

EvaluateNode: EvaluateNodeFunc<GS, M, D> = ...

Callback to evaluate the value of gamestate attached to node

root: Node<GS, M, D>

Original root when tree was created

activeRoot: Node<GS, M, D>

Active root used for the creation and searching of tree. Set to root in constructor.

nodeCount: number = 0

Number of nodes in tree

leafCount: number = 0

Number of leaf nodes in tree

activeDepth: number = 0

Maximum depth of current search

GetMoves: GetMovesFunc<GS, M, D> = ...

Callback to get the moves for a gamestate attached to a Node.

CreateChildNode: CreateChildNodeFunc<GS, M, D> = ...

Callback to create a child of a parent node using a move

Methods

  • Implements the negamax algorithm up to a given depth of search. Recursively calls itself until depth is 0 or LEAF node is reached and evaluated. Node values are then passed back up the tree according to the NodeAim of the parent nodes.

    The colour arguments alternates between 1 and -1, as the node aim alternates between MAX and MIN. This changes the sign of the value applied to node depending on the aim of the parent. Selection of the best child can then be done based on the maximum node value.

    Returns early if timeout occurs

    Returns

    false if time expired during search, true if search should continue

    Parameters

    • node: Node<GS, M, D>

      Node to evaluate or search children

    • depth: number

      Depth to search from this node

    • colour: number

      1 for MAX, -1 for MIN

    • alpha: number

      minimum guaranteed score

    • alpha_path: number

      Best guaranteed path

    • beta: number

      maximum guaranteed score from minimising player

    • beta_path: number

      Best guaranteed path for minimising player

    Returns SearchExit

  • Assigns value to node using eval function. Tracks outcomes count and fulldepth

    Parameters

    • node: Node<GS, M, D>
    • depth: number
    • colour: number
    • leaf: boolean

      true if node is a leaf

    Returns SearchExit

  • Wrapper around default children sort that specifices reverse=false

    Parameters

    • node: Node<GS, M, D>

    Returns Node<GS, M, D>

  • Search the tree according to the options specified by the options (opts).

    Returns

    Result from the tree search

    Returns SearchResult<M>

  • Called during deepening search between each depth. Does nothing until overidden

    Parameters

    Returns void

  • Checks if expiry is enable and has happened

    Returns

    true if time has expired, otherwise false

    Returns boolean

  • Checks if node limit is enabled and returns true if it has been exceeded, otherwise returns false.

    Returns boolean

  • Returns either the array of node children or a generator which may create children on the fly Also presorts children if enabled

    Returns

    An iterable for going through children of node

    Parameters

    • node: Node<GS, M, D>

      Node to return an iterable of children

    Returns Iterable<Node<GS, M, D>>

  • Selects from the nodes children which all have an equal best value (and depth) Uniform random weighting Node.child must already represent best child

    Returns

    randomly selected best child with same value as best

    Parameters

    • node: Node<GS, M, D>

      Parent to select best child from

    Returns Node<GS, M, D>

  • Selects best child from nodes children according to the weighting factor

    Parameters

    • node: Node<GS, M, D>
    • weight: number

    Returns Node<GS, M, D>

  • Generator that yields all the nodes along the optimal path, starting from and including node

    Parameters

    • node: Node<GS, M, D>

    Returns Generator<Node<GS, M, D>, any, unknown>

  • Generator that yields all the moves along the optimal path

    Parameters

    • node: Node<GS, M, D>

    Returns Generator<M, any, unknown>

  • Function to return the list of moves starting from the root that are the 'optimal' path.

    Must be called after evaluating the tree.

    Returns

    A list of moves from the active root Node that represent the optimal sequence through the game.

    Returns M[]

  • Recursively go through node and its children removing all nodes that are not best

    Parameters

    • node: Node<GS, M, D>
    • depth: number
    • keep: boolean

    Returns number

  • Generates all children of given node. Does nothing if branches and children already present

    Returns

    true if nodes created, false if already existed

    Parameters

    • node: Node<GS, M, D>

      Node to create children for

    Returns boolean

  • Creates the full game tree recursively starting from given node

    Parameters

    • node: Node<GS, M, D>

      Parent node

    Returns void

  • Generate all the children of node, starting from those already created

    Parameters

    • node: Node<GS, M, D>

      Node to create children for

    Returns Generator<Node<GS, M, D>, any, unknown>

Generated using TypeDoc