Skip to content

疯狂 Geek 兔

游戏介绍

疯狂 Geek 兔是一只在太空披荆斩棘,希望获得更多的胡萝卜的疯狂兔子。它必须使出超大能量去获得其他太空兔身上的胡萝卜,来达到目的生存下去。

你需要运用代码做技术策略,控制兔子在碰撞时的能量值大小及碰撞方向,来获得对方兔子的胡萝卜(分数)。 能量值 = 攻击力,胡萝卜个数 = 分数。

兔子与兔子互相碰撞,来争夺胡萝卜:

兔子之间发生碰撞时,根据兔子碰撞时的能量值大小判定胜负。能量值大的兔子获得 1 个胡萝卜,加 1 分;反之,能量值小的一方兔子丢失 1 个胡萝卜,扣 1 分。相同能量碰撞,能量会消耗,萝卜互不增减。如三只以上的兔子相碰撞,根据汽车追尾原则进行两两能量值大小判定。

兔子与地图内障碍物碰撞:

兔子碰到场地中的陨石,丢失 1 个胡萝卜,扣 1 分。

游戏规则:

  1. 一场比赛 180 s(3 分钟),游戏帧率 60 FPS,初始兔子能量值(攻击力) 1000,胡萝卜 10 根(10 分)
  2. 每 30 s 系统统一恢复场上所有兔子的能力值至 1000
  3. 道具“无敌金萝卜”实效性 10 s。获得后的能力:任意碰撞都不会扣能量且能够赢得胡萝卜,不过被撞后会影响自己的轨迹。
  4. 如在规定时间 30 s 内不进行兔子之间或者和石头碰撞,会扣 3 个萝卜
  5. 兔子的胡萝卜为 0 则失败

特别提醒:

地图的原点在左上角,x 轴正方向朝右,y 轴正方向朝下。

交互流程

为避免数据传输延迟带来的不确定性,本游戏采用改进版实时对战交互流程,从机制上实现零传输时延。

具体而言,游戏逻辑将逐帧模拟对战场景。每一帧模拟完成后,游戏逻辑会向当前不处于计算状态的玩家发送局面信息,并让这些玩家轮流计算并返回决策结果。

设某玩家用时 \(k\) 帧(向上取整,平台会准确计算玩家的 CPU 用时,且排除通信等待时间)完成计算和决策,那么游戏逻辑会在 \(k\) 帧后应用该玩家的决策。

需要注意的是,处于计算状态的玩家不会收到实时局面信息。这是出于评测流程约束和零时延设计等多方面因素的考量。计算过程中错过的局面信息将在计算结束后一并发送给该玩家。

如果玩家 AI 的计算时间很长,尚未完成决策,又不想错过实时局面信息,则可以发送 noop 指令。但该方法对玩家把握计算时间的能力要求较高,且平台目前只允许玩家 AI 使用单个线程,因此该方法实现难度较高。

doc-frame

如果玩家 AI 在决策时间内超过 60 帧(1 秒)仍未给出任何回应,游戏逻辑将判定该玩家超时。

通讯格式

AI 接收战场数据

游戏开始时,AI 会收到 commandTypestartGame 的初始数据,包含地图及各兔子的初始信息,格式示例如下:

{
    "commandType": "startGame",
    "data": {
        "myId": 0,             // 我的位置,取值为 {0, 1, 2, 3}
        "map": {
            "width": 1440,
            "height": 820,
            "borders": [       // 星球边界范围的数组,每个子元素代表一个边界的坐标信息
                [              // 第一块边界,子元素表示分割为多个凸多边形的顶点坐标集合
                    [          // 分割的凸多边形的各个顶点
                        { "x": 720, "y": 39 },
                        { "x": 720, "y": 0 },
                        { "x": 0, "y": 0 },
                        { "x": 655, "y": 57 }
                    ],
                    [          // 分割的凸多边形的各个顶点
                        { "x": 721, "y": 780 },
                        { "x": 645, "y": 761 },
                        { "x": 720, "y": 820 }
                    ],
                    // ...
                    // ...
                ],
                // ...
                // ...
            ],
            "blocks": [        // 所有石块范围的数组,每个子元素代表一个石块的坐标信息
                [              // 第一个石块,子元素表示分割为多个凸多边形的顶点坐标集合
                    [          // 分割的凸多边形的各个顶点
                        { "x": 952.668, "y": 299.556 },
                        { "x": 960.668, "y": 300.556 },
                        { "x": 973.668, "y": 287.556 },
                        { "x": 970.668, "y": 283.556 },
                        { "x": 956.668, "y": 285.556 },
                        { "x": 953.668, "y": 291.556 }
                    ],
                    // ...
                    // ...
                ],
                [              // 第二个石块,子元素表示分割为多个凸多边形的顶点坐标集合
                    [          // 分割的凸多边形的各个顶点
                        { "x": 569.047, "y": 533.269 },
                        { "x": 563.047, "y": 517.269 },
                        { "x": 546.047, "y": 512.269 },
                        { "x": 531.047, "y": 519.269 },
                        { "x": 526.047, "y": 531.269 },
                        { "x": 526.047, "y": 537.269 },
                        { "x": 549.047, "y": 546.269 }
                    ]
                ]
            ]
        },
        "rabbits": [{ /* ... */ }]  // 兔子信息的格式描述在下一部分给出
    }
}

游戏场景边界和障碍区石块均为不规则多边形。游戏会将多边形分割为多个凸多边形的集合,如下图所示。地图的 JSON 信息中将包含障碍物分割后的各个凸多边形的顶点坐标。

doc-rock-vector

随后,AI 会收到 commandTyperefreshData 的逐帧游戏局面信息,格式示例如下:

{
    "commandType": "refreshData",
    "data": [
        {
            "frame": 114,
            "rabbits": [
                {
                    "index": 0,     // 兔子序号
                    "speed": 5,     // 兔子运动速度,包含碰撞后发生回弹运动的瞬时速度和行驶瞬时速度,单位px/每帧
                    "position": {
                        "x": 779.3980154265994,
                        "y": 175
                    },              // 位置坐标,基于兔子中心点
                    "angle": 1.5707963267948966, // 角度(弧度值 [-PI, PI])
                    "angularSpeed": 0,           // 单位时间角速度,单位:弧度/每帧
                    "width": 60,    // 兔子的宽(像素)
                    "height": 60,   // 兔子的高(像素)
                    "score": 20,    // 兔子得分
                    "moveState": 1, // 兔子移动状态,1为向前,0为停,-1为后退
                    "dirState": 0,  // 兔子转向状态,1为向右,0为直行,-1为向左
                    "active": true, // 兔子是否处于活着状态
                    "rebounding": false,         // 兔子是否处于碰撞回弹状态
                    "reboundAngle": 0,           // 兔子回弹角度,弧度值
                    "invincible": true,          // 兔子是否处于无敌状态
                    "deathCount": 0,
                    "velocity": {
                        "x": 3.061616997868383e-16,
                        "y": 5
                    },              // 兔子位移运动速度向量
                    energy: 50      // 剩余能量值
                }
            ],
            "goldCarrot": {         // 当场上有金萝卜时,该字段会给出金萝卜的坐标信息
                "x": 600,           // 否则,该字段是一个空对象
                "y": 300
            }
        },
        {
            "frame":115,
            // ...
        },
        // ...
    ]
}

AI 发送控制指令

AI 可以发送指令,控制战场上兔子的操作,详细如下:

前进:速度为5px/每帧
{ commandType: "goForward" } 

后退:速度为5px/每帧
{ commandType: "goBack"}

向左转:可设定转向角速度(弧度/帧),有效区间为[0.01, 0.1],默认0.05,收到指令后,在兔子前进或后退过程中生效
{ commandType: "turnLeft", data: 角速度值 }

向右转:可设定转向角速度(弧度/帧),有效区间为[0.01, 0.1],默认0.05,收到指令后,在兔子前进或后退过程中生效
{ commandType: "turnRight", data: 角速度值 }

方向回正:回正行驶方向,转向速度变为0
{ commandType: "steerBack" }

停止:停止移动并回正行驶方向,速度变为0,转向速度变为 0
{ commandType: "stop" }

设置消耗能量值:消耗能量值有效区间为[0, 1000],初始默认值50,但不能超过当前兔子的剩余能量值,否则取剩余最大能量。
{ commandType: "setAttackValue", data: 消耗能量值 }

空操作
{ commandType: "noop" }

AI 可以发送单个指令,也可以发送一个指令的列表。如果发送指令列表,游戏逻辑会按照列表顺序依次应用指令。因此,如果同一帧内发送了多条冲突指令,位于后面的指令会覆盖位于前面的指令。

碰撞及反弹规则

兔兔碰撞、兔物碰撞。兔子边界任意一点与其他兔子或障碍物相交,则判定碰撞发生,碰撞后,兔子根据下面的反弹规则进行反弹,反弹结束后会恢复碰撞前的运行状态,可在反弹过程中发送控制指令覆盖碰撞前的指令,指令在回弹结束后生效。

两兔相撞

假设碰撞瞬间;

兔 A 坐标为 \((x_1, y_1)\);

兔 B 坐标为 \((x_2, y_2)\);

兔 A 位移速度向量为 \((v_{ax}, v_{ay})\);

兔 B 位移速度向量为 \((v_{bx}, v_{by})\);

如果其中一方速度为 \(0\),则设该物体速度向量为有速度兔子的速度向量的反向向量。

碰撞后,兔 A 反弹初速度向量计算如下(向量加乘计算):

\(v_a = (x_1-x_2, y_1-y_2)*0.02+ (v_{ax}, v_{ay})*-0.3 + (v_{bx}, v_{by}) * 0.7\)

兔 B 反弹初速度向量计算如下(向量加乘计算):

\(v_b = (x_2-x_1, y_2-y_1)* 0.02 + (v_{bx}, v_{by}) * -0.3 + (v_{ax}, v_{ay}) * 0.7\)

得到 \(v_a\)\(v_b\) 后,兔子将以该向量大小为初速度、以向量方向为反弹方向做匀减速运动,加速度为 \(-0.15\)px每帧。

如何估计兔 A 的速度向量呢?

如果处于碰撞反弹中:

移动速度向量为: x:兔A.speed * Math.cos(兔A.reboundAngle), y: 兔A.speed * Math.sin(兔A.reboundAngle)

如果汽车处于行驶状态:

移动速度向量为: x:兔A.speed * Math.cos(兔A.angle), y: 兔A.speed * Math.sin(兔A.angle)

或者,直接取 兔A.velocity 属性即可。

兔子与石块碰撞

规则与兔兔碰撞相同,会设石块速度向量为有速度小车速度向量的反向向量,石块不会被反弹。

AI 编写说明

我们提供了 Python 版本的样例代码,可在 Saiblo 游戏页面 上方点击“下载游戏包”按钮下载。

随后,你可以进入“我的 AI”标签页,上传自己的 AI 代码,并“创建房间”,测试自己的算法。

AI 采用标准输入输出读取局面信息并给出决策指令。

每个输入包均为单行 JSON,可采用如下代码读取:

message = json.loads(sys.stdin.readline())

每个输出包需要按照 Saiblo 规定的协议进行输出,可采用如下代码进行输出:

write_to_judger(json.dumps({"commandType": xxx}))

如果要在单帧内给出多条操作指令,可将 json.dumps() 的参数改为一个指令列表。

注意:每条输入和输出语句必须交替进行、一一对应,否则可能读取到错乱的信息,或触发超时。

由于 stdinstdout 用于和评测机交互,调试信息的输出需要通过 stderr 进行。样例代码中封装了 debug() 方法,可用于输出调试信息。

注意:输出调试信息可能引入额外的时间开销,因此请避免输出过多调试信息,且在最终评测时建议去除所有调试信息输出。

样例代码只是给出了很简单的实现,你可以在此基础上加以完善。根据你代码的复杂程度,你也可以不局限于单个 Python 文件。Saiblo 平台能够接受一个 zip 压缩包作为 AI 代码,其中入口文件名须为 main.py

评分

本作业为课堂附加作业,因此评分较为严格。具体评分标准如下(以 DDL 时 GitLab 版本为准):

  • 白盒为强制要求。如果平台代码与 GitLab 上不一致,或助教认为你的代码风格较差、Git 使用有严重错误,或者存在学术诚信问题,则无论排名直接记为 0 分(不影响其他人的排名情况)
  • 成功编写 AI 参与游戏,并通过资格赛:记 1 分
  • 前 11~15 名(如果循环赛参赛少于 20 人则为前 6~10 名,如果少于 10 人则为第 3~5 名,如果少于 5 人则为第二名):记 3 分
  • 前 4~10 名(如果循环赛参赛少于 20 人则为第 3~5 名;如果少于 10 人则为第二名;如果少于 5 人则为第一名):记 4 分
  • 前 2~3 名(如果循环赛参赛少于 20 人则为第二名;如果少于 10 人则为第一名;如果少于 5 人则不评选):记 6 分
  • 第一名(少于 10 人不评选):记 8 分

上述加分不叠加,并且必须通过资格赛才可得到分数。

在游戏结束后,会有额外展示环节,参与的同学视情况也可获得若干加分。此项对排名无限制,只要提交代码并成功运行即可。如果你觉得你的实现在任何方面有值得分享的地方(如状态解析和存储,兔子的运动规划、障碍规避、能量分配、博弈策略等),都欢迎参与分享!

杜绝抄袭。

FAQ

1. 兔子速度固定吗?

是的,恒定的。

2. 能知道其他兔子的位置和剩余能量吗?

可以的。

3. 每次碰撞要花费的能量值是 AI 实时计算的吗?

可以的,不过注意对手可能也会改变能量值。

4. 同等能量值碰撞会发生什么?

相同能量碰撞能量消耗,胡萝卜互不增减。

5. 金萝卜的状态是否会在数据中返回,坐标点存在是否就证明没有被拿走?

是的。

6. 拿到金萝卜无敌的兔子状态是否会在数据中返回?

是的,兔子吃了金萝卜后无敌状态是这个invincible字段。

7. 一局比赛中地图是动态变化的吗,边界和障碍物会变吗?

不会变。