0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

雑貨屋をうろついていたときに、脳トレ系ゲームのデモ機が置いてありました。
触っているうちに「これM5で似たようなもの作れそうだな」って思ったので正月休みの工作として作ってみました。

一方で0ベースで作るのは面倒だったので、GPTを駆使してどこまでできるかチャレンジしてみた。

作りたかったもの

脳トレゲーム機です。

  • 数字が書かれたカードを挿入
  • 小さい数字から順番に押していく
  • タイムアタックを行う

カワダ(Kawada) ナンスピ

実際に作ったもの

デモ動画

3*3マスで作ってみました。なんかそれっぽい。

作成手順

GPTも使いながら最速で実装する方法を言語化してみた。

  1. まず仕様を書き出す。機能ごとに粒度を分けてGPTへ指示・デバックしやすいように。
  2. 仕様メモを元に機能を作っていく
    1. 足りない内容があれば都度仕様メモに追記していく
  3. バグがあればさらに粒度を分解して調査する。
    1. 今回座標の微調整だけは手作業で行った。
  4. 各機能ごとにデバックを行い、バグをつぶしてから次へ

仕様メモ

実際にGPTにインプットしたメモです。
作りながら仕様は都度追記しています。

仕様自体もGPTに相談しながら作ると良いです。

# ゲーム名
数字消しゲーム

# ハードウェア
- M5StackCore2

## 使用ライブラリ
- M5Core2
- LovyanGFX  

# 概要
3*3で数字が表示されます。その数字を小さい順番に押していくゲームです。
時間がスコアになります。


# 仕様
- 3*3の数字が表示されます。
- 数字は1~20までの数字がランダムに表示されます。
- 数字を押すと、その数字が消えます。
- 消す順番は小さい順番に押していく必要があります。
- 全ての数字を消すとクリアです。
- クリアした時間がスコアになります。

# 開発手順

1. 数字の表示
2. タッチした数字を判定する
3. 消す順番が正しいかどうかをチェックする
4. クリア判定とクリア後の処理


## 1. 数字の表示
- 3*3の数字を表示する
- 数字は1~20までの数字をランダムに表示する
- 数字は重複しないようにする
`
## 2. タッチした数字を判定する
- タッチした数字を判定する

## 3. 消す順番が正しいかどうかをチェックする
- 表示されている数字で最小の数字を探す
- 最小の数字をタッチしたら消す
    - 青い枠で囲む
- 最小でない数字をタッチした場合
    - 赤い枠で囲む

## 4. クリア判定とクリア後の処理
- 全ての数字を消したらクリア
- クリアした時にスコアを表示する
- タイムを表示する
- リトライボタンを表示する

コード

#include <M5Core2.h>
#include <vector>
#include <algorithm> // std::shuffle
#include <random>    // std::mt19937, std::random_device

// LovyanGFX 関連
#define LGFX_AUTODETECT
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
#include <LGFX_AUTODETECT.hpp>

static LGFX lcd;                 // LovyanGFX本体
static LGFX_Sprite sprite(&lcd); // スプライトで描画を行う

const int screenWidth = 320;
const int screenHeight = 240;
const int radius = 36;
const int textSize = 4;
const int textHeight = 18; 

const int gridWidth = 3;
const int gridHeight = 3;
const int cellSize = 80;

int numbers[gridWidth][gridHeight];

int gridTotalWidth = cellSize * gridWidth;
int gridTotalHeight = cellSize * gridHeight;

int startX = (screenWidth - gridTotalWidth);
int startY = cellSize / 2;

int lastRemoved = 0;
unsigned long startTime;
unsigned long finalTime = 0;

void displayRandomNumbers()
{
  sprite.fillScreen(TFT_BLACK);

  // 1~20の数字を用意
  std::vector<int> candidates;
  candidates.reserve(20);
  for (int n = 1; n <= 20; n++)
  {
    candidates.push_back(n);
  }

  uint32_t seedVal = esp_random();
  std::mt19937 g(seedVal);
  std::shuffle(candidates.begin(), candidates.end(), g);

  // 先頭9個を 3x3 に配置
  int idx = 0;
  for (int i = 0; i < gridWidth; i++)
  {
    for (int j = 0; j < gridHeight; j++)
    {
      numbers[i][j] = candidates[idx];
      idx++;

      int x = startX + i * cellSize;
      int y = startY + j * cellSize;

      sprite.drawCircle(x, y, radius, TFT_WHITE);

      String numStr = String(numbers[i][j]);

      sprite.setTextSize(textSize);
      sprite.setTextColor(TFT_WHITE);
      sprite.setFont(&fonts::efontJA_10);

      int16_t txtWidth = sprite.textWidth(numStr);
      int16_t txtHeight = sprite.fontHeight();

      int txtX = x - (txtWidth / 2);
      int txtY = y - (txtHeight / 2);

      sprite.setCursor(txtX, txtY);
      sprite.print(numStr);
    }
  }

  sprite.pushSprite(0, 0);
}

// -----------------------------
// 枠線を描画 (赤/青枠)
// -----------------------------
void drawBorder(int color)
{

  sprite.fillRect(0, 0, screenWidth, 10, color);                 // 上端
  sprite.fillRect(0, 0, 10, screenHeight, color);                // 左端
  sprite.fillRect(screenWidth - 10, 0, 10, screenHeight, color); // 右端
  sprite.fillRect(0, screenHeight - 10, screenWidth, 10, color); // 下端

  sprite.pushSprite(0, 0);
}

// -----------------------------
// 最小の数字を調べる
// -----------------------------
int findSmallestNumber()
{
  int smallestNumber = 21;
  for (int i = 0; i < gridWidth; i++)
  {
    for (int j = 0; j < gridHeight; j++)
    {
      if (numbers[i][j] != 0 && numbers[i][j] < smallestNumber)
      {
        smallestNumber = numbers[i][j];
      }
    }
  }
  return smallestNumber;
}

// -----------------------------
// タッチされた数字を消す
// -----------------------------
void removeNumberAtTouch()
{
  int smallestNumber = findSmallestNumber();

  for (int i = 0; i < gridWidth; i++)
  {
    for (int j = 0; j < gridHeight; j++)
    {
      int x = startX + (i * cellSize);
      int y = startY + (j * cellSize);

      TouchPoint_t tp = M5.Touch.getPressPoint();
      if (abs(tp.x - x) <= radius && abs(tp.y - y) <= radius)
      {
        // タッチされた数字
        int touchedNumber = numbers[i][j];
        if (touchedNumber == smallestNumber)
        {
          numbers[i][j] = 0;
          sprite.fillCircle(x, y, radius, TFT_BLACK);

          lastRemoved = touchedNumber;
          drawBorder(TFT_BLUE);
        }
        else
        {
          drawBorder(TFT_RED);
        }
      }
    }
  }
}

// -----------------------------
// ゲームクリア画面を表示 (スプライト)
// -----------------------------
void displayClearTime()
{
  if (finalTime == 0)
  {
    finalTime = millis() - startTime;
  }

  float seconds = finalTime / 1000.0f;

  sprite.fillScreen(TFT_BLACK);

  sprite.setCursor(80, 60);
  sprite.setTextSize(3);
  sprite.setTextColor(TFT_YELLOW);
  sprite.print("Excellent!!");

  sprite.setCursor(80, 100);
  sprite.setTextSize(3);
  sprite.setTextColor(TFT_WHITE);
  sprite.printf("Time: %.1f s", seconds);

  // リトライボタン描画 --------------------------
  int rectX = 80;
  int rectY = 155;
  int rectW = 160;
  int rectH = 40;
  sprite.drawRect(rectX, rectY, rectW, rectH, TFT_GREEN);

  // 中心に「RETRY」文字を描画
  sprite.setTextSize(3);
  sprite.setTextColor(TFT_GREEN);
  String label = "RETRY";
  int16_t labelWidth = sprite.textWidth(label);
  int16_t labelHeight = sprite.fontHeight();
  int labelX = rectX + (rectW - labelWidth) / 2;
  int labelY = rectY + (rectH - labelHeight) / 2;
  sprite.setCursor(labelX, labelY);
  sprite.print(label);

  sprite.pushSprite(0, 0);

  // リトライボタンをタッチしたかチェック -------
  TouchPoint_t tp = M5.Touch.getPressPoint();
  if (tp.x >= rectX && tp.x <= rectX + rectW &&
      tp.y >= rectY && tp.y <= rectY + rectH)
  {
    // ゲームリセット処理(必要十分な処理に絞る)
    finalTime = 0;
    lastRemoved = 0;
    displayRandomNumbers();
    startTime = millis();
  }
}

// -----------------------------
// ゲームの進行状況を監視
// -----------------------------
void checkGameStatus()
{
  bool gameCleared = true;

  for (int i = 0; i < gridWidth; i++)
  {
    for (int j = 0; j < gridHeight; j++)
    {
      if (numbers[i][j] != 0)
      {
        gameCleared = false;
        break;
      }
    }
  }

  if (gameCleared)
  {
    displayClearTime();
  }
}

// -----------------------------
// 初期化
// -----------------------------
void setup()
{
  M5.begin();
  lcd.init();
  randomSeed(micros());

  sprite.setColorDepth(8);
  sprite.createSprite(lcd.width(), lcd.height());

  sprite.fillScreen(TFT_BLACK);
  sprite.pushSprite(0, 0);

  startTime = millis();

  displayRandomNumbers();
}

// -----------------------------
// ループ
// -----------------------------
void loop()
{
  removeNumberAtTouch();
  checkGameStatus();
}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?