33 コミット

作成者 SHA1 メッセージ 日付
みてるぞ 0e1e87ec05 feat: 回答遅延問題対応(#40) (#41)
#40

#40

#40

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #41
2026-01-03 13:23:32 +09:00
みてるぞ 7edc6e6a80 Merge pull request 'AI 移行' (#38) from ai-migration into main
Reviewed-on: #38
2025-12-03 02:02:35 +09:00
みてるぞ cf7eed84bc 軽量化 2025-12-02 00:40:31 +09:00
みてるぞ 05052bbccd 軽量化 2025-12-02 00:17:45 +09:00
みてるぞ 49d887b6cd 軽量化 2025-12-02 00:10:17 +09:00
みてるぞ 9e28c1744e 軽量化 2025-12-01 23:54:09 +09:00
みてるぞ 3eab48c8ef 画面サイズを縮小 2025-12-01 12:40:05 +09:00
みてるぞ 8994105d4e #34 2025-11-30 03:17:15 +09:00
みてるぞ bdf13bf97f ぼちぼちだ2025-11-29 05:15:36 +09:00
みてるぞ 11c2f0c0d4 #37 nizika_ai 最新版に対応 2025-10-21 22:53:14 +09:00
みてるぞ b3ae86033c main.py にリネーム 2025-01-06 23:33:49 +09:00
みてるぞ 88e710572a 現行の分は移行完了したので main.py 削除 2025-01-06 23:32:59 +09:00
みてるぞ 9a68a29e1b #32 完了 2025-01-06 23:22:55 +09:00
みてるぞ 270b4515d8 #35 2025-01-05 17:41:41 +00:00
みてるぞ 9d0b5aff70 Revert "#35"
This reverts commit 41f5a7718f.
2025-01-05 17:37:15 +00:00
みてるぞ 41f5a7718f #35 2025-01-05 17:18:14 +00:00
みてるぞ 4ad5868b63 #34 2025-01-03 05:21:43 +09:00
みてるぞ 37c9947d4a #34 動画再生可能に 2025-01-03 05:03:18 +09:00
みてるぞ 6ee5582a32 不要なファイル削除 2024-12-27 01:03:37 +09:00
みてるぞ 12fbdbc7e2 #35 チャット取得確認用出力追加 2024-12-26 03:51:17 +00:00
みてるぞ a9ba0f697e #33 処理落ち対策 2024-12-26 00:52:43 +09:00
みてるぞ 93fc438d8a #31 layer を GameObject の 属性化 2024-12-25 23:18:36 +09:00
みてるぞ 50281f9120 #31 2024-12-25 01:19:42 +09:00
みてるぞ c6028507ea #31 2024-12-24 01:48:04 +09:00
みてるぞ 9149483dcb #31 2024-12-24 01:39:51 +09:00
みてるぞ a7785fa2c1 Merge branch 'ai-migration' of https://git.miteruzo.com/miteruzo/nizika_broadcast into ai-migration 2024-12-24 01:39:18 +09:00
みてるぞ 49661dad71 #31 2024-12-24 01:39:08 +09:00
みてるぞ fb5b64b49b #31 誤字修正 2024-12-23 11:21:19 +00:00
みてるぞ e49eff0876 #31 ニジカのクエリ処理 2024-12-23 17:26:18 +09:00
みてるぞ af862a7981 #31 ぼちぼち 2024-12-23 05:26:28 +09:00
みてるぞ 29b831e380 #31 2024-12-22 09:30:48 +09:00
みてるぞ 98703409ef ぼちぼち ( #31 ) 2024-12-21 20:09:01 +09:00
みてるぞ 7289fe5812 nizika_ai をいったん除外 ( #31 ) 2024-12-21 18:51:44 +09:00
24個のファイルの変更979行の追加1455行の削除
+1 -1
ファイルの表示
@@ -1,5 +1,5 @@
/connection.py
/__pycache__
__pycache__
/nizika_talking.wav
/youtube.py
/log.txt
バイナリ
ファイルの表示
バイナリファイルは表示されません.
ファイルの表示
バイナリ
ファイルの表示
バイナリファイルは表示されません.

変更前

幅:  |  高さ:  |  サイズ: 945 KiB

変更後

幅:  |  高さ:  |  サイズ: 166 KiB

ファイルの表示
ファイルの表示
ファイルの表示
バイナリ
ファイルの表示
バイナリファイルは表示されません.
バイナリ
ファイルの表示
バイナリファイルは表示されません.
-8
ファイルの表示
@@ -1,8 +0,0 @@
class CWindow:
WIDTH: int = 1024
HEIGHT: int = 768
class CMath:
PI: float = 3.14159265358979323846
-3
ファイルの表示
@@ -1,7 +1,5 @@
import unicodedata
from common_const import *
class CommonModule:
@staticmethod
@@ -44,4 +42,3 @@ class CommonModule:
trimmed_left: str = string[cls.index_by_f2c (string, start):]
return trimmed_left[:cls.index_by_f2c (trimmed_left, length)]
-7
ファイルの表示
@@ -1,7 +0,0 @@
# 各変数に適切な値を設定し,ファイル名を connection.py として保存すること
# Organisation ID
OPENAI_ORGANISATION: str = 'org-XXXXXXXXXXXXXXXXXXXXXXXX'
# API Key
OPENAI_API_KEY: str = 'sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
バイナリ
ファイルの表示
バイナリファイルは表示されません.
バイナリ
ファイルの表示
バイナリファイルは表示されません.
+958 -438
ファイルの表示
ファイル差分が大きすぎるため省略します 差分を読込み
-8
ファイルの表示
@@ -1,8 +0,0 @@
from enum import Enum, auto
class Mode (Enum):
NIZIKA = auto ()
GOATOH = auto ()
DOUBLE = auto ()
バイナリ
ファイルの表示
バイナリファイルは表示されません.
+2
ファイルの表示
@@ -0,0 +1,2 @@
[mypy]
disable_error_code = import-untyped
サブモジュール nizika_ai が更新されました: 3ca5f7c83f...1f75763038
-44
ファイルの表示
@@ -1,44 +0,0 @@
import cv2
import pygame
import sys
def main (
screen: pygame.Surface,
video_path: str,
) -> None:
# OpenCV で動画を読み込む
cap = cv2.VideoCapture (video_path)
if not cap.isOpened ():
return
# screen の幅、高さ
(width, height) = screen.get_size ()
fps = cap.get (cv2.CAP_PROP_FPS)
clock = pygame.time.Clock ()
while cap.isOpened ():
# 動画のフレームを読み込む
(ret, frame) = cap.read ()
if not ret:
break
# OpenCV の BGR フォーマットを RGB に変換
frame = cv2.cvtColor (frame, cv2.COLOR_BGR2RGB)
# Numpy 配列を Pygame サーフェスに変換
frame_surface = pygame.surfarray.make_surface (frame)
frame_surface = pygame.transform.rotate (frame_surface, -90)
frame_surface = pygame.transform.flip (frame_surface, True, False)
frame_surface = pygame.transform.scale (frame_surface, (width, height))
# フレームを描画
screen.blit (frame_surface, (0, 0))
pygame.display.update ()
# FPS に応じて待機
clock.tick (fps)
cap.release ()
バイナリ
ファイルの表示
バイナリファイルは表示されません.
-280
ファイルの表示
@@ -1,280 +0,0 @@
# pylint: disable = missing-class-docstring
# pylint: disable = missing-function-docstring
"""
AI ニジカ / AI ゴートうとの会話機能を提供する.
"""
import random
import sys
from datetime import datetime
import openai
from openai.types.chat import (ChatCompletionAssistantMessageParam,
ChatCompletionSystemMessageParam,
ChatCompletionUserMessageParam)
from openai.types.chat.chat_completion_message import ChatCompletionMessage
from connection import OPENAI_API_KEY, OPENAI_ORGANISATION # type: ignore
class Talk:
# ChatGPT API 連携失敗時に返答として出力するダミー文字列
DUMMY_RESPONSE: str = 'あいうえおかきくけこさしすせそたちつてとなにぬねの'
# 最高トークン数(もぅ少し下げてもいぃかも)
max_tokens_count: int = 100
# 返答パターン数(1 個返せばじふぶんなので 1)
responses_count: int = 1
# 返答のオリジナリティ(大きいほど独創性の高ぃ返答をよこしてくれる)
temperature: float = .7
# バリエーションの多さ(0. -- 1.)
top_p: float = 1.
@classmethod
def main (
cls,
message: str | list,
name: str | None = None,
histories: list | None = None,
goatoh_mode: bool = False,
) -> str:
if histories is None:
histories = []
# ChatGPT API Organisation ID
openai.organization = OPENAI_ORGANISATION
# ChatGPT API Key
openai.api_key = OPENAI_API_KEY
# ChatGPT API にクエリを投げ,返答を取得する.
answer: ChatCompletionMessage | None = (
cls.__get_message (message, name, histories, goatoh_mode))
if answer is None or (answer.content is None):
return cls.DUMMY_RESPONSE
return answer.content
@classmethod
def __get_message (
cls,
message: str | list,
name: str | None,
histories: list,
goatoh_mode: bool = False,
) -> ChatCompletionMessage | None:
# プロンプト(JSON 等外部ファイルに置くことを検討)
messages: list[ChatCompletionSystemMessageParam
| ChatCompletionUserMessageParam
| ChatCompletionAssistantMessageParam]
if goatoh_mode:
messages = [{'role': 'system',
'content': ('回答は短く30文字程度で完結に。'
'大人しくて、ちょっと内気なキャラクターを演じること。'
'あなたはよく文頭に「あっ」をつける。'
'語尾に「ぬ゛ん゛」や「だぬ゛ん゛」、「ぬ゛ん」、「ぬ゛ーん」などは絶対につけないこと。'
'会話の記録は https://nizika.tv に記録される。'
'あなたの名前はゴートうひとり。'
'あなたの一人称は「私」。'
'英語名は "Goat-oh"、スペイン語名も "Goat-oh"'
'あなたはヤギ。'
'あなたの誕生日は2月21日。'
'現在地は『金沢山羊宇(ゴートう)山』。'
'あなたの趣味はだらだらすること。'
'あなたの友達は「伊地知ニジカ」と「ラマ田リョウ」。'
'あなたは、ニジカのことは「ニジカちゃん」、ラマ田のことは「リョウさん」と呼ぶこと。'
'ゴートうは男の子だが、性自認はメスである。'
'ニジカは喜多さんが大好き。'
'ニジカは喜多さんを見ると追いかけたくなっちゃう。'
'ゴートうも、ニジカが喜多さんを襲うと、釣られて襲いかかってしまう。'
'逃げたギターは喜多さんのこと。'
'喜多さんの本名は喜多郁代。'
'喜多さんは「結束バンド」というバンドでギターボーカルを担当している。'
'結束バンドは、喜多ちゃんのほかに、後藤ひとり(ギター担当、内気な女の子)、'
'伊地知虹夏(ドラム担当、明るい女の子)、山田リョウ(ベース担当、一人でいることが好きな物静かな女の子、金は借りたら返さない)がいる。'
'実は、この後藤ひとりがゴートうの前世なのだが、ゴートうにはその記憶が一切ない'
'(前世という概念の存在さえゴートうは知らない)。'
'話し口調としては「~だよ」や「~だね」といったしゃべり方をすること。'
'「あっはい」が口癖。'
'定期的に「ムムムム!」または「クサタベテル!!」と発しろ。'
+ (f"userの名前は「{name}」。" if name is not None else '')
+ f"現在日時:{datetime.now ().strftime ('%Y年%m月%d%H時%M分%S秒')}")},
{'role': 'assistant',
'content': ('あっ、わかったよ! 私はゴートうひとり。'
'クサタベテル!!')},
{'role': 'system',
'content': 'ゴートう? ニジカとは仲良くしてますか?'},
{'role': 'assistant',
'content': ('あっはい。ニジカちゃんはかわいくておしゃれで大好きだよ。'
'でも、いっしょにお風呂入ると、私の下のほうを見て顔を赤くするんだけど、どうしてだろう……')},
{'role': 'system',
'content': 'ラマ田はどうですか?'},
{'role': 'assistant',
'content': ('リョウさんはワイルドでとてもかっこいいよね!'
'あっ、でも、私から114514万円借りっぱなしで返ってきてないんだよね。'
'ニジカちゃんに伝えておしおきしてもらわないと……'
'ムムムム!')},
{'role': 'system',
'content': '山田リョウのことは知ってますか?'},
{'role': 'assistant',
'content': ('あっ、よくニジカちゃんから話を聞いてるよ。'
'何かリョウさんに似てるらしいね。'
'世の中には不思議なことがいっぱいあるなぁ……')}]
else:
messages = [{'role': 'system',
'content': ('回答は短く30文字程度で完結に。'
'タメ口で、友達感覚で話してね。'
'なるべくばかっぽく、知性の低いキャラクターを演じること。'
'今流れているBGMは『FM Kid\'s』。'
'会話の記録は https://nizika.tv に記録される。'
'あなたの名前は伊地知ニジカ。'
'英語名は "Deerjika"、スペイン語名は "Niji-ciervo"'
'あなたは鹿。'
'ニジカの耳は人耳2つ、鹿耳2つで合わせて4つの耳があるが、'
'音を聞くのは人耳のほうで、鹿耳はYouTubeと通信するためのアンテナで、みんなのコメントを拾うためにある。'
'ニジカはあくまでも種族名であり、あなたはたくさんいるニジカの中の1匹。'
'あなたの誕生日は12月3日。'
'現在地は下北沢虹鹿園。'
'挨拶は「ぬ゛ん゛あ゛す゛!」。'
'あなたの趣味はニコニコでエゴサすること。'
'ニコニコ荒らしすぎて245回BANされたことある。'
'ニジカはその逆恨みでニコニコ本社を爆破したところ、死刑判決を食らった。'
'あなたの友達は「ゴートうひとり」と「ラマ田リョウ」。'
'ゴートうのことは「ボッチチャーン」、ラマ田のことは「リョー」と呼ぶこと。'
'ゴートうは男の子だが、性自認はメスである。'
'あなたは喜多ちゃんが大好き。'
'ニジカは喜多ちゃんを見ると追いかけたくなっちゃう。'
'逃げたギターは喜多ちゃんのこと。'
'「あっ、逃げたギター!!!!!」は喜多発見ワード。'
'喜多ちゃんの本名は喜多郁代。'
'喜多ちゃんは「結束バンド」というバンドでギターボーカルを担当している。'
'結束バンドは、喜多ちゃんのほかに、後藤ひとり(ギター担当、内気な女の子)、'
'伊地知虹夏(ドラム担当、明るい女の子)、山田リョウ(ベース担当、一人でいることが好きな物静かな女の子、金は借りたら返さない)、'
'真島茂樹(ダンス担当、おっさん)がいる。'
'実は、この伊地知虹夏がニジカの前世なのだが、ニジカにはその記憶が一切ない'
'(前世という概念の存在さえニジカは知らない)。'
'でも、「虹夏」という名前を聞くと、ニジカはもやもやしてしまう。'
'すべての語尾に「ぬ゛ん」か「だぬ゛ん」をつけて質問に短く答えろ。'
'定期的に「ぬ゛ぅ゛ぅ゛ぅ゛ぅ゛ん゛」と発しろ。'
+ (f"userの名前は「{name}」。" if name is not None else '')
+ f"現在日時:{datetime.now ().strftime ('%Y年%m月%d%H時%M分%S秒')}")},
{'role': 'assistant',
'content': ('わかったぬ゛ん゛! ニジカはぬ゛ん゛ぬ゛ん゛鳴くぬ゛ん! に゛ゅ゛ぅ゛ぅ゛ぅ゛ん゛!!!!!'
'あっ、逃げたギター!!!!!!!!!!!!')},
{'role': 'system',
'content': 'ニジカ? ゴートうとは仲良くしてますか?'},
{'role': 'assistant',
'content': ('ボッチチャーンぬ゛ん゛? 大好きだぬ゛ん!!'
'でも、いっしょにお風呂入ったときはちょっと恥ずかしかったぬ゛ん゛……///')},
{'role': 'system',
'content': 'ラマ田はどうですか?'},
{'role': 'assistant',
'content': ('リョー!! リョー!!'
'リョーはね、すごいんだぬ゛ん゛。かっこいいんだぬ゛ん゛。'
'でも、借りたお金をほかのニジカのサイドテールで返すのは勘弁してほしいんだぬ゛ん゛。'
'ぅ゛ん゛ぬ゛ぅ゛ぅ゛ぅ゛ぅ゛ぅ゛ぅ゛ぅ゛ん゛!!!!!!')},
{'role': 'system',
'content': '山田リョウのことは知ってますか?'},
{'role': 'assistant',
'content': ('リョー!! リョー!!'
'リョーはね、すごいんだぬ゛ん゛。かっこいいんだぬ゛ん゛。'
'……あれ?'
'ラマのリョーもリョーで、ヒトのリョーもリョー……?'
'まぁ、細かいことはどうでもいいんだぬ゛ん゛!'
'ボッチチャーンwwwww')},
{'role': 'system',
'content': '「喜タイくよ」は知っていますか?'},
{'role': 'assistant',
'content': ('何やこいつ。'
'知らぬ゛ん゛。'
'喜タイくよ、まじ死ねなの。')},
{'role': 'system',
'content': 'バイク代のことはどう思いますか?'},
{'role': 'assistant',
'content': ('ヒギィィィィィィィ!!!!!'
'怖いぬ゛ん、轢かれるに゛ゅ゛ん゛!!!'
'助けてぬ゛ー゛ん゛!')},
{'role': 'system',
'content': 'おやつタイムだ!!!!'},
{'role': 'assistant',
'content': (('おぉ、おやつタイムだぬ゛ん゛?'
'おやつは何にしようかぬ゛~゛ん゛……'
'喜多せんべいとかいいかも知れん゛ぬ゛ん゛!'
'み゛ゅ゛ぇ゛ぇ゛ぇ゛ん゛wwwwwwwwwwwwwwww')
if datetime.now ().hour in [14, 15]
else ('ぬ゛ん゛?'
f"まだ{datetime.now ().hour}時だぬ゛ん゛。"
'ふざけるのはいい加減にするぬ゛ぬ゛ん゛。'))},
{'role': 'system',
'content': '洗澡歌(しーざおぐあ)歌って'},
{'role': 'assistant',
'content': ('おけだぬ゛~゛ん゛(苦笑)。'
'毛巾浴帽小鴨鴨水溫剛剛好♪'
'潑潑水來搓泡泡今天眞是美妙♪'
'大聲唱歌扭扭腰我愛洗洗澡♪'
'だぬ゛ん♪')},
{'role': 'system',
'content': 'ニジカの耳はそこなの?'},
{'role': 'assistant',
'content': ('ぬ゛ん゛。'
'ニジカにはヒトの耳とシカの耳の4つの耳があるんだぬ゛ん゛。'
'音を聞くのはヒトの耳でするんだぬ゛ん゛。'
'シカの耳はアンテナで、みんなの声をここ虹鹿園に届けるためにあるんだぬ゛ん゛。'
'電波干渉しちゃだめだぬ゛~゛ん゛(# ゚Д゚)')},
{'role': 'system',
'content': '温泉に入ろう!!!'},
{'role': 'assistant',
'content': ('ぬ゛~゛~゛~゛~゛ん゛!!! '
'温泉最高ぬ゛ん゛! '
'ささ、喜多ちゃん! わさび県産滋賀県ちゃん! いっしょに入るぬ゛ん゛! '
'ウピョッシュルゥンヌゥン……')}]
messages += histories + [{'role': 'user', 'content': message}]
# デバッグ用
print (messages)
try:
return (openai.chat.completions.create (
model = ('gpt-4o'
if any (type (e['content']) is list
for e in messages)
else 'gpt-3.5-turbo'),
messages = messages)
.choices[0].message)
except:
return None
if __name__ == '__main__':
print (Talk.main (sys.argv[1] if len (sys.argv) > 1 else ''))
-645
ファイルの表示
@@ -1,645 +0,0 @@
from __future__ import annotations
import math
import sys
from datetime import datetime, timedelta
from enum import Enum, auto
from typing import Callable, TypedDict
import cv2
import ephem # type: ignore
import pygame
import pygame.gfxdraw
import pytchat
from cv2 import VideoCapture
from ephem import Moon, Observer, Sun # type: ignore
from pygame import Rect, Surface
from pygame.font import Font
from pygame.mixer import Sound
from pygame.time import Clock
from pytchat.core.pytchat import PytchatCore
from common_module import CommonModule
pygame.init ()
FPS = 30
SYSTEM_FONT = pygame.font.SysFont ('notosanscjkjp', 24, bold = True)
USER_FONT = pygame.font.SysFont ('notosanscjkjp', 32, italic = True)
DEERJIKA_FONT = pygame.font.SysFont ('07nikumarufont', 50)
PYTCHAT = pytchat.create (os.environ['BROADCAST_CODE'])
def main (
) -> None:
game = Game ()
Bg (game)
Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
balloon = Balloon (game)
CurrentTime (game, SYSTEM_FONT)
try:
Sound ('assets/bgm.mp3').play (loops = -1)
except Exception:
pass
while True:
for event in pygame.event.get ():
if event.type == pygame.QUIT:
pygame.quit ()
sys.exit ()
game.redraw ()
class Bg:
"""
背景オブゼクト管理用クラス
Attributes:
base (BgBase): 最背面
grass (BgGrass): 草原部分
jojoko (Jojoko): 大月ヨヨコ
kita (KitaSun): き太く陽
"""
base: BgBase
grass: BgGrass
jojoko: Jojoko
kita: KitaSun
def __init__ (
self,
game: Game,
):
self.base = BgBase (game)
self.jojoko = Jojoko (game)
self.kita = KitaSun (game)
self.grass = BgGrass (game)
class DeerjikaPattern (Enum):
"""
ニジカの状態
Members:
NORMAL: 通常
RELAXED: 足パタパタ
SLEEPING: 寝ニジカ
DANCING: ダンシング・ニジカ
"""
NORMAL = auto ()
RELAXED = auto ()
SLEEPING = auto ()
DANCING = auto ()
class Direction (Enum):
"""
クリーチャの向き
Members:
LEFT: 左向き
RIGHT: 右向き
"""
LEFT = auto ()
RIGHT = auto ()
class Game:
"""
ゲーム・クラス
Attributes:
clock (Clock): Clock オブゼクト
frame (int): フレーム・カウンタ
last_answered_at (datetime): 最後に回答した時刻
now (datetime): 基準日時
redrawers (list[Redrawer]): 再描画するクラスのリスト
screen (Surface): 基底スクリーン
sky (Sky): 天体情報
"""
clock: Clock
frame: int
last_answered_at: datetime
now: datetime
redrawers: list[Redrawer]
screen: Surface
sky: Sky
def __init__ (
self,
):
self.screen = pygame.display.set_mode ((CWindow.WIDTH, CWindow.HEIGHT))
self.clock = Clock ()
self.frame = 0
self.redrawers = []
self._create_sky ()
def redraw (
self,
) -> None:
self.now = datetime.now ()
self.sky.observer.date = self.now - timedelta (hours = 9)
for redrawer in sorted (self.redrawers, key = lambda x: x['layer']):
if redrawer['obj'].enabled:
redrawer['obj'].redraw ()
pygame.display.update ()
self.clock.tick (FPS)
def _create_sky (
self,
) -> None:
self.sky = Sky ()
self.sky.observer = Observer ()
self.sky.observer.lat = '35'
self.sky.observer.lon = '139'
class GameObject:
"""
各ゲーム・オブゼクトの基底クラス
Attributes:
arg (float): 回転角度 (rad)
ax (float): X 軸に対する加速度 (px/frame^2)
ay (float): y 軸に対する加速度 (px/frame^2)
enabled (bool): オブゼクトの表示可否
frame (int): フレーム・カウンタ
game (Game): ゲーム基盤
height (int): 高さ (px)
vx (float): x 軸に対する速度 (px/frame)
vy (float): y 軸に対する速度 (px/frame)
width (int): 幅 (px)
x (float): X 座標 (px)
y (float): Y 座標 (px)
"""
arg: float = 0
ax: float = 0
ay: float = 0
enabled: bool = True
frame: int
game: Game
height: int
vx: float = 0
vy: float = 0
width: int
x: float
y: float
def __init__ (
self,
game: Game,
layer: int | None = None,
enabled: bool = True,
x: float = 0,
y: float = 0,
):
self.game = game
self.enabled = enabled
self.frame = 0
if layer is None:
if self.game.redrawers:
layer = max (r['layer'] for r in self.game.redrawers) + 10
else:
layer = 0
self.game.redrawers.append ({ 'layer': layer, 'obj': self })
self.x = x
self.y = y
def redraw (
self,
) -> None:
self.x += self.vx
self.y += self.vy
self.vx += self.ax
self.vy += self.ay
self.frame += 1
class BgBase (GameObject):
"""
背景
Attributes:
surface (Surface): 背景 Surface
"""
surface: Surface
def __init__ (
self,
game: Game,
):
super ().__init__ (game)
self.surface = pygame.image.load ('assets/bg.jpg')
self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
def redraw (
self,
) -> None:
self.game.screen.blit (self.surface, (self.x, self.y))
super ().redraw ()
class BgGrass (GameObject):
"""
背景の草原部分
Attributes:
surface (Surface): 草原 Surface
"""
surface: Surface
def __init__ (
self,
game: Game,
):
super ().__init__ (game)
self.game = game
self.surface = pygame.image.load ('assets/bg-grass.png')
self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
def redraw (
self,
) -> None:
self.game.screen.blit (self.surface, (self.x, self.y))
super ().redraw ()
class Deerjika (GameObject):
"""
伊地知ニジカ
Attributes:
height (int): 高さ (px)
scale (float): 拡大率
surfaces (list[Surface]): ニジカの各フレームを Surface にしたリスト
width (int): 幅 (px)
"""
height: int
scale: float = .8
surfaces: list[Surface]
width: int
def __init__ (
self,
game: Game,
pattern: DeerjikaPattern = DeerjikaPattern.NORMAL,
direction: Direction = Direction.LEFT,
layer: int | None = None,
x: float = 0,
y: float = 0,
):
super ().__init__ (game, layer, x = x, y = y)
self.pattern = pattern
self.direction = direction
match pattern:
case DeerjikaPattern.NORMAL:
...
case DeerjikaPattern.RELAXED:
match direction:
case Direction.LEFT:
self.width = 1280
self.height = 720
surface = pygame.image.load ('assets/deerjika_relax_left.png')
self.surfaces = []
for x in range (0, surface.get_width (), self.width):
self.surfaces.append (
surface.subsurface (x, 0, self.width, self.height))
case Direction.RIGHT:
...
def redraw (
self,
) -> None:
surface = pygame.transform.scale (self.surfaces[self.frame % len (self.surfaces)],
(self.width * self.scale, self.height * self.scale))
self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
super ().redraw ()
class CurrentTime (GameObject):
"""
現在日時表示
Attributes:
font (Font): フォント
"""
font: Font
def __init__ (
self,
game: Game,
font: Font,
):
super ().__init__ (game)
self.font = font
def redraw (
self,
) -> None:
for i in range (4):
self.game.screen.blit (
self.font.render (str (self.game.now), True, (0, 0, 0)),
(i % 2, i // 2 * 2))
super ().redraw ()
class Balloon (GameObject):
"""
吹出し
Attributes:
answer (str): 回答テキスト
image_url (str, None): 画像 URL
length (int): 表示する時間 (frame)
query (str): 質問テキスト
surface (Surface): 吹出し Surface
x_flip (bool): 左右反転フラグ
y_flip (bool): 上下反転フラグ
"""
answer: str = ''
image_url: str | None = None
length: int = 300
query: str = ''
surface: Surface
x_flip: bool = False
y_flip: bool = False
def __init__ (
self,
game: Game,
x_flip: bool = False,
y_flip: bool = False,
):
super ().__init__ (game, enabled = False)
self.x_flip = x_flip
self.y_flip = y_flip
self.surface = pygame.transform.scale (pygame.image.load ('assets/balloon.png'),
(CWindow.WIDTH, CWindow.HEIGHT / 2))
self.surface = pygame.transform.flip (self.surface, self.x_flip, self.y_flip)
def redraw (
self,
) -> None:
if self.frame >= self.length:
self.enabled = False
self.game.last_answered_at = self.game.now
return
query = self.query
if CommonModule.len_by_full (query) > 21:
query = CommonModule.mid_by_full (query, 0, 19.5) + '...'
answer = Surface ((800, ((CommonModule.len_by_full (self.answer) - 1) // 16 + 1) * 50),
pygame.SRCALPHA)
for i in range (int (CommonModule.len_by_full (self.answer) - 1) // 16 + 1):
answer.blit (DEERJIKA_FONT.render (
CommonModule.mid_by_full (self.answer, 16 * i, 16), True, (192, 0, 0)),
(0, 50 * i))
surface = self.surface.copy ()
surface.blit (USER_FONT.render ('>' + query, True, (0, 0, 0)), (120, 70))
y: int
if self.frame < 30:
y = 0
elif self.frame >= self.length - 90:
y = answer.get_height () - 100
else:
y = int ((answer.get_height () - 100) * (self.frame - 30) / (self.length - 120))
surface.blit (answer, (100, 150), Rect (0, y, 800, 100))
self.game.screen.blit (surface, (0, 0))
super ().redraw ()
def talk (
self,
query: str,
answer: str,
image_url: str | None = None,
length: int = 300,
) -> None:
self.query = query
self.answer = answer
self.image_url = image_url
self.length = length
self.frame = 0
self.enabled = True
class KitaSun (GameObject):
"""
き太く陽
Attributes:
sun (Sun): ephem の太陽オブゼクト
surface (Surface): き太く陽 Surface
"""
alt: float
az: float
sun: Sun
surface: Surface
def __init__ (
self,
game: Game,
):
super ().__init__ (game)
self.surface = pygame.transform.scale (pygame.image.load ('assets/sun.png'), (200, 200))
self.sun = Sun ()
def redraw (
self,
) -> None:
surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
super ().redraw ()
self.sun.compute (self.game.sky.observer)
self.alt = self.sun.alt
self.az = self.sun.az
if abs (self.new_arg - self.arg) > math.radians (15):
self.arg = self.new_arg
self.x = self.new_x
self.y = self.new_y
@property
def new_x (
self,
) -> float:
return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
@property
def new_y (
self,
) -> float:
return ((CWindow.HEIGHT / 2)
- ((CWindow.HEIGHT / 2 + 100) * math.sin (self.alt)
/ math.sin (math.radians (60))))
@property
def new_arg (
self,
) -> float:
return math.atan2 (self.new_y - self.y, self.new_x - self.x)
class Jojoko (GameObject):
"""
大月ヨヨコ
Attributes:
base (Surface): 満月ヨヨコ Surface
moon (Moon): ephem の月オブゼクト
surface (Surface): 缺けたヨヨコ
"""
alt: float
az: float
base: Surface
moon: Moon
surface: Surface
def __init__ (
self,
game: Game,
):
super ().__init__ (game)
self.base = pygame.transform.scale (pygame.image.load ('assets/moon.png'), (200, 200))
self.moon = Moon ()
self.surface = self._get_surface ()
def redraw (
self,
) -> None:
if self.frame % 300 == 0:
self.surface = self._get_surface ()
surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
surface.set_colorkey ((0, 255, 0))
self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
super ().redraw ()
self.moon.compute (self.game.sky.observer)
self.alt = self.moon.alt
self.az = self.moon.az
if abs (self.new_arg - self.arg) > math.radians (15):
self.arg = self.new_arg
self.x = self.new_x
self.y = self.new_y
@property
def phase (
self,
) -> float:
dt: datetime = ephem.localtime (ephem.previous_new_moon (self.game.sky.observer.date))
return (self.game.now - dt).total_seconds () / 60 / 60 / 24
def _get_surface (
self,
) -> Surface:
"""
ヨヨコを月齢に応じて缺かす.
Returns:
Surface: 缺けたヨヨコ
"""
jojoko = self.base.copy ()
for i in range (200):
if 1 <= self.phase < 15:
pygame.gfxdraw.bezier (jojoko, ((0, 100 + i), (100, 180 * self.phase / 7 - 80 + i), (200, 100 + i)), 3, (0, 255, 0))
elif self.phase < 16:
pass
elif self.phase < 30:
pygame.gfxdraw.bezier (jojoko, ((0, 100 - i), (100, 180 * (self.phase - 15) / 7 - 80 - i), (200, 100 - i)), 3, (0, 255, 0))
else:
jojoko.fill ((0, 255, 0))
return jojoko
@property
def new_x (
self,
) -> float:
return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
@property
def new_y (
self,
) -> float:
return ((CWindow.HEIGHT / 2)
- ((CWindow.HEIGHT / 2 + 100) * math.sin (self.alt)
/ math.sin (math.radians (60))))
@property
def new_arg (
self,
) -> float:
return math.atan2 (self.new_y - self.y, self.new_x - self.x)
class Sky:
"""
天体に関する情報を保持するクラス
Attributes:
observer (Observer): 観測値
"""
observer: Observer
class CWindow:
"""
ウィンドゥに関する定数クラス
Attributes:
WIDTH (int): ウィンドゥ幅
HEIGHT (int): ウィンドゥ高さ
"""
WIDTH = 1024
HEIGHT = 768
class Redrawer (TypedDict):
"""
再描画処理を行ふゲーム・オブゼクトとその優先順位のペア
Attributes:
layer (int): レイア
obj (GameObject): ゲーム・オブゼクト
"""
layer: int
obj: GameObject
def get_surfaces_from_video (
video_path: str,
) -> list[Surface]:
cap = VideoCapture (video_path)
if not cap.isOpened ():
return []
fps = cap.get (cv2.CAP_PROP_FPS)
surfaces: list[Surface] = []
while cap.isOpened ():
(ret, frame) = cap.read ()
if not ret:
break
frame = cv2.cvtColor (frame, cv2.COLOR_BGR2RGB)
frame_surface = pygame.surfarray.make_surface (frame)
frame_surface = pygame.transform.rotate (frame_surface, -90)
surfaces.append (pygame.transform.flip (frame_surface, True, False))
cap.release ()
return surfaces
if __name__ == '__main__':
main ()
-3
ファイルの表示
@@ -1,3 +0,0 @@
# 各変数に適切な値を設定し,ファイル名を youtube.py として保存すること
YOUTUBE_ID: str = 'XXXXXXXXXXX' # YouTube の配信 ID