手写一个三仙(三公):857行纯前端代码,从牌型识别到四级梯次比对全流程解析

手写一个三仙(三公):857行纯前端代码,从牌型识别到四级梯次比对全流程解析
手写一个三仙三公857行纯前端代码从牌型识别到四级梯次比对全流程解析下载链接https://download.csdn.net/download/qq616491978/93151234一、我为什么要写这个写完了斗兽棋和锄大地之后朋友问我能不能写个扑克牌比大小的游戏。我说锄大地不就是扑克牌吗——他说不一样锄大地太复杂每次要想出什么牌型他想要一个更简单的就是每人发三张牌翻开比谁大的那种。三仙三公广东这边也叫三公每人三张牌J/Q/K叫公牌比牌型、比点数。规则看起来就几句话的事但真正落到代码上有几个地方比我预想的要绕——J/Q/K该算几点、三张K既是三公又是三条该归到哪一类、平局时庄家通吃这条规则怎么塞进比较函数里不出bug。这些细节拆完刚好凑成一篇不长不短的文章。代码全在单个HTML文件里CSSJS加起来857行没引任何框架。运行环境纯原生 HTML CSS JavaScript零依赖浏览器双击即开逻辑测试用 Node.js无需浏览器跑node test/verify.js。本实现基于原生 JS不依赖任何框架或 CDN原理长期适用。文章目录手写一个三仙三公857行纯前端代码从牌型识别到四级梯次比对全流程解析一、我为什么要写这个二、规则先理一遍2.1 牌面与点数2.2 五种牌型从高到低2.3 比大小的优先级三、代码怎么组织的四、数据结构怎么设计4.1 一张牌就是个普通对象4.2 牌型等级表4.3 下注与状态参数五、CSS / UI 怎么画扑克5.1 毛玻璃面板5.2 翻牌靠显示切换不靠重绘5.3 视觉层级5.4 移动端适配六、核心逻辑——RulesValidator6.1 先算点数6.2 evaluateHand——牌型判定的顺序就是答案6.3 compare——四级梯次比对6.4 大牌 1 赔 2七、渲染和交互怎么配合7.1 状态机四个阶段7.2 下注与发牌流程7.3 亮牌结算的关键几行八、怎么判胜负8.1 单局胜负8.2 整局结束九、没有键盘快捷键十、几个值得记住的点十一、适用边界与已知局限十二、还可以做什么十三、关于这篇文章二、规则先理一遍动手前得把规则吃透不然比较函数会越写越乱。这里只讲代码里实际实现的子集。2.1 牌面与点数一副 52 张牌去掉大小王。J、Q、K 是公牌点数算 0A 算 12~10 就是牌面数字。最终点数是三张牌点数之和取个位——比如 78924计 4 点三张 K 是 000计 0 点。点数规则 J / Q / K → 0 点公牌 A → 1 点 2 ~ 10 → 牌面数字 最终点数 三张牌点数之和 取个位2.2 五种牌型从高到低牌型构成等级三公三张都是 J/Q/K5三条三张点数相同非公牌4双公两张公牌 一张普通3单公一张公牌2无公零张公牌1三公和三条都算大牌赢了按1 赔 2结算。这里藏着一个坑三张 K 既是三公又是三条代码里必须让三公先拦截否则会错判成三条。2.3 比大小的优先级比牌不是单看点数。先比牌型等级同等级才比点数点数也相同再比最大那张单牌KQJ…2A再比花色 黑桃红桃梅花方块。全一样就是平局平局庄家通吃。三、代码怎么组织的一个 HTML 文件分成三块【单机离线版】三仙三公.html ├── style → CSS约 318 行管毛玻璃面板与扑克翻面动画 ├── body HTML → 骨架约 60 行庄家区/下注区/玩家区三块面板 └── script → 逻辑约 470 行这是重点 ├── 常量SUITS / RANK_MAP / HAND_TYPES / 下注参数 ├── DeckManager → 建牌库、洗牌、发牌 ├── RulesValidator → 核心算法算点数、判牌型、比大小 ├── UIRenderer → 牌面 DOM、翻牌动画、高亮、筹码 └── GameController → 状态机下注→发牌→亮牌→结算最核心的思路其实就一条把比大小收敛成一个纯函数compare()所有规则都装在里面UI 只负责调它。四、数据结构怎么设计4.1 一张牌就是个普通对象deck.push({rank,// 1~13suit:suit.key,// clubs / diamonds / hearts / spadessuitSymbol:suit.symbol,// ♣ ♦ ♥ ♠suitOrder:suit.order,// 0~3用于花色比大小suitName:suit.name,color:suit.color,// black / redlabel:RANK_MAP[rank].label,points:RANK_MAP[rank].points,isGong:rank11,// J/Q/K 标记为公牌id:${rank}_${suit.key},});建牌时就把points、isGong、suitOrder一起算好存进去。后面判牌型和比花色时直接读不重复计算。4.2 牌型等级表constHAND_TYPES{SAN_GONG:{name:三公,level:5},SAN_TIAO:{name:三条,level:4},SHUANG_GONG:{name:双公,level:3},DAN_GONG:{name:单公,level:2},WU_GONG:{name:无公,level:1},};能提前算好的就不要在运行时算。等级用数字存比较时直接比大小不用写一堆 if。4.3 下注与状态参数constMIN_BET10;constMAX_BET200;constINITIAL_CHIPS1000;五、CSS / UI 怎么画扑克5.1 毛玻璃面板.panel{background:var(--glass);border:1px solidvar(--glass-border);border-radius:18px;backdrop-filter:blur(10px);}整块牌桌用一张径向渐变的绿绒底色面板半透明加模糊出来的质感接近真实赌桌又不用引任何图片。5.2 翻牌靠显示切换不靠重绘.card.flipped .card-face{display:none;}.card.flipped .card-back{display:flex;}.card:not(.flipped) .card-back{display:none;}.card:not(.flipped) .card-face{display:flex;}牌的正面和背面都在 DOM 里翻牌只是切flipped类。这样不丢状态动画也顺。5.3 视觉层级.badge庄家/玩家标签黑底金字。.chip-btn圆形筹码按钮红金渐变All-in 用金底。.hand-type-label亮牌后显示牌型赢了金色、输了灰色。.winner-glow获胜方手牌加金色描边光晕。5.4 移动端适配media(max-width:400px){.card{width:52px;height:76px;}.chip-btn{width:38px;height:38px;font-size:11px;}}小屏自动缩牌和筹码单手能玩。六、核心逻辑——RulesValidator这部分是全文最花功夫的地方也是三公和三条那个坑的所在。6.1 先算点数calculatePoints(cards){constsumcards.reduce((s,c)sc.points,0);returnsum%10;},一句话求和取个位。J/Q/K 的 0 点在这里自然生效。6.2 evaluateHand——牌型判定的顺序就是答案evaluateHand(cards){constgongCountcards.filter(cc.isGong).length;constpointsthis.calculatePoints(cards);constmaxthis.maxCard(cards);constrankscards.map(cc.rank);// 三公三张都是J/Q/Kif(gongCount3){return{type:SAN_GONG,typeName:三公,level:5,points,maxCard:max};}// 三条三张点数相同注意三张J/Q/K已在上方被三公拦截这里不可能再是公牌三条if(ranks[0]ranks[1]ranks[1]ranks[2]){return{type:SAN_TIAO,typeName:三条,level:4,points,maxCard:max};}// 双公if(gongCount2){return{type:SHUANG_GONG,typeName:双公,level:3,points,maxCard:max};}// 单公if(gongCount1){return{type:DAN_GONG,typeName:单公,level:2,points,maxCard:max};}// 无公return{type:WU_GONG,typeName:无公,level:1,points,maxCard:max};},关键点就在第一行if (gongCount 3)。三张 K 的gongCount是 3在三条判断之前就被三公拦走了。如果把这个判断放到三条后面三张 K 会先满足ranks全相等错判成三条——这正是我第一次写时踩的坑。6.3 compare——四级梯次比对compare(playerHand,dealerHand){constpthis.evaluateHand(playerHand);constdthis.evaluateHand(dealerHand);// 比牌型等级if(p.leveld.level)return1;if(d.levelp.level)return-1;// 同牌型比点数if(p.pointsd.points)return1;if(d.pointsp.points)return-1;// 点数相同比最大单张constpValthis.singleCardValue(p.maxCard);constdValthis.singleCardValue(d.maxCard);if(pValdVal)return1;if(dValpVal)return-1;// 完全平局 → 庄家通吃return-1;},四级梯次清清楚楚牌型 → 点数 → 最大单张 → 平局庄家通吃。返回1玩家赢、-1庄家赢。最后一行就是庄家通吃的落点不用在前面每个分支都判断平局。6.4 大牌 1 赔 2isBonusHand(hand){consththis.evaluateHand(hand);returnh.typeSAN_GONG||h.typeSAN_TIAO;},赢了之后revealPhase里用它决定倍数是 2 还是 1。七、渲染和交互怎么配合7.1 状态机四个阶段state:{phase:betting,// betting | dealing | reveal | settleplayerChips:INITIAL_CHIPS,currentBet:0,playerHand:[],dealerHand:[],},下注 → 发牌 → 亮牌 → 结算四个阶段用phase串起来。每个阶段只做自己该做的事互不越界。7.2 下注与发牌流程点筹码() → currentBet 累加夹紧到 10~200 点「发牌」→ 扣筹码、双方各发三张玩家正面、庄家背面 动画结束 → 翻庄家牌 → compare 比大小 → 高亮赢家、结算筹码 → 自动进入下一局 / 筹码耗尽弹结束遮罩7.3 亮牌结算的关键几行constmultiplierRulesValidator.isBonusHand(s.playerHand)?2:1;winAmounts.currentBet*multiplier;s.playerChipss.currentBetwinAmount;赢了不仅拿回本金还按倍数拿彩金。平局或输了本金归庄家。八、怎么判胜负8.1 单局胜负单局胜负完全由RulesValidator.compare()决定逻辑见第六章。赢家手牌加winner-glow光晕顶部显示牌型与点数。8.2 整局结束if(s.playerChipsMIN_BET){UIRenderer.showOverlay(游戏结束,你已输光所有筹码...,再来一局,...);}筹码不够最低下注 10 就弹结束遮罩否则自动开下一局。没有赢够多少就停的设定纯靠筹码耗尽判负。九、没有键盘快捷键这个游戏全程靠鼠标/触屏点按钮没有键盘操作。筹码加减、发牌、清空、重开都是按钮事件符合手机端单手操作的习惯。十、几个值得记住的点牌型判定靠顺序解决歧义三公必须先于三条判断三张 K 才不会错判。比大小用四级梯次收口平局只在最后统一处理分支不重复。把规则算出来的字段点数、是否公牌、花色序建牌时就存好运行时只读取。翻牌用 CSS 切类不重绘状态不丢、动画顺。十一、适用边界与已知局限单文件离线实现规则只覆盖了代码里实际写到的子集未实现的玩法如摆三公免佣等变体不在内。庄家是随机发牌的电脑没有 AI 策略也不会根据玩家下注调整——它只是公平发牌后比大小。比较只在同牌型比点数、同点数比最大单张这一层未实现某些地方玩法里的对子优先特定组合翻倍等额外规则。浏览器兼容性依赖backdrop-filter毛玻璃极旧内核如 IE无模糊效果但不影响玩法其他均为标准 CSS/JS。筹码、下注均为前端内存状态刷新页面即重置没有持久化或联网对战。十二、还可以做什么当前版本已经能下注、发牌、比大小、自动结算。如果想让项目更有意思有几个方向加一个真·AI 庄家。描述现在庄家纯随机可以改成读牌后按胜率调整跟注策略。多回合积分榜。描述把每局输赢记进本地存储凑一个今日战绩。音效与震动。描述翻牌加轻音效手机端赢了震一下氛围会好很多。十三、关于这篇文章完整的 HTML 源码随文附上保存成.html文件双击就能在浏览器里跑。代码总共约 857 行函数和关键逻辑都加了中文注释。下载链接https://download.csdn.net/download/qq616491978/93151234