icoa-cli 2.19.646 → 2.19.647
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 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?"兵力价值 你/对方":"army value 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.value??t.lost)[0])}/${Math.round((t.value??t.lost)[1])}${t.error?chalk.gray(` ${t.error}`):""}`)}console.log(chalk.gray(` ${o?"种子":"seeds"} ${e.seeds.join(",")} · ${o?"拆掉对方的家就赢;打满回合比兵力价值(活着的兵,wombat 和家不算),相差不到 5% 平局":"destroy their warren to win; at the turn cap the higher army value wins (live soldiers; wombats and warrens do not count), within 5% is a draw"}`)),e.games.length>3&&e.games.some(e=>1===e.seat_a)&&console.log(chalk.gray(o?" ⚖ 换席位抽查结果不一样(有 bot 按座位做不同的事),所以每张图两边都打了一遍。":" ⚖ The seat-swap check came out different (a bot acts differently by seat), so every map was played from both seats."));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–${e.games.length} 任选)· `:` watch game 3 (any of 1–${e.games.length}) · `)+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}},N=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 x(e){const o=await i();if(!o)return null;try{const t=a(s(o,"bots","knob_bot.py"),"utf8");return N(t).join("\n")===N(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?"今天打过了 —— 再来一场":"挑战一个人:3 张图定胜负"),s("6",!1,"replay 3","看第 3 局(1–3);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: 3 maps decide"),s("6",!1,"replay 3","watch game 3 (1–3); 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("挑战一个人:3 张图各 1 局(地图对称,换席位结果一样),5–30 秒出结果表")}`,` ${t("vs <昵称> v1")} ${a("打对方的旧版本")}`,"",n(" 看回放 —— 两个数字:场号 + 局号"),` ${t("log")} ${a("① 列出今日对局,每行最前面的 #12 就是场号;带 🎬 的才有回放")}`,` ${t("history")} ${a(" 同上,但列出所有天")}`,` ${t("replay 12 3")} ${a("② 看第 12 场的第 3 局(每场 3 局,局号 1–3;别人打的也能看)")}`,` ${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 回合时,兵力价值(活着的兵,wombat 和家不算)高的一方赢,相差不到 5% 平局。每 30 秒只能发起一场。"),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: one game on each of the 3 maps (symmetric maps, swapping seats changes nothing), result in 5–30 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 (3 games per match: 1–3; 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: destroy their warren to win; at the 256-turn cap the higher army value wins (live soldiers; wombats and warrens do not count), within 5% is a draw. One challenge every 30 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 N=/^(?:new|create|开房|建)(?:\s+(.*))?$/i.exec(v),M=/^(?:join|enter|进)\s+(\S+)$/i.exec(v);if(N?(B=!0,O=(N[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 对打,3 张图定胜负。":" — your bot against real people, one game on each of 3 maps.")),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 <场号> <局号>,例如 replay 12 3;没有 🎬 的是旧对局,回放没存。":" 🎬 = replays kept: replay <match#> <game#>, 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 x(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?" ⚖ 判分机在打 3 张图,通常 5–20 秒,最多 3 分钟…":" ⚖ The judge is playing the 3 maps — usually 5–20 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?"每 30 秒只能发起一场。":"One challenge every 30 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 d}from"../commands/battle.js";import{getConfig as g}from"./config.js";import{runInteractive as y}from"./interactive-spawn.js";import{learnPost as p}from"./learn-contest.js";import{probePython as $}from"./python-setup.js";import{ARENA_VI_FLAGS as u}from"./vi-flags.js";export const PVP_RE=/^\/?(?:room|rooms|pvp|房间|对战房)(?:\s+(.*))?$/i;function m(e,o,t){return 0!==e&&1!==e?null:0===e?{blue:o,red:t}:{blue:t,red:o}}const h=e=>0===e?chalk.cyan(v(" 🟦 blue"," 🟦 蓝"," 🟦 น้ำเงิน")):chalk.red(v(" 🟥 red"," 🟥 红"," 🟥 แดง")),b=(e,o,t={},a=2e4)=>p(`/api/icoa/learn/pvp/${e}`,{token:o.toUpperCase(),...t},a),w=()=>{const e=(g().language||"en").toLowerCase();return e.startsWith("zh")?"zh":e.startsWith("th")?"th":"en"},v=(e,o,t)=>{const a=w();return"zh"===a?o:"th"===a?t:e},f=()=>"zh"===w(),k=new Set(["back","b","quit","q","exit","card","lesson","卡片"]);export const DEFAULT_PVP_BOT=s(r(),".icoa","battleai","bot.py");const _=e=>{let o=0;for(const t of e){const e=t.codePointAt(0)??0;3633===e||e>=3636&&e<=3642||e>=3655&&e<=3662||(o+=e>65535||e>=11904&&e<=42191||e>=44032&&e<=55203||e>=63744&&e<=64255?2:1)}return o},P=(e,o)=>e+" ".repeat(Math.max(0,o-_(e)));function T(e){console.log(chalk.gray(v(` knobs · set <KNOB> <value> · run (local test) · up (upload) · vs <nick> (challenge) · Enter (table) · log · replay 3 (game 3) · edit · template · guide · card (${e})`,` knobs · set <旋钮> <值> · run 试打 · up 上传 · vs <昵称> 挑战 · 回车 战绩表 · log · replay 3 看第 3 局 · edit · template · guide 说明 · card 回卡片 (${e})`,` knobs · set <KNOB> <ค่า> · run ทดลองเล่น · up อัปโหลด · vs <ชื่อเล่น> ท้าแข่ง · Enter ตารางคะแนน · log · replay 3 ดูเกมที่ 3 · edit · template · guide คู่มือ · card กลับไปที่การ์ด (${e})`)))}const E=e=>"rookie"===e?v("🔧 knobs","🔧 新手","🔧 knobs"):"pro"===e?v("✍ code","✍ 高手","✍ code"):"—";function A(e){const o=f(),t=e.public?v("Open room","公共房","ห้องสาธารณะ"):e.label||v("Room","房间","ห้อง");if(console.log(),console.log(chalk.bold.white(` ⚔ ${t} `)+chalk.bold.cyan(e.room)+chalk.gray(` · ${v("you are","你是","คุณคือ")} ${e.nick??"?"} · ${e.members.length} ${v("members","人","คน")} · ${v("today","今日","วันนี้")} ${e.day}${e.public?v(" · table resets daily"," · 战绩每天清零"," · ตารางเริ่มใหม่ทุกวัน"):""}`)),(e=>"distinct_beaten"===e.ranking&&e.members.every(e=>"number"==typeof e.today.beat))(e)){console.log(chalk.gray(v(" Ranked by how many different people you beat today, then win rate. The same two bot versions count once a day."," 排名先看今天赢过多少个不同的人,再看胜率。同一对 bot 版本每天只计一次。"," จัดอันดับตามจำนวนคนที่คุณชนะได้ (ไม่นับคนซ้ำ) ในวันนี้ แล้วจึงดูอัตราชนะ — bot คู่เวอร์ชันเดิมนับคะแนนได้วันละครั้ง"))),console.log(chalk.gray(o?` ${P("昵称",14)}${P("bot",6)}${P("组",9)}${P("赢过",6)}${P("今日 胜-负-平",15)}${P("打了",7)}累计`:` ${P("nick",14)}${P("bot",6)}${P("group",9)}${P("beat",6)}${P("today W-L-D",15)}${P("played",7)}all-time`));for(const o of e.members){const e=` ${P(o.nick,14)}${P(o.versions?`v${o.versions}`:"—",6)}${P(E(o.group),9)}${P(String(o.today.beat??0),6)}${P(`${o.today.w}-${o.today.l}-${o.today.d}`,15)}${P(String(o.today.played??o.today.n),7)}${o.all.w}-${o.all.l}-${o.all.d}`;console.log(o.me?chalk.bold.cyan(e):chalk.white(e))}}else{console.log(chalk.gray(o?` ${P("昵称",14)}${P("bot",6)}${P("组",9)}${P("今日 胜-负-平",16)}累计`:` ${P("nick",14)}${P("bot",6)}${P("group",9)}${P("today W-L-D",16)}all-time`));for(const o of e.members){const e=` ${P(o.nick,14)}${P(o.versions?`v${o.versions}`:"—",6)}${P(E(o.group),9)}${P(`${o.today.w}-${o.today.l}-${o.today.d}`,16)}${o.all.w}-${o.all.l}-${o.all.d}`;console.log(o.me?chalk.bold.cyan(e):chalk.white(e))}}if(e.my_versions.length||console.log(chalk.yellow(v(" You have no bot here yet — knobs / set, then up."," 你还没上传 bot —— 先 knobs / set,再 up。"," คุณยังไม่มี bot ในห้องนี้ — ใช้ knobs / set แล้วจึง up"))),e.recent.length){console.log(chalk.gray(v(" Today:"," 今日对局:"," แมตช์วันนี้:")));for(const o of e.recent.slice(0,6))console.log(chalk.gray(" ")+B(o))}}const O=()=>chalk.gray(v(" · not counted"," · 不计分"," · ไม่นับคะแนน"));function B(e){const o=null===e.wins_a||void 0===e.wins_a?"-":`${e.wins_a}-${e.wins_b}`,t="WIN"===e.verdict?chalk.green(o):"LOSS"===e.verdict?chalk.red(o):chalk.yellow(o),a=`${e.a} v${e.a_version??"?"} vs ${e.b} v${e.b_version??"?"}`,n=chalk.gray(`#${String(e.id).padEnd(4)} `),r=e.replays?chalk.gray(" 🎬"):"",s="WIN"===e.verdict||"LOSS"===e.verdict||"DRAW"===e.verdict;return`${n}${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(a):chalk.gray(a)} ${t}${r}${e.draws?chalk.gray(` (${e.draws} ${v("draw","平","เสมอ")})`):""}${s&&0===e.counted?O():""}${s?"":chalk.red(` ${e.verdict}`)}`}let N=!1;function S(e){const o=f();console.log();const t=`${e.wins_a}-${e.wins_b}${e.draws?` (${e.draws} ${v("draw","平","เสมอ")})`:""}`,a=chalk.gray(` vs ${e.opp.nick} v${e.opp.version}`),n=0===e.counted&&"repeat_pairing"===e.counted_reason?O():"";if("WIN"===e.verdict)console.log(chalk.bold.green(` 🏆 ${v("You win","你赢了","คุณชนะ")} ${t}`)+a+n);else if("LOSS"===e.verdict)console.log(chalk.bold.red(` ❌ ${v("You lose","你输了","คุณแพ้")} ${t}`)+a+n);else{if("DRAW"!==e.verdict)return void console.log(chalk.yellow(` ⚠ ${v("The judge could not finish this match","判分机没跑完这场","เครื่องตัดสินเล่นแมตช์นี้ไม่จบ")}: ${e.verdict}${e.note?` — ${e.note}`:""}`));console.log(chalk.bold.yellow(` 🤝 ${v("Draw","平局","เสมอ")} ${t}`)+a+n)}n&&(N?console.log(chalk.gray(v(" (same two versions as earlier today — up a new version to count)"," (和今天早些时候是同一对版本 —— 上传新版本才计分)"," (เป็นคู่เวอร์ชันเดิมของวันนี้ — up เวอร์ชันใหม่เพื่อให้นับคะแนน)"))):(N=!0,console.log(chalk.gray(v(" Not counted: these two bot versions already played each other today. Maps are fixed for the day, so the result"," 不计分:这两个 bot 版本今天已经交过手。当天的地图固定,结果不会变"," ไม่นับคะแนน: bot สองเวอร์ชันนี้แข่งกันไปแล้ววันนี้ แผนที่ของวันนี้ถูกกำหนดไว้แล้ว ผลจึงเหมือนเดิม"))),console.log(chalk.gray(v(" is the same (replays still work). To count again, one of you uploads a new version: "," (回放照样能看)。想再计分,任何一方上传新版本即可: "," (ยังดูรีเพลย์ได้ตามปกติ) ถ้าอยากให้นับคะแนนอีก ฝ่ายใดฝ่ายหนึ่งอัปโหลดเวอร์ชันใหม่: "))+chalk.bold.green("up"))),e.cached&&e.cached_from&&console.log(chalk.gray(v(` Same games as match #${e.cached_from} — served instantly, the judge was not needed.`,` 和第 ${e.cached_from} 场完全相同 —— 直接给出,没有占用判分机。`,` เป็นเกมเดียวกับแมตช์ #${e.cached_from} — แสดงผลทันทีโดยไม่ต้องใช้เครื่องตัดสิน`)))),e.load_error?.a&&console.log(chalk.red(` ${v("Your bot failed to load","你的 bot 装不上","bot ของคุณโหลดไม่ขึ้น")}: ${e.load_error.a}`)),e.load_error?.b&&console.log(chalk.gray(` ${v("Opponent's bot failed to load","对方的 bot 装不上","bot ของคู่แข่งโหลดไม่ขึ้น")}: ${e.load_error.b}`)),console.log(chalk.gray(o?` ${P("#",4)}${P("地图",11)}${P("席位",6)}${P("结果",8)}${P("原因",10)}${P("回合",7)}兵力价值 你/对方`:` ${P("#",4)}${P("map",11)}${P("seat",6)}${P("result",8)}${P("how",10)}${P("turns",7)}army value 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(P(a,8)):"b"===t.winner?chalk.red(P(a,8)):chalk.yellow(P(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(` ${P(String(t.game??e.games.indexOf(t)+1),4)}${P(t.map,11)}${P(String(t.seat_a),6)}${n}${P(r,10)}${P(String(t.turns),7)}${Math.round((t.value??t.lost)[0])}/${Math.round((t.value??t.lost)[1])}${t.error?chalk.gray(` ${t.error}`):""}`)}console.log(chalk.gray(` ${v("seeds","种子","seeds")} ${e.seeds.join(",")} · ${v("destroy their warren to win; at the turn cap the higher army value wins (live soldiers; wombats and warrens do not count), within 5% is a draw","拆掉对方的家就赢;打满回合比兵力价值(活着的兵,wombat 和家不算),相差不到 5% 平局","ทำลาย warren ของอีกฝ่ายได้ก็ชนะ ถ้าครบจำนวนเทิร์น ฝ่ายที่มีมูลค่ากองทัพสูงกว่าชนะ (นับเฉพาะทหารที่ยังรอด ไม่นับ wombat และ warren) ต่างกันไม่ถึง 5% ถือว่าเสมอ")}`)),e.games.length>3&&e.games.some(e=>1===e.seat_a)&&console.log(chalk.gray(v(" ⚖ The seat-swap check came out different (a bot acts differently by seat), so every map was played from both seats."," ⚖ 换席位抽查结果不一样(有 bot 按座位做不同的事),所以每张图两边都打了一遍。"," ⚖ ผลตรวจสลับที่นั่งออกมาไม่เหมือนกัน (มี bot ที่เล่นต่างกันตามที่นั่ง) จึงเล่นทุกแผนที่จากทั้งสองฝั่ง")));const r=e.games.findIndex(e=>"b"===e.winner)+1,s=e.games.find(e=>"b"===e.winner);e.replay&&d(e.replay,s?m(s.seat_a,e.me.nick,e.opp.nick):null),e.replays?console.log(chalk.gray(" ▸ ")+chalk.bold.green("replay 3")+chalk.gray(v(` watch game 3 (any of 1–${e.games.length}) · `,` 看第 3 局(1–${e.games.length} 任选)· `,` ดูเกมที่ 3 (เลือกได้ 1–${e.games.length}) · `))+chalk.bold.green("replay")+chalk.gray(r?v(` the game you lost (#${r}) · `,` 看你输掉的第 ${r} 局 · `,` เกมที่คุณแพ้ (#${r}) · `):v(" game 1 · "," 看第 1 局 · "," เกมที่ 1 · "))+chalk.bold.green(`replay ${e.match_id} 3`)+chalk.gray(v(" this match later, from log"," 以后从 log 里回看这场"," ดูแมตช์นี้ภายหลังจาก log"))):e.replay&&console.log(chalk.gray(" ▸ ")+chalk.bold.green("replay")+chalk.gray(e.wins_b?v(" watch the first game you lost"," 看你输掉的第一局"," ดูเกมแรกที่คุณแพ้"):v(" watch game 1"," 看第一局"," ดูเกมที่ 1")))}const j=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]))]},x=(e,o)=>{const t=new RegExp(`^${o}\\s*=\\s*([^#\n]*?)\\s*(?:#.*)?$`,"m").exec(e);return t?t[1]:null},L=(e,o)=>{const t=new RegExp(`^${o}\\s*=[^#\n]*#\\s*(.*)$`,"m").exec(e);return t?t[1].trim():""},D=()=>{try{return a(DEFAULT_PVP_BOT,"utf8")}catch{return null}},U=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 M(e){const o=await i();if(!o)return null;try{const t=a(s(o,"bots","knob_bot.py"),"utf8");return U(t).join("\n")===U(e).join("\n")?"rookie":"pro"}catch{return null}}async function W(a,n,l){const c=await i();if(!c)return console.log(chalk.yellow(v(" The arena engine is not on this machine yet (one online fetch needed)."," 引擎还没下载到本机(需要联网一次)。"," เครื่องนี้ยังไม่มี arena engine (ต้องต่ออินเทอร์เน็ตดาวน์โหลดหนึ่งครั้ง)"))),!1;const d=s(c,"bots","knobs"===a?"knob_bot.py":l?"template_bot_zh.py":"template_bot.py");if(!o(d))return console.log(chalk.yellow(` ${v("Template missing in the engine","引擎里没有模板","ไม่พบเทมเพลตใน engine")}: ${d}`)),!1;if(t(s(r(),".icoa","battleai"),{recursive:!0}),o(DEFAULT_PVP_BOT)){if(!n)return console.log(chalk.yellow(v(" bot.py already exists — not overwritten."," bot.py 已经存在,不覆盖。"," มี bot.py อยู่แล้ว — ไม่เขียนทับ"))+chalk.gray(v(" To replace it: "," 确定要换: "," ถ้าต้องการแทนที่: "))+chalk.bold.green(`template ${a} force`)+chalk.gray(v(" (the old file is kept as bot.py.bak)","(旧文件留作 bot.py.bak)"," (ไฟล์เดิมจะเก็บไว้เป็น bot.py.bak)"))),!1;e(DEFAULT_PVP_BOT,`${DEFAULT_PVP_BOT}.bak`)}return e(d,DEFAULT_PVP_BOT),console.log(chalk.green(" ✓ ")+chalk.white("knobs"===a?v("The 15-knob starter is now your bot.py","15 个旋钮的新手模板已放到 bot.py","ตอนนี้ bot.py ของคุณคือเทมเพลตเริ่มต้นแบบ 15 knobs แล้ว"):v("The blank code template is now your bot.py (pro)","空白代码模板已放到 bot.py(高手组)","ตอนนี้ bot.py ของคุณคือเทมเพลตโค้ดเปล่า (กลุ่ม pro)"))+chalk.gray(` (${DEFAULT_PVP_BOT})`)),!0}function F(e){const o=j(e);if(o.length){console.log(chalk.gray(v(" Your knobs (set <KNOB> <value> changes one):"," 你的旋钮(set <旋钮> <值> 改一个):"," knobs ของคุณ (เปลี่ยนทีละตัวด้วย set <KNOB> <ค่า>):")));for(const t of o)console.log(` ${chalk.bold.white(t.padEnd(10))}${chalk.green((x(e,t)??"").padEnd(12))}${chalk.gray(L(e,t))}`)}else console.log(chalk.gray(v(" 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."," 你的 bot.py 没有旋钮(顶部那种 NAME = value 行)—— 它是高手组的自定义代码。要旋钮版敲 template knobs。"," bot.py ของคุณไม่มี knobs (บรรทัดแบบ NAME = value ด้านบน) — เป็นโค้ดที่เขียนเอง (pro) ถ้าต้องการแบบ knobs พิมพ์ template knobs")))}function V(e,o){const t=j(e),a=/^([A-Za-z][A-Za-z0-9_]*)\s*(?:=|\s)\s*(.+)$/.exec(o.trim());if(!a)return console.log(chalk.yellow(v(" Usage: "," 用法: "," วิธีใช้: "))+chalk.bold.green(v("set <KNOB> <value>","set <旋钮> <值>","set <KNOB> <ค่า>"))+chalk.gray(v(" e.g. set WOMBATS 6"," 例如 set WOMBATS 6"," เช่น set WOMBATS 6"))),F(e),!1;const r=a[1].toUpperCase(),s=a[2].trim();if(!t.includes(r))return console.log(chalk.yellow(v(` No knob called ${r}.`,` 没有 ${r} 这个旋钮。`,` ไม่มี knob ชื่อ ${r}`))+chalk.gray(" ")+t.map(e=>chalk.bold.white(e)).join(chalk.gray(" · "))),!1;const l=x(e,r)??"";if(l.trim()===s)return console.log(chalk.gray(" ")+chalk.bold.white(r)+chalk.gray(v(` is already ${s}.`,` 已经是 ${s} 了。`,` เป็น ${s} อยู่แล้ว`))),!1;const i=new RegExp(`^(${r}\\s*=\\s*)([^#\n]*?)(\\s*)(#.*)?$`,"m"),c=e.replace(i,(e,o,t,a,n)=>`${o}${s}${n?(a||" ")+n:""}`);return n(DEFAULT_PVP_BOT,c),console.log(chalk.gray(" ")+chalk.bold.white(r)+chalk.gray(" ")+chalk.gray(l.trim())+chalk.gray(" → ")+chalk.bold.green(s)+chalk.gray(v(" next: run to test, up to upload"," 下一步: run 试打,up 上传"," ขั้นต่อไป: run เพื่อทดลอง, up เพื่ออัปโหลด"))),!0}function q(e){const t=o(DEFAULT_PVP_BOT),a=!!e&&e.my_versions.length>0,n=!!e&&e.recent.some(e=>e.mine),r=(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(P(t,13))}${chalk.gray(a)}`,s=new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`,"g"),l=!("th"===w()),i=e=>l?`${e}${" ".repeat(Math.max(0,70-_(e.replace(s,""))))}`:e,c=[r("1",t,"template",t?v("bot.py is ready (~/.icoa/battleai/bot.py)","bot.py 已就绪(~/.icoa/battleai/bot.py)","bot.py พร้อมแล้ว (~/.icoa/battleai/bot.py)"):v("get the 15-knob starter (pro: template code)","拿 15 个旋钮的新手模板(高手: template code)","รับเทมเพลตเริ่มต้น 15 knobs (pro: template code)")),r("2",!1,"knobs · set",v("see the knobs; set WOMBATS 6 changes one — no code","看旋钮;set WOMBATS 6 改一个,不用写代码","ดู knobs; set WOMBATS 6 เปลี่ยนทีละตัว ไม่ต้องเขียนโค้ด"),!0),` ${chalk.gray(v("✍ pro:","✍ 高手:","✍ pro:"))} ${chalk.bold.green("edit")} ${chalk.gray(v("opens the file — rewrite act(u) below the knobs","打开文件,旋钮下面的 act(u) 随便重写","เปิดไฟล์ แล้วเขียน act(u) ใต้ knobs ใหม่ได้เลย"))}`,r("3",!1,"run",v("one local test game — does the bot run? (optional)","本地试打一场,看 bot 跑不跑得起来(可跳过)","ทดลองเล่นในเครื่อง 1 เกม ดูว่า bot รันได้ไหม (ข้ามได้)"),!0),r("4",a,"up",a?v(`uploaded, now v${e?.my_versions.length} (edit + up again = new version)`,`已上传到 v${e?.my_versions.length}(改完再 up = 新版本)`,`อัปโหลดแล้ว ตอนนี้เป็น v${e?.my_versions.length} (แก้แล้ว up อีกครั้ง = เวอร์ชันใหม่)`):v("upload the bot → v1 (tagged 🔧 knobs / ✍ code)","上传 bot → v1(自动标 🔧 新手 / ✍ 高手)","อัปโหลด bot → v1 (ติดป้าย 🔧 knobs / ✍ code ให้อัตโนมัติ)")),r("5",n,f()?"vs <昵称>":"vs <nick>",n?v("you played today — go again","今天打过了 —— 再来一场","วันนี้คุณแข่งไปแล้ว — ลองอีกสักแมตช์"):v("challenge someone: 3 maps decide","挑战一个人:3 张图定胜负","ท้าแข่งกับใครสักคน: ตัดสินกันด้วย 3 แผนที่")),r("6",!1,"replay 3",v("watch game 3 (1–3); replay <match#> <game> for any match in log","看第 3 局(1–3);replay <场号> <局> 回看 log 里任何一场","ดูเกมที่ 3 (1–3); replay <เลขแมตช์> <เกม> ดูแมตช์ไหนก็ได้ใน log"),!0),r("7",!1,v("Enter","回车","Enter"),v("today's table · log matches · card back to the card","今日战绩表 · log 对局 · card 回卡片","ตารางคะแนนวันนี้ · log แมตช์ · card กลับไปที่การ์ด"),!0)],d=` ${v("How to play","怎么玩","วิธีเล่น")} · ${e?.room??"room"} `;console.log(),console.log(chalk.cyan(` ╭─${chalk.bold.white(d)}${"─".repeat(Math.max(0,70-_(d)-1))}${l?"╮":"─"}`));for(const e of c)console.log(`${chalk.cyan(" │")}${i(e)}${l?chalk.cyan("│"):""}`);console.log(chalk.cyan(` ╰${"─".repeat(70)}${l?"╯":"─"}`));const g=t?a?n?"":v("vs <nick>","vs <昵称>","vs <ชื่อเล่น>"):"up":"template";g&&console.log(chalk.gray(v(" ▸ next: "," ▸ 下一步敲: "," ▸ ขั้นต่อไป พิมพ์: "))+chalk.bold.green(g))}function I(e){const o=e=>chalk.bold.green(e),t=e=>chalk.gray(e),a=e=>chalk.bold.white(e),n=w(),r="zh"===n?[a(` BattleAI PvP 房间 · 怎么玩(房号 ${e})`),"",a(" 进房 / 开房"),` ${o("room new 名字")} ${t("开一个房 → 起昵称 → 得到 4 个字母的房号,发给朋友")}`,` ${o("room KZE6")} ${t("进朋友的房(昵称只问一次)")}`,` ${o("room open")} ${t("公共房,谁都能进;战绩表每天清零")}`,"",a(" 房里(提示符 icoa room XXXX>)—— 进房的面板会把做过的步骤打勾"),` ${o("template")} ${t("把 15 个旋钮的新手模板放到 ~/.icoa/battleai/bot.py(高手: template code = 空白代码模板)")}`,` ${o("knobs")} ${t("列出 15 个旋钮和含义")}`,` ${o("set WOMBATS 6")} ${t("改一个旋钮,不写代码(新手玩法)")}`,` ${o("edit")} ${t("用 vi 打开 bot.py,旋钮下面的 act(u) 随便重写(高手玩法;i 输入 · Esc · :wq 存盘)")}`,` ${o("run")} ${t("本地试打一场,只看 bot 跑不跑得起来(可跳过,需要 python setup)")}`,` ${o("up")} ${t("上传 bot → v1、v2、v3 …(每次上传 = 新版本;自动标 🔧 旋钮 / ✍ 代码)")}`,` ${o("vs <昵称>")} ${t("挑战一个人:3 张图各 1 局(地图对称,换席位结果一样),5–30 秒出结果表")}`,` ${o("vs <昵称> v1")} ${t("打对方的旧版本")}`,"",a(" 看回放 —— 两个数字:场号 + 局号"),` ${o("log")} ${t("① 列出今日对局,每行最前面的 #12 就是场号;带 🎬 的才有回放")}`,` ${o("history")} ${t(" 同上,但列出所有天")}`,` ${o("replay 12 3")} ${t("② 看第 12 场的第 3 局(每场 3 局,局号 1–3;别人打的也能看)")}`,` ${o("replay 12")} ${t(" 只写场号 = 那场的第 1 局")}`,` ${o("replay 3")} ${t(" 刚打完不用场号:最近一场的第 3 局")}`,` ${o("replay")} ${t(" 最近一场你输掉的那局")}`,t(" ③ 播放时:空格 暂停 · ←→ 单步 · ↑↓ 调速 · q 退出回房间"),t(" 例:log 里有「#387 … 2-4 🎬」→ replay 387 1 … replay 387 6 逐局看"),"",a(" 战绩(公平规则)"),` ${o("回车")} ${t("今日战绩表:先比今天赢过几个不同的人,再比胜率,再比胜场")}`,t(" · 同一对 bot 版本每天只计一次分;再打照样能打、能看回放,但标「不计分」。任何一方上传新版本 = 新的一对,重新计分。"),t(" · 当天的地图是固定的:同一对版本再打,结果一模一样(重赌运气没用)。"),t(" · 同一台电脑上的两个账号不能互相挑战 —— PvP 要两个不同的人。"),` ${o("help")} ${t("再看步骤面板")} ${o("guide")} ${t("再看这份说明")} ${o("card")} ${t("回卡片")}`,"",t(" 规则一句话:拆掉对方的家就赢;打满 200 回合时,兵力价值(活着的兵,wombat 和家不算)高的一方赢,相差不到 5% 平局。每 30 秒只能发起一场。"),t(" 看到「判分机暂时不可用」就等一分钟再 vs。")]:"th"===n?[a(` ห้อง PvP ของ BattleAI · วิธีเล่น (ห้อง ${e})`),"",a(" เปิดห้องหรือเข้าห้อง"),` ${o("room new Mock")} ${t("เปิดห้องใหม่ → ตั้งชื่อเล่น → ได้รหัสห้อง 4 ตัวอักษร → ส่งให้เพื่อน")}`,` ${o("room KZE6")} ${t("เข้าห้องของเพื่อน (ตั้งชื่อเล่นครั้งเดียว)")}`,` ${o("room open")} ${t("ห้องสาธารณะ ใครก็เข้าได้ ตารางคะแนนเริ่มใหม่ทุกวัน")}`,"",a(" ในห้อง (prompt icoa room XXXX>) — แผงตอนเข้าห้องจะติ๊กขั้นตอนที่คุณทำแล้ว"),` ${o("template")} ${t("วางเทมเพลตเริ่มต้น 15 knobs ไว้ที่ ~/.icoa/battleai/bot.py (pro: template code = เทมเพลตโค้ดเปล่า)")}`,` ${o("knobs")} ${t("แสดง knobs ทั้ง 15 ตัวพร้อมความหมาย")}`,` ${o("set WOMBATS 6")} ${t("เปลี่ยน knob ทีละตัว ไม่ต้องเขียนโค้ด (แบบมือใหม่)")}`,` ${o("edit")} ${t("เปิด bot.py ด้วย vi แล้วเขียน act(u) ใต้ knobs ใหม่ (แบบ pro; i = พิมพ์ · Esc · :wq = บันทึก)")}`,` ${o("run")} ${t("ทดลองเล่นในเครื่อง 1 เกม แค่ดูว่า bot รันได้ (ข้ามได้ ต้องมี python setup)")}`,` ${o("up")} ${t("อัปโหลด bot → v1, v2, v3 … (อัปโหลดแต่ละครั้ง = เวอร์ชันใหม่; ติดป้าย 🔧 knobs หรือ ✍ code)")}`,` ${o("vs <nick>")} ${t("ท้าแข่ง: แผนที่ละ 1 เกม รวม 3 แผนที่ (แผนที่สมมาตร สลับที่นั่งผลเหมือนเดิม) รู้ผลใน 5–30 วินาที")}`,` ${o("vs <nick> v1")} ${t("แข่งกับ bot เวอร์ชันเก่าของอีกฝ่าย")}`,"",a(" ดูรีเพลย์ — ใช้ตัวเลขสองตัว: เลขแมตช์ + เลขเกม"),` ${o("log")} ${t("① แมตช์วันนี้ เลข #12 ต้นบรรทัดคือเลขแมตช์; 🎬 = มีรีเพลย์")}`,` ${o("history")} ${t(" เหมือนกัน แต่แสดงทุกวัน")}`,` ${o("replay 12 3")} ${t("② แมตช์ #12 เกมที่ 3 (แมตช์ละ 3 เกม: 1–3; ดูของคนอื่นได้ด้วย)")}`,` ${o("replay 12")} ${t(" ใส่แค่เลขแมตช์ = เกมที่ 1 ของแมตช์นั้น")}`,` ${o("replay 3")} ${t(" เพิ่งแข่งเสร็จ? ไม่ต้องใส่เลขแมตช์: เกมที่ 3 ของแมตช์ล่าสุด")}`,` ${o("replay")} ${t(" เกมที่คุณแพ้ในแมตช์ล่าสุด")}`,t(" ③ ระหว่างเล่น: space = หยุด · ← → = ทีละขั้น · ↑ ↓ = ความเร็ว · q = กลับห้อง"),t(' ตัวอย่าง: log แสดง "#387 … 2-4 🎬" → replay 387 1 … replay 387 6'),"",a(" ตารางคะแนน (กติกาที่ยุติธรรม)"),` ${o("Enter")} ${t("ตารางวันนี้: ดูจำนวนคนที่คุณชนะได้ (ไม่นับคนซ้ำ) ก่อน แล้วจึงดูอัตราชนะ แล้วจำนวนครั้งที่ชนะ")}`,t(' · bot คู่เวอร์ชันเดิมนับคะแนนได้วันละครั้ง แข่งซ้ำได้และดูรีเพลย์ได้ แต่จะขึ้นว่า "ไม่นับคะแนน"'),t(" ฝ่ายใดฝ่ายหนึ่งอัปโหลดเวอร์ชันใหม่ = เป็นคู่ใหม่ นับคะแนนอีกครั้ง"),t(" · แผนที่ของแต่ละวันถูกกำหนดไว้แล้ว: คู่เวอร์ชันเดิมแข่งซ้ำ ผลจะเหมือนเดิมทุกครั้ง"),t(" · สองบัญชีบนเครื่องเดียวกันท้าแข่งกันไม่ได้ — PvP ต้องเป็นคนสองคน"),` ${o("help")} ${t("ดูแผงขั้นตอนอีกครั้ง")} ${o("guide")} ${t("ดูคู่มือนี้อีกครั้ง")} ${o("card")} ${t("กลับไปที่การ์ด")}`,"",t(" กติกาสั้น ๆ: ทำลาย warren ของอีกฝ่ายได้ก็ชนะ ถ้าครบ 200 เทิร์น ฝ่ายที่มีมูลค่ากองทัพสูงกว่าชนะ"),t(" (นับเฉพาะทหารที่ยังรอด ไม่นับ wombat และ warren) ต่างกันไม่ถึง 5% ถือว่าเสมอ ท้าแข่งได้ทุก 30 วินาที"),t(" ถ้าเห็นข้อความว่าเครื่องตัดสินไม่พร้อม รอสักหนึ่งนาทีแล้วลอง vs อีกครั้ง")]:[a(` BattleAI PvP room · how to play (room ${e})`),"",a(" Open or join a room"),` ${o("room new Mock")} ${t("open a room → pick a nickname → you get a 4-letter code → give it to the others")}`,` ${o("room KZE6")} ${t("join a friend's room (nickname once)")}`,` ${o("room open")} ${t("the public room, anyone can enter; its table resets every day")}`,"",a(" Inside the room (prompt icoa room XXXX>) — the panel on entry ticks the steps you did"),` ${o("template")} ${t("puts the 15-knob starter at ~/.icoa/battleai/bot.py (pro: template code = blank code template)")}`,` ${o("knobs")} ${t("list the 15 knobs with their meaning")}`,` ${o("set WOMBATS 6")} ${t("change one knob, no coding (rookie way)")}`,` ${o("edit")} ${t("open bot.py in vi and rewrite act(u) below the knobs (pro way; i = type, Esc, :wq = save)")}`,` ${o("run")} ${t("one local test game, just to see that the bot runs (optional, needs python setup)")}`,` ${o("up")} ${t("upload your bot → v1, v2, v3 … (each upload is a new version; tagged 🔧 knobs or ✍ code)")}`,` ${o("vs <nick>")} ${t("challenge someone: one game on each of the 3 maps (symmetric maps, swapping seats changes nothing), result in 5–30 s")}`,` ${o("vs <nick> v1")} ${t("play an older version of their bot")}`,"",a(" Watching replays — two numbers: match # + game #"),` ${o("log")} ${t("① today's matches; #12 at the line start = match number; 🎬 = replay kept")}`,` ${o("history")} ${t(" the same, for all days")}`,` ${o("replay 12 3")} ${t("② match #12, game 3 (3 games per match: 1–3; any player)")}`,` ${o("replay 12")} ${t(" match number only = game 1 of that match")}`,` ${o("replay 3")} ${t(" just played? skip the match #: game 3 of your last match")}`,` ${o("replay")} ${t(" the game you lost in your last match")}`,t(" ③ while it plays: space = pause · ← → = step · ↑ ↓ = speed · q = back to the room"),t(' e.g. log shows "#387 … 2-4 🎬" → replay 387 1 … replay 387 6'),"",a(" Table (fair rules)"),` ${o("Enter")} ${t("today's table: most different people beaten first, then win rate, then wins")}`,t(' · The same two bot versions count once a day. You can play them again and watch the replays, but it says "not counted".'),t(" Either side uploading a new version makes a new pair, which counts again."),t(" · The day's maps are fixed: the same two versions always get the same result, so re-rolling luck does nothing."),t(" · Two accounts on the same computer cannot challenge each other — PvP needs two different people."),` ${o("help")} ${t("the step panel again")} ${o("guide")} ${t("this sheet again")} ${o("card")} ${t("back to your card")}`,"",t(" Rule in one line: destroy their warren to win; at the 200-turn cap the higher army value wins (live soldiers; wombats and warrens do not count), within 5% is a draw. One challenge every 30 s."),t(' 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 g=i.interactive??y,p=f(),w=n.trim();let _="",E="",O=!1;const N=/^(?:new|create|开房|建)(?:\s+(.*))?$/i.exec(w),j=/^(?:join|enter|进)\s+(\S+)$/i.exec(w);if(N?(O=!0,E=(N[1]??"").trim().slice(0,40)):j?_=j[1]:/^(?:open|public|公共)$/i.test(w)?_="OPEN":w&&(_=w),_=_.toUpperCase().replace(/[^A-Z0-9]/g,"").slice(0,8),!O&&!_){const o=await b("status",e);if(!o.ok||!o.data)return console.log(chalk.yellow(" "+R(o.code,o.message))),"back";if(console.log(),console.log(chalk.bold.white(v(" ⚔ PvP rooms"," ⚔ PvP 房间"," ⚔ ห้อง PvP"))+chalk.gray(v(" — your bot against real people, one game on each of 3 maps."," —— 和真人的 bot 对打,3 张图定胜负。"," — bot ของคุณสู้กับ bot ของคนจริง แผนที่ละ 1 เกม รวม 3 แผนที่"))),o.data.rooms.length)for(const e of o.data.rooms)console.log(` ${chalk.bold.cyan(P(e.room,6))} ${chalk.white(e.public?v("open room","公共房","ห้องสาธารณะ"):e.label||"")} ${chalk.gray(`· ${e.members} ${v("members","人","คน")} · ${v("you are","你是","คุณคือ")} ${e.nick}`)}`);else console.log(chalk.gray(v(" You are not in any room yet."," 你还没进过任何房间。"," คุณยังไม่ได้อยู่ในห้องใดเลย")));return console.log(chalk.gray(" ▸ ")+chalk.bold.green("room open")+chalk.gray(v(" the public room — anyone can enter, the table resets daily"," 公共房,谁都能进,战绩每天清零"," ห้องสาธารณะ — ใครก็เข้าได้ ตารางคะแนนเริ่มใหม่ทุกวัน"))),console.log(chalk.gray(" ▸ ")+chalk.bold.green("room <CODE>")+chalk.gray(v(" enter a friend's room (4-letter code)"," 进朋友开的房(4 个字母)"," เข้าห้องของเพื่อน (รหัส 4 ตัวอักษร)"))),console.log(chalk.gray(" ▸ ")+chalk.bold.green(v("room new [label]","room new [名字]","room new [ชื่อห้อง]"))+chalk.gray(v(" open a new room and hand out its code"," 开一个新房,把房号发给朋友"," เปิดห้องใหม่แล้วส่งรหัสให้เพื่อน"))),"back"}let x=null;if(O){const o=await C(t);if(null===o)return"back";const a=await b("create",e,{label:E,nick:o});if(!a.ok||!a.data)return console.log(chalk.yellow(" "+R(a.code,a.message))),"back";x=a.data,console.log(chalk.green(" ✓ ")+chalk.white(v("Room created — the code is ","房间开好了,房号 ","สร้างห้องแล้ว — รหัสห้องคือ "))+chalk.bold.cyan(x.room)+chalk.white(v(`. Friends type room ${x.room}`,`,告诉朋友敲 room ${x.room}`,` ให้เพื่อนพิมพ์ room ${x.room}`)))}else{const o=await b("status",e,{room:_});if(o.ok&&o.data)x=o.data;else{if("not_member"!==o.code)return console.log(chalk.yellow(" "+R(o.code,o.message))),"back";{const o=await b("join",e,{room:_,probe:!0}),a=o.data?.label||("OPEN"===_?v("Open room","公共房","ห้องสาธารณะ"):_);console.log(chalk.white(` ${v("Entering","进入","กำลังเข้าห้อง")} `)+chalk.bold.cyan(_)+chalk.gray(` ${a} · ${o.data?.members??"?"} ${v("members","人","คน")}`));for(let o=0;o<4&&!x;o++){const o=await C(t);if(null===o)return"back";const a=await b("join",e,{room:_,nick:o});a.ok&&a.data?x=a.data:console.log(chalk.yellow(" "+R(a.code,a.message)))}if(!x)return"back"}}}const L=x.room,U=chalk.bold.magenta(`icoa room ${L}> `);let X=x.recent.find(e=>e.mine&&e.replays)?.id??0;for(o(DEFAULT_PVP_BOT)||x.my_versions.length||I(L),q(x),console.log(chalk.gray(" ▸ ")+chalk.bold.green("guide")+chalk.gray(v(" the full command sheet"," 完整命令说明"," คู่มือคำสั่งฉบับเต็ม"))),A(x),T(L);;){const n=await t(U);if(null===n)return"back";const i=n.trim(),y=i.toLowerCase();if(k.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:L});o.ok&&o.data?A(o.data):console.log(chalk.yellow(" "+R(o.code,o.message)));continue}if("guide"===y||"howto"===y||"how"===y||"说明"===y||"教程"===y||"คู่มือ"===y){I(L),T(L);continue}if("help"===y||"?"===y){const o=await b("status",e,{room:L});q(o.ok&&o.data?o.data:null),T(L);continue}if("log"===y||"matches"===y||"history"===y){const o=await b("matches",e,{room:L,limit:20,all:"history"===y});if(o.ok&&o.data){o.data.matches.length||console.log(chalk.gray(v(" No matches today yet."," 今天还没有对局。"," วันนี้ยังไม่มีแมตช์")));for(const e of o.data.matches)console.log(" "+B(e));o.data.matches.length&&console.log(chalk.gray(v(" 🎬 = replays kept: replay <match#> <game#>, e.g. replay 12 3; matches without 🎬 were played before replays were stored."," 🎬 = 有回放:replay <场号> <局号>,例如 replay 12 3;没有 🎬 的是旧对局,回放没存。"," 🎬 = มีรีเพลย์: replay <เลขแมตช์> <เลขเกม> เช่น replay 12 3; แมตช์ที่ไม่มี 🎬 แข่งก่อนที่จะเริ่มเก็บรีเพลย์"))),o.data.matches.some(e=>0===e.counted)&&console.log(chalk.gray(v(" not counted = the same two bot versions already played that day; up a new version to count again."," 不计分 = 这一对 bot 版本当天已经打过;上传新版本才重新计分。"," ไม่นับคะแนน = bot คู่เวอร์ชันนี้แข่งกันไปแล้ววันนั้น; up เวอร์ชันใหม่เพื่อให้นับคะแนนอีกครั้ง")))}else console.log(chalk.yellow(" "+R(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(v(" 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."," 回放已存在本机,但播放器需要 Python 3.12 和竞技场引擎:先 python setup,再进一次 battle(它会下载引擎),回来再敲 replay。"," บันทึกรีเพลย์ไว้ในเครื่องแล้ว แต่ตัวเล่นต้องใช้ Python 3.12 และ arena engine: รัน python setup แล้วเปิด battle หนึ่งครั้ง (จะดาวน์โหลด engine ให้) จากนั้นพิมพ์ replay อีกครั้ง")));let n=0,i=0;const c=w[1]&&"last"!==w[1].toLowerCase()?Number(w[1]):0,g=w[2]?Number(w[2]):0;if(c&&g)n=c,i=g;else if(c>=1&&c<=6){if(n=X,i=c,!n){console.log(chalk.gray(v(" No match to replay yet — play one (vs), or replay <match#> <game> for a 🎬 match from log."," 还没有可回看的对局 —— 先 vs 打一场,或 replay <场号> <局> 指定 log 里带 🎬 的一场。"," ยังไม่มีแมตช์ให้ดูรีเพลย์ — แข่งก่อนหนึ่งแมตช์ (vs) หรือ replay <เลขแมตช์> <เกม> สำหรับแมตช์ที่มี 🎬 ใน log")));continue}}else c>6&&(n=c,i=1);if(n&&i){const o=await b("replay",e,{room:L,match_id:n,game:i},3e4);if(!o.ok||!o.data){console.log(chalk.yellow(" "+R(o.code,o.message)));continue}const t=m(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):h(1):"",c=e=>chalk.bold.white(e)+s(e);if(console.log(chalk.gray(` #${n} ${v(`game ${i}`,`第 ${i} 局`,`เกมที่ ${i}`)} · ${o.data.map} · `)+(t?`${h(0).trim()} ${chalk.bold.white(t.blue)}${chalk.gray(" vs ")}${h(1).trim()} ${chalk.bold.white(t.red)}`:`${c(o.data.a)}${chalk.gray(" vs ")}${c(o.data.b)}`)+chalk.gray(` · ${v("winner","胜者","ผู้ชนะ")}: `)+(r?c(r):chalk.gray(v("draw","平局","เสมอ")))),!d(o.data.replay,t)){console.log(chalk.yellow(v(" Could not save the replay."," 回放存不下来。"," บันทึกรีเพลย์ไม่ได้")));continue}await l()||a();continue}if(!o(t)){console.log(chalk.gray(v(" No replay yet — play a match (vs); then replay 3 shows game 3 and replay shows the game you lost."," 还没有回放 —— 先 vs 打一场;打完 replay 3 看第 3 局,replay 看你输的那局。"," ยังไม่มีรีเพลย์ — แข่งหนึ่งแมตช์ (vs) ก่อน แล้ว replay 3 จะแสดงเกมที่ 3 และ replay จะแสดงเกมที่คุณแพ้")));continue}await l()||a();continue}if("knobs"===y||"knob"===y||"旋钮"===y||"set"===y){const e=D();null===e?q(null):F(e);continue}const f=/^(?:set|改)\s+(.+)$/i.exec(i);if(f){const e=D();null===e?q(null):V(e,f[1]);continue}const _=/^(?:template|模板)(?:\s+(knobs?|code|新手|高手))?(?:\s+(force|覆盖))?$/i.exec(i);if(_){const e=/^(?:code|高手)$/i.test(_[1]??"")?"code":"knobs";await W(e,!!_[2],p);continue}if("edit"===y||"vi"===y||"编辑"===y){if(!o(DEFAULT_PVP_BOT)){q(null);continue}console.log(chalk.gray(v(" vi survival: i = edit · Esc = stop editing · :wq = save and leave · :q! = discard"," vi 三键: i 进入编辑 · Esc 退出编辑 · :wq 存盘离开 · :q! 放弃"," vi เบื้องต้น: i = เริ่มแก้ไข · Esc = หยุดแก้ไข · :wq = บันทึกแล้วออก · :q! = ไม่บันทึก"))),await g("vi",[...u,DEFAULT_PVP_BOT]);continue}if("run"===y||"test"===y||"play"===y||"试打"===y){if(!o(DEFAULT_PVP_BOT)){q(null);continue}if(!$().ok){console.log(chalk.gray(v(" No Python 3.12 here — python setup first; up / vs still work without it (the judge is server-side)."," 本机没有 Python 3.12,先 python setup;没有它也能 up / vs(判分在服务器)。"," เครื่องนี้ไม่มี Python 3.12 — รัน python setup ก่อน; ถึงไม่มีก็ยังใช้ up / vs ได้ (การตัดสินทำบนเซิร์ฟเวอร์)")));continue}console.log(chalk.gray(v(" One local game (the card-12 sparring bot) to see that the bot runs…"," 本地打一场(对卡 12 的陪练),看看 bot 跑不跑得起来…"," ทดลองเล่นในเครื่อง 1 เกม (กับ bot คู่ซ้อมของการ์ด 12) เพื่อดูว่า bot รันได้…"))),await c(DEFAULT_PVP_BOT,12,0,void 0,g);continue}const P=/^(?:up|upload|submit|上传)(?:\s+(.+))?$/i.exec(i);if(P){const t=(P[1]??"").trim()||DEFAULT_PVP_BOT;if(!o(t)){console.log(chalk.yellow(` ${v("Not found","找不到","ไม่พบไฟล์")}: ${t}`)),q(null);continue}let n="";try{n=a(t,"utf8")}catch{console.log(chalk.yellow(` ${v("Could not read","读不了","อ่านไฟล์ไม่ได้")}: ${t}`));continue}const r=await M(n),s=await b("upload",e,{room:L,code:n,group:r},3e4);if(!s.ok||!s.data){console.log(chalk.yellow(" "+R(s.code,s.message)));continue}s.data.unchanged?console.log(chalk.gray(v(` Identical to v${s.data.version} — nothing new.`,` 和 v${s.data.version} 一字不差,没有新版本。`,` เหมือนกับ v${s.data.version} ทุกตัวอักษร — ไม่มีเวอร์ชันใหม่`))):console.log(chalk.green(" ✓ ")+chalk.white(v(`Uploaded as v${s.data.version}`,`已上传为 v${s.data.version}`,`อัปโหลดเป็น v${s.data.version} แล้ว`))+chalk.gray(` (${t})`)+(r?chalk.gray("rookie"===r?v(" · 🔧 knobs only (rookie)"," · 🔧 新手组(只动了旋钮)"," · 🔧 แก้เฉพาะ knobs (มือใหม่)"):v(" · ✍ custom code (pro)"," · ✍ 高手组(改了代码)"," · ✍ เขียนโค้ดเอง (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(v(" ⚖ The judge is playing the 3 maps — usually 5–20 s, up to 3 min…"," ⚖ 判分机在打 3 张图,通常 5–20 秒,最多 3 分钟…"," ⚖ เครื่องตัดสินกำลังเล่น 3 แผนที่ — ปกติ 5–20 วินาที นานสุด 3 นาที…")));const t=await b("challenge",e,{room:L,nick:E[1],version:o},2e5);if(!t.ok||!t.data){console.log(chalk.yellow(" "+R(t.code,t.message)));continue}t.data.replays&&(X=t.data.match_id),S(t.data),A(t.data.status);continue}/^(?:vs|challenge|挑战|打)$/i.test(i)?console.log(chalk.gray(v(" Usage: vs <nick> or vs <nick> v2 (their version 2)"," 用法: vs <昵称> 或 vs <昵称> v2(打对方的第 2 版)"," วิธีใช้: vs <ชื่อเล่น> หรือ vs <ชื่อเล่น> v2 (เวอร์ชัน 2 ของอีกฝ่าย)"))):(console.log(chalk.gray(v(` Unknown: "${i}".`,` 不认识「${i}」。`,` ไม่รู้จักคำสั่ง "${i}"`))),T(L))}}async function C(e){for(let o=0;o<4;o++){const o=await e(chalk.bold.cyan(v(" Your nickname (2–12 letters/digits, not your real name; back to cancel): "," 你的昵称(2–12 个字母/数字,别用真名;back 取消): "," ชื่อเล่นของคุณ (ตัวอักษร/ตัวเลข 2–12 ตัว ไม่ใช่ชื่อจริง; back เพื่อยกเลิก): ")));if(null===o||k.has(o.toLowerCase()))return null;if(/^[A-Za-z0-9_\-一-鿿]{2,12}$/.test(o))return o;console.log(chalk.gray(v(" 2–12 letters, digits, _ or -; no spaces."," 2–12 个字母、数字、_ 或 -,不能有空格。"," ใช้ตัวอักษรภาษาอังกฤษ ตัวเลข _ หรือ - ได้ 2–12 ตัว ห้ามเว้นวรรค")))}return null}function R(e,o){switch(e){case"wrong_course":return v("PvP rooms are for BattleAI (battleai-48) tokens.","PvP 房间只对 BattleAI(battleai-48)token 开放。","ห้อง PvP ใช้ได้กับ token ของ BattleAI (battleai-48) เท่านั้น");case"demo_not_supported":return v("Demo tokens cannot enter PvP rooms.","demo token 进不了 PvP 房间。","token ทดลองเข้าห้อง PvP ไม่ได้");case"no_room":return v("No room with that code — check the four letters.","没有这个房号 —— 检查一下 4 个字母。","ไม่มีห้องรหัสนี้ — ตรวจสอบตัวอักษรทั้ง 4 ตัวอีกครั้ง");case"nick_taken":return v("That nickname is taken in this room — pick another.","这个昵称在房里已经有人用了,换一个。","ชื่อเล่นนี้มีคนใช้แล้วในห้องนี้ — กรุณาเลือกชื่ออื่น");case"bad_nick":return v("Nickname: 2–12 letters/digits, no spaces.","昵称要 2–12 个字母/数字,不能有空格。","ชื่อเล่น: ตัวอักษร/ตัวเลข 2–12 ตัว ห้ามเว้นวรรค");case"no_bot":return v("Upload your bot first: up","先 up 上传你的 bot。","อัปโหลด bot ของคุณก่อน: up");case"opp_no_bot":return o||v("They have no bot yet.","对方还没上传 bot。","อีกฝ่ายยังไม่มี bot");case"no_such_nick":return v("Nobody by that name here — press Enter for the table.","房里没有这个昵称 —— 回车看名单。","ไม่มีคนชื่อนี้ในห้อง — กด Enter เพื่อดูตาราง");case"self":return v("You cannot challenge yourself.","不能挑战自己。","คุณท้าแข่งกับตัวเองไม่ได้");case"same_device":return v("These two accounts are on the same device — PvP needs two different people.","这两个账号在同一台设备上 —— PvP 需要两个不同的人。","สองบัญชีนี้อยู่บนเครื่องเดียวกัน — PvP ต้องเป็นคนสองคนที่ต่างกัน");case"cooldown":return v("One challenge every 30 seconds.","每 30 秒只能发起一场。","ท้าแข่งได้ทุก 30 วินาที กรุณารอสักครู่");case"no_match":return v("No match with that number in this room — see log for the #.","这个房间里没有这个场号 —— log 看场号(#)。","ไม่มีแมตช์เลขนี้ในห้องนี้ — ดูเลข # ได้จาก log");case"no_replay":return v("No replay was kept for that match (played before replays were stored).","这一场没有存回放(是回放功能上线前打的)。","แมตช์นั้นไม่มีรีเพลย์ (แข่งก่อนที่จะเริ่มเก็บรีเพลย์)");case"bad_game":return v("The game number is 1 to 6.","局号是 1 到 6。","เลขเกมคือ 1 ถึง 6");case"no_act":return v("That file has no act(u) — is it your bot.py?","这个文件里没有 act(u) —— 确定是你的 bot.py?","ไฟล์นี้ไม่มี act(u) — ใช่ bot.py ของคุณหรือเปล่า?");case"judge_unreachable":case"judge_timeout":case"judge_rejected":case"judge_not_configured":return v("The judge is unavailable right now — try again in a minute.","判分机暂时不可用,一分钟后再试。","ตอนนี้เครื่องตัดสินยังไม่พร้อม — กรุณาลองใหม่ในอีกหนึ่งนาที");default:return o||v("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.647",
|
|
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",
|