Javascript 面向對象編程之五子棋

前些天, 一個朋友給我分享了一道筆試題, 題目以下
javascript

筆試題目
以爲挺有意思的, 因而就本身實現了一版.

先貼結果

Github倉庫 Demojava

分析需求

拿到題目以後, 首先要作的是分析所給的需求背後的邏輯與實現. 針對題目所給的需求, 我我的的理解以下git

  1. 五子棋遊戲的功能, 包括棋盤繪製, 輪流落子(輪流繪製黑白棋子), 輸贏斷定 (輸贏判斷的邏輯就只存在一種可能, 就是不管黑子白字, 只有在落子以後這個棋子的八個方向是否構成了連續的五子)
  2. 渲染模式的切換(DOM與Canvas的切換), 這一點首先是要針對第一點的需求做出Dom和canvas兩個版本的實現, 其次呢, DOM版本與canvas版原本回切換的過程當中, 遊戲須要具有保存棋局和根據保存的數據恢復棋局的能力, 才能作到在遊戲的過程當中進行切換
  3. 悔棋與撤銷悔棋, 首先假設只能進行一步悔棋操做與撤銷悔棋操做,那麼咱們須要作的有一下幾點
    1. 維護兩個狀態分別保存如下信息 (1). 當前能進行悔棋操做仍是撤銷悔棋操做, (2). 當前須要操做的是哪個棋子
    2. 針對DOM版本和canvas版本須要分別實現移出棋子的功能(DOM版本能夠直接移出棋子的DOM, canvas版本則須要整個重繪)

更詳細代碼的實現會在代碼中討論github

Coding

需求分析完了, 接下來須要循序漸進用代碼實現web

  1. 首先約定一下游戲的各個參數
// config.js
export const CHESS_BOARD_SIZE = 18 // 棋盤尺寸 18 * 18
export const COLUMN_WIDTH = 40 // 棋盤格子間隙
export const CHESS_PIECE_WIDTH = 36 // 棋子直徑

export const CHESS_BOARD_LINE_COLOR = '#000' // 棋盤線的演示
export const CHESS_BOARD_BACKGROUND = '#3096f0' // 棋盤背景顏色
export const CHESS_TYPES = { // 棋子類型
  WHITE_CHESS_PIECE: { color: '#fff', name: '白子' },
  BLACK_CHESS_PIECE: { color: '#000', name: '黑子' }
}
複製代碼
  1. 實現繪製棋盤的方法
// 這裏只講一下dom版本的實現, canvas版本的能夠去源碼中看drawChessBoardCanvas.js的實現
// drawChessBoard.js
import {
  CHESS_BOARD_SIZE,
  COLUMN_WIDTH,
  CHESS_BOARD_LINE_COLOR,
  CHESS_BOARD_BACKGROUND
} from './config'

export default () => {
  const chessBoard = document.createElement('div')
  const boardSize = (CHESS_BOARD_SIZE + 1) * COLUMN_WIDTH
  chessBoard.style.width = boardSize + 'px'
  chessBoard.style.height = boardSize + 'px'
  chessBoard.style.border = `1px solid ${CHESS_BOARD_LINE_COLOR}`
  chessBoard.style.backgroundColor = CHESS_BOARD_BACKGROUND
  // 設置棋盤定位爲relative, 後續棋子定位所有用absolute實現
  chessBoard.style.position = 'relative'

  // 畫棋盤線
  const drawLine = (type = 'H') => (item, index, arr) => {
    const elem = document.createElement('div')
    elem.style.backgroundColor = CHESS_BOARD_LINE_COLOR
    elem.style.position = 'absolute'
    if (type === 'H') {
      elem.style.top = (index + 1) * COLUMN_WIDTH + 'px'
      elem.style.width = boardSize + 'px'
      elem.style.height = 1 + 'px'
    } else {
      elem.style.left = (index + 1) * COLUMN_WIDTH + 'px'
      elem.style.height = boardSize + 'px'
      elem.style.width = 1 + 'px'
    }
    return elem
  }
  const sizeArr = new Array(CHESS_BOARD_SIZE).fill(1)
  // 畫橫線
  sizeArr.map(drawLine('H')).forEach(item => { chessBoard.appendChild(item) })
  // 畫豎線
  sizeArr.map(drawLine('V')).forEach(item => { chessBoard.appendChild(item) })
  return chessBoard
}
複製代碼
  1. 實現一個棋子類
// ChessPiece.js
export default class ChessPiece {
  constructor (x, y, chessType, id) {
    this.x = x // 棋子位於棋盤的格子座標系的x座標
    this.y = y // 棋子位於棋盤格子座標系的y座標
    this.chessType = chessType // 棋子類型黑or白
    this.id = id  // 棋子id, 用做dom id, 移出棋子的時候會用到
  }
  // 傳入棋盤dom, 插入棋子dom節點
  draw (chessBoard) {
    // 建立一個dom, 根據this中的各項棋子狀態繪製棋子
    const chessPieceDom = document.createElement('div')
    chessPieceDom.id = this.id // 設置id
    chessPieceDom.style.width = CHESS_PIECE_WIDTH + 'px'
    chessPieceDom.style.height = CHESS_PIECE_WIDTH + 'px'
    chessPieceDom.style.borderRadius = (CHESS_PIECE_WIDTH / 2) + 'px'
    // 設置棋子顏色
    chessPieceDom.style.backgroundColor = CHESS_TYPES[this.chessType].color
    chessPieceDom.style.position = 'absolute'
    const getOffset = val => (((val + 1) * COLUMN_WIDTH) - CHESS_PIECE_WIDTH / 2) + 'px'
    // 設置棋子位置
    chessPieceDom.style.left = getOffset(this.x)
    chessPieceDom.style.top = getOffset(this.y)
    // 插入dom
    chessBoard.appendChild(chessPieceDom)
  }
  // canvas版棋子繪製方法
  drawCanvas (ctx) {
    // ... 考慮到篇幅具體實現不在這裏貼出來, 感興趣能夠去文章開始的Github倉庫看
  }
}
複製代碼
  1. 實現一個Gamer類, 用於控制遊戲流程
// util.js
/** * 初始化一個存儲五子棋棋局信息的數組 */
export const initChessPieceArr = () => new Array(CHESS_BOARD_SIZE).fill(0).map(() => new Array(CHESS_BOARD_SIZE).fill(null))
/** * 將棋盤座標轉化爲棋盤格子座標 * @param {Number} val 棋盤上的座標 */
export const transfromOffset2Grid = val => ~~((val / COLUMN_WIDTH) - 0.5)
// Gamer.js
/** * Gamer 類, 初始化一局五子棋遊戲 * PS: 我以爲這個類寫的有點亂了... * 總共維護了6個狀態 * isCanvas 是不是canvas模式 * chessPieceArr 保存棋盤中的棋子狀態 * count 棋盤中棋子數量, 關係到落子輪替順序 * lastStep 保存上一次的落子狀況, 用於悔棋操做 (當能夠毀多子的時候, 用數組去維護) * chessBoardDom 保存棋盤Dom節點 * chessBoardCtx 當渲染模式是canvas時, 保存canvas的Context (其實也能夠不保存, 根據dom去getContext便可) */
export default class Gamer {
  constructor ({isCanvas, chessPieceArr = initChessPieceArr()} = {isCanvas: false, chessPieceArr: initChessPieceArr()}) {
    this.chessPieceArr = chessPieceArr
    this.isCanvas = isCanvas
    // getRecoverArray這個方法就一開始分析需求講的從保存的數據中恢復棋局的方法
    const chessTemp = this.getRecoverArray(chessPieceArr)
    this.count = chessTemp.length
    if (this.isCanvas) { // canvas初始化
      this.chessBoardDom = initCanvas()
      const chessBoardCtx = this.chessBoardDom.getContext('2d')
      this.chessBoardCtx = chessBoardCtx
      drawBoardLines(chessBoardCtx)
      // 若是是切換渲染方法觸發的new Gamer(gameConfig), 就將原來棋局中的棋子進行繪製
      chessTemp.forEach(item => { item.drawCanvas(chessBoardCtx) })
    } else { // dom 初始化
      this.chessBoardDom = drawBorad()
      // 若是是切換渲染方法觸發的new Gamer(gameConfig), 就將原來棋局中的棋子進行繪製
      chessTemp.forEach(item => { item.draw(this.chessBoardDom) })
    }
    this.chessBoardDom.onclick = (e) => { this.onBoardClick(e) }
    // 插入dom, 遊戲開始
    if (!isCanvas) {
      document.getElementById('app').appendChild(this.chessBoardDom)
    }
    document.getElementById('cancel').style.display = 'inline'
  }
  // 遍歷二維數組, 返回一個Array<ChessPiece>
  getRecoverArray (chessPieceArr) {
    const chessTemp = []
    // 把初始的棋子拿出來並畫在棋盤上, 這個時候要祭出for循環大法了
    for (let i = 0, len1 = chessPieceArr.length; i < len1; i++) {
      for (let j = 0, len2 = chessPieceArr[i].length; j < len2; j++) {
        let chessSave = chessPieceArr[i][j]
        if (chessSave) {
          let chessPieceNew = new ChessPiece(i, j, chessSave.type, chessSave.id)
          chessTemp.push(chessPieceNew)
        }
      }
    }
    return chessTemp
  }
  onBoardClick ({clientX, clientY}) {
    console.log(this)
    const x = transfromOffset2Grid(clientX)
    const y = transfromOffset2Grid(clientY + window.scrollY)
    // 若是當前位置已經有棋子了, 你們就當作無事發生
    if (!this.chessPieceArr[x][y]) {
      // 控制棋子交替順序
      const type = this.count % 2 === 0 ? 'BLACK_CHESS_PIECE' : 'WHITE_CHESS_PIECE'
      // 維護lastStep這個狀態
      this.lastStep = {x, y, type, id: this.count}
      const cancel = document.getElementById('cancel')
      if (cancel.innerHTML !== '悔棋') { cancel.innerHTML = '悔棋' }
      const chessPiece = new ChessPiece(x, y, type, this.count)
      this.chessPieceArr[x][y] = {type, id: this.count}
      console.log(this.chessPieceArr[x][y])
      this.count++
      if (this.isCanvas) {
        chessPiece.drawCanvas(this.chessBoardCtx)
      } else {
        chessPiece.draw(this.chessBoardDom)
      }
      this.judge(x, y)
    }
  }
  // 悔棋
  cancelLastStep () {
    document.getElementById('cancel').innerHTML = '撤銷悔棋'
    if (this.lastStep) {
      const {x, y} = this.lastStep
      this.count = this.count - 1
      this.chessPieceArr[x][y] = null // 將目標棋子的信息設爲null
      if (this.isCanvas) {
        // canvas版本的悔棋, 將棋盤棋子從新繪製
        const temp = this.getRecoverArray(this.chessPieceArr)
        drawBoardLines(this.chessBoardCtx)
        temp.forEach(item => { item.drawCanvas() })
      } else {
        // Dom版本悔棋, 直接移出棋子dom
        const chessPiece = document.getElementById(this.count)
        chessPiece.parentNode.removeChild(chessPiece)
      }
    }
  }
  // 撤銷悔棋, 將棋子從新繪製
  cancelTheCancel () {
    document.getElementById('cancel').innerHTML = '悔棋'
    const {x, y, type, id} = this.lastStep
    const canceledPiece = new ChessPiece(x, y, type, id)
    if (this.isCanvas) {
      canceledPiece.drawCanvas(this.chessBoardCtx)
    } else {
      canceledPiece.draw(this.chessBoardDom)
    }
    this.chessPieceArr[x][y] = {type, id}
    this.count = this.count + 1
  }
  removeDom () {
    if (!this.isCanvas) {
      this.chessBoardDom.parentNode.removeChild(this.chessBoardDom)
    } else {
      this.chessBoardDom.style.display = 'none'
    }
  }
  /** * 判斷當前棋子是否構成勝利條件, 落子以後, 判斷這個子的八個方向是否連成了五子 * @param {Number} x 棋子x座標 * @param {Number} y 棋子y座標 */
  judge (x, y) {
    const type = this.chessPieceArr[x][y].type
    const isWin = atLeastOneTrue(
      this.judgeX(x, y, type), // 具體實現請看源碼
      this.judgeX_(x, y, type),
      this.judgeY(x, y, type),
      this.judgeY_(x, y, type),
      this.judgeXY(x, y, type),
      this.judgeXY_(x, y, type),
      this.judgeYX(x, y, type),
      this.judgeYX_(x, y, type)
    )
    if (isWin) {
      setTimeout(() => window.alert(`${CHESS_TYPES[type].name}贏了!!!`), 0)
      document.getElementById('cancel').style.display = 'none'
      this.chessBoardDom.onclick = () => { window.alert(`${CHESS_TYPES[type].name}贏了, 別點了...`) }
    }
  }
}
複製代碼
  1. 最後, 就能夠開心的new Gamer(gameconfig) 開始一局遊戲啦
// index.js
import Gamer from './src/Gamer'

let gameConfig = {isCanvas: false}
let game = new Gamer(gameConfig)

// 開始, 從新開始
document.getElementById('start').onclick = () => {
  game.removeDom()
  gameConfig = {isCanvas: gameConfig.isCanvas}
  game = new Gamer(gameConfig)
}
// 切換dom渲染與canvas渲染
document.getElementById('switch').onclick = () => {
  game.removeDom()
  gameConfig = {chessPieceArr: game.chessPieceArr, isCanvas: !gameConfig.isCanvas}
  game = new Gamer(gameConfig)
}
// 悔棋
// ps: 一開始講的須要用一個狀態去維護當前是悔棋仍是撤消悔棋, 我直接用的Dom innerHtml判斷了....
const cancel = document.getElementById('cancel')
cancel.onclick = () => {
  cancel.innerHTML === '悔棋' ? game.cancelLastStep() : game.cancelTheCancel()
}
複製代碼
相關文章
相關標籤/搜索