X commited on
Commit
78e6a45
·
verified ·
1 Parent(s): c592279

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +391 -280
app.py CHANGED
@@ -1,12 +1,13 @@
1
  """
2
- AI PLATFORMER + CHATBOT (RICH GRAPHICS + FIXED PHYSICS)
3
- AABB Collision, Detailed Rendering, Stateful Engine
 
4
  """
5
 
6
- import os, json, random, threading, logging, time
7
- from collections import deque
8
  from dataclasses import dataclass
9
- from typing import Dict, List, Optional
10
  import numpy as np
11
  import torch
12
  import torch.nn as nn
@@ -25,11 +26,314 @@ class Cfg:
25
  BATCH: int = 64; GAMMA: float = 0.99; LR: float = 5e-4
26
  EPS_DEC: float = 0.995; PORT: int = 7860
27
  MODEL: str = "dqn_model.pth"; CHAT: str = "chat_data.json"
 
 
 
28
 
29
  C = Cfg()
30
 
31
  # ============================================================================
32
- # GAME ENGINE: AABB PHYSICS + RICH WORLD
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  # ============================================================================
34
 
35
  class Engine:
@@ -79,11 +383,7 @@ class Engine:
79
  })
80
 
81
  for _ in range(rng.randint(5, 10) + int(diff)):
82
- cns.append({
83
- 'x': bx + rng.randint(2, 28),
84
- 'y': rng.randint(5, C.GROUND - 2),
85
- 'collected': False
86
- })
87
  return {'obs': obs, 'ens': ens, 'cns': cns}
88
 
89
  def _load_chunks(self):
@@ -91,7 +391,6 @@ class Engine:
91
  for i in range(cc - 1, cc + 3):
92
  if i not in self.chunks:
93
  self.chunks[i] = self._gen_chunk(i)
94
-
95
  vl, vr = self.px - C.W / 2, self.px + C.W / 2
96
  self.obs, self.enemies, self.coin_list = [], [], []
97
  for i in range(cc - 1, cc + 3):
@@ -114,94 +413,58 @@ class Engine:
114
  for ww in range(o.get('w', 1)):
115
  for hh in range(o.get('h', 1)):
116
  sx, sy = h + dx + ww, h + dy + hh
117
- if 0 <= sx < C.VIEW and 0 <= sy < C.VIEW:
118
- s[sy, sx] = v
119
  for e in self.enemies:
120
  dx, dy = int(round(e['x'])) - px, int(round(e['y'])) - py
121
- if 0 <= h + dx < C.VIEW and 0 <= h + dy < C.VIEW:
122
- s[h + dy, h + dx] = 0.7
123
  for c in self.coin_list:
124
  dx, dy = int(round(c['x'])) - px, int(round(c['y'])) - py
125
- if 0 <= h + dx < C.VIEW and 0 <= h + dy < C.VIEW:
126
- s[h + dy, h + dx] = 0.3
127
  return s.flatten()
128
 
129
  def step(self, action: int):
130
  sound = None
131
- PW, PH = 0.6, 0.9 # Player hitbox size
132
-
133
- # Input
134
  self.vx = 0.0
135
  if action == 1: self.vx = -C.SPEED
136
  elif action == 2: self.vx = C.SPEED
137
  if action == 3 and self.grounded:
138
- self.vy = C.JUMP
139
- self.grounded = False
140
- sound = 'jump'
141
 
142
- # === X AXIS MOVEMENT + COLLISION ===
143
  self.px += self.vx
144
  for o in self.obs:
145
  if o.get('pit'): continue
146
  if self._aabb(self.px, self.py, PW, PH, o['x'], o['y'], o['w'], o['h']):
147
- if self.vx > 0:
148
- self.px = o['x'] - PW
149
- elif self.vx < 0:
150
- self.px = o['x'] + o['w']
151
  self.vx = 0
152
 
153
- # === Y AXIS MOVEMENT + COLLISION ===
154
- self.vy += C.GRAV
155
- self.py += self.vy
156
- self.grounded = False
157
-
158
- # Ground collision
159
  if self.py >= C.GROUND:
160
- self.py = C.GROUND
161
- self.vy = 0.0
162
- self.grounded = True
163
-
164
- # Platform collision (Y)
165
  for o in self.obs:
166
  if o.get('pit'): continue
167
  if self._aabb(self.px, self.py, PW, PH, o['x'], o['y'], o['w'], o['h']):
168
- if self.vy > 0: # Falling down onto platform
169
- self.py = o['y'] - PH
170
- self.vy = 0.0
171
- self.grounded = True
172
- elif self.vy < 0: # Jumping up into platform
173
- self.py = o['y'] + o['h']
174
- self.vy = 0.0
175
-
176
- # Death: fell off world
177
- if self.py > C.H + 2:
178
- self.alive = False
179
- return self.get_state(), -50.0, True, 'die'
180
 
181
- # Pit death
 
182
  for o in self.obs:
183
  if o.get('pit') and o['x'] <= self.px + PW / 2 <= o['x'] + o['w'] and self.py >= C.GROUND:
184
- self.alive = False
185
- return self.get_state(), -50.0, True, 'die'
186
-
187
- # Enemy collision
188
  for e in self.enemies:
189
  if self._aabb(self.px, self.py, PW, PH, e['x'] - 0.3, e['y'] - 0.3, 0.6, 0.6):
190
- self.alive = False
191
- return self.get_state(), -50.0, True, 'die'
192
 
193
- # Coins
194
  got = 0
195
  for c in self.coin_list:
196
  if not c['collected'] and self._aabb(self.px, self.py, PW, PH, c['x'] - 0.3, c['y'] - 0.3, 0.6, 0.6):
197
- c['collected'] = True
198
- got += 1
199
- if got:
200
- self.coins += got
201
- self.score += got * 10
202
- sound = 'coin'
203
-
204
- # Update enemies
205
  t = time.time()
206
  for e in self.enemies:
207
  if e['type'] == 'walker':
@@ -210,24 +473,17 @@ class Engine:
210
  else:
211
  e['y'] = (C.GROUND - 1) + np.sin(t * e['spd'] * 3) * 0.5
212
 
213
- self.score += 1
214
- self.step_n += 1
215
- self._load_chunks()
216
-
217
  done = self.step_n > 3000
218
- reward = 1.0 + got * 5.0
219
- return self.get_state(), reward, done, sound
220
 
221
  def world_data(self):
222
  return {
223
  'player': [round(self.px, 2), round(self.py, 2)],
224
- 'obstacles': self.obs,
225
- 'entities': self.enemies,
226
  'coins': [c for c in self.coin_list if not c['collected']],
227
- 'ground': C.GROUND,
228
- 'score': self.score,
229
- 'coins_collected': self.coins,
230
- 'alive': self.alive
231
  }
232
 
233
 
@@ -260,8 +516,8 @@ class Agent:
260
  try:
261
  self.model.load_state_dict(torch.load(C.MODEL, map_location=self.dev))
262
  self.target.load_state_dict(self.model.state_dict())
263
- logger.info("✅ Model loaded")
264
- except Exception as e: logger.warning(f"⚠��� Load failed: {e}")
265
 
266
  def act(self, s):
267
  if random.random() <= self.eps: return random.randrange(C.ACTS)
@@ -299,37 +555,12 @@ class Agent:
299
  def save(self): torch.save(self.model.state_dict(), C.MODEL)
300
 
301
 
302
- # ============================================================================
303
- # CHAT MEMORY
304
- # ============================================================================
305
-
306
- class ChatMem:
307
- def __init__(self):
308
- self.data = {}
309
- if os.path.exists(C.CHAT):
310
- try:
311
- with open(C.CHAT, 'r', encoding='utf-8') as f: self.data = json.load(f)
312
- except: pass
313
- def save(self):
314
- with open(C.CHAT, 'w', encoding='utf-8') as f: json.dump(self.data, f, ensure_ascii=False, indent=2)
315
- def add(self, q, a):
316
- self.data[q.lower()] = a; self.save(); return f"✅ {q} → {a}"
317
- def find(self, q):
318
- q = q.lower()
319
- if q in self.data: return self.data[q]
320
- words = q.split(); best, bs = None, 0
321
- for k, v in self.data.items():
322
- sc = sum(1 for w in words if w in k)
323
- if sc > bs: bs, best = sc, v
324
- return best if bs >= len(words) * 0.4 else None
325
-
326
-
327
  # ============================================================================
328
  # GLOBAL STATE
329
  # ============================================================================
330
 
331
  agent = Agent()
332
- chat = ChatMem()
333
  seed = random.randint(0, 999999)
334
  ai_env = Engine(seed)
335
  pl_env = Engine(seed)
@@ -337,7 +568,7 @@ is_training = False
337
 
338
 
339
  # ============================================================================
340
- # RICH GRAPHICS HTML
341
  # ============================================================================
342
 
343
  HTML = """
@@ -346,7 +577,7 @@ HTML = """
346
  <head>
347
  <meta charset="UTF-8">
348
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
349
- <title>🧠 AI Platformer</title>
350
  <style>
351
  *{margin:0;padding:0;box-sizing:border-box}
352
  body{background:#0d1117;color:#eee;font-family:'Segoe UI',sans-serif;display:flex;justify-content:center;padding:20px;min-height:100vh}
@@ -379,12 +610,13 @@ canvas{width:100%;aspect-ratio:4/1;border-radius:8px;display:block;image-renderi
379
  .cm .u{border-left:3px solid #ff6b6b}
380
  .cm .b{border-left:3px solid #4ecdc4}
381
  .hidden{display:none}
 
382
  </style>
383
  </head>
384
  <body>
385
  <div class="wrap">
386
  <h1>🧠 AI vs Player Platformer</h1>
387
- <p class="sub">🤖 Нейросеть слева 🎮 Ты справа (⬅️ ➡️ ⬆️)</p>
388
  <div class="row">
389
  <div class="box"><h3>🤖 Нейросеть</h3><canvas id="ac"></canvas></div>
390
  <div class="box"><h3>🎮 Ты</h3><canvas id="pc"></canvas></div>
@@ -403,17 +635,22 @@ canvas{width:100%;aspect-ratio:4/1;border-radius:8px;display:block;image-renderi
403
  <button class="brs" id="bReset">🔄 Новый уровень</button>
404
  </div>
405
  <div class="tabs">
406
- <div class="tab active" data-tab="chat">💬 Чат</div>
407
- <div class="tab" data-tab="train">🧠 Тренировка</div>
408
  <div class="tab" data-tab="stats">📊 Статистика</div>
409
  </div>
410
  <div class="tc">
411
  <div id="chatTab">
412
- <div class="cm" id="msgs"><div class="b">🤖 Привет! Команды: /ai вопрос, /data вопрос|ответ, /stats, /train</div></div>
413
- <div class="ca"><input id="ci" placeholder="Введите команду..." onkeydown="if(event.key==='Enter')sendChat()"><button onclick="sendChat()">➤</button></div>
 
 
 
 
 
414
  </div>
415
  <div id="trainTab" class="hidden">
416
- <h3>🧠 Тренировка DQN</h3><p>DQN (256→256→128 нейронов)</p>
417
  <button onclick="startTrain()" style="padding:12px 35px;background:linear-gradient(135deg,#ff6b6b,#ee5a24);color:#fff;border:none;border-radius:10px;font-size:1.1em;cursor:pointer;margin-top:10px">🚀 Запустить</button>
418
  <div id="ts" style="margin-top:10px;color:#888">⏸ Остановлена</div>
419
  </div>
@@ -428,176 +665,57 @@ let pA=0;
428
  function draw(ctx,d,show){
429
  const W=ctx.canvas.width,H=ctx.canvas.height,cW=W/80,cH=H/20;
430
  ctx.clearRect(0,0,W,H);
431
-
432
- // Sky gradient
433
  const sg=ctx.createLinearGradient(0,0,0,H);
434
  sg.addColorStop(0,'#0f0c29');sg.addColorStop(0.5,'#302b63');sg.addColorStop(1,'#24243e');
435
  ctx.fillStyle=sg;ctx.fillRect(0,0,W,H);
436
-
437
- // Stars
438
  ctx.fillStyle='rgba(255,255,255,0.3)';
439
- for(let i=0;i<30;i++){
440
- const sx=(i*137+d.player[0]*0.1)%W,sy=(i*97)%((d.ground-2)*cH);
441
- ctx.fillRect(sx,sy,2,2);
442
- }
443
-
444
  const cam=Math.max(0,d.player[0]-40);
445
  function toS(wx,wy){return[(wx-cam)*cW,wy*cH]}
446
-
447
- // Ground layers
448
  const gy=d.ground*cH;
449
  const gg=ctx.createLinearGradient(0,gy,0,H);
450
  gg.addColorStop(0,'#4a7c59');gg.addColorStop(0.15,'#3d6b4e');gg.addColorStop(0.5,'#5c4033');gg.addColorStop(1,'#3e2723');
451
  ctx.fillStyle=gg;ctx.fillRect(0,gy,W,H-gy);
452
- // Grass top
453
  ctx.fillStyle='#6abf69';ctx.fillRect(0,gy,W,cH*0.3);
454
- ctx.fillStyle='#81c784';
455
- for(let gx=0;gx<W;gx+=8){ctx.fillRect(gx,gy-cH*0.1,4,cH*0.15)}
456
-
457
- // Obstacles with detail
458
  for(const o of d.obstacles){
459
  const[x,y]=toS(o.x,o.y);
460
- if(o.pit){
461
- const pg=ctx.createLinearGradient(0,y-cH,0,y+cH);
462
- pg.addColorStop(0,'#1a1a2e');pg.addColorStop(1,'#000');
463
- ctx.fillStyle=pg;ctx.fillRect(x,y-cH,o.w*cW,cH*2);
464
- ctx.fillStyle='#ff4444';ctx.fillRect(x,y-cH*0.5,o.w*cW,2);
465
- }else{
466
- // Brick pattern
467
- const bg=ctx.createLinearGradient(x,y,x,y+o.h*cH);
468
- bg.addColorStop(0,'#8d6e63');bg.addColorStop(1,'#6d4c41');
469
- ctx.fillStyle=bg;ctx.fillRect(x,y,o.w*cW,o.h*cH);
470
- // Brick lines
471
- ctx.strokeStyle='rgba(0,0,0,0.3)';ctx.lineWidth=1;
472
- for(let by=0;by<o.h;by++){
473
- const yy=y+by*cH;
474
- ctx.beginPath();ctx.moveTo(x,yy);ctx.lineTo(x+o.w*cW,yy);ctx.stroke();
475
- const off=(by%2)*cW*0.5;
476
- for(let bx=off;bx<o.w*cW;bx+=cW){
477
- ctx.beginPath();ctx.moveTo(x+bx,yy);ctx.lineTo(x+bx,yy+cH);ctx.stroke();
478
- }
479
- }
480
- // Top highlight
481
- ctx.fillStyle='rgba(255,255,255,0.15)';ctx.fillRect(x,y,o.w*cW,cH*0.15);
482
- // Shadow
483
- ctx.fillStyle='rgba(0,0,0,0.3)';ctx.fillRect(x+o.w*cW,y,3,o.h*cH);
484
- }
485
  }
486
-
487
- // Enemies with animation
488
  const t=Date.now()/200;
489
- for(const e of d.entities){
490
- const[x,y]=toS(e.x,e.y);
491
- const bounce=Math.sin(t+e.x)*2;
492
- ctx.save();ctx.translate(x+cW/2,y+cH/2+bounce);
493
- // Body
494
- const eg=ctx.createRadialGradient(0,0,2,0,0,cH/2);
495
- eg.addColorStop(0,'#ff6b6b');eg.addColorStop(1,'#c0392b');
496
- ctx.fillStyle=eg;ctx.beginPath();ctx.arc(0,0,cH/2.5,0,Math.PI*2);ctx.fill();
497
- // Eyes
498
- ctx.fillStyle='#fff';
499
- ctx.beginPath();ctx.arc(-4,-3,3,0,Math.PI*2);ctx.arc(4,-3,3,0,Math.PI*2);ctx.fill();
500
- ctx.fillStyle='#000';
501
- const ex=e.dir*2;
502
- ctx.beginPath();ctx.arc(-4+ex,-3,1.5,0,Math.PI*2);ctx.arc(4+ex,-3,1.5,0,Math.PI*2);ctx.fill();
503
- // Glow
504
- ctx.shadowColor='#ff6b6b';ctx.shadowBlur=10;
505
- ctx.strokeStyle='#ff6b6b';ctx.lineWidth=1;ctx.beginPath();ctx.arc(0,0,cH/2.2,0,Math.PI*2);ctx.stroke();
506
- ctx.restore();
507
- }
508
-
509
- // Coins with sparkle
510
- for(const c of d.coins){
511
- const[x,y]=toS(c.x,c.y);
512
- const pulse=1+Math.sin(t*2+c.x)*0.15;
513
- ctx.save();ctx.translate(x+cW/2,y+cH/2);ctx.scale(pulse,pulse);
514
- const cg=ctx.createRadialGradient(-2,-2,1,0,0,cH/3);
515
- cg.addColorStop(0,'#fff9c4');cg.addColorStop(0.5,'#ffd700');cg.addColorStop(1,'#f9a825');
516
- ctx.fillStyle=cg;ctx.beginPath();ctx.arc(0,0,cH/3,0,Math.PI*2);ctx.fill();
517
- ctx.shadowColor='#ffd700';ctx.shadowBlur=12;
518
- ctx.strokeStyle='#ffeb3b';ctx.lineWidth=1.5;ctx.beginPath();ctx.arc(0,0,cH/3,0,Math.PI*2);ctx.stroke();
519
- // Shine
520
- ctx.fillStyle='rgba(255,255,255,0.8)';ctx.beginPath();ctx.arc(-3,-3,2,0,Math.PI*2);ctx.fill();
521
- ctx.restore();
522
- }
523
-
524
- // Player
525
- if(show&&d.alive){
526
- const[px,py]=toS(d.player[0],d.player[1]);
527
- ctx.save();
528
- // Glow
529
- ctx.shadowColor='#00ff88';ctx.shadowBlur=20;
530
- // Body gradient
531
- const pg=ctx.createLinearGradient(px,py,px+cW,py+cH);
532
- pg.addColorStop(0,'#00ff88');pg.addColorStop(1,'#00b894');
533
- ctx.fillStyle=pg;
534
- ctx.fillRect(px+2,py+2,cW-4,cH-4);
535
- // Face
536
- ctx.shadowBlur=0;
537
- ctx.fillStyle='#fff';
538
- ctx.fillRect(px+cW*0.2,py+cH*0.25,cW*0.2,cH*0.2);
539
- ctx.fillRect(px+cW*0.6,py+cH*0.25,cW*0.2,cH*0.2);
540
- ctx.fillStyle='#0d1117';
541
- ctx.fillRect(px+cW*0.25,py+cH*0.3,cW*0.1,cH*0.1);
542
- ctx.fillRect(px+cW*0.65,py+cH*0.3,cW*0.1,cH*0.1);
543
- ctx.restore();
544
- }
545
  }
546
 
547
  async function update(){
548
- try{
549
- const r=await fetch('/step',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:pA})});
550
- const d=await r.json();
551
- draw(aC,d.ai,true);draw(pC,d.player,d.player.alive);
552
- document.getElementById('as').textContent=d.ai.score;
553
- document.getElementById('ps').textContent=d.player.score;
554
- document.getElementById('cc').textContent=d.player.coins_collected;
555
- document.getElementById('ep').textContent=d.epsilon.toFixed(3);
556
- document.getElementById('bs').textContent=d.best_score;
557
- }catch(e){}
558
  }
559
-
560
  const sA=v=>{pA=v};
561
  document.getElementById('bL').onmousedown=()=>sA(1);document.getElementById('bL').onmouseup=()=>sA(0);
562
  document.getElementById('bR').onmousedown=()=>sA(2);document.getElementById('bR').onmouseup=()=>sA(0);
563
  document.getElementById('bJ').onmousedown=()=>sA(3);document.getElementById('bJ').onmouseup=()=>sA(0);
564
- document.addEventListener('keydown',e=>{
565
- if(e.key==='ArrowLeft'){e.preventDefault();sA(1)}
566
- else if(e.key==='ArrowRight'){e.preventDefault();sA(2)}
567
- else if(e.key==='ArrowUp'||e.key===' '){e.preventDefault();sA(3)}
568
- });
569
  document.addEventListener('keyup',e=>{if(['ArrowLeft','ArrowRight','ArrowUp',' '].includes(e.key)){e.preventDefault();sA(0)}});
570
-
571
- document.getElementById('bReset').onclick=async()=>{
572
- const r=await fetch('/reset',{method:'POST'});const d=await r.json();
573
- draw(aC,d.ai,true);draw(pC,d.player,true);
574
- document.getElementById('as').textContent=d.ai.score;
575
- document.getElementById('ps').textContent=d.player.score;
576
- };
577
 
578
  async function sendChat(){
579
  const inp=document.getElementById('ci');const msg=inp.value.trim();if(!msg)return;inp.value='';
580
- const m=document.getElementById('msgs');
581
- m.innerHTML+=`<div class="u">👤 ${msg}</div>`;m.scrollTop=m.scrollHeight;
582
  const r=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg})});
583
- const d=await r.json();
584
- m.innerHTML+=`<div class="b">🤖 ${d.response}</div>`;m.scrollTop=m.scrollHeight;
585
- }
586
-
587
- async function startTrain(){
588
- document.getElementById('ts').textContent='⏳ Запуск...';
589
- const r=await fetch('/train',{method:'POST'});const d=await r.json();
590
- document.getElementById('ts').textContent=d.message;
591
  }
592
-
593
  document.querySelectorAll('.tab').forEach(t=>t.onclick=function(){
594
- document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
595
- this.classList.add('active');const n=this.dataset.tab;
596
- document.querySelectorAll('.tc>div').forEach(d=>d.classList.add('hidden'));
597
- document.getElementById(n+'Tab').classList.remove('hidden');
598
- if(n==='stats')fetch('/stats').then(r=>r.json()).then(d=>{
599
- document.getElementById('sc').innerHTML=`<p>🧠 Память: ${d.memory_size}</p><p>🎮 Шагов: ${d.steps}</p><p>📉 ε: ${d.epsilon}</p><p>🏆 Рекорд: ${d.best_score}</p><p>⚡ Тренируется: ${d.training?'✅':'❌'}</p>`;
600
- });
 
601
  });
602
 
603
  setInterval(update,100);update();
@@ -619,18 +737,11 @@ def index(): return render_template_string(HTML)
619
  def step():
620
  global ai_env, pl_env
621
  action = request.json.get('action', 0)
622
- if ai_env.alive:
623
- ai_env.step(agent.act(ai_env.get_state()))
624
- else:
625
- ai_env.reset()
626
- if pl_env.alive:
627
- pl_env.step(action)
628
- else:
629
- pl_env.reset()
630
- return jsonify({
631
- 'ai': ai_env.world_data(), 'player': pl_env.world_data(),
632
- 'epsilon': agent.eps, 'best_score': agent.best
633
- })
634
 
635
  @app.route('/reset', methods=['POST'])
636
  def reset():
@@ -642,18 +753,17 @@ def reset():
642
  @app.route('/chat', methods=['POST'])
643
  def chat_route():
644
  msg = request.json.get('message', '').strip()
645
- if msg.startswith('/ai '):
646
- a = chat.find(msg[4:])
647
- return jsonify({'response': a or "🤖 Не знаю. Обучи через /data"})
648
- elif msg.startswith('/data '):
649
  p = msg[6:].split('|')
650
  if len(p) != 2: return jsonify({'response': "❌ Формат: /data вопрос|ответ"})
651
- return jsonify({'response': chat.add(p[0].strip(), p[1].strip())})
652
  elif msg == '/stats':
653
- return jsonify({'response': f"📊 Память: {len(chat.data)}, Шагов: {agent.steps}"})
 
654
  elif msg == '/train':
655
  return jsonify({'response': start_training()})
656
- return jsonify({'response': "🤖 Команды: /ai, /data, /stats, /train"})
 
657
 
658
  @app.route('/train', methods=['POST'])
659
  def train_route(): return jsonify({'message': start_training()})
@@ -661,8 +771,9 @@ def train_route(): return jsonify({'message': start_training()})
661
  @app.route('/stats')
662
  def stats():
663
  return jsonify({
664
- 'memory_size': len(chat.data), 'steps': agent.steps,
665
- 'epsilon': round(agent.eps, 3), 'best_score': agent.best, 'training': is_training
 
666
  })
667
 
668
  def start_training():
@@ -675,11 +786,11 @@ def start_training():
675
  for ep in range(100):
676
  if not is_training: break
677
  sc = agent.train_ep()
678
- if ep % 10 == 0: logger.info(f"Ep {ep}: score={sc:.1f}, ε={agent.eps:.3f}")
679
- except Exception as e: logger.error(f"Train error: {e}")
680
  finally: is_training = False
681
  threading.Thread(target=_t, daemon=True).start()
682
- return "🚀 Тренировка запущена!"
683
 
684
  if __name__ == '__main__':
685
  app.run(host='0.0.0.0', port=C.PORT, debug=False)
 
1
  """
2
+ AI PLATFORMER + NEURAL CHATBOT (FROM SCRATCH)
3
+ Custom PyTorch NLP model, AABB physics, rich graphics
4
+ No external NLP libraries - everything built from zero
5
  """
6
 
7
+ import os, json, random, threading, logging, time, re, math
8
+ from collections import deque, Counter
9
  from dataclasses import dataclass
10
+ from typing import Dict, List, Optional, Tuple
11
  import numpy as np
12
  import torch
13
  import torch.nn as nn
 
26
  BATCH: int = 64; GAMMA: float = 0.99; LR: float = 5e-4
27
  EPS_DEC: float = 0.995; PORT: int = 7860
28
  MODEL: str = "dqn_model.pth"; CHAT: str = "chat_data.json"
29
+ # Chat NN config
30
+ VOCAB_MAX: int = 2000; EMB_DIM: int = 64; HIDDEN: int = 128
31
+ CHAT_LR: float = 1e-3; CHAT_EPOCHS: int = 80; SIM_THRESH: float = 0.5
32
 
33
  C = Cfg()
34
 
35
  # ============================================================================
36
+ # CUSTOM TOKENIZER (From Scratch)
37
+ # ============================================================================
38
+
39
+ class Tokenizer:
40
+ """Simple character-ngram + word tokenizer built from zero."""
41
+
42
+ PAD = "<PAD>"; UNK = "<UNK>"
43
+
44
+ def __init__(self, max_vocab: int = 2000):
45
+ self.max_vocab = max_vocab
46
+ self.word2idx: Dict[str, int] = {self.PAD: 0, self.UNK: 1}
47
+ self.idx2word: Dict[int, str] = {0: self.PAD, 1: self.UNK}
48
+ self.frozen = False
49
+
50
+ def _tokenize(self, text: str) -> List[str]:
51
+ text = text.lower().strip()
52
+ text = re.sub(r'[^\w\sа-яё]', ' ', text)
53
+ words = text.split()
54
+ # Add char bigrams for fuzzy matching
55
+ tokens = []
56
+ for w in words:
57
+ tokens.append(w)
58
+ if len(w) > 2:
59
+ tokens.extend([w[i:i+2] for i in range(len(w)-1)])
60
+ return tokens
61
+
62
+ def build_vocab(self, texts: List[str]):
63
+ counter = Counter()
64
+ for t in texts:
65
+ counter.update(self._tokenize(t))
66
+
67
+ most_common = counter.most_common(self.max_vocab - 2)
68
+ for word, _ in most_common:
69
+ idx = len(self.word2idx)
70
+ self.word2idx[word] = idx
71
+ self.idx2word[idx] = word
72
+
73
+ self.frozen = True
74
+ logger.info(f"📝 Vocab built: {len(self.word2idx)} tokens")
75
+
76
+ def encode(self, text: str, max_len: int = 32) -> List[int]:
77
+ tokens = self._tokenize(text)[:max_len]
78
+ ids = [self.word2idx.get(t, 1) for t in tokens]
79
+ # Pad
80
+ ids += [0] * (max_len - len(ids))
81
+ return ids
82
+
83
+ @property
84
+ def vocab_size(self) -> int:
85
+ return len(self.word2idx)
86
+
87
+
88
+ # ============================================================================
89
+ # NEURAL CHAT MODEL (From Scratch)
90
+ # ============================================================================
91
+
92
+ class ChatEncoder(nn.Module):
93
+ """Encodes text into semantic embedding vector."""
94
+
95
+ def __init__(self, vocab_size: int, emb_dim: int, hidden: int, out_dim: int):
96
+ super().__init__()
97
+ self.embedding = nn.Embedding(vocab_size, emb_dim, padding_idx=0)
98
+ self.fc1 = nn.Linear(emb_dim, hidden)
99
+ self.fc2 = nn.Linear(hidden, hidden)
100
+ self.fc3 = nn.Linear(hidden, out_dim)
101
+ self.relu = nn.ReLU()
102
+ self.dropout = nn.Dropout(0.2)
103
+ self.ln1 = nn.LayerNorm(hidden)
104
+ self.ln2 = nn.LayerNorm(hidden)
105
+
106
+ def forward(self, x):
107
+ # x: (batch, seq_len)
108
+ emb = self.embedding(x) # (batch, seq_len, emb_dim)
109
+ # Mean pooling over sequence (ignore padding)
110
+ mask = (x != 0).unsqueeze(-1).float() # (batch, seq_len, 1)
111
+ pooled = (emb * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1) # (batch, emb_dim)
112
+
113
+ h = self.ln1(self.fc1(pooled))
114
+ h = self.relu(h)
115
+ h = self.dropout(h)
116
+ h = self.ln2(self.fc2(h))
117
+ h = self.relu(h)
118
+ h = self.dropout(h)
119
+ out = self.fc3(h)
120
+ # L2 normalize for cosine similarity
121
+ return nn.functional.normalize(out, p=2, dim=-1)
122
+
123
+
124
+ class NeuralChat:
125
+ """Chat system with custom-trained neural network."""
126
+
127
+ SEQ_LEN = 32
128
+ OUT_DIM = 64
129
+
130
+ def __init__(self):
131
+ self.tokenizer = Tokenizer(C.VOCAB_MAX)
132
+ self.data: Dict[str, str] = {}
133
+ self.questions: List[str] = []
134
+ self.q_embeddings: Optional[torch.Tensor] = None
135
+ self.model: Optional[ChatEncoder] = None
136
+ self.device = torch.device('cpu')
137
+
138
+ self._load_data()
139
+ self._build_and_train()
140
+ logger.info(f"✅ Neural chat ready. {len(self.data)} entries.")
141
+
142
+ def _load_data(self):
143
+ if os.path.exists(C.CHAT):
144
+ try:
145
+ with open(C.CHAT, 'r', encoding='utf-8') as f:
146
+ self.data = json.load(f)
147
+ except Exception as e:
148
+ logger.warning(f"⚠️ Chat load failed: {e}")
149
+
150
+ if not self.data:
151
+ self.data = {
152
+ "как играть": "Стрелки ⬅️➡️ для движения, ⬆️/Пробел для прыжка",
153
+ "что делает нейросеть": "DQN учится играть методом проб и ошибок, получая награду за монеты",
154
+ "как обучить бота": "Напиши /data вопрос|ответ чтобы добавить знание",
155
+ "какой алгоритм": "Deep Q-Network с replay buffer и target network",
156
+ "зачем эпсилон": "Epsilon-greedy: чем выше ε, тем больше случайных действий для исследования",
157
+ "как сбросить уровень": "Нажми 🔄 Новый уровень",
158
+ "что такое dqn": "Deep Q-Network предсказывает ценность каждого действия в состоянии",
159
+ "сколько нейронов": "1600→256→256→128→4 (вход=40x40 сетка)",
160
+ "привет": "Привет! Я нейросетевой чат-бот платформера 🧠",
161
+ "как работает чат": "Я использую кастомную нейросеть на PyTorch с эмбеддингами и косинусным сходством",
162
+ "кто тебя создал": "Я написан с нуля на PyTorch без внешних NLP библиотек",
163
+ }
164
+ self._save_data()
165
+
166
+ def _save_data(self):
167
+ with open(C.CHAT, 'w', encoding='utf-8') as f:
168
+ json.dump(self.data, f, ensure_ascii=False, indent=2)
169
+
170
+ def _build_and_train(self):
171
+ """Build vocab, create model, train on existing data."""
172
+ self.questions = list(self.data.keys())
173
+
174
+ if not self.questions:
175
+ return
176
+
177
+ # Build vocabulary from all questions
178
+ self.tokenizer.build_vocab(self.questions)
179
+
180
+ # Create model
181
+ self.model = ChatEncoder(
182
+ vocab_size=self.tokenizer.vocab_size,
183
+ emb_dim=C.EMB_DIM,
184
+ hidden=C.HIDDEN,
185
+ out_dim=self.OUT_DIM
186
+ ).to(self.device)
187
+
188
+ # Train on question pairs (contrastive learning)
189
+ self._train_model()
190
+
191
+ # Pre-compute embeddings
192
+ self._update_index()
193
+
194
+ def _train_model(self):
195
+ """Train encoder using contrastive loss on question pairs."""
196
+ if len(self.questions) < 2:
197
+ # Not enough data to train meaningfully, use untrained model
198
+ logger.warning("⚠️ Too few entries to train, using untrained embeddings")
199
+ return
200
+
201
+ optimizer = optim.Adam(self.model.parameters(), lr=C.CHAT_LR)
202
+
203
+ # Encode all questions
204
+ q_ids = torch.LongTensor([
205
+ self.tokenizer.encode(q, self.SEQ_LEN) for q in self.questions
206
+ ]).to(self.device)
207
+
208
+ n = len(self.questions)
209
+ best_loss = float('inf')
210
+
211
+ logger.info(f"🧠 Training chat NN: {n} samples, {C.CHAT_EPOCHS} epochs...")
212
+
213
+ for epoch in range(C.CHAT_EPOCHS):
214
+ total_loss = 0.0
215
+ num_pairs = 0
216
+
217
+ # For each question, positive = itself, negative = random other
218
+ indices = list(range(n))
219
+ random.shuffle(indices)
220
+
221
+ for i in indices:
222
+ anchor = q_ids[i:i+1] # (1, seq)
223
+ positive = q_ids[i:i+1] # same question
224
+
225
+ # Pick negative (different question)
226
+ neg_idx = random.choice([j for j in range(n) if j != i])
227
+ negative = q_ids[neg_idx:neg_idx+1]
228
+
229
+ emb_a = self.model(anchor) # (1, out_dim)
230
+ emb_p = self.model(positive) # (1, out_dim)
231
+ emb_n = self.model(negative) # (1, out_dim)
232
+
233
+ # Cosine similarity
234
+ pos_sim = nn.functional.cosine_similarity(emb_a, emb_p)
235
+ neg_sim = nn.functional.cosine_similarity(emb_a, emb_n)
236
+
237
+ # Contrastive loss: maximize pos_sim, minimize neg_sim
238
+ margin = 0.3
239
+ loss = torch.relu(margin - pos_sim + neg_sim).mean()
240
+
241
+ optimizer.zero_grad()
242
+ loss.backward()
243
+ optimizer.step()
244
+
245
+ total_loss += loss.item()
246
+ num_pairs += 1
247
+
248
+ avg_loss = total_loss / max(num_pairs, 1)
249
+ if avg_loss < best_loss:
250
+ best_loss = avg_loss
251
+
252
+ if (epoch + 1) % 20 == 0:
253
+ logger.info(f" Epoch {epoch+1}/{C.CHAT_EPOCHS}, loss={avg_loss:.4f}")
254
+
255
+ logger.info(f"✅ Chat training complete. Best loss: {best_loss:.4f}")
256
+
257
+ def _update_index(self):
258
+ """Pre-compute embeddings for all stored questions."""
259
+ if not self.questions or self.model is None:
260
+ self.q_embeddings = None
261
+ return
262
+
263
+ self.model.eval()
264
+ with torch.no_grad():
265
+ ids = torch.LongTensor([
266
+ self.tokenizer.encode(q, self.SEQ_LEN) for q in self.questions
267
+ ]).to(self.device)
268
+ self.q_embeddings = self.model(ids) # (n, out_dim)
269
+ self.model.train()
270
+
271
+ def ask(self, query: str) -> str:
272
+ """Semantic search using trained neural embeddings."""
273
+ if not self.questions or self.q_embeddings is None:
274
+ return "🤖 База пуста. Обучи меня: /data вопрос|ответ"
275
+
276
+ self.model.eval()
277
+ with torch.no_grad():
278
+ q_id = torch.LongTensor([self.tokenizer.encode(query, self.SEQ_LEN)]).to(self.device)
279
+ q_emb = self.model(q_id) # (1, out_dim)
280
+
281
+ # Cosine similarity against all stored questions
282
+ sims = nn.functional.cosine_similarity(q_emb, self.q_embeddings)
283
+ best_idx = torch.argmax(sims).item()
284
+ best_score = sims[best_idx].item()
285
+
286
+ self.model.train()
287
+
288
+ if best_score >= C.SIM_THRESH:
289
+ conf = int(best_score * 100)
290
+ return f"{self.data[self.questions[best_idx]]} (🧠 {conf}%)"
291
+
292
+ return f"🤖 Не знаю «{query}». Научи: /data {query}|ответ"
293
+
294
+ def teach(self, question: str, answer: str) -> str:
295
+ """Add knowledge and retrain incrementally."""
296
+ question = question.strip().lower()
297
+ answer = answer.strip()
298
+
299
+ if not question or not answer:
300
+ return "❌ Формат: /data вопрос|ответ"
301
+
302
+ is_new = question not in self.data
303
+ self.data[question] = answer
304
+ self._save_data()
305
+
306
+ # Rebuild everything
307
+ self.questions = list(self.data.keys())
308
+ self.tokenizer = Tokenizer(C.VOCAB_MAX)
309
+ self.tokenizer.build_vocab(self.questions)
310
+
311
+ self.model = ChatEncoder(
312
+ vocab_size=self.tokenizer.vocab_size,
313
+ emb_dim=C.EMB_DIM,
314
+ hidden=C.HIDDEN,
315
+ out_dim=self.OUT_DIM
316
+ ).to(self.device)
317
+
318
+ self._train_model()
319
+ self._update_index()
320
+
321
+ action = "Добавлено" if is_new else "Обновлено"
322
+ return f"✅ {action}: «{question}» → «{answer}». Модель переобучена."
323
+
324
+ @property
325
+ def stats(self) -> dict:
326
+ params = sum(p.numel() for p in self.model.parameters()) if self.model else 0
327
+ return {
328
+ 'entries': len(self.data),
329
+ 'vocab': self.tokenizer.vocab_size,
330
+ 'params': params,
331
+ 'emb_dim': self.OUT_DIM
332
+ }
333
+
334
+
335
+ # ============================================================================
336
+ # GAME ENGINE (AABB Physics + Rich Graphics)
337
  # ============================================================================
338
 
339
  class Engine:
 
383
  })
384
 
385
  for _ in range(rng.randint(5, 10) + int(diff)):
386
+ cns.append({'x': bx + rng.randint(2, 28), 'y': rng.randint(5, C.GROUND - 2), 'collected': False})
 
 
 
 
387
  return {'obs': obs, 'ens': ens, 'cns': cns}
388
 
389
  def _load_chunks(self):
 
391
  for i in range(cc - 1, cc + 3):
392
  if i not in self.chunks:
393
  self.chunks[i] = self._gen_chunk(i)
 
394
  vl, vr = self.px - C.W / 2, self.px + C.W / 2
395
  self.obs, self.enemies, self.coin_list = [], [], []
396
  for i in range(cc - 1, cc + 3):
 
413
  for ww in range(o.get('w', 1)):
414
  for hh in range(o.get('h', 1)):
415
  sx, sy = h + dx + ww, h + dy + hh
416
+ if 0 <= sx < C.VIEW and 0 <= sy < C.VIEW: s[sy, sx] = v
 
417
  for e in self.enemies:
418
  dx, dy = int(round(e['x'])) - px, int(round(e['y'])) - py
419
+ if 0 <= h + dx < C.VIEW and 0 <= h + dy < C.VIEW: s[h + dy, h + dx] = 0.7
 
420
  for c in self.coin_list:
421
  dx, dy = int(round(c['x'])) - px, int(round(c['y'])) - py
422
+ if 0 <= h + dx < C.VIEW and 0 <= h + dy < C.VIEW: s[h + dy, h + dx] = 0.3
 
423
  return s.flatten()
424
 
425
  def step(self, action: int):
426
  sound = None
427
+ PW, PH = 0.6, 0.9
 
 
428
  self.vx = 0.0
429
  if action == 1: self.vx = -C.SPEED
430
  elif action == 2: self.vx = C.SPEED
431
  if action == 3 and self.grounded:
432
+ self.vy = C.JUMP; self.grounded = False; sound = 'jump'
 
 
433
 
434
+ # X axis
435
  self.px += self.vx
436
  for o in self.obs:
437
  if o.get('pit'): continue
438
  if self._aabb(self.px, self.py, PW, PH, o['x'], o['y'], o['w'], o['h']):
439
+ if self.vx > 0: self.px = o['x'] - PW
440
+ elif self.vx < 0: self.px = o['x'] + o['w']
 
 
441
  self.vx = 0
442
 
443
+ # Y axis
444
+ self.vy += C.GRAV; self.py += self.vy; self.grounded = False
 
 
 
 
445
  if self.py >= C.GROUND:
446
+ self.py = C.GROUND; self.vy = 0.0; self.grounded = True
 
 
 
 
447
  for o in self.obs:
448
  if o.get('pit'): continue
449
  if self._aabb(self.px, self.py, PW, PH, o['x'], o['y'], o['w'], o['h']):
450
+ if self.vy > 0: self.py = o['y'] - PH; self.vy = 0.0; self.grounded = True
451
+ elif self.vy < 0: self.py = o['y'] + o['h']; self.vy = 0.0
 
 
 
 
 
 
 
 
 
 
452
 
453
+ if self.py > C.H + 2:
454
+ self.alive = False; return self.get_state(), -50.0, True, 'die'
455
  for o in self.obs:
456
  if o.get('pit') and o['x'] <= self.px + PW / 2 <= o['x'] + o['w'] and self.py >= C.GROUND:
457
+ self.alive = False; return self.get_state(), -50.0, True, 'die'
 
 
 
458
  for e in self.enemies:
459
  if self._aabb(self.px, self.py, PW, PH, e['x'] - 0.3, e['y'] - 0.3, 0.6, 0.6):
460
+ self.alive = False; return self.get_state(), -50.0, True, 'die'
 
461
 
 
462
  got = 0
463
  for c in self.coin_list:
464
  if not c['collected'] and self._aabb(self.px, self.py, PW, PH, c['x'] - 0.3, c['y'] - 0.3, 0.6, 0.6):
465
+ c['collected'] = True; got += 1
466
+ if got: self.coins += got; self.score += got * 10; sound = 'coin'
467
+
 
 
 
 
 
468
  t = time.time()
469
  for e in self.enemies:
470
  if e['type'] == 'walker':
 
473
  else:
474
  e['y'] = (C.GROUND - 1) + np.sin(t * e['spd'] * 3) * 0.5
475
 
476
+ self.score += 1; self.step_n += 1; self._load_chunks()
 
 
 
477
  done = self.step_n > 3000
478
+ return self.get_state(), 1.0 + got * 5.0, done, sound
 
479
 
480
  def world_data(self):
481
  return {
482
  'player': [round(self.px, 2), round(self.py, 2)],
483
+ 'obstacles': self.obs, 'entities': self.enemies,
 
484
  'coins': [c for c in self.coin_list if not c['collected']],
485
+ 'ground': C.GROUND, 'score': self.score,
486
+ 'coins_collected': self.coins, 'alive': self.alive
 
 
487
  }
488
 
489
 
 
516
  try:
517
  self.model.load_state_dict(torch.load(C.MODEL, map_location=self.dev))
518
  self.target.load_state_dict(self.model.state_dict())
519
+ logger.info("✅ DQN loaded")
520
+ except Exception as e: logger.warning(f"⚠ DQN load failed: {e}")
521
 
522
  def act(self, s):
523
  if random.random() <= self.eps: return random.randrange(C.ACTS)
 
555
  def save(self): torch.save(self.model.state_dict(), C.MODEL)
556
 
557
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
  # ============================================================================
559
  # GLOBAL STATE
560
  # ============================================================================
561
 
562
  agent = Agent()
563
+ chat = NeuralChat()
564
  seed = random.randint(0, 999999)
565
  ai_env = Engine(seed)
566
  pl_env = Engine(seed)
 
568
 
569
 
570
  # ============================================================================
571
+ # HTML (Rich Graphics)
572
  # ============================================================================
573
 
574
  HTML = """
 
577
  <head>
578
  <meta charset="UTF-8">
579
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
580
+ <title>🧠 AI Platformer + Neural Chat</title>
581
  <style>
582
  *{margin:0;padding:0;box-sizing:border-box}
583
  body{background:#0d1117;color:#eee;font-family:'Segoe UI',sans-serif;display:flex;justify-content:center;padding:20px;min-height:100vh}
 
610
  .cm .u{border-left:3px solid #ff6b6b}
611
  .cm .b{border-left:3px solid #4ecdc4}
612
  .hidden{display:none}
613
+ .nn-info{font-size:0.85em;color:#888;margin-top:8px;padding:8px;background:#0d1117;border-radius:6px}
614
  </style>
615
  </head>
616
  <body>
617
  <div class="wrap">
618
  <h1>🧠 AI vs Player Platformer</h1>
619
+ <p class="sub">🤖 Нейросеть слева 🎮 Ты справа (⬅️ ➡️ ⬆️) | 💬 Чат = нейросеть с нуля</p>
620
  <div class="row">
621
  <div class="box"><h3>🤖 Нейросеть</h3><canvas id="ac"></canvas></div>
622
  <div class="box"><h3>🎮 Ты</h3><canvas id="pc"></canvas></div>
 
635
  <button class="brs" id="bReset">🔄 Новый уровень</button>
636
  </div>
637
  <div class="tabs">
638
+ <div class="tab active" data-tab="chat">💬 Нейро-чат</div>
639
+ <div class="tab" data-tab="train">🧠 Тренировка DQN</div>
640
  <div class="tab" data-tab="stats">📊 Статистика</div>
641
  </div>
642
  <div class="tc">
643
  <div id="chatTab">
644
+ <div class="cm" id="msgs">
645
+ <div class="b">🧠 Привет! Я нейросетевой чат, написанный с нуля на PyTorch.</div>
646
+ <div class="b">Команды: /data вопрос|ответ, /stats, /train</div>
647
+ <div class="b">Спрашивай что угодно — я ищу по смыслу, не по словам!</div>
648
+ </div>
649
+ <div class="ca"><input id="ci" placeholder="Задай вопрос или /data вопрос|ответ..." onkeydown="if(event.key==='Enter')sendChat()"><button onclick="sendChat()">➤</button></div>
650
+ <div class="nn-info" id="nnInfo">Загрузка модели...</div>
651
  </div>
652
  <div id="trainTab" class="hidden">
653
+ <h3>🧠 Тренировка DQN</h3><p>Deep Q-Network (256→256→128)</p>
654
  <button onclick="startTrain()" style="padding:12px 35px;background:linear-gradient(135deg,#ff6b6b,#ee5a24);color:#fff;border:none;border-radius:10px;font-size:1.1em;cursor:pointer;margin-top:10px">🚀 Запустить</button>
655
  <div id="ts" style="margin-top:10px;color:#888">⏸ Остановлена</div>
656
  </div>
 
665
  function draw(ctx,d,show){
666
  const W=ctx.canvas.width,H=ctx.canvas.height,cW=W/80,cH=H/20;
667
  ctx.clearRect(0,0,W,H);
 
 
668
  const sg=ctx.createLinearGradient(0,0,0,H);
669
  sg.addColorStop(0,'#0f0c29');sg.addColorStop(0.5,'#302b63');sg.addColorStop(1,'#24243e');
670
  ctx.fillStyle=sg;ctx.fillRect(0,0,W,H);
 
 
671
  ctx.fillStyle='rgba(255,255,255,0.3)';
672
+ for(let i=0;i<30;i++){const sx=(i*137+d.player[0]*0.1)%W,sy=(i*97)%((d.ground-2)*cH);ctx.fillRect(sx,sy,2,2)}
 
 
 
 
673
  const cam=Math.max(0,d.player[0]-40);
674
  function toS(wx,wy){return[(wx-cam)*cW,wy*cH]}
 
 
675
  const gy=d.ground*cH;
676
  const gg=ctx.createLinearGradient(0,gy,0,H);
677
  gg.addColorStop(0,'#4a7c59');gg.addColorStop(0.15,'#3d6b4e');gg.addColorStop(0.5,'#5c4033');gg.addColorStop(1,'#3e2723');
678
  ctx.fillStyle=gg;ctx.fillRect(0,gy,W,H-gy);
 
679
  ctx.fillStyle='#6abf69';ctx.fillRect(0,gy,W,cH*0.3);
680
+ ctx.fillStyle='#81c784';for(let gx=0;gx<W;gx+=8)ctx.fillRect(gx,gy-cH*0.1,4,cH*0.15);
 
 
 
681
  for(const o of d.obstacles){
682
  const[x,y]=toS(o.x,o.y);
683
+ if(o.pit){const pg=ctx.createLinearGradient(0,y-cH,0,y+cH);pg.addColorStop(0,'#1a1a2e');pg.addColorStop(1,'#000');ctx.fillStyle=pg;ctx.fillRect(x,y-cH,o.w*cW,cH*2);ctx.fillStyle='#ff4444';ctx.fillRect(x,y-cH*0.5,o.w*cW,2)}
684
+ else{const bg=ctx.createLinearGradient(x,y,x,y+o.h*cH);bg.addColorStop(0,'#8d6e63');bg.addColorStop(1,'#6d4c41');ctx.fillStyle=bg;ctx.fillRect(x,y,o.w*cW,o.h*cH);ctx.strokeStyle='rgba(0,0,0,0.3)';ctx.lineWidth=1;for(let by=0;by<o.h;by++){const yy=y+by*cH;ctx.beginPath();ctx.moveTo(x,yy);ctx.lineTo(x+o.w*cW,yy);ctx.stroke();const off=(by%2)*cW*0.5;for(let bx=off;bx<o.w*cW;bx+=cW){ctx.beginPath();ctx.moveTo(x+bx,yy);ctx.lineTo(x+bx,yy+cH);ctx.stroke()}}ctx.fillStyle='rgba(255,255,255,0.15)';ctx.fillRect(x,y,o.w*cW,cH*0.15);ctx.fillStyle='rgba(0,0,0,0.3)';ctx.fillRect(x+o.w*cW,y,3,o.h*cH)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
  }
 
 
686
  const t=Date.now()/200;
687
+ for(const e of d.entities){const[x,y]=toS(e.x,e.y);const bounce=Math.sin(t+e.x)*2;ctx.save();ctx.translate(x+cW/2,y+cH/2+bounce);const eg=ctx.createRadialGradient(0,0,2,0,0,cH/2);eg.addColorStop(0,'#ff6b6b');eg.addColorStop(1,'#c0392b');ctx.fillStyle=eg;ctx.beginPath();ctx.arc(0,0,cH/2.5,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(-4,-3,3,0,Math.PI*2);ctx.arc(4,-3,3,0,Math.PI*2);ctx.fill();ctx.fillStyle='#000';const ex=e.dir*2;ctx.beginPath();ctx.arc(-4+ex,-3,1.5,0,Math.PI*2);ctx.arc(4+ex,-3,1.5,0,Math.PI*2);ctx.fill();ctx.shadowColor='#ff6b6b';ctx.shadowBlur=10;ctx.strokeStyle='#ff6b6b';ctx.lineWidth=1;ctx.beginPath();ctx.arc(0,0,cH/2.2,0,Math.PI*2);ctx.stroke();ctx.restore()}
688
+ for(const c of d.coins){const[x,y]=toS(c.x,c.y);const pulse=1+Math.sin(t*2+c.x)*0.15;ctx.save();ctx.translate(x+cW/2,y+cH/2);ctx.scale(pulse,pulse);const cg=ctx.createRadialGradient(-2,-2,1,0,0,cH/3);cg.addColorStop(0,'#fff9c4');cg.addColorStop(0.5,'#ffd700');cg.addColorStop(1,'#f9a825');ctx.fillStyle=cg;ctx.beginPath();ctx.arc(0,0,cH/3,0,Math.PI*2);ctx.fill();ctx.shadowColor='#ffd700';ctx.shadowBlur=12;ctx.strokeStyle='#ffeb3b';ctx.lineWidth=1.5;ctx.beginPath();ctx.arc(0,0,cH/3,0,Math.PI*2);ctx.stroke();ctx.fillStyle='rgba(255,255,255,0.8)';ctx.beginPath();ctx.arc(-3,-3,2,0,Math.PI*2);ctx.fill();ctx.restore()}
689
+ if(show&&d.alive){const[px,py]=toS(d.player[0],d.player[1]);ctx.save();ctx.shadowColor='#00ff88';ctx.shadowBlur=20;const pg=ctx.createLinearGradient(px,py,px+cW,py+cH);pg.addColorStop(0,'#00ff88');pg.addColorStop(1,'#00b894');ctx.fillStyle=pg;ctx.fillRect(px+2,py+2,cW-4,cH-4);ctx.shadowBlur=0;ctx.fillStyle='#fff';ctx.fillRect(px+cW*0.2,py+cH*0.25,cW*0.2,cH*0.2);ctx.fillRect(px+cW*0.6,py+cH*0.25,cW*0.2,cH*0.2);ctx.fillStyle='#0d1117';ctx.fillRect(px+cW*0.25,py+cH*0.3,cW*0.1,cH*0.1);ctx.fillRect(px+cW*0.65,py+cH*0.3,cW*0.1,cH*0.1);ctx.restore()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
  }
691
 
692
  async function update(){
693
+ try{const r=await fetch('/step',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:pA})});const d=await r.json();draw(aC,d.ai,true);draw(pC,d.player,d.player.alive);document.getElementById('as').textContent=d.ai.score;document.getElementById('ps').textContent=d.player.score;document.getElementById('cc').textContent=d.player.coins_collected;document.getElementById('ep').textContent=d.epsilon.toFixed(3);document.getElementById('bs').textContent=d.best_score}catch(e){}
 
 
 
 
 
 
 
 
 
694
  }
 
695
  const sA=v=>{pA=v};
696
  document.getElementById('bL').onmousedown=()=>sA(1);document.getElementById('bL').onmouseup=()=>sA(0);
697
  document.getElementById('bR').onmousedown=()=>sA(2);document.getElementById('bR').onmouseup=()=>sA(0);
698
  document.getElementById('bJ').onmousedown=()=>sA(3);document.getElementById('bJ').onmouseup=()=>sA(0);
699
+ document.addEventListener('keydown',e=>{if(e.key==='ArrowLeft'){e.preventDefault();sA(1)}else if(e.key==='ArrowRight'){e.preventDefault();sA(2)}else if(e.key==='ArrowUp'||e.key===' '){e.preventDefault();sA(3)}});
 
 
 
 
700
  document.addEventListener('keyup',e=>{if(['ArrowLeft','ArrowRight','ArrowUp',' '].includes(e.key)){e.preventDefault();sA(0)}});
701
+ document.getElementById('bReset').onclick=async()=>{const r=await fetch('/reset',{method:'POST'});const d=await r.json();draw(aC,d.ai,true);draw(pC,d.player,true);document.getElementById('as').textContent=d.ai.score;document.getElementById('ps').textContent=d.player.score};
 
 
 
 
 
 
702
 
703
  async function sendChat(){
704
  const inp=document.getElementById('ci');const msg=inp.value.trim();if(!msg)return;inp.value='';
705
+ const m=document.getElementById('msgs');m.innerHTML+=`<div class="u">👤 ${msg}</div>`;m.scrollTop=m.scrollHeight;
 
706
  const r=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg})});
707
+ const d=await r.json();m.innerHTML+=`<div class="b">🧠 ${d.response}</div>`;m.scrollTop=m.scrollHeight;
 
 
 
 
 
 
 
708
  }
709
+ async function startTrain(){document.getElementById('ts').textContent='⏳ Запуск...';const r=await fetch('/train',{method:'POST'});const d=await r.json();document.getElementById('ts').textContent=d.message}
710
  document.querySelectorAll('.tab').forEach(t=>t.onclick=function(){
711
+ document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));this.classList.add('active');const n=this.dataset.tab;
712
+ document.querySelectorAll('.tc>div').forEach(d=>d.classList.add('hidden'));document.getElementById(n+'Tab').classList.remove('hidden');
713
+ if(n==='stats')fetch('/stats').then(r=>r.json()).then(d=>{document.getElementById('sc').innerHTML=`<p>🧠 DQN шагов: ${d.steps}</p><p>📉 ε: ${d.epsilon}</p><p>🏆 Рекорд: ${d.best_score}</p><p>⚡ Тренируется: ${d.training?'':'❌'}</p><hr style="border-color:#30363d;margin:8px 0"><p>💬 Чат записей: ${d.chat.entries}</p><p>📝 Словарь: ${d.chat.vocab}</p><p>🔢 Параметров чата: ${d.chat.params.toLocaleString()}</p><p>📐 Эмбеддинг: ${d.chat.emb_dim}D</p>`});
714
+ });
715
+
716
+ // Load NN info
717
+ fetch('/stats').then(r=>r.json()).then(d=>{
718
+ document.getElementById('nnInfo').textContent=`🧠 Чат-нейросеть: ${d.chat.params.toLocaleString()} параметров | Словарь: ${d.chat.vocab} | Эмбеддинг: ${d.chat.emb_dim}D | Записей: ${d.chat.entries}`;
719
  });
720
 
721
  setInterval(update,100);update();
 
737
  def step():
738
  global ai_env, pl_env
739
  action = request.json.get('action', 0)
740
+ if ai_env.alive: ai_env.step(agent.act(ai_env.get_state()))
741
+ else: ai_env.reset()
742
+ if pl_env.alive: pl_env.step(action)
743
+ else: pl_env.reset()
744
+ return jsonify({'ai': ai_env.world_data(), 'player': pl_env.world_data(), 'epsilon': agent.eps, 'best_score': agent.best})
 
 
 
 
 
 
 
745
 
746
  @app.route('/reset', methods=['POST'])
747
  def reset():
 
753
  @app.route('/chat', methods=['POST'])
754
  def chat_route():
755
  msg = request.json.get('message', '').strip()
756
+ if msg.startswith('/data '):
 
 
 
757
  p = msg[6:].split('|')
758
  if len(p) != 2: return jsonify({'response': "❌ Формат: /data вопрос|ответ"})
759
+ return jsonify({'response': chat.teach(p[0], p[1])})
760
  elif msg == '/stats':
761
+ s = chat.stats
762
+ return jsonify({'response': f"🧠 Чат: {s['params']} парам., {s['vocab']} слов, {s['entries']} записей, {s['emb_dim']}D эмбеддинг"})
763
  elif msg == '/train':
764
  return jsonify({'response': start_training()})
765
+ else:
766
+ return jsonify({'response': chat.ask(msg)})
767
 
768
  @app.route('/train', methods=['POST'])
769
  def train_route(): return jsonify({'message': start_training()})
 
771
  @app.route('/stats')
772
  def stats():
773
  return jsonify({
774
+ 'steps': agent.steps, 'epsilon': round(agent.eps, 3),
775
+ 'best_score': agent.best, 'training': is_training,
776
+ 'chat': chat.stats
777
  })
778
 
779
  def start_training():
 
786
  for ep in range(100):
787
  if not is_training: break
788
  sc = agent.train_ep()
789
+ if ep % 10 == 0: logger.info(f"DQN Ep {ep}: score={sc:.1f}, ε={agent.eps:.3f}")
790
+ except Exception as e: logger.error(f"DQN error: {e}")
791
  finally: is_training = False
792
  threading.Thread(target=_t, daemon=True).start()
793
+ return "🚀 Тренировка DQN запущена!"
794
 
795
  if __name__ == '__main__':
796
  app.run(host='0.0.0.0', port=C.PORT, debug=False)