古风华容道小游戏

举报
yd_292659020 发表于 2026/10/13 01:17:59 2026/10/13
【摘要】 一个古风华容道小游戏一篇完整的开发手记:从华容道规则研究、关卡设计、古风 UI 打磨到核心移动算法实现,最后发布到华为云开发者作品展览馆的全流程。 一、项目背景华容道(Klotski)是中国传统益智游戏,源自三国典故"曹操败走华容道"。玩家在 4×5 的棋盘上滑动大小不一的方块,目标是将最大的"曹操"方块移出底部出口。本项目以三国人物为棋子,采用古风视觉设计,包含三关经典阵法,面向青少年及...

一个古风华容道小游戏

一篇完整的开发手记:从华容道规则研究、关卡设计、古风 UI 打磨到核心移动算法实现,最后发布到华为云开发者作品展览馆的全流程。

一、项目背景

华容道(Klotski)是中国传统益智游戏,源自三国典故"曹操败走华容道"。玩家在 4×5 的棋盘上滑动大小不一的方块,目标是将最大的"曹操"方块移出底部出口。本项目以三国人物为棋子,采用古风视觉设计,包含三关经典阵法,面向青少年及益智游戏爱好者。

技术选型:纯 HTML + CSS + JavaScript,零框架、零依赖。一个 python3 -m http.server 就能跑起来。

最终效果预览:


二、游戏规则

  • 棋盘:4 列 × 5 行网格,共 20 个格子
  • 棋子类型:
棋子 尺寸 数量 颜色 说明
曹操 2×2 1 朱红色 最大方块,突围目标
关羽 1×2(横) 1 青绿色 横向长方块
张飞 1×2(竖) 1 墨青色 竖向长方块
赵云 1×2(竖) 1 湖蓝色 竖向长方块
马超 1×2(竖) 1 靛蓝色 竖向长方块
黄忠 1×2(竖) 1 墨绿色 竖向长方块
卒 1×1 4 灰褐色 小方块
  • 移动规则:棋子只能滑入相邻的空格,不可重叠、不可越界
  • 操作方式:点击棋子选中后按方向键(↑↓←→)移动,或直接拖拽
  • 胜利条件:曹操(2×2 大块)左上角到达棋盘底部中央位置(row=3, col=1)
  • 计分方式:每移动一格记一步,步数越少越精妙

三、三关阵法设计

华容道的精髓在于初始布局。不同的布局产生不同的难度和趣味性。本项目设计了三关经典阵法:

第一阵 · 横刀立马(入门,最少 81 步)

最经典的华容道开局,曹操位于棋盘顶部中央,关羽横置其下,四员竖将分列两侧,四卒散布底部:

张飞  曹操  曹操  赵云
张飞  曹操  曹操  赵云
马超  关羽  关羽  黄忠
马超  卒    卒    黄忠
卒    卒    ·     ·

第二阵 · 指挥若定(中等,最少 70 步)

曹操仍在顶部,但武将位置调整——张飞、赵云下移一格,马超、黄忠移至底部两侧,四卒分散在四角和中部:

卒    曹操  曹操  卒
张飞  曹操  曹操  赵云
张飞  关羽  关羽  赵云
马超  卒    卒    黄忠
马超  ·    ·     黄忠

第三阵 · 将拥曹营(较高,最少 72 步)

曹操不再在顶部,而是被围在中间(row=1),四卒封顶,关羽横置底部,形成"将拥曹营"之势:

卒    卒    卒    卒
张飞  曹操  曹操  赵云
张飞  曹操  曹操  赵云
马超  关羽  关羽  黄忠
马超  ·    ·     黄忠

三关布局均经过合法性校验:棋子不重叠、不越界、恰好留 2 格空位,且均为经典可解阵法。


四、架构设计

项目采用三层分离结构,无任何前端框架:

klotski-game/
├── index.html    # 页面结构层(103 行)
├── style.css     # 视觉表现层(369 行)
├── game.js       # 游戏逻辑层(345 行)
└── README.md     # 项目说明

设计分层

┌─────────────────────────────────────────┐
│         表现层 (UI Layer)                │
│   index.html + style.css                │
│   · 卷轴框架 · 古风配色 · 棋子样式       │
├─────────────────────────────────────────┤
│         逻辑层 (Game Logic)              │
│   game.js                               │
│   · 占位检测 · 移动判定 · 胜负检查       │
├─────────────────────────────────────────┤
│         数据层 (Data Layer)              │
│   LEVELS 常量                            │
│   · 三关布局 · 棋子定义 · 胜利条件        │
└─────────────────────────────────────────┘

为什么不用框架?华容道的交互逻辑比数字合并游戏更直观——点击选中、方向键移动,DOM 操作量可控。纯 JS 直接操作 DOM,代码量 345 行,清晰易读,对青少年来说也是很好的学习材料。


五、核心实现

5.1 数据结构:棋子对象

每个棋子是一个带有位置和尺寸信息的对象:

{
  id: "caocao",      // 唯一标识
  name: "曹操",       // 显示名称
  type: "caocao",    // 类型(用于 CSS 样式)
  w: 2,              // 宽(占几列)
  h: 2,              // 高(占几行)
  row: 0,            // 当前行坐标(0 = 顶部)
  col: 1             // 当前列坐标(0 = 左侧)
}

为什么需要 w/h? 华容道有三种尺寸的棋子:1×1(卒)、1×2(武将)、2×2(曹操)。用宽高统一描述,移动检测和渲染都能用同一套逻辑处理,无需为每种棋子写特殊代码。

5.2 关卡数据

三关布局定义为常量数组,每关包含名称、最少步数和棋子初始位置:

const LEVELS = [
  {
    name: "横刀立马",
    minMoves: 81,
    pieces: [
      { id: "caocao", name: "曹操", type: "caocao", w: 2, h: 2, row: 0, col: 1 },
      { id: "guanyu", name: "关羽", type: "guanyu", w: 2, h: 1, row: 2, col: 1 },
      { id: "zhangfei", name: "张飞", type: "zhangfei", w: 1, h: 2, row: 0, col: 0 },
      { id: "zhaoyun", name: "赵云", type: "zhaoyun", w: 1, h: 2, row: 0, col: 3 },
      { id: "machao", name: "马超", type: "machao", w: 1, h: 2, row: 2, col: 0 },
      { id: "huangzhong", name: "黄忠", type: "huangzhong", w: 1, h: 2, row: 2, col: 3 },
      { id: "p1", name: "卒", type: "pawn", w: 1, h: 1, row: 3, col: 1 },
      { id: "p2", name: "卒", type: "pawn", w: 1, h: 1, row: 3, col: 2 },
      { id: "p3", name: "卒", type: "pawn", w: 1, h: 1, row: 4, col: 0 },
      { id: "p4", name: "卒", type: "pawn", w: 1, h: 1, row: 4, col: 1 },
    ],
  },
  // ... 第二阵、第三阵
];

5.3 占位图与碰撞检测

华容道的核心约束是"棋子不能重叠"。每次移动前,先构建当前棋盘的占位图,再检查目标位置是否可用:

function buildOccupancy() {
  const grid = Array.from({ length: ROWS }, () => Array(COLS).fill(null));
  for (const p of pieces) {
    for (let r = 0; r < p.h; r++) {
      for (let c = 0; c < p.w; c++) {
        grid[p.row + r][p.col + c] = p.id;
      }
    }
  }
  return grid;
}

移动检测分两步:边界检查 + 重叠检查:

function canPlace(piece, newRow, newCol) {
  // 1. 边界检查:不能超出棋盘
  if (newRow < 0 || newCol < 0) return false;
  if (newRow + piece.h > ROWS || newCol + piece.w > COLS) return false;
  // 2. 重叠检查:不能与其他棋子重叠
  for (const other of pieces) {
    if (other.id === piece.id) continue;
    if (rectsOverlap(newRow, newCol, piece.w, piece.h,
                     other.row, other.col, other.w, other.h)) {
      return false;
    }
  }
  return true;
}

矩形重叠检测用经典的分离轴定理——两个矩形不重叠,当且仅当在某个轴上分离:

function rectsOverlap(r1, c1, w1, h1, r2, c2, w2, h2) {
  return !(r1 + h1 <= r2 || r2 + h2 <= r1 ||
           c1 + w1 <= c2 || c2 + w2 <= c1);
}

5.4 移动执行

每次移动一格,更新坐标、步数,并检查胜利条件:

function tryMove(piece, dr, dc) {
  const nr = piece.row + dr;
  const nc = piece.col + dc;
  if (canPlace(piece, nr, nc)) {
    piece.row = nr;
    piece.col = nc;
    moves++;
    moveCountEl.textContent = moves;
    renderPositions();
    if (!won && checkWin()) {
      won = true;
      setTimeout(showWin, 250);
    }
    return true;
  }
  return false;
}

5.5 胜利判定

胜利条件非常简洁——曹操的左上角到达指定位置:

const WIN_ROW = 3;
const WIN_COL = 1;

function checkWin() {
  const cao = pieces.find((p) => p.id === "caocao");
  return cao && cao.row === WIN_ROW && cao.col === WIN_COL;
}

为什么是 (row=3, col=1)?因为曹操是 2×2 方块,左上角在 (3, 1) 时,它占据第 3-4 行、第 1-2 列——正好是棋盘底部中央的出口位置。

5.6 交互处理:选中 + 方向键 + 拖拽

游戏支持两种交互方式:

方式一:点击选中 + 方向键移动

// 点击棋子选中
el.addEventListener("click", () => {
  selectedId = p.id;
  renderPositions();  // 更新高亮状态
});

// 方向键移动选中棋子
window.addEventListener("keydown", (e) => {
  if (!selectedId) return;
  const p = pieces.find((x) => x.id === selectedId);
  const map = {
    ArrowUp: [-1, 0], ArrowDown: [1, 0],
    ArrowLeft: [0, -1], ArrowRight: [0, 1],
  };
  const dir = map[e.key];
  if (dir) { e.preventDefault(); tryMove(p, dir[0], dir[1]); }
});

方式二:鼠标拖拽

拖拽时计算鼠标位移方向,尝试沿该方向移动一格。拖拽过程中持续检测,实现"滑入空格"的手感:

function onPointerMove(e) {
  if (!dragging) return;
  const dx = e.clientX - dragStart.x;
  const dy = e.clientY - dragStart.y;
  const cell = getCellSize();
  // 位移超过半格时触发移动
  if (Math.abs(dx) > cell / 2) {
    tryMove(dragPiece, 0, dx > 0 ? 1 : -1);
    dragStart.x = e.clientX;
  } else if (Math.abs(dy) > cell / 2) {
    tryMove(dragPiece, dy > 0 ? 1 : -1, 0);
    dragStart.y = e.clientY;
  }
}

5.7 渲染:绝对定位 + CSS 变量

棋子用绝对定位放置在棋盘上,位置通过 CSS 变量 --cell(格子大小)和 --gap(间距)计算:

function posOf(row, col) {
  const cell = getCellSize();
  const gap = getGap();
  return { x: gap + col * (cell + gap), y: gap + row * (cell + gap) };
}

function sizeOf(w, h) {
  const cell = getCellSize();
  const gap = getGap();
  return { w: w * cell + (w - 1) * gap, h: h * cell + (h - 1) * gap };
}

移动时只更新 left/top 属性,配合 CSS transition 实现平滑滑动动画。


六、古风 UI 设计

6.1 配色方案

整体配色以"宣纸 + 木纹 + 朱墨"为基调,营造三国古战场的氛围:

:root {
  --paper: #f5e6c8;       /* 宣纸底色 */
  --paper-2: #e8d5a3;     /* 宣纸深色 */
  --ink: #2b2118;          /* 墨色文字 */
  --ink-soft: #5a4a38;     /* 淡墨 */
  --wood: #8b6914;         /* 木纹色 */
  --wood-dark: #6e5210;    /* 深木纹 */
  --caocao: #c0392b;       /* 曹操朱红 */
  --general: #2e6e50;      /* 武将青绿 */
  --pawn: #8a7355;         /* 卒灰褐 */
  --gold: #b8860b;         /* 金色装饰 */
}
色彩 色值 用途
宣纸色 #f5e6c8 页面背景,模拟泛黄宣纸
墨色 #2b2118 文字、边框
木纹色 #8b6914 棋盘框架、卷轴轴杆
朱红色 #c0392b 曹操棋子,突出突围目标
青绿色 #2e6e50 武将棋子(关羽)
灰褐色 #8a7355 卒棋子,低调朴素
金色 #b8860b 装饰边角、选中高亮

6.2 卷轴框架

整个游戏界面被包裹在一个"卷轴"中——上下各有一根木纹轴杆,两端带铜钉装饰:

.scroll-rod {
  height: 18px;
  border-radius: 9px;
  background: linear-gradient(180deg, #c9a14a, #8b6914 40%, #6e5210 70%, #4a3608);
  box-shadow: 0 3px 8px rgba(0,0,0,.35), inset 0 1px 2px rgba(255,220,150,.5);
}
.scroll-rod::before, .scroll-rod::after {
  content: "";
  width: 26px; height: 26px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #e6c36a, #8b6914 60%, #4a3608);
}

轴杆用渐变模拟圆柱体质感,两端用径向渐变圆形模拟铜钉帽,配合 box-shadow 营造立体感。

6.3 棋盘与棋子

棋盘用木纹色边框包裹,四角有金色装饰花纹。棋子根据类型有不同的背景色和边框:

.piece.caocao {
  background: linear-gradient(135deg, var(--caocao), var(--caocao-dark));
  border: 2px solid #e74c3c;
  box-shadow: 0 3px 8px rgba(192,57,43,.4);
}
.piece.guanyu {
  background: linear-gradient(135deg, var(--general), #1a5e3e);
  border: 2px solid #3a8e6e;
}
.piece.pawn {
  background: linear-gradient(135deg, var(--pawn), var(--pawn-dark));
  border: 2px solid #a08866;
}

选中状态用金色边框高亮,给玩家明确的视觉反馈:

.piece.selected {
  border-color: var(--gold);
  box-shadow: 0 0 12px rgba(184,134,11,.6), 0 3px 8px rgba(0,0,0,.3);
}

6.4 书法字体

使用 Google Fonts 的三款中文字体,营造古风氛围:

<link href="https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&family=ZCOOL+KuaiLe&family=Noto+Serif+SC:wght@400;700&display=swap" rel="stylesheet">
字体 用途
Ma Shan Zheng(马善政体) 棋子上的武将名,手写书法风格
ZCOOL KuaiLe(站酷快乐体) 标题"华容道·三国古战阵"
Noto Serif SC(思源宋体) 正文、按钮、提示文字

6.5 背景纹理

页面背景用多层渐变叠加,模拟宣纸的质感和泛黄效果:

body {
  background:
    radial-gradient(circle at 20% 10%, rgba(184,134,11,.10), transparent 40%),
    radial-gradient(circle at 80% 90%, rgba(139,105,20,.12), transparent 45%),
    repeating-linear-gradient(45deg, #efe0bf 0 2px, #ead9b0 2px 4px);
}

三层叠加:左上角金色光晕 + 右下角木色光晕 + 45° 斜纹纸纹,形成温润的宣纸底色。

6.6 响应式

使用 clamp() 实现自适应,桌面和移动端共用一套样式:

--cell: clamp(58px, 16vw, 78px);     /* 格子大小随屏宽变化 */
--gap: clamp(3px, 1vw, 4px);         /* 间距自适应 */

6.7 胜利动画

胜利时弹出半透明遮罩 + 居中卡片,展示步数和最少步数对比,配合淡入动画:

@keyframes fadeIn {
  from { opacity: 0; transform: scale(0.8); }
  to   { opacity: 1; transform: scale(1); }
}
.win-card { animation: fadeIn 0.4s ease-out; }

七、部署与发布

7.1 本地运行

python3 -m http.server 8080
# 浏览器打开 http://localhost:8080

7.2 代码托管

代码已推送到 GitCode 仓库:https://gitcode.com/gcw_nfhkuMAb/klotski-game

7.3 发布到华为云开发者作品展览馆

通过华为云高校运营平台发布作品,完整流程包括:

  1. 域名解析:获取高校运营平台域名 ID
  2. STS 凭证生成:创建自委托生成临时安全凭证
  3. 启动服务 + DevBridge 隧道:本地 8082 端口 → 公网可访问地址
  4. 推送代码到 GitCode:OAuth 授权 → 创建仓库 → git init/commit/push
  5. 生成封面截图:Playwright Chromium 自动截图
  6. 生成作品详情:架构图 + 详解文章打包为 zip
  7. 选择投稿活动:选择「布道师通用产品体验」
  8. 发布:门禁复核 → 提交发布请求 → 获得 +200 积分

作品地址:https://gallery.developer.huaweicloud.com/gallery/005d9c238f400000


八、开发心得

为什么选华容道?

华容道是中国传统益智游戏的经典代表,文化底蕴深厚。相比数字合并类游戏(如 1024/2048),华容道更考验空间推理和规划能力——玩家需要在脑海中模拟多步移动,找到腾挪空间。以三国人物为棋子,配合古风视觉设计,让传统文化与编程技术自然融合。

棋子尺寸的统一描述

最初考虑为每种棋子类型写单独的移动和渲染逻辑,但很快发现这会导致大量重复代码。引入 w(宽)和 h(高)两个属性后,所有棋子——1×1 的卒、1×2 的武将、2×2 的曹操——都能用同一套 canPlace()、posOf()、sizeOf() 函数处理。这是"数据驱动设计"的一个小实践:用数据描述差异,用统一逻辑处理共性。

碰撞检测的简洁实现

华容道的碰撞检测本质上是矩形重叠判断。最初用逐格扫描(遍历目标位置的每个格子检查是否被占),后来改用分离轴定理的矩形重叠判断——只需 4 次比较,无需嵌套循环,代码更简洁,逻辑更清晰:

// 两个矩形不重叠 ⟺ 在某条轴上分离
return !(r1 + h1 <= r2 || r2 + h2 <= r1 ||
         c1 + w1 <= c2 || c2 + w2 <= c1);

拖拽与键盘的互补

最初只实现了点击选中 + 方向键移动,但实际试玩发现频繁切换"鼠标点击 → 键盘按键"不够流畅。于是增加了拖拽模式——直接用鼠标拖动棋子滑向空格。两种方式互补:键盘适合精确的单步移动,拖拽适合连续滑动。拖拽时以"半格"为触发阈值,避免误触。

古风设计的克制

古风 UI 容易过度装饰——过多的花纹、阴影、动画反而影响游戏体验。本项目的原则是"氛围到位,不喧宾夺主":卷轴框架只出现在页面边缘,棋子用渐变色块而非贴图,背景纹理保持低对比度。核心交互区域(棋盘)保持简洁,让玩家专注于解题。

零依赖的优势

整个项目 817 行代码(HTML 103 + CSS 369 + JS 345),没有任何 node_modules,没有构建步骤,没有打包工具。git clone 后直接打开 index.html 就能玩。这种简洁性对教学场景特别有价值——学生可以完整理解每一行代码。


九、项目信息

项目 内容
作品名称 华容道·三国古战阵
技术栈 纯 HTML / CSS / JavaScript
代码行数 817 行(HTML 103 + CSS 369 + JS 345)
外部依赖 无(仅引用 Google Fonts CDN)
关卡数量 3 关(横刀立马 / 指挥若定 / 将拥曹营)
GitCode 仓库 https://gitcode.com/gcw_nfhkuMAb/klotski-game
展览馆链接 https://gallery.developer.huaweicloud.com/gallery/005d9c238f400000
投稿活动 布道师通用产品体验

本文记录了华容道小游戏从规则研究到发布展览馆的完整开发过程。华容道看似简单,实则蕴含深刻的空间推理智慧——正如三国古战场,方寸之间,运筹帷幄。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。