五子棋AI演算法(一)

Flying Lu發表於2019-10-18

之前說想寫一些比較大型的演算法,想了半天,還是覺得寫五子棋的AI比較合適。一則因為自己研究過這個,有一些基礎,二則儘管現在網上有很多五子棋AI演算法的資料,但是確實都有些晦澀難懂。就想著借這個機會,憑自己的理解,儘量的講通俗一些。然而,這個演算法確實有些複雜,想要通俗一些需要較大的篇幅,一篇部落格難以講完,這裡就分很多個章節,一點一點的深入探討。為了讓文章更加通俗一些,我會略去一部分很簡單但是佔用篇幅很長的程式碼,改為用幾行註釋說明。

框架的搭建

首先,我們計劃是做一個五子棋AI,也就是說讓玩家和這個AI對下。整個遊戲的框架是這樣的:
五子棋遊戲框架
其中,棋盤是一個Object,存放當前的棋局情況,通知每個Player“輪到你下棋了”、“對方下了什麼棋”、“遊戲結束,XXX獲勝”等訊息,並且從每個Player那裡獲取他下了什麼棋。兩個Player分別是人類玩家和AI。Player的基類應該是一個interface,裡面只有三個方法。人類玩家和AI是它的子類,分別實現這三個方法。

public interface Player {
   Point play();
   void display(Point p);
   void notifyWinner(int color);
}
public final class Point {
   public final int x;
   public final int y;
   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
   @Override
   public String toString() {return "(" + x + "," + y + ")";}
}

解釋一下:

  • play 方法,告知“輪到你下棋了”,並且返回一個Point,也就是下一步下的棋。對於人類玩家,則就是阻塞,等待玩家在介面上選取一個點,並且將這個點的座標返回。對於AI,則是直接開始用我們的AI演算法進行計算,並返回計算結果。
  • void display(Point p) 方法,告知“對方下了什麼棋”。對於人類玩家,則就是將對方下了的棋在介面上顯示出來。對於AI,則是將對方下了的棋記在AI的快取中,以便後續的計算。
  • void notifyWinner(int color) 方法,告知“遊戲結束,XXX玩家贏了”。對於人類玩家,則就是在介面上展示誰贏了的文字及特效,並且從此之後再點選棋盤就不再有反應了。對於AI,則是通知AI不要再計算了。

當然了,如果打算連續下多盤棋,可能還需要一個reset方法,通知人類玩家和AI清空當前棋盤。當然了,這個和我們的演算法關係不大就不列出來了。
然後,我們的interface Player需要兩個實現類,分別叫做HumanPlayerRobotPlayer,這個RobotPlayerplay方法將是五子棋AI演算法的核心內容,後面會花費大量篇幅進行講解。
接下來就是我們的棋盤:

public abstract class Constant {
   public static int MAX_LEN = 15;
}
public class ChessBoard {
   private byte[][] board = new byte[Constant.MAX_LEN][Constant.MAX_LEN];
   private Player[] players = new Player[2];
   private int whoseTurn = 0;
   private int count = 0;
   private boolean isEnd = false;
   
   private boolean checkForWin(Point p) {
      /* 因為篇幅問題,此處省略十幾行程式碼 */
      /* 這個函式就是在下完每一步棋時呼叫,只需要判斷以這步棋若形成五連珠即可判定獲勝 */
      return false;
   }

   public void play() {
      if (isEnd) return;
      Point p = players[whoseTurn].play(); //呼叫Player的play方法,獲取下一步下的棋
      if (board[p.y][p.x] != 0) //嚴謹,以防萬一
         throw new IllegalArgumentException(p.toString() + board[p.y][p.x]);
      board[p.y][p.x] = (byte) (whoseTurn + 1);
      System.out.println((whoseTurn == 0 ? "黑" : "白") + p.toString()); //列印日誌
      if (++count == Constant.MAX_LEN * Constant.MAX_LEN) //嚴謹,如果棋盤下滿了遊戲結束
         isEnd = true;
      if (checkForWin(p)) //如果下了這步棋後贏了,遊戲結束
         isEnd = true;
      whoseTurn = 1 - whoseTurn; //切換當前下棋的人
      players[whoseTurn].display(p); //呼叫Player的display方法,告知他對方下了哪步棋
      if (isEnd) { //如果下完這一步棋後有一方贏了,則呼叫Player的notifyWinner方法通知
         players[0].notifyWinner(2 - whoseTurn);
         players[1].notifyWinner(2 - whoseTurn);
      }
   }
   
   public static void main(String[] args) {
      ChessBoard b = new ChessBoard();
      b.players[0] = new HumanPlayer(1); //這裡我從建構函式中傳入了顏色,例如1表示執黑,2表示執白
      b.players[1] = new RobotPlayer(2);
      while (b.getWinner() == null) {
         b.play();
      }
   }
}

棋盤的程式碼確實很簡單易懂,也做了很多註釋,就不多介紹了。
接下來,就只剩下HumanPlayerRobotPlayer的實現了。

人類玩家

人類玩家無非就是實現三個方法:

  • play 方法,阻塞等待玩家點選棋盤上的一個點並返回這個點
  • display 方法,將AI下的棋展示在介面上
  • notifyWinner 方法,顯示一行字“你贏(輸)了”

這段程式碼與本文無關,就不貼出來了,我把我做的這個醜陋的介面貼出來展示一下,哈哈。
醜陋的介面
(不要吐槽我的介面,這不是重點。)

RobotPlayer

AI才是重點內容,涉及了大量的演算法和數學知識,博弈樹、評估函式、極大極小值搜尋、啟發式搜尋、α-β剪枝等等,將會佔用大量的篇幅。從下篇部落格開始,將對此逐一展開。

相關文章