Python

【Python】RPG(ロールプレイングゲーム)を作ってみた(初心者プログラミング)

takahide

おはようございます。タカヒデです。

本日はPythonで「RPG(ロールプレイングゲーム)」を作ってみました。

STEP形式で解説しているので、「まずは何かを作ってみたい」という初心者の方の参考になれば幸いです。

こんな人にオススメ
  • プログラミング初心者
  • 何から始めればよいか分からない
  • まずは簡単なゲームを作って興味を持ってみたい
  • Python の基礎をゲーム作りで楽しく学びたい

ぜひ実際にコードを打ちながら作成してみてください。

Contents
  1. 「ロールプレイングゲーム」完成イメージ
  2. STEP1:Pygameのウィンドウを用意する
  3. STEP2:フィールド(マップ)を描画する
  4. STEP3:プレイヤーを矢印キーで移動できるようにする
  5. STEP4:メッセージウィンドウを表示する
  6. STEP5:戦闘画面の基本表示を作る
  7. STEP6:こうげき・にげるの戦闘ロジックを実装する
  8. STEP7:ゲームオーバー画面とリスタート機能をつける
  9. 完成

「ロールプレイングゲーム」完成イメージ

まずは、「ロールプレイングゲーム」完成後の最終的なコードと完成イメージです。

import pygame
import random
import os

# --------------------------------
# 定数
# --------------------------------
TILE_SIZE = 32
MAP_WIDTH = 15
MAP_HEIGHT = 10

MAP_PIXEL_HEIGHT = TILE_SIZE * MAP_HEIGHT
UI_HEIGHT = 96

WINDOW_WIDTH = TILE_SIZE * MAP_WIDTH
WINDOW_HEIGHT = MAP_PIXEL_HEIGHT + UI_HEIGHT

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TILESET_PATH = os.path.join(BASE_DIR, "tileset.png")

# 0 = 床, 1 = 壁
MAP_DATA = [
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,0,1,1,0,0,0,1,1,1,0,0,0,0,1],
    [1,0,1,0,0,1,0,0,0,1,0,1,1,0,1],
    [1,0,0,0,1,1,0,1,0,0,0,0,1,0,1],
    [1,0,1,0,0,0,0,1,0,1,1,0,0,0,1],
    [1,0,1,0,1,0,0,0,0,0,1,0,1,0,1],
    [1,0,0,0,0,0,1,1,0,0,0,0,0,0,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
]


class SimpleDQStyleRPG:
    def __init__(self):
        pygame.init()
        pygame.display.set_caption("Simple RPG - DQ Lite")
        self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
        self.clock = pygame.time.Clock()

        self.font = pygame.font.SysFont("msgothic", 18)
        self.font_small = pygame.font.SysFont("msgothic", 14)

        self.load_tileset(TILESET_PATH)

        self.player_max_hp = 30
        self.enemy_max_hp = 28

        self.running = True
        self.reset_game()

    # --------------------------------
    # 初期状態に戻す
    # --------------------------------
    def reset_game(self):
        self.player_x = 1
        self.player_y = 1
        self.player_hp = self.player_max_hp
        self.enemy_hp = self.enemy_max_hp
        self.steps = 0
        self.state = "field"
        self.message = "矢印キーで移動"

    # --------------------------------
    # 画像ロード&分割
    # --------------------------------
    def load_tileset(self, filename):
        sheet = pygame.image.load(filename).convert_alpha()
        sw = sheet.get_width() // 2
        sh = sheet.get_height() // 2

        grass_raw = sheet.subsurface(pygame.Rect(0,    0,  sw, sh))
        wall_raw  = sheet.subsurface(pygame.Rect(sw,   0,  sw, sh))
        hero_raw  = sheet.subsurface(pygame.Rect(0,   sh,  sw, sh))
        slime_raw = sheet.subsurface(pygame.Rect(sw,  sh,  sw, sh))

        def inner_crop(surface):
            w, h = surface.get_width(), surface.get_height()
            if w > 2 and h > 2:
                return surface.subsurface(pygame.Rect(1, 1, w - 2, h - 2))
            return surface

        grass_inner = inner_crop(grass_raw)
        wall_inner = inner_crop(wall_raw)

        self.tile_grass = pygame.transform.scale(grass_inner, (TILE_SIZE, TILE_SIZE))
        self.tile_wall = pygame.transform.scale(wall_inner, (TILE_SIZE, TILE_SIZE))

        self.player_sprite = pygame.transform.scale(hero_raw, (TILE_SIZE, TILE_SIZE))
        self.player_battle_sprite = pygame.transform.scale(hero_raw, (64, 64))
        self.enemy_battle_sprite = pygame.transform.scale(slime_raw, (80, 80))

    # --------------------------------
    # メッセージ制御
    # --------------------------------
    def set_message(self, text: str):
        self.message = text

    def draw_text_multiline(self, surface, text, x, y, font, color, max_width):
        line = ""
        for ch in text:
            if ch == "\n":
                if line:
                    rendered = font.render(line, True, color)
                    surface.blit(rendered, (x, y))
                    y += rendered.get_height()
                    line = ""
                else:
                    y += font.get_height()
                continue

            test_line = line + ch
            w, h = font.size(test_line)
            if w > max_width and line:
                rendered = font.render(line, True, color)
                surface.blit(rendered, (x, y))
                y += h
                line = ch
            else:
                line = test_line

        if line:
            rendered = font.render(line, True, color)
            surface.blit(rendered, (x, y))

    def draw_message_window(self):
        h = 64
        rect = pygame.Rect(
            8,
            WINDOW_HEIGHT - h - 8,
            WINDOW_WIDTH - 16,
            h,
        )
        pygame.draw.rect(self.screen, (0, 0, 64), rect)
        pygame.draw.rect(self.screen, (192, 192, 255), rect, 2)

        max_width = rect.width - 24
        self.draw_text_multiline(
            self.screen,
            self.message,
            rect.x + 12,
            rect.y + 12,
            self.font_small,
            (255, 255, 255),
            max_width,
        )

    # --------------------------------
    # フィールド描画
    # --------------------------------
    def draw_field(self):
        self.screen.fill((0, 96, 0))

        for y in range(MAP_HEIGHT):
            for x in range(MAP_WIDTH):
                tile = MAP_DATA[y][x]
                img = self.tile_wall if tile == 1 else self.tile_grass
                self.screen.blit(img, (x * TILE_SIZE, y * TILE_SIZE))

        px = self.player_x * TILE_SIZE
        py = self.player_y * TILE_SIZE
        self.screen.blit(self.player_sprite, (px, py))

        self.draw_message_window()

    # --------------------------------
    # 戦闘画面描画
    # --------------------------------
    def draw_battle(self):
        self.screen.fill((8, 8, 40))

        status_rect = pygame.Rect(8, 8, WINDOW_WIDTH - 16, 80)
        pygame.draw.rect(self.screen, (0, 0, 64), status_rect)
        pygame.draw.rect(self.screen, (192, 192, 255), status_rect, 2)

        text_p = self.font_small.render(
            f"▶ プレイヤー  HP {self.player_hp}/{self.player_max_hp}",
            True, (255, 255, 255)
        )
        text_e = self.font_small.render(
            f"   てき       HP {self.enemy_hp}/{self.enemy_max_hp}",
            True, (255, 255, 255)
        )
        self.screen.blit(text_p, (status_rect.x + 12, status_rect.y + 10))
        self.screen.blit(text_e, (status_rect.x + 12, status_rect.y + 36))

        enemy_x = WINDOW_WIDTH // 2 - self.enemy_battle_sprite.get_width() // 2
        enemy_y = 110
        self.screen.blit(self.enemy_battle_sprite, (enemy_x, enemy_y))

        self.draw_message_window()

    # --------------------------------
    # 戦闘ロジック
    # --------------------------------
    def start_battle(self):
        self.state = "battle"
        self.player_hp = self.player_max_hp
        self.enemy_hp = self.enemy_max_hp
        self.set_message("てきが あらわれた! Aキーでこうげき / Rキーでにげる")

    def battle_attack(self):
        dmg = random.randint(4, 8)
        self.enemy_hp -= dmg
        if self.enemy_hp <= 0:
            self.enemy_hp = 0
            self.set_message(
                f"プレイヤーの こうげき! てきに {dmg} のダメージ! てきを たおした!"
            )
            self.state = "field"
            return

        self.set_message(f"プレイヤーの こうげき! {dmg} ダメージ! てきの はんげき!")
        self.enemy_attack()

    def enemy_attack(self):
        dmg = random.randint(4, 9)
        self.player_hp -= dmg
        if self.player_hp <= 0:
            self.player_hp = 0
            self.set_message(
                f"てきの こうげき! {dmg} ダメージ! プレイヤーは たおれてしまった…"
            )
            self.state = "gameover"
            return

        self.set_message(
            f"てきの こうげき! {dmg} ダメージ! Aキー:こうげき / Rキー:にげる"
        )

    def battle_run(self):
        if random.random() < 0.5:
            self.set_message("うまく にげきれた! フィールドにもどる。")
            self.state = "field"
            return

        dmg = random.randint(4, 9)
        self.player_hp -= dmg

        if self.player_hp <= 0:
            self.player_hp = 0
            self.set_message(
                "にげだした! …しかし まわりこまれてしまった!\n"
                f"てきの こうげき! {dmg} ダメージ! プレイヤーは たおれてしまった…"
            )
            self.state = "gameover"
        else:
            self.set_message(
                "にげだした! …しかし まわりこまれてしまった!\n"
                f"てきの こうげき! {dmg} ダメージ! Aキー:こうげき / Rキー:にげる"
            )

    # --------------------------------
    # フィールド移動
    # --------------------------------
    def try_move(self, dx, dy):
        nx = self.player_x + dx
        ny = self.player_y + dy

        if not (0 <= nx < MAP_WIDTH and 0 <= ny < MAP_HEIGHT):
            return
        if MAP_DATA[ny][nx] == 1:
            return

        self.player_x = nx
        self.player_y = ny
        self.steps += 1
        self.set_message(f"あるいた かず:{self.steps}")

        if self.steps % 7 == 0:
            self.start_battle()

    # --------------------------------
    # メインループ
    # --------------------------------
    def run(self):
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False

                if event.type == pygame.KEYDOWN:
                    if self.state == "field":
                        if event.key == pygame.K_UP:
                            self.try_move(0, -1)
                        elif event.key == pygame.K_DOWN:
                            self.try_move(0, 1)
                        elif event.key == pygame.K_LEFT:
                            self.try_move(-1, 0)
                        elif event.key == pygame.K_RIGHT:
                            self.try_move(1, 0)

                    elif self.state == "battle":
                        if event.key == pygame.K_a:
                            self.battle_attack()
                        elif event.key == pygame.K_r:
                            self.battle_run()

                    elif self.state == "gameover":
                        if event.key == pygame.K_SPACE:
                            self.reset_game()

            if self.state == "field":
                self.draw_field()
            elif self.state == "battle":
                self.draw_battle()
            elif self.state == "gameover":
                self.screen.fill((0, 0, 0))
                over_text = self.font.render(
                    "GAME OVER - SPACEキーで さいしょから",
                    True, (255, 255, 255)
                )
                self.screen.blit(
                    over_text,
                    (
                        WINDOW_WIDTH // 2 - over_text.get_width() // 2,
                        WINDOW_HEIGHT // 2 - over_text.get_height() // 2,
                    ),
                )

            pygame.display.flip()
            self.clock.tick(60)

        pygame.quit()


if __name__ == "__main__":
    game = SimpleDQStyleRPG()
    game.run()

こんなゲームが作れます。
超シンプルで超簡単な作りになってます。

なお、本ゲームで使用している画像データは以下です。
「tileset.png」という名前でコードと同じフォルダに保存してください。

STEP1:Pygameのウィンドウを用意する

まずは、ゲームの土台となる「ウィンドウ」と「ゲームループ」を作ります。
この STEP では、黒い画面が表示されて、×ボタンで閉じられるところまでを目指します。

# ★STEP1追加
import pygame
import random
import os

# ★STEP1追加
TILE_SIZE = 32
MAP_WIDTH = 15
MAP_HEIGHT = 10

MAP_PIXEL_HEIGHT = TILE_SIZE * MAP_HEIGHT
UI_HEIGHT = 96

WINDOW_WIDTH = TILE_SIZE * MAP_WIDTH
WINDOW_HEIGHT = MAP_PIXEL_HEIGHT + UI_HEIGHT

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TILESET_PATH = os.path.join(BASE_DIR, "tileset.png")

# 0 = 床, 1 = 壁
# ★STEP1追加
MAP_DATA = [
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,0,1,1,0,0,0,1,1,1,0,0,0,0,1],
    [1,0,1,0,0,1,0,0,0,1,0,1,1,0,1],
    [1,0,0,0,1,1,0,1,0,0,0,0,1,0,1],
    [1,0,1,0,0,0,0,1,0,1,1,0,0,0,1],
    [1,0,1,0,1,0,0,0,0,0,1,0,1,0,1],
    [1,0,0,0,0,0,1,1,0,0,0,0,0,0,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
]


# ★STEP1追加
class SimpleDQStyleRPG:
    def __init__(self):
        pygame.init()
        pygame.display.set_caption("Simple RPG - DQ Lite")
        self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
        self.clock = pygame.time.Clock()
        self.running = True

    def run(self):
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False

            # 画面を黒で塗りつぶす
            self.screen.fill((0, 0, 0))

            # 画面更新
            pygame.display.flip()

            # 60FPSに保つ
            self.clock.tick(60)

        pygame.quit()


# ★STEP1追加
if __name__ == "__main__":
    game = SimpleDQStyleRPG()
    game.run()

手順①:Pygame を読み込んで基本設定をする

import pygame
import random
import os

まずは、ゲームを動かすためのライブラリと、画面サイズなどの定数を用意しています。

  • 「import pygame」
    Pygame ライブラリのインポートです。ゲーム画面やキーボード入力など、ゲームに必要な機能をまとめて提供してくれます。
  • 「random」「os」
    今の STEP ではまだ使いませんが、後の STEP で敵のダメージをランダムにしたり、画像ファイルの場所を扱ったりするときに必要になります。
TILE_SIZE = 32
MAP_WIDTH = 15
MAP_HEIGHT = 10

MAP_PIXEL_HEIGHT = TILE_SIZE * MAP_HEIGHT
UI_HEIGHT = 96

WINDOW_WIDTH = TILE_SIZE * MAP_WIDTH
WINDOW_HEIGHT = MAP_PIXEL_HEIGHT + UI_HEIGHT

続いて、画面サイズなどの定数を定義しています。

  • 「TILE_SIZE」
    1マス(タイル)の大きさをピクセル単位で表しています。
  • 「MAP_WIDTH」「MAP_HEIGHT」
    マップの横何マス×縦何マスかを表しています。後の STEP でマップを描くときに使います。
  • 「WINDOW_WIDTH」「WINDOW_HEIGHT」
    実際のウィンドウの横幅・高さです。
    横幅は「タイルサイズ × マップの横マス数」、高さは「マップ部分+UI(メッセージウィンドウなど)」分を足した値になっています。
# 0 = 床, 1 = 壁
MAP_DATA = [
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    ...
]

最後に、マップデータも先に定義しています。

  • 「MAP_DATA」
    「0:床、1:壁」でマップの情報を表しています。
    この配列は、後の STEP で「タイル画像を並べるための設計図」として使います。

手順②:クラスを作り、ゲームのメインループを書く

class SimpleDQStyleRPG:
    def __init__(self):
        pygame.init()
        pygame.display.set_caption("Simple RPG - DQ Lite")
        self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
        self.clock = pygame.time.Clock()
        self.running = True

次に、ゲーム全体をまとめるクラスと、メインループを作っています。

  • 「class SimpleDQStyleRPG:」
    ゲーム全体をひとまとめにするクラスです。
    クラスの中に「初期化」「描画」「入力の処理」などを追加していきます。
  • init
    「初期化メソッド」と呼ばれる特別な関数です。
    クラスからオブジェクトを作ったときに、最初に一度だけ呼ばれます。
  • 「pygame.init()」
    Pygame を使うときの初期化処理です。
  • 「pygame.display.set_caption(…)」
    ウィンドウのタイトルバーに表示される文字列を設定しています。
  • 「pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))」
    実際のウィンドウを作っています。引数に「(横幅, 縦幅)」のタプルを渡します。
  • 「self.clock = pygame.time.Clock()」
    ゲームの更新速度(FPS)を設定します。
  • 「self.running = True」
    ゲームを動かしているかどうかを管理するフラグです。False に変えるとメインループが終了します。
    def run(self):
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False

            # 画面を黒で塗りつぶす
            self.screen.fill((0, 0, 0))

            # 画面更新
            pygame.display.flip()

            # 60FPS(1秒間に60回更新)に保つ
            self.clock.tick(60)

        pygame.quit()

続いて、メインループです。

  • 「while self.running:」
    「running」がTrueの間、ずっと繰り返すという意味です。
    ゲームは基本的に「無限ループ」のように動き続け、毎フレーム「入力を読む → 画面を描く → 画面を更新する」という処理を繰り返します。
  • 「for event in pygame.event.get():」
    キーボードやマウス、ウィンドウの閉じるボタンなどの「イベント」をまとめて取得しています。
  • 「if event.type == pygame.QUIT:」
    ウィンドウ右上の × ボタンが押されたときのイベントです。ここで「self.running = False」としてループを抜けるようにしています。
  • 「self.screen.fill((0, 0, 0))」
    画面全体を黒(RGB で (0,0,0))で塗りつぶします。今は何も描いていないので、真っ黒な画面になります。
  • 「pygame.display.flip()」
    実際に画面を更新して、プレイヤーの目に見える状態にします。
  • 「self.clock.tick(60)」
    1秒間に60回ループを回すように調整しています。
if __name__ == "__main__":
    game = SimpleDQStyleRPG()
    game.run()

最後に、ゲームを起動するためのコードです。
この部分は「このファイルを直接実行したときだけ、ゲームを開始する」という意味があります。

  • 「game = SimpleDQStyleRPG()」
    クラスからゲームオブジェクトを作成し、「game.run()」で先ほどのメインループをスタートさせています。

STEP1が完了した時点では以下のアプリができています。

STEP2:フィールド(マップ)を描画する

STEP2 では、マップデータ(0=床、1=壁)を使って、フィールド画面を描画し、プレイヤーキャラを表示できるようにします。

import pygame
import random
import os

TILE_SIZE = 32
MAP_WIDTH = 15
MAP_HEIGHT = 10

MAP_PIXEL_HEIGHT = TILE_SIZE * MAP_HEIGHT
UI_HEIGHT = 96

WINDOW_WIDTH = TILE_SIZE * MAP_WIDTH
WINDOW_HEIGHT = MAP_PIXEL_HEIGHT + UI_HEIGHT

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TILESET_PATH = os.path.join(BASE_DIR, "tileset.png")

# 0 = 床, 1 = 壁
MAP_DATA = [
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,0,1,1,0,0,0,1,1,1,0,0,0,0,1],
    [1,0,1,0,0,1,0,0,0,1,0,1,1,0,1],
    [1,0,0,0,1,1,0,1,0,0,0,0,1,0,1],
    [1,0,1,0,0,0,0,1,0,1,1,0,0,0,1],
    [1,0,1,0,1,0,0,0,0,0,1,0,1,0,1],
    [1,0,0,0,0,0,1,1,0,0,0,0,0,0,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
]


class SimpleDQStyleRPG:
    def __init__(self):
        pygame.init()
        pygame.display.set_caption("Simple RPG - DQ Lite")
        self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
        self.clock = pygame.time.Clock()

        # ★STEP2追加
        # プレイヤーの位置(マス単位)
        self.player_x = 1  
        self.player_y = 1  

        # ★STEP2追加        
        # タイルセット画像の読み込み
        self.load_tileset(TILESET_PATH)  

        self.running = True
        
    # ★STEP2追加
    def load_tileset(self, filename):
        sheet = pygame.image.load(filename).convert_alpha()
        sw = sheet.get_width() // 2
        sh = sheet.get_height() // 2

        grass_raw = sheet.subsurface(pygame.Rect(0,    0,  sw, sh))
        wall_raw  = sheet.subsurface(pygame.Rect(sw,   0,  sw, sh))
        hero_raw  = sheet.subsurface(pygame.Rect(0,   sh,  sw, sh))
        slime_raw = sheet.subsurface(pygame.Rect(sw,  sh,  sw, sh))

        def inner_crop(surface):
            w, h = surface.get_width(), surface.get_height()
            if w > 2 and h > 2:
                return surface.subsurface(pygame.Rect(1, 1, w - 2, h - 2))
            return surface

        grass_inner = inner_crop(grass_raw)
        wall_inner = inner_crop(wall_raw)

        # マップに並べる床と壁のタイル
        self.tile_grass = pygame.transform.scale(grass_inner, (TILE_SIZE, TILE_SIZE))
        self.tile_wall = pygame.transform.scale(wall_inner, (TILE_SIZE, TILE_SIZE))

        # フィールド上に表示するプレイヤーキャラ
        self.player_sprite = pygame.transform.scale(hero_raw, (TILE_SIZE, TILE_SIZE))

        # バトル用スプライト(このSTEPではまだ未使用)
        self.player_battle_sprite = pygame.transform.scale(hero_raw, (64, 64))
        self.enemy_battle_sprite = pygame.transform.scale(slime_raw, (80, 80))

    # ★STEP2追加
    def draw_field(self):
        # 背景色(緑系)で画面を塗りつぶす
        self.screen.fill((0, 96, 0))

        # マップデータをもとに床・壁タイルを敷き詰める
        for y in range(MAP_HEIGHT):
            for x in range(MAP_WIDTH):
                tile = MAP_DATA[y][x]
                img = self.tile_wall if tile == 1 else self.tile_grass
                self.screen.blit(img, (x * TILE_SIZE, y * TILE_SIZE))

        # プレイヤーを現在位置に描画する
        px = self.player_x * TILE_SIZE
        py = self.player_y * TILE_SIZE
        self.screen.blit(self.player_sprite, (px, py))

    def run(self):
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False

            # フィールドを描画する  ★STEP2変更
            self.draw_field()

            # 画面更新
            pygame.display.flip()

            # 60FPSに保つ
            self.clock.tick(60)

        pygame.quit()


if __name__ == "__main__":
    game = SimpleDQStyleRPG()
    game.run()

手順①:タイルセット画像を読み込んで、床・壁・プレイヤーを切り出す

    def __init__(self):
        ...
        # プレイヤーの位置(マス単位)
        self.player_x = 1
        self.player_y = 1

        # タイルセット画像の読み込み
        self.load_tileset(TILESET_PATH)
        ...

まずは、画像ファイル「tileset.png」から、床・壁・プレイヤーなどの画像を切り出す処理を見ていきます。

  • 「self.player_x」「self.player_y」
    プレイヤーの位置を「マス単位」で管理するための変数です。
    例えば「(1,1)」なら、左から 1 マス目、上から 1 マス目という意味になります。
  • 「self.load_tileset(TILESET_PATH)」
    後で定義している「load_tileset」というメソッドを呼び出し、画像を読み込んでタイル用の画像やプレイヤーのスプライトを準備しています。
    def load_tileset(self, filename):  
        sheet = pygame.image.load(filename).convert_alpha()
        sw = sheet.get_width() // 2
        sh = sheet.get_height() // 2

        grass_raw = sheet.subsurface(pygame.Rect(0,    0,  sw, sh))
        wall_raw  = sheet.subsurface(pygame.Rect(sw,   0,  sw, sh))
        hero_raw  = sheet.subsurface(pygame.Rect(0,   sh,  sw, sh))
        slime_raw = sheet.subsurface(pygame.Rect(sw,  sh,  sw, sh))

続いて、その「load_tileset」メソッドの中身です。

  • 「pygame.image.load(filename)」
    指定したファイル名の画像を読み込みます。
  • 「convert_alpha()」
    透明なピクセルを正しく扱えるようにするための変換です。
  • 「sw」「sh」
    画像を縦横 2 分割するために、横幅と高さを半分にした値です。
  • 「subsurface」
    元の画像の一部だけを切り出す機能です。
    ここでは、
    ・左上:床(grass_raw)
    ・右上:壁(wall_raw)
    ・左下:プレイヤー(hero_raw)
    ・右下:敵(slime_raw)
    として切り出しています。
        def inner_crop(surface):
            w, h = surface.get_width(), surface.get_height()
            if w > 2 and h > 2:
                return surface.subsurface(pygame.Rect(1, 1, w - 2, h - 2))
            return surface

        grass_inner = inner_crop(grass_raw)
        wall_inner = inner_crop(wall_raw)

        # マップに並べる床と壁のタイル
        self.tile_grass = pygame.transform.scale(grass_inner, (TILE_SIZE, TILE_SIZE))
        self.tile_wall = pygame.transform.scale(wall_inner, (TILE_SIZE, TILE_SIZE))

        # フィールド上に表示するプレイヤーキャラ
        self.player_sprite = pygame.transform.scale(hero_raw, (TILE_SIZE, TILE_SIZE))

        # バトル用スプライト(このSTEPではまだ未使用)
        self.player_battle_sprite = pygame.transform.scale(hero_raw, (64, 64))
        self.enemy_battle_sprite = pygame.transform.scale(slime_raw, (80, 80))

次に、少しだけ内側をトリミングしてから、実際に使うサイズに拡大・縮小しています。

  • 「inner_crop」
    画像の外側 1 ピクセル分を削って、少しだけ内側の部分だけを使うための関数です。タイル画像のフチに余白があると、並べたときに線が見えてしまうことがあるための調整です。
  • 「pygame.transform.scale(…, (TILE_SIZE, TILE_SIZE))」
    タイルの大きさに合わせてリサイズしています。
  • 「self.player_sprite」
    フィールド画面に表示するプレイヤー用のスプライトです。
  • 「player_battle_sprite」「enemy_battle_sprite」
    バトル画面用の大きめスプライトです。この STEP ではまだ使いません。

手順②:マップデータをもとにタイルを敷き詰め、プレイヤーを描画する

    def draw_field(self):  
        # 背景色(緑系)で画面を塗りつぶす
        self.screen.fill((0, 96, 0))

        # マップデータをもとに床・壁タイルを敷き詰める
        for y in range(MAP_HEIGHT):
            for x in range(MAP_WIDTH):
                tile = MAP_DATA[y][x]
                img = self.tile_wall if tile == 1 else self.tile_grass
                self.screen.blit(img, (x * TILE_SIZE, y * TILE_SIZE))

        # プレイヤーを現在位置に描画する
        px = self.player_x * TILE_SIZE
        py = self.player_y * TILE_SIZE
        self.screen.blit(self.player_sprite, (px, py))

次に、フィールド画面を描画している部分です。ここでやっていることは、大きく 3 つです。

  1. 背景を塗る
  2. マップデータに応じて床・壁タイルを並べる
  3. プレイヤーを現在位置に描く

「for y in range(MAP_HEIGHT)」「for x in range(MAP_WIDTH)」の二重ループで、マップの縦横すべてのマスを埋めています。

  • 「tile = MAP_DATA[y][x]」
    マップデータの「(x,y) の位置が床か壁か」を取り出しています。
  • 「img = self.tile_wall if tile == 1 else self.tile_grass」
    もし値が 1 なら壁タイル、それ以外(0)なら床タイルを選びます。
  • 「self.screen.blit(img, (x * TILE_SIZE, y * TILE_SIZE))」
    タイル画像を、画面上の「(x マス目, y マス目)」に描画します。

最後に、プレイヤーを描きます。

  • 「px = self.player_x * TILE_SIZE」「py = self.player_y * TILE_SIZE」
    プレイヤーのマス座標をピクセル座標に変換しています。
  • 「self.screen.blit(self.player_sprite, (px, py))」
    プレイヤー画像をその位置に描画します。
    これによって、マップ上のマスにキャラクターが立っているように見えます。

手順③:メインループからフィールド描画を呼び出す

    def run(self):  
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False

            # フィールドを描画する
            self.draw_field()  

            # 画面更新
            pygame.display.flip()

            # 60FPS(1秒間に60回更新)に保つ
            self.clock.tick(60)

        pygame.quit()

最後に、メインループ内で「draw_field」を呼び出すように変更しています。

これで、ゲームの見た目としては「フィールド上に立っている主人公」が確認できる状態になりました。

STEP3:プレイヤーを矢印キーで移動できるようにする

この STEP3 では、キーボードの矢印キーでプレイヤーを上下左右に動かせるようにします。あわせて、「マップの外には出ない」「壁にはめり込まない」といった当たり判定も入れていきます。

import pygame
import random
import os

TILE_SIZE = 32
MAP_WIDTH = 15
MAP_HEIGHT = 10

MAP_PIXEL_HEIGHT = TILE_SIZE * MAP_HEIGHT
UI_HEIGHT = 96

WINDOW_WIDTH = TILE_SIZE * MAP_WIDTH
WINDOW_HEIGHT = MAP_PIXEL_HEIGHT + UI_HEIGHT

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TILESET_PATH = os.path.join(BASE_DIR, "tileset.png")

# 0 = 床, 1 = 壁
MAP_DATA = [
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,0,1,1,0,0,0,1,1,1,0,0,0,0,1],
    [1,0,1,0,0,1,0,0,0,1,0,1,1,0,1],
    [1,0,0,0,1,1,0,1,0,0,0,0,1,0,1],
    [1,0,1,0,0,0,0,1,0,1,1,0,0,0,1],
    [1,0,1,0,1,0,0,0,0,0,1,0,1,0,1],
    [1,0,0,0,0,0,1,1,0,0,0,0,0,0,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
]


class SimpleDQStyleRPG:
    def __init__(self):  
        pygame.init()
        pygame.display.set_caption("Simple RPG - DQ Lite")
        self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
        self.clock = pygame.time.Clock()
        
        # ★STEP3変更
        self.player_x = 1  # マス座標(X)
        self.player_y = 1  # マス座標(Y)
        self.steps = 0     # 歩いた回数を数えるカウンタ(後で戦闘にも使う)
        self.state = "field"  # 今はフィールド状態だけを扱う

        self.load_tileset(TILESET_PATH)

        self.running = True

    def load_tileset(self, filename):
        sheet = pygame.image.load(filename).convert_alpha()
        sw = sheet.get_width() // 2
        sh = sheet.get_height() // 2

        grass_raw = sheet.subsurface(pygame.Rect(0,    0,  sw, sh))
        wall_raw  = sheet.subsurface(pygame.Rect(sw,   0,  sw, sh))
        hero_raw  = sheet.subsurface(pygame.Rect(0,   sh,  sw, sh))
        slime_raw = sheet.subsurface(pygame.Rect(sw,  sh,  sw, sh))

        def inner_crop(surface):
            w, h = surface.get_width(), surface.get_height()
            if w > 2 and h > 2:
                return surface.subsurface(pygame.Rect(1, 1, w - 2, h - 2))
            return surface

        grass_inner = inner_crop(grass_raw)
        wall_inner = inner_crop(wall_raw)

        self.tile_grass = pygame.transform.scale(grass_inner, (TILE_SIZE, TILE_SIZE))
        self.tile_wall = pygame.transform.scale(wall_inner, (TILE_SIZE, TILE_SIZE))

        self.player_sprite = pygame.transform.scale(hero_raw, (TILE_SIZE, TILE_SIZE))
        self.player_battle_sprite = pygame.transform.scale(hero_raw, (64, 64))
        self.enemy_battle_sprite = pygame.transform.scale(slime_raw, (80, 80))

    def draw_field(self):
        self.screen.fill((0, 96, 0))

        for y in range(MAP_HEIGHT):
            for x in range(MAP_WIDTH):
                tile = MAP_DATA[y][x]
                img = self.tile_wall if tile == 1 else self.tile_grass
                self.screen.blit(img, (x * TILE_SIZE, y * TILE_SIZE))

        px = self.player_x * TILE_SIZE
        py = self.player_y * TILE_SIZE
        self.screen.blit(self.player_sprite, (px, py))

    # ★STEP3追加
    def try_move(self, dx, dy):  
        nx = self.player_x + dx
        ny = self.player_y + dy

        # マップの範囲外なら動かさない
        if not (0 <= nx < MAP_WIDTH and 0 <= ny < MAP_HEIGHT):
            return

        # 壁(1)のマスには進めない
        if MAP_DATA[ny][nx] == 1:
            return

        # 床(0)のマスなら移動する
        self.player_x = nx
        self.player_y = ny

        # 歩数カウント(後のSTEPで戦闘開始判定に使う)
        self.steps += 1

    def run(self):  # ★STEP3変更
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False

                # ★STEP3追加
                if event.type == pygame.KEYDOWN:
                    if self.state == "field":
                        if event.key == pygame.K_UP:
                            self.try_move(0, -1)
                        elif event.key == pygame.K_DOWN:
                            self.try_move(0, 1)
                        elif event.key == pygame.K_LEFT:
                            self.try_move(-1, 0)
                        elif event.key == pygame.K_RIGHT:
                            self.try_move(1, 0)

            self.draw_field()

            pygame.display.flip()
            self.clock.tick(60)

        pygame.quit()


if __name__ == "__main__":
    game = SimpleDQStyleRPG()
    game.run()

手順①:プレイヤーの位置管理と「歩く処理」を用意する

    def __init__(self):  
        ...
        self.player_x = 1  # マス座標(X)
        self.player_y = 1  # マス座標(Y)
        self.steps = 0     # 歩いた回数を数えるカウンタ(後で戦闘にも使う)
        self.state = "field"  # 今はフィールド状態だけを扱う
        ...

まずは、プレイヤーの位置や歩数を管理するための変数と、移動処理用メソッドを追加します。

  • 「self.player_x」「self.player_y」
    プレイヤーの位置を「タイル(マス)単位」で管理します。
  • 「self.steps」
    何歩あるいたかを数えるカウンタです。
    今の STEP では画面には出しませんが、後の STEP で「一定歩数ごとに戦闘開始」といった判定に使います。
  • 「self.state」
    ゲーム全体の状態を表す変数です。
    今は「field(フィールド画面)」しか使いませんが、今後「戦闘画面」「ゲームオーバー画面」なども追加していきます。
    def try_move(self, dx, dy):
        nx = self.player_x + dx
        ny = self.player_y + dy

        # マップの範囲外なら動かさない
        if not (0 <= nx < MAP_WIDTH and 0 <= ny < MAP_HEIGHT):
            return

        # 壁(1)のマスには進めない
        if MAP_DATA[ny][nx] == 1:
            return

        # 床(0)のマスなら移動する
        self.player_x = nx
        self.player_y = ny

        # 歩数カウント(後のSTEPで戦闘開始判定に使う)
        self.steps += 1

続いて、実際の移動処理を行う「try_move」メソッドです。ここで「マップ外に出ない」「壁に入らない」という基本的な移動ルールを整えます。

  1. 「行きたい方向」を受け取る
    • 引数「dx」「dy」は、X・Y 方向の変化量です。
    • 例えば「上に1マス」なら (0, -1)、「右に1マス」なら (1, 0) のように指定します。
  2. 行き先のマス座標を計算する
    • 「nx」「ny」は「次に行こうとしているマスの座標」です。
  3. マップ外かどうかを判定する
    • 「0 <= nx < MAP_WIDTH」「0 <= ny < MAP_HEIGHT」を使って、マップの範囲内かどうかをチェックしています。
    • どちらかが範囲外なら、そのまま return して「動かない」ようにします。
  4. 壁かどうかを判定する
    • 「MAP_DATA[ny][nx]」で、そのマスが床(0)か壁(1)かを取得します。
    • もし 1(壁)なら return して動きません。
  5. 問題なければ座標を更新して歩数を増やす
    • 「self.player_x」「self.player_y」を新しい座標に更新します。
    • 「self.steps += 1」で、歩いた回数を1増やしておきます。

手順②:キーボード入力から移動処理を呼び出す

    def run(self):  
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False

                # ★STEP3追加
                if event.type == pygame.KEYDOWN:
                    if self.state == "field":
                        if event.key == pygame.K_UP:
                            self.try_move(0, -1)
                        elif event.key == pygame.K_DOWN:
                            self.try_move(0, 1)
                        elif event.key == pygame.K_LEFT:
                            self.try_move(-1, 0)
                        elif event.key == pygame.K_RIGHT:
                            self.try_move(1, 0)

            self.draw_field()

            pygame.display.flip()
            self.clock.tick(60)

        pygame.quit()

次に、キーボードの矢印キーを押したときに「try_move」を呼び出すよう、メインループを変更しています。

  1. 「KEYDOWN」イベントのチェック
    • 「event.type == pygame.KEYDOWN」で「何かキーが押された瞬間」を検出しています。
  2. 「state」が「field」のときだけ移動を許可
    • 「if self.state == “field”:」で、今がフィールド画面のときだけ矢印キーを受け付けるようにしています。
    • 後で戦闘画面やゲームオーバー画面を追加したときに、そこで矢印キーを押してもプレイヤーが動かないようにするための準備です。
  3. 押されたキーに応じて「try_move」を呼び出す
    • 上キー:K_UP → try_move(0, -1)
    • 下キー:K_DOWN → try_move(0, 1)
    • 左キー:K_LEFT → try_move(-1, 0)
    • 右キー:K_RIGHT → try_move(1, 0)

この段階で実行すると、

  • 矢印キー(↑↓←→)に合わせて、プレイヤーがマス単位で移動する
  • 壁のマスには入れない
  • マップの外側にも出ていかない

という動きが確認できます。

STEP4:メッセージウィンドウを表示する

この STEP4 では、画面下に「メッセージウィンドウ」を表示し、

  • 最初の案内メッセージ
  • 歩いた回数の表示

を出せるようにしていきます。

import pygame
import random
import os

TILE_SIZE = 32
MAP_WIDTH = 15
MAP_HEIGHT = 10

MAP_PIXEL_HEIGHT = TILE_SIZE * MAP_HEIGHT
UI_HEIGHT = 96

WINDOW_WIDTH = TILE_SIZE * MAP_WIDTH
WINDOW_HEIGHT = MAP_PIXEL_HEIGHT + UI_HEIGHT

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TILESET_PATH = os.path.join(BASE_DIR, "tileset.png")

# 0 = 床, 1 = 壁
MAP_DATA = [
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,0,1,1,0,0,0,1,1,1,0,0,0,0,1],
    [1,0,1,0,0,1,0,0,0,1,0,1,1,0,1],
    [1,0,0,0,1,1,0,1,0,0,0,0,1,0,1],
    [1,0,1,0,0,0,0,1,0,1,1,0,0,0,1],
    [1,0,1,0,1,0,0,0,0,0,1,0,1,0,1],
    [1,0,0,0,0,0,1,1,0,0,0,0,0,0,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
]


class SimpleDQStyleRPG:
    def __init__(self):
        pygame.init()
        pygame.display.set_caption("Simple RPG - DQ Lite")
        self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
        self.clock = pygame.time.Clock()

        # ★STEP4追加
        # 文字描画用フォント
        self.font = pygame.font.SysFont("msgothic", 18)       
        self.font_small = pygame.font.SysFont("msgothic", 14) 

        # プレイヤーの位置や状態
        self.player_x = 1
        self.player_y = 1
        self.steps = 0
        self.state = "field"

        # ★STEP4追加
        # メッセージウィンドウに表示する文字列
        self.message = "矢印キーで移動"  

        self.load_tileset(TILESET_PATH)

        self.running = True

    def load_tileset(self, filename):
        sheet = pygame.image.load(filename).convert_alpha()
        sw = sheet.get_width() // 2
        sh = sheet.get_height() // 2

        grass_raw = sheet.subsurface(pygame.Rect(0,    0,  sw, sh))
        wall_raw  = sheet.subsurface(pygame.Rect(sw,   0,  sw, sh))
        hero_raw  = sheet.subsurface(pygame.Rect(0,   sh,  sw, sh))
        slime_raw = sheet.subsurface(pygame.Rect(sw,  sh,  sw, sh))

        def inner_crop(surface):
            w, h = surface.get_width(), surface.get_height()
            if w > 2 and h > 2:
                return surface.subsurface(pygame.Rect(1, 1, w - 2, h - 2))
            return surface

        grass_inner = inner_crop(grass_raw)
        wall_inner = inner_crop(wall_raw)

        self.tile_grass = pygame.transform.scale(grass_inner, (TILE_SIZE, TILE_SIZE))
        self.tile_wall = pygame.transform.scale(wall_inner, (TILE_SIZE, TILE_SIZE))

        self.player_sprite = pygame.transform.scale(hero_raw, (TILE_SIZE, TILE_SIZE))
        self.player_battle_sprite = pygame.transform.scale(hero_raw, (64, 64))
        self.enemy_battle_sprite = pygame.transform.scale(slime_raw, (80, 80))

    # ★STEP4追加
    def set_message(self, text: str):  
        self.message = text

    # ★STEP4追加
    def draw_text_multiline(self, surface, text, x, y, font, color, max_width):
        line = ""
        for ch in text:
            if ch == "\n":
                if line:
                    rendered = font.render(line, True, color)
                    surface.blit(rendered, (x, y))
                    y += rendered.get_height()
                    line = ""
                else:
                    y += font.get_height()
                continue

            test_line = line + ch
            w, h = font.size(test_line)
            if w > max_width and line:
                rendered = font.render(line, True, color)
                surface.blit(rendered, (x, y))
                y += h
                line = ch
            else:
                line = test_line

        if line:
            rendered = font.render(line, True, color)
            surface.blit(rendered, (x, y))

    # ★STEP4追加
    def draw_message_window(self):
        h = 64
        rect = pygame.Rect(
            8,
            WINDOW_HEIGHT - h - 8,
            WINDOW_WIDTH - 16,
            h,
        )
        pygame.draw.rect(self.screen, (0, 0, 64), rect)
        pygame.draw.rect(self.screen, (192, 192, 255), rect, 2)

        max_width = rect.width - 24
        self.draw_text_multiline(
            self.screen,
            self.message,
            rect.x + 12,
            rect.y + 12,
            self.font_small,
            (255, 255, 255),
            max_width,
        )

    def draw_field(self):
        self.screen.fill((0, 96, 0))

        for y in range(MAP_HEIGHT):
            for x in range(MAP_WIDTH):
                tile = MAP_DATA[y][x]
                img = self.tile_wall if tile == 1 else self.tile_grass
                self.screen.blit(img, (x * TILE_SIZE, y * TILE_SIZE))

        px = self.player_x * TILE_SIZE
        py = self.player_y * TILE_SIZE
        self.screen.blit(self.player_sprite, (px, py))

        # ★STEP4追加
        # 画面下部にメッセージウィンドウを表示
        self.draw_message_window()

    def try_move(self, dx, dy):
        nx = self.player_x + dx
        ny = self.player_y + dy

        # マップの範囲外なら動かさない
        if not (0 <= nx < MAP_WIDTH and 0 <= ny < MAP_HEIGHT):
            return

        # 壁(1)のマスには進めない
        if MAP_DATA[ny][nx] == 1:
            return

        # 床(0)のマスなら移動する
        self.player_x = nx
        self.player_y = ny

        # 歩数カウント
        self.steps += 1

        # ★STEP4追加
        # メッセージに歩いた回数を表示する
        self.set_message(f"あるいた かず:{self.steps}")

    def run(self):
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False

                if event.type == pygame.KEYDOWN:
                    if self.state == "field":
                        if event.key == pygame.K_UP:
                            self.try_move(0, -1)
                        elif event.key == pygame.K_DOWN:
                            self.try_move(0, 1)
                        elif event.key == pygame.K_LEFT:
                            self.try_move(-1, 0)
                        elif event.key == pygame.K_RIGHT:
                            self.try_move(1, 0)

            self.draw_field()

            pygame.display.flip()
            self.clock.tick(60)

        pygame.quit()


if __name__ == "__main__":
    game = SimpleDQStyleRPG()
    game.run()

手順①:フォントとメッセージ用の変数を用意する

    def __init__(self):
        ...
        # 文字描画用フォント
        self.font = pygame.font.SysFont("msgothic", 18)
        self.font_small = pygame.font.SysFont("msgothic", 14)

        # プレイヤーの位置や状態
        self.player_x = 1
        self.player_y = 1
        self.steps = 0
        self.state = "field"

        # メッセージウィンドウに表示する文字列
        self.message = "矢印キーで移動"
        ...

まずは、文字を描画するためのフォントと、表示する文章を保存するための変数を追加しました。

  • 「pygame.font.SysFont」
    フォント名を指定して、文字描画用のフォントオブジェクトを作る関数です。
  • 「self.font」「self.font_small」
    大きめの文字と、小さめの文字を使い分けられるように二種類用意しています。
  • 「self.message」
    現在のメッセージウィンドウに表示する文章を保存しておくための変数です。
    def set_message(self, text: str):
        self.message = text

メッセージを書き換えるためのメソッドも定義しています。

手順②:複数行の文字描画とメッセージウィンドウを作る

    def draw_text_multiline(self, surface, text, x, y, font, color, max_width): 
        line = ""
        for ch in text:
            if ch == "\n":
                ...
            test_line = line + ch
            w, h = font.size(test_line)
            if w > max_width and line:
                ...
            else:
                line = test_line

        if line:
            ...

次に、メッセージをウィンドウに表示する処理を見ていきます。

  • 引数「text」
    描画したい全文字列です。途中で「\n」が含まれていると、そこで改行を行います。
  • 「max_width」
    1行の最大幅です。この幅を超えそうになったら、その手前で自動的に改行します。
    def draw_message_window(self):
        h = 64
        rect = pygame.Rect(
            8,
            WINDOW_HEIGHT - h - 8,
            WINDOW_WIDTH - 16,
            h,
        )
        pygame.draw.rect(self.screen, (0, 0, 64), rect)
        pygame.draw.rect(self.screen, (192, 192, 255), rect, 2)

        max_width = rect.width - 24
        self.draw_text_multiline(
            self.screen,
            self.message,
            rect.x + 12,
            rect.y + 12,
            self.font_small,
            (255, 255, 255),
            max_width,
        )

実際のメッセージウィンドウを描画しているのが「draw_message_window」です。

  1. メッセージウィンドウ用の四角形の位置と大きさを決める
  2. 「背景色の四角」と「枠線の四角」を描画する
  3. その中に「self.message」の文字列を描画する

手順③:フィールド描画の中でメッセージウィンドウを呼び出す

    def draw_field(self):
        self.screen.fill((0, 96, 0))

        for y in range(MAP_HEIGHT):
            for x in range(MAP_WIDTH):
                tile = MAP_DATA[y][x]
                img = self.tile_wall if tile == 1 else self.tile_grass
                self.screen.blit(img, (x * TILE_SIZE, y * TILE_SIZE))

        px = self.player_x * TILE_SIZE
        py = self.player_y * TILE_SIZE
        self.screen.blit(self.player_sprite, (px, py))

        # 画面下部にメッセージウィンドウを表示
        self.draw_message_window()

最後に、フィールド描画の中でメッセージウィンドウを重ね描きするように変更しています。

  1. 背景(フィールド全体)を描く
  2. プレイヤーを描く
  3. 一番手前にメッセージウィンドウを描く

というレイヤー構造になっています。

    def try_move(self, dx, dy):
        ...
        # 床(0)のマスなら移動する
        self.player_x = nx
        self.player_y = ny

        # 歩数カウント
        self.steps += 1

        # メッセージに歩いた回数を表示する
        self.set_message(f"あるいた かず:{self.steps}")

さらに、歩いたときにメッセージを更新するよう「try_move」も少し変更しています。

この段階で実行すると、

  • フィールドとプレイヤーに加えて、画面下に青いメッセージウィンドウが表示される
  • 最初は「矢印キーで移動」と表示される
  • 歩くたびに「あるいた かず:○」と歩数が更新される

という動きが確認できるはずです

STEP5:戦闘画面の基本表示を作る

この STEP5 では、

  • プレイヤーと敵の HP を用意する
  • 戦闘画面を描画する
  • 一定歩数ごとに戦闘画面に切り替える

ところまでを実装します。

import pygame
import random
import os

TILE_SIZE = 32
MAP_WIDTH = 15
MAP_HEIGHT = 10

MAP_PIXEL_HEIGHT = TILE_SIZE * MAP_HEIGHT
UI_HEIGHT = 96

WINDOW_WIDTH = TILE_SIZE * MAP_WIDTH
WINDOW_HEIGHT = MAP_PIXEL_HEIGHT + UI_HEIGHT

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TILESET_PATH = os.path.join(BASE_DIR, "tileset.png")

# 0 = 床, 1 = 壁
MAP_DATA = [
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,0,1,1,0,0,0,1,1,1,0,0,0,0,1],
    [1,0,1,0,0,1,0,0,0,1,0,1,1,0,1],
    [1,0,0,0,1,1,0,1,0,0,0,0,1,0,1],
    [1,0,1,0,0,0,0,1,0,1,1,0,0,0,1],
    [1,0,1,0,1,0,0,0,0,0,1,0,1,0,1],
    [1,0,0,0,0,0,1,1,0,0,0,0,0,0,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
]


class SimpleDQStyleRPG:
    def __init__(self):
        pygame.init()
        pygame.display.set_caption("Simple RPG - DQ Lite")
        self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
        self.clock = pygame.time.Clock()

        self.font = pygame.font.SysFont("msgothic", 18)
        self.font_small = pygame.font.SysFont("msgothic", 14)

        self.player_x = 1
        self.player_y = 1
        self.steps = 0
        self.state = "field"

        self.message = "矢印キーで移動"

        self.load_tileset(TILESET_PATH)

        # ★STEP5追加
        # プレイヤーと敵の HP の上限・現在値
        self.player_max_hp = 30  
        self.enemy_max_hp = 28 
        self.player_hp = self.player_max_hp
        self.enemy_hp = self.enemy_max_hp

        self.running = True

    def load_tileset(self, filename):
        sheet = pygame.image.load(filename).convert_alpha()
        sw = sheet.get_width() // 2
        sh = sheet.get_height() // 2

        grass_raw = sheet.subsurface(pygame.Rect(0,    0,  sw, sh))
        wall_raw  = sheet.subsurface(pygame.Rect(sw,   0,  sw, sh))
        hero_raw  = sheet.subsurface(pygame.Rect(0,   sh,  sw, sh))
        slime_raw = sheet.subsurface(pygame.Rect(sw,  sh,  sw, sh))

        def inner_crop(surface):
            w, h = surface.get_width(), surface.get_height()
            if w > 2 and h > 2:
                return surface.subsurface(pygame.Rect(1, 1, w - 2, h - 2))
            return surface

        grass_inner = inner_crop(grass_raw)
        wall_inner = inner_crop(wall_raw)

        self.tile_grass = pygame.transform.scale(grass_inner, (TILE_SIZE, TILE_SIZE))
        self.tile_wall = pygame.transform.scale(wall_inner, (TILE_SIZE, TILE_SIZE))

        self.player_sprite = pygame.transform.scale(hero_raw, (TILE_SIZE, TILE_SIZE))
        self.player_battle_sprite = pygame.transform.scale(hero_raw, (64, 64))
        self.enemy_battle_sprite = pygame.transform.scale(slime_raw, (80, 80))

    def set_message(self, text: str):
        self.message = text

    def draw_text_multiline(self, surface, text, x, y, font, color, max_width):
        line = ""
        for ch in text:
            if ch == "\n":
                if line:
                    rendered = font.render(line, True, color)
                    surface.blit(rendered, (x, y))
                    y += rendered.get_height()
                    line = ""
                else:
                    y += font.get_height()
                continue

            test_line = line + ch
            w, h = font.size(test_line)
            if w > max_width and line:
                rendered = font.render(line, True, color)
                surface.blit(rendered, (x, y))
                y += h
                line = ch
            else:
                line = test_line

        if line:
            rendered = font.render(line, True, color)
            surface.blit(rendered, (x, y))

    def draw_message_window(self):
        h = 64
        rect = pygame.Rect(
            8,
            WINDOW_HEIGHT - h - 8,
            WINDOW_WIDTH - 16,
            h,
        )
        pygame.draw.rect(self.screen, (0, 0, 64), rect)
        pygame.draw.rect(self.screen, (192, 192, 255), rect, 2)

        max_width = rect.width - 24
        self.draw_text_multiline(
            self.screen,
            self.message,
            rect.x + 12,
            rect.y + 12,
            self.font_small,
            (255, 255, 255),
            max_width,
        )

    def draw_field(self):
        self.screen.fill((0, 96, 0))

        for y in range(MAP_HEIGHT):
            for x in range(MAP_WIDTH):
                tile = MAP_DATA[y][x]
                img = self.tile_wall if tile == 1 else self.tile_grass
                self.screen.blit(img, (x * TILE_SIZE, y * TILE_SIZE))

        px = self.player_x * TILE_SIZE
        py = self.player_y * TILE_SIZE
        self.screen.blit(self.player_sprite, (px, py))

        self.draw_message_window()

    # ★STEP5追加
    def draw_battle(self):  
        self.screen.fill((8, 8, 40))

        status_rect = pygame.Rect(8, 8, WINDOW_WIDTH - 16, 80)
        pygame.draw.rect(self.screen, (0, 0, 64), status_rect)
        pygame.draw.rect(self.screen, (192, 192, 255), status_rect, 2)

        text_p = self.font_small.render(
            f"▶ プレイヤー  HP {self.player_hp}/{self.player_max_hp}",
            True, (255, 255, 255)
        )
        text_e = self.font_small.render(
            f"   てき       HP {self.enemy_hp}/{self.enemy_max_hp}",
            True, (255, 255, 255)
        )
        self.screen.blit(text_p, (status_rect.x + 12, status_rect.y + 10))
        self.screen.blit(text_e, (status_rect.x + 12, status_rect.y + 36))

        enemy_x = WINDOW_WIDTH // 2 - self.enemy_battle_sprite.get_width() // 2
        enemy_y = 110
        self.screen.blit(self.enemy_battle_sprite, (enemy_x, enemy_y))

        self.draw_message_window()

    # ★STEP5追加
    def start_battle(self):
        self.state = "battle"
        self.player_hp = self.player_max_hp
        self.enemy_hp = self.enemy_max_hp
        self.set_message("てきが あらわれた! Aキーでこうげき / Rキーでにげる")

    def try_move(self, dx, dy):
        nx = self.player_x + dx
        ny = self.player_y + dy

        if not (0 <= nx < MAP_WIDTH and 0 <= ny < MAP_HEIGHT):
            return

        if MAP_DATA[ny][nx] == 1:
            return

        self.player_x = nx
        self.player_y = ny

        self.steps += 1

        self.set_message(f"あるいた かず:{self.steps}")

        # ★STEP5追加
        # 一定歩数ごとに戦闘開始
        if self.steps % 7 == 0:
            self.start_battle()

    def run(self):  
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False

                if event.type == pygame.KEYDOWN:
                    if self.state == "field":
                        if event.key == pygame.K_UP:
                            self.try_move(0, -1)
                        elif event.key == pygame.K_DOWN:
                            self.try_move(0, 1)
                        elif event.key == pygame.K_LEFT:
                            self.try_move(-1, 0)
                        elif event.key == pygame.K_RIGHT:
                            self.try_move(1, 0)

            # ★STEP5変更
            # 状態に応じて描画内容を切り替える
            if self.state == "field":
                self.draw_field()
            elif self.state == "battle":
                self.draw_battle()

            pygame.display.flip()
            self.clock.tick(60)

        pygame.quit()


if __name__ == "__main__":
    game = SimpleDQStyleRPG()
    game.run()

手順①:プレイヤーと敵のHPを管理する変数を追加する

    def __init__(self):
        ...
        self.load_tileset(TILESET_PATH)

        # プレイヤーと敵の HP の上限・現在値
        self.player_max_hp = 30 
        self.enemy_max_hp = 28
        self.player_hp = self.player_max_hp 
        self.enemy_hp = self.enemy_max_hp

        self.running = True

まずは、戦闘で必要になる「HP(体力)」を管理するための変数を追加します。

  • 「player_max_hp」「enemy_max_hp」
    それぞれプレイヤーと敵の「最大HP」を表します。
  • 「player_hp」「enemy_hp」
    現在の HP を表します。

手順②:戦闘画面のレイアウトを描画する

    def draw_battle(self):
        self.screen.fill((8, 8, 40))

        status_rect = pygame.Rect(8, 8, WINDOW_WIDTH - 16, 80)
        pygame.draw.rect(self.screen, (0, 0, 64), status_rect)
        pygame.draw.rect(self.screen, (192, 192, 255), status_rect, 2)

        text_p = self.font_small.render(
            f"▶ プレイヤー  HP {self.player_hp}/{self.player_max_hp}",
            True, (255, 255, 255)
        )
        text_e = self.font_small.render(
            f"   てき       HP {self.enemy_hp}/{self.enemy_max_hp}",
            True, (255, 255, 255)
        )
        self.screen.blit(text_p, (status_rect.x + 12, status_rect.y + 10))
        self.screen.blit(text_e, (status_rect.x + 12, status_rect.y + 36))

        enemy_x = WINDOW_WIDTH // 2 - self.enemy_battle_sprite.get_width() // 2
        enemy_y = 110
        self.screen.blit(self.enemy_battle_sprite, (enemy_x, enemy_y))

        self.draw_message_window()

次に、戦闘時の画面を描画する「draw_battle」と、戦闘開始時に呼び出す「start_battle」を追加しました。

戦闘画面では、以下の構成にしています。

  • 画面全体の背景を暗い色にする
  • 上部に「ステータス用のウィンドウ」を描く
  • その中に、プレイヤーと敵の HP を「現在値/最大値」の形式で表示する
  • 中央には敵キャラクターを大きく描画する
  • 画面下部には、メッセージウィンドウを重ねる
    def start_battle(self):
        self.state = "battle"
        self.player_hp = self.player_max_hp
        self.enemy_hp = self.enemy_max_hp
        self.set_message("てきが あらわれた! Aキーでこうげき / Rキーでにげる")

続いて「start_battle」です。

  • 「self.state = “battle”」
    現在の状態を「戦闘中」に切り替えます。
  • 「player_hp」「enemy_hp」を最大値に戻す
    戦闘が始まるたびに、プレイヤーと敵の HP を最大値にリセットしています。
  • 「self.set_message(“…”)」
    メッセージを「てきが あらわれた!」に変更しています。

手順③:フィールドから戦闘画面へ切り替える

    def try_move(self, dx, dy):
        ...
        self.player_x = nx
        self.player_y = ny

        self.steps += 1

        self.set_message(f"あるいた かず:{self.steps}")

        # 一定歩数ごとに戦闘開始
        if self.steps % 7 == 0:
            self.start_battle()

フィールドを歩いているときに「一定歩数ごとに戦闘が始まる」処理と、メインループでの描画切り替えを追加しました。

    def run(self):
        while self.running:
            ...
            # 状態に応じて描画内容を切り替える
            if self.state == "field":
                self.draw_field()
            elif self.state == "battle":
                self.draw_battle()

そして、メインループ側では次のように描画処理を切り替えています。

  • 「state」が「field」のときはフィールド画面
  • 「state」が「battle」のときは戦闘画面

というように、画面のモードを切り替えているイメージです。

この段階で実行すると、

  • フィールドを歩いていると、7 歩ごとに戦闘画面へ切り替わる
  • 戦闘画面では、プレイヤーと敵の HP が表示され、敵キャラが中央に表示される
  • 画面下には「てきが あらわれた! Aキーでこうげき / Rキーでにげる」とメッセージが出る

という状態になっています。
まだ Aキー・Rキーは動かないため、戦闘画面に入るとそれ以上は進みません。

STEP6:こうげき・にげるの戦闘ロジックを実装する

この STEP6 では、戦闘ロジックを実装していきます。

import pygame
import random
import os

TILE_SIZE = 32
MAP_WIDTH = 15
MAP_HEIGHT = 10

MAP_PIXEL_HEIGHT = TILE_SIZE * MAP_HEIGHT
UI_HEIGHT = 96

WINDOW_WIDTH = TILE_SIZE * MAP_WIDTH
WINDOW_HEIGHT = MAP_PIXEL_HEIGHT + UI_HEIGHT

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TILESET_PATH = os.path.join(BASE_DIR, "tileset.png")

# 0 = 床, 1 = 壁
MAP_DATA = [
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,0,1,1,0,0,0,1,1,1,0,0,0,0,1],
    [1,0,1,0,0,1,0,0,0,1,0,1,1,0,1],
    [1,0,0,0,1,1,0,1,0,0,0,0,1,0,1],
    [1,0,1,0,0,0,0,1,0,1,1,0,0,0,1],
    [1,0,1,0,1,0,0,0,0,0,1,0,1,0,1],
    [1,0,0,0,0,0,1,1,0,0,0,0,0,0,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
]


class SimpleDQStyleRPG:
    def __init__(self):
        pygame.init()
        pygame.display.set_caption("Simple RPG - DQ Lite")
        self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
        self.clock = pygame.time.Clock()

        self.font = pygame.font.SysFont("msgothic", 18)
        self.font_small = pygame.font.SysFont("msgothic", 14)

        self.player_x = 1
        self.player_y = 1
        self.steps = 0
        self.state = "field"

        self.message = "矢印キーで移動"

        self.load_tileset(TILESET_PATH)

        # プレイヤーと敵の HP の上限・現在値
        self.player_max_hp = 30
        self.enemy_max_hp = 28
        self.player_hp = self.player_max_hp
        self.enemy_hp = self.enemy_max_hp

        self.running = True

    def load_tileset(self, filename):
        sheet = pygame.image.load(filename).convert_alpha()
        sw = sheet.get_width() // 2
        sh = sheet.get_height() // 2

        grass_raw = sheet.subsurface(pygame.Rect(0,    0,  sw, sh))
        wall_raw  = sheet.subsurface(pygame.Rect(sw,   0,  sw, sh))
        hero_raw  = sheet.subsurface(pygame.Rect(0,   sh,  sw, sh))
        slime_raw = sheet.subsurface(pygame.Rect(sw,  sh,  sw, sh))

        def inner_crop(surface):
            w, h = surface.get_width(), surface.get_height()
            if w > 2 and h > 2:
                return surface.subsurface(pygame.Rect(1, 1, w - 2, h - 2))
            return surface

        grass_inner = inner_crop(grass_raw)
        wall_inner = inner_crop(wall_raw)

        self.tile_grass = pygame.transform.scale(grass_inner, (TILE_SIZE, TILE_SIZE))
        self.tile_wall = pygame.transform.scale(wall_inner, (TILE_SIZE, TILE_SIZE))

        self.player_sprite = pygame.transform.scale(hero_raw, (TILE_SIZE, TILE_SIZE))
        self.player_battle_sprite = pygame.transform.scale(hero_raw, (64, 64))
        self.enemy_battle_sprite = pygame.transform.scale(slime_raw, (80, 80))

    def set_message(self, text: str):
        self.message = text

    def draw_text_multiline(self, surface, text, x, y, font, color, max_width):
        line = ""
        for ch in text:
            if ch == "\n":
                if line:
                    rendered = font.render(line, True, color)
                    surface.blit(rendered, (x, y))
                    y += rendered.get_height()
                    line = ""
                else:
                    y += font.get_height()
                continue

            test_line = line + ch
            w, h = font.size(test_line)
            if w > max_width and line:
                rendered = font.render(line, True, color)
                surface.blit(rendered, (x, y))
                y += h
                line = ch
            else:
                line = test_line

        if line:
            rendered = font.render(line, True, color)
            surface.blit(rendered, (x, y))

    def draw_message_window(self):
        h = 64
        rect = pygame.Rect(
            8,
            WINDOW_HEIGHT - h - 8,
            WINDOW_WIDTH - 16,
            h,
        )
        pygame.draw.rect(self.screen, (0, 0, 64), rect)
        pygame.draw.rect(self.screen, (192, 192, 255), rect, 2)

        max_width = rect.width - 24
        self.draw_text_multiline(
            self.screen,
            self.message,
            rect.x + 12,
            rect.y + 12,
            self.font_small,
            (255, 255, 255),
            max_width,
        )

    def draw_field(self):
        self.screen.fill((0, 96, 0))

        for y in range(MAP_HEIGHT):
            for x in range(MAP_WIDTH):
                tile = MAP_DATA[y][x]
                img = self.tile_wall if tile == 1 else self.tile_grass
                self.screen.blit(img, (x * TILE_SIZE, y * TILE_SIZE))

        px = self.player_x * TILE_SIZE
        py = self.player_y * TILE_SIZE
        self.screen.blit(self.player_sprite, (px, py))

        self.draw_message_window()

    def draw_battle(self):
        self.screen.fill((8, 8, 40))

        status_rect = pygame.Rect(8, 8, WINDOW_WIDTH - 16, 80)
        pygame.draw.rect(self.screen, (0, 0, 64), status_rect)
        pygame.draw.rect(self.screen, (192, 192, 255), status_rect, 2)

        text_p = self.font_small.render(
            f"▶ プレイヤー  HP {self.player_hp}/{self.player_max_hp}",
            True, (255, 255, 255)
        )
        text_e = self.font_small.render(
            f"   てき       HP {self.enemy_hp}/{self.enemy_max_hp}",
            True, (255, 255, 255)
        )
        self.screen.blit(text_p, (status_rect.x + 12, status_rect.y + 10))
        self.screen.blit(text_e, (status_rect.x + 12, status_rect.y + 36))

        enemy_x = WINDOW_WIDTH // 2 - self.enemy_battle_sprite.get_width() // 2
        enemy_y = 110
        self.screen.blit(self.enemy_battle_sprite, (enemy_x, enemy_y))

        self.draw_message_window()

    def start_battle(self):
        self.state = "battle"
        self.player_hp = self.player_max_hp
        self.enemy_hp = self.enemy_max_hp
        self.set_message("てきが あらわれた! Aキーでこうげき / Rキーでにげる")

    # ★STEP6追加
    def battle_attack(self):  
        dmg = random.randint(4, 8)
        self.enemy_hp -= dmg
        if self.enemy_hp <= 0:
            self.enemy_hp = 0
            self.set_message(
                f"プレイヤーの こうげき! てきに {dmg} のダメージ! てきを たおした!"
            )
            self.state = "field"
            return

        self.set_message(f"プレイヤーの こうげき! {dmg} ダメージ! てきの はんげき!")
        self.enemy_attack()

    # ★STEP6追加
    def enemy_attack(self):
        dmg = random.randint(4, 9)
        self.player_hp -= dmg
        if self.player_hp <= 0:
            self.player_hp = 0
            self.set_message(
                f"てきの こうげき! {dmg} ダメージ! プレイヤーは たおれてしまった…"
            )
            self.state = "gameover"
            return

        self.set_message(
            f"てきの こうげき! {dmg} ダメージ! Aキー:こうげき / Rキー:にげる"
        )

    # ★STEP6追加
    def battle_run(self):
        if random.random() < 0.5:
            self.set_message("うまく にげきれた! フィールドにもどる。")
            self.state = "field"
            return

        dmg = random.randint(4, 9)
        self.player_hp -= dmg

        if self.player_hp <= 0:
            self.player_hp = 0
            self.set_message(
                "にげだした! …しかし まわりこまれてしまった!\n"
                f"てきの こうげき! {dmg} ダメージ! プレイヤーは たおれてしまった…"
            )
            self.state = "gameover"
        else:
            self.set_message(
                "にげだした! …しかし まわりこまれてしまった!\n"
                f"てきの こうげき! {dmg} ダメージ! Aキー:こうげき / Rキー:にげる"
            )

    def try_move(self, dx, dy):
        nx = self.player_x + dx
        ny = self.player_y + dy

        if not (0 <= nx < MAP_WIDTH and 0 <= ny < MAP_HEIGHT):
            return

        if MAP_DATA[ny][nx] == 1:
            return

        self.player_x = nx
        self.player_y = ny

        self.steps += 1

        self.set_message(f"あるいた かず:{self.steps}")

        if self.steps % 7 == 0:
            self.start_battle()

    def run(self):
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False

                if event.type == pygame.KEYDOWN:
                    if self.state == "field":
                        if event.key == pygame.K_UP:
                            self.try_move(0, -1)
                        elif event.key == pygame.K_DOWN:
                            self.try_move(0, 1)
                        elif event.key == pygame.K_LEFT:
                            self.try_move(-1, 0)
                        elif event.key == pygame.K_RIGHT:
                            self.try_move(1, 0)

                    # ★STEP6追加
                    elif self.state == "battle":
                        if event.key == pygame.K_a:
                            self.battle_attack()
                        elif event.key == pygame.K_r:
                            self.battle_run()

            if self.state == "field":
                self.draw_field()
            elif self.state == "battle":
                self.draw_battle()

            pygame.display.flip()
            self.clock.tick(60)

        pygame.quit()


if __name__ == "__main__":
    game = SimpleDQStyleRPG()
    game.run()

手順①:プレイヤーの攻撃処理(battle_attack)

    def battle_attack(self):
        dmg = random.randint(4, 8)
        self.enemy_hp -= dmg
        if self.enemy_hp <= 0:
            self.enemy_hp = 0
            self.set_message(
                f"プレイヤーの こうげき! てきに {dmg} のダメージ! てきを たおした!"
            )
            self.state = "field"
            return

        self.set_message(f"プレイヤーの こうげき! {dmg} ダメージ! てきの はんげき!")
        self.enemy_attack()

まずは、プレイヤーが敵を攻撃する処理です。流れは次のとおりです。

  1. 「random.randint(4, 8)」でダメージ量を決める
  2. 敵の HP からその分だけ引く
  3. 敵の HP が 0 以下になったか判定
  4. 0未満になったときは0に補正してから、状態を「field」に戻して「return」する。
  5. まだ HP が残っている場合は、「enemy_attack」を呼ぶ。

手順②:敵の攻撃処理(enemy_attack)と逃走処理(battle_run)

    def enemy_attack(self):
        dmg = random.randint(4, 9)
        self.player_hp -= dmg
        if self.player_hp <= 0:
            self.player_hp = 0
            self.set_message(
                f"てきの こうげき! {dmg} ダメージ! プレイヤーは たおれてしまった…"
            )
            self.state = "gameover"
            return

        self.set_message(
            f"てきの こうげき! {dmg} ダメージ! Aキー:こうげき / Rキー:にげる"
        )

次に、敵の攻撃処理です。

  • 敵のダメージ量を 4〜9 の範囲でランダムに決める。
  • プレイヤーの HP が 0 以下になったら「たおれてしまった…」メッセージを出し、状態を「gameover」に変更する。
  • まだ生きている場合は、次のプレイヤーターンの選択肢(Aキー / Rキー)をメッセージで案内する。
    def battle_run(self):
        if random.random() < 0.5:
            self.set_message("うまく にげきれた! フィールドにもどる。")
            self.state = "field"
            return

        dmg = random.randint(4, 9)
        self.player_hp -= dmg

        if self.player_hp <= 0:
            self.player_hp = 0
            self.set_message(
                "にげだした! …しかし まわりこまれてしまった!\n"
                f"てきの こうげき! {dmg} ダメージ! プレイヤーは たおれてしまった…"
            )
            self.state = "gameover"
        else:
            self.set_message(
                "にげだした! …しかし まわりこまれてしまった!\n"
                f"てきの こうげき! {dmg} ダメージ! Aキー:こうげき / Rキー:にげる"
            )

続いて、逃走処理です。

  • 「random.random() < 0.5」で、50%の確率で逃走成功としています。
  • 成功した場合はメッセージを出し、state を「field」に戻します。
  • 失敗した場合は「まわりこまれてしまった!」というメッセージとともに、敵の攻撃を受けます。
  • その攻撃で HP が 0 以下になれば gameover、それ以外なら再びプレイヤーの選択ターンに戻ります。

手順③:戦闘状態でのキー入力(Aキー・Rキー)を受け付ける

    def run(self):  # ★STEP6変更
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False

                if event.type == pygame.KEYDOWN:
                    if self.state == "field":
                        if event.key == pygame.K_UP:
                            self.try_move(0, -1)
                        elif event.key == pygame.K_DOWN:
                            self.try_move(0, 1)
                        elif event.key == pygame.K_LEFT:
                            self.try_move(-1, 0)
                        elif event.key == pygame.K_RIGHT:
                            self.try_move(1, 0)

                    elif self.state == "battle":  # ★STEP6追加
                        if event.key == pygame.K_a:
                            self.battle_attack()
                        elif event.key == pygame.K_r:
                            self.battle_run()

最後に、メインループ内で「battle 状態のときだけ A/R キーを処理する」ように変更しました。

これにより、

  1. フィールドを歩く
  2. 一定歩数ごとに戦闘画面へ
  3. 戦闘画面で Aキー or Rキー
  4. 結果に応じてフィールドへ戻る or gameover

という一連の流れが完成しました。

現時点では「gameover」状態になってもリトライ操作はありません。
「やられたら終わり」の状態です。

STEP7:ゲームオーバー画面とリスタート機能をつける

この STEP7 では、

  • ゲームオーバー専用の画面を表示する
  • SPACEキーで最初からやり直せるようにする
  • あわせて「reset_game」メソッドを用意し、初期化処理をひとまとめにする

ところまでを実装します。
ここまで出来たら完成です。

import pygame
import random
import os

TILE_SIZE = 32
MAP_WIDTH = 15
MAP_HEIGHT = 10

MAP_PIXEL_HEIGHT = TILE_SIZE * MAP_HEIGHT
UI_HEIGHT = 96

WINDOW_WIDTH = TILE_SIZE * MAP_WIDTH
WINDOW_HEIGHT = MAP_PIXEL_HEIGHT + UI_HEIGHT

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TILESET_PATH = os.path.join(BASE_DIR, "tileset.png")

# 0 = 床, 1 = 壁
MAP_DATA = [
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,0,1,1,0,0,0,1,1,1,0,0,0,0,1],
    [1,0,1,0,0,1,0,0,0,1,0,1,1,0,1],
    [1,0,0,0,1,1,0,1,0,0,0,0,1,0,1],
    [1,0,1,0,0,0,0,1,0,1,1,0,0,0,1],
    [1,0,1,0,1,0,0,0,0,0,1,0,1,0,1],
    [1,0,0,0,0,0,1,1,0,0,0,0,0,0,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
]


class SimpleDQStyleRPG:
    # ★STEP7変更
    def __init__(self):  
        pygame.init()
        pygame.display.set_caption("Simple RPG - DQ Lite")
        self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
        self.clock = pygame.time.Clock()

        self.font = pygame.font.SysFont("msgothic", 18)
        self.font_small = pygame.font.SysFont("msgothic", 14)

        self.load_tileset(TILESET_PATH)

        self.player_max_hp = 30
        self.enemy_max_hp = 28

        self.running = True
        self.reset_game()  # ★STEP7追加

    # --------------------------------
    # 初期状態に戻す
    # --------------------------------
    # ★STEP7追加  【def __init__(self):】から移動
    def reset_game(self):  
        self.player_x = 1
        self.player_y = 1
        self.player_hp = self.player_max_hp
        self.enemy_hp = self.enemy_max_hp
        self.steps = 0
        self.state = "field"
        self.message = "矢印キーで移動"

    def load_tileset(self, filename):
        sheet = pygame.image.load(filename).convert_alpha()
        sw = sheet.get_width() // 2
        sh = sheet.get_height() // 2

        grass_raw = sheet.subsurface(pygame.Rect(0,    0,  sw, sh))
        wall_raw  = sheet.subsurface(pygame.Rect(sw,   0,  sw, sh))
        hero_raw  = sheet.subsurface(pygame.Rect(0,   sh,  sw, sh))
        slime_raw = sheet.subsurface(pygame.Rect(sw,  sh,  sw, sh))

        def inner_crop(surface):
            w, h = surface.get_width(), surface.get_height()
            if w > 2 and h > 2:
                return surface.subsurface(pygame.Rect(1, 1, w - 2, h - 2))
            return surface

        grass_inner = inner_crop(grass_raw)
        wall_inner = inner_crop(wall_raw)

        self.tile_grass = pygame.transform.scale(grass_inner, (TILE_SIZE, TILE_SIZE))
        self.tile_wall = pygame.transform.scale(wall_inner, (TILE_SIZE, TILE_SIZE))

        self.player_sprite = pygame.transform.scale(hero_raw, (TILE_SIZE, TILE_SIZE))
        self.player_battle_sprite = pygame.transform.scale(hero_raw, (64, 64))
        self.enemy_battle_sprite = pygame.transform.scale(slime_raw, (80, 80))

    def set_message(self, text: str):
        self.message = text

    def draw_text_multiline(self, surface, text, x, y, font, color, max_width):
        line = ""
        for ch in text:
            if ch == "\n":
                if line:
                    rendered = font.render(line, True, color)
                    surface.blit(rendered, (x, y))
                    y += rendered.get_height()
                    line = ""
                else:
                    y += font.get_height()
                continue

            test_line = line + ch
            w, h = font.size(test_line)
            if w > max_width and line:
                rendered = font.render(line, True, color)
                surface.blit(rendered, (x, y))
                y += h
                line = ch
            else:
                line = test_line

        if line:
            rendered = font.render(line, True, color)
            surface.blit(rendered, (x, y))

    def draw_message_window(self):
        h = 64
        rect = pygame.Rect(
            8,
            WINDOW_HEIGHT - h - 8,
            WINDOW_WIDTH - 16,
            h,
        )
        pygame.draw.rect(self.screen, (0, 0, 64), rect)
        pygame.draw.rect(self.screen, (192, 192, 255), rect, 2)

        max_width = rect.width - 24
        self.draw_text_multiline(
            self.screen,
            self.message,
            rect.x + 12,
            rect.y + 12,
            self.font_small,
            (255, 255, 255),
            max_width,
        )

    def draw_field(self):
        self.screen.fill((0, 96, 0))

        for y in range(MAP_HEIGHT):
            for x in range(MAP_WIDTH):
                tile = MAP_DATA[y][x]
                img = self.tile_wall if tile == 1 else self.tile_grass
                self.screen.blit(img, (x * TILE_SIZE, y * TILE_SIZE))

        px = self.player_x * TILE_SIZE
        py = self.player_y * TILE_SIZE
        self.screen.blit(self.player_sprite, (px, py))

        self.draw_message_window()

    def draw_battle(self):
        self.screen.fill((8, 8, 40))

        status_rect = pygame.Rect(8, 8, WINDOW_WIDTH - 16, 80)
        pygame.draw.rect(self.screen, (0, 0, 64), status_rect)
        pygame.draw.rect(self.screen, (192, 192, 255), status_rect, 2)

        text_p = self.font_small.render(
            f"▶ プレイヤー  HP {self.player_hp}/{self.player_max_hp}",
            True, (255, 255, 255)
        )
        text_e = self.font_small.render(
            f"   てき       HP {self.enemy_hp}/{self.enemy_max_hp}",
            True, (255, 255, 255)
        )
        self.screen.blit(text_p, (status_rect.x + 12, status_rect.y + 10))
        self.screen.blit(text_e, (status_rect.x + 12, status_rect.y + 36))

        enemy_x = WINDOW_WIDTH // 2 - self.enemy_battle_sprite.get_width() // 2
        enemy_y = 110
        self.screen.blit(self.enemy_battle_sprite, (enemy_x, enemy_y))

        self.draw_message_window()

    def start_battle(self):
        self.state = "battle"
        self.player_hp = self.player_max_hp
        self.enemy_hp = self.enemy_max_hp
        self.set_message("てきが あらわれた! Aキーでこうげき / Rキーでにげる")

    def battle_attack(self):
        dmg = random.randint(4, 8)
        self.enemy_hp -= dmg
        if self.enemy_hp <= 0:
            self.enemy_hp = 0
            self.set_message(
                f"プレイヤーの こうげき! てきに {dmg} のダメージ! てきを たおした!"
            )
            self.state = "field"
            return

        self.set_message(f"プレイヤーの こうげき! {dmg} ダメージ! てきの はんげき!")
        self.enemy_attack()

    def enemy_attack(self):
        dmg = random.randint(4, 9)
        self.player_hp -= dmg
        if self.player_hp <= 0:
            self.player_hp = 0
            self.set_message(
                f"てきの こうげき! {dmg} ダメージ! プレイヤーは たおれてしまった…"
            )
            self.state = "gameover"
            return

        self.set_message(
            f"てきの こうげき! {dmg} ダメージ! Aキー:こうげき / Rキー:にげる"
        )

    def battle_run(self):
        if random.random() < 0.5:
            self.set_message("うまく にげきれた! フィールドにもどる。")
            self.state = "field"
            return

        dmg = random.randint(4, 9)
        self.player_hp -= dmg

        if self.player_hp <= 0:
            self.player_hp = 0
            self.set_message(
                "にげだした! …しかし まわりこまれてしまった!\n"
                f"てきの こうげき! {dmg} ダメージ! プレイヤーは たおれてしまった…"
            )
            self.state = "gameover"
        else:
            self.set_message(
                "にげだした! …しかし まわりこまれてしまった!\n"
                f"てきの こうげき! {dmg} ダメージ! Aキー:こうげき / Rキー:にげる"
            )

    def try_move(self, dx, dy):
        nx = self.player_x + dx
        ny = self.player_y + dy

        if not (0 <= nx < MAP_WIDTH and 0 <= ny < MAP_HEIGHT):
            return

        if MAP_DATA[ny][nx] == 1:
            return

        self.player_x = nx
        self.player_y = ny

        self.steps += 1

        self.set_message(f"あるいた かず:{self.steps}")

        if self.steps % 7 == 0:
            self.start_battle()

    # ★STEP7変更
    def run(self):  
        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False

                if event.type == pygame.KEYDOWN:
                    if self.state == "field":
                        if event.key == pygame.K_UP:
                            self.try_move(0, -1)
                        elif event.key == pygame.K_DOWN:
                            self.try_move(0, 1)
                        elif event.key == pygame.K_LEFT:
                            self.try_move(-1, 0)
                        elif event.key == pygame.K_RIGHT:
                            self.try_move(1, 0)

                    elif self.state == "battle":
                        if event.key == pygame.K_a:
                            self.battle_attack()
                        elif event.key == pygame.K_r:
                            self.battle_run()

                    # ★STEP7追加
                    elif self.state == "gameover":
                        if event.key == pygame.K_SPACE:
                            self.reset_game()

            if self.state == "field":
                self.draw_field()
            elif self.state == "battle":
                self.draw_battle()
            # ★STEP7追加
            elif self.state == "gameover":
                self.screen.fill((0, 0, 0))
                over_text = self.font.render(
                    "GAME OVER - SPACEキーで さいしょから",
                    True, (255, 255, 255)
                )
                self.screen.blit(
                    over_text,
                    (
                        WINDOW_WIDTH // 2 - over_text.get_width() // 2,
                        WINDOW_HEIGHT // 2 - over_text.get_height() // 2,
                    ),
                )

            pygame.display.flip()
            self.clock.tick(60)

        pygame.quit()


if __name__ == "__main__":
    game = SimpleDQStyleRPG()
    game.run()

手順①:reset_game で「初期化処理」をひとまとめにする

    def __init__(self):
        ...
        self.load_tileset(TILESET_PATH)

        self.player_max_hp = 30
        self.enemy_max_hp = 28

        self.running = True
        self.reset_game() 

まずは、初期化処理をまとめる「reset_game」メソッドを追加し、「__init__」 からもそれを呼び出すように変更しました。

このようにすることで、

  • ゲーム起動時
  • ゲームオーバーからの再スタート時

どちらの場合も「reset_game」を呼び出すだけで、初期状態を再現できるようになります。

手順②:gameover 状態の描画(GAME OVER 画面)

            if self.state == "field":
                self.draw_field()
            elif self.state == "battle":
                self.draw_battle()
            elif self.state == "gameover": 
                self.screen.fill((0, 0, 0))
                over_text = self.font.render(
                    "GAME OVER - SPACEキーで さいしょから",
                    True, (255, 255, 255)
                )
                self.screen.blit(
                    over_text,
                    (
                        WINDOW_WIDTH // 2 - over_text.get_width() // 2,
                        WINDOW_HEIGHT // 2 - over_text.get_height() // 2,
                    ),
                )

次に、「state が gameover のとき」の描画処理を run の中に追加しました。

ここでは、

  1. 画面全体を真っ黒で塗りつぶす
  2. 「GAME OVER – SPACEキーで さいしょから」という文字列を描画する
  3. 画面の中央にくるように位置を調整する

という処理を行なっています。

手順③:GAME OVER から SPACE キーで再スタート

                if event.type == pygame.KEYDOWN:
                    if self.state == "field":
                        ...
                    elif self.state == "battle":
                        ...
                    elif self.state == "gameover":
                        if event.key == pygame.K_SPACE:
                            self.reset_game()

最後に、「run」メソッドのイベント処理の中で、「state == "gameover"」のときだけ SPACE キーを受け付けるようにしています。

このようにすることで、

  • 戦闘中に HP が 0 になったら self.state = "gameover" になる
  • 描画処理が「GAME OVER 画面」に切り替わる
  • その状態で SPACE キーが押されると reset_game() が呼ばれ、初期状態へ戻る

という一連の流れが完成します。

ゲームとしては、

  1. フィールドを歩く
  2. 戦闘が発生する
  3. こうげき/にげるでバトル
  4. 勝てばフィールドへ、負ければ GAME OVER
  5. GAME OVER から SPACE でやり直し

というループを、何度でも繰り返し遊べるようになりました。

完成

以上でPythonで作る「ロールプレイングゲーム」の完成です。

ぜひ、コードをコピペするのではなく、実際にコードを打って作ってみてください。

お疲れさまでした。

ABOUT ME
タカヒデ
タカヒデ
ITを楽しく勉強中
通信会社の企画職で働く30代 非エンジニアでありながら半分趣味でITエンジニアリングやプログラミングを学習中 IT初心者が楽しいと思えるように発信することを目指しています
記事URLをコピーしました