用原生JS寫一個象棋小遊戲
前言
最近沒什麼需求,藉著上班摸魚的時間,寫點程式碼玩玩。過年的時候總是和爺爺下象棋,這次用原生JS寫了一個象棋小遊戲,但是技術有限,只能自己一個人左右手互博來下棋。象棋規則基本上都實現了,下面就來給大家分享一下。
我正在參與碼上掘金程式設計挑戰賽,請為我點贊吧! jcode
一、繪製棋盤和棋子
遊戲的元素是棋盤和棋子,圖片都是在網上搜,然後用截圖工具截下來,就可以直接使用了。棋子通過遍歷陣列來渲染,因為棋子的種類很多,所以給數組裡的元素添加了一些屬性,num值對應的是棋子的型別,type值對應棋子的陣營。公共的屬性和方法寫在棋子父類裡,每一種的棋子都繼承這個父類的屬性。
**css:**
table {
position: relative;
width: 627px;
height: 600px;
margin: 0 auto;
padding: 6px;
border: 5px solid black;
background: url(./棋盤.png);
}
td {
width: 60px;
height: 64px;
border-radius: 50%;
margin-right: 10px;
cursor: pointer;
}
.bgc1 {
background: url(./車.png);
background-size: 94%;
background-position: 1px 0px;
}
.bgc2 {
background: url(./馬.png);
background-size: 94%;
background-position: 1px 0px;
}
.bgc3 {
background: url(./象.png);
background-size: 94%;
background-position: 1px 0px;
}
.bgc4 {
background: url(./士.png);
background-size: 94%;
background-position: 2px -1px;
}
.bgc5 {
background: url(./將.png);
background-size: 94%;
background-position: 1px 0px;
}
.bgc6 {
background: url(./炮.png);
background-size: 94%;
background-position: 1px 0px;
}
.bgc7 {
background: url(./卒.png);
background-size: 94%;
background-position: 1px 0px;
}
.bgc8{
background: url(./車2.png);
background-size: 94%;
background-position: 1px 0px;
}
.bgc9{
background: url(./馬2.png);
background-size: 94%;
background-position: 1px 0px;
}
.bgc10{
background: url(./相2.png);
background-size: 94%;
background-position: 1px 0px;
}
.bgc11{
background: url(./仕2.png);
background-size: 94%;
background-position: 1px 0px;
}
.bgc12{
background: url(./帥2.png);
background-size: 94%;
background-position: 1px 0px;
}
.bgc13{
background: url(./炮2.png);
background-size: 94%;
background-position: 1px 0px;
}
.bgc14{
background: url(./兵2.png);
background-size: 94%;
background-position: 1px 0px;
}
.message1,.message2{
position: absolute;
left: 45%;
top: 40%;
width: 100px;
height: 100px;
background-color: rgba(138, 136, 136, 0.493);
line-height: 100px;
font-size: 60px;
color: red;
}
.message1{
display: none;
}
.message2{
display: none;
width: 200px;
}
.message3{
position: absolute;
left: 20%;
top: 40%;
width: 100px;
height: 100px;
line-height: 100px;
font-size: 60px;
color: red;
}
/* 棋子選中的樣式 */
.checked {
transform: translateY(-15px);
transition: all .2s;
}
/* 棋子可移動座標顯示的樣式 */
.showWays{
position: relative;
}
.showWays::after{
content:'';
position: absolute;
top: 25px;
left: 25px;
background-color: rgb(19, 23, 235);
width: 10px;
height: 10px;
}
**html:**
<table></table>
<div class="message1">
吃!
</div>
<div class="message2">
將軍!
</div>
<div class="message3">
紅先
</div>
**js:**
let list = [ // 棋盤的初始化佈局,num代表棋子種類,type代表陣營
[{num:1,type:1},{num:2,type:1},{num:3,type:1},{num:4,type:1},{num:5,type:1},{num:4,type:1},{num:3,type:1},{num:2,type:1},{num:1,type:1}],
[{num:0},{num:0},{num:0},{num:0},{num:0},{num:0},{num:0},{num:0},{num:0}],
[{num:0},{num:6,type:1},{num:0},{num:0},{num:0},{num:0},{num:0},{num:6,type:1},{num:0}],
[{num:7,type:1},{num:0},{num:7,type:1},{num:0},{num:7,type:1},{num:0},{num:7,type:1},{num:0},{num:7,type:1}],
[{num:0},{num:0},{num:0},{num:0},{num:0},{num:0},{num:0},{num:0},{num:0}],
[{num:0},{num:0},{num:0},{num:0},{num:0},{num:0},{num:0},{num:0},{num:0}],
[{num:7,type:2},{num:0},{num:7,type:2},{num:0},{num:7,type:2},{num:0},{num:7,type:2},{num:0},{num:7,type:2}],
[{num:0},{num:6,type:2},{num:0},{num:0},{num:0},{num:0},{num:0},{num:6,type:2},{num:0}],
[{num:0},{num:0},{num:0},{num:0},{num:0},{num:0},{num:0},{num:0},{num:0}],
[{num:1,type:2},{num:2,type:2},{num:3,type:2},{num:4,type:2},{num:5,type:2},{num:4,type:2},{num:3,type:2},{num:2,type:2},{num:1,type:2}],
]
let menu = { // 棋子的列舉屬性
0:(index,index2,item2,tr)=>{ // 無棋子
return new Qi(index,index2,item2,tr,0)
},
1:(index,index2,item2,tr,type)=>{ // 車
return new Ju(index,index2,item2,tr,1,type)
},
2:(index,index2,item2,tr,type)=>{ // 馬
return new Ma(index,index2,item2,tr,2,type)
},
3:(index,index2,item2,tr,type)=>{ // 象
return new Xiang(index,index2,item2,tr,3,type)
},
4:(index,index2,item2,tr,type)=>{ // 士
return new Shi(index,index2,item2,tr,4,type)
},
5:(index,index2,item2,tr,type)=>{ // 將
return new Jiang(index,index2,item2,tr,5,type)
},
6:(index,index2,item2,tr,type)=>{ // 炮
return new Pao(index,index2,item2,tr,6,type)
},
7:(index,index2,item2,tr,type)=>{ // 卒
return new Zu(index,index2,item2,tr,7,type)
},
}
let typeList = { // 下棋順序列舉
1:'黑棋',
2:'紅棋'
}
class Qi { // 棋子公共類
constructor(index,index2,item2,tr,name,type){
this.num = name // 棋子列舉屬性
this.type = type // 棋子陣營標識
this.tr = tr
this.td = document.createElement('td')
this.y = index
this.x = index2
this.td.dataset.y = index // 給每一個td元素設定座標
this.td.dataset.x = index2 // 給每一個td元素設定座標
item2 && type == 2 ? this.td.classList.add(`bgc${item2+7}`) : this.td.classList.add(`bgc${item2}`)
this.waysArr = [] // 棋子可移動座標的存放陣列
tr.appendChild(this.td)
}
}
class Ju extends Qi { // 車
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
}
class Ma extends Qi { // 馬
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
}
class Xiang extends Qi { // 象
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
}
class Shi extends Qi { // 士
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
}
class Jiang extends Qi { // 將
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
}
class Pao extends Qi { // 炮
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
}
class Zu extends Qi { // 卒
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
}
//封裝渲染函式
const render = () => {
document.querySelector('table').innerHTML = ''
list.forEach((item, index) => {
let tr = document.createElement('tr')
item.forEach((item2, index2) => {
let num = list[index][index2].num
list[index][index2] = menu[num](index,index2,item2.num,tr,item2.type) // 列舉生成對應的棋子物件
})
document.querySelector('table').appendChild(tr)
})
}
render()
二、點選棋子後的邏輯處理
生成棋子的時候,給每一個td元素添加了座標。所以觸發點選事件的時候,通過這個座標,就能獲取到點選的元素對應的棋子物件了。第一次點選的時候,就呼叫棋子物件裡面對應的方法。如果兩次點選的地方一樣,就將棋子放下來;兩次點選的座標不一樣,就要呼叫棋子的移動方法。選中和放下通過新增和刪除css,來更換狀態。
let flag = 0 // 0:未選中棋子;1:選中棋子
let sequence = false // true:黑棋走;false:紅棋走
let ele,x,y
function clear() { // 清空資料
flag = 0
x = null
y = null
}
document.querySelector('table').addEventListener('click',e=>{
if(x == e.target.dataset.x && y == e.target.dataset.y){ // 兩次點選的地方一樣
clear() // 清空資料
ele.down() // 放下棋子
return
}
x = e.target.dataset.x || x
y = e.target.dataset.y || y
if(list[y][x].num && !flag){ // 選中要移動的棋子
ele = list[y][x]
console.log(ele,'選擇的棋子');
flag = 1
ele.checked() // 選中觸發的方法
}else if(flag){ // 移動棋子
ele.move(x,y)
clear() // 清空資料
}else{
console.log('此處沒有棋子');
clear() // 清空資料
}
})
選中和放下棋子的方法寫在公共類裡,先貼圖,因為裡面還有其他的程式碼,最後再一起附上
三、選中棋子之後,計算出棋子可以移動的座標
選中棋子之後,要計算出棋子可以移動的座標,並且通過小點的樣式展示在棋盤上。因為每一個棋子的移動規則都不同,比如馬走日字,象走田字並且不能過河,所以要在每一個子類裡面單獨去計算座標。
先計算出所有的座標,比如車和炮,就計算棋子所在的x軸和y軸的所有座標。座標計算出來後,再把座標上有同陣營棋子的點、以及不能走的點篩除,只留下空點的座標或者可以被這個棋子吃掉的座標。
計算座標使用getLine()方法,每一個子類都需要根據象棋的規則單獨計算
篩選座標使用filterLine()方法,因為車、炮、將這3個棋的篩選計算與其他的棋子不同,所以這3個的計算方法在子類裡重寫,其他的棋子使用公共類裡的通用方法
公共類的程式碼還是先貼圖
下面是子類棋子的全部程式碼:
class Ju extends Qi { // 車
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
getLine(x,y,num){ // 獲取車棋子可以移動到的所有座標,num值控制是否顯示路徑
console.log(x,y,'選擇棋子的座標');
let lineArr = list.map((item,index)=>{ // 未篩選的所有可移動的座標
return index === +y? item : item[x]
})
let resultArr = this.filterLine(lineArr,x,y,num) // 將路徑上有棋子的座標篩選出來
this.showWays(resultArr,num) // 棋盤上展示可移動的路徑
return resultArr
}
filterLine(arr,x,y,num){ // 篩選移動路徑上的棋子
// 以選中棋子為中心,將x軸和y軸的點分為4份,每一份都要遍歷找出滿足條件的座標,最後再合併
let yArr1 = arr.slice(0,y)
let yArr2 = arr.slice(y+1,arr.length)
let xArr1 = arr[y].slice(0,x)
let xArr2 = arr[y].slice(x+1,arr[y].length)
console.log({yArr1,yArr2,xArr1,xArr2});
// loopMethod:自定義的遍歷方法
return [...this.loopMethod(xArr1,-1,this.num),...this.loopMethod(xArr2,1,this.num),...this.loopMethod(yArr1,-1,this.num),...this.loopMethod(yArr2,1,this.num),
]
}
}
class Ma extends Qi { // 馬
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
getLine(x,y,num){ // 獲取馬可以移動到的所有座標
console.log(x,y,'選擇棋子的座標');
let lineArr = [] // 未篩選的所有可移動的座標
if(!list[y][x-1]?.num){ // 馬為日字形,如果馬腳有棋則不能走
lineArr.push([list[y-1]?.[x-2],list[y+1]?.[x-2]])
}
if(!list[y][x+1]?.num){
lineArr.push([list[y+1]?.[x+2],list[y-1]?.[x+2]])
}
if(!list[y-1]?.[x].num){
lineArr.push([list[y-2]?.[x+1],list[y-2]?.[x-1]])
}
if(!list[y+1]?.[x].num){
lineArr.push([list[y+2]?.[x+1],list[y+2]?.[x-1]])
}
let resultArr = this.filterLine(lineArr,x,y,num) // 將路徑上有棋子的座標篩選出來
this.showWays(resultArr,1,num) // 棋盤上展示可移動的路徑
return resultArr
}
}
class Xiang extends Qi { // 象
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
getLine(x,y,num){ // 獲取象可以移動到的所有座標,象心不能有棋子,並且不能過河
console.log(x,y,'選擇棋子的座標');
let lineArr = [] // 未篩選的所有可移動的座標
if(list[y-1]?.[x-1]?.num === 0 && y!==5){
lineArr.push(list[y-2][x-2])
}
if(list[y+1]?.[x+1]?.num === 0 && y!==4){
lineArr.push(list[y+2][x+2])
}
if(list[y-1]?.[x+1]?.num === 0 && y!==5){
lineArr.push(list[y-2][x+2])
}
if(list[y+1]?.[x-1]?.num === 0 && y!==4){
lineArr.push(list[y+2][x-2])
}
let resultArr = this.filterLine(lineArr,x,y,num) // 將路徑上有棋子的座標篩選出來
this.showWays(resultArr,1,num) // 棋盤上展示可移動的路徑
return resultArr
}
}
class Shi extends Qi { // 士
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
getLine(x,y,num){ // 獲取士可以移動到的所有座標
console.log(x,y,'選擇棋子的座標');
let lineArr = [] // 未篩選的所有可移動的座標
if(x==3 || x==5){ // 士只能在九宮格範圍內斜著走
this.type == 1 ? lineArr.push(list[1][4]) : lineArr.push(list[8][4])
}else{
this.type == 1 ? lineArr.push([list[0][3]],[list[0][5]],[list[2][3]],[list[2][5]])
: lineArr.push([list[7][3]],[list[7][5]],[list[9][3]],[list[9][5]])
}
let resultArr = this.filterLine(lineArr,x,y,num) // 將路徑上有棋子的座標篩選出來
this.showWays(resultArr,1,num)
return resultArr
}
}
class Jiang extends Qi { // 將
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
getLine(x,y,num){ // 獲取將可以移動到的所有座標
console.log(x,y,'選擇棋子的座標');
let lineArr = [] // 未篩選的所有可移動的座標
lineArr.push([list[y][x+1]],[list[y][x-1]],[list[y-1]?.[x]],[list[y+1]?.[x]])
let resultArr = this.filterLine(lineArr,x,y,num) // 將路徑上有棋子的座標篩選出來
this.showWays(resultArr,1,num)
return resultArr
}
filterLine(arr,x,y,num){ // 篩選移動路徑上的棋子
// 獲取敵方帥的座標
let resultArr = arr.filter(item=>{ // 將有棋子的座標篩選出來,剩下的就是可以走的地方
if(this.type == 1){
return item[0]?.x>2&&item[0]?.x<6&&item[0]?.y>=0&&item[0]?.y<3&&item[0]?.type!=1
}else{
return item[0]?.x>2&&item[0]?.x<6&&item[0]?.y>=7&&item[0]?.y<10&&item[0]?.type!=2
}
})
let shuai = list.flat().find(item=>item.num === 5 && item.type !== this.type) // 敵方帥的座標
let jiang = resultArr.flat().find(item=>item.x === shuai.x)
if(jiang){ // 判斷將和帥可以走的座標裡面,有沒有和對方將為同一列的
let res = list.flat().find(item=>{
if(shuai.y>jiang.y){
return item.x === jiang.x && item.y>jiang.y && item.y<shuai.y && item.num
}else{
return item.x === jiang.x && item.y<jiang.y && item.y>shuai.y && item.num
}
})
if(!res){ // 如果兩個帥在同一列,並且中間沒有棋子,就不可以走,把這個座標刪除
let resArr = resultArr.flat().filter(item=>!(item.x==jiang.x&&item.y==jiang.y))
return resArr
}
}
return resultArr.flat()
}
}
class Pao extends Qi { // 炮
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
getLine(x,y,num){ // 獲取炮棋子可以移動到的所有座標
console.log(x,y,'選擇棋子的座標');
let lineArr = list.map((item,index)=>{ // 未篩選的所有可移動的座標
return index === +y? item : item[x]
})
let resultArr = this.filterLine(lineArr,x,y,num) // 將路徑上有棋子的座標篩選出來
this.showWays(resultArr,1,num)
return resultArr
}
filterLine(arr,x,y,num){ // 篩選移動路徑上的棋子
let yArr1 = arr.slice(0,y)
let yArr2 = arr.slice(y+1,arr.length)
let xArr1 = arr[y].slice(0,x)
let xArr2 = arr[y].slice(x+1,arr[y].length)
console.log({yArr1,yArr2,xArr1,xArr2});
return [...this.loopMethod(xArr1,-1,this.num),...this.loopMethod(xArr2,1,this.num),...this.loopMethod(yArr1,-1,this.num),...this.loopMethod(yArr2,1,this.num),
]
}
}
class Zu extends Qi { // 卒
constructor(index,index2,item2,tr,name,type){
super(index,index2,item2,tr,name,type)
}
getLine(x,y,num){ // 獲取卒可以移動到的所有座標
console.log(x,y,'選擇棋子的座標');
let lineArr = [] // 未篩選的所有可移動的座標
if(this.type == 1 && y<5 || this.type == 2 && y>4){ // 過河前的移動路徑
this.type == 1?lineArr.push(list[y+1]?.[x]) : lineArr.push(list[y-1]?.[x])
}else{ // 過河後的移動路徑
if(this.type == 1){
lineArr.push([list[y+1]?.[x],list[y][x+1],list[y][x-1]])
}else{
lineArr.push([list[y-1]?.[x],list[y][x+1],list[y][x-1]])
}
}
let resultArr = this.filterLine(lineArr,x,y,num) // 將路徑上有棋子的座標篩選出來
this.showWays(resultArr,1,num)
return resultArr
}
}
四、計算出可移動座標後,在棋盤上展示出來
將座標上每一個棋子物件裡對應的td元素,新增一個樣式即可。放下棋子的時候,就將樣式隱藏。
五、棋子的移動
棋子的移動就是把兩個座標裡的棋子物件交換位置,只要能拿到座標,實現這個就比較簡單了。但是要判斷一下第二次點選的座標,是不是在棋子的可移動範圍內,畢竟象棋移動是有規則的,不能想去哪就去哪。
六、吃子和將軍的提示,以及勝負的判斷
html裡已經畫好了幾個提示的div盒子,吃子和將軍的時候顯示不同的盒子即可。如果一方的將被吃掉了,那麼就提示遊戲結束,根據type值來判斷是哪一方獲勝。
最後附上棋子公共類的程式碼
class Qi { // 棋子公共類
constructor(index,index2,item2,tr,name,type){
this.num = name // 棋子列舉屬性
this.type = type // 棋子陣營標識
this.tr = tr
this.td = document.createElement('td')
this.y = index
this.x = index2
this.td.dataset.y = index // 給每一個td元素設定座標
this.td.dataset.x = index2 // 給每一個td元素設定座標
item2 && type == 2 ? this.td.classList.add(`bgc${item2+7}`) : this.td.classList.add(`bgc${item2}`)
this.waysArr = [] // 棋子可移動座標的存放陣列
tr.appendChild(this.td)
}
checked(){ // 選中棋子
console.log(list,'棋盤的物件陣列');
if(sequence){ // 處理下棋順序
if(list[this.y][this.x].type == 1){ // 黑棋下
let result = this.getLine(this.x,this.y,1) // 獲取這個棋子的所有可移動路線
this.td.classList.add('checked')
}else{
clear()
}
}else{
if(list[this.y][this.x].type == 2){ // 紅棋下
let result = this.getLine(this.x,this.y,1) // 獲取這個棋子的所有可移動路線
this.td.classList.add('checked')
}else{
clear()
}
}
}
down(){ // 放下棋子
this.td.classList.remove('checked')
this.showWays(this.waysArr,0) // 隱藏移動的座標
}
filterLine(arr,x,y,num){ // 篩選移動路徑上的棋子,通用方法
let resultArr = arr.flat().filter(item=>item?.num === 0 || item?.type&&item.type !== this.type)
return resultArr
}
loopMethod(arr,order,name=1){ // 自定義遍歷方法,order控制遍歷的順序
let resultArr = []
if(!arr.length) return resultArr
if(order < 0){ // 反方向遍歷陣列
for(let i = arr.length-1;i>=0;i--){
if(arr[i].num){
if(name == 1){ // 車的吃子方法
if(arr[i].type != this.type) resultArr.push(arr[i])
break
}else{ // 炮的吃子方法
for(let j = i-1;j>=0;j--){
if(arr[j].type != this.type && arr[j].num){
resultArr.push(arr[j])
break
}
}
break
}
}
resultArr.push(arr[i])
}
}else{
for(let i = 0;i<arr.length;i++){
if(arr[i].num){
if(name == 1){ // 車的吃子方法
if(arr[i].type != this.type) resultArr.push(arr[i])
break
}else{ // 炮的吃子方法
for(let j = i+1;j<arr.length;j++){;
if(arr[j].type != this.type && arr[j].num){
resultArr.push(arr[j])
break
}
}
break
}
}
resultArr.push(arr[i])
}
}
return resultArr
}
showWays(arr,flag){ // 顯示棋子可移動到的座標
this.waysArr = arr
// 選中狀態下顯示可移動座標,未選擇不顯示
this.waysArr.forEach(item=>{
flag? item.td.classList.add('showWays') : item.td.classList.remove('showWays')
})
}
move(x,y){ // 棋子移動
let allowMove = this.waysArr.find(item=>{ // 判斷將要移動的座標是否在可移動範圍內
return item.x == x && item.y == y
})
if(!allowMove){ // 不在可移動範圍,就將棋子放下
return this.down()
}
let nextChess= list[y][x] // 移動到的座標
let nextType = nextChess.type // 如果座標上有其他棋子,獲取它的陣營
let y1 = this.td.dataset.y
let x1 = this.td.dataset.x
list[y1][x1] = menu[0]([y1],[x1],0,this.tr)
list[y][x] = menu[this.num]([y],[x],this.num,this.tr,this.type)
this.td.dataset.y = y
this.td.dataset.x = x
sequence = !sequence // 交換下棋順序
let arr = this.getLine(+x,+y,0) // 計算棋子下一步可移動的座標,判斷是否顯示將軍
if(arr.find(item=>item.num == 5 && item.type !== this.type)){ // 如果棋子下一步的路徑包涵將,就顯示將軍
this.showMessage(2) // 1:吃 ; 2:將軍
}else if(nextType && nextType !== this.type){
this.showMessage(1) // 1:吃 ; 2:將軍
}
render()
if(nextChess.num === 5){ // 將軍被吃掉,遊戲結束
alert(`遊戲結束,${typeList[this.type]}勝!`)
}
}
showMessage(num){ // 吃子和將軍的時候給出提示
if(num == 2){ // 將軍提示
console.log('顯示將軍');
document.querySelector('.message2').style.display = 'block'
setTimeout(()=>{
document.querySelector('.message2').style.display = 'none'
},1000)
}else{ // 吃子提示
console.log('顯示吃');
document.querySelector('.message1').style.display = 'block'
setTimeout(()=>{
document.querySelector('.message1').style.display = 'none'
},1000)
}
}
}
總結
到這裡這個象棋小遊戲就寫完了,象棋的規則和邏輯基本上都能實現了。就是判斷將軍的邏輯處理的不是很好,有些情況下的將軍,顯示不出來。如果走了某一步棋之後,導致自己的帥會被對方吃掉,按理來說這一步棋是不可以走的,這個邏輯的處理並沒有寫。這個象棋只能在一個電腦上面自己跟自己玩,主要是想寫寫程式碼,怕腦子上班摸魚生鏽了,有些的不好的地方歡迎指正,不喜勿噴。