X commited on
Commit
49cc9aa
·
verified ·
1 Parent(s): 6881147

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +334 -709
app.py CHANGED
@@ -1,74 +1,144 @@
1
  """
2
- 🎮 ДВОЙНОЙ ПЛАТФОРМЕР: ИИ vs ИГРОК + ЗВУКИ + ЧАТ-БОТ
3
- Flask Web Server (порт 7860)
4
  """
5
- from flask import Flask, render_template_string, request, jsonify, send_from_directory
6
  import numpy as np
7
  import random
8
  import json
9
  import os
10
- import torch
11
- import torch.nn as nn
12
- import torch.optim as optim
13
- from collections import deque
14
  import time
15
  import threading
16
  import math
17
- import base64
 
 
 
18
  from PIL import Image
19
  import io
20
- import sys
21
-
22
- app = Flask(__name__)
23
 
24
  # =====================================================
25
- # 1. ЗВУКИ
26
  # =====================================================
27
 
28
- def generate_sound(freq=440, duration=0.1, waveform='sine'):
29
- """Генерирует звук в base64"""
30
- import struct
31
- import wave
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- sample_rate = 22050
34
- samples = int(sample_rate * duration)
 
 
 
 
 
35
 
36
- buffer = io.BytesIO()
37
- wav = wave.open(buffer, 'wb')
38
- wav.setnchannels(1)
39
- wav.setsampwidth(2)
40
- wav.setframerate(sample_rate)
41
 
42
- for i in range(samples):
43
- t = i / sample_rate
44
- if waveform == 'sine':
45
- val = int(32767 * 0.5 * math.sin(2 * math.pi * freq * t))
46
- elif waveform == 'square':
47
- val = int(32767 * 0.5 * (1 if math.sin(2 * math.pi * freq * t) > 0 else -1))
48
- else:
49
- f = freq + i * 200 / samples
50
- val = int(32767 * 0.5 * math.sin(2 * math.pi * f * t))
51
- wav.writeframes(struct.pack('<h', val))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- wav.close()
54
- buffer.seek(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- audio_data = base64.b64encode(buffer.read()).decode('utf-8')
57
- return f'data:audio/wav;base64,{audio_data}'
58
-
59
- SOUNDS = {
60
- 'jump': generate_sound(600, 0.08, 'sine'),
61
- 'coin': generate_sound(880, 0.05, 'sine'),
62
- 'die': generate_sound(200, 0.3, 'sweep')
63
- }
64
 
65
  # =====================================================
66
  # 2. ПЛАТФОРМЕР
67
  # =====================================================
68
 
69
- class InfinitePlatformer:
70
  def __init__(self, seed=None):
71
- self.width = 100
72
  self.height = 20
73
  self.seed = seed or random.randint(0, 999999)
74
  self.reset()
@@ -87,8 +157,8 @@ class InfinitePlatformer:
87
  self.coins = []
88
  self.obstacles = []
89
  self.velocity_y = 0
90
- self.gravity = 0.5
91
- self.jump_power = -8
92
  self.on_ground = False
93
  self.alive = True
94
  self.steps = 0
@@ -110,14 +180,9 @@ class InfinitePlatformer:
110
  obstacles = []
111
  enemies = []
112
  coins = []
113
-
114
  base_x = chunk_x * 30
115
 
116
- num_obstacles = random.randint(3, 6) + int(self.difficulty)
117
- num_enemies = random.randint(1, 3) + int(self.difficulty * 0.5)
118
- num_coins = random.randint(5, 10) + int(self.difficulty)
119
-
120
- for _ in range(num_obstacles):
121
  x = base_x + random.randint(5, 25)
122
  height = random.randint(1, 3 + int(self.difficulty * 0.5))
123
  width = random.randint(1, 3)
@@ -134,19 +199,19 @@ class InfinitePlatformer:
134
  'width': width, 'height': 1, 'is_pit': True
135
  })
136
 
137
- for _ in range(num_enemies):
138
  x = base_x + random.randint(5, 25)
139
  y = self.ground_level - 1
140
  enemy_type = random.choice(['walker', 'jumper'])
141
  enemies.append({
142
  'x': x, 'y': y, 'type': enemy_type,
143
  'direction': 1 if random.random() > 0.5 else -1,
144
- 'speed': 0.5 + random.random() * 0.5,
145
  'range': random.randint(3, 8),
146
  'start_x': x
147
  })
148
 
149
- for _ in range(num_coins):
150
  x = base_x + random.randint(2, 28)
151
  y = random.randint(5, self.ground_level - 2)
152
  coins.append({'x': x, 'y': y, 'collected': False})
@@ -156,7 +221,6 @@ class InfinitePlatformer:
156
  'enemies': enemies,
157
  'coins': coins
158
  }
159
-
160
  random.seed()
161
 
162
  def get_state(self):
@@ -206,12 +270,12 @@ class InfinitePlatformer:
206
  if abs(enemy['x'] - enemy['start_x']) > enemy['range']:
207
  enemy['direction'] *= -1
208
  elif enemy['type'] == 'jumper':
209
- enemy['y'] += math.sin(time.time() * enemy['speed'] * 2) * 0.5
210
  if enemy['y'] > self.ground_level - 1:
211
  enemy['y'] = self.ground_level - 1
212
 
213
  for enemy in self.entities:
214
- if abs(enemy['x'] - self.player[0]) < 1.0 and abs(enemy['y'] - self.player[1]) < 1.0:
215
  return True
216
  return False
217
 
@@ -219,7 +283,7 @@ class InfinitePlatformer:
219
  collected = 0
220
  for coin in self.coins:
221
  if not coin['collected']:
222
- if abs(coin['x'] - self.player[0]) < 1.5 and abs(coin['y'] - self.player[1]) < 1.5:
223
  coin['collected'] = True
224
  collected += 1
225
  return collected
@@ -240,9 +304,9 @@ class InfinitePlatformer:
240
  sound = None
241
 
242
  if action == 1:
243
- self.player[0] -= 0.5
244
  elif action == 2:
245
- self.player[0] += 0.5
246
 
247
  if action == 3 and self.on_ground:
248
  self.velocity_y = self.jump_power
@@ -307,39 +371,49 @@ class InfinitePlatformer:
307
  self.coins = [c for c in self.coins if abs(c['x'] - self.player[0]) < self.width]
308
 
309
  self.steps += 1
310
- if self.steps > 2000:
311
  return self.get_state(), self.score, True, sound
312
 
313
  reward = 1 + coins_collected * 5
314
  return self.get_state(), reward, False, sound
315
 
316
- def render(self, show_player=True):
 
317
  grid = np.zeros((self.height, 80, 3), dtype=np.uint8)
318
- grid.fill(200)
319
 
320
- grid[self.ground_level:self.ground_level+2, :] = [100, 80, 40]
 
 
321
 
 
 
 
 
322
  for obs in self.obstacles:
 
323
  if obs.get('is_pit', False):
324
- x = int(obs['x'] - self.player[0] + 40)
325
  for w in range(obs['width']):
326
  if 0 <= x+w < 80:
327
  grid[obs['y']:obs['y']+2, x+w] = [0, 0, 0]
328
  else:
329
- x = int(obs['x'] - self.player[0] + 40)
330
  for w in range(obs['width']):
331
  for h in range(obs['height']):
332
  sx = x + w
333
  sy = obs['y'] + h
334
  if 0 <= sx < 80 and 0 <= sy < self.height:
335
- grid[sy, sx] = [150, 120, 80]
336
 
 
337
  for enemy in self.entities:
338
  x = int(enemy['x'] - self.player[0] + 40)
339
  y = int(enemy['y'])
340
  if 0 <= x < 80 and 0 <= y < self.height:
341
- grid[y, x] = [255, 0, 0]
 
 
342
 
 
343
  for coin in self.coins:
344
  if not coin['collected']:
345
  x = int(coin['x'] - self.player[0] + 40)
@@ -347,103 +421,22 @@ class InfinitePlatformer:
347
  if 0 <= x < 80 and 0 <= y < self.height:
348
  grid[y, x] = [255, 215, 0]
349
 
350
- if show_player:
 
351
  px = 40
352
  py = int(self.player[1])
353
  if 0 <= py < self.height:
354
- grid[py, px] = [0, 255, 0]
355
- grid[py-1, px] = [0, 200, 0]
 
 
 
356
 
357
  img = Image.fromarray(grid, 'RGB')
358
  return img
359
 
360
  # =====================================================
361
- # 3. DQN АГЕНТ (упрощённый, без PyTorch если нет)
362
- # =====================================================
363
-
364
- class SimpleDQNAgent:
365
- def __init__(self, state_size=1600, action_size=4):
366
- self.state_size = state_size
367
- self.action_size = action_size
368
- self.epsilon = 1.0
369
- self.epsilon_min = 0.01
370
- self.epsilon_decay = 0.995
371
- self.memory = deque(maxlen=1000)
372
- self.total_steps = 0
373
- self.best_score = 0
374
-
375
- # Простая Q-таблица (для демо)
376
- self.q_table = {}
377
-
378
- def get_state_key(self, state):
379
- # Упрощаем состояние для хранения в Q-таблице
380
- flat = state.flatten() if hasattr(state, 'flatten') else state
381
- # Берём только первые 100 значений
382
- key = tuple(flat[:100].astype(int))
383
- return key
384
-
385
- def act(self, state):
386
- if np.random.rand() <= self.epsilon:
387
- return random.randrange(self.action_size)
388
-
389
- key = self.get_state_key(state)
390
- if key not in self.q_table:
391
- self.q_table[key] = [0] * self.action_size
392
- return np.argmax(self.q_table[key])
393
-
394
- def remember(self, state, action, reward, next_state, done):
395
- self.memory.append((state, action, reward, next_state, done))
396
- if len(self.memory) > 1000:
397
- self.memory.popleft()
398
-
399
- def replay(self):
400
- if len(self.memory) < 32:
401
- return
402
-
403
- batch = random.sample(self.memory, 32)
404
- for state, action, reward, next_state, done in batch:
405
- key = self.get_state_key(state)
406
- next_key = self.get_state_key(next_state)
407
-
408
- if key not in self.q_table:
409
- self.q_table[key] = [0] * self.action_size
410
- if next_key not in self.q_table:
411
- self.q_table[next_key] = [0] * self.action_size
412
-
413
- current_q = self.q_table[key][action]
414
- max_next_q = max(self.q_table[next_key])
415
- target_q = reward + 0.95 * max_next_q * (not done)
416
-
417
- self.q_table[key][action] += 0.1 * (target_q - current_q)
418
-
419
- if self.epsilon > self.epsilon_min:
420
- self.epsilon *= self.epsilon_decay
421
-
422
- self.total_steps += 1
423
-
424
- def train_episode(self):
425
- env = InfinitePlatformer()
426
- state = env.reset()
427
- total_reward = 0
428
- done = False
429
- steps = 0
430
-
431
- while not done and steps < 300:
432
- action = self.act(state)
433
- next_state, reward, done, _ = env.step(action)
434
- self.remember(state, action, reward, next_state, done)
435
- self.replay()
436
- state = next_state
437
- total_reward += reward
438
- steps += 1
439
-
440
- if total_reward > self.best_score:
441
- self.best_score = total_reward
442
-
443
- return total_reward, steps, env.distance
444
-
445
- # =====================================================
446
- # 4. ЧАТ-ПАМЯТЬ
447
  # =====================================================
448
 
449
  class ChatMemory:
@@ -460,583 +453,154 @@ class ChatMemory:
460
  with open('chat_data.json', 'w', encoding='utf-8') as f:
461
  json.dump(self.data, f, ensure_ascii=False, indent=2)
462
 
463
- def add(self, question, answer):
464
- self.data[question.lower()] = answer
465
  self.save_data()
466
- return f"✅ Добавлено: {question} -> {answer}"
467
-
468
- def find_best_match(self, question):
469
- question_lower = question.lower()
470
- if question_lower in self.data:
471
- return self.data[question_lower]
472
-
473
- words = question_lower.split()
474
- best_match = None
475
  best_score = 0
476
-
477
- for q, a in self.data.items():
478
- q_words = q.split()
479
- score = sum(1 for w in words if w in q_words)
480
  if score > best_score:
481
  best_score = score
482
- best_match = a
483
-
484
  if best_score >= len(words) * 0.4:
485
- return best_match
486
  return None
487
 
488
  # =====================================================
489
- # 5. ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
490
  # =====================================================
491
 
492
- agent = SimpleDQNAgent()
493
  chat_memory = ChatMemory()
494
  is_training = False
495
  current_seed = random.randint(0, 999999)
 
496
 
497
  # =====================================================
498
- # 6. HTML ШАБЛОН
499
  # =====================================================
500
 
501
- HTML_TEMPLATE = """
502
- <!DOCTYPE html>
503
- <html>
504
- <head>
505
- <title>🎮 AI vs Player Platformer</title>
506
- <meta charset="UTF-8">
507
- <style>
508
- * { margin: 0; padding: 0; box-sizing: border-box; }
509
- body {
510
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
511
- background: #1a1a2e;
512
- color: #eee;
513
- min-height: 100vh;
514
- display: flex;
515
- justify-content: center;
516
- padding: 20px;
517
- }
518
- .container {
519
- max-width: 1200px;
520
- width: 100%;
521
- }
522
- h1 {
523
- text-align: center;
524
- padding: 20px 0;
525
- background: linear-gradient(135deg, #e94560, #0f3460);
526
- -webkit-background-clip: text;
527
- -webkit-text-fill-color: transparent;
528
- font-size: 2.5em;
529
- }
530
- .subtitle {
531
- text-align: center;
532
- color: #aaa;
533
- margin-bottom: 20px;
534
- }
535
- .game-row {
536
- display: flex;
537
- gap: 20px;
538
- flex-wrap: wrap;
539
- }
540
- .game-col {
541
- flex: 1;
542
- min-width: 300px;
543
- background: #16213e;
544
- border-radius: 16px;
545
- padding: 15px;
546
- box-shadow: 0 8px 32px rgba(0,0,0,0.5);
547
- }
548
- .game-col h3 {
549
- text-align: center;
550
- margin-bottom: 10px;
551
- color: #e94560;
552
- }
553
- .game-col .label {
554
- text-align: center;
555
- font-size: 0.9em;
556
- color: #888;
557
- margin-top: 5px;
558
- }
559
- .game-canvas {
560
- width: 100%;
561
- aspect-ratio: 4/1;
562
- background: #0a0a1a;
563
- border-radius: 8px;
564
- image-rendering: pixelated;
565
- }
566
- .controls {
567
- display: flex;
568
- justify-content: center;
569
- gap: 15px;
570
- margin: 20px 0;
571
- flex-wrap: wrap;
572
- }
573
- .controls button {
574
- padding: 15px 35px;
575
- font-size: 1.2em;
576
- border: none;
577
- border-radius: 12px;
578
- cursor: pointer;
579
- transition: all 0.2s;
580
- font-weight: bold;
581
- }
582
- .controls button:hover {
583
- transform: scale(1.05);
584
- }
585
- .controls button:active {
586
- transform: scale(0.95);
587
- }
588
- .btn-left { background: #e94560; color: white; }
589
- .btn-right { background: #e94560; color: white; }
590
- .btn-jump { background: #0f3460; color: white; padding: 15px 50px; }
591
- .btn-reset { background: #533483; color: white; }
592
- .stats {
593
- background: #16213e;
594
- border-radius: 12px;
595
- padding: 15px;
596
- margin: 10px 0;
597
- text-align: center;
598
- font-size: 1.2em;
599
- }
600
- .tabs {
601
- display: flex;
602
- gap: 10px;
603
- margin: 20px 0;
604
- flex-wrap: wrap;
605
- }
606
- .tab {
607
- padding: 12px 25px;
608
- background: #16213e;
609
- border-radius: 10px;
610
- cursor: pointer;
611
- border: 2px solid transparent;
612
- transition: all 0.3s;
613
- }
614
- .tab:hover { border-color: #e94560; }
615
- .tab.active { border-color: #e94560; background: #1a1a3e; }
616
- .tab-content {
617
- background: #16213e;
618
- border-radius: 12px;
619
- padding: 20px;
620
- min-height: 200px;
621
- }
622
- .chat-input-area {
623
- display: flex;
624
- gap: 10px;
625
- margin-top: 15px;
626
- }
627
- .chat-input-area input {
628
- flex: 1;
629
- padding: 12px;
630
- border-radius: 8px;
631
- border: 1px solid #333;
632
- background: #0a0a1a;
633
- color: #eee;
634
- font-size: 1em;
635
- }
636
- .chat-input-area button {
637
- padding: 12px 25px;
638
- background: #e94560;
639
- color: white;
640
- border: none;
641
- border-radius: 8px;
642
- cursor: pointer;
643
- font-weight: bold;
644
- }
645
- .chat-messages {
646
- max-height: 300px;
647
- overflow-y: auto;
648
- padding: 10px;
649
- }
650
- .chat-msg {
651
- padding: 8px 12px;
652
- margin: 5px 0;
653
- border-radius: 8px;
654
- background: #0a0a1a;
655
- }
656
- .chat-msg.user { border-left: 3px solid #e94560; }
657
- .chat-msg.bot { border-left: 3px solid #0f3460; }
658
- .commands-hint {
659
- color: #666;
660
- font-size: 0.9em;
661
- padding: 10px;
662
- border-top: 1px solid #222;
663
- margin-top: 10px;
664
- }
665
- .commands-hint code {
666
- background: #0a0a1a;
667
- padding: 2px 8px;
668
- border-radius: 4px;
669
- color: #e94560;
670
- }
671
- @media (max-width: 700px) {
672
- .game-col { min-width: 100%; }
673
- .controls button { padding: 10px 20px; font-size: 1em; }
674
- }
675
- </style>
676
- </head>
677
- <body>
678
- <div class="container">
679
- <h1>🎮 AI vs Player Platformer</h1>
680
- <p class="subtitle">🤖 Нейросеть слева 🆚 Ты справа — одинаковый уровень!</p>
681
-
682
- <div class="game-row">
683
- <div class="game-col">
684
- <h3>🤖 ИИ играет</h3>
685
- <img id="aiCanvas" class="game-canvas" src="data:image/png;base64,{{ai_image}}" alt="AI Game">
686
- <div class="label">ИИ учится играть</div>
687
- </div>
688
- <div class="game-col">
689
- <h3>🎮 Ты играешь</h3>
690
- <img id="playerCanvas" class="game-canvas" src="data:image/png;base64,{{player_image}}" alt="Player Game">
691
- <div class="label">Ты — зелёный</div>
692
- </div>
693
- </div>
694
-
695
- <div class="stats" id="stats">
696
- 🤖 ИИ: {{ai_score}} &nbsp;|&nbsp; 🎮 Ты: {{player_score}} &nbsp;|&nbsp; 🪙 Монет: {{coins}}
697
- </div>
698
-
699
- <div class="controls">
700
- <button class="btn-left" onclick="move(1)">⬅️ Влево</button>
701
- <button class="btn-jump" onclick="move(3)">⬆️ Прыжок</button>
702
- <button class="btn-right" onclick="move(2)">➡️ Вправо</button>
703
- <button class="btn-reset" onclick="resetGame()">🔄 Новый уровень</button>
704
- </div>
705
-
706
- <div class="tabs">
707
- <div class="tab active" onclick="switchTab('chat')">💬 Чат</div>
708
- <div class="tab" onclick="switchTab('train')">🧠 Тренировка</div>
709
- <div class="tab" onclick="switchTab('stats')">📊 Статистика</div>
710
- </div>
711
-
712
- <div class="tab-content" id="tabContent">
713
- <div id="chatTab">
714
- <div class="chat-messages" id="chatMessages">
715
- <div class="chat-msg bot">🤖 Привет! Я нейросеть-помощник. Используй команды:</div>
716
- <div class="chat-msg bot"><code>/ai вопрос</code> — спросить</div>
717
- <div class="chat-msg bot"><code>/data вопрос|ответ</code> — обучить</div>
718
- <div class="chat-msg bot"><code>/stats</code> — статистика</div>
719
- <div class="chat-msg bot"><code>/train</code> — тренировка ИИ</div>
720
- </div>
721
- <div class="chat-input-area">
722
- <input type="text" id="chatInput" placeholder="Введите команду..." onkeydown="if(event.key==='Enter') sendChat()">
723
- <button onclick="sendChat()">Отправить</button>
724
- </div>
725
- <div class="commands-hint">
726
- 💡 Пример: <code>/ai Как пройти уровень?</code> или <code>/data привет|Здравствуй!</code>
727
- </div>
728
- </div>
729
- <div id="trainTab" style="display:none;">
730
- <h3>🧠 Тренировка нейросети</h3>
731
- <p>ИИ учится играть в платформер методом проб и ошибок.</p>
732
- <button onclick="startTrain()" style="padding:15px 40px;background:#e94560;color:white;border:none;border-radius:10px;font-size:1.2em;cursor:pointer;">
733
- 🚀 Запустить тренировку
734
- </button>
735
- <div id="trainStatus" style="margin-top:15px;color:#aaa;"></div>
736
- </div>
737
- <div id="statsTab" style="display:none;">
738
- <h3>📊 Статистика</h3>
739
- <div id="statsContent">
740
- <p>Загрузка...</p>
741
- </div>
742
- </div>
743
- </div>
744
-
745
- <div id="soundContainer"></div>
746
- </div>
747
-
748
- <script>
749
- let currentAction = 0;
750
- let soundEnabled = true;
751
-
752
- function move(action) {
753
- currentAction = action;
754
- fetch('/move', {
755
- method: 'POST',
756
- headers: {'Content-Type': 'application/json'},
757
- body: JSON.stringify({action: action})
758
- })
759
- .then(r => r.json())
760
- .then(data => {
761
- document.getElementById('aiCanvas').src = 'data:image/png;base64,' + data.ai_image;
762
- document.getElementById('playerCanvas').src = 'data:image/png;base64,' + data.player_image;
763
- document.getElementById('stats').innerHTML =
764
- '🤖 ИИ: ' + data.ai_score +
765
- ' &nbsp;|&nbsp; 🎮 Ты: ' + data.player_score +
766
- ' &nbsp;|&nbsp; 🪙 Монет: ' + data.coins;
767
- if (data.sound) {
768
- playSound(data.sound);
769
- }
770
- });
771
- }
772
-
773
- function resetGame() {
774
- fetch('/reset', {method: 'POST'})
775
- .then(r => r.json())
776
- .then(data => {
777
- document.getElementById('aiCanvas').src = 'data:image/png;base64,' + data.ai_image;
778
- document.getElementById('playerCanvas').src = 'data:image/png;base64,' + data.player_image;
779
- document.getElementById('stats').innerHTML =
780
- '🤖 ИИ: ' + data.ai_score +
781
- ' &nbsp;|&nbsp; 🎮 Ты: ' + data.player_score +
782
- ' &nbsp;|&nbsp; 🪙 Монет: ' + data.coins;
783
- });
784
- }
785
-
786
- function playSound(soundType) {
787
- if (!soundEnabled) return;
788
- const sounds = {
789
- 'jump': '{{sounds.jump}}',
790
- 'coin': '{{sounds.coin}}',
791
- 'die': '{{sounds.die}}'
792
- };
793
- if (sounds[soundType]) {
794
- const audio = new Audio(sounds[soundType]);
795
- audio.play().catch(() => {});
796
- }
797
- }
798
-
799
- function sendChat() {
800
- const input = document.getElementById('chatInput');
801
- const msg = input.value.trim();
802
- if (!msg) return;
803
- input.value = '';
804
-
805
- const container = document.getElementById('chatMessages');
806
- container.innerHTML += '<div class="chat-msg user">👤 ' + msg + '</div>';
807
- container.scrollTop = container.scrollHeight;
808
-
809
- fetch('/chat', {
810
- method: 'POST',
811
- headers: {'Content-Type': 'application/json'},
812
- body: JSON.stringify({message: msg})
813
- })
814
- .then(r => r.json())
815
- .then(data => {
816
- container.innerHTML += '<div class="chat-msg bot">🤖 ' + data.response + '</div>';
817
- container.scrollTop = container.scrollHeight;
818
- });
819
- }
820
-
821
- function switchTab(tab) {
822
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
823
- document.querySelectorAll('.tab-content > div').forEach(d => d.style.display = 'none');
824
-
825
- document.querySelector(`.tab:nth-child(${tab === 'chat' ? 1 : tab === 'train' ? 2 : 3})`).classList.add('active');
826
- document.getElementById(tab + 'Tab').style.display = 'block';
827
-
828
- if (tab === 'stats') {
829
- fetch('/stats')
830
- .then(r => r.json())
831
- .then(data => {
832
- document.getElementById('statsContent').innerHTML =
833
- '<p>🧠 Память: ' + data.memory_size + ' пар</p>' +
834
- '<p>🎮 Шагов ИИ: ' + data.steps + '</p>' +
835
- '<p>📉 Эпсилон: ' + data.epsilon + '</p>' +
836
- '<p>🏆 Лучший счёт: ' + data.best_score + '</p>' +
837
- '<p>⚡ Тренируется: ' + (data.training ? '✅ Да' : '❌ Нет') + '</p>';
838
- });
839
- }
840
- }
841
-
842
- function startTrain() {
843
- document.getElementById('trainStatus').innerHTML = '⏳ Тренировка запущена... Смотри консоль!';
844
- fetch('/train', {method: 'POST'})
845
- .then(r => r.json())
846
- .then(data => {
847
- document.getElementById('trainStatus').innerHTML = data.message;
848
- });
849
- }
850
-
851
- // Авто-обновление
852
- setInterval(() => {
853
- if (document.hidden) return;
854
- fetch('/move', {
855
- method: 'POST',
856
- headers: {'Content-Type': 'application/json'},
857
- body: JSON.stringify({action: 0})
858
- })
859
- .then(r => r.json())
860
- .then(data => {
861
- document.getElementById('aiCanvas').src = 'data:image/png;base64,' + data.ai_image;
862
- document.getElementById('playerCanvas').src = 'data:image/png;base64,' + data.player_image;
863
- document.getElementById('stats').innerHTML =
864
- '🤖 ИИ: ' + data.ai_score +
865
- ' &nbsp;|&nbsp; 🎮 Ты: ' + data.player_score +
866
- ' &nbsp;|&nbsp; 🪙 Монет: ' + data.coins;
867
- });
868
- }, 300);
869
- </script>
870
- </body>
871
- </html>
872
- """
873
-
874
- # =====================================================
875
- # 7. FLASK ROUTES
876
- # =====================================================
877
-
878
- def image_to_base64(img):
879
- buffer = io.BytesIO()
880
- img.save(buffer, format='PNG')
881
- return base64.b64encode(buffer.getvalue()).decode('utf-8')
882
-
883
- @app.route('/')
884
- def index():
885
  global current_seed, agent
886
 
887
- # Создаём игру для отображения
888
- ai_env = InfinitePlatformer(seed=current_seed)
889
- player_env = InfinitePlatformer(seed=current_seed)
890
-
891
- ai_env.reset()
892
- player_env.reset()
893
-
894
- ai_img = ai_env.render(show_player=True)
895
- player_img = player_env.render(show_player=True)
896
-
897
- # ИИ делает ход
898
- ai_action = agent.act(ai_env.get_state())
899
- ai_env.step(ai_action)
900
-
901
- return render_template_string(
902
- HTML_TEMPLATE,
903
- ai_image=image_to_base64(ai_img),
904
- player_image=image_to_base64(player_img),
905
- ai_score=0,
906
- player_score=0,
907
- coins=0,
908
- sounds={
909
- 'jump': SOUNDS['jump'],
910
- 'coin': SOUNDS['coin'],
911
- 'die': SOUNDS['die']
912
- }
913
- )
914
-
915
- @app.route('/move', methods=['POST'])
916
- def move():
917
- global current_seed, agent
918
-
919
- data = request.json
920
- player_action = data.get('action', 0)
921
-
922
  # Создаём два экземпляра с одинаковым seed
923
- ai_env = InfinitePlatformer(seed=current_seed)
924
- player_env = InfinitePlatformer(seed=current_seed)
925
 
926
- ai_env.reset()
927
  player_env.reset()
928
 
929
- # Играем несколько шагов
930
- ai_score = 0
931
- player_score = 0
932
- sound = None
933
- coins = 0
934
 
935
- for _ in range(3): # 3 шага за раз
936
  # Ход ИИ
937
- ai_state = ai_env.get_state()
938
- ai_action = agent.act(ai_state)
939
- _, ai_reward, ai_done, ai_sound = ai_env.step(ai_action)
940
- if ai_sound and not sound:
941
- sound = ai_sound
942
 
943
  # Ход игрока
944
- if player_env.alive:
945
- _, player_reward, player_done, player_sound = player_env.step(player_action)
946
- if player_sound and not sound:
947
- sound = player_sound
948
-
949
- ai_score = ai_env.score
950
- player_score = player_env.score
951
- coins = player_env.coins_collected
952
 
953
- if not ai_env.alive and not player_env.alive:
954
  break
955
 
956
- ai_img = ai_env.render(show_player=ai_env.alive)
957
- player_img = player_env.render(show_player=player_env.alive)
958
-
959
- return jsonify({
960
- 'ai_image': image_to_base64(ai_img),
961
- 'player_image': image_to_base64(player_img),
962
- 'ai_score': ai_score,
963
- 'player_score': player_score,
964
- 'coins': coins,
965
- 'sound': sound
966
- })
 
 
967
 
968
- @app.route('/reset', methods=['POST'])
969
- def reset():
970
  global current_seed
971
  current_seed = random.randint(0, 999999)
972
-
973
- ai_env = InfinitePlatformer(seed=current_seed)
974
- player_env = InfinitePlatformer(seed=current_seed)
975
- ai_env.reset()
976
- player_env.reset()
977
-
978
- ai_img = ai_env.render(show_player=True)
979
- player_img = player_env.render(show_player=True)
980
-
981
- return jsonify({
982
- 'ai_image': image_to_base64(ai_img),
983
- 'player_image': image_to_base64(player_img),
984
- 'ai_score': 0,
985
- 'player_score': 0,
986
- 'coins': 0
987
- })
988
 
989
- @app.route('/chat', methods=['POST'])
990
- def chat():
991
- data = request.json
992
- message = data.get('message', '').strip()
993
-
994
- response = process_chat(message)
995
-
996
- return jsonify({'response': response})
997
 
998
- def process_chat(message):
999
- global is_training, agent
1000
-
 
 
 
 
 
1001
  if message.startswith('/ai '):
1002
  question = message[4:].strip()
1003
  if not question:
1004
  return "❌ Напиши вопрос после /ai"
1005
 
1006
- answer = chat_memory.find_best_match(question)
1007
  if answer:
1008
- return answer
1009
-
1010
  return "🤖 Я не знаю ответа. Обучи меня через /data вопрос|ответ"
1011
 
1012
  elif message.startswith('/data '):
1013
  parts = message[6:].split('|')
1014
  if len(parts) != 2:
1015
  return "❌ Используй: /data вопрос|ответ"
1016
- question = parts[0].strip()
1017
- answer = parts[1].strip()
1018
- return chat_memory.add(question, answer)
1019
 
1020
  elif message == '/stats':
1021
- return f"""📊 Статистика:
1022
- - Память: {len(chat_memory.data)} пар
1023
- - Шагов ИИ: {agent.total_steps}
1024
- - Эпсилон: {agent.epsilon:.3f}
1025
- - Лучший счёт: {agent.best_score}
1026
- - Тренируется: {'✅' if is_training else '❌'}"""
 
 
 
1027
 
1028
  elif message == '/train':
1029
  return start_training()
1030
 
 
 
 
 
 
 
 
 
1031
  else:
1032
- return """🤖 Доступные команды:
1033
- /ai <вопрос>спросить
1034
- /data вопрос|ответ — обучить
1035
- /stats — статистика
1036
- /train — тренировка ИИ"""
 
 
1037
 
1038
  def start_training():
1039
- global is_training, agent
 
1040
 
1041
  if is_training:
1042
  return "⏳ Уже тренируется!"
@@ -1046,47 +610,108 @@ def start_training():
1046
  def train():
1047
  global is_training, agent
1048
  try:
1049
- for ep in range(50):
1050
  if not is_training:
1051
  break
1052
- score, steps, dist = agent.train_episode()
1053
  if ep % 10 == 0:
1054
  print(f"📊 Episode {ep}: Score={score:.0f}, Epsilon={agent.epsilon:.3f}")
 
 
1055
  except Exception as e:
1056
- print(f"Ошибка тренировки: {e}")
1057
  finally:
1058
  is_training = False
1059
 
1060
- thread = threading.Thread(target=train)
1061
- thread.start()
1062
 
1063
- return "🚀 Тренировка запущена! Смотри консоль."
1064
 
1065
- @app.route('/stats', methods=['GET'])
1066
- def stats():
1067
- global is_training, agent, chat_memory
1068
- return jsonify({
1069
- 'memory_size': len(chat_memory.data),
1070
- 'steps': agent.total_steps,
1071
- 'epsilon': round(agent.epsilon, 3),
1072
- 'best_score': agent.best_score,
1073
- 'training': is_training
1074
- })
1075
 
1076
- @app.route('/train', methods=['POST'])
1077
- def train_route():
1078
- return jsonify({'message': start_training()})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1079
 
1080
  # =====================================================
1081
- # 8. ЗАПУСК
1082
  # =====================================================
1083
 
1084
- if __name__ == '__main__':
1085
- print("""
1086
- ╔══════════════════════════════════════════════════╗
1087
- ║ 🎮 AI vs Player Platformer ║
1088
- ║ Сервер запущен на http://0.0.0.0:7860 ║
1089
- ║ Нажми Ctrl+C для остановки ║
1090
- ╚══════════════════════════════════════════════════╝
1091
- """)
1092
- app.run(host='0.0.0.0', port=7860, debug=False, threaded=True)
 
1
  """
2
+ 🧠 AI PLATFORMER + CHATBOT
3
+ Hugging Face Space с настоящей нейросетью
4
  """
5
+ import gradio as gr
6
  import numpy as np
7
  import random
8
  import json
9
  import os
 
 
 
 
10
  import time
11
  import threading
12
  import math
13
+ from collections import deque
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.optim as optim
17
  from PIL import Image
18
  import io
 
 
 
19
 
20
  # =====================================================
21
+ # 1. НЕЙРОСЕТЬ (DQN)
22
  # =====================================================
23
 
24
+ class DQNetwork(nn.Module):
25
+ def __init__(self, input_size=1600, output_size=4):
26
+ super().__init__()
27
+ self.net = nn.Sequential(
28
+ nn.Linear(input_size, 256),
29
+ nn.ReLU(),
30
+ nn.Linear(256, 256),
31
+ nn.ReLU(),
32
+ nn.Linear(256, 256),
33
+ nn.ReLU(),
34
+ nn.Linear(256, 128),
35
+ nn.ReLU(),
36
+ nn.Linear(128, output_size)
37
+ )
38
+
39
+ def forward(self, x):
40
+ return self.net(x)
41
+
42
+ class DQNAgent:
43
+ def __init__(self, state_size=1600, action_size=4):
44
+ self.state_size = state_size
45
+ self.action_size = action_size
46
+ self.memory = deque(maxlen=10000)
47
+ self.epsilon = 1.0
48
+ self.epsilon_min = 0.01
49
+ self.epsilon_decay = 0.995
50
+ self.learning_rate = 0.0005
51
+ self.gamma = 0.99
52
+ self.batch_size = 64
53
+ self.total_steps = 0
54
+ self.best_score = 0
55
+ self.training = False
56
+ self.update_target_every = 100
57
+
58
+ self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
59
+ self.model = DQNetwork(state_size, action_size).to(self.device)
60
+ self.target_model = DQNetwork(state_size, action_size).to(self.device)
61
+ self.target_model.load_state_dict(self.model.state_dict())
62
+ self.optimizer = optim.Adam(self.model.parameters(), lr=self.learning_rate)
63
+ self.criterion = nn.MSELoss()
64
+
65
+ # Загружаем модель если есть
66
+ if os.path.exists('dqn_model.pth'):
67
+ self.load_model()
68
 
69
+ def act(self, state):
70
+ if np.random.rand() <= self.epsilon:
71
+ return random.randrange(self.action_size)
72
+ with torch.no_grad():
73
+ state_tensor = torch.FloatTensor(state).unsqueeze(0).to(self.device)
74
+ q_values = self.model(state_tensor)
75
+ return torch.argmax(q_values).item()
76
 
77
+ def remember(self, state, action, reward, next_state, done):
78
+ self.memory.append((state, action, reward, next_state, done))
 
 
 
79
 
80
+ def replay(self):
81
+ if len(self.memory) < self.batch_size:
82
+ return
83
+
84
+ batch = random.sample(self.memory, self.batch_size)
85
+ states = torch.FloatTensor([b[0] for b in batch]).to(self.device)
86
+ actions = torch.LongTensor([b[1] for b in batch]).to(self.device)
87
+ rewards = torch.FloatTensor([b[2] for b in batch]).to(self.device)
88
+ next_states = torch.FloatTensor([b[3] for b in batch]).to(self.device)
89
+ dones = torch.FloatTensor([b[4] for b in batch]).to(self.device)
90
+
91
+ current_q = self.model(states).gather(1, actions.unsqueeze(1)).squeeze()
92
+ next_q = self.target_model(next_states).max(1)[0].detach()
93
+ target_q = rewards + self.gamma * next_q * (1 - dones)
94
+
95
+ loss = self.criterion(current_q, target_q)
96
+ self.optimizer.zero_grad()
97
+ loss.backward()
98
+ self.optimizer.step()
99
+
100
+ if self.epsilon > self.epsilon_min:
101
+ self.epsilon *= self.epsilon_decay
102
+
103
+ self.total_steps += 1
104
+ if self.total_steps % self.update_target_every == 0:
105
+ self.target_model.load_state_dict(self.model.state_dict())
106
 
107
+ def train_episode(self):
108
+ env = PlatformerLogic()
109
+ state = env.reset()
110
+ total_reward = 0
111
+ done = False
112
+ steps = 0
113
+
114
+ while not done and steps < 500:
115
+ action = self.act(state)
116
+ next_state, reward, done, _ = env.step(action)
117
+ self.remember(state, action, reward, next_state, done)
118
+ self.replay()
119
+ state = next_state
120
+ total_reward += reward
121
+ steps += 1
122
+
123
+ if total_reward > self.best_score:
124
+ self.best_score = total_reward
125
+
126
+ return total_reward, steps
127
 
128
+ def save_model(self):
129
+ torch.save(self.model.state_dict(), 'dqn_model.pth')
130
+
131
+ def load_model(self):
132
+ self.model.load_state_dict(torch.load('dqn_model.pth', map_location=self.device))
133
+ self.target_model.load_state_dict(self.model.state_dict())
 
 
134
 
135
  # =====================================================
136
  # 2. ПЛАТФОРМЕР
137
  # =====================================================
138
 
139
+ class PlatformerLogic:
140
  def __init__(self, seed=None):
141
+ self.width = 80
142
  self.height = 20
143
  self.seed = seed or random.randint(0, 999999)
144
  self.reset()
 
157
  self.coins = []
158
  self.obstacles = []
159
  self.velocity_y = 0
160
+ self.gravity = 0.4
161
+ self.jump_power = -7
162
  self.on_ground = False
163
  self.alive = True
164
  self.steps = 0
 
180
  obstacles = []
181
  enemies = []
182
  coins = []
 
183
  base_x = chunk_x * 30
184
 
185
+ for _ in range(random.randint(3, 6) + int(self.difficulty)):
 
 
 
 
186
  x = base_x + random.randint(5, 25)
187
  height = random.randint(1, 3 + int(self.difficulty * 0.5))
188
  width = random.randint(1, 3)
 
199
  'width': width, 'height': 1, 'is_pit': True
200
  })
201
 
202
+ for _ in range(random.randint(1, 3) + int(self.difficulty * 0.5)):
203
  x = base_x + random.randint(5, 25)
204
  y = self.ground_level - 1
205
  enemy_type = random.choice(['walker', 'jumper'])
206
  enemies.append({
207
  'x': x, 'y': y, 'type': enemy_type,
208
  'direction': 1 if random.random() > 0.5 else -1,
209
+ 'speed': 0.3 + random.random() * 0.3,
210
  'range': random.randint(3, 8),
211
  'start_x': x
212
  })
213
 
214
+ for _ in range(random.randint(5, 10) + int(self.difficulty)):
215
  x = base_x + random.randint(2, 28)
216
  y = random.randint(5, self.ground_level - 2)
217
  coins.append({'x': x, 'y': y, 'collected': False})
 
221
  'enemies': enemies,
222
  'coins': coins
223
  }
 
224
  random.seed()
225
 
226
  def get_state(self):
 
270
  if abs(enemy['x'] - enemy['start_x']) > enemy['range']:
271
  enemy['direction'] *= -1
272
  elif enemy['type'] == 'jumper':
273
+ enemy['y'] += math.sin(time.time() * enemy['speed'] * 2) * 0.3
274
  if enemy['y'] > self.ground_level - 1:
275
  enemy['y'] = self.ground_level - 1
276
 
277
  for enemy in self.entities:
278
+ if abs(enemy['x'] - self.player[0]) < 0.8 and abs(enemy['y'] - self.player[1]) < 0.8:
279
  return True
280
  return False
281
 
 
283
  collected = 0
284
  for coin in self.coins:
285
  if not coin['collected']:
286
+ if abs(coin['x'] - self.player[0]) < 1.0 and abs(coin['y'] - self.player[1]) < 1.0:
287
  coin['collected'] = True
288
  collected += 1
289
  return collected
 
304
  sound = None
305
 
306
  if action == 1:
307
+ self.player[0] -= 0.4
308
  elif action == 2:
309
+ self.player[0] += 0.4
310
 
311
  if action == 3 and self.on_ground:
312
  self.velocity_y = self.jump_power
 
371
  self.coins = [c for c in self.coins if abs(c['x'] - self.player[0]) < self.width]
372
 
373
  self.steps += 1
374
+ if self.steps > 3000:
375
  return self.get_state(), self.score, True, sound
376
 
377
  reward = 1 + coins_collected * 5
378
  return self.get_state(), reward, False, sound
379
 
380
+ def render(self):
381
+ """Рисует игровое поле для Gradio"""
382
  grid = np.zeros((self.height, 80, 3), dtype=np.uint8)
383
+ grid.fill(40)
384
 
385
+ # Небо
386
+ for y in range(self.ground_level):
387
+ grid[y, :] = [20, 30, 60]
388
 
389
+ # Земля
390
+ grid[self.ground_level:self.ground_level+2, :] = [80, 60, 30]
391
+
392
+ # Препятствия
393
  for obs in self.obstacles:
394
+ x = int(obs['x'] - self.player[0] + 40)
395
  if obs.get('is_pit', False):
 
396
  for w in range(obs['width']):
397
  if 0 <= x+w < 80:
398
  grid[obs['y']:obs['y']+2, x+w] = [0, 0, 0]
399
  else:
 
400
  for w in range(obs['width']):
401
  for h in range(obs['height']):
402
  sx = x + w
403
  sy = obs['y'] + h
404
  if 0 <= sx < 80 and 0 <= sy < self.height:
405
+ grid[sy, sx] = [140, 110, 80]
406
 
407
+ # Враги
408
  for enemy in self.entities:
409
  x = int(enemy['x'] - self.player[0] + 40)
410
  y = int(enemy['y'])
411
  if 0 <= x < 80 and 0 <= y < self.height:
412
+ grid[y, x] = [255, 50, 50]
413
+ if y > 0:
414
+ grid[y-1, x] = [200, 30, 30]
415
 
416
+ # Монетки
417
  for coin in self.coins:
418
  if not coin['collected']:
419
  x = int(coin['x'] - self.player[0] + 40)
 
421
  if 0 <= x < 80 and 0 <= y < self.height:
422
  grid[y, x] = [255, 215, 0]
423
 
424
+ # Игрок
425
+ if self.alive:
426
  px = 40
427
  py = int(self.player[1])
428
  if 0 <= py < self.height:
429
+ grid[py, px] = [0, 255, 100]
430
+ if py > 0:
431
+ grid[py-1, px] = [0, 200, 80]
432
+ if py > 1:
433
+ grid[py-2, px] = [0, 150, 60]
434
 
435
  img = Image.fromarray(grid, 'RGB')
436
  return img
437
 
438
  # =====================================================
439
+ # 3. ЧАТ-ПАМЯТЬ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  # =====================================================
441
 
442
  class ChatMemory:
 
453
  with open('chat_data.json', 'w', encoding='utf-8') as f:
454
  json.dump(self.data, f, ensure_ascii=False, indent=2)
455
 
456
+ def add(self, q, a):
457
+ self.data[q.lower()] = a
458
  self.save_data()
459
+ return f"✅ Добавлено: {q} -> {a}"
460
+
461
+ def find(self, q):
462
+ q = q.lower()
463
+ if q in self.data:
464
+ return self.data[q]
465
+ words = q.split()
466
+ best = None
 
467
  best_score = 0
468
+ for key, val in self.data.items():
469
+ score = sum(1 for w in words if w in key)
 
 
470
  if score > best_score:
471
  best_score = score
472
+ best = val
 
473
  if best_score >= len(words) * 0.4:
474
+ return best
475
  return None
476
 
477
  # =====================================================
478
+ # 4. ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
479
  # =====================================================
480
 
481
+ agent = DQNAgent()
482
  chat_memory = ChatMemory()
483
  is_training = False
484
  current_seed = random.randint(0, 999999)
485
+ training_thread = None
486
 
487
  # =====================================================
488
+ # 5. GRADIO ФУНКЦИИ
489
  # =====================================================
490
 
491
+ def game_loop(player_action=0):
492
+ """Основной игровой цикл"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  global current_seed, agent
494
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  # Создаём два экземпляра с одинаковым seed
496
+ ai_env = PlatformerLogic(seed=current_seed)
497
+ player_env = PlatformerLogic(seed=current_seed)
498
 
499
+ ai_state = ai_env.reset()
500
  player_env.reset()
501
 
502
+ # Делаем несколько шагов
503
+ ai_alive = True
504
+ player_alive = True
 
 
505
 
506
+ for _ in range(5):
507
  # Ход ИИ
508
+ if ai_alive:
509
+ ai_action = agent.act(ai_state)
510
+ ai_next_state, _, ai_done, _ = ai_env.step(ai_action)
511
+ ai_state = ai_next_state
512
+ ai_alive = not ai_done
513
 
514
  # Ход игрока
515
+ if player_alive:
516
+ _, _, player_done, _ = player_env.step(player_action)
517
+ player_alive = not player_done
 
 
 
 
 
518
 
519
+ if not ai_alive and not player_alive:
520
  break
521
 
522
+ # Рендерим
523
+ ai_img = ai_env.render()
524
+ player_img = player_env.render()
525
+
526
+ return (
527
+ ai_img,
528
+ player_img,
529
+ ai_env.score,
530
+ player_env.score,
531
+ player_env.coins_collected,
532
+ agent.epsilon,
533
+ agent.best_score
534
+ )
535
 
536
+ def reset_game():
 
537
  global current_seed
538
  current_seed = random.randint(0, 999999)
539
+ return game_loop(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
 
541
+ def move_left():
542
+ return game_loop(1)
 
 
 
 
 
 
543
 
544
+ def move_right():
545
+ return game_loop(2)
546
+
547
+ def move_jump():
548
+ return game_loop(3)
549
+
550
+ def chat_response(message):
551
+ """Обработчик чата"""
552
  if message.startswith('/ai '):
553
  question = message[4:].strip()
554
  if not question:
555
  return "❌ Напиши вопрос после /ai"
556
 
557
+ answer = chat_memory.find(question)
558
  if answer:
559
+ return f"🤖 {answer}"
 
560
  return "🤖 Я не знаю ответа. Обучи меня через /data вопрос|ответ"
561
 
562
  elif message.startswith('/data '):
563
  parts = message[6:].split('|')
564
  if len(parts) != 2:
565
  return "❌ Используй: /data вопрос|ответ"
566
+ q = parts[0].strip()
567
+ a = parts[1].strip()
568
+ return chat_memory.add(q, a)
569
 
570
  elif message == '/stats':
571
+ return f"""
572
+ 📊 **Статистика нейросети:**
573
+ - 🧠 Память: {len(chat_memory.data)} пар
574
+ - 🎮 Шагов обучения: {agent.total_steps}
575
+ - 📉 Эпсилон: {agent.epsilon:.3f}
576
+ - 🏆 Лучший счёт: {agent.best_score}
577
+ - ⚡ Тренируется: {'✅' if is_training else '❌'}
578
+ - 🖥️ Устройство: {agent.device}
579
+ """
580
 
581
  elif message == '/train':
582
  return start_training()
583
 
584
+ elif message == '/save':
585
+ agent.save_model()
586
+ return "💾 Модель сохранена!"
587
+
588
+ elif message == '/load':
589
+ agent.load_model()
590
+ return "📂 Модель загружена!"
591
+
592
  else:
593
+ return """🤖 **Доступные команды:**
594
+ - `/ai вопрос`задать вопрос
595
+ - `/data вопрос|ответ` — обучить
596
+ - `/stats` — статистика
597
+ - `/train` — тренировка ИИ
598
+ - `/save` — сохранить модель
599
+ - `/load` — загрузить модель"""
600
 
601
  def start_training():
602
+ """Запускает тренировку в фоновом потоке"""
603
+ global is_training, training_thread, agent
604
 
605
  if is_training:
606
  return "⏳ Уже тренируется!"
 
610
  def train():
611
  global is_training, agent
612
  try:
613
+ for ep in range(100):
614
  if not is_training:
615
  break
616
+ score, steps = agent.train_episode()
617
  if ep % 10 == 0:
618
  print(f"📊 Episode {ep}: Score={score:.0f}, Epsilon={agent.epsilon:.3f}")
619
+ if score > 0 and ep % 20 == 0:
620
+ agent.save_model()
621
  except Exception as e:
622
+ print(f"Ошибка: {e}")
623
  finally:
624
  is_training = False
625
 
626
+ training_thread = threading.Thread(target=train)
627
+ training_thread.start()
628
 
629
+ return "🚀 **Тренировка запущена!** Смотри консоль для прогресса."
630
 
631
+ # =====================================================
632
+ # 6. GRADIO INTERFACE
633
+ # =====================================================
 
 
 
 
 
 
 
634
 
635
+ with gr.Blocks(title="🧠 AI Platformer", theme=gr.themes.Soft()) as demo:
636
+ gr.Markdown("""
637
+ # 🧠 AI vs Player Platformer
638
+
639
+ ### 🤖 Слева — нейросеть (DQN) играет сама
640
+ ### 🎮 Справа — ты управляешь зелёным (⬅️ ➡️ ⬆️)
641
+ ### 💬 Снизу — общий чат с ИИ, который можно обучать!
642
+ """)
643
+
644
+ with gr.Row():
645
+ with gr.Column():
646
+ gr.Markdown("### 🤖 Нейросеть")
647
+ ai_output = gr.Image(label="AI Game", height=300)
648
+ with gr.Column():
649
+ gr.Markdown("### 🎮 Ты")
650
+ player_output = gr.Image(label="Player Game", height=300)
651
+
652
+ with gr.Row():
653
+ ai_score = gr.Number(label="🤖 Счёт ИИ", value=0)
654
+ player_score = gr.Number(label="🎮 Твой счёт", value=0)
655
+ coins = gr.Number(label="🪙 Монет", value=0)
656
+ epsilon = gr.Number(label="🧠 Эпсилон", value=1.0)
657
+ best_score = gr.Number(label="🏆 Рекорд ИИ", value=0)
658
+
659
+ with gr.Row():
660
+ gr.Markdown("### 🎮 Управление:")
661
+ left_btn = gr.Button("⬅️ Влево", size="lg")
662
+ jump_btn = gr.Button("⬆️ Прыжок", size="lg", variant="primary")
663
+ right_btn = gr.Button("➡️ Вправо", size="lg")
664
+ reset_btn = gr.Button("🔄 Новый уровень", size="lg", variant="secondary")
665
+
666
+ with gr.Row():
667
+ train_btn = gr.Button("🧠 Тренировать ИИ", variant="secondary", size="lg")
668
+ save_btn = gr.Button("💾 Сохранить модель", size="lg")
669
+ load_btn = gr.Button("📂 Загрузить модель", size="lg")
670
+ status = gr.Textbox(label="Статус", lines=1)
671
+
672
+ with gr.Tab("💬 Чат с ИИ"):
673
+ gr.Markdown("""
674
+ ### Команды:
675
+ - `/ai <вопрос>` — спросить ИИ
676
+ - `/data <вопрос>|<ответ>` — обучить ИИ
677
+ - `/stats` — статистика
678
+ - `/train` — тренировка
679
+ - `/save` / `/load` — сохранить/загрузить модель
680
+ """)
681
+ chat_input = gr.Textbox(label="Введите команду", placeholder="/ai Как пройти уровень?")
682
+ chat_output = gr.Markdown(label="Ответ")
683
+ chat_btn = gr.Button("Отправить", variant="primary")
684
+
685
+ with gr.Accordion("📊 Расширенная статистика", open=False):
686
+ gr.Markdown("""
687
+ **Как работает нейросеть:**
688
+ - 🧠 Архитектура: 4 слоя (256→256→256→128 нейронов)
689
+ - 📚 Memory: 10000 опыта
690
+ - 🎯 Алгоритм: DQN с целевой сетью
691
+ - 📉 Эпсилон-жадность: исследование vs эксплуатация
692
+ - 🏆 Цель: максимизировать счёт в платформере
693
+ """)
694
+ gr.Markdown("*Модель сохраняется автомат��чески каждые 20 эпизодов*")
695
+
696
+ # Привязка кнопок
697
+ left_btn.click(move_left, outputs=[ai_output, player_output, ai_score, player_score, coins, epsilon, best_score])
698
+ right_btn.click(move_right, outputs=[ai_output, player_output, ai_score, player_score, coins, epsilon, best_score])
699
+ jump_btn.click(move_jump, outputs=[ai_output, player_output, ai_score, player_score, coins, epsilon, best_score])
700
+ reset_btn.click(reset_game, outputs=[ai_output, player_output, ai_score, player_score, coins, epsilon, best_score])
701
+
702
+ train_btn.click(start_training, outputs=[status])
703
+ save_btn.click(lambda: agent.save_model() or "💾 Модель сохранена!", outputs=[status])
704
+ load_btn.click(lambda: agent.load_model() or "📂 Модель загружена!", outputs=[status])
705
+
706
+ chat_btn.click(chat_response, inputs=[chat_input], outputs=[chat_output])
707
+ chat_input.submit(chat_response, inputs=[chat_input], outputs=[chat_output])
708
+
709
+ # Автозапуск при загрузке
710
+ demo.load(reset_game, outputs=[ai_output, player_output, ai_score, player_score, coins, epsilon, best_score])
711
 
712
  # =====================================================
713
+ # 7. ЗАПУСК
714
  # =====================================================
715
 
716
+ if __name__ == "__main__":
717
+ demo.launch(share=True)