前些天, 一個朋友給我分享了一道筆試題, 題目以下
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()
}
複製代碼