[C++] 258 SDL : 暫定順位を表示 Tetris

[M1 Mac, Monterey 12.6.3, clang 13.0.0, SDL 2.26.2, ChatGPT Plus, NO IDE]

テトリスをプレイ中に暫定順位(画面左下のCurrent Rank)を表示するようにしました。

これでベストテン入りにどれくらい近づいているかが分かるようになります。

ゲームアプリでは時間軸というファクターが加わり繰り返し演算し続けるため、これに巻き込まれないように計算しないと変数が知らぬ間に加算されたりします。

C++版の完成度が上がってきたので、そろそろiOS版の製作に着手します。

紫テトロミノの回転が間に合っていたら単独2位だった
int m_score;
int numAllScores;
int rank;
vector<int> scores;
vector<int> allScores;
vector<string> dates;
vector<string> times;

struct Entry {
    int score;
    string date;
	string playTime;
};

void makeRanking(){
	// 暫定順位算出
	allScores.push_back(m_score);
	sort(allScores.begin(), allScores.end(), std::greater<int>());
	numAllScores = allScores.size();

	for (int i = 0; i < allScores.size(); ++i) {
		if (allScores[i] == m_score) {
			rank = i + 1;
			printf("現在順位: %d / %d\n", rank, numAllScores);
			break;
		}
		++rank;
	}

	allScores.pop_back();
}

int TetroTypeGenerator()
{	
	int tetroType;

	if (GeneratorCount == 0){
		arr = {0, 1, 2, 3, 4, 5, 6};
	}

	while(1){
		tetroType = rand() % kNumTetrominoTypes;

		if (arr.size() == 0){
			arr = {0, 1, 2, 3, 4, 5, 6};
			cout << "テトロミノ配列が空になったのでリセットしました" << endl;
		} else if (arr.size() == 1){
			tetroType = arr.front();
			arr = {};

			cout << "tetroType残り1: " << to_string(tetroType) << endl;
			makeRanking();

			GeneratorCount++;
			return tetroType;
		}

		for (auto it = arr.begin(); it != arr.end(); ) {
			if (*it == tetroType) {
				it = arr.erase(it);

				cout << "tetroType: " << to_string(tetroType) << endl;
				makeRanking();

				GeneratorCount++;
				return tetroType;

			} else {
				it++;
			}
		}
	}
}

int GetScoreAndDate() {
	vector<Entry> entries = ReadCSV(csvFile);
	allScores = {};

    // スコアで降順ソート
    sort(entries.begin(), entries.end(),
              [](const Entry& a, const Entry& b) { return a.score > b.score; });

	// 全スコアのvector作成
	for (const auto& entry : entries) {
		allScores.push_back(entry.score);
		// cout << entry.score << " : " << entry.date << " : " << entry.playTime << std::endl;
	}

    // 上位10件を取得して、スコアと日付をvectorに格納
    for (int i = 0; i < std::min(10, static_cast<int>(entries.size())); ++i) {
        scores.push_back(entries[i].score);
        string date = entries[i].date;
		replace(date.begin(), date.end(), '-', '/');
		dates.push_back(date.substr(5, 5));
    }

    return 0;
}

void Game::DrawPlaying( Renderer& renderer )
{
<中略>

snprintf( text, sizeof(text), "Current Rank: %s / %s", to_string(rank).c_str(), to_string(numAllScores).c_str());
renderer.DrawText( text, 20, 530, mFont, 0xffffffff);

<以下略>
}

[C++] 257 SDL : フォントサイズを変えてレンダリング Tetris

[M1 Mac, Monterey 12.6.3, clang 13.0.0, SDL 2.26.2, ChatGPT Plus, NO IDE]

テトリスSDL版のベストスコア6〜10位についてフォントサイズを小さくして表示できるようDrawText関数にフォントの引数を追加しました。

TTF_Font*	mFont;
TTF_Font*	mFont2;

Renderer::Renderer内
int defaultFontSize = 32;
int smallFontSize = 24;
mFont = TTF_OpenFont( "clacon2.ttf", defaultFontSize );
mFont2 = TTF_OpenFont( "clacon2.ttf", smallFontSize );

void Renderer::DrawText( const char* text, int x, int y, TTF_Font* font, uint32_t rgba /*= 0xffffffff */ )
{
	SDL_assert( text );
	SDL_Color color = MakeSDL_Colour( rgba );
	SDL_Surface* pSurface = TTF_RenderUTF8_Solid(font, text, color);
	
	SDL_Texture* pTexture = SDL_CreateTextureFromSurface( m_pSdlRenderer, pSurface );
	int width, height;
	SDL_QueryTexture(pTexture, NULL, NULL, &width, &height);
	SDL_Rect dstRect = { x, y, width, height };
	SDL_RenderCopy( m_pSdlRenderer, pTexture, nullptr, &dstRect );
	SDL_DestroyTexture( pTexture );
	SDL_FreeSurface( pSurface );
}

[C++] 256 SDL : テトリス画面にベストスコア5つを表示 その4 CSVファイル更新

[M1 Mac, Monterey 12.6.3, clang 13.0.0, SDL 2.26.2, ChatGPT Plus, NO IDE]

テトリスのプレイ記録をCSVファイルに保存し、起動時・リスタート時にスコアベスト5を表示するようにしました。XMLでの苦労とは打って変わってのイージーモードでした。

これで最低限の機能を実装しました。ホールド機能などのワールドルールに準拠するかどうかはこれから考えます。

ネット麻雀の待ち牌表示やツモ切り表示のようなヌルい機能はいらない派なので多分実装しないでしょう。

struct Entry {
    int score;
    string date;
    string playtime;
};

vector<Entry> ReadCSV(const string& filename) {
    vector<Entry> entries;

    std::ifstream infile(filename);
    if (!infile) {
        cerr << "Failed to open file: " << filename << endl;
        return entries;
    }

	std::string line;
    std::getline(infile, line); // ヘッダ行を読み飛ばす
    while (std::getline(infile, line)) {
        std::istringstream iss(line);
        std::string score_str, date, playtime;
        std::getline(std::getline(iss, score_str, ','), date, ',');
        std::getline(iss, playtime, ',');
        int score = std::stoi(score_str);
        entries.push_back({score, date, playtime});
    }

    return entries;
}

int GetScoreAndDate() {
	vector<Entry> entries = ReadCSV(csvFile);

    // スコアで降順ソート
    sort(entries.begin(), entries.end(),
              [](const Entry& a, const Entry& b) { return a.score > b.score; });

    // 上位5件を取得して、スコアと日付をvectorに格納
    for (int i = 0; i < std::min(5, static_cast<int>(entries.size())); ++i) {
        scores.push_back(entries[i].score);
        string date = entries[i].date;
		replace(date.begin(), date.end(), '-', '/');
		dates.push_back(date.substr(5, 5));
    }

    return 0;
}

void CopyFile(const std::string& src_file, const std::string& dst_file) {
    std::ifstream src(src_file, std::ios::binary);
    std::ofstream dst(dst_file, std::ios::binary);
    dst << src.rdbuf();
}

void AddDataToCSV()
{
	std::ofstream outfile(csvFile, std::ios_base::app); // ファイルを追記モードで開く
    if (!outfile) {
        cerr << "Failed to open file: " << csvFile << endl;
        return;
    }

    outfile << m_score << "," << startTime << "," << elapsedTime << endl;

	// バックアップを作成する
    if (fs::exists(csvFile)) {
        CopyFile(csvFile, csvFileBackup);
    }
}

[C++] 255 SDL : テトリス画面にベストスコア5つを表示 その3 CSVファイル読み込み ChatGPT

[M1 Mac, Monterey 12.6.3, clang 13.0.0, SDL 2.26.2, ChatGPT Plus, NO IDE]

テトリスのプレイ記録をCSVファイルにまとめ、起動時にこれを読み込んでスコアのベスト5を表示させるようにします。

まずテスト用のCSVファイルについて、これを読み込んで必要なデータをvectorにまとめるコードをChatGPTに書いてもらいました。

内容を検証したところ正常に動作して、最後にvectorの内容を出力してくれるという120点の出来でした。ハマると素晴らしいコードを書いてくれます。

Q:
score,date,playtime
0,2020-03-01 14:20:30,25:00
800,2020-03-03 19:10:45,20:30
500,2020-03-07 08:15:20,15:00
300,2020-03-01 15:22:00,12:30
20,2020-02-20 23:55:09,10:00
1000,2020-02-07 23:55:09,10:00 
C++でこのcsvファイルを読み込み、scoreで降順ソートして上位5件のscoreと月/日をそれぞれvector<int>,vector<string>にして下さい。

以下は、与えられたCSVファイルを読み込み、スコアで降順ソートして上位5件のスコアと日付(月/日)をそれぞれvector<int>とvector<string>に格納するサンプルコードです。

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <algorithm>

struct Entry {
    int score;
    std::string date;
    std::string playtime;
};

// CSVファイルを読み込み、各行をEntry構造体に変換してvectorに格納する
std::vector<Entry> ReadCSV(const std::string& filename) {
    std::vector<Entry> entries;

    std::ifstream infile(filename);
    if (!infile) {
        std::cerr << "Failed to open file: " << filename << std::endl;
        return entries;
    }

    std::string line;
    std::getline(infile, line); // ヘッダ行を読み飛ばす
    while (std::getline(infile, line)) {
        std::istringstream iss(line);
        std::string score_str, date, playtime;
        std::getline(std::getline(iss, score_str, ','), date, ',');
        std::getline(iss, playtime, ',');
        int score = std::stoi(score_str);
        entries.push_back({score, date, playtime});
    }

    return entries;
}

int main() {
    std::vector<Entry> entries = ReadCSV("scores.csv");

    // スコアで降順ソート
    std::sort(entries.begin(), entries.end(),
              [](const Entry& a, const Entry& b) { return a.score > b.score; });

    // 上位5件を取得して、スコアと日付をvectorに格納
    std::vector<int> top_scores;
    std::vector<std::string> dates;
    for (int i = 0; i < std::min(5, static_cast<int>(entries.size())); ++i) {
        top_scores.push_back(entries[i].score);
        std::string month_day = entries[i].date.substr(5, 5);
        dates.push_back(month_day);
    }

    // 結果を出力
    std::cout << "Top 5 scores:" << std::endl;
    for (int i = 0; i < static_cast<int>(top_scores.size()); ++i) {
        std::cout << top_scores[i] << ", " << dates[i] << std::endl;
    }

    return 0;
}

上記のコードを実行すると、出力結果が表示されます。

Top 5 scores:
1000, 02-07
800, 03-03
500, 03-07
300, 03-01
20, 02-20 

[C++] 253 SDL : テトリス画面にベストスコア5つを表示 その1 ChatGPT

[M1 Mac, Monterey 12.6.3, clang 13.0.0, SDL 2.26.2, ChatGPT Plus, NO IDE]

テトリス画面にベストスコア5つを表示するためのxmlファイル作成関数をChatGPTに書かせました。

いともあっさり書いてくれました。検証はしていませんが以下の通りです。おそらく新データは何位であろうと5番目になるはずなので、後で降順ソートが必要でしょう。

ChatGPT Plusは現在月額20ドルですが、今後DAZNのようにハイペースで値上げさせないためにも競合AIの頑張りに期待したいところです。

Q: xmlファイルのentryデータは5個まででscoreが5つの最小値を上回った場合はデータを入れ替える、というコードを書いてください

以下は5つ以下のエントリーがある場合のコードです。

void AddDataToXML(string fileName, string score, string date, string playTime)
{
    // ファイルをパースする
    pugi::xml_document doc;
    pugi::xml_parse_result result = doc.load_file(fileName.c_str());

    if (!result)
    {
        std::cerr << "XMLの読み込みに失敗しました。" << std::endl;
        return;
    }

    // エントリーを格納するノードを取得する
    pugi::xml_node entryNode = doc.child("data").child("entry");

    // 新しいエントリーを作成する
    pugi::xml_node newEntryNode = doc.append_child("entry");
    newEntryNode.append_child("score").text() = score.c_str();
    newEntryNode.append_child("date").text() = date.c_str();
    newEntryNode.append_child("playtime").text() = playTime.c_str();

    // エントリー数をカウントする
    int entryCount = 0;
    for (pugi::xml_node node = entryNode; node; node = node.next_sibling("entry"))
    {
        entryCount++;
    }

    // エントリーが5個未満の場合は新しいエントリーを追加する
    if (entryCount < 5)
    {
        return;
    }

    // スコアが5つの最小値を上回っている場合は新しいエントリーを追加し、古いエントリーを削除する
    pugi::xml_node minScoreNode = entryNode;
    int minScore = std::atoi(entryNode.child_value("score"));
    for (pugi::xml_node node = entryNode.next_sibling("entry"); node; node = node.next_sibling("entry"))
    {
        int scoreVal = std::atoi(node.child_value("score"));
        if (scoreVal < minScore)
        {
            minScoreNode = node;
            minScore = scoreVal;
        }
    }

    if (std::atoi(score.c_str()) > minScore)
    {
        doc.remove_child(minScoreNode);
        doc.append_child(newEntryNode);
    }

    doc.save_file(fileName.c_str());
}

[C++] 252 SDL : テトリス画面に現在時刻と経過時間を表示

[M1 Mac, Monterey 12.6.3, clang 13.0.0, SDL 2.26.2, ChatGPT Plus, NO IDE]

テトリスの画面に現在時刻と経過時間を表示させました。ゲームオーバー時に経過時間をストップさせる仕組みを作るのに一手間掛かりました。メモリを消費する書き方になっていますが、vectorの要素数を常に1個になるようにすれば改善できます。

次はテトロミノのホールド、これから生成するテトロミノの表示あたりでしょうか。このような機能を付けるとかなりヌルくなりますね。

ChatGPTは型推論のautoを多用しますが、使わないよう指示すれば正確な型で書いてくれます。

ここまでプログラミングのハードルが下がるとホビープログラマがかなり増えるのではないでしょうか。あくまで基本を押さえていないと使いこなせないとは思いますが。

extern std::chrono::steady_clock::time_point start;
vector<string> times;

void Game::DrawPlaying( Renderer& renderer )
{
	<中略>

	char text[128];
	snprintf( text, sizeof(text), "Lines: %u", m_numLinesCleared );
	renderer.DrawText( text, 20, 50, 0xffffffff );
	snprintf( text, sizeof(text), "Level: %u", m_level );
	renderer.DrawText( text, 20, 90, 0xffffffff );
	snprintf( text, sizeof(text), "Score: %u", m_score );
	renderer.DrawText( text, 20, 130, 0xffffffff );
	snprintf( text, sizeof(text), "High score: %u", hiScore );
	renderer.DrawText( text, 20, 170, 0xffffffff );

	// 現在日時を取得する
	auto now = std::chrono::system_clock::now();
	auto time_t = std::chrono::system_clock::to_time_t(now);
	std::tm tm = *std::localtime(&time_t);

	// 日時を指定されたフォーマットで文字列に変換する
	std::stringstream ss1;
	ss1 << std::put_time(&tm, "%Y/%m/%d");
	std::string str1 = ss1.str();

	std::stringstream ss2;
	ss2 << std::put_time(&tm, "%H:%M:%S");
	std::string str2 = ss2.str();

	snprintf(text, sizeof(text), "Current time: %s", str1.c_str());
	renderer.DrawText(text, 850, 50, 0xffffffff);

	snprintf(text, sizeof(text), str2.c_str());
	renderer.DrawText(text, 1100, 90, 0xffffffff);

	// 現在時刻を取得する
    std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
	
	// 経過時間を算出する
	std::chrono::milliseconds elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

	// 経過時間を表示する
	if (m_gameState != kGameState_GameOver){
		// 経過時間を"MM:SS"形式で表示する
		snprintf(text, sizeof(text), "Elapsed time: %02d:%02d", elapsed.count() / 1000 / 60, elapsed.count() / 1000 % 60);

		string str(text, text + sizeof(text));
		// cout << "str: " << str << endl;
		times.push_back(str);
		renderer.DrawText(text, 850, 130, 0xffffffff);

	} else {
		string time = times.back();

		// cout << "プレイ時間表示: " << time << endl;
		snprintf(text, sizeof(text), "%s", time.c_str());
		renderer.DrawText(text, 850, 130, 0xffffffff);
	}
}

[C++] 251 SDL : テトリスの最高スコアをxmlファイルに保存

[M1 Mac, Monterey 12.6.3, clang 13.0.0, SDL 2.26.2, ChatGPT Plus, NO IDE]

製作中のテトリスはアプリ起動中の最高スコアを表示していて、閉じるとデータは消えてしまいます。

そこでxmlファイルに最高スコアを保存し、起動の度にxmlファイルを読み込み、ゲームオーバー時に更新するようにしました。同時にバックアップファイルも作成しています。

xmlファイル作成・読込はJavaアプリで扱ったことはありますが、今回はChatGPTを使ってサクッと作りました。Homebrewでインストールしたpugixmlライブラリを使っています。

ある程度プログラミングが分かる方はChatGPT Plusを活用されることをお勧めします。学習・作業効率がこれまでと段違いです。

#include "pugixml.hpp"

extern string xmlFile;
extern string xmlFileBackup;

int readXML(){
	pugi::xml_document doc;

	cout << "readXML関数 xmlFile: " << xmlFile << endl;

    // XMLファイルを読み込む
    pugi::xml_parse_result result = doc.load_file(xmlFile.c_str());

    if (!result) {
        std::cerr << "XMLの読み込みに失敗しました。" << std::endl;
        return -1;
    }

    // ルートノードを取得する
    pugi::xml_node root = doc.child("properties");

    // HISCOREのエントリーを取得する
    pugi::xml_node hiscore_node = root.find_child_by_attribute("entry", "key", "HISCORE");

    // エントリーの値を取得する
    hiScore = hiscore_node.text().as_int();

    std::cout << "HISCOREの値は" << hiScore << "です。" << std::endl;

	return hiScore;
}

int writeXML(){
	pugi::xml_document doc;

	cout << "writeXML関数 xmlFile: " << xmlFile << endl;

    // XMLファイルを読み込む
    pugi::xml_parse_result result = doc.load_file(xmlFile.c_str());

    if (!result) {
        std::cerr << "XMLの読み込みに失敗しました。" << std::endl;
        return -1;
    }

    // ルートノードを取得する
    pugi::xml_node root = doc.child("properties");

    // HISCOREのエントリーを取得する
    pugi::xml_node hiscore_node = root.find_child_by_attribute("entry", "key", "HISCORE");

    // エントリーの値を書き換える
    hiscore_node.text().set(hiScore);

    // XMLファイルを上書きする
    doc.save_file(xmlFile.c_str());

    std::cout << "更新するHISCOREの値は" << hiScore << "です。" << std::endl;

	// ファイルを読み込む
    std::ifstream file(xmlFile);
    if (!file.good()) {
        std::cerr << "ファイルの読み込みに失敗しました。" << std::endl;
        return -1;
    }

    // バックアップファイルを作成する
    std::ofstream backup(xmlFileBackup);
    if (!backup.good()) {
        std::cerr << "バックアップファイルの作成に失敗しました。" << std::endl;
        return -1;
    }

    // バックアップファイルにデータを書き込む
    backup << file.rdbuf();

    // ファイルを閉じる
    file.close();
    backup.close();

    std::cout << "バックアップファイルを作成しました。" << std::endl;
}

[C++] 250 SDL : テトロミノが偏って生成しないようにする Tetris

[M1 Mac, Monterey 12.6.3, clang 13.0.0, SDL 2.26.2, ChatGPT Plus, NO IDE]

テトリスでテトロミノが偏って生成しないようにするため、ワールドルールに従って7種類あるテトロミノ番号を程よく生成する関数を作成しました。

for文の作成にはChatGPTにも手伝ってもらいました。

書いていて気がつきましたが、配列内要素が残り1つになったらその数を自動的に戻り値にすればいいですね。CPUへの負荷も減ります。改良版として追記しておきます。

vector<int> arr;
int GeneratorCount;

int TetroTypeGenerator()
{	
	int tetroType;

	if (GeneratorCount == 0){
		arr = {0, 1, 2, 3, 4, 5, 6};
	}

	while(1){
		tetroType = rand() % kNumTetrominoTypes;

		if (arr.size() == 0){
			arr = {0, 1, 2, 3, 4, 5, 6};
			cout << "テトロミノ配列が空になったのでリセットしました" << endl;
		}

		for (auto it = arr.begin(); it != arr.end(); ) {
			if (*it == tetroType) {
				it = arr.erase(it);

				cout << "tetroType: " << to_string(tetroType) << endl;

				GeneratorCount++;
				return tetroType;

			} else {
				it++;
			}
		}
	}
}
tetroType: 2
tetroType: 4
tetroType: 1
tetroType: 0
tetroType: 3
tetroType: 5
tetroType: 6
テトロミノ配列が空になったのでリセットしました
tetroType: 6
tetroType: 2
tetroType: 1
tetroType: 5
tetroType: 4
tetroType: 0
tetroType: 3
テトロミノ配列が空になったのでリセットしました
tetroType: 5
tetroType: 3
tetroType: 0

以下、改良版

vector<int> arr;
int GeneratorCount;

int TetroTypeGenerator()
{	
	int tetroType;

	if (GeneratorCount == 0){
		arr = {0, 1, 2, 3, 4, 5, 6};
	}

	while(1){
		tetroType = rand() % kNumTetrominoTypes;

		if (arr.size() == 0){
			arr = {0, 1, 2, 3, 4, 5, 6};
			cout << "テトロミノ配列が空になったのでリセットしました" << endl;
		} else if (arr.size() == 1){
			tetroType = arr.front();
			arr = {};

			cout << "tetroType残り1: " << to_string(tetroType) << endl;

			GeneratorCount++;
			return tetroType;
		}

		for (auto it = arr.begin(); it != arr.end(); ) {
			if (*it == tetroType) {
				it = arr.erase(it);

				cout << "tetroType: " << to_string(tetroType) << endl;

				GeneratorCount++;
				return tetroType;

			} else {
				it++;
			}
		}
	}
}
tetroType: 2
tetroType: 4
tetroType: 0
tetroType: 3
tetroType: 1
tetroType: 5
tetroType残り1: 6
テトロミノ配列が空になったのでリセットしました
tetroType: 3
tetroType: 1
tetroType: 2
tetroType: 0
tetroType: 6
tetroType: 5
tetroType残り1: 4
テトロミノ配列が空になったのでリセットしました
tetroType: 1
tetroType: 2
tetroType: 3

[C++] 249 FLTK : Fl_Menu_Itemの書式

[M1 Mac, Monterey 12.6.3, clang 13.0.0, FLTK 3.8.1, ChatGPT Plus, NO IDE]

SFTP通信アプリのメンテナンスです。即時ファイル移動ができないiCloudの代替ツールとして開発しています。PC間のファイル移動が瞬時にできます(Windows機とはSamba接続)。

アプリに新たなPCを登録する際、Fl_Menu_Itemの書き方を間違えたためにプルダウンメニュークリック時に落ちるようになってしまいました。以前からクセのある書式にトラブルの可能性を感じていたのでまとめておきます。

Fl_Menu_ItemオブジェクトはArrayを作成する際、最後にヌルポインタを入れることでArrayの終了が分かるようになっています。これにより要素数を扱わずに済みます。要素数を入れても問題はないですが、最後のヌルポインタもカウントした数値を使用します。

// 要素数を使わない場合
Fl_Menu_Item pc_type[] = {
        {"Mac mini Intel"},
        {"MacBook Air M1"},
        {"MacBook Intel"},
        {"Windows11PC"},
        {"Windows10PC"},
        {"LinuxPC"},
        {0}
};

// 要素数を使う場合
Fl_Menu_Item pc_type[7] = {
        {"Mac mini Intel"},
        {"MacBook Air M1"},
        {"MacBook Intel"},
        {"Windows11PC"},
        {"Windows10PC"},
        {"LinuxPC"},
        {0}
};

// 最後のヌルポインタは省略可能(見た目では数が合わない)
Fl_Menu_Item pc_type[7] = {
        {"Mac mini Intel"},
        {"MacBook Air M1"},
        {"MacBook Intel"},
        {"Windows11PC"},
        {"Windows10PC"},
        {"LinuxPC"}
};