| """ |
| AI PLATFORMER + CHATBOT (RICH GRAPHICS + FIXED PHYSICS) |
| AABB Collision, Detailed Rendering, Stateful Engine |
| """ |
|
|
| import os, json, random, threading, logging, time |
| from collections import deque |
| from dataclasses import dataclass |
| from typing import Dict, List, Optional |
| import numpy as np |
| import torch |
| import torch.nn as nn |
| import torch.optim as optim |
| from flask import Flask, jsonify, request, render_template_string |
|
|
| logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s') |
| logger = logging.getLogger(__name__) |
|
|
| @dataclass |
| class Cfg: |
| W: int = 80; H: int = 20; GROUND: int = 17; CHUNK: int = 30 |
| SAFE: int = 15; VIEW: int = 40 |
| GRAV: float = 0.35; JUMP: float = -6.5; SPEED: float = 0.35 |
| STATE: int = 40 * 40; ACTS: int = 4; MEM: int = 10000 |
| BATCH: int = 64; GAMMA: float = 0.99; LR: float = 5e-4 |
| EPS_DEC: float = 0.995; PORT: int = 7860 |
| MODEL: str = "dqn_model.pth"; CHAT: str = "chat_data.json" |
|
|
| C = Cfg() |
|
|
| |
| |
| |
|
|
| class Engine: |
| def __init__(self, seed=None): |
| self.seed = seed or random.randint(0, 999999) |
| self.reset() |
|
|
| def reset(self): |
| self.px, self.py = 5.0, float(C.GROUND) |
| self.vx, self.vy = 0.0, 0.0 |
| self.grounded = True |
| self.alive = True |
| self.score = 0 |
| self.coins = 0 |
| self.step_n = 0 |
| self.chunks: Dict[int, dict] = {} |
| self.obs: List[dict] = [] |
| self.enemies: List[dict] = [] |
| self.coin_list: List[dict] = [] |
| self._load_chunks() |
| return self.get_state() |
|
|
| def _gen_chunk(self, cid: int) -> dict: |
| rng = random.Random((cid * 1337 + self.seed) % 999999) |
| bx = cid * C.CHUNK |
| obs, ens, cns = [], [], [] |
| diff = max(1.0, abs(cid) * 0.1) |
| safe = bx < C.SAFE |
|
|
| if not safe: |
| for _ in range(rng.randint(3, 6) + int(diff)): |
| x = bx + rng.randint(5, 25) |
| h = rng.randint(1, 3 + int(diff * 0.5)) |
| w = rng.randint(1, 3) |
| obs.append({'x': x, 'y': C.GROUND - h, 'w': w, 'h': h, 'pit': False}) |
| for _ in range(rng.randint(1, 2)): |
| x = bx + rng.randint(10, 20) |
| obs.append({'x': x, 'y': C.GROUND + 1, 'w': rng.randint(2, 4), 'h': 1, 'pit': True}) |
| for _ in range(rng.randint(1, 2)): |
| x = bx + rng.randint(10, 20) |
| ens.append({ |
| 'x': x, 'y': C.GROUND - 1, |
| 'type': rng.choice(['walker', 'jumper']), |
| 'dir': rng.choice([-1, 1]), |
| 'spd': 0.3 + rng.random() * 0.3, |
| 'rng': rng.randint(3, 8), 'ox': x |
| }) |
|
|
| for _ in range(rng.randint(5, 10) + int(diff)): |
| cns.append({ |
| 'x': bx + rng.randint(2, 28), |
| 'y': rng.randint(5, C.GROUND - 2), |
| 'collected': False |
| }) |
| return {'obs': obs, 'ens': ens, 'cns': cns} |
|
|
| def _load_chunks(self): |
| cc = int(self.px // C.CHUNK) |
| for i in range(cc - 1, cc + 3): |
| if i not in self.chunks: |
| self.chunks[i] = self._gen_chunk(i) |
|
|
| vl, vr = self.px - C.W / 2, self.px + C.W / 2 |
| self.obs, self.enemies, self.coin_list = [], [], [] |
| for i in range(cc - 1, cc + 3): |
| ch = self.chunks.get(i, {}) |
| self.obs.extend([o for o in ch.get('obs', []) if vl <= o['x'] <= vr]) |
| self.enemies.extend([e for e in ch.get('ens', []) if vl <= e['x'] <= vr]) |
| self.coin_list.extend([c for c in ch.get('cns', []) if not c['collected'] and vl <= c['x'] <= vr]) |
|
|
| def _aabb(self, ax, ay, aw, ah, bx, by, bw, bh): |
| return ax < bx + bw and ax + aw > bx and ay < by + bh and ay + ah > by |
|
|
| def get_state(self): |
| s = np.zeros((C.VIEW, C.VIEW), dtype=np.float32) |
| h = C.VIEW // 2 |
| px, py = int(round(self.px)), int(round(self.py)) |
| s[h, h] = 1.0 |
| for o in self.obs: |
| dx, dy = int(round(o['x'])) - px, int(round(o['y'])) - py |
| v = -1.0 if o.get('pit') else 0.8 |
| for ww in range(o.get('w', 1)): |
| for hh in range(o.get('h', 1)): |
| sx, sy = h + dx + ww, h + dy + hh |
| if 0 <= sx < C.VIEW and 0 <= sy < C.VIEW: |
| s[sy, sx] = v |
| for e in self.enemies: |
| dx, dy = int(round(e['x'])) - px, int(round(e['y'])) - py |
| if 0 <= h + dx < C.VIEW and 0 <= h + dy < C.VIEW: |
| s[h + dy, h + dx] = 0.7 |
| for c in self.coin_list: |
| dx, dy = int(round(c['x'])) - px, int(round(c['y'])) - py |
| if 0 <= h + dx < C.VIEW and 0 <= h + dy < C.VIEW: |
| s[h + dy, h + dx] = 0.3 |
| return s.flatten() |
|
|
| def step(self, action: int): |
| sound = None |
| PW, PH = 0.6, 0.9 |
|
|
| |
| self.vx = 0.0 |
| if action == 1: self.vx = -C.SPEED |
| elif action == 2: self.vx = C.SPEED |
| if action == 3 and self.grounded: |
| self.vy = C.JUMP |
| self.grounded = False |
| sound = 'jump' |
|
|
| |
| self.px += self.vx |
| for o in self.obs: |
| if o.get('pit'): continue |
| if self._aabb(self.px, self.py, PW, PH, o['x'], o['y'], o['w'], o['h']): |
| if self.vx > 0: |
| self.px = o['x'] - PW |
| elif self.vx < 0: |
| self.px = o['x'] + o['w'] |
| self.vx = 0 |
|
|
| |
| self.vy += C.GRAV |
| self.py += self.vy |
| self.grounded = False |
|
|
| |
| if self.py >= C.GROUND: |
| self.py = C.GROUND |
| self.vy = 0.0 |
| self.grounded = True |
|
|
| |
| for o in self.obs: |
| if o.get('pit'): continue |
| if self._aabb(self.px, self.py, PW, PH, o['x'], o['y'], o['w'], o['h']): |
| if self.vy > 0: |
| self.py = o['y'] - PH |
| self.vy = 0.0 |
| self.grounded = True |
| elif self.vy < 0: |
| self.py = o['y'] + o['h'] |
| self.vy = 0.0 |
|
|
| |
| if self.py > C.H + 2: |
| self.alive = False |
| return self.get_state(), -50.0, True, 'die' |
|
|
| |
| for o in self.obs: |
| if o.get('pit') and o['x'] <= self.px + PW / 2 <= o['x'] + o['w'] and self.py >= C.GROUND: |
| self.alive = False |
| return self.get_state(), -50.0, True, 'die' |
|
|
| |
| for e in self.enemies: |
| if self._aabb(self.px, self.py, PW, PH, e['x'] - 0.3, e['y'] - 0.3, 0.6, 0.6): |
| self.alive = False |
| return self.get_state(), -50.0, True, 'die' |
|
|
| |
| got = 0 |
| for c in self.coin_list: |
| 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): |
| c['collected'] = True |
| got += 1 |
| if got: |
| self.coins += got |
| self.score += got * 10 |
| sound = 'coin' |
|
|
| |
| t = time.time() |
| for e in self.enemies: |
| if e['type'] == 'walker': |
| e['x'] += e['spd'] * e['dir'] |
| if abs(e['x'] - e['ox']) > e['rng']: e['dir'] *= -1 |
| else: |
| e['y'] = (C.GROUND - 1) + np.sin(t * e['spd'] * 3) * 0.5 |
|
|
| self.score += 1 |
| self.step_n += 1 |
| self._load_chunks() |
|
|
| done = self.step_n > 3000 |
| reward = 1.0 + got * 5.0 |
| return self.get_state(), reward, done, sound |
|
|
| def world_data(self): |
| return { |
| 'player': [round(self.px, 2), round(self.py, 2)], |
| 'obstacles': self.obs, |
| 'entities': self.enemies, |
| 'coins': [c for c in self.coin_list if not c['collected']], |
| 'ground': C.GROUND, |
| 'score': self.score, |
| 'coins_collected': self.coins, |
| 'alive': self.alive |
| } |
|
|
|
|
| |
| |
| |
|
|
| class Net(nn.Module): |
| def __init__(self): |
| super().__init__() |
| self.net = nn.Sequential( |
| nn.Linear(C.STATE, 256), nn.ReLU(), |
| nn.Linear(256, 256), nn.ReLU(), |
| nn.Linear(256, 128), nn.ReLU(), |
| nn.Linear(128, C.ACTS) |
| ) |
| def forward(self, x): return self.net(x) |
|
|
| class Agent: |
| def __init__(self): |
| self.dev = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
| self.model = Net().to(self.dev) |
| self.target = Net().to(self.dev) |
| self.target.load_state_dict(self.model.state_dict()) |
| self.opt = optim.Adam(self.model.parameters(), lr=C.LR) |
| self.crit = nn.MSELoss() |
| self.mem = deque(maxlen=C.MEM) |
| self.eps = 1.0; self.steps = 0; self.best = 0; self.training = False |
| if os.path.exists(C.MODEL): |
| try: |
| self.model.load_state_dict(torch.load(C.MODEL, map_location=self.dev)) |
| self.target.load_state_dict(self.model.state_dict()) |
| logger.info("✅ Model loaded") |
| except Exception as e: logger.warning(f"⚠️ Load failed: {e}") |
|
|
| def act(self, s): |
| if random.random() <= self.eps: return random.randrange(C.ACTS) |
| with torch.no_grad(): |
| return torch.argmax(self.model(torch.FloatTensor(s).unsqueeze(0).to(self.dev))).item() |
|
|
| def remember(self, s, a, r, ns, d): self.mem.append((s, a, r, ns, d)) |
|
|
| def replay(self): |
| if len(self.mem) < C.BATCH: return |
| b = random.sample(self.mem, C.BATCH) |
| st = torch.FloatTensor([x[0] for x in b]).to(self.dev) |
| ac = torch.LongTensor([x[1] for x in b]).to(self.dev) |
| rw = torch.FloatTensor([x[2] for x in b]).to(self.dev) |
| ns = torch.FloatTensor([x[3] for x in b]).to(self.dev) |
| dn = torch.FloatTensor([x[4] for x in b]).to(self.dev) |
| q = self.model(st).gather(1, ac.unsqueeze(1)).squeeze() |
| nq = self.target(ns).max(1)[0].detach() |
| tgt = rw + C.GAMMA * nq * (1 - dn) |
| loss = self.crit(q, tgt) |
| self.opt.zero_grad(); loss.backward(); self.opt.step() |
| if self.eps > 0.01: self.eps *= C.EPS_DEC |
| self.steps += 1 |
| if self.steps % 100 == 0: self.target.load_state_dict(self.model.state_dict()) |
|
|
| def train_ep(self): |
| env = Engine(); s = env.reset(); tr = 0.0; d = False; n = 0 |
| while not d and n < 500: |
| a = self.act(s); ns, r, d, _ = env.step(a) |
| self.remember(s, a, r, ns, d); self.replay() |
| s = ns; tr += r; n += 1 |
| if tr > self.best: self.best = tr; self.save() |
| return tr |
|
|
| def save(self): torch.save(self.model.state_dict(), C.MODEL) |
|
|
|
|
| |
| |
| |
|
|
| class ChatMem: |
| def __init__(self): |
| self.data = {} |
| if os.path.exists(C.CHAT): |
| try: |
| with open(C.CHAT, 'r', encoding='utf-8') as f: self.data = json.load(f) |
| except: pass |
| def save(self): |
| with open(C.CHAT, 'w', encoding='utf-8') as f: json.dump(self.data, f, ensure_ascii=False, indent=2) |
| def add(self, q, a): |
| self.data[q.lower()] = a; self.save(); return f"✅ {q} → {a}" |
| def find(self, q): |
| q = q.lower() |
| if q in self.data: return self.data[q] |
| words = q.split(); best, bs = None, 0 |
| for k, v in self.data.items(): |
| sc = sum(1 for w in words if w in k) |
| if sc > bs: bs, best = sc, v |
| return best if bs >= len(words) * 0.4 else None |
|
|
|
|
| |
| |
| |
|
|
| agent = Agent() |
| chat = ChatMem() |
| seed = random.randint(0, 999999) |
| ai_env = Engine(seed) |
| pl_env = Engine(seed) |
| is_training = False |
|
|
|
|
| |
| |
| |
|
|
| HTML = """ |
| <!DOCTYPE html> |
| <html lang="ru"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>🧠 AI Platformer</title> |
| <style> |
| *{margin:0;padding:0;box-sizing:border-box} |
| body{background:#0d1117;color:#eee;font-family:'Segoe UI',sans-serif;display:flex;justify-content:center;padding:20px;min-height:100vh} |
| .wrap{max-width:1100px;width:100%} |
| h1{text-align:center;padding:15px 0;background:linear-gradient(135deg,#ff6b6b,#4ecdc4);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-size:2.2em} |
| .sub{text-align:center;color:#888;margin-bottom:15px} |
| .row{display:flex;gap:20px;flex-wrap:wrap} |
| .box{flex:1;min-width:320px;background:#161b22;border-radius:16px;padding:15px;box-shadow:0 8px 32px rgba(0,0,0,.5);border:1px solid #30363d} |
| .box h3{text-align:center;margin-bottom:10px;color:#c9d1d9} |
| canvas{width:100%;aspect-ratio:4/1;border-radius:8px;display:block;image-rendering:pixelated;background:#0d1117} |
| .ctrl{display:flex;justify-content:center;gap:12px;margin:15px 0;flex-wrap:wrap} |
| .ctrl button{padding:12px 30px;font-size:1.1em;border:none;border-radius:10px;cursor:pointer;font-weight:bold;transition:all .15s;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5)} |
| .ctrl button:hover{transform:scale(1.05);filter:brightness(1.2)} |
| .ctrl button:active{transform:scale(.93)} |
| .bl,.br{background:linear-gradient(135deg,#ff6b6b,#ee5a24)} |
| .bj{background:linear-gradient(135deg,#4ecdc4,#2ecc71);padding:12px 45px} |
| .brs{background:linear-gradient(135deg,#a29bfe,#6c5ce7)} |
| .stats{background:#161b22;border-radius:12px;padding:12px 20px;margin:10px 0;display:flex;justify-content:space-around;flex-wrap:wrap;gap:10px;font-size:1.1em;border:1px solid #30363d} |
| .stats span{color:#ff6b6b;font-weight:bold} |
| .tabs{display:flex;gap:10px;margin:15px 0;flex-wrap:wrap} |
| .tab{padding:10px 22px;background:#161b22;border-radius:10px;cursor:pointer;border:2px solid #30363d;transition:all .3s;color:#c9d1d9} |
| .tab:hover{border-color:#ff6b6b} |
| .tab.active{border-color:#ff6b6b;background:#1c2333} |
| .tc{background:#161b22;border-radius:12px;padding:20px;min-height:200px;border:1px solid #30363d} |
| .ca{display:flex;gap:10px;margin-top:10px} |
| .ca input{flex:1;padding:10px;border-radius:8px;border:1px solid #30363d;background:#0d1117;color:#eee;font-size:1em} |
| .ca button{padding:10px 25px;background:#ff6b6b;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:bold} |
| .cm{max-height:200px;overflow-y:auto;padding:5px} |
| .cm div{padding:6px 12px;margin:3px 0;border-radius:6px;background:#0d1117} |
| .cm .u{border-left:3px solid #ff6b6b} |
| .cm .b{border-left:3px solid #4ecdc4} |
| .hidden{display:none} |
| </style> |
| </head> |
| <body> |
| <div class="wrap"> |
| <h1>🧠 AI vs Player Platformer</h1> |
| <p class="sub">🤖 Нейросеть слева 🎮 Ты справа (⬅️ ➡️ ⬆️)</p> |
| <div class="row"> |
| <div class="box"><h3>🤖 Нейросеть</h3><canvas id="ac"></canvas></div> |
| <div class="box"><h3>🎮 Ты</h3><canvas id="pc"></canvas></div> |
| </div> |
| <div class="stats"> |
| <div>🤖 ИИ: <span id="as">0</span></div> |
| <div>🎮 Ты: <span id="ps">0</span></div> |
| <div>🪙 Монет: <span id="cc">0</span></div> |
| <div>🧠 ε: <span id="ep">1.00</span></div> |
| <div>🏆 Рекорд: <span id="bs">0</span></div> |
| </div> |
| <div class="ctrl"> |
| <button class="bl" id="bL">⬅️ Влево</button> |
| <button class="bj" id="bJ">⬆️ ПРЫЖОК</button> |
| <button class="br" id="bR">➡️ Вправо</button> |
| <button class="brs" id="bReset">🔄 Новый уровень</button> |
| </div> |
| <div class="tabs"> |
| <div class="tab active" data-tab="chat">💬 Чат</div> |
| <div class="tab" data-tab="train">🧠 Тренировка</div> |
| <div class="tab" data-tab="stats">📊 Статистика</div> |
| </div> |
| <div class="tc"> |
| <div id="chatTab"> |
| <div class="cm" id="msgs"><div class="b">🤖 Привет! Команды: /ai вопрос, /data вопрос|ответ, /stats, /train</div></div> |
| <div class="ca"><input id="ci" placeholder="Введите команду..." onkeydown="if(event.key==='Enter')sendChat()"><button onclick="sendChat()">➤</button></div> |
| </div> |
| <div id="trainTab" class="hidden"> |
| <h3>🧠 Тренировка DQN</h3><p>DQN (256→256→128 нейронов)</p> |
| <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> |
| <div id="ts" style="margin-top:10px;color:#888">⏸ Остановлена</div> |
| </div> |
| <div id="statsTab" class="hidden"><h3>📊 Статистика</h3><div id="sc">Загрузка...</div></div> |
| </div> |
| </div> |
| <script> |
| function initC(id){const c=document.getElementById(id);c.width=800;c.height=200;return c.getContext('2d')} |
| const aC=initC('ac'),pC=initC('pc'); |
| let pA=0; |
| |
| function draw(ctx,d,show){ |
| const W=ctx.canvas.width,H=ctx.canvas.height,cW=W/80,cH=H/20; |
| ctx.clearRect(0,0,W,H); |
| |
| // Sky gradient |
| const sg=ctx.createLinearGradient(0,0,0,H); |
| sg.addColorStop(0,'#0f0c29');sg.addColorStop(0.5,'#302b63');sg.addColorStop(1,'#24243e'); |
| ctx.fillStyle=sg;ctx.fillRect(0,0,W,H); |
| |
| // Stars |
| ctx.fillStyle='rgba(255,255,255,0.3)'; |
| 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); |
| } |
| |
| const cam=Math.max(0,d.player[0]-40); |
| function toS(wx,wy){return[(wx-cam)*cW,wy*cH]} |
| |
| // Ground layers |
| const gy=d.ground*cH; |
| const gg=ctx.createLinearGradient(0,gy,0,H); |
| gg.addColorStop(0,'#4a7c59');gg.addColorStop(0.15,'#3d6b4e');gg.addColorStop(0.5,'#5c4033');gg.addColorStop(1,'#3e2723'); |
| ctx.fillStyle=gg;ctx.fillRect(0,gy,W,H-gy); |
| // Grass top |
| ctx.fillStyle='#6abf69';ctx.fillRect(0,gy,W,cH*0.3); |
| ctx.fillStyle='#81c784'; |
| for(let gx=0;gx<W;gx+=8){ctx.fillRect(gx,gy-cH*0.1,4,cH*0.15)} |
| |
| // Obstacles with detail |
| for(const o of d.obstacles){ |
| const[x,y]=toS(o.x,o.y); |
| 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); |
| }else{ |
| // Brick pattern |
| 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); |
| // Brick lines |
| 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(); |
| } |
| } |
| // Top highlight |
| ctx.fillStyle='rgba(255,255,255,0.15)';ctx.fillRect(x,y,o.w*cW,cH*0.15); |
| // Shadow |
| ctx.fillStyle='rgba(0,0,0,0.3)';ctx.fillRect(x+o.w*cW,y,3,o.h*cH); |
| } |
| } |
| |
| // Enemies with animation |
| const t=Date.now()/200; |
| 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); |
| // Body |
| 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(); |
| // Eyes |
| 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(); |
| // Glow |
| 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(); |
| } |
| |
| // Coins with sparkle |
| 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(); |
| // Shine |
| ctx.fillStyle='rgba(255,255,255,0.8)';ctx.beginPath();ctx.arc(-3,-3,2,0,Math.PI*2);ctx.fill(); |
| ctx.restore(); |
| } |
| |
| // Player |
| if(show&&d.alive){ |
| const[px,py]=toS(d.player[0],d.player[1]); |
| ctx.save(); |
| // Glow |
| ctx.shadowColor='#00ff88';ctx.shadowBlur=20; |
| // Body gradient |
| 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); |
| // Face |
| 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(); |
| } |
| } |
| |
| async function update(){ |
| 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){} |
| } |
| |
| const sA=v=>{pA=v}; |
| document.getElementById('bL').onmousedown=()=>sA(1);document.getElementById('bL').onmouseup=()=>sA(0); |
| document.getElementById('bR').onmousedown=()=>sA(2);document.getElementById('bR').onmouseup=()=>sA(0); |
| document.getElementById('bJ').onmousedown=()=>sA(3);document.getElementById('bJ').onmouseup=()=>sA(0); |
| 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)} |
| }); |
| document.addEventListener('keyup',e=>{if(['ArrowLeft','ArrowRight','ArrowUp',' '].includes(e.key)){e.preventDefault();sA(0)}}); |
| |
| 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; |
| }; |
| |
| async function sendChat(){ |
| const inp=document.getElementById('ci');const msg=inp.value.trim();if(!msg)return;inp.value=''; |
| const m=document.getElementById('msgs'); |
| m.innerHTML+=`<div class="u">👤 ${msg}</div>`;m.scrollTop=m.scrollHeight; |
| const r=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg})}); |
| const d=await r.json(); |
| m.innerHTML+=`<div class="b">🤖 ${d.response}</div>`;m.scrollTop=m.scrollHeight; |
| } |
| |
| 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; |
| } |
| |
| document.querySelectorAll('.tab').forEach(t=>t.onclick=function(){ |
| document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active')); |
| this.classList.add('active');const n=this.dataset.tab; |
| document.querySelectorAll('.tc>div').forEach(d=>d.classList.add('hidden')); |
| document.getElementById(n+'Tab').classList.remove('hidden'); |
| if(n==='stats')fetch('/stats').then(r=>r.json()).then(d=>{ |
| 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>`; |
| }); |
| }); |
| |
| setInterval(update,100);update(); |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| |
| |
| |
|
|
| app = Flask(__name__) |
|
|
| @app.route('/') |
| def index(): return render_template_string(HTML) |
|
|
| @app.route('/step', methods=['POST']) |
| def step(): |
| global ai_env, pl_env |
| action = request.json.get('action', 0) |
| if ai_env.alive: |
| ai_env.step(agent.act(ai_env.get_state())) |
| else: |
| ai_env.reset() |
| if pl_env.alive: |
| pl_env.step(action) |
| else: |
| pl_env.reset() |
| return jsonify({ |
| 'ai': ai_env.world_data(), 'player': pl_env.world_data(), |
| 'epsilon': agent.eps, 'best_score': agent.best |
| }) |
|
|
| @app.route('/reset', methods=['POST']) |
| def reset(): |
| global seed, ai_env, pl_env |
| seed = random.randint(0, 999999) |
| ai_env = Engine(seed); pl_env = Engine(seed) |
| return jsonify({'ai': ai_env.world_data(), 'player': pl_env.world_data()}) |
|
|
| @app.route('/chat', methods=['POST']) |
| def chat_route(): |
| msg = request.json.get('message', '').strip() |
| if msg.startswith('/ai '): |
| a = chat.find(msg[4:]) |
| return jsonify({'response': a or "🤖 Не знаю. Обучи через /data"}) |
| elif msg.startswith('/data '): |
| p = msg[6:].split('|') |
| if len(p) != 2: return jsonify({'response': "❌ Формат: /data вопрос|ответ"}) |
| return jsonify({'response': chat.add(p[0].strip(), p[1].strip())}) |
| elif msg == '/stats': |
| return jsonify({'response': f"📊 Память: {len(chat.data)}, Шагов: {agent.steps}"}) |
| elif msg == '/train': |
| return jsonify({'response': start_training()}) |
| return jsonify({'response': "🤖 Команды: /ai, /data, /stats, /train"}) |
|
|
| @app.route('/train', methods=['POST']) |
| def train_route(): return jsonify({'message': start_training()}) |
|
|
| @app.route('/stats') |
| def stats(): |
| return jsonify({ |
| 'memory_size': len(chat.data), 'steps': agent.steps, |
| 'epsilon': round(agent.eps, 3), 'best_score': agent.best, 'training': is_training |
| }) |
|
|
| def start_training(): |
| global is_training |
| if is_training: return "⏳ Уже тренируется!" |
| is_training = True |
| def _t(): |
| global is_training |
| try: |
| for ep in range(100): |
| if not is_training: break |
| sc = agent.train_ep() |
| if ep % 10 == 0: logger.info(f"Ep {ep}: score={sc:.1f}, ε={agent.eps:.3f}") |
| except Exception as e: logger.error(f"Train error: {e}") |
| finally: is_training = False |
| threading.Thread(target=_t, daemon=True).start() |
| return "🚀 Тренировка запущена!" |
|
|
| if __name__ == '__main__': |
| app.run(host='0.0.0.0', port=C.PORT, debug=False) |