C言語の学習がチョットだけ楽しくなりそうなライブラリを簡単に作ってみた

# はじめに
こんにちわ。
とある学校で組込み教えてます。

C言語の学習が面白くなさそうな人が多そうなので、すこしでもC言語の学習が楽しくなるようなライブラリを作ってみました。

組込みマイコンでCやることを目指してC言語習得してもらうんですが、素のCだけで文法学習するのはとてもつらいんですよね。文字だけじゃツライ。scanf()の動きもアヤシイし...
初心者には。何か動きのあるものの方が興味続きそう。

まあつまりは、scanf()とprintf()だけでCの学習をやるには限界を感じてたんですよね。（まだそんなことやってたの！！っていわないで！！）

ってなわけで、動き要素をWindowsコンソールアプリでなんか動きがあるようなモノを作れるようなライブラリを作ることを思つきました。

ちょうど祝日だったのでエイヤってつくりました。。

コンソールアプリ上で、ラズパイとかでいうところの「スイッチ読み取り」「Ｌチカ」くらいの動きがあるモノを作れることを想定しています。

あんまり複雑なことはできません！！
だって、複雑なことできるようにしちゃうと、ライブラリの使い方を覚えないとなにもできないからね！！！


# どんなライブラリか
 - Visual Studioで動きます
 - 言語はC言語です
 - Windows環境を想定しています
 - コンソールアプリを想定してます
 - 組込みでマイコンでCやるときの前座っぽい作り方ができることを想定してます
 - キーボードのキーをノンブロッキングで読み取れます
   - 入力バッファは無視します
   - 別スレッドで定期的に読んでます
 - (メイン)スレッドをms単位で休止する関数を持ってます
   - 組込みアプリっぽい流れが実現できます
 - スクリーン(コンソール)に文字を出力するときに
   - 出力する位置を指定できます
   - 文字色も、限られた色ですが指定できます
 - スクリーンをクリアする関数を備えています

とまあ、全然たいしたことはできません。

詳細はヘッダファイルを見てね！！！

# ライブラリを利用するゲームも作ったよ
ライブラリだけじゃ意味ないので、ライブラリを使った簡単なゲームを作りました。
この記事に貼り付けてあるので見てくださいね。


# 出来映え
コーディングルールとか一貫性とか全無視です。
とりあえず書いた感じ。
これからチャンとしたコードにしていくつもりではいます。

とりあえず、こんな感じの迷路ゲームくらいなら作れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126400/86aad883-da0f-b9b0-fc82-6bc078e595fb.png)


# 資料収集等
資料収集はChatGPTです。
いまはAIに何かコード書け！！っていうと、やってくれるんですね。すごい。
これくらいのものなら色々組み合わせて１日でできるんですね。すごい。


# コード
## ライブラリのコード
### tiny_console_screen.h
```tiny_console_screen.h
#ifndef TINY_CONSOLE_SCREEN_H
#define TINY_CONSOLE_SCREEN_H

#include <Windows.h>

/*ライブラリを初期化する。最初に必ず呼ぶ必要がある。*/
void  init_tiny_console_screen(void);

/* カーソル位置をセットする。引数x, yに座標をセットする。 */
void set_cursor_pos(int x, int y);

/* スクリーンをクリアする */
void clear_screen(void);

/* カーソルを非表示にする */
void set_cursor_off(void);

/* カーソルを表示にする */
void set_cursor_on(void);

typedef enum { TEXT_WHITE, TEXT_BLUE, TEXT_GREEN, TEXT_RED, TEXT_COLOR_MAX } TextColor;

/* カーソルの文字色を指定する */
void set_cursor_color(TextColor color);

#endif
```

### tiny_console_keyboard.h
```tiny_console_keyboard.h

#ifndef TINY_CONSOLE_LIB_H
#define TINY_CONSOLE_LIB_H

#include <Windows.h>

/*ライブラリを初期化する。最初に必ず呼ぶ必要がある。*/
void  init_tiny_console_keyboard(void);

/*
引数に指定されたキーボード上のキーが押されているかを返す。
ブロックせず、関数はすぐに戻る。
戻り値
 0: 押されてない
 1: 押されている
*/
int is_key_down(unsigned char key);

/* ミリ秒単位で休止する。引数に休止する時間をミリ秒単位で指定する。 */
void sleep_ms(unsigned int ms);


/* 標準入力から1文字入力する。ブロックする */
char get_char(void);

#endif
```

 ### tiny_console_screen.c
```tiny_console_screen.c
#include "tiny_console_screen.h"
#include <stdio.h>
#include <Windows.h>

/* ファイルスコープにする */
static COORD cursor_pos;

static HANDLE hConsole;
static CONSOLE_CURSOR_INFO cursorInfo;

static unsigned int text_color[TEXT_COLOR_MAX] = {0x0000, FOREGROUND_BLUE , FOREGROUND_GREEN, FOREGROUND_RED};

/*ライブラリを初期化する。最初に必ず呼ぶ必要がある。*/
void  init_tiny_console_screen(void) {
	cursor_pos.X = 0;
	cursor_pos.Y = 0;

	hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

	// カーソル情報の取得
	GetConsoleCursorInfo(hConsole, &cursorInfo);

	return;
}

/* カーソル位置をセットする。引数x, yに座標をセットする。 */
void set_cursor_pos(int x, int y) {
	cursor_pos.X = x;
	cursor_pos.Y = y;

	SetConsoleCursorPosition(hConsole, cursor_pos);

	return;
}

/* カーソルを非表示にする */
void set_cursor_off(void) {
	// カーソルを非表示に設定
	cursorInfo.bVisible = FALSE;
	SetConsoleCursorInfo(hConsole, &cursorInfo);

	return;
}

/* カーソルを表示にする */
void set_cursor_on(void) {
	// カーソルを表示に設定
	cursorInfo.bVisible = TRUE;
	SetConsoleCursorInfo(hConsole, &cursorInfo);

	return;
}

/* カーソルの文字色を指定する */
void set_cursor_color(TextColor color) {
	// 文字色を設定
	SetConsoleTextAttribute(hConsole, text_color[color] | FOREGROUND_INTENSITY);

	return;
}

/* スクリーンをクリアする */
void clear_screen(void) {
	system("cls");

	return;
}
```
### tiny_console_keyboard.c
``` tiny_console_keyboard.c

#include "tiny_console_keyboard.h"
#include "tiny_console_screen.h"
#include <stdio.h>
#include <Windows.h>

#define CYCLE_TO_READ_KB  (10) /* キーボード押下チェックする間隔、ミリ秒 */
#define KB_READ_MIN (0x00)
#define KB_READ_MAX (0xFE)

#define READ_BUFFER_SIZE  (255)

#define KB_TOUCH_COUNT_INIT (5)

/* ファイルスコープにする */

static HANDLE hThread;
static DWORD dwThreadId;
static DWORD WINAPI readKeyboard(LPVOID lpParam);

static int key_down_count[KB_READ_MAX + 1];
static int key_touched[KB_READ_MAX + 1];

/*ライブラリを初期化する。最初に必ず呼ぶ必要がある。*/
void init_tiny_console_keyboard(void) {

    //配列の内容をクリア
    memset(key_down_count, 0, KB_READ_MAX);
    memset(key_touched, 0, KB_READ_MAX);

    // スレッドを生成
    hThread = CreateThread(NULL, 0, readKeyboard, NULL, 0, &dwThreadId);
    if (hThread == NULL) {
        fprintf(stderr, "Error creating thread\n");
    }

    return;
}

/* 標準入力から1文字入力する。ブロックする */
char get_char(void) {
    unsigned int key_to_read;
    char moji = ' ';
    while (1) {
        for (key_to_read = 'A'; key_to_read <= 'Z'; key_to_read++) {
            if (GetAsyncKeyState(key_to_read) & 0x8000) {
                moji = key_to_read;
                return moji;
            }
        }
        sleep_ms(CYCLE_TO_READ_KB);
    }

    return moji;
}


/* 
引数に指定されたキーボード上のキーが押されているかを返す。
ブロックせず、関数はすぐに戻る。
戻り値
 0: 押されてない
 1: 押されている
*/
int is_key_down(unsigned char key) {
    if (key_down_count[key] > 0 ) {
        return(1);
    } else {
        return(0);
    }
}


/* ミリ秒単位で休止する。引数に休止する時間をミリ秒単位で指定する。 */
void sleep_ms(unsigned int ms) {
    Sleep(ms);
}


/* キーボードが押されているかを、スレッド関数内でチェックする */
static DWORD WINAPI readKeyboard(LPVOID lpParam) {
    unsigned int key_to_read;
    
    while (1) {
        for (key_to_read = KB_READ_MIN; key_to_read < KB_READ_MAX; key_to_read++) {

            if (key_down_count[key_to_read] > 0) {
                key_down_count[key_to_read]--;
            }

            if (GetAsyncKeyState(key_to_read) & 0x8000) {
                key_down_count[key_to_read] = KB_TOUCH_COUNT_INIT;
            }
        }

        sleep_ms(CYCLE_TO_READ_KB);
    }
    return 0;
}

```

## ライブラリを使用する迷路ゲームのコード
### main.c
``` main.c
#include <stdio.h>

#include "tiny_console_keyboard.h"
#include "tiny_console_screen.h"

/* 画面を描画する */
void draw_screen(void);

int main(void) {
    int game_result;
    char moji;

    /* ライブラリを初期化する */
    init_tiny_console_keyboard();
    init_tiny_console_screen();

    /* ゲームが始まった */


    while (1) {
        /* メニューを表示する */
        printf("迷宮ラビリンスへようこそ\n");
        printf("S: ゲームスタート\n");
        printf("E: ゲーム終了\n");

        printf("\n");
        /*すこし間を置く*/
        sleep_ms(500);

        printf("1文字で選択肢を押してください: ");
        moji = get_char();
        printf("\n");

        switch (moji) {
        case 'S':
        case 's':
            break;

        case 'E':
        case 'e':
            printf("さようなら\n");
            exit(0);
            break;

        default:
            printf("無効な選択肢です\n");
            
            continue;
            break;
        }


        /* ゲームが始まる */
        /*ゲームの準備*/
        printf("\n\n\n");
        printf("準備はいいですか？\n");
        /*すこし間を置く*/
        sleep_ms(500);

        printf("A を押すとゲームが始まります。\n");
        printf("ゲーム中にQのキーを押すと、ゲームをリタイアします\n");

        while (get_char() != 'A'); /* A キーの入力を待つ */
        
        printf("ゲームが始まるまでお待ちください\n");
        /*すこし間を置く*/
        sleep_ms(1000);

        /*ゲームを実行する*/
        game_result = game_meiro_main();

        /*すこし間を置く*/
        sleep_ms(1000);

        /*画面をクリアする*/
        clear_screen();

        /* 関数が終わり、ゲームの結果が返ってきた */
        switch (game_result) {
        case 1:
            printf("ゲームをクリアしました。おめでとうございます。\n");
            break;
        case 2:
            printf("敵に殺されました。残念！\n");

            break;
        case 3:
            printf("Qキーが押されてゲームをリタイアしました。どうしました？\n");

            break;
        default:
            printf("ゲームの結果が異常です\n");
            break;
        }

        
    }

    return 0;
}

```

### game_meiro.h
``` game_meiro.h
#ifndef GAME_MEIRO_H
#define GAME_MEIRO_H

/*ゲームの初期化*/
void game_meiro_init();

/* ゲームを進行する */
int game_meiro_main(void);

#endif


```

### game_meiro.c
``` game_meiro.c
#include <stdio.h>
#include "game_meiro.h"
#include "tiny_console_keyboard.h"
#include "tiny_console_screen.h"


#define MEIRO_YOKO_MAX (20)
#define MEIRO_TATE_MAX (20)

static void draw_screen(void);
static int player_pos_x, player_pos_y;

/* 迷路データ */
/* 0: なにもない、1: 壁、(2: プレーヤー、)3: 敵、8:入り口(初期位置)、9:出口(ゴール)*/
static const unsigned char meiro_data[MEIRO_TATE_MAX][MEIRO_YOKO_MAX] =
{ /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9*/
    {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, /* 0*/
    {8,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, /* 1*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, /* 2*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, /* 3*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, /* 4*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1}, /* 5*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1}, /* 6*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1}, /* 7*/
    {1,0,0,0,0,0,0,3,0,0,0,0,0,1,0,0,0,0,0,1}, /* 8*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1}, /* 9*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1}, /* 0*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1}, /* 1*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, /* 2*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, /* 3*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, /* 4*/
    {1,0,1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, /* 5*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, /* 6*/
    {1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, /* 7*/
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9}, /* 8*/
    {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} /* 9*/
};

static char *game_symbol[] = {"□", "■", "★", "敵", "　", "　", "　", "　",  "入" , "出"};

void game_meiro_init() {
    player_pos_x = 0;
    player_pos_y = 0;
}

int game_meiro_main(void) {
    int need_to_draw;
    int tate, yoko;
    int game_result;

    game_result = 0;

    
    /* 迷路の入り口を探す */
    for (tate = 0; tate < MEIRO_TATE_MAX; tate++) {
        for (yoko = 0; yoko < MEIRO_YOKO_MAX; yoko++) {
            if (meiro_data[tate][yoko] == 8) {
                player_pos_x = yoko;
                player_pos_y = tate;
            }
        }

    }

    //カーソルを非表示にする
    set_cursor_off();
    draw_screen();

    while (1) {
        need_to_draw = 0; /*0の場合画面変化なしとする*/
        /*→キー*/
        if (is_key_down(VK_RIGHT)) {
            /*右端でなく、右側に壁がなかったら*/
            if (player_pos_x < MEIRO_YOKO_MAX){
                if (meiro_data[player_pos_y][player_pos_x + 1] != 1) {
                    player_pos_x++;
                    need_to_draw = 1;
                }
              }
        }
        if (is_key_down(VK_LEFT)) {
            /*左端でなく、左側に壁がなかったら*/
            if (player_pos_x > 0) {
                if (meiro_data[player_pos_y][player_pos_x - 1] != 1) {
                    player_pos_x--;
                    need_to_draw = 1;
                }
            }
        }
        if (is_key_down(VK_UP)) {
            /*上端でなく、上側に壁がなかったら*/
            if (player_pos_x > 0) {
                if (meiro_data[player_pos_y - 1][player_pos_x] != 1) {
                    player_pos_y--;
                    need_to_draw = 1;
                }
            }

        }

        if (is_key_down(VK_DOWN)) {
            /*下端でなく、下側に壁がなかったら*/
            if (player_pos_x < MEIRO_TATE_MAX) {
                if (meiro_data[player_pos_y + 1][player_pos_x] != 1) {
                    player_pos_y++;
                    need_to_draw = 1;
                }
            }
        }

        /*ゴール、会敵の判定*/
        if (meiro_data[player_pos_y][player_pos_x] == 9) {
            /*ゴールにたどり着いた*/
            game_result = 1;
            
            break;
        }
        if (meiro_data[player_pos_y][player_pos_x] == 3) {
            /*会敵し、死んだ*/
            game_result = 2;

            break;
        }

        if (is_key_down('Q')) {
            /*Q キーが押されてゲームをリタイアした*/
            game_result = 3;

            break;

        }

        if (need_to_draw != 0) {
            draw_screen();

        }


        /* 100ミリ秒毎に計算を行ってゲームの画面を更新する */
        sleep_ms(100);
    }

    /*色を黒に戻す*/
    set_cursor_color(TEXT_WHITE);

    return(game_result);
}

/* 画面を描画する */
void draw_screen(void) {
    char *p_symbol;
    int tate, yoko;
    clear_screen();

    set_cursor_color(TEXT_BLUE);

    /* 配列に格納されている迷路データを描画する */
    for (int tate = 0; tate < MEIRO_YOKO_MAX; tate++) {
        for (int yoko = 0; yoko < MEIRO_TATE_MAX; yoko++) {
            p_symbol = game_symbol[meiro_data[tate][yoko]];
            set_cursor_pos(yoko * 2, tate);
            printf("%s", p_symbol);
        }
        printf("\n");
    }

    /* プレイヤーを描画する */
    set_cursor_color(TEXT_GREEN);
    set_cursor_pos(player_pos_x * 2, player_pos_y);
    printf(game_symbol[2]);
}
```