我的身份 / 阶段
计算机与前端零基础小白。目前处于日常学习和项目积累阶段。
我的科研 / 学习任务
尝试完成一个包含 CSS 3D 视觉动画、人机对战 AI 逻辑(优先级判断算法)以及网页响应式布局的网页小游戏制作。
我用了什么 AI 工具
ChatGPT / Claude 3.5 Sonnet
页面效果
我是怎么做的
- 需求输入:我向 AI 提出了明确的开发需求:“请帮我写一个单文件 HTML 网页,包含 CSS 和 JS。需要实现一个 3D 视觉风格的井字棋游戏,带有 3D 浮动动画、支持双人对战和人机对战模式切换、得分统计以及胜利判定提示。”
- AI 生成代码:AI 迅速吐出了完整的代码,包括使用 CSS
perspective和transform-style: preserve-3d实现的立体舞台,以及基于优先级的 AI 人机对战算法。 - 核对与测试:我直接将代码保存为
.html文件并在浏览器打开运行。检查了落子动画(dropIn帧动画)、人机 AI 的下棋逻辑(优先胜/阻挡/占中心/占角/随机)以及模式切换时的得分重置。
结果如何
- 大幅提效:从零手写 3D 动画样式和交互逻辑原先可能需要花费大半天,现在通过 AI 辅助仅用 15 分钟 就完成了开发与测试。
- 产出成果:获得了一个结构极其紧凑(仅百余行代码)、纯前端零依赖、可以直接单文件运行的 3D 井字棋网页小游戏。
踩坑和提醒
- 3D 视角点击偏移问题:CSS 3D 旋转角度过大时,鼠标点击位置可能会出现视觉偏移,需要提醒 AI 将
perspective视距和rotateX角度调整到舒适的范围。 - 人机逻辑漏洞:AI 初版代码可能会遗漏某些赢棋分支,必须在交互中多次测试 AI 是否能正常拦截你的三连。
可复制建议
- 要求“单文件交付”:对于小型 demo 开发,让 AI 将 HTML、CSS、JS 整合在单文件内,极大降低调测和部署门槛。
- 结构化需求提示词:提要求时分点写明“UI风格 + 核心功能 + 交互细节”,AI 产出的代码质量会高很多。
代码分享
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D 井字棋</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }
body {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
background: radial-gradient(ellipse at 50% 30%, #2b3a67 0%, #141b2d 60%, #0a0e1a 100%);
color: #e8ecf5;
overflow: hidden;
perspective: 1100px;
}
h1 {
font-size: 42px;
letter-spacing: 8px;
margin-bottom: 8px;
text-shadow: 0 4px 18px rgba(0,0,0,.6), 0 0 30px rgba(120,160,255,.25);
transform-style: preserve-3d;
}
h1 span { color: #7fb2ff; }
#status {
height: 34px;
font-size: 19px;
margin-bottom: 20px;
letter-spacing: 2px;
color: #c6d4f0;
text-shadow: 0 2px 8px rgba(0,0,0,.5);
}
/* ---------- 3D 舞台 ---------- */
#stage {
transform-style: preserve-3d;
animation: stageFloat 6s ease-in-out infinite;
}
@keyframes stageFloat {
0%,100% { transform: rotateX(52deg) rotateZ(-45deg) translateZ(-10px); }
50% { transform: rotateX(50deg) rotateZ(-43deg) translateZ(14px); }
}
#board {
width: 330px;
height: 330px;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 10px;
padding: 14px;
border-radius: 22px;
background: linear-gradient(145deg, rgba(255,255,255,.09), rgba(255,255,255,.02));
border: 1px solid rgba(140,170,230,.28);
box-shadow:
0 40px 60px rgba(0,0,0,.55),
inset 0 1px 0 rgba(255,255,255,.15);
transform-style: preserve-3d;
position: relative;
}
.cell {
background: linear-gradient(145deg, rgba(255,255,255,.07), rgba(255,255,255,.01));
border: 1px solid rgba(150,180,240,.16);
border-radius: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 64px;
font-weight: 800;
transition: background .25s, box-shadow .25s, transform .15s;
transform-style: preserve-3d;
}
.cell:hover { background: rgba(130,170,255,.13); box-shadow: 0 0 18px rgba(120,160,255,.35); }
.cell.x {
color: #ff7d7d;
text-shadow: 0 0 16px rgba(255,90,90,.65), 0 -4px 0 rgba(0,0,0,.25);
animation: dropIn .45s cubic-bezier(.2,1.6,.4,1);
}
.cell.o {
color: #7fd0ff;
text-shadow: 0 0 16px rgba(80,190,255,.65), 0 -4px 0 rgba(0,0,0,.25);
animation: dropIn .45s cubic-bezier(.2,1.6,.4,1);
}
@keyframes dropIn {
from { transform: translateZ(70px) rotateX(-40deg); opacity: 0; }
to { transform: translateZ(0); opacity: 1; }
}
.cell.win {
background: linear-gradient(145deg, rgba(255,220,120,.35), rgba(255,200,80,.15));
box-shadow: 0 0 26px rgba(255,210,110,.8), inset 0 0 12px rgba(255,220,140,.5);
animation: winPulse 1s ease-in-out infinite alternate;
}
@keyframes winPulse {
from { transform: translateZ(0); }
to { transform: translateZ(24px); }
}
/* 棋盘厚度 */
#board::after {
content: "";
position: absolute;
inset: -2px;
z-index: -1;
transform: translateZ(-22px);
border-radius: 24px;
background: linear-gradient(160deg, #1d2742, #10182e);
}
/* ---------- 底部控制 ---------- */
#panel {
margin-top: 46px;
display: flex;
gap: 14px;
align-items: center;
}
button {
font-family: inherit;
font-size: 16px;
letter-spacing: 2px;
padding: 11px 26px;
border-radius: 12px;
border: 1px solid rgba(150,180,240,.35);
background: linear-gradient(145deg, rgba(255,255,255,.12), rgba(255,255,255,.04));
color: #e8ecf5;
cursor: pointer;
transition: all .2s;
backdrop-filter: blur(6px);
}
button:hover {
background: linear-gradient(145deg, rgba(130,170,255,.3), rgba(100,140,230,.12));
box-shadow: 0 0 20px rgba(120,160,255,.4);
transform: translateY(-2px);
}
button:active { transform: translateY(1px); }
#score {
margin-top: 18px;
font-size: 15px;
letter-spacing: 1px;
color: #9db1d8;
}
#score b { color: #ffd98a; }
.hint {
margin-top: 10px;
font-size: 13px;
color: #6d7fa5;
letter-spacing: 1px;
}
</style>
</head>
<body>
<h1>3D <span>井字棋</span></h1>
<div id="status">轮到 ✕ 玩家</div>
<div id="stage">
<div id="board"></div>
</div>
<div id="panel">
<button id="restart">重新开始</button>
<button id="mode">模式:双人对战</button>
</div>
<div id="score">✕ 胜 <b>0</b> | ◯ 胜 <b>0</b> | 平局 <b>0</b></div>
<div class="hint">提示:点击格子落子 · 棋盘会轻微旋转展示立体效果</div>
<script>
const LINES = [
[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("");
let current = "X";
let over = false;
let vsAI = false;
let score = { X: 0, O: 0, D: 0 };
const boardEl = document.getElementById("board");
const statusEl = document.getElementById("status");
const scoreEl = document.getElementById("score");
function render() {
boardEl.innerHTML = "";
board.forEach((v, i) => {
const cell = document.createElement("div");
cell.className = "cell" + (v ? " " + v.toLowerCase() : "");
cell.textContent = v === "X" ? "✕" : v === "O" ? "◯" : "";
cell.onclick = () => play(i);
boardEl.appendChild(cell);
});
}
function play(i) {
if (over || board[i]) return;
board[i] = current;
afterMove();
if (!over && vsAI && current === "O") setTimeout(aiMove, 450);
}
function aiMove() {
// 优先取胜 → 阻挡对手 → 中心 → 角 → 边
const empty = board.map((v,i)=>v?-1:i).filter(i=>i>=0);
const tryFind = (p) => {
for (const i of empty) {
board[i] = p;
const win = checkWin().winner === p;
board[i] = "";
if (win) return i;
}
return -1;
};
let move = tryFind("O");
if (move < 0) move = tryFind("X");
if (move < 0 && !board[4]) move = 4;
if (move < 0) {
const corners = [0,2,6,8].filter(i=>!board[i]);
move = corners.length ? corners[Math.random()*corners.length|0]
: empty[Math.random()*empty.length|0];
}
board[move] = "O";
afterMove();
}
function afterMove() {
const { winner, line } = checkWin();
render();
if (winner) {
over = true;
score[winner]++;
updateScore();
statusEl.textContent = `🎉 ${winner === "X" ? "✕ 玩家" : (vsAI ? "AI" : "◯ 玩家")} 获胜!`;
markWin(line);
} else if (board.every(Boolean)) {
over = true;
score.D++;
updateScore();
statusEl.textContent = "平局!再来一局?";
} else {
current = current === "X" ? "O" : "X";
statusEl.textContent = `轮到 ${current === "X" ? "✕" : "◯"} 玩家`;
}
}
function checkWin() {
for (const l of LINES) {
const [a,b,c] = l;
if (board[a] && board[a] === board[b] && board[a] === board[c])
return { winner: board[a], line: l };
}
return { winner: null, line: null };
}
function markWin(line) {
if (!line) return;
[...boardEl.children].forEach((c,i)=>{
if (line.includes(i)) c.classList.add("win");
});
}
function restart() {
board = Array(9).fill("");
current = "X";
over = false;
statusEl.textContent = "轮到 ✕ 玩家";
render();
}
function updateScore() {
scoreEl.innerHTML =
`✕ 胜 <b>${score.X}</b> | ◯ 胜 <b>${score.O}</b> | 平局 <b>${score.D}</b>`;
}
document.getElementById("restart").onclick = restart;
document.getElementById("mode").onclick = function () {
vsAI = !vsAI;
this.textContent = vsAI ? "模式:人机对战" : "模式:双人对战";
score = { X: 0, O: 0, D: 0 };
updateScore();
restart();
};
render();
</script>
</body>
</html>

