秉承著會就分享,不會就折騰的技術宗旨。自己利用週末的時間將休閒小遊戲-五子棋
重新梳理了一下,整理成一個小的教程,分享出來給大家指點指點。
五子棋規則
五子棋的規則我簡單梳理並且改造如下哈:
- 對局雙方各執一色棋子;
- 空棋盤開局;
- 黑先、白後或者白先、黑後,交替下子,每次只能下一子;
- 橫線、豎線或者斜線上有連續五個同一色的棋子,則遊戲結束;
正式比賽的規則,可以戳百度百科瞭解下哈–五子棋。
程式碼骨架
這裡實現的五子棋
小遊戲是使用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上愉快玩耍的休閒小遊戲-五子棋了。不妥之處還請指正哈 @~@
後話
五子棋的體驗地址–休閒遊戲-五子棋
文章首發地址–github-五子棋遊戲
程式碼倉庫地址–github-五子棋教程
創作文章不易,既然都看到這裡了,留個贊再走唄~