Skip to main content

第6回 解答履歴を表示しよう

第6回 解答履歴を表示しよう

前回は、判定ボタンを押すとHIT数とBLOW数を求められるようにしました。

現在は、次の入力を行うと前の結果が見えなくなります。

今回は、正常に判定できた結果を最大60件まで保存し、画面右側へ15件ずつ表示します。

第5回では、判定直後のHIT数とBLOW数を左側へ表示しました。

第6回からは、正常な判定結果を右側の履歴へ残し、判定後は入力欄を空にして次の入力へ進みます。
左側へ前回のHIT数とBLOW数を残さない仕様へ変更します。

この回は、第5回終了時点のmain.cppから作業を始めます。


このページの進め方

このページでは、読む部分とソースを書き換える部分を分けています。

説明
    仕組みを確認する

STEP ○ ここでソースを書き換える!
    main.cppを実際に変更する

動作確認
    ビルドして実行する

STEPを上から順番に進めてください。
詳しい仕組みは、各STEPの前後にある説明で確認できます。

ソースを書き換える場所は、コード内のここから追加ここを変更この部分を削除などのコメントで示します。


1.今回の完成状態

今回のプログラムでは、次の操作ができるようになります。

  • 正常に判定できた4桁を履歴へ保存する
  • 入力した4桁、HIT数、BLOW数を1組として表示する
  • 判定後に入力欄が空になり、次の入力へ進める
  • 1ページに15件表示する
  • 4枚のタブで履歴ページを切り替える
  • 最大60件を超えて配列へ書き込まない
  • 入力エラーは履歴へ追加しない

60件に到達したときのゲームオーバー処理は、第8回で追加します。


2.1回分の結果を同じ添え字で管理する

1回の判定では、「入力した4桁」「HIT数」「BLOW数」の3種類の情報ができます。
これらを履歴として残すため、それぞれを保存する配列を用意します。

  1. historyInput:入力した4桁を、履歴数分保存する
  2. historyHit:HIT数を、履歴数分保存する
  3. historyBlow:BLOW数を、履歴数分保存する

今回は最大60回分の履歴を残すため、それぞれ最大60回分を保存できるようにします。
3つの配列では、同じ添え字が同じ回の判定結果を表します。

添え字 historyInput historyHit historyBlow
0 1234 1 2
1 5072 0 3
2 8251 3 0
3 8250 4 0

この表では、同じ行が1回分の判定結果です。
たとえば添え字3には、4回目の入力8250、HIT数4、BLOW数0が保存されています。

たとえば、4回目の結果が8250、4 HIT、0 BLOWなら、次のように保存します。

historyInput[3] = "8250"
historyHit[3] = 4
historyBlow[3] = 0

STEP 1 ここでソースを書き換える!

変更する場所:画面配置用の定数とグローバル変数
確認すること:エラーなくビルドできること

履歴用の定数を追加する

JUDGE_BUTTON_HEIGHTの下へ追加します。

// ここから追加
// 解答履歴の最大件数
const int MAX_HISTORY = 60;

// 1ページに表示する履歴の件数
const int HISTORY_PER_PAGE = 15;

// 履歴ページの数
const int HISTORY_PAGE_COUNT = 4;
// ここまで追加

60件を15件ずつ表示するため、ページ数は4です。

60 ÷ 15 = 4ページ

続けて、履歴欄の配置用定数を追加します。

// ここから追加
// 解答履歴欄の配置
const int HISTORY_START_X = 470;
const int HISTORY_TAB_Y = 60;
const int HISTORY_TAB_WIDTH = 78;
const int HISTORY_TAB_HEIGHT = 36;
const int HISTORY_TABLE_Y = 100;
const int HISTORY_TABLE_WIDTH = 312;
const int HISTORY_HEADER_HEIGHT = 28;
const int HISTORY_ROW_HEIGHT = 26;
// ここまで追加

履歴用の配列と変数を追加する

hasResultの下へ追加します。

// ここから追加
// 入力した4桁の履歴
char historyInput[MAX_HISTORY][INPUT_DIGIT_COUNT + 1];

// HIT数の履歴
int historyHit[MAX_HISTORY];

// BLOW数の履歴
int historyBlow[MAX_HISTORY];

// 現在保存されている履歴の件数
int historyCount;

// 現在表示している履歴ページ
int currentHistoryPage;
// ここまで追加

historyInputは二次元配列です。

60件分
    ×
1件につき5文字

1件につき5文字なのは、4桁の数字に加えて、文字列の最後へ'\0'が必要だからです。


STEP 2 ここでソースを書き換える!

変更する場所:画像ハンドルの宣言とLoadGameImages()
確認すること:エラーなくビルドできること

タブ画像のハンドルを追加する

judgeImageの下へ追加します。

// ここから追加
// 選択中の履歴タブ画像
int activeTabImages[HISTORY_PAGE_COUNT];

// 選択されていない履歴タブ画像
int inactiveTabImages[HISTORY_PAGE_COUNT];
// ここまで追加

配布画像は、次のように使用します。

画像 用途
tab1.pngtab4.png 現在選択されているページ
tab1a.pngtab4a.png 選択されていないページ

LoadGameImagesへ追加する

judgeImageの読み込みとエラー確認の後へ、次の処理を追加します。

// ここから追加
// 履歴ページのタブ画像を読み込む
for (i = 0; i < HISTORY_PAGE_COUNT; i++)
{
    sprintf_s(fileName, sizeof(fileName), "images/tab%d.png", i + 1);

    activeTabImages[i] = LoadGraph(fileName);

    if (activeTabImages[i] == -1)
    {
        return 1;
    }

    sprintf_s(fileName, sizeof(fileName), "images/tab%da.png", i + 1);

    inactiveTabImages[i] = LoadGraph(fileName);

    if (inactiveTabImages[i] == -1)
    {
        return 1;
    }
}
// ここまで追加

配列の添え字は0から始まりますが、画像名は1から始まります。

i = 0 → tab1.png
i = 1 → tab2.png
i = 2 → tab3.png
i = 3 → tab4.png

そのため、ファイル名を作るときはi + 1を使用します。


STEP 3 ここでソースを書き換える!

変更する場所Initialize()内のClearInput()の後
確認すること:エラーなくビルドできること

ClearInput()の直後へ、履歴件数と表示ページの初期化を追加します。

ClearInput();

// ここから追加
// 履歴を空の状態から始める
historyCount = 0;
currentHistoryPage = 0;
// ここまで追加

mouseX = 0;
mouseY = 0;
previousMouseInput = 0;

currentHistoryPageは、配列の添え字と同じく0から始めます。

0 → 画面上の1ページ目
1 → 画面上の2ページ目
2 → 画面上の3ページ目
3 → 画面上の4ページ目

3.履歴へ追加する順番

正常に判定できたら、次の順番で処理します。

判定が終わったら、次の順番で処理します。

  1. HITとBLOWを求める
  2. inputhitblowを履歴へ保存する
  3. historyCountを1増やす
  4. 入力欄を空にする

入力欄を先に空にしてはいけません。

先にinput[0] = '\0'
        ↓
空文字列を履歴へコピーしてしまう

必ず、現在の入力を履歴へコピーしてから、次の入力へ進みます。

疑似言語で表すと、次のようになります。

▲ historyCount < MAX_HISTORY
|・historyInput[historyCount] ← input
|・historyHit[historyCount] ← hit
|・historyBlow[historyCount] ← blow
|・historyCount ← historyCount + 1
▼

STEP 4 ここでソースを書き換える!

変更する場所ClearInput()JudgeInput()の間
確認すること:エラーなくビルドできること

次の関数を追加します。

// ここから関数を追加
// 正常に判定できた1回分を履歴へ追加する
void AddHistory(void)
{
    // 配列の範囲を超える場合は追加しない
    if (historyCount >= MAX_HISTORY)
    {
        return;
    }

    // 入力した4桁を履歴へコピーする
    strcpy_s(historyInput[historyCount], sizeof(historyInput[historyCount]), input);

    // 同じ添え字へHIT数とBLOW数を保存する
    historyHit[historyCount] = hit;

    historyBlow[historyCount] = blow;

    // 履歴件数を1増やす
    historyCount++;

    // 今追加した履歴があるページを表示する
    currentHistoryPage = (historyCount - 1) / HISTORY_PER_PAGE;
}
// ここまで関数を追加

historyCountの2つの役割

historyCountには、次の2つの役割があります。

現在保存されている履歴の件数
        と
次に履歴を保存する添え字

履歴が3件ある場合は、次の状態です。

使用中の添え字:0、1、2
historyCount:3
次に保存する添え字:3

文字列は代入できない

文字配列は、次のように=でまとめて代入できません。

// この書き方はできない
historyInput[historyCount] = input;

そのため、strcpy_s()で文字列をコピーします。

strcpy_s(historyInput[historyCount], sizeof(historyInput[historyCount]), input);

4.追加した履歴があるページを表示する

履歴は1ページに15件表示します。

図のように、履歴配列の添え字0~59を15件ずつ区切り、currentHistoryPage0~3に対応させます。

プログラムでは、履歴件数を1増やした後に次の計算を行います。

currentHistoryPage = (historyCount - 1) / HISTORY_PER_PAGE;

たとえば、16件目を追加した直後は次のようになります。

historyCount = 16

(16 - 1) / 15
    ↓
15 / 15
    ↓
1

画面上の2ページ目は、添え字では1です。


STEP 5 ここでソースを書き換える!

変更する場所inputError == 0の中
確認すること:判定後に入力欄が空になる

JudgeInput()の中で、CountBlow()hasResult = true;の直後を、下のコメント付きコードへ変更します。

// ここから変更
// 履歴が満杯でなければ、
// 判定結果を履歴へ保存する
if (historyCount < MAX_HISTORY)
{
    AddHistory();

    // 次の入力を始められるように、
    // 入力欄と表示状態を空にする
    inputCount = 0;
    input[0] = '\0';
    inputError = 0;
    hasResult = false;
}
// ここまで変更

正常な4桁だけがinputError == 0の中へ入るため、入力エラーは履歴へ追加されません。

確認

この時点では履歴をまだ描画していません。

正常な4桁を入力して判定ボタンを押し、入力欄が空になれば成功です。

4桁未満や重複した数字を判定した場合は、これまでどおりエラーを表示します。


STEP 6 ここでソースを書き換える!

変更する場所Update()内の判定ボタン処理の後
確認すること:エラーなくビルドできること

leftClickedの中へ、次の処理を追加します。

// ここから追加
// 履歴ページのタブを順番に調べる
for (i = 0; i < HISTORY_PAGE_COUNT; i++)
{
    int tabX;

    tabX = HISTORY_START_X + i * HISTORY_TAB_WIDTH;

    if (IsPointInRect(mouseX, mouseY, tabX, HISTORY_TAB_Y, tabX + HISTORY_TAB_WIDTH, HISTORY_TAB_Y + HISTORY_TAB_HEIGHT))
    {
        currentHistoryPage = i;
        break;
    }
}
// ここまで追加

4枚のタブを横一列に並べるため、タブのX座標は次の式で求めます。

HISTORY_START_X
    +
タブ番号 × HISTORY_TAB_WIDTH

数字ボタンと同じように、描画とクリック判定で同じ座標計算を使用します。


5.表示する履歴の範囲を求める

現在のページによって、表示する配列の範囲が変わります。

startIndex = currentHistoryPage * HISTORY_PER_PAGE;

endIndex = startIndex + HISTORY_PER_PAGE;

各ページの範囲は次のようになります。

currentHistoryPage startIndex 表示する添え字
0 0 0~14
1 15 15~29
2 30 30~44
3 45 45~59

まだ保存されていない配列要素は表示しません。

if (endIndex > historyCount)
{
    endIndex = historyCount;
}

たとえば履歴が18件で2ページ目を表示する場合は、添え字15~17だけを描画します。


STEP 7 ここでソースを書き換える!

変更する場所Draw()の前と、Draw()の右側デバッグ表示
確認すること:解答履歴が完成する

DrawHistory関数を追加する

タブ画像は、クリック判定と同じ78×36へ調整してあります。
そのため、ここでもDrawGraph()でそのままの大きさを表示します。

Draw()の前へ、次の関数を追加します。

// ここから関数を追加
// 解答履歴とページタブを描画する
void DrawHistory(void)
{
    unsigned int white = GetColor(255, 255, 255);

    unsigned int gray = GetColor(150, 160, 175);

    unsigned int lineColor = GetColor(80, 95, 115);

    unsigned int backgroundColor = GetColor(25, 30, 40);

    int tableBottom;
    int startIndex;
    int endIndex;
    int i;

    DrawString(HISTORY_START_X, 30, "解答履歴", white);

    // 4ページ分のタブを描画する
    for (i = 0; i < HISTORY_PAGE_COUNT; i++)
    {
        int tabX;
        int imageHandle;

        tabX = HISTORY_START_X + i * HISTORY_TAB_WIDTH;

        imageHandle = inactiveTabImages[i];

        if (i == currentHistoryPage)
        {
            imageHandle = activeTabImages[i];
        }

        DrawGraph(tabX, HISTORY_TAB_Y, imageHandle, TRUE);
    }

    tableBottom = HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT + HISTORY_PER_PAGE * HISTORY_ROW_HEIGHT;

    // 履歴表の背景と外枠を描画する
    DrawBox(HISTORY_START_X, HISTORY_TABLE_Y, HISTORY_START_X + HISTORY_TABLE_WIDTH, tableBottom, backgroundColor, TRUE);

    DrawBox(HISTORY_START_X, HISTORY_TABLE_Y, HISTORY_START_X + HISTORY_TABLE_WIDTH, tableBottom, lineColor, FALSE);

    // 表の見出しを描画する
    DrawString(HISTORY_START_X + 7, HISTORY_TABLE_Y + 5, "No", gray);

    DrawString(HISTORY_START_X + 50, HISTORY_TABLE_Y + 5, "INPUT", gray);

    DrawString(HISTORY_START_X + 151, HISTORY_TABLE_Y + 5, "HIT", gray);

    DrawString(HISTORY_START_X + 221, HISTORY_TABLE_Y + 5, "BLOW", gray);

    // 縦線を描画する
    DrawLine(HISTORY_START_X + 40, HISTORY_TABLE_Y, HISTORY_START_X + 40, tableBottom, lineColor);

    DrawLine(HISTORY_START_X + 140, HISTORY_TABLE_Y, HISTORY_START_X + 140, tableBottom, lineColor);

    DrawLine(HISTORY_START_X + 210, HISTORY_TABLE_Y, HISTORY_START_X + 210, tableBottom, lineColor);

    // 見出しと履歴行を分ける線を描画する
    DrawLine(
        HISTORY_START_X,
        HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT,
        HISTORY_START_X + HISTORY_TABLE_WIDTH,
        HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT,
        lineColor);

    // 15行分の横線を描画する
    for (i = 1; i <= HISTORY_PER_PAGE; i++)
    {
        int lineY;

        lineY = HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT + i * HISTORY_ROW_HEIGHT;

        DrawLine(HISTORY_START_X, lineY, HISTORY_START_X + HISTORY_TABLE_WIDTH, lineY, lineColor);
    }

    // 現在のページで表示する履歴の範囲を求める
    startIndex = currentHistoryPage * HISTORY_PER_PAGE;

    endIndex = startIndex + HISTORY_PER_PAGE;

    if (endIndex > historyCount)
    {
        endIndex = historyCount;
    }

    // 現在のページにある履歴を表示する
    for (i = startIndex; i < endIndex; i++)
    {
        int row;
        int y;

        row = i - startIndex;

        y = HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT + 4 + row * HISTORY_ROW_HEIGHT;

        DrawFormatString(HISTORY_START_X + 7, y, white, "%02d", i + 1);

        DrawString(HISTORY_START_X + 50, y, historyInput[i], white);

        DrawFormatString(HISTORY_START_X + 165, y, white, "%d", historyHit[i]);

        DrawFormatString(HISTORY_START_X + 242, y, white, "%d", historyBlow[i]);
    }
}
// ここまで関数を追加

右側の仮表示を解答履歴へ置き換える

第4回までのDraw()の最後には、確認用として次の3つを表示しています。

  • マウス座標
  • 「右側は第6回で」
  • 「解答履歴に使います。」

この3つの表示をすべて削除します。

// この3つの表示を削除
DrawFormatString(500, 35, gray, "マウス:(%d, %d)", mouseX, mouseY);
DrawString(480, 85, "右側は第6回で", gray);
DrawString(480, 115, "解答履歴に使います。", gray);

削除した場所へ、解答履歴を描画する処理を追加します。

// ここに追加
// 右側へ解答履歴を表示する
DrawHistory();

これで、右側の仮表示が実際の解答履歴へ置き換わります。


6.ここまで正しく動くか確認しよう

Ctrl + F5で実行し、次の順番で確認します。

確認1 正常な入力

  1. 重複しない4桁を入力する
  2. 判定ボタンを押す
  3. 右側の履歴へ1件追加されることを確認する
  4. 入力欄が空になることを確認する

確認2 同じ数字

  1. 1123と入力する
  2. 判定ボタンを押す
  3. 重複エラーが表示されることを確認する
  4. 履歴件数が増えないことを確認する

確認3 履歴の組み合わせ

履歴の1行について、次の3つが同じ判定結果になっているか確認します。

入力した4桁
HIT数
BLOW数

確認4 タブの切り替え

  1. 1~4のタブをクリックする
  2. クリックしたタブの色が変わることを確認する
  3. 履歴のないページでは空の表が表示されることを確認する

確認5 16件目

16件目を追加すると、自動的に2ページ目へ切り替わることを確認します。

何度も手入力するのが大変な場合、この確認は第8回の最終テストで行っても構いません。


7.うまく動かない場合

判定すると入力欄は消えるが、履歴が増えない

次を確認します。

  • AddHistory()JudgeInput()から呼び出しているか
  • historyCount++を書いたか
  • Draw()の最後でDrawHistory()を呼び出しているか

履歴の入力が空欄になる

input[0] = '\0'を、AddHistory()より前に実行していないか確認します。

正しい順番

AddHistory()
    ↓
input[0] = '\0'

HIT数とBLOW数が別の入力と組み合わさる

3つの履歴配列へ、同じhistoryCountを添え字として使用しているか確認します。

historyInput[historyCount]
historyHit[historyCount]
historyBlow[historyCount]

16件目が1ページ目へ表示される

ページ計算が次の式になっているか確認します。

currentHistoryPage = (historyCount - 1) / HISTORY_PER_PAGE;

タブの画像とクリック範囲がずれる

タブの描画とクリック判定の両方で、次の計算を使用しているか確認します。

tabX = HISTORY_START_X + i * HISTORY_TAB_WIDTH;

8.プログラム全体

ここまでの内容をすべて入れたmain.cppは、次のようになります。

#include "DxLib.h"

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

// 画面の大きさ
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 600;

// ゲーム内部を1秒間に更新する回数
const int UPDATE_FPS = 60;

// Update1回分の時間
const double FIXED_DELTA_TIME = 1.0 / UPDATE_FPS;

// 長時間処理が止まった場合に、
// 一度に処理する経過時間の上限
const double MAX_FRAME_TIME = 0.25;

// HIT&BLOWで使用する数字の数
const int NUMBER_COUNT = 10;

// 入力する桁数
const int INPUT_DIGIT_COUNT = 4;

// 入力欄の配置
const int INPUT_START_X = 50;
const int INPUT_START_Y = 110;
const int INPUT_IMAGE_SIZE = 80;
const int INPUT_IMAGE_GAP = 10;

// 数字ボタンの配置
const int NUMBER_BUTTON_START_X = 30;
const int NUMBER_BUTTON_START_Y = 335;
const int NUMBER_BUTTON_SIZE = 70;
const int NUMBER_BUTTON_GAP = 8;
const int NUMBER_BUTTON_COLUMNS = 5;

// 判定ボタンの配置
const int JUDGE_BUTTON_X = 120;
const int JUDGE_BUTTON_Y = 510;
const int JUDGE_BUTTON_WIDTH = 200;
const int JUDGE_BUTTON_HEIGHT = 46;


// ===== 【第6回で追加・変更】履歴件数・ページ表示用の定数を追加 ここから =====
// 解答履歴の最大件数
const int MAX_HISTORY = 60;

// 1ページに表示する履歴の件数
const int HISTORY_PER_PAGE = 15;

// 履歴ページの数
const int HISTORY_PAGE_COUNT = 4;

// 解答履歴欄の配置
const int HISTORY_START_X = 470;
const int HISTORY_TAB_Y = 60;
const int HISTORY_TAB_WIDTH = 78;
const int HISTORY_TAB_HEIGHT = 36;
const int HISTORY_TABLE_Y = 100;
const int HISTORY_TABLE_WIDTH = 312;
const int HISTORY_HEADER_HEIGHT = 28;
const int HISTORY_ROW_HEIGHT = 26;

// ===== 【第6回で追加・変更】履歴件数・ページ表示用の定数を追加 ここまで =====
// ゲームの正解
char answer[5];

// プレイヤーの入力
char input[5];

// 現在入力されている桁数
int inputCount;

// 入力内容のエラー番号
int inputError;

// 判定結果
int hit;
int blow;

// 判定結果を表示できる状態か
bool hasResult;

// ===== 【第6回で追加・変更】入力・HIT・BLOWの履歴を保存する配列を追加 ここから =====
// 入力した4桁の履歴
char historyInput[MAX_HISTORY][INPUT_DIGIT_COUNT + 1];

// HIT数の履歴
int historyHit[MAX_HISTORY];

// BLOW数の履歴
int historyBlow[MAX_HISTORY];

// 現在保存されている履歴の件数
int historyCount;

// 現在表示している履歴ページ
int currentHistoryPage;

// ===== 【第6回で追加・変更】入力・HIT・BLOWの履歴を保存する配列を追加 ここまで =====
// マウスカーソルの座標
int mouseX;
int mouseY;

// 1回前のUpdateで取得したマウス状態
int previousMouseInput;

// 0~9の数字ボタン画像
int numberImages[NUMBER_COUNT];

// 入力欄へ表示する0~9の画像
int displayNumberImages[NUMBER_COUNT];

// 未入力の桁に表示する画像
int blankImage;

// 判定ボタン画像
int judgeImage;
// ===== 【第6回で追加・変更】履歴ページ用タブ画像のハンドルを追加 ここから =====

// 選択中の履歴タブ画像
int activeTabImages[HISTORY_PAGE_COUNT];

// 選択されていない履歴タブ画像
int inactiveTabImages[HISTORY_PAGE_COUNT];
// ===== 【第6回で追加・変更】履歴ページ用タブ画像のハンドルを追加 ここまで =====

// 画像の読み込み結果
int imageLoadError;


// 重複しない4桁の問題を作る
void MakeProblem(char outputAnswer[5])
{
    char temp[] = "0123456789";
    int i;

    for (i = 0; i < 100; i++)
    {
        int index1 = rand() % 10;
        int index2 = rand() % 10;
        char work;

        work = temp[index1];
        temp[index1] = temp[index2];
        temp[index2] = work;
    }

    for (i = 0; i < 4; i++)
    {
        outputAnswer[i] = temp[i];
    }

    outputAnswer[4] = '\0';
}

// 入力内容に問題がないか確認する
int CheckAnswerError(char inputData[])
{
    int i;
    int j;

    // 4桁か確認する
    if (strlen(inputData) != 4)
    {
        return 1;
    }

    // すべて数字か確認する
    for (i = 0; i < 4; i++)
    {
        if (!isdigit((unsigned char)inputData[i]))
        {
            return 2;
        }
    }

    // 同じ数字が含まれていないか確認する
    for (i = 0; i < 4; i++)
    {
        for (j = i + 1; j < 4; j++)
        {
            if (inputData[i] == inputData[j])
            {
                return 3;
            }
        }
    }

    return 0;
}

// HIT数を数える
int CountHit(char correctAnswer[], char inputData[])
{
    int count = 0;
    int i;

    for (i = 0; i < 4; i++)
    {
        if (correctAnswer[i] == inputData[i])
        {
            count++;
        }
    }

    return count;
}

// BLOW数を数える
int CountBlow(char correctAnswer[], char inputData[])
{
    int count = 0;
    int i;
    int j;

    for (i = 0; i < 4; i++)
    {
        for (j = 0; j < 4; j++)
        {
            if (inputData[i] == correctAnswer[j] && i != j)
            {
                count++;
            }
        }
    }

    return count;
}

// ゲームで使用する画像を読み込む
int LoadGameImages(void)
{
    char fileName[64];
    int i;

    // 0~9の画像を読み込む
    for (i = 0; i < NUMBER_COUNT; i++)
    {
        // 青い数字ボタンのファイル名を作る
        sprintf_s(fileName, sizeof(fileName), "images/%d.png", i);

        numberImages[i] = LoadGraph(fileName);

        if (numberImages[i] == -1)
        {
            return 1;
        }

        // 緑色の数字画像のファイル名を作る
        sprintf_s(fileName, sizeof(fileName), "images/%d%d.png", i, i);

        displayNumberImages[i] = LoadGraph(fileName);

        if (displayNumberImages[i] == -1)
        {
            return 1;
        }
    }

    // 未入力の桁に表示する画像を読み込む
    blankImage = LoadGraph("images/kome.png");

    if (blankImage == -1)
    {
        return 1;
    }

    // 判定ボタン画像を読み込む
    judgeImage = LoadGraph("images/judge.png");

    if (judgeImage == -1)
    {
        return 1;
    }

    // ===== 【第6回で追加・変更】履歴ページ用タブ画像を読み込む ここから =====
    // 履歴ページのタブ画像を読み込む
    for (i = 0; i < HISTORY_PAGE_COUNT; i++)
    {
        sprintf_s(fileName, sizeof(fileName), "images/tab%d.png", i + 1);

        activeTabImages[i] = LoadGraph(fileName);

        if (activeTabImages[i] == -1)
        {
            return 1;
        }

        sprintf_s(fileName, sizeof(fileName), "images/tab%da.png", i + 1);

        inactiveTabImages[i] = LoadGraph(fileName);

        if (inactiveTabImages[i] == -1)
        {
            return 1;
        }
    }

    // ===== 【第6回で追加・変更】履歴ページ用タブ画像を読み込む ここまで =====
    return 0;
}

// 点が長方形の中にあるか調べる
bool IsPointInRect(int pointX, int pointY, int left, int top, int right, int bottom)
{
    return left <= pointX && pointX < right && top <= pointY && pointY < bottom;
}

// 入力中の4桁をすべて消す
void ClearInput(void)
{
    inputCount = 0;
    input[0] = '\0';

    inputError = 0;
    hit = 0;
    blow = 0;
    hasResult = false;
}

// ===== 【第6回で追加・変更】1回分の判定結果を履歴へ保存する処理を追加 ここから =====
// 正常に判定できた1回分を履歴へ追加する
void AddHistory(void)
{
    // 配列の範囲を超える場合は追加しない
    if (historyCount >= MAX_HISTORY)
    {
        return;
    }

    // 入力した4桁を履歴へコピーする
    strcpy_s(historyInput[historyCount], sizeof(historyInput[historyCount]), input);

    // 同じ添え字へHIT数とBLOW数を保存する
    historyHit[historyCount] = hit;

    historyBlow[historyCount] = blow;

    // 履歴件数を1増やす
    historyCount++;

    // 今追加した履歴があるページを表示する
    currentHistoryPage = (historyCount - 1) / HISTORY_PER_PAGE;
}

// ===== 【第6回で追加・変更】1回分の判定結果を履歴へ保存する処理を追加 ここまで =====
// 入力内容を確認し、HIT数とBLOW数を求める
void JudgeInput(void)
{
    // 入力内容のエラー番号を受け取る
    inputError = CheckAnswerError(input);

    // 以前の判定結果をいったん消す
    hit = 0;
    blow = 0;
    hasResult = false;

    // 入力内容に問題がない場合だけ判定する
    if (inputError == 0)
    {
        hit = CountHit(answer, input);

        blow = CountBlow(answer, input);

        hasResult = true;
// ===== 【第6回で追加・変更】正常な判定結果を履歴へ追加 ここから =====

        // 履歴が満杯でなければ、
        // 判定結果を履歴へ保存する
        if (historyCount < MAX_HISTORY)
        {
            AddHistory();

            // 次の入力を始められるように、
            // 入力欄と表示状態を空にする
            inputCount = 0;
            input[0] = '\0';
            inputError = 0;
            hasResult = false;
        }
// ===== 【第6回で追加・変更】正常な判定結果を履歴へ追加 ここまで =====
    }
}

// ゲーム開始時に1回だけ実行する
void Initialize(void)
{
    // 実行するたびに乱数の並びを変える
    srand((unsigned int)time(NULL));

    // 重複しない4桁の問題を作る
    MakeProblem(answer);

    ClearInput();
// ===== 【第6回で追加・変更】履歴を空の状態から開始 ここから =====

    // 履歴を空の状態から始める
    historyCount = 0;
    currentHistoryPage = 0;
// ===== 【第6回で追加・変更】履歴を空の状態から開始 ここまで =====

    mouseX = 0;
    mouseY = 0;
    previousMouseInput = 0;

    SetMouseDispFlag(TRUE);

    // ゲームで使用する画像を読み込む
    imageLoadError = LoadGameImages();

}

// 入力確認とゲームの更新を行う
void Update(void)
{
    int mouseInput;
    bool leftClicked;
    bool rightClicked;

    // 現在のマウス座標を取得する
    GetMousePoint(&mouseX, &mouseY);

    // 現在のマウスボタン状態を取得する
    mouseInput = GetMouseInput();

    // 左クリックした瞬間を求める
    leftClicked = (mouseInput & MOUSE_INPUT_LEFT) != 0 && (previousMouseInput & MOUSE_INPUT_LEFT) == 0;

    // 右クリックした瞬間を求める
    rightClicked = (mouseInput & MOUSE_INPUT_RIGHT) != 0 && (previousMouseInput & MOUSE_INPUT_RIGHT) == 0;

    if (leftClicked)
    {
        int i;

        // 0~9の数字ボタンを順番に調べる
        for (i = 0; i < NUMBER_COUNT; i++)
        {
            int column;
            int row;
            int x;
            int y;

            column = i % NUMBER_BUTTON_COLUMNS;

            row = i / NUMBER_BUTTON_COLUMNS;

            x = NUMBER_BUTTON_START_X + column * (NUMBER_BUTTON_SIZE + NUMBER_BUTTON_GAP);

            y = NUMBER_BUTTON_START_Y + row * (NUMBER_BUTTON_SIZE + NUMBER_BUTTON_GAP);

            if (IsPointInRect(mouseX, mouseY, x, y, x + NUMBER_BUTTON_SIZE, y + NUMBER_BUTTON_SIZE))
            {
                // 4桁未満なら数字を追加する
                if (inputCount < INPUT_DIGIT_COUNT)
                {
                    input[inputCount] = (char)('0' + i);

                    inputCount++;

                    input[inputCount] = '\0';

                    // 入力を変更したので、
                    // 前回のエラーや判定結果を消す
                    inputError = 0;
                    hit = 0;
                    blow = 0;
                    hasResult = false;
                }

                break;
            }
        }

        // 判定ボタンをクリックしたか
        if (IsPointInRect(
            mouseX,
            mouseY,
            JUDGE_BUTTON_X,
            JUDGE_BUTTON_Y,
            JUDGE_BUTTON_X + JUDGE_BUTTON_WIDTH,
            JUDGE_BUTTON_Y + JUDGE_BUTTON_HEIGHT))
        {
            JudgeInput();
        }

// ===== 【第6回で追加・変更】履歴ページのタブをクリックして切り替える ここから =====

        // 履歴ページのタブを順番に調べる
        for (i = 0; i < HISTORY_PAGE_COUNT; i++)
        {
            int tabX;

            tabX = HISTORY_START_X + i * HISTORY_TAB_WIDTH;

            if (IsPointInRect(mouseX, mouseY, tabX, HISTORY_TAB_Y, tabX + HISTORY_TAB_WIDTH, HISTORY_TAB_Y + HISTORY_TAB_HEIGHT))
            {
                currentHistoryPage = i;
                break;
            }
        }
// ===== 【第6回で追加・変更】履歴ページのタブをクリックして切り替える ここまで =====
    }

    // 右クリックしたら入力中の数字をすべて消す
    if (rightClicked)
    {
        ClearInput();
    }

    // 今回の状態を次回の比較用に保存する
    previousMouseInput = mouseInput;
}

// ===== 【第6回で追加・変更】解答履歴とページタブを描画 ここから =====

// 解答履歴とページタブを描画する
void DrawHistory(void)
{
    unsigned int white = GetColor(255, 255, 255);

    unsigned int gray = GetColor(150, 160, 175);

    unsigned int lineColor = GetColor(80, 95, 115);

    unsigned int backgroundColor = GetColor(25, 30, 40);

    int tableBottom;
    int startIndex;
    int endIndex;
    int i;

    DrawString(HISTORY_START_X, 30, "解答履歴", white);

    // 4ページ分のタブを描画する
    for (i = 0; i < HISTORY_PAGE_COUNT; i++)
    {
        int tabX;
        int imageHandle;

        tabX = HISTORY_START_X + i * HISTORY_TAB_WIDTH;

        imageHandle = inactiveTabImages[i];

        if (i == currentHistoryPage)
        {
            imageHandle = activeTabImages[i];
        }

        DrawGraph(tabX, HISTORY_TAB_Y, imageHandle, TRUE);
    }

    tableBottom = HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT + HISTORY_PER_PAGE * HISTORY_ROW_HEIGHT;

    // 履歴表の背景と外枠を描画する
    DrawBox(HISTORY_START_X, HISTORY_TABLE_Y, HISTORY_START_X + HISTORY_TABLE_WIDTH, tableBottom, backgroundColor, TRUE);

    DrawBox(HISTORY_START_X, HISTORY_TABLE_Y, HISTORY_START_X + HISTORY_TABLE_WIDTH, tableBottom, lineColor, FALSE);

    // 表の見出しを描画する
    DrawString(HISTORY_START_X + 7, HISTORY_TABLE_Y + 5, "No", gray);

    DrawString(HISTORY_START_X + 50, HISTORY_TABLE_Y + 5, "INPUT", gray);

    DrawString(HISTORY_START_X + 151, HISTORY_TABLE_Y + 5, "HIT", gray);

    DrawString(HISTORY_START_X + 221, HISTORY_TABLE_Y + 5, "BLOW", gray);

    // 縦線を描画する
    DrawLine(HISTORY_START_X + 40, HISTORY_TABLE_Y, HISTORY_START_X + 40, tableBottom, lineColor);

    DrawLine(HISTORY_START_X + 140, HISTORY_TABLE_Y, HISTORY_START_X + 140, tableBottom, lineColor);

    DrawLine(HISTORY_START_X + 210, HISTORY_TABLE_Y, HISTORY_START_X + 210, tableBottom, lineColor);

    // 見出しと履歴行を分ける線を描画する
    DrawLine(
        HISTORY_START_X,
        HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT,
        HISTORY_START_X + HISTORY_TABLE_WIDTH,
        HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT,
        lineColor);

    // 15行分の横線を描画する
    for (i = 1; i <= HISTORY_PER_PAGE; i++)
    {
        int lineY;

        lineY = HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT + i * HISTORY_ROW_HEIGHT;

        DrawLine(HISTORY_START_X, lineY, HISTORY_START_X + HISTORY_TABLE_WIDTH, lineY, lineColor);
    }

    // 現在のページで表示する履歴の範囲を求める
    startIndex = currentHistoryPage * HISTORY_PER_PAGE;

    endIndex = startIndex + HISTORY_PER_PAGE;

    if (endIndex > historyCount)
    {
        endIndex = historyCount;
    }

    // 現在のページにある履歴を表示する
    for (i = startIndex; i < endIndex; i++)
    {
        int row;
        int y;

        row = i - startIndex;

        y = HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT + 4 + row * HISTORY_ROW_HEIGHT;

        DrawFormatString(HISTORY_START_X + 7, y, white, "%02d", i + 1);

        DrawString(HISTORY_START_X + 50, y, historyInput[i], white);

        DrawFormatString(HISTORY_START_X + 165, y, white, "%d", historyHit[i]);

        DrawFormatString(HISTORY_START_X + 242, y, white, "%d", historyBlow[i]);
    }
}

// ===== 【第6回で追加・変更】解答履歴とページタブを描画 ここまで =====
// ゲーム画面を描画する
void Draw(void)
{
    unsigned int white = GetColor(255, 255, 255);

    unsigned int yellow = GetColor(255, 255, 0);

    unsigned int red = GetColor(255, 80, 80);

    // ゲームタイトルを表示する
    DrawString(40, 25, "HIT & BLOW", white);

    // 完成時には削除する正解表示
    DrawFormatString(40, 55, yellow, "デバッグ用正解:%s", answer);

    // 画像の読み込みに失敗していた場合
    if (imageLoadError != 0)
    {
        DrawString(40, 110, "画像を読み込めませんでした。", red);

        DrawString(40, 140, "imagesフォルダーの場所を確認してください。", red);

        return;
    }

    DrawString(40, 85, "数字を4桁入力してください", white);

    // 4桁の入力欄を描画する
    for (int i = 0; i < INPUT_DIGIT_COUNT; i++)
    {
        int x;
        int imageHandle;

        // i番目の桁を表示するX座標
        x = INPUT_START_X + i * (INPUT_IMAGE_SIZE + INPUT_IMAGE_GAP);

        // 未入力の場合に表示する画像
        imageHandle = blankImage;

        // 入力済みで、数字が入っている場合
        if (i < inputCount)
        {
            int number;

            // 文字の数字を整数へ変換する
            number = input[i] - '0';

            // 対応する緑色の数字画像を選ぶ
            imageHandle = displayNumberImages[number];
        }

        // 入力欄の画像を80×80で表示する
        DrawGraph(x, INPUT_START_Y, imageHandle, TRUE);
    }

    DrawFormatString(40, 215, white, "入力:%s", input);

    // 正しく判定できた場合はHIT数とBLOW数を表示する
    if (hasResult)
    {
        DrawFormatString(40, 245, white, "HIT :%d", hit);

        DrawFormatString(40, 275, white, "BLOW:%d", blow);
    }
    // 入力に問題がある場合は、
    // エラー番号に対応したメッセージを表示する
    else if (inputError == 1)
    {
        DrawString(40, 245, "4桁入力してください。", red);
    }
    else if (inputError == 2)
    {
        DrawString(40, 245, "0~9の数字だけを入力してください。", red);
    }
    else if (inputError == 3)
    {
        DrawString(40, 245, "同じ数字を2回使わないでください。", red);
    }

    DrawString(40, 300, "数字ボタン", white);

    // 0~9の数字ボタンを描画する
    for (int i = 0; i < NUMBER_COUNT; i++)
    {
        int column;
        int row;
        int x;
        int y;

        // 5個ずつ並べるための列と行を求める
        column = i % NUMBER_BUTTON_COLUMNS;

        row = i / NUMBER_BUTTON_COLUMNS;

        // 列と行から画面座標を求める
        x = NUMBER_BUTTON_START_X + column * (NUMBER_BUTTON_SIZE + NUMBER_BUTTON_GAP);

        y = NUMBER_BUTTON_START_Y + row * (NUMBER_BUTTON_SIZE + NUMBER_BUTTON_GAP);

        // 数字ボタンを70×70で表示する
        DrawGraph(x, y, numberImages[i], TRUE);
    }

    // 判定ボタンを表示する
    DrawGraph(JUDGE_BUTTON_X, JUDGE_BUTTON_Y, judgeImage, TRUE);

    // 右クリックで入力を消せることを表示する
    DrawString(330, 525, "右クリック:入力を消す", gray);

    // ===== 【第6回で追加・変更】ゲーム画面から履歴描画を呼び出す ここから =====
    // 右側へ解答履歴を表示する
    DrawHistory();
    // ===== 【第6回で追加・変更】ゲーム画面から履歴描画を呼び出す ここまで =====
}

// Update1回分の時間を秒で返す
double DeltaTime(void)
{
    return FIXED_DELTA_TIME;
}

int WINAPI WinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nCmdShow)
{
    LONGLONG previousTime;
    double accumulator;

    // ウィンドウモードで起動する
    ChangeWindowMode(TRUE);

    // 画面の大きさを設定する
    SetGraphMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32);

    // ScreenFlipで垂直同期を待つ
    SetWaitVSyncFlag(TRUE);

    // DxLibを初期化する
    if (DxLib_Init() == -1)
    {
        return -1;
    }

    // 描画先を裏画面にする
    SetDrawScreen(DX_SCREEN_BACK);

    // ゲームを初期化する
    Initialize();

    // 時間計測を開始する
    previousTime = GetNowHiPerformanceCount();

    // まだUpdateで処理していない時間
    accumulator = 0.0;

    // ゲームループ
    while (ProcessMessage() == 0)
    {
        LONGLONG currentTime;
        double frameTime;

        // ESCキーが押されたら終了する
        if (CheckHitKey(KEY_INPUT_ESCAPE) != 0)
        {
            break;
        }

        // 現在時刻を取得する
        currentTime = GetNowHiPerformanceCount();

        // 前回から実際に経過した時間を秒で求める
        frameTime = (currentTime - previousTime) / 1000000.0;

        // 次の計測に備えて現在時刻を保存する
        previousTime = currentTime;

        // 長時間止まった場合は、処理する時間を制限する
        if (frameTime > MAX_FRAME_TIME)
        {
            frameTime = MAX_FRAME_TIME;
        }

        // 実際に経過した時間をためる
        accumulator += frameTime;

        // 1/60秒以上たまっている間、ゲームを更新する
        while (accumulator >= DeltaTime())
        {
            Update();

            // Updateで処理した時間を引く
            accumulator -= DeltaTime();
        }

        // 裏画面を黒で消す
        ClearDrawScreen();

        // ゲーム画面を描画する
        Draw();

        // 裏画面を表画面へ表示する
        ScreenFlip();
    }

    // DxLibを終了する
    DxLib_End();

    return 0;
}

9.今回のまとめ

今回は、正常に判定できた結果を3つの配列へ保存しました。

historyInput
historyHit
historyBlow

同じ添え字が、同じ1回分の結果を表します。

また、履歴を15件ずつ4ページに分けました。

1ページ目:1~15件目
2ページ目:16~30件目
3ページ目:31~45件目
4ページ目:46~60件目

正常な判定後は、結果を履歴へ保存してから入力欄を空にします。

次回は、タイトル画面、ゲーム画面、クリア画面を切り替えられるようにします。