前端談談實現五子棋

call_me_R發表於2019-02-28

秉承著會就分享,不會就折騰的技術宗旨。自己利用週末的時間將休閒小遊戲-五子棋重新梳理了一下,整理成一個小的教程,分享出來給大家指點指點。

喬巴圖01

五子棋規則

五子棋的規則我簡單梳理並且改造如下哈:

  1. 對局雙方各執一色棋子;
  2. 空棋盤開局;
  3. 黑先、白後或者白先、黑後,交替下子,每次只能下一子;
  4. 橫線、豎線或者斜線上有連續五個同一色的棋子,則遊戲結束;

正式比賽的規則,可以戳百度百科瞭解下哈–五子棋

程式碼骨架

這裡實現的五子棋小遊戲是使用javascript語言進行編寫的,使用到了es6語法,物件導向的思想進行。

// 設定五子棋類
class Gobang { 
  constructor(options={}){
    this.options = options;
    this.init();
  }
  init() {
    const { options } = this;
  }
}
// 例項化物件
let gobang = new Gobang({}); 
複製程式碼

上面的Gobang類中,包含了一個constructor和init方法。其中constructor方法是類預設的方法,通過new命令生成物件例項時候,自動呼叫該方法。一個類必須有一個constructor方法,如果沒有顯式定義,一個空的constructor方法會預設新增。然後就是init方法了,這裡我是整個類的初始化的入口方法。使用類進行的物件導向方法進行編寫,比較好管理程式碼和功能的擴充套件。

繪製棋盤

棋盤分為兩種,一種是視覺(物理)上的棋盤,另外一個是邏輯上的棋盤,你是看不見的。下面的一張圖就很形象地展示了20*20棋盤的物理和邏輯方式。

物理和邏輯棋盤

繪製物理棋盤,我們這裡使用到了canvas的相關知識點,控制畫筆繪製棋盤:

// 繪製出物理棋盤
drawChessBoard() {
  const context = this.chessboard.getContext(`2d`);
  const {padding, count, borderColor} = this.options.gobangStyle;
  let half_padding = padding/2;
  this.chessboard.width = this.chessboard.height = padding * count;
  context.strokeStyle = borderColor;
  // 畫棋盤
  for(var i = 0; i < count; i++){
    context.moveTo(half_padding+i*padding, half_padding);
    context.lineTo(half_padding+i*padding, padding*count-half_padding);
    context.stroke(); // 這裡繪製出的是豎軸
    context.moveTo(half_padding, half_padding+i*padding);
    context.lineTo(count*padding-half_padding, half_padding+i*padding);
    context.stroke(); // 這裡繪製出的是橫軸
  }
}
複製程式碼

這裡使用到的padding,count,borderColor等都是在例項化的時候傳進去的。這樣提高了可配置性和管理。上面的程式碼是繪製物理上的棋盤,那麼邏輯上的棋盤雖然不能夠繪製出來,但是我們可以表示出來。這裡我們使用了二維陣列的方法去記錄邏輯位置,比如(0,0)點對應的陣列下標是[0][0];然後(1,2)點對應的下標是[1][2]…以此類推。然後我們再為這個邏輯點賦值為0,表示當前點沒有落子。

// 繪製邏輯矩陣棋盤
initChessboardMatrix(){
  const {count} = this.options.gobangStyle;
  const checkerboard = [];
  for(let x = 0; x < count; x++){
    checkerboard[x] = [];
    for(let y = 0; y < count; y++){
      checkerboard[x][y] = 0;
    }
  }
}
複製程式碼

物理棋盤和邏輯棋盤有了之後,就可以考慮到將物理棋盤和邏輯棋盤關聯起來了。這個比較簡單,就是要計算真實的單元格位置進行除法操作即可。這步的管理在後面的落子步驟有提到。

繪製棋子

五子棋的棋子有且僅有兩種–黑色棋子或者白色棋子。這裡也是使用canvas的知識點來繪製棋子。

黑色棋子和白色棋子
// 繪製黑棋或白棋
drawChessman(x , y, isBlack){
  const context = this.chessboard.getContext(`2d`);
  let gradient = context.createRadialGradient(x, y, 10, x-5, y-5, 0);
  context.beginPath();
  context.arc(x, y, 10, 0, 2 * Math.PI);
  context.closePath();
  if(isBlack){
    gradient.addColorStop(0,`#0a0a0a`); 
    gradient.addColorStop(1,`#636766`); 
  }else{ 
    gradient.addColorStop(0,`#d1d1d1`);
    gradient.addColorStop(1,`#f9f9f9`);
  }
  context.fillStyle = gradient;
  context.fill();
}
複製程式碼

落子實現人人對戰

上一節的繪製黑棋和白棋的方法是在單獨一個頁面出來繪製的。現在我們將繪製棋子和棋盤整合,並實現人人對戰的下棋模式。

落子實現人人對戰

我們要監聽點選在棋盤上的事件,然後關聯物理棋盤和邏輯棋盤點,之後在相應的地方刻畫棋子即可。

// 監聽落子
listenDownChessman() {
  // 監聽點選棋盤物件事件
  this.chessboard.onclick = event => {
    let {padding} = this.options.gobangStyle;
    let {
        offsetX: x,
        offsetY: y,
    } = event;
    x = Math.abs(Math.round((x-padding/2)/this.lattice.width));
    y = Math.abs(Math.round((y-padding/2)/this.lattice.height));
    if(this.checkerboard[x][y] !== undefined && Object.is(this.checkerboard[x][y],0)){
      this.checkerboard[x][y] = this.role;
      // 這裡呼叫刻畫棋子的方法
      this.drawChessman(x,y,Object.is(this.role , 1));
      // 切換棋子的角色
      this.role = Object.is(this.role , 1) ? 2 : 1;
    }
  }
}
複製程式碼

實現悔棋

在雙方下棋的時候,允許雙方對已經下的棋子進行調整,也就是悔棋。如下截圖展示功能:

悔棋

實現悔棋功能的時候,需要知道下棋的歷史記錄和當前的落子步數和角色。對於歷史的記錄,這裡對每一步的落子都使用一個物件進行儲存,並放到一個history的陣列裡面進行儲存。

// 悔棋
regretChess() {
  if(this.history.length){
    const prev = this.history[this.currentStep - 1];
    if(prev){
      const {
        x,
        y,
        role
      } = prev;
      this.minusStep(x,y);
      this.checkerboard[prev.x][prev.y] = 0;
      this.currentStep--;
      this.role = Object.is(role,1) ? 1 : 2;
    }
  }
}
// 銷燬棋子
minusStep(x, y) {
  const context = this.chessboard.getContext(`2d`);
  const {padding, count} = this.options.gobangStyle;
  context.clearRect(x*padding, y*padding, padding,padding);
}
複製程式碼

上面的程式碼確實是實現了悔棋功能,但是,在實現悔棋的時候,已經破壞掉了棋盤的UI,因為我們是使用canvas的clearRect方法,將撤銷的棋子使用新的四邊形進行覆蓋,那也就覆蓋了撤銷棋子處的物理棋盤了。為了彌補這個被覆蓋的物理棋盤,我們得重新繪製出此處座標的新物理棋盤線條。這裡的修復要考慮到落子在棋盤的不同位置,要分九種不同的情況進行修復:

  • 左上角棋盤
  • 左邊緣棋盤
  • 左下角棋盤
  • 下邊緣棋盤
  • 右下角棋盤
  • 右邊緣棋盤
  • 右上角棋盤
  • 上邊緣棋盤
  • 中間(非邊界)棋盤
// 修補刪除後的棋盤,將九種情況的不同引數傳過來即可
fixchessboard (a , b, c , d , e , f , g , h){
  const context = this.chessboard.getContext(`2d`);
  const {borderColor, lineWidth} = this.options.gobangStyle;
  context.strokeStyle = borderColor;
  context.lineWidth = lineWidth;
  context.beginPath();
  context.moveTo(a , b);
  context.lineTo(c , d);
  context.moveTo(e, f);
  context.lineTo(g , h);
  context.stroke();
}
複製程式碼

實現撤銷悔棋

有允許悔棋,那麼就有允許撤銷悔棋這樣子才合理。同悔棋功能,撤銷悔棋是需要知道下棋的歷史記錄和當前的步驟和棋子角色的。

// 撤銷悔棋
revokedRegretChess(){
  const next = this.history[this.currentStep]; 
  if(next) {
    this.drawChessman(next.x, next.y, next.role === 1);
    this.checkerboard[next.x][next.y] = next.role;
    this.currentStep++; 
    this.role = Object.is(this.role, 1) ? 2 : 1; 
  }
}
複製程式碼
實現撤銷悔棋

勝利提示/遊戲結束

五子棋的的結束也就是必須要決出勝利者,或者是棋盤沒有位置可以下棋了。這裡考慮決出勝利為遊戲結束的切入點,上面也說到了如何才算是一方獲勝–橫線、豎線或者斜線上有連續五個同一色的棋子。那麼我們就對這四種情況進行處理,我們在矩陣中記錄當前點選的陣列點中是否有連續的五個1(黑子)或者連續的五個2(白子)即可。如下截圖的x軸上的白子獲勝情況,注意gif圖右側列印出來的陣列內容:

勝利提示/遊戲結束
// 裁判觀察棋子,判斷獲勝一方
checkReferee(x , y , role) {
  if((x == undefined)||(y == undefined)||(role==undefined)) return;
  const XContinuous = this.checkerboard.map(x => x[y]); // x軸上連殺
  const YContinuous = this.checkerboard[x]; // y軸上連殺
  const S1Continuous = []; // 儲存左斜線連殺
  const S2Continuous = []; // 儲存右斜線連殺
  this.checkerboard.forEach((_y,i) => {
    // 左斜線
    const S1Item = _y[y - (x - i)];
    if(S1Item !== undefined){
      S1Continuous.push(S1Item);
    }
    // 右斜線
    const S2Item = _y[y + (x - i)];
    if(S2Item !== undefined) {
      S2Continuous.push(S2Item);
    }
  });
}
複製程式碼

至此,已經一步步講解完如何開發一個能夠在pc上愉快玩耍的休閒小遊戲-五子棋了。不妥之處還請指正哈 @~@

喬巴圖02

後話

五子棋的體驗地址–休閒遊戲-五子棋

文章首發地址–github-五子棋遊戲

程式碼倉庫地址–github-五子棋教程

創作文章不易,既然都看到這裡了,留個贊再走唄~

喬巴圖03

相關文章