漢字シューター

人生を辿るシューティングゲーム

画面説明

実行方法

画面構成

操作

ソースコード


"""
#漢字シューター
#第1.0版 2025.3.29 初版
#第1.1版 2025.3.30 画面を縦長に変更、難易度、スコアを再調整

MIT License

Copyright (c) 2025  Current Color Co. Ltd.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

import pygame
import pygame.midi
import sys
import threading
import random
import math

# 初期化
pygame.init()
try:
    joy = pygame.joystick.Joystick(0)
except pygame.error:
    print('ジョイスティックが接続されていません')

# 画面設定
G_WIDTH = 600
G_HEIGHT = 800
screen = pygame.display.set_mode((G_WIDTH, G_HEIGHT))
pygame.display.set_caption("漢字シューター")
g_font = pygame.font.Font('ipaexg.ttf', 60)

G_POS_MAX = 8000
G_POS_UNIT = 770
G_POS_GOAL = 400

# 色の定義
C_BLACK = (0, 0, 0)
C_WHITE = (255, 255, 255)
C_GRAY = (128, 128, 128)
C_BLUE = (0, 255, 255)
C_RED = (255, 0, 0)
C_ENEMY = (255, 128, 0)
C_FRIEND =  (0, 180, 255)
C_OLD = (255,180,80)
C_ILL = (255,255,80)
C_ATTACK =  (255, 220, 220)
C_LOVE = (128,255,200)
C_PARENT = (128,255,200)
C_VIRTUE = (000,255,000)

# ゲーム定数
ST_TITLE = 0
ST_PLAYING = 1
ST_GAME_OVER_1 = 2
ST_GAME_OVER_2 = 3
ST_GAME_CLEAR = 4

OPTION_STATE_NONE = 0
OPTION_STATE_FLOAT = 1
OPTION_STATE_NORMAL = 2
OPTION_STATE_OUT = 3

NPC_TYPE_ENEMY = 0
NPC_TYPE_FRIEND = 1
NPC_TYPE_FATHER = 2
NPC_TYPE_MOTHER = 3
NPC_TYPE_OLD = 4
NPC_TYPE_BOSS = 5
NPC_TYPE_DISASTER = 6

NPC_STATE_IN = 0
NPC_STATE_NORMAL = 1
NPC_STATE_OUT = 2

BULLET_TYPE_PLAYER_A = 0
BULLET_TYPE_PLAYER_L = 1
BULLET_TYPE_ENEMY = 2
BULLET_TYPE_FRIEND = 3
BULLET_TYPE_PARENT = 4
BULLET_TYPE_ILL = 5
BULLET_TYPE_VIRTUE = 6

ITEM_TYPE_WIFE = 0
ITEM_TYPE_HUSBAND = 1
ITEM_TYPE_CHILD = 2
ITEM_TYPE_FORCE = 3
ITEM_TYPE_POISON = 4

ITEM_STATE_NORMAL = 0
ITEM_STATE_OUT = 1

BG_TYPE_LINE = 0

EFFECT_TYPE_EXPLOSION  = 0

SE_TYPE_SHOOT_ATTACK = 0
SE_TYPE_SHOOT_LOVE = 1
SE_TYPE_HIT_ATTACK = 2
SE_TYPE_HIT_LOVE = 3
SE_TYPE_EXPLOSION = 4
SE_TYPE_GET_ITEM = 5
SE_TYPE_DEATH = 6
SE_TYPE_GET_VIRTUE = 7

#グローバル変数

player = None
npcs = []
player_bullets = []
npc_bullets = []
items = []
effects = []

g_pos = G_POS_MAX
g_married = False
g_ending = None
g_score = 0
g_sound = None
g_debug = False

######################################
# プレイヤー(自機)のクラス
class Player:

    global g_sound
    img_p = g_font.render("自", True, C_WHITE)

    RAPID_COUNT_MAX = 20

    def __init__(self):
        self.x = G_WIDTH // 2
        self.y = G_HEIGHT - 50
        self.speed = 5
        self.size = 20
        self.hit = 10
        self.energy = 0
        self.max_shot = 2
        self.rapid_count = self.RAPID_COUNT_MAX
        self.image_p = scale_image_w(self.img_p, self.size * 2)

        self.options = []
        self.trace_max = 100
        self.trace = [(self.x, self.y)] * self.trace_max
        self.trace_idx = 0
        
        self.add_energy(0)

    def move(self, dx, dy):
        # 画面外に出ないように移動
        self.x = max(self.size, min(G_WIDTH - self.size, self.x + dx * self.speed))
        self.y = max(self.size, min(G_HEIGHT - self.size, self.y + dy * self.speed))
        # 軌跡を登録
        if dx != 0 or dy != 0:
            self.trace[self.trace_idx] = (self.x, self.y)
            self.trace_idx = (self.trace_idx + 1) % self.trace_max
        # 連射間隔
        self.rapid_count = max(0, self.rapid_count - 1)
        # オプションの動作
        for option in reversed(self.options):
            (x,y) = self.trace[ ((self.trace_idx - option.trace_pos) % self.trace_max) ]
            if option.update(x,y) == False:
                self.options.remove(option)

    def draw(self):
        screen.blit(self.image_p, (self.x - self.size, self.y - self.size))
        for option in self.options:
            option.draw()

    def shoot_love(self, player_bullets):
        if self.rapid_count == 0:
            self.rapid = self.RAPID_COUNT_MAX

            num = 0
            for bullet in player_bullets:
                if bullet.op == 1:
                    num += 1
            if num < self.max_shot:
                blt = Bullet(self.x, self.y - self.size, BULLET_TYPE_PLAYER_L)
                blt.set_op(1)
                player.add_energy(-0.5)
                player_bullets.append(blt)
                g_sound.play_se(SE_TYPE_SHOOT_LOVE)

    def shoot_attack(self, player_bullets):
        if self.rapid_count == 0:
            self.rapid = self.RAPID_COUNT_MAX

            num = 0
            for bullet in player_bullets:
                if bullet.op == 1:
                    num += 1
            if num < self.max_shot:
                blt = Bullet(self.x, self.y - self.size, BULLET_TYPE_PLAYER_A)
                blt.set_op(1)
                player.add_energy(-0.5)
                player_bullets.append(blt)
                g_sound.play_se(SE_TYPE_SHOOT_ATTACK)

            for i, option in enumerate(self.options):
                num = 0
                for bullet in player_bullets:
                    if bullet.op == i+2:
                        num += 1
                if num < self.max_shot and option.state != OPTION_STATE_OUT:
                    blt = Bullet(option.x, option.y - option.size, BULLET_TYPE_PLAYER_A)
                    blt.set_op(i+2)
                    player_bullets.append(blt)
 
    def add_energy(self, energy):
        self.energy += energy
        self.energy = min(100, self.energy)
        self.energy = max(0, self.energy)
        self.speed = 1 + 5 * (self.energy / 100)
        self.max_shot = 1 + int(3 * (self.energy  / 100))

######################################
# オプションのクラス
class Option:

    img_w = g_font.render("妻", True, C_WHITE)
    img_h = g_font.render("夫", True, C_WHITE)
    img_c = g_font.render("子", True, C_WHITE)

    float_speed = 3

    def __init__(self, type, op_num, x, y):

        if type == ITEM_TYPE_WIFE:
            self.size = 20 * 0.9
            self.image = scale_image_w(self.img_w, self.size * 2)
            self.trace_pos = 10
            self.op_num = 2
            self.lifespan = random.randint(2700, 3200)
        elif type == ITEM_TYPE_HUSBAND:
            self.size = 20 * 0.9
            self.image = scale_image_h(self.img_h, self.size * 2)
            self.trace_pos = 10
            self.op_num = 2
            self.lifespan = random.randint(2500, 3000)
        elif type == ITEM_TYPE_CHILD:
            self.size = 20 * 0.8
            self.image = scale_image_w(self.img_c, self.size * 2)
            self.trace_pos = 10 + 8 * (op_num - 2)
            self.op_num = op_num
            self.lifespan = random.randint(2800,3000)

        self.type = type
        self.state = OPTION_STATE_FLOAT

        self.x = x - self.size
        self.y = y - self.size
        self.dx = 1
        self.dy = 0

    def update(self, x, y):
        if self.state == OPTION_STATE_FLOAT:
            dx = x - self.x
            dy = y - self.y
            dist = math.sqrt(dx**2 + dy**2)
            if dist > 0:
                self.dx = (dx / dist) * self.float_speed
                self.dy = (dy / dist) * self.float_speed
            else:
                self.dx = 0
                self.dy = self.float_speed
            self.x += self.dx
            self.y += self.dy
            if dist < 2:
                self.state = OPTION_STATE_NORMAL
            return True
        elif self.state == OPTION_STATE_NORMAL:
            self.x = x - self.size
            self.y = y - self.size
            self.lifespan -= 1
            if self.lifespan < 0:
                self.state = OPTION_STATE_OUT
                if self.x > G_WIDTH / 2:
                    self.dx = 1
                else:
                    self.dx = -1
                self.image.set_alpha(128)
            return True
        elif self.state == OPTION_STATE_OUT:
            self.x += self.dx
            if self.x < -20 or self.x > G_WIDTH:
                return False
            return True

    def draw(self):
        screen.blit(self.image, (self.x,self.y))

######################################
# キャラクター(NPC)のクラス
class NPC:

    img_e = g_font.render("敵", True, C_ENEMY)
    img_f = g_font.render("友", True, C_FRIEND)
    img_fa = g_font.render("父", True, C_FRIEND)
    img_ma = g_font.render("母", True, C_FRIEND)
    img_old = g_font.render("老", True, C_OLD)
    img_boss = g_font.render("悪", True, C_ENEMY)
    img_disaster = g_font.render("災", True, C_ENEMY)

    def __init__(self, type):
        self.type = type
        self.x = random.randint(50, G_WIDTH - 50)
        self.x_0 = self.x
        self.y = -30
        self.dx = 0
        self.dy = 1
        self.speed = random.uniform(1, 3)
        self.size = 20
        self.shot_cooldown = 0
        self.lifespan = 100
        self.cnt = 0
        self.hp = 50

        if self.type == NPC_TYPE_ENEMY:
            self.image = scale_image_w(self.img_e, self.size * 2)
            self.state = NPC_STATE_NORMAL
        elif self.type == NPC_TYPE_FRIEND:
            self.image = scale_image_w(self.img_f, self.size * 2)
            self.state = NPC_STATE_NORMAL
        elif self.type == NPC_TYPE_FATHER:
            self.image = scale_image_w(self.img_fa, self.size * 2)
            self.state = NPC_STATE_IN
            self.x = G_WIDTH / 2 - 200
            self.dy = 2
            self.lifespan = 600
        elif self.type == NPC_TYPE_MOTHER:
            self.image = scale_image_w(self.img_ma, self.size * 2)
            self.state = NPC_STATE_IN
            self.x = G_WIDTH / 2 + 200
            self.dy = 2
            self.lifespan = 800
        elif self.type == NPC_TYPE_OLD:
            self.image = scale_image_w(self.img_old, self.size * 2)
            self.state = NPC_STATE_NORMAL
        elif self.type == NPC_TYPE_BOSS:
            self.size = 60
            self.image = scale_image_w(self.img_boss, self.size * 2)
            self.y = -60
            self.x = G_WIDTH / 2
            self.dy = 2
            self.lifespan = 1000
            self.state = NPC_STATE_IN
        elif self.type == NPC_TYPE_DISASTER:
            self.size = 30
            self.image = scale_image_w(self.img_disaster, self.size * 2)
            self.y = -10
            self.dy = 5

    def draw(self):
        screen.blit(self.image, (self.x - self.size, self.y - self.size))
 
    def update(self, npc_bullets, player):

        #敵
        if self.type == NPC_TYPE_ENEMY:
            self.y += self.dy * self.speed
            self.cnt += 10
            if self.cnt > 360:
                 self.cnt -= 360
            self.x = self.x_0 + 10*math.cos(math.radians(self.cnt))
            # 弾の発射判定
            self.shot_cooldown -= 1
            if self.shot_cooldown <= 0 and random.random() < 0.025:
                self.shot_cooldown = 80
                # 自機の位置を狙って発射
                npc_bullets.append(Bullet(self.x, self.y + self.size, 
                              BULLET_TYPE_ENEMY,  player.x, player.y))
            # 画面下に到達したら削除対象
            if self.y > G_HEIGHT + 50:
                return False
            return True

        #友
        elif self.type == NPC_TYPE_FRIEND:
            self.y += self.dy * self.speed
            # 画面下に到達したら削除対象とする
            if self.y > G_HEIGHT + 50:
                return False
            return True

        #父・母
        elif self.type == NPC_TYPE_FATHER or self.type == NPC_TYPE_MOTHER:
            if self.state == NPC_STATE_IN:
                self.y += self.dy
                if self.y > G_HEIGHT / 3:
                    self.state = NPC_STATE_NORMAL
                return True
            elif self.state == NPC_STATE_NORMAL:
                self.shot_cooldown -= 1
                if self.shot_cooldown <= 0:
                    self.shot_cooldown = 10
                    npc_bullets.append(Bullet(self.x, self.y + self.size, 
                                  BULLET_TYPE_PARENT,  player.x, player.y))
                self.lifespan -= 1
                if self.lifespan <= 0:
                    self.state = NPC_STATE_OUT
                    self.dx = 1 if self.x > G_WIDTH / 2 else -1
                    self.image.set_alpha(128)
                return True
            elif self.state == NPC_STATE_OUT:
                self.x += self.dx
                # 画面外に出たら削除対象とする
                if ( self.y < -10 or self.y > G_HEIGHT + 10 or 
                     self.x < -10 or self.x > G_WIDTH + 10):
                    return False
                return True

        #老
        elif self.type == NPC_TYPE_OLD:
            self.y += self.dy * self.speed
            # 弾の発射判定
            self.shot_cooldown -= 1
            if self.shot_cooldown <= 0:
                self.shot_cooldown = 45
                # 自機の位置を狙って発射
                if random.random() < 0.3:
                    npc_bullets.append(Bullet(self.x, self.y + self.size, 
                              BULLET_TYPE_VIRTUE,  player.x, player.y))
                else:
                    npc_bullets.append(Bullet(self.x, self.y + self.size, 
                              BULLET_TYPE_ILL,  player.x, player.y))
            # 画面下に到達したら削除対象
            if self.y > G_HEIGHT + 50:
                return False
            return True

        #悪
        elif self.type == NPC_TYPE_BOSS:
            if self.state == NPC_STATE_IN:
                self.y += self.dy
                if self.y > G_HEIGHT / 3:
                    self.state = NPC_STATE_NORMAL
                    self.dx = 1
                return True
            elif self.state == NPC_STATE_NORMAL:
                if self.y < G_HEIGHT / 3:
                    self.y += 1
                if self.dx == 1:
                    self.x += self.dx
                    if self.x > G_WIDTH / 2 + 200:
                        self.dx = -1
                elif self.dx == -1:
                    self.x += self.dx
                    if self.x < G_WIDTH / 2 - 200:
                        self.dx = +1
                self.shot_cooldown -= 1
                if self.shot_cooldown <= 0:
                    self.shot_cooldown = 25
                    npc_bullets.append(Bullet(self.x, self.y + self.size, 
                                  BULLET_TYPE_ENEMY,  player.x, player.y))
                self.lifespan -= 1
                if self.lifespan <= 0:
                    self.state = NPC_STATE_OUT
                    self.dy = -1
                    self.image.set_alpha(128)
                return True
            elif self.state == NPC_STATE_OUT:
                self.y += self.dy
                # 画面外に出たら削除対象とする
                if ( self.y < -60 or self.y > G_HEIGHT + 10):
                    return False
                return True

        #災
        elif self.type == NPC_TYPE_DISASTER:
            self.y += self.dy
            if self.y > G_HEIGHT / 2:
                radius = 10
                for angle in range(0, 360, 30):
                    rad = math.radians(angle)
                    tx = self.x + radius * math.cos(rad)
                    ty = self.y + radius * math.sin(rad)
                    npc_bullets.append(Bullet(self.x, self.y, 
                                  BULLET_TYPE_ENEMY,  tx, ty))
                for angle in range (45, 360, 90):
                    rad = math.radians(angle)
                    tx = self.x + radius * math.cos(rad)
                    ty = self.y + radius * math.sin(rad)
                    npc_bullets.append(Bullet(self.x, self.y, 
                                  BULLET_TYPE_ILL,  tx, ty))
                return False
            return True

        return False

    def hit(self):
        if self.y > G_HEIGHT / 5:
            self.y -= 10
        self.hp -= 1
        return self.hp

######################################
# 弾のクラス
class Bullet:

    img_pa = g_font.render("撃", True, C_ATTACK)
    img_pl = g_font.render("愛", True, C_LOVE)
    img_e = g_font.render("弾", True, C_ENEMY)
    img_f = g_font.render("励", True, C_FRIEND)
    img_par = g_font.render("愛", True, C_PARENT)
    img_ill = g_font.render("病", True, C_ILL)
    img_virtue = g_font.render("徳", True, C_VIRTUE)

    def __init__(self, x, y, type, target_x=None, target_y=None):
        self.x = x
        self.y = y
        self.type = type
        self.op = 0
        self.cnt = 0

        # 方向ベクトルを設定
        if target_x is not None and target_y is not None:
            dx = target_x - x
            dy = target_y - y
            dist = math.sqrt(dx**2 + dy**2)
            if dist > 0:
                self.dx = dx / dist
                self.dy = dy / dist
            else:
                self.dx = 0
                self.dy = 1

        if  type == BULLET_TYPE_PLAYER_A:
            self.size = 18
            self.image = scale_image_w(self.img_pa, self.size * 2)
            self.speed = 9
            self.dx = 0
            self.dy = -1
        elif  type == BULLET_TYPE_PLAYER_L:
            self.size = 18
            self.image = scale_image_w(self.img_pl, self.size * 2)
            self.speed = 9
            self.dx = 0
            self.dy = -1
        elif type == BULLET_TYPE_ENEMY:
            self.size = 15
            self.image = scale_image_w(self.img_e, self.size * 2)
            self.speed = 4
        elif type == BULLET_TYPE_FRIEND:
            self.size = 15
            self.image = scale_image_w(self.img_f, self.size * 2)
            self.speed = 6
            self.dx = 0
            self.dy = -1
        elif type == BULLET_TYPE_PARENT:
            self.size = 18
            self.image = scale_image_w(self.img_par, self.size * 2)
            self.speed = 8
            self.dx = 0
            self.dy = 1
        elif type == BULLET_TYPE_ILL:
            self.size = 15
            self.image = scale_image_w(self.img_ill, self.size * 2)
            self.speed = 3
        elif type == BULLET_TYPE_VIRTUE:
            self.size = 20
            self.image = scale_image_w(self.img_virtue, self.size * 2)
            self.speed = 2

    def draw(self):
        screen.blit(self.image, (self.x - self.size, self.y - self.size))
        
    def update(self, target_x=None, target_y=None):

        if (self.type == BULLET_TYPE_FRIEND or
            self.type == BULLET_TYPE_PARENT):
            if target_x is not None and target_y is not None:
                self.dx, self.dy = rotate_towards_target(self.x, self.y, self.dx, self.dy,
                       target_x, target_y, 5)

        if self.type == BULLET_TYPE_ILL or self.type == BULLET_TYPE_VIRTUE:
            if target_x is not None and target_y is not None:
                if self.cnt < 180: #追尾時間
                    self.cnt += 1
                    self.dx, self.dy = rotate_towards_target(self.x, self.y, self.dx, self.dy,
                           target_x, target_y, 1)

        # 方向ベクトルに沿って移動
        self.x += self.dx * self.speed
        self.y += self.dy * self.speed
        
        # 画面外に出たら削除対象
        if (self.y < -10 or self.y > G_HEIGHT + 10 or
            self.x < -10 or self.x > G_WIDTH + 10):
            return False
        return True

    def set_op(self, op):
        self.op = op #どのオプションが放った弾なのかを記録

######################################
# アイテムのクラス
class Item:

    img_w = g_font.render("妻", True, C_WHITE)
    img_h = g_font.render("夫", True, C_WHITE)
    img_c = g_font.render("子", True, C_WHITE)
    img_f = g_font.render("強", True, C_WHITE)
    img_p = g_font.render("毒", True, C_RED)

    def __init__(self, type):
        self.type = type
        self.x = G_WIDTH / 2
        self.y = 0
        self.dx = 0
        self.dy = 1
        self.state = ITEM_STATE_NORMAL

        if self.type == ITEM_TYPE_WIFE:
            self.size = 16
            self.x = G_WIDTH / 2 + 200
            self.dy = 0.5
            self.image = scale_image_w(self.img_w, self.size * 2)
        elif self.type == ITEM_TYPE_HUSBAND:
            self.size = 16
            self.x = G_WIDTH / 2 - 200
            self.dy = 0.5
            self.image = scale_image_w(self.img_h, self.size * 2)
        elif self.type == ITEM_TYPE_CHILD:
            self.size = 16
            self.image = scale_image_w(self.img_c, self.size * 2)
        elif self.type == ITEM_TYPE_FORCE:
            self.size = 16
            self.image = scale_image_w(self.img_f, self.size * 2)
        elif self.type == ITEM_TYPE_POISON:
            self.size = 16
            self.image = scale_image_w(self.img_p, self.size * 2)

    def update(self):
        self.x += self.dx
        self.y += self.dy
        # 画面外に出たら削除対象
        if (self.y < -10 or self.y > G_HEIGHT + 10 or
            self.x < -10 or self.x > G_WIDTH + 10):
            return False
        return True

    def exit(self):
        self.state = ITEM_STATE_OUT
        self.dx = 1 if self.x > G_WIDTH / 2 else -1
        self.dy = 0
        self.image.set_alpha(128)

    def draw(self):
        screen.blit(self.image, (self.x - self.size, self.y - self.size))

######################################
# エフェクトのクラス
class Effect:

    img_ex = g_font.render("破", True, C_RED)

    def __init__(self, x, y, type):
        self.x = x
        self.y = y
        self.type = type
        self.size = 20
        self.image = scale_image_w(self.img_ex, self.size * 2)

        if  self.type == EFFECT_TYPE_EXPLOSION:
            self.size = 20
            self.speed = 6
            self.radius = 10

    def update(self):
        if self.type == EFFECT_TYPE_EXPLOSION:
            self.radius += 5
            self.size -= 1
            self.image = scale_image_w(self.img_ex, self.size * 2)
            if self.size < 5:
                return False
            return True

    def draw(self):
        if self.type == EFFECT_TYPE_EXPLOSION:
            for i in range(0,360,45):
                angle = math.radians(i)
                x = self.x + self.radius * math.cos(angle) - self.size
                y = self.y + self.radius * math.sin(angle) - self.size
                screen.blit(self.image, (x,y))

#################################
# 音声のクラス
class Sound:

    CHANNEL_1 = 0
    CHANNEL_2 = 1

    #生成
    def __init__(self):
        pygame.midi.init()

        self.midi = pygame.midi.Output(pygame.midi.get_default_output_id())

        self.playing = False
        self.stop_event = threading.Event()
        self.play_thread = None
        self.lock = threading.Lock()

    #メロディー再生
    def play(self, prg, volume, melody, priority=True, channel=0):
        #優先度に基づいて再生判断
        with self.lock:
            if self.playing:
                if priority == False:
                    return False #再生しない
                else: 
                    self.stop_event.set() #先行音を強制停止
                    self.stop(channel)
                    if self.play_thread and self.play_thread.is_alive():
                        self.play_thread.join(0.01)
            self.stop_event.clear()

        def play_sequence():
            self.playing = True #再生中
            with self.lock:
                self.midi.set_instrument(prg, channel)
            for note, duration in melody:
                if self.stop_event.is_set():
                    break
                with self.lock:
                    self.midi.note_on(note, volume, channel)
                if self.stop_event.wait(duration):
                    break #停止イベント
                with self.lock:
                    self.midi.note_off(note, volume, channel)
            with self.lock:
                self.playing = False

        self.play_thread = threading.Thread(target=play_sequence)
        self.play_thread.start()
        return True

    def play_se(self, type):

        if  type == SE_TYPE_SHOOT_ATTACK:
            melody = [ (82, 0.05), (83, 0.05) ]
            self.play(81, 80, melody, False)
        elif type == SE_TYPE_HIT_ATTACK:
            melody = [ (98, 0.05) ]
            self.play(81, 80, melody, False)
        elif type == SE_TYPE_EXPLOSION:
            melody = [ (20, 0.5)]
            self.play(30, 127, melody, True)
        elif type == SE_TYPE_SHOOT_LOVE:
            melody = [ (85, 0.05), (86, 0.05) ]
            self.play(81, 80, melody, False)
        elif type == SE_TYPE_HIT_LOVE:
            melody = [ (87, 0.05), (88, 0.05) ]
            self.play(81, 80, melody, False)
        elif type == SE_TYPE_GET_ITEM:
            melody = [ (60, 0.1), (64, 0.1), (67,0.2) ]
            self.play(8, 127, melody, True)
        elif type == SE_TYPE_DEATH:
            melody = [ (40, 0.15), (39, 0.15), (38,0.3) ]
            self.play(36, 127, melody, True)
        elif type == SE_TYPE_GET_VIRTUE:
            melody = [ (72, 0.6) ]
            self.play(52, 127, melody, True)

    def stop(self, ch):
        self.midi.write_short(0xB0 + ch, 0x7B, 0x00)

    def stop_se(self):
        self.stop(self.CHANNEL_1)

######################################
# 背景のクラス
class BG:

    bgs = [
        (G_POS_GOAL,  BG_TYPE_LINE, "百歳"),
        (G_POS_GOAL + G_POS_UNIT,  BG_TYPE_LINE, "九十歳"),
        (G_POS_GOAL + G_POS_UNIT * 2,  BG_TYPE_LINE, "八十歳"),
        (G_POS_GOAL + G_POS_UNIT * 3,  BG_TYPE_LINE, "七十歳"),
        (G_POS_GOAL + G_POS_UNIT * 4,  BG_TYPE_LINE, "六十歳"),
        (G_POS_GOAL + G_POS_UNIT * 5,  BG_TYPE_LINE, "五十歳"),
        (G_POS_GOAL + G_POS_UNIT * 6,  BG_TYPE_LINE, "四十歳"),
        (G_POS_GOAL + G_POS_UNIT * 7,  BG_TYPE_LINE, "三十歳"),
        (G_POS_GOAL + G_POS_UNIT * 8,  BG_TYPE_LINE, "二十歳"),
        (G_POS_GOAL + G_POS_UNIT * 9,  BG_TYPE_LINE, "十歳"),
        (G_POS_GOAL + G_POS_UNIT * 10,  BG_TYPE_LINE, "誕生"),
    ]

    def draw(self):
        global g_pos
        for (gy, type, str) in self.bgs:
            y = gy - g_pos
            if y > -30 and y < G_HEIGHT +30:
                if type == BG_TYPE_LINE:
                    img1 = g_font.render(str, True, C_GRAY)
                    img2 = scale_image_h(img1, 30)
                    screen.blit(img2, (0,y-32))
                    pygame.draw.line(screen, C_GRAY, (0,y),(G_WIDTH,y),1)

######################################
# 臨終のクラス
class Ending:

    img_fa = g_font.render("父", True, C_FRIEND)
    img_ma = g_font.render("母", True, C_FRIEND)
    img_w = g_font.render("妻", True, C_WHITE)
    img_h = g_font.render("夫", True, C_WHITE)
    img_c = g_font.render("子", True, C_WHITE)
    img_gc = g_font.render("孫", True, C_WHITE)
    img_f = g_font.render("友", True, C_FRIEND)

    def __init__(self):
        self.count = 0
        self.img_pt = None
        self.child = 0
        self.friend = 0

        self.img_f2 = scale_image_w(self.img_f, 40)
        self.img_fa2 = scale_image_w(self.img_fa, 60)
        self.img_ma2 = scale_image_w(self.img_ma, 60)
        self.img_c2 = scale_image_w(self.img_c, 50)
        self.img_gc2 = scale_image_w(self.img_gc, 40)

    def set_partner(self, type):
        if type == ITEM_TYPE_WIFE:
            self.img_pt = scale_image_w(self.img_w, 60)
        if type == ITEM_TYPE_HUSBAND:
            self.img_pt = scale_image_w(self.img_h, 60)

    def set_child(self, num):
        self.child = num

    def add_friend(self):
        self.friend += 1

    def update(self):
        self.count += 1

    def draw(self):
        diff = max(0, 380 - self.count * 0.8)

        screen.blit(self.img_fa2, (G_WIDTH / 2 - 200 - 30 , G_HEIGHT /2 - 100 - diff))
        screen.blit(self.img_ma2, (G_WIDTH / 2 + 200 - 30 , G_HEIGHT /2 - 100 - diff))

        if self.img_pt is not None:
            screen. blit(self.img_pt, (G_WIDTH / 2 - 30, 100 - diff))

        num_friend = int(self.friend / 10)
        if num_friend > 0:
            unit = G_HEIGHT / (num_friend + 1)
            for i in range(num_friend):
                screen.blit(self.img_f2, (G_WIDTH / 2 - 180 - 20 - diff, (i+1) * unit - 20))
                screen.blit(self.img_f2, (G_WIDTH / 2 + 180 - 20 + diff, (i+1) * unit - 20))

        if self.child > 0:
            unit = G_WIDTH / (self.child + 1)
            for i in range(self.child):
                screen.blit(self.img_c2, ((i+1) * unit - 25, G_HEIGHT - 300 + diff))
            unit = G_WIDTH / (self.child * 2 + 1)
            for i in range(self.child * 2):
                screen.blit(self.img_gc2, ((i+1) * unit - 20, G_HEIGHT - 190 + diff))

######################################
# アイテム生成クラス
class Item_Gen:

    def generate(self, items, g_pos):
        if (g_pos == int(G_POS_MAX * 0.88)):
            items.append(Item(ITEM_TYPE_POISON))
        if g_pos == int(G_POS_MAX * 0.82):
            items.append(Item(ITEM_TYPE_WIFE))
            items.append(Item(ITEM_TYPE_HUSBAND))
        if (g_pos == int(G_POS_MAX * 0.75) and g_married == True): 
            items.append(Item(ITEM_TYPE_CHILD))
        if (g_pos == int(G_POS_MAX * 0.70) and g_married == True):
            items.append(Item(ITEM_TYPE_CHILD))
        if (g_pos == int(G_POS_MAX * 0.65) and g_married == True):
            items.append(Item(ITEM_TYPE_CHILD))
        if (g_pos == int(G_POS_MAX * 0.55)):
            items.append(Item(ITEM_TYPE_POISON))
        if (g_pos == int(G_POS_MAX * 0.25)):
            items.append(Item(ITEM_TYPE_FORCE))


######################################
# NPC生成クラス(難易度調整)
class NPC_Gen:

    npc_gen_units = []

    def __init__(self):
        self.npc_gen_units.append(NPC_Gen_Unit(NPC_TYPE_ENEMY))
        self.npc_gen_units.append(NPC_Gen_Unit(NPC_TYPE_FRIEND))
        self.npc_gen_units.append(NPC_Gen_Unit(NPC_TYPE_OLD))

    def generate(self, npcs, g_pos):
        # NPC生成パラメータ(難易度)設定
        if g_pos == int(G_POS_MAX):
            self.set_param(100,200,0,0,0,0)
        if g_pos == int(G_POS_MAX * 0.95):
            self.set_param(80,160,0,0,0,0)
        if g_pos == int(G_POS_MAX * 0.90):
            self.set_param(60,120,120,180,0,0) 
        if g_pos == int(G_POS_MAX * 0.80):
            self.set_param(30,60,120,180,0,0)
        if g_pos == int(G_POS_MAX * 0.70):
            self.set_param(20,50,120,180,0,0)
        if g_pos == int(G_POS_MAX * 0.50):
            self.set_param(20,40,120,180,0,0)
        if g_pos == int(G_POS_MAX * 0.35):
            self.set_param(20,50,120,180,240,480)
        if g_pos == int(G_POS_MAX * 0.28):
            self.set_param(60,120,120,180,120,240)
        if g_pos == int(G_POS_MAX * 0.19):
            self.set_param(120,240,120,180, 60,120)
        if g_pos == int(G_POS_MAX * 0.12):
            self.set_param(120,240,120,180, 60,120)
        if g_pos == G_POS_GOAL + 200:   #敵が出なくなる
            self.set_param(0,0,0,0,0,0)

        # NPCの生成(乱数)
        for npc_gen_unit in self.npc_gen_units:
            npc_gen_unit.generate(npcs)

        # NPCの生成(固定座標)
        if g_pos == int(G_POS_MAX * 0.98):
            npcs.append(NPC(NPC_TYPE_FATHER))
            npcs.append(NPC(NPC_TYPE_MOTHER))
        if g_pos == int(G_POS_MAX * 0.58):
            npcs.append(NPC(NPC_TYPE_BOSS))
        if g_pos == int(G_POS_MAX * 0.40):
            npcs.append(NPC(NPC_TYPE_DISASTER))
        if g_pos == int(G_POS_MAX * 0.30):
            npcs.append(NPC(NPC_TYPE_DISASTER))
        if g_pos == int(G_POS_MAX * 0.20):
            npcs.append(NPC(NPC_TYPE_DISASTER))

    def set_param(self, min1, max1, min2, max2, min3, max3):
        self.npc_gen_units[0].min = min1
        self.npc_gen_units[0].max = max1
        self.npc_gen_units[1].min = min2
        self.npc_gen_units[1].max = max2
        self.npc_gen_units[2].min = min3
        self.npc_gen_units[2].max = max3

class NPC_Gen_Unit:

    min = 0
    max = 0
    timer = 0
    type = NPC_TYPE_ENEMY

    def __init__(self, type):
        self.type = type

    def set_interval(min, max):
         self.min = min
         self.max = max

    def generate(self, npcs):
        if self.max > 0:
            self.timer -= 1
            if self.timer < 0:
               self.timer = random.randint(self.min, self.max)
               npcs.append(NPC(self.type))
 
######################################
# イメージの拡大・縮小関数
def scale_image_w(image, new_width):
    new_height = int(image.get_height() * new_width / image.get_width())
    return pygame.transform.scale(image, (new_width, new_height))

def scale_image_h(image, new_height):
    new_width = int(image.get_width() * new_height / image.get_height())
    return pygame.transform.scale(image, (new_width, new_height))
 
######################################
# 衝突判定関数
def check_collision(x1, y1, x2, y2, size):
    dx = x1 - x2
    dy = y1 - y2
    distance = math.sqrt(dx**2 + dy**2)
    return distance < size

######################################
# 回転関数
# 引数:中心座標(x,y)、速度ベクトル(dx,dy)、目標座標(tx,ty)、
#    最大回転角度 max_angle_deg
# 戻り値:回転後の速度ベクトル new_dx, new_dy 
def rotate_towards_target(x, y, dx, dy, tx, ty, max_angle_deg):
    target_angle = math.atan2(ty - y, tx - x)
    current_angle = math.atan2(dy, dx)
    # 角度差を求める(-π から π の範囲に正規化)
    angle_diff = (target_angle - current_angle + math.pi) % (2 * math.pi) - math.pi
 
    max_rotation = math.radians(max_angle_deg)
    if abs(angle_diff) > max_rotation:
        angle_diff = math.copysign(max_rotation, angle_diff)
 
    new_angle = current_angle + angle_diff
    new_dx = math.cos(new_angle)
    new_dy = math.sin(new_angle)
    
    return new_dx, new_dy

######################################
# テキスト描画関数
def draw_text_center(text, size, x, y, color=C_WHITE):
    font = pygame.font.Font(None, size)
    text_surface = font.render(text, True, color)
    text_rect = text_surface.get_rect()
    text_rect.center = (x, y)
    screen.blit(text_surface, text_rect)

def draw_text(text, size, x, y, color=C_WHITE):
    font = pygame.font.Font(None, size)
    text_surface = font.render(text, True, color)
    screen.blit(text_surface, (x,y))

######################################
# タイトル画面
def show_title_screen():
    screen.fill(C_BLACK)
    draw_text_center("KANJI SHOOTER", 64, G_WIDTH // 2, G_HEIGHT // 3)
    draw_text_center("PUSH R KEY", 48, G_WIDTH // 2, G_HEIGHT // 2)

######################################
# ゲームオーバー画面
def show_game_over_screen():
    draw_text_center("GAME OVER", 64, G_WIDTH // 2, G_HEIGHT // 2, C_RED)

######################################
# クリア画面
def show_game_clear_screen():
    screen.fill(C_BLACK)
    draw_text_center("CONGRATULATIONS", 64, G_WIDTH // 2, G_HEIGHT // 3, C_BLUE)
    text = "FINAL SCORE : " + str(g_score)
    draw_text_center(text, 40, G_WIDTH // 2, G_HEIGHT // 2)
    draw_text_center("PUSH R KEY TO TITLE", 48, G_WIDTH // 2, G_HEIGHT // 2 + 200)

######################################
# UI画面
def show_ui_screen():
    global g_score

    draw_text("SCORE", 48, 20, 5)
    draw_text(str(g_score).rjust(8), 48, 200, 5)

######################################
# ゲームリセット
def reset_game():
    global player, g_pos, g_married, g_ending, g_score
    global player_bullets, npcs, npc_bullets, items, effects, g_sound

    player = Player()
    g_pos = G_POS_MAX
    g_married = False
    g_ending = Ending()
    g_score = 0
    npcs = []
    player_bullets = []
    npc_bullets = []
    items = []
    effects = []

    return 
 
######################################
# メイン関数
def main():
    global player, g_pos, g_married, g_ending, g_score
    global player_bullets, npcs, npc_bullets, items, effects, g_sound

    clock = pygame.time.Clock()
    g_sound = Sound()
    game_state = ST_TITLE
    
    reset_game()
    bg = BG()
    item_gen = Item_Gen()
    npc_gen = NPC_Gen()
    game_over_npc_bullet = None
    game_over_count = 0
    
    while True:
        # イベント処理
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_r:
                    if game_state == ST_TITLE:
                       game_state = ST_PLAYING
                       reset_game()
                    elif game_state == ST_GAME_CLEAR:
                       game_state = ST_TITLE
                elif event.key == pygame.K_g:
                    if game_state == ST_PLAYING:
                        player.shoot_love(player_bullets)
                elif event.key == pygame.K_f:
                    if game_state == ST_PLAYING:
                        player.shoot_attack(player_bullets)

            if event.type == pygame.JOYBUTTONDOWN:
                # print(f"Joystick button {event.button} pressed")
                if event.button == 7:
                    if game_state == ST_TITLE:
                       game_state = ST_PLAYING
                       reset_game()
                    elif game_state == ST_GAME_CLEAR:
                       game_state = ST_TITLE
                elif event.button == 1:
                    if game_state == ST_PLAYING:
                        player.shoot_love(player_bullets)
                elif event.button == 0:
                    if game_state == ST_PLAYING:
                        player.shoot_attack(player_bullets)

        # ゲーム状態に応じた処理
        if game_state == ST_TITLE:
            show_title_screen()
        elif game_state == ST_GAME_CLEAR:
            show_game_clear_screen()
        elif game_state == ST_PLAYING:
            dx, dy = 0, 0
            # 自機の移動
            # キー入力による移動
            keys = pygame.key.get_pressed()
            if keys[pygame.K_a]:  # 左
                dx = -1
            if keys[pygame.K_d]:  # 右
                dx = 1
            if keys[pygame.K_w]:  # 上
                dy = -1
            if keys[pygame.K_s]:  # 下
                dy = 1
            # ハットスイッチによる移動
            hat_input = joy.get_hat(0)
            if hat_input[0] == -1:  # 左
                dx = -1
            if hat_input[0] == 1:  # 右
                dx = 1
            if hat_input[1] == 1:  # 上
                dy = -1
            if hat_input[1] == -1:  # 下
                dy = 1

            player.move(dx, dy)

            # 自弾の更新            
            for p_bullet in reversed(player_bullets):
                if p_bullet.update() == False:  
                    player_bullets.remove(p_bullet) # 画面外に出た場合
                    continue
                    
                # 自弾とNPCとの衝突判定
                for npc in reversed(npcs):
                    if check_collision(p_bullet.x, p_bullet.y, npc.x, npc.y, npc.size + p_bullet.size):
                        if ( p_bullet.type == BULLET_TYPE_PLAYER_A and
                             ( npc.type == NPC_TYPE_ENEMY or npc.type == NPC_TYPE_OLD
                                or npc.type == NPC_TYPE_DISASTER )):
                            effects.append(Effect(npc.x, npc.y, EFFECT_TYPE_EXPLOSION))
                            npcs.remove(npc)
                            player_bullets.remove(p_bullet)
                            g_score += 100
                            g_sound.play_se(SE_TYPE_EXPLOSION)
                            break
                        if ( p_bullet.type == BULLET_TYPE_PLAYER_A and
                             npc.type == NPC_TYPE_BOSS ):
                            if npc.hit() <= 0:
                                effects.append(Effect(npc.x, npc.y, EFFECT_TYPE_EXPLOSION))
                                npcs.remove(npc)
                                g_score += 2000
                                g_sound.play_se(SE_TYPE_EXPLOSION)
                            else:
                                g_score += 10
                                g_sound.play_se(SE_TYPE_HIT_ATTACK)
                            player_bullets.remove(p_bullet)
                            break
                        elif ( p_bullet.type == BULLET_TYPE_PLAYER_L and
                                npc.type == NPC_TYPE_FRIEND):
                            player_bullets.remove(p_bullet)
                            npc.y -= 20
                            npc_bullets.append(Bullet(npc.x, npc.y + npc.size,
                                      BULLET_TYPE_FRIEND, player.x, player.y))
                            g_ending.add_friend()
                            g_score += 80
                            g_sound.play_se(SE_TYPE_HIT_LOVE)
                            break

            # NPCの生成 
            npc_gen.generate(npcs, g_pos)
            # アイテムの生成
            item_gen.generate(items, g_pos)

            # NPCの更新
            for npc in reversed(npcs):
                if npc.update(npc_bullets, player) == False:
                    npcs.remove(npc) # 画面外に出た場合
                    continue
                    
            # NPC弾の更新
            for npc_bullet in reversed(npc_bullets):
                if npc_bullet.update(player.x, player.y) == False:  
                    npc_bullets.remove(npc_bullet) # 画面外に出た場合
                    continue                    
                # NPC弾と自機の衝突判定
                if check_collision(npc_bullet.x, npc_bullet.y, player.x, player.y,
                                         player.hit + npc_bullet.size):
                    if (npc_bullet.type == BULLET_TYPE_ENEMY):
                        g_sound.play_se(SE_TYPE_DEATH)
                        if g_debug == False:
                            game_over_npc_bullet = npc_bullet
                            game_over_count = 0
                            game_state = ST_GAME_OVER_1
                    elif (npc_bullet.type == BULLET_TYPE_FRIEND or
                          npc_bullet.type == BULLET_TYPE_PARENT):
                        npc_bullets.remove(npc_bullet)
                        player.add_energy(6)
                    elif (npc_bullet.type == BULLET_TYPE_ILL):
                        npc_bullets.remove(npc_bullet)
                        player.add_energy(-10)
                    elif (npc_bullet.type == BULLET_TYPE_VIRTUE):
                        npc_bullets.remove(npc_bullet)
                        g_score += 300
                        player.add_energy(3)
                        g_sound.play_se(SE_TYPE_GET_VIRTUE)

            # アイテムの更新
            for item in reversed(items):
                if item.update() == False:
                    items.remove(item)
                # アイテムと自機の衝突判定
                if check_collision(item.x, item.y, player.x, player.y,
                                         player.size + npc_bullet.size):
                    if (item.type == ITEM_TYPE_WIFE and
                        g_married == False):
                        player.options.append(Option(ITEM_TYPE_WIFE, 2, 
                                  item.x, item.y))
                        g_married = True
                        for item2 in items:
                            if item2.type == ITEM_TYPE_HUSBAND:
                                item2.exit()
                                break
                        g_ending.set_partner(ITEM_TYPE_WIFE)
                        g_sound.play_se(SE_TYPE_GET_ITEM)
                        items.remove(item)
                    elif (item.type == ITEM_TYPE_HUSBAND and
                        g_married == False):
                        player.options.append(Option(ITEM_TYPE_HUSBAND, 2, 
                                   item.x, item.y))
                        g_married = True
                        for item2 in items:
                            if item2.type == ITEM_TYPE_WIFE:
                                item2.exit()
                                break
                        g_ending.set_partner(ITEM_TYPE_HUSBAND)
                        g_sound.play_se(SE_TYPE_GET_ITEM)
                        items.remove(item)
                    elif item.type == ITEM_TYPE_CHILD:
                        player.options.append(Option(ITEM_TYPE_CHILD, len(player.options)+2,
                                  item.x, item.y))
                        g_ending.set_child(len(player.options)-1)
                        g_sound.play_se(SE_TYPE_GET_ITEM)
                        items.remove(item)
                    elif item.type == ITEM_TYPE_FORCE:
                        player.add_energy(100)
                        g_sound.play_se(SE_TYPE_GET_ITEM)
                        items.remove(item)
                    elif item.type == ITEM_TYPE_POISON:
                        player.add_energy(-100)
                        g_score += 10000
                        g_sound.play_se(SE_TYPE_GET_ITEM)
                        items.remove(item)

            # EFFECTの更新
            for effect in reversed(effects):
                if effect.update() == False:
                    effects.remove(effect)

            # 描画
            if g_pos > G_POS_GOAL:
                screen.fill(C_BLACK)
            else:
                c = (G_POS_GOAL - g_pos) / G_POS_GOAL * 255
                screen.fill((c,c,c))

            bg.draw()

            player.draw()
            for npc in npcs:
                npc.draw()
            for bullet in player_bullets:
                bullet.draw()
            for bullet in npc_bullets:
                bullet.draw()
            for item in items:
                item.draw()
            for effect in effects:
                effect.draw()

            g_pos -= 1
            if g_pos <= 0:
                game_state = ST_GAME_CLEAR
            if g_pos <= G_POS_GOAL:
                g_ending.update()
                g_ending.draw()

            show_ui_screen()

        elif game_state == ST_GAME_OVER_1:
            screen.fill(C_BLACK)
            player.draw()
            bg.draw()
            for bullet in npc_bullets:
                bullet.draw()
            game_over_count += 1
            if game_over_count > 60:
                game_over_count = 0
                game_state = ST_GAME_OVER_2
                npc_bullets.remove(game_over_npc_bullet)
                g_sound.stop_se()
            show_ui_screen()

        elif game_state == ST_GAME_OVER_2:
            show_game_over_screen()
            game_over_count += 1
            if game_over_count > 120:
                game_state = ST_TITLE
            show_ui_screen()

        pygame.display.flip()
        clock.tick(60)  # 60FPS

if __name__ == "__main__":
    main()