news 2026/7/19 14:35:50

纯HTML单文件实现玛雅十字帕托利 —— 用52格闭环与5颗豆子还原古玛雅双人棋

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
纯HTML单文件实现玛雅十字帕托利 —— 用52格闭环与5颗豆子还原古玛雅双人棋

手写一个玛雅十字帕托利: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)];constN=52;constSAFE=newSet([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){returnp===1?0:26;}functionidxToProgress(p,idx){consts=startOf(p);return(idx-s+N)%N;// 归一化到 0..51}functionprogressToIdx(p,prog){consts=startOf(p);return(s+prog)%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=>{constd=Math.random()<0.5;// 圆点朝上概率约 50%dots.push(d);if(d)cnt++;b.classList.remove('rolling');b.classList.toggle('dot',d);});beanDots=dots;if(cnt===5){diceValue=5;extraTurn=true;}else{diceValue=(cnt===0?10:cnt);extraTurn=false;}

5 颗豆子各自抛一次,圆点朝上记 1。全平(0 点)走 10 步,全圆点(5 点)走 5 步并标记额外回合,中间几颗就是对应步数。换算完立刻computeMoves()算出本回合所有合法走法。

6.3 getLegalMoves:一个函数装下经典规则

functiongetLegalMoves(from){constp=currentPlayer;constopp=p===1?2:1;constownCount=(idx)=>board[idx]['p'+p];// 目标格己方棋子数constoppCount=(idx)=>board[idx]['p'+opp];// 目标格对方棋子数// —— 情况 A:从待入场区放一枚新子到起点 ——if(from==='home'){consts=startOf(p);if(ownCount(s)>=1)return[];// 经典规则:己方不可叠子,起点已有己方棋子则无法入场return[s];}// —— 情况 B:移动场上已有棋子 ——consti=from;constprog=idxToProgress(p,i);// 当前已走步数consttargetProg=prog+diceValue;// 前进后的步数if(targetProg>N)return[];// 必须正好越过终点,多走则非法if(targetProg===N)return['done'];// 正好走满 → 移出棋盘完成constj=progressToIdx(p,targetProg);// 落点棋盘索引// 经典帕托利:己方棋子不可叠放,不能落在己方已占的格(安全格同理)if(ownCount(j)>=1)return[];// 对方棋子(无论几枚)不形成「封锁」:非安全格落在对方子上即吃子;安全格双方共存。均合法。return[j];}

这个函数把规则收得极干净:入场先看起点有没有己方棋子(有就不可叠子入场);移动先看是否越界(精确终点);再判落点是否已有己方棋子;安全格用共存豁免,攻防平衡靠「己方不叠子 + 落点吃子」两条铁律,而不是什么封锁。

6.4 吃子与安全格落子

functionmovePiece(from,to){constp=currentPlayer;constopp=p===1?2:1;if(from==='home'){// 从待入场区放子到起点(经典:己方不叠子,直接置 1)consts=startOf(p);if(board[s]['p'+opp]>=1&&!SAFE.has(s)){// 起点有对方棋子(非安全格)→ 入场即吃子board[s]['p'+opp]=0;if(opp===1)p1Home++;elsep2Home++;}board[s]['p'+p]=1;if(p===1)p1Home--;elsep2Home--;return;}consti=from;board[i]['p'+p]=0;// 离开原格(原必为 1)if(to==='done'){// 越过终点,移出棋盘if(p===1)p1Score++;elsep2Score++;return;}constj=to;// —— 经典吃子:落点有对方棋子且非安全格 → 吃子,对方退回待入场区 ——if(board[j]['p'+opp]>=1&&!SAFE.has(j)){board[j]['p'+opp]=0;if(opp===1)p1Home++;elsep2Home++;}// 落入目标格;安全格上可与对方共存(不被吃),非安全格则上方已清掉对方board[j]['p'+p]=1;}

吃子判定只认「落点有对方棋子且非安全格」。安全格上即便有对方棋子也照常落入、双方共存——这一处和 6.3 的共存豁免呼应,是安全格机制的两个锚点。己方不叠子则由board[j]['p'+p] = 1强制保证,离开原格时置 0,整盘不会出现同格同色两枚。

6.5 坐标换算怎么串起整盘

玩家点一个棋子,逻辑拿它的索引iidxToProgress算出已走步数;加上diceValue得到目标步数;progressToIdx再把它变回棋盘索引。整盘移动、入场、终点判定都复用这套换算,不写第二套坐标逻辑。


七、渲染和交互怎么配合

7.1 全量重绘

for(leti=0;i<N;i++){constcell=cellEls[i];cell.classList.remove('target','source','sel');constc=board[i];constslot=cell.querySelector('.slot');lethtml='';if(c.p1>0)html+=`<div class="piece p1">${c.p1>1?c.p1:''}</div>`;if(c.p2>0)html+=`<div class="piece p2">${c.p2>1?c.p2:''}</div>`;slot.innerHTML=html;}

每回合把 52 个槽位清空重画。状态简单时这比增量更新省心——不会出现「点击没反应、高亮卡住」这类脏状态。52 格全量重绘在浏览器里感觉不到延迟。

7.2 点击流程

掷豆(rollDice) └─> computeMoves() 算出 legalSourceSet / legalTargetSet 选择来源(onCellClick / onHomeChip) └─> selectedSource = 该格 选择落点(onCellClick / onFinishChip) └─> doMove(from, to) → movePiece → endOfRoll → switchTurn

7.3 交互细节

functiononHomeChip(p){if(busy||finished||diceValue===null)return;if(p!==currentPlayer)return;// 仅当前玩家的入场 chip 可响应if(!legalSourceSet.has('home'))return;selectedSource=(selectedSource==='home')?null:'home';render();}

入场 chip 和完成 chip 都带玩家编号,点击时先校验p !== currentPlayer直接忽略——这是后面测试抓出来的防呆点。


八、怎么判胜负

8.1 走子收尾触发胜利

functiondoMove(from,to){movePiece(from,to);selectedSource=null;diceValue=null;beanDots=null;if(p1Score>=6||p2Score>=6){finished=true;render();showWin();return;}endOfRoll();}

每枚棋子移出棋盘,p1Score/p2Score加一;任一方到 6,立刻finished = true并弹遮罩,不再切换回合。

8.2 胜利遮罩

functionshowWin(){constw=p1Score>=6?1:2;document.getElementById('winText').textContent=`玩家${w}${w===1?'玉石绿':'黑曜石黑'})获胜!`;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。把legalSourceSetmoves直接喂给一个评分函数,优先吃子、抢安全格、逼近终点,不用改渲染层。

记走子历史。每步把board快照压栈,加「悔棋」按钮,误操作能退。

音效与动画增强。豆子翻滚、吃子闪红、胜利撒花,纯 CSS/WebAudio 都能做,不引外部资源。

改棋盘主题。把COORDSSAFE抽成配置,就能快速换别的十字类棋型。


十二、关于这篇文章

完整的 HTML 源码随文附上,保存成.html文件双击就能在浏览器里跑。代码总共约 680 行,函数和关键逻辑都加了中文注释。

下载地址:https://download.csdn.net/download/qq616491978/93137182

适用边界与已知局限

  • 本实现仅覆盖代码里写出来那套规则子集:52 格闭环、5 豆步数映射、8 安全格、精确终点、己方不可叠子、无封锁、落点吃子。更繁复的历史变体(如不同安全格布局、赌注规则)未实现。
  • 纯前端单文件,状态全部在内存;刷新页面即重置,没有存档。
  • 仅双人对战,尚无 AI;棋盘用 CSS Grid 绘制,浏览器需支持 Grid(现代浏览器均支持)。
  • 测试基于 Node.js + Playwright 驱动真实页面,逻辑已回归验证;但随机掷豆带来的极端运气局不在自动断言范围内。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/19 14:35:42

2026年聚四氟乙烯厂家排行榜前十名品牌有哪些?

核心导读&#xff1a; 国内聚四氟乙烯&#xff08;PTFE&#xff09;品牌排名需区分原材料生产商与制品加工商两大维度。本文综合全球产能数据与中国氟硅协会调研结果&#xff0c;按序列号盘点国内十大代表性PTFE品牌&#xff0c;涵盖产能规模、技术实力与核心产品&#xff0c;帮…

作者头像 李华
网站建设 2026/7/19 14:35:23

nRF52芯片DFU升级方案与优化实践

1. nRF52 DFU升级方案概述nRF52系列芯片作为Nordic Semiconductor推出的低功耗蓝牙解决方案&#xff0c;其设备固件升级(DFU)功能是产品开发中的关键环节。DFU over USB和DFU over UART是两种最常用的无线升级方式&#xff0c;开发者需要根据具体应用场景选择适合的传输通道。我…

作者头像 李华
网站建设 2026/7/19 14:35:17

ESP32-S3音乐播放器开发实战:WAV解码与I2S音频接口应用

1. ESP32-S3音乐播放器项目概述在物联网和嵌入式音频处理领域&#xff0c;ESP32-S3凭借其强大的双核处理能力和丰富的外设接口&#xff0c;成为开发智能音频设备的理想选择。本实验基于正点原子DNESP32S3开发板&#xff0c;通过板载的ES8388音频编解码芯片实现了一个完整的WAV格…

作者头像 李华
网站建设 2026/7/19 14:35:09

AI时代的程序员修养:测试是 AI 编程的安全带

《AI时代的程序员修养》从这一篇进入第三卷&#xff1a;让 AI 生成物进入工程体系。前面讲了程序、数据结构、接口、数据库、高并发&#xff0c;这些都是“怎么设计”。但设计再好&#xff0c;最后还是要落到代码。AI 写代码很快&#xff0c;快到人很容易放松警惕。 测试在 AI…

作者头像 李华
网站建设 2026/7/19 14:34:17

TMS320F2838x时钟与安全监控:从架构解析到高可靠系统设计实战

1. 项目概述&#xff1a;深入TMS320F2838x的时钟与安全监控核心在工业控制、电机驱动这类对实时性和可靠性要求极高的嵌入式应用里&#xff0c;芯片的“心跳”与“健康监测”系统设计&#xff0c;往往是项目成败的第一个分水岭。这个“心跳”就是时钟系统&#xff0c;它决定了C…

作者头像 李华