Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ローグライクマニュアル

ナビゲーション: ガイド | ドキュメントルート

目次

  1. この文書とソースの関係
  2. この例が実演すること
  3. ビルドと実行
  4. 操作
  5. ゲームプレイのルール
  6. ホストとスクリプトのアーキテクチャ
  7. ゲームティックスクリプトを読む
  8. ダンジョンジェネレータを読む
  9. ホットリロード
  10. プレイヤー AI スクリプトを読む
  11. 戦闘スクリプトを読む
  12. 人工知能のアーキタイプを読む
  13. アイテム効果スクリプトを読む
  14. ベスティアリスクリプトを読む
  15. consume スクリプトと descend スクリプトを読む
  16. 読者のための演習
  17. 総まとめプロジェクト
  18. 参照テーブル

この文書とソースの関係

ローグライクの例は、そのソースを2つのディレクトリに分割しています。Rust ホストコードは examples/rogue/ の下にあります。24個の Keleusma スクリプトは examples/scripts/rogue/ の下にあります。examples/rogue/main.rs 内の include_str! 行は相対パスを通じてスクリプトディレクトリを参照し、同じファイル内の SCRIPT_DIR 定数はホットリロードパスのためにそこを指します。このマニュアルは例に対する長編の解説書です。ゲームのルール、ホストのアーキテクチャ、各スクリプトの責務、および読者が組み込みパターンへの習熟を深めるために試みることのできる段階的な演習セットを記述しています。

ベスティアリ、アイテム、ステータスのテーブルは、このマニュアルに再掲するのではなく、ホストソース内にインラインで定義されています。ゲームプレイの節で引用される数値は安定した設計上のデフォルトですが、それらがいつか食い違った場合はソースが権威を持ちます。

この例が実演すること

この例はシンクライアントの哲学を軸に構築されています。Rust ホストは3つのこと、それのみを行います。すなわち、ユーザー入力の取得、ユーザー出力の表示、そして初期化とネイティブの接続を含む Keleusma スクリプト呼び出しの管理です。すべてのゲームプレイのルールは Keleusma スクリプトに存在します。ホストのネイティブは、表示と入力とゲームロジックの間のアプリケーションプログラミングインターフェースの境界です。

7つのパターンが示されています。

第一に、すべてのゲームティックを駆動する loop main スクリプトです。rogue_game.kel はこの例のターンごとのオーケストレータです。ホストはプレイヤー入力ごとに一度これを再開します。本体はプレイヤーコマンドを適用し、すべてのモンスターを反復処理し、各モンスターのアーキタイプをディスパッチし、ターン終了時の記帳処理をティックします。ゲームティックスクリプトを読むを参照してください。

第二に、ワンショットのジェネレータスクリプトです。rogue_dungen.kel はホストネイティブを通じてマップを書き込みます。このスクリプトはフロアへの降下ごとに一度完了まで実行されます。これは副作用のあるネイティブを伴う fn main の自然な使い方です。

第三に、イベントごとの純粋関数です。8つの人工知能アーキタイプのうち7つは、モンスターとワールドの状態のスナップショットを受け取り、アクションタプルを返す fn main スクリプトです。スクリプトはワールドを直接変更しません。ホストが返されたアクションを検証し、変更をコミットします。いくつかのモンスター種は各アーキタイプを共有し、ステータスの差はホスト側の図鑑テーブルに由来します。

第四に、呼び出しをまたいで状態を保持する loop main スクリプトです。ボスのアーキタイプは yield を伴うストリームチャンクの形を使います。データセグメントのターンカウンタが呼び出しをまたいで持続するため、ボスは複数ターンにわたる攻撃パターンを実行します。ボスの loop main の形を参照してください。

第五に、薄いスクリプトを伴うホスト駆動のディスパッチです。アイテム効果スクリプトは、効果識別子を差分とステータスコードに対応付ける小さな match テーブルです。ホストが差分を適用し、ステータスアクションを実行します。この分割により、エンジンに触れるコードはホストに、ゲームプレイのルールはスクリプトに保たれます。

第六に、ホットリロードです。F5 キーバインドは、すべてのスクリプトをディスクから再読み込みし、再コンパイルし、新しいモジュールを実行中の仮想マシンにアトミックにスワップします。ワールドの状態はスワップを生き延びます。ホットリロードを参照してください。

第七に、アクターとしてのプレイヤーです。rogue_player_ai.kel は他のすべての人工知能スクリプトと同じ形をしています。ホストは、モンスターに使うのと同じアクターごとのパスを通じてこれをディスパッチします。プレイヤーの違いは意図の出所であり、ディスパッチの形ではありません。戦闘計算も rogue_combat.kel を通じてスクリプトに存在します。プレイヤーAIスクリプトを読むおよび戦闘スクリプトを読むを参照してください。

この例は保守的検証の規律も示しています。すべてのスクリプトのすべてのループには、静的に抽出可能な反復回数の上限があります。動的な上限は、条件付き本体を持つ固定上限のループとして書かれています。再帰はありません。検証器は出荷されるすべてのスクリプトを受理します。

既知の先送り項目

これらの挙動は、出荷される機能ではなく、意図的に演習として残されています。それぞれは読者のための演習セクションに、該当するエントリ番号とともに記載されています。

  • Sleep、Confusion、Remove Curse の巻物はプレースホルダのメッセージを出力します。スクリプト側のディスパッチは正しいステータスコードを生成しますが、ホスト側の適用は先送りされています。演習3.7を参照してください。
  • 飢餓の調整が行き過ぎました。 半減された空腹の周期と死体のドロップの組み合わせにより、飢餓は事実上不可能になっています。演習5.1を参照してください。
  • 図鑑と装備のスクリプトはF5ホットリロードのパス上にありません。 どちらも起動時に一度読み込まれます。これらのスクリプトを編集するモッダーは再起動しなければなりません。このパターンはリロードを許容しますが、配線はまだ整っていません。
  • 武器と防具の名前はホスト側の定数 WEAPON_NAMESARMOR_NAMES に存在します。 モンスター名はすでに図鑑スクリプトに移されています。装備スクリプトについての同等の移行は機械的であり、演習4.3として登録されています。
  • プレースホルダのポーション効果(Speed、Levitation、See Invisible)にはスクリプト側のハンドラがありますが、ホスト側の応答がありません。 ステータスコードは伝播しますが、ホストはそれらを汎用的なメッセージを伴う何もしない操作として扱います。

これらのいずれも通常のプレイを妨げません。出荷される構成で、ゲームは第1フロアから第100フロアの出口まで到達可能です。

ビルドと実行

cargo run --release --example rogue --features sdl3-example

この例には sdl3-example Cargoフィーチャが必要であり、これはウィンドウとイベントループを支える Simple DirectMedia Layer 3 の依存関係を引き込みます。アイテムメッセージシステムが使用する静的な文字列リテラルは V0.2.0 では無条件です。廃止された V0.1.x の text cargoフィーチャはもはや存在しません。

ホストは64×40タイルのグリッドウィンドウを開きます。2行のヘッドアップディスプレイがグリッドの上に、メッセージ行が下に配置されます。各表示タイルは16ピクセル四方なので、ウィンドウは1024×712ピクセルとなり、これはマップ領域の16対10のアスペクト比に一致し、標準的なノートパソコンのディスプレイに無理なく収まります。手続き的なスプライトアートは24ピクセルで作成され、コピー時に16ピクセルに縮小されるため、より大きい作成サイズが元のスプライトの詳細を保ちます。

操作

キーアクション
矢印キー、hjkl上下左右の移動、1回の押下につき1タイル。
yubn斜め移動。
ピリオド、スペースその場で1ターン待機。
Q持っている薬を飲む。薬の効果は即座に解決され、スロットは空になります。
R持っているスクロールを読む。スクロールの効果は即座に解決され、スロットは空になります。
F5ディスクからすべてのKeleusmaスクリプトをホットリロード。ホットリロードを参照。
エスケープ例を終了。

インベントリ管理サーフェスはありません。食料は接触で食べます。金の山は接触でスコアに加算されます。武器と防具はアップグレードが踏み超えられたとき自動装備されます。非アップグレードの武器と防具は、セルをブロックして残されるのではなく接触で破壊されます。薬とスクロールは対応するスロットが空のとき自動拾得されます。スロットがいっぱいの場合、メッセージが地面のアイテムを変装した名前で、持っているアイテムを変装した名前で説明します。

ヘッドアップディスプレイは2行に分かれます。トップ行はヒットポイントピップストリップだけです。プレイヤーのヒットポイント上限は階段の降下ごとに3伸びるので、深い階ではストリップがウィンドウのほとんどを横断できます。それに行全体を与えると、以前の単一行設計が高い階数で生んでいたレイアウトの衝突がなくなります。2行目は左から右に、装備武器のアイコンとティアピップストリップ、装備防具のアイコンとティアピップストリップ、中央のシアン深さティック、現在の階数とプレイヤーの金スコアカウンターを表示するテキスト読み出し、ランごとの外観色で着色された持っている薬とスクロールのアイコン、右の琥珀色の空腹ピップを運びます。ティアピップストリップは0〜19スケールで装備レベルごとに1ピップを埋めます。ビットマップフォントテキストレンダリングはexamples/rogue/text.rsを通じて例にローカルです。

死または勝利時、ゲームはゲームプレイ入力をブロックし、結果のタイトル、最終階、金、ターン数を表示する中央のパネルをオーバーレイします。パネル表示中のキー押下で例を終了します。

ゲームプレイのルール

戦闘

  • モンスターに歩み寄ると近接攻撃が始まります。
  • ヒットロールは1d20 + attacker_skill >= 10 + defender_evasionです。
  • ナチュラル1は自動的なミス。ナチュラル20は自動的なヒット。クリティカルヒットは防具を引く前に攻撃者のダメージ入力を倍にします。
  • ダメージはattacker_damage - defender_armorで、1で底打ちです。
  • プレイヤーの防御回避はプレイヤーの現在のレベルです。プレイヤーの防御armorは装備armorの防御値です。
  • Fastアーキタイプのモンスターはターンごとに2回行動します。

ヒットポイント、空腹、回復

  • プレイヤーは12/12ヒットポイントで始まります。
  • 空腹は100で始まり、2ターンごとに1ティックダウンします。食料は40空腹を回復します。空腹0で、プレイヤーは飢餓から1ターンごとに1ヒットポイントを失います。
  • プレイヤーは空腹が正で現在のヒットポイントが最大より低いとき、10ターンごとに1ヒットポイントを回復します。

レベルアップ

  • プレイヤーは新しい階に階段を降りるとレベルを得ます。最大ヒットポイントが3増えます。スキルが1増えます。現在のヒットポイントは最大と同じデルタを得るので、新しく追加されたスロットは満杯で入りますが、既存のダメージは持続します。

階とbestiaryの分布

  • 100階。下への階段がより深くにつながります。100階は下への階段ではなく出口タイルを持ちます。
  • 各階には好みのモンスター種があります。階で生成されるモンスターの半分は好みの種です。残りの半分は、プレイヤーがすでに以前の階で遭遇したモンスター種のプールから等しい重みでサンプルされます。
  • 1階は1階の好みのみを持ちます。2階以降は累積プールから抽選します。

アイテム

  • 持っている薬スロットと持っているスクロールスロットはそれぞれ1つのアイテムを運びます。持っているアイテムを飲むまたは読むと、効果を解決してスロットを空にします。
  • アイテム効果スクリプトが効果を決めます。ホストはスクリプトの返したデルタとステータスアクションを適用します。
  • 各薬とスクロールは安定したランごとの隠された identity を持ちます。薬瓶の色やスクロールの模擬タイトルが、プレイヤーがその種のアイテムを最初に使うまでメッセージに表示されます。最初の使用のあと、その種への将来のすべてのメッセージはその真の名前で参照します。
  • 倒されたモンスターは倒れたセルに死体を残す確率があります。ドロップ確率と死体の空腹とヒットポイントへの効果は bestiary エントリの shape から来ます。大きな生き物はより多くの肉を産みます。蛇、昆虫、魔導士の shape は毒で、食べると小さなヒットポイントペナルティを与えます。骸骨、幽霊、スライムは何も残しません。死体に足を踏み入れると、同じターンで自動拾得して食べます。毒の死体を避けたいプレイヤーは、攻撃する生き物を廊下ではなく開けた部屋で殺すべきです。そうすれば死体を回避できます。

勝利と死

  • 階段の下タイルに足を踏み入れると自動的に降下します。100階の出口タイルに足を踏み入れるとゲームに勝ちます。
  • 0ヒットポイントに達するとゲームが終わります。

プレイヤーが移動ではなくテレポートを通じて階段に到着した場合、テレポートが移動リゾルバを通らないので、自動降下は発火しません。階段から離れてまた乗ると、降下が通常通り起動します。

ホストとスクリプトのアーキテクチャ

ホストがすべての可変ゲーム状態を所有します。マップ、プレイヤー、モンスターテーブル、アイテムテーブル、視野バッファ、乱数生成器の状態はすべてRustに住みます。スクリプトは関数パラメータを通じて入力を読み、戻り値を通じて出力を書きます。数少ない変更を行うネイティブはダンジョン生成器のサーフェスに限定されています。

+-------------------+         +-------------------+
|  examples/rogue/  |  Arc<>  |   World state     |
|     host code     | <-----> |  (map, player,    |
|  (Rust + SDL3)    |         |   monsters, ...)  |
+-------------------+         +-------------------+
        |
        | per-virtual-machine `register_native_closure` and `vm.call(...)`
        v
+------------------------------------------+
|  Twenty-four Keleusma virtual machines   |
|  - rogue_game.kel          (loop main)   |
|  - rogue_dungen.kel        (one-shot)    |
|  - rogue_player_ai.kel     (pure fn)     |
|  - rogue_combat.kel        (pure fn)     |
|  - rogue_book_keeping.kel  (pure fn)     |
|  - rogue_pickup.kel        (pure fn)     |
|  - rogue_move_resolve.kel  (pure fn)     |
|  - rogue_ai_idle.kel       (pure fn)     |
|  - rogue_ai_chaser.kel     (pure fn)     |
|  - rogue_ai_wander.kel     (uses rng)    |
|  - rogue_ai_sleeper.kel    (pure fn)     |
|  - rogue_ai_ranged.kel     (pure fn)     |
|  - rogue_ai_fast.kel       (pure fn)     |
|  - rogue_ai_smart.kel      (pure fn)     |
|  - rogue_ai_boss.kel       (loop main)   |
|  - rogue_ai_tracker.kel    (loop main)   |
|  - rogue_ai_hunter.kel     (loop main)   |
|  - rogue_item_potion.kel   (pure fn)     |
|  - rogue_item_scroll.kel   (pure fn)     |
|  - rogue_descend.kel       (pure fn)     |
|  - rogue_consume.kel       (uses natives)|
|  - rogue_scroll_apply.kel  (uses natives)|
|  - rogue_bestiary.kel      (startup load)|
|  - rogue_gear.kel          (startup load)|
+------------------------------------------+

ホストの役割は意図的に狭いです。Simple DirectMedia Layer 3イベントポンプを通じてユーザー入力をキャプチャし、Simple DirectMedia Layer 3レンダリングを通じて世界の状態を表示し、仮想マシンを管理します(ソースをコンパイル、プールを構築、ネイティブを登録、ディスパッチを駆動)。ゲームプレイのルールはスクリプトに住みます。戦闘の数学、プレイヤー入力の解釈、モンスターの振る舞い、アイテム効果、ダンジョン生成はすべてスクリプト側です。ホストのネイティブはほとんどがプリミティブアクセサと、スクリプトが検証器の境界内で複製できないオーケストレーション接着剤です。

各仮想マシンには独自のアリーナがあります。アリーナはプログラムの存続期間中生きます。スクリプトは呼び出しごとにvm.call(...)を呼びます。マシンは呼び出しの間にリセットされます。

ホストソースのモジュール。

モジュール責任
main.rsエントリポイント、SDL3セットアップ、スクリプトコンパイル、イベントループ。
world.rsマップ、プレイヤー、モンスター、アイテム、メッセージログ、視野バッファ。
bestiary.rs100種のモンスター、易しいから難しいに整理。
items.rs武器、防具、薬、スクロールのテーブル。ランごとにシャッフルされた外観。
tiles.rsSDL3テクスチャ上のプリミティブから構築されたプロシージャルスプライトアトラス。
render.rsヘッドアップディスプレイ、視野シェーディングのあるタイルグリッド、モンスターとアイテムの描画、メッセージバー。
input.rsキーボードからコマンドへの翻訳。
fov.rs8つの八分円での再帰的シャドウキャスティング。
combat.rsd20ロールをサンプルし、戦闘仮想マシンをディスパッチし、ダメージを世界に適用する薄いラッパー。
ai.rsAI、アイテム効果、プレイヤー、戦闘仮想マシンのプール。
natives.rsスクリプトが呼ぶホストネイティブ。ゲームティックネイティブ(host::run_player_turnhost::monster_counthost::run_monster_aihost::tick_book_keeping)とダンジョン生成器ネイティブを含む。

ゲームティックスクリプトを読む

rogue_game.kelはホストがプレイヤー入力ごとに1度再開するloop mainスクリプトです。各ターン本体はプレイヤーのコマンドを適用し、階のすべてのモンスターを反復し、各モンスターのAIスクリプトをディスパッチし、ターン終わりの帳簿をティックします。スクリプトはターンごとに1つの結果コードをyieldし、ホストがそれを読んでプレイを続けるか、次の階を生成するか、勝利でランを終わらせるか、プレイヤーの死でランを終わらせるかを決めます。

loop main(cmd: Word) -> Word {
    let player_outcome = host::run_player_turn(cmd);
    let outcome = if player_outcome == 0 {
        let count = host::monster_count();
        for i in 0..24 {
            if i < count {
                host::run_monster_ai(i);
            };
        }
        host::tick_book_keeping()
    } else {
        player_outcome
    };
    let _ = yield outcome;
    0
}

これは例の中で2つのパターンを同時に最も直接実演するものです。

1つ目、loop mainと持続状態。スクリプトはホスト呼び出しごとに本体が再実行されるコルーチンです。データセグメントはスクリプトが呼び出し間で覚えていたい任意の状態を運びます。ボスAIスクリプトはターンカウンターを通じて非自明な状態を実演します。ゲームティックスクリプトはデータセグメントを空のままにします。各ターンはネイティブを通じて照会された世界の状態から新たに計算されるからです。

2つ目、each monster forパターン。スクリプトは固定境界forループですべてのモンスタースロットを反復し、現在のモンスター数を超える反復をスキップします。本体の内側で、host::run_monster_ai(i)がモンスターごとの作業を行います。ネイティブが重い作業を行います。モンスターのアーキタイプを調べ、AIプールをロックし、モンスターの現在の位置とプレイヤーの位置とline-of-sightフラグで一致する仮想マシンをディスパッチし、プールロックを解放し、返されたアクションを世界に対して適用します。スクリプトは反復をオーケストレーションするだけです。

スクリプトが消費する4つのネイティブ。

ネイティブ効果
host::run_player_turn(cmd)プレイヤーAIスクリプトをプレイヤーの現在位置と提供されたキー押下でディスパッチし、返されたアクションをモンスターアクションを扱うのと同じアクターごとリゾルバを通じて経路指定。0を返してターンを続行、1で階段が降りられた、2で100階の出口に到達、3でプレイヤーが死んだ。
host::monster_count()階の現在のモンスター数。
host::run_monster_ai(idx)モンスターidxのAIをディスパッチして、返されたアクションを適用。Fastアーキタイプの2アクションターンを内部で扱う。
host::tick_book_keeping()2ターンごとに空腹を1進め、空腹なら飢餓ダメージを適用し、条件が許せば1ヒットポイントを回復し、視野を再計算。生きていれば0、空腹で死んだら3を返す。

スクリプトとホストが合意するコマンドコード。

コード意味
0待つ
1〜8北、南、西、東、北西、北東、南西、南東に移動
9階段を降りるか出口に踏み込む
10持っている薬を飲む
11持っているスクロールを読む

固定ループ境界は24です。検証器は境界がリテラルなのでループを受け入れます。動的なモンスター数は本体内のif i < countガードによって強制されます。

ダンジョン生成器を読む

rogue_dungen.kelは階の降下ごとに1度呼ばれる1ショットfn main(floor: Word) -> Wordです。スクリプトはホストネイティブを通じてroomsと corridorsのマップをレイアウトします。

高レベルの形。

  1. host::clear_floorを呼んでマップとエンティティリストをリセット。
  2. ランダムな位置に8つの長方形部屋を置く。部屋の寸法は軸ごとに4〜9タイル。
  3. 連続する部屋を配置順に、中心の間のL字型回廊で接続する。7つの回廊のあと、すべての部屋は部屋0から到達可能。
  4. プレイヤーを部屋0の中心に置く。
  5. 階段の下を部屋7の中心に置く。100階では出口タイルを代わりに置く。
  6. 階の分布に従ってモンスターを生成する。半分は階の好み種で、残りの半分は前の階のプールから抽選される。
  7. アイテムを生成する。食料3〜5、薬2〜4、スクロール2〜4、武器と防具のアップグレード0〜1、金の山4〜7。

連鎖は正しさで安全です。carve_roomは内部を floor として書くだけで、部屋のアウトラインのためにhost::clear_floorが残した固体の壁に依存しています。2つの重なる部屋は床を 1 つの連結領域にマージし、重なるセルに対して争うのではありません。早期のバージョンのカーバは内部を carve する前に部屋の長方形全体に壁を書いていて、それは重なる部屋を分割して、chain corridor が突破できない小さな到達不能ポケットを生んでいました。その破壊的な壁充填を取り除くと、1 行のコードのコストで連結性が復元されました。

検証器駆動のイディオム。スクリプト内のすべてのループは固定上限と条件付き本体を使うので、構造検証器が反復境界を受け入れます。スクリプトが動的なカウントを欲しがる場合、ループは可能な最大カウントまで走り、本体はif i < countでガードされます。部屋ストレージはデータセグメントで宣言された固定サイズ配列を使います。検証器が動的成長を却下するからです。

スクリプトが消費するホストネイティブ(一部抜粋)。

ネイティブ効果
host::clear_floor()すべてのマップセルを wall にリセット、すべてのモンスターとアイテムを落とす。
host::map_set(x, y, tile)(x, y) のタイル識別子を設定。
host::map_get(x, y)(x, y) のタイル識別子を読み取ります。
host::map_w()host::map_h()マップの寸法。
host::place_player(x, y)プレイヤーを配置します。
host::place_stairs(x, y)host::place_exit(x, y)下り階段または出口。
host::spawn_monster(kind, x, y)図鑑からスポーンします。
host::spawn_item(kind, subtype, x, y)アイテムテーブルにスポーンします。
host::rng_range(lo, hi)[lo, hi) の範囲のランダムな整数。
host::floor()現在のフロア番号。

タイル識別子は安定しています。0が床、1が壁、2が閉じたドア、3が開いたドア、4が下り階段、5が出口です。スクリプトはこれらを整数リテラルとして使います。

ホットリロード

F5 を押すと、すべての Keleusma スクリプトをディスクから再読み込みし、それぞれを再コンパイルし、実行中の仮想マシンをアトミックに置き換えます。ワールドの状態には触れません。プレイヤーは現在のヒットポイント、空腹度、装備、フロアを保ちます。次のモンスターのターンは新しく再読み込みされた人工知能スクリプトに対してディスパッチされ、次の階段の降下は新しく再読み込みされたダンジョンジェネレータを呼び出します。

リロードは、concat!(env!("CARGO_MANIFEST_DIR"), "/examples/scripts/rogue") を通じてコンパイル時に記録されたディレクトリから読み込みます。初回のスクリプト読み込みは include_str! を使うため、この例はファイルシステムへのアクセスなしで動作します。ホットリロードには、記録されたパスにスクリプトファイルが存在することが必要です。

リロードはアトミックです。すべてのスクリプトが読み込まれ、その後すべてのスクリプトがコンパイルされます。いずれかのソースの読み込みまたはコンパイルが失敗した場合、どの仮想マシンにも触れず、メッセージログが問題のあるスクリプト名とともに失敗を記録します。すべてのソースがコンパイルされれば、すべての仮想マシンが一度にスワップされます。

これは Keleusma 駆動のモッディングワークフローにおける主要なメカニズムです。作者は別のウィンドウでスクリプトを編集し、実行中のゲームで F5 を押し、新しい挙動を即座に観察します。一般的なワークフローの例を示します。

  • rogue_ai_chaser.kel でモンスターの追跡挙動を調整し、ファイルを保存し、F5 を押して、現在画面上にいるモンスターに対して変更を検証します。
  • rogue_item_potion.kel を編集して新しいポーション効果を追加し、対応するポーションを飲んで新しい分岐をテストします。
  • rogue_dungen.kel でダンジョン生成のパラメータを調整し、階段を降りて新しいルールで次のフロアを生成します。

プレイヤーAIスクリプトを読む

この例は、プレイヤーを独自の人工知能スクリプトを持つアクターとして扱います。rogue_player_ai.kel はモンスターのアーキタイプと形が対称的です。ホストは、すべてのモンスターをディスパッチするのと同じアクターごとのパターンを通じて、ターンごとに一度これをディスパッチします。唯一の違いは意図の出所です。モンスターの意図はアーキタイプごとのロジックに由来します。プレイヤーの意図はキーボードに由来し、小さな整数として符号化されます。

fn main(mx: Word, my: Word, cmd: Word) -> (Word, Word, Word) {
    // cmd 0..=8: wait or move (eight directions)
    // cmd 9: descend stairs
    // cmd 10: quaff held potion
    // cmd 11: read held scroll
    // Returns (action, tx, ty) in the same shape monster archetypes use.
}

ホストの host::run_player_turn(cmd) ネイティブはこのスクリプトをディスパッチし、返されたアクションをモンスターのアクションを処理するのと同じリゾルバに通します。移動と近接攻撃は同じ MoveOrMelee のパスを流れます。プレイヤー専用のアクション(降下、飲む、読む)は、モンスターのアーキタイプが決して発行しない追加のアクションコードです。

この対称性の利点は機械的というよりは概念的です。モンスターのターンがどのように動作するかを理解した読者は、プレイヤーのターンがどのように動作するかを自動的に理解します。

戦闘スクリプトを読む

rogue_combat.kel は命中とダメージのルールを保持します。ホストは乱数生成器から d20 のロールをサンプリングし、攻撃者の技能、攻撃者のダメージ、防御者の回避、防御者の防具、そしてロールを伴って戦闘仮想マシンをディスパッチします。スクリプトは (hit_kind, damage) を返します。ここで hit_kind は、ミスなら0、通常の命中なら1、クリティカルヒットなら2です。

fn main(
    attacker_skill: Word,
    attacker_damage: Word,
    defender_evasion: Word,
    defender_armor: Word,
    roll: Word,
) -> (Word, Word)

戦闘計算は現在、読者がホストに触れることなく再調整できる単一の Keleusma ファイルです。戦闘のRust側は examples/rogue/combat.rs の薄いラッパであり、ロールをサンプリングし、スクリプトをディスパッチし、返されたダメージをワールドに適用し、メッセージを掲示します。

人工知能アーキタイプを読む

図鑑の各モンスター種は人工知能アーキタイプを指名します。ホストはアーキタイプごとに1つの Keleusma 仮想マシンをインスタンス化します。モンスターのターンごとに、ホストはモンスターの位置、プレイヤーの位置、そして視線フラグを伴ってアーキタイプの仮想マシンを呼び出し、アクションタプルを読み返します。

呼び出し規約。

fn main(mx: Word, my: Word, px: Word, py: Word, sees_player: Word)
    -> (Word, Word, Word)

戻り値のタプル (action, tx, ty)

アクション意味
0待つ。(tx, ty) は無視。
1セル(tx, ty)に移動または近接攻撃。ホストは歩行可能なら移動を適用、セルがプレイヤーのものなら近接攻撃を解決、隣接でないターゲットを却下。
2セル(tx, ty)で遠距離攻撃。ホストは(tx, ty)がプレイヤーのセルである場合のみ攻撃を適用。

例で出荷される8つのアーキタイプ。

スクリプト振る舞い
rogue_ai_idle.kel待つ。固定生物が使う。
rogue_ai_chaser.kelプレイヤーが見えるとき貪欲な1ステップ追跡。
rogue_ai_wander.kelプレイヤーが見えないときランダムなカーディナルステップ。見えるとき貪欲な追跡。
rogue_ai_sleeper.kelプレイヤーが視線に入るまで待ち、それから追跡。
rogue_ai_ranged.kel隣接で後退。視界内で近接外のとき遠距離攻撃。
rogue_ai_fast.kelchaserのように振る舞う。ホストがターンごとにスクリプトを2回呼び出す。
rogue_ai_smart.kelプレイヤーが見えるとき、優勢な軸でのヒューリスティックステップ。
rogue_ai_boss.kel遠距離と追跡を交互する4ターン攻撃パターン。データセグメントのターンカウンターを持つloop mainとして実装され、phaseが呼び出し間で持続。現在は100階のBalrog Lordにのみ束縛。ボスのloop main形を参照。
rogue_ai_tracker.kelデータセグメントでプレイヤーの最後に見られた位置を覚える。プレイヤーが見えるとき直接追跡、視界外のとき覚えているセルに向かって移動。3スロットデータセグメントを持つloop mainとして実装。レイス、スペクター、バンパイア・スポーン、マインドフレイヤー、ボーンデビルに束縛。

wanderスクリプトはランダム方向のためにhost::rng_rangeを使います。ボス以外のすべてのアーキタイプは入力の純粋関数です。

アーキタイプは意図的に最小で、パターンが読みやすいです。より精巧な振る舞いを欲しがる読者は新しいスクリプトを書いて、それをAiKind列挙を通じて bestiary エントリに割り当てます。下の演習セクションでこのケースを通します。

ボスのloop main形

Balrog LordのAIは例のloop mainの実演です。他のすべてのアーキタイプが1ショットfn mainであるのに対し、ボススクリプトはターンカウンターを持つデータセグメントを宣言し、ホスト呼び出しごとに1つのアクションをyieldします。

data state {
    turn: Word,
}

loop main(input: (Word, Word, Word, Word, Word)) -> (Word, Word, Word) {
    // 本体はアクションを計算する。
    // ...
    state.turn = state.turn + 1;
    let _ = yield action;
    (0, 0, 0)
}

5タプル入力は、他のアーキタイプが別々の引数として受け取るのと同じ5フィールドをパックします。タプルへのパックは必要です。vm.resumeがちょうど1つのValueを受け取るからです。本体はstate.turn % 4に基づいて標準の遠距離-そして-追跡判断を実行し、カウンターを増やし、アクションをyieldします。

ホストはボスを他のアーキタイプと違ってディスパッチします。最初のターンはvm.call(...)を呼びます。それ以降のすべてのターンはvm.resume(...)を呼びます。各callまたはresumeは本体を現在の位置から1度走らせます。yieldのあとの末尾の(0, 0, 0)式に到達すると、仮想マシンはVmState::Resetを発します。ホストはResetを歩き越して、再びvm.resumeを呼んで本体を次の反復まで駆動します。結果はホスト駆動の論理的ターンごとに1つのyieldされたアクションです。

データセグメントは呼び出し間で持続するのでstate.turnは単調に増加します。ホットリロードはデータセグメントをリセットするので、スクリプトが置き換えられるたびに4ターンパターンがphase 0から再起動します。

将来のマルチターン振る舞いを持つアーキタイプは同じパターンに従います。ウェイポイント間をパトロールするガーディアン、定期的にミニオンを生成するネクロマンサー、ダメージを取るごとに新しい頭を生やすヒドラは、すべて状態を持つloop mainに自然に適合します。

ゲームオーバー後の loop main スクリプトの再起動

ターンごとのケイデンスを駆動するのと同じReset意味論が、例にゲームオーバーでのクリーンなリプレイ経路も与えます。ホストはプレイヤーがRを押して新しいランを始めたとき、ボス、トラッカー、ハンター仮想マシンを再構築しません。それらのデータセグメントをゼロ化して、仮想マシンを現在のyield点に駐車したままにします。次のディスパッチで本体が再開し、新たにリセットされた状態を観測し、新しいランの最初のアクションを元のランの最初のアクションと形が同じものを生みます。末尾のタプルから本体の先頭への巻き戻しは、すでに 1 つのターンを次から分離している既存の Reset 境界を通して起こります。

データセグメントのリセットが必要な唯一のステップです。本体は反復ごとに世界をネイティブを通じて再読み込みするからです。持続状態は2つの場所だけに住みます。bestiaryエントリはホスト所有でランの間同じです。データセグメントは仮想マシンごとで、リセットがクリアするものです。本体のローカル変数はyieldを越えて持続しません。スタックフレームはスクリプト全体ではなく1つの本体実行の活性化レコードだからです。新鮮なWorldと新しいダンジョンと組み合わさって、リセットされた仮想マシンに対する次の呼び出しは、最初のランと等価のクリーンなリプレイを生みます。

実装は例のai.rsAiPool::reset_loop_main_dataです。ボス、トラッカー、ハンターのデータセグメントに 3 つのスロット ゼロ化ポインタを歩き、各々に対してゼロを書きます。データセグメントを運ばないモンスターごとのfn mainアーキタイプには等価のリセットは必要ありません。

アイテム効果スクリプトを読む

rogue_item_potion.kelrogue_item_scroll.kelは、プレイヤーが持っているアイテムを飲むか読むときホストが呼ぶ1ショットfn mainスクリプトです。スクリプトは効果識別子でディスパッチし、5要素タプル(hp_delta, max_hp_delta, skill_delta, status_code, status_arg)を返します。ホストはデルタをプレイヤーに適用し、ステータスアクションを実行します。

ステータスコード(抜粋)。

コードアクション
0なし。
1Magic mapping。階全体を探索済みとして明らかにする。
2Teleport。プレイヤーをランダムな歩行可能セルにワープ。
3Identify。すべての薬を識別済みとマーク。
4Enchant weapon。装備武器ティアを status_arg だけ進める。
5Enchant armor。装備armorティアを status_arg だけ進める。
6Light。プレイヤーの周りの小さな半径を探索済みとマーク。
7モンスターを検知します。フロアのモンスター数をメッセージログに掲示します。
8Sleep。プレースホルダ、効果なし。
9Confusion。プレースホルダ、効果なし。
10Remove curse。プレースホルダ、効果なし。
11Restoration。プレイヤーをヒットポイント満タンまで回復します。

効果ロジックをスクリプトに、ステータスアクションをホストに置くという分割は意図的です。スクリプトは効果が生み出すものを記述します。ホストはエンジン固有の変更を適用します。差分のみの挙動を持つ効果はスクリプトに留まります。視野バッファ、乱数生成器、または装備テーブルに触れる効果は、ステータスアクションの仕組みを通じて処理されます。

ステータスアクションの仕組み自体は2つ目のスクリプトで実行されます。rogue_scroll_apply.kel(status_code, status_arg) のペアを受け取り、8つのきめ細かいネイティブのうちの1つにディスパッチします。すなわち host::set_explored_allhost::set_explored_radiushost::teleport_player_randomhost::identify_all_potionshost::change_weapon_tierhost::change_armor_tierhost::sense_monstershost::scroll_placeholder です。各ネイティブはそのワールド変更を適用し、そのメッセージをプッシュします。この分割は、モッダーがどのステータスコードがどのネイティブをトリガーするかを変更したり、新しいステータスコードを追加したり、ステータスコードごとに複数のネイティブを組み合わせたりすることを、すべてスクリプトの編集で可能にすることを意味します。ホストのネイティブは小さく直交したまま保たれます。

図鑑スクリプトを読む

rogue_bestiary.kel はモンスターシステムのデータ側の半分です。ホストは起動時にモンスターIDごとに一度このスクリプトを実行し、スクリプトのデータセグメントから解決された値を読み取ります。結果は examples/rogue/bestiary.rsOnceLock の背後にキャッシュされるため、bestiary::kind(idx) を通じた実行時アクセスは Vec<MonsterKind> に対する単純な読み取りです。

データローダーパターン

図鑑スクリプトは、この例が Keleusma について示すイディオムの実践例です。このパターンはKeleusma クックブックのデータローダのレシピで詳細に記載されています。以下に短い要約を示します。クックブックは最小限の例、バリエーション、そしてこのパターンに手を伸ばすべきタイミングを扱っています。

このパターンは3つの技法を組み合わせます。それぞれは個別に知られています。この組み合わせは Keleusma の特定の制約(モジュールスコープの定数がないこと、固定サイズのデータセグメント、有界な実行)に特によく適合し、この組み合わせこそがここでイディオムたらしめているものです。

第一に、多頭関数ディスパッチが定数テーブルを符号化します。Keleusma にはレコードの配列のためのモジュールスコープの const はありませんが、検証器は整数パターンを伴う多頭関数定義を受理します。エントリごとに1つの頭を持ち、各本体がそのエントリのフィールドを代入することは、機能的に定数配列と等価です。この符号化は、あらゆる本体が直線的なコードであるため検証器に優しく、整数キーが密である場合、ディスパッチ自体はジャンプテーブルにコンパイルされます。

第二に、データセグメントがホストとスクリプトの入出力構造体として機能します。データセグメントは通常、スクリプトが loop main の再開をまたいで保存する状態に使われます。ここでは、ワンショットの純粋関数の出力フィールドを運びます。ホストは vm.call_with_shared を通じて共有バッファを貸し出し、入力(エントリのインデックス)を呼び出し引数として書き込み、呼び出しが返った後に vm.get_shared を通じてバッファから出力(エントリのフィールド)を読み取ります。共有バッファと get_shared/set_shared はホスト境界の一部であるため言語の変更は不要です。転用はスクリプト側にあります。

第三に、負のインデックス規約がテーブルサイズを発見しますfn main(-1) を呼び出すと、スクリプト内部でインデックスが MONSTER_COUNT - 1 に解決され、最後のエントリのフィールドをデータセグメントに書き込み、返ります。ホストは id スロットを読み取って1回の呼び出しでテーブルサイズを知ります。したがってホストは、並行するホスト側の定数からではなくスクリプトからキャッシュのサイズを決めることができ、あらゆる乖離をアサーションが捕捉します。

このパターンは「整数の小さな固定サイズ構造体、序数でインデックス付け」という形を共有する他のテーブルにもよく適します。図鑑の形状ごとの死体ステータスもこのパターンを使います。同じスクリプト内の fn corpse_fill(N) ディスパッチャは、fn fill(N) のステップがすでに書き込んだスロットから形状の序数を読み取り、12エントリのテーブルからさらに3つのスロットを埋めます。

実践例

データセグメントは出力列ごとに1つのフィールドを宣言します。fill(N) の頭が直接設定するフィールド(id、shape、3つのプライマリカラーチャンネル、3つのアクセントカラーチャンネル、5つの戦闘ステータス、aiアーキタイプ、初出フロア、スコア)。corpse_fill(shape) のステップが間接的に設定するフィールド(ドロップ確率、満腹度、ヒットポイント差分)。100個の多頭 fill(N) 関数がエントリごとの定数を書き込みます。12個の多頭 corpse_fill(N) 関数が形状ごとの死体ステータスを書き込みます。fn main(n) エントリポイントは負のインデックスを MONSTER_COUNT + n に解決し、fill(i) を呼び出し、それから corpse_fill(state.shape) に連鎖します。

data state {
    id: Word,
    shape: Word,
    primary_r: Word, primary_g: Word, primary_b: Word,
    accent_r: Word, accent_g: Word, accent_b: Word,
    max_hp: Word, skill: Word, evasion: Word, damage: Word, armor: Word,
    ai: Word,
    first_floor: Word,
    score: Word,
    corpse_drop_chance: Word,
    corpse_satiation: Word,
    corpse_hp_delta: Word,
}

fn main(n: Word) -> Word {
    let count = 100;
    let i = if n < 0 { count + n } else { n };
    fill(i);
    corpse_fill(state.shape);
    0
}

fn fill(0) -> Word {
    state.id = 0; state.shape = 0;
    state.primary_r = 120; state.primary_g = 90; state.primary_b = 60;
    state.accent_r = 60; state.accent_g = 40; state.accent_b = 30;
    state.max_hp = 3; state.skill = 0; state.evasion = 1;
    state.damage = 1; state.armor = 0;
    state.ai = 2; state.first_floor = 1; state.score = 1;
    0
}
// ninety-nine more fill heads
fn fill(_n: Word) -> Word { 0 }

fn corpse_fill(0) -> Word {
    state.corpse_drop_chance = 50; state.corpse_satiation = 8; state.corpse_hp_delta = 0;
    0
}  // Tiny
// eleven more corpse_fill heads
fn corpse_fill(_n: Word) -> Word { 0 }

モンスター名もスクリプトに存在します。3つ目の多頭ディスパッチャ fn name(N) -> Text は、各エントリの名前を静的文字列として返します。図鑑スクリプトの fn main は最後の式として name(i) を返すため、ホストはデータセグメントが数値フィールドを運ぶ一方で、名前を呼び出しの Finished(StaticStr(...)) ペイロードとして受け取ります。ホストは起動時に一度、返された文字列をリークして、キャッシュ用の &'static str を得ます。Keleusma のデータセグメントは現在ソースで文字列フィールドを受け付けませんが、関数の戻り値はきれいな代替であり、ホスト側の名前テーブルを持たないという結果を許容します。ホストの MONSTER_COUNT 定数はスクリプトの count リテラルを反映します。起動時のアサーションが両者の間のあらゆる乖離を捕捉します。

出荷されるスクリプトは、3つのディスパッチャを合わせておよそ380行になります(100個の fill 頭、12個の corpse_fill 頭、100個の name 頭)。以前のRustの MonsterKind 構造体リテラル形式は、エントリごとに14行に加えて並行する100行の名前配列、加えて3つの12腕の死体メソッドを要していました。図鑑の移行は、エントリごとの密度が最も重要だった箇所に集中した明確な正味の削減です。

消費と降下のスクリプトを読む

2つの短いスクリプトが繰り返し発生するワールド変更をラップするため、ホストの自動拾得ドライバと階段降下のパスを狭いまま保つことができます。

rogue_consume.kel は種類ごとの消費テーブルです。rogue_pickup.kelconsume を返した後、ホストはアイテムの種類とサブタイプを伴って rogue_consume.kel を呼び出します。スクリプトは7つのきめ細かいネイティブのうちの1つにディスパッチします。すなわち host::consume_foodhost::take_goldhost::equip_weaponhost::equip_armorhost::stash_potionhost::stash_scrollhost::eat_corpse です。各ネイティブは対応するワールド変更を適用し、対応するメッセージをプッシュします。新しいアイテムの種類を追加するには、1つの新しいネイティブとスクリプトの1つの新しい腕が必要です。

rogue_descend.kel はフロアごとのレベルアップ計算器です。ホストはプレイヤーの現在のレベル、ヒットポイントの上限、ヒットポイント、技能、そして現在のフロアをスナップショットします。スクリプトは降下後の5要素タプルを返します。出荷されるスクリプトは、ヒットポイントの上限に3を、現在のヒットポイントに3を加え、技能に1を加え、レベルを1増分します。モッダーは、ホストに触れることなく成長曲線を変更したり、増分をステータス間で分割したりできます。

読者のための演習

以下の演習は深さで段階分けされています。第一階層の演習は既存のスクリプトやテーブルの値を変更します。第二階層の演習は既存のディスパッチの形に合う新しいコンテンツを導入します。第三階層の演習はディスパッチの形自体を変更します。

各演習は、何を変更するか、どこで変更するか、そして検証の提案を挙げています。ほとんどはテストを書かずに答えられます。徹底したい読者は、追加内容をカバーするように tests/rogue_scripts.rs を拡張できます。

第一階層:パラメータの調整

演習 1.1. プレイヤーの初期ヒットポイントを12から20に引き上げてください。変更箇所を examples/rogue/world.rs の中に見つけ、新しい実行を開始してヘッドアップディスプレイに20個のピップが描画されることを確認してください。検証すべき仮説。ヘッドアップディスプレイの行はウィンドウの全幅にわたるため、ヒットポイントゲージのレイアウトは依然として収まります。

演習 1.2. 空腹の周期を調整してください。出荷時の構成では空腹が2ターンごとに1ずつ減少します。examples/scripts/rogue/rogue_book_keeping.kel の中でティック箇所を見つけ、より速い周期(毎ターン)またはより遅い周期(3ターンごと)を試してください。フロア数が10、20、50の場合の実行時間への影響を観察してください。仮説。速い周期は死体を得るために積極的なモンスター討伐を強います。遅い周期は空腹を無意味にします。いつ戦い、いつ逃げ、いつ毒のある死体を食べるかについて最も興味深い意思決定を生む周期を選んでください。

演習 1.3. ダメージ42の「soulrender」と呼ばれる9番目の武器ティアを追加してください。武器テーブルは examples/rogue/items.rs にあります。ダンジョン生成器のティアクランプ式が、最深フロアに依然として正しく配置することを確認してください。

演習 1.4. Sewer Rat の初出時のヒットポイントを3ではなく5から始まるようにしてください。モンスター図鑑テーブルは examples/rogue/bestiary.rs にあります。新しいゲームを実行し、プレイヤーがラットを倒すのに何回の攻撃を受けるかを観察してください。

演習 1.5. ダンジョン生成器の部屋数を8から12に変更してください。rogue_dungen.kel の部屋ストレージは固定サイズ配列を使用しています。配列宣言は for i in 0..8 ループ内のリテラルと歩調を合わせて変更しなければなりません。推論。配列の境界とループのリテラルが食い違う場合、検証器はスクリプトを拒否します。

ティア2: 形の定まった追加

演習 2.1. Coward と呼ばれる新しい人工知能アーキタイプを追加してください。これはヒットポイントが半分を上回るときにプレイヤーを追跡し、下回るときに逃走します。このスクリプトは他のすべてのアーキタイプと同じ5つの入力を取りますが、monster_hp > monster_max_hp / 2 のときにプレイヤーへ向かう移動アクションを返し、それ以外の場合には離れる移動アクションを返します。スクリプトは現在モンスターのヒットポイントを受け取らないため、この演習には2つの部分があります。第一に、ヒットポイントを追加のパラメータとして受け入れるよう呼び出し規約を拡張してください。第二に、スクリプトを書いてください。調整が必要なホストの呼び出し箇所は examples/rogue/natives.rs::run_one_monster_turnexamples/rogue/ai.rs::AiPool::dispatch にあります。推論。パラメータの追加はすべてのアーキタイプスクリプトに影響するため、呼び出し規約の変更のほうが大きな取り組みとなります。

演習 2.2. プレイヤーの回避を1増加させる「agility」と呼ばれる新しいポーション効果を追加してください。ステータスコードシステムは、コード12を導入してホスト側でそれを適用すれば、すでにこれをサポートしています。スクリプトの変更は rogue_item_potion.kel にあります。ホストの変更は examples/rogue/natives.rs::apply_potion_status にあります。プレイヤーには専用の回避ステータスがありません。examples/rogue/world.rs::Player に1つ追加し、examples/scripts/rogue/rogue_combat.kel にそれを反映させる必要があります。

演習 2.3. ダンジョン生成器にドアを追加してください。廊下が部屋の壁を横切るとき、交差点のタイルは0(床)ではなくタイル識別子2(閉じたドア)にすべきです。rogue_dungen.kel の廊下掘削ヘルパーは現在、壁の上に無条件で床を書き込みます。この演習は、壁の横断ケースを検出して代わりにドアを書き込むことです。ホストはすでに開いたドアと閉じたドアの両方のスプライトをレンダリングします。

演習 2.4. 次の20ターンの間、フロア上のすべてのモンスターを明らかにする「rangedeyes」ステータスコードを追加してください。detect-monsters の巻物は現在、フロアのモンスター数に関する単一のメッセージを返します。本物の全モンスター表示効果は、ホスト側のタイマーを設定し、タイマーがカウントダウンする間、視野に関係なくすべてのモンスターをレンダリングします。ホストの作業は、タイマーフィールドについては examples/rogue/world.rs に、条件付きモンスター描画については examples/rogue/render.rs にあります。

演習 2.5. 標準的な部屋と廊下のレイアウトを、中央に宝を配置し対称的に配置したプレハブ部屋で時折置き換えるヴォールト生成器を追加してください。ヴォールトは10より深いフロアで5分の1の確率で出現すべきです。この演習は、rogue_dungen.kelvault_floor(floor) 分岐を書き、それをフロアとランダムな抽選でゲートすることです。

演習 2.6. バックグラウンドミュージックを追加してください。examples/piano_roll.rs の例は、Keleusma の loop main スコアシーケンサに対する完全な SDL3 オーディオパイプラインをすでに実演しています。その設計は、SDL3 オーディオデバイスを開き、8ボイスの配列を SDL3 オーディオコールバックと Arc<Mutex<_>> の下で共有し、オーディオスレッド上で矩形波と三角波のサンプルをレンダリングし、16分音符のティックで Keleusma スクリプトからノートトリガーを駆動します。この演習は、そのパターンをローグライクのホストに適応させることです。ソースコードのフックは、SDL3 初期化箇所における examples/rogue/main.rsadd audio processing here コメントです。スクリプトの作業は、ティックごとにノートトリガーを yield する新しい rogue_music.kelloop main スクリプトです。ホストの作業は、オーディオデバイスを開き、オーディオスレッドを起動し、選択した周期(ターンごと、秒ごと、または新しいフロアのスティングのためのフロア降下ごと)で rogue_music.kel を再開することです。対処すべき懸念。オーディオスレッドとゲームティックは独立して動作します。スコアは、ゲームスレッドをブロックすることなくオーディオスレッドによってサンプリングされるべきです。ピアノロールのミューテックス保護されたボイス配列はこれに対応します。仮説。フロアの深さとともに変化する遅いベースラインは、このターン制の例が現在依存しているプレイヤー入力間の静寂から気を散らすことなく雰囲気を運ぶでしょう。

ティア3: ディスパッチとアーキテクチャの変更

演習 3.1. シャドウキャスティングの視野アルゴリズムを、プレイヤーから半径内のすべてのセルへ光線をたどる対称的なレイキャスティング実装で置き換えてください。同じダンジョン上で2つのアルゴリズム間の可視範囲を比較してください。アルゴリズムの置換は examples/rogue/fov.rs にあります。トレードオフに関する推論。シャドウキャスティングはより高速で、薄い壁をきれいに扱います。レイキャスティングは読みやすく、ホストがすでに個別に実行しているモンスターがプレイヤーを見るチェックのために対称にするのが容易です。

演習 3.2. セーブとロードの面を追加してください。ワールドレコードは、1つ書けば Serialize 可能になります。この演習は、専用のキーバインドでワールド全体をファイルにダンプし、次回の実行時にそれをロードし直すことです。Keleusma スクリプトを変更する必要はありません。ホストの作業は examples/rogue/main.rsexamples/rogue/world.rs にあります。未対処の懸念。アリーナの状態はワールドの一部ではありません。データセグメントに呼び出しごとの状態を保持するスクリプトは、リロード時にそれを失うでしょう。この例に同梱されているダンジョン生成、人工知能、アイテムスクリプトは呼び出しをまたいでステートレスであるため、現在の顔ぶれではこの懸念は理論上のものです。

演習 3.3. ホスト駆動のモンスターターンループを、ターンごとのドライバスクリプトで置き換えてください。現在のアーキテクチャでは、ホストがモンスターを反復処理してそのアーキタイプをディスパッチします。代替案は、ホストがターンごとに1回ワールド状態を入力として呼び出す rogue_game.kel スクリプトであり、ホストはスクリプトが発行したコマンドをコミットします。この演習はマニュアル中で最大のものです。なぜなら、ワールドに触れるすべてのホストネイティブが新しいスクリプトからの呼び出しを受け入れる必要があるからです。見返りは、ターンループのシーケンス全体がユーザー編集可能なコードになることです。

演習 3.4. 手続き的スプライトアトラスを、Portable Network Graphics ファイルから読み込んだスプライトシートで置き換えてください。ホストはすでに SDL3 サーフェスを通じてテクスチャを構築します。Portable Network Graphics ファイルを読み込んでタイルごとのサブテクスチャに切り分けることは、examples/rogue/tiles.rs における機械的な作業です。この演習の本当の課題は、スプライトシートを選ぶことです。モンスター図鑑には100のエントリがあります。

演習 3.5. プレイヤーの遠隔攻撃を追加してください。現在のルールでは、プレイヤーには近接攻撃のみが与えられています。遠隔攻撃の追加は、ワンドまたは弓のアイテムタイプ、照準攻撃のキーバインド、プレイヤーから対象セルへの視線判定、そして距離を考慮したダメージ計算式を意味します。ホストの作業は、新しいアクションコードを認識してそれをリゾルバに通すために examples/rogue/natives.rs にあります。スクリプトの作業は、遠隔アクションを発行するために examples/scripts/rogue/rogue_player_ai.kel に、そして距離減衰を適用するために examples/scripts/rogue/rogue_combat.kel にあります。仮説。モンスター図鑑の遠隔アーキタイプのモンスターは、プレイヤーがその攻撃範囲外から反撃できる場合、不釣り合いに簡単になります。したがってバランスを保つために、ダメージ計算式には距離減衰が必要かもしれません。

演習 3.6. 対称的なモンスターの視線ルールを、モンスターごとのシャドウキャストで置き換えてください。examples/rogue/natives.rs::monster_sees_player における現在の実装は、プレイヤーの視野ビットマップを真実として扱います。プレイヤーがモンスターを見ることができれば、モンスターはプレイヤーを見ることができます。これは構成上対称的ですが、モンスターの知覚をプレイヤーの視点に結びつけます。モンスターのセルから発する同じ8タイル半径の独立したモンスターごとのキャストは、柱のような壁の構成では異なる結果を生むでしょう。キャストを実装して比較してください。仮説。独立したキャストはより「公平」に感じられますが、より高コストです。現在の規模ではコストは無視できます。

演習 3.7. プレースホルダのポーションと巻物の効果を実装してください。ステータスコードのディスパッチ基盤はすでに整っています。残っている部分は次のとおりです。

  • Potion of Speed(効果7)。プレイヤーに追加のターンを与えます。プレイヤー状態に extra_turns カウンタを追加してください。各ティックで、正であればデクリメントし、yield する前にプレイヤーターンを実行します。仮説。プレイヤーは実質的に2回動きます。ポーションは消費されるため、コストは釣り合っています。
  • Potion of Levitation(効果8)。プレイヤー状態に levitating タイマーを追加してください。正である間、罠を無視し(将来のリビジョンで罠が実装されたとき)、落とし穴の部屋のユースケースのためにセルを歩行可能として扱います。現在罠は存在しないため、この効果は現在のコンテンツに対しては何もしませんが、メッセージ上では正しく読めます。
  • Potion of See Invisible(効果9)。モンスターの種類とインスタンスに invisible フラグを追加してください。See Invisible が有効な間、レンダラは不可視のモンスターを可視であるかのように表示します。モンスター図鑑が不可視フラグと、それを使用するモンスターのティアを得ることが必要です。
  • Scroll of Sleep(効果8ステータスコード)。各モンスターに sleeping_turns フィールドを追加してください。正である間、ホストのモンスターごとのディスパッチはただちに Wait を返し、アーキタイプの仮想マシンを呼び出すことなくカウンタをデクリメントします。読んだときの有効範囲は2です。
  • Scroll of Confusion(効果9ステータスコード)。各モンスターに confused_turns フィールドを追加してください。正である間、ホストは人工知能が返したアクションを混乱させます。50パーセントの確率で、アクションはランダムな隣接ステップとして再抽選されます。
  • Scroll of Remove Curse(ステータスコード10)。武器と防具に呪いフラグを追加してください。一部の武器は呪われた状態で出現します。呪われた装備は外すことができません。Remove Curse はフラグを解除します。

各効果はホスト側で数行です。既存のステータスコード値を再利用すれば、スクリプト側を変更する必要はありません。

ティア4: 研究的な問い

演習 4.1. 100フロアの降下を通じて、依然として認識可能なローグの感触を生み出す人工知能アーキタイプスクリプトの最小集合は何でしょうか。現在の集合は9つです。一度に1つずつ削除してみて、ゲームプレイが目立って劣化するかどうかを評価してください。推論。ボスアーキタイプは単一のモンスターに結び付けられているため、最も置き換えやすいものです。

演習 4.2. 単一フロア上のダンジョン生成器の最悪実行時間は何でしょうか。構造検証器はスクリプトを受け入れますが、測定されたプロファイルは支配的なコストを特定するでしょう。スクリプトのループごとの本体を読み、ホストネイティブ呼び出しを数えることは、公正な出発点です。アリーナに境界づけられたテキストサイズ解析はすでに最悪メモリ使用量が有界であることを証明しています。この演習はその時間版を求めます。

演習 4.3. モンスター図鑑は現在、モンスター名を含めて完全に rogue_bestiary.kel にあります。上記の モンスター図鑑スクリプトを読む の節を参照してください。名前はデータセグメントではなく、スクリプトの戻り値を通じて流れます。残る未解決の問いは、rogue_gear.kel の武器と防具の名前です。同じ「戻り値を名前とする」パターンをギアスクリプトに適用し、examples/rogue/items.rs から WEAPON_NAMESARMOR_NAMES を削除してください。移行は、モンスター図鑑の前例があるので機械的です。

演習 4.4. Keleusma の例において、ホストとスクリプトの間の正しい責任分担は何でしょうか。ピアノロールの例はほとんどすべてをスクリプトに置きます。ローグライクの例は行数でおおよそ半々に分けます。両方の例にわたって一方の極端かもう一方かを論じる短いエッセイを書いてください。

ティア5: ゲームバランス

演習 5.1. 飢餓を再び本物の脅威に調整してください。最初のプレイテストではフロア4より前に食料が尽き、不公平に感じられました。出荷時の修正は2つの変更を組み合わせました。空腹は現在、毎ターンではなく2ターンごとに1ずつ減少し、単一の初期糧食での実行時間を倍にしました。倒されたモンスターは現在、プレイヤーが踏んで食べられる死体を残す、形状に由来する確率を持ちます。この組み合わせは行き過ぎました。飢餓はもはやまったく本物の脅威ではありません。とりわけ死体の拾得は、最初の数回の戦闘を過ぎるとプレイヤーを無限に飢えさせないようにします。この演習は、序盤の飢餓を不可避にすることなく、プレイヤーを再び危険地帯に置く構成を見つけることです。調整する候補。examples/scripts/rogue/rogue_book_keeping.kel の空腹周期。examples/rogue/natives.rs::autopickup の食料回復量。examples/scripts/rogue/rogue_dungen.kel::spawn_items の食料出現数。examples/rogue/bestiary.rs の死体ドロップ確率と形状ごとの満腹度。ゲームバランスの技巧は、いつ毒のある死体を食べるか、空腹のティックを節約するために戦いをスキップするか、そして食料を探すのではなく次のフロアへ進むかについて、興味深い意思決定を生む組み合わせを見つけることです。

演習 5.2. フロアの難易度曲線を較正してください。5回の新しい実行をプレイし、フロア20までの各フロアでプレイヤーが何ターンと何ヒットポイントを失うかを記録してください。検証すべき仮説。難易度は滑らかに上昇すべきです。実際には、曲線には新しい形状を導入するフロア境界(サーペントが現れるフロア12あたり、ドラゴンが現れるフロア36あたり)で目に見える不連続性があります。この演習は、モンスター図鑑の first_floor フィールドとダンジョン生成器のモンスター数スケーリングのタイミングを取り直すことで曲線を滑らかにすることです。推論。滑らかな曲線はプレイヤーに成長する力の感覚を与えます。とげとげした曲線はスパイクフロアで苛立ちを生みます。

演習 5.3. 戦利品の分布を監査してください。5フロアの実行でプレイヤーが遭遇する各アイテム種類の数を数えてください。出荷時の構成は、ポーションと巻物を合わせたよりも多くの金塊を生み出す傾向があります。金は現在のルールでは純粋にスコアカウンタであるため、金ドロップの氾濫は無意味に感じられます。この演習は、プレイヤーが食料、ポーション、巻物、金塊にほぼ等しい数で遭遇するようダンジョン生成器の spawn_items を再調整することです。仮説。等しい数はすべての拾得を意図的に感じさせます。偏った数は、稀なドロップを不釣り合いに興奮させます。望むプレイ感触に合う構成を選んでください。

演習 5.4. 戦闘ダメージのスケーリング。出荷時の武器テーブルはティア0のダメージ2からティア19のダメージ118まで、防具テーブルは防御0から40まで進みます。モンスター図鑑のモンスターヒットポイントは害獣の2からボスの200までの範囲です。この演習は、実行をプレイして、プレイヤーの現在の武器が満足のいくものでなくなるフロアを特定することです。武器のダメージ進行またはモンスターのヒットポイントスケーリングのいずれかを調整して、フロア30から50がフロア1から10と同じくらい決定的に感じられるようにしてください。仮説。決定的な感触は、絶対的なダメージではなく攻撃あたりの撃破数から生まれます。そのフロアでモンスター図鑑の半分を一撃で倒す武器は、撃破ごとに3回の振りを要する武器とは異なるリズムを生みます。

総合プロジェクト

これらのプロジェクトはそれぞれ、数日間の集中した作業を要します。複数のティア3の演習を1つの完全な機能に統合します。

総合プロジェクト A. 遠隔戦闘アップデート。 プレイヤーの遠隔攻撃を実装し(演習 3.5)、ヘッドアップディスプレイに矢筒の矢数を追跡する弓のアイテムタイプを追加し、弓を射るキーバインドを追加し、新しい戦闘オプションが意味を持つが支配的にならないようにフロア20から40のモンスター図鑑エントリを再調整してください。成果物。遠隔攻撃を少なくとも10回使用する、フロア1からフロア20までのプレイ可能な実行。

総合プロジェクト B. 実行ごとの永続化。 セーブとロードを実装し(演習 3.2)、階段の降下ごとにオートセーブを追加し、ゲームオーバーを生き延びるハイスコアテーブルを追加してください。成果物。フロアの途中で終了して再開できる、洗練された実行。

総合プロジェクト C. アセット駆動のレンダラ。 手続き的スプライトアトラスを置き換え(演習 3.4)、人型モンスターとプレイヤーに4方向対応のスプライトを追加し、水、溶岩、そしてフロア100の出口タイルにタイルごとのアニメーションフレームを追加してください。成果物。手続き的スプライト版とは目に見えて異なる、レンダリングされた実行。

総合プロジェクト D. クエストシステム。 フロア入場時にホストが問い合わせるクエストボードのネイティブを追加してください。各フロアのクエストは文字列と達成条件(ある種類を10体倒す、ある種類のアイテムを収集する、特定のタイルに到達する)です。プレイヤー構造体にクエスト追跡フィールドを追加してください。達成時に小さな報酬を追加してください。成果物。フロア3のクエストが目に見えて追跡され達成される実行。

参照テーブル

タイル識別子

IDタイル
0
1
2閉じたドア
3開いたドア
4下り階段
5出口

アイテム種類識別子

ID種類
0食料
1
2武器
3防具
4ポーション
5巻物
6死体

人工知能アクションコード

コードアクション
0待つ
1対象セルへの移動または近接攻撃
2対象セルへの遠隔攻撃

アイテム効果ステータスコード

完全な表については上記の アイテム効果スクリプトを読む を参照してください。

デフォルトの調整パラメータ

パラメータデフォルト
マップサイズ64×40タイル
表示タイルサイズ16×16ピクセル。スプライトアートは24ピクセルで作成され、コピー時に16へ縮小されます。
視野半径8タイル
初期ヒットポイント12
初期空腹100
空腹ティック2ターンごとにマイナス1
食料回復空腹40
ヒットポイント回復空腹が正のとき10ターンごとに1
飢餓ダメージ空腹ゼロのとき毎ターン1
レベルごとのヒットポイント増加3
レベルごとのスキル増加1
フロアごとの部屋数8
フロアごとのモンスター数フロア数プラス4、上限12
フロアごとの食料3から5
フロアごとのポーション2から4
フロアごとの巻物2から4
フロアごとの武器0から1
フロアごとの防具0から1
フロアごとの金塊4から7