
手写一个玛雅十字帕托利680 行纯前端代码从十字棋盘到 5 颗豆子的胜负手下载地址https://download.csdn.net/download/qq616491978/93137182文章目录手写一个玛雅十字帕托利680 行纯前端代码从十字棋盘到 5 颗豆子的胜负手一、我为什么要写这个二、先把规则理清楚2.1 棋盘长什么样2.2 核心要素2.3 特殊机制的作用三、代码怎么组织的四、数据结构怎么设计4.1 棋盘数组4.2 坐标表与常量4.3 坐标换算常量五、CSS / UI 怎么渲染5.1 用网格留白画十字5.2 视觉层级5.3 关键视觉元素5.4 移动端适配六、核心逻辑——走法引擎6.1 先定好状态变量6.2 豆子换算步数6.3 getLegalMoves一个函数装下经典规则6.4 吃子与安全格落子6.5 坐标换算怎么串起整盘七、渲染和交互怎么配合7.1 全量重绘7.2 点击流程7.3 交互细节八、怎么判胜负8.1 走子收尾触发胜利8.2 胜利遮罩九、规则修正与防呆处理十、几个值得记住的点十一、还可以做什么十二、关于这篇文章适用边界与已知局限一、我为什么要写这个前阵子翻到古玛雅的帕托利Patolli规则挺有意思一块十字棋盘双方各掷豆子决定走几步谁先把六枚棋子绕完一圈谁赢。可网上能直接玩的单机版少得可怜要么要联网要么塞了一堆看不懂的依赖。开发的棋牌游戏只是用来娱乐与开发学习请勿用于非法用途。我就想这不正好是个练手的纯前端小项目吗一个 HTML 文件就能装下全部逻辑不联网也能跑发给朋友双击就能对弈。于是花了一个周末把它写出来过程中最绕的不是规则而是怎么把那个「十字」画对、以及怎么把吃子、安全格、精确终点这一堆例外收敛进一个函数里。写完第一版我自己跑测试结果核对原始玩法时发现我最初把「己方可叠子」「2 枚敌子封锁格子」写进去了那其实是帕奇西印度十字棋的路子。经典帕托利里己方不能叠子、落点有对方子直接吃、根本不存在格子封锁。于是我把核心函数推倒重写成现在的样子——这部分在第九章讲。运行环境纯原生 HTML CSS Grid JavaScript零依赖、零框架、零 CDN浏览器直接打开【单机离线版】玛雅十字帕托利.html即可运行逻辑验证需要 Node.js Playwright本实现长期适用不依赖任何会过期的库版本。二、先把规则理清楚动手前得把帕托利的骨架吃透。它和西洋跳棋不同路径不是方形的而是一个十字闭环。2.1 棋盘长什么样我把 52 个格子按顺时针手描出来落在 15×15 的网格上让「厚 3 格的加号」外轮廓正好串成一圈顺时针方向从玩家1 起点索引 0顶臂左上角出发沿顶臂右沿下到右侧横臂、绕到底臂、再沿左臂回到起点附近。玩家2 起点在索引 26底臂正下角正好差半圈两人终点分别是索引 51标「终①」和索引 25标「终②」。2.2 核心要素要素说明玩家玩家1 玉石绿起点 0玩家2 黑曜石黑起点 26棋子每方 6 枚初始全在「待入场」区绕完一圈移出即完成豆子5 颗掷出后数圆点朝上的数量决定步数安全格8 个金色圆点不可被吃、对方也能共存终点必须正好走满 52 步越过多走一步该子本回合不能动2.3 特殊机制的作用安全格是这盘棋的拉锯点。普通格上只要落点有对方棋子且非安全格就会被吃对方退回待入场安全格上两色可以共存、谁也吃不了谁。于是进攻方常常卡在对方棋子前进退两难只能等下一次点数绕开。5 颗豆子全圆点时会走 5 步并额外再掷一次这一条让节奏忽然变快是翻盘的关键。经典帕托利里没有任何「格子封锁」机制——你能落的地方就能落吃子只发生在非安全格的单点对抗上。三、代码怎么组织的整个游戏就一个 HTML 文件分成三块玛雅十字帕托利.html ├── style → CSS约 196 行管十字棋盘与棋子视觉 ├── body HTML → 骨架约 55 行两侧信息面板 中央棋盘 掷骰区 └── script → 逻辑约 426 行这是重点 ├── 常量COORDS / SAFE / N ├── 全局状态board / currentPlayer / diceValue … ├── 坐标换算startOf / idxToProgress / progressToIdx ├── rollDice → 豆子换算步数 ├── getLegalMoves → 走法合法性不可叠子/吃子/精确终点 ├── movePiece / doMove → 执行与收尾 ├── render → 全量重绘 └── 点击交互onCellClick / onHomeChip / onFinishChip最核心的一条思路棋盘状态只存一个board数组每回合render()一把全量重绘逻辑层和视图层不纠缠。四、数据结构怎么设计4.1 棋盘数组letboard;// 长度 52 的数组board[i] { p1:数量, p2:数量 }letp1Home,p1Score;// 玩家1待入场数 / 已完成数letp2Home,p2Score;letcurrentPlayer;// 1 或 2letdiceValue;// 当前骰子换算出的步数null 表示本回合尚未掷骰每格只记两色各有多少枚棋子而不是给每枚棋子单独建对象。经典规则下同格里同色最多 1 枚所以p1/p2的值通常是 0 或 1吃子、移出都只是对这两个数做加减简单到不用想。渲染层仍保留 1时显示数字的兼容写法只是正常对局不会再触发。4.2 坐标表与常量constCOORDS[[0,6],[0,7],[0,8],// 0,1,2 顶臂上沿[1,8],[2,8],[3,8],[4,8],[5,8],// 3-7 顶臂右沿[6,9],[6,10],[6,11],[6,12],[6,13],[6,14],// 8-13 右臂上沿[7,14],[8,14],// 14,15 右臂右沿[8,13],[8,12],[8,11],[8,10],[8,9],// 16-20 右臂下沿[9,8],[10,8],[11,8],[12,8],[13,8],[14,8],// 21-26 底臂右沿26玩家2起点[14,7],[14,6],// 27,28 底臂下沿[13,6],[12,6],[11,6],[10,6],[9,6],// 29-33 底臂左沿[8,5],[8,4],[8,3],[8,2],[8,1],[8,0],// 34-39 左臂下沿[7,0],[6,0],// 40,41 左臂左沿[6,1],[6,2],[6,3],[6,4],[6,5],// 42-46 左臂上沿[5,6],[4,6],[3,6],[2,6],[1,6]// 47-51 顶臂左沿回到起点 0];constN52;constSAFEnewSet([1,14,27,40,8,21,34,47]);COORDS是我照着 15×15 网格一个一个格子描出来的顺时针序列。它把「棋盘长相」和「逻辑索引」绑死逻辑只认 0…51 的顺序渲染时再查COORDS[i]拿到[行,列]交给 CSS Grid。安全格取「四臂端点」(1,14,27,40) 加「紧邻中心四格」(8,21,34,47)共 8 格。4.3 坐标换算常量functionstartOf(p){returnp1?0:26;}functionidxToProgress(p,idx){constsstartOf(p);return(idx-sN)%N;// 归一化到 0..51}functionprogressToIdx(p,prog){constsstartOf(p);return(sprog)%N;// 把进度还原成棋盘索引}能提前算好的就不在运行时反复推导。玩家起点差半圈所以「自起点走了几步」用取模一归一化就通用了。五、CSS / UI 怎么渲染5.1 用网格留白画十字.board{width:100%;height:100%;display:grid;grid-template-columns:repeat(15,1fr);grid-template-rows:repeat(15,1fr);gap:2px;}.cell{position:relative;display:flex;align-items:center;justify-content:center;}关键不在「画路径」而在「只给 52 个路径格生成可见的圆槽其余 15×15 网格位置留空」。CSS Grid 把 225 个格子位排好我只在COORDS对应的 52 个位置放.cell其余位置根本没元素——十字形状就自己浮现出来了不用画一根线。5.2 视觉层级.cell.target .slot合法落点绿色脉动光晕.cell.sel .slot已选中的来源金色描边.cell.safe .slot安全格金色圆点角标.piece.p1/.piece.p2玉石绿与黑曜石黑两种棋子5.3 关键视觉元素.piece.p1{background:radial-gradient(circle at 32% 30%,#6ee7b7,var(--p1)70%);}.piece.p2{background:radial-gradient(circle at 32% 30%,#475569,var(--p2)80%);color:var(--p2-edge);border:1px solidvar(--p2-edge);}棋子不用贴图一个径向渐变加高光就出陶瓷质感。同格多子时margin-left:-30%让它们轻微重叠一眼能数出几枚。5.4 移动端适配media(max-width:760px){.panel{width:46%;flex:1 1 140px;}.game{gap:10px;}}两侧信息面板在窄屏改成各占 46%棋盘用min(92vw,540px)自适应手机上不会挤。六、核心逻辑——走法引擎这部分是全篇最花功夫的也是后来被我推翻重写的地方。6.1 先定好状态变量letdiceValue;// 当前步数letbeanDots;// 5 颗豆子的圆点布尔数组letbusy;// 防呆动画/计算中禁用点击letfinished;// 是否分出胜负letselectedSource;// 当前选中的来源home 或 棋盘索引null未选letextraTurn;// 是否处于「全圆点」带来的额外回合letmoves;// source - [targetIndex | done, ...]letlegalSourceSet;// 所有可作为来源的位置集合letlegalTargetSet;// 所有合法落点集合用于初始提示光晕6.2 豆子换算步数beans.forEach(b{constdMath.random()0.5;// 圆点朝上概率约 50%dots.push(d);if(d)cnt;b.classList.remove(rolling);b.classList.toggle(dot,d);});beanDotsdots;if(cnt5){diceValue5;extraTurntrue;}else{diceValue(cnt0?10:cnt);extraTurnfalse;}5 颗豆子各自抛一次圆点朝上记 1。全平0 点走 10 步全圆点5 点走 5 步并标记额外回合中间几颗就是对应步数。换算完立刻computeMoves()算出本回合所有合法走法。6.3 getLegalMoves一个函数装下经典规则functiongetLegalMoves(from){constpcurrentPlayer;constoppp1?2:1;constownCount(idx)board[idx][pp];// 目标格己方棋子数constoppCount(idx)board[idx][popp];// 目标格对方棋子数// —— 情况 A从待入场区放一枚新子到起点 ——if(fromhome){constsstartOf(p);if(ownCount(s)1)return[];// 经典规则己方不可叠子起点已有己方棋子则无法入场return[s];}// —— 情况 B移动场上已有棋子 ——constifrom;constprogidxToProgress(p,i);// 当前已走步数consttargetProgprogdiceValue;// 前进后的步数if(targetProgN)return[];// 必须正好越过终点多走则非法if(targetProgN)return[done];// 正好走满 → 移出棋盘完成constjprogressToIdx(p,targetProg);// 落点棋盘索引// 经典帕托利己方棋子不可叠放不能落在己方已占的格安全格同理if(ownCount(j)1)return[];// 对方棋子无论几枚不形成「封锁」非安全格落在对方子上即吃子安全格双方共存。均合法。return[j];}这个函数把规则收得极干净入场先看起点有没有己方棋子有就不可叠子入场移动先看是否越界精确终点再判落点是否已有己方棋子安全格用共存豁免攻防平衡靠「己方不叠子 落点吃子」两条铁律而不是什么封锁。6.4 吃子与安全格落子functionmovePiece(from,to){constpcurrentPlayer;constoppp1?2:1;if(fromhome){// 从待入场区放子到起点经典己方不叠子直接置 1constsstartOf(p);if(board[s][popp]1!SAFE.has(s)){// 起点有对方棋子非安全格→ 入场即吃子board[s][popp]0;if(opp1)p1Home;elsep2Home;}board[s][pp]1;if(p1)p1Home--;elsep2Home--;return;}constifrom;board[i][pp]0;// 离开原格原必为 1if(todone){// 越过终点移出棋盘if(p1)p1Score;elsep2Score;return;}constjto;// —— 经典吃子落点有对方棋子且非安全格 → 吃子对方退回待入场区 ——if(board[j][popp]1!SAFE.has(j)){board[j][popp]0;if(opp1)p1Home;elsep2Home;}// 落入目标格安全格上可与对方共存不被吃非安全格则上方已清掉对方board[j][pp]1;}吃子判定只认「落点有对方棋子且非安全格」。安全格上即便有对方棋子也照常落入、双方共存——这一处和 6.3 的共存豁免呼应是安全格机制的两个锚点。己方不叠子则由board[j][pp] 1强制保证离开原格时置 0整盘不会出现同格同色两枚。6.5 坐标换算怎么串起整盘玩家点一个棋子逻辑拿它的索引iidxToProgress算出已走步数加上diceValue得到目标步数progressToIdx再把它变回棋盘索引。整盘移动、入场、终点判定都复用这套换算不写第二套坐标逻辑。七、渲染和交互怎么配合7.1 全量重绘for(leti0;iN;i){constcellcellEls[i];cell.classList.remove(target,source,sel);constcboard[i];constslotcell.querySelector(.slot);lethtml;if(c.p10)htmldiv classpiece p1${c.p11?c.p1:}/div;if(c.p20)htmldiv classpiece p2${c.p21?c.p2:}/div;slot.innerHTMLhtml;}每回合把 52 个槽位清空重画。状态简单时这比增量更新省心——不会出现「点击没反应、高亮卡住」这类脏状态。52 格全量重绘在浏览器里感觉不到延迟。7.2 点击流程掷豆(rollDice) └─ computeMoves() 算出 legalSourceSet / legalTargetSet 选择来源(onCellClick / onHomeChip) └─ selectedSource 该格 选择落点(onCellClick / onFinishChip) └─ doMove(from, to) → movePiece → endOfRoll → switchTurn7.3 交互细节functiononHomeChip(p){if(busy||finished||diceValuenull)return;if(p!currentPlayer)return;// 仅当前玩家的入场 chip 可响应if(!legalSourceSet.has(home))return;selectedSource(selectedSourcehome)?null:home;render();}入场 chip 和完成 chip 都带玩家编号点击时先校验p ! currentPlayer直接忽略——这是后面测试抓出来的防呆点。八、怎么判胜负8.1 走子收尾触发胜利functiondoMove(from,to){movePiece(from,to);selectedSourcenull;diceValuenull;beanDotsnull;if(p1Score6||p2Score6){finishedtrue;render();showWin();return;}endOfRoll();}每枚棋子移出棋盘p1Score/p2Score加一任一方到 6立刻finished true并弹遮罩不再切换回合。8.2 胜利遮罩functionshowWin(){constwp1Score6?1:2;document.getElementById(winText).textContent玩家${w}${w1?玉石绿:黑曜石黑}获胜;document.getElementById(overlay).classList.add(show);}九、规则修正与防呆处理这一段是写测试逼出来的也包含了把第一版改成经典帕托利的过程。第一最初的getLegalMoves我写的是「己方可叠子、2 枚以上敌子封锁格子」。核对原始玩法后才意识到那是帕奇西的路子。经典帕托利没有格子封锁于是改成现在这样己方不可叠子、落点对方子直接吃、无封锁。改动只在三四行但语义差别很大。重写后computeMoves会把起点已有己方棋子的入场判为非法把「落点己方已占」判为非法其余一律可落。第二onHomeChip/onFinishChip原来没校验点击者是不是当前玩家。于是玩家2 的回合点玩家1 的入场 chip 也能放子回合乱套。加上if (p ! currentPlayer) return;才修好。第三rollDice里这层守卫很关键functionrollDice(){if(busy||finished||diceValue!null)return;// 防呆必须先用完当前点数...}diceValue ! null保证「上一步没走完就不能再掷」否则步数会被覆盖、合法走法错乱。写完用 Playwright 接真实浏览器跑回归覆盖核心规则、精确终点、安全格共存、吃子、己方不可叠子、入场即吃等场景最终32 项断言全部通过输出ALL TESTS PASS。验证命令nodetest/verify.js# 预期 验证结果通过 32失败 0 / ALL TESTS PASS十、几个值得记住的点棋盘形状靠「网格留白」而不是「画线」状态少、视觉自然。把不可叠子/吃子/安全格收进getLegalMoves一个函数规则改动只动一处。坐标换算用取模归一化两个玩家共用一套逻辑。全量重绘比增量更新省心小棋盘根本感觉不到开销。防呆不是可选项交互入口一定要校验当前玩家和回合状态。十一、还可以做什么当前版本已经能双人对弈、吃子、安全格共存、精确终点、额外回合。想更有意思有几个方向加一个简单 AI。把legalSourceSet和moves直接喂给一个评分函数优先吃子、抢安全格、逼近终点不用改渲染层。记走子历史。每步把board快照压栈加「悔棋」按钮误操作能退。音效与动画增强。豆子翻滚、吃子闪红、胜利撒花纯 CSS/WebAudio 都能做不引外部资源。改棋盘主题。把COORDS和SAFE抽成配置就能快速换别的十字类棋型。十二、关于这篇文章完整的 HTML 源码随文附上保存成.html文件双击就能在浏览器里跑。代码总共约 680 行函数和关键逻辑都加了中文注释。下载地址https://download.csdn.net/download/qq616491978/93137182适用边界与已知局限本实现仅覆盖代码里写出来那套规则子集52 格闭环、5 豆步数映射、8 安全格、精确终点、己方不可叠子、无封锁、落点吃子。更繁复的历史变体如不同安全格布局、赌注规则未实现。纯前端单文件状态全部在内存刷新页面即重置没有存档。仅双人对战尚无 AI棋盘用 CSS Grid 绘制浏览器需支持 Grid现代浏览器均支持。测试基于 Node.js Playwright 驱动真实页面逻辑已回归验证但随机掷豆带来的极端运气局不在自动断言范围内。