伊地知ニジカ放送局だぬ゛ん゛. https://www.youtube.com/@deerjika
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test.py 17 KiB

1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. from __future__ import annotations
  2. import math
  3. import sys
  4. from datetime import datetime, timedelta
  5. from enum import Enum, auto
  6. from typing import Callable, TypedDict
  7. import cv2
  8. import ephem # type: ignore
  9. import pygame
  10. import pygame.gfxdraw
  11. import pytchat
  12. from cv2 import VideoCapture
  13. from ephem import Moon, Observer, Sun # type: ignore
  14. from pygame import Rect, Surface
  15. from pygame.font import Font
  16. from pygame.mixer import Sound
  17. from pygame.time import Clock
  18. from pytchat.core.pytchat import PytchatCore
  19. from common_module import CommonModule
  20. pygame.init ()
  21. FPS = 30
  22. SYSTEM_FONT = pygame.font.SysFont ('notosanscjkjp', 24, bold = True)
  23. USER_FONT = pygame.font.SysFont ('notosanscjkjp', 32, italic = True)
  24. DEERJIKA_FONT = pygame.font.SysFont ('07nikumarufont', 50)
  25. PYTCHAT = pytchat.create (os.environ['BROADCAST_CODE'])
  26. def main (
  27. ) -> None:
  28. game = Game ()
  29. Bg (game)
  30. Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  31. balloon = Balloon (game)
  32. CurrentTime (game, SYSTEM_FONT)
  33. try:
  34. Sound ('assets/bgm.mp3').play (loops = -1)
  35. except Exception:
  36. pass
  37. while True:
  38. for event in pygame.event.get ():
  39. if event.type == pygame.QUIT:
  40. pygame.quit ()
  41. sys.exit ()
  42. game.redraw ()
  43. class Bg:
  44. """
  45. 背景オブゼクト管理用クラス
  46. Attributes:
  47. base (BgBase): 最背面
  48. grass (BgGrass): 草原部分
  49. jojoko (Jojoko): 大月ヨヨコ
  50. kita (KitaSun): き太く陽
  51. """
  52. base: BgBase
  53. grass: BgGrass
  54. jojoko: Jojoko
  55. kita: KitaSun
  56. def __init__ (
  57. self,
  58. game: Game,
  59. ):
  60. self.base = BgBase (game)
  61. self.jojoko = Jojoko (game)
  62. self.kita = KitaSun (game)
  63. self.grass = BgGrass (game)
  64. class DeerjikaPattern (Enum):
  65. """
  66. ニジカの状態
  67. Members:
  68. NORMAL: 通常
  69. RELAXED: 足パタパタ
  70. SLEEPING: 寝ニジカ
  71. DANCING: ダンシング・ニジカ
  72. """
  73. NORMAL = auto ()
  74. RELAXED = auto ()
  75. SLEEPING = auto ()
  76. DANCING = auto ()
  77. class Direction (Enum):
  78. """
  79. クリーチャの向き
  80. Members:
  81. LEFT: 左向き
  82. RIGHT: 右向き
  83. """
  84. LEFT = auto ()
  85. RIGHT = auto ()
  86. class Game:
  87. """
  88. ゲーム・クラス
  89. Attributes:
  90. clock (Clock): Clock オブゼクト
  91. frame (int): フレーム・カウンタ
  92. last_answered_at (datetime): 最後に回答した時刻
  93. now (datetime): 基準日時
  94. redrawers (list[Redrawer]): 再描画するクラスのリスト
  95. screen (Surface): 基底スクリーン
  96. sky (Sky): 天体情報
  97. """
  98. clock: Clock
  99. frame: int
  100. last_answered_at: datetime
  101. now: datetime
  102. redrawers: list[Redrawer]
  103. screen: Surface
  104. sky: Sky
  105. def __init__ (
  106. self,
  107. ):
  108. self.screen = pygame.display.set_mode ((CWindow.WIDTH, CWindow.HEIGHT))
  109. self.clock = Clock ()
  110. self.frame = 0
  111. self.redrawers = []
  112. self._create_sky ()
  113. def redraw (
  114. self,
  115. ) -> None:
  116. self.now = datetime.now ()
  117. self.sky.observer.date = self.now - timedelta (hours = 9)
  118. for redrawer in sorted (self.redrawers, key = lambda x: x['layer']):
  119. if redrawer['obj'].enabled:
  120. redrawer['obj'].redraw ()
  121. pygame.display.update ()
  122. self.clock.tick (FPS)
  123. def _create_sky (
  124. self,
  125. ) -> None:
  126. self.sky = Sky ()
  127. self.sky.observer = Observer ()
  128. self.sky.observer.lat = '35'
  129. self.sky.observer.lon = '139'
  130. class GameObject:
  131. """
  132. 各ゲーム・オブゼクトの基底クラス
  133. Attributes:
  134. arg (float): 回転角度 (rad)
  135. ax (float): X 軸に対する加速度 (px/frame^2)
  136. ay (float): y 軸に対する加速度 (px/frame^2)
  137. enabled (bool): オブゼクトの表示可否
  138. frame (int): フレーム・カウンタ
  139. game (Game): ゲーム基盤
  140. height (int): 高さ (px)
  141. vx (float): x 軸に対する速度 (px/frame)
  142. vy (float): y 軸に対する速度 (px/frame)
  143. width (int): 幅 (px)
  144. x (float): X 座標 (px)
  145. y (float): Y 座標 (px)
  146. """
  147. arg: float = 0
  148. ax: float = 0
  149. ay: float = 0
  150. enabled: bool = True
  151. frame: int
  152. game: Game
  153. height: int
  154. vx: float = 0
  155. vy: float = 0
  156. width: int
  157. x: float
  158. y: float
  159. def __init__ (
  160. self,
  161. game: Game,
  162. layer: int | None = None,
  163. enabled: bool = True,
  164. x: float = 0,
  165. y: float = 0,
  166. ):
  167. self.game = game
  168. self.enabled = enabled
  169. self.frame = 0
  170. if layer is None:
  171. if self.game.redrawers:
  172. layer = max (r['layer'] for r in self.game.redrawers) + 10
  173. else:
  174. layer = 0
  175. self.game.redrawers.append ({ 'layer': layer, 'obj': self })
  176. self.x = x
  177. self.y = y
  178. def redraw (
  179. self,
  180. ) -> None:
  181. self.x += self.vx
  182. self.y += self.vy
  183. self.vx += self.ax
  184. self.vy += self.ay
  185. self.frame += 1
  186. class BgBase (GameObject):
  187. """
  188. 背景
  189. Attributes:
  190. surface (Surface): 背景 Surface
  191. """
  192. surface: Surface
  193. def __init__ (
  194. self,
  195. game: Game,
  196. ):
  197. super ().__init__ (game)
  198. self.surface = pygame.image.load ('assets/bg.jpg')
  199. self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
  200. def redraw (
  201. self,
  202. ) -> None:
  203. self.game.screen.blit (self.surface, (self.x, self.y))
  204. super ().redraw ()
  205. class BgGrass (GameObject):
  206. """
  207. 背景の草原部分
  208. Attributes:
  209. surface (Surface): 草原 Surface
  210. """
  211. surface: Surface
  212. def __init__ (
  213. self,
  214. game: Game,
  215. ):
  216. super ().__init__ (game)
  217. self.game = game
  218. self.surface = pygame.image.load ('assets/bg-grass.png')
  219. self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
  220. def redraw (
  221. self,
  222. ) -> None:
  223. self.game.screen.blit (self.surface, (self.x, self.y))
  224. super ().redraw ()
  225. class Deerjika (GameObject):
  226. """
  227. 伊地知ニジカ
  228. Attributes:
  229. height (int): 高さ (px)
  230. scale (float): 拡大率
  231. surfaces (list[Surface]): ニジカの各フレームを Surface にしたリスト
  232. width (int): 幅 (px)
  233. """
  234. height: int
  235. scale: float = .8
  236. surfaces: list[Surface]
  237. width: int
  238. def __init__ (
  239. self,
  240. game: Game,
  241. pattern: DeerjikaPattern = DeerjikaPattern.NORMAL,
  242. direction: Direction = Direction.LEFT,
  243. layer: int | None = None,
  244. x: float = 0,
  245. y: float = 0,
  246. ):
  247. super ().__init__ (game, layer, x = x, y = y)
  248. self.pattern = pattern
  249. self.direction = direction
  250. match pattern:
  251. case DeerjikaPattern.NORMAL:
  252. ...
  253. case DeerjikaPattern.RELAXED:
  254. match direction:
  255. case Direction.LEFT:
  256. self.width = 1280
  257. self.height = 720
  258. surface = pygame.image.load ('assets/deerjika_relax_left.png')
  259. self.surfaces = []
  260. for x in range (0, surface.get_width (), self.width):
  261. self.surfaces.append (
  262. surface.subsurface (x, 0, self.width, self.height))
  263. case Direction.RIGHT:
  264. ...
  265. def redraw (
  266. self,
  267. ) -> None:
  268. surface = pygame.transform.scale (self.surfaces[self.frame % len (self.surfaces)],
  269. (self.width * self.scale, self.height * self.scale))
  270. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  271. super ().redraw ()
  272. class CurrentTime (GameObject):
  273. """
  274. 現在日時表示
  275. Attributes:
  276. font (Font): フォント
  277. """
  278. font: Font
  279. def __init__ (
  280. self,
  281. game: Game,
  282. font: Font,
  283. ):
  284. super ().__init__ (game)
  285. self.font = font
  286. def redraw (
  287. self,
  288. ) -> None:
  289. for i in range (4):
  290. self.game.screen.blit (
  291. self.font.render (str (self.game.now), True, (0, 0, 0)),
  292. (i % 2, i // 2 * 2))
  293. super ().redraw ()
  294. class Balloon (GameObject):
  295. """
  296. 吹出し
  297. Attributes:
  298. answer (str): 回答テキスト
  299. image_url (str, None): 画像 URL
  300. length (int): 表示する時間 (frame)
  301. query (str): 質問テキスト
  302. surface (Surface): 吹出し Surface
  303. x_flip (bool): 左右反転フラグ
  304. y_flip (bool): 上下反転フラグ
  305. """
  306. answer: str = ''
  307. image_url: str | None = None
  308. length: int = 300
  309. query: str = ''
  310. surface: Surface
  311. x_flip: bool = False
  312. y_flip: bool = False
  313. def __init__ (
  314. self,
  315. game: Game,
  316. x_flip: bool = False,
  317. y_flip: bool = False,
  318. ):
  319. super ().__init__ (game, enabled = False)
  320. self.x_flip = x_flip
  321. self.y_flip = y_flip
  322. self.surface = pygame.transform.scale (pygame.image.load ('assets/balloon.png'),
  323. (CWindow.WIDTH, CWindow.HEIGHT / 2))
  324. self.surface = pygame.transform.flip (self.surface, self.x_flip, self.y_flip)
  325. def redraw (
  326. self,
  327. ) -> None:
  328. if self.frame >= self.length:
  329. self.enabled = False
  330. self.game.last_answered_at = self.game.now
  331. return
  332. query = self.query
  333. if CommonModule.len_by_full (query) > 21:
  334. query = CommonModule.mid_by_full (query, 0, 19.5) + '...'
  335. answer = Surface ((800, ((CommonModule.len_by_full (self.answer) - 1) // 16 + 1) * 50),
  336. pygame.SRCALPHA)
  337. for i in range (int (CommonModule.len_by_full (self.answer) - 1) // 16 + 1):
  338. answer.blit (DEERJIKA_FONT.render (
  339. CommonModule.mid_by_full (self.answer, 16 * i, 16), True, (192, 0, 0)),
  340. (0, 50 * i))
  341. surface = self.surface.copy ()
  342. surface.blit (USER_FONT.render ('>' + query, True, (0, 0, 0)), (120, 70))
  343. y: int
  344. if self.frame < 30:
  345. y = 0
  346. elif self.frame >= self.length - 90:
  347. y = answer.get_height () - 100
  348. else:
  349. y = int ((answer.get_height () - 100) * (self.frame - 30) / (self.length - 120))
  350. surface.blit (answer, (100, 150), Rect (0, y, 800, 100))
  351. self.game.screen.blit (surface, (0, 0))
  352. super ().redraw ()
  353. def talk (
  354. self,
  355. query: str,
  356. answer: str,
  357. image_url: str | None = None,
  358. length: int = 300,
  359. ) -> None:
  360. self.query = query
  361. self.answer = answer
  362. self.image_url = image_url
  363. self.length = length
  364. self.frame = 0
  365. self.enabled = True
  366. class KitaSun (GameObject):
  367. """
  368. き太く陽
  369. Attributes:
  370. sun (Sun): ephem の太陽オブゼクト
  371. surface (Surface): き太く陽 Surface
  372. """
  373. alt: float
  374. az: float
  375. sun: Sun
  376. surface: Surface
  377. def __init__ (
  378. self,
  379. game: Game,
  380. ):
  381. super ().__init__ (game)
  382. self.surface = pygame.transform.scale (pygame.image.load ('assets/sun.png'), (200, 200))
  383. self.sun = Sun ()
  384. def redraw (
  385. self,
  386. ) -> None:
  387. surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
  388. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  389. super ().redraw ()
  390. self.sun.compute (self.game.sky.observer)
  391. self.alt = self.sun.alt
  392. self.az = self.sun.az
  393. if abs (self.new_arg - self.arg) > math.radians (15):
  394. self.arg = self.new_arg
  395. self.x = self.new_x
  396. self.y = self.new_y
  397. @property
  398. def new_x (
  399. self,
  400. ) -> float:
  401. return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
  402. @property
  403. def new_y (
  404. self,
  405. ) -> float:
  406. return ((CWindow.HEIGHT / 2)
  407. - ((CWindow.HEIGHT / 2 + 100) * math.sin (self.alt)
  408. / math.sin (math.radians (60))))
  409. @property
  410. def new_arg (
  411. self,
  412. ) -> float:
  413. return math.atan2 (self.new_y - self.y, self.new_x - self.x)
  414. class Jojoko (GameObject):
  415. """
  416. 大月ヨヨコ
  417. Attributes:
  418. base (Surface): 満月ヨヨコ Surface
  419. moon (Moon): ephem の月オブゼクト
  420. surface (Surface): 缺けたヨヨコ
  421. """
  422. alt: float
  423. az: float
  424. base: Surface
  425. moon: Moon
  426. surface: Surface
  427. def __init__ (
  428. self,
  429. game: Game,
  430. ):
  431. super ().__init__ (game)
  432. self.base = pygame.transform.scale (pygame.image.load ('assets/moon.png'), (200, 200))
  433. self.moon = Moon ()
  434. self.surface = self._get_surface ()
  435. def redraw (
  436. self,
  437. ) -> None:
  438. if self.frame % 300 == 0:
  439. self.surface = self._get_surface ()
  440. surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
  441. surface.set_colorkey ((0, 255, 0))
  442. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  443. super ().redraw ()
  444. self.moon.compute (self.game.sky.observer)
  445. self.alt = self.moon.alt
  446. self.az = self.moon.az
  447. if abs (self.new_arg - self.arg) > math.radians (15):
  448. self.arg = self.new_arg
  449. self.x = self.new_x
  450. self.y = self.new_y
  451. @property
  452. def phase (
  453. self,
  454. ) -> float:
  455. dt: datetime = ephem.localtime (ephem.previous_new_moon (self.game.sky.observer.date))
  456. return (self.game.now - dt).total_seconds () / 60 / 60 / 24
  457. def _get_surface (
  458. self,
  459. ) -> Surface:
  460. """
  461. ヨヨコを月齢に応じて缺かす.
  462. Returns:
  463. Surface: 缺けたヨヨコ
  464. """
  465. jojoko = self.base.copy ()
  466. for i in range (200):
  467. if 1 <= self.phase < 15:
  468. pygame.gfxdraw.bezier (jojoko, ((0, 100 + i), (100, 180 * self.phase / 7 - 80 + i), (200, 100 + i)), 3, (0, 255, 0))
  469. elif self.phase < 16:
  470. pass
  471. elif self.phase < 30:
  472. pygame.gfxdraw.bezier (jojoko, ((0, 100 - i), (100, 180 * (self.phase - 15) / 7 - 80 - i), (200, 100 - i)), 3, (0, 255, 0))
  473. else:
  474. jojoko.fill ((0, 255, 0))
  475. return jojoko
  476. @property
  477. def new_x (
  478. self,
  479. ) -> float:
  480. return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
  481. @property
  482. def new_y (
  483. self,
  484. ) -> float:
  485. return ((CWindow.HEIGHT / 2)
  486. - ((CWindow.HEIGHT / 2 + 100) * math.sin (self.alt)
  487. / math.sin (math.radians (60))))
  488. @property
  489. def new_arg (
  490. self,
  491. ) -> float:
  492. return math.atan2 (self.new_y - self.y, self.new_x - self.x)
  493. class Sky:
  494. """
  495. 天体に関する情報を保持するクラス
  496. Attributes:
  497. observer (Observer): 観測値
  498. """
  499. observer: Observer
  500. class CWindow:
  501. """
  502. ウィンドゥに関する定数クラス
  503. Attributes:
  504. WIDTH (int): ウィンドゥ幅
  505. HEIGHT (int): ウィンドゥ高さ
  506. """
  507. WIDTH = 1024
  508. HEIGHT = 768
  509. class Redrawer (TypedDict):
  510. """
  511. 再描画処理を行ふゲーム・オブゼクトとその優先順位のペア
  512. Attributes:
  513. layer (int): レイア
  514. obj (GameObject): ゲーム・オブゼクト
  515. """
  516. layer: int
  517. obj: GameObject
  518. def get_surfaces_from_video (
  519. video_path: str,
  520. ) -> list[Surface]:
  521. cap = VideoCapture (video_path)
  522. if not cap.isOpened ():
  523. return []
  524. fps = cap.get (cv2.CAP_PROP_FPS)
  525. surfaces: list[Surface] = []
  526. while cap.isOpened ():
  527. (ret, frame) = cap.read ()
  528. if not ret:
  529. break
  530. frame = cv2.cvtColor (frame, cv2.COLOR_BGR2RGB)
  531. frame_surface = pygame.surfarray.make_surface (frame)
  532. frame_surface = pygame.transform.rotate (frame_surface, -90)
  533. surfaces.append (pygame.transform.flip (frame_surface, True, False))
  534. cap.release ()
  535. return surfaces
  536. if __name__ == '__main__':
  537. main ()