STEP05:map層の実装(25x25マップ描画・壁・敵・宝箱・施設)

STEP05では、探索RPGの心臓部になる 25x25マップ を扱う map層を実装します。
この層の目的は「マップのデータ管理」と「描画」と「移動できるか判定」を ゲーム本体の処理から切り離す ことです。

このSTEPでできるようになること

  • 25x25のマップ(床・壁)を生成できる。
  • 敵・宝箱・施設(宿・教会)を配置できる。
  • マップを描画できる(プレイヤー位置は上書き表示)。
  • 壁にぶつかる移動を防げる(当たり判定)。

今回追加するファイル

ファイル役割
map.hmap層の公開API(構造体、関数宣言、タイル定義)
map.c25x25マップ生成・配置・描画・判定の実装

タイル設計(床・壁・敵・宝箱・施設)

まず「マップ上の1マスが何を表すか」を決めます。
今回は扱いやすさ優先で 1文字(char) で表現します。

種類タイル文字意味移動可能
.通路
#通れない
E戦闘が起きる○(踏める)
宝箱Tアイテム入手○(踏める)
宿I全回復○(踏める)
教会C復活/蘇生○(踏める)

プレイヤーはマップ自体には書き込まず、描画時に P を重ねます(上書き表示)。
こうすると「プレイヤーが動いたとき、元の床や施設を壊さない」ので扱いがラクです。

map層で提供する関数一覧

関数役割
map_init25x25マップを初期化(床+外周壁+簡易迷路)
map_place_features宿・教会を固定配置
map_place_random敵や宝箱をランダム配置
map_drawマップを描画(プレイヤー位置を上書き)
map_in_bounds座標が範囲内か判定
map_is_walkableそのマスに移動できるか判定(壁は不可)
map_get / map_setタイル取得・設定
map_try_move移動を試みる(壁なら止める)+踏んだタイルを返す

map.h(公開ヘッダ)

/* map.h */
#ifndef MAP_H
#define MAP_H

#include "common.h"   /* bool を使っている想定(無いなら stdbool.h に置き換えOK) */

#define MAP_W 25
#define MAP_H 25

/* タイル定義(1文字で表す) */
#define TILE_FLOOR   '.'
#define TILE_WALL    '#'
#define TILE_ENEMY   'E'
#define TILE_TREASURE 'T'
#define TILE_INN     'I'
#define TILE_CHURCH  'C'

typedef struct Map {
    char cells[MAP_H][MAP_W];
} Map;

/* 初期化・配置 */
void map_init(Map *m);
void map_place_features(Map *m);                 /* 宿・教会など固定施設 */
void map_place_random(Map *m, int enemies, int treasures);

/* 描画 */
void map_draw(const Map *m, int px, int py);

/* 判定・取得 */
bool map_in_bounds(int x, int y);
bool map_is_walkable(const Map *m, int x, int y); /* 壁は移動不可 */
char map_get(const Map *m, int x, int y);
void map_set(Map *m, int x, int y, char tile);

/* 移動(成功したら座標更新、踏んだタイルを返す。失敗なら壁で止まり床扱いを返す) */
char map_try_move(const Map *m, int *px, int *py, int dx, int dy);

#endif

map.c(実装)

ポイントは次の3つです。

  • マップ生成は「外周壁+床」でベースを作る
  • 内部の壁は簡易パターン(迷路っぽく)にする(今回は分かりやすさ優先)
  • 敵・宝箱は「床にだけ置く」「スタート地点と施設は避ける」
/* map.c */
#include "map.h"
#include <stdio.h>
#include <stdlib.h>

/* STEP02のplatform層があるなら使う(無ければこのままでも動く) */
void platform_set_color_default(void);
void platform_set_color_wall(void);
void platform_set_color_enemy(void);
void platform_set_color_treasure(void);
void platform_set_color_facility(void);

static int is_floor_like(char t) {
    /* 敵・宝箱・施設も「踏める床」扱いにしたいので、壁以外は床側として扱う */
    return (t != TILE_WALL);
}

bool map_in_bounds(int x, int y) {
    return (x >= 0 && x < MAP_W && y >= 0 && y < MAP_H);
}

char map_get(const Map *m, int x, int y) {
    if (!map_in_bounds(x, y)) return TILE_WALL; /* 範囲外は壁扱いで安全にする */
    return m->cells[y][x];
}

void map_set(Map *m, int x, int y, char tile) {
    if (!map_in_bounds(x, y)) return;
    m->cells[y][x] = tile;
}

bool map_is_walkable(const Map *m, int x, int y) {
    char t = map_get(m, x, y);
    return (t != TILE_WALL);
}

/* 簡易な内部壁の生成(見た目を迷路っぽくする) */
static void build_simple_maze(Map *m) {
    /* 縦壁:x=4,8,12,16,20 などに壁を作り、ところどころ穴を空ける */
    for (int x = 4; x <= 20; x += 4) {
        for (int y = 1; y < MAP_H - 1; y++) {
            map_set(m, x, y, TILE_WALL);
        }
        /* 穴を空ける(通路) */
        map_set(m, x, 3, TILE_FLOOR);
        map_set(m, x, 9, TILE_FLOOR);
        map_set(m, x, 15, TILE_FLOOR);
        map_set(m, x, 21, TILE_FLOOR);
    }

    /* 横壁:y=6,12,18 に薄く壁を置いて変化を出す */
    for (int y = 6; y <= 18; y += 6) {
        for (int x = 1; x < MAP_W - 1; x++) {
            if (x % 4 != 0) { /* 縦壁と完全に塞がないようにする */
                map_set(m, x, y, TILE_WALL);
            }
        }
        /* 穴を空ける */
        map_set(m, 2, y, TILE_FLOOR);
        map_set(m, 10, y, TILE_FLOOR);
        map_set(m, 18, y, TILE_FLOOR);
        map_set(m, 22, y, TILE_FLOOR);
    }

    /* スタート想定地点(1,1)周辺だけは通れるように軽く整える */
    map_set(m, 1, 1, TILE_FLOOR);
    map_set(m, 2, 1, TILE_FLOOR);
    map_set(m, 1, 2, TILE_FLOOR);
    map_set(m, 2, 2, TILE_FLOOR);
}

void map_init(Map *m) {
    /* まず床で埋める */
    for (int y = 0; y < MAP_H; y++) {
        for (int x = 0; x < MAP_W; x++) {
            m->cells[y][x] = TILE_FLOOR;
        }
    }

    /* 外周を壁で囲う */
    for (int x = 0; x < MAP_W; x++) {
        m->cells[0][x] = TILE_WALL;
        m->cells[MAP_H - 1][x] = TILE_WALL;
    }
    for (int y = 0; y < MAP_H; y++) {
        m->cells[y][0] = TILE_WALL;
        m->cells[y][MAP_W - 1] = TILE_WALL;
    }

    /* 内部に簡易迷路を作る */
    build_simple_maze(m);
}

void map_place_features(Map *m) {
    /* 宿と教会は固定配置(分かりやすい) */
    /* 置く場所が壁だった場合は床に戻してから置く */
    map_set(m, 3, 3, TILE_FLOOR);
    map_set(m, 3, 3, TILE_INN);

    map_set(m, 21, 21, TILE_FLOOR);
    map_set(m, 21, 21, TILE_CHURCH);
}

/* 指定タイルを「床にだけ」ランダムに置く */
static void place_random_on_floor(Map *m, char tile, int count, int avoid_x, int avoid_y) {
    int placed = 0;
    int safety = 20000; /* 無限ループ防止 */

    while (placed < count && safety-- > 0) {
        int x = rand() % MAP_W;
        int y = rand() % MAP_H;

        /* 外周は壁なので避ける(ついでに範囲内で床を狙う) */
        if (!map_in_bounds(x, y)) continue;

        /* スタート地点は避ける */
        if (x == avoid_x && y == avoid_y) continue;

        /* 既に何か置いてある場所(床以外)は避ける */
        if (map_get(m, x, y) != TILE_FLOOR) continue;

        map_set(m, x, y, tile);
        placed++;
    }
}

void map_place_random(Map *m, int enemies, int treasures) {
    /* スタート想定(1,1)は避ける */
    place_random_on_floor(m, TILE_ENEMY, enemies, 1, 1);
    place_random_on_floor(m, TILE_TREASURE, treasures, 1, 1);
}

/* 1マス描画(色があるならここで切り替える) */
static void draw_tile(char t) {
    /* 色分け(platform層が未実装なら、これら関数は空実装にしてOK) */
    if (t == TILE_WALL) {
        platform_set_color_wall();
        putchar('#');
        platform_set_color_default();
        return;
    }
    if (t == TILE_ENEMY) {
        platform_set_color_enemy();
        putchar('E');
        platform_set_color_default();
        return;
    }
    if (t == TILE_TREASURE) {
        platform_set_color_treasure();
        putchar('T');
        platform_set_color_default();
        return;
    }
    if (t == TILE_INN || t == TILE_CHURCH) {
        platform_set_color_facility();
        putchar(t);
        platform_set_color_default();
        return;
    }

    /* 床 */
    platform_set_color_default();
    putchar('.');
}

void map_draw(const Map *m, int px, int py) {
    for (int y = 0; y < MAP_H; y++) {
        for (int x = 0; x < MAP_W; x++) {
            if (x == px && y == py) {
                /* プレイヤーは上書き表示 */
                platform_set_color_facility();
                putchar('P');
                platform_set_color_default();
            } else {
                draw_tile(m->cells[y][x]);
            }
        }
        putchar('\n');
    }
}

/* 移動を試みて、踏んだタイル(床/敵/宝箱/施設など)を返す */
char map_try_move(const Map *m, int *px, int *py, int dx, int dy) {
    int nx = *px + dx;
    int ny = *py + dy;

    /* 壁なら止まる */
    if (!map_is_walkable(m, nx, ny)) {
        return TILE_FLOOR;
    }

    *px = nx;
    *py = ny;

    /* 踏んだ先のタイルを返す(イベント判定に使う) */
    return map_get(m, nx, ny);
}

platform層(色)がまだ無い場合の対処

map.c では platform_set_color_xxx() を呼んでいますが、STEP02でまだ色分け関数を用意していないなら、platform.c 側に空実装を置けばコンパイルできます。

/* platform.c に入れておく(暫定) */
void platform_set_color_default(void) {}
void platform_set_color_wall(void) {}
void platform_set_color_enemy(void) {}
void platform_set_color_treasure(void) {}
void platform_set_color_facility(void) {}

後でANSIカラー対応やWindows用API対応を入れたら、ここを差し替えるだけでマップ表示が派手になります。

動作確認用 main.c(STEP05テスト)

STEP05の時点では「歩ける」「踏める」「描ける」が確認できればOKです。
入力は input層(STEP03)を使って、テンキー移動(8/2/4/6)で動かしてみます。

/* main.c(STEP05 動作確認用) */
#include <time.h>
#include "ui.h"
#include "input.h"
#include "map.h"

/* STEP02 */
void clear_screen(void);
void sfx_beep(int kind);

static void show_hint(void) {
    printf("Move: 8=Up 2=Down 4=Left 6=Right  5=Status  0=Exit\n");
}

int main(void) {
    srand((unsigned)time(NULL));

    for (;;) {
        UiTitleChoice c = ui_title_screen();
        if (c == UI_TITLE_QUIT) break;
        if (c == UI_TITLE_HOWTO) { ui_show_howto(); continue; }

        /* マップ準備 */
        Map m;
        map_init(&m);
        map_place_features(&m);
        map_place_random(&m, 12, 8); /* 敵12、宝箱8 */

        int px = 1, py = 1; /* スタート位置 */

        for (;;) {
            ui_print_header("ADVENTURE (STEP05 TEST)");
            show_hint();
            putchar('\n');

            map_draw(&m, px, py);
            putchar('\n');

            int cmd = choose_menu("Command (0/2/4/5/6/8): ", 0, 8);

            if (cmd == 0) {
                sfx_beep(0);
                break;
            }

            if (cmd == 5) {
                sfx_beep(0);
                UiStatusRow party[3] = {
                    {"Hero", 1, 30, 30, 10, 10},
                    {"Mage", 1, 18, 18, 25, 25},
                    {"Warrior", 1, 40, 40, 5, 5}
                };
                ui_print_header("STATUS");
                ui_show_party_status(party, 3, 120);
                ui_wait_enter("Press Enter to return...");
                continue;
            }

            int dx = 0, dy = 0;
            if (cmd == 8) dy = -1;
            else if (cmd == 2) dy = 1;
            else if (cmd == 4) dx = -1;
            else if (cmd == 6) dx = 1;
            else {
                /* 1,3,7,9 を入れても無視(今回は使わない) */
                continue;
            }

            char stepped = map_try_move(&m, &px, &py, dx, dy);

            /* 踏んだタイルでイベントが起きる想定(STEP06以降で本実装) */
            if (stepped == TILE_ENEMY) {
                sfx_beep(1);
                ui_print_header("ENCOUNTER (DUMMY)");
                printf("Enemy appeared! (battle will be implemented later)\n\n");
                ui_wait_enter("Press Enter...");
            } else if (stepped == TILE_TREASURE) {
                sfx_beep(0);
                ui_print_header("TREASURE (DUMMY)");
                printf("You found a treasure! (items will be implemented later)\n\n");
                ui_wait_enter("Press Enter...");
            } else if (stepped == TILE_INN) {
                sfx_beep(2);
                ui_print_header("INN (DUMMY)");
                printf("Fully recovered! (services will be implemented later)\n\n");
                ui_wait_enter("Press Enter...");
            } else if (stepped == TILE_CHURCH) {
                sfx_beep(0);
                ui_print_header("CHURCH (DUMMY)");
                printf("Revive service! (services will be implemented later)\n\n");
                ui_wait_enter("Press Enter...");
            }
        }
    }

    ui_show_ending(0);
    return 0;
}

このSTEPのまとめ

  • map層が「マップの持ち方」「描画」「当たり判定」を担当するようになった。
  • プレイヤーはマップに書き込まず、描画で重ねる方式にした。
  • 敵・宝箱・施設は “踏める” タイルとして扱い、イベント判定は後続STEPへ回せる形にした。

次のSTEP06へ(予告)

STEP06では、今ダミー表示になっているイベント部分を本格化します。

  • services層(宿・教会の処理)
  • items層(宝箱の中身、装備)
  • enemies/battle層(敵との戦闘)

map層はこのまま「踏んだタイルを返す」だけにしておくと、後の拡張がすごくラクになります。