自己 coding 也有些年了 , 記得非常遠古以前學 java 的時候 , 曾經問要怎麼做出象棋 or 暗棋 , 不過對方卻回說非常困難 , 你還是放棄吧 ~ 直接勸退的概念 這次剛好弄到 vue drag & drop 就順便開開腦洞來寫看看 , 此外我還有搞個 angular 版本
See the Pen
龜小暗棋 by 喇賽人 (@weber87na )
on CodePen .
資料結構 棋子 首先棋子的部分主要有以下幾個屬性id
=> 區分每個子的唯一識別編號name
=> 顯示該棋子是 兵
炮
或其他兵種使用suit
=> 紅色或黑色lv
=> 棋子的等級 , 由此判斷他能吃什麼子 , 或他的移動行為isDie
=> 判定這個子是否被打掛了 , 這個後來沒用到 , 不然應該要做個陣亡區之類的isOpen
=> 判斷是否已經翻開x
=> 目前的 x 座標y
=> 目前的 y 座標
1 { id: 1, name: '兵', suit: 'red', lv: 1, isDie: false, isOpen: false, x: 0, y: 0 },
接著把所有棋子都定義好 , 然後塞到一個 cheesPool 的 array 裡面即可
棋盤 棋盤其實就是由一堆格子構成 , 它的結構比較簡單id
=> 區分每格的唯一識別編號x
=> 該格的 x 座標y
=> 該格的 y 座標chees
=> 這格上面的棋子
1 { id: 1, x: 0, y: 0, chees: undefined }
如同棋子一樣 , 先定義 cells
這個 array 然後把格子塞進去就變成棋盤了 最後用 shuffle
方法把棋子設定擺滿整個棋盤 , 並且洗亂順序
1 2 3 4 5 6 7 8 9 10 11 12 13 function shuffle ( ) { cheesPool.value = cheesPool.value.sort(() => Math .random() - 0.5 ) for (let i = 0 ; i < cells.value.length; i++) { let cell = cells.value[i] let chees = cheesPool.value[i] chees.isOpen = false chees.isDie = false chees.x = cell.x chees.y = cell.y cell.chees = chees } }
UI 設計 html 首先 drag & drop 主要原理可以看這篇
接著關鍵如下所述
先用 v-for
對整個棋盤進行 loop
每個格子 cell
@drop
需要設定 onDrop
讓棋子可以被拖拉到格子上@dragover
& @dragover
都要設定 prevent
然後在格子內塞個 div v-if
去判斷這格是否有棋子 , 如果有的話則顯示該子 , 並讓他的事件 & 樣式可以觸發 首先看 :data-xy
這個會把該子的座標利用 attr
傳進去給 css
裡面方便 debug
@click
的 onCheesClick
方法則負責翻子的動作:class
內設定這個棋子是否被翻開及套用它的花色:draggable
需要該子已經翻開後 , 才可以觸發 drag
的事件@dragstart
裡面放 onDrag
這個事件 最後 div
內的文字則依照該子是否翻開才進行顯示
status-panel
的部分則 show 出現在是誰的回合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <div id ="app" class ="wrap" > <div class ="board" > <div class ="cell" v-for ="cell in cells" @drop ="onDrop($event, cell)" @dragover.prevent @dragenter.prevent > <div v-if ="cell.chees" > <div class ="chees" :data-xy ="`x:${cell.chees.x},y:${cell.chees.y}`" @click ="onCheesClick($event, cell.chees)" :class ="[{ 'chees-open': cell.chees.isOpen }, cell.chees.suit]" :draggable ="cell.chees.isOpen" @dragstart ="onDrag($event, cell)" > {{ cell.chees.isOpen ? cell.chees.name : '' }} </div > </div > </div > </div > <div class ="status-panel" > <button id ="go" @click ="shuffle()" > GO</button > <h3 > 誰的回合: <span :class ="currentPlayer" > {{ currentPlayer }}</span > </h3 > </div >
css 核心關鍵如下 首先先用 flex 繪製一個居中的區塊 , 接著裡面擺上 grid 當作棋盤即可 我的想法是棋盤是方格子 , 所以用 grid 應該比較可以快速建構布局 , 自己對 grid 也比較陌生就玩看看 真的不行還有這個工具 可以輔助產生 grid 棋盤一共 32 格所以可以定義如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 .board { width : 647px ; height : 323px ; display : grid; grid-template-columns : repeat (8 , 1 fr); grid-template-rows : repeat (4 , 1 fr); grid-column-gap : 1px ; grid-row-gap : 1px ; background-color : red; }
棋子被翻開要設定 chees-open
讓他的色彩為白色 , 反之綠色則為蓋起來的時候
1 2 3 .chees-open { background-color : #fff ; }
接著利用偽元素的 after
設定 content: attr(data-xy)
讓棋子有 xy 座標方便 debug
1 2 3 4 5 6 7 8 9 10 11 12 .chees ::after { color : black; font-family : '微軟正黑體' ; font-size : 8px ; content : attr (data-xy); line-height : 20px ; height : 20px ; width : 60px ; position : absolute; bottom : 0 ; right : 0 ; }
最後則是花色部分記得要設定 red
& black
這樣棋子翻開後上面的字就會是該顏色
1 2 3 4 5 6 7 .red { color : red; } .black { color : #000 ; }
核心邏輯說明 切換玩家 先定義現在是誰的回合 , 接著定義 togglePlayer
即可
1 2 3 4 5 6 const currentPlayer = ref('black' )function togglePlayer ( ) { if (currentPlayer.value === 'black' ) currentPlayer.value = 'red' else currentPlayer.value = 'black' }
翻開棋子 如果該子尚未翻開設定他翻開 , 並且切換回合
1 2 3 4 5 6 7 8 9 10 function onCheesClick (event, chees ) { console .log(event) console .log(chees) if (chees.isOpen === false ) { chees.isOpen = true togglePlayer() } }
移動到空白格 如果移動過去則回傳 true 反之 false , 如果該格上面沒子的話才會觸發 , 設定座標 & 棋子到這格上面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function moveToEmptyCell (dragCell, dropCell ) { let isMove = false const dragChees = dragCell.chees if (dropCell.chees === undefined ) { console .log(dragCell) dragCell.chees = undefined dragChees.x = dropCell.x dragChees.y = dropCell.y dropCell.chees = dragChees isMove = true } return isMove }
判斷該子是否陣亡 當吃子的動作觸發時便會呼叫這個函數 比較特別的是炮正常狀況不能吃子 , 需要跳躍後才能 小兵可以打掉將 , 將沒辦法打小兵 其他則比較等級大小即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function checkIsDie (dragCell, dropCell ) { if (dragCell.chees.lv === 2 ) { return false } if (dragCell.chees.lv === 1 && dropCell.chees.lv === 7 ) { return true } if (dragCell.chees.lv === 7 && dropCell.chees.lv === 1 ) { return false } if (dragCell.chees.lv >= dropCell.chees.lv) { return true } return false }
攻擊棋子及移動的動作 要滿足以下條件才會執行 首先該格要有棋子 該子已經是翻開的狀態 drag & drop 花色要相異 另外攻擊棋子也隱含著移動的動作 , 所以當把棋子打掛之後 , 要移動過去 倘若該格沒子則是正常移動過去 , 所以把 moveToEmptyCell
寫在最後面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function attackChees (dragCell, dropCell ) { if (dropCell.chees !== undefined ) { if (dragCell.chees.isOpen === true && dropCell.chees.isOpen === true ) { if (dragCell.chees.suit !== dropCell.chees.suit) { if (checkIsDie(dragCell, dropCell)) { dropCell.chees.isDie = true dropCell.chees = undefined } } } } let isMove = moveToEmptyCell(dragCell, dropCell) return isMove }
判斷棋子能否移動到該格 這裡設定正常可以移動的狀況 isIllegalMove
做為一個收攏函數 如果是非法移動則回傳 true
正常移動回傳 false
isDistanceOutofRange
用來判斷是否移動超過一格以上或是有人白目原地移動 另外白目的人也會斜著移動 , 很邪惡 XDisMoveTopLeft
是否向左上移動isMoveTopRight
是否向右上移動isMoveBottomRight
是否向右下移動isMoveBottomLeft
是否向左下移動
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 function isIllegalMove (dragCell, dropCell ) { if (isDistanceOutofRange(dragCell, dropCell)) return true if (isMoveTopLeft(dragCell, dropCell)) return true if (isMoveTopRight(dragCell, dropCell)) return true if (isMoveBottomRight(dragCell, dropCell)) return true if (isMoveBottomLeft(dragCell, dropCell)) return true return false } function isDistanceOutofRange (dragCell, dropCell ) { let distanceX = Math .abs(dragCell.x - dropCell.x) let distanceY = Math .abs(dragCell.y - dropCell.y) if (distanceX > 1 || distanceY > 1 ) return true if (distanceX === 0 && distanceY === 0 ) return true return false } function isMoveTopLeft (dragCell, dropCell ) { let moveX = dragCell.x - 1 let moveY = dragCell.y - 1 if (dropCell.x === moveX && dropCell.y === moveY) { return true } return false } function isMoveTopRight (dragCell, dropCell ) { let moveX = dragCell.x + 1 let moveY = dragCell.y - 1 if (dropCell.x === moveX && dropCell.y === moveY) { return true } return false } function isMoveBottomRight (dragCell, dropCell ) { let moveX = dragCell.x + 1 let moveY = dragCell.y + 1 if (dropCell.x === moveX && dropCell.y === moveY) { return true } return false } function isMoveBottomLeft (dragCell, dropCell ) { let moveX = dragCell.x - 1 let moveY = dragCell.y + 1 if (dropCell.x === moveX && dropCell.y === moveY) { return true } return false }
計算炮的移動中間有幾個子 當炮要移動或是吃子時有飛這個動作 , 簡單的說就是中間要相隔一顆子 , 這裡統計往上下左右移動時有幾顆子 這裡關鍵就是 i
由 1
開始 , 且 小於 distance
然後利用 find
函數去找到該格上的棋子進行統計 我本來是想用遞迴寫這個邏輯 , 不過後來想想只要計算兩個子之間有幾顆子即可 , 所以看到 findLeftCell findRightCell findTopCell findBottomCell
這幾個函數都不用理他們 不然應該也可以先用遞迴去找第一顆當砲台的子後 , 然後找第二顆子確認是攻擊目標
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 function calcLeftAtkRangeCount (dragCell, dropCell ) { let distanceX = Math .abs(dragCell.x - dropCell.x) let result = 0 for (let i = 1 ; i < distanceX; i++) { let isFind = cells.value.find(cell => cell.x == dragCell.x - i && cell.y == dragCell.y) if (isFind.chees) { result++ } } return result } function calcTopAtkRangeCount (dragCell, dropCell ) { let distanceY = Math .abs(dragCell.y - dropCell.y) let result = 0 for (let i = 1 ; i < distanceY; i++) { let isFind = cells.value.find(cell => cell.y == dragCell.y - i && cell.x == dragCell.x) if (isFind.chees) { result++ } } return result } function calcRightAtkRangeCount (dragCell, dropCell ) { let distanceX = Math .abs(dragCell.x - dropCell.x) let result = 0 for (let i = 1 ; i < distanceX; i++) { let isFind = cells.value.find(cell => cell.x == dragCell.x + i && cell.y == dragCell.y) if (isFind.chees) { result++ } } return result } function calcBottomAtkRangeCount (dragCell, dropCell ) { let distanceY = Math .abs(dragCell.y - dropCell.y) let result = 0 for (let i = 1 ; i < distanceY; i++) { let isFind = cells.value.find(cell => cell.y == dragCell.y + i && cell.x == dragCell.x) if (isFind.chees) { result++ } } return result }
Drag 棋子 先看看該格子上是否有棋子 , 有的話設定 dataTransfer
讓 Drop
時可以接收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function onDrag (event, cell ) { let chees = cell.chees if (chees === undefined ) return console .log('drag' , event) console .log('drag' , chees) console .log('drag' , cell) event.dataTransfer.dropEffect = 'move' event.dataTransfer.effectAllowed = 'move' event.dataTransfer.setData('cheesId' , chees.id) event.dataTransfer.setData('cellId' , cell.id) }
Drop 棋子 這裡應該要重構下炮的移動不過太晚沒啥精力了 主要分為兩個狀況 , 如果是炮的話則使用 if (dragCell.chees.lv === 2 && dropCell.chees?.suit != dragCell.chees?.suit)
內的條件式 , 反之就是其他子
先講正常狀況 , 在正常狀況先判斷是否為非法移動 if (isIllegalMove(dragCell, dropCell)) return
接著判斷該格子是否為空 , 若為空直接呼叫 moveToEmptyCell
移動到該格 反之則呼叫 attackChees
最後如果有移動的話則切換回合
接著講炮的邏輯他有可能是橫向移動 if (dragCell.y === dropCell.y && Math.abs(dragCell.x - dropCell.x) > 1)
或是垂直移動 else if (dragCell.x === dropCell.x && Math.abs(dragCell.y - dropCell.y) > 1)
以橫向移動為例 , 首先判斷是往左打還是往右打 , 接著呼叫 calcXXXAtkRangeCount
系列函數得知有幾個子 如果中間是一個子的話則表示可以攻擊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 if (dragCell.y === dropCell.y && Math .abs(dragCell.x - dropCell.x) > 1 ) { if (dropCell.chees === undefined ) return if (dragCell.x > dropCell.x) { let inAtkRangeCount = calcLeftAtkRangeCount(dragCell, dropCell) console .log('inAtkRangeCount' , inAtkRangeCount) if (inAtkRangeCount === 1 ) { if (dropCell?.chees) { console .log('find' ) if (dropCell?.chees?.isOpen) { dropCell.isDie = true dropCell.chees = undefined moveToEmptyCell(dragCell, dropCell) togglePlayer() return } } } } if (dragCell.x < dropCell.x) { if (dropCell.chees === undefined ) return let inAtkRangeCount = calcRightAtkRangeCount(dragCell, dropCell) console .log('inAtkRangeCount' , inAtkRangeCount) if (inAtkRangeCount === 1 ) { if (dropCell?.chees) { console .log('find' ) if (dropCell?.chees?.isOpen) { dropCell.isDie = true dropCell.chees = undefined moveToEmptyCell(dragCell, dropCell) togglePlayer() return } } } } }
完整的 onDrop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 function onDrop (event, dropCell ) { console .log('drop' , event) console .log('drop' , dropCell) const dragCellId = parseInt (event.dataTransfer.getData('cellId' )) const dragCell = cells.value.find(x => x.id == dragCellId) if (dragCell?.chees?.suit !== currentPlayer.value) return if (dragCell.chees.lv === 2 && dropCell.chees?.suit != dragCell.chees?.suit) { if (dragCell.y === dropCell.y && Math .abs(dragCell.x - dropCell.x) > 1 ) { if (dropCell.chees === undefined ) return if (dragCell.x > dropCell.x) { let inAtkRangeCount = calcLeftAtkRangeCount(dragCell, dropCell) console .log('inAtkRangeCount' , inAtkRangeCount) if (inAtkRangeCount === 1 ) { if (dropCell?.chees) { console .log('find' ) if (dropCell?.chees?.isOpen) { dropCell.isDie = true dropCell.chees = undefined moveToEmptyCell(dragCell, dropCell) togglePlayer() return } } } } if (dragCell.x < dropCell.x) { if (dropCell.chees === undefined ) return let inAtkRangeCount = calcRightAtkRangeCount(dragCell, dropCell) console .log('inAtkRangeCount' , inAtkRangeCount) if (inAtkRangeCount === 1 ) { if (dropCell?.chees) { console .log('find' ) if (dropCell?.chees?.isOpen) { dropCell.isDie = true dropCell.chees = undefined moveToEmptyCell(dragCell, dropCell) togglePlayer() return } } } } } else if (dragCell.x === dropCell.x && Math .abs(dragCell.y - dropCell.y) > 1 ){ if (dropCell.chees === undefined ) return if (dragCell.y > dropCell.y) { let inAtkRangeCount = calcTopAtkRangeCount(dragCell, dropCell) console .log('inAtkRangeCount' , inAtkRangeCount) if (inAtkRangeCount === 1 ) { if (dropCell?.chees) { console .log('find' ) if (dropCell?.chees?.isOpen) { dropCell.isDie = true dropCell.chees = undefined moveToEmptyCell(dragCell, dropCell) togglePlayer() return } } } } if (dragCell.y < dropCell.y) { if (dropCell.chees === undefined ) return let inAtkRangeCount = calcBottomAtkRangeCount(dragCell, dropCell) console .log('inAtkRangeCount' , inAtkRangeCount) if (inAtkRangeCount === 1 ) { if (dropCell?.chees) { console .log('find' ) if (dropCell?.chees?.isOpen) { dropCell.isDie = true dropCell.chees = undefined moveToEmptyCell(dragCell, dropCell) togglePlayer() return } } } } } else { if (isIllegalMove(dragCell, dropCell)) return } } else { if (isIllegalMove(dragCell, dropCell)) return } let isMove = false if (dropCell.chees === undefined ) { isMove = moveToEmptyCell(dragCell, dropCell) } else { isMove = attackChees(dragCell, dropCell) } if (isMove) togglePlayer() }