再談成麻結賬程式2.0
上一篇我說到有做2.0版本,本來一開始想著優化一下程式碼就能完成,結果突然發現之前的專案沒有做到面向物件,特別是如果後期要增加使用者的操作,基本上是不可以的,那麼如果要徹底解決這個問題,就不能把使用者寫固定,那麼就要引入面向物件思想,我們把打牌或者買馬的人都當成一個個牌者,都是一類人,首先我們去定義牌者這麼一個物件,然後在用到的時候去例項化,如果後面需要加減人,我們只需要去關注對應例項化的物件即可,當然這一期我還是主要以4個人為主,先不去考慮買馬和加人的問題。既然都寫到這裡了,那麼說明我肯定還想對其他部分進行優化,但是想法有點多,我想還是應該先有一個思維導圖比較好,然後就有了下面這張圖:
畫這張圖的目的,主要還是想先養成一個比較好的習慣,把一些重要的點先構思出來,然後根據這些設計一點點去完善功能,這張圖完成之後,就著手寫程式碼了,首先這次主介面佈局我採用的是一個九宮格的方式,佈局思路如下圖:
那如何區分出使用者呢?應該可以看到標綠的數字,2,4,6,8,剛好是偶數除以2就是1234,當成是4個使用者,寫一個1到9的for迴圈,針對每個需要的索引做不同的佈局。下面的程式碼就是九宮格的html程式碼:
1 <ul> 2 <li v-for="index in 9"> 3 <template v-if="index == 1"><!--規則層,主要是用來說明規則,還有不常用的按鈕,比如清局和清理快取功能--> 4 <div @click="shuomingVisiable = true" class="guize">規則</div> 5 </template> 6 <template v-if="index == 5"><!--公共牌面模組--> 7 <div class="flex-x-y-center"> 8 <div v-if="orderArr.length==0"@click="changePosition()" class="circle">換位</div> 9 <template v-if="!changePositionVisiable"> 10 <div v-if="orderArr.length==0" @click="changeName()" class="circle">改名</div> 11 <template> 12 <div v-if="!jiesuanVisiable && orderArr.length" @click="jiesuan()" class="circle">結算 13 </div> 14 <div v-if="jiesuanVisiable" @click="nextTime()" class="circle">下局</div> 15 </template> 16 <div v-if="orderArr.length>0" @click="huiqi()" class="circle">悔棋</div> 17 <div @click="zongpanCount();zongpanVisiable = true" class="circle">總盤</div> 18 </template> 19 </div> 20 </template> 21 <template v-if="index % 2 == 0"><!--使用者模組--> 22 <p class="username" v-if="!changeNameVisiable"> 23 {{persons["person"+index/2].showname}} <span 24 class="red">{{persons['person'+index/2]['total']}}</span></p> 25 <input v-else type="text" v-model='persons["person"+index/2].showname'> 26 <div @click="hexinjisuan(index,'bagang',1)" class="circle bagang">巴槓</div> 27 <div @click="hexinjisuan(index,'angang',1)" class="circle angang">暗槓</div> 28 <div @click="dianji(index,'gangshow')" class="circle gang">槓</div> 29 <div @click="hexinjisuan(index,'zimo',1)" class="circle zimo">自摸</div> 30 <div @click="dianji(index,'hushow')" class="circle hupai">胡牌</div> 31 <div @click="dianji(index,'hushow','jiao')" class="circle hupai" 32 v-if="!persons['person'+index/2]['over'] && chajiaoStatus">查叫</div> 33 <div v-if="persons['person'+index/2]['gangshow'] || persons['person'+index/2]['hushow']" 34 class="choose flex-x-y-center"> 35 <!-- 這裡是被槓選擇的層--> 36 <div v-if="persons['person'+index/2]['gangshow']" class="beigang "> 37 <div class="circle bkc7" @click="hexinjisuan(index,'diangang',2)">點槓了</div> 38 </div> 39 <!-- 這裡是被胡牌選擇的層 --> 40 <div v-if="persons['person'+index/2]['hushow']" class="beihu"> 41 <div class="circle bkc7" @click="hexinjisuan(index,'dianpao',2)">點炮了</div> 42 </div> 43 </div> 44 <div class="no-click" v-if="persons['person'+index/2]['over']"></div><!--結束彈層--> 45 <div class="position" v-if="changePositionVisiable"><!--換位彈層--> 46 <ul> 47 <li :class="`${(index!=oindex && oindex%2 == 0)?'cursor':''} flex-x-y-center`" 48 v-for="oindex in 9" @click="movePosition(index/2,oindex/2)"> 49 <!-- 這裡的隱藏很巧妙,應該都是採用9宮格方式,在每個位置不需要顯示對應上左右下家的小格 --> 50 <template v-if="index != oindex"> 51 <div>{{['上移','左移','右移','下移'][oindex/2 - 1]}}</div> 52 </template> 53 </li> 54 </ul> 55 </div> 56 </template> 57 </li> 58 </ul>
相對於1.0版本,這裡我把使用者的得分在每一個操作都進行體現,增加了一個換位置的功能,只要是在當前局沒有操作的時候都可以進行換位操作,其他的按鈕元素和1.0功能都差不多。針對於使用者模組這裡要單獨提出來說一下,之前是用的4個user元件來固定寫死使用者,這一次我實現對使用者定義了一個類,然後再進行例項化物件生成了4個使用者物件,然後所有的操作都是去改變使用者物件資料,最後再根據物件的裡面的屬性進行結算等操作。
首先定義變數的程式碼如下,相比起1.0減少了很多變數,程式碼如下:
1 data: { 2 persons: { 3 person1: new Person("person1", false), 4 person2: new Person("person2", false), 5 person3: new Person("person3", false), 6 person4: new Person("person4", false), 7 }, 8 zhuangIndex: 0, 9 orderArr: [], 10 textArr: [], 11 getTxtObj: { 12 bagang1: '巴槓', 13 angang1: '暗槓', 14 zimo1: '自摸', 15 diangang1: '槓', 16 dianpao1: '胡牌', 17 bagang2: '被巴槓', 18 angang2: '被暗槓', 19 zimo2: '被自摸', 20 diangang2: '被槓', 21 dianpao2: '被胡牌', 22 }, 23 single: 2, 24 chajiaoStatus: false, 25 order: 0, //記錄每一個操作標識,目前沒放在快取裡面,還是重置一下。 26 changeNameVisiable: false, 27 changePositionVisiable: false, 28 jiesuanVisiable: false, 29 zongpanVisiable: false, 30 totalArr: [], //總盤裡面頭部總計陣列 31 singleTotalArr: [], //總盤裡面列表總計陣列 32 totalResultArr: [], //所有的data資料集合,放在快取裡面,重新整理之後讀取最後一條資料的showname,重新整理之後所有的資料來源都是從這裡來的,清存操作也主要是清理這個對應的快取 33 typeArr: ["bagang", "diangang", "angang", "dianpao", "zimo"], 34 zancunVisiable: false, //這裡設定一個暫存的標識,如果是暫存的話就顯示下一局,如果不是暫存的話就不能下一局產生追加資料 35 shuomingVisiable: false 36 },
第一個persons物件就是用來例項化4個使用者的,這裡先做4個,如果後面有其他追加使用者可以單獨寫一個方法,根據型別例項化不同角色的使用者,比如第5人,買馬第1人,那麼追加的使用者如何進行結算?這個在後面揭曉。定義Person類的程式碼如下:
1 initTime = 0; 2 function Person(name, show) { 3 initTime += 1; 4 this.name = name; //當前使用者名稱字 5 this.showname = function () { 6 // return ['上家', '左家', '右家', '下家'][initTime - 1]; 7 return ['老婆', '媽媽', '爸爸', '秦秦'][initTime - 1]; 8 }(); //當前使用者顯示的名字 9 this.gangshow = show; //顯示槓彈層 10 this.hushow = show; //顯示胡牌彈層 11 this.bagang = []; //巴槓 12 this.diangang = []; //點槓 13 this.dianpao = []; //點炮 14 this.angang = []; //暗槓 15 this.zimo = []; //自摸 16 this.over = false; //是否結束 17 this.total = 0; 18 }
頁面初始化的時候依然用到了讀取快取操作,但是這次的快取和之前不一樣,我是在點下一局的時候,儲存的是當然data物件,這邊方便在後面查詢當前局的所有資訊,程式碼如下:
1 mounted() { 2 this.totalResultArr = localStorage.getItem("totalResultArr") && JSON.parse(localStorage.getItem( 3 "totalResultArr")) || []; 4 let len = this.totalResultArr.length; 5 len && (() => { 6 let _data = this.totalResultArr[len - 1]; 7 this.persons = JSON.parse(JSON.stringify(_data["persons"])) 8 this.jiesuanVisiable = _data.jiesuanVisiable 9 this.orderArr = _data.orderArr 10 this.textArr = _data.textArr 11 this.restart() 12 })() 13 },
現在我們要開始說一下這次針對於槓和胡牌幾種型別操作的處理方式了,1.0版我採用的是1對多是陣列,1對1是單個字元變數,這樣在結算的時候還要根據不同型別去單獨處理,這次我都統一做成了陣列,一個物件有一個操作名稱,一個當前人的名稱,一個對方的名稱陣列,然後一個是型別,型別是關乎輸還是贏的型別,另外還有一個操作標誌符和預設番數,這樣就構成了當前操作的一個物件,寫到這裡,我突然發現我應該把這個操作抽象成一個類,後面就針對這個類進行例項化,這樣對於程式碼理解和維護就更優(PS下一版再優化),當前操作物件的定義如下:
1 let obj = { 2 type: -1,//輸贏的型別,1是贏,-1是輸, 3 name: person,//當前使用者標識 4 flag,//本次操作標識,在悔棋時有用 5 list: type == 1 ? [currentPerson.name] : [zhuangPerson.name],//本次操作物件的集合 6 typename,//本次操作型別名,由按鈕點選事件決定 7 fanshu: 0,//預設本次操作的番數為0,主要是後面用來處理自摸和胡牌手動改變番數 8 showList: type == 1 ? [currentPerson.showname][zhuangPerson.showname],//展示當前使用者的顯示名,區別於上面的name,寫到這裡突然想起換位之後,如果後面要做每局的回看,這裡也是對應要變的。 9 }
上面的操作是輸錢的程式碼,下面的程式碼是贏錢的物件,為什麼輸贏要單獨寫呢?因為比如說我自摸或者巴槓之類的,要去找到一對多的那個多是哪些人,我得先去迴圈使用者數組裡面沒有over的使用者組,但是1對1就不需要,最終我拿到這個多和1的時候,就可以書寫輸錢的物件程式碼,如下:
1 //這裡要做一個判斷,根據型別得到贏錢的那個人。 2 yingPerson = type == 1 ? currentPerson : zhuangPerson; 3 let obj = { 4 type: 1, 5 name: yingPerson.name, 6 flag, 7 list: toArr, 8 typename, 9 fanshu: 0 10 }
最後再把這次操作的說明插入下文字列表裡面,整體的程式碼如下:
1 hexinjisuan(index, typename, type) { 2 let currentIndex = index / 2; 3 this.order += 1; 4 let flag = this.order; //操作標識戳 5 let currentPerson = this.persons[`person${currentIndex}`]; //當前操作的人 6 let zhuangPerson = this.persons[`person${this.zhuangIndex}`]; 7 let toArr = []; //被槓的人列表,第二次迴圈的時候放入贏的人list陣列。 8 for (let person in this.persons) { 9 let toPerson = this.persons[person]; 10 if ((type == 1 && currentPerson.name != person) || (type == 2 && toPerson.name == 11 currentPerson.name)) { 12 //如果不是自己 13 //如果對方還沒胡牌 14 !toPerson.over && (() => { 15 let obj = { 16 type: -1,//輸贏的型別,1是贏,-1是輸 17 name: person,//當前使用者標識 18 flag,//本次操作標識,在悔棋時有用 19 list: type == 1 ? [currentPerson.name] : [zhuangPerson.name],//本次操作物件的集合 20 typename,//本次操作型別名,由按鈕點選事件決定 21 fanshu: 0,//預設本次操作的番數為0,主要是後面用來處理自摸和胡牌手動改變番數 22 showList: type == 1 ? [currentPerson.showname] : [zhuangPerson 23 .showname 24 ],//展示當前使用者的顯示名,區別於上面的name,寫到這裡突然想起換位之後,如果後面要做每局的回看,這裡也是對應要變的。 25 } 26 toPerson[typename].push(obj) 27 toArr.push(person) 28 this.textArr.push({ 29 flag, 30 type: -1, 31 fanshu: 0, 32 typename, 33 text: `${obj.showList}${this.getTxtObj[typename + 1]}了,${this.persons[obj.name].showname}輸錢` 34 }) 35 if (!this.orderArr.includes(flag)) { 36 this.orderArr.push(flag)//把當前操作標誌加入到順序數組裡面,便於後面進行悔棋操作,也許可以用順序號來直接計算倒敘計算,但是考慮到後面會有隨機刪除操作,還是用標識號陣列比較好。 37 } 38 })() 39 } 40 } 41 toArr.length && (() => { 42 //這裡要做一個判斷,根據型別得到贏錢的那個人。 43 yingPerson = type == 1 ? currentPerson : zhuangPerson; 44 let obj = { 45 type: 1, 46 name: yingPerson.name, 47 flag, 48 list: toArr, 49 typename, 50 fanshu: 0 51 } 52 yingPerson[typename].push(obj); 53 !this.chajiaoStatus && (typename == "dianpao" || typename == "zimo") && (() => { 54 yingPerson.over = true; 55 })() 56 this.textArr.push({ 57 flag, 58 type: 1, 59 fanshu: 0, 60 typename, 61 text: `${this.persons[obj.name].showname}${this.getTxtObj[typename + 1]}了,綜合上面${[obj.list.length]}條資料` 62 }) 63 })() 64 this.closeModel();//如果是胡牌或者點槓操作就要把彈出層關閉 65 this.finalJiesuan(false)//每次操作完進行一次實時結算 66 },
根據自摸或者胡牌手動操作番數的程式碼如下(PS突然發現這裡的if巢狀有點多,得想想可以怎麼優化下):
1 countFan: function (flag, type) { 2 for (let person in this.persons) {//這裡是根據當前操作的flag標識,找到對應使用者,然後把當前操作後的番數寫入到使用者操作物件裡面,便於結算。 3 for (let info in this.persons[person]) { 4 if (info == "zimo" || info == "dianpao") { 5 this.persons[person][info].forEach((item, index) => { 6 if (item.flag == flag) { 7 let fanshu = this.persons[person][info][index]["fanshu"] + type; 8 if (fanshu >= 0 && fanshu <= 3) { 9 this.persons[person][info][index]["fanshu"] = fanshu; 10 } 11 } 12 }) 13 } 14 } 15 } 16 this.textArr.forEach((item, index) => { 17 item.flag == flag && (() => { 18 let fanshu = this.textArr[index]["fanshu"] + type; 19 if (fanshu >= 0 && fanshu <= 3) { 20 this.textArr[index]["fanshu"] = fanshu;//根據flag修改textArr對應的文字列表 21 } 22 })() 23 }) 24 this.finalJiesuan(false)//最後還是要臨時結算一下 25 },
終於到了結算的程式碼,下面多了一個引數,因為這裡要區分一下是臨時結算還是本局完了的結算,以免出現下一局按鈕,在結算的時候加入了底的變數,雖然這期還沒做手動修改底錢,但是把這個變數先加上,後面修改就快多了,主要還是提倡莫打大了,2塊足矣。另外Math.pow是幾次冪的函式,總的程式碼如下:
1 finalJiesuan(isjiesuan) { 2 //isjiesuan是用來判斷是否是真正的最後打完了結算 3 this.jiesuanVisiable = isjiesuan; 4 for (let person in this.persons) { 5 this.persons[person].total = 0; 6 let total = 0; 7 for (let key in this.persons[person]) { 8 switch (key) { 9 case "diangang": 10 case "bagang": 11 case "angang": 12 this.persons[person][key].forEach((info) => { 13 total += this.single * info.list.length * info.type * (key == 14 "angang" || key == "diangang" ? 2 : 1) 15 }) 16 break; 17 case "dianpao": 18 this.persons[person][key].forEach((info) => { 19 total += Math.pow(this.single, info.fanshu + 1) * info.list.length * 20 info.type; 21 }) 22 break; 23 case "zimo": 24 this.persons[person][key].forEach((info) => { 25 let count = info.list.length * info.type; 26 total += Math.pow(this.single, info.fanshu + 1) * count + this 27 .single * count; 28 }) 29 break; 30 default: 31 break; 32 } 33 } 34 this.persons[person].total = total; 35 } 36 },
Hold on!突然發現說漏了一個比較重要的功能,就是查叫,就是當胡牌使用者(如果追加買馬之類的話可能就需要加一個在座型別)少於3個點選結算的時候,就是牌摸完了還有至少兩個人沒胡牌的情況,就要查叫了,這裡就涉及到1.0說的,如果有至少1家有叫沒胡牌,並且至少兩家沒叫的話,2家就要賠1家情況,這個時候的查叫其實也相當於胡牌,只是沒有自動over,可以一直查下去,這裡我就設定了一個公共變數chajiaoStatus,如果為true的話,在後面的一系列操作,就一路開綠燈,佈局程式碼如下:
1 jiesuan() { 2 let overCount = 0; 3 for (let person in this.persons) { 4 if (this.persons[person].over) { 5 overCount++; 6 } 7 } 8 if (overCount < 3) { 9 // alert(`只有${overCount}家胡牌`) 10 let _this = this; 11 //詢問框 12 layer.open({ 13 content: `只有${overCount}家胡牌,需要查叫嗎?`, 14 btn: ['需要', '不要'], 15 yes: function (index) { 16 _this.chajiaoStatus = true//在這裡把查叫狀態置為true 17 layer.close(index); 18 }, 19 no: function (index) { 20 _this.finalJiesuan(true) 21 } 22 }); 23 } else { 24 this.finalJiesuan(true) 25 } 26 },
當然還有一個悔棋操作,這裡的悔棋主要還是從orderArr去拿到flag,然後再到每個使用者裡面去找到對應操作的flag陣列物件進行刪除。程式碼如下:
1 huiqi() { 2 if (this.jiesuanVisiable) { 3 this.jiesuanVisiable = false; 4 return; 5 } 6 //悔棋首先要去找到orderArr裡面最新的flag,然後根據flag去找到對應使用者裡面的操作並去掉,涉及到結束彈窗的操作需要判斷dianpiao和zimo裡面的type是否等於1,如果等於1就需要找到對應person的over,置為false. 7 let flag = this.orderArr.pop(); 8 this.textArr = [...this.textArr.filter((item, index) => { 9 return item.flag != flag 10 })] 11 for (let person in this.persons) { 12 for (let name in this.persons[person]) { 13 if (Array.isArray(this.persons[person][name])) { //這裡判斷是否為陣列,如果是的話就是幾個型別之一 14 this.persons[person][name].forEach((item, index) => { 15 if (item.flag == flag) { 16 if ((name == "zimo" || name == "dianpao") && item.type == 1) { 17 this.persons[person].over = false 18 } 19 } 20 }) 21 this.persons[person][name] = [...this.persons[person][name].filter((item, 22 index) => { 23 return item.flag != flag 24 })] 25 } 26 } 27 } 28 this.finalJiesuan(false) 29 },
總的來說,整個2.0計算功能已經說完了,然後再說下增加的一個換位置功能,換位的話主要是座位不換,要把總盤資料totalResultArr裡面對應使用者的操作物件陣列的person進行交換,這裡我寫了一個方法,分別把要替換的使用者變成第另一個使用者加一個*號做標識,最後再統一去掉*號做這樣一個轉換,主要的實現程式碼如下:
1 changeNamePlus(index, toindex, type) { 2 let changeIndex = index; 3 let changeVal = 'person' + toindex + '*'; 4 this.totalResultArr.forEach((item, i) => { 5 let persons = item.persons; 6 for (let person in persons) { 7 this.typeArr.forEach((key) => { 8 let keyArr = persons[person][key]; 9 keyArr.forEach((info, j) => { 10 let out_name = info.name; 11 if (type == "*") { 12 if (out_name.indexOf("*") >= 0) { 13 this.totalResultArr[i]["persons"][person][key][ 14 j]["name"] = out_name.split("*")[0]; 15 } 16 } else { 17 //這裡是判斷外面的name是否和當前索引值一致,如果一致就替換成新的值。 18 if (out_name == `person${changeIndex}`) { 19 this.totalResultArr[i]["persons"][person][key][ 20 j]["name"] = changeVal; 21 } 22 } 23 info.list.forEach((in_name, k) => { 24 if (type == "*") { 25 if (in_name.indexOf("*") >= 0) { 26 this.totalResultArr[i]["persons"][ 27 person 28 ][key][j]["list"][k] = in_name 29 .split("*")[0]; 30 } 31 } else { 32 //這裡是判斷裡面list陣列迴圈的值是否和當前索引值一致,如果一致就替換成新的值。 33 if (in_name == `person${changeIndex}`) { 34 this.totalResultArr[i]["persons"][ 35 person 36 ][key][j]["list"][k] = changeVal; 37 } 38 } 39 }) 40 }) 41 }) 42 } 43 }) 44 },
然後其他的清存、下一局和總盤功能都和1.0版差不多,總的來說2.0版本的程式碼更抽象,邏輯看起來更順一些,我做了一個比較,html程式碼1.0版本寫了186行,2.0版本只寫了133行,js程式碼1.0版本寫了410行,2.0版本寫了442,總的程式碼1.0寫了596行,2.0寫了574行,但是2.0的版本從功能上面來說就多了查叫、換位這兩個大的功能,而且體驗應該更好,但是程式碼卻減少得比較多,當然一方面肯定是1.0寫得有點水,但是另一方面也說明我們還是應該不斷重構和優化程式碼,才能更好的提高程式設計能力和突破自己,我在想還有幾個點能抽象出來的,程式碼應該還能減少一點,而且後期維護起來也會更簡單一些,還是那句話,希望是寫更少的程式碼,得到更多的功能。
另外,連結後面再補上。