ソースコード
"""
#漢字シューター
#第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()