neuron / app.py
X
Update app.py
c592279 verified
Raw
History Blame
28.3 kB
"""
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()
# ============================================================================
# GAME ENGINE: AABB PHYSICS + RICH WORLD
# ============================================================================
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 # Player hitbox size
# Input
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'
# === X AXIS MOVEMENT + COLLISION ===
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
# === Y AXIS MOVEMENT + COLLISION ===
self.vy += C.GRAV
self.py += self.vy
self.grounded = False
# Ground collision
if self.py >= C.GROUND:
self.py = C.GROUND
self.vy = 0.0
self.grounded = True
# Platform collision (Y)
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: # Falling down onto platform
self.py = o['y'] - PH
self.vy = 0.0
self.grounded = True
elif self.vy < 0: # Jumping up into platform
self.py = o['y'] + o['h']
self.vy = 0.0
# Death: fell off world
if self.py > C.H + 2:
self.alive = False
return self.get_state(), -50.0, True, 'die'
# Pit death
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'
# Enemy collision
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'
# Coins
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'
# Update enemies
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
}
# ============================================================================
# DQN AGENT
# ============================================================================
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)
# ============================================================================
# CHAT MEMORY
# ============================================================================
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
# ============================================================================
# GLOBAL STATE
# ============================================================================
agent = Agent()
chat = ChatMem()
seed = random.randint(0, 999999)
ai_env = Engine(seed)
pl_env = Engine(seed)
is_training = False
# ============================================================================
# RICH GRAPHICS HTML
# ============================================================================
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>
"""
# ============================================================================
# FLASK ROUTES
# ============================================================================
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)