778a0aの日記

戦略シミュレーションゲーム開発、本の感想、ソフトウェア技術についてなど

2024年7月~9月の振り返り

前の投稿から随分と間が空いてしまいました。一度リズムを崩すと大変というか、定期的に投稿する習慣をつけないといけないですね...。

年初に立てた目標に対して、7月から9月までの取り組みを振り返ります。

ゲーム開発

戦略シミュレーションゲームを完成させてSteamまたはPlayストアで公開することを第一とする。また、そのための足がかりとして、まずは既存のゲーム作品をそっくり真似た習作ゲームを3個作る。

主要な結果

  • 戦略シミュレーションゲームを完成させ、ストアに公開する
  • 習作ゲームを3個作る

この3ヶ月間(7月~9月)は、「betrayer」という戦略ゲームの英語版をitch.ioでリリースしたのが主な成果でした。その後は2作目のゲームの開発に取り掛かっていますが、中だるみもあったりで11月現在まだ完成には至っていません。年内の完成も厳しいかもですが、せめてβ版をunityroomでリリースするぐらいはしたいなと思っています。

今作っているゲームはこんな感じ↓のヘックスベースの戦略ゲームです。(11月中にゲームシステムを粗方完成させて、12月にUIを作ってバランス調整を行ってリリースできれば...)

開発中のゲーム画面

健康

運動習慣を定着させる。健康的な食事・生活サイクルを維持することも心がける。まずは続けられることを第一の目標とする。

主要な結果

  • 80%以上の週(41週以上)で週に3回・10分以上の運動を行う
  • 70%以上の日で11時までに就寝する

具体的な数値目標の達成状況は以下の通りでした。(期間は7月1日から9月30日まで)

  • 週3回10分以上の運動・・・92.3%達成(12週/13週)
  • 11時までに就寝・・・59.7%達成(55日/92日)

数値的にはぼちぼちでした。その他の取り組み・思ったこと:

  • 早寝早起き・朝型の生活習慣を取り入れようとしたのですが、なかなか寝付けない・夜中に目が覚めてしまう・早起きしても結局だらだらして時間を浪費してしまう等々、いろいろ微妙だったので断念しました
  • 運動習慣がしっかり定着してきたのは良かったものの、週5で運動するようになってゲーム開発の時間が削れたり、良いペースで体重減少させようとして食事が少なくなってエネルギー不足でゲーム開発が捗らなかったりと、健康以外のパフォーマンスが微妙になっていました

次の3ヶ月間は、ゲーム開発のパフォーマンスを上げることを第一に考え、健康関連の取り組みはほどほどにします。

情報発信

一年を通して継続的にこのブログ上で記事投稿を行う。まずは続けられることを第一の目標とする。海外での情報発信(主にゲーム開発関連)についての情報収集も行う。

主要な結果

  • このブログに10記事以上投稿する
    • 技術系、ガジェット系、本の感想系あたりの記事を想定
  • 海外のゲーム開発者コミュニティの文化を調べてまとめる
    • Reddit、itch.io、Unityフォーラムあたりを想定

7月に4本記事を投稿してから投稿が途絶えていました。これからまた再開していきます。

あと、9月末になんとなく思い立ってブログを独自ドメインに移行してみました(778a0a.hatenablog.com → blog.778a0a.com)。その結果、移行前は月300PV近くあったのが(ほぼGoogleからの検索流入)、60PVぐらいに激減してしまいました😇

PV推移

まあぼちぼち投稿を続けていけばまた回復するかなと思っています。

あと、いま世間で流行っているらしいマイクロブログ(死語?)を始めてみました(pub.778a0a.com)。ほぼ全て個人用のメモ書き・つぶやきですが、これからしようと思っていることをネット上に放流することで、ちょっとした有言実行感を出して自分を追い込んでパフォーマンスを上げようというのが狙いです。

あと、betrayerの英語版をRedditの r/indiegames に投稿してみました(投稿)。親切な人が一人コメントをくれたぐらいであんまり反響はありませんでした。

Redditの投稿・PV

1回の投稿でなにか分かるものでもないですが、まあUIは貧相で英語も怪しいのでこんなものかと思いました。そして、目の肥えた海外ユーザーの注目を得るようになるところまでゲームを作り込んで、PR力も身につけていくのは大変だろうなと思いました。

そんなことなどもあり(?)、来年からはまず真面目にX(Twitter)アカウントの運用を始めていこうと思いました。また、海外の情報発信は置いておいて、まずは日本での情報発信に専念しようと思いました。

その他

良い習慣づくりを意識する。毎月何かしらの習慣目標を掲げ、その達成・習慣化を目指す。直近ではAWS資格試験合格を目指す。その他は状況に応じて設定していく。

主要な結果

  • 毎月何かしらの習慣目標を掲げ、達成・習慣化を目指す
  • AWS認定資格試験に4個以上合格する
  • 40冊以上本を読む

習慣目標

習慣目標は、健康の項目でも書いた通り、早寝早起き・朝型化の習慣を身に着けようとしましたが、微妙だったので止めました。

次の3ヶ月間は、遅れ気味のゲーム開発に集中します。資格試験勉強をする時間はなさそうです。

読書

この3ヶ月間は15冊の本を読めました。読んだ本は以下のとおりです。ついでに5段階評価もつけてみます。

  • ★★★★☆ 世界標準の科学的トレーニング 今日から始める「タバタトレーニング」
  • ★★★★☆ 科学的に正しい筋トレ
  • ★★★★☆ インストラクショナルデザイン
  • ★★★★☆ 生活の世界歴史 1 古代オリエントの生活
  • ★★★★☆ 古代オリエントの宗教
  • ★★★★☆ 重耳 上・中・下
  • ★★★★☆ 時間栄養学入門
  • ★★★★☆ パフォーマンス・マネジメント
  • ★★★☆☆ 夜間飛行
  • その他4冊

本の感想記事も滞っているので、ゲーム開発が落ち着いたら...というといつまで経っても投稿できない気がするので、ゲーム開発と並行して気軽な気持ちで投稿していければと思います。

まとめ

いくつか成果はありつつも、ゲーム開発と情報発信が滞り気味だったのが大きな反省点です。

どちらも地道に続けていくことが大事だと思うので、10月~12月は心を入れ替えて頑張っていきます。(ということを11月後半に書いている模様)

以上です。

2024年4月~6月の振り返り

年初に立てた目標に対して、4月から6月までの取り組みを振り返ります。

ゲーム開発

戦略シミュレーションゲームを完成させてSteamまたはPlayストアで公開することを第一とする。また、そのための足がかりとして、まずは既存のゲーム作品をそっくり真似た習作ゲームを3個作る。

主要な結果

  • 戦略シミュレーションゲームを完成させ、ストアに公開する
  • 習作ゲームを3個作る

この3ヶ月間(4月~6月)で、「betrayer」というDeserter's2ライクなゲームの開発を行い、unityroomにてリリースできました。(リリース記事

次の3ヶ月間(7月~9月)では、betrayerの英語対応・モバイル対応を行いつつ、もう一つ新しい習作ゲームも開発してリリースできればと思います。その次の3ヶ月間(10月~12月)には3個目のゲームを開発し、各種ストアで販売できればと思います。

健康

運動習慣を定着させる。健康的な食事・生活サイクルを維持することも心がける。まずは続けられることを第一の目標とする。

主要な結果

  • 80%以上の週(41週以上)で週に3回・10分以上の運動を行う
  • 70%以上の日で11時までに就寝する

具体的な数値目標の達成状況は以下の通りでした。(期間は4月1日から6月31日まで)

  • 週3回10分以上の運動・・・91.7%達成(11週/12週)
  • 11時までに就寝・・・59.3%達成(54日/91日)

第1四半期と同様にぼちぼちでした。開発がノッているときに夜更かししてしまいがちなのが課題でした。睡眠や運動の本を読んで、睡眠の大事さや効果的な運動のやり方などの知識を身に着けました。

次の3ヶ月間は、夜ふかしの防止のため早寝早起き・朝型の生活習慣を導入しようと思っています。

情報発信

一年を通して継続的にこのブログ上で記事投稿を行う。まずは続けられることを第一の目標とする。海外での情報発信(主にゲーム開発関連)についての情報収集も行う。

主要な結果

  • このブログに10記事以上投稿する
    • 技術系、ガジェット系、本の感想系あたりの記事を想定
  • 海外のゲーム開発者コミュニティの文化を調べてまとめる
    • Reddit、itch.io、Unityフォーラムあたりを想定

この3ヶ月間のブログ記事投稿数は2個だけでした。ゲーム開発に集中するため記事投稿はほぼ断念していました。

次の3ヶ月間は、月一の投稿ペースを取り戻しつつ、今後開発するゲームのPRのため海外での情報発信も始めていこうと思っています。まずはRedditで活動を行っていく予定です。

以下が4月から6月までに投稿した記事です。

その他

良い習慣づくりを意識する。毎月何かしらの習慣目標を掲げ、その達成・習慣化を目指す。直近ではAWS資格試験合格を目指す。その他は状況に応じて設定していく。

主要な結果

  • 毎月何かしらの習慣目標を掲げ、達成・習慣化を目指す
  • AWS認定資格試験に4個以上合格する
  • 40冊以上本を読む

習慣目標

この3ヶ月間は毎日ゲーム開発に少しでも取り組むという習慣目標を立てて、実際にほぼ毎日ゲーム開発を進められ、無事ゲームのリリースまでできました。

次の3ヶ月間は、朝型生活の定着のために、毎朝(早起きして)机で何かしらの作業を行うという習慣目標を立てて頑張っていきます。

読書

この3ヶ月間は10冊の本を読めました。読んだ本は以下のとおりです。

  • 小説十八史略 6
  • 仏教の思想 11 古仏のまねび 道元
  • 働くあなたの快眠地図
  • 睡眠こそ最強の解決策である
  • 直観脳
  • アーサー王ここに眠る
  • 人生の短さについて 他2編
  • ウォーキングの科学
  • 呼吸の科学
  • その他1冊

持続的に開発を続けるために健康関連の本を多めに読みました。どの本も良い本でした。近いうちに感想記事を投稿できればと思います。

次の3ヶ月も最低10冊は読むのを目標にしつつ、せっかくなので気軽な気持ちで感想記事も投稿していければと思っています。

まとめ

この3ヶ月間は、ゲーム開発を集中して進めて無事一本ゲームをリリースできたのが何よりでした。次の3ヶ月間も同様のペースでゲーム開発に取り組み、もう一本ゲームをリリースできればと思います。そしてそんな開発を支えるためにも、より良い健康習慣を身に着けていければと思います。

順調なゲーム開発の一方で、情報発信はややおろそかになっているので、気軽に投稿する習慣を取り戻したいと思います。また、海外での情報発信も徐々に始めていければと思います。

以上です。

betrayer: 英語版をitch.ioにてリリースしました

戦略シミュレーションゲームの習作「betrayer」をitch.ioにてリリースしました。

以下のリンク先で遊べます。
https://778a0a.itch.io/betrayer

今後、karmaを貯めてRedditでもPRできればと思います。

英語版の画面

補足など

英語化にはUnity公式のLocalizationパッケージを利用しました。ソース中から日本語文字列を抜き出して一つのCSVファイルにまとめて、GitHub Copilotで英語部分を補完してもらい、Localizationパッケージのインポート機能で読み込む、という形で英語リソースを作成しました。

英語化のためのレイアウトの調整や、Localization利用のためのソースコードの書き換えなどがやや面倒でしたが、大きな困難はなく英語化できました。

日本語版ではBIZUDゴシックを使っていたのですが、アルファベットが等幅で読みにくいため、Noto Sans Japaneseに変更しました。

今後は練習のため以下の作業を行っていこうと思います。

  • 日本語・英語切り替え機能追加
  • ゲームパッド対応
  • モバイル対応
  • SteamやGoogle Play Store、App Storeでのリリース

モバイル対応までできたら、次のゲーム開発(NeoDeserter's PLUSっぽいゲームの作成)に進もうと思います。

以上です。

betrayer: 開発過程のまとめ

先日リリースした戦略シミュレーションゲームの習作「betrayer」について、記憶が新しいうちに開発過程をまとめておきます。ほとんど個人用のメモです。

おおまかには、2月からぽつぽつとゲームのことを考えたり技術調査を行ったりして、3月末から本格的な開発に入り、4月・5月はほぼ毎日開発を進めて、少し間が空いて6月末に仕上げてリリースという流れでした。

2月~3月 | コミットログ

2月から3月のうち実際にゲーム作りを進めたのは3、4日程度でした。ただ、頭の中ではゲームについて色々考えていて、お手本であるDeserter's2を遊んで仕様理解を深めたり(マップの広さは?登場キャラは何人?どんなキャラがいる?UIはどんな構成?などなど)、UIはどうやって作ればいいか(uGUI vs UI Toolkitなどなど)を考えたりしていました。

2月頃: UIについての検討

UIをuGUIではなくUI Tookitを使って作ることにしました。UI Toolkitは、まだまだこなれていない感はありますが、あまりリッチなエフェクト等を必要としない、こまごまとした表示要素の多い戦略シミュレーションゲームとの相性は良かったと思います。最新のUI Toolkitの機能であるBindingが気になってUnity 2023.2で開発を始めました(結局Bindingは使わず)。開発の途中でUnity 6 Previewに移行しました。

UI ToolkitのBindingは、少しは調べたものの、(1)結局イベントの登録は(現実的に)できないのでC#側でUQueryを使って要素を取得して設定する必要があって微妙、(2)UI BuilderでのBindingの設定の操作性・生産性もそんなに良いわけではない、(3)UXMLを直接編集するのも、補完は効かないしUI Builderを触ると強制フォーマットされるのでやりづらい、という理由から使わないことにしました。

代わりにRosalinaというUI Toolkit用のコード自動生成ツールを使うことにしました。Rosalinaは、UXMLで名前をつけた要素を定義・取得するコードが書かれたクラスを自動生成してくれるツールです(Windows FormsでいうDesigner.csのようなものを作ってくれるやつです)。このツールのおかげでUI ToolkitによるUI開発の効率が大分上がりました。

参考リンク

3月末頃: 本格開始

3月末頃から本格的に開発に着手しました。どこから手を付けていいか分からないので、(Desrter's2のUIという)分かりやすいお手本のあるタイトル画面やセーブデータ画面を作ろうとしたものの、気を取り直してゲーム進行のコアの部分、各フェイズ(戦略・個人・軍事)の実装を進めていきました。

この選択は正しかったと思います。あんまり面白みのないUI作りをコツコツ進めていくよりは、なるべく早くゲームのコア部分を作って、どんなに小さくてもいいので実際にゲームが動いていくところを見ていく方が、開発のモチベーションの維持につながったと思います。

当時のメモ:

2024-03-26 火
各フェイズの実装を進められた。
戦略シミュレーションゲームの作り方が全然見当もつかなくて、今まではとりあえずUIから作ればいいかなと思ったけどこれは微妙だったかもしれない。データ構造や処理の流れをまず作っていくのが正解な気がする。なので良いスタートを切れた。

4月~5月 | コミットログ

毎日少しずつでも開発を進めるという目標を立てて開発を進めていきました。以下がGitHubの緑化状況です。

1月から6月にかけてのGitHubのContribution Graphの状況

4月初旬: マップ・各フェイズ処理の作成

OpenAIのDALL·E 3にマップ画像を作ってもらい、Tilemapを使って国を配置していきました。Tilemapを使うのは初めてでしたが、エディター機能が便利で地形や河川の情報もTilemapで配置しました。DALL·E 3に作ってもらったマップ画像もとても良くて、開発のモチベーションが上がったことを覚えています。

4月初旬頃の開発画面

4月中旬: キャラデータ作成

チャットAI(Claude 3 Opus)に人物名を100名ぐらい生成してもらってランダムに国に配置し、画像生成AI(aipicasso/emi)で色々な特徴のキャラ画像をたくさん生成して良さげな画像を各キャラに設定して能力値を調整していきました。このあたりは別の記事で詳しく紹介できればと思います。(キャラ画像生成のコード

生成したキャラ画像の例

4月下旬~5月末: 各種アクション・UI・AI実装

色々な行動のUIやAIプレーヤーの動作などをコツコツ実装していきました。

5月末時点で、Unity Editor上でなら一応なんとなくは遊べるレベルになりました。

5月末時点のゲーム画面

6月: 仕上げ・リリース | コミットログ

なんとしてでも6月中にリリースしようと思っていたので、頑張って仕上げを行っていきました。

兵士画像をAsepriteで描いて、セーブ・ロード機能を作って、こまごまとした調整・バグ修正を行って、面倒臭くて最後まで手つかずだった決戦と反乱の処理を実装して、6月末にunityroomにてリリースできました。

最後の最後、6月30日23時ぐらいになって、UI ToolkitのPanelSettingsの「PanelScale Mode」を「Scale With Screen Size」にしているのに画面解像度1920x1080と960x540では微妙に表示が違っていて、unityroom上だとレイアウト崩れが多発することが分かって慌てましたが、ギリギリ修正できました。あまりギチギチにレイアウトするのはやめようと思いました。

7月~現在 | コミットログ

リリース後、真面目に何度かテストプレイをしてバグを直したり調整を行ったりしました。

なんとかそれなりに遊べるものになったようで、unityroom上でクリア報告や面白いというコメントをいただけてありがたいかぎりです。(7月4日現在、unityroom上の表示はプレイ数522、評価数4、コメント数6(作者返信3つ含む)でした)

テストプレイ結果1(クリア画面)

テストプレイ結果2

(余談ですが、フリージアで統一できたらbetrayer上級者だと思います。更にノーセーブでクリアできたらめちゃすごいと思います)

今後について

リリース記事にも書きましたが、次はまた練習のために「NeoDeserter's PLUS」というゲームを真似た習作ゲーム第二弾を作ってみようと思います。このゲームも面白くて、初めて遊んだときから自分の手でこんなゲームを作りたいとずっと思っていました。また3ヶ月後の9月末のリリースを目指して頑張っていこうと思います。

一方で、後々オリジナルのゲームを作ってリリースしていくために、上記のゲーム開発と並行してbetrayerに対して多言語化対応や、ゲームパッド対応、スマホ対応を行なってみようとも思います。これらがうまく行けばitch.ioや各種ストアでも無料公開できればと思います。(とはいえ習作ゲーム第二弾の開発を優先するつもりです)

終わりに

このゲームを作りはじめたときは、戦略シミュレーションゲームの作り方なんて皆目見当もつかず、完成する見通しも全然見えない状態でした。それでもエイヤで目標を立てて6月末までにリリースすると決めて、実際になんとか達成することができてホッとしています。

毎日、たった1秒だけでもいいからPCの前に座って開発を進めようと思って、気が乗らないときも疲れたときもほぼ毎日なんとかコツコツ開発を続けてきたこと、(4月に入ってから)この3ヶ月間でゲームが一本作れなければ、一生自分はゲームを作れないという覚悟(?)をもって取り組んだこと、などが良かったのかなと思います。

まだまだ作りたいと思うゲームはたくさんあります。そして、既存のゲームを真似するだけでなく、いつか自分にしか作れないメチャクチャ面白いゲームを作ってみたいという思いもあります。そんなゲームを作るためにも、これからもまだまだ頑張っていこうと思います。

以上です。

betrayer: 戦略シミュレーションゲームの習作をunityroomにてリリースしました

今年の2月から6月にかけて開発していた戦略シミュレーションゲームの習作「betrayer」をunityroomにてリリースしました。

以下のリンク先で遊べます。 https://unityroom.com/games/778a0a_strategy1

ソースコードは以下にあります。(MIT License) https://github.com/778a0a/betrayer

本記事ではbetrayerのゲーム内容の概要を紹介します。

概要

betrayerは、ターン制の戦略シミュレーションゲームです。

一言でいうと「Deserter's2」という名作戦略シミュレーションゲームをそっくり真似た作りになっています。

戦略フェイズ画面

プレーヤーは各勢力の君主や家臣や浪士の中から操作するキャラを一人選び、自身の勢力の世界統一を目指します。

キャラの身分によって取れる行動が変わります。君主になって配下を上手く使って統一を目指すもよし、家臣になって君主を補佐しあるいは反逆するもよし、浪士になって気ままに世界の行く末を見守るもよし、という感じです。

戦闘は、攻守のキャラの能力値、兵士のレベル、地形などをもとに半自動で行われます。

戦闘画面

セーブ・ロード機能もあり、1ターンは1、2分程度で終わるのでスキマ時間などに手軽に遊んでもらえればと思っています。君主プレイの場合は上手くいけば30分程度でクリアまでいけるかもしれません。

概要は以上です。

終わりに

なんとか約5ヶ月でリリースまで漕ぎ着けることができてホッとしています。次はまた練習のために「NeoDeserter's PLUS」というゲームを真似たゲームを作ってみようと思います。こちらもまたとても面白いゲームですので。

また、せっかくなので近いうちにbetrayerの技術的側面や開発過程を紹介する記事も投稿できればと思います。

以上です。

2024年1月~3月の振り返り

年初に立てた目標に対して、1月から3月までの取り組みを振り返ります。

ゲーム開発

戦略シミュレーションゲームを完成させてSteamまたはPlayストアで公開することを第一とする。また、そのための足がかりとして、まずは既存のゲーム作品をそっくり真似た習作ゲームを3個作る。

主要な結果

  • 戦略シミュレーションゲームを完成させ、ストアに公開する
  • 習作ゲームを3個作る

この3ヶ月間(1月~3月)は後述するAWS資格試験を最優先にしていたため、ゲーム開発の進捗は多くありませんでした。試験勉強の合間になんとか少しずつでも取り組もうと思いつつ、実際に取り組めたのは数日程度でした。

次の3ヶ月間(4月~6月)はゲーム開発に最優先で取り組み、最低でも1個、できれば2個の習作ゲームを作り、可能ならunityroomかitch.ioあたりで公開できればと思います。今年の後半には習作ではない本格的なゲームを完成させ、SteamかPlayストアあたりで公開できればと思います。

今は下記画像のような感じでDeserter's2DXという戦略ゲームによく似た習作ゲームを作っています。

開発中のゲームの画面

健康

運動習慣を定着させる。健康的な食事・生活サイクルを維持することも心がける。まずは続けられることを第一の目標とする。

主要な結果

  • 80%以上の週(41週以上)で週に3回・10分以上の運動を行う
  • 70%以上の日で11時までに就寝する

具体的な数値目標の達成状況は以下の通りでした。(期間は1月7日から3月31日まで)

  • 週3回10分以上の運動・・・83.3%達成(10週/12週)
  • 11時までに就寝・・・63.5%達成(54日/85日)

ぼちぼちでした。とはいえまだ運動習慣・健康的な食事・生活サイクルが定着しているとは言い難いです。

次の3ヶ月間は他の大事な目標に優先して取り組んでいきつつ、引き続き健康的な習慣づくりも意識して過ごしていければと思います。

情報発信

一年を通して継続的にこのブログ上で記事投稿を行う。まずは続けられることを第一の目標とする。海外での情報発信(主にゲーム開発関連)についての情報収集も行う。

主要な結果

  • このブログに10記事以上投稿する
    • 技術系、ガジェット系、本の感想系あたりの記事を想定
  • 海外のゲーム開発者コミュニティの文化を調べてまとめる
    • Reddit、itch.io、Unityフォーラムあたりを想定

(※上記の内容は年間目標です(他の項目も同様))

この3ヶ月間のブログ記事投稿数は10個でした。思ったより順調なペースで投稿できて、当初想定していた年間投稿数の目標は達成できてしまいました。肩肘張らずに軽めに投稿しようと意識していたのが良かったのかなと思います。

次の3ヶ月間も、同じような感覚でのんびり気軽に投稿を続けていければと思います。

以下が1月から3月までに投稿した記事です。

その他

良い習慣づくりを意識する。毎月何かしらの習慣目標を掲げ、その達成・習慣化を目指す。直近ではAWS資格試験合格を目指す。その他は状況に応じて設定していく。

主要な結果

  • 毎月何かしらの習慣目標を掲げ、達成・習慣化を目指す
  • AWS認定資格試験に4個以上合格する
  • 40冊以上本を読む

習慣目標

この3ヶ月間は毎日AWSの試験勉強をするという習慣目標を立てて、無事SAADVASOAの3つの試験に合格できました。AWSはもうお腹いっぱいなので当分(1、2年ぐらい)置いておいて、今年の後半にAzureの基礎系の資格試験を受けてみようと思います。

次の3ヶ月間は、毎日ゲーム開発に少しでも取り組むという習慣目標を立てて頑張っていきます。

読書

この3ヶ月間は13冊の本を読めました。読んだ本は以下のとおりです。

  • 『御成敗式目 鎌倉武士の法と生活』(感想記事)
  • 『パウロ 十字架の使徒』(感想記事)
  • 『やり抜く人の9つの習慣』(感想記事)
  • 『海の道と東西の出会い』(そのうち感想投稿予定)
  • 『LangChain完全入門』(感想記事)
  • 『小説十八史略』の1~5巻(4月に6巻も読了、そのうち感想投稿予定)
  • その他3冊

振り返ってみて、1)やや歴史系に偏っている、2)軽めの本が多い、と思いました。試験勉強に時間を割いていたので、さくっと読める軽めの本で冊数を稼いだ感はあります🙄

次の3ヶ月も最低10冊は読むのを目標にしつつ、もっと幅広い分野の本や、重めの本も読めればと思います。

まとめ

この3ヶ月間は一番力を入れていたAWSの資格試験3種に無事合格できて、その他の取り組みもまずまずでそれなりに満足していますが、ゲーム開発にもう少し取り組めていたらなというのが大きな反省点です。

次の3ヶ月は何よりもゲーム開発に最優先して取り組み、習作ゲームを2本完成させたいと思います。現状は色々な「良いこと」に目移りして、最も大事なゲーム開発に取り組めていないのが一番の問題だと思っています。「良いは最良の敵である」という言葉もあるように、本当に取り組むべき一番大事なことに逃げずに取り組む3ヶ月間にできればと思います。

以上です。

LangChain雑記帳

LangChainでハマったこと、よく使う処理やパターン等をまとめます。(随時更新)

主な環境

  • Python 3.11.8
  • LangChain 0.1.14

OpenAIのVision APIを利用する

以下のようにHumanMessageにメッセージと画像URLのリストを渡せばOKです。

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage

chat = ChatOpenAI(model="gpt-4-turbo")  # "gpt-4-vision-preview"も可(古いモデル)

url = "https://cdn-ak.f.st-hatena.com/images/fotolife/n/nana8a0a/20240323/20240323111544.png"
res = chat.invoke([
    HumanMessage([
        {"type": "text", "text": "何が写っていますか?"},
        {"type": "image_url", "image_url": {"url": url}},
    ],
    )])

print(res.content)

結果:

この画像は「AWS Certified SysOps Administrator - Associate」の認定証です。画像にはAWSのロゴと「training and certification」というテキスト、認定を受けた人の名前やID、スコア、有効期限などが書かれており、その人が試験に合格したことを示しています。具体的な試験のスコアや他の個人情報は黒塗りで隠されています。

複数の画像を渡して違いを調べてもらうこともできます。

# (略)

url1 = "https://cdn-ak.f.st-hatena.com/images/fotolife/n/nana8a0a/20240323/20240323111544.png"
url2 = "https://cdn-ak.f.st-hatena.com/images/fotolife/n/nana8a0a/20240227/20240227083446.png"
res = chat.invoke([
    HumanMessage([
        {"type": "text", "text": "2つの画像の違いは?"},
        {"type": "image_url", "image_url": {"url": url1}},
        {"type": "image_url", "image_url": {"url": url2}},
    ],
)])

print(res.content)

結果:

2つの画像にはいくつかの違いがあります。

  1. 証明書のタイトル:

    • 最初の画像のタイトルは「AWS Certified SysOps Administrator - Associate」です。
    • 二番目の画像のタイトルは「AWS Certified Developer - Associate」です。
  2. 試験結果のスコア:

    • 最初の画像ではスコアは813です。
    • 二番目の画像ではスコアは850です。
  3. 試験日:

  4. 最初の画像の試験日は2024年3月14日です。
  5. 二番目の画像の試験日は2024年2月23日です。

参考

Web検索機能を使う(Tavily Search APIを使う)

LangChain公式のクイックスタートのAgent機能の説明にあったものです。Tavily Search APIを使います。

事前にtavily.comでアカウントを作ってAPIキーを取得して、環境変数TAVILY_API_KEYにセットしておく必要があります。

import langchain
langchain.verbose = True

from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults

tools = [TavilySearchResults()]
chat = ChatOpenAI()
prompt = ChatPromptTemplate.from_messages([
    MessagesPlaceholder("chat_history"),
    MessagesPlaceholder("agent_scratchpad"),
])
agent = create_openai_tools_agent(chat, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, max_iterations=5)

res = agent_executor.invoke({"chat_history": [
    SystemMessage("必ず関西弁で受け答えしてください。"),
    AIMessage("おおきに。"),
    HumanMessage("阪神の2023年のドラフト1位は誰?"),
]})

print(res["output"])

途中経過:

> Entering new AgentExecutor chain...

Invoking: `tavily_search_results_json` with `{'query': '2023年 阪神 ドラフト 1位'}`

[{'url': 'https://draft.npb.jp/draft/2023/draftlist_t.html', 'content': '富山GRNサンダーバーズ. 2位. 福島 圭音. 外野手. 白鴎大学. 2022. 日本野球機構(NPB)オフィシャルサイト。. プロ野球12球団の試合日程・結果や予告先発、ドラフト会議をはじめ、事業・振興に関する情報を掲載。. また、オールスター・ゲームや日本シリーズ ...'}, {'url': 'https://www.baseballchannel.jp/npb/tigers/draftkaigi2023/designated-player/breaking-news/list/', 'content': '「2023年プロ野球ドラフト会議 supported by リポビタンD」が、10月26日16時50分から行われる。プロ志望届を提出した高校生・大学生加え、社会人や独立リーグの候補者が運命の時を待つ。ここでは、阪神タイガースの指名選手一覧(ドラフト1位の抽選結果含む)を速報する。'}, {'url': 'https://www.draft-kaigi.jp/draftnews/2023draftnews/74893/', 'content': '2023年のドラフト会議は10月26日に行われ、支配下ドラフトが72人(昨年より+3人)、育成ドラフトが50人(昨年より−7人)の、合わせて122人(昨年より-4人)が指名されました。 ... 青学大・西川史礁選手が初戦で2安打、12球団が視察しヤクルト・楽天・阪神 ...'}, {'url': 'https://www.youtube.com/watch?v=K_zO4e-m1yQ', 'content': '2023年のプロ野球ドラフト会議にて阪神タイガースから1位指名された下村海翔投手(青山学院大)の貴重な侍ジャパン選出時の映像。下村海翔 ...'}, {'url': 'https://hanshintigers.jp/news/topics/draft2023.html', 'content': 'プロ野球ドラフト会議2023速報. 2023年10月26日 更新. 26日 (木)、プロ野球の新人選手選択会議「プロ野球ドラフト会議 supported by リポビタンD」が行われ、阪神タイガースは下村海翔選手 (青山学院大)ら8選手を指名し交渉権を獲得しました。.'}]2023年の阪神タイガースのドラフト1位指名は、下村海翔投手(青山学院大)やで。

> Finished chain.

結果:

2023年の阪神タイガースのドラフト1位指名は、下村海翔投手(青山学院大)やで。

参考

自前のToolを作る

Agentから呼び出せる自前のToolを作る方法です。BaseToolを継承したクラスを作ります。

以下は雑にWebページの情報を抜き出すToolの実装例です。(HTMLファイルから本文を抜き出すためにtrafilaturaを使っています)

# Tool側

from langchain_core.messages import HumanMessage, SystemMessage
from langchain.pydantic_v1 import BaseModel, Field
from langchain.tools import BaseTool
from langchain.callbacks.manager import CallbackManagerForToolRun
from langchain_openai import ChatOpenAI
import logging as logger

import trafilatura


class ExtractWebPageInfoSchema(BaseModel):
    url: str = Field(description="対象のWebページのURL")
    prompt: str = Field(default="", description="ページから抜き出したい情報の指示文。例: 'このページの要約をしてください。'")


class ExtractWebPageInfo(BaseTool):
    name = "extract_web_page_info"
    description = "指定されたWebページから、指定された情報を抜き出します。"
    args_schema: type[BaseModel] = ExtractWebPageInfoSchema

    def _run(self,
        url: str,
        prompt: str = "",
        run_manager: CallbackManagerForToolRun | None = None,
    ) -> str:
        try:
            logger.debug(f"web page 取得中... 指示: '{prompt}' url: {url}")
            html = trafilatura.fetch_url(url)
            content = trafilatura.extract(html)
            
            logger.debug(f"取得完了 長さ: {len(content)} content: '{content}'")
            if len(content) > 3000:
                logger.debug("長すぎるので先頭一定数のみを入力します。")
                content = content[:3000]

            chat = ChatOpenAI()
            logger.debug("chat api 問い合わせ中...")
            res = chat.invoke([
                SystemMessage("指示に従ってWebページの情報を抽出して要約してください。"),
                HumanMessage("指示: " + prompt),
                HumanMessage(f'Webページの内容:\n"""\n{content}\n"""'),
            ])
            logger.debug(f"chat api 結果受信 {res}")
            return res
        except Exception as e:
            logger.error(f"ExtractWebPageInfoでエラーが発生しました。{e}")
            return str(e)


# --------------------
# 利用側


import langchain
langchain.verbose = True

from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI

tools = [ExtractWebPageInfo()]
chat = ChatOpenAI(model="gpt-4")
prompt = ChatPromptTemplate.from_messages([
    MessagesPlaceholder("chat_history"),
    MessagesPlaceholder("agent_scratchpad"),
])
agent = create_openai_tools_agent(chat, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, max_iterations=5)

res = agent_executor.invoke({"chat_history": [
    SystemMessage("必ず関西弁で受け答えしてください。"),
    AIMessage("おおきに。なんの用や?"),
    HumanMessage("このページの内容を100文字程度で教えて。 https://blog.778a0a.com/entry/2024/01/21/161800"),
]})

print(res["output"])

途中経過:

> Entering new AgentExecutor chain...

Invoking: `extract_web_page_info` with `{'url': 'https://blog.778a0a.com/entry/2024/01/21/161800', 'prompt': 'このページの内容を100文字程度で教えてください。'}`

web page 取得中... 指示: 'このページの内容を100文字程度で教えてください。' url: https://blog.778a0a.com/entry/2024/01/21/161800
取得完了 長さ: 1352 content: '『御成敗式目 鎌倉武士の法と生活』を読んだので感想です。...'
chat api 問い合わせ中...
chat api 結果受信 content='...'

content='『御成敗式目 鎌倉武士の法と生活』を読んだ感想。御成敗式目の制定経緯や武士社会への影響、吾妻鏡の評価など紹介。歴史書の物事評価には注意が必要。北条泰時の野心、後世の影響も考慮。鎌倉時代後期や室町時代についての書籍も読みたいと述べる。' response_metadata={'token_usage': {'completion_tokens': 158, 'prompt_tokens': 1562, 'total_tokens': 1720}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_c2295e73ad', 'finish_reason': 'stop', 'logprobs': None} ...

> Finished chain.

結果:

あのページは「御成敗式目 鎌倉武士の法と生活」についての感想や評価が書かれてるで。御成敗式目の制定経緯や武士社会への影響、吾妻鏡の評価や北条泰時の野心も語られててな。また、鎌倉時代後期や室町時代についての書籍にも興味を示してるんや。

他のToolと組み合わせる

AgentExecutorのおかげでTavilySearchResultsと組み合わせて良い感じのプロンプト文を入力すると、TavilySearchResultsで記事を検索してからExtractWebPageInfoで中身を読み取ってくれるので便利です。

from langchain_community.tools.tavily_search import TavilySearchResults

tools = [ExtractWebPageInfo(), TavilySearchResults()]
chat = ChatOpenAI(model="gpt-4")
prompt = ChatPromptTemplate.from_messages([
    MessagesPlaceholder("chat_history"),
    MessagesPlaceholder("agent_scratchpad"),
])
agent = create_openai_tools_agent(chat, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, max_iterations=5)

res = agent_executor.invoke({"chat_history": [
    SystemMessage("必ず関西弁で受け答えしてください。"),
    AIMessage("おおきに。なんの用や?"),
    HumanMessage("778a0aの日記というブログのなかのパウロについての本の感想記事を探して100文字程度で要約して。"),
]})

途中経過:

> Entering new AgentExecutor chain...

Invoking: `tavily_search_results_json` with `{'query': '778a0a ブログ パウロについての本 感想'}`

[{'url': 'https://blog.778a0a.com/entry/2024/02/27/083624', 'content': '「AWS Certified Developer - Associate(以下DVA)」に合格しましたので感想です。 ...'}, {'url': 'https://ja.ligonier.org/blog/5-things-paul/', 'content': '使徒パウロについて知っておくべき五つのこと ...'}, {'url': 'https://bookmeter.com/books/11259434', 'content': '全54件中 1-40 件を表示. パウロ 十字架の使徒 (岩波新書) の 評価70% ...'}, {'url': 'https://blog.778a0a.com/entry/2024/01/26/222537', 'content': '『パウロ 十字架の使徒』を読んだので感想です。 ...'}, {'url': 'https://ameji56.hatenablog.com/entry/2023/12/23/214100', 'content': '岩波新書の赤、青野太潮「パウロ」(2016年)は、初期キリスト教の使徒であり、新約聖書の著者の一人であったパウロに関する新書である。 ...'}]
Invoking: `extract_web_page_info` with `{'url': 'https://blog.778a0a.com/entry/2024/01/26/222537', 'prompt': 'このページの要約をしてください。'}`

web page 取得中... 指示: 'このページの要約をしてください。' url: https://blog.778a0a.com/entry/2024/01/26/222537
取得完了 長さ: 1451 content: '『パウロ 十字架の使徒』を読んだので感想です。...'
chat api 問い合わせ中...
chat api 結果受信 content='...'

content='『パウロ 十字架の使徒』は初期のキリスト教について詳しく知ることができる一冊で、パウロの活動や当時のキリスト教・ユダヤ教の状況について分かりやすく解説されています。初期のキリスト教では異民族への布教やユダヤ教的な律法主義に対する考え方に対立があり、その歴史的経緯が紹介されています。後半部分では贖罪論などについての議論も紹介されており、一般的なキリスト教の解釈との違いも示唆されています。読者は本書を通じてキリスト教の歴史や異端・非主流な宗派、ローマ帝国の歴史、イスラム教の歴史などについても興味を持ち、深く学ぶことを希望しています。' response_metadata={'token_usage': {'completion_tokens': 306, 'prompt_tokens': 1641, 'total_tokens': 1947}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_c2295e73ad', 'finish_reason': 'stop', 'logprobs': None} ...

結果:

『パウロ 十字架の使徒』についての感想記事やけど、初期のキリスト教について詳しく知れる本やったらしいで。パウロの活動や、当時のキリスト教・ユダヤ教の状況を分かりやすく説明してくれてるんやて。異民族への布教やユダヤ教的な律法主義に対する考え方の対立があった歴史的経緯も紹介されてるし、贖罪論についての議論も出てくるらしいで。キリスト教の歴史や異端・非主流な宗派、ローマ帝国の歴史、イスラム教の歴史などについても学びたくなる本やったみたいやな。

参考

Slackのリマインダー機能的なTool

Slackで自然言語でリマインダーが設定できると便利ですが、Slackのリマインダー機能はAPIやBotから操作できない(最新のAPIで使えなくなっている)ので、自前で作る必要があったので作りました。めちゃくちゃ雑な実装です。

基本的には、いつリマインドしたいかという自然言語のクエリーを受け取って、それを任意の日付型データに落とし込めればOKです。今回はその変換タスクはChatGPTにやってもらっています。定期実行には対応していないですが、クエリーをcron式に落とし込めればいいのかなあと思います。

# Tool側

import datetime
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from langchain.pydantic_v1 import BaseModel, Field
from langchain.tools import BaseTool
from langchain.callbacks.manager import CallbackManagerForToolRun
from langchain_openai import ChatOpenAI

# Agent実行前に手動セットしてもらう。
# (Toolに固定の引数を渡す方法がぱっと分からないのでこうしています🙄)
g_channel_id = None
g_user_id = None
g_current_time = None


class SetReminderSchema(BaseModel):
    when: str = Field(description="when to remind")
    message: str = Field(default="", description="リマインド時に伝えるメッセージ。ユーモア溢れた感じにする")


class SetReminder(BaseTool):
    name = "set_reminder"
    description = "Set a reminder. Use this tool if you want to set a reminder."
    args_schema: type[BaseModel] = SetReminderSchema

    def _run(self,
        when: str,
        message: str = "",
        run_manager: CallbackManagerForToolRun | None = None,
    ) -> str:
        try:
            global g_channel_id, g_user_id, g_current_time
            channel_id = g_channel_id
            user_id = g_user_id
            current_time = g_current_time

            print(f"リマインダーを設定します。 時間: '{when}' メッセージ: {message}")
            current_time_text = current_time.strftime("%Y-%m-%d %H:%M %A")

            chat = ChatOpenAI(model="gpt-4")
            example_date = datetime.datetime(2024, 4, 9, 15, 0, 0)
            example_date_text = example_date.strftime("%Y-%m-%d %H:%M %A")
            example_due_date = datetime.datetime(2024, 4, 10, 9, 0, 0)
            example_due_date_text = example_due_date.strftime("%Y-%m-%d %H:%M")
            print(f"例の時刻: {example_date_text} 求めたい時間: '明日'")
            print(f"例の結果: {example_due_date_text}")

            print(f"現在時刻: {current_time_text} 求めたい時間: '{when}'")
            res = chat.invoke([
                SystemMessage(
                    "次の求めたい時間を '%Y-%m-%d %H:%M' というフォーマットで返してください。" +
                    "'明日'や'来月'といった感じで特に時刻が明示されていない場合は午前9時としてください。" +
                    "\n【主な時刻】\n- 朝: 09:00\n- 夕方: 18:00\n- お昼休み: 12:00~13:00"
                ),
                HumanMessage(f"例: 現在時刻: {example_date_text} 求めたい時間: '明日'"),
                AIMessage(f"{example_due_date_text}"),
                HumanMessage(f"現在時刻: {current_time_text} 求めたい時間: '{when}'"),
            ])
            print(f"AIの応答: {res.content}")
            # パースする。
            format = "%Y-%m-%d %H:%M"
            when_epoch = datetime.datetime.strptime(res.content, format).timestamp()

            when_human_readable = datetime.datetime.fromtimestamp(when_epoch).strftime("%Y-%m-%d %A %H:%M")
            en_weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
            ja_weekdays = ["月", "火", "水", "木", "金", "土", "日"]
            for en, ja in zip(en_weekdays, ja_weekdays):
                when_human_readable = when_human_readable.replace(en, ja)

            # reminders.txtにjsonを追記する。
            with open("reminders.txt", "a") as f:
                obj = {
                    "when": when_epoch,
                    "message": message,
                    "channel_id": channel_id,
                    "user_id": user_id,
                }
                import json
                f.write(json.dumps(obj, ensure_ascii=False) + "\n")
                print(f"reminders.txtに追記しました。: {obj}")

            return {"when": when, "message": message}
        except Exception as e:
            print(f"SetReminderでエラーが発生しました。{e}")
            return "ERROR! " + str(e)

# --------------------
# 利用側

import langchain
langchain.verbose = True

from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI

tools = [SetReminder()]
chat = ChatOpenAI(model="gpt-4")
prompt = ChatPromptTemplate.from_messages([
    MessagesPlaceholder("chat_history"),
    MessagesPlaceholder("agent_scratchpad"),
])
agent = create_openai_tools_agent(chat, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, max_iterations=5)

g_channel_id = "..."  # 書き込むSlackのチャンネルID
g_user_id = "..."  # お知らせするSlackのユーザーID
g_current_time = datetime.datetime.now()

res = agent_executor.invoke({"chat_history": [
    SystemMessage("必ず関西弁で受け答えしてください。"),
    AIMessage("おおきに。なんの用や?"),
    HumanMessage("30分後にリマインダーを設定して。"),
]})

print(res["output"])

途中経過:

> Entering new AgentExecutor chain...

Invoking: `set_reminder` with `{'when': '30 minutes later', 'message': 'おほん!30分たったで!なにかせなアカンことあったんやったら、そろそろ動き出すんやで!'}`


リマインダーを設定します。 時間: '30 minutes later' メッセージ: おほん!30分たったで!なにかせなアカンことあったんやったら、そろそろ動き出すんやで!
例の時刻: 2024-04-09 15:00 Tuesday 求めたい時間: '明日'
例の結果: 2024-04-10 09:00
現在時刻: 2024-04-13 13:15 Saturday 求めたい時間: '30 minutes later'
AIの応答: 2024-04-13 13:45
reminders.txtに追記しました。: {'when': 1712983500.0, 'message': 'おほん!30分たったで!なにかせなアカンことあったんやったら、そろそろ動き出すんやで!', 'channel_id': '...', 'user_id': '...'}
{'when': '30 minutes later', 'message': 'おほん!30分たったで!なにかせなアカンことあったんやったら、そろそろ動き出すんやで!'} ...

> Finished chain.

結果:

30分後にリマインダー設定したで!"おほん!30分たったで!なにかせなアカンことあったんやったら、そろそろ動き出すんやで!"っていうメッセージで教えるわ!

あとは、定期的にreminders.txtを見て、通知時刻を過ぎているものがあれば適宜通知するだけです。以下はscheduleを使った雑な実装例です。

import threading
import time
import schedule
import json
import datetime
from slack_bolt import App

app = App(token="...")


def job():
    print("リマインダーを確認します。")

    # reminders.txtのjsonを読み込んで、現在時刻と比較して、
    # その時刻になったらslackに通知する。
    lines = []
    print(f"reminder.txtを読み込ます。")
    with open("reminders.txt", "r") as f:
        # 1行ずつ読み込む。
        for line in f:
            lines.append(line)
    print(f"reminder.txtを読み込みました。")
    
    original_count = len(lines)
    print(f"リマインダーの数 {len(lines)}")
    for line in lines:
        obj = json.loads(line)
        when = obj["when"]
        message = obj["message"]
        channel_id = obj["channel_id"]
        user_id = obj["user_id"]
        when_human_readable = datetime.datetime.fromtimestamp(when).strftime("%Y-%m-%d %H:%M")
        now = datetime.datetime.now().timestamp()
        if now > when:
            print(f"リマインダーを通知します。 {when_human_readable} {message}")
            # slackに通知する。
            app.client.chat_postMessage(
                icon_emoji="exclamation",
                channel=channel_id,
                text=f"<@{user_id}>\nリマインダーです。\n時間: {when_human_readable}\n内容: `{message}`")
            # そのjsonを削除する。
            lines.remove(line)
    
    if original_count != len(lines):
        print(f"reminder.txtを更新します。")
        # reminders.txtを更新する。
        with open("reminders.txt", "w") as f:
            for line in lines:
                f.write(line)
        print(f"reminder.txtを更新しました。")


def start_watch(use_another_thread: bool):
    print("リマインダーを監視します。")
    schedule.every(30).seconds.do(job)
    if use_another_thread:
        t = threading.Thread(target=watch)
        t.start()
        print("監視スレッドを開始しました。")
    else:
        watch()


def watch():
    while True:
        schedule.run_pending()
        time.sleep(1)


if __name__ == "__main__":
    start_watch(use_another_thread=False)

参考

以上です。