icoa-cli 2.19.651 → 2.19.652

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{spawn as e,spawnSync as t}from"node:child_process";import{existsSync as o,readFileSync as s,writeFileSync as n}from"node:fs";import{homedir as a}from"node:os";import{join as r}from"node:path";import chalk from"chalk";import{getConfig as i}from"../lib/config.js";import{runInteractive as c}from"../lib/interactive-spawn.js";import{lazyJson as l}from"../lib/lazy-assets.js";import{learnPost as u}from"../lib/learn-contest.js";import{listLearnSessions as g}from"../lib/learn-state.js";import{logCommand as p}from"../lib/logger.js";import{ARENA_VI_FLAGS as d}from"../lib/vi-flags.js";const h=()=>{const e=(i().language||"en").toLowerCase();return e.startsWith("zh")?"zh":e.startsWith("th")?"th":"en"},b=(e,t,o)=>{const s=h();return"zh"===s?t:"th"===s?o:e};export const CUP_HOME=r(a(),".icoa","duck","cup");const m=r(CUP_HOME,"recipe.py"),$=r(CUP_HOME,"last-run.json"),k=r(CUP_HOME,"state.json"),f=r(CUP_HOME,"cup.json"),w={read:"read",r:"read",recipe:"read",info:"read",edit:"edit",e:"edit",vi:"edit",set:"set",knob:"set",change:"set",run:"run",train:"run",go:"run",play:"run",watch:"watch",replay:"watch",show:"watch",w:"watch",brain:"brain",b:"brain",table:"brain",legs:"legs",leg:"legs",walker:"legs",submit:"submit",s:"submit",handin:"submit",join:"join",enter:"join",passcode:"join",status:"status",me:"status",state:"status",bracket:"bracket",draw:"bracket",results:"bracket",guide:"guide",help:"guide","?":"guide",h:"guide",steps:"guide",back:"back",quit:"back",q:"back",exit:"back",menu:"menu"};export function normalizeCupVerb(e){const t=e.trim().replace(/^\//,"").split(/\s+/).filter(Boolean);return 0===t.length?{verb:"unknown",args:[]}:{verb:w[t[0].toLowerCase()]??"unknown",args:t.slice(1)}}const y=new Set(["EPISODES","EXPLORE","EXPLORE_END","LEARN_RATE","LOOK_AHEAD","DICE"]),E={LEGS:["factory","sprint","calm"],COURSES:["practice","mixed"]},S=["progress","flag","finish","time","fall","wall","speed","smooth"];export function setRecipeKnob(e,t,o){const s=t.toUpperCase(),n=t.toLowerCase(),a=e=>/^-?(\d+\.?\d*|\.\d+)$/.test(e);if(y.has(s)){if(!a(o))return{ok:!1,why:"value"};const t=new RegExp(`^(${s}\\s*=\\s*)([^#\\n]*?)(\\s*(?:#.*)?)$`,"m");return t.test(e)?{ok:!0,src:e.replace(t,`$1${o}$3`),shown:`${s} = ${o}`}:{ok:!1,why:"missing"}}if(E[s]){const t=o.replace(/^["']|["']$/g,"").toLowerCase();if(!E[s].includes(t))return{ok:!1,why:"value"};const n=new RegExp(`^(${s}\\s*=\\s*)([^#\\n]*?)(\\s*(?:#.*)?)$`,"m");return n.test(e)?{ok:!0,src:e.replace(n,`$1"${t}"$3`),shown:`${s} = "${t}"`}:{ok:!1,why:"missing"}}if(S.includes(n)){if(!a(o))return{ok:!1,why:"value"};const t=new RegExp(`^(\\s*["']${n}["']\\s*:\\s*)([^,#\\n]*)(.*)$`,"m");return t.test(e)?{ok:!0,src:e.replace(t,`$1${o}$3`),shown:`REWARD["${n}"] = ${o}`}:{ok:!1,why:"missing"}}return{ok:!1,why:"name"}}export function recipeValue(e,t){const o=new RegExp(`^${t}\\s*=\\s*([^#\\n]*?)\\s*(?:#.*)?$`,"m").exec(e);return o?o[1].replace(/^["']|["']$/g,""):null}export function sparkline(e,t=0,o=130){return e.map(e=>" ▁▂▃▄▅▆▇█"[Math.min(8,Math.max(0,Math.floor((e-t)/(o-t)*8)))]).join("")}function j(e,o,s,n=12e4){const a=t(e,[r(o,"cup","cup_tool.py"),...s,"--home",CUP_HOME],{encoding:"utf-8",timeout:n,env:{...process.env,PYTHONDONTWRITEBYTECODE:"1",OMP_NUM_THREADS:"1",MUJOCO_GL:"disable"}}),i=String(a.stdout??"").trim().split("\n").pop()||"";try{return JSON.parse(i)}catch{return{ok:!1,error:"engine",message:String(a.stderr??"").trim().split("\n").slice(-2).join(" ")}}}function C(t,o){return new Promise(s=>{const n=e(t,[r(o,"cup","cup_tool.py"),"train","--home",CUP_HOME],{env:{...process.env,PYTHONDONTWRITEBYTECODE:"1",OMP_NUM_THREADS:"1",MUJOCO_GL:"disable"},stdio:["ignore","pipe","pipe"]});let a="",i={ok:!1,error:"engine",message:"no output"},c="";n.stdout.on("data",e=>{a+=e.toString("utf-8");let t=a.indexOf("\n");for(;t>=0;){const e=a.slice(0,t);a=a.slice(t+1),t=a.indexOf("\n");try{const t=JSON.parse(e);if("ep"===t.type){const e=t.fell?b("fell","摔倒","ล้ม"):`${t.flags} ${b("flags","面旗","ธง")}`;process.stdout.write(`\r${chalk.gray(` ${b("episode","回合","รอบ")} ${String(t.episode).padStart(3)}/${t.of} · ${b("explore","探索","สำรวจ")} ${Number(t.explore).toFixed(2)} · ${b("points","比赛分","คะแนน")} ${Number(t.points).toFixed(1)} (${e})`)}`)}else i=t}catch{}}}),n.stderr.on("data",e=>{c+=e.toString("utf-8")}),n.on("close",()=>{process.stdout.write("\n"),!1===i.ok&&!i.message&&c&&(i.message=c.trim().split("\n").slice(-2).join(" ")),s(i)})})}function O(t,o,s){return new Promise(n=>{e(t,[r(o,"cup","cup_tool.py"),"view","--file",s],{env:{...process.env,PYTHONDONTWRITEBYTECODE:"1"},stdio:["ignore","inherit","inherit"]}).on("close",()=>n())})}const v=()=>{try{const e=JSON.parse(s(f,"utf-8"));return e.code?e:null}catch{return null}};function x(){try{const e=g();return(e.find(e=>e.curriculumId?.startsWith("mduck"))??e[0])?.token??null}catch{return null}}const N=(e,t,o)=>u(`/api/icoa/learn/duckcup/${e}`,{token:t.toUpperCase(),...o},2e4),_=(e,t)=>{const o=t/2**(e-1);return 2===o?b("Final","决赛","รอบชิง"):4===o?b("Semi-finals","半决赛","รอบรองชนะเลิศ"):`${b("Round of","","รอบ")} ${o}${"zh"===h()?" 强":""}`};function R(e,t){console.log(chalk.bold.white(` 🏆 ${e.title??"Duck Cup"} · ${e.players} ${b("players","名选手","ผู้เล่น")} · ${e.size}`));for(const o of e.rounds){console.log(chalk.bold.white(` ${_(o.round,e.size)}`)+chalk.gray(` · ${b("course levels","赛道难度","ระดับสนาม")} ${o.levels.join("/")}`));const s=o.matches.filter(e=>t&&(e.a.nick===t||e.b.nick===t)),n=o.matches.length<=8?o.matches:s;for(const e of n){const o=e=>`${e.bot?chalk.gray("[bot] "):""}${e.nick===t?chalk.cyan(e.nick):chalk.white(e.nick)}`,s=e.winner===e.a.slot?"a":"b";console.log(` ${chalk.gray(e.id.padEnd(7))} ${"a"===s?"✅":" "} ${o(e.a)} ${chalk.gray(e.pts_a.map(e=>e.toFixed(0)).join("/"))}`+chalk.gray(" vs ")+`${"b"===s?"✅":" "} ${o(e.b)} ${chalk.gray(e.pts_b.map(e=>e.toFixed(0)).join("/"))} ${chalk.gray(e.how)}`)}o.matches.length>8&&!s.length&&console.log(chalk.gray(b(` (${o.matches.length} matches — you are not in this round)`,` (${o.matches.length} 场 —— 这一轮没有你)`,` (${o.matches.length} แมตช์ — คุณไม่ได้อยู่ในรอบนี้)`)))}console.log(chalk.green(` 🥇 ${b("Champion","冠军","แชมป์")}: ${e.champion}`)),console.log(chalk.gray(b(" ▸ watch <match> <course 1-3> — e.g. watch R1-M3 2 replays that race"," ▸ watch <场次> <赛道 1-3> —— 例如 watch R1-M3 2 回放那一场"," ▸ watch <แมตช์> <สนาม 1-3> — เช่น watch R1-M3 2 ดูการแข่งนั้น")))}const P=e=>{try{n(k,JSON.stringify(e))}catch{}};function D(e){console.log(chalk.bold.white(b(" Duck Cup — train your duck’s brain"," Duck Cup —— 训练你的鸭脑"," Duck Cup — ฝึกสมองเป็ดของคุณ"))),console.log(chalk.gray(b(" The legs already walk (a policy trained by RL on a GPU). You teach the brain WHEN to go, turn or stop."," 鸭腿已经会走(GPU 上用强化学习训出来的)。你来教鸭脑:什么时候走、转、停。"," ขาเดินได้แล้ว (ฝึกด้วย RL บน GPU) คุณสอนสมองว่าเมื่อไรจะเดิน เลี้ยว หรือหยุด"))),[[e.trained,"run","train on this laptop (~30 s), watch the learning curve","在本机训练(约 30 秒),看学习曲线","ฝึกบนเครื่องนี้ (~30 วินาที) ดูกราฟการเรียนรู้"],[e.watched,"watch","replay your duck on the practice course","在练习赛道上回放你的鸭子","ดูเป็ดของคุณวิ่งบนสนามซ้อม"],[e.brain,"brain","see what it learned: one move per situation","看它学会了什么:每种情况一个动作","ดูว่ามันเรียนรู้อะไร: หนึ่งท่าต่อหนึ่งสถานการณ์"],[e.set,"set EPISODES 150","change one knob, then run again (read lists them)","改一个旋钮再 run(read 列出全部)","เปลี่ยนหนึ่ง knob แล้ว run ใหม่ (read แสดงทั้งหมด)"],[e.joined,"join <passcode>","enter the Cup with your teacher’s passcode","用老师给的口令加入杯赛","เข้าร่วม Cup ด้วยรหัสจากครู"],[e.submitted,"submit","hand your brain in (again any time until the Cup starts)","交鸭脑(开赛前可反复交,最后一份算数)","ส่งสมอง (ส่งใหม่ได้จนกว่า Cup จะเริ่ม)"],[e.bracket,"bracket","after the Cup: the draw, your path, replays","赛后:对阵表、你的路线、回放","หลังแข่ง: สายการแข่ง เส้นทางของคุณ รีเพลย์"]].forEach(([e,t,o,s,n],a)=>{var r;console.log(` ${r=e,r?"✅":"⬜"} ${a+1} ${chalk.bold.green(t.padEnd(18))} ${chalk.gray(b(o,s,n))}`)}),console.log(chalk.gray(b(" read · edit · legs · guide (this panel) · back (one level up) · menu"," read · edit · legs · guide(这块面板)· back(回上一层)· menu"," read · edit · legs · guide (แผงนี้) · back (ถอยหนึ่งชั้น) · menu")))}function U(){let e="";try{e=s(m,"utf-8")}catch{}console.log(chalk.bold.white(b(" Your recipe — ~/.icoa/duck/cup/recipe.py"," 你的配方 —— ~/.icoa/duck/cup/recipe.py"," สูตรของคุณ — ~/.icoa/duck/cup/recipe.py")));for(const t of e.split("\n"))/^[A-Z]/.test(t)||/^\s+"/.test(t)?console.log(` ${chalk.white(t)}`):t.trim()&&console.log(` ${chalk.gray(t)}`);console.log(chalk.gray(b(" ▸ set <KNOB> <value> e.g. set EPISODES 150 · set flag 10 · set LEGS sprint · set COURSES mixed ▸ edit for the whole file"," ▸ set <旋钮> <值> 例如 set EPISODES 150 · set flag 10 · set LEGS sprint · set COURSES mixed ▸ edit 打开整个文件"," ▸ set <KNOB> <ค่า> เช่น set EPISODES 150 · set flag 10 · set LEGS sprint · set COURSES mixed ▸ edit เปิดทั้งไฟล์")))}function L(e){if("recipe"===e.error)return console.log(chalk.yellow(b(" ✗ The recipe has a problem"," ✗ 配方有问题"," ✗ สูตรมีปัญหา")+(e.line?` (line ${e.line})`:"")+`: ${e.message??""}`)),e.text&&console.log(chalk.yellow(` ${String(e.text)}`)),void console.log(chalk.gray(b(" ▸ read shows the recipe · edit to fix it"," ▸ read 看配方 · edit 去改"," ▸ read ดูสูตร · edit แก้ไข")));"no_brain"!==e.error?console.log(chalk.yellow(b(" Engine error: "," 引擎出错:"," เอนจินผิดพลาด: ")+String(e.message??e.error??""))):console.log(chalk.gray(b(" No brain yet — run trains one."," 还没有鸭脑 —— 先 run 训练一个。"," ยังไม่มีสมอง — run เพื่อฝึก")))}export async function runDuckCup(e,t,i){if(p("duck cup"),!o(r(i,"cup","cup_tool.py")))return console.log(chalk.yellow(b(" This engine package has no Duck Cup yet — leave duck and come back to refresh it."," 引擎包里还没有 Duck Cup —— 退出 duck 再进来刷新一次。"," แพ็กเอนจินนี้ยังไม่มี Duck Cup — ออกจาก duck แล้วเข้าใหม่เพื่ออัปเดต"))),"back";const u=j(t,i,["init"]);if(!0!==u.ok)return L(u),"back";const g=(()=>{try{return JSON.parse(s(k,"utf-8"))}catch{return{}}})();console.log(),D(g);let h=0;for(;;){const u=await e(chalk.bold.cyan("icoa duck cup> "));if(null===u)return"back";if(!u.trim())continue;const{verb:k,args:w}=normalizeCupVerb(u);switch("unknown"!==k&&(h=0),k){case"back":return"back";case"menu":return"menu";case"guide":D(g);break;case"read":U();break;case"edit":console.log(chalk.gray(b(" vi survival: i = edit · Esc = stop editing · :wq = save and leave · :q! = discard"," vi 三键: i 进入编辑 · Esc 退出编辑 · :wq 存盘离开 · :q! 放弃"," vi: i = แก้ไข · Esc = หยุดแก้ · :wq = บันทึกแล้วออก · :q! = ทิ้ง"))),await c("vi",[...d,m]);break;case"set":{if(w.length<2){console.log(chalk.gray(b(" set <KNOB> <value> — e.g. set EPISODES 150 · set explore 0.5 · set flag 10 · set LEGS sprint"," set <旋钮> <值> —— 例如 set EPISODES 150 · set explore 0.5 · set flag 10 · set LEGS sprint"," set <KNOB> <ค่า> — เช่น set EPISODES 150 · set explore 0.5 · set flag 10 · set LEGS sprint")));break}const e=setRecipeKnob(s(m,"utf-8"),w[0],w.slice(1).join(" "));if(!e.ok){const t="name"===e.why?b(`no knob called "${w[0]}" — read lists them`,`没有叫「${w[0]}」的旋钮 —— read 列出全部`,`ไม่มี knob ชื่อ "${w[0]}" — read แสดงทั้งหมด`):"value"===e.why?b(`"${w.slice(1).join(" ")}" is not a value ${w[0]} takes`,`「${w.slice(1).join(" ")}」不是 ${w[0]} 能取的值`,`"${w.slice(1).join(" ")}" ใช้กับ ${w[0]} ไม่ได้`):b("that line is not in your recipe any more — edit to put it back","配方里找不到那一行了 —— edit 补回去","ไม่พบบรรทัดนั้นในสูตรแล้ว — edit เพื่อใส่กลับ");console.log(chalk.yellow(` ✗ ${t}`));break}n(m,e.src),p(`duck cup set ${w.join(" ")}`),g.set=!0,P(g),console.log(chalk.green(` ✓ ${e.shown}`)+chalk.gray(b(" ▸ run to train with it"," ▸ run 用它训练"," ▸ run เพื่อฝึกด้วยค่านี้")));break}case"run":{if(w.length>=1){const e=Number(w[0]),o=Number(w[1]??1);if(!Number.isInteger(e)||!Number.isInteger(o)||o<0||o>3){console.log(chalk.yellow(b(" run <seed> <level 0-3> — e.g. run 202 2"," run <种子> <难度 0-3> —— 例如 run 202 2"," run <seed> <ระดับ 0-3> — เช่น run 202 2")));break}const s=j(t,i,["run","--seed",String(e),"--level",String(o)]);if(!0!==s.ok){L(s);break}await O(t,i,String(s.replay)),console.log(chalk.white(b(` Unseen course ${e} (level ${o}): ${s.points} points, ${s.flags}/${s.of} flags`,` 没见过的赛道 ${e}(难度 ${o}):${s.points} 分,${s.flags}/${s.of} 面旗`,` สนามใหม่ ${e} (ระดับ ${o}): ${s.points} คะแนน, ${s.flags}/${s.of} ธง`)));break}p("duck cup run"),console.log(chalk.gray(b(" 🦆 learning…"," 🦆 学习中…"," 🦆 กำลังเรียนรู้…")));const e=await C(t,i);if(!0!==e.ok){L(e);break}const o=e.curve??[];console.log(chalk.gray(b(` learning curve (points per episode, ${e.train_s} s):`,` 学习曲线(每回合的比赛分,${e.train_s} 秒):`,` กราฟการเรียนรู้ (คะแนนต่อรอบ, ${e.train_s} วินาที):`)));for(let e=0;e<o.length;e+=60)console.log(` ${chalk.cyan(sparkline(o.slice(e,e+60)))}`);const s=e.practice;console.log(chalk.green(b(` Practice course, no random moves: ${s.points} points, ${s.flags}/3 flags`,` 练习赛道(不再随机试):${s.points} 分,${s.flags}/3 面旗`,` สนามซ้อม (ไม่สุ่มแล้ว): ${s.points} คะแนน, ${s.flags}/3 ธง`)+(s.finish_s?b(`, nest at ${s.finish_s.toFixed(1)} s`,`,${s.finish_s.toFixed(1)} 秒到窝`,`, ถึงรังที่ ${s.finish_s.toFixed(1)} วินาที`):""))+chalk.gray(` · legs ${e.legs}`)),g.trained=!0,P(g),console.log(chalk.gray(b(" ▸ watch to see it run · brain to see what it learned · run 202 2 on a course it has never seen"," ▸ watch 看它跑 · brain 看它学到什么 · run 202 2 跑一条没见过的赛道"," ▸ watch ดูมันวิ่ง · brain ดูสิ่งที่เรียนรู้ · run 202 2 สนามที่ไม่เคยเห็น")));break}case"watch":if(w.length>=1){const e=v(),o=w[0].toUpperCase(),s=Number(w[1]??1);if(!e||!/^R\d+-M\d+$/.test(o)||![1,2,3].includes(s)){console.log(chalk.yellow(b(" watch <match> <course 1-3> — e.g. watch R1-M3 2 (bracket lists the matches)"," watch <场次> <赛道 1-3> —— 例如 watch R1-M3 2(bracket 列出场次)"," watch <แมตช์> <สนาม 1-3> — เช่น watch R1-M3 2 (bracket แสดงแมตช์)")));break}const n=`duckcup/${e.code}/matches/${o}-c${s}.json`;if(!await l(n,15e3,{force:!0})){console.log(chalk.yellow(b(" That replay is not published (yet)."," 这场回放还没发布。"," ยังไม่มีรีเพลย์นี้")));break}await O(t,i,r(a(),".icoa","assets",n));break}if(!o($)){console.log(chalk.gray(b(" Nothing to replay yet — run first."," 还没有可回放的 —— 先 run。"," ยังไม่มีอะไรให้ดู — run ก่อน")));break}await O(t,i,$),g.watched=!0,P(g);break;case"brain":{const e=j(t,i,["brain"]);if(!0!==e.ok){L(e);break}console.log(chalk.bold.white(b(" Your duck’s brain (what it does in each situation)"," 你的鸭脑(每种情况它会做什么)"," สมองเป็ดของคุณ (ทำอะไรในแต่ละสถานการณ์)")));for(const t of String(e.text).split("\n"))console.log(chalk.white(` ${t}`));console.log(chalk.gray(b(` ? = never met this situation while learning · legs ${e.legs} · ${e.episodes} episodes`,` ? = 学习时没遇到过这种情况 · 腿 ${e.legs} · ${e.episodes} 回合`,` ? = ไม่เคยเจอสถานการณ์นี้ตอนเรียน · ขา ${e.legs} · ${e.episodes} รอบ`))),g.brain=!0,P(g);break}case"legs":{let e="factory";try{e=recipeValue(s(m,"utf-8"),"LEGS")??"factory"}catch{}console.log(chalk.bold.white(b(" The legs menu (walkers trained by RL on a GPU)"," 腿型菜单(GPU 上强化学习训出来的走路程序)"," เมนูขา (โปรแกรมเดินที่ฝึกด้วย RL บน GPU)")));const t=[["factory","the standard walker","标准走法","ขาเดินมาตรฐาน"],["sprint","trained to chase speed: ~40% faster, shakier, worse at turning","为速度训练:快约 40%,更抖,转弯差","ฝึกเพื่อความเร็ว: เร็วขึ้น ~40% สั่นกว่า เลี้ยวแย่กว่า"],["calm","trained to be smooth: steadier, but slower","为平稳训练:更稳,但更慢","ฝึกให้นิ่ง: นิ่งกว่า แต่ช้ากว่า"]];for(const[o,s,n,a]of t)console.log(` ${o===e?"▶":" "} ${chalk.bold.green(o.padEnd(8))} ${chalk.gray(b(s,n,a))}`);console.log(chalk.gray(b(" ▸ set LEGS sprint → run (a brain must be trained ON the legs it races with)"," ▸ set LEGS sprint → run(鸭脑必须在它比赛用的腿上训练)"," ▸ set LEGS sprint → run (สมองต้องฝึกบนขาที่ใช้แข่ง)")));break}case"join":{const t=(w[0]??"").toUpperCase().replace(/[^A-Z0-9]/g,"");if(t.length<3){console.log(chalk.gray(b(" join <passcode> — your teacher writes it on the board"," join <口令> —— 老师会写在黑板上"," join <รหัส> — ครูจะเขียนบนกระดาน")));break}const o=x();if(!o){console.log(chalk.yellow(b(" No learn token on this laptop — paste your camp token in learn first."," 这台电脑上没有学习 token —— 先在 learn 里粘贴营地 token。"," เครื่องนี้ไม่มี learn token — วาง token ค่ายใน learn ก่อน")));break}let s=w.slice(1).join(" ").trim();if(s||(s=(await e(chalk.white(b(" Your Cup nickname (2–14 letters): "," 杯赛昵称(2–14 个字母或数字):"," ชื่อเล่นใน Cup (2–14 ตัว): ")))??"").trim()),!s)break;const a=await N("join",o,{cup:t,nick:s});if(!a.ok||!a.data){console.log(chalk.yellow(` ✗ ${a.message??a.code??""}`));break}n(f,JSON.stringify({code:t,nick:s})),p(`duck cup join ${t}`),g.joined=!0,P(g),console.log(chalk.green(b(` ✓ You are in ${a.data.title??t} as ${s}`,` ✓ 已加入 ${a.data.title??t},昵称 ${s}`,` ✓ เข้าร่วม ${a.data.title??t} ในชื่อ ${s}`))+chalk.gray(b(` · ${a.data.entrants} joined · ▸ submit to hand in your brain`,` · 已有 ${a.data.entrants} 人 · ▸ submit 交鸭脑`,` · เข้าร่วมแล้ว ${a.data.entrants} คน · ▸ submit เพื่อส่งสมอง`)));break}case"status":{const e=v(),t=x();if(!e||!t){console.log(chalk.gray(b(" Not in a Cup yet — join <passcode>."," 还没加入杯赛 —— join <口令>。"," ยังไม่ได้เข้า Cup — join <รหัส>")));break}const o=await N("status",t,{cup:e.code});if(!o.ok||!o.data){console.log(chalk.yellow(` ✗ ${o.message??o.code??""}`));break}const s=o.data,n={open:b("open — brains can still change","开放 —— 还能改鸭脑","เปิดอยู่ — ยังเปลี่ยนสมองได้"),locked:b("locked — the Cup is being judged","已锁定 —— 正在判分","ล็อกแล้ว — กำลังตัดสิน"),done:b("done — type bracket","已结束 —— 敲 bracket","จบแล้ว — พิมพ์ bracket"),closed:b("closed","已关闭","ปิดแล้ว")}[s.state];console.log(chalk.white(` ${s.title??s.cup} · ${n} · ${s.entrants} ${b("joined","人加入","คนเข้าร่วม")}, ${s.with_brain} ${b("with a brain","人已交鸭脑","คนส่งสมองแล้ว")}`)),s.me&&console.log(chalk.gray(s.me.sha?b(` you: ${s.me.nick} · brain ${s.me.sha.slice(0,8)} · legs ${s.me.legs} · handed in ${s.me.submits}× · last ${s.me.submitted_at}`,` 你:${s.me.nick} · 鸭脑 ${s.me.sha.slice(0,8)} · 腿 ${s.me.legs} · 交了 ${s.me.submits} 次 · 最后 ${s.me.submitted_at}`,` คุณ: ${s.me.nick} · สมอง ${s.me.sha.slice(0,8)} · ขา ${s.me.legs} · ส่ง ${s.me.submits} ครั้ง · ล่าสุด ${s.me.submitted_at}`):b(` you: ${s.me.nick} · no brain handed in yet — submit`,` 你:${s.me.nick} · 还没交鸭脑 —— submit`,` คุณ: ${s.me.nick} · ยังไม่ได้ส่งสมอง — submit`)));break}case"submit":{const e=v(),o=x();if(!e||!o){console.log(chalk.gray(b(" Join the Cup first: join <passcode> (your teacher gives it)."," 先加入杯赛:join <口令>(老师会给)。"," เข้าร่วม Cup ก่อน: join <รหัส> (ครูจะให้)")));break}const s=j(t,i,["pack"]);if(!0!==s.ok){L(s);break}const n=await N("submit",o,{cup:e.code,brain:s.submission});if(!n.ok||!n.data){console.log(chalk.yellow(` ✗ ${n.message??n.code??""}`));break}p(`duck cup submit ${e.code} ${n.data.me?.sha??""}`),g.submitted=!0,P(g),console.log(chalk.green(b(` ✓ Brain handed in (${n.data.me?.sha?.slice(0,8)}, legs ${n.data.me?.legs})`,` ✓ 鸭脑已交(${n.data.me?.sha?.slice(0,8)},腿 ${n.data.me?.legs})`,` ✓ ส่งสมองแล้ว (${n.data.me?.sha?.slice(0,8)}, ขา ${n.data.me?.legs})`))+chalk.gray(b(" · improve and submit again any time until the Cup starts — the last one counts"," · 开赛前可以继续改、再交 —— 最后一份算数"," · ปรับแล้วส่งใหม่ได้จนกว่า Cup จะเริ่ม — นับครั้งสุดท้าย")));break}case"bracket":{const e=v();if(!e){console.log(chalk.gray(b(" Not in a Cup yet — join <passcode>."," 还没加入杯赛 —— join <口令>。"," ยังไม่ได้เข้า Cup — join <รหัส>")));break}const t=await l(`duckcup/${e.code}/results.json`,15e3,{force:!0});if(!t){console.log(chalk.gray(b(" No results yet — the teacher publishes them when the Cup has been judged. status shows where it is."," 还没有结果 —— 判完后老师会发布。status 看进度。"," ยังไม่มีผล — ครูจะประกาศเมื่อตัดสินเสร็จ status ดูความคืบหน้า")));break}R(t,e.nick),g.bracket=!0,P(g);break}default:if(h+=1,console.log(chalk.yellow(b(` Don't know "${u.trim()}".`,` 不认识「${u.trim()}」。`,` ไม่รู้จัก "${u.trim()}"`))+chalk.gray(b(" Try: run · watch · brain · set · join · submit · bracket · guide · back"," 可用:run · watch · brain · set · join · submit · bracket · guide · back"," ลอง: run · watch · brain · set · join · submit · bracket · guide · back"))),h>=3)return console.log(chalk.gray(b(" Three misses — going back one level."," 连续三次没对上 —— 回上一层。"," พลาดสามครั้ง — ถอยกลับหนึ่งชั้น"))),"back"}}}
1
+ import{spawn as e,spawnSync as t}from"node:child_process";import{existsSync as o,readFileSync as s,writeFileSync as n}from"node:fs";import{homedir as a}from"node:os";import{join as r}from"node:path";import chalk from"chalk";import{getConfig as i}from"../lib/config.js";import{runInteractive as c}from"../lib/interactive-spawn.js";import{lazyJson as l}from"../lib/lazy-assets.js";import{learnPost as u}from"../lib/learn-contest.js";import{listLearnSessions as g}from"../lib/learn-state.js";import{logCommand as p}from"../lib/logger.js";import{ARENA_VI_FLAGS as d}from"../lib/vi-flags.js";const h=()=>{const e=(i().language||"en").toLowerCase();return e.startsWith("zh")?"zh":e.startsWith("th")?"th":"en"},b=(e,t,o)=>{const s=h();return"zh"===s?t:"th"===s?o:e};export const CUP_HOME=r(a(),".icoa","duck","cup");const m=r(CUP_HOME,"recipe.py"),$=r(CUP_HOME,"last-run.json"),k=r(CUP_HOME,"state.json"),f=r(CUP_HOME,"cup.json"),y={read:"read",r:"read",recipe:"read",info:"read",edit:"edit",e:"edit",vi:"edit",set:"set",knob:"set",change:"set",run:"run",train:"run",go:"run",play:"run",watch:"watch",replay:"watch",show:"watch",w:"watch",brain:"brain",b:"brain",table:"brain",legs:"legs",leg:"legs",walker:"legs",submit:"submit",s:"submit",handin:"submit",join:"join",enter:"join",passcode:"join",status:"status",me:"status",state:"status",bracket:"bracket",draw:"bracket",results:"bracket",guide:"guide",help:"guide","?":"guide",h:"guide",steps:"guide",back:"back",quit:"back",q:"back",exit:"back",menu:"menu"};export function normalizeCupVerb(e){const t=e.trim().replace(/^\//,"").split(/\s+/).filter(Boolean);return 0===t.length?{verb:"unknown",args:[]}:{verb:y[t[0].toLowerCase()]??"unknown",args:t.slice(1)}}const w=new Set(["EPISODES","EXPLORE","EXPLORE_END","LEARN_RATE","LOOK_AHEAD","DICE"]),E={LEGS:["factory","sprint","calm"],COURSES:["practice","mixed"]},S=["progress","flag","finish","time","fall","wall","speed","smooth"];export function setRecipeKnob(e,t,o){const s=t.toUpperCase(),n=t.toLowerCase(),a=e=>/^-?(\d+\.?\d*|\.\d+)$/.test(e);if(w.has(s)){if(!a(o))return{ok:!1,why:"value"};const t=new RegExp(`^(${s}\\s*=\\s*)([^#\\n]*?)(\\s*(?:#.*)?)$`,"m");return t.test(e)?{ok:!0,src:e.replace(t,`$1${o}$3`),shown:`${s} = ${o}`}:{ok:!1,why:"missing"}}if(E[s]){const t=o.replace(/^["']|["']$/g,"").toLowerCase();if(!E[s].includes(t))return{ok:!1,why:"value"};const n=new RegExp(`^(${s}\\s*=\\s*)([^#\\n]*?)(\\s*(?:#.*)?)$`,"m");return n.test(e)?{ok:!0,src:e.replace(n,`$1"${t}"$3`),shown:`${s} = "${t}"`}:{ok:!1,why:"missing"}}if(S.includes(n)){if(!a(o))return{ok:!1,why:"value"};const t=new RegExp(`^(\\s*["']${n}["']\\s*:\\s*)([^,#\\n]*)(.*)$`,"m");return t.test(e)?{ok:!0,src:e.replace(t,`$1${o}$3`),shown:`REWARD["${n}"] = ${o}`}:{ok:!1,why:"missing"}}return{ok:!1,why:"name"}}export function recipeValue(e,t){const o=new RegExp(`^${t}\\s*=\\s*([^#\\n]*?)\\s*(?:#.*)?$`,"m").exec(e);return o?o[1].replace(/^["']|["']$/g,""):null}export function sparkline(e,t=0,o=130){return e.map(e=>" ▁▂▃▄▅▆▇█"[Math.min(8,Math.max(0,Math.floor((e-t)/(o-t)*8)))]).join("")}function j(e,o,s,n=12e4){const a=t(e,[r(o,"cup","cup_tool.py"),...s,"--home",CUP_HOME],{encoding:"utf-8",timeout:n,env:{...process.env,PYTHONDONTWRITEBYTECODE:"1",OMP_NUM_THREADS:"1",MUJOCO_GL:"disable"}}),i=String(a.stdout??"").trim().split("\n").pop()||"";try{return JSON.parse(i)}catch{return{ok:!1,error:"engine",message:String(a.stderr??"").trim().split("\n").slice(-2).join(" ")}}}function C(t,o){return new Promise(s=>{const n=e(t,[r(o,"cup","cup_tool.py"),"train","--home",CUP_HOME],{env:{...process.env,PYTHONDONTWRITEBYTECODE:"1",OMP_NUM_THREADS:"1",MUJOCO_GL:"disable"},stdio:["ignore","pipe","pipe"]});let a="",i={ok:!1,error:"engine",message:"no output"},c="";n.stdout.on("data",e=>{a+=e.toString("utf-8");let t=a.indexOf("\n");for(;t>=0;){const e=a.slice(0,t);a=a.slice(t+1),t=a.indexOf("\n");try{const t=JSON.parse(e);if("ep"===t.type){const e=t.fell?b("fell","摔倒","ล้ม"):`${t.flags} ${b("flags","面旗","ธง")}`;process.stdout.write(`\r${chalk.gray(` ${b("episode","回合","รอบ")} ${String(t.episode).padStart(3)}/${t.of} · ${b("explore","探索","สำรวจ")} ${Number(t.explore).toFixed(2)} · ${b("points","比赛分","คะแนน")} ${Number(t.points).toFixed(1)} (${e})`)}`)}else i=t}catch{}}}),n.stderr.on("data",e=>{c+=e.toString("utf-8")}),n.on("close",()=>{process.stdout.write("\n"),!1===i.ok&&!i.message&&c&&(i.message=c.trim().split("\n").slice(-2).join(" ")),s(i)})})}function O(t,o,s){return new Promise(n=>{e(t,[r(o,"cup","cup_tool.py"),"view","--file",s],{env:{...process.env,PYTHONDONTWRITEBYTECODE:"1"},stdio:["ignore","inherit","inherit"]}).on("close",()=>n())})}const v=()=>{try{const e=JSON.parse(s(f,"utf-8"));return e.code?e:null}catch{return null}};function x(){try{const e=g();return(e.find(e=>e.curriculumId?.startsWith("mduck"))??e[0])?.token??null}catch{return null}}const N=(e,t,o)=>u(`/api/icoa/learn/duckcup/${e}`,{token:t.toUpperCase(),...o},2e4),_=(e,t)=>{const o=t/2**(e-1);return 2===o?b("Final","决赛","รอบชิง"):4===o?b("Semi-finals","半决赛","รอบรองชนะเลิศ"):`${b("Round of","","รอบ")} ${o}${"zh"===h()?" 强":""}`};function R(e,t){console.log(chalk.bold.white(` 🏆 ${e.title??"Duck Cup"} · ${e.players} ${b(1===e.players?"player":"players","名选手","ผู้เล่น")} · ${e.size}`));for(const o of e.rounds){console.log(chalk.bold.white(` ${_(o.round,e.size)}`)+chalk.gray(` · ${b("course levels","赛道难度","ระดับสนาม")} ${o.levels.join("/")}`));const s=o.matches.filter(e=>t&&(e.a.nick===t||e.b.nick===t)),n=o.matches.length<=8?o.matches:s;for(const e of n){const o=e=>`${e.bot?chalk.gray("[bot] "):""}${e.nick===t?chalk.cyan(e.nick):chalk.white(e.nick)}`,s=e.winner===e.a.slot?"a":"b";console.log(` ${chalk.gray(e.id.padEnd(7))} ${"a"===s?"✅":" "} ${o(e.a)} ${chalk.gray(e.pts_a.map(e=>e.toFixed(0)).join("/"))}`+chalk.gray(" vs ")+`${"b"===s?"✅":" "} ${o(e.b)} ${chalk.gray(e.pts_b.map(e=>e.toFixed(0)).join("/"))} ${chalk.gray(e.how)}`)}o.matches.length>8&&!s.length&&console.log(chalk.gray(b(` (${o.matches.length} matches — you are not in this round)`,` (${o.matches.length} 场 —— 这一轮没有你)`,` (${o.matches.length} แมตช์ — คุณไม่ได้อยู่ในรอบนี้)`)))}console.log(chalk.green(` 🥇 ${b("Champion","冠军","แชมป์")}: ${e.champion}`)),console.log(chalk.gray(b(" ▸ watch <match> <course 1-3> — e.g. watch R1-M3 2 replays that race"," ▸ watch <场次> <赛道 1-3> —— 例如 watch R1-M3 2 回放那一场"," ▸ watch <แมตช์> <สนาม 1-3> — เช่น watch R1-M3 2 ดูการแข่งนั้น")))}const P=e=>{try{n(k,JSON.stringify(e))}catch{}};function D(e){console.log(chalk.bold.white(b(" Duck Cup — train your duck’s brain"," Duck Cup —— 训练你的鸭脑"," Duck Cup — ฝึกสมองเป็ดของคุณ"))),console.log(chalk.gray(b(" The legs already walk (a policy trained by RL on a GPU). You teach the brain WHEN to go, turn or stop."," 鸭腿已经会走(GPU 上用强化学习训出来的)。你来教鸭脑:什么时候走、转、停。"," ขาเดินได้แล้ว (ฝึกด้วย RL บน GPU) คุณสอนสมองว่าเมื่อไรจะเดิน เลี้ยว หรือหยุด"))),[[e.trained,"run","train on this laptop (~30 s), watch the learning curve","在本机训练(约 30 秒),看学习曲线","ฝึกบนเครื่องนี้ (~30 วินาที) ดูกราฟการเรียนรู้"],[e.watched,"watch","replay your duck on the practice course","在练习赛道上回放你的鸭子","ดูเป็ดของคุณวิ่งบนสนามซ้อม"],[e.brain,"brain","see what it learned: one move per situation","看它学会了什么:每种情况一个动作","ดูว่ามันเรียนรู้อะไร: หนึ่งท่าต่อหนึ่งสถานการณ์"],[e.set,"set EPISODES 150","change one knob, then run again (read lists them)","改一个旋钮再 run(read 列出全部)","เปลี่ยนหนึ่ง knob แล้ว run ใหม่ (read แสดงทั้งหมด)"],[e.joined,"join <passcode>","enter the Cup with your teacher’s passcode","用老师给的口令加入杯赛","เข้าร่วม Cup ด้วยรหัสจากครู"],[e.submitted,"submit","hand your brain in (again any time until the Cup starts)","交鸭脑(开赛前可反复交,最后一份算数)","ส่งสมอง (ส่งใหม่ได้จนกว่า Cup จะเริ่ม)"],[e.bracket,"bracket","after the Cup: the draw, your path, replays","赛后:对阵表、你的路线、回放","หลังแข่ง: สายการแข่ง เส้นทางของคุณ รีเพลย์"]].forEach(([e,t,o,s,n],a)=>{var r;console.log(` ${r=e,r?"✅":"⬜"} ${a+1} ${chalk.bold.green(t.padEnd(18))} ${chalk.gray(b(o,s,n))}`)}),console.log(chalk.gray(b(" read · edit · legs · guide (this panel) · back (one level up) · menu"," read · edit · legs · guide(这块面板)· back(回上一层)· menu"," read · edit · legs · guide (แผงนี้) · back (ถอยหนึ่งชั้น) · menu")))}function U(){let e="";try{e=s(m,"utf-8")}catch{}console.log(chalk.bold.white(b(" Your recipe — ~/.icoa/duck/cup/recipe.py"," 你的配方 —— ~/.icoa/duck/cup/recipe.py"," สูตรของคุณ — ~/.icoa/duck/cup/recipe.py")));for(const t of e.split("\n"))/^[A-Z]/.test(t)||/^\s+"/.test(t)?console.log(` ${chalk.white(t)}`):t.trim()&&console.log(` ${chalk.gray(t)}`);console.log(chalk.gray(b(" ▸ set <KNOB> <value> e.g. set EPISODES 150 · set flag 10 · set LEGS sprint · set COURSES mixed ▸ edit for the whole file"," ▸ set <旋钮> <值> 例如 set EPISODES 150 · set flag 10 · set LEGS sprint · set COURSES mixed ▸ edit 打开整个文件"," ▸ set <KNOB> <ค่า> เช่น set EPISODES 150 · set flag 10 · set LEGS sprint · set COURSES mixed ▸ edit เปิดทั้งไฟล์")))}function L(e){if("recipe"===e.error)return console.log(chalk.yellow(b(" ✗ The recipe has a problem"," ✗ 配方有问题"," ✗ สูตรมีปัญหา")+(e.line?` (line ${e.line})`:"")+`: ${e.message??""}`)),e.text&&console.log(chalk.yellow(` ${String(e.text)}`)),void console.log(chalk.gray(b(" ▸ read shows the recipe · edit to fix it"," ▸ read 看配方 · edit 去改"," ▸ read ดูสูตร · edit แก้ไข")));"no_brain"!==e.error?console.log(chalk.yellow(b(" Engine error: "," 引擎出错:"," เอนจินผิดพลาด: ")+String(e.message??e.error??""))):console.log(chalk.gray(b(" No brain yet — run trains one."," 还没有鸭脑 —— 先 run 训练一个。"," ยังไม่มีสมอง — run เพื่อฝึก")))}export async function runDuckCup(e,t,i){if(p("duck cup"),!o(r(i,"cup","cup_tool.py")))return console.log(chalk.yellow(b(" This engine package has no Duck Cup yet — leave duck and come back to refresh it."," 引擎包里还没有 Duck Cup —— 退出 duck 再进来刷新一次。"," แพ็กเอนจินนี้ยังไม่มี Duck Cup — ออกจาก duck แล้วเข้าใหม่เพื่ออัปเดต"))),"back";const u=j(t,i,["init"]);if(!0!==u.ok)return L(u),"back";const g=(()=>{try{return JSON.parse(s(k,"utf-8"))}catch{return{}}})();console.log(),D(g);let h=0;for(;;){const u=await e(chalk.bold.cyan("icoa duck cup> "));if(null===u)return"back";if(!u.trim())continue;const{verb:k,args:y}=normalizeCupVerb(u);switch("unknown"!==k&&(h=0),k){case"back":return"back";case"menu":return"menu";case"guide":D(g);break;case"read":U();break;case"edit":console.log(chalk.gray(b(" vi survival: i = edit · Esc = stop editing · :wq = save and leave · :q! = discard"," vi 三键: i 进入编辑 · Esc 退出编辑 · :wq 存盘离开 · :q! 放弃"," vi: i = แก้ไข · Esc = หยุดแก้ · :wq = บันทึกแล้วออก · :q! = ทิ้ง"))),await c("vi",[...d,m]);break;case"set":{if(y.length<2){console.log(chalk.gray(b(" set <KNOB> <value> — e.g. set EPISODES 150 · set explore 0.5 · set flag 10 · set LEGS sprint"," set <旋钮> <值> —— 例如 set EPISODES 150 · set explore 0.5 · set flag 10 · set LEGS sprint"," set <KNOB> <ค่า> — เช่น set EPISODES 150 · set explore 0.5 · set flag 10 · set LEGS sprint")));break}const e=setRecipeKnob(s(m,"utf-8"),y[0],y.slice(1).join(" "));if(!e.ok){const t="name"===e.why?b(`no knob called "${y[0]}" — read lists them`,`没有叫「${y[0]}」的旋钮 —— read 列出全部`,`ไม่มี knob ชื่อ "${y[0]}" — read แสดงทั้งหมด`):"value"===e.why?b(`"${y.slice(1).join(" ")}" is not a value ${y[0]} takes`,`「${y.slice(1).join(" ")}」不是 ${y[0]} 能取的值`,`"${y.slice(1).join(" ")}" ใช้กับ ${y[0]} ไม่ได้`):b("that line is not in your recipe any more — edit to put it back","配方里找不到那一行了 —— edit 补回去","ไม่พบบรรทัดนั้นในสูตรแล้ว — edit เพื่อใส่กลับ");console.log(chalk.yellow(` ✗ ${t}`));break}n(m,e.src),p(`duck cup set ${y.join(" ")}`),g.set=!0,P(g),console.log(chalk.green(` ✓ ${e.shown}`)+chalk.gray(b(" ▸ run to train with it"," ▸ run 用它训练"," ▸ run เพื่อฝึกด้วยค่านี้")));break}case"run":{if(y.length>=1){const e=Number(y[0]),o=Number(y[1]??1);if(!Number.isInteger(e)||!Number.isInteger(o)||o<0||o>3){console.log(chalk.yellow(b(" run <seed> <level 0-3> — e.g. run 202 2"," run <种子> <难度 0-3> —— 例如 run 202 2"," run <seed> <ระดับ 0-3> — เช่น run 202 2")));break}const s=j(t,i,["run","--seed",String(e),"--level",String(o)]);if(!0!==s.ok){L(s);break}await O(t,i,String(s.replay)),console.log(chalk.white(b(` Unseen course ${e} (level ${o}): ${s.points} points, ${s.flags}/${s.of} flags`,` 没见过的赛道 ${e}(难度 ${o}):${s.points} 分,${s.flags}/${s.of} 面旗`,` สนามใหม่ ${e} (ระดับ ${o}): ${s.points} คะแนน, ${s.flags}/${s.of} ธง`)));break}p("duck cup run"),console.log(chalk.gray(b(" 🦆 learning…"," 🦆 学习中…"," 🦆 กำลังเรียนรู้…")));const e=await C(t,i);if(!0!==e.ok){L(e);break}const o=e.curve??[];console.log(chalk.gray(b(` learning curve (points per episode, ${e.train_s} s):`,` 学习曲线(每回合的比赛分,${e.train_s} 秒):`,` กราฟการเรียนรู้ (คะแนนต่อรอบ, ${e.train_s} วินาที):`)));for(let e=0;e<o.length;e+=60)console.log(` ${chalk.cyan(sparkline(o.slice(e,e+60)))}`);const s=e.practice;console.log(chalk.green(b(` Practice course, no random moves: ${s.points} points, ${s.flags}/3 flags`,` 练习赛道(不再随机试):${s.points} 分,${s.flags}/3 面旗`,` สนามซ้อม (ไม่สุ่มแล้ว): ${s.points} คะแนน, ${s.flags}/3 ธง`)+(s.finish_s?b(`, nest at ${s.finish_s.toFixed(1)} s`,`,${s.finish_s.toFixed(1)} 秒到窝`,`, ถึงรังที่ ${s.finish_s.toFixed(1)} วินาที`):""))+chalk.gray(` · legs ${e.legs}`)),g.trained=!0,P(g),console.log(chalk.gray(b(" ▸ watch to see it run · brain to see what it learned · run 202 2 on a course it has never seen"," ▸ watch 看它跑 · brain 看它学到什么 · run 202 2 跑一条没见过的赛道"," ▸ watch ดูมันวิ่ง · brain ดูสิ่งที่เรียนรู้ · run 202 2 สนามที่ไม่เคยเห็น")));break}case"watch":if(y.length>=1){const e=v(),o=y[0].toUpperCase(),s=Number(y[1]??1);if(!e||!/^R\d+-M\d+$/.test(o)||![1,2,3].includes(s)){console.log(chalk.yellow(b(" watch <match> <course 1-3> — e.g. watch R1-M3 2 (bracket lists the matches)"," watch <场次> <赛道 1-3> —— 例如 watch R1-M3 2(bracket 列出场次)"," watch <แมตช์> <สนาม 1-3> — เช่น watch R1-M3 2 (bracket แสดงแมตช์)")));break}const n=`duckcup/${e.code}/matches/${o}-c${s}.json`;if(!await l(n,15e3,{force:!0})){console.log(chalk.yellow(b(" That replay is not published (yet)."," 这场回放还没发布。"," ยังไม่มีรีเพลย์นี้")));break}await O(t,i,r(a(),".icoa","assets",n));break}if(!o($)){console.log(chalk.gray(b(" Nothing to replay yet — run first."," 还没有可回放的 —— 先 run。"," ยังไม่มีอะไรให้ดู — run ก่อน")));break}await O(t,i,$),g.watched=!0,P(g);break;case"brain":{const e=j(t,i,["brain"]);if(!0!==e.ok){L(e);break}console.log(chalk.bold.white(b(" Your duck’s brain (what it does in each situation)"," 你的鸭脑(每种情况它会做什么)"," สมองเป็ดของคุณ (ทำอะไรในแต่ละสถานการณ์)")));for(const t of String(e.text).split("\n"))console.log(chalk.white(` ${t}`));console.log(chalk.gray(b(` ? = never met this situation while learning · legs ${e.legs} · ${e.episodes} episodes`,` ? = 学习时没遇到过这种情况 · 腿 ${e.legs} · ${e.episodes} 回合`,` ? = ไม่เคยเจอสถานการณ์นี้ตอนเรียน · ขา ${e.legs} · ${e.episodes} รอบ`))),g.brain=!0,P(g);break}case"legs":{let e="factory";try{e=recipeValue(s(m,"utf-8"),"LEGS")??"factory"}catch{}console.log(chalk.bold.white(b(" The legs menu (walkers trained by RL on a GPU)"," 腿型菜单(GPU 上强化学习训出来的走路程序)"," เมนูขา (โปรแกรมเดินที่ฝึกด้วย RL บน GPU)")));const t=[["factory","the standard walker","标准走法","ขาเดินมาตรฐาน"],["sprint","trained to chase speed: ~40% faster, shakier, worse at turning","为速度训练:快约 40%,更抖,转弯差","ฝึกเพื่อความเร็ว: เร็วขึ้น ~40% สั่นกว่า เลี้ยวแย่กว่า"],["calm","trained to be smooth: steadier, but slower","为平稳训练:更稳,但更慢","ฝึกให้นิ่ง: นิ่งกว่า แต่ช้ากว่า"]];for(const[o,s,n,a]of t)console.log(` ${o===e?"▶":" "} ${chalk.bold.green(o.padEnd(8))} ${chalk.gray(b(s,n,a))}`);console.log(chalk.gray(b(" ▸ set LEGS sprint → run (a brain must be trained ON the legs it races with)"," ▸ set LEGS sprint → run(鸭脑必须在它比赛用的腿上训练)"," ▸ set LEGS sprint → run (สมองต้องฝึกบนขาที่ใช้แข่ง)")));break}case"join":{const t=(y[0]??"").toUpperCase().replace(/[^A-Z0-9]/g,"");if(t.length<3){console.log(chalk.gray(b(" join <passcode> — your teacher writes it on the board"," join <口令> —— 老师会写在黑板上"," join <รหัส> — ครูจะเขียนบนกระดาน")));break}const o=x();if(!o){console.log(chalk.yellow(b(" No learn token on this laptop — paste your camp token in learn first."," 这台电脑上没有学习 token —— 先在 learn 里粘贴营地 token。"," เครื่องนี้ไม่มี learn token — วาง token ค่ายใน learn ก่อน")));break}let s=y.slice(1).join(" ").trim();if(s||(s=(await e(chalk.white(b(" Your Cup nickname (2–14 letters): "," 杯赛昵称(2–14 个字母或数字):"," ชื่อเล่นใน Cup (2–14 ตัว): ")))??"").trim()),!s)break;const a=await N("join",o,{cup:t,nick:s});if(!a.ok||!a.data){console.log(chalk.yellow(` ✗ ${a.message??a.code??""}`));break}n(f,JSON.stringify({code:t,nick:s})),p(`duck cup join ${t}`),g.joined=!0,P(g),console.log(chalk.green(b(` ✓ You are in ${a.data.title??t} as ${s}`,` ✓ 已加入 ${a.data.title??t},昵称 ${s}`,` ✓ เข้าร่วม ${a.data.title??t} ในชื่อ ${s}`))+chalk.gray(b(` · ${a.data.entrants} joined · ▸ submit to hand in your brain`,` · 已有 ${a.data.entrants} 人 · ▸ submit 交鸭脑`,` · เข้าร่วมแล้ว ${a.data.entrants} คน · ▸ submit เพื่อส่งสมอง`)));break}case"status":{const e=v(),t=x();if(!e||!t){console.log(chalk.gray(b(" Not in a Cup yet — join <passcode>."," 还没加入杯赛 —— join <口令>。"," ยังไม่ได้เข้า Cup — join <รหัส>")));break}const o=await N("status",t,{cup:e.code});if(!o.ok||!o.data){console.log(chalk.yellow(` ✗ ${o.message??o.code??""}`));break}const s=o.data,n={open:b("open — brains can still change","开放 —— 还能改鸭脑","เปิดอยู่ — ยังเปลี่ยนสมองได้"),locked:b("locked — the Cup is being judged","已锁定 —— 正在判分","ล็อกแล้ว — กำลังตัดสิน"),done:b("done — type bracket","已结束 —— 敲 bracket","จบแล้ว — พิมพ์ bracket"),closed:b("closed","已关闭","ปิดแล้ว")}[s.state];console.log(chalk.white(` ${s.title??s.cup} · ${n} · ${s.entrants} ${b("joined","人加入","คนเข้าร่วม")}, ${s.with_brain} ${b("with a brain","人已交鸭脑","คนส่งสมองแล้ว")}`)),s.me&&console.log(chalk.gray(s.me.sha?b(` you: ${s.me.nick} · brain ${s.me.sha.slice(0,8)} · legs ${s.me.legs} · handed in ${s.me.submits}× · last ${s.me.submitted_at}`,` 你:${s.me.nick} · 鸭脑 ${s.me.sha.slice(0,8)} · 腿 ${s.me.legs} · 交了 ${s.me.submits} 次 · 最后 ${s.me.submitted_at}`,` คุณ: ${s.me.nick} · สมอง ${s.me.sha.slice(0,8)} · ขา ${s.me.legs} · ส่ง ${s.me.submits} ครั้ง · ล่าสุด ${s.me.submitted_at}`):b(` you: ${s.me.nick} · no brain handed in yet — submit`,` 你:${s.me.nick} · 还没交鸭脑 —— submit`,` คุณ: ${s.me.nick} · ยังไม่ได้ส่งสมอง — submit`)));break}case"submit":{const e=v(),o=x();if(!e||!o){console.log(chalk.gray(b(" Join the Cup first: join <passcode> (your teacher gives it)."," 先加入杯赛:join <口令>(老师会给)。"," เข้าร่วม Cup ก่อน: join <รหัส> (ครูจะให้)")));break}const s=j(t,i,["pack"]);if(!0!==s.ok){L(s);break}const n=await N("submit",o,{cup:e.code,brain:s.submission});if(!n.ok||!n.data){console.log(chalk.yellow(` ✗ ${n.message??n.code??""}`));break}p(`duck cup submit ${e.code} ${n.data.me?.sha??""}`),g.submitted=!0,P(g),console.log(chalk.green(b(` ✓ Brain handed in (${n.data.me?.sha?.slice(0,8)}, legs ${n.data.me?.legs})`,` ✓ 鸭脑已交(${n.data.me?.sha?.slice(0,8)},腿 ${n.data.me?.legs})`,` ✓ ส่งสมองแล้ว (${n.data.me?.sha?.slice(0,8)}, ขา ${n.data.me?.legs})`))+chalk.gray(b(" · improve and submit again any time until the Cup starts — the last one counts"," · 开赛前可以继续改、再交 —— 最后一份算数"," · ปรับแล้วส่งใหม่ได้จนกว่า Cup จะเริ่ม — นับครั้งสุดท้าย")));break}case"bracket":{const e=v();if(!e){console.log(chalk.gray(b(" Not in a Cup yet — join <passcode>."," 还没加入杯赛 —— join <口令>。"," ยังไม่ได้เข้า Cup — join <รหัส>")));break}const t=await l(`duckcup/${e.code}/results.json`,15e3,{force:!0});if(!t){console.log(chalk.gray(b(" No results yet — the teacher publishes them when the Cup has been judged. status shows where it is."," 还没有结果 —— 判完后老师会发布。status 看进度。"," ยังไม่มีผล — ครูจะประกาศเมื่อตัดสินเสร็จ status ดูความคืบหน้า")));break}R(t,e.nick),g.bracket=!0,P(g);break}default:if(h+=1,console.log(chalk.yellow(b(` Don't know "${u.trim()}".`,` 不认识「${u.trim()}」。`,` ไม่รู้จัก "${u.trim()}"`))+chalk.gray(b(" Try: run · watch · brain · set · join · submit · bracket · guide · back"," 可用:run · watch · brain · set · join · submit · bracket · guide · back"," ลอง: run · watch · brain · set · join · submit · bracket · guide · back"))),h>=3)return console.log(chalk.gray(b(" Three misses — going back one level."," 连续三次没对上 —— 回上一层。"," พลาดสามครั้ง — ถอยกลับหนึ่งชั้น"))),"back"}}}