Cialtion commited on
Commit
16d6043
·
verified ·
1 Parent(s): 22b966f

Update demos/flappy_bird.html

Browse files
Files changed (1) hide show
  1. demos/flappy_bird.html +470 -0
demos/flappy_bird.html ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Flappy Bird AI - SimpleTool Demo</title>
7
+ <style>
8
+ *{margin:0;padding:0;box-sizing:border-box}
9
+ body{background:linear-gradient(180deg,#1a1a2e 0%,#16213e 100%);min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:'Segoe UI',system-ui,sans-serif;color:#fff}
10
+ .container{display:flex;gap:20px;align-items:flex-start}
11
+ .game-wrapper{display:flex;flex-direction:column;align-items:center}
12
+ .title{font-size:28px;font-weight:bold;margin-bottom:15px;background:linear-gradient(90deg,#ffd700,#ff6b6b);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
13
+ .stats-bar{display:flex;gap:20px;margin-bottom:10px;font-size:14px}
14
+ .stat{padding:8px 16px;background:rgba(255,215,0,.1);border:1px solid #ffd700;border-radius:20px}
15
+ .stat span{color:#ffd700;font-weight:bold}
16
+ .game-area{position:relative}
17
+ canvas{border:2px solid #ffd700;box-shadow:0 0 30px rgba(255,215,0,.3);border-radius:8px}
18
+ .controls{margin-top:15px;display:flex;gap:10px}
19
+ .btn{padding:10px 25px;font-size:14px;font-weight:bold;background:transparent;border:2px solid #ffd700;color:#ffd700;cursor:pointer;border-radius:6px;transition:all .2s}
20
+ .btn:hover{background:#ffd700;color:#000}
21
+ .btn.stop{border-color:#f44;color:#f44}
22
+ .btn.stop:hover{background:#f44;color:#fff}
23
+ .panel{width:320px;background:rgba(0,0,0,.8);border:1px solid #ffd700;border-radius:10px;padding:15px;font-size:12px}
24
+ .panel h3{color:#ffd700;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid #333}
25
+ .config-row{display:flex;align-items:center;justify-content:space-between;margin:8px 0}
26
+ .config-row label{color:#aaa}
27
+ .config-row input{width:200px;padding:6px 10px;background:#111;border:1px solid #ffd700;color:#fff;border-radius:4px;font-size:12px}
28
+ .config-row select{background:#111;border:1px solid #ffd700;color:#fff;padding:6px;border-radius:4px}
29
+ .status-row{display:flex;align-items:center;gap:8px;margin:10px 0}
30
+ .status-dot{width:10px;height:10px;border-radius:50%;background:#666}
31
+ .status-dot.ok{background:#0f0;box-shadow:0 0 8px #0f0}
32
+ .status-dot.err{background:#f44}
33
+ .log-section{max-height:180px;overflow-y:auto;background:#050505;border-radius:6px;padding:10px;margin-top:10px}
34
+ .log-entry{padding:4px 8px;margin:2px 0;background:rgba(255,215,0,.05);border-left:3px solid #ffd700;font-family:monospace;font-size:11px}
35
+ .log-entry.flap{border-color:#0f0;background:rgba(0,255,0,.1)}
36
+ .log-entry.wait{border-color:#888}
37
+ .log-entry .action{font-weight:bold}
38
+ .log-entry .ms{color:#f0f}
39
+ .env-display{background:#111;padding:10px;border-radius:6px;font-family:monospace;font-size:10px;color:#888;margin-top:10px;white-space:pre-wrap;max-height:120px;overflow-y:auto}
40
+ .overlay{position:absolute;inset:0;background:rgba(0,0,0,.9);display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:8px}
41
+ .overlay.hidden{display:none}
42
+ .overlay h2{font-size:36px;color:#ffd700;margin-bottom:10px}
43
+ .overlay .score-display{font-size:24px;color:#0ff;margin:10px 0}
44
+ .high-score{color:#f0f;font-size:14px;margin-top:5px}
45
+ </style>
46
+ </head>
47
+ <body>
48
+ <div class="container">
49
+ <div class="game-wrapper">
50
+ <div class="title">🐦 FLAPPY BIRD AI</div>
51
+ <div class="stats-bar">
52
+ <div class="stat">Score: <span id="score">0</span></div>
53
+ <div class="stat">Best: <span id="best">0</span></div>
54
+ <div class="stat">Flaps: <span id="flaps">0</span></div>
55
+ <div class="stat">Avg: <span id="avg-latency">--</span>ms</div>
56
+ </div>
57
+ <div class="game-area">
58
+ <canvas id="game" width="400" height="600"></canvas>
59
+ <div class="overlay" id="overlay">
60
+ <h2 id="overlay-title">FLAPPY BIRD AI</h2>
61
+ <div class="score-display" id="overlay-score"></div>
62
+ <div class="high-score" id="overlay-best"></div>
63
+ <button class="btn" id="start-btn">START</button>
64
+ </div>
65
+ </div>
66
+ <div class="controls">
67
+ <button class="btn" id="restart-btn">RESTART</button>
68
+ <button class="btn stop" id="stop-btn">STOP</button>
69
+ </div>
70
+ </div>
71
+ <div class="panel">
72
+ <h3>⚙️ Configuration</h3>
73
+ <div class="config-row">
74
+ <label>Server URL</label>
75
+ </div>
76
+ <div class="config-row">
77
+ <input type="text" id="server-url" value="http://localhost:8899">
78
+ </div>
79
+ <div class="status-row">
80
+ <div class="status-dot" id="status-dot"></div>
81
+ <span id="status-text">Click Start to connect</span>
82
+ </div>
83
+ <div class="config-row">
84
+ <label>Difficulty</label>
85
+ <select id="difficulty">
86
+ <option value="easy">Easy (wide gaps)</option>
87
+ <option value="normal" selected>Normal</option>
88
+ <option value="hard">Hard (narrow gaps)</option>
89
+ </select>
90
+ </div>
91
+ <div class="config-row">
92
+ <label>AI Decision Rate</label>
93
+ <select id="decision-rate">
94
+ <option value="1">Every frame</option>
95
+ <option value="3" selected>Every 3 frames</option>
96
+ <option value="5">Every 5 frames</option>
97
+ </select>
98
+ </div>
99
+ <h3>📊 AI Decision Log</h3>
100
+ <div class="log-section" id="log-section"></div>
101
+ <div class="env-display" id="env-display">Environment will show here...</div>
102
+ </div>
103
+ </div>
104
+ <script>
105
+ const $ = id => document.getElementById(id);
106
+
107
+ // Tool Definition - 二元决策
108
+ const TOOLS = [{
109
+ type: "function",
110
+ function: {
111
+ name: "act",
112
+ description: "Flappy bird action. Flap to gain height, wait to fall by gravity.",
113
+ parameters: {
114
+ type: "object",
115
+ properties: {
116
+ action: {
117
+ type: "string",
118
+ enum: ["flap", "wait"],
119
+ description: "flap=jump up ~50px, wait=fall by gravity ~3px/frame"
120
+ }
121
+ },
122
+ required: ["action"]
123
+ }
124
+ }
125
+ }];
126
+
127
+ class FCClient {
128
+ constructor(url) { this.url = url.replace(/\/$/, ''); }
129
+ async health() {
130
+ try {
131
+ const r = await fetch(`${this.url}/health`, { signal: AbortSignal.timeout(3000) });
132
+ const d = await r.json();
133
+ return { ok: d.loaded === true || d.status === 'ok' };
134
+ } catch(e) { return { ok: false }; }
135
+ }
136
+ async call(messages, env) {
137
+ const t0 = performance.now();
138
+ try {
139
+ const r = await fetch(`${this.url}/v1/function_call`, {
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify({ messages, tools: TOOLS, environment: env })
143
+ });
144
+ const d = await r.json();
145
+ const ms = performance.now() - t0;
146
+ let action = String(d.args?.action || d.heads?.arg1 || '').toLowerCase().trim();
147
+ if (!['flap','wait'].includes(action)) action = 'wait';
148
+ return { action, ms, raw: d };
149
+ } catch(e) {
150
+ return { action: 'wait', ms: performance.now() - t0, error: e.message };
151
+ }
152
+ }
153
+ }
154
+
155
+ class FlappyGame {
156
+ constructor() {
157
+ this.canvas = $('game');
158
+ this.ctx = this.canvas.getContext('2d');
159
+ this.W = this.canvas.width;
160
+ this.H = this.canvas.height;
161
+ this.client = null;
162
+ this.running = false;
163
+ this.bestScore = 0;
164
+ this.latencies = [];
165
+ this.reset();
166
+ }
167
+
168
+ getDifficultyParams() {
169
+ const d = $('difficulty').value;
170
+ return {
171
+ easy: { gapSize: 180, pipeSpeed: 2.5 },
172
+ normal: { gapSize: 150, pipeSpeed: 3 },
173
+ hard: { gapSize: 120, pipeSpeed: 4 }
174
+ }[d];
175
+ }
176
+
177
+ reset() {
178
+ const params = this.getDifficultyParams();
179
+ this.bird = { x: 80, y: this.H / 2, vy: 0, radius: 15 };
180
+ this.gravity = 0.4;
181
+ this.flapStrength = -7;
182
+ this.pipes = [];
183
+ this.pipeWidth = 60;
184
+ this.gapSize = params.gapSize;
185
+ this.pipeSpeed = params.pipeSpeed;
186
+ this.pipeInterval = 100;
187
+ this.frameCount = 0;
188
+ this.score = 0;
189
+ this.flapCount = 0;
190
+ this.latencies = [];
191
+ this.pendingDecision = null;
192
+ this.decisionRate = parseInt($('decision-rate').value);
193
+ this.spawnPipe();
194
+ this.updateStats();
195
+ this.render();
196
+ }
197
+
198
+ spawnPipe() {
199
+ const minY = 80;
200
+ const maxY = this.H - this.gapSize - 80;
201
+ const gapY = minY + Math.random() * (maxY - minY);
202
+ this.pipes.push({
203
+ x: this.W + 50,
204
+ gapY: gapY,
205
+ passed: false
206
+ });
207
+ }
208
+
209
+ async start() {
210
+ this.client = new FCClient($('server-url').value);
211
+ const health = await this.client.health();
212
+ $('status-dot').className = 'status-dot ' + (health.ok ? 'ok' : 'err');
213
+ $('status-text').textContent = health.ok ? 'Connected' : 'Connection failed';
214
+ if (!health.ok) return;
215
+
216
+ this.reset();
217
+ this.running = true;
218
+ $('overlay').classList.add('hidden');
219
+ this.loop();
220
+ }
221
+
222
+ stop() {
223
+ this.running = false;
224
+ }
225
+
226
+ async loop() {
227
+ if (!this.running) return;
228
+
229
+ this.frameCount++;
230
+
231
+ // AI 决策(按频率)
232
+ if (this.frameCount % this.decisionRate === 0) {
233
+ await this.aiDecision();
234
+ }
235
+
236
+ // 物理更新
237
+ this.bird.vy += this.gravity;
238
+ this.bird.y += this.bird.vy;
239
+
240
+ // 管道移动
241
+ for (const pipe of this.pipes) {
242
+ pipe.x -= this.pipeSpeed;
243
+ if (!pipe.passed && pipe.x + this.pipeWidth < this.bird.x) {
244
+ pipe.passed = true;
245
+ this.score++;
246
+ }
247
+ }
248
+
249
+ // 移除离开屏幕的管道
250
+ this.pipes = this.pipes.filter(p => p.x > -this.pipeWidth);
251
+
252
+ // 生成新管道
253
+ if (this.pipes.length === 0 || this.pipes[this.pipes.length - 1].x < this.W - this.pipeInterval) {
254
+ this.spawnPipe();
255
+ }
256
+
257
+ // 碰撞检测
258
+ if (this.checkCollision()) {
259
+ this.gameOver();
260
+ return;
261
+ }
262
+
263
+ this.updateStats();
264
+ this.render();
265
+ requestAnimationFrame(() => this.loop());
266
+ }
267
+
268
+ async aiDecision() {
269
+ const bird = this.bird;
270
+ const nextPipe = this.pipes.find(p => p.x + this.pipeWidth > bird.x);
271
+
272
+ if (!nextPipe) return;
273
+
274
+ const pipeCenter = nextPipe.gapY + this.gapSize / 2;
275
+ const birdToCenter = pipeCenter - bird.y;
276
+ const distToPipe = nextPipe.x - bird.x;
277
+
278
+ // 预测未来位置
279
+ const futureY = bird.y + bird.vy * 5 + 0.5 * this.gravity * 25;
280
+ const willBeAboveGap = futureY < nextPipe.gapY + 20;
281
+ const willBeBelowGap = futureY > nextPipe.gapY + this.gapSize - 20;
282
+
283
+ // Environment
284
+ const env = [
285
+ `bird_y=${Math.round(bird.y)}`,
286
+ `bird_vy=${bird.vy.toFixed(1)}`,
287
+ `pipe_x=${Math.round(distToPipe)}`,
288
+ `gap_top=${Math.round(nextPipe.gapY)}`,
289
+ `gap_bottom=${Math.round(nextPipe.gapY + this.gapSize)}`,
290
+ `gap_center=${Math.round(pipeCenter)}`,
291
+ `bird_to_center=${Math.round(birdToCenter)}`,
292
+ `predicted_y=${Math.round(futureY)}`,
293
+ `ceiling=${0}`,
294
+ `floor=${this.H}`
295
+ ];
296
+
297
+ // Query - 明确的决策指令
298
+ let instruction;
299
+ if (bird.y < 30) {
300
+ instruction = `DANGER: Too high! WAIT to fall.`;
301
+ } else if (bird.y > this.H - 50) {
302
+ instruction = `DANGER: Too low! FLAP now!`;
303
+ } else if (distToPipe < 100) {
304
+ // 即将穿越管道
305
+ if (bird.y > pipeCenter + 20) {
306
+ instruction = `Approaching pipe, below center. FLAP to rise!`;
307
+ } else if (bird.y < pipeCenter - 30) {
308
+ instruction = `Approaching pipe, above center. WAIT to fall.`;
309
+ } else {
310
+ instruction = `Aligned with gap. ${bird.vy > 2 ? 'FLAP to slow descent.' : 'WAIT to maintain.'}`;
311
+ }
312
+ } else {
313
+ // 远离管道,调整高度
314
+ if (birdToCenter > 40) {
315
+ instruction = `Far from pipe. Below gap center by ${Math.round(birdToCenter)}px. FLAP!`;
316
+ } else if (birdToCenter < -40) {
317
+ instruction = `Far from pipe. Above gap center. WAIT.`;
318
+ } else {
319
+ instruction = `Good position. ${bird.vy > 3 ? 'FLAP to control speed.' : 'WAIT.'}`;
320
+ }
321
+ }
322
+
323
+ const query = `Flappy bird. Screen ${this.W}x${this.H}. ${instruction} Call act(flap) or act(wait).`;
324
+ $('env-display').textContent = `Query: ${query}\n\nEnv:\n${env.join('\n')}`;
325
+
326
+ const result = await this.client.call([{ role: 'user', content: query }], env);
327
+ this.latencies.push(result.ms);
328
+ this.logDecision(result);
329
+
330
+ if (result.action === 'flap') {
331
+ this.bird.vy = this.flapStrength;
332
+ this.flapCount++;
333
+ }
334
+ }
335
+
336
+ checkCollision() {
337
+ const b = this.bird;
338
+ // 天花板和地板
339
+ if (b.y - b.radius < 0 || b.y + b.radius > this.H) return true;
340
+
341
+ // 管道碰撞
342
+ for (const pipe of this.pipes) {
343
+ if (b.x + b.radius > pipe.x && b.x - b.radius < pipe.x + this.pipeWidth) {
344
+ if (b.y - b.radius < pipe.gapY || b.y + b.radius > pipe.gapY + this.gapSize) {
345
+ return true;
346
+ }
347
+ }
348
+ }
349
+ return false;
350
+ }
351
+
352
+ gameOver() {
353
+ this.running = false;
354
+ if (this.score > this.bestScore) this.bestScore = this.score;
355
+ $('overlay').classList.remove('hidden');
356
+ $('overlay-title').textContent = 'GAME OVER';
357
+ $('overlay-score').textContent = `Score: ${this.score}`;
358
+ $('overlay-best').textContent = `Best: ${this.bestScore}`;
359
+ $('best').textContent = this.bestScore;
360
+ }
361
+
362
+ updateStats() {
363
+ $('score').textContent = this.score;
364
+ $('flaps').textContent = this.flapCount;
365
+ const avg = this.latencies.length ? Math.round(this.latencies.reduce((a,b)=>a+b,0)/this.latencies.length) : '--';
366
+ $('avg-latency').textContent = avg;
367
+ }
368
+
369
+ logDecision(result) {
370
+ const entry = document.createElement('div');
371
+ entry.className = 'log-entry ' + result.action;
372
+ entry.innerHTML = `<span class="action">${result.action.toUpperCase()}</span> <span class="ms">${Math.round(result.ms)}ms</span>`;
373
+ $('log-section').insertBefore(entry, $('log-section').firstChild);
374
+ while ($('log-section').children.length > 30) $('log-section').lastChild.remove();
375
+ }
376
+
377
+ render() {
378
+ const ctx = this.ctx;
379
+
380
+ // 背景渐变
381
+ const grad = ctx.createLinearGradient(0, 0, 0, this.H);
382
+ grad.addColorStop(0, '#1a1a2e');
383
+ grad.addColorStop(1, '#16213e');
384
+ ctx.fillStyle = grad;
385
+ ctx.fillRect(0, 0, this.W, this.H);
386
+
387
+ // 星星背景
388
+ ctx.fillStyle = 'rgba(255,255,255,0.3)';
389
+ for (let i = 0; i < 30; i++) {
390
+ const x = (i * 37 + this.frameCount * 0.2) % this.W;
391
+ const y = (i * 23) % this.H;
392
+ ctx.beginPath();
393
+ ctx.arc(x, y, 1, 0, Math.PI * 2);
394
+ ctx.fill();
395
+ }
396
+
397
+ // 管道
398
+ for (const pipe of this.pipes) {
399
+ // 上管道
400
+ const topGrad = ctx.createLinearGradient(pipe.x, 0, pipe.x + this.pipeWidth, 0);
401
+ topGrad.addColorStop(0, '#0a5');
402
+ topGrad.addColorStop(0.5, '#0d8');
403
+ topGrad.addColorStop(1, '#0a5');
404
+ ctx.fillStyle = topGrad;
405
+ ctx.fillRect(pipe.x, 0, this.pipeWidth, pipe.gapY);
406
+ ctx.fillRect(pipe.x - 5, pipe.gapY - 20, this.pipeWidth + 10, 20);
407
+
408
+ // 下管道
409
+ ctx.fillRect(pipe.x, pipe.gapY + this.gapSize, this.pipeWidth, this.H - pipe.gapY - this.gapSize);
410
+ ctx.fillRect(pipe.x - 5, pipe.gapY + this.gapSize, this.pipeWidth + 10, 20);
411
+
412
+ // 发光边缘
413
+ ctx.strokeStyle = '#0f0';
414
+ ctx.lineWidth = 2;
415
+ ctx.shadowBlur = 10;
416
+ ctx.shadowColor = '#0f0';
417
+ ctx.strokeRect(pipe.x, 0, this.pipeWidth, pipe.gapY);
418
+ ctx.strokeRect(pipe.x, pipe.gapY + this.gapSize, this.pipeWidth, this.H - pipe.gapY - this.gapSize);
419
+ }
420
+
421
+ // 小鸟
422
+ ctx.shadowBlur = 20;
423
+ ctx.shadowColor = '#ffd700';
424
+ ctx.fillStyle = '#ffd700';
425
+ ctx.beginPath();
426
+ ctx.arc(this.bird.x, this.bird.y, this.bird.radius, 0, Math.PI * 2);
427
+ ctx.fill();
428
+
429
+ // 小鸟眼睛
430
+ ctx.shadowBlur = 0;
431
+ ctx.fillStyle = '#fff';
432
+ ctx.beginPath();
433
+ ctx.arc(this.bird.x + 5, this.bird.y - 3, 5, 0, Math.PI * 2);
434
+ ctx.fill();
435
+ ctx.fillStyle = '#000';
436
+ ctx.beginPath();
437
+ ctx.arc(this.bird.x + 7, this.bird.y - 3, 2, 0, Math.PI * 2);
438
+ ctx.fill();
439
+
440
+ // 小鸟嘴
441
+ ctx.fillStyle = '#f44';
442
+ ctx.beginPath();
443
+ ctx.moveTo(this.bird.x + this.bird.radius, this.bird.y);
444
+ ctx.lineTo(this.bird.x + this.bird.radius + 10, this.bird.y + 3);
445
+ ctx.lineTo(this.bird.x + this.bird.radius, this.bird.y + 6);
446
+ ctx.closePath();
447
+ ctx.fill();
448
+
449
+ // 翅膀(根据速度调整角度)
450
+ ctx.fillStyle = '#e5c100';
451
+ ctx.save();
452
+ ctx.translate(this.bird.x - 5, this.bird.y);
453
+ ctx.rotate(this.bird.vy * 0.05);
454
+ ctx.fillRect(-10, -3, 10, 8);
455
+ ctx.restore();
456
+
457
+ ctx.shadowBlur = 0;
458
+ }
459
+ }
460
+
461
+ const game = new FlappyGame();
462
+
463
+ $('start-btn').onclick = () => game.start();
464
+ $('restart-btn').onclick = () => { game.stop(); game.start(); };
465
+ $('stop-btn').onclick = () => { game.stop(); $('overlay').classList.remove('hidden'); $('overlay-title').textContent = 'PAUSED'; };
466
+
467
+ game.render();
468
+ </script>
469
+ </body>
470
+ </html>