从零手写经典井字棋(Tic-Tac-Toe):MiniAI实现 + 华为云临时沙箱部署与持续迭代指南

举报
yd_229606956 发表于 2026/10/28 11:12:53 2026/10/28
【摘要】 适合人群:前端初学者、算法入门者、想体验免服务器秒级发布上线的云原生开发者最终成品:一个 13KB 的纯原生单文件 index.html,支持人机对战(极小化极大算法)、同屏双人、获胜线高亮与战绩统计部署交付:借助华为云开发者空间临时沙箱 + DevBridge 隧道实现零公网 IP 全网分发 写在前面:为什么选择井字棋?井字棋(Tic-Tac-Toe / 三连棋)规则极其直观:在 3×3 ...

适合人群:前端初学者、算法入门者、想体验免服务器秒级发布上线的云原生开发者
最终成品:一个 13KB 的纯原生单文件 index.html,支持人机对战(极小化极大算法)、同屏双人、获胜线高亮与战绩统计
部署交付:借助华为云开发者空间临时沙箱 + DevBridge 隧道实现零公网 IP 全网分发


写在前面:为什么选择井字棋?

井字棋(Tic-Tac-Toe / 三连棋)规则极其直观:在 3×3 九宫格中,双方轮流落子,率先将三子连成一线者获胜。

虽然规则简单,但它涵盖了现代独立小游戏的全部经典架构:

  1. 状态机与交互闭环:轮流落子、禁用重复点击、和棋满格判定;
  2. 博弈算法启蒙:九宫格总状态数仅 39=196833^9 = 19683 种(去除对称后仅数百种),非常适合用来教学经典的极小化极大算法(Minimax),打造绝不犯错的"无敌 AI";
  3. 极简交付范式:全套 HTML + CSS + JS 纯原生内联,完全不依赖任何 npm、Node.js、Vite 或构建工具,随时双击即玩。

本文将带你一步步手写实现,并在文末通过华为云 DevKit 临时沙箱免运维发布到公网,最后给出一套清晰的可持续迭代架构。


一、开发前准备

你只需准备:

  1. 一个现代浏览器(Chrome、Edge 等);
  2. 任意文本编辑器(VS Code、文本编辑器均可)。

新建 index.html,搭建基础单文件骨架:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <title>经典井字棋</title>
  <style>/* CSS 内联 */</style>
</head>
<body>
  <div class="container">
    <!-- UI 元素 -->
  </div>
  <script>/* JS 游戏逻辑内联 */</script>
</body>
</html>

二、手把手分步实现

Step 1|数据模型与获胜线常量

井字棋棋盘一共 9 个格子,可以用一个长度为 9 的扁平数组 board 来存储状态(值为 'X'、'O' 或 null)。

// 8条预设获胜线(3条横线、3条竖线、2条对角线)
const WINNING_COMBOS = [
  [0, 1, 2], [3, 4, 5], [6, 7, 8], // 横向
  [0, 3, 6], [1, 4, 7], [2, 5, 8], // 纵向
  [0, 4, 8], [2, 4, 6]             // 对角线
];

let board = Array(9).fill(null);
let currentPlayer = 'X'; // X 先手,O 后手
let isGameOver = false;
let gameMode = 'PVE';   // PVE (人机) | PVP (双人)

自测要点:在控制台中打印 WINNING_COMBOS.length,确认刚好是 8 种胜利排列。


Step 2|响应式九宫格与拟物发光样式

使用现代 CSS Grid 布局,仅需几行代码就能搞定自适应九宫格:

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  gap: 8px;
  width: 100%;
  aspect-ratio: 1 / 1;
}

.cell {
  background: rgba(15, 23, 42, 0.85);
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 48px;
  font-weight: 900;
  cursor: pointer;
  transition: all 0.15s ease;
}

.cell.x {
  color: #38bdf8;
  text-shadow: 0 0 18px rgba(56, 189, 248, 0.6);
}

.cell.o {
  color: #f43f5e;
  text-shadow: 0 0 18px rgba(244, 63, 94, 0.6);
}

为了让游戏在触屏设备上体验更佳,我们在 head 中设置了 user-scalable=no 避免双击缩放。


Step 3|胜负判定逻辑

每次落子后,只需在 WINNING_COMBOS 中检索是否有任意一条线上的三格符号全部相同:

function checkWin(currentBoard) {
  for (let combo of WINNING_COMBOS) {
    const [a, b, c] = combo;
    if (currentBoard[a] && currentBoard[a] === currentBoard[b] && currentBoard[a] === currentBoard[c]) {
      return { winner: currentBoard[a], line: combo };
    }
  }
  // 如果没有胜者,但所有格子均已落子,判定为平局
  if (currentBoard.every(cell => cell !== null)) {
    return { winner: 'tie', line: null };
  }
  return null;
}

优化技巧:返回包含获胜线的索引数组 line,获胜时直接给这 3 个格子赋予 .win 样式,加上呼吸灯动画,终局反馈极其醒目!


Step 4|实现不败的 AI:极小化极大算法 (Minimax)

如果 AI 只是随机落子,玩家很容易失去兴趣。利用 Minimax 算法,我们可以让 AI 洞察全局所有未来的可能分支:

  • AI(O)希望得分最大化(+10 分);
  • 玩家(X)希望得分最小化(-10 分);
  • 越早获胜得分越高,因此引入搜索深度 depth 衰减。
function minimax(currentBoard, depth, isMaximizing) {
  const result = checkWin(currentBoard);
  if (result) {
    if (result.winner === 'O') return 10 - depth; // AI获胜
    if (result.winner === 'X') return depth - 10; // 玩家获胜
    if (result.winner === 'tie') return 0;        // 平局
  }

  if (isMaximizing) {
    let maxScore = -Infinity;
    for (let i = 0; i < 9; i++) {
      if (currentBoard[i] === null) {
        currentBoard[i] = 'O';
        let score = minimax(currentBoard, depth + 1, false);
        currentBoard[i] = null; // 回溯撤销
        maxScore = Math.max(score, maxScore);
      }
    }
    return maxScore;
  } else {
    let minScore = Infinity;
    for (let i = 0; i < 9; i++) {
      if (currentBoard[i] === null) {
        currentBoard[i] = 'X';
        let score = minimax(currentBoard, depth + 1, true);
        currentBoard[i] = null; // 回溯撤销
        minScore = Math.min(score, minScore);
      }
    }
    return minScore;
  }
}

遍历所有空位,寻找调用 minimax 分数最高的一个位置作为 AI 落子点。由于井字棋搜索树深度最多为 9,浏览器在 2 毫秒内即可推演完毕,毫无卡顿!


Step 5|平滑对局与模式切换

为了避免 AI 落子太快像机器故障,我们在玩家落子后设置了 setTimeout(makeAiMove, 220) 拟真延迟;同时提供一键在「人机对战」和「双人同屏」间切换的模式开关,满足不同聚会娱乐场景。


三、调试技巧与常见避坑指南

现象 / 缺陷 核心原因 解决方案
点击已落子区域仍被重写 未对 board[index] !== null 做拦截 落子前严格阻断已占用索引
终局后点击依然可以落子 未绑定 isGameOver 状态锁 胜负产生后立即设置 isGameOver = true
AI 预测深度得分失真 评估返回值未加入 depth 偏移 增加 10 - depth 优先选择最快获胜分支
移动端双击页面放大变形 缺少移动端视口防护 设置 user-scalable=no, maximum-scale=1.0
连续对战比分丢失 未对比分对象做统一管理 声明全局 scores 对象,支持清空局数

四、基于华为云临时沙箱的免备案公网部署实录

单文件游戏写好了,怎么发给朋友或在社交平台上试玩?传统做法买云主机、配 Nginx、买域名、提备案,链路长达数天。

这里介绍利用华为云开发者空间(AI DevSpace)内置的云原生临时沙箱 + DevBridge 开发隧道,4 个指令跑完发布,零服务器购置成本、零公网 IP。

1. 为什么临时沙箱特别适合轻量作品分发?

+---------------------+           +-------------------------+           +---------------------+
|    本地开发工作区   |           |    华为云 DevStation    |           |    全球移动/PC终端  |
|   (单文件 index.html)| --------> |        云原生沙箱       | --------> |   (扫码即可直接体验)|
|                     | 单文件秒传| Nginx 静态服务:8087     | 隧道穿透  | 即时访问 HTTPS 域名 |
+---------------------+           +-------------------------+           +---------------------+
                                               |
                                               v
                                    DevBridge 安全中继网关
  • 免公网 IP:宿主端主动与华为云 DevBridge 边缘节点握手,两端均为出站长连接,彻底摆脱防火墙和家庭宽带没有独立公网 IP 的限制;
  • 免环境搭建:沙箱系统自带高可用 Nginx、Node.js 运行时及开发工具集;
  • 随用随走:具有租期保活与超时回收机制,无需担心忘记关机造成资费扣除。

2. 标准部署四步法

第 1 步:连接沙箱环境

在支持华为云 DevKit 的工具链中建立安全连接并注入会话鉴权:

// huaweicloud_sandbox_connect
{ "source": "WEB" }

实战提示:

  • 若遇到 HDKIT_RESOURCE_BUSY:活动高峰���服务器容器池排队,使用退避策略间隔 10~15 秒重试即可自动拉起;
  • 若遇到 HDKIT_SANDBOX_MAINTENANCE:代表平台底层正处于升级优化窗口,待维护窗口结束后即可顺畅重连。

第 2 步:单文件传输至工作空间

无需上传庞大的 node_modules 文件夹,仅需上传单个 index.html:

sandbox_upload_file(
  local_path  = ".../tic-tac-toe/index.html",
  remote_path = "/workspace/tictactoe/index.html"
)
chmod -R o+rX /workspace/tictactoe

第 3 步:Nginx 独立端口配置

在 /etc/nginx/conf.d/tictactoe.conf 中为应用挂载独立的 8087 端口:

server {
    listen 8087;
    root /workspace/tictactoe;
    index index.html;

    location / {
        try_files $uri $uri.html $uri/ =404;
        autoindex off;
    }
}

运行 sudo nginx -t 语法检测,并通过 sudo nginx -s reload 热生效,沙箱内部自测 curl -s http://localhost:8087 返回 200。

第 4 步:通过 DevBridge 隧道暴露公网

devbridge host -p 8087 -e 72

命令执行完毕即可输出专属于你的安全 HTTPS 域名(例如:https://<tunnel-id>-8087.devbridge-s2.hwtunnel.com)。

多项目聚合技巧:如果你在同一台沙箱中部署了多个小游戏(如 8084 俄罗斯方块、8085 消消乐、8087 井字棋),务必在同一条命令中传参:devbridge host -p 8084 -p 8085 -p 8087 -e 72,一条隧道即可把所有端口全部带出公网,避免多进程相互覆盖。

最后使用终端命令执行探活验证:

curl -sI https://<tunnel-id>-8087.devbridge-s2.hwtunnel.com
# 验证收到 HTTP/1.1 200 OK 即可放心分享二维码给他人试玩!

五、可持续迭代架构与进阶方向

井字棋虽然短小精悍,但在工程和算法层面上拥有巨大的可持续扩展空间。如果你想将它演进为一个完整的商业化或高阶全栈作品,推荐以下四个阶段递进:

[阶段一: 算法难度分级] ──> [阶段二: 棋盘规模升级] ──> [阶段三: WebSocket联机] ──> [阶段四: 云端对战与留存]
  (简单/中等/地狱)          (4×4/五子棋/可变网格)         (双人异地实时对决)          (华为云GaussDB/积分)

1. 算法分级(概率抖动)

目前的 Minimax 属于完全理性的"无敌 AI",玩家只能打成平局或输掉,容易产生挫败感。

  • 可迭代点:引入"难度选择"(简单 / 进阶 / 地狱)。在简单模式下,AI 有 40% 的概率随机走子;中等模式下采用只搜索 1~2 层的浅层贪心算法;地狱模式才开启完整 Minimax。

2. 棋盘规格跃迁(五子棋 / 拓展井字棋)

3×3 棋盘过小导致局面必然死板。

  • 可迭代点:将数据结构升级为支持动态 N×MN \times M 网格(如 4×4 四子棋、8×8 翻转棋或 15×15 五子棋)。此时因为状态空间爆炸(无法全树搜索),你可以引入经典的 Alpha-Beta 剪枝(α\alpha-β\beta Pruning)与启发式评估函数,大幅提升进阶算法思维。

3. 基于 WebSocket / 华为云 FunctionGraph 的跨端联机对决

目前双人模式仅支持同一屏幕轮流点击。

  • 可迭代点:结合华为云函数工作流(FunctionGraph)或沙箱内起一个极其微型的 Node.js WebSocket 服务,支持玩家生成"房间号"邀请异地好友远程联机,实现真正的多人轻对战。

4. 战绩持久化与积分排行榜

  • 可迭代点:本地使用 localStorage 缓存个人连胜记录;远端可对接华为云 RDS 或 GaussDB 数据库,建立全服积分排行榜并加入个人中心,把单机 Demo 演变为社区互动作品。

六、总结

回顾整个开发与交付过程,我们体验到了现代极简 Web 原型开发的核心价值:

  1. 轻量纯粹:摆脱打包脚手架的厚重枷锁,纯单文件 HTML/CSS/JS 即可承载高质感交互与递归博弈算法;
  2. 云原生敏捷:华为云临时沙箱与 DevBridge 隧道打通了从"本地代码"到"公网访问"的最短链路,让每一位开发者都能随手将想法变成人人可用的在线服务。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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