🔖

ノベルゲームを記述するための KAG 記法の構文定義を考える

2024/01/01に公開

はじめに

KAG (Kirikiri Adventure Game) [1] とはある種のスクリプトエンジンで解釈されるシナリオを記述するための記法(あるいは言語)です。ちなみにこの名称は吉里吉里 [2] というスクリプトエンジンが由来です。
本稿では、 KAG 記法を確認したのち、 KAG の構文定義を導きつつ、 KAG のシナリオの一例を解読していきます。

注意事項・免責事項

本稿はコミックマーケット101へ向けて執筆されました。
このイベントが終了してから1年以内にインターネット上で公開する予定です。
また「zenn.dev」の『ノベルゲームを記述するための KAG 記法の構文定義を考える』https://zenn.dev/articles/92ee658bf110a8/ にて正誤表や関連記事へのリンクを公開する予定です。

本稿の中で例示されるコードの断片は実行可能かどうかを保証できません。なぜならば、本稿の執筆時点で構文解析器が実装できていないためです。
それを踏まえて、本稿内のコード断片をある種の処理系で実行しようとする際に生じうる不都合や不具合については保証できかねますのでご了承ください。

KAG の記法

KAG のシナリオはいくつかの要素の組み合わせで記述されます。
本稿ではそれぞれの要素を「地の文」と「コメント」と「シーン」と「行為者表示」と「動作」と呼びます。

地の文は LaTeX や HTML のようにそのまま表示されます。

Hello, World!
と呟いてみた。

コメントはセミコロン記号(;)から行末まで[3]で表現されます。

; この行はコメントです。

シーンのブロックはアスタリスク記号(*)が文頭にある1行で表現されます。「いつ」を表現すると解釈できます。

*Chapter1_Akane_Speaks_Japanese

行為者の表示はハッシュ記号(#)から行末までで表現されます。行末との間にコロン記号(:)が挟まれた場合は発言者の表情を表すとされています。「誰が」を表現すると解釈できます。

#Akane:Smiling

舞台装置の動作・行為者の動作のために2種類の表現方法が提供されています。
丸々一行だけ有効な動作はアットマーク(@)で表現されます。
同様に複数行または文中で有効な動作は始め角括弧([)から終わり角括弧(])までで表現されます。
動作には引数をとるものがあります。引数の表記については、本稿では割愛します。

[call storage="scenario.kag" target="*Chapter1_Akane_Speaks_Japanese"]
@call storage="scenario.kag" target="*Chapter1_Akane_Speaks_Japanese"

KAG 記法の構文定義

抽象構文木 (Abstract Syntax Tree: AST) [4] とは、構文木 (Syntax Tree: ST) [5] の情報から言語の意味に関係する要素のみを取り出した木構造です。
構文定義はバッカス・ナウア記法 (Bacus-Naur Form: BNF) [6] で記述されることがあります。
そのバッカス・ナウア記法を元にして実践的に使いやすくされた拡張バッカス・ナウア記法は EBNF (Extended BNF) または ABNF (Argumented BNF) [7] と表記されます。
本稿では ABNF を用いて構文定義を記述します。

KAG 記法の簡易的な ABNF を以下に示します。
ここで前後を半角空白記号で囲まれたマイナス記号(-)を用いて差集合を表します。

; Comment
; -------
author-comment-line = author-comment-marker *CHAR CRLF
author-comment-interrupting = author-comment-marker *CHAR
author-comment-marker = ";"

; Scenario
; -------
scenario = *( 
        author-comment-line ; This can be used as a scene comment 
        / scene-block 
        )

; Scene
; -----
scene-block = [scene-heading-line] scene-body
scene-heading-line = *HFWSP scene-marker scene-name *HFWSP [author-comment-interrupting] CRLF
scene-marker = "*"
scene-name = identifier
scene-body =  *(
        author-comment-line ; This can be Action Lines or a Big Print
        / narration
        / action 
        )

; Narration
; ---------
narration = narration-block
narration-block = [narration-heading-line] narration-body
narration-heading-line = ( *HFWSP 
        narration-marker narrator-name 
        [narrative-expression-marker narrative-expression] 
        *HFWSP CRLF ) ; Each component couldn't have any white-spaces
narration-marker = "#"
narrator-name = identifier
narrative-expression-marker = ":"
narrative-expression = identifier
narration-body = *( dialogue / action )

; Dialogue
dialogue = ( dialogue-body )
dialogue-line = *(ANYCHAR - any-marker) [ author-comment-interrupting ] CRLF
dialogue-block = dialogue-interrupting action-block
dialogue-interrupting = *(ANYCHAR - any-marker)
dialogue-body = *(
        author-comment-line
        / action  ; a personal action
        / dialogue-line 
        / dialogue-block 
        )

; Action
; ------
action = action-line / action-block
action-line = (*HFWSP 
        action-line-marker action-name 
        *( LHFWSP action-specification ) 
        [ author-comment-interrupting ] CRLF)
action-line-marker = "@"
action-block = *(
        author-comment-line
        / ( action-block-begin-marker 
        *LHFWSP action-name 
        *( LHFWSP action-specification ) 
        *LHFWSP action-block-end-marker 
        ))
action-block-begin-marker = "["
action-block-end-marker = "]"
action-name = identifier
action-specification = action-flag / action-hash-body
action-flag = identifier
action-hash-body = *( 
        author-comment-line
        / ( action-specification-key 
        *LHFWSP "=" 
        *LHFWSP action-specification-value 
        ))
action-specification-key = identifier
action-specification-value = ( 
        string-quoted 
        / numeric 
        / boolean 
        / variable-evaluation-line
        / script-expression-line
        / scene-marker scene-name 
        / 1*(FCHAR - (SINGLE-QUOTE / DOUBLE-QUOTE)
        )
script-expression-line = DOUBLE-QUOTE *(ANYCHAR - DOUBLE-QUOTE) DOUBLE-QUOTE
variable-evaluation-line = variable-evaluation-marker identifiler
variable-evaluation-marker = "&"

; Variables
; ---------
any-marker = scene-marker / narration-marker / narrative-expression-marker / action-
identifier = (FCHAR - DIGIT) *FCHAR
numeric = 1*DIGIT
boolean = "true" / "false"
string-quoted = DOUBLE-QUOTE *(ANYCHAR - DOUBLE-QUOTE) DOUBLE-QUOTE 
        / SINGLE-QUOTE *(ANYCHAR - SINGLE-QUOTE) SINGLE-QUOTE 
ANYCHAR = CHAR / fullwidth-character ; Except EOLs
HFWSP = WSP / fullwidth-whitespace
LHFWSP = HFWSP / CRLF

全ての KAG 文法要素を含んだシナリオの ESNode 表現

全ての KAG 文法要素を含んだシナリオの一例を示します。

scenario.kag
*Chapter1_Akane_Speaks_Japanese
#Akane:Smiling
I can speak Japanese![l]
[w]
こんにちは世界![l]
@call target="*Chapter1_Akane_Speaks_Japanese"

このシナリオの Unist [8] もどき表現は次の通りです。
なお、 Unist では JSON で表記されていましたが、紙面の関係から YAML で表記しました。

scenario.yaml
type: Scenario
children: 
- type: SceneBlock
  children: 
  - type: SceneHeadingLine
    children: 
    - type: SceneMarker
      value: "*"
      children: 
    - type: SceneName
      children:
      - type: Identifier
        children: 
        - type: Str
          value: "Chapter1_Akane_Speaks_Japanese"
  - type: SceneBody
    children: 
    - type: Narration
      children: 
      - type: NarrationBlock
        children:
        - type: NarrationHeadingLine
          children: 
          - type: NarrationMarker
            value: "#"
          - type: NarratorName
            children:
            - type: Identifier
              children: 
              - type: Str
                value: "Akane"
          - type: NarrationExpressionMarker
            value: ":"
          - type: NarrativeExpression
            children:
            - type: Identifier
              value: "Smiling"
        - type: NarrationBody
          children:
          - type: Dialogue
            children: 
            - type: DialogueBody
              children: 
              - type: DialogueBlock
                children: 
                - type: DialogueInterrupting
                  value: "I can speak Japanese!"
                - type: ActionBlock
                  children: 
                  - type: ActionBlockBeginMarker
                    children: "["
                  - type: ActionName
                    children:
                    - type: Identifier
                      value: "l"
                  - type: ActionBlockEndMarker
                    children: "]"
              - type: Action
                children: 
                - type: ActionBlock
                  children: 
                  - type: ActionBlockBeginMarker
                    children: "["
                  - type: ActionName
                    children:
                    - type: Identifier
                      value: "w"
                  - type: ActionBlockEndMarker
                    children: "]"
              - type: DialogueBlock
                children: 
                - type: DialogueInterrupting
                  value: "こんにちは世界!"
                - type: ActionBlock
                  children: 
                  - type: ActionBlockBeginMarker
                    children: "["
                  - type: ActionName
                    children:
                    - type: Identifier
                      value: "l"
                  - type: ActionBlockEndMarker
                    children: "]"
              - type: Action
                children: 
                - type: ActionLine
                  children: 
                  - type: ActionLineMarker
                    value: "@"
                  - type: ActionName
                    children:
                    - type: Identifier
                      value: "call"
                  - type: ActionSpecification
                    children: 
                    - type: ActionHashBody
                      children: 
                      - type: ActionSpecificationKey
                        children: 
                        - type: Identifier
                          value: "target"
                      - type: ActionSpecificationValue
                        children: 
                        - type: StringQuoted
                          value: "*Chapter1_Akane_Speaks_Japanese"

おわりに

本稿では KAG 構文定義について考えました。
構文定義をあらかじめ示しておくことにより、コードエディタやリンタ上で活用する下地を作れました。
次の段階として筆者はこの構文定義による(抽象)構文木の構文解析器を実装する予定です。

本稿が読者の皆様の学習の一助になれば光栄です。
お読みいただきありがとうございました。

脚注
  1. 『吉里吉里2 リファレンス』 https://hydrozoa.felisworks.com/doc/kr2doc/contents/index.html を2022年12月14日に参照し参考にしました。 ↩︎

  2. 『吉里吉里 ダウンロードページ』 http://kikyou.info/tvp/ のアーカイブhttps://web.archive.org/web/20171208011348/http://kikyou.info/tvp/ を2022年12月14日に参照し参考にしました。 ↩︎

  3. 複数行コメントが実装されている KAG 処理系もあります。 ↩︎

  4. 『抽象構文木 - Wikipedia』https://ja.wikipedia.org/wiki/抽象構文木 を2022年12月13日に参照しました。 ↩︎

  5. 『Parsing tree - Wikipedia』https://en.wikipedia.org/wiki/Parse_tree を2022年12月13日参照し参考にしました。 ↩︎

  6. 『バッカス・ナウア記法 - Wikipedia』https://ja.wikipedia.org/wiki/バッカス・ナウア記法 を2022年12月13日に参照し参考にしました。 ↩︎

  7. 『ABNF - Wikipedia』https://ja.wikipedia.org/wiki/ABNF を2022年12月13日に参照し参考にしました。 ↩︎

  8. 『Universal Syntax Tree used by @unifiedjs』 https://github.com/syntax-tree/unist を2022年12月13日に参照し参考にしました。 ↩︎

Discussion