跳弾(ちょうだん)のしくみ
あなたの担当 この章は、bot の中で使う計算の説明です。使うだけなら → はね返りで撃つ
弾は鉄ブロック(STEEL)に当たると 1 回だけはね返ります。 この性質を使うと、壁の陰にいて直接は狙えない敵にも当てられます。
ここでは、はね返りがどう計算されているかを見ます。 規則そのものは短いのですが、「角にちょうど当たったとき」だけは特別扱いが要ります。 その理由まで含めて説明します。
ブロックの種類とふるまい
Section titled “ブロックの種類とふるまい”| 地形 | grid の値 | 弾が当たると |
|---|---|---|
| 空き地(EMPTY) | 0 | 何も起きない(通過) |
| 木箱(BRICK) | 1 | はね返らない。 耐久が 1 減り、弾は消える |
| 鉄(STEEL) | 2 | はね返る。 2 回目の接触で消える |
| 水(WATER) | 3 | 通過する(戦車は入れないが、弾は飛び越える) |
はね返るのは鉄だけです。木箱を撃つと壊れていき、耐久 2 から 0 で消えます。
壁に当たった弾は、鏡に映したように向きを変えます。 このとき使うのが、壁の面に垂直な向きを表す法線ベクトル です。
入ってきた速度を 、はね返ったあとを とすると
は内積で、 のことです。 これは「 のうち、壁に向かって突っ込んでいる成分の大きさ」を表します。 その成分だけを 2 倍引くので、壁に垂直な向きだけが逆転し、壁に沿う向きはそのままになります。
この式が、軸に沿った壁だと一気に簡単になる
Section titled “この式が、軸に沿った壁だと一気に簡単になる”CodeTank Arena のフィールドはマス目です。鉄ブロックの面は必ず縦か横を向いていて、 法線は 4 通りしかありません。
| 当たった面 | 法線 | の結果 |
|---|---|---|
| 左面・右面(X 面) | — の符号を反転 | |
| 上面・下面(Y 面) | — の符号を反転 |
法線の向き( か )は結果に影響しません。 が 2 回掛かるので符号が打ち消し合うためです。 つまり実装は「どちらの軸の面に当たったかを決めて、その軸の速度の符号を反転する」だけで済みます。 三角関数も平方根も要りません。
当たった面をどう見分けるか
Section titled “当たった面をどう見分けるか”弾の半径は 0.08 マス、1 フレームの移動量は約 0.146 マスで、どちらもマスの幅 1.0 マスよりずっと小さい。 なので壁をすり抜けること(トンネリング)は起きません。判定は次の手順です。
- 動かしたあとの弾の中心が、どのマスに入ったかを調べる
- そのマスが STEEL なら、動かす前と後の「マス座標」を見比べる
- cellX も cellY も変わった → 角に当たった(後述のタイブレーク)
- cellX だけ変わった → X 面
- それ以外 → Y 面
- まだはね返っていなければ、速度を反転し、位置を接触点まで戻す。すでに 1 回はね返っていれば、弾を消す
「動かす前と後で、どちらのマス座標がまたいだか」を見るだけです。 横にまたいだなら左右の面、縦にまたいだなら上下の面に当たったということです。
角に当たったとき:X 軸の反転を優先する
Section titled “角に当たったとき:X 軸の反転を優先する”問題は、cellX と cellY が同時に変わったときです。 弾がブロックの角をかすめると、1 フレームのあいだに縦にも横にもマスをまたぎます。
このとき「X 面に当たった」と読むか「Y 面に当たった」と読むかで、 はね返る向きが 90 度変わります。そしてどちらの解釈も、幾何学的には等しく正しいのです。
そこで、エンジンは規則で決め打ちします。
なぜ規則で決め打ちするのか
Section titled “なぜ規則で決め打ちするのか”「どちらでもよい」場面に選択の余地を残さないためです。
CodeTank Arena は決定論を守ります。 同じ試合は、どの端末で再生しても、1 ビットたりとも違わない結果にならなければいけません。 試合は映像ではなく短い数字の列(リプレイ)として保存され、再生のたびに計算し直されるからです。
もしここで「実装の都合で先に見つかったほう」を採ると、 配列を回す順番や、コンパイラの最適化や、端末の違いで答えが変わり得ます。 曖昧さは、いつか必ず食い違いになります。
だから「X を優先する」と先に決めてあります。 X が Y より優れているからではありません。決まっていること自体に意味があります。
これは跳弾に限った話ではなく、同じ考え方がエンジン全体を貫いています。
| 同時に起きたとき | 決めてあること |
|---|---|
| 弾が角に当たった | X 軸の反転を優先 |
| 戦車と壁が重なった | X → Y の順で押し戻す |
| 戦車の中心が完全に一致した | A を 、B を へ押し出す |
| 複数の弾が同時に相殺した | 生成順 ID の昇順で処理する |
はね返った弾は、自分にも当たる
Section titled “はね返った弾は、自分にも当たる”跳弾は撃った本人にも命中します。自爆があります。
狭い場所で壁に向かって撃つと、はね返ってきた弾が自分に刺さります。
state にはその情報が入っているので、読んで避けられます。
def update(state): danger = False i = 0 while i < len(state.my_missiles): m = state.my_missiles[i] if m.bounced: # 1 回はね返ったあとの自分の弾 d = distance(state.me.x, state.me.y, m.x, m.y) if d < 2.0: danger = True i = i + 1
# 危なければ撃たずに下がる if danger: return {"drive": "backward", "steer": state.me.angle, "fire": False, "barrier": False}
return {"drive": "forward", "steer": state.me.angle, "fire": True, "barrier": False}はね返った自分の弾は incoming_missiles(飛んでくる弾)にも載ります。
そちらは危ない順(eta の昇順)に並んでいるので、incoming_missiles[0] が
「いちばん先に当たる弾」です。この順番は仕様なので、bot が頼ってかまいません。
壁を使って回り込む(鏡像法)
Section titled “壁を使って回り込む(鏡像法)”はね返りを攻めに使うときの考え方も、鏡の話に落ちます。
縦の鉄の壁が にあるとき、その壁を鏡だと思って敵を鏡の向こうに映します。
そして鏡像をまっすぐ狙って撃ちます。 弾は壁で折れ曲がり、ちょうど本物の敵に届きます。 折れ線で考えると難しい経路が、直線 1 本の問題に変わるのがこの方法の利点です。
横の壁なら を折り返します。 です。
実際に撃つ前には、2 本の線がどちらも通っているかを line_of_sight で確かめてください。
- 自分 → 壁の反射点まで、さえぎるものがないか
- 反射点 → 敵まで、さえぎるものがないか
どちらかが別のブロックでふさがれていれば、その跳弾は届きません。 また、反射は 1 回きりなので、反射点の手前に別の鉄があると、そこではね返って終わりです。
- 動く敵に当てる計算 → 偏差射撃
- 上のコードで使った構文の範囲 → Sub-Python の構文