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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +430 -267
app.py CHANGED
@@ -1,8 +1,8 @@
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
@@ -14,8 +14,8 @@ 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)
@@ -62,7 +62,6 @@ class DQNAgent:
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
 
@@ -377,63 +376,17 @@ class PlatformerLogic:
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)
420
- y = int(coin['y'])
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. ЧАТ-ПАМЯТЬ
@@ -485,233 +438,443 @@ 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 "⏳ Уже тренируется!"
607
-
608
- is_training = True
609
-
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)
 
1
  """
2
  🧠 AI PLATFORMER + CHATBOT
3
+ Hugging Face Space (Flask + HTML)
4
  """
5
+ from flask import Flask, render_template_string, request, jsonify
6
  import numpy as np
7
  import random
8
  import json
 
14
  import torch
15
  import torch.nn as nn
16
  import torch.optim as optim
17
+
18
+ app = Flask(__name__)
19
 
20
  # =====================================================
21
  # 1. НЕЙРОСЕТЬ (DQN)
 
62
  self.optimizer = optim.Adam(self.model.parameters(), lr=self.learning_rate)
63
  self.criterion = nn.MSELoss()
64
 
 
65
  if os.path.exists('dqn_model.pth'):
66
  self.load_model()
67
 
 
376
  reward = 1 + coins_collected * 5
377
  return self.get_state(), reward, False, sound
378
 
379
+ def get_world_data(self):
380
+ return {
381
+ 'player': self.player,
382
+ 'obstacles': self.obstacles,
383
+ 'entities': self.entities,
384
+ 'coins': [c for c in self.coins if not c['collected']],
385
+ 'ground_level': self.ground_level,
386
+ 'score': self.score,
387
+ 'coins_collected': self.coins_collected,
388
+ 'alive': self.alive
389
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
  # =====================================================
392
  # 3. ЧАТ-ПАМЯТЬ
 
438
  training_thread = None
439
 
440
  # =====================================================
441
+ # 5. HTML
442
+ # =====================================================
443
+
444
+ HTML = """
445
+ <!DOCTYPE html>
446
+ <html>
447
+ <head>
448
+ <meta charset="UTF-8">
449
+ <title>🧠 AI Platformer</title>
450
+ <style>
451
+ * { margin: 0; padding: 0; box-sizing: border-box; }
452
+ body {
453
+ background: #0a0a1a;
454
+ color: #eee;
455
+ font-family: 'Segoe UI', sans-serif;
456
+ display: flex;
457
+ justify-content: center;
458
+ padding: 20px;
459
+ min-height: 100vh;
460
+ }
461
+ .container { max-width: 1100px; width: 100%; }
462
+ h1 {
463
+ text-align: center;
464
+ padding: 15px 0;
465
+ background: linear-gradient(135deg, #e94560, #0f3460);
466
+ -webkit-background-clip: text;
467
+ -webkit-text-fill-color: transparent;
468
+ font-size: 2.2em;
469
+ }
470
+ .sub { text-align: center; color: #666; margin-bottom: 15px; }
471
+ .game-row { display: flex; gap: 20px; flex-wrap: wrap; }
472
+ .game-box {
473
+ flex: 1; min-width: 300px;
474
+ background: #16213e;
475
+ border-radius: 16px;
476
+ padding: 15px;
477
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
478
+ }
479
+ .game-box h3 { text-align: center; margin-bottom: 10px; }
480
+ canvas {
481
+ width: 100%; aspect-ratio: 80/20;
482
+ background: #1a1a2e;
483
+ border-radius: 8px;
484
+ display: block;
485
+ image-rendering: pixelated;
486
+ }
487
+ .controls {
488
+ display: flex; justify-content: center;
489
+ gap: 12px; margin: 15px 0; flex-wrap: wrap;
490
+ }
491
+ .controls button {
492
+ padding: 12px 30px; font-size: 1.1em;
493
+ border: none; border-radius: 10px;
494
+ cursor: pointer; font-weight: bold;
495
+ transition: all 0.15s; color: white;
496
+ }
497
+ .controls button:hover { transform: scale(1.05); }
498
+ .controls button:active { transform: scale(0.93); }
499
+ .btn-left { background: #e94560; }
500
+ .btn-right { background: #e94560; }
501
+ .btn-jump { background: #0f3460; padding: 12px 45px; }
502
+ .btn-reset { background: #533483; }
503
+ .stats-bar {
504
+ background: #16213e;
505
+ border-radius: 12px;
506
+ padding: 12px 20px;
507
+ margin: 10px 0;
508
+ display: flex;
509
+ justify-content: space-around;
510
+ flex-wrap: wrap;
511
+ gap: 10px;
512
+ font-size: 1.1em;
513
+ }
514
+ .stats-bar span { color: #e94560; font-weight: bold; }
515
+ .tabs {
516
+ display: flex; gap: 10px;
517
+ margin: 15px 0; flex-wrap: wrap;
518
+ }
519
+ .tab {
520
+ padding: 10px 22px;
521
+ background: #16213e;
522
+ border-radius: 10px;
523
+ cursor: pointer;
524
+ border: 2px solid transparent;
525
+ transition: all 0.3s;
526
+ }
527
+ .tab:hover { border-color: #e94560; }
528
+ .tab.active { border-color: #e94560; background: #1a1a3e; }
529
+ .tab-content {
530
+ background: #16213e;
531
+ border-radius: 12px;
532
+ padding: 20px;
533
+ min-height: 200px;
534
+ }
535
+ .chat-area {
536
+ display: flex; gap: 10px; margin-top: 10px;
537
+ }
538
+ .chat-area input {
539
+ flex: 1; padding: 10px;
540
+ border-radius: 8px;
541
+ border: 1px solid #333;
542
+ background: #0a0a1a;
543
+ color: #eee;
544
+ font-size: 1em;
545
+ }
546
+ .chat-area button {
547
+ padding: 10px 25px;
548
+ background: #e94560;
549
+ color: white;
550
+ border: none;
551
+ border-radius: 8px;
552
+ cursor: pointer;
553
+ font-weight: bold;
554
+ }
555
+ .chat-msgs {
556
+ max-height: 200px;
557
+ overflow-y: auto;
558
+ padding: 5px;
559
+ }
560
+ .chat-msgs div {
561
+ padding: 6px 12px;
562
+ margin: 3px 0;
563
+ border-radius: 6px;
564
+ background: #0a0a1a;
565
+ }
566
+ .chat-msgs .user { border-left: 3px solid #e94560; }
567
+ .chat-msgs .bot { border-left: 3px solid #0f3460; }
568
+ .hidden { display: none; }
569
+ </style>
570
+ </head>
571
+ <body>
572
+ <div class="container">
573
+ <h1>🧠 AI vs Player Platformer</h1>
574
+ <p class="sub">🤖 Нейросеть слева 🎮 Ты справа (⬅️ ➡️ ⬆️)</p>
575
+
576
+ <div class="game-row">
577
+ <div class="game-box">
578
+ <h3>🤖 Нейросеть</h3>
579
+ <canvas id="aiCanvas" width="800" height="200"></canvas>
580
+ </div>
581
+ <div class="game-box">
582
+ <h3>🎮 Ты</h3>
583
+ <canvas id="playerCanvas" width="800" height="200"></canvas>
584
+ </div>
585
+ </div>
586
+
587
+ <div class="stats-bar">
588
+ <div>🤖 ИИ: <span id="aiScore">0</span></div>
589
+ <div>🎮 Ты: <span id="playerScore">0</span></div>
590
+ <div>🪙 Монет: <span id="coinsCount">0</span></div>
591
+ <div>🧠 Эпсилон: <span id="epsilon">1.00</span></div>
592
+ <div>🏆 Рекорд: <span id="bestScore">0</span></div>
593
+ </div>
594
+
595
+ <div class="controls">
596
+ <button class="btn-left" id="btnLeft">⬅️ Влево</button>
597
+ <button class="btn-jump" id="btnJump">⬆️ Прыжок</button>
598
+ <button class="btn-right" id="btnRight">➡️ Вправо</button>
599
+ <button class="btn-reset" id="btnReset">🔄 Новый уровень</button>
600
+ </div>
601
+
602
+ <div class="tabs">
603
+ <div class="tab active" data-tab="chat">💬 Чат</div>
604
+ <div class="tab" data-tab="train">🧠 Тренировка</div>
605
+ <div class="tab" data-tab="stats">📊 Статистика</div>
606
+ </div>
607
+
608
+ <div class="tab-content">
609
+ <div id="chatTab">
610
+ <div class="chat-msgs" id="chatMsgs">
611
+ <div class="bot">🤖 Привет! Команды:</div>
612
+ <div class="bot"><code>/ai вопрос</code> — спросить</div>
613
+ <div class="bot"><code>/data вопрос|ответ</code> — обучить</div>
614
+ <div class="bot"><code>/stats</code> — статистика</div>
615
+ <div class="bot"><code>/train</code> — тренировка</div>
616
+ </div>
617
+ <div class="chat-area">
618
+ <input id="chatInput" placeholder="Введите команду..." onkeydown="if(event.key==='Enter') sendChat()">
619
+ <button onclick="sendChat()">➤</button>
620
+ </div>
621
+ </div>
622
+ <div id="trainTab" class="hidden">
623
+ <h3>🧠 Тренировка нейросети</h3>
624
+ <p>DQN с 4 слоями (256→256→256→128 нейронов)</p>
625
+ <button onclick="startTrain()" style="padding:12px 35px;background:#e94560;color:white;border:none;border-radius:10px;font-size:1.1em;cursor:pointer;">
626
+ 🚀 Запустить
627
+ </button>
628
+ <div id="trainStatus" style="margin-top:10px;color:#aaa;">⏸ Остановлена</div>
629
+ </div>
630
+ <div id="statsTab" class="hidden">
631
+ <h3>📊 Статистика</h3>
632
+ <div id="statsContent">Загрузка...</div>
633
+ </div>
634
+ </div>
635
+ </div>
636
+
637
+ <script>
638
+ let playerAction = 0;
639
+
640
+ function drawGame(ctx, data, showPlayer) {
641
+ const W = ctx.canvas.width, H = ctx.canvas.height;
642
+ const cellW = W / 80, cellH = H / 20;
643
+ ctx.clearRect(0, 0, W, H);
644
+
645
+ const grad = ctx.createLinearGradient(0, 0, 0, H);
646
+ grad.addColorStop(0, '#0a0a2e');
647
+ grad.addColorStop(0.7, '#1a1a4e');
648
+ ctx.fillStyle = grad;
649
+ ctx.fillRect(0, 0, W, H);
650
+
651
+ const cx = data.player[0] - 40;
652
+ function toScreen(wx, wy) {
653
+ return [(wx - cx) * cellW, wy * cellH];
654
+ }
655
+
656
+ const gy = data.ground_level * cellH;
657
+ ctx.fillStyle = '#4a3a2a';
658
+ ctx.fillRect(0, gy, W, cellH * 2);
659
+ ctx.fillStyle = '#3a2a1a';
660
+ ctx.fillRect(0, gy + cellH * 0.5, W, cellH * 0.5);
661
+
662
+ for (const obs of data.obstacles) {
663
+ const [x, y] = toScreen(obs.x, obs.y);
664
+ if (obs.is_pit) {
665
+ ctx.fillStyle = '#000';
666
+ ctx.fillRect(x, y - cellH, obs.width * cellW, cellH * 2);
667
+ } else {
668
+ ctx.fillStyle = '#8a7a6a';
669
+ ctx.fillRect(x, y, obs.width * cellW, obs.height * cellH);
670
+ }
671
+ }
672
+
673
+ for (const enemy of data.entities) {
674
+ const [x, y] = toScreen(enemy.x, enemy.y);
675
+ ctx.fillStyle = '#e94560';
676
+ ctx.beginPath();
677
+ ctx.arc(x + cellW/2, y + cellH/2, cellH/2.5, 0, Math.PI*2);
678
+ ctx.fill();
679
+ }
680
+
681
+ for (const coin of data.coins) {
682
+ const [x, y] = toScreen(coin.x, coin.y);
683
+ ctx.fillStyle = '#ffd700';
684
+ ctx.beginPath();
685
+ ctx.arc(x + cellW/2, y + cellH/2, cellH/3, 0, Math.PI*2);
686
+ ctx.fill();
687
+ }
688
+
689
+ if (showPlayer && data.alive) {
690
+ const [px, py] = toScreen(data.player[0], data.player[1]);
691
+ ctx.fillStyle = '#00ff88';
692
+ ctx.shadowColor = '#00ff88';
693
+ ctx.shadowBlur = 15;
694
+ ctx.fillRect(px + 2, py + 2, cellW - 4, cellH - 4);
695
+ ctx.shadowBlur = 0;
696
+ }
697
+ }
698
+
699
+ async function updateGame() {
700
+ try {
701
+ const res = await fetch('/step', {
702
+ method: 'POST',
703
+ headers: {'Content-Type': 'application/json'},
704
+ body: JSON.stringify({action: playerAction})
705
+ });
706
+ const data = await res.json();
707
+
708
+ const aiCanvas = document.getElementById('aiCanvas');
709
+ const playerCanvas = document.getElementById('playerCanvas');
710
+ drawGame(aiCanvas.getContext('2d'), data.ai, true);
711
+ drawGame(playerCanvas.getContext('2d'), data.player, data.player.alive);
712
+
713
+ document.getElementById('aiScore').textContent = data.ai.score;
714
+ document.getElementById('playerScore').textContent = data.player.score;
715
+ document.getElementById('coinsCount').textContent = data.player.coins_collected;
716
+ document.getElementById('epsilon').textContent = data.epsilon.toFixed(3);
717
+ document.getElementById('bestScore').textContent = data.best_score;
718
+ } catch(e) {}
719
+ }
720
+
721
+ // Управление
722
+ document.getElementById('btnLeft').addEventListener('mousedown', () => { playerAction = 1; });
723
+ document.getElementById('btnLeft').addEventListener('mouseup', () => { playerAction = 0; });
724
+ document.getElementById('btnRight').addEventListener('mousedown', () => { playerAction = 2; });
725
+ document.getElementById('btnRight').addEventListener('mouseup', () => { playerAction = 0; });
726
+ document.getElementById('btnJump').addEventListener('mousedown', () => { playerAction = 3; });
727
+ document.getElementById('btnJump').addEventListener('mouseup', () => { playerAction = 0; });
728
+
729
+ document.addEventListener('keydown', (e) => {
730
+ if (e.key === 'ArrowLeft') { e.preventDefault(); playerAction = 1; }
731
+ else if (e.key === 'ArrowRight') { e.preventDefault(); playerAction = 2; }
732
+ else if (e.key === 'ArrowUp' || e.key === ' ') { e.preventDefault(); playerAction = 3; }
733
+ });
734
+ document.addEventListener('keyup', (e) => {
735
+ if (['ArrowLeft', 'ArrowRight', 'ArrowUp', ' '].includes(e.key)) {
736
+ e.preventDefault(); playerAction = 0;
737
+ }
738
+ });
739
+
740
+ document.getElementById('btnReset').addEventListener('click', async () => {
741
+ const res = await fetch('/reset', {method: 'POST'});
742
+ const data = await res.json();
743
+ document.getElementById('aiScore').textContent = data.ai.score;
744
+ document.getElementById('playerScore').textContent = data.player.score;
745
+ document.getElementById('coinsCount').textContent = data.player.coins_collected;
746
+ });
747
+
748
+ // Чат
749
+ async function sendChat() {
750
+ const input = document.getElementById('chatInput');
751
+ const msg = input.value.trim();
752
+ if (!msg) return;
753
+ input.value = '';
754
+
755
+ const container = document.getElementById('chatMsgs');
756
+ container.innerHTML += '<div class="user">👤 ' + msg + '</div>';
757
+ container.scrollTop = container.scrollHeight;
758
+
759
+ const res = await fetch('/chat', {
760
+ method: 'POST',
761
+ headers: {'Content-Type': 'application/json'},
762
+ body: JSON.stringify({message: msg})
763
+ });
764
+ const data = await res.json();
765
+ container.innerHTML += '<div class="bot">🤖 ' + data.response + '</div>';
766
+ container.scrollTop = container.scrollHeight;
767
+ }
768
+
769
+ // Тренировка
770
+ async function startTrain() {
771
+ document.getElementById('trainStatus').textContent = '⏳ Запуск...';
772
+ const res = await fetch('/train', {method: 'POST'});
773
+ const data = await res.json();
774
+ document.getElementById('trainStatus').textContent = data.message;
775
+ }
776
+
777
+ // Табы
778
+ document.querySelectorAll('.tab').forEach(tab => {
779
+ tab.addEventListener('click', function() {
780
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
781
+ this.classList.add('active');
782
+ const tabName = this.dataset.tab;
783
+ document.querySelectorAll('.tab-content > div').forEach(d => d.classList.add('hidden'));
784
+ document.getElementById(tabName + 'Tab').classList.remove('hidden');
785
+ if (tabName === 'stats') {
786
+ fetch('/stats').then(r => r.json()).then(data => {
787
+ document.getElementById('statsContent').innerHTML = `
788
+ <p>🧠 Память: ${data.memory_size} пар</p>
789
+ <p>🎮 Шагов: ${data.steps}</p>
790
+ <p>📉 Эпсилон: ${data.epsilon}</p>
791
+ <p>🏆 Рекорд: ${data.best_score}</p>
792
+ <p>⚡ Тренируется: ${data.training ? '✅' : '❌'}</p>
793
+ `;
794
+ });
795
+ }
796
+ });
797
+ });
798
+
799
+ setInterval(updateGame, 200);
800
+ updateGame();
801
+ </script>
802
+ </body>
803
+ </html>
804
+ """
805
+
806
+ # =====================================================
807
+ # 6. FLASK ROUTES
808
  # =====================================================
809
 
810
+ @app.route('/')
811
+ def index():
812
+ return render_template_string(HTML)
813
+
814
+ @app.route('/step', methods=['POST'])
815
+ def step():
816
  global current_seed, agent
817
 
818
+ data = request.json
819
+ player_action = data.get('action', 0)
820
+
821
  ai_env = PlatformerLogic(seed=current_seed)
822
  player_env = PlatformerLogic(seed=current_seed)
823
 
824
+ ai_env.reset()
825
  player_env.reset()
826
 
 
 
 
 
827
  for _ in range(5):
828
+ if ai_env.alive:
829
+ ai_action = agent.act(ai_env.get_state())
830
+ ai_env.step(ai_action)
831
+
832
+ if player_env.alive:
833
+ player_env.step(player_action)
834
+
835
+ if not ai_env.alive and not player_env.alive:
 
 
 
 
 
836
  break
837
 
838
+ return jsonify({
839
+ 'ai': ai_env.get_world_data(),
840
+ 'player': player_env.get_world_data(),
841
+ 'epsilon': agent.epsilon,
842
+ 'best_score': agent.best_score
843
+ })
 
 
 
 
 
 
 
844
 
845
+ @app.route('/reset', methods=['POST'])
846
+ def reset():
847
  global current_seed
848
  current_seed = random.randint(0, 999999)
849
+
850
+ ai_env = PlatformerLogic(seed=current_seed)
851
+ player_env = PlatformerLogic(seed=current_seed)
852
+ ai_env.reset()
853
+ player_env.reset()
854
+
855
+ return jsonify({
856
+ 'ai': ai_env.get_world_data(),
857
+ 'player': player_env.get_world_data()
858
+ })
859
 
860
+ @app.route('/chat', methods=['POST'])
861
+ def chat():
862
+ data = request.json
863
+ message = data.get('message', '').strip()
864
+
865
  if message.startswith('/ai '):
866
+ q = message[4:].strip()
867
+ answer = chat_memory.find(q)
868
+ return jsonify({'response': answer or "🤖 Не знаю. Обучи через /data"})
 
 
 
 
 
869
 
870
  elif message.startswith('/data '):
871
  parts = message[6:].split('|')
872
  if len(parts) != 2:
873
+ return jsonify({'response': "❌ Используй: /data вопрос|ответ"})
874
+ return jsonify({'response': chat_memory.add(parts[0].strip(), parts[1].strip())})
 
 
875
 
876
  elif message == '/stats':
877
+ return jsonify({'response': f"📊 Память: {len(chat_memory.data)} пар, Шагов: {agent.total_steps}"})
 
 
 
 
 
 
 
 
878
 
879
  elif message == '/train':
880
+ return json