Flutterでバトルシップパズルを作成する1

## 概要
Flutterでバトルシップパズルを作成する
GitHubに登録する
FirebaseでWebアプリを公開する

- [Flutterでバトルシップパズルを作成する2]
(https://qiita.com/iharakenji/items/137d02f3d045af3d68ff)

### Flutterについて
英語: https://flutter.dev/docs
日本語: https://flutter.ctrnost.com/

### バトルシップについて
- https://ja.wikipedia.org/wiki/%E3%83%90%E3%83%88%E3%83%AB%E3%82%B7%E3%83%83%E3%83%97

###  実行環境
#### Mac
```sh
$ sw_vers
ProductName:    macOS
ProductVersion: 11.6
BuildVersion:   20G165
```

#### Flutter
```sh
$ flutter --version
Flutter 2.5.0 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 4cc385b4b8 (11 days ago) • 2021-09-07 23:01:49 -0700
Engine • revision f0826da7ef
Tools • Dart 2.14.0
```

## 手順
### Flutterのプロジェクトを作成
```sh
flutter create --org xxx.xxx.xxx battleship
```

### 作成したプロジェクトに移動
```sh
cd battleship
```

## アプリの内容
### メイン処理
```dart:lib/main.dart
import 'package:flutter/material.dart';

void main() => runApp(BattleshipApp());

class BattleshipApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: const Text('BattleshipPuzzle'),
          ),
          body: _BattleshipHome(),
        ),
      );
}
```

### 状態管理(StatefulWidget)
```dart
class _BattleshipHome extends StatefulWidget {
  @override
  _BattleshipHomeState createState() => _BattleshipHomeState();
}
```

### ステート
```dart
class _BattleshipHomeState extends State<_BattleshipHome> {
  // セルのサイズ
  final _size = 10;
  // セルデータの生成
  var _cellTypes = CellTypeGenerator.generate();
  // タップされたか？
  final _tapped = List.generate(10, (_) => List.generate(10, (_) => false));
  // タップ数
  var _tapCount = 0;

  @override
  Widget build(BuildContext context) {
    return _buildContainer();
  }

  Widget _buildContainer() => Center(
        child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
          Padding(
            padding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
            child: Text('TapCount: $_tapCount'),
          ),
          // リセットボタン
          Padding(
            padding: const EdgeInsets.fromLTRB(0, 0, 0, 20),
            child: ElevatedButton(
              onPressed: () => _pushReset(),
              child: const Text('Reset'),
            ),
          ),
          Container(
            width: 440,
            height: 440,
            child: _buildColumn(),
          ),
        ]),
      );

  Widget _buildColumn() {
    final columns = Column(
      children: List.generate(
        _size,
        (index) => Container(child: _buildRow(index)),
      ),
    );

    columns.children.add(Row(
        children: List.generate(_size, (cellIndex) {
      final _cellTypesOfCell =
          List.generate(_size, (rowIndex) => _cellTypes[rowIndex][cellIndex]);
      return _buildShipCount(_cellTypesOfCell);
    })));

    return columns;
  }

  Widget _buildRow(int rowIndex) {
    final rows = Row(
      children: List.generate(_size, (index) => _buildCell(rowIndex, index)),
    );
    rows.children.add(_buildShipCount(_cellTypes[rowIndex]));
    return rows;
  }

  Widget _buildCell(int rowIndex, int columnIndex) {
    final cellType = _cellTypes[rowIndex][columnIndex];
    final alreadyTapped = _tapped[rowIndex][columnIndex];

    final cell = GestureDetector(
      onTap: () => setState(() {
        if (cellType == CellType.Wave) {
        } else if (alreadyTapped) {
        } else {
          _tapCount++;
          _tapped[rowIndex][columnIndex] = true;
        }
      }),
      child: Container(
        width: 40,
        height: 40,
        decoration: BoxDecoration(
          border: Border.all(color: Colors.blue),
        ),
        child: _buildCellCentent(cellType, alreadyTapped),
      ),
    );
    return cell;
  }

  // セルの状態によって表示を変更する
  Widget _buildCellCentent(CellType cellType, bool alreadyTapped) {
    if (alreadyTapped) {
      // タップされている場合
      switch (cellType) {
        case CellType.Wave:
          return Icon(Icons.waves);
        case CellType.None:
          return Icon(Icons.check);
        case CellType.ShipCircle:
          return Container(
            margin: EdgeInsets.all(5),
            decoration: const BoxDecoration(
              shape: BoxShape.circle,
              color: Colors.black,
            ),
          );
        case CellType.ShipSquare:
          return Container(
            margin: EdgeInsets.all(5),
            decoration: const BoxDecoration(
              shape: BoxShape.rectangle,
              color: Colors.black,
            ),
          );
        case CellType.ShipRoundTop:
          return Container(
            margin: EdgeInsets.all(5),
            decoration: const BoxDecoration(
              borderRadius: BorderRadius.only(
                topLeft: Radius.circular(20.0),
                topRight: Radius.circular(20.0),
              ),
              color: Colors.black,
            ),
          );
        case CellType.ShipRoundLeft:
          return Container(
            margin: EdgeInsets.all(5),
            decoration: const BoxDecoration(
              borderRadius: BorderRadius.only(
                topLeft: Radius.circular(20.0),
                bottomLeft: Radius.circular(20.0),
              ),
              color: Colors.black,
            ),
          );
        case CellType.ShipRoundBottom:
          return Container(
            margin: EdgeInsets.all(5),
            decoration: const BoxDecoration(
              borderRadius: BorderRadius.only(
                bottomLeft: Radius.circular(20.0),
                bottomRight: Radius.circular(20.0),
              ),
              color: Colors.black,
            ),
          );
        case CellType.ShipRoundRight:
          return Container(
            margin: EdgeInsets.all(5),
            decoration: const BoxDecoration(
              borderRadius: BorderRadius.only(
                topRight: Radius.circular(20.0),
                bottomRight: Radius.circular(20.0),
              ),
              color: Colors.black,
            ),
          );
        default:
          return Container();
      }
    } else {
      // タップされていない場合
      switch (cellType) {
        case CellType.Wave:
          return Icon(Icons.waves);
        default:
          return Container();
      }
    }
  }

  // 行(列)ごとの戦艦の数
  Widget _buildShipCount(List<CellType> cellTypes) {
    final shipCount = cellTypes.where((cellType) {
      switch (cellType) {
        case CellType.ShipCircle:
        case CellType.ShipSquare:
        case CellType.ShipRoundTop:
        case CellType.ShipRoundLeft:
        case CellType.ShipRoundBottom:
        case CellType.ShipRoundRight:
          return true;
        default:
          return false;
      }
    }).length;

    return Container(
      width: 40,
      height: 40,
      child: Center(
          child: Text(shipCount.toString(), style: const TextStyle(fontSize: 18))),
    );
  }

  // リセットが押された時
  void _pushReset() {
    setState(() {
      _tapCount = 0;
      _cellTypes = CellTypeGenerator.generate();
      for (var i = 0; i < _size; i++) {
        for (var j = 0; j < _size; j++) {
          _tapped[i][j] = false;
        }
      }
    });
  }
}
```

### セルデータの作成
TODO: 自動生成できるようにする

```dart
class CellTypeGenerator  {
  static List<List<CellType>> generate() =>
      [
        [
          CellType.Wave,
          CellType.None,
          CellType.ShipCircle,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.ShipRoundTop,
          CellType.None
        ],
        [
          CellType.ShipRoundTop,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.ShipRoundTop,
          CellType.None,
          CellType.None,
          CellType.ShipSquare,
          CellType.None
        ],
        [
          CellType.ShipRoundBottom,
          CellType.None,
          CellType.ShipRoundLeft,
          CellType.ShipRoundRight,
          CellType.None,
          CellType.ShipSquare,
          CellType.None,
          CellType.None,
          CellType.ShipRoundBottom,
          CellType.None
        ],
        [
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.ShipRoundBottom,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None
        ],
        [
          CellType.ShipCircle,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None
        ],
        [
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.ShipRoundTop,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None
        ],
        [
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.ShipRoundBottom,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None
        ],
        [
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None
        ],
        [
          CellType.ShipCircle,
          CellType.None,
          CellType.Wave,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.None
        ],
        [
          CellType.None,
          CellType.None,
          CellType.ShipCircle,
          CellType.None,
          CellType.None,
          CellType.None,
          CellType.ShipRoundLeft,
          CellType.ShipSquare,
          CellType.ShipSquare,
          CellType.ShipRoundRight
        ],
      ];
}
```

### セルの状態
```dart
enum CellType {
  Wave,
  None,
  ShipCircle,
  ShipSquare,
  ShipRoundTop,
  ShipRoundLeft,
  ShipRoundBottom,
  ShipRoundRight,
}
```

| Wave | None | ShipCircle | ShipSquare |
|:-:|:-:|:-:|:-:|
| ![Wave](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/b7595995-37f1-3a70-9c8f-3ceace76c980.png "Wave") | ![None](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/3b1e7577-13eb-b798-a767-edcb7a504f5b.png "None") | ![ShipCircle](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/eb93d58b-e4dc-276c-42e6-0e39a8fccbb7.png "ShipCircle") | ![ShipSquare](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/6ff11196-93e5-92c2-6541-6f1062952e2c.png "ShipSquare") |

| ShipRoundTop | ShipRoundLeft | ShipRoundBottom | ShipRoundRight |
|:-:|:-:|:-:|:-:|
| ![ShipRoundTop](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/740e0dc6-4d40-7c88-24ab-780f68c3c8ac.png "ShipRoundTop") | ![ShipRoundLeft](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/f6ae2d96-b577-bbac-ae6e-c131e144b10c.png "ShipRoundLeft") | ![ShipRoundBottom](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/a6a2d6de-af75-18f4-66df-dbd7d099ac1c.png "ShipRoundBottom") | ![ShipRoundRight](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/e11c517c-2532-fee6-cc09-ca0be00c07a9.png "ShipRoundRight") |

## アプリの実行
### アプリを起動
```sh
flutter run
```

### 実行結果
![battleship.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/62145c2b-23e0-f65c-a4da-4fe8beccbd7b.gif)

## GitHubに登録
- https://github.com/iharakenji/battleship

## Firebase設定
### Firebaseプロジェクト作成
https://console.firebase.google.com/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/7a5dfb73-80b9-f2c5-69fa-1153f1081467.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/cb4dd675-e979-c33f-63c9-8f90ec1ee80a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109450/cf7fc51c-8815-5b78-d3eb-cd451c57df53.png)

### Firebaseプロジェクトを初期化
```sh
firebase init hosting
```

### GitHubでデプロイする設定
```yml:.github/workflows/firebase-hosting-merge.yml
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools

name: Deploy to Firebase Hosting on merge
'on':
  push:
    branches:
      - main
jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: git clone https://github.com/flutter/flutter.git
      - run: echo "$(pwd)/flutter/bin" >> $GITHUB_PATH
      - run: flutter pub get
      - run: flutter build web
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: '${{ secrets.GITHUB_TOKEN }}'
          firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_BATTLESHIP_74D4F }}'
          channelId: live
          projectId: battleship-74d4f
```

```yml:.github/workflows/firebase-hosting-pull-request.yml
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools

name: Deploy to Firebase Hosting on PR
'on': pull_request
jobs:
  build_and_preview:
    if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: git clone https://github.com/flutter/flutter.git
      - run: echo "$(pwd)/flutter/bin" >> $GITHUB_PATH
      - run: flutter pub get
      - run: flutter build web
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: '${{ secrets.GITHUB_TOKEN }}'
          firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_BATTLESHIP_74D4F }}'
          projectId: battleship-74d4f
```

### Webアプリ公開
- https://battleship-74d4f.web.app/