icoa-cli 2.19.642 → 2.19.643

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{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 p}from"./interactive-spawn.js";import{learnPost as y}from"./learn-contest.js";import{probePython as u}from"./python-setup.js";import{ARENA_VI_FLAGS as m}from"./vi-flags.js";export const PVP_RE=/^\/?(?:room|rooms|pvp|房间|对战房)(?:\s+(.*))?$/i;const $=(e,o,t={},a=2e4)=>y(`/api/icoa/learn/pvp/${e}`,{token:o.toUpperCase(),...t},a),h=()=>(d().language||"en").toLowerCase().startsWith("zh"),b=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 · guide 说明 · card 回卡片 (${e})`:` knobs · set <KNOB> <value> · run (local test) · up (upload) · vs <nick> (challenge) · Enter (table) · log · replay 3 (game 3) · edit · template · guide · 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]))]},E=(e,o)=>{const t=new RegExp(`^${o}\\s*=\\s*([^#\n]*?)\\s*(?:#.*)?$`,"m").exec(e);return t?t[1]:null},A=(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}},B=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=a(s(o,"bots","knob_bot.py"),"utf8");return B(t).join("\n")===B(e).join("\n")?"rookie":"pro"}catch{return null}}async function x(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 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((E(e,o)??"").padEnd(12))}${chalk.gray(A(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 S(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")),N(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=E(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))}function D(e,o){const t=e=>chalk.bold.green(e),a=e=>chalk.gray(e),n=e=>chalk.bold.white(e),r=e?[n(` BattleAI PvP 房间 · 怎么玩(房号 ${o})`),"",n(" 进房 / 开房"),` ${t("room new 名字")} ${a("开一个房 → 起昵称 → 得到 4 个字母的房号,发给朋友")}`,` ${t("room KZE6")} ${a("进朋友的房(昵称只问一次)")}`,` ${t("room open")} ${a("公共房,谁都能进;战绩表每天清零")}`,"",n(" 房里(提示符 icoa room XXXX>)—— 进房的面板会把做过的步骤打勾"),` ${t("template")} ${a("把 15 个旋钮的新手模板放到 ~/.icoa/battleai/bot.py(高手: template code = 空白代码模板)")}`,` ${t("knobs")} ${a("列出 15 个旋钮和含义")}`,` ${t("set WOMBATS 6")} ${a("改一个旋钮,不写代码(新手玩法)")}`,` ${t("edit")} ${a("用 vi 打开 bot.py,旋钮下面的 act(u) 随便重写(高手玩法;i 输入 · Esc · :wq 存盘)")}`,` ${t("run")} ${a("本地试打一场,只看 bot 跑不跑得起来(可跳过,需要 python setup)")}`,` ${t("up")} ${a("上传 bot → v1、v2、v3 …(每次上传 = 新版本;自动标 🔧 旋钮 / ✍ 代码)")}`,` ${t("vs <昵称>")} ${a("挑战一个人:6 局(3 张图 × 换席位),5–40 秒出结果表")}`,` ${t("vs <昵称> v1")} ${a("打对方的旧版本")}`,"",n(" 看回放 —— 两个数字:场号 + 局号"),` ${t("log")} ${a("① 列出今日对局,每行最前面的 #12 就是场号;带 🎬 的才有回放")}`,` ${t("history")} ${a(" 同上,但列出所有天")}`,` ${t("replay 12 3")} ${a("② 看第 12 场的第 3 局(每场 6 局,局号 1–6;别人打的也能看)")}`,` ${t("replay 12")} ${a(" 只写场号 = 那场的第 1 局")}`,` ${t("replay 3")} ${a(" 刚打完不用场号:最近一场的第 3 局")}`,` ${t("replay")} ${a(" 最近一场你输掉的那局")}`,a(" ③ 播放时:空格 暂停 · ←→ 单步 · ↑↓ 调速 · q 退出回房间"),a(" 例:log 里有「#387 … 2-4 🎬」→ replay 387 1 … replay 387 6 逐局看"),"",n(" 战绩"),` ${t("回车")} ${a("今日战绩表(胜-负-平 · 组 · 版本)")}`,` ${t("help")} ${a("再看步骤面板")} ${t("guide")} ${a("再看这份说明")} ${t("card")} ${a("回卡片")}`,"",a(" 规则一句话:打满 256 回合时,打掉对方价值更多的一方赢 —— 躲在家里不算赢。每 10 秒只能发起一场。"),a(" 看到「判分机暂时不可用」就等一分钟再 vs。")]:[n(` BattleAI PvP room · how to play (room ${o})`),"",n(" Open or join a room"),` ${t("room new Mock")} ${a("open a room → pick a nickname → you get a 4-letter code → give it to the others")}`,` ${t("room KZE6")} ${a("join a friend's room (nickname once)")}`,` ${t("room open")} ${a("the public room, anyone can enter; its table resets every day")}`,"",n(" Inside the room (prompt icoa room XXXX>) — the panel on entry ticks the steps you did"),` ${t("template")} ${a("puts the 15-knob starter at ~/.icoa/battleai/bot.py (pro: template code = blank code template)")}`,` ${t("knobs")} ${a("list the 15 knobs with their meaning")}`,` ${t("set WOMBATS 6")} ${a("change one knob, no coding (rookie way)")}`,` ${t("edit")} ${a("open bot.py in vi and rewrite act(u) below the knobs (pro way; i = type, Esc, :wq = save)")}`,` ${t("run")} ${a("one local test game, just to see that the bot runs (optional, needs python setup)")}`,` ${t("up")} ${a("upload your bot → v1, v2, v3 … (each upload is a new version; tagged 🔧 knobs or ✍ code)")}`,` ${t("vs <nick>")} ${a("challenge someone: 6 games (3 maps × both seats), result table in 5–40 s")}`,` ${t("vs <nick> v1")} ${a("play an older version of their bot")}`,"",n(" Watching replays — two numbers: match # + game #"),` ${t("log")} ${a("① today's matches; #12 at the line start = match number; 🎬 = replay kept")}`,` ${t("history")} ${a(" the same, for all days")}`,` ${t("replay 12 3")} ${a("② match #12, game 3 (6 games per match: 1–6; any player)")}`,` ${t("replay 12")} ${a(" match number only = game 1 of that match")}`,` ${t("replay 3")} ${a(" just played? skip the match #: game 3 of your last match")}`,` ${t("replay")} ${a(" the game you lost in your last match")}`,a(" ③ while it plays: space = pause · ← → = step · ↑ ↓ = speed · q = back to the room"),a(' e.g. log shows "#387 … 2-4 🎬" → replay 387 1 … replay 387 6'),"",n(" Table"),` ${t("Enter")} ${a("today's table (W-L-D, group, version)")}`,` ${t("help")} ${a("the step panel again")} ${t("guide")} ${a("this sheet again")} ${t("card")} ${a("back to your card")}`,"",a(" Rule in one line: at the 256-turn cap the side that destroyed more value wins — hiding at home does not. One challenge every 10 s."),a(' If you see "the judge is unavailable", wait a minute and try vs again.')];console.log();for(const e of r)console.log(e)}export async function runPvpRoom(e,t,n,i={}){const d=i.interactive??p,y=h(),w=n.trim();let T="",E="",A=!1;const B=/^(?:new|create|开房|建)(?:\s+(.*))?$/i.exec(w),V=/^(?:join|enter|进)\s+(\S+)$/i.exec(w);if(B?(A=!0,E=(B[1]??"").trim().slice(0,40)):V?T=V[1]:/^(?:open|public|公共)$/i.test(w)?T="OPEN":w&&(T=w),T=T.toUpperCase().replace(/[^A-Z0-9]/g,"").slice(0,8),!A&&!T){const o=await $("status",e);if(!o.ok||!o.data)return console.log(chalk.yellow(" "+F(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 W=null;if(A){const o=await U(t,y);if(null===o)return"back";const a=await $("create",e,{label:E,nick:o});if(!a.ok||!a.data)return console.log(chalk.yellow(" "+F(a.code,a.message))),"back";W=a.data,console.log(chalk.green(" ✓ ")+chalk.white(y?"房间开好了,房号 ":"Room created — the code is ")+chalk.bold.cyan(W.room)+chalk.white(y?",告诉朋友敲 room "+W.room:`. Friends type room ${W.room}`))}else{const o=await $("status",e,{room:T});if(o.ok&&o.data)W=o.data;else{if("not_member"!==o.code)return console.log(chalk.yellow(" "+F(o.code,o.message))),"back";{const o=await $("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&&!W;o++){const o=await U(t,y);if(null===o)return"back";const a=await $("join",e,{room:T,nick:o});a.ok&&a.data?W=a.data:console.log(chalk.yellow(" "+F(a.code,a.message)))}if(!W)return"back"}}}const M=W.room,C=chalk.bold.magenta(`icoa room ${M}> `);let I=W.recent.find(e=>e.mine&&e.replays)?.id??0;for(o(DEFAULT_PVP_BOT)||W.my_versions.length||D(y,M),L(y,W),console.log(chalk.gray(" ▸ ")+chalk.bold.green("guide")+chalk.gray(y?" 完整命令说明":" the full command sheet")),k(W),v(M);;){const n=await t(C);if(null===n)return"back";const i=n.trim(),p=i.toLowerCase();if(b.has(p))return"back";if("menu"===p||"home"===p)return"menu";if(""===p||"who"===p||"board"===p||"table"===p||"r"===p||"refresh"===p){const o=await $("status",e,{room:M});o.ok&&o.data?k(o.data):console.log(chalk.yellow(" "+F(o.code,o.message)));continue}if("guide"===p||"howto"===p||"how"===p||"说明"===p||"教程"===p){D(y,M),v(M);continue}if("help"===p||"?"===p){const o=await $("status",e,{room:M});L(y,o.ok&&o.data?o.data:null),v(M);continue}if("log"===p||"matches"===p||"history"===p){const o=await $("matches",e,{room:M,limit:20,all:"history"===p});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(" "+F(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=I,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 $("replay",e,{room:M,match_id:n,game:i},3e4);if(!o.ok||!o.data){console.log(chalk.yellow(" "+F(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"===p||"knob"===p||"旋钮"===p||"set"===p){const e=O();null===e?L(y,null):N(e,y);continue}const w=/^(?:set|改)\s+(.+)$/i.exec(i);if(w){const e=O();null===e?L(y,null):S(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 x(e,!!f[2],y);continue}if("edit"===p||"vi"===p||"编辑"===p){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"===p||"test"===p||"play"===p||"试打"===p){if(!o(DEFAULT_PVP_BOT)){L(y,null);continue}if(!u().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 j(n),s=await $("upload",e,{room:M,code:n,group:r},3e4);if(!s.ok||!s.data){console.log(chalk.yellow(" "+F(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 E=/^(?:vs|challenge|fight|挑战|打)\s+(\S+)(?:\s+v?(\d+))?$/i.exec(i);if(E){const o=E[2]?Number(E[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 $("challenge",e,{room:M,nick:E[1],version:o},2e5);if(!t.ok||!t.data){console.log(chalk.yellow(" "+F(t.code,t.message)));continue}t.data.replays&&(I=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(M))}}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||b.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 F(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.")}}
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 y}from"./interactive-spawn.js";import{learnPost as p}from"./learn-contest.js";import{probePython as u}from"./python-setup.js";import{ARENA_VI_FLAGS as m}from"./vi-flags.js";export const PVP_RE=/^\/?(?:room|rooms|pvp|房间|对战房)(?:\s+(.*))?$/i;function $(e,o,t){return 0!==e&&1!==e?null:0===e?{blue:o,red:t}:{blue:t,red:o}}const h=(e,o)=>0===e?chalk.cyan(o?" 🟦 蓝":" 🟦 blue"):chalk.red(o?" 🟥 红":" 🟥 red"),b=(e,o,t={},a=2e4)=>p(`/api/icoa/learn/pvp/${e}`,{token:o.toUpperCase(),...t},a),w=()=>(d().language||"en").toLowerCase().startsWith("zh"),f=new Set(["back","b","quit","q","exit","card","lesson","卡片"]);export const DEFAULT_PVP_BOT=s(r(),".icoa","battleai","bot.py");const v=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},k=(e,o)=>e+" ".repeat(Math.max(0,o-v(e)));function _(e){console.log(chalk.gray(w()?` knobs · set <旋钮> <值> · run 试打 · up 上传 · vs <昵称> 挑战 · 回车 战绩表 · log · replay 3 看第 3 局 · edit · template · guide 说明 · card 回卡片 (${e})`:` knobs · set <KNOB> <value> · run (local test) · up (upload) · vs <nick> (challenge) · Enter (table) · log · replay 3 (game 3) · edit · template · guide · card (${e})`))}function P(e){const o=w(),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(` ${k(o?"昵称":"nick",14)}${k("bot",6)}${k(o?"组":"group",9)}${k(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=` ${k(t.nick,14)}${k(t.versions?`v${t.versions}`:"—",6)}${k(e,9)}${k(`${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(" ")+T(o))}}function T(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 E(e){const o=w();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(` ${k("#",4)}${k(o?"地图":"map",11)}${k(o?"席位":"seat",6)}${k(o?"结果":"result",8)}${k(o?"原因":"how",10)}${k(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(k(a,8)):"b"===t.winner?chalk.red(k(a,8)):chalk.yellow(k(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(` ${k(String(t.game??e.games.indexOf(t)+1),4)}${k(t.map,11)}${k(String(t.seat_a),6)}${n}${k(r,10)}${k(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,n=e.games.find(e=>"b"===e.winner);e.replay&&g(e.replay,n?$(n.seat_a,e.me.nick,e.opp.nick):null),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 A=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]))]},O=(e,o)=>{const t=new RegExp(`^${o}\\s*=\\s*([^#\n]*?)\\s*(?:#.*)?$`,"m").exec(e);return t?t[1]:null},B=(e,o)=>{const t=new RegExp(`^${o}\\s*=[^#\n]*#\\s*(.*)$`,"m").exec(e);return t?t[1].trim():""},j=()=>{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 S(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 L(e,o){const t=A(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((O(e,o)??"").padEnd(12))}${chalk.gray(B(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 D(e,o,t){const a=A(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")),L(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=O(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 U(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(k(t,13))}${chalk.gray(a)}`,l=new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`,"g"),i=e=>`${e}${" ".repeat(Math.max(0,70-v(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-v(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))}function F(e,o){const t=e=>chalk.bold.green(e),a=e=>chalk.gray(e),n=e=>chalk.bold.white(e),r=e?[n(` BattleAI PvP 房间 · 怎么玩(房号 ${o})`),"",n(" 进房 / 开房"),` ${t("room new 名字")} ${a("开一个房 → 起昵称 → 得到 4 个字母的房号,发给朋友")}`,` ${t("room KZE6")} ${a("进朋友的房(昵称只问一次)")}`,` ${t("room open")} ${a("公共房,谁都能进;战绩表每天清零")}`,"",n(" 房里(提示符 icoa room XXXX>)—— 进房的面板会把做过的步骤打勾"),` ${t("template")} ${a("把 15 个旋钮的新手模板放到 ~/.icoa/battleai/bot.py(高手: template code = 空白代码模板)")}`,` ${t("knobs")} ${a("列出 15 个旋钮和含义")}`,` ${t("set WOMBATS 6")} ${a("改一个旋钮,不写代码(新手玩法)")}`,` ${t("edit")} ${a("用 vi 打开 bot.py,旋钮下面的 act(u) 随便重写(高手玩法;i 输入 · Esc · :wq 存盘)")}`,` ${t("run")} ${a("本地试打一场,只看 bot 跑不跑得起来(可跳过,需要 python setup)")}`,` ${t("up")} ${a("上传 bot → v1、v2、v3 …(每次上传 = 新版本;自动标 🔧 旋钮 / ✍ 代码)")}`,` ${t("vs <昵称>")} ${a("挑战一个人:6 局(3 张图 × 换席位),5–40 秒出结果表")}`,` ${t("vs <昵称> v1")} ${a("打对方的旧版本")}`,"",n(" 看回放 —— 两个数字:场号 + 局号"),` ${t("log")} ${a("① 列出今日对局,每行最前面的 #12 就是场号;带 🎬 的才有回放")}`,` ${t("history")} ${a(" 同上,但列出所有天")}`,` ${t("replay 12 3")} ${a("② 看第 12 场的第 3 局(每场 6 局,局号 1–6;别人打的也能看)")}`,` ${t("replay 12")} ${a(" 只写场号 = 那场的第 1 局")}`,` ${t("replay 3")} ${a(" 刚打完不用场号:最近一场的第 3 局")}`,` ${t("replay")} ${a(" 最近一场你输掉的那局")}`,a(" ③ 播放时:空格 暂停 · ←→ 单步 · ↑↓ 调速 · q 退出回房间"),a(" 例:log 里有「#387 … 2-4 🎬」→ replay 387 1 … replay 387 6 逐局看"),"",n(" 战绩"),` ${t("回车")} ${a("今日战绩表(胜-负-平 · 组 · 版本)")}`,` ${t("help")} ${a("再看步骤面板")} ${t("guide")} ${a("再看这份说明")} ${t("card")} ${a("回卡片")}`,"",a(" 规则一句话:打满 256 回合时,打掉对方价值更多的一方赢 —— 躲在家里不算赢。每 10 秒只能发起一场。"),a(" 看到「判分机暂时不可用」就等一分钟再 vs。")]:[n(` BattleAI PvP room · how to play (room ${o})`),"",n(" Open or join a room"),` ${t("room new Mock")} ${a("open a room → pick a nickname → you get a 4-letter code → give it to the others")}`,` ${t("room KZE6")} ${a("join a friend's room (nickname once)")}`,` ${t("room open")} ${a("the public room, anyone can enter; its table resets every day")}`,"",n(" Inside the room (prompt icoa room XXXX>) — the panel on entry ticks the steps you did"),` ${t("template")} ${a("puts the 15-knob starter at ~/.icoa/battleai/bot.py (pro: template code = blank code template)")}`,` ${t("knobs")} ${a("list the 15 knobs with their meaning")}`,` ${t("set WOMBATS 6")} ${a("change one knob, no coding (rookie way)")}`,` ${t("edit")} ${a("open bot.py in vi and rewrite act(u) below the knobs (pro way; i = type, Esc, :wq = save)")}`,` ${t("run")} ${a("one local test game, just to see that the bot runs (optional, needs python setup)")}`,` ${t("up")} ${a("upload your bot → v1, v2, v3 … (each upload is a new version; tagged 🔧 knobs or ✍ code)")}`,` ${t("vs <nick>")} ${a("challenge someone: 6 games (3 maps × both seats), result table in 5–40 s")}`,` ${t("vs <nick> v1")} ${a("play an older version of their bot")}`,"",n(" Watching replays — two numbers: match # + game #"),` ${t("log")} ${a("① today's matches; #12 at the line start = match number; 🎬 = replay kept")}`,` ${t("history")} ${a(" the same, for all days")}`,` ${t("replay 12 3")} ${a("② match #12, game 3 (6 games per match: 1–6; any player)")}`,` ${t("replay 12")} ${a(" match number only = game 1 of that match")}`,` ${t("replay 3")} ${a(" just played? skip the match #: game 3 of your last match")}`,` ${t("replay")} ${a(" the game you lost in your last match")}`,a(" ③ while it plays: space = pause · ← → = step · ↑ ↓ = speed · q = back to the room"),a(' e.g. log shows "#387 … 2-4 🎬" → replay 387 1 … replay 387 6'),"",n(" Table"),` ${t("Enter")} ${a("today's table (W-L-D, group, version)")}`,` ${t("help")} ${a("the step panel again")} ${t("guide")} ${a("this sheet again")} ${t("card")} ${a("back to your card")}`,"",a(" Rule in one line: at the 256-turn cap the side that destroyed more value wins — hiding at home does not. One challenge every 10 s."),a(' If you see "the judge is unavailable", wait a minute and try vs again.')];console.log();for(const e of r)console.log(e)}export async function runPvpRoom(e,t,n,i={}){const d=i.interactive??y,p=w(),v=n.trim();let A="",O="",B=!1;const x=/^(?:new|create|开房|建)(?:\s+(.*))?$/i.exec(v),M=/^(?:join|enter|进)\s+(\S+)$/i.exec(v);if(x?(B=!0,O=(x[1]??"").trim().slice(0,40)):M?A=M[1]:/^(?:open|public|公共)$/i.test(v)?A="OPEN":v&&(A=v),A=A.toUpperCase().replace(/[^A-Z0-9]/g,"").slice(0,8),!B&&!A){const o=await b("status",e);if(!o.ok||!o.data)return console.log(chalk.yellow(" "+W(o.code,o.message))),"back";if(console.log(),console.log(chalk.bold.white(p?" ⚔ PvP 房间":" ⚔ PvP rooms")+chalk.gray(p?" —— 和真人的 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(k(e.room,6))} ${chalk.white(k(e.public?p?"公共房":"open room":e.label||"",18))} ${chalk.gray(`${e.members} ${p?"人":"members"} · ${p?"你是":"you are"} ${e.nick}`)}`);else console.log(chalk.gray(p?" 你还没进过任何房间。":" You are not in any room yet."));return console.log(chalk.gray(" ▸ ")+chalk.bold.green("room open")+chalk.gray(p?" 公共房,谁都能进,战绩每天清零":" the public room — anyone can enter, the table resets daily")),console.log(chalk.gray(" ▸ ")+chalk.bold.green("room <CODE>")+chalk.gray(p?" 进朋友开的房(4 个字母)":" enter a friend's room (4-letter code)")),console.log(chalk.gray(" ▸ ")+chalk.bold.green(p?"room new [名字]":"room new [label]")+chalk.gray(p?" 开一个新房,把房号发给朋友":" open a new room and hand out its code")),"back"}let C=null;if(B){const o=await V(t,p);if(null===o)return"back";const a=await b("create",e,{label:O,nick:o});if(!a.ok||!a.data)return console.log(chalk.yellow(" "+W(a.code,a.message))),"back";C=a.data,console.log(chalk.green(" ✓ ")+chalk.white(p?"房间开好了,房号 ":"Room created — the code is ")+chalk.bold.cyan(C.room)+chalk.white(p?",告诉朋友敲 room "+C.room:`. Friends type room ${C.room}`))}else{const o=await b("status",e,{room:A});if(o.ok&&o.data)C=o.data;else{if("not_member"!==o.code)return console.log(chalk.yellow(" "+W(o.code,o.message))),"back";{const o=await b("join",e,{room:A,probe:!0}),a=o.data?.label||("OPEN"===A?p?"公共房":"Open room":A);console.log(chalk.white(` ${p?"进入":"Entering"} `)+chalk.bold.cyan(A)+chalk.gray(` ${a} · ${o.data?.members??"?"} ${p?"人":"members"}`));for(let o=0;o<4&&!C;o++){const o=await V(t,p);if(null===o)return"back";const a=await b("join",e,{room:A,nick:o});a.ok&&a.data?C=a.data:console.log(chalk.yellow(" "+W(a.code,a.message)))}if(!C)return"back"}}}const I=C.room,R=chalk.bold.magenta(`icoa room ${I}> `);let q=C.recent.find(e=>e.mine&&e.replays)?.id??0;for(o(DEFAULT_PVP_BOT)||C.my_versions.length||F(p,I),U(p,C),console.log(chalk.gray(" ▸ ")+chalk.bold.green("guide")+chalk.gray(p?" 完整命令说明":" the full command sheet")),P(C),_(I);;){const n=await t(R);if(null===n)return"back";const i=n.trim(),y=i.toLowerCase();if(f.has(y))return"back";if("menu"===y||"home"===y)return"menu";if(""===y||"who"===y||"board"===y||"table"===y||"r"===y||"refresh"===y){const o=await b("status",e,{room:I});o.ok&&o.data?P(o.data):console.log(chalk.yellow(" "+W(o.code,o.message)));continue}if("guide"===y||"howto"===y||"how"===y||"说明"===y||"教程"===y){F(p,I),_(I);continue}if("help"===y||"?"===y){const o=await b("status",e,{room:I});U(p,o.ok&&o.data?o.data:null),_(I);continue}if("log"===y||"matches"===y||"history"===y){const o=await b("matches",e,{room:I,limit:20,all:"history"===y});if(o.ok&&o.data){o.data.matches.length||console.log(chalk.gray(p?" 今天还没有对局。":" No matches today yet."));for(const e of o.data.matches)console.log(" "+T(e));o.data.matches.length&&console.log(chalk.gray(p?" 🎬 = 有回放: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(" "+W(o.code,o.message)));continue}const w=/^(?:replay|回放)(?:\s+(last|\d+))?(?:\s+(\d+))?$/i.exec(i);if(w){const t=s(r(),".icoa","battleai","last.mobreplay"),a=()=>console.log(chalk.yellow(p?" 回放已存在本机,但播放器需要 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=w[1]&&"last"!==w[1].toLowerCase()?Number(w[1]):0,d=w[2]?Number(w[2]):0;if(c&&d)n=c,i=d;else if(c>=1&&c<=6){if(n=q,i=c,!n){console.log(chalk.gray(p?" 还没有可回看的对局 —— 先 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:I,match_id:n,game:i},3e4);if(!o.ok||!o.data){console.log(chalk.yellow(" "+W(o.code,o.message)));continue}const t=$(o.data.seat_a,o.data.a,o.data.b),r="a"===o.data.winner?o.data.a:"b"===o.data.winner?o.data.b:null,s=e=>t?t.blue===e?h(0,p):h(1,p):"",c=e=>chalk.bold.white(e)+s(e);if(console.log(chalk.gray(` #${n} ${p?"第":"game"} ${i}${p?" 局":""} · ${o.data.map} · `)+(t?`${h(0,p).trim()} ${chalk.bold.white(t.blue)}${chalk.gray(" vs ")}${h(1,p).trim()} ${chalk.bold.white(t.red)}`:`${c(o.data.a)}${chalk.gray(" vs ")}${c(o.data.b)}`)+chalk.gray(` · ${p?"胜者":"winner"}: `)+(r?c(r):chalk.gray(p?"平局":"draw"))),!g(o.data.replay,t)){console.log(chalk.yellow(p?" 回放存不下来。":" Could not save the replay."));continue}await l()||a();continue}if(!o(t)){console.log(chalk.gray(p?" 还没有回放 —— 先 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"===y||"knob"===y||"旋钮"===y||"set"===y){const e=j();null===e?U(p,null):L(e,p);continue}const v=/^(?:set|改)\s+(.+)$/i.exec(i);if(v){const e=j();null===e?U(p,null):D(e,v[1],p);continue}const k=/^(?:template|模板)(?:\s+(knobs?|code|新手|高手))?(?:\s+(force|覆盖))?$/i.exec(i);if(k){const e=/^(?:code|高手)$/i.test(k[1]??"")?"code":"knobs";await S(e,!!k[2],p);continue}if("edit"===y||"vi"===y||"编辑"===y){if(!o(DEFAULT_PVP_BOT)){U(p,null);continue}console.log(chalk.gray(p?" 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"===y||"test"===y||"play"===y||"试打"===y){if(!o(DEFAULT_PVP_BOT)){U(p,null);continue}if(!u().ok){console.log(chalk.gray(p?" 本机没有 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(p?" 本地打一场(对卡 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 A=/^(?:up|upload|submit|上传)(?:\s+(.+))?$/i.exec(i);if(A){const t=(A[1]??"").trim()||DEFAULT_PVP_BOT;if(!o(t)){console.log(chalk.yellow(` ${p?"找不到":"Not found"}: ${t}`)),U(p,null);continue}let n="";try{n=a(t,"utf8")}catch{console.log(chalk.yellow(` ${p?"读不了":"Could not read"}: ${t}`));continue}const r=await N(n),s=await b("upload",e,{room:I,code:n,group:r},3e4);if(!s.ok||!s.data){console.log(chalk.yellow(" "+W(s.code,s.message)));continue}s.data.unchanged?console.log(chalk.gray(p?` 和 v${s.data.version} 一字不差,没有新版本。`:` Identical to v${s.data.version} — nothing new.`)):console.log(chalk.green(" ✓ ")+chalk.white(p?`已上传为 v${s.data.version}`:`Uploaded as v${s.data.version}`)+chalk.gray(` (${t})`)+(r?chalk.gray("rookie"===r?p?" · 🔧 新手组(只动了旋钮)":" · 🔧 knobs only (rookie)":p?" · ✍ 高手组(改了代码)":" · ✍ custom code (pro)"):""));continue}const O=/^(?:vs|challenge|fight|挑战|打)\s+(\S+)(?:\s+v?(\d+))?$/i.exec(i);if(O){const o=O[2]?Number(O[2]):void 0;console.log(chalk.gray(p?" ⚖ 判分机在打 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:I,nick:O[1],version:o},2e5);if(!t.ok||!t.data){console.log(chalk.yellow(" "+W(t.code,t.message)));continue}t.data.replays&&(q=t.data.match_id),E(t.data),P(t.data.status);continue}/^(?:vs|challenge|挑战|打)$/i.test(i)?console.log(chalk.gray(p?" 用法: vs <昵称> 或 vs <昵称> v2(打对方的第 2 版)":" Usage: vs <nick> or vs <nick> v2 (their version 2)")):(console.log(chalk.gray(p?` 不认识「${i}」。`:` Unknown: "${i}".`)),_(I))}}async function V(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||f.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 W(e,o){const t=w();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.642",
3
+ "version": "2.19.643",
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",