Writing AI for the game Gomoku (5 in a row)

The computer played with crosses
The computer played with crosses

While developing the browser game Gomoku (5 in a row) in JavaScript, I was faced with the need to implement a computer adversary (AI). This article briefly describes the main components of AI, and also compares the search algorithms: NegaMax, NegaScout, and MTD-F.





The main components of AI: a function for assessing the state of the game, a generator of moves, a search algorithm, an algorithm for determining a victory.





Algorithm for determining victory

. . , , . , , .





function negamax(newBoard, player, depth, a, b, hash, restrictions, last_i, last_j) {
  ...
   if (checkwin(newBoard, last_i, last_j)) {
        return -2000000 + (MaximumDepth - depth) //  ,       /  
    }
  ...
}
      
      



function checkwin(Board, x, y) {  
    const Directions = get_directions(Board, x, y)
    for (let i = 0; i < 4; i++) { //  4 
        if (check_directions(Directions[i])) {
            return true
        }
    }
}
      
      



function get_directions(Board, x, y) { // 
    const Directions = [[], [], [], []];
    for (let i = -4; i < 5; i++) {
        if (x + i >= 0 && x + i <= Rows - 1) {
            Directions[0].push(Board[x + i][y])
            if (y + i >= 0 && y + i <= Columns - 1) {
                Directions[2].push(Board[x + i][y + i])
            }
        }
        if (y + i >= 0 && y + i <= Columns - 1) {
            Directions[1].push(Board[x][y + i])
            if (x - i >= 0 && x - i <= Rows - 1) {
                Directions[3].push(Board[x - i][y + i])
            }
        }
    }
    return Directions
}
      
      



4 directions
4

, 5 4 .





function check_directions(arr) {
    for (let i = 0; i < arr.length - 4; i++) {
        if (arr[i] !== 0) {
            if (arr[i] === arr[i + 1] && arr[i] === arr[i + 2] && arr[i] === arr[i + 3] && arr[i] === arr[i + 4]) {
                return true
            }
        }
    }
}
      
      



JS, . ( TypedArray, ).

1 - , -1 - .





(Transposition Table)

— . , . (Zobrist hashing).





Zobrist hashing

:





1.  , N , N= * *





2.  xor ,









– c , .





function Table_init() {
  for (let i = 0; i < Rows; i++) {
    Table[i] = [];
    for (let j = 0; j < Columns; j++) {
      Table[i][j] = []
      Table[i][j][0] = random32(); //1
      Table[i][j][1] = random32(); //2
    }
  }
      
      



function hash(board) { //    
  let h = 0;
  let p;
  for (let i = 0; i < Rows; i++) {
    for (let j = 0; j < Columns; j++) {
      const Board_value = board[i][j];
      if (Board_value !== 0) { //  
        if (Board_value === -1) {
          p = 0  // 0
        } else {
          p = 1 // 1
        }
        h = h ^ Table[i][j][p];
      }
    }
  }
  return h;
}
      
      



function update_hash(hash, player, row, col) { // 
    if (player === -1) {
        player = 0
    } else {
        player = 1
    }
    hash = hash ^ Table[row][col][player];
    return hash
}
      
      







function BoardGenerator(restrictions, Board, player) {
  const availSpots_score = []; //c is j  r is i;
  const min_r = restrictions[0];
  const min_c = restrictions[1];
  const max_r = restrictions[2];
  const max_c = restrictions[3];;
  for (let i = min_r - 2; i <= max_r + 2; i++) {
    for (let j = min_c - 2; j <= max_c + 2; j++) {
      if (Board[i][j] === 0 && !remoteCell(Board, i, j)) {
        const move = {}
        move.i = i;
        move.j = j;
        move.score = evaluate_move(Board, i, j, player) //        ,   score. 
        if (move.score === WIN_DETECTED) {
          return [move]
        }
        availSpots_score.push(move)
      }
    }
  }
  availSpots_score.sort(compare) //    score
  //  return availSpots_score.slice(0,10)
  return availSpots_score;
}
      
      







:





1.    ( )





2.    ( )





3.    , ( )









Move ordering

- , , . score , 4 : , , . score - score .





function evaluate_move(Board, x, y, player) {
  let score = 0;
  const Directions = get_directions(Board, x, y);
  let temp_score;
  for (let i = 0; i < 4; i++) {
    temp_score = evaluate_direction(Directions[i], player);
    if (temp_score === WIN_DETECTED) { //    ,    
      return WIN_DETECTED
    } else {
      score += temp_score
    }
  }
  return score;
}
      
      



function evaluate_direction(direction_arr, player) {
  let score = 0;
  for (let i = 0; (i + 4) < direction_arr.length; i++) {
    let you = 0;
    let enemy = 0;
    for (let j = 0; j <= 4; j++) {
      if (direction_arr[i + j] === player) {
        you++
      } else if (direction_arr[i + j] === -player) {
        enemy++
      }
    }
    score += evalff(get_seq(you, enemy));
    if ((score >= 800000)) {
      return WIN_DETECTED;
    }
  }
  return score
}
      
      



function evalff(seq) {
    switch (seq) {
        case 0:
            return 7;
        case 1:
            return 35;
        case 2:
            return 800;
        case 3:
            return 15000;
        case 4:
            return 800000;
        case -1:
            return 15;
        case -2:
            return 400;
        case -3:
            return 1800;
        case -4:
            return 100000;
        case 17:
            return 0;
    }
}

function get_seq(y, e) {
    if (y + e === 0) {
        return 0;
    }
    if (y !== 0 && e === 0) {
        return y
    }
    if (y === 0 && e !== 0) {
        return -e
    }
    if (y !== 0 && e !== 0) {
        return 17
    }
}
      
      



, - , . - , . , , .





function evaluate_state(Board, player, hash, restrictions) {
    const  black_score = eval_board(Board, -1, restrictions);
    const  white_score = eval_board(Board, 1, restrictions);
    let score = 0;
    if (player == -1) {
        score = (black_score - white_score);
    } else {
        score = (white_score - black_score);
    }
    StateCache.set(hash,score); 
    StateCachePuts++;
    return score;
}
      
      



eval_board . score, (Live) (Dead) .





: _xx_ - , _oxx_ - .





const LiveOne = 10;
const DeadOne = 1;
const LiveTwo = 100;
const DeadTwo = 10;
const LiveThree = 1000;
const DeadThree = 100;
const LiveFour = 10000;
const DeadFour = 1000;
const Five = 100000;
      
      



Iterative Deepening

, . - N, N+2. ( 2, . ). (, , ), . , .





NegaMax, NegaScout, MTDF

, chessprogramming.org, . , .





Negamax -

, .





- - , . Best case - (





move ordering).





n b( ) = 40





(n)





Minimax(Alpha-beta pruning worst case)





Alpha-beta pruning(best case)





0





1





1





1





40





40





2





1,600





79





3





64,000





1,639





4





2,560,000





3,199





5





102,400,000





65,569





6





4,096,000,000





127,999





7





163,840,000,000





2,623,999





8





6,553,600,000,000





5,119,999





NegaScout

NegaScout – --, . Deep Blue.





MTD-F

MTD(F) - , -. , NegaScout, ( , MTD-F NegaScout)





:





function MTDF(root : node_type; f : integer; d : integer) : integer;
      g := f;
      upperbound := +INFINITY;
      lowerbound := -INFINITY;
      repeat
            if g == lowerbound then beta := g + 1 else beta := g;
            g := AlphaBetaWithMemory(root, beta - 1, beta, d);
            if g < beta then upperbound := g else lowerbound := g;
      until lowerbound >= upperbound;
      return g;
      
      



, .





//  return availSpots_score.slice(0,10)
      
      



, Move ordering 10 , (.. , 10).





(=8)

MTD(f) 10 - 10 .





StateCache - .





(bo3) (60 s)

.





c Iterative Deepening.





1 – , 0.5 –









Negamax





NegaScout





MTD(f)





MTD(f) 10





Negamax









0:2





0:2





0:2





NegaScout





2:0









1:2





0:2





MTD(f)





2:0





2:1









0:2





MTD(f) 10





2:0





2:0





2:0









MTD(f) 10  1.5:0.5  NegaScout 10





mtdf(10)









,





2





0.032





4





0.11





6





0.428





8





3.286





10





22.787





, . , 3-7 .





(js)





(MTD(f) 10 - )





http://gomoku.yjyao.com/





2:0  (10s )





https://github.com/lihongxun945/gobang





2:0  (10s )





https://logic-games.spb.ru/gomoku/





2:0  (10s )





https://ixjamesyoo.github.io/Gomoku/





2:0  (10s )





( , 10 )





( )

1d (, )





bitboards ( , js)





/Move ordering

, . , . , . ( )

, . . (+ , tensorflow.js )





(), ( ) javascript.





2 : MCTS - .





MCTS , . AlphaZero/MuZero , MCTS + NN , , , , NN.









ai(mtdf10, 21x21) - https://4battle.ru/play_offline





, , .





:





js ()





++ ( C++, js . C++ 2-2.5 , )





(Good Related Articles: 1 , 2 )








All Articles