前些天, 一个朋友给我分享了一道笔试题, 题目以下
javascript
拿到题目以后, 首先要作的是分析所给的需求背后的逻辑与实现. 针对题目所给的需求, 我我的的理解以下git
更详细代码的实现会在代码中讨论github
需求分析完了, 接下来须要循序渐进用代码实现web
// 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: '黑子' }
}
复制代码
// 这里只讲一下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
}
复制代码
// 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仓库看
}
}
复制代码
// 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}赢了, 别点了...`) }
}
}
}
复制代码
// 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()
}
复制代码