1. 项目概述:当俄罗斯方块遇上AI决策
这个项目本质上是在传统俄罗斯方块游戏的基础上,引入了一套基于权重评估算法的AI决策系统。不同于普通俄罗斯方块游戏完全依赖玩家手动操作,这个版本能够自动分析当前方块状态,计算最优落点,实现"自己玩自己"的效果。
俄罗斯方块作为经典游戏,其核心规则简单明了:随机下落的七种不同形状方块(称为Tetromino),玩家通过旋转和移动操作,使方块在底部堆叠时尽可能填满水平行。当某一行被完全填满时,该行消除并获得分数。游戏结束条件是方块堆叠到顶部。
而本项目的关键创新点在于:
- 用JavaScript完全在浏览器端实现游戏逻辑
- 引入权重评估算法替代人工决策
- 保持传统游戏体验的同时增加AI观察模式
- 可视化展示AI的决策过程
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心算法设计:权重评估体系剖析
2.1 评估维度选择
要让AI玩好俄罗斯方块,需要建立一套科学的评估体系。经过多次实验验证,我们确定了以下关键评估维度:
- 行消除潜力:评估当前放置位置对后续行消除的贡献度
- 高度均衡性:计算放置后各列高度的标准差
- 空洞数量:统计被封闭无法消除的空格数量
- 粗糙度:相邻列高度差的绝对值之和
- 紧迫度:最高列高度与游戏区域顶部的距离
2.2 权重分配方案
每个评估维度需要分配不同的权重值,经过数百局游戏的参数调优,最终确定的权重分配如下:
| 评估维度 | 权重系数 | 计算方式 |
|---|---|---|
| 行消除潜力 | 0.35 | 潜在可消除行数 × 2^(连续消除数) |
| 高度均衡性 | 0.25 | 1 / (标准差 + 1) |
| 空洞数量 | 0.20 | -0.1 × 空洞数 |
| 粗糙度 | 0.15 | -0.05 × 总高度差 |
| 紧迫度 | 0.05 | -0.01 × (最高高度)^2 |
提示:权重系数总和为1,负值表示该指标越小越好
2.3 决策流程实现
完整的AI决策流程如下:
- 生成当前方块所有可能的放置状态(考虑所有旋转和水平位置)
- 对每个候选状态计算上述5个评估维度的值
- 加权求和得到总分
- 选择总分最高的放置方案
- 执行移动和旋转操作
javascript复制function evaluatePosition(board, piece) {
// 模拟放置方块
const simulatedBoard = placePiece(board, piece);
// 计算各项指标
const lines = checkLineClears(simulatedBoard);
const heightStd = calculateHeightStd(simulatedBoard);
const holes = countHoles(simulatedBoard);
const roughness = calculateRoughness(simulatedBoard);
const urgency = maxHeight(simulatedBoard);
// 加权计算总分
const score =
0.35 * lineClearScore(lines) +
0.25 * (1 / (heightStd + 1)) +
0.20 * (-0.1 * holes) +
0.15 * (-0.05 * roughness) +
0.05 * (-0.01 * urgency * urgency);
return score;
}
3. 游戏实现关键技术点
3.1 游戏核心数据结构
采用二维数组表示游戏板状态是最直观的方案:
javascript复制const ROWS = 20;
const COLS = 10;
let board = Array(ROWS).fill().map(() => Array(COLS).fill(0));
其中:
- 0表示空格
- 1-7表示不同颜色的方块
- 当前下落方块单独存储位置和旋转状态
3.2 方块旋转算法
俄罗斯方块的旋转是游戏逻辑中最复杂的部分之一。我们采用矩阵旋转结合偏移补偿的方案:
javascript复制function rotate(piece) {
// 创建新矩阵
const N = piece.length;
const newPiece = Array(N).fill().map(() => Array(N).fill(0));
// 矩阵旋转
for (let i = 0; i < N; i++) {
for (let j = 0; j < N; j++) {
newPiece[j][N-1-i] = piece[i][j];
}
}
// 检查旋转后是否碰撞
if (!collision(newPiece)) {
return newPiece;
}
// 尝试偏移补偿(墙踢)
const kicks = [[0,0], [-1,0], [1,0], [0,-1]];
for (const [dx, dy] of kicks) {
if (!collision(newPiece, dx, dy)) {
currentX += dx;
currentY += dy;
return newPiece;
}
}
// 无法旋转则返回原状态
return piece;
}
3.3 游戏循环与渲染
使用requestAnimationFrame实现流畅的游戏循环:
javascript复制let lastTime = 0;
const dropInterval = 1000; // 初始下落速度
function gameLoop(timestamp) {
const deltaTime = timestamp - lastTime;
if (deltaTime > dropInterval) {
moveDown();
lastTime = timestamp;
}
render();
requestAnimationFrame(gameLoop);
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制已落下方块
drawBoard();
// 绘制当前方块
drawPiece();
// 绘制预测位置
if (showPrediction) {
drawGhost();
}
}
4. AI实现细节与优化
4.1 决策速度优化
原始算法需要评估所有可能位置,在JavaScript中可能造成卡顿。我们采用以下优化:
- 位置预计算:提前计算方块的旋转状态和可能位置
- 剪枝策略:当某位置明显不优时提前终止评估
- Web Workers:将计算任务放到后台线程
javascript复制// 预计算所有可能的旋转状态
const pieceRotations = {};
TETROMINOES.forEach(piece => {
pieceRotations[piece.name] = [piece.shape];
let rotated = rotateMatrix(piece.shape);
while (!matrixEquals(rotated, piece.shape)) {
pieceRotations[piece.name].push(rotated);
rotated = rotateMatrix(rotated);
}
});
// 在Worker中进行计算
const aiWorker = new Worker('ai-worker.js');
aiWorker.onmessage = (e) => {
const { move } = e.data;
executeMove(move);
};
4.2 自适应难度调整
为了让AI表现更接近人类水平,我们实现了动态难度调整:
- 根据游戏进度调整下落速度
- 随机引入"思考延迟"模拟人类反应时间
- 按概率故意选择次优方案
javascript复制function getDynamicDelay() {
const level = Math.floor(clearedLines / 10);
const baseDelay = Math.max(100, 1000 - level * 50);
// 添加随机延迟
const randomDelay = Math.random() * 200;
// 10%概率增加额外延迟
const extraDelay = Math.random() < 0.1 ? 300 : 0;
return baseDelay + randomDelay + extraDelay;
}
4.3 可视化决策过程
为了帮助理解AI的思考过程,我们添加了决策可视化:
- 用不同颜色显示评估分数
- 绘制最佳落点的"幽灵方块"
- 实时显示各项评估指标值
javascript复制function drawEvaluation() {
const cellSize = 20;
const padding = 2;
possiblePositions.forEach(pos => {
const x = pos.x * cellSize;
const y = pos.y * cellSize;
// 根据分数设置颜色
const hue = Math.floor(pos.score * 120); // 0-120对应红到绿
ctx.fillStyle = `hsl(${hue}, 80%, 50%)`;
ctx.globalAlpha = 0.5;
ctx.fillRect(x + padding, y + padding,
cellSize - 2*padding, cellSize - 2*padding);
});
}
5. 完整实现步骤
5.1 HTML基础结构
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>AI俄罗斯方块</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
background: #f0f0f0;
}
#game-container {
display: flex;
margin-top: 20px;
}
#tetris {
border: 2px solid #333;
background: #111;
}
#side-panel {
margin-left: 20px;
width: 200px;
}
.control-panel {
margin-bottom: 20px;
padding: 10px;
background: #ddd;
border-radius: 5px;
}
</style>
</head>
<body>
<h1>AI俄罗斯方块</h1>
<div id="game-container">
<canvas id="tetris" width="200" height="400"></canvas>
<div id="side-panel">
<div class="control-panel">
<button id="start-btn">开始游戏</button>
<button id="ai-toggle">AI模式</button>
</div>
<div class="stats-panel">
<p>分数: <span id="score">0</span></p>
<p>消除行数: <span id="lines">0</span></p>
<p>当前等级: <span id="level">1</span></p>
</div>
<div class="ai-info">
<h3>AI决策信息</h3>
<div id="ai-decision"></div>
</div>
</div>
</div>
<script src="tetris.js"></script>
</body>
</html>
5.2 JavaScript核心实现
javascript复制// tetris.js
const canvas = document.getElementById('tetris');
const ctx = canvas.getContext('2d');
const scale = 20; // 每个方块的像素大小
const ROWS = 20;
const COLS = 10;
// 游戏状态
let board = createBoard();
let score = 0;
let gameOver = false;
let isAI = false;
let aiInterval;
// 方块定义
const TETROMINOES = [
{ name: 'I', shape: [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]], color: 'cyan' },
{ name: 'J', shape: [[1,0,0], [1,1,1], [0,0,0]], color: 'blue' },
// 其他方块定义...
];
// 初始化游戏板
function createBoard() {
return Array(ROWS).fill().map(() => Array(COLS).fill(0));
}
// 游戏主循环
function update() {
if (gameOver) return;
draw();
if (isAI) {
clearInterval(aiInterval);
aiInterval = setInterval(makeAIMove, getDynamicDelay());
}
requestAnimationFrame(update);
}
// AI决策函数
function makeAIMove() {
const moves = generatePossibleMoves();
const bestMove = evaluateMoves(moves);
executeMove(bestMove);
updateAIDisplay(bestMove);
}
// 启动游戏
document.getElementById('start-btn').addEventListener('click', () => {
resetGame();
update();
});
// 切换AI模式
document.getElementById('ai-toggle').addEventListener('click', () => {
isAI = !isAI;
this.textContent = isAI ? '手动模式' : 'AI模式';
if (isAI) {
aiInterval = setInterval(makeAIMove, getDynamicDelay());
} else {
clearInterval(aiInterval);
}
});
5.3 权重调优实战经验
经过大量测试,总结出以下调优经验:
- 初期重点:游戏早期应更关注高度均衡性(权重0.3-0.4),避免过早堆高
- 中期平衡:游戏中期需要平衡消除和高度控制(各项权重均匀分配)
- 后期紧急:当高度接近危险区域时,紧迫度权重应动态增加到0.1-0.15
- 连续奖励:连续消除时应临时提高行消除潜力权重(如每次连续消除增加0.05)
实现动态权重调整:
javascript复制function getDynamicWeights() {
const maxHeight = getMaxHeight();
const dangerLevel = Math.max(0, maxHeight - 15) / 5; // 0-1
return {
lineClear: 0.35 - dangerLevel * 0.1,
heightBalance: 0.25 + dangerLevel * 0.05,
holes: 0.20,
roughness: 0.15 - dangerLevel * 0.05,
urgency: 0.05 + dangerLevel * 0.1
};
}
6. 常见问题与解决方案
6.1 方块旋转穿墙问题
现象:方块在靠近墙壁旋转时可能穿墙或位置异常
解决方案:
- 实现墙踢机制(Wall Kick)
- 旋转前预检测碰撞
- 限制旋转后的位置修正范围
javascript复制function rotate(piece) {
const newPiece = rotateMatrix(piece);
const kicks = [
[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]
];
for (const [dx, dy] of kicks) {
if (!collision(newPiece, dx, dy)) {
currentX += dx;
currentY += dy;
return newPiece;
}
}
return piece; // 无法旋转则保持原状
}
6.2 AI决策卡顿问题
现象:AI思考时游戏明显卡顿
优化方案:
- 限制评估深度
- 使用setTimeout分步计算
- 预计算常见位置评估值
javascript复制function optimizeAICalculation() {
// 只评估可见区域上方的位置
const topVisibleRow = getHighestBlock();
const evaluationDepth = Math.min(5, ROWS - topVisibleRow);
// 分步计算
let bestScore = -Infinity;
let bestMove = null;
let i = 0;
function evaluateNext() {
if (i >= possibleMoves.length) {
executeMove(bestMove);
return;
}
const move = possibleMoves[i];
const score = evaluatePosition(move);
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
i++;
setTimeout(evaluateNext, 0);
}
evaluateNext();
}
6.3 移动设备适配问题
挑战:触控操作与AI模式的切换
解决方案:
- 添加触摸事件支持
- 优化按钮大小和间距
- 实现手势操作
javascript复制// 触摸事件处理
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
function handleTouchStart(e) {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
e.preventDefault();
}
function handleTouchMove(e) {
if (!touchStartX || !touchStartY) return;
const touchEndX = e.touches[0].clientX;
const touchEndY = e.touches[0].clientY;
const dx = touchEndX - touchStartX;
const dy = touchEndY - touchStartY;
if (Math.abs(dx) > Math.abs(dy)) {
if (dx > 0) moveRight();
else moveLeft();
} else {
if (dy > 0) moveDown();
else rotate();
}
touchStartX = null;
touchStartY = null;
e.preventDefault();
}
7. 项目扩展方向
7.1 机器学习优化权重
当前权重是手动设置的,可以考虑用遗传算法自动优化:
- 随机生成多组权重参数
- 让每组参数进行多局游戏
- 选择表现最好的参数进行"繁殖"
- 加入随机变异
- 重复迭代优化
javascript复制class GeneticOptimizer {
constructor(populationSize = 20) {
this.population = Array(populationSize).fill().map(() => this.randomWeights());
}
randomWeights() {
const weights = {
lineClear: Math.random(),
heightBalance: Math.random(),
holes: Math.random(),
roughness: Math.random(),
urgency: Math.random()
};
// 归一化
const sum = Object.values(weights).reduce((a, b) => a + b);
for (const key in weights) {
weights[key] /= sum;
}
return weights;
}
evaluate(weights, games = 10) {
let totalScore = 0;
for (let i = 0; i < games; i++) {
totalScore += runGameWithWeights(weights);
}
return totalScore / games;
}
nextGeneration() {
// 评估当前种群
const scores = this.population.map(w => this.evaluate(w));
// 选择前50%
const sorted = this.population
.map((w, i) => ({ weights: w, score: scores[i] }))
.sort((a, b) => b.score - a.score);
const survivors = sorted.slice(0, this.population.length / 2);
// 繁殖新一代
const newPopulation = [];
while (newPopulation.length < this.population.length) {
const parent1 = survivors[Math.floor(Math.random() * survivors.length)];
const parent2 = survivors[Math.floor(Math.random() * survivors.length)];
const child = this.crossover(parent1.weights, parent2.weights);
newPopulation.push(this.mutate(child));
}
this.population = newPopulation;
return sorted[0].score; // 返回最佳得分
}
crossover(weights1, weights2) {
const child = {};
for (const key in weights1) {
child[key] = Math.random() < 0.5 ? weights1[key] : weights2[key];
}
return child;
}
mutate(weights, rate = 0.1) {
const mutated = {...weights};
if (Math.random() < rate) {
const keys = Object.keys(mutated);
const keyToMutate = keys[Math.floor(Math.random() * keys.length)];
mutated[keyToMutate] = Math.max(0, mutated[keyToMutate] + (Math.random() - 0.5) * 0.2);
// 重新归一化
const sum = Object.values(mutated).reduce((a, b) => a + b);
for (const key in mutated) {
mutated[key] /= sum;
}
}
return mutated;
}
}
7.2 多人对战模式扩展
在AI基础上实现多人对战功能:
- WebSocket实现实时对战
- 攻击机制:消除行数转换为攻击行数
- AI可以作为陪练或替代掉线玩家
javascript复制// 简单的WebSocket对战实现
const socket = new WebSocket('wss://game-server.example.com');
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
switch (message.type) {
case 'OPPONENT_MOVE':
updateOpponentBoard(message.board);
break;
case 'ATTACK':
addGarbageLines(message.lines);
break;
case 'GAME_OVER':
showGameResult(message.winner);
break;
}
};
function sendMove() {
const message = {
type: 'PLAYER_MOVE',
board: getBoardState(),
lines: lastClearedLines
};
socket.send(JSON.stringify(message));
}
function sendAttack(lines) {
const message = {
type: 'ATTACK',
lines: lines
};
socket.send(JSON.stringify(message));
}
7.3 3D可视化升级
使用Three.js实现3D版俄罗斯方块:
- 将2D方块转换为3D模型
- 添加物理引擎模拟下落
- 实现更丰富的视觉效果
javascript复制// 使用Three.js的基本设置
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建3D方块
function create3DBlock(type, x, y) {
const geometry = new THREE.BoxGeometry(0.9, 0.9, 0.9);
const material = new THREE.MeshPhongMaterial({
color: getColor(type),
shininess: 30
});
const cube = new THREE.Mesh(geometry, material);
cube.position.set(x - 5, 10 - y, 0);
scene.add(cube);
return cube;
}
// 3D游戏循环
function animate() {
requestAnimationFrame(animate);
// 更新3D方块位置
currentPiece3D.position.x = currentX - 5;
currentPiece3D.position.y = 10 - currentY;
renderer.render(scene, camera);
}
animate();
这个HTML5俄罗斯方块项目通过引入权重评估算法,实现了游戏的自动决策功能。从核心算法设计到具体实现,再到优化和扩展,展示了如何将传统游戏与AI技术相结合。实际开发中最大的挑战在于权重参数的调优,这需要大量的测试和迭代。通过可视化决策过程和动态难度调整,使得AI行为更加透明和可理解。
