HTML游戏:870行纯前端代码,把中国跳棋的“对称跳“用一个DFS吃透)
【单机离线版】手写一个玻珠棋波子棋HTML游戏870行纯前端代码把中国跳棋的对称跳用一个DFS吃透一、我为什么要写这个家里小孩迷上了跳棋可我翻遍手机应用商店装上的不是要登录就是塞满广告。断网就打不开一局玩到一半还蹦弹窗。烦。开发的棋牌游戏只是用来娱乐与开发学习请勿用于非法用途。于是我想跳棋规则不复杂干脆自己写一个。要求就三条一个文件、双击能开、断网照跑。不引框架不连 CDN源码摊开谁都能看懂。真动手才发现跳棋最有意思的不是走法是跳法。相邻跳、隔一颗跳、连跳……一开始我写了一堆 if 分支越写越乱。后来想通一件事这些跳法本质上是同一件事——以某颗棋子为对称中心跳到它的镜像位置。想通这层几十行分支塌缩成一个 DFS。这篇就围绕这个展开。下载链接https://download.csdn.net/download/qq616491978/93113633二、先把规则理清楚代码里实现的规则我用自己的话说一遍不照搬任何棋谱。2.1 棋盘长什么样棋盘是 6×6 的方格但只用到两个相对的角。左上角一个三角是红方电脑右下角一个三角是蓝方玩家各 6 颗子按 1、2、3 三层铺开。6×6 方格两个相对三角阵地 R . . . . . R 红方(电脑)本营 R R . . . . . 空格 R R R . . . B 蓝方(玩家)本营 . . . B B B . . . . B B . . . . . B整块背板在 CSS 里旋转了 45°看起来就是一个上下左右对称的菱形。棋子是带高光的玻璃珠。2.2 棋子怎么走两种走法走法说明单步沿上下左右走一格到相邻空格不能斜走跳跃以某颗同行/同列的棋子为对称中心跳到它另一侧的镜像空格跳跃有个硬约束从起点到中心、从中心到落点中间不能夹别的棋子。也就是整条线路上除了被跳过的那颗子不能有第二颗。这一条禁掉了隔山跳。一次操作可以连着跳好几步只要每一跳都合法。2.3 怎么算赢把对方初始三角的 6 个格子全占了就赢。另外有个防磨蹭的规则第 20、25、30 轮时离开本营的棋子分别要凑够 2、3、6 颗凑不够的一方判消极失败。三、代码怎么组织的一个 HTML 文件三块【单机离线版】玻珠棋波子棋.html ├── style → CSS约 165 行管菱形棋盘和玻璃珠质感 ├── body HTML → 骨架约 40 行状态栏 棋盘 两个遮罩 └── script → 逻辑约 650 行重点 ├── 常量ROWS/COLS、DIRS 四方向 ├── BoardManager → 棋盘数据与本营判定 ├── RulesValidator → 走法生成、胜负判定 ├── AIController → 电脑走法评分 ├── UIRenderer → DOM 渲染与动画 └── GameController → 流程调度、模式切换一句话总结数据和视图分开grid二维数组是唯一真相DOM 只是它的投影。四、数据结构怎么设计4.1 棋盘就是一个二维数组constROWS6,COLS6;constPLAYERplayer,AIai;grid[r][c]要么是player、ai要么是null。初始化时直接手摆两个三角init(){constgrid[];for(letr0;rROWS;r){grid[r][];for(letc0;cCOLS;c){grid[r][c]null;}}// 电脑红方左上顶点三角grid[0][0]AI;grid[1][0]AI;grid[0][1]AI;grid[2][0]AI;grid[1][1]AI;grid[0][2]AI;// 玩家蓝方右下顶点三角镜像grid[5][5]PLAYER;grid[4][5]PLAYER;grid[5][4]PLAYER;grid[3][5]PLAYER;grid[4][4]PLAYER;grid[5][3]PLAYER;returngrid;}4.2 本营用曼哈顿距离判不硬编码哪些格属于本营我没有把 6 个坐标写死而是用它到顶点的曼哈顿距离baseCells(who){constcells[];for(letr0;rROWS;r)for(letc0;cCOLS;c){if(whoAI){if(rc2)cells.push({r,c});// 红方左上三角}else{if((ROWS-1-r)(COLS-1-c)2)cells.push({r,c});// 蓝方右下三角}}returncells;}红方距(0,0)的距离rc ≤ 2蓝方距(5,5)的距离(5-r)(5-c) ≤ 2。要改棋盘大小只动常量就行。4.3 方向向量constDIRS[{dr:-1,dc:0},// 上{dr:1,dc:0},// 下{dr:0,dc:-1},// 左{dr:0,dc:1},// 右];只有四个正交方向。跳棋不能斜走这个数组从源头上就把斜向排除了。五、CSS 怎么把方格拼成菱形5.1 旋转 45° 是关键传统跳棋盘是菱形但用 DOM 摆菱形很别扭。我的偷懒办法正常摆一个正方形网格然后把整个背板转 45°。.board-wrapper{background:var(--board-bg);border-radius:28px;padding:40px 40px;transform:rotate(45deg);transform-origin:center center;margin:110px auto;}转完之后正方形的四个角朝上下左右视觉上就是对称菱形。margin: 110px是留白——旋转后对角线比边长大了 √2 倍不留空间会顶到上下文字。5.2 视觉层级.cell::before灰色小圆点是棋盘上的落子位.cell.valid-move::before绿色大圆点合法落点提示.cell.selected::after金色描边选中反馈.piece.player/.piece.ai蓝/红玻璃珠5.3 玻璃珠靠径向渐变.piece.player{background:radial-gradient(circle at 35% 30%,#b8dffb,var(--blue)55%,#2c5f8a 100%);box-shadow:0 3px 8pxrgba(0,0,0,0.2),inset 0 -2px 4pxrgba(0,0,0,0.2),inset 0 2px 3pxrgba(255,255,255,0.5);}渐变的圆心偏到左上35% 30%模拟光从左上打下来的高光。内阴影一亮一暗珠子就有了立体感。5.4 移动端适配media(max-width:400px){:root{--cell-size:40px;}.piece{width:28px;height:28px;}}小屏把格子和棋子缩一号一个 CSS 变量搞定。六、核心逻辑——一个 DFS 统一三种跳法这是全文最花功夫的地方。6.1 走法的入口getValidMoves(grid,r,c){constwhogrid[r][c];if(!who)return[];constmoves[];// 1) 单步走法constneighborsBoardManager.getNeighbors(r,c);for(constnbofneighbors){if(grid[nb.r][nb.c]null){moves.push({from:{r,c},to:{r:nb.r,c:nb.c},path:[]});}}// 2) 连跳/对称跳走法 —— DFSconstvisitednewSet();visited.add(${r},${c});this._dfsJumps(grid,r,c,who,[],visited,moves,{r,c});returnmoves;}单步很平凡四个邻居里挑空的。跳跃全交给_dfsJumps。6.2 对称跳——把三种跳合成一种我最初的版本写了相邻跳和隔子跳两套分支还得单独处理连跳改一处忘一处。后来发现它们是同一个式子落点 2 × 中心 − 起点以任意一颗同行或同列的棋子当对称中心把起点关于它做镜像反射就是落点。相邻跳只是中心贴着起点的特例。_dfsJumps(grid,r,c,who,path,visited,out,origin){// 遍历棋盘上所有其他格作为对称中心 midfor(letmr0;mrROWS;mr){for(letmc0;mcCOLS;mc){if(mrrmcc)continue;if(grid[mr][mc]null)continue;// 中心必须有子constdrmr-r,dcmc-c;if(dr!0dc!0)continue;// 只允许同行/同列consttoRmrdr,toCmcdc;// 镜像落点if(toR0||toRROWS||toC0||toCCOLS)continue;if(grid[toR][toC]!null)continue;// 落点必须空// 线路必须干净起点→中心、中心→落点 之间不能有子if(!this._pathClear(grid,r,c,mr,mc))continue;if(!this._pathClear(grid,mr,mc,toR,toC))continue;constkey${toR},${toC};if(visited.has(key))continue;// 防止绕圈visited.add(key);constnewPath[...path,{r:mr,c:mc}];out.push({from:{r:origin.r,c:origin.c},to:{r:toR,c:toC},path:newPath});this._dfsJumps(grid,toR,toC,who,newPath,visited,out,origin);// 继续连跳}}}dr ! 0 dc ! 0这一行卡掉了斜向对称跳。origin始终是整条连跳的原始起点所以哪怕跳了五步落子时也知道棋子是从哪儿出发的。递归 visited天然把连跳串起来还不会绕死循环。6.3 线路清洁检查——禁掉隔山跳第一版对称跳有个 bug只要同一列上有颗子隔多远都能跳过去。三连子(5,5)(4,5)(3,5)(5,5)居然能直接跳到(1,5)中间压根不管。补上这个函数就好了_pathClear(grid,r1,c1,r2,c2){constdrMath.sign(r2-r1),dcMath.sign(c2-c1);letrr1dr,cc1dc;while(r!r2||c!c2){if(grid[r][c]!null)returnfalse;rdr;cdc;}returntrue;}从一端逐格走到另一端中间碰到任何棋子就返回 false。起点到中心、中心到落点各查一遍隔山跳就没了。相邻跳因为两端之间没有格子while一次都不进自然放行。6.4 连跳的样子连跳就是 DFS 递归展开的结果。选中起跳子后所有能到达的落点含多步都会高亮出来。七、渲染和交互怎么配合7.1 坐标不靠 getBoundingClientRect背板转了 45°浏览器测出来的元素矩形会被旋转干扰。我干脆不测量直接用布局时算好的坐标_cellOffset(r,c){constcellthis.cellEls[r][c];if(!cell)return{left:0,top:0};return{left:parseFloat(cell.style.left),top:parseFloat(cell.style.top)};}摆格子时left/top是我自己写进去的读回来最准旋转多少度都不影响。7.2 点击流程点击格子 ├─ 是当前行动方的棋子 → 选中算合法走法高亮 ├─ 已选中 点到合法落点 → 执行移动 └─ 其它 → 取消选中对应代码里onCellClick就这三条分支。判断是不是当前行动方的棋子用的是clicked this.currentTurn这一改让双人模式白拿——红方玩家也能用鼠标操作了。7.3 落子单点收口不管是人走、电脑走、还是双人里的另一方走最后都汇到applyMoveasyncapplyMove(move,who){this.lockedtrue;this.selectednull;this.validMoves[];UIRenderer.clearHighlights();const{from,to,path}move;awaitUIRenderer.animateMove(from.r,from.c,to.r,to.c,path);this.grid[from.r][from.c]null;this.grid[to.r][to.c]who;if(RulesValidator.checkWin(this.grid,who)){this.declareWin(who);return;}this.endTurn(who);}动画、改数据、判胜、交回合一条龙。谁调它都走同一套不会有人走了会判胜、电脑走了忘判胜这种漏。八、怎么判胜负8.1 占满对方三角checkWin(grid,who){consttargetsBoardManager.targetCells(who);for(consttoftargets){if(grid[t.r][t.c]!who)returnfalse;}returntrue;}targetCells返回对方本营那 6 格逐格看是不是自己的子。全中就赢。8.2 强制出营防磨蹭checkForcedOut(round){constneed{20:2,25:3,30:6}[round];if(!need)return;constpOutBoardManager.leftBaseCount(this.grid,PLAYER);constaOutBoardManager.leftBaseCount(this.grid,AI);// ……离营不达标的一方判负}只在第 20/25/30 轮触发用一个字面量对象查表比一串if清爽。九、两种对战模式开局先弹一个遮罩让你选电脑对战还是双人对战init(){UIRenderer.init();this.gridBoardManager.init();UIRenderer.buildBoard((r,c)this.onCellClick(r,c));UIRenderer.renderPieces(this.grid);UIRenderer.el.restartBtn.addEventListener(click,()this.restart());document.getElementById(modePveBtn).addEventListener(click,()this.startGame(this.MODE_PVE));document.getElementById(modePvpBtn).addEventListener(click,()this.startGame(this.MODE_PVP));this.showModeSelect();}两种模式共用全部落子逻辑唯一的分叉在endTurn人机模式轮到电脑时自动setTimeout调 AI双人模式就停下来等另一个人点。电脑的走法评分很朴素落点离对方阵地越近加分越多能连跳额外加分快占满目标时再补一刀。够打但不难赢——所以我才补了双人模式。十、几个值得记住的点能用数学式子统一的分支就别写成一堆 if。对称跳那个2×中心−起点是全篇的转折。本营、目标格这类位置集合用距离公式生成别硬编码坐标改棋盘尺寸时省心。视图旋转后坐标从布局数据里读别信getBoundingClientRect。落子逻辑单点收口人和电脑走同一条路漏洞少一半。十一、还可以做什么更聪明的电脑。现在只看一步的贪心评分可以加个浅层 minimax或者对连跳链做前瞻。六角满盘。当前只用了两个三角扩成标准的六角星盘、支持 2~6 人baseCells和targetCells的距离公式改改就能撑起来。残局分享。把grid序列化成一段字符串就能存档和复盘。十二、关于这篇文章完整 HTML 源码随文附上存成.html双击就能跑约 870 行关键函数都有中文注释。下载链接https://download.csdn.net/download/qq616491978/93113633