X commited on
Commit
c7df0bf
·
verified ·
1 Parent(s): a39e92e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +602 -233
app.py CHANGED
@@ -1,8 +1,8 @@
1
  """
2
  🎮 ДВОЙНОЙ ПЛАТФОРМЕР: ИИ vs ИГРОК + ЗВУКИ + ЧАТ-БОТ
3
- Hugging Face Space (Gradio)
4
  """
5
- import gradio as gr
6
  import numpy as np
7
  import random
8
  import json
@@ -11,20 +11,22 @@ import torch
11
  import torch.nn as nn
12
  import torch.optim as optim
13
  from collections import deque
14
- from transformers import GPT2LMHeadModel, GPT2Tokenizer
15
  import time
16
  import threading
17
  import math
18
  import base64
19
  from PIL import Image
20
  import io
 
 
 
21
 
22
  # =====================================================
23
- # 1. ЗВУКИ (только эффекты, без фона)
24
  # =====================================================
25
 
26
  def generate_sound(freq=440, duration=0.1, waveform='sine'):
27
- """Генерирует простой звук в base64"""
28
  import struct
29
  import wave
30
 
@@ -43,9 +45,7 @@ def generate_sound(freq=440, duration=0.1, waveform='sine'):
43
  val = int(32767 * 0.5 * math.sin(2 * math.pi * freq * t))
44
  elif waveform == 'square':
45
  val = int(32767 * 0.5 * (1 if math.sin(2 * math.pi * freq * t) > 0 else -1))
46
- elif waveform == 'noise':
47
- val = int(32767 * 0.3 * random.random())
48
- else: # sweep
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))
@@ -62,13 +62,8 @@ SOUNDS = {
62
  'die': generate_sound(200, 0.3, 'sweep')
63
  }
64
 
65
- def get_sound_html(sound_type):
66
- if sound_type in SOUNDS:
67
- return f'<audio autoplay><source src="{SOUNDS[sound_type]}" type="audio/wav"></audio>'
68
- return ''
69
-
70
  # =====================================================
71
- # 2. БЕСКОНЕЧНЫЙ ПЛАТФОРМЕР
72
  # =====================================================
73
 
74
  class InfinitePlatformer:
@@ -91,7 +86,6 @@ class InfinitePlatformer:
91
  self.entities = []
92
  self.coins = []
93
  self.obstacles = []
94
- self.camera_x = 0
95
  self.velocity_y = 0
96
  self.gravity = 0.5
97
  self.jump_power = -8
@@ -203,9 +197,6 @@ class InfinitePlatformer:
203
  if 0 <= sx < state_size and 0 <= sy < state_size:
204
  state[sy, sx] = 0.3
205
 
206
- for x in range(state_size):
207
- state[state_size-3, x] = 0.2
208
-
209
  return state.flatten()
210
 
211
  def update_entities(self):
@@ -245,8 +236,7 @@ class InfinitePlatformer:
245
  return True
246
  return False
247
 
248
- def step(self, action, is_ai=False):
249
- px, py = self.player[0], self.player[1]
250
  sound = None
251
 
252
  if action == 1:
@@ -349,8 +339,6 @@ class InfinitePlatformer:
349
  y = int(enemy['y'])
350
  if 0 <= x < 80 and 0 <= y < self.height:
351
  grid[y, x] = [255, 0, 0]
352
- if enemy['type'] == 'jumper':
353
- grid[y-1, x] = [200, 0, 0]
354
 
355
  for coin in self.coins:
356
  if not coin['collected']:
@@ -365,115 +353,97 @@ class InfinitePlatformer:
365
  if 0 <= py < self.height:
366
  grid[py, px] = [0, 255, 0]
367
  grid[py-1, px] = [0, 200, 0]
368
- grid[py-2, px] = [0, 150, 0]
369
 
370
  img = Image.fromarray(grid, 'RGB')
371
  return img
372
 
373
  # =====================================================
374
- # 3. DQN АГЕНТ
375
  # =====================================================
376
 
377
- class DQN(nn.Module):
378
- def __init__(self, input_size, output_size):
379
- super().__init__()
380
- self.net = nn.Sequential(
381
- nn.Linear(input_size, 256),
382
- nn.ReLU(),
383
- nn.Linear(256, 256),
384
- nn.ReLU(),
385
- nn.Linear(256, 256),
386
- nn.ReLU(),
387
- nn.Linear(256, output_size)
388
- )
389
-
390
- def forward(self, x):
391
- return self.net(x)
392
-
393
- class DQNAgent:
394
  def __init__(self, state_size=1600, action_size=4):
395
  self.state_size = state_size
396
  self.action_size = action_size
397
- self.memory = deque(maxlen=10000)
398
  self.epsilon = 1.0
399
  self.epsilon_min = 0.01
400
- self.epsilon_decay = 0.998
401
- self.model = DQN(state_size, action_size)
402
- self.target_model = DQN(state_size, action_size)
403
- self.target_model.load_state_dict(self.model.state_dict())
404
- self.optimizer = optim.Adam(self.model.parameters(), lr=0.0001)
405
- self.criterion = nn.MSELoss()
406
- self.gamma = 0.99
407
- self.batch_size = 64
408
  self.total_steps = 0
409
- self.update_target_every = 100
410
- self.training = False
411
- self.episode_score = 0
412
  self.best_score = 0
413
- self.seed = None
 
 
 
 
 
 
 
 
 
414
 
415
  def act(self, state):
416
  if np.random.rand() <= self.epsilon:
417
  return random.randrange(self.action_size)
418
- with torch.no_grad():
419
- state_tensor = torch.FloatTensor(state).unsqueeze(0)
420
- q_values = self.model(state_tensor)
421
- return torch.argmax(q_values).item()
 
422
 
423
  def remember(self, state, action, reward, next_state, done):
424
  self.memory.append((state, action, reward, next_state, done))
 
 
425
 
426
  def replay(self):
427
- if len(self.memory) < self.batch_size:
428
  return
429
 
430
- batch = random.sample(self.memory, self.batch_size)
431
- states = torch.FloatTensor([b[0] for b in batch])
432
- actions = torch.LongTensor([b[1] for b in batch])
433
- rewards = torch.FloatTensor([b[2] for b in batch])
434
- next_states = torch.FloatTensor([b[3] for b in batch])
435
- dones = torch.FloatTensor([b[4] for b in batch])
436
-
437
- current_q = self.model(states).gather(1, actions.unsqueeze(1)).squeeze()
438
- next_q = self.target_model(next_states).max(1)[0]
439
- target_q = rewards + self.gamma * next_q * (1 - dones)
440
-
441
- loss = self.criterion(current_q, target_q.detach())
442
- self.optimizer.zero_grad()
443
- loss.backward()
444
- self.optimizer.step()
445
 
446
  if self.epsilon > self.epsilon_min:
447
  self.epsilon *= self.epsilon_decay
448
 
449
  self.total_steps += 1
450
- if self.total_steps % self.update_target_every == 0:
451
- self.target_model.load_state_dict(self.model.state_dict())
452
 
453
  def train_episode(self):
454
- env = InfinitePlatformer(seed=self.seed)
455
  state = env.reset()
456
  total_reward = 0
457
  done = False
458
  steps = 0
459
 
460
- while not done and steps < 500:
461
  action = self.act(state)
462
- next_state, reward, done, _ = env.step(action, is_ai=True)
463
  self.remember(state, action, reward, next_state, done)
464
  self.replay()
465
  state = next_state
466
  total_reward += reward
467
  steps += 1
468
 
469
- self.episode_score = total_reward
470
  if total_reward > self.best_score:
471
  self.best_score = total_reward
472
 
473
  return total_reward, steps, env.distance
474
 
475
  # =====================================================
476
- # 4. ЧАТ-БОТ
477
  # =====================================================
478
 
479
  class ChatMemory:
@@ -511,76 +481,522 @@ class ChatMemory:
511
  best_score = score
512
  best_match = a
513
 
514
- if best_score >= len(words) * 0.5:
515
  return best_match
516
  return None
517
 
518
  # =====================================================
519
- # 5. ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ (объявлены ДО использования)
520
  # =====================================================
521
 
522
- agent = DQNAgent()
523
  chat_memory = ChatMemory()
524
- training_thread = None
525
  is_training = False
526
  current_seed = random.randint(0, 999999)
527
 
528
  # =====================================================
529
- # 6. ИГРОВОЙ ЦИКЛ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  # =====================================================
531
 
532
- def game_loop(player_action=None):
 
 
 
 
 
 
533
  global current_seed, agent
534
 
 
535
  ai_env = InfinitePlatformer(seed=current_seed)
536
  player_env = InfinitePlatformer(seed=current_seed)
537
 
538
- ai_state = ai_env.reset()
539
- player_state = player_env.reset()
 
 
 
 
 
 
 
540
 
541
- frames_ai = []
542
- frames_player = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
  ai_score = 0
544
  player_score = 0
545
- ai_alive = True
546
- player_alive = True
547
- sound_html = ''
548
-
549
- for step in range(200):
550
- if ai_alive:
551
- ai_action = agent.act(ai_state)
552
- ai_next_state, ai_reward, ai_done, ai_sound = ai_env.step(ai_action, is_ai=True)
553
- if ai_sound and ai_alive:
554
- sound_html += get_sound_html(ai_sound)
555
- ai_state = ai_next_state
556
- ai_score = ai_env.score
557
- ai_alive = not ai_done
558
-
559
- if player_alive and player_action is not None:
560
- player_next_state, player_reward, player_done, player_sound = player_env.step(player_action, is_ai=False)
561
- if player_sound and player_alive:
562
- sound_html += get_sound_html(player_sound)
563
- player_state = player_next_state
564
- player_score = player_env.score
565
- player_alive = not player_done
566
-
567
- frames_ai.append(ai_env.render(show_player=ai_alive))
568
- frames_player.append(player_env.render(show_player=player_alive))
569
-
570
- if not ai_alive and not player_alive:
571
  break
572
 
573
- final_img_ai = frames_ai[-1] if frames_ai else ai_env.render()
574
- final_img_player = frames_player[-1] if frames_player else player_env.render()
575
 
576
- return final_img_ai, final_img_player, f"🤖 ИИ: {ai_score} | 🎮 Ты: {player_score} | 🪙 Монет: {player_env.coins_collected}", sound_html
 
 
 
 
 
 
 
577
 
578
- # =====================================================
579
- # 7. CHAT FUNCTIONS
580
- # =====================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
 
582
- def chat_ai(message):
583
- global training_thread, is_training, current_seed
 
 
 
 
 
 
 
 
 
584
 
585
  if message.startswith('/ai '):
586
  question = message[4:].strip()
@@ -589,19 +1005,9 @@ def chat_ai(message):
589
 
590
  answer = chat_memory.find_best_match(question)
591
  if answer:
592
- return f"🤖 {answer}"
593
 
594
- try:
595
- tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
596
- model = GPT2LMHeadModel.from_pretrained('gpt2')
597
- inputs = tokenizer.encode(question, return_tensors='pt')
598
- outputs = model.generate(inputs, max_length=100, num_return_sequences=1, temperature=0.7)
599
- answer = tokenizer.decode(outputs[0], skip_special_tokens=True)
600
- if len(answer) > len(question):
601
- answer = answer[len(question):].strip()
602
- return f"🤖 {answer}"
603
- except:
604
- return "🤖 Используй /data для обучения"
605
 
606
  elif message.startswith('/data '):
607
  parts = message[6:].split('|')
@@ -612,112 +1018,75 @@ def chat_ai(message):
612
  return chat_memory.add(question, answer)
613
 
614
  elif message == '/stats':
615
- return f"""
616
- 📊 **Статистика:**
617
  - Память: {len(chat_memory.data)} пар
618
- - Игровой агент: {agent.total_steps} шагов
619
  - Эпсилон: {agent.epsilon:.3f}
620
- - Лучший счёт: {agent.best_score:.0f}
621
- - Тренируется: {is_training}
622
- """
623
 
624
  elif message == '/train':
625
- if not is_training:
626
- is_training = True
627
- def train():
628
- global is_training, agent
629
- for ep in range(100):
630
- if not is_training:
631
- break
632
- agent.train_episode()
633
- if ep % 10 == 0:
634
- print(f"📊 Episode {ep}: Epsilon={agent.epsilon:.3f}")
635
- is_training = False
636
- training_thread = threading.Thread(target=train)
637
- training_thread.start()
638
- return "🚀 Тренировка запущена! Смотри консоль."
639
- return "⏳ Уже тренируется!"
640
 
641
  else:
642
- return "🤖 Команды:\n/ai <вопрос>\n/data вопрос|ответ\n/stats\n/train"
 
 
 
 
643
 
644
- def player_move(action):
645
- return game_loop(action)
646
-
647
- # =====================================================
648
- # 8. GRADIO INTERFACE
649
- # =====================================================
650
-
651
- with gr.Blocks(title="🎮 AI vs Player Platformer", theme=gr.themes.Soft()) as demo:
652
- gr.Markdown("""
653
- # 🎮 Двойной Платформер: ИИ против Тебя!
654
 
655
- ### 🤖 Слева — нейросеть играет сама, справа — ты на том же уровне!
656
- ### 🎵 Звуки: прыжок, монетка, смерть (без фоновой музыки)
657
- """)
 
658
 
659
- with gr.Row():
660
- with gr.Column():
661
- gr.Markdown("### 🤖 ИИ играет")
662
- ai_output = gr.Image(label="AI Game", height=400)
663
- with gr.Column():
664
- gr.Markdown("### 🎮 Ты играешь")
665
- player_output = gr.Image(label="Player Game", height=400)
666
-
667
- with gr.Row():
668
- stats_output = gr.Textbox(label="Счёт", lines=2)
669
-
670
- with gr.Row():
671
- gr.Markdown("### 🎮 Управление:")
672
- left_btn = gr.Button("⬅️ Влево", size="lg")
673
- jump_btn = gr.Button("⬆️ Прыжок", size="lg", variant="primary")
674
- right_btn = gr.Button("➡️ Вправо", size="lg")
675
- reset_btn = gr.Button("🔄 Новый уровень", size="lg", variant="secondary")
676
-
677
- with gr.Row():
678
- sound_output = gr.HTML(label="Звуки")
679
-
680
- with gr.Tab("💬 Чат"):
681
- gr.Markdown("""
682
- ### Команды:
683
- - `/ai <вопрос>` - задать вопрос
684
- - `/data вопрос|ответ` - обучить
685
- - `/stats` - статистика
686
- - `/train` - тренировка ИИ
687
- """)
688
- chat_input = gr.Textbox(label="Команда", placeholder="/ai Как дела?")
689
- chat_output = gr.Markdown(label="Ответ")
690
- chat_btn = gr.Button("Отправить", variant="primary")
691
-
692
- # Привязка кнопок
693
- def reset_game():
694
- global current_seed
695
- current_seed = random.randint(0, 999999)
696
- return game_loop(0)
697
-
698
- def move_left():
699
- return game_loop(1)
700
-
701
- def move_right():
702
- return game_loop(2)
703
-
704
- def move_jump():
705
- return game_loop(3)
706
-
707
- reset_btn.click(reset_game, outputs=[ai_output, player_output, stats_output, sound_output])
708
- left_btn.click(move_left, outputs=[ai_output, player_output, stats_output, sound_output])
709
- right_btn.click(move_right, outputs=[ai_output, player_output, stats_output, sound_output])
710
- jump_btn.click(move_jump, outputs=[ai_output, player_output, stats_output, sound_output])
711
-
712
- chat_btn.click(chat_ai, inputs=[chat_input], outputs=[chat_output])
713
- chat_input.submit(chat_ai, inputs=[chat_input], outputs=[chat_output])
714
-
715
- # Автозапуск
716
- demo.load(reset_game, outputs=[ai_output, player_output, stats_output, sound_output])
717
 
718
  # =====================================================
719
- # 9. ЗАПУСК
720
  # =====================================================
721
 
722
- if __name__ == "__main__":
723
- demo.launch(share=True)
 
 
 
 
 
 
 
 
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
 
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
 
 
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))
 
62
  'die': generate_sound(200, 0.3, 'sweep')
63
  }
64
 
 
 
 
 
 
65
  # =====================================================
66
+ # 2. ПЛАТФОРМЕР
67
  # =====================================================
68
 
69
  class InfinitePlatformer:
 
86
  self.entities = []
87
  self.coins = []
88
  self.obstacles = []
 
89
  self.velocity_y = 0
90
  self.gravity = 0.5
91
  self.jump_power = -8
 
197
  if 0 <= sx < state_size and 0 <= sy < state_size:
198
  state[sy, sx] = 0.3
199
 
 
 
 
200
  return state.flatten()
201
 
202
  def update_entities(self):
 
236
  return True
237
  return False
238
 
239
+ def step(self, action):
 
240
  sound = None
241
 
242
  if action == 1:
 
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']:
 
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:
 
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()
 
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('|')
 
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 "⏳ Уже тренируется!"
1043
+
1044
+ is_training = True
1045
 
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)