前言
两年前,当 GPT-4 刚刚横空出世时,我怀着激动的心情尝试用它写一个五子棋游戏。那段经历我记录在了掘金文章《GPT-4多轮对话生成五子棋游戏》中。当时,为了得到一个能跑的 Demo,我与 GPT-4 进行了漫长的多轮对话:修复 Bug、调整样式、纠正逻辑,仿佛在带一个虽然聪明但由于"健忘"和"粗心"而需要不断纠正的实习生。
时光荏苒,两年后的今天,我拿到了 Gemini 3 Pro。抱着同样的命题,我再次输入了那个需求:"帮我写一个五子棋人机对战游戏"。
这一次,没有多轮对话,没有报错调试。它直接甩给了我一个单文件,我复制、粘贴、打开------它不仅能跑,而且"懂"战术,甚至连 UI 的渐变色都做好了。
这篇文章,不仅是对代码的复盘,更是对这两年 AI 发展速度的一次微观见证。
1. 直观对比:从"拼积木"到"3D打印"
两年前的 GPT-4 体验
在两年前的那篇文章中,开发过程是碎片化的:
- 上下文丢失:代码太长时,GPT-4 会中断,需要我提示"继续"。
- 逻辑割裂:HTML、CSS 和 JS 往往是分开生成的,需要我手动组装。
- 调试循环:最开始生成的 AI 逻辑经常只会随机落子,或者不仅不堵我的棋,还下在已经被占用的格子上。我需要反复告诉它:"你这里逻辑不对,应该先判断是否为空"。
今天的 Gemini 3 Pro 体验
这是 Gemini 3 Pro 给我生成的完整代码(见文末附录)。
结果是惊人的:
- One-Shot(一次成型) :它在一个 HTML 文件中完整封装了结构、样式和逻辑,没有任何截断。
- 审美在线 :它没有给我一个黑白格子的简陋棋盘,而是直接使用了
#dcb35c木纹底色,棋子甚至使用了RadialGradient径向渐变来模拟光影立体感。 - 逻辑完备 :赢法数组 (
wins)、赢法统计 (myWin,computerWin)、启发式评分 (myScore,computerScore) 一气呵成。
2. 代码深度解析:AI 真的懂"算法"了
让我们看看 Gemini 3 Pro 生成的核心 AI 逻辑,这在两年前是需要我引导很久才能写出来的。
赢法数组的预计算
代码中有一个非常经典的五子棋算法实现------赢法数组。
JavaScript
csharp
// 初始化赢法数组
function initWins(){
// ... 省略循环 ...
// 横线、竖线、斜线、反斜线
// 统计出所有可能连成5子的线,总共572种(15x15棋盘)
}
Gemini 3 Pro 在初始化时,直接计算了棋盘上所有横、竖、撇、捺能构成五连珠的情况,并在内存中构建了一个三维数组。这意味着它"知道"五子棋的胜利条件不仅仅是"看到5个子",而是基于数学上的组合可能性。
启发式评分系统
最让我惊讶的是 computerAI 函数中的评分逻辑:
JavaScript
scss
// 拦截玩家
if(myWin[k] == 1) myScore[i][j] += 200;
else if(myWin[k] == 2) myScore[i][j] += 400;
else if(myWin[k] == 3) myScore[i][j] += 2000;
else if(myWin[k] == 4) myScore[i][j] += 10000;
// 电脑进攻
if(computerWin[k] == 1) computerScore[i][j] += 220;
else if(computerWin[k] == 2) computerScore[i][j] += 420;
else if(computerWin[k] == 3) computerScore[i][j] += 2100;
else if(computerWin[k] == 4) computerScore[i][j] += 20000;
注意这里的细节:
- 攻守兼备:它同时计算了"封堵玩家"的分数和"自己进攻"的分数。
- 权重差异:注意看,同样是连3子,电脑进攻的分数 (2100) 略高于拦截玩家的分数 (2000)。这说明 AI 被设定为**"在确保安全的情况下,优先进攻"**的激进策略。
- 绝杀判断:当达到 4 子时,分数直接跳跃到万级,确保 AI 能够识别"冲四"和"防守冲四"的最高优先级。
两年前,GPT-4 生成的代码往往只是随机找空位,或者仅仅判断周围一格是否有子。而现在的 Gemini 3 Pro 直接写出了一个具备贪心算法雏形的 AI。
3. 两年的技术跨越:我们经历了什么?
通过这个五子棋 Demo,我们可以清晰地看到 AI 进化的三个维度:
1. 上下文窗口与推理深度的质变
两年前,我们需要把大任务拆解成小任务喂给 AI。现在,Gemini 3 Pro 的上下文窗口和推理能力允许它在一次输出中处理全局依赖 。它清楚定义在第 10 行的 CSS 类名需要在第 50 行的 HTML 中使用,以及第 100 行的 JS 逻辑要操作第 20 行的 Canvas DOM。这种全局一致性是最大的进步。
2. 从"模仿者"到"工程师"
以前的 AI 像是在 Stack Overflow 上搜索代码片段并拼接(常常拼错)。现在的 AI 更像是一个工程师。它考虑了:
- UI 交互 :点击后稍作延迟 (
setTimeout) 再让电脑落子,为了让 UI 刷新,避免浏览器假死。 - 边界条件 :游戏结束后锁定点击事件 (
if (over) return)。 - 代码规范:变量命名清晰,注释详尽。
3. 多模态理解的反哺
虽然这次是纯代码生成,但 Gemini 3 Pro 对颜色的运用(木纹色、渐变棋子)暗示了它对物理世界"五子棋"视觉样貌的深刻理解。它不仅仅是在写代码,它是在"构建"一个它认知中的游戏场景。
4. 结语
两年前,我在 Juejin 文章的结尾可能还在感叹:"AI 辅助编程的时代来了,但还需要人来把关。"
今天,看着 Gemini 3 Pro 生成的这个无需修改、直接运行、甚至有点难赢的五子棋游戏,我必须更新我的观点:AI 已经从"副驾驶"开始尝试握住方向盘了。
对于开发者而言,这意味着门槛的再一次提高和降低------编写基础代码的门槛降到了零,但理解复杂系统、优化算法权重的门槛依然存在。
附:Gemini 3 Pro 生成的完整代码与效果图

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>五子棋人机对战</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
background-color: #f0f0f0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
h1 {
color: #333;
margin-bottom: 10px;
}
#game-container {
position: relative;
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
border-radius: 4px;
padding: 10px;
background-color: #dcb35c; /* 木纹底色 */
}
canvas {
background-color: #dcb35c;
cursor: pointer;
display: block;
}
.status-bar {
margin-top: 20px;
display: flex;
gap: 20px;
align-items: center;
}
#status {
font-size: 1.2em;
font-weight: bold;
color: #333;
}
button {
padding: 10px 20px;
font-size: 1em;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
transition: background 0.3s;
}
button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<h1>五子棋 - 人机对战</h1>
<div id="game-container">
<canvas id="chess" width="450" height="450"></canvas>
</div>
<div class="status-bar">
<div id="status">当前回合: 玩家 (黑棋)</div>
<button onclick="restartGame()">重新开始</button>
</div>
<script>
// 配置参数
const canvas = document.getElementById('chess');
const context = canvas.getContext('2d');
const statusText = document.getElementById('status');
const gridSize = 30; // 格子大小
const padding = 15; // 棋盘边缘留白
const boardSize = 15; // 15x15 棋盘
let chessBoard = []; // 棋盘状态数组 0:空, 1:黑(玩家), 2:白(电脑)
let me = true; // true: 玩家回合, false: 电脑回合
let over = false; // 游戏结束标志
//赢法数组
let wins = [];
//赢法统计数组
let myWin = [];
let computerWin = [];
let count = 0; // 赢法总数
// 初始化游戏
window.onload = function() {
initGame();
};
function initGame() {
// 重置变量
me = true;
over = false;
chessBoard = [];
statusText.innerText = "当前回合: 玩家 (黑棋)";
statusText.style.color = "#333";
// 初始化棋盘数组
for(let i = 0; i < boardSize; i++){
chessBoard[i] = [];
for(let j = 0; j < boardSize; j++){
chessBoard[i][j] = 0;
}
}
// 初始化赢法统计
initWins();
// 绘制棋盘
drawChessBoard();
}
// 绘制棋盘网格
function drawChessBoard() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.strokeStyle = "#555";
for (let i = 0; i < boardSize; i++) {
// 竖线
context.moveTo(padding + i * gridSize, padding);
context.lineTo(padding + i * gridSize, canvas.height - padding);
context.stroke();
// 横线
context.moveTo(padding, padding + i * gridSize);
context.lineTo(canvas.width - padding, padding + i * gridSize);
context.stroke();
}
}
// 绘制棋子
function oneStep(i, j, me) {
context.beginPath();
context.arc(padding + i * gridSize, padding + j * gridSize, 13, 0, 2 * Math.PI);
context.closePath();
let gradient = context.createRadialGradient(
padding + i * gridSize + 2, padding + j * gridSize - 2, 13,
padding + i * gridSize + 2, padding + j * gridSize - 2, 0
);
if (me) { // 黑棋
gradient.addColorStop(0, "#0a0a0a");
gradient.addColorStop(1, "#636766");
} else { // 白棋
gradient.addColorStop(0, "#d1d1d1");
gradient.addColorStop(1, "#f9f9f9");
}
context.fillStyle = gradient;
context.fill();
}
// 玩家点击落子
canvas.onclick = function(e) {
if (over || !me) return;
let x = e.offsetX;
let y = e.offsetY;
let i = Math.floor(x / gridSize);
let j = Math.floor(y / gridSize);
if (chessBoard[i][j] == 0) {
oneStep(i, j, true);
chessBoard[i][j] = 1; // 1代表黑棋
// 检查玩家是否胜利
for(let k = 0; k < count; k++){
if(wins[i][j][k]){
myWin[k]++;
computerWin[k] = 6; // 设置异常值,因为对方不可能在这个赢法上赢了
if(myWin[k] == 5){
statusText.innerText = "恭喜你!你赢了!";
statusText.style.color = "red";
over = true;
}
}
}
if (!over) {
me = !me;
statusText.innerText = "当前回合: 电脑 (思考中...)";
setTimeout(computerAI, 50); // 稍作延迟,让UI刷新
}
}
}
// 电脑AI逻辑
function computerAI() {
let myScore = []; // 玩家分数(防守)
let computerScore = []; // 电脑分数(进攻)
let max = 0; // 最高分
let u = 0, v = 0; // 最佳落子点
// 初始化分数数组
for(let i = 0; i < boardSize; i++){
myScore[i] = [];
computerScore[i] = [];
for(let j = 0; j < boardSize; j++){
myScore[i][j] = 0;
computerScore[i][j] = 0;
}
}
// 遍历棋盘所有空点进行评分
for(let i = 0; i < boardSize; i++){
for(let j = 0; j < boardSize; j++){
if(chessBoard[i][j] == 0){
// 遍历所有赢法
for(let k = 0; k < count; k++){
if(wins[i][j][k]){
// 计算拦截玩家的分数
if(myWin[k] == 1) myScore[i][j] += 200;
else if(myWin[k] == 2) myScore[i][j] += 400;
else if(myWin[k] == 3) myScore[i][j] += 2000;
else if(myWin[k] == 4) myScore[i][j] += 10000;
// 计算电脑进攻的分数
if(computerWin[k] == 1) computerScore[i][j] += 220;
else if(computerWin[k] == 2) computerScore[i][j] += 420;
else if(computerWin[k] == 3) computerScore[i][j] += 2100;
else if(computerWin[k] == 4) computerScore[i][j] += 20000;
}
}
// 综合评分,决定最佳落子
// 只要该点总分比当前最高分高,就更新
if(myScore[i][j] > max){
max = myScore[i][j];
u = i; v = j;
} else if(myScore[i][j] == max){
if(computerScore[i][j] > computerScore[u][v]){
u = i; v = j;
}
}
if(computerScore[i][j] > max){
max = computerScore[i][j];
u = i; v = j;
} else if(computerScore[i][j] == max){
if(myScore[i][j] > myScore[u][v]){
u = i; v = j;
}
}
}
}
}
// 电脑落子
oneStep(u, v, false);
chessBoard[u][v] = 2; // 2代表白棋
// 检查电脑是否胜利
for(let k = 0; k < count; k++){
if(wins[u][v][k]){
computerWin[k]++;
myWin[k] = 6;
if(computerWin[k] == 5){
statusText.innerText = "遗憾,电脑赢了!";
statusText.style.color = "blue";
over = true;
}
}
}
if (!over) {
me = !me;
statusText.innerText = "当前回合: 玩家 (黑棋)";
}
}
// 初始化赢法数组(这是算法核心:预先计算所有可能的连线)
function initWins(){
wins = [];
for(let i=0; i<boardSize; i++){
wins[i] = [];
for(let j=0; j<boardSize; j++){
wins[i][j] = [];
}
}
count = 0;
// 横线赢法
for(let i=0; i<boardSize; i++){
for(let j=0; j<boardSize-4; j++){
for(let k=0; k<5; k++){
wins[i][j+k][count] = true;
}
count++;
}
}
// 竖线赢法
for(let i=0; i<boardSize; i++){
for(let j=0; j<boardSize-4; j++){
for(let k=0; k<5; k++){
wins[j+k][i][count] = true;
}
count++;
}
}
// 斜线赢法 ( \ )
for(let i=0; i<boardSize-4; i++){
for(let j=0; j<boardSize-4; j++){
for(let k=0; k<5; k++){
wins[i+k][j+k][count] = true;
}
count++;
}
}
// 反斜线赢法 ( / )
for(let i=0; i<boardSize-4; i++){
for(let j=boardSize-1; j>3; j--){
for(let k=0; k<5; k++){
wins[i+k][j-k][count] = true;
}
count++;
}
}
// 初始化统计数组
for(let i=0; i<count; i++){
myWin[i] = 0;
computerWin[i] = 0;
}
}
function restartGame() {
initGame();
}
</script>
</body>
</html>