【Python】RPG(ロールプレイングゲーム)を作ってみた(初心者プログラミング)
おはようございます。タカヒデです。
本日はPythonで「RPG(ロールプレイングゲーム)」を作ってみました。
STEP形式で解説しているので、「まずは何かを作ってみたい」という初心者の方の参考になれば幸いです。
- プログラミング初心者
- 何から始めればよいか分からない
- まずは簡単なゲームを作って興味を持ってみたい
- Python の基礎をゲーム作りで楽しく学びたい
ぜひ実際にコードを打ちながら作成してみてください。
「ロールプレイングゲーム」完成イメージ
まずは、「ロールプレイングゲーム」完成後の最終的なコードと完成イメージです。
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 つです。
- 背景を塗る
- マップデータに応じて床・壁タイルを並べる
- プレイヤーを現在位置に描く
「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」メソッドです。ここで「マップ外に出ない」「壁に入らない」という基本的な移動ルールを整えます。
- 「行きたい方向」を受け取る
- 引数「dx」「dy」は、X・Y 方向の変化量です。
- 例えば「上に1マス」なら (0, -1)、「右に1マス」なら (1, 0) のように指定します。
- 行き先のマス座標を計算する
- 「nx」「ny」は「次に行こうとしているマスの座標」です。
- マップ外かどうかを判定する
- 「0 <= nx < MAP_WIDTH」「0 <= ny < MAP_HEIGHT」を使って、マップの範囲内かどうかをチェックしています。
- どちらかが範囲外なら、そのまま return して「動かない」ようにします。
- 壁かどうかを判定する
- 「MAP_DATA[ny][nx]」で、そのマスが床(0)か壁(1)かを取得します。
- もし 1(壁)なら return して動きません。
- 問題なければ座標を更新して歩数を増やす
- 「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」を呼び出すよう、メインループを変更しています。
- 「KEYDOWN」イベントのチェック
- 「event.type == pygame.KEYDOWN」で「何かキーが押された瞬間」を検出しています。
- 「state」が「field」のときだけ移動を許可
- 「if self.state == “field”:」で、今がフィールド画面のときだけ矢印キーを受け付けるようにしています。
- 後で戦闘画面やゲームオーバー画面を追加したときに、そこで矢印キーを押してもプレイヤーが動かないようにするための準備です。
- 押されたキーに応じて「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」です。
- メッセージウィンドウ用の四角形の位置と大きさを決める
- 「背景色の四角」と「枠線の四角」を描画する
- その中に「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()
最後に、フィールド描画の中でメッセージウィンドウを重ね描きするように変更しています。
- 背景(フィールド全体)を描く
- プレイヤーを描く
- 一番手前にメッセージウィンドウを描く
というレイヤー構造になっています。
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()
まずは、プレイヤーが敵を攻撃する処理です。流れは次のとおりです。
- 「random.randint(4, 8)」でダメージ量を決める
- 敵の HP からその分だけ引く
- 敵の HP が 0 以下になったか判定
- 0未満になったときは0に補正してから、状態を「field」に戻して「return」する。
- まだ 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 キーを処理する」ように変更しました。
これにより、
- フィールドを歩く
- 一定歩数ごとに戦闘画面へ
- 戦闘画面で Aキー or Rキー
- 結果に応じてフィールドへ戻る 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 の中に追加しました。
ここでは、
- 画面全体を真っ黒で塗りつぶす
- 「GAME OVER – SPACEキーで さいしょから」という文字列を描画する
- 画面の中央にくるように位置を調整する
という処理を行なっています。
手順③: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()が呼ばれ、初期状態へ戻る
という一連の流れが完成します。
ゲームとしては、
- フィールドを歩く
- 戦闘が発生する
- こうげき/にげるでバトル
- 勝てばフィールドへ、負ければ GAME OVER
- GAME OVER から SPACE でやり直し
というループを、何度でも繰り返し遊べるようになりました。
完成
以上でPythonで作る「ロールプレイングゲーム」の完成です。
ぜひ、コードをコピペするのではなく、実際にコードを打って作ってみてください。

お疲れさまでした。
