CHaser解説(Python編)

第2部 Python編

4章 ターンの仕組みと、攻撃

4章 ターンの仕組みと、攻撃

入門編の第2部7・8章(ステージ5・6)に対応します。

CHaserのしくみの要点

  • 対戦では、CoolとHotが1手ずつ交互に行動します。自分の番が来るまでは、行動できません。
  • 1回の番で、行動できるのは1回だけです。walk・put・look・searchの4つは、どれも1回の行動で、使うとその番は終わりです。
  • プログラムの基本の流れは、「get_ready()で番を待つ → 行動を1つ → get_ready()で次の番を待つ → 行動を1つ → …」の繰り返しです。
  • チュートリアルでは、相手(CPU)のいるステージ(5-1、6-1など)で、毎ターンget_ready()が必要です。書かないと、最初の1回動いたところで止まります。CPUのいないステージでは省略できます。本番では、必ず必要です。
  • put(ブロックを置く)は、自分はその場から動きません。置いたぶんだけ、自分は1手使います。
  • 自分の番が来た瞬間は、相手はまだ動いていません。その瞬間に相手が上下左右のどれかに隣り合っていれば、その方向にputすることで、確実にアタック勝ちになります。

ターンを待つ:get_ready

while True:
    map_info = get_ready()
    walk(Right)

get_ready()は、自分の番が来るまで待ち、番が来たら、そのときの周辺情報を返します。この1行で、「待つ」と「最新の周辺情報を受け取る」の両方をしています。かっこの中が空なのは、方向を指定する必要がない(ただ待つだけ)からです。

get_ready()は、while True:の中、判断の直前に置きます。

while True:
    map_info = get_ready()
    if map_info[Right] == Enemy:
        put(Right)
    else:
        walk(Right)

「get_ready()で最新の情報を受け取る」→「その情報をもとにifで判断する」という2段階を、毎ターン繰り返すのが基本の形です。もしget_ready()をwhile True:の外に1回だけ書くと、最初の1回しか待たず、それ以降は古い情報のまま判断し続けてしまいます。

ブロックを置く:put

put(Down)

put(方向)は、方向のマスにブロックを置く命令です。書き方はwalk(方向)と同じで、動詞がwalkからputに変わっただけです。相手のいるマスに置けばアタック勝ちですが、自分の逃げ道をふさげば、自分が動けなくなる可能性もあります。

上の例のように、「右隣がEnemyならput、そうでなければwalk」という形が、アタックの基本形です(ステージ6-1)。

落とし穴:距離の偶奇

相手が動き回るステージでは、移動だけを繰り返しても、絶対に勝てない場合があります。

自分も相手も、毎ターン必ず1マス動きます。自分が1回動いて、相手が1回動くたびに、2人のマンハッタン距離(縦のずれ+横のずれ)は、「2縮む」「変わらない」「2広がる」のどれかにしかなりません。だから、距離が偶数なら偶数のまま、奇数なら奇数のままで、偶奇は永久に変わらないのです。

アタック勝ちには、距離1(奇数)で隣り合う必要があります。初めの距離が偶数で、相手が動き回るなら、移動だけを繰り返しても、一度も隣り合えません。

ステージ6-2(初めの距離8)と6-4(初めの距離4)が、これにあたります。実機のチュートリアル(Python)で、6-2に、先頭のputがないコードを3回試したところ、3回ともクリアできませんでした。先頭にputを入れたコードは、3回ともクリアできました。

解決法は、putです。putは自分が動かない行動なので、1回置くと、自分だけ足踏みして、偶奇がずれます。プログラムの先頭に、1回putを入れるだけで、以降は距離が奇数になり、隣り合えるようになります。

map_info = get_ready()
put(Left)           # 先頭で1回だけ:自分の後ろに置いて、距離の偶奇をずらす

while True:
    map_info = get_ready()
    # ここから、通常の追いかけ(判断と移動)

置く方向は、自分の通り道の邪魔にならない、後ろ側にするのが無難です。

上下左右を順に確認する(ステージ6-4)

ステージ6-4のマップとクリア条件

ステージ6-4のマップ
ステージ6-4のマップ
クリア条件 内容
勝ち方 相手(Hot)にブロックを置いて撃破する
ターン数 30ターン以内
相手 CPU(レベル2)がいる

黄色の枠が自分(Cool)のスタート位置です。すべての条件を同時に満たすとクリアです。

ステージ6-4は、中央に3×3のブロックの輪があり、その左右にCoolとHotが立ちます。相手はランダムに動きます。次のコードは、「その方角に相手がいれば置く/空きマスなら進む/ブロックなら次の方角を確認する」という判断を、下→右→上→左の順に、入れ子にして重ねたものです。

map_info = get_ready()
put(Up)             # 先頭の1回:距離の偶奇をずらす
map_info = get_ready()
while True:
    if map_info[Down] == Enemy:
        put(Down)
    elif map_info[Down] == Floor:
        walk(Down)
    else:
        while True:
            if map_info[Right] == Enemy:
                put(Right)
            elif map_info[Right] == Floor:
                walk(Right)
            else:
                while True:
                    if map_info[Up] == Enemy:
                        put(Up)
                    elif map_info[Up] == Floor:
                        walk(Up)
                    else:
                        while True:
                            if map_info[Left] == Enemy:
                                put(Left)
                            elif map_info[Left] == Floor:
                                walk(Left)
                            else:
                                walk(Right)
                            map_info = get_ready()
                    map_info = get_ready()
            map_info = get_ready()
    map_info = get_ready()

かなり長く見えますが、同じパターンの繰り返しです。ある方角が空きマスでなくなる(ブロックにぶつかる)と、elseの中の新しいwhile True:に入り、次の方角を見張ります。1段深くなるたびに、見張る方角が1つずつ変わっています。深い入れ子のどこにいても、次の判断の前には、必ずmap_info = get_ready()で最新の情報を取り直しています。

先頭のput(Up)は、上で説明した距離の偶奇をずらすための1回です。このコードは、実機のチュートリアル(Python)のステージ6-4で、6回試して6回ともクリアできました。

長いコードを読むコツ

入れ子が深いコードは、いきなり全体を理解しようとせず、次の順に読みます。

  1. いちばん外側のwhile True:が何をしているかだけを、大づかみにつかむ。
  2. if・elif・elseが、どんな条件で分かれているかを確認する。
  3. elseの中に新しいwhile True:があれば、「新しい繰り返しの始まり」として、そこで区切って読む。
  4. 同じインデントの深さの行を目で追って、対応するifとelseを、ペアで確認する。

このコードは、Blocklyから変換された形なので、入れ子が深くなっています。次の節のように、リストを使うと、短く書けます(演習問題3)。

同じ動きを、短く書く

入れ子にしなくても、同じ動きを、短く書けます。見張る方角をリストにして、「いま見張っている方角の番号」を、変数iに覚えさせます。

directions = [Down, Right, Up, Left]   # 見張る方角の順番
i = 0                                  # いま見張っている方角の番号
map_info = get_ready()
put(Up)                                # 先頭の1回:距離の偶奇をずらす
while True:
    map_info = get_ready()
    # 相手もいない・空きマスでもない(ふさがった)方角は、次の方角へ
    while i < 3 and map_info[directions[i]] not in (Enemy, Floor):
        i += 1
    d = directions[i]
    if map_info[d] == Enemy:
        put(d)
    elif map_info[d] == Floor:
        walk(d)
    else:
        walk(Right)

入れ子のコードでは、ふさがった方角に出会うと、1段深いところへ進み、前の方角には戻りませんでした。ここでは、iを進めることが、その「1段深く進む」にあたります。iは増えるだけで、減りません。最後の方角(Left)までふさがっていたら、walk(Right)です。in (Enemy, Floor)は、「EnemyかFloorのどちらかである」という意味で、not inは、その反対です。このコードは、実機のチュートリアル(Python)のステージ6-4で、8回試して8回ともクリアできました(入れ子のコードは6回中6回)。

注意したいこと

  • get_ready()を、while True:の外に1回だけ書く:2回目以降は、古い情報のまま判断します。
  • get_ready()の戻り値を、受け取らない:get_ready()だけを書いてmap_info = を忘れると、最新の情報を使えません。
  • 行動を、get_ready()なしで続けて呼ぶ:walkやputをget_ready()なしで続けて呼ぶと、意図したタイミングで動かないことがあります。
  • 入れ子の中のget_ready()を、一部の分岐で書き忘れる:その分岐だけ、情報が更新されません。
  • 移動だけで勝てると思い込む:初めの距離が偶数で、相手が動き回るなら、先頭のputが必要です。

演習問題

  1. get_ready()をwhile True:の外に1回だけ書いた場合と、中に毎回書いた場合とで、動きにどんな違いが出るか、コードを書いて比べてみましょう。
  2. 「自分のターンを待ってから、右に相手がいればブロックを置く」処理を、get_ready()とputを使って書いてみましょう。
  3. 6-4のコードを、方角のリストを使って、入れ子にせずに、短く書き直してみましょう。(ヒント:いま見張っている方角の番号を、変数に覚えさせます)
  4. 距離の偶奇について、初めの距離が奇数のとき、先頭のputは必要でしょうか。理由といっしょに答えてみましょう。

YouTubeチャンネル

U-15プロコン舞鶴のYouTubeチャンネルを開設しました。大会の情報などを発信しています。 チャンネルを見る