icoa-cli 2.19.639 → 2.19.640
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.
package/dist/lib/learn-pvp.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{copyFileSync as e,existsSync as o,mkdirSync as t,readFileSync as n,writeFileSync as a}from"node:fs";import{homedir as r}from"node:os";import{join as s}from"node:path";import chalk from"chalk";import{playSavedReplay as l,resolveEngineDir as i,runMobLocal as c,saveJudgeReplay as g}from"../commands/battle.js";import{getConfig as d}from"./config.js";import{runInteractive as u}from"./interactive-spawn.js";import{learnPost as y}from"./learn-contest.js";import{probePython as p}from"./python-setup.js";import{ARENA_VI_FLAGS as m}from"./vi-flags.js";export const PVP_RE=/^\/?(?:room|rooms|pvp|房间|对战房)(?:\s+(.*))?$/i;const b=(e,o,t={},n=2e4)=>y(`/api/icoa/learn/pvp/${e}`,{token:o.toUpperCase(),...t},n),h=()=>(d().language||"en").toLowerCase().startsWith("zh"),$=new Set(["back","b","quit","q","exit","card","lesson","卡片"]);export const DEFAULT_PVP_BOT=s(r(),".icoa","battleai","bot.py");const f=e=>{let o=0;for(const t of e){const e=t.codePointAt(0)??0;o+=e>65535||e>=11904&&e<=42191||e>=44032&&e<=55203||e>=63744&&e<=64255?2:1}return o},w=(e,o)=>e+" ".repeat(Math.max(0,o-f(e)));function v(e){console.log(chalk.gray(h()?` knobs 看旋钮 · set <旋钮> <值> 改一个 · run 本地试打 · up 上传 · vs <昵称> 挑战 · 回车 战绩表 · log · replay · edit 改代码 · template 换模板 · card 回卡片 (${e})`:` knobs · set <KNOB> <value> · run (local test) · up (upload) · vs <nick> (challenge) · Enter (table) · log · replay · edit · template · card (${e})`))}function k(e){const o=h(),t=e.public?o?"公共房":"Open room":e.label||(o?"房间":"Room");console.log(),console.log(chalk.bold.white(` ⚔ ${t} `)+chalk.bold.cyan(e.room)+chalk.gray(` · ${o?"你是":"you are"} ${e.nick??"?"} · ${e.members.length} ${o?"人":"members"} · ${o?"今日":"today"} ${e.day}${e.public?o?" · 战绩每天清零":" · table resets daily":""}`)),console.log(chalk.gray(` ${w(o?"昵称":"nick",14)}${w("bot",6)}${w(o?"组":"group",9)}${w(o?"今日 胜-负-平":"today W-L-D",16)}${o?"累计":"all-time"}`));for(const t of e.members){const e="rookie"===t.group?o?"🔧 新手":"🔧 knobs":"pro"===t.group?o?"✍ 高手":"✍ code":"—",n=` ${w(t.nick,14)}${w(t.versions?`v${t.versions}`:"—",6)}${w(e,9)}${w(`${t.today.w}-${t.today.l}-${t.today.d}`,16)}${t.all.w}-${t.all.l}-${t.all.d}`;console.log(t.me?chalk.bold.cyan(n):chalk.white(n))}if(e.my_versions.length||console.log(chalk.yellow(o?" 你还没上传 bot —— 先 knobs / set,再 up。":" You have no bot here yet — knobs / set, then up.")),e.recent.length){console.log(chalk.gray(o?" 今日对局:":" Today:"));for(const o of e.recent.slice(0,6))console.log(chalk.gray(" ")+_(o))}}function _(e){const o="WIN"===e.verdict?chalk.green(`${e.wins_a}-${e.wins_b}`):"LOSS"===e.verdict?chalk.red(`${e.wins_a}-${e.wins_b}`):chalk.yellow(`${e.wins_a}-${e.wins_b}`),t=`${e.a} v${e.a_version??"?"} vs ${e.b} v${e.b_version??"?"}`;return`${chalk.gray((e=>{const o=new Date(e);return Number.isNaN(o.getTime())?"":`${String(o.getHours()).padStart(2,"0")}:${String(o.getMinutes()).padStart(2,"0")}`})(e.ts))} ${e.mine?chalk.white(t):chalk.gray(t)} ${o}${e.draws?chalk.gray(` (${e.draws} draw)`):""}${"WIN"!==e.verdict&&"LOSS"!==e.verdict&&"DRAW"!==e.verdict?chalk.red(` ${e.verdict}`):""}`}function P(e){const o=h();console.log();const t=`${e.wins_a}-${e.wins_b}${e.draws?` (${e.draws} ${o?"平":"draw"})`:""}`;if("WIN"===e.verdict)console.log(chalk.bold.green(` 🏆 ${o?"你赢了":"You win"} ${t}`)+chalk.gray(` vs ${e.opp.nick} v${e.opp.version}`));else if("LOSS"===e.verdict)console.log(chalk.bold.red(` ❌ ${o?"你输了":"You lose"} ${t}`)+chalk.gray(` vs ${e.opp.nick} v${e.opp.version}`));else{if("DRAW"!==e.verdict)return void console.log(chalk.yellow(` ⚠ ${o?"判分机没跑完这场":"The judge could not finish this match"}: ${e.verdict}${e.note?` — ${e.note}`:""}`));console.log(chalk.bold.yellow(` 🤝 ${o?"平局":"Draw"} ${t}`)+chalk.gray(` vs ${e.opp.nick} v${e.opp.version}`))}e.load_error?.a&&console.log(chalk.red(` ${o?"你的 bot 装不上":"Your bot failed to load"}: ${e.load_error.a}`)),e.load_error?.b&&console.log(chalk.gray(` ${o?"对方的 bot 装不上":"Opponent's bot failed to load"}: ${e.load_error.b}`)),console.log(chalk.gray(` ${w(o?"地图":"map",11)}${w(o?"席位":"seat",6)}${w(o?"结果":"result",8)}${w(o?"原因":"how",10)}${w(o?"回合":"turns",7)}${o?"损失 你/对方":"lost you/them"}`));for(const t of e.games){const e="a"===t.winner?o?"胜":"win":"b"===t.winner?o?"负":"loss":o?"平":"draw",n="a"===t.winner?chalk.green(w(e,8)):"b"===t.winner?chalk.red(w(e,8)):chalk.yellow(w(e,8)),a="warrens"===t.reason?o?"拆家":"warrens":"timeout"===t.reason?o?"超时":"timeout":"crash"===t.reason?o?"崩溃":"crash":"load_error"===t.reason?o?"装不上":"no load":o?"回合满":"turn cap";console.log(` ${w(t.map,11)}${w(String(t.seat_a),6)}${n}${w(a,10)}${w(String(t.turns),7)}${Math.round(t.lost[0])}/${Math.round(t.lost[1])}${t.error?chalk.gray(` ${t.error}`):""}`)}console.log(chalk.gray(` ${o?"种子":"seeds"} ${e.seeds.join(",")} · ${o?"回合满时比打掉的价值,躲家里不算赢":"at the turn cap the side that destroyed more value wins — hiding at home does not"}`)),e.replay&&g(e.replay)&&console.log(chalk.gray(" ▸ ")+chalk.bold.green("replay")+chalk.gray(e.wins_b?o?" 看你输掉的第一局":" watch the first game you lost":o?" 看第一局":" watch game 1"))}const T=e=>{const o=e.search(/^# ── KNOBS/m);let t=e;if(o>=0){const n=e.slice(o).split("\n").slice(1),a=n.findIndex(e=>/^# ───/.test(e));t=(a>=0?n.slice(0,a):n).join("\n")}return[...new Set([...t.matchAll(/^([A-Z][A-Z0-9_]*)\s*=\s*[^#\n]+/gm)].map(e=>e[1]))]},A=(e,o)=>{const t=new RegExp(`^${o}\\s*=\\s*([^#\n]*?)\\s*(?:#.*)?$`,"m").exec(e);return t?t[1]:null},O=(e,o)=>{const t=new RegExp(`^${o}\\s*=[^#\n]*#\\s*(.*)$`,"m").exec(e);return t?t[1].trim():""},E=()=>{try{return n(DEFAULT_PVP_BOT,"utf8")}catch{return null}},x=e=>e.split("\n").map(e=>e.replace(/\s+#.*$/,"").trimEnd()).filter(e=>e.trim()&&!e.trim().startsWith("#")&&!/^[A-Z][A-Z0-9_]*\s*=/.test(e));async function j(e){const o=await i();if(!o)return null;try{const t=n(s(o,"bots","knob_bot.py"),"utf8");return x(t).join("\n")===x(e).join("\n")?"rookie":"pro"}catch{return null}}async function B(n,a,l){const c=await i();if(!c)return console.log(chalk.yellow(l?" 引擎还没下载到本机(需要联网一次)。":" The arena engine is not on this machine yet (one online fetch needed).")),!1;const g=s(c,"bots","knobs"===n?"knob_bot.py":l?"template_bot_zh.py":"template_bot.py");if(!o(g))return console.log(chalk.yellow(` ${l?"引擎里没有模板":"Template missing in the engine"}: ${g}`)),!1;if(t(s(r(),".icoa","battleai"),{recursive:!0}),o(DEFAULT_PVP_BOT)){if(!a)return console.log(chalk.yellow(l?" bot.py 已经存在,不覆盖。":" bot.py already exists — not overwritten.")+chalk.gray(l?" 确定要换: ":" To replace it: ")+chalk.bold.green(`template ${n} force`)+chalk.gray(l?"(旧文件留作 bot.py.bak)":" (the old file is kept as bot.py.bak)")),!1;e(DEFAULT_PVP_BOT,`${DEFAULT_PVP_BOT}.bak`)}return e(g,DEFAULT_PVP_BOT),console.log(chalk.green(" ✓ ")+chalk.white("knobs"===n?l?"15 个旋钮的新手模板已放到 bot.py":"The 15-knob starter is now your bot.py":l?"空白代码模板已放到 bot.py(高手组)":"The blank code template is now your bot.py (pro)")+chalk.gray(` (${DEFAULT_PVP_BOT})`)),!0}function N(e,o){const t=T(e);if(t.length){console.log(chalk.gray(o?" 你的旋钮(set <旋钮> <值> 改一个):":" Your knobs (set <KNOB> <value> changes one):"));for(const o of t)console.log(` ${chalk.bold.white(o.padEnd(10))}${chalk.green((A(e,o)??"").padEnd(12))}${chalk.gray(O(e,o))}`)}else console.log(chalk.gray(o?" 你的 bot.py 没有旋钮(顶部那种 NAME = value 行)—— 它是高手组的自定义代码。要旋钮版敲 template knobs。":" Your bot.py has no knobs (the NAME = value lines at the top) — it is custom code (pro). For the knob starter type template knobs."))}function U(e,o,t){const n=T(e),r=/^([A-Za-z][A-Za-z0-9_]*)\s*(?:=|\s)\s*(.+)$/.exec(o.trim());if(!r)return console.log(chalk.yellow(t?" 用法: ":" Usage: ")+chalk.bold.green(t?"set <旋钮> <值>":"set <KNOB> <value>")+chalk.gray(t?" 例如 set WOMBATS 6":" e.g. set WOMBATS 6")),N(e,t),!1;const s=r[1].toUpperCase(),l=r[2].trim();if(!n.includes(s))return console.log(chalk.yellow(t?` 没有 ${s} 这个旋钮。`:` No knob called ${s}.`)+chalk.gray(" ")+n.map(e=>chalk.bold.white(e)).join(chalk.gray(" · "))),!1;const i=A(e,s)??"";if(i.trim()===l)return console.log(chalk.gray(" ")+chalk.bold.white(s)+chalk.gray(t?` 已经是 ${l} 了。`:` is already ${l}.`)),!1;const c=new RegExp(`^(${s}\\s*=\\s*)([^#\n]*?)(\\s*)(#.*)?$`,"m"),g=e.replace(c,(e,o,t,n,a)=>`${o}${l}${a?(n||" ")+a:""}`);return a(DEFAULT_PVP_BOT,g),console.log(chalk.gray(" ")+chalk.bold.white(s)+chalk.gray(" ")+chalk.gray(i.trim())+chalk.gray(" → ")+chalk.bold.green(l)+chalk.gray(t?" 下一步: run 试打,up 上传":" next: run to test, up to upload")),!0}function S(e,t){const n=o(DEFAULT_PVP_BOT),a=!!t&&t.my_versions.length>0,r=!!t&&t.recent.some(e=>e.mine),s=(e,o,t,n,a=!1)=>` ${chalk.gray(e)} ${((e,o=!1)=>e?chalk.green("✅"):o?chalk.gray("○ "):chalk.white("⬜"))(o,a)} ${chalk.bold.green(w(t,13))}${chalk.gray(n)}`,l=new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`,"g"),i=e=>`${e}${" ".repeat(Math.max(0,70-f(e.replace(l,""))))}`,c=e?[s("1",n,"template",n?"bot.py 已就绪(~/.icoa/battleai/bot.py)":"拿 15 个旋钮的新手模板(高手: template code)"),s("2",!1,"knobs · set","看旋钮;set WOMBATS 6 改一个,不用写代码",!0),` ${chalk.gray("✍ 高手:")} ${chalk.bold.green("edit")} ${chalk.gray("打开文件,旋钮下面的 act(u) 随便重写")}`,s("3",!1,"run","本地试打一场,看 bot 跑不跑得起来(可跳过)",!0),s("4",a,"up",a?`已上传到 v${t?.my_versions.length}(改完再 up = 新版本)`:"上传 bot → v1(自动标 🔧 新手 / ✍ 高手)"),s("5",r,"vs <昵称>",r?"今天打过了 —— 再来一场":"挑战一个人:6 局定胜负,replay 看你输的那局"),s("6",!1,"回车","今日战绩表 · log 对局 · card 回卡片",!0)]:[s("1",n,"template",n?"bot.py is ready (~/.icoa/battleai/bot.py)":"get the 15-knob starter (pro: template code)"),s("2",!1,"knobs · set","see the knobs; set WOMBATS 6 changes one — no code",!0),` ${chalk.gray("✍ pro:")} ${chalk.bold.green("edit")} ${chalk.gray("opens the file — rewrite act(u) below the knobs")}`,s("3",!1,"run","one local test game — does the bot run? (optional)",!0),s("4",a,"up",a?`uploaded, now v${t?.my_versions.length} (edit + up again = new version)`:"upload the bot → v1 (tagged 🔧 knobs / ✍ code)"),s("5",r,"vs <nick>",r?"you played today — go again":"challenge someone: 6 games; then replay = the game you lost"),s("6",!1,"Enter","today's table · log matches · card back to the card",!0)],g=e?` 怎么玩 · ${t?.room??"room"} `:` How to play · ${t?.room??"room"} `;console.log(),console.log(chalk.cyan(` ╭─${chalk.bold.white(g)}${"─".repeat(Math.max(0,70-f(g)-1))}╮`));for(const e of c)console.log(`${chalk.cyan(" │")}${i(e)}${chalk.cyan("│")}`);console.log(chalk.cyan(` ╰${"─".repeat(70)}╯`));const d=n?a?r?"":e?"vs <昵称>":"vs <nick>":"up":"template";d&&console.log(chalk.gray(e?" ▸ 下一步敲: ":" ▸ next: ")+chalk.bold.green(d))}export async function runPvpRoom(e,t,a,i={}){const g=i.interactive??u,d=h(),y=a.trim();let f="",T="",A=!1;const O=/^(?:new|create|开房|建)(?:\s+(.*))?$/i.exec(y),x=/^(?:join|enter|进)\s+(\S+)$/i.exec(y);if(O?(A=!0,T=(O[1]??"").trim().slice(0,40)):x?f=x[1]:/^(?:open|public|公共)$/i.test(y)?f="OPEN":y&&(f=y),f=f.toUpperCase().replace(/[^A-Z0-9]/g,"").slice(0,8),!A&&!f){const o=await b("status",e);if(!o.ok||!o.data)return console.log(chalk.yellow(" "+L(o.code,o.message))),"back";if(console.log(),console.log(chalk.bold.white(d?" ⚔ PvP 房间":" ⚔ PvP rooms")+chalk.gray(d?" —— 和真人的 bot 对打,6 局定胜负。":" — your bot against real people, six games a match.")),o.data.rooms.length)for(const e of o.data.rooms)console.log(` ${chalk.bold.cyan(w(e.room,6))} ${chalk.white(w(e.public?d?"公共房":"open room":e.label||"",18))} ${chalk.gray(`${e.members} ${d?"人":"members"} · ${d?"你是":"you are"} ${e.nick}`)}`);else console.log(chalk.gray(d?" 你还没进过任何房间。":" You are not in any room yet."));return console.log(chalk.gray(" ▸ ")+chalk.bold.green("room open")+chalk.gray(d?" 公共房,谁都能进,战绩每天清零":" the public room — anyone can enter, the table resets daily")),console.log(chalk.gray(" ▸ ")+chalk.bold.green("room <CODE>")+chalk.gray(d?" 进朋友开的房(4 个字母)":" enter a friend's room (4-letter code)")),console.log(chalk.gray(" ▸ ")+chalk.bold.green(d?"room new [名字]":"room new [label]")+chalk.gray(d?" 开一个新房,把房号发给朋友":" open a new room and hand out its code")),"back"}let F=null;if(A){const o=await D(t,d);if(null===o)return"back";const n=await b("create",e,{label:T,nick:o});if(!n.ok||!n.data)return console.log(chalk.yellow(" "+L(n.code,n.message))),"back";F=n.data,console.log(chalk.green(" ✓ ")+chalk.white(d?"房间开好了,房号 ":"Room created — the code is ")+chalk.bold.cyan(F.room)+chalk.white(d?",告诉朋友敲 room "+F.room:`. Friends type room ${F.room}`))}else{const o=await b("status",e,{room:f});if(o.ok&&o.data)F=o.data;else{if("not_member"!==o.code)return console.log(chalk.yellow(" "+L(o.code,o.message))),"back";{const o=await b("join",e,{room:f,probe:!0}),n=o.data?.label||("OPEN"===f?d?"公共房":"Open room":f);console.log(chalk.white(` ${d?"进入":"Entering"} `)+chalk.bold.cyan(f)+chalk.gray(` ${n} · ${o.data?.members??"?"} ${d?"人":"members"}`));for(let o=0;o<4&&!F;o++){const o=await D(t,d);if(null===o)return"back";const n=await b("join",e,{room:f,nick:o});n.ok&&n.data?F=n.data:console.log(chalk.yellow(" "+L(n.code,n.message)))}if(!F)return"back"}}}const V=F.room,M=chalk.bold.magenta(`icoa room ${V}> `);for(S(d,F),k(F),v(V);;){const a=await t(M);if(null===a)return"back";const i=a.trim(),u=i.toLowerCase();if($.has(u))return"back";if("menu"===u||"home"===u)return"menu";if(""===u||"who"===u||"board"===u||"table"===u||"r"===u||"refresh"===u){const o=await b("status",e,{room:V});o.ok&&o.data?k(o.data):console.log(chalk.yellow(" "+L(o.code,o.message)));continue}if("help"===u||"?"===u){const o=await b("status",e,{room:V});S(d,o.ok&&o.data?o.data:null),v(V);continue}if("log"===u||"matches"===u||"history"===u){const o=await b("matches",e,{room:V,limit:20,all:"history"===u});if(o.ok&&o.data){o.data.matches.length||console.log(chalk.gray(d?" 今天还没有对局。":" No matches today yet."));for(const e of o.data.matches)console.log(" "+_(e));o.data.matches.length&&console.log(chalk.gray(d?" replay 只播你最近一场 vs 输掉的那局;历史对局暂不能回放。":" replay plays the lost game of YOUR latest vs only; older matches cannot be replayed yet."))}else console.log(chalk.yellow(" "+L(o.code,o.message)));continue}if("replay"===u||"replay last"===u){const e=s(r(),".icoa","battleai","last.mobreplay");if(!o(e)){console.log(chalk.gray(d?" 还没有回放 —— 先 vs 打一场,判分机会把你输掉的第一局送回来(只保留最近一场,只有发起挑战的一方拿到)。":" No replay yet — challenge someone (vs): the judge sends back the first game you lost (only the latest match is kept, and only for the challenger)."));continue}await l()||console.log(chalk.yellow(d?" 回放已存在本机,但播放器需要 Python 3.12 和竞技场引擎:先 python setup,再进一次 battle(它会下载引擎),回来再敲 replay。":" The replay is saved here, but the viewer needs Python 3.12 and the arena engine: run python setup, open battle once (it fetches the engine), then replay again."));continue}if("knobs"===u||"knob"===u||"旋钮"===u||"set"===u){const e=E();null===e?S(d,null):N(e,d);continue}const y=/^(?:set|改)\s+(.+)$/i.exec(i);if(y){const e=E();null===e?S(d,null):U(e,y[1],d);continue}const h=/^(?:template|模板)(?:\s+(knobs?|code|新手|高手))?(?:\s+(force|覆盖))?$/i.exec(i);if(h){const e=/^(?:code|高手)$/i.test(h[1]??"")?"code":"knobs";await B(e,!!h[2],d);continue}if("edit"===u||"vi"===u||"编辑"===u){if(!o(DEFAULT_PVP_BOT)){S(d,null);continue}console.log(chalk.gray(d?" vi 三键: i 进入编辑 · Esc 退出编辑 · :wq 存盘离开 · :q! 放弃":" vi survival: i = edit · Esc = stop editing · :wq = save and leave · :q! = discard")),await g("vi",[...m,DEFAULT_PVP_BOT]);continue}if("run"===u||"test"===u||"play"===u||"试打"===u){if(!o(DEFAULT_PVP_BOT)){S(d,null);continue}if(!p().ok){console.log(chalk.gray(d?" 本机没有 Python 3.12,先 python setup;没有它也能 up / vs(判分在服务器)。":" No Python 3.12 here — python setup first; up / vs still work without it (the judge is server-side)."));continue}console.log(chalk.gray(d?" 本地打一场(对卡 12 的陪练),看看 bot 跑不跑得起来…":" One local game (the card-12 sparring bot) to see that the bot runs…")),await c(DEFAULT_PVP_BOT,12,0,void 0,g);continue}const f=/^(?:up|upload|submit|上传)(?:\s+(.+))?$/i.exec(i);if(f){const t=(f[1]??"").trim()||DEFAULT_PVP_BOT;if(!o(t)){console.log(chalk.yellow(` ${d?"找不到":"Not found"}: ${t}`)),S(d,null);continue}let a="";try{a=n(t,"utf8")}catch{console.log(chalk.yellow(` ${d?"读不了":"Could not read"}: ${t}`));continue}const r=await j(a),s=await b("upload",e,{room:V,code:a,group:r},3e4);if(!s.ok||!s.data){console.log(chalk.yellow(" "+L(s.code,s.message)));continue}s.data.unchanged?console.log(chalk.gray(d?` 和 v${s.data.version} 一字不差,没有新版本。`:` Identical to v${s.data.version} — nothing new.`)):console.log(chalk.green(" ✓ ")+chalk.white(d?`已上传为 v${s.data.version}`:`Uploaded as v${s.data.version}`)+chalk.gray(` (${t})`)+(r?chalk.gray("rookie"===r?d?" · 🔧 新手组(只动了旋钮)":" · 🔧 knobs only (rookie)":d?" · ✍ 高手组(改了代码)":" · ✍ custom code (pro)"):""));continue}const w=/^(?:vs|challenge|fight|挑战|打)\s+(\S+)(?:\s+v?(\d+))?$/i.exec(i);if(w){const o=w[2]?Number(w[2]):void 0;console.log(chalk.gray(d?" ⚖ 判分机在打 6 局(3 张图 × 换席位),通常 5–30 秒,最多 3 分钟…":" ⚖ The judge is playing 6 games (3 maps × both seats) — usually 5–30 s, up to 3 min…"));const t=await b("challenge",e,{room:V,nick:w[1],version:o},2e5);if(!t.ok||!t.data){console.log(chalk.yellow(" "+L(t.code,t.message)));continue}P(t.data),k(t.data.status);continue}/^(?:vs|challenge|挑战|打)$/i.test(i)?console.log(chalk.gray(d?" 用法: vs <昵称> 或 vs <昵称> v2(打对方的第 2 版)":" Usage: vs <nick> or vs <nick> v2 (their version 2)")):(console.log(chalk.gray(d?` 不认识「${i}」。`:` Unknown: "${i}".`)),v(V))}}async function D(e,o){for(let t=0;t<4;t++){const t=await e(chalk.bold.cyan(o?" 你的昵称(2–12 个字母/数字,别用真名;back 取消): ":" Your nickname (2–12 letters/digits, not your real name; back to cancel): "));if(null===t||$.has(t.toLowerCase()))return null;if(/^[A-Za-z0-9_\-一-鿿]{2,12}$/.test(t))return t;console.log(chalk.gray(o?" 2–12 个字母、数字、_ 或 -,不能有空格。":" 2–12 letters, digits, _ or -; no spaces."))}return null}function L(e,o){const t=h();switch(e){case"wrong_course":return t?"PvP 房间只对 BattleAI(battleai-48)token 开放。":"PvP rooms are for BattleAI (battleai-48) tokens.";case"demo_not_supported":return t?"demo token 进不了 PvP 房间。":"Demo tokens cannot enter PvP rooms.";case"no_room":return t?"没有这个房号 —— 检查一下 4 个字母。":"No room with that code — check the four letters.";case"nick_taken":return t?"这个昵称在房里已经有人用了,换一个。":"That nickname is taken in this room — pick another.";case"bad_nick":return t?"昵称要 2–12 个字母/数字,不能有空格。":"Nickname: 2–12 letters/digits, no spaces.";case"no_bot":return t?"先 up 上传你的 bot。":"Upload your bot first: up";case"opp_no_bot":return o||(t?"对方还没上传 bot。":"They have no bot yet.");case"no_such_nick":return t?"房里没有这个昵称 —— 回车看名单。":"Nobody by that name here — press Enter for the table.";case"self":return t?"不能挑战自己。":"You cannot challenge yourself.";case"cooldown":return t?"每 10 秒只能发起一场。":"One challenge every 10 seconds.";case"no_act":return t?"这个文件里没有 act(u) —— 确定是你的 bot.py?":"That file has no act(u) — is it your bot.py?";case"judge_unreachable":case"judge_timeout":case"judge_rejected":case"judge_not_configured":return t?"判分机暂时不可用,一分钟后再试。":"The judge is unavailable right now — try again in a minute.";default:return o||(t?"连不上服务器。":"Could not reach the server.")}}
|
|
1
|
+
import{copyFileSync as e,existsSync as o,mkdirSync as t,readFileSync as a,writeFileSync as n}from"node:fs";import{homedir as r}from"node:os";import{join as s}from"node:path";import chalk from"chalk";import{playSavedReplay as l,resolveEngineDir as i,runMobLocal as c,saveJudgeReplay as g}from"../commands/battle.js";import{getConfig as d}from"./config.js";import{runInteractive as u}from"./interactive-spawn.js";import{learnPost as y}from"./learn-contest.js";import{probePython as p}from"./python-setup.js";import{ARENA_VI_FLAGS as m}from"./vi-flags.js";export const PVP_RE=/^\/?(?:room|rooms|pvp|房间|对战房)(?:\s+(.*))?$/i;const b=(e,o,t={},a=2e4)=>y(`/api/icoa/learn/pvp/${e}`,{token:o.toUpperCase(),...t},a),h=()=>(d().language||"en").toLowerCase().startsWith("zh"),$=new Set(["back","b","quit","q","exit","card","lesson","卡片"]);export const DEFAULT_PVP_BOT=s(r(),".icoa","battleai","bot.py");const w=e=>{let o=0;for(const t of e){const e=t.codePointAt(0)??0;o+=e>65535||e>=11904&&e<=42191||e>=44032&&e<=55203||e>=63744&&e<=64255?2:1}return o},f=(e,o)=>e+" ".repeat(Math.max(0,o-w(e)));function v(e){console.log(chalk.gray(h()?` knobs · set <旋钮> <值> · run 试打 · up 上传 · vs <昵称> 挑战 · 回车 战绩表 · log · replay 3 看第 3 局 · edit · template · card 回卡片 (${e})`:` knobs · set <KNOB> <value> · run (local test) · up (upload) · vs <nick> (challenge) · Enter (table) · log · replay 3 (game 3) · edit · template · card (${e})`))}function k(e){const o=h(),t=e.public?o?"公共房":"Open room":e.label||(o?"房间":"Room");console.log(),console.log(chalk.bold.white(` ⚔ ${t} `)+chalk.bold.cyan(e.room)+chalk.gray(` · ${o?"你是":"you are"} ${e.nick??"?"} · ${e.members.length} ${o?"人":"members"} · ${o?"今日":"today"} ${e.day}${e.public?o?" · 战绩每天清零":" · table resets daily":""}`)),console.log(chalk.gray(` ${f(o?"昵称":"nick",14)}${f("bot",6)}${f(o?"组":"group",9)}${f(o?"今日 胜-负-平":"today W-L-D",16)}${o?"累计":"all-time"}`));for(const t of e.members){const e="rookie"===t.group?o?"🔧 新手":"🔧 knobs":"pro"===t.group?o?"✍ 高手":"✍ code":"—",a=` ${f(t.nick,14)}${f(t.versions?`v${t.versions}`:"—",6)}${f(e,9)}${f(`${t.today.w}-${t.today.l}-${t.today.d}`,16)}${t.all.w}-${t.all.l}-${t.all.d}`;console.log(t.me?chalk.bold.cyan(a):chalk.white(a))}if(e.my_versions.length||console.log(chalk.yellow(o?" 你还没上传 bot —— 先 knobs / set,再 up。":" You have no bot here yet — knobs / set, then up.")),e.recent.length){console.log(chalk.gray(o?" 今日对局:":" Today:"));for(const o of e.recent.slice(0,6))console.log(chalk.gray(" ")+_(o))}}function _(e){const o="WIN"===e.verdict?chalk.green(`${e.wins_a}-${e.wins_b}`):"LOSS"===e.verdict?chalk.red(`${e.wins_a}-${e.wins_b}`):chalk.yellow(`${e.wins_a}-${e.wins_b}`),t=`${e.a} v${e.a_version??"?"} vs ${e.b} v${e.b_version??"?"}`,a=chalk.gray(`#${String(e.id).padEnd(4)}`),n=e.replays?chalk.gray(" 🎬"):"";return`${a}${chalk.gray((e=>{const o=new Date(e);return Number.isNaN(o.getTime())?"":`${String(o.getHours()).padStart(2,"0")}:${String(o.getMinutes()).padStart(2,"0")}`})(e.ts))} ${e.mine?chalk.white(t):chalk.gray(t)} ${o}${n}${e.draws?chalk.gray(` (${e.draws} draw)`):""}${"WIN"!==e.verdict&&"LOSS"!==e.verdict&&"DRAW"!==e.verdict?chalk.red(` ${e.verdict}`):""}`}function P(e){const o=h();console.log();const t=`${e.wins_a}-${e.wins_b}${e.draws?` (${e.draws} ${o?"平":"draw"})`:""}`;if("WIN"===e.verdict)console.log(chalk.bold.green(` 🏆 ${o?"你赢了":"You win"} ${t}`)+chalk.gray(` vs ${e.opp.nick} v${e.opp.version}`));else if("LOSS"===e.verdict)console.log(chalk.bold.red(` ❌ ${o?"你输了":"You lose"} ${t}`)+chalk.gray(` vs ${e.opp.nick} v${e.opp.version}`));else{if("DRAW"!==e.verdict)return void console.log(chalk.yellow(` ⚠ ${o?"判分机没跑完这场":"The judge could not finish this match"}: ${e.verdict}${e.note?` — ${e.note}`:""}`));console.log(chalk.bold.yellow(` 🤝 ${o?"平局":"Draw"} ${t}`)+chalk.gray(` vs ${e.opp.nick} v${e.opp.version}`))}e.load_error?.a&&console.log(chalk.red(` ${o?"你的 bot 装不上":"Your bot failed to load"}: ${e.load_error.a}`)),e.load_error?.b&&console.log(chalk.gray(` ${o?"对方的 bot 装不上":"Opponent's bot failed to load"}: ${e.load_error.b}`)),console.log(chalk.gray(` ${f("#",4)}${f(o?"地图":"map",11)}${f(o?"席位":"seat",6)}${f(o?"结果":"result",8)}${f(o?"原因":"how",10)}${f(o?"回合":"turns",7)}${o?"损失 你/对方":"lost you/them"}`));for(const t of e.games){const a="a"===t.winner?o?"胜":"win":"b"===t.winner?o?"负":"loss":o?"平":"draw",n="a"===t.winner?chalk.green(f(a,8)):"b"===t.winner?chalk.red(f(a,8)):chalk.yellow(f(a,8)),r="warrens"===t.reason?o?"拆家":"warrens":"timeout"===t.reason?o?"超时":"timeout":"crash"===t.reason?o?"崩溃":"crash":"load_error"===t.reason?o?"装不上":"no load":o?"回合满":"turn cap";console.log(` ${f(String(t.game??e.games.indexOf(t)+1),4)}${f(t.map,11)}${f(String(t.seat_a),6)}${n}${f(r,10)}${f(String(t.turns),7)}${Math.round(t.lost[0])}/${Math.round(t.lost[1])}${t.error?chalk.gray(` ${t.error}`):""}`)}console.log(chalk.gray(` ${o?"种子":"seeds"} ${e.seeds.join(",")} · ${o?"回合满时比打掉的价值,躲家里不算赢":"at the turn cap the side that destroyed more value wins — hiding at home does not"}`));const a=e.games.findIndex(e=>"b"===e.winner)+1;e.replay&&g(e.replay),e.replays?console.log(chalk.gray(" ▸ ")+chalk.bold.green("replay 3")+chalk.gray(o?" 看第 3 局(1–6 任选)· ":" watch game 3 (any of 1–6) · ")+chalk.bold.green("replay")+chalk.gray(a?o?` 看你输掉的第 ${a} 局 · `:` the game you lost (#${a}) · `:o?" 看第 1 局 · ":" game 1 · ")+chalk.bold.green(`replay ${e.match_id} 3`)+chalk.gray(o?" 以后从 log 里回看这场":" this match later, from log")):e.replay&&console.log(chalk.gray(" ▸ ")+chalk.bold.green("replay")+chalk.gray(e.wins_b?o?" 看你输掉的第一局":" watch the first game you lost":o?" 看第一局":" watch game 1"))}const T=e=>{const o=e.search(/^# ── KNOBS/m);let t=e;if(o>=0){const a=e.slice(o).split("\n").slice(1),n=a.findIndex(e=>/^# ───/.test(e));t=(n>=0?a.slice(0,n):a).join("\n")}return[...new Set([...t.matchAll(/^([A-Z][A-Z0-9_]*)\s*=\s*[^#\n]+/gm)].map(e=>e[1]))]},A=(e,o)=>{const t=new RegExp(`^${o}\\s*=\\s*([^#\n]*?)\\s*(?:#.*)?$`,"m").exec(e);return t?t[1]:null},E=(e,o)=>{const t=new RegExp(`^${o}\\s*=[^#\n]*#\\s*(.*)$`,"m").exec(e);return t?t[1].trim():""},O=()=>{try{return a(DEFAULT_PVP_BOT,"utf8")}catch{return null}},x=e=>e.split("\n").map(e=>e.replace(/\s+#.*$/,"").trimEnd()).filter(e=>e.trim()&&!e.trim().startsWith("#")&&!/^[A-Z][A-Z0-9_]*\s*=/.test(e));async function N(e){const o=await i();if(!o)return null;try{const t=a(s(o,"bots","knob_bot.py"),"utf8");return x(t).join("\n")===x(e).join("\n")?"rookie":"pro"}catch{return null}}async function B(a,n,l){const c=await i();if(!c)return console.log(chalk.yellow(l?" 引擎还没下载到本机(需要联网一次)。":" The arena engine is not on this machine yet (one online fetch needed).")),!1;const g=s(c,"bots","knobs"===a?"knob_bot.py":l?"template_bot_zh.py":"template_bot.py");if(!o(g))return console.log(chalk.yellow(` ${l?"引擎里没有模板":"Template missing in the engine"}: ${g}`)),!1;if(t(s(r(),".icoa","battleai"),{recursive:!0}),o(DEFAULT_PVP_BOT)){if(!n)return console.log(chalk.yellow(l?" bot.py 已经存在,不覆盖。":" bot.py already exists — not overwritten.")+chalk.gray(l?" 确定要换: ":" To replace it: ")+chalk.bold.green(`template ${a} force`)+chalk.gray(l?"(旧文件留作 bot.py.bak)":" (the old file is kept as bot.py.bak)")),!1;e(DEFAULT_PVP_BOT,`${DEFAULT_PVP_BOT}.bak`)}return e(g,DEFAULT_PVP_BOT),console.log(chalk.green(" ✓ ")+chalk.white("knobs"===a?l?"15 个旋钮的新手模板已放到 bot.py":"The 15-knob starter is now your bot.py":l?"空白代码模板已放到 bot.py(高手组)":"The blank code template is now your bot.py (pro)")+chalk.gray(` (${DEFAULT_PVP_BOT})`)),!0}function S(e,o){const t=T(e);if(t.length){console.log(chalk.gray(o?" 你的旋钮(set <旋钮> <值> 改一个):":" Your knobs (set <KNOB> <value> changes one):"));for(const o of t)console.log(` ${chalk.bold.white(o.padEnd(10))}${chalk.green((A(e,o)??"").padEnd(12))}${chalk.gray(E(e,o))}`)}else console.log(chalk.gray(o?" 你的 bot.py 没有旋钮(顶部那种 NAME = value 行)—— 它是高手组的自定义代码。要旋钮版敲 template knobs。":" Your bot.py has no knobs (the NAME = value lines at the top) — it is custom code (pro). For the knob starter type template knobs."))}function j(e,o,t){const a=T(e),r=/^([A-Za-z][A-Za-z0-9_]*)\s*(?:=|\s)\s*(.+)$/.exec(o.trim());if(!r)return console.log(chalk.yellow(t?" 用法: ":" Usage: ")+chalk.bold.green(t?"set <旋钮> <值>":"set <KNOB> <value>")+chalk.gray(t?" 例如 set WOMBATS 6":" e.g. set WOMBATS 6")),S(e,t),!1;const s=r[1].toUpperCase(),l=r[2].trim();if(!a.includes(s))return console.log(chalk.yellow(t?` 没有 ${s} 这个旋钮。`:` No knob called ${s}.`)+chalk.gray(" ")+a.map(e=>chalk.bold.white(e)).join(chalk.gray(" · "))),!1;const i=A(e,s)??"";if(i.trim()===l)return console.log(chalk.gray(" ")+chalk.bold.white(s)+chalk.gray(t?` 已经是 ${l} 了。`:` is already ${l}.`)),!1;const c=new RegExp(`^(${s}\\s*=\\s*)([^#\n]*?)(\\s*)(#.*)?$`,"m"),g=e.replace(c,(e,o,t,a,n)=>`${o}${l}${n?(a||" ")+n:""}`);return n(DEFAULT_PVP_BOT,g),console.log(chalk.gray(" ")+chalk.bold.white(s)+chalk.gray(" ")+chalk.gray(i.trim())+chalk.gray(" → ")+chalk.bold.green(l)+chalk.gray(t?" 下一步: run 试打,up 上传":" next: run to test, up to upload")),!0}function L(e,t){const a=o(DEFAULT_PVP_BOT),n=!!t&&t.my_versions.length>0,r=!!t&&t.recent.some(e=>e.mine),s=(e,o,t,a,n=!1)=>` ${chalk.gray(e)} ${((e,o=!1)=>e?chalk.green("✅"):o?chalk.gray("○ "):chalk.white("⬜"))(o,n)} ${chalk.bold.green(f(t,13))}${chalk.gray(a)}`,l=new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`,"g"),i=e=>`${e}${" ".repeat(Math.max(0,70-w(e.replace(l,""))))}`,c=e?[s("1",a,"template",a?"bot.py 已就绪(~/.icoa/battleai/bot.py)":"拿 15 个旋钮的新手模板(高手: template code)"),s("2",!1,"knobs · set","看旋钮;set WOMBATS 6 改一个,不用写代码",!0),` ${chalk.gray("✍ 高手:")} ${chalk.bold.green("edit")} ${chalk.gray("打开文件,旋钮下面的 act(u) 随便重写")}`,s("3",!1,"run","本地试打一场,看 bot 跑不跑得起来(可跳过)",!0),s("4",n,"up",n?`已上传到 v${t?.my_versions.length}(改完再 up = 新版本)`:"上传 bot → v1(自动标 🔧 新手 / ✍ 高手)"),s("5",r,"vs <昵称>",r?"今天打过了 —— 再来一场":"挑战一个人:6 局定胜负"),s("6",!1,"replay 3","看第 3 局(1–6);replay <场号> <局> 回看 log 里任何一场",!0),s("7",!1,"回车","今日战绩表 · log 对局 · card 回卡片",!0)]:[s("1",a,"template",a?"bot.py is ready (~/.icoa/battleai/bot.py)":"get the 15-knob starter (pro: template code)"),s("2",!1,"knobs · set","see the knobs; set WOMBATS 6 changes one — no code",!0),` ${chalk.gray("✍ pro:")} ${chalk.bold.green("edit")} ${chalk.gray("opens the file — rewrite act(u) below the knobs")}`,s("3",!1,"run","one local test game — does the bot run? (optional)",!0),s("4",n,"up",n?`uploaded, now v${t?.my_versions.length} (edit + up again = new version)`:"upload the bot → v1 (tagged 🔧 knobs / ✍ code)"),s("5",r,"vs <nick>",r?"you played today — go again":"challenge someone: 6 games decide"),s("6",!1,"replay 3","watch game 3 (1–6); replay <match#> <game> for any match in log",!0),s("7",!1,"Enter","today's table · log matches · card back to the card",!0)],g=e?` 怎么玩 · ${t?.room??"room"} `:` How to play · ${t?.room??"room"} `;console.log(),console.log(chalk.cyan(` ╭─${chalk.bold.white(g)}${"─".repeat(Math.max(0,70-w(g)-1))}╮`));for(const e of c)console.log(`${chalk.cyan(" │")}${i(e)}${chalk.cyan("│")}`);console.log(chalk.cyan(` ╰${"─".repeat(70)}╯`));const d=a?n?r?"":e?"vs <昵称>":"vs <nick>":"up":"template";d&&console.log(chalk.gray(e?" ▸ 下一步敲: ":" ▸ next: ")+chalk.bold.green(d))}export async function runPvpRoom(e,t,n,i={}){const d=i.interactive??u,y=h(),w=n.trim();let T="",A="",E=!1;const x=/^(?:new|create|开房|建)(?:\s+(.*))?$/i.exec(w),F=/^(?:join|enter|进)\s+(\S+)$/i.exec(w);if(x?(E=!0,A=(x[1]??"").trim().slice(0,40)):F?T=F[1]:/^(?:open|public|公共)$/i.test(w)?T="OPEN":w&&(T=w),T=T.toUpperCase().replace(/[^A-Z0-9]/g,"").slice(0,8),!E&&!T){const o=await b("status",e);if(!o.ok||!o.data)return console.log(chalk.yellow(" "+D(o.code,o.message))),"back";if(console.log(),console.log(chalk.bold.white(y?" ⚔ PvP 房间":" ⚔ PvP rooms")+chalk.gray(y?" —— 和真人的 bot 对打,6 局定胜负。":" — your bot against real people, six games a match.")),o.data.rooms.length)for(const e of o.data.rooms)console.log(` ${chalk.bold.cyan(f(e.room,6))} ${chalk.white(f(e.public?y?"公共房":"open room":e.label||"",18))} ${chalk.gray(`${e.members} ${y?"人":"members"} · ${y?"你是":"you are"} ${e.nick}`)}`);else console.log(chalk.gray(y?" 你还没进过任何房间。":" You are not in any room yet."));return console.log(chalk.gray(" ▸ ")+chalk.bold.green("room open")+chalk.gray(y?" 公共房,谁都能进,战绩每天清零":" the public room — anyone can enter, the table resets daily")),console.log(chalk.gray(" ▸ ")+chalk.bold.green("room <CODE>")+chalk.gray(y?" 进朋友开的房(4 个字母)":" enter a friend's room (4-letter code)")),console.log(chalk.gray(" ▸ ")+chalk.bold.green(y?"room new [名字]":"room new [label]")+chalk.gray(y?" 开一个新房,把房号发给朋友":" open a new room and hand out its code")),"back"}let V=null;if(E){const o=await U(t,y);if(null===o)return"back";const a=await b("create",e,{label:A,nick:o});if(!a.ok||!a.data)return console.log(chalk.yellow(" "+D(a.code,a.message))),"back";V=a.data,console.log(chalk.green(" ✓ ")+chalk.white(y?"房间开好了,房号 ":"Room created — the code is ")+chalk.bold.cyan(V.room)+chalk.white(y?",告诉朋友敲 room "+V.room:`. Friends type room ${V.room}`))}else{const o=await b("status",e,{room:T});if(o.ok&&o.data)V=o.data;else{if("not_member"!==o.code)return console.log(chalk.yellow(" "+D(o.code,o.message))),"back";{const o=await b("join",e,{room:T,probe:!0}),a=o.data?.label||("OPEN"===T?y?"公共房":"Open room":T);console.log(chalk.white(` ${y?"进入":"Entering"} `)+chalk.bold.cyan(T)+chalk.gray(` ${a} · ${o.data?.members??"?"} ${y?"人":"members"}`));for(let o=0;o<4&&!V;o++){const o=await U(t,y);if(null===o)return"back";const a=await b("join",e,{room:T,nick:o});a.ok&&a.data?V=a.data:console.log(chalk.yellow(" "+D(a.code,a.message)))}if(!V)return"back"}}}const C=V.room,M=chalk.bold.magenta(`icoa room ${C}> `);let W=V.recent.find(e=>e.mine&&e.replays)?.id??0;for(L(y,V),k(V),v(C);;){const n=await t(M);if(null===n)return"back";const i=n.trim(),u=i.toLowerCase();if($.has(u))return"back";if("menu"===u||"home"===u)return"menu";if(""===u||"who"===u||"board"===u||"table"===u||"r"===u||"refresh"===u){const o=await b("status",e,{room:C});o.ok&&o.data?k(o.data):console.log(chalk.yellow(" "+D(o.code,o.message)));continue}if("help"===u||"?"===u){const o=await b("status",e,{room:C});L(y,o.ok&&o.data?o.data:null),v(C);continue}if("log"===u||"matches"===u||"history"===u){const o=await b("matches",e,{room:C,limit:20,all:"history"===u});if(o.ok&&o.data){o.data.matches.length||console.log(chalk.gray(y?" 今天还没有对局。":" No matches today yet."));for(const e of o.data.matches)console.log(" "+_(e));o.data.matches.length&&console.log(chalk.gray(y?" 🎬 = 有回放:replay <场号> <局 1–6>,例如 replay 12 3;没有 🎬 的是旧对局,回放没存。":" 🎬 = replays kept: replay <match#> <game 1–6>, e.g. replay 12 3; matches without 🎬 were played before replays were stored."))}else console.log(chalk.yellow(" "+D(o.code,o.message)));continue}const h=/^(?:replay|回放)(?:\s+(last|\d+))?(?:\s+(\d+))?$/i.exec(i);if(h){const t=s(r(),".icoa","battleai","last.mobreplay"),a=()=>console.log(chalk.yellow(y?" 回放已存在本机,但播放器需要 Python 3.12 和竞技场引擎:先 python setup,再进一次 battle(它会下载引擎),回来再敲 replay。":" The replay is saved here, but the viewer needs Python 3.12 and the arena engine: run python setup, open battle once (it fetches the engine), then replay again."));let n=0,i=0;const c=h[1]&&"last"!==h[1].toLowerCase()?Number(h[1]):0,d=h[2]?Number(h[2]):0;if(c&&d)n=c,i=d;else if(c>=1&&c<=6){if(n=W,i=c,!n){console.log(chalk.gray(y?" 还没有可回看的对局 —— 先 vs 打一场,或 replay <场号> <局> 指定 log 里带 🎬 的一场。":" No match to replay yet — play one (vs), or replay <match#> <game> for a 🎬 match from log."));continue}}else c>6&&(n=c,i=1);if(n&&i){const o=await b("replay",e,{room:C,match_id:n,game:i},3e4);if(!o.ok||!o.data){console.log(chalk.yellow(" "+D(o.code,o.message)));continue}const t="a"===o.data.winner?o.data.a:"b"===o.data.winner?o.data.b:y?"平局":"draw";if(console.log(chalk.gray(` #${n} ${y?"第":"game"} ${i}${y?" 局":""} · ${o.data.map} · ${o.data.a} vs ${o.data.b} · ${y?"胜者":"winner"}: ${t}`)),!g(o.data.replay)){console.log(chalk.yellow(y?" 回放存不下来。":" Could not save the replay."));continue}await l()||a();continue}if(!o(t)){console.log(chalk.gray(y?" 还没有回放 —— 先 vs 打一场;打完 replay 3 看第 3 局,replay 看你输的那局。":" No replay yet — play a match (vs); then replay 3 shows game 3 and replay shows the game you lost."));continue}await l()||a();continue}if("knobs"===u||"knob"===u||"旋钮"===u||"set"===u){const e=O();null===e?L(y,null):S(e,y);continue}const w=/^(?:set|改)\s+(.+)$/i.exec(i);if(w){const e=O();null===e?L(y,null):j(e,w[1],y);continue}const f=/^(?:template|模板)(?:\s+(knobs?|code|新手|高手))?(?:\s+(force|覆盖))?$/i.exec(i);if(f){const e=/^(?:code|高手)$/i.test(f[1]??"")?"code":"knobs";await B(e,!!f[2],y);continue}if("edit"===u||"vi"===u||"编辑"===u){if(!o(DEFAULT_PVP_BOT)){L(y,null);continue}console.log(chalk.gray(y?" vi 三键: i 进入编辑 · Esc 退出编辑 · :wq 存盘离开 · :q! 放弃":" vi survival: i = edit · Esc = stop editing · :wq = save and leave · :q! = discard")),await d("vi",[...m,DEFAULT_PVP_BOT]);continue}if("run"===u||"test"===u||"play"===u||"试打"===u){if(!o(DEFAULT_PVP_BOT)){L(y,null);continue}if(!p().ok){console.log(chalk.gray(y?" 本机没有 Python 3.12,先 python setup;没有它也能 up / vs(判分在服务器)。":" No Python 3.12 here — python setup first; up / vs still work without it (the judge is server-side)."));continue}console.log(chalk.gray(y?" 本地打一场(对卡 12 的陪练),看看 bot 跑不跑得起来…":" One local game (the card-12 sparring bot) to see that the bot runs…")),await c(DEFAULT_PVP_BOT,12,0,void 0,d);continue}const T=/^(?:up|upload|submit|上传)(?:\s+(.+))?$/i.exec(i);if(T){const t=(T[1]??"").trim()||DEFAULT_PVP_BOT;if(!o(t)){console.log(chalk.yellow(` ${y?"找不到":"Not found"}: ${t}`)),L(y,null);continue}let n="";try{n=a(t,"utf8")}catch{console.log(chalk.yellow(` ${y?"读不了":"Could not read"}: ${t}`));continue}const r=await N(n),s=await b("upload",e,{room:C,code:n,group:r},3e4);if(!s.ok||!s.data){console.log(chalk.yellow(" "+D(s.code,s.message)));continue}s.data.unchanged?console.log(chalk.gray(y?` 和 v${s.data.version} 一字不差,没有新版本。`:` Identical to v${s.data.version} — nothing new.`)):console.log(chalk.green(" ✓ ")+chalk.white(y?`已上传为 v${s.data.version}`:`Uploaded as v${s.data.version}`)+chalk.gray(` (${t})`)+(r?chalk.gray("rookie"===r?y?" · 🔧 新手组(只动了旋钮)":" · 🔧 knobs only (rookie)":y?" · ✍ 高手组(改了代码)":" · ✍ custom code (pro)"):""));continue}const A=/^(?:vs|challenge|fight|挑战|打)\s+(\S+)(?:\s+v?(\d+))?$/i.exec(i);if(A){const o=A[2]?Number(A[2]):void 0;console.log(chalk.gray(y?" ⚖ 判分机在打 6 局(3 张图 × 换席位),通常 5–30 秒,最多 3 分钟…":" ⚖ The judge is playing 6 games (3 maps × both seats) — usually 5–30 s, up to 3 min…"));const t=await b("challenge",e,{room:C,nick:A[1],version:o},2e5);if(!t.ok||!t.data){console.log(chalk.yellow(" "+D(t.code,t.message)));continue}t.data.replays&&(W=t.data.match_id),P(t.data),k(t.data.status);continue}/^(?:vs|challenge|挑战|打)$/i.test(i)?console.log(chalk.gray(y?" 用法: vs <昵称> 或 vs <昵称> v2(打对方的第 2 版)":" Usage: vs <nick> or vs <nick> v2 (their version 2)")):(console.log(chalk.gray(y?` 不认识「${i}」。`:` Unknown: "${i}".`)),v(C))}}async function U(e,o){for(let t=0;t<4;t++){const t=await e(chalk.bold.cyan(o?" 你的昵称(2–12 个字母/数字,别用真名;back 取消): ":" Your nickname (2–12 letters/digits, not your real name; back to cancel): "));if(null===t||$.has(t.toLowerCase()))return null;if(/^[A-Za-z0-9_\-一-鿿]{2,12}$/.test(t))return t;console.log(chalk.gray(o?" 2–12 个字母、数字、_ 或 -,不能有空格。":" 2–12 letters, digits, _ or -; no spaces."))}return null}function D(e,o){const t=h();switch(e){case"wrong_course":return t?"PvP 房间只对 BattleAI(battleai-48)token 开放。":"PvP rooms are for BattleAI (battleai-48) tokens.";case"demo_not_supported":return t?"demo token 进不了 PvP 房间。":"Demo tokens cannot enter PvP rooms.";case"no_room":return t?"没有这个房号 —— 检查一下 4 个字母。":"No room with that code — check the four letters.";case"nick_taken":return t?"这个昵称在房里已经有人用了,换一个。":"That nickname is taken in this room — pick another.";case"bad_nick":return t?"昵称要 2–12 个字母/数字,不能有空格。":"Nickname: 2–12 letters/digits, no spaces.";case"no_bot":return t?"先 up 上传你的 bot。":"Upload your bot first: up";case"opp_no_bot":return o||(t?"对方还没上传 bot。":"They have no bot yet.");case"no_such_nick":return t?"房里没有这个昵称 —— 回车看名单。":"Nobody by that name here — press Enter for the table.";case"self":return t?"不能挑战自己。":"You cannot challenge yourself.";case"cooldown":return t?"每 10 秒只能发起一场。":"One challenge every 10 seconds.";case"no_match":return t?"这个房间里没有这个场号 —— log 看场号(#)。":"No match with that number in this room — see log for the #.";case"no_replay":return t?"这一场没有存回放(是回放功能上线前打的)。":"No replay was kept for that match (played before replays were stored).";case"bad_game":return t?"局号是 1 到 6。":"The game number is 1 to 6.";case"no_act":return t?"这个文件里没有 act(u) —— 确定是你的 bot.py?":"That file has no act(u) — is it your bot.py?";case"judge_unreachable":case"judge_timeout":case"judge_rejected":case"judge_not_configured":return t?"判分机暂时不可用,一分钟后再试。":"The judge is unavailable right now — try again in a minute.";default:return o||(t?"连不上服务器。":"Could not reach the server.")}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "icoa-cli",
|
|
3
|
-
"version": "2.19.
|
|
3
|
+
"version": "2.19.640",
|
|
4
4
|
"description": "ICOA CLI — AI-native competition runtime of the International Cyber Olympiad in AI: learn, practise, and compete in AI & cybersecurity from the terminal (AI4CTF · CTF4AI · CTF4EAI)",
|
|
5
5
|
"author": "Charlie Zhu",
|
|
6
6
|
"type": "module",
|