【Arduino】1602 LCD デジタルインベーダー (電卓インベーダー)

# はじめに
何やらリバイバルされたゲーム電卓が人気なのだそうですね。

 - [CASIO SL-880](https://casio.jp/dentaku/general/game/)

これを [Arduino Uno](https://ht-deko.com/arduino/uno.html) と [LCD + キーパッドシールド](https://ht-deko.com/arduino/shield_lcd_keypad.html)で作ってみようと思いました。

# 作ってみた

ゲーム電卓の実機は持っていないので昔のポケコンの移植版みたいなフィーリングで適当にデッチあげてみました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/0560dafc-5a1b-0e7d-4a22-9e4329ca75b8.png)


## ソースコード
単一の ino です。

```cpp:Digital_Invaders.ino
/*
  Digital_Invaders.ino - 1602 LCD DIGITAL INVADERS                                             
  Created by Hideaki Tominaga, May 3, 2018
  Released into the public domain.
*/
// #define ENABLE_EEPROM // ハイスコアを EEPROM に保存する場合にはアンコメント

#include <LiquidCrystal.h>
#ifdef ENABLE_EEPROM
#include <EEPROM.h>
#endif

LiquidCrystal lcd(8, 9, 4, 5, 6, 7);                 // お使いの LCD に合わせて
const int NUM_KEYS = 5;
int adc_key_val[NUM_KEYS] = {70, 240, 420, 620, 880}; // 個体差があります
const int KEY_AIM  = 4;                               // [SELECT] key
const int KEY_SHOT = 3;                               // [LEFT] key
const int PLAYER = 3;
const int MAX_APPEAR_CNT = 50;
const int UFO_SCORE = 300;
const int MAX_AMMO = 30;
const int PERIOD = 16;

byte indicator[4][8] =
{
  {
    B00000,
    B00000,
    B00000,
    B00000,
    B00000,
    B00000,
    B00000,
  },
  {
    B00000,
    B00000,
    B00000,
    B00000,
    B00000,
    B00000,
    B01110,
  },
  {
    B00000,
    B00000,
    B00000,
    B01110,
    B00000,
    B00000,
    B01110,
  },
  {
    B01110,
    B00000,
    B00000,
    B01110,
    B00000,
    B00000,
    B01110,
  }
};

const int UFO = 10;
char num[11] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'n'};
long HiScore = 0;
int OldKey = -1;
int RawKey = -1;

/* キーの状態を取得 */
int inkey()
{
  int key = -1;
  int v = analogRead(0);
  for (int k = 0; k < NUM_KEYS; k++)
    if (v < adc_key_val[k]) {
      key = k;
      break;
    }
  RawKey = key;
  if (OldKey == RawKey)
    key = -1;
  OldKey = RawKey;
  return key;
}

/* スコアの表示 */
void dispScore(long aScore) {
  String s = "      ";
  s.concat(String(aScore, DEC));
  lcd.setCursor(9, 1);
  lcd.print(s.substring(s.length() - 7));
}

/* タイトル表示 */
void GameTitle() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("DIGITAL INVADERS");
  lcd.setCursor(0, 1);
  lcd.print("HI-SCORE:");
  dispScore(HiScore);
  while (inkey() < 0);
  delay(500);
}

/* ゲーム開始 */
void GameMain() {
  int Key;
  int Idx;
  int Hit = 0;
  int Ammo;
  int AppearCnt = MAX_APPEAR_CNT;
  long Score = 0;
  String Enemies;
  bool hasUFO = false;

  // 表示初期化
  lcd.setCursor(0, 1);
  lcd.print("        :");
  dispScore(Score);

  // 残機の分ループ
  for (int i = PLAYER; i > 0; i--) {
    Enemies = "      ";
    int Cnt = 0;
    Idx = 0;
    Ammo = MAX_AMMO; // 弾薬のリロード
    // 残機表示
    lcd.setCursor(1, 1);
    int Left = (i > 3) ? 3 : i;
    lcd.write(byte(Left));
    do {
      // 敵移動＆表示
      if (Cnt > AppearCnt) {
        // 自陣まで到達されたか？
        if (!Enemies.startsWith(" "))
          break;
        // 敵出現＆移動
        Cnt = 0;
        if ((Score > 0) && ((Score % 10) == 0) && (!hasUFO)) {
          hasUFO = true;
          Enemies.concat(String(num[UFO]));        // UFO
        }
        else
          Enemies.concat(String(num[random(10)])); // INVADER
        Enemies = Enemies.substring(1);
        lcd.setCursor(2, 1);
        lcd.print(Enemies);
      }
      // 自機表示
      lcd.setCursor(0, 1);
      lcd.print(num[Idx]);
      // キー入力
      Key = inkey();
      // 照準キーが押されたら
      if (Key == KEY_AIM) {
        Idx++;
        Idx %= 11;
      }
      // ショットキーが押されたら
      if ((Key == KEY_SHOT) && (Ammo > 0)) {
        int Pos = Enemies.indexOf(num[Idx]);
        if (Pos >= 0) {
          // ヒット
          String Dmy = String(" ");
          Dmy.concat(Enemies.substring(0, Pos));
          Dmy.concat(Enemies.substring(Pos + 1));
          Enemies = Dmy;
          lcd.setCursor(2, 1);
          lcd.print(Enemies);
          if (Idx == UFO)
            Score += UFO_SCORE;
          else {
            hasUFO = false;
            Score += Idx;
          }
          dispScore(Score);
          // レベル上昇
          Hit++;
          Hit %= PERIOD;
          if (Hit == (PERIOD - 1)) {
            AppearCnt -= 2;   // 速度上昇
            Ammo = MAX_AMMO;  // 弾薬のリロード
          }
          if (AppearCnt < 10)
            AppearCnt = MAX_APPEAR_CNT; // 速度のリセット
        }
        Ammo--; // 残弾 -1
      }
      Cnt++;
      delay(50);
    } while (true);

    // 残機表示を点滅
    if (Left > 1) {
      for (int l = 0; l < 4; l++) {
        for (int k = 0; k < 2; k++) {
          lcd.setCursor(1, 1);
          lcd.write(byte(i - k));
          delay(500);
        }
      }
    }
  }
  delay(500);
  // ハイスコアの記録
  if (HiScore < Score) {
    HiScore = Score;
#ifdef ENABLE_EEPROM
    // ハイスコアを EEPROM へ保存
    EEPROM.put(EEPROM.length() - sizeof(HiScore), HiScore);
#endif
  }
}

/* ゲームオーバー */
void GameOver() {
  // GAMEOVER 表示
  lcd.setCursor(0, 1);
  lcd.print("GAMEOVER:");
  delay(500);
  // 10秒程ループ (キー押下でスキップ)
  for (int i = 0; i < 100; i++) {
    if (inkey() >= 0) {
      delay(500);
      return;
    }
    delay(100);
  }
}

/* setup() */
void setup() {
  // ランダムの種を "浮いているアナログピン" から取得
  randomSeed(analogRead(1));
  // 残機表示用キャラクタを生成
  for (int i = 0; i < 4; i++)
    lcd.createChar(i, indicator[i]);
  // LCD 初期化
  lcd.begin(16, 2);
  lcd.noBlink();
  lcd.noCursor();
  lcd.clear();
#ifdef ENABLE_EEPROM
  // ハイスコアを EEPROM から読み込み
  EEPROM.get(EEPROM.length() - sizeof(HiScore), HiScore);
  if ((HiScore < 0) || (HiScore > 10000000))
    HiScore = 0;
#endif
}

/* loop() */
void loop() {
  GameTitle();
  GameMain();
  GameOver();
}
```

## 遊び方

### タイトル画面

タイトル画面です。[LCD + キーパッドシールド](https://ht-deko.com/arduino/shield_lcd_keypad.html) のいずれかのキーを押すとゲーム開始です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/7093bed5-dd88-a4bb-fbda-52b84582b3d2.png)

### ゲーム画面

ゲーム画面です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/180af6c7-3272-194d-cc7d-ae08ba59d4a5.png)

[照準] キーで照準を変更し、[ショット] キーで弾を発射します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/3f56500e-69a7-09cd-b802-dd787a40423b.png)


| キー      | 機能|
|:----------|:------------------|
| SELECT    | 照準 (AIM)|
| LEFT      | ショット (SHOT)|

[LCD + キーパッドシールド](https://ht-deko.com/arduino/shield_lcd_keypad.html) は抵抗分圧でキーを判別する仕組みなので、キーの同時押しはできません。

ショットの装弾数は 30 で、インベーダーを 16 機撃ち落とす度にリロードされます。つまり、ミスショットしまくると弾切れになる事があります。

 - インベーダーを撃つと数字がそのまま得点となります。
 - インベーダー (0) は 0 点です。
 - UFO (n) は 300 点です。
 - UFO (n) はスコアの下一桁が 0 になった時に出現します。
 - スコアの下一桁が 0 の時にインベーダー (0) を撃っても UFO は出現します。
 - ...が、UFO 出現前にインベーダーを撃って得点すると UFO の出現はキャンセルされます。
 - インベーダー (or UFO) を 16 機撃ち落とすと難易度が上がります。
 - 難易度が上がると弾薬がリロードされます (30 発)。
 - 難易度がある程度まで上がるとまたリセットされます。
 - ラウンド制ではありません (面の概念はない)。

オリジナルとは細かいルールが異なります。

### ゲームオーバー画面

最終スコアが表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/5371b67c-3830-c155-81fd-4f3779907ba9.png)

そのまま放っておくか任意のキーを押すとタイトル画面に戻ります。

# おわりに
音が出るようにしたり、イロイロ改造してみてください...ゲームバランスもおかしいと思うので。

**See Also:**

 - [Arduino Uno (ht-deko.com)](https://ht-deko.com/arduino/uno.html) 
 - [LCD + キーパッドシールド (ht-deko.com)](https://ht-deko.com/arduino/shield_lcd_keypad.html)
 - [TLCD5x7 (LCD コンポーネント) (Delphi Forum)](https://ht-deko.com/delphiforum/?vasthtmlaction=viewtopic&t=1243.0)