mobileNet/game/贪吃蛇/snake_game.html
2025-08-20 16:41:21 +08:00

829 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 姿态控制贪吃蛇</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #a8c0ff, #3f2b96);
color: #fff;
margin: 0;
padding: 20px;
}
h1 {
margin-bottom: 20px;
color: #fff;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
#game-area {
display: flex;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
}
#snake-canvas-container {
position: relative;
border: 5px solid #00f2fe;
box-shadow: 0 0 20px #00f2fe, 0 0 40px #00f2fe;
background-color: #333;
border-radius: 10px;
overflow: hidden;
}
#snakeCanvas {
background-color: #333;
display: block;
}
#game-info-panel {
background: rgba(0, 0, 0, 0.6);
padding: 25px;
border-radius: 10px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
min-width: 300px;
display: flex;
flex-direction: column;
gap: 15px;
}
.info-item {
font-size: 1.1em;
display: flex;
justify-content: space-between;
}
.info-item span:first-child {
font-weight: bold;
color: #a8c0ff;
}
#status-display {
background: #222;
padding: 10px;
border-radius: 5px;
font-size: 0.9em;
min-height: 30px;
color: #00f2fe;
text-align: center;
}
.gesture-indicator {
font-size: 1.2em;
color: #00ff00;
margin-top: 10px;
padding: 5px;
background: #2c2c2c;
border-radius: 5px;
text-align: center;
min-height: 25px;
}
.control-buttons {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
}
.control-buttons button {
padding: 12px 25px;
font-size: 1.1em;
border: none;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
color: white;
}
#startBtn {
background: linear-gradient(45deg, #2196F3, #21CBF3);
}
#startBtn:hover {
background: linear-gradient(45deg, #1976D2, #1AC8EF);
transform: translateY(-2px);
}
#startBtn:disabled {
background: #607d8b;
cursor: not-allowed;
opacity: 0.7;
}
#importModelBtn {
background: linear-gradient(45deg, #FFC107, #FFEB3B);
}
#importModelBtn:hover {
background: linear-gradient(45deg, #FFA000, #FFD700);
transform: translateY(-2px);
}
#importModelBtn:disabled {
background: #9E9E9E;
cursor: not-allowed;
opacity: 0.7;
}
#video-feed-container {
position: absolute;
top: 20px;
left: 20px;
width: 320px;
height: 240px;
border: 3px solid #6a1b9a;
border-radius: 10px;
background: #000;
overflow: hidden;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.6);
z-index: 100; /* 确保在游戏画布之上 */
}
#video-feed-container video {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 7px;
}
#video-feed-container canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 7px;
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
color: white;
font-size: 2em;
text-align: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.overlay.active {
opacity: 1;
visibility: visible;
}
.overlay h2 {
font-size: 3em;
margin-bottom: 20px;
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.5);
}
.overlay button {
margin-top: 30px;
font-size: 1.5em;
padding: 15px 30px;
background: linear-gradient(45deg, #28a745, #20c997);
color: white;
border: none;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3);
transition: transform 0.2s, box-shadow 0.2s;
}
.overlay button:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
}
.overlay button:active {
transform: translateY(0);
}
.instructions {
background: rgba(0, 0, 0, 0.6);
padding: 15px;
border-radius: 8px;
margin-top: 20px;
font-size: 0.9em;
line-height: 1.6;
width: 80%;
max-width: 500px;
text-align: left;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
animation: fadeIn 1s ease-out;
}
.instructions h3 {
color: #00f2fe;
margin-bottom: 10px;
text-align: center;
}
.instructions ul {
list-style: none;
padding-left: 0;
}
.instructions li {
margin-bottom: 5px;
display: flex;
align-items: center;
}
.instructions li strong {
color: #a8c0ff;
min-width: 80px;
}
.instructions li span {
flex-grow: 1;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
<!-- 引入 TensorFlow.js 核心库 -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.20.0/dist/tf.min.js"></script>
<!-- 引入 pose-detection 库 (包含 MoveNet) -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/pose-detection@2.1.3/dist/pose-detection.min.js"></script>
<!-- 引入 KNN 分类器 -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/knn-classifier@1.2.2/dist/knn-classifier.min.js"></script>
</head>
<body>
<h1>AI 姿态控制贪吃蛇</h1>
<div id="game-area">
<div id="video-feed-container">
<video id="videoFeed" autoplay muted playsinline></video>
<canvas id="poseCanvas"></canvas>
</div>
<div id="snake-canvas-container">
<canvas id="snakeCanvas"></canvas>
</div>
<div id="game-info-panel">
<div class="info-item"><span>得分:</span> <span id="score">0</span></div>
<div class="info-item"><span>姿态识别:</span> <span id="currentGesture">未识别</span></div>
<div class="info-item"><span>控制指令:</span> <span id="controlCommand">静止</span></div>
<div id="status-display">等待模型导入...</div>
<div class="gesture-indicator" id="gestureConfidence"></div>
<div class="control-buttons">
<button id="startBtn" disabled>开始游戏 (Enter)</button>
<input type="file" id="fileImporter" accept=".json" style="display: none;">
<button id="importModelBtn">导入模型</button>
</div>
<div class="instructions">
<h3>姿态控制说明</h3>
<ul>
<li><strong>向上:</strong> <span>双手举过头顶</span></li>
<li><strong>向下:</strong> <span>双手放在身体两侧或下垂</span></li>
<li><strong>向左:</strong> <span>左手平举</span></li>
<li><strong>向右:</strong> <span>右手平举</span></li>
<li><strong>静止:</strong> <span>保持站立姿势 (无特定动作)</span></li>
</ul>
<p style="font-size:0.8em;text-align:center;margin-top:10px;color:#ccc;">(确保在摄像头画面中能清晰识别全身)</p>
</div>
</div>
</div>
<!-- 游戏结束/开始 Overlay -->
<div id="gameOverOverlay" class="overlay">
<h2 id="overlayText">游戏结束!</h2>
<p>最终得分: <span id="finalScore">0</span></p>
<button id="restartBtn">重新开始 (Enter)</button>
</div>
<script>
// ==========================================================
// 全局变量和 DOM 引用
// ==========================================================
const videoElement = document.getElementById('videoFeed');
const poseCanvas = document.getElementById('poseCanvas');
const poseCtx = poseCanvas.getContext('2d');
const snakeCanvas = document.getElementById('snakeCanvas');
const snakeCtx = snakeCanvas.getContext('2d');
const scoreDisplay = document.getElementById('score');
const currentGestureDisplay = document.getElementById('currentGesture');
const controlCommandDisplay = document.getElementById('controlCommand');
const statusDisplay = document.getElementById('status-display');
const gestureConfidenceDisplay = document.getElementById('gestureConfidence');
const startBtn = document.getElementById('startBtn');
const importModelBtn = document.getElementById('importModelBtn');
const fileImporter = document.getElementById('fileImporter');
const gameOverOverlay = document.getElementById('gameOverOverlay');
const overlayText = document.getElementById('overlayText');
const finalScoreDisplay = document.getElementById('finalScore');
const restartBtn = document.getElementById('restartBtn');
let detector; // MoveNet 姿态检测器
let classifier; // KNN 分类器
let isPoseDetectionReady = false; // 姿态检测系统是否就绪
let isModelLoaded = false; // KNN 模型是否已加载
let animationFrameId; // 用于姿态检测的 requestAnimationFrame ID
let gameLoopId; // 用于贪吃蛇游戏循环的 setInterval ID
let gameStatus = 'initial'; // 'initial', 'loading', 'ready', 'playing', 'paused', 'gameOver'
let currentDetectedClassId = null; // 当前检测到的姿态分类ID
let currentConfidence = 0; // 当前检测到的置信度
// 用于映射姿态分类ID到游戏方向
// 请确保这些ID与您训练模型时使用的类别ID一致
const gestureClassToGameDirection = {
'0': 'UP', // 类别0 -> 向上
'1': 'DOWN', // 类别1 -> 向下
'2': 'LEFT', // 类别2 -> 向左
'3': 'RIGHT' // 类别3 -> 向右
};
// 用于显示给用户的类别名称加载模型时从JSON中读取
let importedClassNames = {};
let snakeDirection = 'RIGHT'; // 贪吃蛇当前方向
// ==========================================================
// 贪吃蛇游戏设置
// ==========================================================
const GRID_SIZE = 20; // 网格大小(像素)
const CANVAS_WIDTH = 600;
const CANVAS_HEIGHT = 400;
snakeCanvas.width = CANVAS_WIDTH;
snakeCanvas.height = CANVAS_HEIGHT;
let snake = [{ x: 10, y: 10 }]; // 蛇的身体 (以网格坐标表示)
let food = {}; // 食物位置
let score = 0;
let gameSpeed = 300; // 游戏速度(毫秒)
// ==========================================================
// 初始化函数
// ==========================================================
document.addEventListener('DOMContentLoaded', initApp);
async function initApp() {
updateGameStatus('loading');
statusDisplay.textContent = '正在加载 MoveNet 模型和摄像头...';
startBtn.disabled = true;
importModelBtn.disabled = true;
try {
// 初始化 KNN 分类器
classifier = knnClassifier.create();
// 初始化 MoveNet 检测器
const detectorConfig = { modelType: poseDetection.movenet.modelType.SINGLEPOSE_LIGHTNING };
detector = await poseDetection.createDetector(poseDetection.SupportedModels.MoveNet, detectorConfig);
// 设置摄像头
await setupCamera();
isPoseDetectionReady = true;
statusDisplay.textContent = 'MoveNet 和摄像头已就绪。请导入姿态模型。';
importModelBtn.disabled = false; // 启用导入按钮
// 启动姿态检测循环(只进行检测和绘制,不预测,直到模型导入)
startPoseDetectionLoop();
// 绑定按钮事件
startBtn.addEventListener('click', startGame);
importModelBtn.addEventListener('click', () => fileImporter.click());
fileImporter.addEventListener('change', handleModelImport);
restartBtn.addEventListener('click', resetGame);
// 键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
if (gameStatus === 'ready' || gameStatus === 'gameOver') {
startBtn.click(); // 模拟点击开始按钮
} else if (gameStatus === 'playing') {
// 可选:添加暂停功能
}
}
});
updateGameUI();
} catch (error) {
console.error("应用初始化失败:", error);
statusDisplay.textContent = `初始化失败: ${error.message}`;
alert(`应用初始化失败: ${error.message}\n请检查摄像头权限或网络连接。`);
}
}
// ==========================================================
// 摄像头和姿态检测相关
// ==========================================================
async function setupCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: videoElement.width }, // 使用videoFeed的width/height
height: { ideal: videoElement.height },
facingMode: 'user'
}
});
videoElement.srcObject = stream;
return new Promise((resolve, reject) => {
videoElement.onloadedmetadata = () => {
videoElement.play().then(() => {
// 确保 poseCanvas 和 videoFeed 尺寸匹配
poseCanvas.width = videoElement.videoWidth;
poseCanvas.height = videoElement.videoHeight;
resolve();
}).catch(reject);
};
setTimeout(() => reject(new Error('摄像头元数据加载或播放超时')), 10000);
});
} catch (error) {
if (error.name === 'NotAllowedError') {
throw new Error('用户拒绝了摄像头权限。');
} else if (error.name === 'NotFoundError') {
throw new Error('未找到摄像头设备。');
} else {
throw error;
}
}
}
async function startPoseDetectionLoop() {
if (!isPoseDetectionReady) return;
async function detectAndPredict() {
try {
const poses = await detector.estimatePoses(videoElement, { flipHorizontal: true }); // 镜像摄像头
poseCtx.clearRect(0, 0, poseCanvas.width, poseCanvas.height);
if (poses && poses.length > 0) {
drawPose(poses[0]); // 绘制检测到的姿态
if (isModelLoaded && gameStatus === 'playing') { // 仅在游戏进行中时才预测并控制贪吃蛇
const poseTensor = flattenPose(poses[0]);
if (classifier.getNumClasses() > 0) {
const prediction = await classifier.predictClass(poseTensor);
poseTensor.dispose(); // 释放张量内存
const predictedClassId = prediction.label;
const confidence = prediction.confidences[predictedClassId];
currentConfidence = (confidence * 100).toFixed(1);
// 设定一个置信度阈值,例如 70%
if (confidence > 0.70) {
currentDetectedClassId = predictedClassId;
const gameDirection = gestureClassToGameDirection[predictedClassId];
if (gameDirection) {
changeSnakeDirection(gameDirection); // 控制贪吃蛇移动
controlCommandDisplay.textContent = gameDirection;
} else {
controlCommandDisplay.textContent = '未知方向';
}
} else {
currentDetectedClassId = null; // 置信度不足,视为未识别
currentConfidence = 0;
controlCommandDisplay.textContent = '静止 (置信度不足)';
}
} else {
poseTensor.dispose(); // 确保张量被释放
currentDetectedClassId = null;
currentConfidence = 0;
controlCommandDisplay.textContent = '静止 (模型无数据)';
}
} else if (!isModelLoaded) {
currentDetectedClassId = null;
currentConfidence = 0;
controlCommandDisplay.textContent = '静止 (等待模型)';
} else if (gameStatus !== 'playing') {
currentDetectedClassId = null;
currentConfidence = 0;
controlCommandDisplay.textContent = '静止';
}
} else {
currentDetectedClassId = null;
currentConfidence = 0;
currentGestureDisplay.textContent = '未检测到姿态';
controlCommandDisplay.textContent = '静止';
}
updateGameUI(); // 更新显示
} catch (error) {
console.error('姿态检测或预测出错:', error);
statusDisplay.textContent = `检测错误: ${error.message}`;
// 如果错误持续发生,考虑停止循环或提供更多反馈
} finally {
animationFrameId = requestAnimationFrame(detectAndPredict);
}
}
animationFrameId = requestAnimationFrame(detectAndPredict); // 启动循环
}
// 展平姿态关键点并归一化
function flattenPose(pose) {
// 将所有关键点的 (x, y) 坐标展平为一个一维数组
// 并进行归一化 (除以视频的宽度和高度),使得特征独立于图像大小
const keypoints = pose.keypoints.map(p => [p.x / videoElement.videoWidth, p.y / videoElement.videoHeight]).flat();
return tf.tensor(keypoints); // 转换为 TensorFlow 张量
}
// 绘制 MoveNet 姿态骨骼图
function drawPose(pose) {
if (pose.keypoints) {
// 绘制关键点
for (const keypoint of pose.keypoints) {
if (keypoint.score > 0.3) { // 仅绘制置信度高的关键点
poseCtx.beginPath();
poseCtx.arc(keypoint.x, keypoint.y, 5, 0, 2 * Math.PI);
poseCtx.fillStyle = '#00f2fe'; // 浅蓝色
poseCtx.fill();
}
}
// 绘制骨骼连接线
const adjacentPairs = poseDetection.util.getAdjacentPairs(poseDetection.SupportedModels.MoveNet);
adjacentPairs.forEach(([i, j]) => {
const kp1 = pose.keypoints[i];
const kp2 = pose.keypoints[j];
if (kp1.score > 0.3 && kp2.score > 0.3) { // 仅连接置信度高的关键点
poseCtx.beginPath();
poseCtx.moveTo(kp1.x, kp1.y);
poseCtx.lineTo(kp2.x, kp2.y);
poseCtx.strokeStyle = '#a8c0ff'; // 紫蓝色
poseCtx.lineWidth = 2;
poseCtx.stroke();
}
});
}
}
// ==========================================================
// 模型导入导出
// ==========================================================
async function handleModelImport(event) {
const file = event.target.files[0];
if (!file) return;
updateGameStatus('loading');
statusDisplay.textContent = '正在导入模型...';
startBtn.disabled = true;
importModelBtn.disabled = true;
try {
await loadModelFromFile(file);
statusDisplay.textContent = '姿态模型导入成功!';
isModelLoaded = true;
startBtn.disabled = false; // 启用开始按钮
importModelBtn.disabled = true; // 禁用导入按钮
updateGameStatus('ready');
} catch (error) {
console.error('模型导入失败:', error);
statusDisplay.textContent = `模型导入失败: ${error.message}`;
alert(`模型导入失败: ${error.message}\n请确保文件是正确的模型JSON文件。`);
startBtn.disabled = true; // 导入失败则不能开始
importModelBtn.disabled = false; // 可以再试一次导入
isModelLoaded = false;
updateGameStatus('initial');
} finally {
fileImporter.value = ''; // 清空文件输入
}
}
async function loadModelFromFile(file) {
// 这段逻辑与您之前在 script.js 中 `importModel` 的核心逻辑一致
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const loadedModelData = JSON.parse(e.target.result);
if (!loadedModelData || !loadedModelData.dataset || !loadedModelData.classMap) {
throw new Error('模型数据结构不正确 (缺少 classMap 或 dataset)。');
}
classifier.clearAllClasses();
const dataset = {};
let totalExamples = 0;
for (const classId in loadedModelData.dataset) {
const classData = loadedModelData.dataset[classId];
if (classData && classData.length > 0) {
if (classData.some(sample => !Array.isArray(sample) || sample.some(val => typeof val !== 'number'))) {
throw new Error(`类别 ${classId} 包含无效的样本数据 (不是数字数组)。`);
}
const tensors = classData.map(data => tf.tensor1d(data));
const stacked = tf.stack(tensors);
dataset[classId] = stacked;
totalExamples += classData.length;
tensors.forEach(t => t.dispose());
} else {
console.warn(`类别 ${classId} 没有样本数据。`);
}
}
classifier.setClassifierDataset(dataset);
importedClassNames = loadedModelData.classMap; // 更新类别名称映射
console.log(`模型加载成功!共加载 ${totalExamples} 个样本。`);
console.log('类别映射 (导入):', importedClassNames);
resolve();
} catch (error) {
reject(error);
}
};
reader.onerror = (error) => {
reject(new Error('文件读取失败。'));
};
reader.readAsText(file);
});
}
// ===================================
// 贪吃蛇游戏逻辑
// ===================================
function startGame() {
if (!isPoseDetectionReady) {
alert('姿态检测系统未准备就绪,请稍候。');
return;
}
if (!isModelLoaded) {
alert('请先导入姿态模型!');
return;
}
if (!classifier || classifier.getNumClasses() === 0) {
alert('已导入的模型中没有训练数据,请导入一个有效的模型文件。');
return;
}
gameOverOverlay.classList.remove('active');
updateGameStatus('playing');
resetGameState();
generateFood();
gameLoopId = setInterval(drawGame, gameSpeed); // 开始游戏循环
startBtn.disabled = true;
importModelBtn.disabled = true;
}
function resetGame() {
gameOverOverlay.classList.remove('active');
updateGameStatus('ready');
resetGameState(); // 重置游戏状态
snakeDirection = 'RIGHT'; // 重新开始时默认为向右
startBtn.disabled = false;
importModelBtn.disabled = true; // 模型加载后禁用导入按钮
updateGameUI();
}
function resetGameState() {
clearInterval(gameLoopId);
snake = [{ x: 10, y: 10 }]; // 蛇回到起始位置长度为1
food = {};
score = 0;
updateScoreDisplay();
}
function generateFood() {
food = {
x: Math.floor(Math.random() * (CANVAS_WIDTH / GRID_SIZE)),
y: Math.floor(Math.random() * (CANVAS_HEIGHT / GRID_SIZE))
};
// 确保食物不生成在蛇的身体上
for (let i = 0; i < snake.length; i++) {
if (food.x === snake[i].x && food.y === snake[i].y) {
generateFood(); // 重新生成
}
}
}
function drawGame() {
// 清空 snakeCanvas
snakeCtx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 绘制食物
snakeCtx.fillStyle = '#ff3333'; // 红色
snakeCtx.fillRect(food.x * GRID_SIZE, food.y * GRID_SIZE, GRID_SIZE, GRID_SIZE);
// 绘制蛇
for (let i = 0; i < snake.length; i++) {
snakeCtx.fillStyle = (i === 0) ? '#00cc00' : '#009900'; // 蛇头绿色,身体深绿
snakeCtx.fillRect(snake[i].x * GRID_SIZE, snake[i].y * GRID_SIZE, GRID_SIZE, GRID_SIZE);
snakeCtx.strokeStyle = '#222'; // 边框
snakeCtx.strokeRect(snake[i].x * GRID_SIZE, snake[i].y * GRID_SIZE, GRID_SIZE, GRID_SIZE);
}
// 移动蛇头
let headX = snake[0].x;
let headY = snake[0].y;
switch (snakeDirection) {
case 'LEFT': headX--; break;
case 'UP': headY--; break;
case 'RIGHT': headX++; break;
case 'DOWN': headY++; break;
}
// 碰撞检测
// 撞墙
if (headX < 0 || headX >= CANVAS_WIDTH / GRID_SIZE ||
headY < 0 || headY >= CANVAS_HEIGHT / GRID_SIZE) {
endGame();
return;
}
// 撞自己
for (let i = 1; i < snake.length; i++) {
if (headX === snake[i].x && headY === snake[i].y) {
endGame();
return;
}
}
// 吃到食物
if (headX === food.x && headY === food.y) {
score++;
updateScoreDisplay();
generateFood();
// 暂时不增加速度,保持稳定
} else {
// 如果没吃到食物,移除蛇尾,实现移动效果
snake.pop();
}
// 添加新蛇头
snake.unshift({ x: headX, y: headY });
}
function changeSnakeDirection(newDirection) {
// 防止蛇立即反向掉头
if (newDirection === 'LEFT' && snakeDirection !== 'RIGHT') snakeDirection = 'LEFT';
else if (newDirection === 'UP' && snakeDirection !== 'DOWN') snakeDirection = 'UP';
else if (newDirection === 'RIGHT' && snakeDirection !== 'LEFT') snakeDirection = 'RIGHT';
else if (newDirection === 'DOWN' && snakeDirection !== 'UP') snakeDirection = 'DOWN';
}
function endGame() {
updateGameStatus('gameOver');
clearInterval(gameLoopId);
overlayText.textContent = '游戏结束!';
finalScoreDisplay.textContent = score;
gameOverOverlay.classList.add('active');
startBtn.disabled = true; // 游戏结束后开始按钮不可用,直到点击重玩
importModelBtn.disabled = true; // 保持禁用
}
// ==========================================================
// UI 更新
// ==========================================================
function updateGameStatus(status) {
gameStatus = status;
console.log("Game status updated to:", gameStatus);
}
function updateScoreDisplay() {
scoreDisplay.textContent = score;
}
function updateGameUI() {
// 更新姿态识别显示
if (currentDetectedClassId !== null) {
const className = importedClassNames[currentDetectedClassId] || `未知类别 ${currentDetectedClassId}`;
currentGestureDisplay.textContent = `${className} (${currentConfidence}%)`;
gestureConfidenceDisplay.textContent = `置信度: ${currentConfidence}%`;
gestureConfidenceDisplay.style.color = currentConfidence > 70 ? '#00ff00' : '#ffaa00';
} else {
currentGestureDisplay.textContent = '未识别';
gestureConfidenceDisplay.textContent = '';
}
// 更新状态信息
if (gameStatus === 'initial') {
statusDisplay.textContent = '等待模型导入...';
} else if (gameStatus === 'loading') {
// 状态文本已经在 initApp 或 handleModelImport 中设置
} else if (gameStatus === 'ready') {
statusDisplay.textContent = '模型已加载点击“开始游戏”或按“Enter”键。';
} else if (gameStatus === 'playing') {
statusDisplay.textContent = '游戏进行中...';
} else if (gameStatus === 'gameOver') {
statusDisplay.textContent = '游戏结束点击“重新开始”或按“Enter”键。';
}
}
</script>
</body>
</html>