如何在不被测挂的情况下训 20 个加强版咋克?
- 注:截至 11 月 13 日,本文中的内容在原版和 QOJ 版通用。
- 注 2:由于联系不到管理,本文改投至与之比较相关的科技·工程版。
- 注 3:就在过审的瞬间我调了一下里面的数值,能基本确保所有咋克都 AK 而其他国家被区分。麻烦管理大大重审一下吧 /kk 实在不好意思
众所周知,最近在休闲娱乐版爆火了一款新的游戏——OI 教练模拟器。
于是经过一晚上的努力,我们获得了这样的战绩:
想知道这么多咋克是怎么来的吗?那就一起看看吧!
启程
众所周知,在网页地址前加入 view-source: 就可以看到源代码。
然后这种写着玩的东西显然是不可能很好地防熊的,所以你可以直接扒拉里面的 .js 文件。
欸,一找一个准。
<script src="tutorial.js"></script>
<script src="events.js"></script>
<script src="lib/constants.js"></script>
<script src="lib/utils.js"></script>
<script src="lib/models.js"></script>
<script src="lib/talent.js"></script>
<script src="lib/task.js"></script>
<script src="lib/competitions.js"></script>
<script src="lib/contest-ui.js"></script>
<script src="lib/contest-integration.js"></script>
<script src="lib/national-team.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
<script src="game.js"></script>
<script src="render.js"></script>
<script src="debug.js"></script>
这些文件的命名是十分明确地表示内容的。所以下面这些一看就很没用:
<script src="tutorial.js"></script>
<script src="events.js"></script>
<script src="lib/constants.js"></script>
<script src="lib/utils.js"></script>
<script src="lib/models.js"></script>
<script src="lib/talent.js"></script>
<script src="lib/task.js"></script>
<script src="lib/competitions.js"></script>
<script src="lib/contest-ui.js"></script>
<script src="lib/contest-integration.js"></script>
<script src="lib/national-team.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
<script src="render.js"></script>
于是我们发现只有 game.js 和 debug.js 看上去比较有用,考虑从这两个入手。
又根据群里发现的 debugzak() 函数,我们决定先看 debug.js。
为咋克添加能力
作为 Box3 开服玩家我对 js 的基本语法有一点点了解。当然不了解其实也没事,不难注意到 js 的语法和 cpp 还是蛮像的,所以沟通障碍应该不大。
打开 debug.js,映入眼帘的就是我们要找的 debugzak() 函数。外加上 AI 清楚到极致的调试信息,我们可以快速地摸清:学生是用一个名为 Student 的类存的。扒拉其他的 js 程序,在 models.js 中我们找到了这个类的定义:
class Student {
constructor(name,thinking,coding,mental){
this.name=name; this.thinking=thinking; this.coding=coding; this.mental=mental;
// talents: 特质/技能列表(Set of strings)
// 预留接口:TalentManager 可以注册具体特质的触发逻辑,游戏事件/比赛等可调用 student.triggerTalents(eventName, ctx)
this.talents = new Set();
this.knowledge_ds = KNOWLEDGE_ABLILTY_START;
this.knowledge_graph = KNOWLEDGE_ABLILTY_START;
this.knowledge_string = KNOWLEDGE_ABLILTY_START;
this.knowledge_math = KNOWLEDGE_ABLILTY_START;
this.knowledge_dp = KNOWLEDGE_ABLILTY_START;
this.pressure=20; this.comfort=50;
...
}
...
}
可以看到:
- 构造函数中要依次传入选手名、思维能力、代码能力、心理素质。
- 五大板块能力则直接顾变量名思义即可。
我们回到 debugzak() 函数中,直接复制一份,把数值稍微拉大点可以写出下面的程序:
game.students = [];
let superStudent = new Student('zak', 500000000, 500000000, 500000000);
superStudent.knowledge_ds = 500000000;
superStudent.knowledge_graph = 500000000;
superStudent.knowledge_string = 500000000;
superStudent.knowledge_math = 500000000;
superStudent.knowledge_dp = 500000000;
superStudent.pressure = 0;
superStudent.comfort = 500000000;
superStudent.sick_weeks = 0;
superStudent.active = true;
game.students.push(superStudent);
回车,除了控制台输出了一个 1 外,没有反应。
我们接着往下读 debugzak() 函数,发现其每次更新后需要手动调用重新渲染的函数 renderAll()。于是去控制台输入 renderAll(),我们就有一个可以玩的强化版咋克了。
为咋克添加天赋
但是我们肯定不会满足于这点简单的操作。我们想给咋克安上所有的正面 buff,使其能在比赛中更好地发挥。这时根据顾名思义的原则我们的目光落到了一个名为 lib/talent.js 的程序上。
我们发现他定义了一个名为 TalentManager 的常量,诶,他还贴心的告诉了我操作方法。
/* talent.js - 学生特质(talent)管理器
目的:提供一个集中注册/触发学生特质的管理器(TalentManager),便于扩展特质逻辑并在事件或比赛中调用。
使用说明:在页面中先于 `game.js` 加载本文件,或者确保在触发 student.triggerTalents 之前 window.TalentManager 已就绪。
API:
- register(name, handler): 注册特质处理器。handler(student, eventName, ctx) -> 可修改学生/游戏状态,返回可选描述字符串。
- clear(): 清除所有已注册特质
- handleStudentEvent(student, eventName, ctx): 对单个学生按其特质逐一调用已注册 handler
- registerDefaultTalents(game, utils): 注册一些示例特质(可选)
- getTalentInfo(name): 获取天赋的描述信息(用于UI显示)
- setTalentInfo(name, info): 设置天赋的描述信息
*/
但是正面天赋一个个找出来注册实在太多了。好在 registerDefaultTalents() 不传参的时候是自动注册所有天赋的,而且这个函数中天赋的内容也透露了很多有用的东西:
// 冷静:比赛开始触发,临时提升所有能力 20%
this.registerTalent({
name: '冷静',
description: '比赛开始时有较高概率在比赛中保持冷静,所有能力临时+20%。',
color: '#4CAF50',
prob: 0.10,
beneficial: true,
handler: function(student, eventName, ctx){
try{
ensureTemp(student);
if(eventName !== 'contest_start') return null;
// 触发概率基础 60%,赛前压力>=60 每超 10 点额外 +10%
let base = 0.6;
const pressure = Number(student.pressure) || 0;
if(pressure >= 60){
base += Math.floor((pressure - 60) / 10) * 0.1;
}
if(getRandom() < base){
// 备份并放大
if(!student._talent_backup['冷静']){
// backup raw original values (avoid forcing fallback to 0 which may mask bugs)
student._talent_backup['冷静'] = { thinking: student.thinking, coding: student.coding, constmental: (student._talent_state && student._talent_state.constmental) || student.mental };
student.thinking = clamp(Number(student.thinking||0) * 1.2);
student.coding = clamp(Number(student.coding||0) * 1.2);
// adjust the per-contest constmental rather than the base mental
student._talent_state.constmental = clamp(Number(student._talent_state.constmental||student.mental||50) * 1.2);
student._talent_state['冷静'] = true;
return '冷静发动:全能力 +20%(赛中临时)';
}
}
}catch(e){ console.error('冷静 天赋错误', e); }
return null;
}
});
也就是说,对于一个天赋,其使用了 beneficial 成员存储这个天赋是否正面。
于是我们只需要先获取所有天赋,再枚举找出其中的正面天赋,将其加入我们定义的选手中即可。
TalentManager.registerDefaultTalents();
let sss=Object.values(TalentManager._talents || {});//将所有天赋提取到数组中
//枚举天赋并判断是否正面,这里枚举到 50(共 51 个),跳过最后一个清理临时天赋
for(let i=0;i<50;i++){
if(sss[i].beneficial){
superStudent.addTalent(sss[i].name);
}
}
将选手加入并重新渲染,我们就得到了一个满能力+满天赋的咋克。
为咋克搞钱+升级设施
如果你认真玩过,你会发现如果出去集训,就容易资金不足直接倒闭。为了长期压榨咋克,我们需要一个稳定的资金供应。而且,咋克这种神佬当然需要一个强大的机房。
调整金钱的方法在 debugzak() 里已经告诉我们了:直接修改 game.budget 即可。
而充分调用我们的英语水平,在 lib/models.js 中我们发现了一个名叫 Facilities 的类:
class Facilities {
constructor(){ this.computer=1; this.ac=1; this.dorm=1; this.library=1; this.canteen=1; }
...
}
上来第一行构造函数我们就找到了各个设施的变量名。于是直接安排:
let tfac=new Facilities();
tfac.computer=10000000;
tfac.ac=10000000;
tfac.dorm=10000000;
tfac.canteen=10000000;
tfac.library=10000000;
game.facilities=tfac;
game.budget=1e+100;
渲染一下,我们就把设施也升级完成了。
为咋克添加比赛权限
debugzak() 函数可以帮我们把 zak 直接跳到 NOI 之前。
显然我们有这么强的咋克我们连 NOI 也懒得打,于是我们直接跳到
game.week=36;
我 IOI 呢???咋赛季直接结束了?
仔细观察 debugzak() 函数,原来需要为咋克添加比赛权限。我们直接不找了,复制他给的改改:
const secondYearIOI = competitions.find(c => c.name === 'IOI' && c.week > WEEKS_PER_HALF);
const targetWeek = secondYearIOI ? secondYearIOI.week - 1 : 36;
if(game.week < targetWeek) {
const weeksToJump = targetWeek - game.week;
game.week = targetWeek;
}
const halfIndex = 1;
if(!game.qualification[halfIndex]) {
game.qualification[halfIndex] = {};
}
for(let compName of COMPETITION_ORDER) {
if(!game.qualification[halfIndex][compName]) {
game.qualification[halfIndex][compName] = new Set();
}
game.qualification[halfIndex][compName].add(superStudent.name);
}
if(!game.completedCompetitions) {
game.completedCompetitions = new Set();
}
for(let comp of competitions) {
if(comp.week < targetWeek && comp.week > WEEKS_PER_HALF) {
const key = `${halfIndex}_${comp.name}_${comp.week}`;
game.completedCompetitions.add(key);
}
}
renderAll();
但是赛季仍然结束了。
输出 secondYearIOI 的值,显示 undefined。
我们考虑正常游戏进入 IOI。此时开着控制台,我们会发现一个恶心的问题:IOI 初始是不在比赛列表里的!
所以我们刚才的操作当然无法激活 IOI 的权限,也就导致了赛季直接结束。
于是我们在跳转之前把 IOI 添加到比赛列表里即可:
competitions.push({week: 37, name:"IOI", difficulty:550, maxScore:600, numProblems:6, nationalTeam:true,subtasksPerProblem:15});
但赛季特么为什么还是结束了??!
我们只好打开国家队相关的程序 lib/national-team.js 开始翻。
经过阅读代码,我们发现,想打 IOI 有三步。
- 一是进入集训队,即
game.inNationalTeam = true;。 - 二是进入国家队,即你的名字出现在
game.nationalTeamResults.ioiQualified数组中。 - 三是这个比赛存在,游戏中 CTT 及之后的比赛只有能打才会被加入比赛列表,也正因如此它无法实现高一就去参加 CTT、CTS 和 IOI,即使我们获得了金牌。这里我们只需要把 IOI 加进去就行。
所以我们把所有开始时没有的变量等全部手动设置一下:
competitions.push({week: 37, name:"IOI", difficulty:550, maxScore:600, numProblems:6, nationalTeam:true,subtasksPerProblem:15});
game.inNationalTeam = true;
game.nationalTeamResults = {
ioiQualified: []
};
game.nationalTeamResults.ioiQualified.push('zak');
这样我们的 zak 就可以打爆 NOI 前所有的比赛,并参加 IOI 了。
让咋克稳稳拿到第一
我们发现游戏中模拟的其他国家选手实在太过强势,使得咋克拿下 AK 都不一定能金牌,金牌也不一定能 rk1,这明显有损 zak 神仙的水平。
但是眼尖的我们在把 IOI 加到到比赛列表时就发现了,IOI 的难度是可以调的。这不给他来个一百倍?
competitions.push({week: 37, name:"IOI", difficulty:55000, maxScore:600, numProblems:6, nationalTeam:true,subtasksPerProblem:15});
添加更多咋克并突破国家队人数限制
我们可以循环上述过程,添加大量的咋克。国家队人数的限制机制出现在 CTS 后的结算过程中,但由于我们不是通过 CTS 将咋克们加入国家队的,而是直接编辑国家队名单,所以我们根本不会被这个限制影响。于是我们将上面用到的代码拼凑一下,套上循环得到完整的代码:
//将 IOI 加入比赛列表
competitions.push({week: 37, name:"IOI", difficulty:300000000, maxScore:600, numProblems:6, nationalTeam:true,subtasksPerProblem:15});
game.inNationalTeam = true;
game.nationalTeamResults = {
ioiQualified: []
};
game.students = [];
for(let i=0;i<20;i++){
let superStudent = new Student('zak'+String(i), 500000000, 500000000, 500000000);
superStudent.knowledge_ds = 500000000;
superStudent.knowledge_graph = 500000000;
superStudent.knowledge_string = 500000000;
superStudent.knowledge_math = 500000000;
superStudent.knowledge_dp = 500000000;
superStudent.pressure = 0;
superStudent.comfort = 500000000;
superStudent.sick_weeks = 0;
superStudent.active = true;
game.nationalTeamResults.ioiQualified.push('zak'+String(i));
TalentManager.registerDefaultTalents();
let sss=Object.values(TalentManager._talents || {});
for(let i=0;i<50;i++){
if(sss[i].beneficial){
superStudent.addTalent(sss[i].name);
}
}
game.students.push(superStudent);
const secondYearIOI = competitions.find(c => c.name === 'IOI' && c.week > WEEKS_PER_HALF);
const targetWeek = secondYearIOI ? secondYearIOI.week - 1 : 36;
if(game.week < targetWeek) {
const weeksToJump = targetWeek - game.week;
game.week = targetWeek;
}
const halfIndex = 1;
if(!game.qualification[halfIndex]) {
game.qualification[halfIndex] = {};
}
for(let compName of COMPETITION_ORDER) {
if(!game.qualification[halfIndex][compName]) {
game.qualification[halfIndex][compName] = new Set();
}
game.qualification[halfIndex][compName].add(superStudent.name);
}
if(!game.completedCompetitions) {
game.completedCompetitions = new Set();
}
for(let comp of competitions) {
if(comp.week < targetWeek && comp.week > WEEKS_PER_HALF) {
const key = `${halfIndex}_${comp.name}_${comp.week}`;
game.completedCompetitions.add(key);
}
}
}
let tfac=new Facilities();
tfac.computer=10000000;
tfac.ac=10000000;
tfac.dorm=10000000;
tfac.canteen=10000000;
tfac.library=10000000;
game.facilities=tfac;
game.budget=1e+100;
renderAll();
这样,即使字典序十分靠后,由于其他国家的选手实在太菜了,我们的咋克仍然稳稳地拿下了金牌!
上述代码的使用方式是,浏览器打开游戏界面后按 F12,然后在弹出的窗口顶部找到“控制台”或“Console”,如果没有则单击顶部右侧的“>”,在弹出的菜单中找到“控制台”或“Console”。这时窗口会变成一个类似于 cmd 的可以输代码的地方,把刚才的代码贴进去按回车即可。
防止测挂
在源代码页面容易找到测挂程序 lib/devtools-detector.js(笔者编写本文时被删除了,不知道什么时候加回来,以及加回来后这些方法还是否靠谱)。
不难注意到其主要原理是检测控制台是否打开。所以我们只需要不打开控制台就可以了。
于是我们想到可以使用用户脚本。
在 js 中,我们可以使用 setTimeout 来执行想向控制台中输入的命令。格式是 setTimeout(<命令>,<时间>),其中命令是字符串,时间是整数,单位为毫秒。js 中可以通过在行末添加 \ 来设置多行字符串(也可以每行打分号后压成一行)。所以将刚才的代码进行这些操作(行末添加 \ 可以用 cpp 完成),就得到了:
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 2025-11-13
// @description try to take over the world!
// @author You
// @match https://seve42.github.io/OItrainer/game.html?*
// @icon https://www.google.com/s2/favicons?sz=64&domain=https://seve42.github.io/OItrainer/game.html
// @grant none
// ==/UserScript==
(function() {
'use strict';
setTimeout("competitions.push({week: 37, name:'IOI', difficulty:300000000, maxScore:600, numProblems:6, nationalTeam:true,subtasksPerProblem:15});\
game.inNationalTeam = true;\
game.nationalTeamResults = {\
ioiQualified: []\
};\
game.students = [];\
for(let i=0;i<20;i++){\
let superStudent = new Student('zak'+String(i), 500000000, 500000000, 500000000);\
superStudent.knowledge_ds = 500000000;\
superStudent.knowledge_graph = 500000000;\
superStudent.knowledge_string = 500000000;\
superStudent.knowledge_math = 500000000;\
superStudent.knowledge_dp = 500000000;\
superStudent.pressure = 0;\
superStudent.comfort = 500000000;\
superStudent.sick_weeks = 0;\
superStudent.active = true;\
game.nationalTeamResults.ioiQualified.push('zak'+String(i));\
\
TalentManager.registerDefaultTalents();\
let sss=Object.values(TalentManager._talents || {});\
for(let i=0;i<50;i++){\
if(sss[i].beneficial){\
superStudent.addTalent(sss[i].name);\
}\
}\
\
game.students.push(superStudent);\
\
const secondYearIOI = competitions.find(c => c.name === 'IOI' && c.week > WEEKS_PER_HALF);\
const targetWeek = secondYearIOI ? secondYearIOI.week - 1 : 36;\
\
if(game.week < targetWeek) {\
const weeksToJump = targetWeek - game.week;\
game.week = targetWeek;\
}\
\
const halfIndex = 1;\
if(!game.qualification[halfIndex]) {\
game.qualification[halfIndex] = {};\
}\
\
for(let compName of COMPETITION_ORDER) {\
if(!game.qualification[halfIndex][compName]) {\
game.qualification[halfIndex][compName] = new Set();\
}\
game.qualification[halfIndex][compName].add(superStudent.name);\
}\
\
if(!game.completedCompetitions) {\
game.completedCompetitions = new Set();\
}\
\
for(let comp of competitions) {\
if(comp.week < targetWeek && comp.week > WEEKS_PER_HALF) {\
const key = `${halfIndex}_${comp.name}_${comp.week}`;\
game.completedCompetitions.add(key);\
}\
}\
}\
let tfac=new Facilities();\
tfac.computer=10000000;\
tfac.ac=10000000;\
tfac.dorm=10000000;\
tfac.canteen=10000000;\
tfac.library=10000000;\
game.facilities=tfac;\
game.budget=1e+100;\
renderAll();",3000 );
})();
在油猴创建一个用户脚本并将内容换成上面这些就大功告成了!
现在打开游戏,你就会发现:在游戏开始
彩蛋
众所周知很多人不同意鲁棒性这个翻译。AI 也是。
在 lib/contest-integration.js 中:
// 健壮的学生列表获取