海戦ゲーム 1

Swift Playgrounds にアルゴリズム系ゲームが追加されました

Swift Playgrounds に新しい課題が追加されたのでレポートです。

ゲームの様子、折り紙の鶴のようなところは、攻撃されて炎が出ている場所

今回追加されたのは上のようなゲームです。海に敵の軍艦がいて攻撃して沈めるという設定です。軍艦は5隻あり、2マスから5マスの長さを持ちます。はじめはどのマスに軍艦がいるかわかりません。どれかのマスをタップして(またはプログラムでマスを決定して)攻撃します。軍艦に当たると炎が出ます。外れるとバツ印が付きます。出来るだけ少ない攻撃ですべての当たり箇所を見つけることがこの課題の目的です。軍艦の一部だけでなく、全体に当てることが必要です。

プログラムで攻撃する場合、攻撃するマスを一回一回指定します。

  • 前回攻撃した場所
  • 特定のマスが攻撃されたか、攻撃されたなら当たりだったかはずれだったか

などの情報が得られるので、それを基にして次に攻撃する場所を判断します。作戦は難解なレベルまであると思いますが、 Swift Playgrounds の編集機能で作ろうとするとけっこう大変です。

あらかじめプログラムが与えられています。難易度別に3つあります。それを読んでどんな風に処理を行っていくかつかむことが出来ます。

ここからはアプリをやりながら読むことを前提にします。

概要

まずは「概要」のチャプターです。2プレーヤー版があるそうですが、このアプリ上では1プレーヤーで行うもののみ扱います。

軍艦の種類と占めるマスの数の紹介があります。

TODO

ゲームを行ってゲームの内容を把握する。タップで攻撃できます。iPad を縦向きで行うとマスが小さくなって操作しづらいので横向きが良いでしょう。

横向きの方がマスがでかい

軍艦を沈める

次に「軍艦を沈める」のチャプターです。ここから具体的にプログラムを扱っていきます。

TODO 1

初めから与えられているプログラムがあるので、それを実行します。このプログラムではランダムにマスを選んで攻撃し、当たりだったらその右を攻撃します。右を攻撃した結果が当たりだったらさらに右を攻撃します。右を攻撃して外れだったり端まで行って右がなかったら、またランダムに選んで攻撃します。

実行について

海の領域をタップすることで攻撃を一回行います。攻撃するマスはプログラムで決定するので、(初めの課題とは違って)タップの位置と攻撃の位置が別になります。

軍艦の位置が全て確定するのに数十回の攻撃が必要なゲームなので、実行を速めるために右上に「1回プレイ」と「100回プレイ」が用意されてます。

  • 「1回プレイ」は軍艦の位置が全て確定するまでを自動で行います
  • 「100回プレイ」は軍艦の位置が全て確定するまでの工程を100回行います。100回行って、その平均の攻撃回数を出すことによってアルゴリズムの優秀さを確認できます。攻撃の様子を見ることは出来ません。

プログラムを見ていく

初めから与えられているプログラムを見ていきます。例によってプログラムのほとんどの部分は裏に隠れているので、アプリ実行者から見えるのは firstCoordinate メソッドと nextCoordinate メソッドだけです。

  • firstCoordinate 一手目に攻撃するマスを決定します
  • nextCoordinate 二手目以降に攻撃するマスを決定します。一つ前に攻撃したマスが引数として与えられます。

ここからはプログラムを読むのに必要な情報です。画面に表示されている以外のソースコードは、

3点リーダーアイコン → 詳細 → 補助ソースファイルを表示

とたどると見れます。裏のソースコードについてはここから見たソースに基づいて書いています。

まずは firstCoordinate メソッドの内容です。

grid.randomCoordinate()

randomCoordinate()は攻撃していないマスをランダムに選んで返します。

次に nextCoordinate メソッドの内容です。前回攻撃したマスの情報を引数 previousTile: Tile で受けます。

各マスの情報は Tile というクラスが受け持ちます。 Tilestate: TileStateというプロパティを持ちます。

public enum TileState {
case unexplored //攻撃されていない
case hit //当たり
case miss //外れ
case suggested
case invalid //無効
}

suggested だけ使っている箇所がわかりませんでした。invalid は右端からさらに右を取得すると invalid の Tile が返ってくるという使い方をします。

メソッドの中身に入ります。

if previousTile.state == .hit

前回攻撃したマスが当たりだった場合 true になります。

let neighborCoordinate = previousTile.coordinate.advanced(by: 1, inDirection; .east)

右の座標を取得しています。すでに右端にいたとしても、さらに右の座標を取得します。

let neighborTile = grid.tileAt(neighborCoordinate)

取得した座標からタイルを取得します。範囲外の座標の場合はタイルの state は .invalid になります。

if neighborTile.state == .unexplored

取得した右側のタイルが実在していてまだ攻撃されていない場合 true になります。

ステップ実行について

Swift Playgrounds にはもともと実行スピードが3種類あります。

  • コードを実行
  • コードをステップ実行
  • コードをゆっくりステップ実行

の3つです。Swift Playgrounds バージョン 1.2 では、プログラムの通りに攻撃すべき状況でも、ステップ実行中はタップした位置に攻撃が加えられてしまうようになっています。プログラムで攻撃しているのに手動攻撃が混ざってしまうと正しい結果にならないので、タップしないほうがよいでしょう。

TODO 2

2つのメソッドの内容を改造して攻撃回数の平均が75を切るようにします。

やってみた

1つおきに攻撃していって当たりが出たらその周りをひとつづつ攻撃するやり方で平均65攻撃になりました。

enum Kind {
case step
case rounding
}
var currentColumn = 0
var currentRow = 0
var kind: Kind = .step
var target: Direction? = nil
func firstCoordinate() -> Coordinate {
return Coordinate(column: currentColumn, row: currentRow)
}
func nextCoordinate(previousTile: Tile) -> Coordinate {
switch kind {
case .step:
if previousTile.state == .hit {
kind = .rounding
target = nil
fallthrough
}
case .rounding:
while kind == .rounding {

switch target {
case nil:
target = .east
case .east?:
target = .south
case .south?:
target = .west
case .west?:
target = .north
case .north?:
target = nil
kind = .step
}

if let target_uw = target {
let neighborCoordinate =
Coordinate(column: currentColumn,
row: currentRow)
.advanced(by: 1, inDirection: target_uw)
let neighborTile = grid.tileAt(neighborCoordinate)
if neighborTile.state == .unexplored {
return neighborCoordinate
}
}
}
}

if currentColumn < 8 {
currentColumn += 2
} else if currentRow < 9 {
currentRow += 1
currentColumn = (9 - currentColumn)
}
return Coordinate(column: currentColumn, row: currentRow)
}

fallthroughは Swift Playgrounds 内で出てきてないような気がしますが、次の case に移行する命令です。

.east? はパターンマッチで使われる表記法です。

北を攻撃した直後をどうするかが難しいところでした。 .step の方を走らせると当たりだったときにまた周辺攻撃が始まってしまうし、それを避けるための分岐も初見でわかりにくくなりそうだったので、北を攻撃した直後はもう一回 .rounding の方にいれています。このように処理の流れを見やすくするためにいろいろ変更した結果、攻撃の方向はtarget: Direction? = nil とオプショナルにして、周辺攻撃開始のときに初期値 nil を入れています。周辺攻撃中は .rounding は最大5回走るのですが、 nil を使うことで5つの分岐が行えるようになって、見やすくなりました。

軍艦を探す

次は「軍艦を沈める」のチャプターです。さらにいくつかのやり方が文章で紹介されています。しかしここで説明されているやり方の具体的なプログラムは用意されてないようです。

何かの選択を迫るあやしげな文章

バージョン 1.2 ではプログラム領域の先頭にあやしげな選択肢が表示されます。

前のページで75回以下の目標を達成していない場合は

  • 前のページに戻る
  • このページでコーディングを開始

という選択肢が出ます。

前のページで75回以下の目標を達成した場合は

  • 自分のコードをコピー(前のプログラムをこのページに複製する)
  • このページでコーディングを開始

という選択肢になります。

このページでコーディングを始める方を選ぶとより作戦が複雑になったプログラムがあらかじめ表示されます。

プログラムを前のページからこのページに複製するメリットは特にないので、ちょっと意図がわからないところです。

まずは、あらかじめ用意されたプログラムを表示するとして話を進めます。

TODO 1

あらかじめ用意されたプログラム実行してみて、処理の内容を把握する。

このプログラムでは、ランダムに攻撃していき(この記事ではランダム攻撃ということにします)、当たりが出たらその隣を攻撃します。そこが当たりだったらさらに同じ方向に進めて攻撃します。これをはじめに当たりが出たマスを中心にして4方向行います(これも周辺攻撃ということにします)。

プログラムを見ていく

今までと同じように、はじめに攻撃する場所を指定するメソッドと、2回目以降の攻撃場所を指定するメソッドの2つです。このページのはじめに文章で説明されているやり方とは別の方法です。

プログラムを見ていきます。

var tileToFollowFrom = Tile()

起点となるマスです。ひとつの方向への攻撃が終了して次の方向に切り替えるときの為にこれを記憶しておくことが必要になります。オプショナル化を避けるために Tile()で初期化してあります。

var directionsToFollow = [Direction]()

方向を配列です。周辺攻撃開始時に東西南北を設定します。ある方向の攻撃が終了したらその方向を削除します。要素数が0になったら周辺攻撃終了の処理に移ります。

ここから nextCoordinate メソッドの内容です。

if directionsToFollow.isEmpty

directionsToFollow は周辺攻撃開始時に東西南北の要素を入れて、周辺攻撃中に減っていき、要素が0になったら周辺攻撃終了です。つまりこの if 文はランダム攻撃時に true になるようになっています。

var tileToCheck = previousTile

このプログラムで一番理解が難しいのがこの tileToCheck です。使われ方は次のようになります。周辺攻撃中に当たりかどうかの判定はこの変数に対して行われます。

  • ランダム攻撃で当たりが見つかったとき、 previousTile が入ります。このとき previousTiletileToFollowFrom と同じです。(当たり判定は当然 true になります)
  • ある方向に周辺攻撃したあとは、 previousTile が入ります。今度は previousTile には直前に攻撃したマスが入っています。
  • ある方向の周辺攻撃が終了すると tileToFollowFrom が入ります。これは次の方向への攻撃の準備です。(当たり判定は当然 true になります)
directionsToFollow.removeFirst()
tileToCheck = tileToFollowFrom

ひとつの方向の攻撃が終わったときの処理です。方向配列の先頭を削除します。 tileToChecktileToFollowFrom を入れます。ここで周辺攻撃が終わりの場合、 tileToCheck への代入は意味がありませんが、分岐するほどのことでもないと思います。

ややこしい処理になっているのでステップ実行をうまく利用することがお勧めです。

TODO 2

自作のプログラムをさらに進める。このページのあらかじめ与えられるプログラムは複雑なので残しておきたい場合、前のページでプログラム作成するほうがおすすめ。前のページのあらかじめ与えられるプログラムは単純なプログラムなので消しても構わない感じです。

感想

裏のソースコードを見るとき、iPad のスペースでは横幅がなくてプログラムがすぐに折り返してしまうのですが、文字が全部黒色で表示されるので、コメントがどこまでかわかりにくい。コメントだけでも色分けしてほしい。

この海戦ゲームの課題は自分でプログラムを書こうとすると初めからあるやつを消すかコメントアウトしないといけないのが少し不便。書いてみようと言われてもどこに書けばいいんだろうと思ってしまう。プログラム部分が空になっている「やってみよう」のチャプターがほしい。

数字を入力するところでいちいち数値入力の小部品が出て来るのがわずらわしい。

タップ操作に単語選択とカーソル操作のふたつが割り当てられているのがつらい。

グローバル変数を定義したときに謎の実行時エラーが出ることがある。アプリを操作している人からはグローバル変数に見えるが実際は、どこかのクラスのインスタンス変数。原因と対策わからず。

その2に続きます。