icoa-cli 2.19.635 → 2.19.637

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- export function localized(e,t){const r=(t||"en").slice(0,2).toLowerCase();if("en"===r)return e;const n=e[`_${r}`];if(!n||"object"!=typeof n)return e;const a=n,o={...e};for(const e of Object.keys(a))void 0!==a[e]&&(o[e]=a[e]);return o}export function overlayLanguages(e){const t=new Set;for(const r of e)for(const e of Object.keys(r)){const r=/^_([a-z]{2})$/.exec(e);r&&"en"!==r[1]&&t.add(r[1])}return[...t].sort()}export function isValidCurriculum(e){if(!e||"object"!=typeof e)return!1;const t=e;return"string"==typeof t.id&&"number"==typeof t.totalCards&&Array.isArray(t.cards)&&Array.isArray(t.modules)}import{readFileSync as e,writeFileSync as t,mkdirSync as r,existsSync as n,statSync as a,utimesSync as o}from"node:fs";import{join as i}from"node:path";import{homedir as c}from"node:os";import{getDeviceFingerprint as s}from"./access.js";import{getCliVersion as u}from"./version.js";import{httpFetch as l}from"./http.js";import{transportAdvice as d,transportCauseCode as p}from"./tls-trust.js";import{parseHometask as f}from"./learn-hometask.js";import{parseFaqs as m,parseReplies as _}from"./learn-replies.js";const g=i(c(),".icoa","learn-cache");export const LEARN_SERVER="https://practice.icoa2026.au";function y(e){const t=e.replace(/[^A-Za-z0-9_-]/g,"_");return i(g,`${t}.json`)}function h(e){const t=e.replace(/[^A-Za-z0-9_-]/g,"_");return i(g,`${t}.etag`)}let k=null;export function getLastCurriculumFailure(){return k}export async function loadCurriculumById(i,c,s=!1){"ctf4eai-12"===i&&(i="LEARNDEMO01");const l=function(t){const r=y(t);if(!n(r))return null;try{const t=a(r),n=Date.now()-t.mtimeMs;return{curriculum:JSON.parse(e(r,"utf-8")),ageMs:n}}catch{return null}}(i);if(!s&&l&&l.ageMs<6048e5&&isValidCurriculum(l.curriculum))return l.curriculum;k=null;const d=await async function(t,r){const a=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/curriculum/"+encodeURIComponent(t),o={},i=function(t){const r=h(t);if(!n(r))return null;try{return e(r,"utf-8").trim()||null}catch{return null}}(t);i&&(o["If-None-Match"]=i),r&&(o["X-Learn-Token"]=r.toUpperCase()),o["X-ICOA-Client"]=u();try{const e=await fetch(a,{headers:o,signal:AbortSignal.timeout(6e4)});if(304===e.status)return{kind:"unchanged"};if(404===e.status)return{kind:"notfound"};if(!e.ok)return{kind:"error"};const t=await e.json();if(!t.success||!t.data)return{kind:"error"};if(!isValidCurriculum(t.data))return{kind:"error"};const r=e.headers.get("etag")||e.headers.get("ETag")||void 0;return{kind:"fresh",curriculum:t.data,etag:r||void 0}}catch{return{kind:"error"}}}(i,c);return"fresh"===d.kind?(function(e,n,a){try{r(g,{recursive:!0}),t(y(e),JSON.stringify(n)),a&&t(h(e),a)}catch{}}(i,d.curriculum,d.etag),d.curriculum):"unchanged"===d.kind&&l&&isValidCurriculum(l.curriculum)?(function(e){try{const t=y(e);if(n(t)){const e=Date.now()/1e3;o(t,e,e)}}catch{}}(i),l.curriculum):l&&isValidCurriculum(l.curriculum)?l.curriculum:(k="notfound"===d.kind?"notfound":"offline",null)}export function parseUpgradedTo(e){if(!e||"object"!=typeof e)return null;const t=e,r=String(t.token??"").toUpperCase(),n=String(t.curriculum_id??"");return/^[A-Z]{2}[A-Z0-9]{8}$/.test(r)&&n?{token:r,curriculumId:n}:null}export function validateResultShape(e){return{curriculumId:String(e.curriculum_id??""),status:String(e.status??""),validUntil:String(e.valid_until??""),aiEnabled:1===e.ai_enabled||!0===e.ai_enabled,aiBasicPerCard:"number"==typeof e.ai_basic_per_card&&e.ai_basic_per_card>0?e.ai_basic_per_card:3,gated:1===e.gated||!0===e.gated,replies:_(e.replies),hometask:f(e.hometask),upgradedTo:parseUpgradedTo(e.upgraded_to)}}export async function validateEAToken(e){const t=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/validate";try{const r=await l(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:e.toUpperCase(),device_fingerprint:s()}),signal:AbortSignal.timeout(8e3)});if(!r.ok){const e=await r.json().catch(()=>({}));return{ok:!1,message:e.message||`HTTP ${r.status}`,code:e.code}}const n=await r.json();return n.success&&n.data?{ok:!0,...validateResultShape(n.data)}:{ok:!1,message:n.message||"Validation failed"}}catch(e){const t=e instanceof Error?e.message:String(e),r=p(e);return{ok:!1,message:`Network error: ${t}${r?` (${r})`:""}`,code:"network",advice:d(e)}}}export async function postFlowEvent(e,t,r,n){if("LEARNDEMO01"===e.toUpperCase())return;const a=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/flow/event";try{await l(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:e.toUpperCase(),curriculum_id:t,card:r,fork_id:n.fork_id,first:n.first,final:n.final,walls:n.walls,ms:n.ms,choices:n.choices,started_at:n.started_at,device_fingerprint:s()}),signal:AbortSignal.timeout(5e3)})}catch{}}export async function syncProgress(e,t,r){if("LEARNDEMO01"===e.toUpperCase())return;const n=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/progress/"+e.toUpperCase();try{await l(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({card_number:t.card_number,event_type:t.event_type,mcq_answer:t.mcq_answer,mcq_correct:t.mcq_correct?1:0,check_answer:t.check_answer,check_correct:t.check_correct?1:0,time_on_card_ms:t.time_on_card_ms,device_fingerprint:s(),reply_ack:r&&r.length>0?r:void 0}),signal:AbortSignal.timeout(5e3)})}catch{}}export async function postFocusSession(e,t){if("LEARNDEMO01"===e.toUpperCase())return!0;const r=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/focus/"+e.toUpperCase();try{return(await l(r,{method:"POST",headers:{"Content-Type":"application/json","X-ICOA-Client":u()},body:JSON.stringify({...t,device_fingerprint:s()}),signal:AbortSignal.timeout(5e3)})).ok}catch{return!1}}export async function fetchServerProgress(e){if("LEARNDEMO01"===e.toUpperCase())return null;const t=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/progress/"+e.toUpperCase();try{const e=await l(t,{signal:AbortSignal.timeout(5e3)});if(!e.ok)return null;const r=await e.json();return r?.success&&Array.isArray(r.data?.events)?r.data.events:null}catch{return null}}export async function fetchCardFaqs(e){const t=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/faq/"+encodeURIComponent(e.toUpperCase());try{const e=await l(t,{signal:AbortSignal.timeout(5e3)});if(!e.ok)return[];const r=await e.json();return r.success&&r.data?m(r.data.faqs):[]}catch{return[]}}export async function syncCardFeedback(e,t){const r=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/feedback/"+e.toUpperCase();try{return(await fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({curriculum_id:t.curriculum_id,card_number:t.card_number,feedback_text:t.feedback_text,time_on_card_ms:t.time_on_card_ms}),signal:AbortSignal.timeout(5e3)})).ok}catch{return!1}}
1
+ export function localized(e,t){const r=(t||"en").slice(0,2).toLowerCase();if("en"===r)return e;const n=e[`_${r}`];if(!n||"object"!=typeof n)return e;const a=n,o={...e};for(const e of Object.keys(a))void 0!==a[e]&&(o[e]=a[e]);return o}export function overlayLanguages(e){const t=new Set;for(const r of e)for(const e of Object.keys(r)){const r=/^_([a-z]{2})$/.exec(e);r&&"en"!==r[1]&&t.add(r[1])}return[...t].sort()}export function isValidCurriculum(e){if(!e||"object"!=typeof e)return!1;const t=e;return"string"==typeof t.id&&"number"==typeof t.totalCards&&Array.isArray(t.cards)&&Array.isArray(t.modules)}import{readFileSync as e,writeFileSync as t,mkdirSync as r,existsSync as n,statSync as a,utimesSync as o}from"node:fs";import{join as c}from"node:path";import{homedir as i}from"node:os";import{getDeviceFingerprint as s}from"./access.js";import{getCliVersion as u}from"./version.js";import{httpFetch as l}from"./http.js";import{transportAdvice as d,transportCauseCode as p}from"./tls-trust.js";import{parseHometask as f}from"./learn-hometask.js";import{parseFaqs as m,parseReplies as _}from"./learn-replies.js";const g=c(i(),".icoa","learn-cache");export const LEARN_SERVER=process.env.ICOA_LEARN_SERVER||"https://practice.icoa2026.au";function y(e){const t=e.replace(/[^A-Za-z0-9_-]/g,"_");return c(g,`${t}.json`)}function h(e){const t=e.replace(/[^A-Za-z0-9_-]/g,"_");return c(g,`${t}.etag`)}let E=null;export function getLastCurriculumFailure(){return E}export async function loadCurriculumById(c,i,s=!1){"ctf4eai-12"===c&&(c="LEARNDEMO01");const l=function(t){const r=y(t);if(!n(r))return null;try{const t=a(r),n=Date.now()-t.mtimeMs;return{curriculum:JSON.parse(e(r,"utf-8")),ageMs:n}}catch{return null}}(c);if(!s&&l&&l.ageMs<6048e5&&isValidCurriculum(l.curriculum))return l.curriculum;E=null;const d=await async function(t,r){const a=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/curriculum/"+encodeURIComponent(t),o={},c=function(t){const r=h(t);if(!n(r))return null;try{return e(r,"utf-8").trim()||null}catch{return null}}(t);c&&(o["If-None-Match"]=c),r&&(o["X-Learn-Token"]=r.toUpperCase()),o["X-ICOA-Client"]=u();try{const e=await fetch(a,{headers:o,signal:AbortSignal.timeout(6e4)});if(304===e.status)return{kind:"unchanged"};if(404===e.status)return{kind:"notfound"};if(!e.ok)return{kind:"error"};const t=await e.json();if(!t.success||!t.data)return{kind:"error"};if(!isValidCurriculum(t.data))return{kind:"error"};const r=e.headers.get("etag")||e.headers.get("ETag")||void 0;return{kind:"fresh",curriculum:t.data,etag:r||void 0}}catch{return{kind:"error"}}}(c,i);return"fresh"===d.kind?(function(e,n,a){try{r(g,{recursive:!0}),t(y(e),JSON.stringify(n)),a&&t(h(e),a)}catch{}}(c,d.curriculum,d.etag),d.curriculum):"unchanged"===d.kind&&l&&isValidCurriculum(l.curriculum)?(function(e){try{const t=y(e);if(n(t)){const e=Date.now()/1e3;o(t,e,e)}}catch{}}(c),l.curriculum):l&&isValidCurriculum(l.curriculum)?l.curriculum:(E="notfound"===d.kind?"notfound":"offline",null)}export function parseUpgradedTo(e){if(!e||"object"!=typeof e)return null;const t=e,r=String(t.token??"").toUpperCase(),n=String(t.curriculum_id??"");return/^[A-Z]{2}[A-Z0-9]{8}$/.test(r)&&n?{token:r,curriculumId:n}:null}export function validateResultShape(e){return{curriculumId:String(e.curriculum_id??""),status:String(e.status??""),validUntil:String(e.valid_until??""),aiEnabled:1===e.ai_enabled||!0===e.ai_enabled,aiBasicPerCard:"number"==typeof e.ai_basic_per_card&&e.ai_basic_per_card>0?e.ai_basic_per_card:3,gated:1===e.gated||!0===e.gated,replies:_(e.replies),hometask:f(e.hometask),upgradedTo:parseUpgradedTo(e.upgraded_to)}}export async function validateEAToken(e){const t=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/validate";try{const r=await l(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:e.toUpperCase(),device_fingerprint:s()}),signal:AbortSignal.timeout(8e3)});if(!r.ok){const e=await r.json().catch(()=>({}));return{ok:!1,message:e.message||`HTTP ${r.status}`,code:e.code}}const n=await r.json();return n.success&&n.data?{ok:!0,...validateResultShape(n.data)}:{ok:!1,message:n.message||"Validation failed"}}catch(e){const t=e instanceof Error?e.message:String(e),r=p(e);return{ok:!1,message:`Network error: ${t}${r?` (${r})`:""}`,code:"network",advice:d(e)}}}export async function postFlowEvent(e,t,r,n){if("LEARNDEMO01"===e.toUpperCase())return;const a=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/flow/event";try{await l(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:e.toUpperCase(),curriculum_id:t,card:r,fork_id:n.fork_id,first:n.first,final:n.final,walls:n.walls,ms:n.ms,choices:n.choices,started_at:n.started_at,device_fingerprint:s()}),signal:AbortSignal.timeout(5e3)})}catch{}}export async function syncProgress(e,t,r){if("LEARNDEMO01"===e.toUpperCase())return;const n=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/progress/"+e.toUpperCase();try{await l(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({card_number:t.card_number,event_type:t.event_type,mcq_answer:t.mcq_answer,mcq_correct:t.mcq_correct?1:0,check_answer:t.check_answer,check_correct:t.check_correct?1:0,time_on_card_ms:t.time_on_card_ms,device_fingerprint:s(),reply_ack:r&&r.length>0?r:void 0}),signal:AbortSignal.timeout(5e3)})}catch{}}export async function postFocusSession(e,t){if("LEARNDEMO01"===e.toUpperCase())return!0;const r=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/focus/"+e.toUpperCase();try{return(await l(r,{method:"POST",headers:{"Content-Type":"application/json","X-ICOA-Client":u()},body:JSON.stringify({...t,device_fingerprint:s()}),signal:AbortSignal.timeout(5e3)})).ok}catch{return!1}}export async function fetchServerProgress(e){if("LEARNDEMO01"===e.toUpperCase())return null;const t=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/progress/"+e.toUpperCase();try{const e=await l(t,{signal:AbortSignal.timeout(5e3)});if(!e.ok)return null;const r=await e.json();return r?.success&&Array.isArray(r.data?.events)?r.data.events:null}catch{return null}}export async function fetchCardFaqs(e){const t=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/faq/"+encodeURIComponent(e.toUpperCase());try{const e=await l(t,{signal:AbortSignal.timeout(5e3)});if(!e.ok)return[];const r=await e.json();return r.success&&r.data?m(r.data.faqs):[]}catch{return[]}}export async function syncCardFeedback(e,t){const r=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/feedback/"+e.toUpperCase();try{return(await fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({curriculum_id:t.curriculum_id,card_number:t.card_number,feedback_text:t.feedback_text,time_on_card_ms:t.time_on_card_ms}),signal:AbortSignal.timeout(5e3)})).ok}catch{return!1}}
@@ -0,0 +1 @@
1
+ import{copyFileSync as o,existsSync as e,mkdirSync as t,readFileSync as n,writeFileSync as r}from"node:fs";import{homedir as a}from"node:os";import{join as s}from"node:path";import chalk from"chalk";import{playSavedReplay as l,resolveEngineDir as i,runMobLocal as c,saveJudgeReplay as g}from"../commands/battle.js";import{getConfig as d}from"./config.js";import{runInteractive as u}from"./interactive-spawn.js";import{learnPost as y}from"./learn-contest.js";import{probePython as p}from"./python-setup.js";import{ARENA_VI_FLAGS as m}from"./vi-flags.js";export const PVP_RE=/^\/?(?:room|rooms|pvp|房间|对战房)(?:\s+(.*))?$/i;const b=(o,e,t={},n=2e4)=>y(`/api/icoa/learn/pvp/${o}`,{token:e.toUpperCase(),...t},n),$=()=>(d().language||"en").toLowerCase().startsWith("zh"),h=new Set(["back","b","quit","q","exit","card","lesson","卡片"]);export const DEFAULT_PVP_BOT=s(a(),".icoa","battleai","bot.py");const w=(o,e)=>o+" ".repeat(Math.max(0,e-(o=>{let e=0;for(const t of o){const o=t.codePointAt(0)??0;e+=o>65535||o>=11904&&o<=42191||o>=44032&&o<=55203||o>=63744&&o<=64255?2:1}return e})(o)));function f(o){console.log(chalk.gray($()?` knobs 看旋钮 · set <旋钮> <值> 改一个 · run 本地试打 · up 上传 · vs <昵称> 挑战 · 回车 战绩表 · log · replay · edit 改代码 · template 换模板 · card 回卡片 (${o})`:` knobs · set <KNOB> <value> · run (local test) · up (upload) · vs <nick> (challenge) · Enter (table) · log · replay · edit · template · card (${o})`))}function v(o){const e=$(),t=o.public?e?"公共房":"Open room":o.label||(e?"房间":"Room");console.log(),console.log(chalk.bold.white(` ⚔ ${t} `)+chalk.bold.cyan(o.room)+chalk.gray(` · ${e?"你是":"you are"} ${o.nick??"?"} · ${o.members.length} ${e?"人":"members"} · ${e?"今日":"today"} ${o.day}${o.public?e?" · 战绩每天清零":" · table resets daily":""}`)),console.log(chalk.gray(` ${w(e?"昵称":"nick",14)}${w("bot",6)}${w(e?"组":"group",9)}${w(e?"今日 胜-负-平":"today W-L-D",16)}${e?"累计":"all-time"}`));for(const t of o.members){const o="rookie"===t.group?e?"🔧 新手":"🔧 knobs":"pro"===t.group?e?"✍ 高手":"✍ code":"—",n=` ${w(t.nick,14)}${w(t.versions?`v${t.versions}`:"—",6)}${w(o,9)}${w(`${t.today.w}-${t.today.l}-${t.today.d}`,16)}${t.all.w}-${t.all.l}-${t.all.d}`;console.log(t.me?chalk.bold.cyan(n):chalk.white(n))}if(o.my_versions.length||console.log(chalk.yellow(e?" 你还没上传 bot —— 先 knobs / set,再 up。":" You have no bot here yet — knobs / set, then up.")),o.recent.length){console.log(chalk.gray(e?" 今日对局:":" Today:"));for(const e of o.recent.slice(0,6))console.log(chalk.gray(" ")+k(e))}}function k(o){const e="WIN"===o.verdict?chalk.green(`${o.wins_a}-${o.wins_b}`):"LOSS"===o.verdict?chalk.red(`${o.wins_a}-${o.wins_b}`):chalk.yellow(`${o.wins_a}-${o.wins_b}`),t=`${o.a} v${o.a_version??"?"} vs ${o.b} v${o.b_version??"?"}`;return`${chalk.gray((o=>{const e=new Date(o);return Number.isNaN(e.getTime())?"":`${String(e.getHours()).padStart(2,"0")}:${String(e.getMinutes()).padStart(2,"0")}`})(o.ts))} ${o.mine?chalk.white(t):chalk.gray(t)} ${e}${o.draws?chalk.gray(` (${o.draws} draw)`):""}${"WIN"!==o.verdict&&"LOSS"!==o.verdict&&"DRAW"!==o.verdict?chalk.red(` ${o.verdict}`):""}`}function _(o){const e=$();console.log();const t=`${o.wins_a}-${o.wins_b}${o.draws?` (${o.draws} ${e?"平":"draw"})`:""}`;if("WIN"===o.verdict)console.log(chalk.bold.green(` 🏆 ${e?"你赢了":"You win"} ${t}`)+chalk.gray(` vs ${o.opp.nick} v${o.opp.version}`));else if("LOSS"===o.verdict)console.log(chalk.bold.red(` ❌ ${e?"你输了":"You lose"} ${t}`)+chalk.gray(` vs ${o.opp.nick} v${o.opp.version}`));else{if("DRAW"!==o.verdict)return void console.log(chalk.yellow(` ⚠ ${e?"判分机没跑完这场":"The judge could not finish this match"}: ${o.verdict}${o.note?` — ${o.note}`:""}`));console.log(chalk.bold.yellow(` 🤝 ${e?"平局":"Draw"} ${t}`)+chalk.gray(` vs ${o.opp.nick} v${o.opp.version}`))}o.load_error?.a&&console.log(chalk.red(` ${e?"你的 bot 装不上":"Your bot failed to load"}: ${o.load_error.a}`)),o.load_error?.b&&console.log(chalk.gray(` ${e?"对方的 bot 装不上":"Opponent's bot failed to load"}: ${o.load_error.b}`)),console.log(chalk.gray(` ${w(e?"地图":"map",11)}${w(e?"席位":"seat",6)}${w(e?"结果":"result",8)}${w(e?"原因":"how",10)}${w(e?"回合":"turns",7)}${e?"损失 你/对方":"lost you/them"}`));for(const t of o.games){const o="a"===t.winner?e?"胜":"win":"b"===t.winner?e?"负":"loss":e?"平":"draw",n="a"===t.winner?chalk.green(w(o,8)):"b"===t.winner?chalk.red(w(o,8)):chalk.yellow(w(o,8)),r="warrens"===t.reason?e?"拆家":"warrens":"timeout"===t.reason?e?"超时":"timeout":"crash"===t.reason?e?"崩溃":"crash":"load_error"===t.reason?e?"装不上":"no load":e?"回合满":"turn cap";console.log(` ${w(t.map,11)}${w(String(t.seat_a),6)}${n}${w(r,10)}${w(String(t.turns),7)}${Math.round(t.lost[0])}/${Math.round(t.lost[1])}${t.error?chalk.gray(` ${t.error}`):""}`)}console.log(chalk.gray(` ${e?"种子":"seeds"} ${o.seeds.join(",")} · ${e?"回合满时比打掉的价值,躲家里不算赢":"at the turn cap the side that destroyed more value wins — hiding at home does not"}`)),o.replay&&g(o.replay)&&console.log(chalk.gray(" ▸ ")+chalk.bold.green("replay")+chalk.gray(o.wins_b?e?" 看你输掉的第一局":" watch the first game you lost":e?" 看第一局":" watch game 1"))}const P=o=>{const e=o.search(/^# ── KNOBS/m);let t=o;if(e>=0){const n=o.slice(e).split("\n").slice(1),r=n.findIndex(o=>/^# ───/.test(o));t=(r>=0?n.slice(0,r):n).join("\n")}return[...new Set([...t.matchAll(/^([A-Z][A-Z0-9_]*)\s*=\s*[^#\n]+/gm)].map(o=>o[1]))]},T=(o,e)=>{const t=new RegExp(`^${e}\\s*=\\s*([^#\n]*?)\\s*(?:#.*)?$`,"m").exec(o);return t?t[1]:null},A=(o,e)=>{const t=new RegExp(`^${e}\\s*=[^#\n]*#\\s*(.*)$`,"m").exec(o);return t?t[1].trim():""},O=()=>{try{return n(DEFAULT_PVP_BOT,"utf8")}catch{return null}},E=o=>o.split("\n").map(o=>o.replace(/\s+#.*$/,"").trimEnd()).filter(o=>o.trim()&&!o.trim().startsWith("#")&&!/^[A-Z][A-Z0-9_]*\s*=/.test(o));async function N(o){const e=await i();if(!e)return null;try{const t=n(s(e,"bots","knob_bot.py"),"utf8");return E(t).join("\n")===E(o).join("\n")?"rookie":"pro"}catch{return null}}async function B(n,r,l){const c=await i();if(!c)return console.log(chalk.yellow(l?" 引擎还没下载到本机(需要联网一次)。":" The arena engine is not on this machine yet (one online fetch needed).")),!1;const g=s(c,"bots","knobs"===n?"knob_bot.py":l?"template_bot_zh.py":"template_bot.py");if(!e(g))return console.log(chalk.yellow(` ${l?"引擎里没有模板":"Template missing in the engine"}: ${g}`)),!1;if(t(s(a(),".icoa","battleai"),{recursive:!0}),e(DEFAULT_PVP_BOT)){if(!r)return console.log(chalk.yellow(l?" bot.py 已经存在,不覆盖。":" bot.py already exists — not overwritten.")+chalk.gray(l?" 确定要换: ":" To replace it: ")+chalk.bold.green(`template ${n} force`)+chalk.gray(l?"(旧文件留作 bot.py.bak)":" (the old file is kept as bot.py.bak)")),!1;o(DEFAULT_PVP_BOT,`${DEFAULT_PVP_BOT}.bak`)}return o(g,DEFAULT_PVP_BOT),console.log(chalk.green(" ✓ ")+chalk.white("knobs"===n?l?"15 个旋钮的新手模板已放到 bot.py":"The 15-knob starter is now your bot.py":l?"空白代码模板已放到 bot.py(高手组)":"The blank code template is now your bot.py (pro)")+chalk.gray(` (${DEFAULT_PVP_BOT})`)),!0}function j(o,e){const t=P(o);if(t.length){console.log(chalk.gray(e?" 你的旋钮(set <旋钮> <值> 改一个):":" Your knobs (set <KNOB> <value> changes one):"));for(const e of t)console.log(` ${chalk.bold.white(e.padEnd(10))}${chalk.green((T(o,e)??"").padEnd(12))}${chalk.gray(A(o,e))}`)}else console.log(chalk.gray(e?" 你的 bot.py 没有旋钮(顶部那种 NAME = value 行)—— 它是高手组的自定义代码。要旋钮版敲 template knobs。":" Your bot.py has no knobs (the NAME = value lines at the top) — it is custom code (pro). For the knob starter type template knobs."))}function U(o,e,t){const n=P(o),a=/^([A-Za-z][A-Za-z0-9_]*)\s*(?:=|\s)\s*(.+)$/.exec(e.trim());if(!a)return console.log(chalk.yellow(t?" 用法: ":" Usage: ")+chalk.bold.green(t?"set <旋钮> <值>":"set <KNOB> <value>")+chalk.gray(t?" 例如 set WOMBATS 6":" e.g. set WOMBATS 6")),j(o,t),!1;const s=a[1].toUpperCase(),l=a[2].trim();if(!n.includes(s))return console.log(chalk.yellow(t?` 没有 ${s} 这个旋钮。`:` No knob called ${s}.`)+chalk.gray(" ")+n.map(o=>chalk.bold.white(o)).join(chalk.gray(" · "))),!1;const i=T(o,s)??"";if(i.trim()===l)return console.log(chalk.gray(" ")+chalk.bold.white(s)+chalk.gray(t?` 已经是 ${l} 了。`:` is already ${l}.`)),!1;const c=new RegExp(`^(${s}\\s*=\\s*)([^#\n]*?)(\\s*)(#.*)?$`,"m"),g=o.replace(c,(o,e,t,n,r)=>`${e}${l}${r?(n||" ")+r:""}`);return r(DEFAULT_PVP_BOT,g),console.log(chalk.gray(" ")+chalk.bold.white(s)+chalk.gray(" ")+chalk.gray(i.trim())+chalk.gray(" → ")+chalk.bold.green(l)+chalk.gray(t?" 下一步: run 试打,up 上传":" next: run to test, up to upload")),!0}function x(o,e){console.log(),console.log(chalk.bold.white(o?" 怎么玩":" How to play")),console.log(chalk.gray(o?" 你的 bot = ":" Your bot = ")+chalk.white(DEFAULT_PVP_BOT)),console.log(chalk.gray(" 🔧 ")+chalk.bold.white(o?"新手组":"Rookie")+chalk.gray(o?":只动顶部 15 个旋钮 —— ":": only the 15 knobs at the top — ")+chalk.bold.green("knobs")+chalk.gray(o?" 看,":" to see them, ")+chalk.bold.green("set WOMBATS 6")+chalk.gray(o?" 改一个,不用写代码。":" to change one. No code.")),console.log(chalk.gray(" ✍ ")+chalk.bold.white(o?"高手组":"Pro")+chalk.gray(o?":":": ")+chalk.bold.green("edit")+chalk.gray(o?" 打开文件,旋钮下面的 act(u) 随便重写。":" opens the file; rewrite act(u) below the knobs however you like.")),console.log(chalk.gray(o?" 然后 ":" Then ")+chalk.bold.green("run")+chalk.gray(o?" 本地试打一场 → ":" plays one local test game → ")+chalk.bold.green("up")+chalk.gray(o?" 上传(每次上传 = 新版本)→ ":" uploads (each upload = a new version) → ")+chalk.bold.green(o?"vs <昵称>":"vs <nick>")+chalk.gray(o?" 挑战,6 局定胜负。":" challenges: six games decide.")),e||console.log(chalk.yellow(o?" 还没有 bot.py —— 敲 ":" No bot.py yet — type ")+chalk.bold.green("template")+chalk.yellow(o?" 拿新手旋钮模板(或 template code 拿空白代码模板)。":" for the knob starter (or template code for the blank code template)."))}export async function runPvpRoom(o,t,r,a={}){const s=a.interactive??u,i=$(),g=r.trim();let d="",y="",P=!1;const T=/^(?:new|create|开房|建)(?:\s+(.*))?$/i.exec(g),A=/^(?:join|enter|进)\s+(\S+)$/i.exec(g);if(T?(P=!0,y=(T[1]??"").trim().slice(0,40)):A?d=A[1]:/^(?:open|public|公共)$/i.test(g)?d="OPEN":g&&(d=g),d=d.toUpperCase().replace(/[^A-Z0-9]/g,"").slice(0,8),!P&&!d){const e=await b("status",o);if(!e.ok||!e.data)return console.log(chalk.yellow(" "+L(e.code,e.message))),"back";if(console.log(),console.log(chalk.bold.white(i?" ⚔ PvP 房间":" ⚔ PvP rooms")+chalk.gray(i?" —— 和真人的 bot 对打,6 局定胜负。":" — your bot against real people, six games a match.")),e.data.rooms.length)for(const o of e.data.rooms)console.log(` ${chalk.bold.cyan(w(o.room,6))} ${chalk.white(w(o.public?i?"公共房":"open room":o.label||"",18))} ${chalk.gray(`${o.members} ${i?"人":"members"} · ${i?"你是":"you are"} ${o.nick}`)}`);else console.log(chalk.gray(i?" 你还没进过任何房间。":" You are not in any room yet."));return console.log(chalk.gray(" ▸ ")+chalk.bold.green("room open")+chalk.gray(i?" 公共房,谁都能进,战绩每天清零":" the public room — anyone can enter, the table resets daily")),console.log(chalk.gray(" ▸ ")+chalk.bold.green("room <CODE>")+chalk.gray(i?" 进朋友开的房(4 个字母)":" enter a friend's room (4-letter code)")),console.log(chalk.gray(" ▸ ")+chalk.bold.green(i?"room new [名字]":"room new [label]")+chalk.gray(i?" 开一个新房,把房号发给朋友":" open a new room and hand out its code")),"back"}let E=null;if(P){const e=await D(t,i);if(null===e)return"back";const n=await b("create",o,{label:y,nick:e});if(!n.ok||!n.data)return console.log(chalk.yellow(" "+L(n.code,n.message))),"back";E=n.data,console.log(chalk.green(" ✓ ")+chalk.white(i?"房间开好了,房号 ":"Room created — the code is ")+chalk.bold.cyan(E.room)+chalk.white(i?",告诉朋友敲 room "+E.room:`. Friends type room ${E.room}`))}else{const e=await b("status",o,{room:d});if(e.ok&&e.data)E=e.data;else{if("not_member"!==e.code)return console.log(chalk.yellow(" "+L(e.code,e.message))),"back";{const e=await b("join",o,{room:d,probe:!0}),n=e.data?.label||("OPEN"===d?i?"公共房":"Open room":d);console.log(chalk.white(` ${i?"进入":"Entering"} `)+chalk.bold.cyan(d)+chalk.gray(` ${n} · ${e.data?.members??"?"} ${i?"人":"members"}`));for(let e=0;e<4&&!E;e++){const e=await D(t,i);if(null===e)return"back";const n=await b("join",o,{room:d,nick:e});n.ok&&n.data?E=n.data:console.log(chalk.yellow(" "+L(n.code,n.message)))}if(!E)return"back"}}}const S=E.room,F=chalk.bold.magenta(`icoa room ${S}> `);for(v(E),x(i,e(DEFAULT_PVP_BOT)),f(S);;){const r=await t(F);if(null===r)return"back";const a=r.trim(),g=a.toLowerCase();if(h.has(g))return"back";if("menu"===g||"home"===g)return"menu";if(""===g||"who"===g||"board"===g||"table"===g||"r"===g||"refresh"===g){const e=await b("status",o,{room:S});e.ok&&e.data?v(e.data):console.log(chalk.yellow(" "+L(e.code,e.message)));continue}if("help"===g||"?"===g){f(S);continue}if("log"===g||"matches"===g||"history"===g){const e=await b("matches",o,{room:S,limit:20,all:"history"===g});if(e.ok&&e.data){e.data.matches.length||console.log(chalk.gray(i?" 今天还没有对局。":" No matches today yet."));for(const o of e.data.matches)console.log(" "+k(o))}else console.log(chalk.yellow(" "+L(e.code,e.message)));continue}if("replay"===g||"replay last"===g){await l()||console.log(chalk.gray(i?" 还没有回放 —— 先 vs 打一场。":" No replay yet — challenge someone first (vs)."));continue}if("knobs"===g||"knob"===g||"旋钮"===g||"set"===g){const o=O();null===o?x(i,!1):j(o,i);continue}const d=/^(?:set|改)\s+(.+)$/i.exec(a);if(d){const o=O();null===o?x(i,!1):U(o,d[1],i);continue}const u=/^(?:template|模板)(?:\s+(knobs?|code|新手|高手))?(?:\s+(force|覆盖))?$/i.exec(a);if(u){const o=/^(?:code|高手)$/i.test(u[1]??"")?"code":"knobs";await B(o,!!u[2],i);continue}if("edit"===g||"vi"===g||"编辑"===g){if(!e(DEFAULT_PVP_BOT)){x(i,!1);continue}console.log(chalk.gray(i?" vi 三键: i 进入编辑 · Esc 退出编辑 · :wq 存盘离开 · :q! 放弃":" vi survival: i = edit · Esc = stop editing · :wq = save and leave · :q! = discard")),await s("vi",[...m,DEFAULT_PVP_BOT]);continue}if("run"===g||"test"===g||"play"===g||"试打"===g){if(!e(DEFAULT_PVP_BOT)){x(i,!1);continue}if(!p().ok){console.log(chalk.gray(i?" 本机没有 Python 3.12,先 python setup;没有它也能 up / vs(判分在服务器)。":" No Python 3.12 here — python setup first; up / vs still work without it (the judge is server-side)."));continue}console.log(chalk.gray(i?" 本地打一场(对卡 12 的陪练),看看 bot 跑不跑得起来…":" One local game (the card-12 sparring bot) to see that the bot runs…")),await c(DEFAULT_PVP_BOT,12,0,void 0,s);continue}const y=/^(?:up|upload|submit|上传)(?:\s+(.+))?$/i.exec(a);if(y){const t=(y[1]??"").trim()||DEFAULT_PVP_BOT;if(!e(t)){console.log(chalk.yellow(` ${i?"找不到":"Not found"}: ${t}`)),x(i,!1);continue}let r="";try{r=n(t,"utf8")}catch{console.log(chalk.yellow(` ${i?"读不了":"Could not read"}: ${t}`));continue}const a=await N(r),s=await b("upload",o,{room:S,code:r,group:a},3e4);if(!s.ok||!s.data){console.log(chalk.yellow(" "+L(s.code,s.message)));continue}s.data.unchanged?console.log(chalk.gray(i?` 和 v${s.data.version} 一字不差,没有新版本。`:` Identical to v${s.data.version} — nothing new.`)):console.log(chalk.green(" ✓ ")+chalk.white(i?`已上传为 v${s.data.version}`:`Uploaded as v${s.data.version}`)+chalk.gray(` (${t})`)+(a?chalk.gray("rookie"===a?i?" · 🔧 新手组(只动了旋钮)":" · 🔧 knobs only (rookie)":i?" · ✍ 高手组(改了代码)":" · ✍ custom code (pro)"):""));continue}const $=/^(?:vs|challenge|fight|挑战|打)\s+(\S+)(?:\s+v?(\d+))?$/i.exec(a);if($){const e=$[2]?Number($[2]):void 0;console.log(chalk.gray(i?" ⚖ 判分机在打 6 局(3 张图 × 换席位),通常 5–30 秒,最多 3 分钟…":" ⚖ The judge is playing 6 games (3 maps × both seats) — usually 5–30 s, up to 3 min…"));const t=await b("challenge",o,{room:S,nick:$[1],version:e},2e5);if(!t.ok||!t.data){console.log(chalk.yellow(" "+L(t.code,t.message)));continue}_(t.data),v(t.data.status);continue}/^(?:vs|challenge|挑战|打)$/i.test(a)?console.log(chalk.gray(i?" 用法: vs <昵称> 或 vs <昵称> v2(打对方的第 2 版)":" Usage: vs <nick> or vs <nick> v2 (their version 2)")):(console.log(chalk.gray(i?` 不认识「${a}」。`:` Unknown: "${a}".`)),f(S))}}async function D(o,e){for(let t=0;t<4;t++){const t=await o(chalk.bold.cyan(e?" 你的昵称(2–12 个字母/数字,别用真名;back 取消): ":" Your nickname (2–12 letters/digits, not your real name; back to cancel): "));if(null===t||h.has(t.toLowerCase()))return null;if(/^[A-Za-z0-9_\-一-鿿]{2,12}$/.test(t))return t;console.log(chalk.gray(e?" 2–12 个字母、数字、_ 或 -,不能有空格。":" 2–12 letters, digits, _ or -; no spaces."))}return null}function L(o,e){const t=$();switch(o){case"wrong_course":return t?"PvP 房间只对 BattleAI(battleai-48)token 开放。":"PvP rooms are for BattleAI (battleai-48) tokens.";case"demo_not_supported":return t?"demo token 进不了 PvP 房间。":"Demo tokens cannot enter PvP rooms.";case"no_room":return t?"没有这个房号 —— 检查一下 4 个字母。":"No room with that code — check the four letters.";case"nick_taken":return t?"这个昵称在房里已经有人用了,换一个。":"That nickname is taken in this room — pick another.";case"bad_nick":return t?"昵称要 2–12 个字母/数字,不能有空格。":"Nickname: 2–12 letters/digits, no spaces.";case"no_bot":return t?"先 up 上传你的 bot。":"Upload your bot first: up";case"opp_no_bot":return e||(t?"对方还没上传 bot。":"They have no bot yet.");case"no_such_nick":return t?"房里没有这个昵称 —— 回车看名单。":"Nobody by that name here — press Enter for the table.";case"self":return t?"不能挑战自己。":"You cannot challenge yourself.";case"cooldown":return t?"每 10 秒只能发起一场。":"One challenge every 10 seconds.";case"no_act":return t?"这个文件里没有 act(u) —— 确定是你的 bot.py?":"That file has no act(u) — is it your bot.py?";case"judge_unreachable":case"judge_timeout":case"judge_rejected":case"judge_not_configured":return t?"判分机暂时不可用,一分钟后再试。":"The judge is unavailable right now — try again in a minute.";default:return e||(t?"连不上服务器。":"Could not reach the server.")}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.635",
3
+ "version": "2.19.637",
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",