import tkinter as tk,random,time,math,json,os
from tkinter import ttk
class C:
    W,H,TITLE,BG,PNL,ACC,SUC,WRN,TXT,TXM,FNT,FNB,FSM=900,650,"048.目标练习·鼠标点击训练","#1a1a2e","#16213e","#e94560","#4CAF50","#FF9800","#e0e0e0","#888888",("Consolas",22,"bold"),("Consolas",12,"bold"),("Consolas",9)
TR,SP,MAX_MISS,SH,BP=18,2000,10,100,50
LF,PF="target_practice_leaderboard.json","#0f0f23"
class P:
    def __init__(s,c):s.ca,s.p=c,[]
    def s(s,x,y,col,c=12):
        for _ in range(c):a,sp=random.uniform(0,math.pi*2),random.uniform(1.5,4.5);dx,dy=math.cos(a)*sp,math.sin(a)*sp;sz=random.uniform(2,5);i=s.ca.create_oval(x,y,x+sz,y+sz,fill=col,outline="",state="hidden");s.p.append({"id":i,"x":float(x),"y":float(y),"dx":dx,"dy":dy,"l":random.uniform(0.4,0.9),"a":0.0,"s":sz});s.ca.itemconfig(i,state="normal")
    def u(s,dt):
        i=0
        while i<len(s.p):
            p=s.p[i];p["a"]+=dt
            if p["a"]>=p["l"]:s.ca.delete(p["id"]);s.p.pop(i);continue
            p["x"]+=p["dx"];p["y"]+=p["dy"];p["dy"]+=0.15;al=1.0-p["a"]/p["l"];s.ca.coords(p["id"],p["x"],p["y"],p["x"]+p["s"],p["y"]+p["s"]);s.ca.itemconfig(p["id"],stipple="gray"+str(int(al*100)));i+=1
class R:
    def __init__(s,c):s.ca,s.r=c,[]
    def s(s,x,y,col):
        i=s.ca.create_oval(x-5,y-5,x+5,y+5,outline=col,width=2,state="hidden");s.r.append({"id":i,"x":x,"y":y,"cr":5,"mr":40,"l":0.5,"a":0.0,"c":col});s.ca.itemconfig(i,state="normal")
    def u(s,dt):
        i=0
        while i<len(s.r):
            rp=s.r[i];rp["a"]+=dt
            if rp["a"]>=rp["l"]:s.ca.delete(rp["id"]);s.r.pop(i);continue
            pr=rp["a"]/rp["l"];rp["cr"]=5+int((rp["mr"]-5)*pr);al=1.0-pr;s.ca.coords(rp["id"],rp["x"]-rp["cr"],rp["y"]-rp["cr"],rp["x"]+rp["cr"],rp["y"]+rp["cr"]);s.ca.itemconfig(rp["id"],outline=rp["c"],stipple="gray"+str(int(al*100)));i+=1
class T:
    def __init__(s,x,y,r,st,lt,col,ci=-1,a=True):s.x,s.y,s.r,s.st,s.lt,s.col,s.ci,s.a=x,y,r,st,lt,col,ci,a
class S:
    def __init__(s):s.p,s.targets,s.sc,s.c,s.misses,s.max_combo,s.tt,s.ht,s.st,s.le,s.d,s.l="idle",[],0,0,0,0,30,[],0,0,"普通",[];s.ld()
    def ld(s):
        if os.path.exists(LF):
            try:
                with open(LF,"r",encoding="utf-8")as f:s.l=json.load(f)
            except:s.l=[]
    def sv(s):
        with open(LF,"w",encoding="utf-8")as f:json.dump(s.l[:20],f,ensure_ascii=False,indent=2)
    def ap(s,n,sc,h,at):s.l.append({"n":n,"s":sc,"h":h,"a":round(at,1),"t":time.strftime("%m/%d %H:%M")});s.l.sort(key=lambda x:-x["s"]);s.l=s.l[:20];s.sv()
class A:
    def __init__(s,root):
        s.root,s.state=root,S()
        root.title(C.TITLE);root.geometry(f"{C.W}x{C.H}");root.resizable(False,False);root.configure(bg=C.BG);root.bind("<Escape>",lambda e:s.q())
        s.bu();s.di()
    def bu(s):
        tk.Label(s.root,text="🎯 目标练习 · 鼠标点击训练",font=C.FNT,fg=C.ACC,bg=C.BG).pack(pady=12)
        mf=tk.Frame(s.root,bg=C.BG);mf.pack(fill=tk.BOTH,expand=True,padx=10,pady=5);mf.columnconfigure(0,weight=1);mf.columnconfigure(1,weight=0)
        s.c=tk.Canvas(mf,width=700,height=500,bg=PF,highlightthickness=0,cursor="crosshair");s.c.grid(row=0,column=0,padx=(0,10),sticky="nsew");s.c.bind("<Button-1>",s.oc)
        pf=tk.Frame(mf,width=180,bg=C.PNL);pf.grid(row=0,column=1,sticky="nsew");pf.columnconfigure(0,weight=1)
        tk.Label(pf,text="📊 实时数据",font=C.FNB,fg=C.TXT,bg=C.PNL).pack(pady=(10,5))
        s.sl=tk.Label(pf,text="得分：0",font=C.FSM,fg=C.ACC,bg=C.PNL);s.sl.pack(anchor="w",padx=15,pady=2)
        s.cl=tk.Label(pf,text="连击：0",font=C.FSM,fg=C.SUC,bg=C.PNL);s.cl.pack(anchor="w",padx=15,pady=2)
        s.hl=tk.Label(pf,text="命中：0",font=C.FSM,fg=C.TXT,bg=C.PNL);s.hl.pack(anchor="w",padx=15,pady=2)
        s.ml=tk.Label(pf,text="未中：0",font=C.FSM,fg=C.WRN,bg=C.PNL);s.ml.pack(anchor="w",padx=15,pady=2)
        s.al=tk.Label(pf,text="平均：--ms",font=C.FSM,fg=C.TXT,bg=C.PNL);s.al.pack(anchor="w",padx=15,pady=2)
        s.bl=tk.Label(pf,text="最佳：--ms",font=C.FSM,fg=C.TXT,bg=C.PNL);s.bl.pack(anchor="w",padx=15,pady=2)
        s.tl=tk.Label(pf,text="时间：--",font=C.FSM,fg=C.TXT,bg=C.PNL);s.tl.pack(anchor="w",padx=15,pady=2)
        tk.Label(pf,text="\n难度：",font=C.FSM,fg=C.TXM,bg=C.PNL).pack(anchor="w",padx=15);s.dv=tk.StringVar(value="普通")
        for t in["简单","普通","困难","地狱"]:tk.Radiobutton(pf,text=t,variable=s.dv,value=t,bg=C.PNL,fg=C.TXT,selectcolor=C.PNL,activebackground=C.PNL,font=C.FSM).pack(anchor="w",padx=25,pady=1)
        tk.Label(pf,text="\n时间：",font=C.FSM,fg=C.TXM,bg=C.PNL).pack(anchor="w",padx=15);s.tv=tk.StringVar(value="30")
        for t in["15","30","60","120"]:tk.Radiobutton(pf,text=f"{t}秒",variable=s.tv,value=t,bg=C.PNL,fg=C.TXT,selectcolor=C.PNL,activebackground=C.PNL,font=C.FSM).pack(anchor="w",padx=25,pady=1)
        bf=tk.Frame(s.root,bg=C.BG);bf.pack(pady=10)
        s.sb=tk.Button(bf,text="▶ 开始训练",width=14,height=2,bg=C.ACC,fg="white",font=("微软雅黑",11,"bold"),command=s.stg);s.sb.pack(side=tk.LEFT,padx=8)
        tk.Button(bf,text="📋 排行榜",width=12,height=2,bg=C.PNL,fg=C.TXT,font=("微软雅黑",10),command=s.shlb).pack(side=tk.LEFT,padx=8)
        tk.Button(bf,text="退出",width=10,height=2,bg="#333333",fg="white",font=("微软雅黑",10),command=s.q).pack(side=tk.LEFT,padx=8)
        s.stl=tk.Label(s.root,text="准备就绪 · 选择难度和时间后点击开始",font=C.FSM,fg=C.TXM,bg=C.BG);s.stl.pack(side=tk.BOTTOM,fill=tk.X,pady=5)
        s.ov=tk.Canvas(s.root,width=700,height=500,bg=PF,highlightthickness=0);s.ov.place(x=0,y=0,relwidth=1,relheight=1,in_=s.c);s.ov.bind("<Button-1>",lambda e:None)
    def di(s):
        s.ov.delete("all")
        s.ov.create_text(350,200,text="🎯",font=("Arial",64),fill=C.ACC)
        s.ov.create_text(350,280,text="目标练习 · 鼠标点击训练",font=("Consolas",16),fill=C.TXT)
        s.ov.create_text(350,320,text="点击目标得分 · 测试你的反应速度",font=C.FSM,fill=C.TXM)
        s.ov.create_text(350,360,text="选择难度和时间 → 点击「开始训练」",font=C.FSM,fill=C.TXM)
    def stg(s):
        s.state.d,s.state.tt=s.dv.get(),int(s.tv.get());s.state.p,s.state.sc,s.state.c,s.state.misses,s.state.max_combo,s.state.st,s.state.le,s.state.tt="playing",0,0,0,0,time.time(),float(s.state.tt)
        s.state.ht.clear();s.state.targets.clear();s.ov.place_forget()
        s.sb.config(state="disabled");s.stl.config(text="训练中...")
        s.root.after(3000,lambda:s.be());s.root.after(1000,lambda:s.uc(3))
    def uc(s,n):
        if n<=0:return
        s.ov.place(x=0,y=0,relwidth=1,relheight=1,in_=s.c)
        s.ov.delete("all");s.ov.create_text(350,250,text=str(n),font=("Consolas",36,"bold"),fill=C.ACC);s.root.after(1000,lambda:s.uc(n-1))
    def be(s):
        s.state.p="playing";s.ps=P(s.c);s.rs=R(s.c);s.st();s.gl()
    def st(s):
        if s.state.p!="playing":return
        d=s.state.d
        if d=="简单":r,l=random.randint(35,45),3000
        elif d=="困难":r,l=random.randint(18,28),1500
        elif d=="地狱":r,l=random.randint(14,22),1000
        else:r,l=random.randint(22,35),2000
        m=r+10;x,y=random.randint(m,700-m),random.randint(m,500-m)
        cols=["#e94560","#4CAF50","#2196F3","#FF9800","#9C27B0","#00BCD4"];col=random.choice(cols)
        t=T(x,y,r,time.time(),l,col);s.state.targets.append(t)
        t.ci=s.c.create_oval(x-r,y-r,x+r,y+r,fill=col,outline="white",width=2)
        s.root.after(l,lambda:s.to(t))
    def to(s,t):
        if not t.a or s.state.p!="playing":return
        t.a=False;s.c.delete(t.ci);s.state.misses+=1;s.state.c=max(0,s.state.c-BP);s.up();s.sn()
    def sn(s):
        if s.state.misses>=MAX_MISS:s.eg();return
        s.root.after(SP,s.st)
    def oc(s,e):
        if s.state.p!="playing":return
        mx,my=s.c.canvasx(e.x),s.c.canvasy(e.y);hit,db=None,float("inf")
        for t in s.state.targets:
            if not t.a:continue
            dx,dy=mx-t.x,my-t.y;ds=dx*dx+dy*dy
            if ds<=t.r*t.r and ds<db:hit=t;db=ds
        if hit:s.oh(hit,math.sqrt(db))
        else:s.om(mx,my)
    def oh(s,t,dist):
        t.a=False;s.c.delete(t.ci);now=time.time();rt=(now-t.st)*1000;cb=min(s.state.c,5)*20;ab=int((1-dist/t.r)*BP);s.state.sc+=SH+cb+ab;s.state.c+=1;s.state.max_combo=max(s.state.max_combo,s.state.c);s.state.ht.append(rt)
        s.ft(t.x,t.y-dist-15,f"+{SH+cb+ab}",C.SUC);s.ps.s(t.x,t.y,t.col,12);s.rs.s(t.x,t.y,t.col);s.up();s.sn()
    def om(s,x,y):
        s.state.c=0;s.state.misses+=1;s.ft(x,y,"MISS",C.ACC);s.ps.s(x,y,"#555555",6);s.up();s.sn()
    def ft(s,x,y,t,c):
        fid=s.c.create_text(x,y,text=t,font=("Consolas",12,"bold"),fill=c);s.c.tag_raise(fid);i=0
        def ani():
            nonlocal i
            if i<20:s.c.coords(fid,x,y-i*2);s.c.itemconfig(fid,stipple="gray"+str(min(i*5,100)));i+=1;s.root.after(30,ani)
        ani()
    def up(s):
        s.sl.config(text=f"得分：{s.state.sc}");s.cl.config(text=f"连击：{s.state.c}（最大 {s.state.max_combo}）");s.hl.config(text=f"命中：{len(s.state.ht)}");s.ml.config(text=f"未中：{s.state.misses} / {MAX_MISS}");s.tl.config(text=f"时间：{int(s.state.le)}s")
    def gl(s):
        if s.state.p!="playing":return
        el=time.time()-s.state.st;s.state.le=max(0,s.state.tt-el)
        if s.state.le<=0:s.eg();return
        if hasattr(s,'ps'):s.ps.u(0.03)
        if hasattr(s,'rs'):s.rs.u(0.03)
        s.tl.config(text=f"时间：{int(s.state.le)}s")
        s.root.after(30,s.gl)
    def eg(s):
        s.state.p="ended";tot=len(s.state.ht)+s.state.misses;acc=(len(s.state.ht)/tot*100)if tot else 0;avg=sum(s.state.ht)/len(s.state.ht)if s.state.ht else 0;best=min(s.state.ht)if s.state.ht else 0;worst=max(s.state.ht)if s.state.ht else 0
        s.ov.place(x=0,y=0,relwidth=1,relheight=1,in_=s.c)
        s.ov.delete("all")
        s.ov.create_text(350,80,text="⏰ 时间到！",font=("Consolas",36,"bold"),fill=C.ACC)
        s.ov.create_text(350,140,text=f"最终得分：{s.state.sc}",font=("Consolas",18),fill=C.TXT)
        s.ov.create_text(350,175,text=f"命中：{len(s.state.ht)}  未中：{s.state.misses}  准确率：{acc:.1f}%",font=C.FSM,fill=C.TXT)
        s.ov.create_text(350,205,text=f"最大连击：{s.state.max_combo}",font=C.FSM,fill=C.SUC)
        s.ov.create_text(350,235,text=f"平均反应：{avg:.0f}ms",font=C.FSM,fill=C.TXT)
        s.ov.create_text(350,265,text=f"最佳反应：{best:.0f}ms",font=C.FSM,fill=C.TXT)
        s.ov.create_text(350,295,text=f"最慢反应：{worst:.0f}ms",font=C.FSM,fill=C.WRN)
        s.ov.create_text(350,345,text="输入名字保存到排行榜（Enter 提交）",font=C.FSM,fill=C.TXM)
        en=tk.Entry(s.ov,font=("Consolas",14),width=16,justify="center",bg=C.PNL,fg=C.TXT,insertbackground=C.TXT);s.ov.create_window(350,375,window=en);en.bind("<Return>",lambda e:s.ss(en.get(),avg))
        s.sb.config(state="normal");s.stl.config(text="游戏结束！输入名字保存成绩")
    def ss(s,nm,avg):
        if nm.strip():s.state.ap(nm.strip(),s.state.sc,len(s.state.ht),avg);s.shlb()
    def shlb(s):
        w=tk.Toplevel(s.root);w.title("排行榜");w.geometry("500x450");w.configure(bg=C.BG)
        tk.Label(w,text="🏆 排行榜",font=C.FNB,fg=C.ACC,bg=C.BG).pack(pady=10)
        tf=tk.Frame(w,bg=C.BG);tf.pack(fill=tk.BOTH,expand=True,padx=20,pady=5)
        st=ttk.Style();st.theme_use("clam");st.configure("Treeview",background=C.PNL,foreground=C.TXT,fieldbackground=C.PNL,font=C.FSM);st.configure("Treeview.Heading",background="#1a1a2e",foreground=C.ACC,font=("Consolas",10,"bold"))
        tr=ttk.Treeview(tf,columns=("排名","名字","得分","命中","平均(ms)","日期"),show="headings",height=15)
        for c in tr["columns"]:tr.heading(c,text=c);tr.column(c,width=70 if c!="名字"else 100,anchor="center")
        tr.pack(fill=tk.BOTH,expand=True)
        for i,en in enumerate(s.state.l):tr.insert("",tk.END,values=(i+1,en["n"],en["s"],en["h"],en["a"],en.get("t","")))
        if not s.state.l:tr.insert("",tk.END,values=("","暂无记录","","","",""))
        tk.Button(w,text="关闭",bg="#333",fg="white",font=C.FSM,command=w.destroy).pack(pady=10)
    def q(s):s.root.quit()
if __name__=="__main__":rt=tk.Tk();A(rt);rt.mainloop()