欢迎来到绿茵赛场!AI世界杯是一款 2 对 2 的竞技足球编程游戏。它最特别的地方在于:场上奔跑的球员、场边指挥的教练,全都不是普通角色,而是一个个由你的代码"注入灵魂"的 AI Agent(智能体)。
你可以把 AI Agent 理解为一个"会自己思考、自己行动"的程序小人。你不需要用鼠标一步步操控它,而是提前写好一套决策逻辑(例如"如果我有球就射门,否则就去追球"),然后 AI Agent 就会在比赛中不停地按照这套逻辑自动做出判断和动作。你的代码写得越聪明,你的 AI Agent 踢得就越好——这正是"AI世界杯"这个名字的由来。
在这场比赛里,你的代码将同时操控三个 AI Agent。它们分工不同,请先记住它们各自的角色:
本竞技场支持用 Python 和 C++ 两种语言来编写 AI Agent 的逻辑。两种语言的游戏思路完全一致——都是"为球员写线程处理函数,为教练写主循环",只是语法写法不同。
本篇文档中的每一段示例代码,都会同时给出 Python 和 C++ 两个版本,你可以点击代码框上方的语言标签来切换查看。初学者建议先从 Python 入手,它的写法更接近自然语言、更容易读懂;熟悉之后再尝试 C++ 也会很轻松。
目标非常简单,和真实足球一样:把球打进对方球门,让自己的进球数比对手多。下面我们把"比赛怎么进行""怎么算赢"一条条讲清楚。
一场比赛分为两个回合(就像足球的上下半场),每个回合时长为 params.halfTime 秒。两个回合分别编号为第 0 回合和第 1 回合(程序里习惯从 0 开始数):
0 回合(上半场)开球。1 回合(下半场)开球。之所以让两队轮流开球,是为了公平——开球方会先碰到球,稍占一点便宜,两回合各开一次就抵消了。
当第二回合结束的那一刻,系统会立即按下面的顺序判定谁赢:
| 优先级 | 判定标准 |
|---|---|
| ① 首先看 | 进球数高者获胜。这是最直接的胜负标准。 |
| ② 若进球数相同 | 粉丝支持度更高的队伍获胜(支持度是什么,见下一节)。 |
| ③ 若支持度也相同 | 掷硬币决定——但这个"硬币"是基于种子(seed)的,同一场比赛无论重播多少次,结果都完全一样。 |
观众来看球,最爱看的是积极进攻的足球。所以游戏设计了一个"粉丝支持度"机制来奖励主动进攻的一方。比赛过程中,双方都会不断积累粉丝支持度,它会直接显示为你教练 AI Agent(英雄)的生命值数字——数字越高,说明你的球队踢得越受欢迎。
以下行为会为你积累支持度:
| 行为 | 支持度奖励 |
|---|---|
| 成功把球传给自己的队友 | params.activity.pass |
| 赢得一次抢断对抗(clash) | params.activity.clash |
| 射向对方球门(无论被扑出还是进球) | params.activity.shot |
| 把球留在对方半场里 | 每秒 params.activity.ballInOpponentHalfPerSecond |
你控制两名球员 AI Agent。前面说过,每名球员都在自己独立的线程里运行你的处理函数。比赛开始时,这个处理函数会为每名球员各自调用一次;一旦调用,函数里的循环就会一直跑到比赛结束(球员永远不会死亡,也不会重生),所以你通常会在函数里写一个 while 无限循环,让球员不停地思考和行动。
要让球员 AI Agent 动起来,你必须先"注册"一个处理函数,告诉系统:"每当一名球员登场,就用这个函数来控制它。"注册的方式是调用 hero.on("spawn-mate", 函数名)。
hero.on("spawn-mate", ...)!如果没有这一行,球员线程根本不会启动,你会看到两名球员站在原地一动不动。请务必记得加上它。
下面是一段最基础的球员逻辑:"有球就射门,没球就追球。"
def onSpawnMate(mate): while True: if mate.hasBall: mate.shoot() # 有球就射门 else: mate.chaseBall() # 没球就追球 hero.on("spawn-mate", onSpawnMate)
auto onSpawnMate(auto mate) { while (true) { if (mate.hasBall) { mate.shoot(); // 有球就射门 } else { mate.chaseBall(); // 没球就追球 } } } int main() { hero.on("spawn-mate", onSpawnMate); return 0; }
mate.mateId(值为 0 或 1)来区分它们,从而分配不同角色。例如:让 0 号一直往前压当"前锋",让 1 号留在后场当"后卫",这样球队才有层次,不会两个人挤在一起抢同一个球。
球员 AI Agent 的移动和测距,靠下面两个基础指令:
| 指令 | 说明 |
|---|---|
mate.moveTo(x, y) | 朝目标点 (x, y) 移动一步(步长为 params.stepDistance)后就返回。注意:它一次只走一小步!这一步期间线程会被阻塞(暂停),走完这步之后你的代码会重新执行、重新做决策。所以要走到远处,通常要放在循环里反复调用。 |
mate.distanceTo(target) | 返回本球员到任意带有 x、y 坐标的对象(比如球、对手、球门)之间的距离。常用来判断"够不够得着"。 |
params.carrierSpeedFactor(默认 0.75,即降到 75%)。这带来一个重要结论:空手的追赶者总能追上带球的盘带者。所以别指望一个人一路带球过全场,那一定会被抢断——正确做法是靠传球把球快速推进。另外,被眩晕的球员无法移动,此时调用 moveTo 会一直卡住,直到眩晕结束才返回。
mate.clash() 是球员 AI Agent 唯一的主动技能:从敌方持球者脚下把球抢过来。它的冷却时间是 params.clashCooldown 秒,而且每次尝试都会消耗冷却——也就是说,无论你抢成功还是失败,都得等冷却结束才能再抢一次。
| 结果 | 触发条件与效果 |
|---|---|
| ✅ 成功 | 当敌方持球者在 params.clashRange 范围内、且不处于免疫状态时:球立刻转移到你脚下,对手被眩晕 params.clashStunTarget 秒,同时你还获得一小段控球免疫(这段时间对手抢不回去)。 |
| ❌ 失败 | 如果目标超出了抢断范围、或者对手正处于免疫状态:抢断落空,反而是你自己被眩晕 params.clashStunSelf 秒——白白暴露一个大破绽。 |
isImmune(是否免疫)!对一个正在免疫中的持球者抢断,必定失败并让自己眩晕,等于主动送给对手一次机会。养成"先看 isImmune,再决定要不要 clash"的习惯。
mate.roll(x, y, power) 是把球踢出去的唯一方式。这里有一个非常重要的设定:在本游戏里,"传球"和"射门"其实是同一个动作——都是把球朝某个点滚出去,区别只在于你瞄准的是队友、还是球门。
其中 power(力量)决定球滚多远:
# 最大射程 = power × params.rollRangeFactor # 如果 power 超过 params.maxPower,会被自动限制到上限 mate.roll(x, y, power)
// 最大射程 = power × params.rollRangeFactor // 如果 power 超过 params.maxPower,会被自动限制到上限 mate.roll(x, y, power);
params.selfRecaptureDelay 秒内接回自己刚踢出的球。过了这段短暂时间后,"踢一脚给自己"的直塞式自传球(把球往前捅一下再追上去)是允许的。为了让你少写代码,游戏内置了一批常用的"宏指令"——它们其实是把常见操作打包好的快捷方法,直接调用即可:
| 宏指令 | 效果 |
|---|---|
mate.chaseBall() | 朝球的方向移动一步(追球)。 |
mate.shoot() | 以最大力量把球射向对方球门中心(射门)。 |
mate.passTo() | 把球传给你的搭档(你只有一名搭档,所以不用指定传给谁)。 |
mate.cross() | 朝远门柱区域横传(cross,即"传中")——这是本竞技场的招牌打法,非常好用。 |
mate.defendGoal() | 朝"球—本方球门"的连线上退守一步(回防站位)。 |
下面这些属性是只读的(你只能读取、不能修改),用来了解球员当前的状态,方便你的代码做判断:
| 属性 | 类型 | 说明 |
|---|---|---|
x / y | 数值 | 球员当前所在位置的横 / 纵坐标。 |
mateId | 数值 | 0 或 1,用来区分两名球员、分配角色。 |
color | 字符串 | 本队颜色,"red" 或 "blue"。 |
hasBall | 布尔值 | 当前是否持球(true / false)。 |
isStunned | 布尔值 | 是否被眩晕(眩晕时无法移动、抢断或接球)。 |
isImmune | 布尔值 | 是否拥有控球免疫(免疫时无法被对手抢断)。 |
clashReady | 布尔值 | 抢断技能是否已经冷却好、可以使用。 |
要踢好球,得先搞懂"球是怎么被抢到、被守住、被拦截的"。这一节的规则决定了控球权的归属,非常值得细读。
球是"粘"的:一颗无人控制的自由球,只要进入了某名球员的 params.captureRadius 范围,就会自动"吸附"到那名球员脚下,变成他控球。之后,持球者会一直控着球,直到发生下面三种情况之一:他主动出球(roll)、被敌方成功抢断(clash)、或者被守门员扑住。
params.possessionImmunity 秒的免疫时间,这段时间里他不会被抢断。换句话说,每一次拿到球,你都保证有一次"从容不被打扰"的行动机会。正因如此,对一个免疫中的持球者抢断必定失败(还会自晕),所以出手前一定要先看 isImmune。
释放气泡(Release Bubble):一颗刚被踢出、正在滚动的球,在它离开释放点、飞行距离还没达到 params.rollReleaseRadius 之前,任何人都无法接住它(连守门员也不行)。这就好比球刚出脚时外面罩着一层保护泡泡。因此,即使对手就贴在你身边,你也能安全地把球踢出去。而与之相对,一颗完全静止的球,任何时候都可以被捡走。
拦截(Interception):只要你的球员站在滚球的路径上、并且位于释放气泡之外的位置,球飞过来就归你了。由于球的运动轨迹是直线,你完全可以通过 hero.ball 提供的位置和速度信息 (x, y, vx, vy),用数学算出球接下来会经过哪里,从而提前跑位去拦截。
红队始终防守左侧球门,蓝队始终防守右侧球门。当球进入某个球门区域(params.gates)时就算进球——而且不管球是谁踢进去的!
mate.hasBall 的情况,别让防守球员糊里糊涂地把球送进自家大门。
每次进球之后(以及每个回合刚开始时),所有球员的位置和技能冷却都会重置回初始阵型,球会被重新放到球场中央,并朝开球方偏移一小段距离 params.kickoffOffset——这样开球方就会确定性地先碰到球。
每个球门都由一名 NPC 守门员把守。它不是 AI Agent,没有人给它写代码——它的行为是系统固定的、完全可预测的。而"可预测"恰恰是你的机会:只要摸清它的规律,就能设计出稳定的破门方法。
| 特性 | 说明 |
|---|---|
| 移动受限 | 守门员只在自己的专属区域(params.keeperZones)内移动,速度为 params.keeperSpeed,并且始终朝"区域内离球最近的那个点"移动。它移动得不快,它的反应滞后,就是你的得分窗口。 |
| 物理扑救 | 它的扑救是物理性的、而非"守住整个球门":只有当它的身体真的靠近到 params.keeperCatchRadius 范围内时,才能接住滚来的球。所以快速射向它够不到的远角,就能攻破一个被引偏了位置的守门员。 |
| 不可撼动 | 守门员无法被抢断(clash),它手里握着的球也无法被抢走。不过你可以自由进入它的区域,无需顾忌。 |
守门员拿到球并持球 params.keeperHoldTime 秒之后,会以 params.superKickSpeed 的极快速度,朝离它最近的、且没被眩晕的自家球员射出一道超级长传——直线飞行、中途不减速。关于这道解围球,有几点要知道:
params.captureRadius 范围内,球会被直接交到他脚下,省去飞行过程。cross(横传/传中)成为招牌打法的原因:它能把守门员往一侧拽,再打空档。
主线程运行的就是你的教练 AI Agent。和场上跑动的球员不同,教练不亲自碰球,而是像一位真正的主教练那样站在场边:它能看见场上发生的一切,并适时使用各种即时技能(每个技能都有自己独立的冷却时间),从战术层面影响比赛走向。教练是你统筹全局、临场调度的"指挥中枢"。
| 方法 | 说明 |
|---|---|
hero.wait(duration) | 让教练的主线程暂停 duration 秒。常用来控制主循环的节奏,避免每一帧都做重复判断。 |
hero.random() | 返回一个 [0, 1) 之间的伪随机数。它基于种子生成,每场比赛结果确定(不会真的随机乱变)。 |
hero.abilityReady(name) | 检查某个技能是否已冷却好,好了返回 true。技能名有四个:"inspire"、"press"、"whistle"、"distract"。 |
教练 AI Agent 有四个技能,两个用来"增益自己人",两个用来"干扰对手":
| 技能 | 效果 |
|---|---|
hero.inspire(mate) 激励 | 让指定的自家球员提速(速度 × inspire.factor),持续 inspire.duration 秒。冷却 inspire.cooldown 秒。适合在反击、追球时使用。 |
hero.press(enemyMate) 压迫 | 让指定的敌方球员减速(速度 × press.factor),持续 press.duration 秒。冷却 press.cooldown 秒。适合拖慢对方持球者。 |
hero.whistle(mate) 哨声 | 立刻解除自家球员身上的眩晕。冷却 whistle.cooldown 秒。这是专门克制"抢断流"对手的利器——被抢断眩晕后一声哨响就能立刻恢复。 |
hero.distract() 干扰 | 让敌方守门员减速(速度 × distract.factor),持续 distract.duration 秒。冷却 distract.cooldown 秒。射门前用它,能让守门员更难扑救。 |
true;如果技能还没冷却好、或者游戏当前未激活,则返回 false。所以稳妥的做法是:调用前先用 abilityReady(...) 判断一下,避免浪费。
教练能读取全场信息,这些都是只读属性,是你做决策的"眼睛":
| 属性 | 说明 |
|---|---|
hero.ball | 球的信息 {x, y, vx, vy, owner, heldByKeeper};其中 owner 是当前持球球员的视图,若没人持球则为空(Python 中为 None,C++ 中为 nullptr)。 |
hero.myMates / hero.theirMates | 我方 / 对方球员的视图数组,每个含 {x, y, id, color, hasBall, isStunned, isImmune, clashReady}。 |
hero.myKeeper / hero.theirKeeper | 我方 / 对方守门员的信息 {x, y, hasBall}。 |
hero.myGoal / hero.theirGoal | 我方 / 对方球门的信息 {x, y, width},即球门线上的中心点。 |
hero.score / hero.opponentScore | 当前我方 / 对方的进球数。 |
hero.color | 你所在队伍的颜色,"red" 或 "blue"。 |
hero.gameTime | 整场比赛已经进行的秒数。 |
hero.roundTime | 当前回合已经进行的秒数。 |
hero.round | 当前回合编号,0(第一回合)或 1(第二回合);本回合剩余时间 = params.halfTime - hero.roundTime。 |
hero.gameActive | 游戏是否处于进行状态;暂停时为 false(此时所有动作和技能都无效)。 |
下面这段教练逻辑做了两件聪明事:帮被眩晕的队友吹哨解控,以及在对方持球时压迫拖慢对手。
while True: # 谁被眩晕了,就吹哨帮他解除 for m in hero.myMates: if m.isStunned and hero.abilityReady("whistle"): hero.whistle(m) # 敌方持球时,用压迫拖慢对手 carrier = hero.ball.owner if carrier and carrier.color != hero.color and hero.abilityReady("press"): hero.press(carrier) hero.wait(0.3)
int main() { while (true) { // 谁被眩晕了,就吹哨帮他解除 for (int i = 0; i < hero.myMates.size(); i++) { auto m = hero.myMates[i]; if (m.isStunned && hero.isReady("whistle")) { hero.whistle(m); } } // 敌方持球时,用压迫拖慢对手 auto carrier = hero.ball.owner; if (carrier && carrier.color != hero.color && hero.isReady("press")) { hero.press(carrier); } hero.wait(0.3); } return 0; }
把球员 AI Agent、教练 AI Agent、守门员这三套系统串起来,一套稳健、适合新手起步的 AI世界杯思路大致如下:
hero.on("spawn-mate", ...),否则球员 AI Agent 根本不会动——这是最容易忘、也最致命的一步。mateId 区分角色——比如一人主攻、抢球射门,一人拉边接应或回防。每个角色都要处理 hasBall,防止后卫乌龙。passTo / cross,甚至撞墙传球来撕开防线。clash 前先查对手的 isImmune,避免对免疫者抢断而害自己眩晕。hero.distract() 拖慢它。inspire 给自家球员提速;防守时用 press 拖慢敌方持球者;自家球员被抢断眩晕时,立刻 whistle 解控。你可以通过 params 对象访问下面所有参数。以下均为默认值——如果文档过时,实际比赛可能采用不同数值,请以游戏内实时的 params 为准。
| 参数 | 默认值 | 含义 |
|---|---|---|
params.field | { minX:4, minY:0, maxX:96, maxY:64 } | 场地边界 |
params.gates.red | { x0:0, y0:24, x1:4, y1:40 } | 红队球门区 |
params.gates.blue | { x0:96, y0:24, x1:100, y1:40 } | 蓝队球门区 |
params.kickoffOffset | 4 | 开球偏移量 |
params.halfTime | 60 | 每回合时长(秒) |
| 参数 | 默认值 | 含义 |
|---|---|---|
params.stepDistance | 8 | 移动步长 |
params.mateSpeed | 20 | 球员基础速度 |
params.carrierSpeedFactor | 0.75 | 持球速度系数 |
params.captureRadius | 4 | 接球范围 |
| 参数 | 默认值 | 含义 |
|---|---|---|
params.clashRange | 4 | 抢断距离 |
params.clashStunTarget | 1.2 | 抢断成功眩晕对手时间 |
params.clashStunSelf | 0.8 | 抢断失败自晕时间 |
params.clashCooldown | 3 | 抢断冷却时间 |
params.possessionImmunity | 0.5 | 控球免疫时间 |
params.selfRecaptureDelay | 0.3 | 自传球接球延迟 |
| 参数 | 默认值 | 含义 |
|---|---|---|
params.rollReleaseRadius | 8 | 球释放安全半径 |
params.maxPower | 100 | 最大踢球力量 |
params.rollRangeFactor | 0.45 | 踢球射程系数 |
params.rollFriction | 55 | 滚动摩擦系数 |
| 参数 | 默认值 | 含义 |
|---|---|---|
params.keeperSpeed | 4 | 守门员移动速度 |
params.keeperCatchRadius | 3 | 守门员扑救范围 |
params.keeperHoldTime | 1 | 守门员持球时间 |
params.keeperZones.red | { x0:4, y0:20, x1:12, y1:44 } | 红队守门员区域 |
params.keeperZones.blue | { x0:88, y0:20, x1:96, y1:44 } | 蓝队守门员区域 |
params.superKickSpeed | 60 | 守门员解围速度 |
| 参数 | 默认值 | 含义 |
|---|---|---|
params.coach.inspire | { cooldown:6, duration:2, factor:1.5 } | 激励(提速) |
params.coach.press | { cooldown:6, duration:2, factor:0.6 } | 压迫(减速) |
params.coach.whistle | { cooldown:8 } | 哨声(解眩晕) |
params.coach.distract | { cooldown:10, duration:2, factor:0.5 } | 干扰(拖慢守门员) |
| 参数 | 默认值 | 含义 |
|---|---|---|
params.activity.pass | 1 | 成功传球加分 |
params.activity.clash | 2 | 赢得抢断加分 |
params.activity.shot | 2 | 射门加分 |
params.activity.ballInOpponentHalfPerSecond | 0.1 | 控球于对方半场每秒加分 |
| 系统 | 关键命令 / 概念 | 要点 |
|---|---|---|
| 目标 | 进球数 > 对手 | 平局比支持度,仍平则种子掷币 |
| 比赛 | 两回合 × halfTime 秒 | 红队先开第 0 回合,蓝队开第 1 回合 |
| 支持度 | 传球/抢断/射门/压对方半场 | 显示为教练血量;攻势球胜消极球 |
| 球员启动 | hero.on("spawn-mate", fn) | 必须写,否则球员不动;用 mateId 分工 |
| 移动 | mate.moveTo / chaseBall | 一步一决策;持球变慢,会被追上 |
| 抢断 | mate.clash() | 先查 isImmune,失败会自晕 |
| 出球 | mate.roll(x, y, power) | 传球=射门;可撞墙反弹;出球不阻塞 |
| 宏 | shoot/passTo/cross/defendGoal | cross 横传远门柱是招牌打法 |
| 控球 | captureRadius / possessionImmunity | 粘球;接球后有一次免疫窗口 |
| 守门员 | 固定 NPC,追球移动 | 引开后射远角;distract 拖慢它 |
| 教练 | inspire/press/whistle/distract | 即时技能,各自冷却,主线程运行 |
球员追风,教练运筹,智取门将 ⚽🧠
现在,去 AI世界杯的赛场上,让你的 AI Agent 踢出制胜一球吧!🏆
扣哒世界 编程知识 · 多人竞技场:AI世界杯(Chaotic Crossing Clash)
扣哒世界(CodeCombat)教研团队