ボードゲーム「カタン」の得点を計算するAndroidアプリを作ってみた。

# カタンとは
([Wikipedia](https://ja.wikipedia.org/wiki/カタンの開拓者たち))
>『カタンの開拓者たち』（カタンのかいたくしゃたち、Die Siedler von Catan）、または『カタン』は、ドイツで生まれたボードゲームである。1995年にコスモス社から販売された。
大航海時代に発見された無人島を複数の入植者たちが開拓していき、もっとも繁栄したプレイヤーが勝利するという内容のボードゲーム。



![DSC_1005.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/566051/0dbf8abd-eaef-5e9e-7738-ec54f75eba23.jpeg)


# なぜアプリを作ろうと思ったか

###複雑な得点計算ルール
カタンはいろいろなルールがある、けっこう複雑なボードゲームです。
複雑な点の１つに、その<b>得点計算ルール</b>があげられます。

- 「都市」を作ると２点
- 「開拓地」を作ると１点
- 連続した５本以上の「街道」を作成したプレイヤーのうち、最も長い距離の連続した街道を作成しているプレイヤーに２点<b>（最長交易路）</b>
- 「騎士カード」の使用枚数が３枚以上のプレイヤーのうち、最も使用枚数が多いプレイヤーに２点<b>（最大騎士力）</b>
- 「最長交易路」「最大騎士力」は、より長い街道を持つ／より多く騎士カードを使用したプレイヤーが現れた時点で、該当するプレイヤーが入れ替わる
- 「発展カード」の一種「勝利ポイントカード」を獲得すると１点

###どこかで起きているかもしれない会話

A「今、誰が点数１番高いっけ？」
B「Cが１番高いよ！」
C「いや、最長交易路忘れてるじゃん。最長交易路の２点足してDが１番だよ」
D「あれ？いつの間にか俺よりBの方が交易路長くなってね？」
A「ほんと？1,2,3,4・・・ほんとだ！じゃあDじゃなくてBに＋2点で、Bは6点だね！」
B「違うよ、私は7点だよ」
A「あれ？ああ、都市なのに開拓地だと思って１点で計算しちゃってた」

### Androidアプリを作ろう

- <b>各プレイヤーの情報を記録し、得点を計算して表示するアプリ</b>があれば、上記のような会話をすることがなく、<b>プレイに集中できる</b>と考えました。
- ちょうどスマホアプリを勉強しようと思っていた時期だったので、まず最初の練習として、このカタンの得点計算アプリの開発を試みました。
- 自分はAndroidユーザーなので、Androidアプリを選択しました。


# 完成したアプリ
[Google Play](https://play.google.com/store/apps/details?id=com.shima218.catanpoint)
[GitHub](https://github.com/shima-218/CatanPoint)
## UI
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/566051/29b77706-a5ec-7074-9edf-18dc2b72c87e.png" width=250>　　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/566051/be4ff3f8-2a12-d40d-293d-b598584f5b53.png" width=250>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/566051/150879ee-5320-6955-23d9-7222e632c278.png" width=600>
## 機能
- プレイヤーの情報を記録する
    - プレイヤー名 
    - 都市、開拓地の数
    - 連続する最長の街道(交易路)の長さ
    - 騎士カードの使用枚数
    - 発展カードのポイント数
- 得点を計算する
    - 都市：２点
    - 開拓地：１点
    - 最長交易路：５つ以上の最も長い交易路を持つプレイヤーに２点
    - 最大騎士力：３回以上の最も多く騎士カードを使用したプレイヤーに２点
    - 発展カード：１点
- ゲームの状況を表示する
    - プレイヤーの情報
    - プレイヤーの得点 
    - 最長交易路、最大騎士力



# 開発過程

## パッケージ構成
 - [こちらのサイト](http://kobakei.hatenadiary.jp/entry/2017/04/10/173226)を参考にさせていただき、パッケージ構成を決めました。
 - 当初、パッケージ名をcom.exampleにしてしまっていたら、Google Playにリリース申請をする際にエラーになってしまったので、[こちらのサイト](https://po-what.com/android-studio-package-rename)を参考にパッケージ名を変更しました。

## 苦労したこと①：同じ部品を複数個入れ子にするレイアウトについて

このアプリのレイアウトは
・メイン画面に、最大４人の「プレイヤー情報」を表示する
・１プレイヤーにつき、都市・開拓地・最長交易路・騎士力・発展の５つの「数情報」を表示する
という<b>入れ子構造</b>になっています。

入れ子にしたい部品毎にxmlを分けて、<b>includeタグ</b>を使い、親となるxmlに子のxmlを書き加えていきました。

#### メイン画面：４人分のプレイヤー情報を子レイアウトに持つ

```activity_main.xml
    <include
        android:id="@+id/player1"
        layout="@layout/player_info"
        (省略)
        app:layout_constraintTop_toTopOf="parent" />

    <include
        android:id="@+id/player2"
        layout="@layout/player_info"
        (省略)
        app:layout_constraintTop_toBottomOf="@id/player1" />

    <include
        android:id="@+id/player3"
        layout="@layout/player_info"
        (省略)
        app:layout_constraintTop_toBottomOf="@id/player2" />

    <include
        android:id="@+id/player4"
        layout="@layout/player_info"
        (省略)
        app:layout_constraintTop_toBottomOf="@id/player3" />


```

#### プレイヤー情報：５つの数情報の領域を子レイアウトに持つ

```player_info.xml
    <include
        android:id="@+id/cities"
        layout="@layout/single_count"
        (省略)
        app:layout_constraintStart_toEndOf="@+id/player_name"
        app:layout_constraintEnd_toStartOf="@+id/settlements"/>

    <include
        android:id="@+id/settlements"
        layout="@layout/single_count"
        (省略)
        app:layout_constraintStart_toEndOf="@+id/cities"
        app:layout_constraintEnd_toStartOf="@+id/roads"/>

    <include
        android:id="@+id/roads"
        layout="@layout/single_count"
        (省略)
        app:layout_constraintStart_toEndOf="@+id/settlements"
        app:layout_constraintEnd_toStartOf="@+id/knights"/>

    <include
        android:id="@+id/knights"
        layout="@layout/single_count"
        (省略)
        app:layout_constraintStart_toEndOf="@+id/roads"
        app:layout_constraintEnd_toStartOf="@+id/develops"/>

    <include
        android:id="@+id/develops"
        layout="@layout/single_count"
        (省略)
        app:layout_constraintStart_toEndOf="@+id/knights"
        app:layout_constraintEnd_toEndOf="parent"/>
```

#### 数情報：タイトル、数、＋ボタン、-ボタン

```single_count.xml
    <TextView
        android:id="@+id/count_title"
        (省略)
        app:layout_constraintWidth_min="45dp"/>

    <TextView
        android:id="@+id/count"
        (省略)
        app:layout_constraintVertical_weight="3"/>

    <Button
        android:id="@+id/up"
        (省略)
        app:layout_constraintDimensionRatio="W,1:1"/>

    <Button
        android:id="@+id/down"
        (省略)
        app:layout_constraintDimensionRatio="W,1:1"/>
```

ここで困った問題が発生しました。
<b>どのプレイヤーのどのボタンを押しても、プレイヤー１の都市の数ばかり更新されてしまいます。</b>
<b>同じ画面に同じ要素を複数個includeしている</b>ため、ボタンのIDが一意にならず特定できないことが原因でした。</b>
最終的に、押されたボタンを特定するときに、<b>ボタンのIDだけではなく、ボタンの親要素のIDも参照するように記述する</b>ことで解決しました。

```sample.kt
//実際の実装ではリストを使ってfor文にしたりもしていますが、イメージはこんな感じ

//例１
if (view.parent.parent = findViewById(R.id.player1)) { 
    //処理 
}

//例２
if (view = findViewById<View>(R.id.player1).findViewById<View>(R.id.cities).findViewById<Button>(R.id.up)){
    //処理
}

```

参考
[一つのlayout xmlを複数includeした時にViewへの参照を取得する方法](https://anton0825.hatenablog.com/entry/2015/03/28/000000)

よく解説サイトには、includeタグを使う際は<b>mergeタグ</b>も同時に使うようにと記載があります。
しかし、mergeタグを使ってしまうと、階層構造ができず、一番親のxmlに全てフラットに書かれているようになる（親要素という概念が無くなる）ため、親要素のIDによって押されたボタンを区別することができなくなってしまうようです。

参考
[Androidの\<merge>の大きな欠点？](http://crapp.hatenablog.com/entry/2013/01/05/043447)

## 苦労した点②：ボタンの色を、xmlではなくkotlin側で指定

カタンには、赤・オレンジ・青・クリーム色の４色のコマがあります。それぞれのプレイヤーに別々の色のコマが渡されるので、赤のコマは誰々、オレンジのコマは誰々というように、コマの色でプレイヤーの区別が付きます。

そこで、それぞれのプレイヤーの領域の色をコマの色に対応させるUIを考えました。

要素の色などは、xml側で指定するのが一般的なようですが、
上で述べたように、同じ部品を別々のプレイヤーで使い回しているので、xml側で色を指定することはできず、kotlin側でプレイヤーに応じた異なる色を、同じ部品に対して指定しなくてはいけません。

```sample.kt
//kotlin側でテキストカラーを指定する
view.setTextColor(red)
//kotlin側で背景色を指定する
view.setBackgroundColor(red)
```

このあたりは簡単に見つかったのですが、
<b>ボタンのデザインをプレイヤー毎に変える方法</b>がなかなか見つからず苦労しました

結論として
<b>①ボタンを定義するxmlファイルをres/drawable/button.xmlとして作成する
②GradientDrawableクラスとしてそのxmlを読み込む
③読み込んだクラスに対して色などを変更する
④view.backgroundに、読み込んだボタンのクラスを指定する</b>
という方法に辿り着きました。

```MainActivity.kt
       //ボタンを定義
        val btn: GradientDrawable =
            ResourcesCompat.getDrawable(
                resources,
                R.drawable.button,
                null
            ) as GradientDrawable
        btn.setColor(pink)
        btn.setStroke(2, red)
        //+ボタン
        val upView =
            findViewById<View>(R.id.player1).findViewById<View>(R.id.cities).findViewById<TextView>(R.id.up)
        upView.setTextColor(red)
        upView.background = btn
        //-ボタン
        val downView =
            findViewById<View>(R.id.player1).findViewById<View>(R.id.cities).findViewById<TextView>(R.id.down)
        downView.setTextColor(red)
        downView.background = btn
```


## 苦労したこと③：いろいろな画面サイズにレイアウトを対応させる

当初、それぞれの部品の大きさをハードコーディングしてしまっていたせいで、レイアウト崩れが発生してしまいました。

[Android Studioの公式](https://developer.android.com/training/multiscreen/screensizes?hl=ja)を見ると、いろいろな画面サイズでレイアウトが崩れないための方法がいろいろ書かれていたのですが・・・

>レイアウト サイズのハード コーディングを避ける
>さまざまな画面サイズに合わせて調整できる柔軟なレイアウトにするには、ほとんどのビュー コンポーネントの幅と高さに、ハードコードされたサイズではなく "wrap_content" と "match_parent" を使用します。

これがよく分かりませんでした。wrap_contentは、コンテンツの大きさが大きければ、それに合わせて無限にビューの大きさが広がってしまい、画面からはみ出してしまいます。
「画面に占めるビューの大きさ（比率）を固定にして、コンテンツの大きさを、ビューの大きさに合わせて変化させる」ことが重要に思いますし、その点でwrap_contentはその逆・・・「コンテンツの大きさに合わせてビューの大きさを変える」という仕様になっており、画面レイアウトが崩れてしまうと感じてしまいます。（間違っていたら教えてください）

結論として、いろいろな画面サイズでレイアウトが崩れないようにするために以下の方法を用いました。
[公式](https://developer.android.com/training/multiscreen/screensizes?hl=ja)では紹介されていませんでしたが、比率の指定にとても助けられました。

- 部品サイズのハードコーディングは極力避ける
    - 今回の開発では、一部（RESET,QUITボタンの大きさやマージン指定）のみハードコーディングを残しました。
- <b>wrap_contentの使用も極力避ける</b>
    - 一部を除きwrap_contentをやめました
- できるだけ<b>width=0dp,height=0dp</b>とし、<b>他の要素との制約</b>や<b>比率</b>を使ってどんな画面サイズでも応用できる表現で、位置と大きさを表現する
    - 他の要素との制約
        - app:layout_constraintStart_toStartOf
        - app:layout_constraintStart_toEndOf
        - app:layout_constraintEnd_toEndOf
        - app:layout_constraintEnd_toStartOf
        - app:layout_constraintTop_toTopOf
        - app:layout_constraintTop_toBottomOf
        - app:layout_constraintBottom_toTopOf
        - app:layout_constraintBottom_toBottomOf
    - 比率
        - app:layout_constraintHorizontal_weight
        - app:layout_constraintVeritical_weight
        - app:layout_constraintWidth_percent
        - app:layout_constraintVertical_bias
        - app:layout_constraintDimensionRatio
- テキストサイズもハードコーディングを極力避け、app:autoSizeTextType="uniform"を使用する 

## どこまで動作確認を行うか？

調べたところ、360dp*640dpの画面サイズが多数を占めていることが分かりました。
[2016年発売Android端末のdp解像度まとめ](http://nein37.hatenablog.com/entry/2017/01/03/200142)
[Android端末の画面サイズと解像度](https://d2flat.com/web_service/blog4_15.html)
[画面幅のシェアの統計情報](https://lab.syncer.jp/Statistic/Screen-Size/#Mobile)

また、<b>幅320-410dp</b>に対応できていれば良いのではないかという記載が多数見つかりました。

幅は320-410dpの範囲で動作確認をすれば良いとして、高さはどの範囲が動作確認の目安なのか？と疑問に思いましたが、それについての記載は見つけられませんでした。

多くのアプリは、高さに関しては縦スクロールを使って表示できれば良いので、高さについては特に制約として考えていないのかもしれません。

ですが、今回開発したアプリは、<b>スクロールさせることなく、全てのプレイヤーの得点を同時に画面に映して、ユーザーが確認できるようにしたかった</b>ので、縦スクロールは使えません。結局、320-410dpにほぼ対応するような、533-845dpの高さ範囲で動作確認を行いました。

# まとめ
### 今回学んだこと
- 基本的な画面の作り方（Constraint Layout）
- ボタン押下による処理の実装
- 画面遷移の実装
- 画面回転の実装
- 戻るボタンの実装
- ダイアログの実装
- 端末サイズに依存しない画面レイアウト作成
- Google Play へのリリース

### 今回やらなかったこと
- サーバーサイドの処理
- DB（[Room](https://developer.android.com/training/data-storage/room)・Realmとか）
- 非同期通信
- カメラやBluetoothや位置情報など「スマホらしい」機能の利用
- Twitterのような、縦スクロールのあるデザイン（RecyclerViewを使うようなデザイン）

### 最後に

ニッチなアプリではありますが、「自分の作ったものをリリースする」という経験を初めてできた点は良かったと考えています。
とても簡単なアプリではありますが、Androidアプリの第一歩を踏み出せたと感じています。