预览地址:http://hudong.miaos.me/majiang/new.html
上一篇我说到有做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写得有点水,但是另一方面也说明我们还是应该不断重构和优化代码,才能更好的提高编程能力和突破自己,我在想还有几个点能抽象出来的,代码应该还能减少一点,而且后期维护起来也会更简单一些,还是那句话,希望是写更少的代码,得到更多的功能。
另外,链接后面再补上。