伊地知ニジカ放送局だぬ゛ん゛. 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.

684 lines
19 KiB

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