Javascript 面向对象编程之五子棋

本文介绍了一个五子棋游戏的实现过程,包括棋盘绘制、轮流落子、输赢判定等功能,并支持DOM与Canvas两种渲染模式的切换及悔棋操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前些天, 一个朋友给我分享了一道笔试题, 题目如下

觉得挺有意思的, 于是就自己实现了一版.

先贴结果

Github仓库 Demo

分析需求

拿到题目之后, 首先要做的是分析所给的需求背后的逻辑与实现. 针对题目所给的需求, 我个人的理解如下

  1. 五子棋游戏的功能, 包括棋盘绘制, 轮流落子(轮流绘制黑白棋子), 输赢判定 (输赢判断的逻辑就只存在一种可能, 就是无论黑子白字, 只有在落子之后这个棋子的八个方向是否构成了连续的五子)
  2. 渲染模式的切换(DOM与Canvas的切换), 这一点首先是要针对第一点的需求作出Dom和canvas两个版本的实现, 其次呢, DOM版本与canvas版本来回切换的过程中, 游戏需要具备保存棋局和根据保存的数据恢复棋局的能力, 才能做到在游戏的过程中进行切换
  3. 悔棋与撤销悔棋, 首先假设只能进行一步悔棋操作与撤销悔棋操作,那么我们需要做的有一下几点
    1. 维护两个状态分别保存以下信息 (1). 当前能进行悔棋操作还是撤销悔棋操作, (2). 当前需要操作的是哪一个棋子
    2. 针对DOM版本和canvas版本需要分别实现移出棋子的功能(DOM版本可以直接移出棋子的DOM, canvas版本则需要整个重绘)

更详细代码的实现会在代码中讨论

Coding

需求分析完了, 接下来需要按部就班用代码实现

  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()
}
复制代码

转载于:https://juejin.im/post/5aae05086fb9a028bd4c17d3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值