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()
}
复制代码
相关文章
相关标签/搜索