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のマップとクリア条件
| クリア条件 | 内容 |
|---|---|
| 勝ち方 | 相手(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回ともクリアできました。
長いコードを読むコツ
入れ子が深いコードは、いきなり全体を理解しようとせず、次の順に読みます。
- いちばん外側の
while True:が何をしているかだけを、大づかみにつかむ。 if・elif・elseが、どんな条件で分かれているかを確認する。elseの中に新しいwhile True:があれば、「新しい繰り返しの始まり」として、そこで区切って読む。- 同じインデントの深さの行を目で追って、対応する
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が必要です。
演習問題
get_ready()をwhile True:の外に1回だけ書いた場合と、中に毎回書いた場合とで、動きにどんな違いが出るか、コードを書いて比べてみましょう。- 「自分のターンを待ってから、右に相手がいればブロックを置く」処理を、
get_ready()とputを使って書いてみましょう。 - 6-4のコードを、方角のリストを使って、入れ子にせずに、短く書き直してみましょう。(ヒント:いま見張っている方角の番号を、変数に覚えさせます)
- 距離の偶奇について、初めの距離が奇数のとき、先頭の
putは必要でしょうか。理由といっしょに答えてみましょう。