<![CDATA[東京科学大学デジタル創作同好会traP]]>https://trap.jp/https://trap.jp/favicon.png東京科学大学デジタル創作同好会traPhttps://trap.jp/Ghost 5.54Sun, 22 Mar 2026 02:43:39 GMT60<![CDATA[ヤケクソなタイピングゲームを作る]]>

これは2023年10月に部内向けに書いたブログを外部公開向けに書き直したものです。

はじめに

こんにちは。koukawa_ppで

]]>
https://trap.jp/post/2823/69a017bee2394c00016dfb09Sun, 22 Mar 2026 00:00:35 GMT

これは2023年10月に部内向けに書いたブログを外部公開向けに書き直したものです。

はじめに

こんにちは。koukawa_ppです。
さて、今回はヤケクソなタイピングゲームを作るというタイトルで、ブログを書きます。
開発言語はPythonです。

ここでは、以下のものを作ります。

  • ゲーム本体
  • タイピングゲームのTASツール

また、コードは こちらの GitHub リポジトリ にて公開しております。

開発環境

  • Windows 10 Home 64bit
  • Python 3.11.4
  • Visual Studio Code

作ることになったいきさつ

まずそもそもタイピングゲームとは、皆さんご存じの通り、ある文章を出され、それをいかに早く正確に打ち込めるかを楽しむゲームです。
まあでもタイピングゲームが得意な方なら、この手のシンプルなタイピングゲームはまあ瞬殺でしょう。
退屈していらっしゃるのではないでしょうか。
そうするとシンプルなタイピングゲームを作成するのは野暮というもの。

そのような中で、このタイピングゲームが得意な方でもタイプが遅くなるようなチャレンジングなタイピングゲームを作れないだろうか。
このような考えのもと、この企画を考えました。

ぶっちゃけて言うと、正道では対抗できそうにありません。
そこで、何か普通のタイピングゲームより難しい何か、を作ることを考えました。

ここで、以下のような工夫点を導入したタイピングゲームを作りました。

  • 文字列をランダムにする
  • 特殊文字を入れるようにする
  • シーザー暗号を導入する

まず最初の要素によって、「このような組み合わせはよく出てくるな」や、打ちなれているキーだけでできるタイピングゲームを排除します。
次に、特殊文字を入れることにより、あたかも複雑なパスワードを入力しているように感じさせます。
これら二つの要素は、ひょっとするとどこかのタイピングゲームにあるかもしれませんが、最後の要素を導入することで、多分他にはないタイピングゲームを完成させました。
最後の「シーザー暗号を導入する」とは、以下のようなものです。

eznoafowe (2)

とあったら、これら各々の文字を2つ後ろにずらして入力する必要があるということです。
つまりこの場合は、

gbpqchqyg

と入力する必要があるわけです。
つまりこのタイピングゲームは、タイピング速度もそこそこに、アルファベットの順番を即座にずらせることも求めたものなのです。

ゲームで遊ぶ

venv を作成する

まず、今回のプログラムでは pyyaml というサードパーティ製ライブラリが必要ですので、それをインストールする仮想環境 (venv) を用意します。

今回は typing_env という名称の仮想環境を作ります。

python -m venv typing_env

そしてこれを起動します。

./typing_env/Scripts/activate

もし Linux や、または macOS もそうだったと思うのですがその場合は、

source typing_env/bin/activate

とする必要があります。


そうすると、このような感じになります。

(typing_env) PS [directory name]

環境をアクティベートしたら、pip でライブラリをインストールします。

pip install pyyaml

これでいったん仮想環境は完成です。

configファイルを作成する

では次に、conffigファイルを作成します。
configファイルはyamlファイルで提供しており、この内容を変えることでゲームの設定を変えることができます。

game_config.yamlnum_problems: 10
min_chars_length: 20
max_chars_length: 30

contain_capital_char: true
contain_number: false

# If you want problems to contain special char, you have to set.
# Otherwise, set null
contain_special_char:
  - null

caesar: 0
random_caesar: false
not_negative: true

初期設定としてはこんな感じだと思います。
各々について説明します。

名前 初期値 説明
num_problems 10 問題数を表します。
min_chars_length 20 問題の最小文字数を表します。
max_chars_length 30 問題の最大文字数を表します。
contain_capital_char true 問題にアルファベット大文字を含むかを表します。
contain_number false 問題に算用数字を含むかを表します。
contain_special_char null 問題に含む特殊文字を、リスト形式で表してください。
caesar 0 シーザー暗号において、何文字ずらすかの最大値を表します。0の場合はシーザー暗号形式の問題を出題しません。特殊文字を入れたい場合は0に設定してください。
random_caesar false この値がtrueのとき、not_negativeの値によって何文字ずらすかの値の範囲を決定します。not_negativeがtrueなら、0以上caesar以下の値にして、not_negativeがfalseなら、-caesar以上caesar以下の値で文字をずらします。
not_negative true random_caesarの説明の通りです。

この部分を変更して、遊んでみてください。

実際に動かす

では、以下のようにしてプログラムを実行してください。

python yakekuso_typing.py

そうすると、始まります。

(typing_env) PS [directory name]> python .\yakekuso_typing.py
press Enter to start. 
1   onEhBqIsdDbxGLcskUThOiK
>>> onEhBqIsdDbxGLcskUThOiK
2   DqxMHGXdneoacvZBmqQuUkuKmw
>>> DqxMHGXdneoacvZBmqQuUkuKmw
3   GnlJjOpBsiHxAdYsrObjLCxVFUpc
>>> GnlJjOpBsiHxAdYsrObjLCxVFUpc
4   cLXctVjCSZPehUEPBmPmGeUiXH
>>> cLXctVjCSZPehUEPBmPmGeUiXH
5   RXnkbMTSGvuDVoyyZLZqilL
>>> RXnkbMTSGvuDVoyyZLZqilL
6   FKuUavxxRPTRhKybsdtmJ
>>> FKuUavxxRPTRhKybsdtmJ
7   RrGVvVbYLgBrveHnIStyzuILto
>>> RrGVvVbYLgBrveHnIStyzuILto
8   YwMhVThewpHiJxZigwsJG
>>> YwMhVThewpHiJxZigwsJG
9   kRdaNZRnmMyRaXaQVIQINaf
>>> kRdaNZRnmMyRaXaQVIQINaf
10  faBdlXTMBGFJzicJWOLP
>>> faBdlXTMBGFJzicJWOLP

Spent time: 91.1295325756073
2.601 types per second.
(typing_env) PS [directory name]>

プレイにかかった時間はEnterを押してからカウントされるので、
準備ができたらEnterを押すという形でOKです。

シーザー暗号のパターンもできます。

game_config.yaml# omitted

caesar: 2
random_caesar: true
not_negative: true

このようにconfigファイルを変更して、実行してみてください。

(typing_env) PS [directory_name]> python .\yakekuso_typing.py
press Enter to start. 
1   QXXORCCWKlKtThBSQOquYSQAfZYW (1)
>>> RYYPSDDXLmLuUiCTRPrvZTRBgAZX
2   TmSKfJbZUCxpvJlBnFMPdJTrkm (2)
>>> VoUMhLdBWEzrxLnDpHORfLVtmo
3   NtrQMpnVTwmgoGZADRPWddC (0)
>>> NtrQMpnVTwmgoGZADRPWddC 
4   DeyxfmYDWTWPherccZKvfYsLm (2)
>>> FgazhoAFYVYRjgteeBMxhAuNo
5   TuwjoLtzqhRtvbdQCRfolDUl (2)
>>> VwylqNvbsjTvxdfSEThqnFWn
6   cwZdDkVSbCJvNWfNbFEaiqxoS (2)
>>> eyBfFmXUdELxPYhPdHGckszqU
7   YllpIgYwwCoNXFNItGmIcx (1)
>>> ZmmqJhZxxDpOYGOJuHnJdy
8   dVPCvUpIvNYTuJhAUewG (1)
>>> eWQDwVqJwOZUvKiBVfxH
9   qmCwtKihENzegpBomAWrQzykzBhkLH (1)
>>> rnDxuLjiFOafhqCpnBXsRazlaCilMI
10  YIWRSszClsfPoeZYDpremakA (0)
>>> YIWRSszClsfPoeZYDpremakA

Spent time: 372.90026021003723
0.662 types per second.
(typing_env) PS [directory_name]>

なぜタイピングの問題10問で6分も掛けなければならないのか、という話ですが、これ相当難しいです。
日ごろから任意の英文について文字ずらしをしていれば出来たかもしれませんが、そんな毎日毎日暗号化したい英文なんて扱うわけがないですから。
(というか今シーザー暗号してもすぐバレる)
ただとりあえず、うまくシーザー暗号のように文字ずらしができていることが分かります。
ぜひいろいろなconfigにして、試してみてください。

TASツールを作る

さて、タイピングゲームとなれば、よく使われるのはTASツールです。
しかもオープンソースのタイピングゲームみたいなものですから、アルゴリズムも丸わかりです。
そうなると、最速でクリアしたいという感想が出てくるのは当たりまえです。
無論人力で最速を目指せればそれもすごいですが、TASツールを作るのもまた一つの手だと思います。

次のようなコードを作成しました。

tas_typing_game.py# Reference:
# https://qiita.com/HidKamiya/items/e192a55371a2961ca8a4#subprocesspopen%E3%81%AB%E3%82%88%E3%82%8B%E8%87%AA%E7%94%B1%E5%BA%A6%E3%81%AE%E9%AB%98%E3%81%84%E5%87%A6%E7%90%86

import re
import subprocess

from yakekuso_typing import caesar_encrypt

if __name__ == '__main__':
    p = subprocess.Popen(['typing_env/Scripts/python.exe', 'yakekuso_typing.py'],
                         stdin=subprocess.PIPE,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                         encoding='utf8')

    p.stdin.write('\n')
    p.stdin.flush()
    while p.poll() is None:
        line = p.stdout.readline().strip()
        
        if re.search('press Enter to', line):
            p.stdin.write('\n')
            p.stdin.flush()
        elif re.search('Spent time', line) or re.search('types per second', line):
            print(line, flush=True)
            p.stdin.write('print()\n')
            p.stdin.flush()
        elif re.search('[0-9][0-9\s][0-9\s] (\S+) \([\-0-9]+\)', line):
            print(line, flush=True)
            m = re.search('([0-9][0-9\s][0-9\s]) (\S+) \(([\-0-9]+)\)', line)
            p.stdin.write(f'{caesar_encrypt(m.group(2), int(m.group(3)))}\n')
            p.stdin.flush()
        elif re.search('[0-9][0-9\s][0-9\s] (\S+)', line):
            print(line, flush=True)
            m = re.search('([0-9][0-9\s][0-9\s]) (\S+)', line)
            p.stdin.write(f'{m.group(2)}\n')
            p.stdin.flush()

今回は subprocess という標準ライブラリを使っています。
これを使うことで、このPythonプログラムを実行しながら、他のプログラムを別プロセスで実行できます。
また、reは、正規表現を扱うためのライブラリです。
Regular Expression の略です。
また、yakekuso_typing.pyからシーザー暗号で暗号化する関数をインポートしています。
ソースコードをそのまま保存しているので、簡単に再利用できます。

さて、実行においては yaml が必要なので、これまでと同様 typing_env 内で実行してください。

(typing_env) PS [directory name]> python .\tas_typing_game.py
>>> 1   vYfBBhgPBIrHEApzddZJlWO (2)
>>> 2   nycYSUdOjGrVYhNqKyzULNNQILz (1)
>>> 3   YwezVOGnscquhFrbFKFukvydOZgpVa (0)
>>> 4   DCVqjKxsgaVdDggsfxcPtYu (1)
>>> 5   CdKVamkVelTcJqYfBtOAUItzLEoAO (0)
>>> 6   dhAVcJTEbKLCChYDTvOGemH (0)
>>> 7   juIZbrkwWrZvBNBSagdYDvMI (2)
>>> 8   vCiGCzAVWGBASuJSQLWnGOiM (0)
>>> 9   DvUsENKbVrZdsoczGaVagEQIv (1)
>>> 10  BSJcmfjmvoEIuIygfmyaAnYkLZqdD (1)
Spent time: 0.00768733024597168
33431.633 types per second.
(typing_env) PS [directory_name]> 

こんな感じで出てきます。
平均して一秒あたり33431回ほどタイプしていることになっていることが分かります。

ちなみに configcaesar を0にしたとき、
tas_typing_game.pyは以下のような感じで実行できます。

(typing_env) PS [directory name]> python .\tas_typing_game.py
>>> 1   SgzxWXQVOAyJYDOJXXAWxh
>>> 2   tDevbinTnlfxSvSKmkptYYZ      
>>> 3   dnWLvBhusCwsmeBSXalSjVpircjoO
>>> 4   EdFNZcrsVoDZuxbKmltS
>>> 5   zFTmkzrpIWeRFwzMgtRsaAZi     
>>> 6   IRvROZVmKRyFhPMrlRqkuTmKwCFW 
>>> 7   VToRbXqNYizmRHMpomehdrTnhlH  
>>> 8   OHMSWCVhGPGqZsnPBuHtBU       
>>> 9   fXWJrPUMJECsvRlbxajwL        
>>> 10  lYgJSVgSOwCUKoVjSJCgV        
Spent time: 0.0046956539154052734    
50472.204 types per second.
(typing_env) PS [directory name]>

ここでは caesar_encrypt などを動かす必要が無くなるので、
実行速度がまた速くなります。
平均して一秒あたり50472回ほどタイプしていることになっていることが分かります。

体験版

以下に今回のタイピングゲームで、文字がズレるバージョンを実装いたしました。
このバージョンのコードも GitHub に上げておりますので、よろしければご覧ください。

時間などは測定しておりません。
下の入力欄には0以上3以下の数字を入力できます。最大この文字数分だけずれます。
入力欄において数字を入力したら、「変更」ボタンを押すことでその範囲内で出題されます。

おわりに

本日はタイピングゲームを作り、紹介してきました。
ぜひプレイして楽しんでください。
最後までお読みいただき、ありがとうございました。

]]>
<![CDATA[いろんな螺旋を描く]]>

このブログ記事は、2023年8月に部内向けに書いたものを外部公開向けに変更したものです。

はじめに

こんにちは

]]>
https://trap.jp/post/2821/699ff849e2394c00016df946Sun, 22 Mar 2026 00:00:15 GMT

このブログ記事は、2023年8月に部内向けに書いたものを外部公開向けに変更したものです。

はじめに

こんにちは。koukawa_ppです。

さて、螺旋はお好きですか?
ここでいう螺旋とは、二次元平面における螺旋です。

こういうタイプのもです。
こういった螺旋について、今回は深く掘り下げていこうというお話です。

なお、今回取り上げる螺旋には、以下のようなものがあります。

  • アルキメデスの螺旋
  • サックスの螺旋
  • ウラムの螺旋

これらについて、特にPythonのコードも交えながら、色々螺旋を描いてみましたので、共有したいと思います。

なお、今回のコードや画像ファイルは全てこちらのGitHubリポジトリに入れてあります。
ぜひご覧ください。

実行環境

  • Windows 10 Home 64bit
  • Python 3.9.13
  • Visual Studio Code

アルキメデスの螺旋を描く

そもそも、なぜ螺旋を描きたいとなったのかと言いますと、まずどこかで、「素数螺旋」というものがある、ということを耳にしたためです。
とは言えその時は螺旋について詳しく知らなかったので、とりあえず何も考えずにオーソドックスな螺旋を描こうという話になりました。

ここでいうオーソドックスな螺旋こそが、アルキメデスの螺旋です。
すなわち、極座標表示すれば、 と表される曲線です。
最初は無知故適当に見えればよいかという形で描画していたのですが、あとから調べてみるとこちら や数Ⅲの教科書などに載っていたので、これに則って描画することにしました。
この参考のページには、コンパスと定規で描く近似法を示していただいています。

次の問題です。
という式を得られたわけですから、
あとはこれを という形で変数変換すれば、
これを描画することはお手の物です。
しかし、このまま何も考えずに ラジアンずつなどと描画すると、こうなります。

そうです、直径が大きくなればなるほど、
もっと言ってしまえば が大きくなればなるほど、
点と次の点の間があまりにも空きすぎてしまい、
綺麗な螺旋を描けなくなってしまうのです。

これを改善するために、螺旋上に等間隔に点を打つ必要が出てきました。
これを実現する方法は簡単です。

  1. ある基準の長さ を設定する。
  2. ある時点で最後に打った点と基準線のなす角度を として、次に打つべき点と基準線のなす角度を とするとき、螺旋の の部分における曲線の長さが、 と一致すればよい。
  3. 2.から、適切な角度 を求める。

発想としてはそこまで難しくありません。
しかし、行うは難しです。
螺旋の長さなんてそう簡単に求められるわけがありません。
……いや、求めます。

調べてみると、極座標における曲線の長さを求める公式がありました。
から までにおける曲線の長さ は、

で与えられるとのことです。詳しくは こちら をご覧ください。いつもお世話になっております。
なのでこの曲線の長さは、

と表されます。
次に こちら によれば

とのことですので、つまり

というわけです。これが と等しくなる を見つけられれば、晴れて螺旋上に等間隔で点を打つことができます。


では、ある が与えられたとき、どのようにして を求めれば良いのでしょうか。
直感から、以下の方程式を解けばいいことが分かります。

ここは、文明の利器、scipy を頼ることにします。
scipy.optimize.fsolve() 関数は、第一引数に の解を求めたい関数 、第二引数にその解の値に近い の値を与えることで、その方程式の近似解を求めることができるという優れものです。

ここで、 については、draw_spiral() 関数の引数の radiusdistance をそれぞれ与えればよく、また は、前回の の値をそのまま使えばよいです。
では、fsolve() 関数の第二引数には何を与えるかということですが、 に近い値を与えればよいということなので、ここは素直に の値を与えればよいということになります。

これらを実現するために使っているコードというのが、

draw_spiral.pyfrom scipy import optimize

def _length(x):
    return a * (x * np.sqrt(x * x + 1) - theta * np.sqrt(theta * theta + 1) + np.log(x + np.sqrt(x * x + 1)) - np.log(theta + np.sqrt(theta * theta + 1))) / 2 - d

def draw_spiral(
        draw_index: set[int] = None,
        width: int = 1000,
        height: int = 1000,
        radius: float = 5.0,
        distance: float = 1.0,
        num_of_cycle: int = 10,
        file_name: str = None
    ) -> None:
    # 中略
    while theta < 2 * math.pi * num_of_cycle:
        # 中略
        theta = optimize.fsolve(_length, theta)[0]

というわけです。
また、どうも optimize.fsolve の第一引数にとる関数は、引数に ndarray を取る必要があるらしいので、_length() 関数にはやむなく numpy の関数を用いて、optimize.fsolve() 関数の0番目を theta に代入するという形になっています。
(最初 math を使ってたらwarningが出てきた……)
これを使って、等間隔に点を打つと以下のような感じになります。

点と次の点の間が等しくなったような気がしませんか?
これでうまく機能しました。


次に、draw_spiral() 関数に draw_index という引数があると思いますが、ここは初期値として None になっています。
ここに set を渡すと、指定されたインデックスだけの点が表示されるようになります。
たとえば、draw_index0, 4, 6, 9 と代入されているとすると、0, 4, 6, 9番目の点だけが表示されるようになります。
これでお分かりのように、点は0番目から開始します。
set にランダムに数字を追加すると、以下のようなランダムな螺旋が描けます。

なお、最後に ImageOps.flip() を実行していると思います。
これは、僕が左上が原点だと思っていたところ、pillow によると左下が原点であるということらしいので、つじつまを合わせるために上下反転させたということです。
GitHub の方に API リファレンスを載せておきますので、draw_spiral.py でいろいろ遊びたくなりましたらそちらをご覧ください。


さて、これで螺旋をランダムに描くと、以下のような感じになります。

まあ綺麗っちゃ綺麗かもしれませんが、
ランダムさが生んだ綺麗さですよね。

無理数も無理やり螺旋にしてみましょう。
たとえば、 は、1.41421356...と続きますが、0から始めて各位の数字を足していくという数列を考えてみましょう。
つまり、 の小数第 位を として、今回 set に追加する数字の内小さい方から 番目を とするなら、

とすれば良いわけです。
こうすると、 と求められていきます。
時々 となりますが、それは として処理することにして、そうすると無理数から作る螺旋も一意に決定することができます。
こうして から作った螺旋は以下のようになります。

は以下の通り。

は以下の通り。

良さげですね。

では、待ちに待った素数螺旋を描いてみましょう。
素数のところだけ表示させた螺旋は以下のようになります。

ん?規則、的?

……まあ確かに途切れてるところは似ているし?
周期的に途切れてるし?
まあ綺麗に見えるし?
素数螺旋ってこういうものなのかぁ~(棒)

ウラムの螺旋

koukawa_ppはこんらんした!
そすうの らせんが そこまで うまく みえない!


さて、こんなところで終わっていられません。
こういった模様になってくると、人によって評価が分かれるところ。
本来大多数の人が規則的だと思えなければ、あとは定量的に測るなどして、(無理やり)規則性を発見できなければ、それは単なる見た目だろ、という話になってしまいます。
多くの人に認められた黄金比でさえも、確かに美しくは見えるがなぜかはわからない、といったように、あるものが美しい理由というのが科学的に解明されたようなものは決して多いわけではありませんが、今回のこれについては大多数が美しいと感じるかと言われると、多分ハテナマークを浮かべる人が一定数いると思ったわけです。

そこでネットをいろいろ漁ってみると、「ウラムの螺旋」というものを見つけました。

こちら によると、ウラムの螺旋は、中心から螺旋を描くように描画されます。
数字は格子点上にあるようにも解釈できます。
このことから、ウラムの螺旋もPythonで描いてみようということになりました。


そうして作ったのが、draw_ulam_spiral.py です。
これは、アルゴリズムの部分が若干ややこしくなっています。

17 16 15 14 13
18 5 4 3 12
19 6 1 2 11
20 7 8 9 10
21 22 23 24 25

このように螺旋を描画していくのが、ウラムの螺旋です。
まず1のところを座標(0,0)として、2のところを(1,0), 4のところを(0,1)とします。
このとき、ある方向に進んで、曲がって、進んで、曲がってを繰り返すと思いますが、
この曲がる点というのは、上下左右に4つあります。

17 16 15 14 13
18 5 4 3 12
19 6 1 2 11
20 7 8 9 10
21 22 23 24 25

そして、各々の点について、次に曲がる点は青字のところになります。

17 16 15 14 13
18 5 4 3 12
19 6 1 2 11
20 7 8 9 10
21 22 23 24 25

つまり、

位置 座標
右下 (1, 0)
右上 (1, 1)
左上 (-1, 1)
左下 (-1, -1)

としておいて、これらの座標に到達したときは、
右下の点の座標は右下に、
右上の点の座標は右上に、
左上の点の座標は左上に、
左下の点の座標は左下にずらせばよいわけです。

つまり2(1, 1)にたどり着いた時は、

17 16 15 14 13
18 5 4 3 12
19 6 1 2 11
20 7 8 9 10
21 22 23 24 25

とすればよいのです。

さて、これらのことを使ってアルゴリズムを見ていきましょう。
まず、いくつか変数を定義しています。

draw_ulam_spiral.pyx, y = 0, 0
lrx, lry = 1, 0
urx, ury = 1, 1
ulx, uly = -1, 1
llx, lly = -1, -1
dx, dy = 1, 0

x, yは、現在いる座標を示しています。
lrx, lryは右下の座標、urx, uryは右上の座標、
ulx, ulyは左上の座標、llx, llyは左下の座標です。
また、dx, dyは次x方向およびy方向にどれだけ進むかを表しています。

(x, y)を描画したら、次の座標を求めるために、こうします。

draw_ulam_spiral.pyx += dx
y += dy

次に、今いる座標が右下、右上、左上、左下のいずれかの場合は、dxおよびdy、また右下、右上、左上、左下のうち現在いる場所の座標を次のものに変更させる必要があります。

まず右下にいる場合、次は上の方向に進む必要があるので、

draw_ulam_spiral.pydx = 0
dy = 1

とすればよいことが分かります。
次に、右下の座標を一つ右下にずらすので、

draw_ulam_spiral.pylrx += 1
lry -= 1

とすればよいことが分かります。これが、

draw_ulam_spiral.pyif x == lrx and y == lry:
    dx = 0
    dy = 1
    lrx += 1
    lry -= 1

となっている理由です。
これを他にも右上、左上、左下についてelifでくっつければOKです。
本当はdictとかで取得すればよかったと、今さらながら気がつきました。

あとこのまま行くと、点と点の間の長さを決められなくなるので、実際に描画する際は、xおよびydistanceを掛けることで最終的な座標を求めています。

これを使って実際にウラムの螺旋を描くと、こうなります。

全てを描いたウラムの螺旋

……まあ、螺旋も何もないですよね。
なぜならこれは若干厄介な方法で格子点を描画しているのに過ぎないのですから。
この螺旋の本領を発揮するのは、先ほどアルキメデスの螺旋でもやったように、部分的に描画しない点を設定することです。

この描画する点を指定するのは、先ほど同様 draw_index です。
先ほどと同じように指定できます。
ただここで一つ違うのが、先ほど点は0番目からカウントと言っていましたが、ここでは base_val 番目からカウントを行います。
それゆえ、draw_spiral() 関数の引数で指定をしない限り、1番目からのカウントになることに注意してください。
ではこれで素数螺旋を描画すると、こうなります。

おっ、きれいですね。
何かx状に模様が入っています。
ただこれは、ウラムの螺旋だからこうなっただけかもしれません。
ランダムに螺旋を作ってみましょう。

確かに、先ほどのように整った螺旋には見えません。
で、さっきと同じルールで作った螺旋も見てみましょう。

特段規則性は見られませんね。
素数でないと、今のところはああいった模様は描けないみたいです。

あともう一つ、先ほどの記事によれば、こんなこともあるらしいですね。

ウラムの螺旋を1始まりではなく41始まりにすると、
素数螺旋の点はより一直線に並ぶ。

そしたらやってみるしかないですよね。
その実、そのために base_val を設定したのもあります。
というわけで、41で見てみると、以下のようになります。

確かに中心部にかなりはっきりと斜め線が見えている……。
これはオイラー素数の話に繋がってくるみたいですが、
ここではその話はしません。

ウラムの螺旋と最小公倍数

ウラムの螺旋については、これともう一つ面白いものを見つけたので、そちらも紹介していきましょう。

最初のセッティング通り、base_val を初期値1のままにして、以下のようなプログラムを実行しました。

draw_rule_ulam_spiral.pyfrom draw_ulam_spiral import draw_spiral

hmp = 150000
for rule in range(1, 1001):
    draw_index = set([i for i in range(hmp) if i % rule == 0])
    draw_spiral(draw_index=draw_index, num_all_points=hmp, file_name='rule_ulam_spiral_{}.jpg'.format(rule))

ここでのdraw_ulam_spiralは、draw_ulam_spiral.pyのことです。
こうすると、rule個おきに点を打ったウラムの螺旋の画像ファイルをゲットできます。
なお、 です。
これについて、ruleが2, 3, 6の場合を見てみましょう。

同じ雰囲気を感じませんか?
そうです、思い付けば「当然だよね」となりますが、よく考えてみると、このruleが6の場合のウラムの螺旋は、
ruleが2の場合の螺旋とruleが3の場合の螺旋を比べて、どちらにも描画されている点からなる螺旋であるということです。
これは、6が2と3の最小公倍数であることから求まります。

24の場合も同様で、8と3の螺旋のうち、重なっている点のところだけが描画されます。
下の図は、上からruleが3, 8, 24のウラムの螺旋です。

8や24の場合、右上に斜め線が見えるという形なんですよね。
実は16や32の場合は、左下に斜め線が見えます。
下の図です。

かなり不思議です。
ウラムの螺旋は今回初めて知りましたが、面白かったです。

サックスの螺旋

しかし、このまま行くと、アルキメデスの螺旋において、等間隔に点を打つということを実現するため、難しい積分の話までしていたことが無駄になってしまいます。
何とかしてアルキメデスの螺旋を使った、綺麗な素数螺旋を描くことはできないものかと調べてみました。
先ほどの記事では同時に、「サックスの螺旋」というものがあることも紹介されていました。
書き方としては、こちらのページ が詳しいと思います。
中心には0を置き、あとは周回ごとに平方数を配置するというものです。

アルゴリズム的にはアルキメデスの螺旋とさほど変わりませんが、やることは極端に複雑になっていきます。

まず、素数の処理を簡単にするために、0と1については最初に配置しておくことにします。
これを行っているのが、以下の部分です。

draw_sacks_spiral.pyd = _local_length(2 * math.pi, 4 * math.pi) / 3
if all_show:
    draw.point((width / 2, height / 2), fill=(255, 255, 255))
    num_of_points += 1

    r = radius * 2 * math.pi
    draw.point((width / 2 + r * math.cos(2 * math.pi), height / 2 + r * math.sin(2 * math.pi)), fill=(255, 255, 255))
    theta = optimize.fsolve(_length, theta)[0]

これはall_showTrueの場合にのみ実行されるので、素数のみを見せる場合、すなわちall_showFalseの場合は実行されません。

まず最初のdraw.pointが、0の点を描画する処理です。
その次のdraw.pointの周辺が、1の点を描画する処理です。

次に、1, 4, 9, 16, ...を配置するのは、それぞれtheta, , , , ...となるときです。
そして、からの間に2と3、からの間に5から8、からの間に10から15……といったように、それぞれ等間隔に配置する必要があります。

これは言いかえると、
からの間を3等分、
からの間を5等分、
からの間を7等分、
……という感じだと言えます。
これを一般化すると、
2nπから2(n+1)πの間を2n+1等分と言えます。
このことを使って、thetaを求めていきます。

draw_sacks_spiral.pynext_i = 2
while i < num_of_cycle * num_of_cycle + 1:
    # 中略
    if i == next_i * next_i - 1:
        theta = 2 * next_i * math.pi
    elif i == next_i * next_i:
        d = _local_length(2 * next_i * math.pi, 2 * (next_i + 1) * math.pi) / (2 * next_i + 1)
        next_i += 1
        theta = optimize.fsolve(_length, theta)[0]
    else:
        theta = optimize.fsolve(_length, theta)[0]

まず、next_iとは、現在のiより大きい最小の平方数の正の平方根を与えます。
それゆえ、iを2で初期化するときに、next_iも2で初期化されています。
(2より大きい最小の平方数は4であり、この正の平方根は2であるため)

まず、inext_i * next_iより1小さいときは、次のinext_i * next_iであることが分かります。
それゆえ、誤差を無くすために、theta2 * next_i * math.piを代入しています。
なぜ2 * next_i * math.piなのかということですが、これは具体例を考えればよいです。
たとえば現在のi3のとき、次の平方数は4であり、next_iは2であるので、inext_i * next_iより1小さいことが分かります。
4はthetaのところに配置されるべきなので、2 * next_i * math.piは成り立ちます。
または、現在のi8のとき、次の平方数は9であり、next_iは3であるので、inext_i * next_iより1小さいことが分かります。
9はthetaのところに配置されるべきなので、2 * next_i * math.piは成り立ちます。

次に、inext_i * next_iと等しい場合を考えます。
このとき、iは平方数です。これより、次のいくつかについて、どの間隔で点を配置すべきかを求める必要があります。
先ほどの話より、現在のtheta2 * next_i * math.piであることが分かります。
つまり、

これを一般化すると、
2nπから2(n+1)πの間を2n+1等分と言えます。

先ほど記述したこのルールに則ると、
2 * next_i * math.pi から 2 * (next_i + 1) * math.pi の間を 2 * next_i + 1 等分した値を、dに代入する必要があるということになります。

これは_local_length 関数において、α=2 * next_i * math.pi, β=2 * (next_i + 1) * math.pi を代入し、これを 2 * next_i + 1 で割ったものと一致します。
これより、dは、

draw_sacks_spiral.pyd = _local_length(2 * next_i * math.pi, 2 * (next_i + 1) * math.pi) / (2 * next_i + 1)

という更新式が得られます。
次に、next_i * next_i より大きい最小の平方数は、next_i + 1 * next_i + 1 ですから、この正の平方根は、next_i + 1 です。
ゆえに、next_i を1増やす必要があります。
そのあとはいつものように、thetaを更新すればよいです。

iがこのいずれの条件も満たさない場合は、thetaを普通に更新すればよいです。

これらのことをまとめると、先ほどのようなコードが得られます。


他のことについては、アルキメデスの螺旋でやったこととほぼ同じです。
ただし、draw_indexを無くし、その代わりにall_show引数を追加しました。
ここでall_showTrueにすると、全ての点が表示されます。
反面、all_showFalseにすると、素数の点だけが表示されます。

さて、all_showTrueにして実行してみると、以下のようになります。

ここで、素数に切り替えてみます。すると、

なんか素数が連なっている部分もありそうですね。
radiusを小さくして、より大きな範囲を表示すると、以下のようになります。

なんと!という感じです。
やっぱり、なんか綺麗ですよね。
よくできてます。

おわりに

本日は、様々な螺旋をご紹介しました。
何かお気に入りの螺旋(?)は見つかりましたか?
また、付属のプログラムもいろいろいじっていただければ幸いです。
最後までお読みいただき、ありがとうございました。

]]>
<![CDATA[🔍️数学科目 取る モチベ 数理計算科学系]]>

こんにちは。数理計算科学系 新 年のフナムシです。東京科学大学 数理計算科学系 年までで学ぶ大学数学に

]]>
https://trap.jp/post/2862/69bea1b6e2394c00016e2e56Sat, 21 Mar 2026 18:55:39 GMT

こんにちは。数理計算科学系 新 年のフナムシです。東京科学大学 数理計算科学系 年までで学ぶ大学数学について、面白みを感じた応用例を紹介します。数理計算で何をやるのか気になっている人や、大学数学、モチベがよくわからんという人におすすめです。

導入

皆様、大学数学の勉強は捗っておりますでしょうか?
大学数学はよく抽象化をしますが、必ずしも応用を示しません。その結果、ヨクワカラナイ言葉で書かれたヨクワカラナイ道具が爆誕し、結果 ヶ月後に
僕「なぁ、アレって結局何だったん?」
人「知らん」
...
という話題で花を咲かせる悲劇が高校以上に起こりやすいです。しかし、我々は 年間は大学で学ぶわけで、何かしら面白い計算の つでもして見せたいところです。
本稿では、数理計算科学系で学ぶ数学の一部について、筆者が面白いと思う応用例を紹介します。なるべく身近であって、かつ入門書の中盤以降で出てくる言葉で解くような題材を用意したつもりです。雰囲気で読んで頂けると助かります。

お品書き

  • 線形代数
  • 関数解析(集合と位相)
  • 代数

線形代数

賢い空調システムづくり / Ax 計算高速化

筆者が躓いた所

線形代数の本を開くと、行列は実は線形写像と等価ですとか、ユニタリ変換がどうのこうのとか言った話を良く目にします。はて、それらを使ってどんな面白い計算が出来るのでしょうか。

応用例1 : システムの安定性評価

問題設定

システム(ビルの空調システムや火力発電所を想像して頂けると良いです)を安定に運用できる条件を考えます。

時刻が になる時、内部状態が時刻 のパラメータの線形結合に変化するとします。

例えば、空調システムにおいて、 つの状態


として、

といった具合です。この場合、係数がシステム固有の情報を表しており、係数をいい感じに設定することで を保ちたいな〜そのうえで 小さいと良いなぁ〜などと思うわけです。

ひとまず。

システムが安定、つまり暴走しない事を保証する上で、以下の事を保証する必要があります。

システム運用中に、設定温度からの偏差や空調機の出力が際限なく上昇するといった、状態の歯止めのきかない変化が起きない。

また、システムを安定に運用できる範囲内で、効率を最大化したいです。なので、システムにはこちらが設定できるパラメータ が存在するとします。

つまり我々の興味は、システムが安定な の範囲です。

定式化例

時刻 におけるシステムの状態を で表し、システムをパラメータ を用いて行列 で表す。この時、

である。

システムが安定であることは次で定式化される。

ここで、 が対角化可能であるならば、 は次の表示を持つ。

今任意の を考えており、 は正則行列なので、結局

である。

応用例2: 行列の分解

問題設定

行列とします。

の計算を高速化したいです。 ここで、 は一般とは限りません。お好きに(くだらなくならない程度に)性質を付加して構いません。

定式化例

とする。ここで、行列 であって、

を満たすものが存在する。

ならば、

とすることで計算が高速化される。

計算量は から になる。例えば の時, 倍速になる。

感想

世の中の多くの物は行列でかけます。なので、その行列について詳しいと嬉しいというメリットがあると思います。

空調の話について、システムが暴走しないでほしいという我々のふわっとした要求が、ある数値 を用いて というとても簡単な条件で出てきます。これならばチェックも簡単でマニュアル化等できますし、のちの応用でも使える気がします。  
なお、 を条件とすることで、 なんと を保証できます。つまり、設定温度への収束を保証しつつ将来的な消費電力 まで保証できるわけで、これはちょっとアガります。

の話について、これは機械学習で良く起こると言われており、計算資源に難がある研究環境で特に頻繁に実用されています。

関数解析(集合と位相)

ゲーム AI 作成

筆者が躓いた所

私たちは の世界の問題を解きたいのであって、それより一般化した話はちょっと、怖いです。

応用例: 動的計画法(マルコフ決定過程文脈)によるゲームAI

問題設定

ゲームAIを作ることを考えます。

ゲームとして、ブロック崩しやテトリス、ルービックキューブを考えます(抽象的に言うと、マルコフ性を持つ 人プレイゲームを仮定していますが、深く考える必要はないです)。

特定のゲームを選ぶと、状態集合 , 行動集合 , 即時報酬関数 , 遷移確率関数 , 割引率 が既知あるいは設定できるパラメータとして手に入ります(詳しくは説明しません、諸々の定数と思っていただければ良いです。また、

行動価値関数 を定義します。

この時、次の命題が成立します:

についての方程式(ベルマン最適方程式)

を満たす を (存在するならば) と置く。
ここで、各盤面 について

を達成する行動 を選択する戦略は最適戦略の1つである。つまり、即時報酬関数によって定義される、ゲーム全体を通した収益を最大化する戦略である。

つまり、我々の興味は、 が与えられ、かつ を定義した時、ベルマン方程式を満たすような関数 を求める事です。 が一度求まれば命題の戦略を取ることで文字通り最適なプレイが実現され、それは時に人のプレイを凌駕するでしょう。

なお、このアプローチによるゲーム解析を動的計画法と呼びます。

定式化例

距離空間から出てくる著名な定理の1つに、バナッハの不動点定理というものがある。

バナッハの不動点定理(縮小写像の原理)
を完備距離空間とし、 から 自身への縮小写像とする。この時、次の つの命題が成立する:

  1. を満たす がただ つ存在する
  2. 内の任意の初期点 を選び、漸化式 によって点列 を定義すると、この点列は のとき不動点 に収束する

今回、ベルマン作用素

とすると、 は一様ノルムに置いて縮小写像であり、かつ を満たす。よって、

と更新していくことで が求まる。なお、収束の速度は指数的である。

感想

が満たすべき方程式が手に入ったと思ったら、それを漸化式とみなして更新を繰り返すだけで が求まってしまいました。あっけないものです。

まぁ、実際の(面白みがある)ゲームでは とか とかになってしまって、計算回らないんですけどね。

ともかく。、 つまり で記述された方程式の問題が、抽象化を推し進める分野から出た結果によって鮮やかに解かれたわけです。

また、今回 は引数がせいぜい可算無限ですが、ゲームの設定によっては は引数に実数を取りえます。これは行動空間や状態空間が非加算無限であれば良く、例えば車の自動運転などでは実数として扱うのが自然でしょう。

そして、その場合でも大筋は変わりません。これは成功裏に「関数同士の距離」や「関数の収束」を考えている事になり、これらの拡張を踏まえると集合と位相・関数解析のような抽象化した議論を経るのが自然です。

代数

頭が良い符号作成

筆者が躓いた所

私たちは の世界の問題を解きたいのであって、それより一般化した話はちょっと、怖いです。

応用例: 情報理論における誤り訂正符号

ある通信局から別の通信局に 列を送る事を考えます。

通信の過程で、ノイズが入ってしまいます。具体的には、各文字について、確率 で反転してしまいます。

ここで、送りたい文字列に追加で誤り訂正のための符号を追加することで、誤りに強くする方法を考察します。

単純なものとして、繰り返し符号化を紹介します。例えば

を送りたい時、

を送るという取り決めを考えます(空白は見やすさのためで、実際に送られる文字列にはありません)。

文字列を復元する側はこう考えれば良いです:前から3文字ずつ見ていく。 で多い方を採用する。

前回の例で言えば、

が届いたならば、

と復元することになります。

文字ごとの誤り率は です。 , 例えば ならば となり、送る文字列長が短い間は実用的になるでしょう。

ここまではちょっとした算数パズルのようですね。

ここからは、より効率的な符号を考えたいです。

符号の誤り訂正能力が であることを次で定義します:

符号に発生したノイズが 個以下である時、元の符号を正しく復元できる。なお、個数には訂正の為に追加した bit に発生したものを含む。

我々が求めたいのは、元々送りたい 01 列の長さに対して割合が小さい bit 数で、かつ誤り訂正能力が高い符号です。さらに、要求する誤り訂正能力に対応して付加する訂正 bit を少なく出来ると理想的です。

ただし、簡略化のため以下の仮定を入れます:

  • 送りたい 文字列の長さは全て等しい
  • 付加する誤り訂正 bit の個数も全て等しい

議論の文脈がないのでアレなんですが、結構自然な仮定です。

定式化例

ここで、 符号を導入する。

概要:

の原始元を とし、 以下の任意の正整数、 を満たす を導入する。この時、生成多項式を次のように定義する:

この時、 により生成される巡回符号(という、別で定義される符号)を BCH符号と呼ぶ。

性能:

全体の符号長 =
元々送りたかった01列長
誤り訂正能力

つまり、誤り訂正能力 を持つ符号化方を、追加 bit 数 程度で得られる。

感想

長さ 列を送りたい時、最初の「3個繰り返し符号」は訂正能力=1を得るために の追加 bit を付加していました。

しかし、代数学の力を借りた 符号では、 の追加 bit でこれを実現しています。つまり、オーダーレベルでの削減に成功しているわけです。

おまけに 符号は誤り訂正能力を簡単に調整可能ですので、遥かに実用性が高いでしょう。

元々は 列を効率的に送りたかったというだけなのに、こういった議論が突然出てくるのだから驚きます。

終わりに

いかがでしたでしょうか。他にも、オートマトンは回路の設計・効率化やコンパイラ作成に使えるなどという低レイヤな話や、複素解析はとりあえず が便利という低レベルな話などぜひ紹介したかったのです。が、あまり張り切りすぎるのも良くない、というか書けるか怪しいということでここまでにします。 年間の振り返り的なノリで書いたのですが、読める文章になっているか少し不安です。
この記事が学習の一助になれば幸いです。

]]>
<![CDATA[Web Speed Hackathon 2026 参加記]]>このブログは新歓ブログリレー2026のブログではありません

はじめに

2026年3月20日~3月21日にかけて CyberAgent が主催

]]>
https://trap.jp/post/2864/69bea323e2394c00016e2e90Sat, 21 Mar 2026 15:09:03 GMT

このブログは新歓ブログリレー2026のブログではありません

はじめに

2026年3月20日~3月21日にかけて CyberAgent が主催する Web Speed Hackathon 2026 が開催されました。ざっくり言うとめちゃめちゃ重いWebサイトを渡されて、それをいかに速くできるかを競う大会です。詳しいことが知りたい人はコンテストのページとか過去の参加記とかを読んでみると面白いと思います。

Web Speed Hackathon 2026
Web Speed Hackathonとは、予め準備してあるWebアプリケーションのパフォーマンスを改善することで競い合うハッカソンです。主にWeb技術(フロントエンドおよびNode.js)に関するチューニングを出題いたします。表示に非常に時間がかかるサービスをどこまで高速化できるかを競います。
Web Speed Hackathon 2026 参加記

結果

負けました。

Web Speed Hackathon 2026 参加記
https://web-speed-hackathon-scoring-board-2026.fly.dev/

でも言い訳をすると、本当にちょっとの変更で落ちてて、かなり惜しかったです。苦しい。

やったこと

リポジトリはこれです。運営が用意した fly.io にデプロイしてたので、全ての情報がここに入ってます。

GitHub - cp-20/web-speed-hackathon-2026: https://cyberagent.connpass.com/event/371488/
https://cyberagent.connpass.com/event/371488/. Contribute to cp-20/web-speed-hackathon-2026 development by creating an account on GitHub.
Web Speed Hackathon 2026 参加記

ゆるっと時系列順に並んでますが、やや入れ替えてるところもあります。

最初

とりあえず最初は重すぎてアプリを開くのもままならない感じなので、デグレ同行とかをあんまり考えずにガシガシ変更を入れてました。

  • バンドラーの設定いじる
  • サーバーの配信設定がデチューニングされてるの直す
  • AIに適当に調べさせたところで簡単に直せそうなところをAIに投げる

バンドラー移行する

将来的にバンドラー移行したいと思ってたので、変なパッケージを削る作業もしてました。

  • ALT周りの操作をサーバーサイドに寄せる (画像は常にwebpで扱うように)
  • bm25/kuromoji による検索周りもサーバーサイドに寄せる
  • 動画・音声周りの ffmpeg もサーバーサイドに寄せる

その上で webpack → vite の移行をしました。AI に書かせたらスッとやってくれたので、楽ちんでした。前にやった時は tsup → vite があんまり上手く行かなかったのでドキドキしてたんですが、今回は全然詰まらなくて良かったです。

重そうな箇所を適当に直す

適当にページを見て Lighthouse 回して重いところ探したり、bundle analyzer 見て重いパッケージ消したりしてました。どこを直すかは自分で決めて、改善はほとんどAIにやらせてました。

  • 嘘 infinite scroll を直す
  • negaposi analyzer 周りをサーバーサイドに寄せる
  • moment を Temporal / Intl に置き換える (Temporal が PlayWright で動かなかったので後で Date にした)
  • LLM 周りのパッケージを lazy loading
  • lodash 消す (実はこの時点では redux が依存しててまだいる)
  • 翻訳周りを lazy loading
  • Tailwind CDN やめる (Vite Plugin に)
  • AspectRatioBox やめる
  • 画像に loading=lazy つける

E2E回しつつ、改善する

ここら辺までくると割とまともに E2E が回るようになってくるので、回してました。VRT は expected を上手く作れないので全然機能してなかった気がしますが、機能面でのテストはいくつか本当のバグを発見してくれました。

具体的には検索でそもそも 500 エラーで何も返ってこなかったり、なんか件数が少なかったり、エラー文言が違ったりといったバグを見つけられました。

ただアプリケーションを改善するにつれて、E2E テストがアプリケーションにそぐわなくなってしまうところは E2E テスト自体をいじって改善していました。例えば動画が canvas で再生されること前提になっていたので、video で再生してもちゃんと E2E が通るように修正していたりしました。

  • タイムラインとかの LCP 要素を preload
  • プロフィール画像を圧縮する
  • 音声の svg を事前生成
  • フォントのサブセット化
  • sendJSON の gzip 消した
  • redux-form を剥がす (redux も同時に剥がせる)
  • database.sqlite をリポジトリに含めずに、Docker ビルド時に動的に生成させる
  • プロフィールヘッダーの色を事前計算 (Tailwind のところでバグらせていたが、ここで直った)
  • Crok のサジェストのリクエストを debounce
  • ReDoS を直した (何個か)
  • タイピング入力中のリクエストを throttling
  • 翻訳を Web Translator API に置き換え

SSRしたい

基本 JS とかメディアとかを削って転送量を削減して FCP/LCP を改善しつつ、変なリレンダリングを削って CLS を改善していたんですが、ここら辺で FCP/LCP が結構頭打ちになってきました。ということで奥の手 SSR を発動します。

SSR すると TTFB は悪化しますが、JS を読み込まなくても FCP/LCP の要素を読み込めるのでこれらの指標の改善が期待できます。もっと言えばキャッシュできる部分をキャッシュして ISR っぽくすることで TTFB の悪化をできるだけ抑えることができます。

2~3時間格闘した結果、SSR できました。が、あんまりスコアは改善せず、、

ファーストビュー速くする

せっかく SSR したのでファーストビューを極限まで速くしたいですよね。

  • ALT の dialog を初期レンダリングから除外
  • タイムラインの初期表示件数を少なく
  • Crok ページを lazy loading
  • アイコン周りを個別に svg を読み込むように
  • react-helmet / react-router を自前実装に置き換え

サーバーも速くする

  • DM一覧のAPIのレスポンスを小さく
  • DM詳細のAPIのレスポンスを小さく
  • アップロード時の圧縮は控えめに
  • 不要なバリデーションを削る
  • sendFile に gzip 圧縮を噛ませる

所感

基本はAIが全部書いてくれて楽で速いんですが、結構壊してくるのでそこの担保をちゃんとやる必要がありますね。あと得意不得意はかなりハッキリあって、サーバーサイドで使われてた sequelize というライブラリはかなり苦手そうでした。特に今期テストの外で宣言されているものが重要だったりして、バグったコードを平気で書いてくることもありました。

途中から E2E は結構回してて、かつ最後は initial commit の状態と目視で比較して差分を調べてて、レギュレーションは気を付けてたつもりなんですが、落ちちゃいました。最後の1時間でDM一覧の画面が結構壊れてた (APIの返すものが全然違った) のを直したのはかなりドキドキしました。

おわりに

参加者の皆さんお疲れさまでした。最後の方はかなりチューニングが頭打ちになってたのでボクよりもスコア高い人は本当にすごいと思ってます。

そして運営の皆さんも本当にお疲れさまでした。いつも面白い問題と厳格なレギュレーションチェックをありがとうございます。無事引っ掛かりました。

来年こそは優勝します。

]]>
<![CDATA[自炊のすゝめ]]>はじめに

この記事は新歓ブログリレー2026の16日目の記事です。

どうもこんにちは、初めましての人は初めまして

]]>
https://trap.jp/post/2851/69b02d1fe2394c00016e1c84Sat, 21 Mar 2026 09:00:34 GMTはじめに自炊のすゝめ

この記事は新歓ブログリレー2026の16日目の記事です。

どうもこんにちは、初めましての人は初めまして。
25BのgenMira(ミラと発音してください)と申します。

この春から科学大生になった方には一人暮らしを始めるつもりの方もいると思います。
しかしそこで立ちはだかるのが─────自炊の壁。

コンビニ弁当では栄養が偏るし、外食は高すぎる。とはいえ、自炊をする気も起きない。
毎日毎食作るのは大変すぎる、そもそも今まであまり料理をしたことがなく何をどうやって作ればいいのかもわからない。

そういう人、たくさん見てきました。

そこで本日は自炊をやり始めたいけど手が出せない皆さんのために、自炊をスタートさせるためのコツとして僕が普段気にしていることを説明していこうと思います。

自炊のお気持ち〜食費編〜

そもそも自炊は何のために行うのでしょうか。
まず思いつくのは食費を外食等に頼るのに比べて浮かせるという目的です。
確かに、自炊によって食費を大幅に削ることはできます......が、それはあくまで上手くやればの話。
初心者がよくやるミスの例を紹介しましょう。

「よし、俺も自炊に挑戦だ!最初に作る料理は何がいいかな、調べてみるか.....お、ミネストローネとか良さそうじゃん!早速食材を買い出しに──」

はいストップ。
すごくいい心がけですね。しかし注意しないといけないのは、この例の時点では別に食費は削れてないということです。

それはなぜか。食材を1から全部買っているからです。
考えてみてください。食材は一種類当たり数百円はしますから、1から全部買い揃えるのにはそれなりにお金がかかります。近年の物価高騰の影響も考えれば尚のことです。

それならむしろ、大量生産、大量消費を行っているレストランのミネストローネの方が原価を抑えられると思いませんか?

自炊でなぜ食費が削れるかというと、同じ食材を使いまわせるからです。

買ってきた材料を100%使うわけではありません。その余った食材で次の料理を作るのです。例えば、ミネストローネを作ったとして、余った玉ねぎとキャベツを野菜炒めに転用するとか、じゃがいもやにんじんを味噌汁に使うといった具合です。これにより次の料理を作るときに購入する必要のある食材を減らせます。これで食費を削るのです。

あなたのお母さんが冷蔵庫を見て「今日は豚肉があるから、豚の生姜焼きね」などと言っている姿を見たことがあるでしょうか。あれは今ある食材を見て、それを使い回す料理を考えるということをやってたわけです。

そういう意味では味噌汁って神料理なんですよ。とりあえず余った食材を茹でて味噌を入れれば完成するんですから。あれだけ多用されるのも納得ですね。

まとめると、自炊では今ある食材をいかに活用した料理を作るかが大切ということです。今日はミネストローネ、明日は豚の生姜焼き、その次の日は麻婆豆腐......とやってたら食費は浮きません。料理の時間考えたら冷凍食品で食べる方がいいです。

自炊のお気持ち〜作り置き編〜

自炊のもう一つのいいところはコンビニ弁当などよりも栄養バランスがしっかりするということです。

そこに欠かせないのが一汁三菜;主食、汁物、主菜、副菜をきちんと用意することです。洋食なら主食、スープ、メインディッシュ、サイドメニュー。(あれ、なんか違う気が.....)

え、毎日こんなに何種類も作るのしんどいって? 奇遇ですね、僕もです。

しかし自炊にはこの問題を解決する手段があるのだ! それは、作り置きというもの。
やり方は簡単で、100均などでタッパーを大量に買いましょう。そしてそこに作った料理を入れて冷蔵庫に入れるだけ。

例えばほうれん草の胡麻和えや大根の煮物、もやしや茄子のナムル、白菜やきゅうりの漬物、ひじきの煮物...etc.

こういうのは一度作ってしまえば数日は持ちます。その上、10分もあればできてしまうのがほとんどです。

多くの人が自炊と聞いて考えるのは主菜のことです。しかしそれではよろしくない。栄養バランスを保つのに重要なのは副菜なので、作り置きを活用してちゃんと用意しましょう。それに主菜だけの時より食事の満足感も爆上がりです。

自炊のお気持ち〜醍醐味編〜

自炊の醍醐味といったらそれはもちろん、自分の食べたいものを、自分の食べたい材料を使って、自分の食べたい分量で好きに作ることができる点です。

例えば自分でラーメンを作るとして、あなたがもやしが大好きな人ならば、好きなだけもやしを山盛りにすればいいし、最近野菜不足で野菜を多く摂りたいと思っていたなら、野菜マシマシにすればいいし、脂多いのやだなーと思えば油を使わずヘルシーに仕上げれば良いのです。

なんたって、自炊で作った料理を食べる相手は自分以外いないのですから、自分の好きなように作れます。これは外食にも冷凍食品にもない長所です。

よくある調理方法

さて、色々と御託はわかったと、それでもこっちは料理の仕方がわからないんだよという方もいると思います。そういう方に向けて、ここからはよくある簡単な調理方法とその注意点を述べていこうと思います。もっと具体的なレシピについては後日投稿予定なのでお楽しみに!

炒める

王道の調理方法である一方、注意を怠ると焦がしてしまうことがあります。

焦がさないようにするためには、菜箸やフライ返しなどでよくかき混ぜると良いでしょう。
それは実践している方も多いですが、他にも火を強火ではなく中火くらいに留めておくことも有効です。中火というと火の先端がフライパンにちょうど届くくらいですね。

例えば好きな野菜(ピーマン、もやし、玉ねぎ、キャベツなど)を炒めれば野菜炒めになります。豚肉やソーセージといった肉類を入れると美味しいですし、好みによっては舞茸や椎茸といったきのこ類を入れるのもありです。

とりあえず余った野菜などがあったら炒め物にしておけばなんとかなります。
味付けは(簡単に済ませるのなら)塩胡椒か、醤油をかければ大体美味しいです。

煮る

深底の鍋に水を入れて火にかけるだけです。焦がすことはないので放置でいいのが利点ですが、沸騰によって鍋から水が溢れてしまうことがあるので、やはり中火や弱火がいいです。強火は中華料理を作るときだけにしましょう。
一般的に食材は大きめに切る方がいいとされています。大抵の場合適当な具材に菜箸を突き刺してよく通ったら完成の合図です。

この方法の一番難しいところは味付けの仕方ですね。まず出汁をどうするべきか。和風なら鰹出汁や昆布出汁、洋風ならコンソメなどですね。そこに醤油や味醂、味噌汁なら味噌などで適切に味付けしましょう。ここはレシピを参考にしてちょうど良くしないと美味しくならないので分量には気をつけましょう。

電子レンジ

実は電子レンジだけでも十分な調理ができる料理があります。フライパンなどを用意する必要がないので種類は制限されますが一番手軽な調理方法ですね。
「電子レンジ レシピ」などと検索するとレシピがたくさんヒットします。著者は主に副菜を作るのに使っています。

終わりに

いかがでしたでしょうか。僕の話で自炊についてなんとなく雰囲気が掴めたら幸いです。具体的なことはほとんど書いてないので、自炊いいなーと思った人はここからYouTubeなどでさらに調べてみてください。

明日の投稿者は@zoi_dayoさんです。お楽しみに!
(ちなみにその翌日オススメレシピ集を投稿予定です)

ではまたどこかで!

]]>
<![CDATA[好きなファイルを音楽にする]]>

こちらの記事は2022年11月に部内向けに書いたブログを、外部公開向けに書き直したものです。
そのため不勉強な

]]>
https://trap.jp/post/2819/699fe3b7e2394c00016df7e0Sat, 21 Mar 2026 00:00:48 GMT

こちらの記事は2022年11月に部内向けに書いたブログを、外部公開向けに書き直したものです。
そのため不勉強な部分もあるかと思いますが、温かい目でご覧いただければ幸いです。

はじめに

こんにちは。koukawa_ppです。

唐突ですが、「一度はやってみたいな」と思うことはありますか?
僕はタイトルにもありますが、「任意のファイルを音楽にできないか」という野望を抱えながら、つい最近まで過ごしてきました。
とはいえ、「どうすれば任意のファイルを音楽にできるだろうか?」という壁にぶち当たります。
今回は自分なりにですが、任意のファイルをmidiファイルに変換するプログラムを作成しましたので、皆さんに紹介したいと思います。

なお、使ったファイルは こちらのGitHubリポジトリ に入れてあります。

環境

  • Windows 10 Home 64bit
  • Visual Studio Code
  • Python 3.11.0(ここまで新しくなくても動作するはずです)

いきさつ

ファイルをもとにして、何かしらの他のものに変えるというのは案外やりやすいと思っています。
なぜならファイルのバイナリを用いれば、そこから他のものを作り出すということはかなり容易だからです。
適当な話、これらビットに何かしらの役割を付与して、そこからmidiファイルを作り出そうという発想はありました。

では、得られたそのビット列を、どのようにして音楽にするかということです。
もちろん適当にコードとメロディーとリズムを組み合わせるだけで、それを音楽というのはいささか気が引けます。
素晴らしい音楽になるかはさておき、とりあえず「こういう曲ありそうだな」レベルまでには引き上げる必要があるわけです。

そこでこのビット列から、どのようにそういったものを作成する努力をしたか、説明していきたいと思います。

バイトの振り当て方

概要

このファイルを作るにあたって、以下のようなメモを用意しました。

1バイト
ドラムのリズム設定

2バイト
バスドラムについて1

3バイト
バスドラムについて2

4バイト
バスドラムについて3

5バイト
バスドラムについて4

6バイト
バスドラムについて5

7バイト
フィルインで叩くドラムを設定1

8バイト
フィルインで叩くドラムを設定2

9バイト
フィルインで叩くドラムを設定3

10バイト
フィルインで叩くドラムを設定4

11バイト
(1小節目)リズムを選択する

12バイト
(1小節目)コードを選択する

13~20バイト
(1小節目)鳴らす音を設定
(鳴らさないところは無視)

21バイト
(2小節目)リズムを選択する

22バイト
(2小節目)コードを選択する

23~30バイト
(2小節目)鳴らす音を設定
(鳴らさないところは無視)

31バイト
(3小節目)リズムを選択する

32バイト
(3小節目)コードを選択する

33~40バイト
(3小節目)鳴らす音を設定
(鳴らさないところは無視)

41バイト
(4小節目)リズムを選択する

42バイト
(4小節目)コードを選択する

43~50バイト
(4小節目)鳴らす音を設定
(鳴らさないところは無視)

以上のような設定に則って、どのように作ることにしたのかということをざっくり説明します。

ドラムのリズム設定(1バイト目)

そもそもドラムっていうのは、適当に作ってもかっこよく聞こえます(経験上)。
ただしそれが叩けるかどうかは別問題と言ったところです。
今回作ったリズムでは、ハイハットと同時にフィルインも鳴っているので、この状況においては叩けないリズムも作られていると思います。
いつも僕が作るときには、基本なるべく生身の人間でも叩けるように作っています。

今回、一度決めたドラムは基本的に4小節単位で繰り返されます。

まず1バイト目では、おおよそドラムのリズムを設定しています。
ビット列については、以下のように決めています。

桁数 概要
1, 2, 3, 4 ハイハットのリズムを選択する
5, 6 スネアドラムを鳴らすか(00…鳴らさない, 01…4拍目で鳴らす, 10…3拍目で鳴らす, 11…2,4拍目で鳴らす)
7, 8 最初にクラッシュシンバルを鳴らすか(00で鳴らさない、他は鳴らす)

ハイハットのリズムの選択方法については、後で説明します。

バスドラムについて(2~5バイト目)

バスドラムについては、以下のように決定しています。

2バイト目

桁数 概要
1, 2, 3 一拍目でバスドラムを鳴らすか(000で鳴らさない)
4, 5, 6 二拍目でバスドラムを鳴らすか(000で鳴らさない)
7, 8 一拍目裏でバスドラムを鳴らすか(00で鳴らさない)

3バイト目

桁数 概要
1, 2, 3 三拍目でバスドラムを鳴らすか(000で鳴らさない)
4, 5, 6 四拍目でバスドラムを鳴らすか(000で鳴らさない)
7, 8 二拍目裏でバスドラムを鳴らすか(00で鳴らさない)

4バイト目

桁数 概要
1, 2, 3 一拍目二つ目の十六分音符でバスドラムを鳴らすか(111で鳴らす)
4, 5, 6 二拍目二つ目の十六分音符でバスドラムを鳴らすか(111で鳴らす)
7, 8 三拍目裏でバスドラムを鳴らすか(00で鳴らさない)

5バイト目

桁数 概要
1, 2, 3 三拍目二つ目の十六分音符でバスドラムを鳴らすか(111で鳴らす)
4, 5, 6 四拍目二つ目の十六分音符でバスドラムを鳴らすか(111で鳴らす)
7, 8 四拍目裏でバスドラムを鳴らすか(00で鳴らさない)

6バイト目

桁数 概要
1, 2 一拍目四つ目の十六分音符でバスドラムを鳴らすか(11で鳴らす)
3, 4 二拍目四つ目の十六分音符でバスドラムを鳴らすか(11で鳴らす)
5, 6 三拍目四つ目の十六分音符でバスドラムを鳴らすか(11で鳴らす)
7, 8 四拍目四つ目の十六分音符でバスドラムを鳴らすか(11で鳴らす)

二つ目とか四つ目の十六分音符とは何かということですが、

十六分音符

上のような感じです。
このような表現方法は後でも出てきます。

このことから分かるように、バスドラムについては、鳴らす確率が以下のように決められていると分かります。
(ただし、ビットの並び方が同様に確からしいとき)

バスドラムのインデックス 確率
1 7/8
2 1/8
3 3/4
4 1/4

基本的に1拍目と3拍目が鳴りやすいように細工しています。
2つ目だけが鳴るということはかなり稀です。

フィルインで叩くドラムを設定(7~10バイト目)

ここでは、フィルインでどういった楽器を鳴らすかを決定します。
その表は以下の通りです。
基本的に4ビットで決定していることが分かると思います。
ただし、*はワイルドカードだと思ってください。

ビット列 楽器
000* スネアドラム
001* ハイ・トム
010* ハイ・ミッド・トム
011* ロー・ミッド・トム
100* ロー・トム
101* フロア・トム
1100 ライド・シンバル
1101 クローズド・ハイハット
1110 オープン・ハイハット
1111 何も鳴らさない

1/16の確率で何も鳴らない(休符になる)ということです。
そして、タイミングは以下の通りです。

7バイト目

桁数 概要
1, 2, 3, 4桁目 3拍目1個目の十六分音符で何を叩くか
5, 6, 7, 8桁目 3拍目2個目の十六分音符で何を叩くか

8バイト目

桁数 概要
1, 2, 3, 4 3拍目3個目の十六分音符で何を叩くか
5, 6, 7, 8 3拍目4個目の十六分音符で何を叩くか

9バイト目

桁数 概要
1, 2, 3, 4 4拍目1個目の十六分音符で何を叩くか
5, 6, 7, 8 4拍目2個目の十六分音符で何を叩くか

10バイト目

桁数 概要
1, 2, 3, 4 4拍目3個目の十六分音符で何を叩くか
5, 6, 7, 8 4拍目4個目の十六分音符で何を叩くか

メロディー・コード作り(11~50バイト)

めちゃくちゃ長いように見えますが、実際はそこまで大変ではありません。
というのもここはかなりズルしているからです。

内訳としては、

  • 11~20が1小節目
  • 21~30が2小節目
  • 31~40が3小節目
  • 41~50が4小節目

となっています。
ここでは、各バイトについて、10で割った余りのバイトについて、どのような役割を担っているか説明します。

10で割った余りが1のバイトについて

ここでは、メロディーのリズムを決定しています。
方法としては、323個のリズムパターンの中から、ビット列によってリズムを決定するというものです。
0~255までしか表現できないのに、323個あったって仕方ないと思われるかもしれませんが、確かにその通りです。
とはいえ、ここでどう考えても選ばれなかったリズムについては、そこまでということにしてあります。

10で割った余りが2のバイトについて

ここでは、コード(和音)を選択しています。
方法としては、7個のダイアトニック・コードから、ビット列により一つ選択するといった具合です。
ビット列は0~255まで表現できますから、7個のダイアトニック・コードから和音をランダムに選択するのは容易です。
ダイアトニック・コードとは、あるスケールにおける音のみによって構成された和音のことを言います。
C-majorにおいて、ダイアトニック・コードを五線上において表せば、以下の通りです。

ダイアトニック・コード

一つも#や♭が付かない親切設計というわけです。
これをつなげることにより、多少の違和感はあれど、
そこまで大きな違和感にならないというのが、今回のミソです。

10で割った余りが3~9と、0のバイトについて

10で割った余りが1のバイトにおいては、メロディーのリズムを選択しました。
これは、多くとも8分音符8つからなるメロディーです。
すなわち、使われるか否かはとりあえず置いておいて、8つの音の候補さえ用意しておけば、メロディーを完成させることができるという寸法です。

メロディーの音の選び方についてですが、もちろん聞いて選ぶことはできません。
そこで、和音の構成音からメロディーの音を選択するという方法を取ります。
昔は僕もそういう作り方をしていた時代もありましたし、今も和音の音に釣られて音を決めることは多々あります。
そういうわけで、これが一番シンプルな決め方なのです。

さて、
10で割った余りが3のバイトを1つ目の八分音符の音にして、
10で割った余りが4のバイトを2つ目の八分音符の音にして……と続け、
10で割った余りが9のバイトを7つ目の八分音符の音にして、
10で割り切れるバイトを8つ目の八分音符の音にすれば、
全ての音符に音を割り当てることが出来るようになります。

リズムパターンによっては使われないバイトも出てきますが、その時はその時でしょう。

さて、各バイトについて、どのように音を決めているかは以下の通りです。

桁数 概要
1, 2 和音のどの構成音を基準にするか(00, 01なら主音、10なら三度に値する音(長三度、短三度)、11なら五度に値する音(完全五度、増五度、減五度))
3 4, 5桁目で「そのまま」以外が選択されたとき、基準音から上がるか、下がるか(0なら上がる、1なら下がる)
4, 5 音を何度変化させるか(00...そのまま, 01...二度, 10...三度, 11...四度、変化はスケールに則る)
6, 7, 8 これらの桁が全て0かつ、1が0, 4, 5が01のとき、すなわち基準音が主音で、一つとなりの音が選択されたとき、この音を半音上げるか下げる。上げるか下げるかについては与えられたビットによってランダムに選択される。

これらを8つのバイトについて行えば、音が選択できるはずです。
時々ぶつかる音もあると思いますが、それもそれで一興です。

バイト割り当てのまとめ

以上をプログラムに書き換えることが出来れば、目的としては達成です。
今回は50バイトで4小節作ることが出来る形になっています。

プログラムを作成する

さて、ここまでは楽典でしたが、ここからはプログラムパートです。
実行だけされたい方は、GitHub のリポジトリからファイルをダウンロードして実行してください。
(「プログラムを実行する」の項まで飛んでいただきますと使い方をご覧いただけます。)

データを作成する

データは以下のようなものです。
長いので途中省略しています。
フルバージョンはGitHubからご覧ください。

projectData.pyrhythm_data = [
    # 略
]

note_list = [
    'C',
    'C#',
    'D',
    'Eb',
    'E',
    'F',
    'F#',
    'G',
    'Ab',
    'A',
    'Bb',
    'B'
]

scale_list = [
    0,
    2,
    4,
    5,
    7,
    9,
    11
]

chord_list = [
    ['C4', 'E4', 'G4'],
    ['D4', 'F4', 'A4'],
    ['E4', 'G4', 'B4'],
    ['F4', 'A4', 'C4'],
    ['G4', 'B4', 'D5'],
    ['A4', 'C5', 'E5'],
    ['B4', 'D5', 'F5'],
]

hihat_list = [
    [0, 0, 0, 0],
    [1, 0, 0, 0],
    [0, 0, 1, 0],
    [1, 0, 1, 0],
    [0, 0, 1, 1],
    [1, 1, 1, 0],
    [1, 1, 0, 1],
    [1, 0, 1, 1],
    [0, 1, 1, 1],
    [1, 1, 1, 1],
    [2, 0, 0, 0],
    [0, 0, 2, 0],
    [1, 0, 2, 0],
    [0, 1, 2, 0],
    [1, 1, 2, 0],
    [1, 1, 2, 2],
]

これを、この後作成するプログラムと同じディレクトリに保存してください。

プログラムに必要なライブラリを用意する

今回は、midiファイルを書き出す必要がありますので、そのためのライブラリを手に入れることにします。
名前は、pretty_midiです。
必要に応じて仮想環境を作成し、その中で

pip install pretty_midi

などでインストールできますので、コマンドプロンプトやターミナルなどから実行してください。
こちらのライブラリはかなり優れもので、分かりやすく書かれています。
General Midiの規格をある程度知っていれば、様々なmidiファイルを作ることができると思うのでオススメです。

プログラムの定義部を書き出す

先ほどprojectData.pyを保存したディレクトリに、filetomidi.pyというファイルを作成してください。
ここに作曲のためのプログラムを書いていきます。

filetomidi.pyimport os
import math

import pretty_midi as pm

import projectData as pd

# 変更すべき場所
bpm = 120
chord_vel = 80
drum_vel = 80
melody_vel = 100
song_path = "fileToMidi_py.mid"
file_path = "filetomidi.py"
log_path = "log.txt"

# 曲の小節の一単位。基本的にこの小節ごとにドラムのリズム等が変化する。
bar_unit = 4

# barUnit小節作るのに必要なバイト数。必要に応じて変更する必要がある。
bar4_data = 50

bar_time = 240 / bpm
beat_time = bar_time / 4
st_time = bar_time / 16

# グローバル変数
buf = []

一番上の変更すべき場所は、このプログラムを使ううえで変更することがほぼ必須のものです。
これらの説明は以下の通りです。

変数名 概要
bpm 曲のテンポを設定します。
chord_vel 和音の音量を設定します。100前後をお勧めします。
drum_vel ドラムの音量を設定します。100前後をお勧めします。
melody_vel メロディーの音量を設定します。100前後をお勧めします。
song_path midiファイルの名称を設定してください。プログラム中でos.path.dirname(file)が補われるので、フルパスは入力しないでください。
file_path midiファイルに変換するファイルの名称を設定してください。プログラム中で os.path.dirname(file)が補われるので、フルパスは入力しないでください。
log_path ログファイルを保存する場合、この名前で保存されます。初期ではログファイルは出力されません。プログラム中でos.path.dirname(file)が補われるので、フルパスは入力しないでください。

プログラムを実行する

さて、あとはプログラムを実行するだけです。
一つ上で若干実行方法をお伝えしましたが、ここでもう一回説明します。

1. ファイルの準備

  • filetomidi.py
  • projectData.py
  • midiファイルに変換したいファイル

これら三つのファイルを同じディレクトリに入れます。

2. プログラムを変更する

プログラム「filetomidi.py」を若干変更します。

分からなければ、song_pathとfile_pathだけ変更しましょう。

filetomidi.pyimport os
import math

import pretty_midi as pm

import projectData as pd

# 変更すべき場所
bpm = 120
chord_vel = 80
drum_vel = 80
melody_vel = 100
song_path = "fileToMidi_py.mid"
file_path = "filetomidi.py"
log_path = "log.txt"

この上のsong_pathを、変換後のmidiファイルのファイル名、
file_pathを、midiファイルに変換するファイルのファイル名に変更してください。

3. 実行

あとはfiletomidi.pyを実行するだけです。
しばらく待てば、同じディレクトリに、指定した名前でmidiファイルが生成されるはずです。

4. 聞いてみる

せっかくなので聞いてみましょう。

注意点

一つだけお知らせしておきます。

大きめのファイルを変換すると、それだけでかなり時間が掛かります。
この「時間が掛かる」というのは、生成する時間も聞く時間もです。
生成する時間については(僕個人としては)待てるレベルですが、
再生時間は僕は待てません。

たとえば先ほど、50バイトで4小節作れると言いました。
bpmを120のままにしていれば、50バイトで8秒間の曲を作ります。
さて、100kBのファイルを入れると、曲はどれくらいの時間になるかというと、
16384秒の曲が爆誕します。これは4時間超の曲となります。
とてもではないですが聞いてられないと思います。
逆算しましょう。3分間の、カップラーメンが作れる時間であれば、
これはおよそ1kBほどのファイルが望ましいという結果になります。

試しに1kBのファイルで作ってみましたが、2分40秒でした。
このくらいが多分ちょうどいいと思います。

本来は4小節や8小節作るのにもっとビット列を使えばいいのですが、
これ以上どうやって組み合わせようかと迷ったので、
ここまでにしてあります。
もし何かほかに「こうしてほしい」などありましたら、
お知らせいただけるとやってみるかもしれません。

いろいろ作ってみる

さて、ここまでくると、持っているファイルでいろいろ試すのもありですが、
ランダムなビット列についてもやってみたいと思った方もいらっしゃるかもしれません。

そこで僕は、同様にPythonで、指定されたファイルサイズでランダムにビットを埋めるプログラムを作成しました。
以下の通りです。GitHub にも同じファイルをアップロードしています。
もしよろしければご利用ください。

filemaker.pyimport os
import math
import random

# input file name
file_path = "sample_1k"
# input file size(bytes)
file_size = 1024

with open(os.path.dirname(__file__) + "/" + file_path, "wb") as f:
    number_list = [math.floor(random.random() * 256) for _ in range(file_size)]
    f.write(bytes(number_list))

今回製作されたmidiファイルについて

今回このfiletomidi.pyで作成されたmidiファイルにつきましては、koukawa_ppは権利を主張いたしません。
ただし、この楽曲はfiletomidi.pyによって作成されたということを明記いただけますとありがたいです。

また、元のプログラムはMITライセンスで提供しておりますので、これをさらに発展させたプログラム等も大歓迎です。

おわりに

今回の内容は確かに難しいとは思いましたが、一度アイデアが出てしまえばどうにかなったので、取り組み始めのころからすれば、割合うまく事が運んだのではないかと思っています。

何かこのプログラムにおいて、「こういったものを追加してみるといいのでは」といったものがあれば、ぜひ追加してみてください。GitHubでのフォークなども大歓迎です。
最後までお読みいただき、ありがとうございました。

参考文献

https://github.com/craffel/pretty-midi
↑こちらはpretty_midiのGitHubです。

https://craffel.github.io/pretty-midi/
↑関数の説明がかなり丁寧になされています。

https://qiita.com/marshi/items/18bf9199b1b164ec1856
↑みんな大好きQiitaです。

]]>
<![CDATA[「夏目漱石」を機械学習する]]>当記事は2022年7月17日に、部内向けに書いたブログを外部公開向けに、また現在の時勢に合わせて改めて書き直

]]>
https://trap.jp/post/2818/699f0f25e2394c00016df4f7Sat, 21 Mar 2026 00:00:42 GMT当記事は2022年7月17日に、部内向けに書いたブログを外部公開向けに、また現在の時勢に合わせて改めて書き直したものになります。
そのため画像や情報が一部古い場合、また不勉強な部分もありますがなるべく当時のものをそのまま載せています。あらかじめご了承ください。
GitHub に示しているプログラムについては現在の環境(Windows 11 Home & Visual Studio 2026)で動くことを確認しています。

はじめに

こんにちは。koukawa_pp です。
当サークルはデジタル創作同好会ということで、デジタルに関するあらゆる創作を包含する存在を目指しており、サークル内では「機械学習講習会」というものがかなり前に開催されていたようです。
ここからも分かるように、機械学習は今や「誰もが一度はやってみたいもの」となりました。
しかし生成AIがかなり広まった今となっても、機械学習というものそれ自体はかなり難しそう、という印象はまだぬぐえないですし、そのためにがっつりコーディングする必要があるのではないかと思われる方は多いと思います。
しかし例えばPythonではたくさんのライブラリが登場しており、かなりコーディング量を減らして機械学習を実現できるようになったのはもちろん、先述のとおり生成AIを使えば基本的な機械学習コードはいとも簡単に作ることができるようになりました。
その流れもあり、「機械学習と言えばPython」というのは、もはや常套句のように思えます。

ただここではあえて、C#を使って機械学習をしてみたいという方向を取ります。
「C#じゃめちゃくちゃコード書かないといけないんじゃないか」と思われるかもしれません。
ところがどっこい、C#のNuGetパッケージでも、機械学習を行えるものがオープンソースでMicrosoftから出されています。
その名前は、「ML.NET」というものです。
僕もいまいちよく分かっていないので勉強中ですが、とりあえずこの「ML.NET」というものを用いれば、様々なものの機械学習を行うことができるという触れ込みです。
そこで今回は、「夏目漱石」を機械学習してみようということです。

やることはざっくり言えば、

ある文が渡されたとき、それはどの作品に含まれる文か?

を当てるといったものです。
先行研究を探せば、多分たくさん出てくると思いますし、例えば「○○が書いたような文章」を生成するといったテーマで研究されている方もたくさんいらっしゃると思いますが、今回はごく簡単なものということでやってみます。

そういえば機械生成ではなく作家さんが書かれた文章ですが、「もし文豪たちが カップ焼きそばの作り方を書いたら」という本がかなり面白かったのを記憶しています。また読みたいなと思っています。

また、今回は以下の作品を用います。

  • こころ
  • 吾輩は猫である
  • 三四郎
  • 私の個人主義
  • 草枕
  • 坊っちゃん

選択した作品は適当です。
これらの文章を二つに分け、一方は学習、もう一方はテストに使います。
なお、今回使用したファイルはこちらのGitHubレポジトリで公開しています。
プログラムについてはMITライセンスで再利用いただけます。作品データについては青空文庫さまの利用規約に則ってご利用ください。

環境

  • Windows 10 Home 64bit
  • Visual Studio 2022 Community
  • Visual Studio Code
  • (メモ帳)

データを準備する

まず、今回用いるデータの準備を行います。
この文章に関しては、青空文庫さまのデータを使います。
ファイルの取り扱い方についてはこちらを参考にしています。
夏目漱石作品はすでに著作権が切れていますので、こちらで機械学習しやすいようにデータを編集できます。

ファイルをダウンロードする

こちらに夏目漱石作品の一覧があります。
これらの中から、

  • こころ
  • 吾輩は猫である
  • 三四郎
  • 私の個人主義
  • 草枕
  • 坊っちゃん

についてページを開き、それぞれzipファイルをダウンロードします。
もちろん、ダウンロードしたファイルは解凍してください。

「こころ」のデータの一部

ちなみに、ダウンロードしたファイルは以上のような感じです。

なお、ファイル末尾にある「記載事項」については、
青空文庫さま側は削除されないことを希望されていますが、
ここでは機械学習を行う都合上あとで削除します。
しかし、原本および変更の作業履歴を配布しますので、
そちらをご覧の上、これをまた再配布される場合は
このことを配布先にも提示いただきますようお願いします。

データを編集する

もっと簡単な編集方法があるかもしれませんが、僕は以下の方法で編集しました。
以下では「こころ」について示しますが、他のファイルも同様の編集を行ってください。

1. kokoro.txtをコピーし、kokoro_edit.txtを作成する

原本は残しておくつもりでいるので、一旦「kokoro.txt」を複製し、同じフォルダにコピーしてください。
また、複製したファイルの名称を「kokoro_edit.txt」に編集します。

2. kokoro_edit.txtをVSCodeで開く

詳細な文字の置換は、VSCodeでやるのが便利だと思っています。
開くと、以下のようになると思います。

kokoro_edit.txt の一部

3. ルビを削除する

今回の学習においては、ルビは削除します。
ここで、VSCodeの強力な「置換」機能を使います。

「正規表現を使用する」をチェックして、以下のように「検索」ボックスに入力してください。

置換機能

この正規表現は、

まずに合致する文字を捜し出す
その後、でない文字を任意個並べた部分を探し、
最後にとついている文字列

を探しています。

本来なら《.*》と入力したいところですが、
これだと

《ひとこと》二言《ふたこと》

と選択されてしまうため、不都合です。
ここで、を除外することにより、

《ひとこと》二言《ふたこと》

と選択されるようになります。

こう入力したら、「置換」のところを空欄にして、「すべて置換」を選択してください。

置換した結果

上部「テキスト中に現れる記号について」のルビのところも消えていますが、
ここはあとで削除するので特に不都合はありません。

4. 「ルビの付く文字列の始まりを特定する記号」を削除する

こちらはそこまで難しくないです。
|を検索ボックスに入力し、「置換」のところを空欄にしてから、
「すべて置換」を押してください。

5. 「入力者注」を削除する

ここが実は一番大変です。
まず、以下のようなものを削除します。

[#5字下げ]一[#「一」は中見出し]

これだけ見ると分かりづらいですが、
この[#5字下げ]一[#「一」は中見出し]の上下には
一行ずつ空行が存在している状態です。
これらの空行も削除します。

まず、検索ボックスの中に\n[.*]\n\nと入力してください。
半角と全角を間違えないでください。
そうすると、以下のようにハイライトされるはずです。

入力者注がハイライトされたkokoro_edit.txt

これでいったん「すべて置換」してください。

次に、検索ボックスの中に[[^]]*]と入力してください。
ブラケットは、左から全角、半角、全角、半角、全角です。

前後に空行が存在しない入力者注をハイライト

よさげですね。
これもまた「置換」のところを空欄にしてから、「すべて置換」を押してください。

6. 最初と最後を削除する

最初と最後の脚注を削除してください。
ここまで来たら、「kokoro_edit.txt」を保存してください。
また、VSCodeを閉じて大丈夫です。

7. メモ帳で「kokoro_edit.txt」を開き、文字コードを変更して保存する

そもそも初めからだと思うのですが、ファイルをダウンロードした時点ですでに文字コードが「ANSI」になっています。
これはこの後プログラムで扱うときに非常に面倒なので、メモ帳で「kokoro_edit.txt」を開いたのち、「名前を付けて保存」で文字コードを「UTF-8(BOM付き)」を選択して保存してください。
名称は僕は「kokoro_utf.txt」としました。

8. (私の個人主義のみ)一部削除

――大正三年十一月二十五日学習院輔仁会において述――

も削除してください。

データを(本格的に)作成する

ここまではデータの準備です。なので、ここからはデータを作成します。

やり方としては単純です。

  1. 各テキストのファイルを読み込む
  2. およそ10個に一つ程度の割合で、文章をテスト用データに登録し、それ以外は訓練用データに登録する
  3. 最後にそれらをファイルとして書き出す

これを実行してくれるコンソールプログラムを作成しましょう。
ただ繰り返しになりますが、このコードは2022年7月時点に書いたコードになります。
現在のコンソールアプリでは以下のような namespaceclass, はては Main 関数の記述すらなく、トップレベルのステートメントが使用されるようになっています。
この形式のコードを記述する際には、「最上位レベルのステートメントを使用しない」チェックボックスをOnにすることによって行えます。
プロジェクト名は「MakeSosekiDatas」としました。

Program.csに、コードを以下の通り入力しました。

Program.csnamespace MakeSosekiData
{
    internal class Program
    {
        static int seed = Environment.TickCount;

        static readonly string _kokoroPath = Path.Combine(Environment.CurrentDirectory, "Data", "kokoro_utf.txt");
        static readonly string _wagahaiPath = Path.Combine(Environment.CurrentDirectory, "Data", "wagahaiwa_nekodearu_utf.txt");
        static readonly string _sanshiroPath = Path.Combine(Environment.CurrentDirectory, "Data", "sanshiro_utf.txt");
        static readonly string _kozinPath = Path.Combine(Environment.CurrentDirectory, "Data", "watashino_kojinshugi_utf.txt");
        static readonly string _kusaPath = Path.Combine(Environment.CurrentDirectory, "Data", "kusamakura_utf.txt");
        static readonly string _bochanPath = Path.Combine(Environment.CurrentDirectory, "Data", "bocchan_utf.txt");

        static readonly string _trainDataPath = Path.Combine(Environment.CurrentDirectory, "Data", "traindata.csv");
        static readonly string _testDataPath = Path.Combine(Environment.CurrentDirectory, "Data", "testdata.csv");

        static readonly List<(string, int)> trainDatas = new();
        static readonly List<(string, int)> testDatas = new();

        static void Main(string[] args)
        {
            //こころのデータを作る
            using FileStream kokoroStream = new(_kokoroPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            using StreamReader kokoroReader = new(kokoroStream);
            ReaderToData(kokoroReader, 0);

            //吾輩は猫であるのデータを作る
            using FileStream wagahaiStream = new(_wagahaiPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            using StreamReader wagahaiReader = new(wagahaiStream);
            ReaderToData(wagahaiReader, 1);

            //三四郎のデータを作る
            using FileStream sanshiroStream = new(_sanshiroPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            using StreamReader sanshiroReader = new(sanshiroStream);
            ReaderToData(sanshiroReader, 2);

            //私の個人主義のデータを作る
            using FileStream kozinStream = new(_kozinPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            using StreamReader kozinReader = new(kozinStream);
            ReaderToData(kozinReader, 3);

            //草枕のデータを作る
            using FileStream kusaStream = new(_kusaPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            using StreamReader kusaReader = new(kusaStream);
            ReaderToData(kusaReader, 4);

            //坊っちゃんのデータを作る
            using FileStream bochanStream = new(_bochanPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            using StreamReader bochanReader = new(bochanStream);
            ReaderToData(bochanReader, 5);

            //trainDatasを保存する。
            using FileStream trainStream = new(_trainDataPath, FileMode.Create, FileAccess.Write, FileShare.Read);
            using StreamWriter trainWriter = new(trainStream, System.Text.Encoding.GetEncoding("utf-8"));
            foreach (var item in trainDatas)
            {
                trainWriter.WriteLine(item.Item1 + "," + item.Item2.ToString());
            }

            //testDatasを保存する。
            using FileStream testStream = new(_testDataPath, FileMode.Create, FileAccess.Write, FileShare.Read);
            using StreamWriter testWriter = new(testStream, System.Text.Encoding.GetEncoding("utf-8"));
            foreach (var item in testDatas)
            {
                testWriter.WriteLine(item.Item1 + "," + item.Item2.ToString());
            }
        }

        static void ReaderToData(StreamReader streamReader, int sakuhinIndex)
        {
            while (!streamReader.EndOfStream)
            {
                string? str = streamReader.ReadLine();

                if (str == null)
                    break;

                if (str.Contains('。'))
                {
                    string[] vs = str.Split('。');
                    for (int i = 0; i < vs.Length; i++)
                    {
                        if (vs[i] != "")
                            RegisterData(vs[i] + "。", sakuhinIndex);
                    }
                }
                else
                    RegisterData(str, sakuhinIndex);
            }
        }

        static void RegisterData(string text, int sakuhinIndex)
        {
            if (text == "") return;

            Random random = new(seed);
            seed = random.Next();

            if (text[..1] == " ")
            {
                if (random.Next(10) == 0)
                    testDatas.Add((text[1..], sakuhinIndex));
                else
                    trainDatas.Add((text[1..], sakuhinIndex));
            }
            else
            {
                if (random.Next(10) == 0)
                    testDatas.Add((text, sakuhinIndex));
                else
                    trainDatas.Add((text, sakuhinIndex));
            }
        }
    }
}

コードはかなりごついですが、ほとんど繰り返しなのでやっていることは単純です。

Main()では最初の方で6つのテキストデータを読み込み、ReaderToData(StreamReader, int)でデータを作成しています。
その後、StreamWriterを用いて指定されたパスにデータを保存しています。

ReaderToData(StreamReader, int)では、テキストファイルの終わりまで文字列の読み込みを繰り返し、で文章を分割して一つの文にし、それに作品のインデックスを付けることによりラベリングしています。
実際の登録はRegisterData(string, int)で行っています。

RegisterData(string, int)は、

  • 10%の確率で、string + "," + intをテスト用データに追加し、
  • 90%の確率で、string + "," + intを訓練用データに追加しています。

ちなみに段落明けのため、一字下げが行われているところでは、一字下げを戻すようにしています。

さて、このまま実行するとFileNotFoundExceptionを吐き出して止まるので、ファイルをきちんと登録していきましょう。

ファイル登録の手順

上のように行います。

  1. まず、「MakeSosekiDatas」を右クリックし、「追加」→「新しいフォルダ」を行い、「Data」フォルダを追加します。
  2. その中に、「○○_utf.txt」をコピーします。
  3. その後、そのファイルを選択し、「プロパティ」から「出力ディレクトリにコピー」を、「新しい場合はコピーする」に変更します。

その後、各作品について、2. および 3. を繰り返します。
それを行うことにより、データの追加は完了です。

これで準備完了です。プログラムを実行してください。
こうすると、実行ファイルと同じフォルダ(最新版なら一般に MakeSosekiDatas/bin/Debug/net10.0 です)に「Data」フォルダが追加され、その中に「traindata.csv」と「testdata.csv」が作成されます。
これが今回使うデータです。

なお、データの選択はプログラムの乱数によって適当に行っているので、人によって実行結果は異なります。

機械学習を行う

いよいよ本題です。機械学習していきましょう。
とはいえ僕もきちんと分かっているわけではないので、予防線としてML.NETのチュートリアルのページを貼っておきます。
今回は「どの作品か」を当てるものなので、この中でも「多クラス分類」が一番望ましいと考えられます。
したがってここでは、「多クラス分類」のチュートリアルに則って、プログラムを作成していきましょう。
ほとんどチュートリアルのコードそのままなので、分かりづらいところはドキュメントページをご覧ください。

プロジェクトの準備を行う

まず、プロジェクトの準備を行います。
まず、NuGetパッケージから、「Microsoft.ML」をインストールしてください。
これがないと機械学習できません。

ここで一つ重要なことです。
2026年2月現在においては、.NET 8 と .NET 10 がありますが、.NET 8 を選択してください
なぜか .NET 10 だと同じコードベースでも性能が出ません。
原因が不明なので、もし分かる方いらっしゃったら GitHub の issue に投げていただけると助かります。

次に、データを登録します。
先ほどと同様に、以下のようにデータを登録します。

データ登録

上記ではコードをすでに入力していますが、無視してください。

  1. 「MLSoseki」を右クリックし、「追加」→「新しいフォルダ」で、「Data」フォルダを作成します。
  2. testdata.csvを追加します。
  3. testdata.csvのプロパティにおいて、「出力ディレクトリにコピー」を、「新しい場合はコピーする」に変更します。
  4. traindata.csvを追加します。
  5. traindata.csvのプロパティにおいて、「出力ディレクトリにコピー」を、「新しい場合はコピーする」に変更します。

なお、もちろん2~3と4~5は逆でも問題ありません。

必要なクラスを作成する

次にコードを入力します。
まず、必要なクラスを作成します。
先ほどと同様に「MLSoseki」を右クリックし、「追加」→「クラス」として、ファイル名を「SosekiSentence.cs」に変更してください。
コードは以下のようにしてください。

SosekiSentence.csusing Microsoft.ML.Data;

namespace MLSoseki
{
    public class SosekiSentence
    {
        [LoadColumn(0)]
        public string Sentence { get; set; }

        [LoadColumn(1)]
        public float StoryID { get; set; }
    }

    public class SosekiPrediction
    {
        [ColumnName("PredictedLabel")]
        public float StoryID;
    }
}

Microsoft.ML.Dataを先頭に追加するのを忘れないでください。
LoadColumn()属性を付与することにより、読み込んだデータをここに登録することが出来るようです。
また、ColumnName()属性を付与し、その引数に"PredictedLabel"を与えることにより、推測を行う対象を明示することが出来るそうです。
これらについては後で使います。

なお、StoryIDのところにおいて、作品を表すインデックスを登録し、それを推測するような機械学習を行うこととします。

必要なクラス変数を定義する

では次に、Program.cs に戻り、必要な変数を定義します。
クラスのトップレベルに以下の三つの変数を定義してください。

Program.csstatic readonly string _trainDataPath = Path.Combine(Environment.CurrentDirectory, "Data", "traindata.csv");
static readonly string _testDataPath = Path.Combine(Environment.CurrentDirectory, "Data", "testdata.csv");
static readonly string _modelPath = Path.Combine(Environment.CurrentDirectory, "Data", "model.zip");

これで、訓練データ、テストデータとモデルのパスを定義できました。

ProcessData(MLContext) を作成する

次に、ProcessData(MLContext) という静的関数を作成します。
Program.csに戻り、以下のような関数を追加します。

Program.csusing Microsoft.ML;

namespace MLSoseki
{
    internal class Program
    {
        static IEstimator<ITransformer> ProcessData(MLContext mlContext)
        {
            var pipeline = mlContext.Transforms.Conversion.MapValueToKey(inputColumnName: "StoryID", outputColumnName: "Label")
                .Append(mlContext.Transforms.Text.FeaturizeText(inputColumnName: "Sentence", outputColumnName: "SentenceFeaturized"))
                .Append(mlContext.Transforms.Concatenate("Features", "SentenceFeaturized"));
            return pipeline;
        }
    }
}

一行目では、inputColumnNameのところに、予測したい値の変数名を指定し、outputColumnNameのところには"Label"を指定します。

二行目Append...のところについては、様々なオプションを付与しています。
例えばML.NETにおける機械学習においては、文字列型のデータを読み込むことは不可能とのことなので、文字列型のデータであるSentenceを数値データに変換するため、mlContext.Transforms.Text.FeaturizeText()を用いて変換しています。
これにより、Sentenceが、数値データであるSentenceFeaturizedに変換されます。

三行目Append...のところでは、
どの要素を機械学習の要素として指定するかといったところを指定します。
今回の場合、例えば読み込んだデータの中に「筆者」データがあった場合、今回は全て筆者は夏目漱石なので、機械学習の結果には影響はありませんね。
筆者だけならまだよいですが、それ以外に推測するもの(今回はどの作品であるか)と相関関係のないもの(例えばこの文の文字数など)を指定してしまった場合、モデルが関係のない値ももとに学習・推論してしまうことになる可能性があり、性能を残念なものにしてしまう可能性も考えられ面倒です。
そのため、ここでは機械学習に用いるデータ系列を指定しているわけです。
なお、第一引数には"Features"を指定することがほぼ暗黙的に決定しているようです。
第二引数以降に、必要なデータ系列を必要なだけ指定します。
ここでは数値データ"SentenceFeaturized"のみを指定します。
必要に応じて任意個指定することが出来ます。

これにより、IEstimator<ITransformer>型の変数pipelineが代入されます。

BuildAndTrainModel(MLContext, IDataView, IEstimator<ITransformer>)を作成する

次は、いよいよ与えられたデータを用いて学習する部分です。

Program.csstatic ITransformer BuildAndTrainModel(MLContext mlContext, IDataView trainDataView, IEstimator<ITransformer> pipeline)
{
    var trainingPipeline = pipeline.Append(mlContext.MulticlassClassification.Trainers.SdcaMaximumEntropy("Label", "Features"))
        .Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel"));
    ITransformer trainedModel = trainingPipeline.Fit(trainDataView);
    return trainedModel;
}

基本的に先ほどのチュートリアル記事の内容に則っています。

Evaluate(MLContext, DataViewSchema, ITransformer) を作成する

ここではモデルの評価を行います。
また最後の行でモデルを保存しています。

Program.csstatic void Evaluate(MLContext mlContext, DataViewSchema trainingDataViewSchema, ITransformer trainedModel)
{
    IDataView testDataView = mlContext.Data.LoadFromTextFile<SosekiSentence>(_testDataPath, hasHeader: false, separatorChar: ',');
    var testMetrics = mlContext.MulticlassClassification.Evaluate(trainedModel.Transform(testDataView));
    Console.WriteLine($"*************************************************************************************************************");
    Console.WriteLine($"*       Metrics for Multi-class Classification model - Test Data     ");
    Console.WriteLine($"*------------------------------------------------------------------------------------------------------------");
    Console.WriteLine($"*       MicroAccuracy:    {testMetrics.MicroAccuracy:0.###}");
    Console.WriteLine($"*       MacroAccuracy:    {testMetrics.MacroAccuracy:0.###}");
    Console.WriteLine($"*       LogLoss:          {testMetrics.LogLoss:#.###}");
    Console.WriteLine($"*       LogLossReduction: {testMetrics.LogLossReduction:#.###}");
    Console.WriteLine($"*************************************************************************************************************");

    mlContext.Model.Save(trainedModel, trainingDataViewSchema, _modelPath);
}

Main() を作成する

いよいよ最後に Main() 関数を作成します。

Program.csstatic void Main(string[] args)
{
    MLContext mlContext = new(seed: 0);
    IDataView dataView = mlContext.Data.LoadFromTextFile<SosekiSentence>(_trainDataPath, hasHeader: false, separatorChar: ',');
    var pipeline = ProcessData(mlContext);
    ITransformer trainedModel = BuildAndTrainModel(mlContext, dataView, pipeline);
    Evaluate(mlContext, dataView.Schema, trainedModel);
}

とはいってもこれまで書いてきた処理を最初から呼び出すだけです。
ちなみに

Program.csIDataView dataView = mlContext.Data.LoadFromTextFile<SosekiSentence>(_trainDataPath, hasHeader: false, separatorChar: ',');

はデータを読み込んでいるだけです。
Microsoft.MLに存在するIDataViewというインターフェースが、ML.NETにおけるデータの取り扱いで非常に便利なようです。
工夫すれば、ここに全てのデータを読み込ませて、訓練用とテスト用に分けることもできるようなのですが、僕はちょっと分かっていないのでドキュメントに任せることにします。
引数もそこまで面倒ではないですよね。
しかし引数の設定忘れは面倒なのでお気を付けください。
(特にseparatorCharを指定するのを忘れないでください)

実行する

この状態で、プログラムを実行してみてください。

実行結果

これが結果ですが、実は精度はあまり悪くありません。

チュートリアルのソリューションの実行結果

こちらがチュートリアルをそのまま実行した結果なのですが、

項目 今回 チュートリアル
MicroAccuracy 0.666 0.738
MacroAccuracy 0.547 0.67
LogLoss 0.918 0.908
LogLossReduction 0.417 0.648

こうやって比較してみると、割と悪くない結果を出してくれているように思うわけです。

実際に予測してもらう

ではここで、試しにいくつかの例について、実際にどの作品なのかを予測してもらいましょう。

僕が実際に生成したデータに基づくことにしますが、僕が実際に生成したtestdata.csvの中から、いくつかピックアップして実際にやってみます。

各々の作品から4つずつ文をピックアップしています。

こころ

  • 先生はそれでなくても、冷たい眼で研究されるのを絶えず恐れていたのである。
  • その感じが私をKの墓へ毎月行かせます。
  • 私は彼の生前に雑司ヶ谷近辺をよくいっしょに散歩した事があります。
  • 彼の血潮の大部分は、幸い彼の蒲団に吸収されてしまったので、畳はそれほど汚れないで済みましたから、後始末はまだ楽でした。

吾輩は猫である

  • 妻君が袋戸の奥からタカジヤスターゼを出して卓の上に置くと、主人は「それは利かないから飲まん」という。
  • A君は是非固形体を食うなという。
  • 下女は国事の秘密でも語る時のように大得意である。
  • 神楽坂の方から汽車がヒューと鳴って土手下を通り過ぎる。

三四郎

  • 三四郎は富士山の事をまるで忘れていた。
  • ところが広田さんはそれでやめてしまった。
  • 麹町からあれを千駄木まで引いてくるのに、手間が五円ほどかかったなどと言う。
  • 図書館へもはいったがやっぱり見当らなかった。

私の個人主義

  • 私は今日初めてこの学習院というものの中に這入りました。
  • 私は高等学校へ周旋してくれた先輩に半分承諾を与えながら、高等師範の方へも好い加減な挨拶をしてしまったので、事が変な具合にもつれてしまいました。
  • その苦痛は無論鈍痛ではありましたが、年々歳々感ずる痛には相違なかったのであります。
  • 世界の大勢に幾分か関係していないとも限らない。

草枕

  • 越す事のならぬ世が住みにくければ、住みにくい所をどれほどか、寛容て、束の間の命を、束の間でも住みよくせねばならぬ。
  • 雲雀の声を聞いたときに魂のありかが判然する。
  • はっと思う間に、小女郎が、またはたと襖を立て切った。
  • 「和尚さん、あなたには、御目に懸けた事があったかな」

坊っちゃん

  • すると清は澄したものでお兄様はお父様が買ってお上げなさるから構いませんと云う。
  • 卒業してから八日目に校長が呼びに来たから、何か用だろうと思って、出掛けて行ったら、四国辺のある中学校で数学の教師が入る。
  • これも親譲りの無鉄砲が祟ったのである。
  • おれは江戸っ子で華奢に小作りに出来ているから、どうも高い所へ上がっても押しが利かない。

さて、適当に選んだと見せかけて、かなりいろいろ考えました。
いくつかの基準を設けています。

  • 登場人物の名称を入れてみる
  • 地名を入れてみる
  • あらすじによって推測できそうなものを入れる
  • 逆に全く関係のなさそうなものも入れる

まず最初の「登場人物」は、かなり重要な要素ですよね。
ある登場人物がほかの作品に出てくるといえば、ダイパに出ていたポケモンがベストウィッシュの後半でも出てくるとかない限りはほぼないはずです。

次に地名ですが、夏目漱石は物語の舞台を、詳細な地名も入れながら書いている気がします。
例えば愛媛県では「坊っちゃん列車」といったものなどがあるくらいです。

次にあらすじに関係するもの、または全く関係しないものですが、僕はこの中でも「こころ」と「私の個人主義」は読んだことがあるような気がしており、「こころ」はそういう意味ではかなりきちんと選んでいます。
「私の個人主義」を選んだ理由ですが、これは他の作品と違い、どちらかと言えば評論文に近いものがあります。
そういった部分も推測できるかが見ものです。

さて、お待たせしました。コードを追加しましょう。

Program.csstatic void PredictWork(MLContext mlContext, ITransformer trainedModel)
{
    SosekiSentence ss00 = new() { Sentence = "先生はそれでなくても、冷たい眼で研究されるのを絶えず恐れていたのである。" };
    SosekiSentence ss01 = new() { Sentence = "その感じが私をKの墓へ毎月行かせます。" };
    SosekiSentence ss02 = new() { Sentence = "私は彼の生前に雑司ヶ谷近辺をよくいっしょに散歩した事があります。" };
    SosekiSentence ss03 = new() { Sentence = "彼の血潮の大部分は、幸い彼の蒲団に吸収されてしまったので、畳はそれほど汚れないで済みましたから、後始末はまだ楽でした。" };

    SosekiSentence ss10 = new() { Sentence = "妻君が袋戸の奥からタカジヤスターゼを出して卓の上に置くと、主人は「それは利かないから飲まん」という。" };
    SosekiSentence ss11 = new() { Sentence = "A君は是非固形体を食うなという。" };
    SosekiSentence ss12 = new() { Sentence = "下女は国事の秘密でも語る時のように大得意である。" };
    SosekiSentence ss13 = new() { Sentence = "神楽坂の方から汽車がヒューと鳴って土手下を通り過ぎる。" };

    SosekiSentence ss20 = new() { Sentence = "三四郎は富士山の事をまるで忘れていた。" };
    SosekiSentence ss21 = new() { Sentence = "ところが広田さんはそれでやめてしまった。" };
    SosekiSentence ss22 = new() { Sentence = "麹町からあれを千駄木まで引いてくるのに、手間が五円ほどかかったなどと言う。" };
    SosekiSentence ss23 = new() { Sentence = "図書館へもはいったがやっぱり見当らなかった。" };

    SosekiSentence ss30 = new() { Sentence = "私は今日初めてこの学習院というものの中に這入りました。" };
    SosekiSentence ss31 = new() { Sentence = "私は高等学校へ周旋してくれた先輩に半分承諾を与えながら、高等師範の方へも好い加減な挨拶をしてしまったので、事が変な具合にもつれてしまいました。" };
    SosekiSentence ss32 = new() { Sentence = "その苦痛は無論鈍痛ではありましたが、年々歳々感ずる痛には相違なかったのであります。" };
    SosekiSentence ss33 = new() { Sentence = "世界の大勢に幾分か関係していないとも限らない。" };

    SosekiSentence ss40 = new() { Sentence = "越す事のならぬ世が住みにくければ、住みにくい所をどれほどか、寛容て、束の間の命を、束の間でも住みよくせねばならぬ。" };
    SosekiSentence ss41 = new() { Sentence = "雲雀の声を聞いたときに魂のありかが判然する。" };
    SosekiSentence ss42 = new() { Sentence = "はっと思う間に、小女郎が、またはたと襖を立て切った。" };
    SosekiSentence ss43 = new() { Sentence = "「和尚さん、あなたには、御目に懸けた事があったかな」" };

    SosekiSentence ss50 = new() { Sentence = "すると清は澄したものでお兄様はお父様が買ってお上げなさるから構いませんと云う。" };
    SosekiSentence ss51 = new() { Sentence = "卒業してから八日目に校長が呼びに来たから、何か用だろうと思って、出掛けて行ったら、四国辺のある中学校で数学の教師が入る。" };
    SosekiSentence ss52 = new() { Sentence = "これも親譲りの無鉄砲が祟ったのである。" };
    SosekiSentence ss53 = new() { Sentence = "おれは江戸っ子で華奢に小作りに出来ているから、どうも高い所へ上がっても押しが利かない。" };

    PredictionEngine<SosekiSentence, SosekiPrediction> predictionEngine = mlContext.Model.CreatePredictionEngine<SosekiSentence, SosekiPrediction>(trainedModel);
    SosekiPrediction sp00 = predictionEngine.Predict(ss00);
    SosekiPrediction sp01 = predictionEngine.Predict(ss01);
    SosekiPrediction sp02 = predictionEngine.Predict(ss02);
    SosekiPrediction sp03 = predictionEngine.Predict(ss03);

    SosekiPrediction sp10 = predictionEngine.Predict(ss10);
    SosekiPrediction sp11 = predictionEngine.Predict(ss11);
    SosekiPrediction sp12 = predictionEngine.Predict(ss12);
    SosekiPrediction sp13 = predictionEngine.Predict(ss13);
    
    SosekiPrediction sp20 = predictionEngine.Predict(ss20);
    SosekiPrediction sp21 = predictionEngine.Predict(ss21);
    SosekiPrediction sp22 = predictionEngine.Predict(ss22);
    SosekiPrediction sp23 = predictionEngine.Predict(ss23);
    
    SosekiPrediction sp30 = predictionEngine.Predict(ss30);
    SosekiPrediction sp31 = predictionEngine.Predict(ss31);
    SosekiPrediction sp32 = predictionEngine.Predict(ss32);
    SosekiPrediction sp33 = predictionEngine.Predict(ss33);
    
    SosekiPrediction sp40 = predictionEngine.Predict(ss40);
    SosekiPrediction sp41 = predictionEngine.Predict(ss41);
    SosekiPrediction sp42 = predictionEngine.Predict(ss42);
    SosekiPrediction sp43 = predictionEngine.Predict(ss43);
    
    SosekiPrediction sp50 = predictionEngine.Predict(ss50);
    SosekiPrediction sp51 = predictionEngine.Predict(ss51);
    SosekiPrediction sp52 = predictionEngine.Predict(ss52);
    SosekiPrediction sp53 = predictionEngine.Predict(ss53);

    Console.WriteLine($"=============== Single Prediction - Sentence: {ss00.Sentence},\n Answer: {GetSentenceName(0)}, Result: {GetSentenceName(sp00.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss01.Sentence},\n Answer: {GetSentenceName(0)}, Result: {GetSentenceName(sp01.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss02.Sentence},\n Answer: {GetSentenceName(0)}, Result: {GetSentenceName(sp02.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss03.Sentence},\n Answer: {GetSentenceName(0)}, Result: {GetSentenceName(sp03.StoryID)} ===============");
    Console.WriteLine();
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss10.Sentence},\n Answer: {GetSentenceName(1)}, Result: {GetSentenceName(sp10.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss11.Sentence},\n Answer: {GetSentenceName(1)}, Result: {GetSentenceName(sp11.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss12.Sentence},\n Answer: {GetSentenceName(1)}, Result: {GetSentenceName(sp12.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss13.Sentence},\n Answer: {GetSentenceName(1)}, Result: {GetSentenceName(sp13.StoryID)} ===============");
    Console.WriteLine();
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss20.Sentence},\n Answer: {GetSentenceName(2)}, Result: {GetSentenceName(sp20.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss21.Sentence},\n Answer: {GetSentenceName(2)}, Result: {GetSentenceName(sp21.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss22.Sentence},\n Answer: {GetSentenceName(2)}, Result: {GetSentenceName(sp22.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss23.Sentence},\n Answer: {GetSentenceName(2)}, Result: {GetSentenceName(sp23.StoryID)} ===============");
    Console.WriteLine();
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss30.Sentence},\n Answer: {GetSentenceName(3)}, Result: {GetSentenceName(sp30.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss31.Sentence},\n Answer: {GetSentenceName(3)}, Result: {GetSentenceName(sp31.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss32.Sentence},\n Answer: {GetSentenceName(3)}, Result: {GetSentenceName(sp32.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss33.Sentence},\n Answer: {GetSentenceName(3)}, Result: {GetSentenceName(sp33.StoryID)} ===============");
    Console.WriteLine();
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss40.Sentence},\n Answer: {GetSentenceName(4)}, Result: {GetSentenceName(sp40.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss41.Sentence},\n Answer: {GetSentenceName(4)}, Result: {GetSentenceName(sp41.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss42.Sentence},\n Answer: {GetSentenceName(4)}, Result: {GetSentenceName(sp42.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss43.Sentence},\n Answer: {GetSentenceName(4)}, Result: {GetSentenceName(sp43.StoryID)} ===============");
    Console.WriteLine();
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss50.Sentence},\n Answer: {GetSentenceName(5)}, Result: {GetSentenceName(sp50.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss51.Sentence},\n Answer: {GetSentenceName(5)}, Result: {GetSentenceName(sp51.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss52.Sentence},\n Answer: {GetSentenceName(5)}, Result: {GetSentenceName(sp52.StoryID)} ===============");
    Console.WriteLine($"=============== Single Prediction - Sentence: {ss53.Sentence},\n Answer: {GetSentenceName(5)}, Result: {GetSentenceName(sp53.StoryID)} ===============");
}

static string GetSentenceName(float index)
{
    return index switch
    {
        0 => "こころ",
        1 => "吾輩は猫である",
        2 => "三四郎",
        3 => "私の個人主義",
        4 => "草枕",
        5 => "坊っちゃん",
        _ => "",
    };
}

リストにしておけばよかったと後悔したのは後の祭りです。
あと皆さまにつきましても、データを自分でコードを実行して作成した場合は、訓練用データにこれらの文章が含まれている可能性があるので、あまり当てにはならないかもしれません。
必要に応じて、GitHubから僕が作成したデータをご利用ください。

そしてこう書いたら、

Program.csstatic void Main(string[] args)
{
    MLContext mlContext = new(seed: 0);
    IDataView dataView = mlContext.Data.LoadFromTextFile<SosekiSentence>(_trainDataPath, hasHeader: false, separatorChar: ',');
    var pipeline = ProcessData(mlContext);
    ITransformer trainedModel = BuildAndTrainModel(mlContext, dataView, pipeline);
    Evaluate(mlContext, dataView.Schema, trainedModel);
    PredictWork(mlContext, trainedModel);
}

のように、最後にコードを追加してください。
これで実行してみましょう。

予測結果

これが結果です。
三四郎については全問正解ですね。
ただ余計に三四郎と答えている部分はあるようですが……。
また、「図書館へも……」を当てるとは思いませんでした……。

「これも親譲りの……」に関しては、一行目「親譲りの無鉄砲で……」を
学習させていたので行けるかな?と思いましたが無理でした。

「その感じが私をKの墓へ……」については、「A君」に引っ張られたのでしょうか。

意外だったのは「彼の血潮の大部分は……」を当てたところですね。
うまく推測できたのでしょうか。

おわりに

これは他の作家ならどうなんだろう?とか思いました。
実際に時間があればそちらでもやってみようと思います。

また、今度は複数の作家についてこのように学習させ、
「誰が書いた一文か」というのもしてみたいとも思っています。

最後までお読みいただき、ありがとうございました。

]]>
<![CDATA[2048でショートコーディング]]>

この記事は新歓ブログリレー2026 15日目の記事です

こんにちは、25Bのくあらんてぃんです。traPショートコーディン

]]>
https://trap.jp/post/2844/69bcccd0e2394c00016e2b14Fri, 20 Mar 2026 14:00:00 GMT
2048でショートコーディング

この記事は新歓ブログリレー2026 15日目の記事です

こんにちは、25Bのくあらんてぃんです。traPショートコーディング部の部長をしています(空虚な真)。

皆さんはプログラミングをしたことはありますか?プログラムコードの量というものは、基本的に時間をかけるにつれて長くなっていくものです。それが良いことだとしても悪いことだとしても。しかし、一部の人々は時間をかけてコードの長さを削ろうとします。この営みはショートコーディング、もしくはコードゴルフと呼ばれ、traP内でも細々と楽しまれています。

ゲーム班の活動を一切していないことに気付いたので、今回はゲーム界のFizzBuzzこと2048を実装してみたいと思います。

JavaScriptを用いてWebで実装してもよかったのですが、僕はPythonのほうが得意なので、Pythonでのコードゴルフがしやすそうなターミナル上のゲームとして実装してみます。

__init__

2048でショートコーディング
コードを表示

実装してみました。遊んでみるとこんな感じです。

2048でショートコーディング

いくつか技術的な部分を説明してみます。

27行目で stty raw -echo というコマンドを使用して、ターミナル上で矢印などの入力をリアルタイムに受け取れるモードに切り替えています。このモードでは \n の改行はカーソルを1つ下に下げることしかしてくれないらしいため、\r と組み合わせて使用しています。
whileループ内に散見される \x1b というのはエスケープシーケンスの開始を意味する文字であり、\x1bc は画面とカーソル位置のリセットを、\x1b[A\x1b[D はそれぞれ矢印キーの上下右左を表しています。

2048の仕様は僕の経験則です。盤面とスコアがわかると鶴亀算的に4のタイルが生成される確率が10%と求められたりします。盤面の回転を実装することで上下左右の移動を1つの処理にまとめたのが自慢ポイントです。

それではこれを短くしていきます。

盤面を一次元配列に

まずは盤面 b を一次元配列にしてみます。

2048でショートコーディング
コードを表示

こうなりました。byte数変化はプラマイゼロです。二次元配列の平坦化は sum(2darr, []) を使うとできます。この平坦化やスライスの取り方などが微妙に長さを取るため、今のままだとあまりメリットが感じられないという結果になりました。

add()

現状は if empty: で空きセルが存在するかを確認していますが、よく考えたら空きセルが存在するときしか add() は呼ばれません。確認をなくしたらワンライナーで書けそうです。さらに三項演算子の部分もより短い書き方がありそう……?

2048でショートコーディング

こうなりました。73bytes減です。三項演算子は if ... else が固定でかかるので意外と弱いです。2 + (random.random() < 0.1) * 2 などもありですが、2が9個、4が1個入っているリストから1つ選ぶ方が、random.choice の共通化もできるのでよさそうな気がします。あと、非負整数だとわかっているならば == 0< 1 とした方が短いです。

merge()

現状は、0を取り除く→合成→再度0を取り除く→足りない0を補う、というアルゴリズムです。
0を取り除くのを2回行っている時点で明らかに何か最適化できそうですね。

2048でショートコーディング

70bytes減です。最初に0を十分に加えて後で最初の4つを返すという戦法ですね。4つ加えれば足りるだろ~と思ったらエラー吐いたので1桁の最大数の分加えておきました。

print()

31-33行目の出力部分もいい感じにまとめたいですね。

2048でショートコーディング

フォーマッターをかけると長すぎて改行されてしまい、文字数自体は増えてしまいましたが、改行をなくせば43bytes減です。2重 str.join() でも書けますが、条件付き改行みたいにしたほうが短そうでした。

残りの部分について

向きの取得と回転の部分ももう少しよさそうな方法がありそうです。

2048でショートコーディング

-87bytes。4回のループのうち、ちょうどいいタイミングで merge() を実行します。前半はもっと削れそうですが一旦これくらいで。

play()

普通に play() 要らないんで外に出しちゃいましょう。

2048でショートコーディング
コードを表示

print() が改行されてしまうのは甘んじて受け入れましょう。1058bytesになりました。ここからが本番です。フォーマッターを解除して、余分な改行や空白をすべて取り除いてみます。

code.strip()

2048でショートコーディング
コードを表示

727bytesになりました。331bytes減ですね。実は、空白の片方の隣が記号の場合、その空白はなくてもよいです。AIがやってくれなかったので温かみのある手作業で消していきます。……と思ったら消し忘れがありましたね。面倒なので次の作業のときに一緒に消しておきます。

code.replace()

変数名や繰り返し登場する関数名は1文字でいいです。置換していきます。

2048でショートコーディング
コードを表示

-86で641bytesです。やったのは置換だけですね。ついでに True1 で置換したりもしています。

:=

セイウチです。セイウチ演算子は代入を式として行える貴重な演算子です。今回使えそうなところは少ない気がしますが使っていきましょう。

2048でショートコーディング
コードを表示

626bytes。printNone を返すため、評価をつなげることで while の継続判定 r(1)!="\x03" を入れ込みます。また、同時にセイウチで k に代入します。僕はここ以外でセイウチを上手く使えなかったのですが、上手い人は他にも使いどころを見出せるのだと思います。

any%

ぶっちゃけ try ... finally 要らない気がしてきた。矢印キーと Ctrl + C 以外入力されないという前提でもよくね?ということで仕様を変えて短くしてみます。

2048でショートコーディング
コードを表示

507bytes。矢印キーの英大文字の部分だけに反応させるようにしました。A などを打っても反応するのはご愛嬌。止めた後の出力が壊れるのは reset を打ったりして対応してください。

諸々

諸々気になった部分を短縮します。

2048でショートコーディング
コードを表示

495bytesになりました。n(4) をさらに共通化させるのと、\x1b を1文字にまとめるのとで短縮しています。500を切ったのでひとまず満足ですね。ここからさらに短くするなら何かしらimportを一つ消したり等号をうまく言い換えたりする感じかと思います。もしくは Ctrl + C で止まらなくするような仕様を短縮しやすいものに変えるという方法もありそう。

base64.b85encode(zlib.compress(code, 9))

おまけです。上のコードを圧縮すると486bytesになりました。CTFではこのように圧縮して難読化されたファイルを解析する問題が出たりします。

2048でショートコーディング
コードを表示

コードゴルフは、最終的な1バイト単位の切り詰めに関しては言語仕様との闘いだったりしますが、序盤のアルゴリズム単位の改善についてはゲームやサービス製作でも活用できるようなアイデアやテクニックが含まれているんじゃないかなーと思っています。気になった人はCodinGameAtCoderの問題でコードゴルフに挑戦してみてください!

この2048をもっと短く書けた!という方も任意の手段で教えてください!お願いします。

明日の担当は @genMira です!お楽しみに!

]]>
<![CDATA[え!!PixelComposerでリッチなドット絵エフェクトを!?]]>

この記事は新歓ブログリレー2026 14日目の記事です。

はじめに

25BのMimiです。ゲーム班とグラフィック班ドット絵部

]]>
https://trap.jp/post/2834/69a6e226e2394c00016e0352Wed, 18 Mar 2026 15:01:21 GMTえ!!PixelComposerでリッチなドット絵エフェクトを!?

この記事は新歓ブログリレー2026 14日目の記事です。

はじめに

25BのMimiです。ゲーム班とグラフィック班ドット絵部をメインに活動しています。
このブログでは、PixelComposerというツールの紹介をしていきます。

Pixel Composer on Steam
A Node-based pixel art generator, editor, and VFX compositor. Create beautiful and complex effects in a non-destructive manner. Pixel Composer comes with a powerful graph system that supports multiple image manipulation, feedback effects, loop, physics, and fluid simulation.
え!!PixelComposerでリッチなドット絵エフェクトを!?

ドット絵のエフェクトって...

突然ですが、皆さんはゲームを作るにあたってドット絵のエフェクトを描いたことがありますか?
僕は描いたことがあるので、早速ですが載せます。

え!!PixelComposerでリッチなドット絵エフェクトを!?

これはゲーム内の攻撃のエフェクトのつもりで描きました。イメージとしてはソニックの空Nです。

ちょっと眺めてみて

いきなりこれがゲーム内でお出しされた時に、どう感じるでしょうか?
そもそもエフェクト注視しねえよというのは置いておいて
ぱっと見で「なんかカクカクしてるな...」「パーティクルみたいなのついてるけどあんま動いてないな....」「地味だな....」などいろいろ思い浮かぶと思います。単に僕の画力が足りていないのもありますし、もっと工数をかければこれより良いエフェクトはもちろん作れると思います。

ドット絵のゲームにおけるエフェクトの制約

しかし、ドット絵のエフェクトが満たさなければいけない制約は割と多いです。
僕がエフェクトを作るうえで遭遇した障壁は
他の絵と解像度を合わせなければいけない
フレームレートは高くしたい
細かいパーツ(パーティクルなど)を描くのがしんどい
など、たくさんありました。
特に爆発エフェクトとか粉が舞うエフェクトを描くのはしんどかったです。

そしてできれば、そんなにエフェクトに工数を掛けたくない です。個人制作だったり、少人数チームでの制作ならエフェクトに時間をかけている暇はあまり無いと思います。

では、我々はこの業を抱えながら生きなければいけないのでしょうか....?
簡単にエフェクトを作る事はできないんでしょうか....

出来らぁっ!

できます。PixelComposerならね。

Pixel Composer
え!!PixelComposerでリッチなドット絵エフェクトを!?

これなに?って感じだと思います。
PixelComposerは、ノードベースで扱えるピクセルアート向きのVFXツールです。UnityのShaderGraphみたいなもんです。

え!!PixelComposerでリッチなドット絵エフェクトを!?
え!!PixelComposerでリッチなドット絵エフェクトを!?

出来る事がとても多いです。
Asepriteファイルを読み込めるかつホットリロードにも対応していたり、Luaでスクリプトも書けたりするので色々カスタマイズもできます。
開発者のXに色々作品が載っているので見てみると良いことがあります(断言)。

実際に作ってみた

じゃあ実際どれぐらい楽に作れるの?って話なので、見せます。

え!!PixelComposerでリッチなドット絵エフェクトを!?

先ほどのものと比べて、
・ヌルヌル動いている
・なんかちょっと光ってる
・パーティクルも出てる
・解像度も高い
など問題がかなり改善されて †リッチ† になっていますね。うれしい!!

これを手書きで描くのは人間のやることとは思えません。僕だったら全然半日以上かかってます。
しかし、このエフェクトは20分ぐらいで作れました。PixelComposerすごい!

エフェクト以外にも....

PixelComposer、実はエフェクト以外にも全然使えます。静止画でもアニメーションでも応用の幅がとても広いです。
このブログのヘッダ画像もPixelComposerで作ったものです。Xのアイコンにしています。

え!!PixelComposerでリッチなドット絵エフェクトを!?

これ、実は3Dの球とカメラを良い感じに加工しています。
PixelComposerは3Dモデルをインポートして、ライティングなどをノードベースでいじることもできます。すごい!

終わりに

いかかでしたでしょうか?
この記事があなたのドット絵ゲームライフを豊かにする事を願っています。

明日の記事は @quarantineeeeeeeeee の記事です。お楽しみに!

]]>
<![CDATA[Grafana ObservabilityCON on the Road 参加記]]>

この記事は 新歓ブログリレー 2026 13日目のものです
他の記事を見たい方は こちら↑ のリンクをクリック!

こんにち

]]>
https://trap.jp/post/2727/69251903c49b960001b67c27Wed, 18 Mar 2026 13:30:07 GMT
Grafana ObservabilityCON on the Road 参加記

この記事は 新歓ブログリレー 2026 13日目のものです
他の記事を見たい方は こちら↑ のリンクをクリック!

こんにちは! 23B のぷぐまです
今回は昨日 3/17(火) に開催されました Grafana ObservabilityCON on the Road についてまとめます!

ObservabilityCON on the Road: Tokyo | Grafana Labs
Grafana is the open source analytics & monitoring solution for every database.
Grafana ObservabilityCON on the Road 参加記

新歓の内容かというとちょっと怪しいですが (ちょっと…???) 、せっかくですし新入生の方々も「世間にはこんなイベントがあるんだ〜」って思っていただければと思います (traP には技術イベントの運営に携わっている人もいますし!)

Grafana ObservabilityCON on the Road とは?

Grafana Labs. が開催する、オブザーバビリティの最新動向を学べるイベントです
参加費は通常で ¥6,500 でしたが、私は早めに申し込んだので早割がきいて ¥4,500 でした
開催場所は、東京・京橋の TODA HALL & CONFERENCE TOKYO でした

TODA HALL & CONFERENCE TOKYO
TODA HALL & CONFERENCE TOKYO 公式サイト
Grafana ObservabilityCON on the Road 参加記

少々長いので、以下では ObservabilityCON と記します

オブザーバビリティとは?

この単語に馴染みがない方もいらっしゃるかもしれないので、ここで軽くまとめておきます
調べたところ、 Grafana 公式ドキュメント に説明が書かれていました

Observability is the process of making a system’s internal state more transparent. Systems are made observable by the data they produce, which in turn helps you to determine if your infrastructure or application is healthy and functioning normally.

大雑把すぎることを許容してひとことでまとめると、
「アプリケーションやシステムが正常に動いているかどうかを分かりやすくすること」
と言い換えられるかと思います

大規模なシステムが運用される現代において、非常に重要なことだと言えるでしょう

イベントで印象に残ったところ

ここでは、イベントの中で自分の印象に強く残っている部分をいくつかピックアップします
イベント全体の流れが気になる方は、 イベント公式ページ #agenda をご覧いただけるとより詳しく見ることができます!

ObservabilityCON on the Road: Tokyo | Grafana Labs
Grafana is the open source analytics & monitoring solution for every database.
Grafana ObservabilityCON on the Road 参加記
↑ のリンクでタイムテーブル部分に飛べます

Grafana における OpenTelemetry 統合について

OpenTelemetry とは、オブザーバビリティを高めるツールやライブラリ、フレームワークのことです
これを Grafana Cloud に組み込んで活用する方法についてのセッションがありました

OpenTelemetry
The open standard for telemetry
Grafana ObservabilityCON on the Road 参加記

このツールを使って取り出したデータを Grafana で可視化・活用するためにどういうことをすればよいかという内容でした

ここでの内容は traP で運用している OSS 版の Grafana においても適用できそうな話で、ぜひともここでの学びを活用したいです

Grafana におけるユーザー体験の可視化について

こちらは別のセッションです
スマートフォンや PC の環境に依存するアプリケーションでの不具合の修正についてでした

個人的には、統計情報を取得・管理するのは主にサーバーサイドアプリケーションやインフラだと考えていました
ユーザー側の端末や、特定の地域だけで発生している不具合などを可視化する方法については今回のセッションで初めて聞きました

具体的には

  • Synthesic Monitoring
  • Frontend Observability
  • K6

を活用して実際に問題を解決し、それがユーザー側に改善として反映されていることを確認できるデモが行われていました

懇親会で Ted Young さんと直接話せた!

OpenTelemetry 共同創設者である Ted さんは、 OpenTelemetry を Grafana Cloud に組み込むことについてのセッションで登壇されていました
この方と、イベント終了後の懇親会で話すことができました!

他の何人かと一緒に話にいったんですが、私からは

  • OpenTelemetry をどういう場面で思いついたのか?
  • OpenTelemetry でプロトタイプなどを作るときに Java / Go / Python の 3 つの言語を選んでいるのはなぜか?

という質問をさせていただきました

最後に一緒にお写真も取らせていただきました
ありがとうございました!

どうだった?

正直なところ、イベント自体が企業向けでしたし学生の自分が楽しみきれるかな、、、という不安はありました

しかし、実際のところは

  • ここまでで挙げたセッションでの学び
  • 他に参加している方々が、企業でどのような利活用をしているか
  • イベントに登壇・参加していた著名な方との交流

を通して非常に有意義で楽しい一日を過ごすことができました!

予定が空いてるからと軽率に申し込んでよかった()

おわりに

今回は ObservabilityCON についてお伝えしました!
この記事を通して、オブザーバビリティや技術カンファレンス・イベントに少しでも興味をもっていただければ幸いです

最後に…

明日は @Mimi_year くんの記事が投稿されます!
お楽しみに!

]]>
<![CDATA[手帳を使おう]]>

この記事は 新歓ブログリレー2026 13日目の記事です。

はじめに

みなさん初めまして。23B の @sakura と申します。
Live2D を触っ

]]>
https://trap.jp/post/2860/69abe18ae2394c00016e0db4Wed, 18 Mar 2026 01:00:23 GMT手帳を使おう

この記事は 新歓ブログリレー2026 13日目の記事です。

はじめに

みなさん初めまして。23B の @sakura と申します。
Live2D を触ったり、外部イベントの運営をしたりしています。

さて、早速本題に入ろうと思いますが、みなさんは手帳を使っていますか ?

この間まで高校生、もしくは今高校生だよ! ってみなさんは使っている人も多いんじゃないかなと思います。とはいえ世は電子化。大学生になると使わない人も...

この記事では、大学生でも手帳を使うといいよ! と思う私が手帳の良さを紹介したいと思います。

手帳の何がいいの?

みなさんご存知の通り、手帳とは、1日の予定を管理したり、行動を記録したりするものです。メモにも便利。
大学生になると授業が変則的になったり、平日に他の予定が入ることも多々...予定管理は必須になっていきます。

「リマインダーアプリやカレンダーアプリがあるから手帳はいらない! 」って思われる方もいらっしゃるかもしれません。

そんな皆様に「紙の手帳だから」良いことを 3つ。

時と場合を選ばず使える

アルバイト、大学の先生、企業の方、etc...
大学生になるとさまざまな方と対面する機会があります。こんな時にスマホでぽちぽちするの、少し難しい場面もありますよね。紙の手帳は相手や場面に関係なく開くことができるので、特に真面目な場面で役立ちます。

本来の用途以外に時間を使わない

予定確認のためにスマホを開いたのに、気がついたらSNSを触っている、ゲームを開いている...
こんなこと、ありませんか? 紙の手帳は本来の目的を超えた使い方ができないので、時間を溶かさなくて済みます。

手書きの習慣・文具を楽しめる

授業資料がこれまで以上に電子化される大学生、紙とペンを使う機会が本当に減ります。意識して使う習慣がないと1ヶ月ペンを持たないことも...
手帳を使うと、毎日ペンを持つことになるので文具を使う習慣ができます。

毎日ペンを使うなら、好きな文具を揃えて楽しむこともできます。文具を選ぶ楽しみもできて一石二鳥ですね !

私の手帳の使い方

ここまで、紙の手帳だからこその良い点を話してきました。とはいえ、急な予定変更やタスクなど、スマホにある方が嬉しいこともあります。私もリマインダーやGoogleカレンダーを併用しています。(消しゴムを使うのは少々手間なため...)

では、どうやって紙の手帳を使っていくのか。今日はあくまで私の手帳の使い方を紹介します。

スマホでやること

スマホでタスク管理・細かいスケジュール管理をしています。

手帳に一個一個のタスクを書くと書ききれなくなってしまうので、リマインダーに入れています。

細かいスケジュールも、急な変更や数が多いと手帳で管理することが大変になってしまうので、私はGoogleカレンダーに入れています。

手帳に書くこと

手帳には、大まかなその日のイベント、行動記録、メモを書いています。

大まかなイベントを書いておくことで、その日に何がメインの予定としてあるのか一発でわかります。大まかな予定を一つ書いておくことで、その日のスケジュールをなんとなく思い出すこともできるので、スマホを使いづらい場面で予定を立てるときに便利です。

日ごとの欄には、その日1日の行動記録をつけます。考えたことや気づいたことも併せて書いておくことで、後々見返すときにその時のことを思い出しやすくなります。

アルバイトや大学の先生、企業の方と話すときのメモも手帳に残しておきます。開く場面を選ばないので、手帳に書くと見返しながら話すこともできます。

おすすめの手帳

では、そんな私のおすすめする、使いやすい手帳のポイントです。

  1. 1日あたり 2件 は書けるサイズのカレンダーがあること
    大まかな予定を書いておけるための枠がある、カレンダーがついていると良いと思います。絶対あかんダブルブッキングを回避することができます。

  2. 1日ごとの時間軸のメモがあること
    1日の行動記録を書くのに役立ちます。メモを書ける場所があるとその日の振り返りを書くこともできるのでおすすめです。

  3. メモだけのページがしっかりあること
    これまで書いた通り、真面目な場面でメモをするときに大事です。1年間あるので、しっかり書くスペースがあると良いと思います。

...ということで、具体的にいくつか紹介します。

※個人の趣味嗜好によるものです。案件ではありません、信じて

2000円くらい出せる人向け

少し高いですが、NOLTY の手帳は使いやすいものが多いです。いろんなタイプのものがあるので、使い方に合わせて選ぶことができます。
週間バーチカル(縦軸で時間管理ができる)
週間レフト(横軸で時間管理ができ、その日のメモも書ける)

1000円くらいまでなら出せる人向け

機能性も欲しいけどそんなにお金をかけたくない人におすすめです。十分必要な情報が書けます。
週間バーチカル
週間レフト

とりあえず LOFT か 本屋さん か 文具屋さん に行くといいです、行きましょう

おわりに

いかがでしたでしょうか。ここまで読んでくださる方はほぼ間違いなく手帳に興味がある方だと思うので、ぜひ大学生活でも手帳を使っていただけたらと思います。

明日の記事は @Mimi_year の記事です! お楽しみに!

]]>
<![CDATA[式変形で導出する Static Top Tree]]>

22B の noya2 です. ずいぶん前に抽象化された全方位木 DP のライブラリの記事を書いたのですが,今回は「木 DP の更新」

]]>
https://trap.jp/post/2861/69b90e35e2394c00016e2689Tue, 17 Mar 2026 09:01:58 GMT

22B の noya2 です. ずいぶん前に抽象化された全方位木 DP のライブラリの記事を書いたのですが,今回は「木 DP の更新」を扱います.

Static Top Tree と呼ばれるデータ構造があります. 以下,STT と省略します. STT は ABC 351-G Hash on Tree において ABC の想定解法として初めて明示的に扱われました. 公式解説が丁寧に書かれており,その後も STT に関する出題や上位勢による積極的な言及もあったことから,約 2 年の月日が経ったいま,STT はだんだんと典型になっているように感じます.

つい最近行なわれた The 2026 ICPC Asia Pacific Championship において,D 問題に STT を使うだけで解ける問題が出題されました. その少し前に,ICPC 用のお手軽実装がないか,より直接的に木 DP を扱う考え方がないかを検討し,自分なりに納得のいく結果が得られていたため,本番ではすんなり通すことができました. この記事では,木 DP を変形することによって,cluster の概念を経由せずに「 の STT」を理解することを目指します.

木の構成に自由度があり,その部分を工夫すると になりますが,工夫せずに segment tree に丸投げすることで実装がかなり楽になっていると主張しています.

木 DP の変形

乱暴な記法や大胆な省略を行なっていますが,察してください.

根付き木上の DP を次のように定式化します.

  • 根付き木を rooted_tree = pair<vertex, set<rooted_tree> > として構成しています.
  • は根付き木に辺をつけたようなものの集合( set<rooted_tree> )に根を付け加える関数です.
  • は根付き木の根に親に伸びる辺をつけたようなものの単元集合を生成する関数です.
  • は根付き木に辺をつけたようなものの集合どうしの非交和を取る関数で,集合を列だと思うことにすると可換モノイドです.

次に, が葉でないときに, のある子 を特別視します.

そして,関数 を次のように定義します.

このもとで

と書けます. が葉であるときは は定数で,

とします. 特別視した子を辿って から葉まで至る列を とすると,

となります. HLD は最も大きな部分木を持つ子を特別視していくつかの heavy path に分解する手法ですが,ここでの つの heavy path に乗っているということになります. つまり,heavy child に優先的に潜る dfs による dfs order で たちの関数合成を segment tree に乗せれば, は segment tree 上の区間取得で計算できます.

木 DP の更新とは, が更新されるということです. これらの更新に対して たちの更新を追従させることを目指します. たとえば,頂点 が与えられて, の値のみが更新されたとします. このとき,

  • の属する heavy path の最も根に近い頂点(top) の親
  • の属する heavy path の top の親

に対応する が更新されます. これらの更新点の個数は HLD の計算量解析と同じで根付き木の頂点数に対して対数です. は更新されませんが, の更新に伴って が更新されるため,これを の更新に反映する必要があります. この際, を再計算するわけにはいかないので,この差分更新は工夫する必要があります. に逆元が存在する場合は簡単ですが,一般には を乗せた segment tree の一点変更として処理することになります. この segment tree には,ある頂点の軽い子 が連続して並ぶような順序(たとえば bfs order)で を乗せておけばよく,このとき の計算は区間取得になります.

各更新では,次のものがその時点の計算結果として正確に保持されています.

  • すべての頂点 に対する
  • すべての頂点 に対する
  • すべての heavy path の top に対する

を関数合成を乗せた segment tree で, を乗せた segment tree でそれぞれ管理すれば, つくらいの時間計算量で更新に追従できます.

例:The 2026 ICPC Asia Pacific Championship - D. Christmas Tree Un-decoration

問題はこちら(Codeforces)

を根とする部分木で必要な回数 とすると,遷移は

となります. わざわざあてはめる方が分かりにくいと思いますが,上の定式化で対応するものを書いてみます.

のような感じになると思います. 記号は雰囲気で使っています. 今回は を使わずに をそのまま返すので,もっと簡単です.

が葉でないとき, の子として を特別視して

と書けます.

とすると, です.

が葉のときは とします. としても良いですが, を(平行移動つきの)clamp として統一的に扱うことにします.

clamp どうしの合成は clamp です. したがって,関数合成は簡単に計算・保持できます.

クエリでは が更新されますが,これに応じて を更新します.

  • まず, が更新されます.
  • 次に の属する heavy path の top について が更新されます.
  • それに伴って, の親 について が更新されます.
  • ...

が更新されたとき, を更新することは, について を保持しておくことで,差分更新できます. が逆元を持っているおかげで, を集約する segment tree は不要になっています.

これで, の変更にすべての変更が追従できました. 最初なので,実装を載せておきます.

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
bool chmin(auto &a, auto b){ return a > b ? a = b, 1 : 0; }
bool chmax(auto &a, auto b){ return a < b ? a = b, 1 : 0; }

template<class S, auto op, auto e>
struct segtree {
    int sz;
    vector<S> d;
    segtree(vector<S> a){
        int n = a.size();
        sz = bit_ceil<uint32_t>(n);
        d.assign(sz*2,e());
        for (int i = 0; i < n; i++){
            d[sz+i] = a[i];
        }
        for (int i = sz-1; i >= 1; i--){
            d[i] = op(d[i*2],d[i*2+1]);
        }
    }
    void set(int p, S x){
        p += sz;
        d[p] = x;
        while (p > 1){
            p >>= 1;
            d[p] = op(d[p*2],d[p*2+1]);
        }
    }
    S prod(int l, int r){
        l += sz, r += sz;
        S sml = e(), smr = e();
        while (l < r){
            if (l & 1) sml = op(sml,d[l++]);
            if (r & 1) smr = op(d[--r],smr);
            l >>= 1;
            r >>= 1;
        }
        return op(sml,smr);
    }
};

//(平行移動つきの)clamp
struct S {
    // max(a, x + b)
    ll a, b;
    // x = any
    ll eval(){
        return max(a,b);
    }
};

// max(f.a, max(g.a, x + g.b) + f.b)
S op(S f, S g){
    chmax(f.a,g.a+f.b);
    f.b += g.b;
    return f;
}
S e(){
    return S{-1LL<<60,0};
}

void solve(){
    int n, q; cin >> n >> q;
    vector<int> par(n);
    vector<vector<int>> g(n);
    par[0] = -1;
    for (int i = 1; i < n; i++){
        cin >> par[i]; par[i]--;
        g[par[i]].emplace_back(i);
    }
    vector<ll> a(n);
    for (int i = 0; i < n; i++){
        cin >> a[i];
    }
    // ord[v] : dfs で v に訪ずれた時刻
    // leaf[v] : v が属する heavy path の最も深い(根から遠い)頂点
    // top[v] : v が属する heavy path の最も浅い(根に近い)頂点
    vector<int> ord(n), leaf(n), top(n); // <- HLD
    // light[v] : \bigoplus_{vc\in\vec{E}_-} E(vc,dp[c])
    vector<ll> light(n), dp(n); // <- ad-hoc
    {
        auto dfs_sz = [&](auto sfs, int v) -> int {
            int sub = 1;
            int ma = -1, id = -1;
            for (int i = -1; auto u : g[v]){
                i++;
                int ch = sfs(sfs,u);
                if (chmax(ma,ch)){
                    id = i;
                }
                sub += ch;
            }
            if (id != -1){
                swap(g[v][0],g[v][id]); // heavy child を先頭に
            }
            return sub;
        };
        dfs_sz(dfs_sz,0);
        int t = 0;
        auto dfs_hld = [&](auto sfs, int v, int ctop) -> int {
            ord[v] = t++;
            top[v] = ctop;
            if (g[v].empty()){
                leaf[v] = v;
                dp[v] = a[v]; // ad-hoc
                return v;
            }
            leaf[v] = sfs(sfs,g[v][0],ctop);
            for (int u : g[v] | views::drop(1)){
                sfs(sfs,u,u);
                light[v] += dp[u]; // ad-hoc
            }
            dp[v] = max(a[v], dp[g[v][0]] + light[v]); // ad-hoc
            return leaf[v];
        };
        dfs_hld(dfs_hld,0,0);
    }
    segtree<S,op,e> seg([&]{
        vector<S> b(n);
        for (int v = 0; v < n; v++){
            b[ord[v]] = S{a[v],light[v]};
        }
        return b;
    }());
    cout << dp[0] << '\n';
    while (q--){
        int v; cin >> v; v--;
        ll x; cin >> x;
        a[v] = x;
        while (v != -1){
            // f_v update
            seg.set(ord[v], S{a[v], light[v]});
            // dp[t] update
            int t = top[v];
            ll dpt = seg.prod(ord[t],ord[leaf[t]]+1).eval();
            int p = par[t];
            if (p != -1){
                light[p] -= dp[t];
                light[p] += dpt;
            }
            dp[t] = dpt;
            v = p;
        }
        cout << dp[0] << '\n';
    }
}

int main(){
    cin.tie(0)->sync_with_stdio(0);
    int t; cin >> t;
    while (t--){
        solve();
    }
}

例:AtCoder Beginner Contest 351 - G. Hash on Tree

問題はこちら(AtCoder)

は問題文中の とします. 問題文中に遷移が書かれていますが,

です. ただし,葉だけ変であることに注意してください. を経由すれば整合性が取れます. すいません, の型が同じで,これも簡単ですね.

例によって, が葉でないとき, の子 を特別視して,

と書きます.

とすることで となります. は affine 変換です. 葉の場合は とすればよいです. そして,affine 変換どうしの合成は affine 変換です.

には逆元がないので,light edge をまとめる方の segment tree も使います.

実装は bfs order 順の segment tree も追加で使う以外はほとんど一緒です.

実装(AtCoderへの提出)

例:Library Checker Point Set Tree Path Composite Sum (Fixed Root)

問題はこちら(Library Checker)

を根とする部分木で を定義したときの の総和

を根とする部分木の頂点数

とします.

遷移は affine 変換が線形であることを用いていろいろ分解できて

と書けます.

先述の定式化においては

  • (ベクトルとしての加算)

です. ようやく辺重みが登場しました.

例によって, が葉でないとき, の子 を特別視して,

と書きます.

とすることで となります. は affine 変換です. 葉の場合は とすればよいです.

の更新があったとき,その辺が heavy edge であるか light edge であるかで,更新すべきものが変わります. heavy edge のときは が更新されます. light edge のときは が更新されます. いずれの場合も,その先の更新は頂点 に更新があったものと思って更新していけばよいです.

affine 変換と書きましたが,これは

として 次元ベクトルとして見ています. ここで,これを愚直に表現すると として 次元ベクトル 行列 を持つことになりますが, 行目は明らかに部分木サイズの情報だけで事足りる上にこれは static なので,削減することで定数倍ではありますが大きく改善するでしょう. そもそも DP をもう少し違う形で書いた方が良いです. ここでは DP で計算するべき値がスカラーではない場合でも扱えることを確認するために,このままにしています.

実装(Library Checker への提出)

部分木サイズが static であることを用いた高速化(Library Checker への提出)

への道

heavy path 上の頂点に対する を乗せた segment tree には,異なる heavy path に跨がる区間の区間取得が来ることはありません. を集約する segment tree についても同様のことが言えます. このことから,そもそも集約に通常の segment tree を用いるのは無駄があると言えるでしょう. もう少し無駄を観察してみます.

  • を集約する segment tree には,heavy path ごとに分割すれば,suffix にしか取得クエリが来ない
  • を集約する segment tree には,特定の頂点の light child ごとに分割すれば,全体にしか取得クエリが来ない

これの改善を突き詰めていくと,完全ではない二分木を segment tree と見なすことで,うまく二分木を構築すると になります. そして,そのことは,HLD が木を heavy path からなる高さが頂点数の対数になる木に分解する手法であったのと同様に,STT が木を cluster からなる高さが頂点数の対数になる二分木に分解する手法であるものとして解釈できるはずです. 筆者は cluster の扱いについて詳しくないため,これ以上のことは書けません.

根付き木の構成として,ここでは,最も一般的だと思われる rooted_tree = pair<vertex, set<rooted_tree> > を採用し,そのまま木 DP の更新を導出しました. と言うのは誤解があり,HLD は rooted_tree = vector<pair<vertex, set<rooted_tree>>> として構成していて,実際のところはこの構成に計算を乗せていると思うことができます. heavy path というのは pair::first だけ取り出した vector<vertex> にあたるものです. 難しいのは辺の情報をどこに乗せるかだと筆者は思っていて,cluster などの概念を学ぶと辺の扱いの見通しが良くなるかもしれません. ここで強調したいのは,「辺の情報は子の方が持つ」のような抽象的(標語的)な理解をする必要はなくて,式変形から直接実装が導出できるということです.

全方位化

少し難しいですが,できます. 気持ちは同じで,「子のうちひとつを特別視する」です.

頂点の木を取ります. 全方位木 DP で部分木として扱うものは 種類です. 個は各頂点を根にしたときの全体の木に対する DP で,残りの 個は各辺の各向きについてそれが指す先の頂点を根とするその向きの根付き木です(伝われ〜).

根をひとつ固定して,辺の親側・子側を定めます. 親から子へ向かう辺 について, に対応する DP はこれまでの方法ですでに計算できています. 一方 に対応する DP はどうでしょうか. この場合は の親 を特別視します.

が heavy edge であるときは と同値で, は更新できるのでした.

とすると です. heavy path に逆向きに の関数合成を乗せれば良さそうなことが分かりました. 一方, が light edge であるときは, を同様に定義しても の更新はできません. の次数が大きな場合を考えると分かると思います. このときは諦めて定義通り計算することにします. はすべて を集計する segment tree に乗っているので,そこから の部分だけ取り除いたものも計算できます. 問題は を求める部分ですが,この部分は再帰的に計算します. この再帰は が根になるまで続きますが,light edge はそれまでに対数回しか現れないので大丈夫です.

各頂点を根にしたときの DP の値は,その頂点から親に向かう DP が計算できるのなら,当然できます.

全方位にすることで新たに保持して更新に追従するべきものは,heavy edge に対する逆向きの関数 だけです.

実装例(Point Set Tree Path Composite Sum)(Library Checker への提出)

他の問題例

UCup の問題

JOI の問題

おわりに

特に数式の扱いが雑になってしまいました. ちゃんと書くと雰囲気を掴むことの邪魔になってしまいそうだった,と言い訳させていただきます. なにか分からないことがあれば X(twitter) で聞いてください. なんでも答えます.

]]>
<![CDATA[【代表が語る】traPのあそびかた]]>この記事は新歓ブログリレー2026 12日目の記事です

こんにちは

こんにちは。新入生の皆さん始めまして。現在traPの

]]>
https://trap.jp/post/2858/69b8f424e2394c00016e25f6Tue, 17 Mar 2026 07:15:54 GMT

この記事は新歓ブログリレー2026 12日目の記事です

こんにちは

こんにちは。新入生の皆さん始めまして。現在traPの代表をしている、24Bの @zoi_dayo です。

この記事を開いてくれたということで、traPに1mm以上興味がある、でもよくわかっていない、という人が多いと思います。
traPは現在およそ750人程度が所属している巨大なサークルです。僕はこのサークルの代表で、入部してから2年弱が経過しているのですが、それでも「traPの活動をすべて把握している!!」とまでは言えないです。デカサークルすぎる!

ただ、これまでかなりtraPで遊んできたという自覚はあるので、「traPをどう楽しめばいいか」についてはいろいろ喋れる気がします。ぜひぜひ楽しんでいってください!

2026年度 新入生歓迎 特設ページ
新入生の皆さんへ! 新入生の皆さん、合格おめでとうございます! 私たちtraPの活動について知ってもらうためのページを用意しました。 新入生の皆さんと一緒に活動できる日を楽しみにしております! 新歓イベントもたくさん用意しています。ぜひ参加してみてください! 新歓イベントの詳細はこちらへどうぞ! 新歓Discordサーバーも用意してます。ぜひ遊びに来てください! Discordサーバー「traP 2026 Welcome」に参加しよう!traPの2026年度新歓Discordサーバーです! | 120人のメンバーDiscord 目次 * traPってどんなサークル? * 新歓イベント紹介 * 活動紹介 * プロジェクト活動 * ハッカソン * ゲーム班 * グラフィック班 * サウンド班 * アルゴリズム班 * CTF班 * SysAd班 * Kaggle班 * らん☆ぷろ * 入部方法 traPってどんなサークル? traPとは? 『デジタル創作
【代表が語る】traPのあそびかた

traPとは?

まずこのサークルはなんなんだ、ということです。

traPははじめゲーム制作サークルとしてスタートしたサークルですが、現在はめちゃくちゃ活動範囲が広がっています。「デジタル創作同好会」という名前ですが、つまりは「パソコンを使ってなにかする」ことならだいたい誰かがやっているんじゃないでしょうか。プログラミングはもちろん、絵を描いたり、曲を作ったりも。

このままではまとまりがなさすぎるため、メンバーは主に「班」というグループに所属して活動することになります。班は現在「アルゴリズム班」「CTF班」「ゲーム班」「グラフィック班」「Kaggle班」「サウンド班」「SysAd班」の7個あり、複数に所属することもできます。(僕は7個全部に入っています。)
班に入ったからといってなにか制約が増えるみたいなことは全くなく、単に「〇〇班所属」の肩書がもらえる程度です。「私はこの分野に興味があります」という意思表示ですね。

「何も経験ないんですけど...」

さて、traP紹介を読んで「でもプログラミングも作曲もやったことないし...タイピング苦手だし...」と思った人も多いと思います。というか、毎年入部相談回みたいなのをやってるんですが、いつもこれを聞かれます。

おそらく、「traPは経験者が無双している」みたいなイメージが持たれがちなのかなと思います。確かに、科学大には「中高からパソコン一筋」みたいな強者が一定数入学しがちですし、そういう人はtraPに入って活動しがちです。

ただ実際のところ、traPに入る人はほぼほぼ未経験者です。というより、いくら科学大とはいえ今年の入学者1000人にガチ経験者が100人も居るわけがないです。

さて、そんな未経験者ばかり入部しているはずのtraPですが、1つ上の学年はプロだらけのように見えるんじゃないかなと思います。理由は簡単で、未経験者でも1年もあれば十分チーム開発に参加できるレベルの実力になれている、ということです。本当です。というわけで、その成長を支える講習会をご紹介。

講習会

traPが誇る講習会たちの紹介です。「未経験者でも1ヶ月半でそれなりの実力者になれる」...というと胡散臭いですが、本当にそうなんです。信じてください。
もちろん、新入生じゃなく2年・3年などの上級生もぜひ聞きに来てください!!

種類が多すぎて全部は書ききれないのですが、いくつかピックアップしてみました。長いので自分の興味ある分野だけ読んでみるとよいでしょう。日程上おそらくほぼ全部に参加することはできるはず...? なのですが、普通に体力がもたないのであまりおすすめはしません。

プログラミング基礎講習会

traPの班7つのうち、「サウンド班」「グラフィック班」を除く5つの班では基本的にプログラミングができないといけません。ので、それらの班に興味があるな、という人はとりあえずこれを受けましょう。
本当の初心者、つまり、「パソコンでYouTube見るくらいはできる・共テ情報は頑張ったけどあんまり覚えてないかも...」くらいの人を対象に、4日間かけて「プログラミング」ができるようになるための講習会です。

上級生が大量にTAとして歩き回っているので、なにか躓いても全部質問できます。

個人的には全然これを受けるだけで部費 (年4000円) だけの価値はあると思っています。
代表がそんなこと言うのどうなんだ...? ではありますが、とりあえず1年だけ入部してこれだけ受けてみる、というのもありだと思います。ついでに興味が出れば他の講習会も受けてもらえると...!
これからの大学生活で、そして社会人になっても、これほど「プログラミング経験者が部屋に大量にいて手軽に質問できる」環境を整えるのはかなり難しいと思います。いつかプログラミングできるようになりたい、という人には超おすすめ。

プログラミング基礎講習会を開催しました!
traP で現在代表補佐を努めています たけ (たけのひと) です!5/10 〜 5/20 に渡って、5回に分けてプログラミング基礎講習会を開催しました!この記事はその開催記です。 開催概要本講習会は「プログラミング初学者」に向けたもので、基礎的なプログラミングを一から学び、今後各班で活動する上で最低限必要な知識をいくらか知ってもらうことを目的としたものです。100~150分× 5回 (初回は4時間)のカリキュラムで対面で開催しました。講師は私 Takeno_hito と代講で helgevの2人、TAの総参加者数は25人前後、参加した新入部員は(ぴったり)計100人という弊サークルでも一大規模のイベントとなりました。 開催のモチベ去年東工大に入学したたけのひとは、traP に入って講習会に参加しました。参加してまず思ったのは、「講習会のレベルが高い!」ということ。traPがめっちゃすごい(語彙力)サークルである理由が講習会に詰まっていると思えるほどレベルの高い講習会がたくさんあって、自分は講習会を通じて成長することができました。 しかし、同時にもう一つ、「初学者に対
【代表が語る】traPのあそびかた
プログラミング基礎講習会を開催しました!2024
去年に引き続き、今年もプログラミング基礎講習会を開催しました!この記事は、その開催記です。 開催概要 プログラミング基礎講習会は、traPの各班で活動するうえで最低限必要になるプログラミングの技能を短期集中で習得することを目的としています。対象は主にプログラミング初心者や未経験者の新入部員で、環境構築から基本的な概念や構文までを扱います。 今年のプログラミング基礎講習会は、4/30〜5/14の日程で開催されました。受講者は100名を超え、traPの講習会としては最大規模です。 プログラミング基礎講習会は昨年から始まりました。それ以前は講習会は未経験者向け・経験者向けが一つの制度のもとに開催されていましたが、昨年からは初心者向け講習会が「0→1講習会」と分離され、この目玉として新設されたのがこの講習会です。traPでは各班が活動内容に合わせてそれぞれ講習会を企画するのが一般的ですが、本講習会はそれらの講習会の基礎として班を横断する形で開催されています。 内容は基本的に去年のものをベースとしていますが、スケジュールとの兼ね合いで扱う範囲を絞り、去年から1回減らした全4
【代表が語る】traPのあそびかた

Unity講習会

ゲーム開発のための講習会です。Unityというのはゲーム開発のための有名なツールですね。プログラミング基礎講習会を受けた人を対象にしています。
「ゲーム班」はtraPの中で現在一番所属者が多い班となっています。やはり自分でゲームを作れるというのは面白いですからね。
ただし、ゲーム開発はただプログラミングができればいいということではありません。先程紹介したUnityというツールの使い方を知って、画像や音声、画面とプログラムをつなぎ合わせないといけません。
適当に本屋に行くと10cmくらいの分厚い本が売っている闇のツール「Unity」ですが、そのなかで最初に必要な機能に注目し、実習含め2日間程度の講習会として開講される予定です。traPではゲーム開発プロジェクトが非常に多く設立されているので、ぜひ講習会を受けて開発者として参加していきましょう!

ゲーム班の歩き方
この記事は新歓ブログリレー2024、48日目の記事です。 やあ こんにちは、23Bのゲーム班に入っているwal(ワル)です! 新入生の皆さんは東工大での生活にはもう慣れましたか? サークルや部活などでは、新歓にいっぱい参加してたくさんおごってもらいましょう 来年からはおごる側になってしまいますからね そしてtraPに入部してくれたみなさん、ありがとうございます 新入生がこのtraPに入ってくることをとても楽しみにしていました 一緒に†強く†成長していきましょう! ということで この記事は、ゲーム班で活動していきたいという新入生に向けてとなっています 「†強く†なりたいけど何をすればいいの?」「ゲーム班としてどのように活動をしていけばいいの?」という疑問に答えていきたいと思います ぜひこの記事を参考にして、”進捗”・”圧倒的成長”をしていきましょう! 0.ゲーム班の活動 ここでは、「初級編」「中級編」「上級編」の三つにカテゴリーを分けて紹介します それぞれのターゲット層は以下の通りです * ★☆☆☆☆「初級編」 *
【代表が語る】traPのあそびかた

グラフィック講習会

グラフィック班では、いわゆる「お絵描き」をするイラスト部、ドット絵を描くドット部、そして今年度から設立予定の映像部など、いろいろな範囲での活動をしています。それぞれ使うツールも知識もぜんぜん違うので、各分野の講習会が立つことになっています。

これらは前提知識は全くいらないです。一応、たとえばイラストをやるなら「紙の上で鉛筆を動かすと線が引ける」ことくらいは知っておくとよいと思います。

グラフィック班の活動紹介 2025
この記事は新歓ブログリレー2025 31日目の記事です。 はじめに みなさん初めまして。23Bのmadaraです。 本記事では新入生のみなさんに向けてグラフィック班の活動を紹介していきます。 また、後半では班員の作品紹介も行うのでぜひ最後までご覧ください。 グラフィック班とは グラフィック班では、デジタルイラスト、ドット絵、3DCG、ロゴデザインなどの制作を行っています。これに加えて、一般のイベントで頒布する画集の制作や、部員同士で知識や技術を教え合う講習会の開催なども行っています。 また、traPにおけるグラフィック系制作物のほぼ全てがグラフィック班の班員によって制作されています。 以下にいくつか例をあげます。 画集 COMITIA148 COMITIA149 コミックマーケット105 traP内依頼による制作物 新歓クリアファイル&看板 M3ジャケット 工大祭メインビジュアル また、部内チャットツール上にはグラフィック班の班員が質問できるチャンネルが用意されています。誰でも
【代表が語る】traPのあそびかた

サウンド講習会

サウンド班による作曲のための講習会です。音楽理論の基礎、そしてDAW (作曲ソフト) の操作方法などを解説します。

作曲というとなんだか「センス」が必要なイメージがありますが (僕だけかも?) 、そもそもピアノの鍵盤は何をしている? コードって何? みたいなところから理論立てて説明します。
DAWのおかげで、パソコンさえ持っていれば楽器がまったく触れなくても作曲ができます。もちろんそれだけ多機能でごちゃごちゃした画面なのですが、実際はシンプルな作りになっているのでぜひ使えるようになりましょう!

traPサウンド班の活動紹介(Ver.2024)
この記事は新歓ブログリレー2024、 51日目の記事です。 目次 * はじめに * サウンド班とは * 新歓コンピ * サウンド班制作物の紹介 * サウンド班員の曲紹介 * おわりに はじめに こんにちは、23Bの@Cd_48です。 本記事ではサウンド班の活動や、班員の作品の紹介を行います。サウンド班の魅力が少しでも伝われば幸いです。 皆さんと一緒に活動できることを楽しみにしています!!! サウンド班とは サウンド班では、パソコンを使った音楽制作や、ゲームのBGM・効果音制作を主に行っています。制作物をプロジェクトやイベントに提供したり、コンピレーションアルバムを制作してM3などの同人即売会で頒布したり、班内で講評会を開催したりと、様々な活動をしています。また、楽曲制作に関する知見共有や相談、音源・プラグイン情報の収集、講習会の開催等も行っています。 ※コンピレーションアルバム: 複数人が曲を提供するアルバム 新歓コンピ 新入生歓迎のために、今年度も新歓コンピアルバムを作成しました!班員のオリジナル楽曲が収録された全8曲のア
【代表が語る】traPのあそびかた

アルゴリズム基礎講習会

アルゴリズム班による競技プログラミングの講習会です。競技プログラミングというのは、プログラミング、そしてコンピュータの計算力を活かして、数学やパズル的な問題を解いていこうというコンテストです。実はtraPは競プロ強豪サークルで、強い人が大量にいます。
初めて問題文を見ると、知らない単語が出てきたり、また不正解になっても何が間違っているのかわからないといったことになりがちですが、コードの書き方、手元でのテスト方法、典型テクニックなどを幅広く紹介します。
AtCoderというサイトで毎週コンテストが開かれており、問題を解く中でプログラミング・タイピングの実力が上がっていきます。

The 2026 ICPC Asia Pacific Championship 参加記(AMATSUKAZE/noya2 視点)
はじめに 台湾の桃園で行なわれた The 2026 ICPC Asia Pacific Championship にチーム AMATSUKAZE として参加し、7 位(学内 1 位)でした。11 月に Dubai(UAE) で開催予定の世界大会に出場することになります。 チーム紹介 ICPC2025-2026 に AMATSUKAZE というチームで出場しています。チームメンバーは以下の通りです。 * noya2 (私) * B4 * 幾何と典型が得意 * ABC 全埋めをしたつもりが、直前の ABC で出た 675 点問題が解けないまま本番へ * shobonvip(shobon さん) * M1 * 重実装と ad-hoc な考察が得意 * JOI 埋めに取り組んでもらっていました * Rice_tawara459(
【代表が語る】traPのあそびかた

CTF講習会

CTFはCapture The Flagの略で、セキュリティのコンテストのことです。つまり、ちょっと脆弱な場所にパスワードが隠されているので、暗号やコンピュータの仕組みを使ってそれを読み出す...といった技術を競います。
映画にありがちな「黒い画面をカタカタしてパスワードを解読する」ということができます。
ひとえにセキュリティと言っても、暗号の知識が必要だったり、Webの知識が必要だったりと様々です。そのため、6分野ほどに分けてそれぞれ講習会を行います。CTFは基本的にチーム戦なので、なにか興味のある一分野に特化すればチームで大活躍することができます!

ハッキングをはじめよう
この記事は新歓ブログリレー 28日目の記事です。 こんにちは。CTF班長の@ramdosです。 コンピューターを学ぼうとするみなさんの多くは、一度はいわゆるホワイトハッカーに憧れるものだと思います。 この際「ハッカーじゃなくてクラッカーだ」「ホワイトハッカーは攻撃的なセキュリティ研究者(offensive security researcher)に言い換えられるべきだ」といった非本質的なことは忘れましょう。我々はハッカーになりたいのです。 CTFとは? (サイバーセキュリティにおける)CTF(Capture The Flag)は、運営側が作成した暗号やアプリなどの脆弱性(セキュリティ上の弱点)を見つけ、機密情報に見立てた「Flag」と呼ばれる文字列を奪取することを目的とする競技です。 この競技なら合法的に堂々とハッキングができます。嬉しいですね。 何が嬉しいのか? 競技としての面白さ CTFでは、基本的にソースコードなどを読み、その弱点を探す競技です。巧妙に埋め込まれた弱点を見つけたときの快感は筆舌に尽くしがたいものがあります。 また、CTFはそのほとんどがチ
【代表が語る】traPのあそびかた
traPavilion CTFコンテスト開催記
こんにちは、25Bのくあらんてぃんです。10/13に開催されたtraPの10周年イベント「traPavilion」において、CTF班展示としてCTFコンテストを開催しました。本記事は、その舞台裏の紹介と公式Writeupを兼ねた開催記としてお届けします。 traPavilionのイベント全体に関しては、公式サイトなどもご覧ください。 CTFについて知りたい方は以下の記事などを読むといいでしょう。 CTFを始めよう【新歓ブログリレー2020 5日目】この記事は、新歓ブログリレー2020 [https://trap.jp/tag/welcome-relay-2020/] の5日目、3月13日の担当記事です。 15B / 19M の @nari です。この記事では、Capture The Flag という競技を知らない人に向けて紹介したいと思います。 Capture The Flag とはCapture The Flag、縮めて CTF とは、サイバーセキュリティに関する競技の一つです。 サイバーセキュリティとはサイバーなセキュリティなことで、つまり インターネットやパソコンにまつわる
【代表が語る】traPのあそびかた

Webエンジニアになろう講習会

SysAd班で活動したい人のための講習会です。プログラミング基礎講習会を受けたくらいの人を対象としていて、「Webページ」「Webアプリ」を作るために必要な技術をまるっと解説します。
この講習会は名前の通り、「Webってなんですか」からWebエンジニアとして仕事にできるレベルになることを目標にしています。のでめちゃくちゃ長く、おそらく15講くらいあります。
が、実際のところ「チームで役割分担して開発する」「既存のプロジェクトに参加する」程度ならそれら全てを理解している必要はありません。最初の4、5講くらいを受ければ全然チーム参加できるようになるので、そこからは実際になにか開発しつつ、知識として受けていくのがおすすめです。

2024 年度 Web エンジニアになろう講習会を開催しました!
こんにちは、 23B の @Alt--er @masky5859 @Pugma @ramdos です この記事は、 2024 年の前期に開催した「Web エンジニアになろう講習会」についてのものです 概要 Web エンジニアになろう講習会とは、初心者が自分一人で Web アプリケーションを作れるようになるための講習会です 部内では、よく「なろう講習会」と略されます Web アプリケーションは * Web ブラウザで実際に動く部分を作るフロントエンド * サーバーでデータを管理するバックエンド * データを保存するためのデータベース など、制作時に必要なものが複数のものにまたがっています これらを一つの講習会で一貫して扱うことで、効果的に学べるようにしています 全体で 4 部構成になっていますが、前期では第 1 部 と第 2 部を実施しました テキストは一般公開しています!気になる方はぜひ! https://traptitech.github.io/naro-text/
【代表が語る】traPのあそびかた

機械学習講習会

Kaggle班による、AIを作るための講習会です。プログラミング基礎講習会程度の知識があれば数学がわからなくても問題ないです。
もちろんChatGPTのような賢いAIを作れるというわけではないですが、それでも手書き数字の認識くらいのタスクならかなり正確に答えられるようになります。
ここで学んだ知識を使うと、Kaggleなどのサイトで開催されているコンペ (コンテスト) に出場して競技に参加することが出来ます。いまAIが世界的に注目されていることもあり、かなり高額な賞金が出ていることもあります。チャンスです。

Kaggle班で機械学習講習会と部内データ分析コンペを開催しました!
こんばんは ! 情報工学系 B3 の @abap34 です。Kaggle 班の班長をしています。最近は、財布を落として教務課の人に怒られました。 さて、梅雨ですね。。。梅雨といえば機械学習です ! Kaggle班では、新入生教育・部内イベントの一環として、 * 機械学習講習会 * 部内データ分析コンペ (traP competition #00) を開催しました。 本ブログはその開催記録です。昨今のKaggle班の情勢、そして講習会、コンペ運営について書いていきます。 Kaggle班とは? この記事は、Kaggle班が今年度に出す最初のブログなので、まず簡単に Kaggle班の紹介をしたいと思います。 Kaggle班は、きちんと書くと「Kaggleをはじめとしたデータ分析コンペなどの参加を見据えつつ、機械学習について部員同士で学び、知識を深める」ということを目的とした組織です。 ...簡単にいえば、数学やプログラムを書くことが好きな人が集まって機械学習を勉強している集団です。このブログが投稿されるサイトである https://trap.jp/ を見ればわかるとお
【代表が語る】traPのあそびかた

ハッカソン

さて、各班の講習会をざっと解説してみました。7班もあるので軽い紹介でもすごい量になってしまった...

さて、これらの講習会はおおむね5月はじめ〜6月中旬くらいに詰め込まれることになっています。この期間は常に何かの講習会が開かれていると思ってよいでしょう。
ではなぜこの期間に詰め込むのか、というと、6月中旬頃に「春ハッカソン」というイベントがあります。
これは、「新入生と上級生で6人程度のチームを組み、2日間でゲームやWebアプリを作ろう!」というイベントです。多くの新入生にとっては初めての開発であり、初めてのチーム開発であり、講習会で学んだ内容の実践編となります。だからこれ以前に講習会が詰め込まれているわけですね。
2日間って短くないか? と思われるかもしれませんが、短いです。が、そんなことはチームの上級生がどうにかしてくれます。「自分が参加しても力になれない...」なんてことはなく、リーダーが適度な難易度のタスクを降ってくれるので、安心して参加してください!!

2025年度 春ハッカソンを開催しました!!
はじめに 2025年6月21・22日の二日間にかけて、部内春ハッカソンを実施いたしました!今回の春ハッカソンは26チーム、180人の参加となりました。テーマを「ぐるぐる」・「じゅう」と設定し、テーマに上手く絡めた作品が多く誕生しました。現在も製作に取り組んでいる班もあり、続々と完成報告が届いています。受賞作品の中にも制作進行中の作品もございますので、続報をお待ちください!(一部作品は部内限定公開となっております) なお、今回のハッカソンは ナレッジワーク様 のご協賛をいただいております。この場を借りてお礼申し上げます。 株式会社ナレッジワーク私たちは「できる喜び」をお互いに届けあえる機会が毎日訪れる社会の実現に貢献します。株式会社ナレッジワーク 優秀賞 今回の優秀賞は3つの班が受賞しました。 7班「SOLSTICE」 ・ad astra per aspera ──困難を通じて天へ──宇宙・時間をテーマとしたモーショングラフィックスのMVです。 14班 死戦屋台「満漢★全席 デバウアー」 ・食うか、食われるか。食いしん坊の女の子が主人公のボーカル
【代表が語る】traPのあそびかた

プロジェクト

ここまでの紹介を見てなんとなくわかったかと思うのですが、班ごとにやっていることはかなりバラバラです。まるでサークルが7個集まっているような感じです。

では班をまたいだ活動はないのかと言うと、全然そんなことはないです。たとえばゲーム制作の場合、プログラムだけではなくイラストやBGM、効果音も必要です。このような場合、班をまたいでメンバーを募集し、「プロジェクト」というまとまりで活動します。

traPでは大量のプロジェクトが同時並行で進行しています。多分20個くらいあります。多くはゲームやサービスの開発ですが、例えば動画投稿を目標にしているプロジェクトなど、最近はどんどん新しい形式のプロジェクトが増えてきています。

おそらく秋ごろにプロジェクトのメンバー募集があります。講習会、ハッカソンを抜け、少し落ち着いたタイミングで長期の「チーム開発」に参加してみるのはどうでしょう。
プロジェクト側も「初心者でも教えるのでぜひ来て」という温度感で募集している事が多いです。自信がなくても、興味があれば参加してみると良いでしょう!

工大祭2025にて音ゲー「Senirenol Bloom」を展示しました
公式𝕏: https://x.com/Senirenol_traP公式サイト: https://t.co/av75Pgj43R はじめに こんにちは!Senirenolプロジェクトのリーダーhijoushikiです。今年の11/2~11/3の2日間にかけて行われた工大祭2025にて、私たちが開発する音ゲー「Senirenol」シリーズ最新作の「Senirenol Bloom」を展示したのでご紹介します。 Senirenol Bloomとは 「Senirenol」シリーズは、専用コントローラーでプレイする8鍵の新感覚音ゲーです。工大祭での展示を第一目標に制作を行っており、毎年工大祭のテーマに合わせてバージョンアップを行っています。 シリーズ4作目にあたる今回のテーマは「Bloom」で、花をモチーフとして新規楽曲の追加や新機能の追加などを行いました。 ゲーム説明 ゲームルールは非常にシンプルで、一般的な音ゲーと同じように、曲に合わせて流れてくるノーツをタイミング良く押します。見た目もCHUNITHMやプロセカなどと同じような感じなので、初見でも親しみやすいと思います
【代表が語る】traPのあそびかた
FRENZ2025に出展しました: きつねっこ♪スキンシップ
FRENZ(フレンズ)は、完全新作の映像作品を上映するイベントです。イベント名「FRENZ」は、「FRIENDS(仲間)」と「FRENZY(熱狂)」を掛け合わせた造語で、創る側と観る側が一体となり、熱量を共有する場を目指しています。 FRENZ 2025 - Friends(仲間達)とFrenzy(熱狂)する新作映像ライブイベントFRENZ 2025 - Friends(仲間達)とFrenzy(熱狂)する新作映像ライブイベント『FRENZ 2025』Friends(仲間達)とFrenzy(熱狂)する新作映像ライブイベント このイベントに出展しました。traPとしての出展は3年目になりますが、メンバーはリーダー含めてほぼ新規メンバーです。 制作物 狐っ子の姉妹2人がけもみみしっぽをもふもふなでなでするMVです。 制作メンバー 開発メンバーは9人, 期間は制作3ヶ月(+プロトタイプ1.5ヶ月)です。 * リーダー・原案・
【代表が語る】traPのあそびかた

コミュニティ

いちばん大切なことを忘れていました。コミュニティです。
traP内ではtraQという自作ツールを使って会話が行われています。Discord、あるいはSlack、Misskeyを想像してもらえると良いでしょう。
現在累計800万ものメッセージがあり、日常のつぶやき、事務連絡、雑談、技術的な議論までなんでも投稿されています。traPは (700人も入る部屋はないので) 基本的にオンライン活動がメインなのですが、常にtraQ上でメッセージが飛び交っており、非常に賑やかです。

traQ投稿数1位を支える技術
アドベントカレンダー2023 24日目の記事です。traQ投稿数2023年度内1位を誇るしーぴーが、どういうことを投稿すればいいのか、どうやってそれを維持していくのかというのをお話しします。これを読んで皆さんもtraQ廃を目指しましょう!
【代表が語る】traPのあそびかた

また、traQがあることで、他の班の人の活動も目に入りやすいです。たとえば競技プログラミングを専門にしている人でも、イラスト専門の人のつぶやきを見に行くことができます。
他のサークルでは、多くの場合競技プログラミングとイラスト制作は別のサークルとなっており、すこし距離が遠いです。しかし、traPはそれらがすべて1つのサークルとしてまとまっているので、めちゃくちゃ周囲から刺激をもらうことが出来ます。

いろんな班に首を突っ込もう

自分語りになってしまうのですが、例えば僕は入部時はWeb開発と競技プログラミングに興味がありました。が、1年の冬にゲーム開発を、2年になるころにイラストを、2年の夏休みに作曲を始め、2年の冬には動画制作もやるようになりました。
traPには班が7個もあり、もちろん人によって分野の向き・不向きがあります。ので、まずは興味が出たところから参加して、可能であれば2年くらいかけて全分野ちょっとづつ触ってみるというのも良いと思います。自分にセンスがあるかどうかはやってみないとわからないので。これは日本中でもtraPくらいでしかできない遊び方なんじゃないかと思います。軽率にいろんな班・イベントに参加してみてください!

まとめ

さて長々と話してきました。が、結局何が言いたいのかというと、ただ「楽しんでください」というだけです。

traPは創作を楽しむ同好会です。traPには7つの班、そしてそのそれぞれにいろんな分野があります。どれか1分野でも、新入生の皆さんが「楽しい」と思って取り組んでもらえる分野があったらいいな、と思っています。

では、皆さんのご入学をお待ちしております。気が向いたらtraPを覗きに来てもらえるとありがたいです!

明日の新歓ブログリレーは @sakura さんです! 楽しみ〜

]]>
<![CDATA[【簡単】ユーザースクリプトで有意義な競プロライフを【Tampermonkey】]]>はじめに

この記事は新歓ブログリレー2026 11日目の記事です。

こんにちは!25B の @o_o です
みなさんは AtCoder で問題を解

]]>
https://trap.jp/post/2859/69b7d747e2394c00016e23dfMon, 16 Mar 2026 14:30:33 GMTはじめに【簡単】ユーザースクリプトで有意義な競プロライフを【Tampermonkey】

この記事は新歓ブログリレー2026 11日目の記事です。

こんにちは!25B の @o_o です
みなさんは AtCoder で問題を解くとき、ユーザースクリプト使っていますか?

こういうのとか

【簡単】ユーザースクリプトで有意義な競プロライフを【Tampermonkey】

こういうの

【簡単】ユーザースクリプトで有意義な競プロライフを【Tampermonkey】

使ってない方はインストールの方法を検索してみてください。丸投げです。

使っている方へ、
問題を解くときにちょっと不便だな、でもそれを改善するユーザースクリプトないなってなったことはありませんか?
私は普段このように画面を二つに分けているのですが、コンテスト名が見えないのが不便だなと思っていました。(大きいディスプレイを使うべき)

【簡単】ユーザースクリプトで有意義な競プロライフを【Tampermonkey】

左側にでかでかとコンテスト名が見えてほしいですよね。

Tampermonkey ってなに

簡単に言うと、今見ているWebページの見た目や動きを、好きなように簡単に改造できちゃうブラウザ拡張機能です。間違っていたらごめんなさい

JavaScriptを書くことで、特定のサイト(今回ならAtCoder)を開いたときに、自動で自分の用意したプログラムを走らせることができます。
「ここにボタンが欲しいな」「この文字もっと大きくしたいな」「コンテスト名をでかでかと表示させたいな」みたいなことをめんどくさいことを無視して書けちゃいます!

どうやって書くの

Tampermonkey インストールされていますか?
インストールされているなら

【簡単】ユーザースクリプトで有意義な競プロライフを【Tampermonkey】

ここの「新規スクリプトを追加...」ボタンを押して

【簡単】ユーザースクリプトで有意義な競プロライフを【Tampermonkey】

こんな画面が開かれたら成功です!

ご丁寧に Your code here... とあるのでここに自分のコードを書いたら動きます!

以下が書いたコードです

// ==UserScript==
// @name         atcoder Problem Name Inserter
// @namespace    http://tampermonkey.net/
// @version      2025-12-13
// @description  window サイズが小さくてもコンテスト名が表示されるようにします
// @author       You
// @match        https://atcoder.jp/contests/*/tasks/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const contestId = location.pathname.split("/")[2].toUpperCase()

    const markup = `
  <div id="contest-name"
  class="h3">
  ${contestId}
  </div>
  `;

  document.querySelector("#contest-nav-tabs + div").insertAdjacentHTML("afterbegin", markup);
})();

解説

やっていることは、ざっくり以下の4ステップです!誰でもできちゃいますね

1. 説明とか書く

ここではどのページでこのプログラムを実行するのかや表示名、説明を書きます。
公開する予定がないのであれば以下だけ書けば大丈夫です。

// ==UserScript==
// @name         atcoder Problem Name Inserter
// @namespace    http://tampermonkey.net/
// @version      2025-12-13
// @description  window サイズが小さくてもコンテスト名が表示されるようにします
// @author       You
// @match        https://atcoder.jp/contests/*/tasks/*
// @grant        none
// ==/UserScript==

name

こんな感じで表示されます。自分がわかれば問題ないです。

【簡単】ユーザースクリプトで有意義な競プロライフを【Tampermonkey】

description

ホバーすると出てきます。自分がわかれば問題ないです。

match

一番重要です!どのページで実行するかを指定します。
https://atcoder.jp/contests/*/tasks/* のようにどんなものでもいいところを * にして書きます。

1. URL からコンテスト名を持ってくる

コンテスト名を表示したいので、URL から引っ張ってきましょう

const contestId = location.pathname.split("/")[2].toUpperCase()

location.pathname で現在のURLのパス(例: /contests/abc000/tasks/abc000_a) を取得します。
それを split("/")/ ごとに切り刻み、3番目の要素(インデックスは [2])を取り出しています。
コンテスト名はやっぱり大文字のイメージがあるので、最後に toUpperCase() を使って、小文字を大文字(abc000 → ABC000)に変換しています

2. 差し込みたい HTML 要素を定義する

先ほど取得した contestId を使って、画面に表示させるための DOM の中身を markup という変数に用意します。AtCoder の既存のデザインに馴染むように class="h3" を指定して、いい感じの大きさの見出しになるようにしています。

3. 差し込みたい場所を探して差し込む

ブラウザの開発者モード使って差し込む場所を特定します。
querySelector はページ内で一番最初に一致した要素を取得できます。id やクラス名で指定できるため、差し込みたい場所の周辺で他とかぶってなさそうなところを選択します。
今回は contest-nav-tabs を選択しました。

【簡単】ユーザースクリプトで有意義な競プロライフを【Tampermonkey】

この contest-nav-tabs の次の div のすぐ内側、最初の子の前に差し込みたいため、querySelector("#contest-nav-tabs + div")で指定します。
次に、insertAdjacentHTML("afterbegin", markup)で差し込みます。
insertAdjacentHTML には afterbegin のほかにも引数はあるので適切なものを選んでください。

https://developer.mozilla.org/ja/docs/Web/API/Element/insertAdjacentHTML#引数

おわりに

できました!!!
見た目にそこまでこだわらない自分用の簡単な機能であればこれだけでできちゃいます!

【簡単】ユーザースクリプトで有意義な競プロライフを【Tampermonkey】

皆さんもぜひ、ユーザースクリプトで快適な競技プログラミングライフを送りましょう!

明日の投稿者は@zoi_dayo さんです!

]]>
<![CDATA[LaTeX in VSCode 快適執筆編]]>

これは新歓ブログリレー2026 10日目(?)の記事です。

また、この記事は LaTeX in VSCode第二弾です。第一弾はこちら->LaTeX in VSCode

]]>
https://trap.jp/post/2563/67faa4a42b18d60001610531Sun, 15 Mar 2026 21:00:22 GMTLaTeX in VSCode 快適執筆編

これは新歓ブログリレー2026 10日目(?)の記事です。

また、この記事は LaTeX in VSCode第二弾です。第一弾はこちら->LaTeX in VSCode 環境構築編

はじめに

こんにちは! 23Bの@Hueterです。

LaTeX in VSCode第二弾です。前回は環境構築編としてLaTeXの環境をVSCodeを用いて手元に用意してみました。今回は快適執筆編ということで、LaTeXを快適・便利に使えるようになる情報を紹介しようかなと思います。内容としては、パッケージ・スニペット・フォーマッターの大きく分けて三つについて扱います。
書いているうちにだんだんと内容が多くなってしまい、個々の内容について詳しく書けなかった部分もありますが、概念を知る機会として役立ててもらえたらなと思います。

それでは、より良いLaTeXライフを目指して。

パッケージ

LaTeXのデフォルト状態だとセクションや図表の挿入等、基礎的な機能しか入っていません。ただ、LaTeXには様々なパッケージが存在しており、これを入れることで機能を追加することができます。環境構築編の動作確認でも記述した、\begin{align}...(数式をより綺麗に位置を調節して書くためのコマンド)も\usepackage{amsmath,amsfonts}によりパッケージを入れることで初めて使えるコマンドです。
パッケージは\usepackage{パッケージ名}を冒頭(動作確認で書いたコード参照)に書くことで入れることができます。
ということで、ここでは私が使っているものを主にいくつか紹介しようと思います。

graphicx (図の挿入)

先ほど軽く取り上げましたが、LaTeXで画像を入れる際に用いるパッケージです。
画像の入れ方は、画像データを用意してから以下のように書けば良いです。画像の指定は、.texファイルから見た画像ファイルの相対パスを書くことでできるため、画像ファイルがたくさんある場合はフォルダ分けをしても良いですね。

\begin{figure}[htbp]
  \centering
  \includegraphics{image.png} % 画像の相対パス
  \caption{<caption>}
  \label{<label>}
\end{figure}

ただし、上のような書き方だと挿入する画像サイズそのままでPDF出力されるため入れる画像によって画像が大きかったり小さかったりするかもしれません。そこで、オプションから画像サイズ等を指定していきます。上記のコードの\includegraphics{}の行を以下のようにしてみましょう。

  \includegraphics[width=.70\columnwidth]{image.png}

そうすると、画像の横幅がPDFの幅の70%になります。width=...には数字でpxを指定することもできますが、PDFの横幅が何pxなのか考える手間があるので私は.70\columnwidthの方をよく用いています。

here 図表の位置指定

図表の位置を特定の図や文の間に挟みたい、という場面で使えます。
本来は図表の位置を[htbp]と指定すると、その場所(h)→ページ上部(t)→ページ下部(p)→最後のページ(p)という順番に場所を見て、図表を置けるスペースが一番最初にあった場所に図表が置かれます。そのため、変な場所に図表が置かれるということがよく起こるのですが、hereパッケージを入れて[htbp][H]に置き換えることで文章と図表の順番が書いたとおりの順に完全に固定されます。ただし、順番が完全固定されることから不自然な空白がよくできるのでPDFにビルドした後に図表を入れる位置の調整が必要になるのが欠点です。

amsmath等 数式

数式の記述を簡単にして、数式出力の品質を向上させるさまざまな機能を提供してくれるパッケージの一つです。これはアメリカ数学会が開発したもののひとつで、他にもamsfontsamssymbなど数式関連のパッケージがあります。私は今出した三つぐらいしか使ってませんでしたが、気になる人は調べてみてください。(参考リンク)
 amsmathでよく使っていたものとしてはalignという数式環境です。というかこれぐらいしかよく使ってなかった気がします。amsmathの数式環境では&を使うことで複数の数式の位置を縦でそろえることができます。式変形などで数行にわたって式を書く際に見た目をきれいにできるのでおすすめです。

LaTeX in VSCode 快適執筆編

mhchem 化学式

化学式を書く際に、上付き文字や下付き文字をいい感じに変換してくれるパッケージです。\usepackage[version=3]{mhchem}で入れられます。例えば硫酸では\ce{H2SO4}と入力するだけで数字はすべて下付き文字に変えてくれるうえ、水の電気分解は\ce{2H2O -> 2H2 + O2}とすれば添え字と係数の判別もしてくれます。ただし、添え字に変数が入ったり、大文字小文字を間違えるとうまくいかないので注意です。

siunitx 単位

「数値と単位との間にスペースを空けること」、「単位は必ず立体にすること」など国際単位系として数字・単位の表記にはいろいろなルールがあります。適当なレポートではそこまで気にする必要はないですが、後々論文を書く際に必要になるかもしれないので、これらを楽にできるパッケージの紹介です。
 ざっくりいうと\qty[オプション]{数値}{単位}とすることで数字と単位のセットを出力できます。(例:\qty{550}{\degreeCelsius})詳しくはこちらのsiunitxの使い方という記事を参考にどうぞ。単位などのマクロも追加されるので、変換で出した単位だとエラーが出てビルドができないというときは使ってみてください。SI単位(国際単位系) - siunitxパッケージのマクロ

スニペット

スニペットとは簡単に言うとプログラミング版の辞書登録機能です。頻繁に使う一定の語句を少ない文字を打つだけで変換するというようなものです。
ここでは3つのスニペット機能について紹介します。

スニペット ① LaTeX Workshop

環境構築編でVSCodeに入れたLaTeX Workshopですが、これ入れると一部のスニペットがすぐ使えるようになります。例えば、SSEと入力してEnterを押すと\section{}に変わったり、BALと入力してEnterを押すと、

\begin{align}
  
\end{align}

に変わったりします。
他にもたくさんありますが、詳しくは以下の記事を参考にしてください。
LaTeX Workshop をもう少し使いこなす

スニペット ② ユーザースニペット

①で紹介したのはLaTeX Workshopが提供しているものですが、VSCodeの機能を使うことで自分でもスニペットを作成することができます。
まず、VSCode左下の管理(歯車アイコン)からスニペットを選択し、latex.jsonを選択します。そうするとlatex.jsonというファイルが開けるので、そこに自分のスニペットを定義していきます。
最初の方にあるコメント部分は記述方法ですが、ざっくり以下の通りです。

{
  "辞書の名前": {
    "prefix": "変換元",
    "body": [
      "変換後",
      "複数行のものについてはカンマ区切りで記述する"
    ],
    "description": "授業レポート用テンプレート"
  },
  "次の辞書": {
      ...
  }
}

こうすることで「変換元」の文字を入力して変換候補から選択するとbodyに記述した変換後の文字が入力されます。変換後に関しては複数行でも問題ないので、最初に書くLaTeXの雛形をスニペットに登録しておくと楽です。
 また、変換後の部分に${1}というものを記述すると変換後に${1}を書いた場所に入力のバーを移動させることもできます。さらに、${2}のように中の数字を増やしていけば、タブキーを押すと数字の順にまた入力のバーを移動できます。スニペットの中で変更を加えるところが決まっているなら設定するとマウスを触らなくていいので少し楽できます。ただし、矢印キーで移動をするとタブの移動ができなくなるので注意です。
 スニペットを作るうえで気を付けることとしては、LaTeXのコードをそのまま上のbody部分に入れることはできず、①各行を""で囲み、②最後の行以外の末尾に,を入れ、③\\\に書き換える必要があります。特に最後のバックスラッシュについては書き換え漏れをしやすいので忘れずに。面倒ならGeminiにでも変換を任せちゃいましょう。

私が今使っているユーザースニペットは記事の一番下に張り付けているので見てみてください。

スニペット③  HyperSnips Extension

先ほどまでのは、VSCodeのユーザースニペットを利用したものでしたが今度は HyperSnips Extension を用いたスニペットです。HyperSnips Extensionのスニペットの特徴は、ユーザースニペットとは異なりVSCodeの変換に出てこない代わりに、変換前に設定した文字が入力されると即刻変換後の文字に変わることです。分かりにくい人は参考にしたブログにデモ動画があるので見てみてください(HyperSnips Extension スニペットデモ動画)
 導入方法・使い方としては、HyperSnips ExtensionをVSCodeに入れ、latex.hsnipsというファイルを所定の場所に作成するとできます。場所は以下の通りです。今回はLaTeXなので(language)latexになっていますが名前を変更すれば別の言語でもできるはずです。(試したことないのでわかりません。)

Windows: %APPDATA%\Code\User\globalStorage\draivin.hsnips\hsnips\(language).hsnips
Mac: $HOME/Library/Application Support/Code/User/globalStorage/draivin.hsnips/hsnips/(language).hsnips
Linux: $HOME/.config/Code/User/globalStorage/draivin.hsnips/hsnips/(language).hsnips

latex.hsnipsの中身についてはすべてを自分で書くのは難しいので、まずは既存のものを使うといいです。私はこちらの中身をコピペして多少書き加える形で使っています。書き加えは数式爆速入力のための LaTeX 動的スニペット in VSCodeという記事を見ながらしてみてください。
 このlatex.hsnipsでは;;を打つだけで数式の枠(\( \))を出してくれたり、数式の枠内にいるときにzともう一つアルファベットを入力すると対応するギリシャ文字に変換してくれたりする機能が入ってます。(例:za\alpha,zt\theta)また、地の文でこのような変換が起きると大変なので、これら変換は基本的に数式モード(勝手につけた名前です)の時にしか起きないようになっています。ここで言う「数式モード」は\( \)\begin{align}...の中など、文字が数式特有のものに変化する領域の時としています。地の文で定義すると既存の英単語を誤変換しないよう定義に注意が必要ですが、変換を数式モードの時限定することにより、かなり攻めた変換を定義することができています。
 使用する際の注意点としては、基本的には要素の間には半角スペースを入れるようにすることです。一部例外(*/\frac{*}{}等)はありますがzaではなく+zaみたいに直前に記号や別の文字があると認識されません。これに関してはコピペ元のlatex.hsnipsでそのように定義されているから、と言うしかないです。定義方法が正規表現で書かれているため、正義表現がわかるという人はlatex.hsnipsの中身を見てみると半角スペースが必要なものかどうかの判別がつくかもしれません。ただ、数式を書く上で記号・数字・文字の間は半角スペースを空けたほうが見直しやすいですし、出力先のPDFには何も影響がないので気を付け得です。
 他にもユーザースニペットの時と同様に$1,$2...を用いることで入力のバーの移動を定義でき、Tabキーから移動できます。最後に移動するまでに矢印キー等で入力のバーを移動させたときにこのTabキーの移動が使えなくなるのも同じです。

フォーマッター latexindent

フォーマッターはファイルのフォーマットを指定したとおりに整えてくれるものです。整えるのは.texファイルの中身なので出力されるPDFの見た目は一切変わりませんが、整った.texファイルは後で見返しやすいので余力があるなら環境を整えてもいいでしょう。

LaTeXのフォーマッターには latexindent というものを使います。VSCodeのsetting.jsonに以下の二行を追加すると動きます。

"latex-workshop.formatting.latex": "latexindent",
"latex-workshop.formatting.latexindent.path": "latexindent",

これでCtrl + sで保存する際にフォーマッターが動く...かもしれません。
実は、latexindent はPerlという言語で書かれておりPerlの実行環境がないと動きませんが、TeX Liveにバイナリが同梱されているのでWindowsではPerlが不要となっています。ただし、バイナリがうまくダウンロードできてない場合はPerl環境を入れる必要があります。Perlのインストール方法はPerlのダウンロードとインストールという記事を参考にしてみてください。

これで、latexindentを使えるようになりましたがデフォルトの設定で不満がある場合は各自で設定を変えることができます。詳しい方法は latexindentの設定をカスタマイズして使うという記事を見てみてください。

おわりに

いかがでしたか?内容が多いのもあり後半は参考にしたサイトの紹介みたいなことになりましたが、「こんな機能欲しかった!」というものが見つけられていれば幸いです。

次回は@o_oさんの記事です。お楽しみに。

参考資料

siunitxの使い方
数式爆速入力のための LaTeX 動的スニペット in VSCode
Windows環境での LaTeX in Vscode

付録

参考までに私が使っているユーザースニペットを張り付けておきます。
すぐ使いたい、という人はコピペで使ってみてください。

{
    // Place your snippets for latex here. Each snippet is defined under a snippet name and has a prefix, body and 
    // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
    // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the 
    // same ids are connected.
    // Example:
    // "Print to console": {
    // 	"prefix": "log",
    // 	"body": [
    // 		"console.log('$1');",
    // 		"$2"
    // 	],
    // 	"description": "Log output to console"
    // }
    "report": {
        "prefix": "report",
        "body": [
            "\\documentclass[${1:a4paper,11pt}]{${2:jsarticle}}",
            "",
            "",
            "% 数式",
            "\\usepackage{amsmath,amsfonts}",
            "\\usepackage{bm}",
            "",
            "% 画像",
            "\\usepackage[dvipdfmx]{graphicx}",
            "${3}",
            "",
            "\\newcommand{\\unit}[1]{\\,\\mathrm{ #1 }}",
            "",
            "\\begin{document}",
            "",
            "\\title{${4}}",
            "\\author{${5}}",
            "\\date{${6}}",
            "\\maketitle",
            "",
            "$0",
            "\\end{document}"
        ],
        "description": "授業レポート用テンプレート"
    },
    "experiment": {
        "prefix": "experiment",
        "body": [
            "% \\documentclass[a4paper,dvipdfmx,twocolumn]{jsarticle}",
            "\\documentclass[a4paper,dvipdfmx]{jsarticle}",
            "",
            "",
            "% 数式",
            "\\usepackage{amssymb,amsmath,amsfonts}",
            "\\usepackage{bm}",
            "\\usepackage{siunitx}",
            "",
            "% 画像",
            "\\usepackage[dvipdfmx]{graphicx}",
            "\\usepackage[hang]{caption}",
            "\\usepackage{here}",
            "\\usepackage{subcaption}",
            "",
            "%PDF挿入用",
            "\\usepackage{pdfpages}",
            "",
            "%化学式",
            "\\usepackage[version=3]{mhchem}",
            "",
            "%URL",
            "\\usepackage{url}",
            "",
            "%表中の罫線",
            "\\usepackage{booktabs}",
            "\\usepackage{multirow}",
            "",
            "% sectionの前に空行",
            "\\usepackage{titlesec}",
            "",
            "",
            "% 諸々の設定",
            "% 二段組の線追加",
            "% \\setlength{\\columnseprule}{0.4pt}",
            "",
            "% sectionの前に空行",
            "\\titlespacing*{\\subsection}{0pt}{.5\\baselineskip}{0.0\\baselineskip}",
            "",
            "\\newcommand{\\unit}[1]{\\,\\mathrm{ #1 }}",
            "",
            "\\newcommand{\\two}{I\\hspace{-1.2pt}I}",
            "\\newcommand{\\three}{I\\hspace{-1.2pt}I\\hspace{-1.2pt}I}",
            "\\newcommand{\\ctext}[1]{\\raise0.2ex\\hbox{\\textcircled{\\scriptsize{#1}}}}",
            "",
            "\\begin{document}",
            "",
            "",
            "\\begin{titlepage}",
            "  \\huge",
            "  \\begin{center}",
            "    実験レポート",
            "  \\end{center}",
            "  \\vskip3 \\baselineskip",
            "  \\begin{center}",
            "    \\underline{実験題目\\qquad }",
            "  \\end{center}",
            "  ",
            "  ",
            "  \\vskip4 \\baselineskip",
            "  \\LARGE",
            "  \\hspace{4em}実験日\\hspace{3em}202年月日\\,-\\,202年月日",
            "  \\vskip0.5 \\baselineskip",
            "  \\hspace{4em}提出日\\hspace{3em}202年月日",
            "  \\vskip2 \\baselineskip",
            "  \\hspace{4em}学籍番号\\hspace{2em}",
            "  \\vskip0.5 \\baselineskip",
            "  \\hspace{4em}実験班名\\hspace{2em}班",
            "  \\vskip0.5 \\baselineskip",
            "  \\hspace{4em}氏名\\hspace{4em}\\,",
            "  \\vskip0.5 \\baselineskip",
            "  ",
            "  \\normalsize",
            "  % タイトルページにもページ数を割り当てたい場合は下をコメントアウトする",
            "  \\thispagestyle{empty}",
            "\\end{titlepage}",
            "% タイトルページにもページ数を割り当てたい場合は下のコメントを消す",
            "% \\addtocounter{page}{1}",
            "",
            "",
            "\\tableofcontents",
            "\\clearpage",
            "",
            "",
            "\\section{目的}",
            "",
            "",
            "\\section{実験方法}",
            "\\subsection{1}",
            "\\subsection{2}",
            "\\subsection{3}",
            "\\subsection{4}",
            "\\subsection{5}",
            "",
            "",
            "\\section{実験結果}",
            "\\subsection{1}",
            "\\subsection{2}",
            "\\subsection{3}",
            "\\subsection{4}",
            "\\subsection{5}",
            "",
            "",
            "\\section{考察}",
            "\\subsection{1}",
            "\\subsection{2}",
            "\\subsection{3}",
            "",
            "",
            "\\section{問題}",
            "",
            "",
            "\\section{結論}",
            "",
            "",
            "\\begin{thebibliography}{99}",
            "  \\bibitem{01}",
            "\\end{thebibliography}",
            "",
            "",
            "\\end{document}"
        ],
        "description": "実験レポート用テンプレート"
    },
    "beginFigure": {
        "prefix": "BEGINFIG",
        "body": [
            "\\begin{figure}[H]",
            "  \\centering",
            "  \\includegraphics[width=.${1:90}\\columnwidth]{${2}}",
            "  \\caption{${3:<caption>}}",
            "  \\label{${4:<label>}}",
            "\\end{figure}",
        ],
        "description": "画像挿入"
    },
    "beginTable": {
        "prefix": "BEGINTAB",
        "body": [
            "\\begin{table}[H]",
            "  \\centering",
            "  \\caption{${1:<caption>}}",
            "  \\label{${2:<label>}}",
            "  \\begin{tabular}{${3:<columns>}} \\hline",
            "  ${4}",
            "  \\hline",
            "  \\end{tabular}",
            "\\end{table}",
        ],
        "description": "表挿入"
    },
    "beginMiniPageFigure": {
        "prefix": "BEGINMINIFIG",
        "body": [
            "\\begin{figure}[H]",
            "  \\centering",
            "  \\begin{minipage}[c]{.${1:48}\\columnwidth}",
            "    \\centering",
            "    \\includegraphics[width=.90\\columnwidth]{${2}}",
            "    \\caption{${3:<caption1>}}",
            "    \\label{${4:<label1>}}",
            "  \\end{minipage}",
            "  \\begin{minipage}[c]{.${1:48}\\columnwidth}",
            "    \\centering",
            "    \\includegraphics[width=.90\\columnwidth]{${5}}",
            "    \\caption{${6:<caption2>}}",
            "    \\label{${7:<label2>}}",
            "  \\end{minipage}",
            "\\end{figure}",
        ],
        "description": "minipage画像挿入"
    }
}
]]>