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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +221 -194
app.py CHANGED
@@ -1,7 +1,7 @@
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
@@ -17,69 +17,66 @@ from flask import Flask, jsonify, request, render_template_string
17
  logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
18
  logger = logging.getLogger(__name__)
19
 
 
20
  @dataclass
21
  class Cfg:
22
  W: int = 80; H: int = 20; GROUND: int = 17; CHUNK: int = 30
23
  SAFE: int = 15; VIEW: int = 40
24
- GRAV: float = 0.35; JUMP: float = -6.5; SPEED: float = 0.35
 
 
25
  STATE: int = 40 * 40; ACTS: int = 4; MEM: int = 10000
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)
@@ -90,8 +87,6 @@ class Tokenizer:
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)
@@ -102,14 +97,11 @@ class ChatEncoder(nn.Module):
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)
@@ -117,16 +109,13 @@ class ChatEncoder(nn.Module):
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] = {}
@@ -134,11 +123,10 @@ class NeuralChat:
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:
@@ -146,7 +134,6 @@ class NeuralChat:
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
  "как играть": "Стрелки ⬅️➡️ для движения, ⬆️/Пробел для прыжка",
@@ -162,181 +149,113 @@ class NeuralChat:
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:
 
 
 
340
  def __init__(self, seed=None):
341
  self.seed = seed or random.randint(0, 999999)
342
  self.reset()
@@ -344,44 +263,29 @@ class Engine:
344
  def reset(self):
345
  self.px, self.py = 5.0, float(C.GROUND)
346
  self.vx, self.vy = 0.0, 0.0
347
- self.grounded = True
348
- self.alive = True
349
- self.score = 0
350
- self.coins = 0
351
- self.step_n = 0
352
  self.chunks: Dict[int, dict] = {}
353
- self.obs: List[dict] = []
354
- self.enemies: List[dict] = []
355
- self.coin_list: List[dict] = []
356
  self._load_chunks()
357
  return self.get_state()
358
 
359
  def _gen_chunk(self, cid: int) -> dict:
360
  rng = random.Random((cid * 1337 + self.seed) % 999999)
361
- bx = cid * C.CHUNK
362
- obs, ens, cns = [], [], []
363
- diff = max(1.0, abs(cid) * 0.1)
364
- safe = bx < C.SAFE
365
-
366
  if not safe:
367
  for _ in range(rng.randint(3, 6) + int(diff)):
368
- x = bx + rng.randint(5, 25)
369
- h = rng.randint(1, 3 + int(diff * 0.5))
370
- w = rng.randint(1, 3)
371
  obs.append({'x': x, 'y': C.GROUND - h, 'w': w, 'h': h, 'pit': False})
372
  for _ in range(rng.randint(1, 2)):
373
  x = bx + rng.randint(10, 20)
374
  obs.append({'x': x, 'y': C.GROUND + 1, 'w': rng.randint(2, 4), 'h': 1, 'pit': True})
375
  for _ in range(rng.randint(1, 2)):
376
  x = bx + rng.randint(10, 20)
377
- ens.append({
378
- 'x': x, 'y': C.GROUND - 1,
379
- 'type': rng.choice(['walker', 'jumper']),
380
- 'dir': rng.choice([-1, 1]),
381
- 'spd': 0.3 + rng.random() * 0.3,
382
- 'rng': rng.randint(3, 8), 'ox': x
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}
@@ -389,8 +293,7 @@ class Engine:
389
  def _load_chunks(self):
390
  cc = int(self.px // C.CHUNK)
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):
@@ -403,10 +306,8 @@ class Engine:
403
  return ax < bx + bw and ax + aw > bx and ay < by + bh and ay + ah > by
404
 
405
  def get_state(self):
406
- s = np.zeros((C.VIEW, C.VIEW), dtype=np.float32)
407
- h = C.VIEW // 2
408
- px, py = int(round(self.px)), int(round(self.py))
409
- s[h, h] = 1.0
410
  for o in self.obs:
411
  dx, dy = int(round(o['x'])) - px, int(round(o['y'])) - py
412
  v = -1.0 if o.get('pit') else 0.8
@@ -424,47 +325,52 @@ class Engine:
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':
@@ -568,7 +474,7 @@ is_training = False
568
 
569
 
570
  # ============================================================================
571
- # HTML (Rich Graphics)
572
  # ============================================================================
573
 
574
  HTML = """
@@ -665,57 +571,173 @@ let pA=0;
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();
@@ -724,6 +746,7 @@ setInterval(update,100);update();
724
  </html>
725
  """
726
 
 
727
  # ============================================================================
728
  # FLASK ROUTES
729
  # ============================================================================
@@ -741,7 +764,10 @@ def step():
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():
@@ -759,7 +785,7 @@ def chat_route():
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:
@@ -792,5 +818,6 @@ def start_training():
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)
 
1
  """
2
  AI PLATFORMER + NEURAL CHATBOT (FROM SCRATCH)
3
+ Fixed jump physics, larger player, AABB collision, rich graphics
4
+ Custom PyTorch NLP model - no external NLP libraries
5
  """
6
 
7
  import os, json, random, threading, logging, time, re, math
 
17
  logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
18
  logger = logging.getLogger(__name__)
19
 
20
+
21
  @dataclass
22
  class Cfg:
23
  W: int = 80; H: int = 20; GROUND: int = 17; CHUNK: int = 30
24
  SAFE: int = 15; VIEW: int = 40
25
+ GRAV: float = 0.45 # Fixed: was 0.35
26
+ JUMP: float = -3.8 # Fixed: was -6.5 (~3.6 tiles high now)
27
+ SPEED: float = 0.35
28
  STATE: int = 40 * 40; ACTS: int = 4; MEM: int = 10000
29
  BATCH: int = 64; GAMMA: float = 0.99; LR: float = 5e-4
30
  EPS_DEC: float = 0.995; PORT: int = 7860
31
  MODEL: str = "dqn_model.pth"; CHAT: str = "chat_data.json"
 
32
  VOCAB_MAX: int = 2000; EMB_DIM: int = 64; HIDDEN: int = 128
33
  CHAT_LR: float = 1e-3; CHAT_EPOCHS: int = 80; SIM_THRESH: float = 0.5
34
 
35
  C = Cfg()
36
 
37
+
38
  # ============================================================================
39
  # CUSTOM TOKENIZER (From Scratch)
40
  # ============================================================================
41
 
42
  class Tokenizer:
 
 
43
  PAD = "<PAD>"; UNK = "<UNK>"
44
+
45
  def __init__(self, max_vocab: int = 2000):
46
  self.max_vocab = max_vocab
47
  self.word2idx: Dict[str, int] = {self.PAD: 0, self.UNK: 1}
48
  self.idx2word: Dict[int, str] = {0: self.PAD, 1: self.UNK}
49
  self.frozen = False
50
+
51
  def _tokenize(self, text: str) -> List[str]:
52
  text = text.lower().strip()
53
  text = re.sub(r'[^\w\sа-яё]', ' ', text)
54
  words = text.split()
 
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
  most_common = counter.most_common(self.max_vocab - 2)
67
  for word, _ in most_common:
68
  idx = len(self.word2idx)
69
  self.word2idx[word] = idx
70
  self.idx2word[idx] = word
 
71
  self.frozen = True
72
  logger.info(f"📝 Vocab built: {len(self.word2idx)} tokens")
73
+
74
  def encode(self, text: str, max_len: int = 32) -> List[int]:
75
  tokens = self._tokenize(text)[:max_len]
76
  ids = [self.word2idx.get(t, 1) for t in tokens]
 
77
  ids += [0] * (max_len - len(ids))
78
  return ids
79
+
80
  @property
81
  def vocab_size(self) -> int:
82
  return len(self.word2idx)
 
87
  # ============================================================================
88
 
89
  class ChatEncoder(nn.Module):
 
 
90
  def __init__(self, vocab_size: int, emb_dim: int, hidden: int, out_dim: int):
91
  super().__init__()
92
  self.embedding = nn.Embedding(vocab_size, emb_dim, padding_idx=0)
 
97
  self.dropout = nn.Dropout(0.2)
98
  self.ln1 = nn.LayerNorm(hidden)
99
  self.ln2 = nn.LayerNorm(hidden)
100
+
101
  def forward(self, x):
102
+ emb = self.embedding(x)
103
+ mask = (x != 0).unsqueeze(-1).float()
104
+ pooled = (emb * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1)
 
 
 
105
  h = self.ln1(self.fc1(pooled))
106
  h = self.relu(h)
107
  h = self.dropout(h)
 
109
  h = self.relu(h)
110
  h = self.dropout(h)
111
  out = self.fc3(h)
 
112
  return nn.functional.normalize(out, p=2, dim=-1)
113
 
114
 
115
  class NeuralChat:
 
 
116
  SEQ_LEN = 32
117
  OUT_DIM = 64
118
+
119
  def __init__(self):
120
  self.tokenizer = Tokenizer(C.VOCAB_MAX)
121
  self.data: Dict[str, str] = {}
 
123
  self.q_embeddings: Optional[torch.Tensor] = None
124
  self.model: Optional[ChatEncoder] = None
125
  self.device = torch.device('cpu')
 
126
  self._load_data()
127
  self._build_and_train()
128
  logger.info(f"�� Neural chat ready. {len(self.data)} entries.")
129
+
130
  def _load_data(self):
131
  if os.path.exists(C.CHAT):
132
  try:
 
134
  self.data = json.load(f)
135
  except Exception as e:
136
  logger.warning(f"⚠️ Chat load failed: {e}")
 
137
  if not self.data:
138
  self.data = {
139
  "как играть": "Стрелки ⬅️➡️ для движения, ⬆️/Пробел для прыжка",
 
149
  "кто тебя создал": "Я написан с нуля на PyTorch без внешних NLP библиотек",
150
  }
151
  self._save_data()
152
+
153
  def _save_data(self):
154
  with open(C.CHAT, 'w', encoding='utf-8') as f:
155
  json.dump(self.data, f, ensure_ascii=False, indent=2)
156
+
157
  def _build_and_train(self):
 
158
  self.questions = list(self.data.keys())
 
159
  if not self.questions:
160
  return
 
 
161
  self.tokenizer.build_vocab(self.questions)
 
 
162
  self.model = ChatEncoder(
163
  vocab_size=self.tokenizer.vocab_size,
164
+ emb_dim=C.EMB_DIM, hidden=C.HIDDEN, out_dim=self.OUT_DIM
 
 
165
  ).to(self.device)
 
 
166
  self._train_model()
 
 
167
  self._update_index()
168
+
169
  def _train_model(self):
 
170
  if len(self.questions) < 2:
171
+ logger.warning("⚠️ Too few entries to train")
 
172
  return
 
173
  optimizer = optim.Adam(self.model.parameters(), lr=C.CHAT_LR)
 
 
174
  q_ids = torch.LongTensor([
175
  self.tokenizer.encode(q, self.SEQ_LEN) for q in self.questions
176
  ]).to(self.device)
 
177
  n = len(self.questions)
178
  best_loss = float('inf')
 
179
  logger.info(f"🧠 Training chat NN: {n} samples, {C.CHAT_EPOCHS} epochs...")
 
180
  for epoch in range(C.CHAT_EPOCHS):
181
+ total_loss = 0.0; num_pairs = 0
182
+ indices = list(range(n)); random.shuffle(indices)
 
 
 
 
 
183
  for i in indices:
184
+ anchor = q_ids[i:i+1]
185
+ positive = q_ids[i:i+1]
 
 
186
  neg_idx = random.choice([j for j in range(n) if j != i])
187
  negative = q_ids[neg_idx:neg_idx+1]
188
+ emb_a = self.model(anchor)
189
+ emb_p = self.model(positive)
190
+ emb_n = self.model(negative)
 
 
 
191
  pos_sim = nn.functional.cosine_similarity(emb_a, emb_p)
192
  neg_sim = nn.functional.cosine_similarity(emb_a, emb_n)
193
+ loss = torch.relu(0.3 - pos_sim + neg_sim).mean()
194
+ optimizer.zero_grad(); loss.backward(); optimizer.step()
195
+ total_loss += loss.item(); num_pairs += 1
 
 
 
 
 
 
 
 
 
196
  avg_loss = total_loss / max(num_pairs, 1)
197
+ if avg_loss < best_loss: best_loss = avg_loss
 
 
198
  if (epoch + 1) % 20 == 0:
199
  logger.info(f" Epoch {epoch+1}/{C.CHAT_EPOCHS}, loss={avg_loss:.4f}")
 
200
  logger.info(f"✅ Chat training complete. Best loss: {best_loss:.4f}")
201
+
202
  def _update_index(self):
 
203
  if not self.questions or self.model is None:
204
+ self.q_embeddings = None; return
 
 
205
  self.model.eval()
206
  with torch.no_grad():
207
  ids = torch.LongTensor([
208
  self.tokenizer.encode(q, self.SEQ_LEN) for q in self.questions
209
  ]).to(self.device)
210
+ self.q_embeddings = self.model(ids)
211
  self.model.train()
212
+
213
  def ask(self, query: str) -> str:
 
214
  if not self.questions or self.q_embeddings is None:
215
  return "🤖 База пуста. Обучи меня: /data вопрос|ответ"
 
216
  self.model.eval()
217
  with torch.no_grad():
218
  q_id = torch.LongTensor([self.tokenizer.encode(query, self.SEQ_LEN)]).to(self.device)
219
+ q_emb = self.model(q_id)
 
 
220
  sims = nn.functional.cosine_similarity(q_emb, self.q_embeddings)
221
  best_idx = torch.argmax(sims).item()
222
  best_score = sims[best_idx].item()
 
223
  self.model.train()
 
224
  if best_score >= C.SIM_THRESH:
225
+ return f"{self.data[self.questions[best_idx]]} (🧠 {int(best_score*100)}%)"
 
 
226
  return f"🤖 Не знаю «{query}». Научи: /data {query}|ответ"
227
+
228
  def teach(self, question: str, answer: str) -> str:
229
+ question = question.strip().lower(); answer = answer.strip()
230
+ if not question or not answer: return "❌ Формат: /data вопрос|ответ"
 
 
 
 
 
231
  is_new = question not in self.data
232
+ self.data[question] = answer; self._save_data()
 
 
 
233
  self.questions = list(self.data.keys())
234
  self.tokenizer = Tokenizer(C.VOCAB_MAX)
235
  self.tokenizer.build_vocab(self.questions)
 
236
  self.model = ChatEncoder(
237
  vocab_size=self.tokenizer.vocab_size,
238
+ emb_dim=C.EMB_DIM, hidden=C.HIDDEN, out_dim=self.OUT_DIM
 
 
239
  ).to(self.device)
240
+ self._train_model(); self._update_index()
 
 
 
241
  action = "Добавлено" if is_new else "Обновлено"
242
  return f"✅ {action}: «{question}» → «{answer}». Модель переобучена."
243
+
244
  @property
245
  def stats(self) -> dict:
246
  params = sum(p.numel() for p in self.model.parameters()) if self.model else 0
247
+ return {'entries': len(self.data), 'vocab': self.tokenizer.vocab_size,
248
+ 'params': params, 'emb_dim': self.OUT_DIM}
 
 
 
 
249
 
250
 
251
  # ============================================================================
252
+ # GAME ENGINE (Fixed Physics + Larger Player)
253
  # ============================================================================
254
 
255
  class Engine:
256
+ PW = 0.8 # Player width — FIXED: was 0.6
257
+ PH = 0.95 # Player height — FIXED: was 0.9
258
+
259
  def __init__(self, seed=None):
260
  self.seed = seed or random.randint(0, 999999)
261
  self.reset()
 
263
  def reset(self):
264
  self.px, self.py = 5.0, float(C.GROUND)
265
  self.vx, self.vy = 0.0, 0.0
266
+ self.grounded = True; self.alive = True
267
+ self.score = 0; self.coins = 0; self.step_n = 0
 
 
 
268
  self.chunks: Dict[int, dict] = {}
269
+ self.obs: List[dict] = []; self.enemies: List[dict] = []; self.coin_list: List[dict] = []
 
 
270
  self._load_chunks()
271
  return self.get_state()
272
 
273
  def _gen_chunk(self, cid: int) -> dict:
274
  rng = random.Random((cid * 1337 + self.seed) % 999999)
275
+ bx = cid * C.CHUNK; obs, ens, cns = [], [], []
276
+ diff = max(1.0, abs(cid) * 0.1); safe = bx < C.SAFE
 
 
 
277
  if not safe:
278
  for _ in range(rng.randint(3, 6) + int(diff)):
279
+ x = bx + rng.randint(5, 25); h = rng.randint(1, 3 + int(diff * 0.5)); w = rng.randint(1, 3)
 
 
280
  obs.append({'x': x, 'y': C.GROUND - h, 'w': w, 'h': h, 'pit': False})
281
  for _ in range(rng.randint(1, 2)):
282
  x = bx + rng.randint(10, 20)
283
  obs.append({'x': x, 'y': C.GROUND + 1, 'w': rng.randint(2, 4), 'h': 1, 'pit': True})
284
  for _ in range(rng.randint(1, 2)):
285
  x = bx + rng.randint(10, 20)
286
+ ens.append({'x': x, 'y': C.GROUND - 1, 'type': rng.choice(['walker', 'jumper']),
287
+ 'dir': rng.choice([-1, 1]), 'spd': 0.3 + rng.random() * 0.3,
288
+ 'rng': rng.randint(3, 8), 'ox': x})
 
 
 
 
 
289
  for _ in range(rng.randint(5, 10) + int(diff)):
290
  cns.append({'x': bx + rng.randint(2, 28), 'y': rng.randint(5, C.GROUND - 2), 'collected': False})
291
  return {'obs': obs, 'ens': ens, 'cns': cns}
 
293
  def _load_chunks(self):
294
  cc = int(self.px // C.CHUNK)
295
  for i in range(cc - 1, cc + 3):
296
+ if i not in self.chunks: self.chunks[i] = self._gen_chunk(i)
 
297
  vl, vr = self.px - C.W / 2, self.px + C.W / 2
298
  self.obs, self.enemies, self.coin_list = [], [], []
299
  for i in range(cc - 1, cc + 3):
 
306
  return ax < bx + bw and ax + aw > bx and ay < by + bh and ay + ah > by
307
 
308
  def get_state(self):
309
+ s = np.zeros((C.VIEW, C.VIEW), dtype=np.float32); h = C.VIEW // 2
310
+ px, py = int(round(self.px)), int(round(self.py)); s[h, h] = 1.0
 
 
311
  for o in self.obs:
312
  dx, dy = int(round(o['x'])) - px, int(round(o['y'])) - py
313
  v = -1.0 if o.get('pit') else 0.8
 
325
 
326
  def step(self, action: int):
327
  sound = None
328
+ # Input
329
  self.vx = 0.0
330
  if action == 1: self.vx = -C.SPEED
331
  elif action == 2: self.vx = C.SPEED
332
  if action == 3 and self.grounded:
333
  self.vy = C.JUMP; self.grounded = False; sound = 'jump'
334
 
335
+ # X axis movement + collision
336
  self.px += self.vx
337
  for o in self.obs:
338
  if o.get('pit'): continue
339
+ if self._aabb(self.px, self.py, self.PW, self.PH, o['x'], o['y'], o['w'], o['h']):
340
+ if self.vx > 0: self.px = o['x'] - self.PW
341
  elif self.vx < 0: self.px = o['x'] + o['w']
342
  self.vx = 0
343
 
344
+ # Y axis movement + collision
345
  self.vy += C.GRAV; self.py += self.vy; self.grounded = False
346
  if self.py >= C.GROUND:
347
  self.py = C.GROUND; self.vy = 0.0; self.grounded = True
348
  for o in self.obs:
349
  if o.get('pit'): continue
350
+ if self._aabb(self.px, self.py, self.PW, self.PH, o['x'], o['y'], o['w'], o['h']):
351
+ if self.vy > 0:
352
+ self.py = o['y'] - self.PH; self.vy = 0.0; self.grounded = True
353
+ elif self.vy < 0:
354
+ self.py = o['y'] + o['h']; self.vy = 0.0
355
 
356
+ # Death checks
357
  if self.py > C.H + 2:
358
  self.alive = False; return self.get_state(), -50.0, True, 'die'
359
  for o in self.obs:
360
+ if o.get('pit') and o['x'] <= self.px + self.PW / 2 <= o['x'] + o['w'] and self.py >= C.GROUND:
361
  self.alive = False; return self.get_state(), -50.0, True, 'die'
362
  for e in self.enemies:
363
+ if self._aabb(self.px, self.py, self.PW, self.PH, e['x'] - 0.3, e['y'] - 0.3, 0.6, 0.6):
364
  self.alive = False; return self.get_state(), -50.0, True, 'die'
365
 
366
+ # Coins
367
  got = 0
368
  for c in self.coin_list:
369
+ if not c['collected'] and self._aabb(self.px, self.py, self.PW, self.PH, c['x'] - 0.3, c['y'] - 0.3, 0.6, 0.6):
370
  c['collected'] = True; got += 1
371
  if got: self.coins += got; self.score += got * 10; sound = 'coin'
372
 
373
+ # Update enemies
374
  t = time.time()
375
  for e in self.enemies:
376
  if e['type'] == 'walker':
 
474
 
475
 
476
  # ============================================================================
477
+ # HTML (Rich Graphics + Larger Player Rendering)
478
  # ============================================================================
479
 
480
  HTML = """
 
571
  function draw(ctx,d,show){
572
  const W=ctx.canvas.width,H=ctx.canvas.height,cW=W/80,cH=H/20;
573
  ctx.clearRect(0,0,W,H);
574
+
575
+ // Sky
576
  const sg=ctx.createLinearGradient(0,0,0,H);
577
  sg.addColorStop(0,'#0f0c29');sg.addColorStop(0.5,'#302b63');sg.addColorStop(1,'#24243e');
578
  ctx.fillStyle=sg;ctx.fillRect(0,0,W,H);
579
+
580
+ // Stars
581
  ctx.fillStyle='rgba(255,255,255,0.3)';
582
  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)}
583
+
584
  const cam=Math.max(0,d.player[0]-40);
585
  function toS(wx,wy){return[(wx-cam)*cW,wy*cH]}
586
+
587
+ // Ground
588
  const gy=d.ground*cH;
589
  const gg=ctx.createLinearGradient(0,gy,0,H);
590
  gg.addColorStop(0,'#4a7c59');gg.addColorStop(0.15,'#3d6b4e');gg.addColorStop(0.5,'#5c4033');gg.addColorStop(1,'#3e2723');
591
  ctx.fillStyle=gg;ctx.fillRect(0,gy,W,H-gy);
592
  ctx.fillStyle='#6abf69';ctx.fillRect(0,gy,W,cH*0.3);
593
  ctx.fillStyle='#81c784';for(let gx=0;gx<W;gx+=8)ctx.fillRect(gx,gy-cH*0.1,4,cH*0.15);
594
+
595
+ // Obstacles
596
  for(const o of d.obstacles){
597
  const[x,y]=toS(o.x,o.y);
598
+ if(o.pit){
599
+ const pg=ctx.createLinearGradient(0,y-cH,0,y+cH);
600
+ pg.addColorStop(0,'#1a1a2e');pg.addColorStop(1,'#000');
601
+ ctx.fillStyle=pg;ctx.fillRect(x,y-cH,o.w*cW,cH*2);
602
+ ctx.fillStyle='#ff4444';ctx.fillRect(x,y-cH*0.5,o.w*cW,2);
603
+ }else{
604
+ const bg=ctx.createLinearGradient(x,y,x,y+o.h*cH);
605
+ bg.addColorStop(0,'#8d6e63');bg.addColorStop(1,'#6d4c41');
606
+ ctx.fillStyle=bg;ctx.fillRect(x,y,o.w*cW,o.h*cH);
607
+ ctx.strokeStyle='rgba(0,0,0,0.3)';ctx.lineWidth=1;
608
+ for(let by=0;by<o.h;by++){
609
+ const yy=y+by*cH;ctx.beginPath();ctx.moveTo(x,yy);ctx.lineTo(x+o.w*cW,yy);ctx.stroke();
610
+ const off=(by%2)*cW*0.5;
611
+ 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()}
612
+ }
613
+ ctx.fillStyle='rgba(255,255,255,0.15)';ctx.fillRect(x,y,o.w*cW,cH*0.15);
614
+ ctx.fillStyle='rgba(0,0,0,0.3)';ctx.fillRect(x+o.w*cW,y,3,o.h*cH);
615
+ }
616
  }
617
+
618
+ // Enemies
619
  const t=Date.now()/200;
620
+ for(const e of d.entities){
621
+ const[x,y]=toS(e.x,e.y);const bounce=Math.sin(t+e.x)*2;
622
+ ctx.save();ctx.translate(x+cW/2,y+cH/2+bounce);
623
+ const eg=ctx.createRadialGradient(0,0,2,0,0,cH/2);
624
+ eg.addColorStop(0,'#ff6b6b');eg.addColorStop(1,'#c0392b');
625
+ ctx.fillStyle=eg;ctx.beginPath();ctx.arc(0,0,cH/2.5,0,Math.PI*2);ctx.fill();
626
+ 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();
627
+ ctx.fillStyle='#000';const ex=e.dir*2;
628
+ 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();
629
+ ctx.shadowColor='#ff6b6b';ctx.shadowBlur=10;
630
+ ctx.strokeStyle='#ff6b6b';ctx.lineWidth=1;ctx.beginPath();ctx.arc(0,0,cH/2.2,0,Math.PI*2);ctx.stroke();
631
+ ctx.restore();
632
+ }
633
+
634
+ // Coins
635
+ for(const c of d.coins){
636
+ const[x,y]=toS(c.x,c.y);const pulse=1+Math.sin(t*2+c.x)*0.15;
637
+ ctx.save();ctx.translate(x+cW/2,y+cH/2);ctx.scale(pulse,pulse);
638
+ const cg=ctx.createRadialGradient(-2,-2,1,0,0,cH/3);
639
+ cg.addColorStop(0,'#fff9c4');cg.addColorStop(0.5,'#ffd700');cg.addColorStop(1,'#f9a825');
640
+ ctx.fillStyle=cg;ctx.beginPath();ctx.arc(0,0,cH/3,0,Math.PI*2);ctx.fill();
641
+ ctx.shadowColor='#ffd700';ctx.shadowBlur=12;
642
+ ctx.strokeStyle='#ffeb3b';ctx.lineWidth=1.5;ctx.beginPath();ctx.arc(0,0,cH/3,0,Math.PI*2);ctx.stroke();
643
+ ctx.fillStyle='rgba(255,255,255,0.8)';ctx.beginPath();ctx.arc(-3,-3,2,0,Math.PI*2);ctx.fill();
644
+ ctx.restore();
645
+ }
646
+
647
+ // PLAYER — FIXED: larger, more visible
648
+ if(show&&d.alive){
649
+ const[px,py]=toS(d.player[0],d.player[1]);
650
+ const pw=cW*0.85, ph=cH*0.95;
651
+ const ox=px+(cW-pw)/2, oy=py+(cH-ph)/2;
652
+ ctx.save();
653
+ ctx.shadowColor='#00ff88';ctx.shadowBlur=25;
654
+ const pg=ctx.createLinearGradient(ox,oy,ox+pw,oy+ph);
655
+ pg.addColorStop(0,'#00ff88');pg.addColorStop(0.5,'#00e676');pg.addColorStop(1,'#00b894');
656
+ ctx.fillStyle=pg;ctx.fillRect(ox,oy,pw,ph);
657
+ ctx.shadowBlur=0;
658
+ ctx.strokeStyle='#b9f6ca';ctx.lineWidth=2;ctx.strokeRect(ox,oy,pw,ph);
659
+ // Eyes
660
+ ctx.fillStyle='#fff';
661
+ ctx.fillRect(ox+pw*0.15,oy+ph*0.2,pw*0.25,ph*0.22);
662
+ ctx.fillRect(ox+pw*0.6,oy+ph*0.2,pw*0.25,ph*0.22);
663
+ ctx.fillStyle='#0d1117';
664
+ ctx.fillRect(ox+pw*0.22,oy+ph*0.27,pw*0.12,ph*0.1);
665
+ ctx.fillRect(ox+pw*0.67,oy+ph*0.27,pw*0.12,ph*0.1);
666
+ // Mouth
667
+ ctx.fillStyle='#0d1117';
668
+ ctx.fillRect(ox+pw*0.3,oy+ph*0.6,pw*0.4,ph*0.08);
669
+ ctx.restore();
670
+ }
671
  }
672
 
673
  async function update(){
674
+ try{
675
+ const r=await fetch('/step',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:pA})});
676
+ const d=await r.json();
677
+ draw(aC,d.ai,true);draw(pC,d.player,d.player.alive);
678
+ document.getElementById('as').textContent=d.ai.score;
679
+ document.getElementById('ps').textContent=d.player.score;
680
+ document.getElementById('cc').textContent=d.player.coins_collected;
681
+ document.getElementById('ep').textContent=d.epsilon.toFixed(3);
682
+ document.getElementById('bs').textContent=d.best_score;
683
+ }catch(e){}
684
  }
685
+
686
  const sA=v=>{pA=v};
687
  document.getElementById('bL').onmousedown=()=>sA(1);document.getElementById('bL').onmouseup=()=>sA(0);
688
  document.getElementById('bR').onmousedown=()=>sA(2);document.getElementById('bR').onmouseup=()=>sA(0);
689
  document.getElementById('bJ').onmousedown=()=>sA(3);document.getElementById('bJ').onmouseup=()=>sA(0);
690
+ document.addEventListener('keydown',e=>{
691
+ if(e.key==='ArrowLeft'){e.preventDefault();sA(1)}
692
+ else if(e.key==='ArrowRight'){e.preventDefault();sA(2)}
693
+ else if(e.key==='ArrowUp'||e.key===' '){e.preventDefault();sA(3)}
694
+ });
695
  document.addEventListener('keyup',e=>{if(['ArrowLeft','ArrowRight','ArrowUp',' '].includes(e.key)){e.preventDefault();sA(0)}});
696
+
697
+ document.getElementById('bReset').onclick=async()=>{
698
+ const r=await fetch('/reset',{method:'POST'});const d=await r.json();
699
+ draw(aC,d.ai,true);draw(pC,d.player,true);
700
+ document.getElementById('as').textContent=d.ai.score;
701
+ document.getElementById('ps').textContent=d.player.score;
702
+ };
703
 
704
  async function sendChat(){
705
  const inp=document.getElementById('ci');const msg=inp.value.trim();if(!msg)return;inp.value='';
706
+ const m=document.getElementById('msgs');
707
+ m.innerHTML+=`<div class="u">👤 ${msg}</div>`;m.scrollTop=m.scrollHeight;
708
  const r=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg})});
709
+ const d=await r.json();
710
+ m.innerHTML+=`<div class="b">🧠 ${d.response}</div>`;m.scrollTop=m.scrollHeight;
711
+ }
712
+
713
+ async function startTrain(){
714
+ document.getElementById('ts').textContent='⏳ Запуск...';
715
+ const r=await fetch('/train',{method:'POST'});const d=await r.json();
716
+ document.getElementById('ts').textContent=d.message;
717
  }
718
+
719
  document.querySelectorAll('.tab').forEach(t=>t.onclick=function(){
720
+ document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
721
+ this.classList.add('active');const n=this.dataset.tab;
722
+ document.querySelectorAll('.tc>div').forEach(d=>d.classList.add('hidden'));
723
+ document.getElementById(n+'Tab').classList.remove('hidden');
724
+ if(n==='stats')fetch('/stats').then(r=>r.json()).then(d=>{
725
+ document.getElementById('sc').innerHTML=`
726
+ <p>🧠 DQN шагов: ${d.steps}</p>
727
+ <p>📉 ε: ${d.epsilon}</p>
728
+ <p>🏆 Рекорд: ${d.best_score}</p>
729
+ <p>⚡ Тренируется: ${d.training?'✅':'❌'}</p>
730
+ <hr style="border-color:#30363d;margin:8px 0">
731
+ <p>💬 Чат записей: ${d.chat.entries}</p>
732
+ <p>📝 Словарь: ${d.chat.vocab}</p>
733
+ <p>🔢 Параметров чата: ${d.chat.params.toLocaleString()}</p>
734
+ <p>📐 Эмбеддинг: ${d.chat.emb_dim}D</p>`;
735
+ });
736
  });
737
 
 
738
  fetch('/stats').then(r=>r.json()).then(d=>{
739
+ document.getElementById('nnInfo').textContent=
740
+ `🧠 Чат-нейросеть: ${d.chat.params.toLocaleString()} параметров | Словарь: ${d.chat.vocab} | Эмбеддинг: ${d.chat.emb_dim}D | Записей: ${d.chat.entries}`;
741
  });
742
 
743
  setInterval(update,100);update();
 
746
  </html>
747
  """
748
 
749
+
750
  # ============================================================================
751
  # FLASK ROUTES
752
  # ============================================================================
 
764
  else: ai_env.reset()
765
  if pl_env.alive: pl_env.step(action)
766
  else: pl_env.reset()
767
+ return jsonify({
768
+ 'ai': ai_env.world_data(), 'player': pl_env.world_data(),
769
+ 'epsilon': agent.eps, 'best_score': agent.best
770
+ })
771
 
772
  @app.route('/reset', methods=['POST'])
773
  def reset():
 
785
  return jsonify({'response': chat.teach(p[0], p[1])})
786
  elif msg == '/stats':
787
  s = chat.stats
788
+ return jsonify({'response': f"🧠 Чат: {s['params']} парам., {s['vocab']} слов, {s['entries']} записей, {s['emb_dim']}D"})
789
  elif msg == '/train':
790
  return jsonify({'response': start_training()})
791
  else:
 
818
  threading.Thread(target=_t, daemon=True).start()
819
  return "🚀 Тренировка DQN запущена!"
820
 
821
+
822
  if __name__ == '__main__':
823
  app.run(host='0.0.0.0', port=C.PORT, debug=False)