icoa-cli 2.19.650 → 2.19.652
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/ai4ctf.js +1 -1
- package/dist/commands/ctf4ai-demo.js +1 -1
- package/dist/commands/ctf4vla.js +1 -1
- package/dist/commands/duck-cup.js +1 -0
- package/dist/commands/duck.js +1 -1
- package/dist/commands/exam.js +1 -1
- package/dist/lib/learn-cup.js +1 -1
- package/dist/lib/learn-pvp.js +1 -1
- package/package.json +1 -1
package/dist/lib/learn-cup.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{copyFileSync as e,existsSync as o,mkdirSync as t,readFileSync as n}from"node:fs";import{dirname as a,join as r}from"node:path";import chalk from"chalk";import{playSavedReplay as s,resolveEngineDir as l,saveJudgeReplay as c}from"../commands/battle.js";import{getConfig as i}from"./config.js";import{learnPost as u}from"./learn-contest.js";import{DEFAULT_PVP_BOT as d}from"./learn-pvp.js";export const CUP_RE=/^\/?(?:cup|cups|杯赛|ถ้วย)(?:\s+(.*))?$/i;const p="https://practice.icoa2026.au/assets/translations/cup/index.html?c=",g=(e,o,t)=>{const n=(()=>{const e=(i().language||"en").toLowerCase();return e.startsWith("zh")?"zh":e.startsWith("th")?"th":"en"})();return"zh"===n?o:"th"===n?t??e:e},y=(e,o,t={},n=2e4)=>u(`/api/icoa/learn/cup/${e}`,{token:o.toUpperCase(),...t},n),b=new Set(["back","b","quit","q","exit","card","lesson","卡片"]),h={1:["Round of 64","64 强","รอบ 64 ทีม"],2:["Round of 32","32 强","รอบ 32 ทีม"],3:["Round of 16","16 强","รอบ 16 ทีม"],4:["Quarter-final","8 强","รอบ 8 ทีม"],5:["Semi-final","半决赛","รอบรองชนะเลิศ"],6:["Final","决赛","รอบชิงชนะเลิศ"]},$=e=>{const o=h[e];return o?g(o[0],o[1],o[2]):`R${e}`},m=e=>({1:"R64",2:"R32",3:"R16",4:"QF",5:"SF",6:"F"}[e]??`R${e}`),f=(e,o)=>e+" ".repeat(Math.max(0,o-(e=>{let o=0;for(const t of e){const e=t.codePointAt(0)??0;e>=3633&&e<=3642&&3634!==e&&3635!==e||e>=3655&&e<=3662||(o+=e>65535||e>=11904&&e<=42191||e>=44032&&e<=55203||e>=63744&&e<=64255?2:1)}return o})(e))),w=e=>{if(!e)return chalk.gray("—");const o="filler"===e.kind?`🤖 ${e.nick}`:e.nick,t=chalk.gray(` #${e.slot}`);return e.forfeit?chalk.gray(`${o} #${e.slot} (${g("no bot","没交 bot","ไม่มี bot")})`):(e.me?chalk.bold.yellow(o):chalk.white(o))+t};function k(e,o){switch(e){case"no_cup":return g("No cup with that passcode — check it with your teacher.","没有这个口令的杯赛 —— 跟老师核对一下口令。","ไม่มีถ้วยที่ใช้รหัสนี้ — ลองถามครูอีกครั้ง");case"closed":return g("Registration is closed — the bracket has started.","报名已截止 —— 对阵已经开始。","ปิดรับสมัครแล้ว — การแข่งขันเริ่มแล้ว");case"full":return g("The cup is full (64 players).","杯赛满了(64 人)。","ถ้วยเต็มแล้ว (64 คน)");case"same_device":return g("This computer already has a player in this cup — one person, one computer, one entry.","这台电脑已经有人报名了这个杯赛 —— 一人、一台电脑、一个名额。","คอมพิวเตอร์เครื่องนี้มีผู้เล่นในถ้วยนี้แล้ว — หนึ่งคน หนึ่งเครื่อง หนึ่งสิทธิ์");case"nick_taken":return g("That nickname is taken — pick another.","这个昵称被用了 —— 换一个。","ชื่อเล่นนี้มีคนใช้แล้ว — เลือกชื่ออื่น");case"bad_nick":return g("Nickname: 2–12 letters, digits, _ or - (no spaces, not your real name).","昵称:2–12 个字母、数字、_ 或 -(不要空格,不要真名)。","ชื่อเล่น: ตัวอักษรอังกฤษ ตัวเลข _ หรือ - จำนวน 2–12 ตัว (ไม่มีช่องว่าง ไม่ใช้ชื่อจริง)");case"locked":return g("Bots were locked when round 1 started — you play with the version shown above.","第一轮开始时 bot 已锁定 —— 比赛用的是上面显示的那个版本。","bot ถูกล็อกตอนเริ่มรอบแรกแล้ว — ใช้เวอร์ชันที่แสดงด้านบน");case"has_slot":return g("You already have a number — it stays yours.","你已经有号码了,不能再换。","คุณมีหมายเลขแล้ว — เปลี่ยนไม่ได้");case"wrong_course":return g("The cup is for BattleAI tokens (battleai-48).","杯赛只对 BattleAI 课程 token(battleai-48)开放。","ถ้วยนี้สำหรับ token ของ BattleAI (battleai-48) เท่านั้น");default:return o||g("The server did not answer — try again.","服务器没有回应 —— 再试一次。","เซิร์ฟเวอร์ไม่ตอบ — ลองอีกครั้ง")}}const v=(e,o)=>{const t=e.entrants.find(e=>e.slot===o);return t?{slot:o,nick:t.nick,kind:t.kind,level:t.level,forfeit:0,me:e.me?.slot===o}:null};function _(e,o){const t=e.a?.slot===o,[n,a]=t?[e.wins_a,e.wins_b]:[e.wins_b,e.wins_a],r=e.winner_slot===o,s="tiebreak"===e.decided_by?g(" (tie-break game)","(加赛一局定胜负)"," (เกมตัดสิน)"):"value"===e.decided_by?g(" (army value)","(按兵力价值)"," (ตัดสินด้วยมูลค่ากองทัพ)"):"coin"===e.decided_by?g(" (coin toss)","(抽签)"," (เสี่ยงทาย)"):"walkover"===e.decided_by?g(" (walkover)","(对手弃权)"," (ชนะบาย)"):"admin"===e.decided_by?g(" (decided by the teacher)","(老师判定)"," (ครูตัดสิน)"):"",l=null!=n&&null!=a?` ${n}-${a}`:"";return(r?chalk.green(`✅ ${g("won","赢","ชนะ")}${l}`):chalk.red(`❌ ${g("lost","输","แพ้")}${l}`))+chalk.gray(s)}function j(e){console.log();const o="open"===e.state?g("registration open — waiting for the teacher to start","报名中 —— 等老师开赛","เปิดรับสมัคร — รอครูเริ่มการแข่งขัน"):"done"===e.state?g("finished","已结束","จบแล้ว"):`${$(e.round)} · ${"judging"===e.round_state?g("playing now","正在判","กำลังแข่ง"):"done"===e.round_state?g("done — next round when the teacher starts it","本轮结束 —— 等老师开下一轮","จบรอบแล้ว — รอครูเริ่มรอบถัดไป"):e.round_state}`;console.log(chalk.bold.white(` 🏆 ${e.label||e.cup}`)+chalk.gray(` · ${o} · ${e.players} ${g("players","人","คน")}`));const t=e.me;if(!t)return;const n=t.slot;if(console.log(chalk.white(` ${g("You","你","คุณ")}: `)+chalk.bold.yellow(t.nick)+chalk.gray(n?` #${n}`:` · ${g("no number yet","还没选号","ยังไม่ได้เลือกหมายเลข")}`)+chalk.gray(t.locked_version?` · 🔒 bot v${t.locked_version}`:t.forfeit?` · ${g("no bot — out","没交 bot,弃权","ไม่มี bot — ตกรอบ")}`:t.versions.length?` · bot v${t.versions[t.versions.length-1].version}`:` · ${g("no bot uploaded yet","还没上传 bot","ยังไม่ได้อัปโหลด bot")}`)),!n)return;const a=function(e){const o=new Set;for(const t of e.bracket)for(const e of t)if("done"===e.state)for(const t of[e.a,e.b])t&&t.slot!==e.winner_slot&&o.add(t.slot);return o}(e);let r=!t.forfeit;for(let o=1;o<=6;o++){const t=chalk.cyan(f(m(o),4)),s=e.bracket[o-1]?.find(e=>e.a?.slot===n||e.b?.slot===n);if(s){const e=s.a?.slot===n?s.b:s.a,o="done"===s.state?_(s,n):"judging"===s.state?chalk.yellow(`⚖ ${g("playing…","正在判…","กำลังแข่ง…")}`):"error"===s.state?chalk.yellow(g("⚖ judge hiccup — the teacher will re-run it","⚖ 判分出了点问题,老师会重判","⚖ ระบบตัดสินขัดข้อง — ครูจะสั่งแข่งใหม่")):chalk.gray(g("waiting","等待","รอ")),a=s.replays?chalk.gray(` 🎬 replay ${s.id} 1`):"";if(console.log(` ${t} vs ${w(e)} ${o}${a}`),"done"===s.state&&s.winner_slot!==n){r=!1,console.log(chalk.gray(g(" ↳ Your run ends here. Keep watching: bracket, and replay any game."," ↳ 你的比赛到这里结束。可以继续看:bracket 看对阵,replay 看任何一局。"," ↳ เส้นทางของคุณจบที่นี่ ดูต่อได้: bracket ดูสาย และ replay ดูเกมไหนก็ได้")));break}continue}if(!r)break;const l=1<<o-1,c=(n-1>>o<<o)+((n-1>>o-1)%2==0?l:0)+1,i=[];for(let o=c;o<c+l;o++){if(a.has(o))continue;const t=v(e,o);t&&i.push(t)}const u=i.slice(0,3).map(e=>w(e)),d=i.length>3?chalk.gray(" +"+(i.length-3)):"",p=chalk.gray(l>1?` (#${c}–${c+l-1})`:` (#${c})`);"open"===e.state&&1===o?console.log(` ${t} vs ${i.length?u.join(chalk.gray(" · ")):chalk.gray(g("a bot, drawn when round 1 starts","开赛时抽到的 bot","bot ที่จะสุ่มตอนเริ่มรอบแรก"))}${p}`):console.log(` ${t} ${chalk.gray(g("next: one of","可能对上:","อาจเจอ:"))} ${i.length?u.join(chalk.gray(" · "))+d:chalk.gray("open"===e.state?g("bots, drawn at the start","开赛时抽到的 bot","bot ที่จะสุ่มตอนเริ่ม"):g("(to be decided)","(待定)","(ยังไม่รู้)"))}${p}`)}if("done"===e.state&&e.champion_slot){const o=v(e,e.champion_slot);console.log(chalk.bold.yellow(` 🏆 ${g("Champion","冠军","แชมป์")}: `)+(o?w(o):"?")+(o&&o.slot===n?chalk.bold.green(g(" — that is you! 🎉"," —— 就是你!🎉"," — คือคุณ! 🎉")):""))}}function x(e,o){const t=e.bracket[o-1];if(t){console.log(chalk.bold.white(` ${$(o)}`)+chalk.gray(` · ${t.length} ${g("matches","场","นัด")}`));for(const e of t){const o="done"===e.state&&null!=e.winner_slot&&e.winner_slot===e.a?.slot,t="done"===e.state&&null!=e.winner_slot&&e.winner_slot===e.b?.slot,n="done"===e.state?null!=e.wins_a?`${e.wins_a}-${e.wins_b}`:"walkover"===e.decided_by?"w/o":"—":"judging"===e.state?"⚖":"…",a=e.a?"filler"===e.a.kind?`🤖${e.a.nick}`:e.a.nick:"—",r=e.b?"filler"===e.b.kind?`🤖${e.b.nick}`:e.b.nick:"—",s=e.a?.me||e.b?.me,l=(o,t)=>t?chalk.bold.green(o):"done"===e.state?chalk.gray(o):chalk.white(o);console.log(` ${chalk.gray(f(`#${e.id}`,6))}${l(f(a,14),o)} ${chalk.cyan(f(n,5))} ${l(f(r,14),t)}`+(e.replays?chalk.gray(" 🎬"):"")+(s?chalk.yellow(g(" ← you"," ← 你"," ← คุณ")):""))}console.log(chalk.gray(g(" replay <match#> <game#> — e.g. replay 41 1"," replay <场号> <局号> —— 例如 replay 41 1"," replay <เลขนัด> <เลขเกม> — เช่น replay 41 1")))}else console.log(chalk.gray(g(` ${$(o)} has not started yet.`,` ${$(o)}还没开始。`,` ${$(o)} ยังไม่เริ่ม`)))}function N(e){const o=e.entrants.filter(e=>"player"===e.kind);console.log(chalk.bold.white(` ${o.length} ${g("players","人报名","ผู้เล่น")}`));const t=o.map(e=>`${e.nick}${e.slot?`#${e.slot}`:""}${e.has_bot?" ✓":""}`);for(let e=0;e<t.length;e+=3)console.log(" "+t.slice(e,e+3).map(e=>chalk.white(f(e,20))).join(""));console.log(chalk.gray(g(" ✓ = bot uploaded"," ✓ = 已上传 bot"," ✓ = อัปโหลด bot แล้ว")))}function R(e){const o=new Set(e.entrants.map(e=>e.slot).filter(e=>!!e)),t=[];for(let e=1;e<=64;e++)o.has(e)||t.push(e);return t}function C(e){const o=e.me,t=e=>chalk.bold.green(e),n=(e,o)=>console.log(` ${e?"✅":"⬜"} ${e?chalk.gray(o):chalk.white(o)}`),a=!!o?.versions.length;console.log(),n(!!o,g(`joined as ${o?.nick??"?"}`,`已报名,昵称 ${o?.nick??"?"}`,`สมัครแล้ว ชื่อ ${o?.nick??"?"}`)),n(!!o?.slot,o?.slot?g(`your number: #${o.slot}`,`你的号码:#${o.slot}`,`หมายเลขของคุณ: #${o.slot}`):g("pick a number 1–64: pick 17 (or pick random)","选一个 1–64 的号码:pick 17(或 pick random)","เลือกหมายเลข 1–64: pick 17 (หรือ pick random)")),n(a||!!o?.locked_version,o?.locked_version?g(`bot locked: v${o.locked_version}`,`bot 已锁定:v${o.locked_version}`,`bot ถูกล็อก: v${o.locked_version}`):a?g(`bot uploaded: v${o.versions[o.versions.length-1].version} (up again to replace it before the start)`,`已上传 bot:v${o.versions[o.versions.length-1].version}(开赛前可以再 up 换掉)`,`อัปโหลด bot แล้ว: v${o.versions[o.versions.length-1].version} (ก่อนเริ่มแข่ง up ใหม่เพื่อเปลี่ยนได้)`):g("upload your bot: up (no bot by round 1 = you forfeit)","上传 bot:up(第一轮开始还没交 = 弃权)","อัปโหลด bot: up (ไม่มี bot ตอนเริ่มรอบแรก = สละสิทธิ์)")),n("open"!==e.state,"open"===e.state?g("wait for the teacher to start round 1","等老师开第一轮","รอครูเริ่มรอบแรก"):g("the bracket has started","对阵已开始","การแข่งขันเริ่มแล้ว"));const r=o?.slot?a||"open"!==e.state?"path":"up":"pick 17";console.log(chalk.gray(" ▸ ")+chalk.gray(g("next: ","下一步敲 ","พิมพ์ต่อไป: "))+t(r)+chalk.gray(" ▸ ")+t("guide")+chalk.gray(g(" the full sheet"," 完整说明"," คำอธิบายทั้งหมด")))}function T(e){console.log(chalk.gray(g(` Enter = my path · pick N · up · bracket [round] · who · replay <match#> <game#> · web · guide · card (${e})`,` 回车 = 我的路线 · pick N 选号 · up 上传 · bracket [轮] · who · replay <场号> <局号> · web · guide · card 回卡片 (${e})`,` Enter = เส้นทางของฉัน · pick N · up · bracket [รอบ] · who · replay <นัด> <เกม> · web · guide · card (${e})`)))}function A(e){const o=e=>chalk.bold.green(e),t=e=>chalk.gray(e);console.log(),console.log(chalk.bold.white(g(" 🏆 The cup — how it works"," 🏆 杯赛怎么玩"," 🏆 ถ้วย — เล่นอย่างไร")));const n=[["pick 17","choose your number 1–64. Taken? pick again; the 2nd clash → the system draws one for you","选 1–64 的号码。被占了就再选;第二次撞号由系统随机给一个","เลือกหมายเลข 1–64 ถ้ามีคนใช้แล้วให้เลือกใหม่ ชนครั้งที่ 2 ระบบสุ่มให้"],["pick random","let the system draw a free number","让系统随机给一个空号","ให้ระบบสุ่มหมายเลขว่างให้"],["up","upload ~/.icoa/battleai/bot.py (the same bot as in room). Again = replace it, until round 1 starts","上传 ~/.icoa/battleai/bot.py(和 room 里同一个 bot)。开赛前可以反复上传替换","อัปโหลด ~/.icoa/battleai/bot.py (bot เดียวกับใน room) อัปโหลดซ้ำเพื่อเปลี่ยนได้จนกว่ารอบแรกจะเริ่ม"],["template","no bot.py yet? this puts the 15-knob starter there","还没有 bot.py?这条命令放一个 15 个旋钮的起步 bot","ยังไม่มี bot.py? คำสั่งนี้จะสร้าง bot เริ่มต้นแบบ 15 knobs ให้"],["Enter","your path: this round, who you may meet next, the road to the final","我的路线:这一轮对手、下一轮可能遇到谁、到决赛的路","เส้นทางของคุณ: คู่แข่งรอบนี้ ใครที่อาจเจอต่อไป และทางสู่รอบชิง"],["bracket 2","one round, every match (no number = the current round)","看某一轮的全部对阵(不写 = 当前轮)","ดูทุกนัดของรอบหนึ่ง (ไม่ใส่เลข = รอบปัจจุบัน)"],["replay 41 1","watch match #41, game 1 (any match, any player)","看第 41 场的第 1 局(任何一场都能看)","ดูนัด #41 เกมที่ 1 (ดูได้ทุกนัด)"],["who","everyone in the cup and their numbers","所有报名的人和号码","ผู้เล่นทุกคนและหมายเลข"],["web","the address of the big bracket page (projector / phone)","大屏 / 手机看的对阵图网址","ที่อยู่หน้าสายการแข่งขัน (โปรเจกเตอร์ / มือถือ)"]];for(const[e,a,r,s]of n)console.log(` ${o(f(e,13))}${t(g(a,r,s))}`);console.log(),console.log(chalk.bold.white(g(" Rules"," 规则"," กติกา")));for(const[e,o,n]of[["64 numbers. Numbers nobody picked get graded bots (gauntlet levels, spread across the bracket).","64 个号码。没人选的号码由分档 bot 补上(挑战台等级,分散在整个对阵里)。","64 หมายเลข หมายเลขที่ไม่มีใครเลือกจะใส่ bot ตามระดับ (กระจายทั่วสาย)"],["Knock-out: R64 → R32 → R16 → QF → SF → Final. The teacher starts every round.","单败淘汰:64 → 32 → 16 → 8 → 4 → 决赛。每一轮由老师开始。","แพ้คัดออก: 64 → 32 → 16 → 8 → 4 → ชิง ครูเป็นคนเริ่มทุกรอบ"],["A match = one game on each of the 3 maps; more game wins goes through.","一场 = 3 张图各一局,赢得局数多的晋级。","หนึ่งนัด = 3 แผนที่ แผนที่ละ 1 เกม ชนะมากกว่าได้ไปต่อ"],["Level after 3 games → one tie-break game; still level → higher army value.","3 局打平 → 加赛一局;还是平 → 比兵力价值。","เสมอหลัง 3 เกม → เกมตัดสิน 1 เกม ยังเสมอ → เทียบมูลค่ากองทัพ"],["Your bot is LOCKED when round 1 starts. No bot by then = you forfeit.","第一轮开始时 bot 锁定。那时还没上传 = 弃权。","bot จะถูกล็อกตอนเริ่มรอบแรก ถ้ายังไม่อัปโหลด = สละสิทธิ์"],["One person, one computer, one entry: a second account on the same computer is refused.","一人、一台电脑、一个名额:同一台电脑的第二个账号进不来。","หนึ่งคน หนึ่งเครื่อง หนึ่งสิทธิ์: บัญชีที่สองบนเครื่องเดียวกันจะถูกปฏิเสธ"]])console.log(t(` · ${g(e,o,n)}`));console.log(t(` ${g("Big bracket","大屏对阵图","สายการแข่งขัน")}: ${p}${e}`))}async function K(e){for(;;){const o=await e(chalk.white(g(" Nickname for the cup (2–12 letters/digits, not your real name; empty = cancel): "," 杯赛昵称(2–12 个字母/数字,不要真名;直接回车 = 取消):"," ชื่อเล่นในถ้วย (2–12 ตัวอักษร/ตัวเลข ไม่ใช่ชื่อจริง; Enter เปล่า = ยกเลิก): ")));if(null===o)return null;const t=o.trim();if(!t)return null;if(/^[A-Za-z0-9_-]{2,12}$/.test(t))return t;console.log(chalk.yellow(" "+k("bad_nick")))}}async function B(){if(o(d))return!0;const n=await l();if(!n)return!1;const s=r(n,"bots","knob_bot.py");return!!o(s)&&(t(a(d),{recursive:!0}),e(s,d),!0)}export async function runCup(e,t,a,r={}){const l=a.trim().replace(/^(?:join|enter|进)\s+/i,"").toUpperCase().replace(/[^A-Z0-9]/g,"").slice(0,8);if(!l){const o=await y("status",e);if(console.log(),console.log(chalk.bold.white(g(" 🏆 Cup"," 🏆 杯赛"," 🏆 ถ้วย"))+chalk.gray(g(" — the 64-player knock-out your teacher runs."," —— 老师组织的 64 强淘汰赛。"," — การแข่งแบบแพ้คัดออก 64 คนที่ครูจัด"))),o.ok&&o.data?.cups.length)for(const e of o.data.cups)console.log(` ${chalk.bold.cyan(f(e.cup,7))} ${chalk.white(f(e.label||"",22))} ${chalk.gray(`${g("you are","你是","คุณคือ")} ${e.nick}`)}`);else o.ok||console.log(chalk.yellow(" "+k(o.code,o.message)));return console.log(chalk.gray(" ▸ ")+chalk.bold.green(g("cup <PASSCODE>","cup <口令>","cup <รหัส>"))+chalk.gray(g(" the passcode comes from your teacher, e.g. cup BKK26"," 口令由老师给,例如 cup BKK26"," รหัสได้จากครู เช่น cup BKK26"))),"back"}let i=null;const u=await y("status",e,{cup:l});if(u.ok&&u.data)i=u.data;else{if("not_member"!==u.code)return console.log(chalk.yellow(" "+k(u.code,u.message))),"back";{const o=await y("join",e,{cup:l,probe:!0});if(!o.ok||!o.data)return console.log(chalk.yellow(" "+k(o.code,o.message))),"back";if(console.log(chalk.white(` ${g("Entering","进入","กำลังเข้า")} `)+chalk.bold.cyan(l)+chalk.gray(` ${o.data.label||""} · ${o.data.players} ${g("players","人","คน")}`)),"open"!==o.data.state)return console.log(chalk.yellow(" "+k("closed"))+chalk.gray(` ${p}${l}`)),"back";for(let o=0;o<4&&!i;o++){const o=await K(t);if(null===o)return"back";const n=await y("join",e,{cup:l,nick:o});if(n.ok&&n.data)i=n.data;else if(console.log(chalk.yellow(" "+k(n.code,n.message))),"same_device"===n.code||"closed"===n.code||"full"===n.code)return"back"}if(!i)return"back";A(l)}}const h=chalk.bold.magenta(`icoa cup ${l}> `),$=async()=>{const o=await y("status",e,{cup:l});return o.ok&&o.data?(i=o.data,!0):(console.log(chalk.yellow(" "+k(o.code,o.message))),!1)};for(C(i),j(i),T(l);;){const a=await t(h);if(null===a)return"back";const u=a.trim(),m=u.toLowerCase();if(b.has(m))return"back";if("menu"===m)return"menu";if(!u||/^(?:path|me|route|路线|เส้นทาง|status)$/i.test(u)){await $()&&j(i),T(l);continue}if(/^(?:help|\?|帮助|ช่วย)$/i.test(u)){await $(),C(i),T(l);continue}if(/^(?:guide|说明|คู่มือ)$/i.test(u)){A(l),T(l);continue}if(/^(?:web|url|网页)$/i.test(u)){console.log(chalk.white(` ${p}${l}`)+chalk.gray(g(" — open on the projector or a phone"," —— 投影或手机打开"," — เปิดบนโปรเจกเตอร์หรือมือถือ")));continue}if(/^(?:who|players|名单|ผู้เล่น)$/i.test(u)){await $()&&N(i);continue}const f=/^(?:bracket|round|对阵|สาย)(?:\s+(\d))?$/i.exec(u);if(f){await $()&&x(i,f[1]?Number(f[1]):Math.max(1,i.round));continue}const w=/^(?:pick|选|เลือก)(?:\s+(random|\d+))?$/i.exec(u);if(w){if(!w[1]){console.log(chalk.gray(g(" pick 17 — a number 1–64, or pick random"," pick 17 —— 1–64 的号码,或 pick random"," pick 17 — หมายเลข 1–64 หรือ pick random")));continue}const o="random"===w[1].toLowerCase(),t=await y("pick",e,o?{cup:l,random:!0}:{cup:l,slot:Number(w[1])});if(t.ok&&t.data){i=t.data;const e=t.data.how??"";console.log(chalk.green(" ✓ ")+chalk.white(g(`Your number is #${t.data.picked}`,`你的号码是 #${t.data.picked}`,`หมายเลขของคุณคือ #${t.data.picked}`))+chalk.gray("random_after_2_clashes"===e?g(" — second clash, so the system drew it for you"," —— 第二次撞号,系统帮你抽的"," — ชนเป็นครั้งที่สอง ระบบจึงสุ่มให้"):"random_asked"===e?g(" — drawn at random"," —— 随机抽的"," — สุ่มให้"):"")),C(i)}else if("taken"===t.code){if(console.log(chalk.yellow(g(" That number is taken — pick another one. Next clash: the system picks for you."," 这个号码被占了 —— 再选一个。再撞一次就由系统随机给。"," หมายเลขนี้มีคนใช้แล้ว — เลือกใหม่ ถ้าชนอีกครั้งระบบจะสุ่มให้"))),await $()){const e=R(i);console.log(chalk.gray(` ${g("free","空号","ว่าง")}: ${e.slice(0,24).join(" ")}${e.length>24?" …":""}`))}}else console.log(chalk.yellow(" "+k(t.code,t.message)));continue}if(/^(?:template|模板)$/i.test(u)){o(d)?console.log(chalk.gray(` ${g("Already there","已经有了","มีอยู่แล้ว")}: ${d}`)):await B()?console.log(chalk.green(" ✓ ")+chalk.white(`${d}`)+chalk.gray(g(" — now type up"," —— 接着敲 up"," — พิมพ์ up ต่อได้เลย"))):console.log(chalk.yellow(g(" The arena engine is not on this computer yet: type card, then battle once (it downloads the engine), then come back."," 这台电脑还没有竞技场引擎:先 card 回卡片,进一次 battle(会下载引擎),再回来。"," เครื่องนี้ยังไม่มี engine: พิมพ์ card แล้วเข้า battle หนึ่งครั้ง (จะดาวน์โหลด engine) แล้วกลับมา")));continue}const v=/^(?:up|upload|submit|上传|อัปโหลด)(?:\s+(.+))?$/i.exec(u);if(v){const t=(v[1]??"").trim()||d;if(!o(t)){console.log(chalk.yellow(` ${g("Not found","找不到","ไม่พบไฟล์")}: ${t}`)+chalk.gray(g(" — type template first"," —— 先敲 template"," — พิมพ์ template ก่อน")));continue}let a="";try{a=n(t,"utf8")}catch{console.log(chalk.yellow(` ${g("Could not read","读不了","อ่านไฟล์ไม่ได้")}: ${t}`));continue}const r=await y("upload",e,{cup:l,code:a},3e4);if(!r.ok||!r.data){console.log(chalk.yellow(" "+k(r.code,r.message)));continue}console.log(r.data.unchanged?chalk.gray(g(` Identical to v${r.data.version} — nothing new.`,` 和 v${r.data.version} 一字不差,没有新版本。`,` เหมือนกับ v${r.data.version} ทุกตัวอักษร — ไม่มีเวอร์ชันใหม่`)):chalk.green(" ✓ ")+chalk.white(g(`Uploaded as v${r.data.version} — this is your cup bot unless you upload again before the start.`,`已上传为 v${r.data.version} —— 开赛前不再上传,比赛就用它。`,`อัปโหลดเป็น v${r.data.version} แล้ว — นี่คือ bot ที่ใช้แข่ง เว้นแต่จะอัปโหลดใหม่ก่อนเริ่ม`))),await $(),C(i);continue}const _=/^(?:replay|回放|รีเพลย์)(?:\s+(\d+))?(?:\s+(\d+))?$/i.exec(u);if(_){if(!_[1]){console.log(chalk.gray(g(" replay <match#> <game#> — e.g. replay 41 1 (match numbers are in bracket and your path)"," replay <场号> <局号> —— 例如 replay 41 1(场号在 bracket 和你的路线里)"," replay <เลขนัด> <เลขเกม> — เช่น replay 41 1 (เลขนัดอยู่ใน bracket และเส้นทางของคุณ)")));continue}const o=await y("replay",e,{cup:l,match_id:Number(_[1]),game:Number(_[2]??1)},3e4);if(!o.ok||!o.data){console.log(chalk.yellow(" "+k(o.code,o.message)));continue}const t=o.data,n=0===t.seat_a?{blue:t.a,red:t.b}:1===t.seat_a?{blue:t.b,red:t.a}:null,a="a"===t.winner?t.a:"b"===t.winner?t.b:null;if(console.log(chalk.gray(` #${_[1]} ${g("game","第","เกม")} ${_[2]??1}${g(""," 局","")}${t.tiebreak?g(" (tie-break)","(加赛)"," (เกมตัดสิน)"):""} · ${t.map} · `)+(n?`🟦 ${chalk.bold.white(n.blue)}${chalk.gray(" vs ")}🟥 ${chalk.bold.white(n.red)}`:`${t.a} vs ${t.b}`)+chalk.gray(` · ${g("winner","胜者","ผู้ชนะ")}: `)+(a?chalk.bold.white(a):chalk.gray(g("draw","平局","เสมอ")))),!c(t.replay,n)){console.log(chalk.yellow(g(" Could not save the replay."," 回放存不下来。"," บันทึกรีเพลย์ไม่ได้")));continue}console.log(chalk.gray(g(" space = pause · ← → = step · ↑ ↓ = speed · q = back to the cup"," 空格 = 暂停 · ← → = 单步 · ↑ ↓ = 速度 · q = 回到杯赛"," space = หยุด · ← → = ทีละขั้น · ↑ ↓ = ความเร็ว · q = กลับไปที่ถ้วย"))),await s(r.interactive)||console.log(chalk.yellow(g(" The replay is saved, but the viewer needs Python 3.12 and the arena engine: run python setup, open battle once, then replay again."," 回放已存在本机,但播放器需要 Python 3.12 和竞技场引擎:先 python setup,再进一次 battle,回来再 replay。"," บันทึกรีเพลย์แล้ว แต่ตัวเล่นต้องใช้ Python 3.12 และ engine: รัน python setup แล้วเข้า battle หนึ่งครั้ง จากนั้น replay อีกครั้ง")));continue}console.log(chalk.gray(g(` Not a cup command: ${u}`,` 不是杯赛命令:${u}`,` ไม่ใช่คำสั่งของถ้วย: ${u}`))),T(l)}}
|
|
1
|
+
import{existsSync as e,readFileSync as o}from"node:fs";import chalk from"chalk";import{playSavedReplay as t,resolveEngineDir as n,saveJudgeReplay as a}from"../commands/battle.js";import{getConfig as s,saveConfig as r}from"./config.js";import{learnPost as i}from"./learn-contest.js";import{DEFAULT_PVP_BOT as l,printKnobs as c,setKnob as u,writeTemplate as d}from"./learn-pvp.js";export const CUP_RE=/^\/?(?:cup|cups|杯赛|ถ้วย)(?:\s+(.*))?$/i;const p="https://practice.icoa2026.au/assets/translations/cup/index.html?c=",b=()=>{const e=(s().language||"en").toLowerCase();return e.startsWith("zh")?"zh":e.startsWith("th")?"th":"en"},y=(e,o,t)=>{const n=b();return"zh"===n?o:"th"===n?t??e:e},g=(e,o,t={},n=2e4)=>i(`/api/icoa/learn/cup/${e}`,{token:o.toUpperCase(),...t},n),h=new Set(["back","b","quit","q","exit","card","lesson","卡片"]),k={1:["Round of 64","64 强","รอบ 64 ทีม"],2:["Round of 32","32 强","รอบ 32 ทีม"],3:["Round of 16","16 强","รอบ 16 ทีม"],4:["Quarter-final","8 强","รอบ 8 ทีม"],5:["Semi-final","半决赛","รอบรองชนะเลิศ"],6:["Final","决赛","รอบชิงชนะเลิศ"]},$=e=>{const o=k[e];return o?y(o[0],o[1],o[2]):`R${e}`},m=e=>"knobs"===e?y("🔧 KNOBS cup","🔧 旋钮杯","🔧 ถ้วย KNOBS"):"code"===e?y("✍ CODE cup","✍ 代码杯","✍ ถ้วย CODE"):"",f=e=>"knobs"===e?y("this cup accepts only the 15-knob bot: template knobs, then set <KNOB> <value>, then up","这个杯赛只收 15 个旋钮的 bot:template knobs,再 set <旋钮> <值>,再 up","ถ้วยนี้รับเฉพาะ bot แบบ 15 knobs: template knobs แล้ว set <KNOB> <ค่า> แล้ว up"):"code"===e?y("any Python bot — write your own act(u) (the knob bot is fine too)","任何 Python bot 都可以 —— 自己写 act(u)(旋钮 bot 也行)","รับ bot Python แบบใดก็ได้ — เขียน act(u) เองได้ (bot แบบ knobs ก็ได้)"):"",v=e=>({1:"R64",2:"R32",3:"R16",4:"QF",5:"SF",6:"F"}[e]??`R${e}`),w=(e,o)=>e+" ".repeat(Math.max(0,o-(e=>{let o=0;for(const t of e){const e=t.codePointAt(0)??0;e>=3633&&e<=3642&&3634!==e&&3635!==e||e>=3655&&e<=3662||(o+=e>65535||e>=11904&&e<=42191||e>=44032&&e<=55203||e>=63744&&e<=64255?2:1)}return o})(e))),_=e=>{if(!e)return chalk.gray("—");const o="filler"===e.kind?`🤖 ${e.nick}`:e.nick,t=chalk.gray(` #${e.slot}`);return e.forfeit?chalk.gray(`${o} #${e.slot} (${y("no bot","没交 bot","ไม่มี bot")})`):(e.me?chalk.bold.yellow(o):chalk.white(o))+t};function N(e){if(!e?.versions.length)return null;const o=e.chosen_version;return o&&e.versions.some(e=>e.version===o)?o:e.versions[e.versions.length-1].version}function O(e){const o=e.me;if(!o?.versions.length)return void console.log(chalk.gray(y(" No versions yet — up uploads your bot as v1."," 还没有版本 —— up 上传后就是 v1。"," ยังไม่มีเวอร์ชัน — up เพื่ออัปโหลดเป็น v1")));const t=o.locked_version??N(o);console.log(),console.log(chalk.bold.white(y(" Your versions in this cup"," 你在这个杯赛的版本"," เวอร์ชันของคุณในถ้วยนี้")));for(const e of o.versions){const n=(e.ts||"").replace("T"," ").slice(5,16),a=e.version===t;console.log(` ${a?o.locked_version?"🔒":"★":" "} ${a?chalk.bold.green(`v${e.version}`):chalk.white(`v${e.version}`)}`+chalk.gray(` ${n} UTC · ${e.sha.slice(0,8)}`)+(a?chalk.green(o.locked_version?y(" ← locked, this one plays"," ← 已锁定,比赛用它"," ← ล็อกแล้ว ตัวนี้ลงแข่ง"):y(" ← your entry"," ← 正式参赛版本"," ← ตัวที่ส่งแข่ง")):""))}!o.locked_version&&"open"===e.state&&o.versions.length>1&&console.log(chalk.gray(" ▸ ")+chalk.bold.green(`use ${(o.versions.find(e=>e.version!==t)??o.versions[0]).version}`)+chalk.gray(y(" make another version your entry (until round 1 starts)"," 把另一个版本设为正式参赛版本(第一轮开始前都能换)"," เปลี่ยนเวอร์ชันที่ส่งแข่ง (เปลี่ยนได้จนกว่ารอบแรกจะเริ่ม)")))}function B(e,o){switch(e){case"no_cup":return y("No cup with that passcode — check it with your teacher.","没有这个口令的杯赛 —— 跟老师核对一下口令。","ไม่มีถ้วยที่ใช้รหัสนี้ — ลองถามครูอีกครั้ง");case"closed":return y("Registration is closed — the bracket has started.","报名已截止 —— 对阵已经开始。","ปิดรับสมัครแล้ว — การแข่งขันเริ่มแล้ว");case"full":return y("The cup is full (64 players).","杯赛满了(64 人)。","ถ้วยเต็มแล้ว (64 คน)");case"same_device":return y("This computer already has a player in this cup — one person, one computer, one entry.","这台电脑已经有人报名了这个杯赛 —— 一人、一台电脑、一个名额。","คอมพิวเตอร์เครื่องนี้มีผู้เล่นในถ้วยนี้แล้ว — หนึ่งคน หนึ่งเครื่อง หนึ่งสิทธิ์");case"nick_taken":return y("That nickname is taken — pick another.","这个昵称被用了 —— 换一个。","ชื่อเล่นนี้มีคนใช้แล้ว — เลือกชื่ออื่น");case"bad_nick":return y("Nickname: 2–12 letters, digits, _ or - (no spaces, not your real name).","昵称:2–12 个字母、数字、_ 或 -(不要空格,不要真名)。","ชื่อเล่น: ตัวอักษรอังกฤษ ตัวเลข _ หรือ - จำนวน 2–12 ตัว (ไม่มีช่องว่าง ไม่ใช้ชื่อจริง)");case"locked":return y("Bots were locked when round 1 started — you play with the version shown above.","第一轮开始时 bot 已锁定 —— 比赛用的是上面显示的那个版本。","bot ถูกล็อกตอนเริ่มรอบแรกแล้ว — ใช้เวอร์ชันที่แสดงด้านบน");case"has_slot":return y("You already have a number — it stays yours.","你已经有号码了,不能再换。","คุณมีหมายเลขแล้ว — เปลี่ยนไม่ได้");case"wrong_course":return y("The cup is for BattleAI tokens (battleai-48).","杯赛只对 BattleAI 课程 token(battleai-48)开放。","ถ้วยนี้สำหรับ token ของ BattleAI (battleai-48) เท่านั้น");default:return o||y("The server did not answer — try again.","服务器没有回应 —— 再试一次。","เซิร์ฟเวอร์ไม่ตอบ — ลองอีกครั้ง")}}const T=(e,o)=>{const t=e.entrants.find(e=>e.slot===o);return t?{slot:o,nick:t.nick,kind:t.kind,level:t.level,forfeit:0,me:e.me?.slot===o}:null};function C(e,o){const t=e.a?.slot===o,[n,a]=t?[e.wins_a,e.wins_b]:[e.wins_b,e.wins_a],s=e.winner_slot===o,r="tiebreak"===e.decided_by?y(" (tie-break game)","(加赛一局定胜负)"," (เกมตัดสิน)"):"value"===e.decided_by?y(" (army value)","(按兵力价值)"," (ตัดสินด้วยมูลค่ากองทัพ)"):"coin"===e.decided_by?y(" (coin toss)","(抽签)"," (เสี่ยงทาย)"):"walkover"===e.decided_by?y(" (walkover)","(对手弃权)"," (ชนะบาย)"):"admin"===e.decided_by?y(" (decided by the teacher)","(老师判定)"," (ครูตัดสิน)"):"",i=null!=n&&null!=a?` ${n}-${a}`:"";return(s?chalk.green(`✅ ${y("won","赢","ชนะ")}${i}`):chalk.red(`❌ ${y("lost","输","แพ้")}${i}`))+chalk.gray(r)}function x(e){console.log();const o="open"===e.state?y("registration open — waiting for the teacher to start","报名中 —— 等老师开赛","เปิดรับสมัคร — รอครูเริ่มการแข่งขัน"):"done"===e.state?y("finished","已结束","จบแล้ว"):`${$(e.round)} · ${"judging"===e.round_state?y("playing now","正在判","กำลังแข่ง"):"done"===e.round_state?y("done — next round when the teacher starts it","本轮结束 —— 等老师开下一轮","จบรอบแล้ว — รอครูเริ่มรอบถัดไป"):e.round_state}`;console.log(chalk.bold.white(` 🏆 ${e.label||e.cup}`)+(e.division?chalk.bold.cyan(` · ${m(e.division)}`):"")+chalk.gray(` · ${o} · ${e.players} ${y("players","人","คน")}`));const t=e.me;if(!t)return;const n=t.slot;if(console.log(chalk.white(` ${y("You","你","คุณ")}: `)+chalk.bold.yellow(t.nick)+chalk.gray(n?` #${n}`:` · ${y("no number yet","还没选号","ยังไม่ได้เลือกหมายเลข")}`)+chalk.gray(t.locked_version?` · 🔒 bot v${t.locked_version}`:t.forfeit?` · ${y("no bot — out","没交 bot,弃权","ไม่มี bot — ตกรอบ")}`:t.versions.length?` · bot v${t.versions[t.versions.length-1].version}`:` · ${y("no bot uploaded yet","还没上传 bot","ยังไม่ได้อัปโหลด bot")}`)),!n)return;const a=function(e){const o=new Set;for(const t of e.bracket)for(const e of t)if("done"===e.state)for(const t of[e.a,e.b])t&&t.slot!==e.winner_slot&&o.add(t.slot);return o}(e);let s=!t.forfeit;for(let o=1;o<=6;o++){const t=chalk.cyan(w(v(o),4)),r=e.bracket[o-1]?.find(e=>e.a?.slot===n||e.b?.slot===n);if(r){const e=r.a?.slot===n?r.b:r.a,o="done"===r.state?C(r,n):"judging"===r.state?chalk.yellow(`⚖ ${y("playing…","正在判…","กำลังแข่ง…")}`):"error"===r.state?chalk.yellow(y("⚖ judge hiccup — the teacher will re-run it","⚖ 判分出了点问题,老师会重判","⚖ ระบบตัดสินขัดข้อง — ครูจะสั่งแข่งใหม่")):chalk.gray(y("waiting","等待","รอ")),a=r.replays?chalk.gray(` 🎬 replay ${r.id} 1`):"";if(console.log(` ${t} vs ${_(e)} ${o}${a}`),"done"===r.state&&r.winner_slot!==n){s=!1,console.log(chalk.gray(y(" ↳ Your run ends here. Keep watching: bracket, and replay any game."," ↳ 你的比赛到这里结束。可以继续看:bracket 看对阵,replay 看任何一局。"," ↳ เส้นทางของคุณจบที่นี่ ดูต่อได้: bracket ดูสาย และ replay ดูเกมไหนก็ได้")));break}continue}if(!s)break;const i=1<<o-1,l=(n-1>>o<<o)+((n-1>>o-1)%2==0?i:0)+1,c=[];for(let o=l;o<l+i;o++){if(a.has(o))continue;const t=T(e,o);t&&c.push(t)}const u=c.slice(0,3).map(e=>_(e)),d=c.length>3?chalk.gray(" +"+(c.length-3)):"",p=chalk.gray(i>1?` (#${l}–${l+i-1})`:` (#${l})`);"open"===e.state&&1===o?console.log(` ${t} vs ${c.length?u.join(chalk.gray(" · ")):chalk.gray(y("a bot, drawn when round 1 starts","开赛时抽到的 bot","bot ที่จะสุ่มตอนเริ่มรอบแรก"))}${p}`):console.log(` ${t} ${chalk.gray(y("next: one of","可能对上:","อาจเจอ:"))} ${c.length?u.join(chalk.gray(" · "))+d:chalk.gray("open"===e.state?y("bots, drawn at the start","开赛时抽到的 bot","bot ที่จะสุ่มตอนเริ่ม"):y("(to be decided)","(待定)","(ยังไม่รู้)"))}${p}`)}if("done"===e.state&&e.champion_slot){const o=T(e,e.champion_slot);console.log(chalk.bold.yellow(` 🏆 ${y("Champion","冠军","แชมป์")}: `)+(o?_(o):"?")+(o&&o.slot===n?chalk.bold.green(y(" — that is you! 🎉"," —— 就是你!🎉"," — คือคุณ! 🎉")):""))}}function S(e,o){const t=e.bracket[o-1];if(t){console.log(chalk.bold.white(` ${$(o)}`)+chalk.gray(` · ${t.length} ${y("matches","场","นัด")}`));for(const e of t){const o="done"===e.state&&null!=e.winner_slot&&e.winner_slot===e.a?.slot,t="done"===e.state&&null!=e.winner_slot&&e.winner_slot===e.b?.slot,n="done"===e.state?null!=e.wins_a?`${e.wins_a}-${e.wins_b}`:"walkover"===e.decided_by?"w/o":"—":"judging"===e.state?"⚖":"…",a=e.a?"filler"===e.a.kind?`🤖${e.a.nick}`:e.a.nick:"—",s=e.b?"filler"===e.b.kind?`🤖${e.b.nick}`:e.b.nick:"—",r=e.a?.me||e.b?.me,i=(o,t)=>t?chalk.bold.green(o):"done"===e.state?chalk.gray(o):chalk.white(o);console.log(` ${chalk.gray(w(`#${e.id}`,6))}${i(w(a,14),o)} ${chalk.cyan(w(n,5))} ${i(w(s,14),t)}`+(e.replays?chalk.gray(" 🎬"):"")+(r?chalk.yellow(y(" ← you"," ← 你"," ← คุณ")):""))}console.log(chalk.gray(y(" replay <match#> <game#> — e.g. replay 41 1"," replay <场号> <局号> —— 例如 replay 41 1"," replay <เลขนัด> <เลขเกม> — เช่น replay 41 1")))}else console.log(chalk.gray(y(` ${$(o)} has not started yet.`,` ${$(o)}还没开始。`,` ${$(o)} ยังไม่เริ่ม`)))}function j(e){const o=e.entrants.filter(e=>"player"===e.kind);console.log(chalk.bold.white(` ${o.length} ${y("players","人报名","ผู้เล่น")}`));const t=o.map(e=>`${e.nick}${e.slot?`#${e.slot}`:""}${e.has_bot?" ✓":""}`);for(let e=0;e<t.length;e+=3)console.log(" "+t.slice(e,e+3).map(e=>chalk.white(w(e,20))).join(""));console.log(chalk.gray(y(" ✓ = bot uploaded"," ✓ = 已上传 bot"," ✓ = อัปโหลด bot แล้ว")))}function K(e){const o=new Set(e.entrants.map(e=>e.slot).filter(e=>!!e)),t=[];for(let e=1;e<=64;e++)o.has(e)||t.push(e);return t}function E(o){const t=o.me,n=e=>chalk.bold.green(e),a=(e,o)=>console.log(` ${e?"✅":"⬜"} ${e?chalk.gray(o):chalk.white(o)}`),s=!!t?.versions.length;console.log(),o.division&&console.log(` ${chalk.bold.cyan(m(o.division))}${chalk.gray(` — ${f(o.division)}`)}`),a(!!t,y(`joined as ${t?.nick??"?"}`,`已报名,昵称 ${t?.nick??"?"}`,`สมัครแล้ว ชื่อ ${t?.nick??"?"}`)),a(!!t?.slot,t?.slot?y(`your number: #${t.slot}`,`你的号码:#${t.slot}`,`หมายเลขของคุณ: #${t.slot}`):y("pick a number 1–64: pick 17 (or pick random)","选一个 1–64 的号码:pick 17(或 pick random)","เลือกหมายเลข 1–64: pick 17 (หรือ pick random)"));const r=N(t),i=t?.versions.length??0;a(s||!!t?.locked_version,t?.locked_version?y(`bot locked: v${t.locked_version}`,`bot 已锁定:v${t.locked_version}`,`bot ถูกล็อก: v${t.locked_version}`):s?y(`your entry: v${r} (${i} version${1===i?"":"s"}) — up = new version · versions = list · use N = switch`,`正式参赛版本:v${r}(共 ${i} 个版本)—— up = 新版本 · versions = 列表 · use N = 换版本`,`ตัวที่ส่งแข่ง: v${r} (มี ${i} เวอร์ชัน) — up = เวอร์ชันใหม่ · versions = รายการ · use N = เปลี่ยน`):"knobs"===o.division?y("make and upload your knob bot: template knobs → set WOMBATS 6 → up (no bot by round 1 = you forfeit)","做好并上传旋钮 bot:template knobs → set WOMBATS 6 → up(第一轮开始还没交 = 弃权)","สร้างและอัปโหลด bot แบบ knobs: template knobs → set WOMBATS 6 → up (ไม่มี bot ตอนเริ่มรอบแรก = สละสิทธิ์)"):y("upload your bot: up (no bot by round 1 = you forfeit)","上传 bot:up(第一轮开始还没交 = 弃权)","อัปโหลด bot: up (ไม่มี bot ตอนเริ่มรอบแรก = สละสิทธิ์)")),a("open"!==o.state,"open"===o.state?y("wait for the teacher to start round 1","等老师开第一轮","รอครูเริ่มรอบแรก"):y("the bracket has started","对阵已开始","การแข่งขันเริ่มแล้ว"));const c=t?.slot?s||"open"!==o.state?"path":"knobs"!==o.division||e(l)?"up":"template knobs":"pick 17";console.log(chalk.gray(" ▸ ")+chalk.gray(y("next: ","下一步敲 ","พิมพ์ต่อไป: "))+n(c)+chalk.gray(" ▸ ")+n("guide")+chalk.gray(y(" the full sheet"," 完整说明"," คำอธิบายทั้งหมด")))}function R(e,o){"knobs"===o&&console.log(chalk.gray(y(" 🔧 template knobs · knobs (see them) · set <KNOB> <value> · up"," 🔧 template knobs · knobs 看旋钮 · set <旋钮> <值> · up"," 🔧 template knobs · knobs (ดู knobs) · set <KNOB> <ค่า> · up"))),console.log(chalk.gray(y(` Enter = my path · pick N · up · versions · use N · bracket [round] · who · replay <match#> <game#> · web · guide · lang en|zh|th · card (${e})`,` 回车 = 我的路线 · pick N 选号 · up 上传 · versions 版本 · use N 选版本 · bracket [轮] · who · replay <场号> <局号> · web · guide · lang en|zh|th · card 回卡片 (${e})`,` Enter = เส้นทางของฉัน · pick N · up · versions · use N · bracket [รอบ] · who · replay <นัด> <เกม> · web · guide · lang en|zh|th · card (${e})`)))}function z(e,o){const t=e=>chalk.bold.green(e),n=e=>chalk.gray(e);console.log(),console.log(chalk.bold.white(y(" 🏆 The cup — how it works"," 🏆 杯赛怎么玩"," 🏆 ถ้วย — เล่นอย่างไร"))),o&&(console.log(` ${chalk.bold.cyan(m(o))}${n(` — ${f(o)}`)}`),console.log(n(y(" There are two cups today, each with its own passcode: 🔧 KNOBS (only the 15 knob values) and ✍ CODE (your own Python). You may enter both — one bot in each."," 今天有两个杯赛,各有自己的口令:🔧 旋钮杯(只改 15 个旋钮的值)和 ✍ 代码杯(自己写 Python)。两个都可以参加 —— 每个杯赛各交一个 bot。"," วันนี้มีสองถ้วย แต่ละถ้วยมีรหัสของตัวเอง: 🔧 KNOBS (เปลี่ยนเฉพาะค่า 15 knobs) และ ✍ CODE (เขียน Python เอง) เข้าได้ทั้งสองถ้วย — ถ้วยละหนึ่ง bot"))));const a=[["pick 17","choose your number 1–64. Taken? pick again; the 2nd clash → the system draws one for you","选 1–64 的号码。被占了就再选;第二次撞号由系统随机给一个","เลือกหมายเลข 1–64 ถ้ามีคนใช้แล้วให้เลือกใหม่ ชนครั้งที่ 2 ระบบสุ่มให้"],["pick random","let the system draw a free number","让系统随机给一个空号","ให้ระบบสุ่มหมายเลขว่างให้"],["up","upload ~/.icoa/battleai/bot.py as a NEW version (v1, v2, v3 …); the newest becomes your entry. Old versions are kept","把 ~/.icoa/battleai/bot.py 上传成一个新版本(v1、v2、v3…),最新的自动成为正式参赛版本;旧版本都保留","อัปโหลด ~/.icoa/battleai/bot.py เป็นเวอร์ชันใหม่ (v1, v2, v3 …) ตัวล่าสุดจะเป็นตัวที่ส่งแข่ง เวอร์ชันเก่ายังเก็บไว้"],["versions","list every version you uploaded; ★ = your entry","列出你上传过的所有版本;★ = 正式参赛版本","แสดงทุกเวอร์ชันที่อัปโหลด ★ = ตัวที่ส่งแข่ง"],["use 2","make v2 your entry instead (any version, as often as you like, until round 1 starts)","改用 v2 作为正式参赛版本(任意版本、换几次都行,第一轮开始前有效)","ใช้ v2 เป็นตัวที่ส่งแข่งแทน (เวอร์ชันใดก็ได้ เปลี่ยนกี่ครั้งก็ได้ จนกว่ารอบแรกจะเริ่ม)"],["lang th","switch the language: lang en · lang zh · lang th","切换语言:lang en · lang zh · lang th","เปลี่ยนภาษา: lang en · lang zh · lang th"],["template knobs","puts the 15-knob starter at bot.py (already have one? template knobs force; the old file becomes bot.py.bak)","把 15 个旋钮的起步 bot 放到 bot.py(已经有了?template knobs force,旧文件留作 bot.py.bak)","วาง bot เริ่มต้นแบบ 15 knobs ไว้ที่ bot.py (มีอยู่แล้ว? template knobs force ไฟล์เดิมจะเป็น bot.py.bak)"],["knobs","list the 15 knobs and their values","列出 15 个旋钮和现在的值","แสดง knobs ทั้ง 15 ตัวและค่าปัจจุบัน"],["set WOMBATS 6","change one knob value in bot.py (no editor needed)","改 bot.py 里一个旋钮的值(不用编辑器)","เปลี่ยนค่า knob หนึ่งตัวใน bot.py (ไม่ต้องเปิด editor)"],["Enter","your path: this round, who you may meet next, the road to the final","我的路线:这一轮对手、下一轮可能遇到谁、到决赛的路","เส้นทางของคุณ: คู่แข่งรอบนี้ ใครที่อาจเจอต่อไป และทางสู่รอบชิง"],["bracket 2","one round, every match (no number = the current round)","看某一轮的全部对阵(不写 = 当前轮)","ดูทุกนัดของรอบหนึ่ง (ไม่ใส่เลข = รอบปัจจุบัน)"],["replay 41 1","watch match #41, game 1 (any match, any player)","看第 41 场的第 1 局(任何一场都能看)","ดูนัด #41 เกมที่ 1 (ดูได้ทุกนัด)"],["who","everyone in the cup and their numbers","所有报名的人和号码","ผู้เล่นทุกคนและหมายเลข"],["web","the address of the big bracket page (projector / phone)","大屏 / 手机看的对阵图网址","ที่อยู่หน้าสายการแข่งขัน (โปรเจกเตอร์ / มือถือ)"]];for(const[e,o,s,r]of a)console.log(` ${t(w(e,16))}${n(y(o,s,r))}`);console.log(),console.log(chalk.bold.white(y(" Rules"," 规则"," กติกา")));for(const[e,o,t]of[["64 numbers. Numbers nobody picked get graded bots (gauntlet levels, spread across the bracket).","64 个号码。没人选的号码由分档 bot 补上(挑战台等级,分散在整个对阵里)。","64 หมายเลข หมายเลขที่ไม่มีใครเลือกจะใส่ bot ตามระดับ (กระจายทั่วสาย)"],["Knock-out: R64 → R32 → R16 → QF → SF → Final. The teacher starts every round.","单败淘汰:64 → 32 → 16 → 8 → 4 → 决赛。每一轮由老师开始。","แพ้คัดออก: 64 → 32 → 16 → 8 → 4 → ชิง ครูเป็นคนเริ่มทุกรอบ"],["A match = one game on each of the 3 maps; more game wins goes through.","一场 = 3 张图各一局,赢得局数多的晋级。","หนึ่งนัด = 3 แผนที่ แผนที่ละ 1 เกม ชนะมากกว่าได้ไปต่อ"],["Level after 3 games → one tie-break game; still level → higher army value.","3 局打平 → 加赛一局;还是平 → 比兵力价值。","เสมอหลัง 3 เกม → เกมตัดสิน 1 เกม ยังเสมอ → เทียบมูลค่ากองทัพ"],["Your ENTRY version (★ in versions) is LOCKED when round 1 starts. No bot by then = you forfeit.","第一轮开始时锁定你的正式参赛版本(versions 里的 ★)。那时还没上传 = 弃权。","เวอร์ชันที่ส่งแข่ง (★ ใน versions) จะถูกล็อกตอนเริ่มรอบแรก ถ้ายังไม่อัปโหลด = สละสิทธิ์"],["One person, one computer, one entry: a second account on the same computer is refused.","一人、一台电脑、一个名额:同一台电脑的第二个账号进不来。","หนึ่งคน หนึ่งเครื่อง หนึ่งสิทธิ์: บัญชีที่สองบนเครื่องเดียวกันจะถูกปฏิเสธ"]])console.log(n(` · ${y(e,o,t)}`));console.log(n(` ${y("Big bracket","大屏对阵图","สายการแข่งขัน")}: ${p}${e}`))}async function A(e){for(;;){const o=await e(chalk.white(y(" Nickname for the cup (2–12 letters/digits, not your real name; empty = cancel): "," 杯赛昵称(2–12 个字母/数字,不要真名;直接回车 = 取消):"," ชื่อเล่นในถ้วย (2–12 ตัวอักษร/ตัวเลข ไม่ใช่ชื่อจริง; Enter เปล่า = ยกเลิก): ")));if(null===o)return null;const t=o.trim();if(!t)return null;if(/^[A-Za-z0-9_-]{2,12}$/.test(t))return t;console.log(chalk.yellow(" "+B("bad_nick")))}}export async function runCup(s,i,k,$={}){const v=k.trim().replace(/^(?:join|enter|进)\s+/i,"").toUpperCase().replace(/[^A-Z0-9]/g,"").slice(0,8);if(!v){const e=await g("status",s);if(console.log(),console.log(chalk.bold.white(y(" 🏆 Cup"," 🏆 杯赛"," 🏆 ถ้วย"))+chalk.gray(y(" — the 64-player knock-out your teacher runs."," —— 老师组织的 64 强淘汰赛。"," — การแข่งแบบแพ้คัดออก 64 คนที่ครูจัด"))),e.ok&&e.data?.cups.length)for(const o of e.data.cups)console.log(` ${chalk.bold.cyan(w(o.cup,7))} ${chalk.white(w(o.label||"",22))} ${chalk.cyan(w(m(o.division),14))}${chalk.gray(`${y("you are","你是","คุณคือ")} ${o.nick}`)}`);else e.ok||console.log(chalk.yellow(" "+B(e.code,e.message)));return console.log(chalk.gray(" ▸ ")+chalk.bold.green(y("cup <PASSCODE>","cup <口令>","cup <รหัส>"))+chalk.gray(y(" the passcode comes from your teacher, e.g. cup BKK26"," 口令由老师给,例如 cup BKK26"," รหัสได้จากครู เช่น cup BKK26"))),"back"}let _=null;const N=await g("status",s,{cup:v});if(N.ok&&N.data)_=N.data;else{if("not_member"!==N.code)return console.log(chalk.yellow(" "+B(N.code,N.message))),"back";{const e=await g("join",s,{cup:v,probe:!0});if(!e.ok||!e.data)return console.log(chalk.yellow(" "+B(e.code,e.message))),"back";if(console.log(chalk.white(` ${y("Entering","进入","กำลังเข้า")} `)+chalk.bold.cyan(v)+chalk.gray(` ${e.data.label||""} · ${e.data.players} ${y("players","人","คน")}`)),e.data.division&&console.log(` ${chalk.bold.cyan(m(e.data.division))}${chalk.gray(` — ${f(e.data.division)}`)}`),"open"!==e.data.state)return console.log(chalk.yellow(" "+B("closed"))+chalk.gray(` ${p}${v}`)),"back";for(let e=0;e<4&&!_;e++){const e=await A(i);if(null===e)return"back";const o=await g("join",s,{cup:v,nick:e});if(o.ok&&o.data)_=o.data;else if(console.log(chalk.yellow(" "+B(o.code,o.message))),"same_device"===o.code||"closed"===o.code||"full"===o.code)return"back"}if(!_)return"back";z(v,_.division)}}const T=chalk.bold.magenta(`icoa cup ${v}> `),C=async()=>{const e=await g("status",s,{cup:v});return e.ok&&e.data?(_=e.data,!0):(console.log(chalk.yellow(" "+B(e.code,e.message))),!1)};for(E(_),x(_),R(v,_.division);;){const k=await i(T);if(null===k)return"back";const m=k.trim(),f=m.toLowerCase();if(h.has(f))return"back";if("menu"===f)return"menu";if(!m||/^(?:path|me|route|路线|เส้นทาง|status)$/i.test(m)){await C()&&x(_),R(v,_.division);continue}if(/^(?:help|\?|帮助|ช่วย)$/i.test(m)){await C(),E(_),R(v,_.division);continue}if(/^(?:guide|说明|คู่มือ)$/i.test(m)){z(v,_.division),R(v,_.division);continue}if(/^(?:web|url|网页)$/i.test(m)){console.log(chalk.white(` ${p}${v}`)+chalk.gray(y(" — open on the projector or a phone"," —— 投影或手机打开"," — เปิดบนโปรเจกเตอร์หรือมือถือ")));continue}if(/^(?:who|players|名单|ผู้เล่น)$/i.test(m)){await C()&&j(_);continue}const w=/^(?:bracket|round|对阵|สาย)(?:\s+(\d))?$/i.exec(m);if(w){await C()&&S(_,w[1]?Number(w[1]):Math.max(1,_.round));continue}const N=/^(?:pick|选|เลือก)(?:\s+(random|\d+))?$/i.exec(m);if(N){if(!N[1]){console.log(chalk.gray(y(" pick 17 — a number 1–64, or pick random"," pick 17 —— 1–64 的号码,或 pick random"," pick 17 — หมายเลข 1–64 หรือ pick random")));continue}const e="random"===N[1].toLowerCase(),o=await g("pick",s,e?{cup:v,random:!0}:{cup:v,slot:Number(N[1])});if(o.ok&&o.data){_=o.data;const e=o.data.how??"";console.log(chalk.green(" ✓ ")+chalk.white(y(`Your number is #${o.data.picked}`,`你的号码是 #${o.data.picked}`,`หมายเลขของคุณคือ #${o.data.picked}`))+chalk.gray("random_after_2_clashes"===e?y(" — second clash, so the system drew it for you"," —— 第二次撞号,系统帮你抽的"," — ชนเป็นครั้งที่สอง ระบบจึงสุ่มให้"):"random_asked"===e?y(" — drawn at random"," —— 随机抽的"," — สุ่มให้"):"")),E(_)}else if("taken"===o.code){if(console.log(chalk.yellow(y(" That number is taken — pick another one. Next clash: the system picks for you."," 这个号码被占了 —— 再选一个。再撞一次就由系统随机给。"," หมายเลขนี้มีคนใช้แล้ว — เลือกใหม่ ถ้าชนอีกครั้งระบบจะสุ่มให้"))),await C()){const e=K(_);console.log(chalk.gray(` ${y("free","空号","ว่าง")}: ${e.slice(0,24).join(" ")}${e.length>24?" …":""}`))}}else console.log(chalk.yellow(" "+B(o.code,o.message)));continue}const A=/^lang(?:\s+(\S+))?$/i.exec(m);if(A){const e=(A[1]??"").toLowerCase().slice(0,2);if(!["en","zh","th"].includes(e)){console.log(chalk.gray(` ${y("language","语言","ภาษา")}: `)+chalk.white(b())+chalk.gray(" · ")+["en","zh","th"].map(e=>chalk.bold.green(`lang ${e}`)).join(chalk.gray(" / ")));continue}r({language:e}),console.log(chalk.green(" ✓ ")+chalk.gray("zh"===e?"语言 → ":"th"===e?"ภาษา → ":"language → ")+chalk.white(e)),await C()&&E(_),R(v,_.division);continue}if(/^(?:versions?|版本|เวอร์ชัน)$/i.test(m)){await C()&&O(_);continue}const P=/^(?:use|用|ใช้)(?:\s+v?(\d+))?$/i.exec(m);if(P){if(!P[1]){await C()&&O(_);continue}const e=await g("use",s,{cup:v,version:Number(P[1])});if(!e.ok||!e.data){console.log(chalk.yellow(" "+("no_version"===e.code?y(`You have no v${P[1]} in this cup — type versions.`,`你在这个杯赛没有 v${P[1]} —— 敲 versions 看列表。`,`คุณไม่มี v${P[1]} ในถ้วยนี้ — พิมพ์ versions`):B(e.code,e.message))));continue}_=e.data,console.log(chalk.green(" ✓ ")+chalk.white(y(`v${P[1]} is now your entry — it is locked when round 1 starts.`,`v${P[1]} 现在是正式参赛版本 —— 第一轮开始时锁定。`,`ตอนนี้ v${P[1]} เป็นตัวที่ส่งแข่ง — จะถูกล็อกตอนเริ่มรอบแรก`))),O(_);continue}if(/^(?:knobs?|旋钮)$/i.test(m)||/^set$/i.test(m)){if(!e(l)){console.log(chalk.yellow(y(" No bot.py yet — "," 还没有 bot.py —— "," ยังไม่มี bot.py — "))+chalk.bold.green("template knobs"));continue}c(o(l,"utf8"));continue}const Y=/^(?:set|改)\s+(.+)$/i.exec(m);if(Y){if(!e(l)){console.log(chalk.yellow(y(" No bot.py yet — "," 还没有 bot.py —— "," ยังไม่มี bot.py — "))+chalk.bold.green("template knobs"));continue}u(o(l,"utf8"),Y[1],y(" next: up uploads it to the cup"," 下一步:up 上传到杯赛"," ขั้นต่อไป: up เพื่ออัปโหลดเข้าถ้วย"));continue}const D=/^(?:template|模板)(?:\s+(knobs?|code|新手|高手|旋钮|代码))?(?:\s+(force|覆盖))?$/i.exec(m);if(D){const e=/^(?:code|高手|代码)$/i.test(D[1]??"")?"code":"knobs";if("code"===e&&"knobs"===_.division){console.log(chalk.yellow(y(" This is the 🔧 KNOBS cup — it takes only the knob bot. Type template knobs (custom code goes in the ✍ CODE cup)."," 这是 🔧 旋钮杯 —— 只收旋钮 bot。请敲 template knobs(自己写的代码去 ✍ 代码杯)。"," นี่คือถ้วย 🔧 KNOBS — รับเฉพาะ bot แบบ knobs พิมพ์ template knobs (โค้ดที่เขียนเองให้ส่งในถ้วย ✍ CODE)")));continue}if(!await n()){console.log(chalk.yellow(y(" The arena engine is not on this computer yet: type card, then battle once (it downloads the engine), then come back."," 这台电脑还没有竞技场引擎:先 card 回卡片,进一次 battle(会下载引擎),再回来。"," เครื่องนี้ยังไม่มี engine: พิมพ์ card แล้วเข้า battle หนึ่งครั้ง (จะดาวน์โหลด engine) แล้วกลับมา")));continue}await d(e,!!D[2],"zh"===b())&&console.log(chalk.gray(" ▸ ")+chalk.bold.green("knobs"===e?"set WOMBATS 6":"up")+chalk.gray("knobs"===e?y(" change a knob, then up"," 改一个旋钮,然后 up"," เปลี่ยน knob แล้ว up"):y(" after you edit bot.py"," 改好 bot.py 之后"," หลังจากแก้ bot.py")));continue}const W=/^(?:up|upload|submit|上传|อัปโหลด)(?:\s+(.+))?$/i.exec(m);if(W){const t=(W[1]??"").trim()||l;if(!e(t)){console.log(chalk.yellow(` ${y("Not found","找不到","ไม่พบไฟล์")}: ${t}`)+chalk.gray(y(" — type template first"," —— 先敲 template"," — พิมพ์ template ก่อน")));continue}let n="";try{n=o(t,"utf8")}catch{console.log(chalk.yellow(` ${y("Could not read","读不了","อ่านไฟล์ไม่ได้")}: ${t}`));continue}const a=await g("upload",s,{cup:v,code:n,lang:b()},3e4);if(!a.ok||!a.data){console.log(chalk.yellow(" "+B(a.code,a.message)));continue}console.log(a.data.unchanged?chalk.gray(y(` Identical to v${a.data.version} — nothing new.`,` 和 v${a.data.version} 一字不差,没有新版本。`,` เหมือนกับ v${a.data.version} ทุกตัวอักษร — ไม่มีเวอร์ชันใหม่`)):chalk.green(" ✓ ")+chalk.white(y(`Uploaded as v${a.data.version} — it is now your entry. Older versions are kept: versions · use N.`,`已上传为 v${a.data.version} —— 现在它是正式参赛版本。旧版本都保留:versions 看列表 · use N 换回去。`,`อัปโหลดเป็น v${a.data.version} แล้ว — ตอนนี้เป็นตัวที่ส่งแข่ง เวอร์ชันเก่ายังอยู่: versions · use N`))),await C(),E(_);continue}const F=/^(?:replay|回放|รีเพลย์)(?:\s+(\d+))?(?:\s+(\d+))?$/i.exec(m);if(F){if(!F[1]){console.log(chalk.gray(y(" replay <match#> <game#> — e.g. replay 41 1 (match numbers are in bracket and your path)"," replay <场号> <局号> —— 例如 replay 41 1(场号在 bracket 和你的路线里)"," replay <เลขนัด> <เลขเกม> — เช่น replay 41 1 (เลขนัดอยู่ใน bracket และเส้นทางของคุณ)")));continue}const e=await g("replay",s,{cup:v,match_id:Number(F[1]),game:Number(F[2]??1)},3e4);if(!e.ok||!e.data){console.log(chalk.yellow(" "+B(e.code,e.message)));continue}const o=e.data,n=0===o.seat_a?{blue:o.a,red:o.b}:1===o.seat_a?{blue:o.b,red:o.a}:null,r="a"===o.winner?o.a:"b"===o.winner?o.b:null;if(console.log(chalk.gray(` #${F[1]} ${y("game","第","เกม")} ${F[2]??1}${y(""," 局","")}${o.tiebreak?y(" (tie-break)","(加赛)"," (เกมตัดสิน)"):""} · ${o.map} · `)+(n?`🟦 ${chalk.bold.white(n.blue)}${chalk.gray(" vs ")}🟥 ${chalk.bold.white(n.red)}`:`${o.a} vs ${o.b}`)+chalk.gray(` · ${y("winner","胜者","ผู้ชนะ")}: `)+(r?chalk.bold.white(r):chalk.gray(y("draw","平局","เสมอ")))),!a(o.replay,n)){console.log(chalk.yellow(y(" Could not save the replay."," 回放存不下来。"," บันทึกรีเพลย์ไม่ได้")));continue}console.log(chalk.gray(y(" space = pause · ← → = step · ↑ ↓ = speed · q = back to the cup"," 空格 = 暂停 · ← → = 单步 · ↑ ↓ = 速度 · q = 回到杯赛"," space = หยุด · ← → = ทีละขั้น · ↑ ↓ = ความเร็ว · q = กลับไปที่ถ้วย"))),await t($.interactive)||console.log(chalk.yellow(y(" The replay is saved, but the viewer needs Python 3.12 and the arena engine: run python setup, open battle once, then replay again."," 回放已存在本机,但播放器需要 Python 3.12 和竞技场引擎:先 python setup,再进一次 battle,回来再 replay。"," บันทึกรีเพลย์แล้ว แต่ตัวเล่นต้องใช้ Python 3.12 และ engine: รัน python setup แล้วเข้า battle หนึ่งครั้ง จากนั้น replay อีกครั้ง")));continue}console.log(chalk.gray(y(` Not a cup command: ${m}`,` 不是杯赛命令:${m}`,` ไม่ใช่คำสั่งของถ้วย: ${m}`))),R(v,_.division)}}
|
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 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 u}from"./python-setup.js";import{ARENA_VI_FLAGS as $}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 (ติดป้าย 🔧 / ✍ ให้เอง)")),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("game 3 of your last match · replay <#> <game>: any","最近一场第 3 局 · replay <场号> <局>: 任一场","เกมที่ 3 ล่าสุด · replay <#> <เกม>: นัดใดก็ได้"),!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-1 🎬」→ replay 387 1 … replay 387 3 逐局看"),"",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-1 🎬" → replay 387 1 … replay 387 3'),"",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-1 🎬" → replay 387 1 … replay 387 3'),"",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,y=w[2]?Number(w[2]):0;if(c&&y)n=c,i=y;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(g)||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(g)||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",[...$,DEFAULT_PVP_BOT]);continue}if("run"===y||"test"===y||"play"===y||"试打"===y){if(!o(DEFAULT_PVP_BOT)){q(null);continue}if(!u().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.","连不上服务器。","เชื่อมต่อเซิร์ฟเวอร์ไม่ได้")}}
|
|
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 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 $}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 b=e=>0===e?chalk.cyan(v(" 🟦 blue"," 🟦 蓝"," 🟦 น้ำเงิน")):chalk.red(v(" 🟥 red"," 🟥 红"," 🟥 แดง")),h=(e,o,t={},a=2e4)=>y(`/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 x(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 S=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]))]},j=(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}}export async function writeTemplate(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}export function printKnobs(e){const o=S(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((j(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")))}export function setKnob(e,o,t){const a=S(e),r=/^([A-Za-z][A-Za-z0-9_]*)\s*(?:=|\s)\s*(.+)$/.exec(o.trim());if(!r)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"))),printKnobs(e),!1;const s=r[1].toUpperCase(),l=r[2].trim();if(!a.includes(s))return console.log(chalk.yellow(v(` No knob called ${s}.`,` 没有 ${s} 这个旋钮。`,` ไม่มี knob ชื่อ ${s}`))+chalk.gray(" ")+a.map(e=>chalk.bold.white(e)).join(chalk.gray(" · "))),!1;const i=j(e,s)??"";if(i.trim()===l)return console.log(chalk.gray(" ")+chalk.bold.white(s)+chalk.gray(v(` is already ${l}.`,` 已经是 ${l} 了。`,` เป็น ${l} อยู่แล้ว`))),!1;const c=new RegExp(`^(${s}\\s*=\\s*)([^#\n]*?)(\\s*)(#.*)?$`,"m"),d=e.replace(c,(e,o,t,a,n)=>`${o}${l}${n?(a||" ")+n:""}`);return n(DEFAULT_PVP_BOT,d),console.log(chalk.gray(" ")+chalk.bold.white(s)+chalk.gray(" ")+chalk.gray(i.trim())+chalk.gray(" → ")+chalk.bold.green(l)+chalk.gray(t??v(" next: run to test, up to upload"," 下一步: run 试打,up 上传"," ขั้นต่อไป: run เพื่อทดลอง, up เพื่ออัปโหลด"))),!0}function W(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 (ติดป้าย 🔧 / ✍ ให้เอง)")),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("game 3 of your last match · replay <#> <game>: any","最近一场第 3 局 · replay <场号> <局>: 任一场","เกมที่ 3 ล่าสุด · replay <#> <เกม>: นัดใดก็ได้"),!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 F(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-1 🎬」→ replay 387 1 … replay 387 3 逐局看"),"",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-1 🎬" → replay 387 1 … replay 387 3'),"",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-1 🎬" → replay 387 1 … replay 387 3'),"",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??p,y=f(),w=n.trim();let _="",E="",O=!1;const N=/^(?:new|create|开房|建)(?:\s+(.*))?$/i.exec(w),S=/^(?:join|enter|进)\s+(\S+)$/i.exec(w);if(N?(O=!0,E=(N[1]??"").trim().slice(0,40)):S?_=S[1]:/^(?:open|public|公共)$/i.test(w)?_="OPEN":w&&(_=w),_=_.toUpperCase().replace(/[^A-Z0-9]/g,"").slice(0,8),!O&&!_){const o=await h("status",e);if(!o.ok||!o.data)return console.log(chalk.yellow(" "+K(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 j=null;if(O){const o=await V(t);if(null===o)return"back";const a=await h("create",e,{label:E,nick:o});if(!a.ok||!a.data)return console.log(chalk.yellow(" "+K(a.code,a.message))),"back";j=a.data,console.log(chalk.green(" ✓ ")+chalk.white(v("Room created — the code is ","房间开好了,房号 ","สร้างห้องแล้ว — รหัสห้องคือ "))+chalk.bold.cyan(j.room)+chalk.white(v(`. Friends type room ${j.room}`,`,告诉朋友敲 room ${j.room}`,` ให้เพื่อนพิมพ์ room ${j.room}`)))}else{const o=await h("status",e,{room:_});if(o.ok&&o.data)j=o.data;else{if("not_member"!==o.code)return console.log(chalk.yellow(" "+K(o.code,o.message))),"back";{const o=await h("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&&!j;o++){const o=await V(t);if(null===o)return"back";const a=await h("join",e,{room:_,nick:o});a.ok&&a.data?j=a.data:console.log(chalk.yellow(" "+K(a.code,a.message)))}if(!j)return"back"}}}const L=j.room,U=chalk.bold.magenta(`icoa room ${L}> `);let q=j.recent.find(e=>e.mine&&e.replays)?.id??0;for(o(DEFAULT_PVP_BOT)||j.my_versions.length||F(L),W(j),console.log(chalk.gray(" ▸ ")+chalk.bold.green("guide")+chalk.gray(v(" the full command sheet"," 完整命令说明"," คู่มือคำสั่งฉบับเต็ม"))),A(j),T(L);;){const n=await t(U);if(null===n)return"back";const i=n.trim(),p=i.toLowerCase();if(k.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 h("status",e,{room:L});o.ok&&o.data?A(o.data):console.log(chalk.yellow(" "+K(o.code,o.message)));continue}if("guide"===p||"howto"===p||"how"===p||"说明"===p||"教程"===p||"คู่มือ"===p){F(L),T(L);continue}if("help"===p||"?"===p){const o=await h("status",e,{room:L});W(o.ok&&o.data?o.data:null),T(L);continue}if("log"===p||"matches"===p||"history"===p){const o=await h("matches",e,{room:L,limit:20,all:"history"===p});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(" "+K(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,p=w[2]?Number(w[2]):0;if(c&&p)n=c,i=p;else if(c>=1&&c<=6){if(n=q,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 h("replay",e,{room:L,match_id:n,game:i},3e4);if(!o.ok||!o.data){console.log(chalk.yellow(" "+K(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?b(0):b(1):"",c=e=>chalk.bold.white(e)+s(e);if(console.log(chalk.gray(` #${n} ${v(`game ${i}`,`第 ${i} 局`,`เกมที่ ${i}`)} · ${o.data.map} · `)+(t?`${b(0).trim()} ${chalk.bold.white(t.blue)}${chalk.gray(" vs ")}${b(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(g)||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(g)||a();continue}if("knobs"===p||"knob"===p||"旋钮"===p||"set"===p){const e=D();null===e?W(null):printKnobs(e);continue}const f=/^(?:set|改)\s+(.+)$/i.exec(i);if(f){const e=D();null===e?W(null):setKnob(e,f[1]);continue}const _=/^(?:template|模板)(?:\s+(knobs?|code|新手|高手))?(?:\s+(force|覆盖))?$/i.exec(i);if(_){const e=/^(?:code|高手)$/i.test(_[1]??"")?"code":"knobs";await writeTemplate(e,!!_[2],y);continue}if("edit"===p||"vi"===p||"编辑"===p){if(!o(DEFAULT_PVP_BOT)){W(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",[...$,DEFAULT_PVP_BOT]);continue}if("run"===p||"test"===p||"play"===p||"试打"===p){if(!o(DEFAULT_PVP_BOT)){W(null);continue}if(!u().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}`)),W(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 h("upload",e,{room:L,code:n,group:r},3e4);if(!s.ok||!s.data){console.log(chalk.yellow(" "+K(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 h("challenge",e,{room:L,nick:E[1],version:o},2e5);if(!t.ok||!t.data){console.log(chalk.yellow(" "+K(t.code,t.message)));continue}t.data.replays&&(q=t.data.match_id),x(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 V(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 K(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.652",
|
|
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",
|