UNPKG

ai-for-shogi-like-games

Version:

javascript library of A.I. algorithms for Shogi-like games

201 lines (179 loc) 9.14 kB
// @flow 'use strict'; (function() { const sourceMapSupport = require('source-map-support'); sourceMapSupport.install(); })(); import assert from 'assert'; import _ from 'lodash'; import not_used from './number-prototype.js'; import {GameBoard} from './board-lib.js'; import {Node, TREE_NODE_ID_SYMBOL_KEY} from './trees.js'; import {Side} from './side.js'; import {theOne} from './utils.js'; import {Move, BoardMove, DropMove} from './moves.js'; import {EvaluationModel} from './eval-model.js'; function moveTreeBuilder(gb: GameBoard, sideToMoveNext: boolean, depth: number): Node<GameBoard, string> { const root:Node<GameBoard, string> = new Node(gb); function _moveTreeBuilder(root: Node<GameBoard, string>, gb: GameBoard, sideToMoveNext: boolean, depthRemaining: number): void { if (depthRemaining===0) return; // put a floor to the recursion if we reached maximum depth if (gb.boardImmediateWinSide()!==null) return; // put a floor to the recursion if we reached a game end state const nextMoves: Map<string, GameBoard> = gb.nextStates(sideToMoveNext); nextMoves.forEach( (board: GameBoard, k: string) => { const n: Node<GameBoard, string> = new Node(board); root.set(k, n); _moveTreeBuilder(n, board, !sideToMoveNext, depthRemaining-1); }); } _moveTreeBuilder(root, gb, sideToMoveNext, depth); assertConsistent(root); return root; } function bestMove(gb: GameBoard, sideA: boolean, depth: number, evalModel: EvaluationModel, pieceSet: Array<IConcretePiece>): any { // BoardMove | DropMove // TODO const moveTree: Node<GameBoard, string> = moveTreeBuilder(gb, sideA, depth); evaluateLeaves(moveTree, evalModel); if (false) console.log(`\n\n\n**** Upon leaves evaluation:\n${moveTree.print(true)}`); pullEvaluationsUp(sideA, moveTree); if (false) console.log(`\n\n\n**** When evaluations are pulled up:\n${moveTree.print(true)}`); const scoreSelector : (a: number, b: number)=> number = sideA? Math.max: Math.min; const cmp : (a: number, b: number) => boolean = sideA? (a,b)=>a>b : (a,b)=>a<b; let currentlyBestScore: number = sideA? -Infinity: Infinity; let currentlyShallowestDepth = Infinity; let selectedEdge: ?string = null; if (moveTree.children!=null) { moveTree.children.forEach( (v: Node<GameBoard, string>, e: string) => { assert(v.isAdorned()); if (selectedEdge === null) // initialize the selected edge (for hopeless situations where every move leads to the same infinity outcome and the if below would never be triggered) // TODO maybe I can lose that guard as the newly added currentlyShallowestDepth check should take care of this edge case selectedEdge = e; // if ((scoreSelector(v.adornment.v, currentlyBestScore)!=currentlyBestScore) || // ((scoreSelector(v.adornment.v, currentlyBestScore)==currentlyBestScore) && if (cmp(v.adornment.v, currentlyBestScore) || ((v.adornment.v === currentlyBestScore) && (v.adornment.d < currentlyShallowestDepth))) { if (false) console.log(`trying the edge ${e} led to a score of ${v.adornment.v} which is preferrable (given moving side ${sideA?'side A':'side B'}) to that of ${currentlyBestScore} or the depth of ${v.adornment.d} is shallower than the currently shallowest depth of ${currentlyShallowestDepth}`); selectedEdge = e; currentlyBestScore = scoreSelector(v.adornment.v, currentlyBestScore); currentlyShallowestDepth = v.adornment.d; } }); if (selectedEdge!=null) { return Move.fromString(pieceSet, selectedEdge); } else throw new Error(`inconceivable that it was impossible to select an edge`); } else throw new Error(`inconceivable to ask for bestMove on a leaf!`); } function dynamicEvaluationOfBoard(gb: GameBoard, sideAIsMoving: boolean, depth: number, evalModel: EvaluationModel, pieceSet: Array<IConcretePiece>): number { const moveTree: Node<GameBoard, string> = moveTreeBuilder(gb, sideAIsMoving, depth); evaluateLeaves(moveTree, evalModel); if (false) console.log(`\n\n\n**** Upon leaves evaluation:\n${moveTree.print(true, null, (x)=>{ if (x===null) return 'null'; else return x.v; })}`); pullEvaluationsUp(sideAIsMoving, moveTree); if (false) console.log(`\n\n\n**** Upon leaves pulling evaluation:\n${moveTree.print(true, null, (x)=>{ if (x===null) return 'null'; else return x.v; })}`); if (moveTree.adornment!=null) { return moveTree.adornment.v; } else throw new Error('root node should be adorned after evaluations are pulled up'); } function evaluateLeaves(moveTree: Node<GameBoard, string>, evalModel: EvaluationModel): void { function adornLeaf(node: Node<GameBoard, string> ):void { if (node.isLeaf()) node.adorn({v: evalModel.evaluateBoard(node.value), d: 0}); } moveTree.depthFirstTraversal(adornLeaf, true); } function pullEvaluationsUp(sideA: boolean, currentNode: Node<GameBoard, string>, assertNoRecursion: boolean = false): void { if (false) // $SuppressFlowFinding: access of computed property/element. Indexable signature not found in ... console.log(`call to PEU at node: ${currentNode[Symbol.for(TREE_NODE_ID_SYMBOL_KEY)]}`); function isAdorned(n: Node<GameBoard,string>): boolean { return n.isAdorned(); } if (currentNode.allChildrenSatisfy(isAdorned)) { let allChildrenValues: Array<any> = currentNode.getChildrenAdornments(); let thisNodeAdorn = (()=>{ let cmp = sideA?(a,b)=>a>b:(a,b)=>a<b; let bestNodeAdornSoFar; allChildrenValues.forEach ( (adorn) => { if (bestNodeAdornSoFar===undefined) bestNodeAdornSoFar = Object.assign({}, adorn, {d: adorn.d+1}); else { if (cmp(adorn.v, bestNodeAdornSoFar.v)) bestNodeAdornSoFar = Object.assign({}, adorn, {d: adorn.d+1}); } }); return bestNodeAdornSoFar; })(); assert(currentNode.adorn(thisNodeAdorn)===null); } else { if (assertNoRecursion) throw new Error('recursion was not expected'); if (currentNode.children!=null) { currentNode.children.forEach( function(n: Node<GameBoard, string>, e: string) { if (!n.isAdorned()) pullEvaluationsUp(!sideA, n); }); pullEvaluationsUp(sideA, currentNode, true); } else throw new Error('bug'); } assert(currentNode.isAdorned()); } function assertConsistent(tree: Node<GameBoard, string>): void { function _assertConsistent(tree: Node<GameBoard, string>, expectedSide: ?Side): void { if (!tree.isLeaf()) { if (tree.children!=null) { if (expectedSide!=null) { tree.children.forEach( (newBoard: Node<GameBoard, string>, moveS: string)=> { const side: Side = tree.value.sideOfMoveSNoPieceSetInfo(moveS); if (side!==expectedSide) throw new Error(); _assertConsistent(newBoard, side.theOther()); } ); } else { tree.children.forEach( (newBoard: Node<GameBoard, string>, moveS: string)=> { const side: Side = tree.value.sideOfMoveSNoPieceSetInfo(moveS); _assertConsistent(newBoard, side.theOther()); } ); } } else throw new Error('bug'); } } _assertConsistent(tree, null); } function sideThatMovesNext(tree: Node<GameBoard, string>): Side { assert(!tree.isLeaf()); assertConsistent(tree); if (tree.children!=null) { let discoveredSide: ?Side = null; tree.children.forEach( (_: Node<GameBoard, string>, moveS: string)=> { const side: Side = tree.value.sideOfMoveSNoPieceSetInfo(moveS); if (discoveredSide===null) discoveredSide = side; if ((discoveredSide!==null) && (discoveredSide!=side)) throw new Error(); }); if (discoveredSide!=null) return discoveredSide; else throw new Error('impossible to be unable to pronounce discovered side on a non-leaf tree'); } else throw new Error('bug'); } exports.moveTreeBuilder = moveTreeBuilder; exports.sideThatMovesNext = sideThatMovesNext; exports.evaluateLeaves = evaluateLeaves; exports.pullEvaluationsUp = pullEvaluationsUp; exports.bestMove = bestMove; exports.dynamicEvaluationOfBoard = dynamicEvaluationOfBoard;