りんご競争 (4)

さて、りんご競争の続きです。周辺部分ばかりに触れていて、なかなかゲーム本体にたどり着かない。なので、ここでもう一気に掲載してしまいます。ゲーム部分のスクリプトを。

Script Editor で開く

property racers : {2, 4, 6, 8, 10}
property laneCount : {0, 0, 0, 0, 0}
property racingLength : 39

set playerLane to "========================================|" & return
set playerLaneProperties to {size:14, font:"Monaco", color:{0, 0, 65535}}
set otherLane to "----------------------------------------|" & return
set laneProperties to {size:14, font:"Monaco", color:{0, 0, 0}}
set theRacer to "@" & return
set racerProperties to {size:14, font:"Monaco", color:{65535, 0, 0}}

tell application "TextEdit"
    activate
    close every document saving no

    display dialog "コースを選択(1から5の半角数字):" default answer ""
    set n to text returned of result as integer

    make new document at end of documents
    set name of window 1 to "Another Apple Race"

    set text of front document to ""

    repeat with i from 1 to 6
        if i is n or (i - 1) is n then
            make new paragraph at after last paragraph of text of front document with data playerLane with properties playerLaneProperties
        else
            make new paragraph at after last paragraph of text of front document with data otherLane with properties laneProperties
        end if
        if i < 6 then
            make new paragraph at after last paragraph of text of front document with data (i & theRacer) as text with properties racerProperties
        end if
    end repeat

    repeat with i from 1 to 5
        set item i of laneCount to 0
    end repeat

    set winningLane to 0
    set loopIndex to 0
    set high to 0
    repeat
        make new character at after character 1 of paragraph (item 1 of racers) of text of front document with data " " with properties racerProperties
        make new character at after character 1 of paragraph (item 2 of racers) of text of front document with data " " with properties racerProperties
        make new character at after character 1 of paragraph (item 3 of racers) of text of front document with data " " with properties racerProperties
        make new character at after character 1 of paragraph (item 4 of racers) of text of front document with data " " with properties racerProperties
        make new character at after character 1 of paragraph (item 5 of racers) of text of front document with data " " with properties racerProperties

        set racerIndex to random number from 1 to 5
        make new character at after character 1 of paragraph (item racerIndex of racers) of text of front document with data " " with properties racerProperties
        set item racerIndex of laneCount to (item racerIndex of laneCount) + 1
        set loopIndex to loopIndex + 1

        repeat with i from 1 to 5
            if (item i of laneCount) + loopIndex is greater than or equal to racingLength then
                if high < (item i of laneCount) + loopIndex then
                    set high to (item i of laneCount) + loopIndex
                    set winningLane to item i of racers
                end if
            end if
        end repeat

        if winningLane > 0 then exit repeat
    end repeat
end tell

2 年以上前に作成したものなので、見苦しい点はご容赦を。今となれば、なんでこんなことをしているのだろうと思う部分が散見されるのですが。

エラーのチェックしていません。なので、最初にダイアログでコース番号を聞かれますが、半角数字の 1 〜 5 以外を入力するとおかしくなります。ゲームエンジンは、オリジナルのりんご競争とほぼ同一です。変わっているのは、主に描画の部分です。また、Script Editor では、りんごマークを入力できないので変わりに「@」記号を使っています。@ マークが走ります。

TextEdit でなにが面倒かというと、特定の場所に文字を挿入することです。set 命令を使うよりも make 命令を使う方が簡単なのです。make 命令を使うと特定の場所を指定し、文字の属性も指定できます。

オリジナルのりんご競争では、レースを行う前に次のようなことをしています。

set text of window 1 to return & return & return & return & return & return & return & return & return & return & return & return & return & return & return

ゲームに必要な行数を入力しているんですね。もちろん、これをそのまま TextEdit で実行することは可能です。次にりんごが走るレーンを作るわけですが、この部分はオリジナルでは copy 命令を使って以下のようなことをしています。

copy "--------" to paragraph x of window 1

この window 1 を document 1 にすれば、TextEdit でも動きます。ただ、改行がなくなります。以下のスクリプトを実行するとよく分かります。

Script Editor で開く

tell application "TextEdit"
    activate
    set theText to ""
    repeat with i from 1 to 15
        set theText to theText & (i as Unicode text) & return
    end repeat
    set text of document 1 to theText
    delay 3
    repeat with i from 1 to 5
        copy "--------" to paragraph i of text of document 1
        delay 1
    end repeat
end tell

copy するたびに一行ずつ減っていくという何とも恐ろしいことになっています。これは、TextEdit の paragraph クラスが改行まで含むようになっているからなんですね。個人的な AppleScript の感覚からすると、奇妙な結果です。ですので、単純に set (copy)命令を使うと痛い目に遭います。AppleScript から行った変更は、「編集」メニューの「取り消し」が利かないですし。

これがどのスクリプタブル Cocoa アプリケーションでも同じ動作を行う、というのならまだいいのです。しかし、上記のスクリプトの対象アプリケーション(tell application "TextEdit" を tell application "CotEditor" に変える)を CotEditor にすると、エラーになります。また、QuoEdit なら期待通りの動き(15 行目まで改行を挿入し、その後、5行目までを変更)になります。

個人的な感覚でしかないのですが、Mac OS X 以前の AppleScript では、QuoEdit のような結果が基本的な動作だったと思います。が、それが Mac OS X の Cocoa アプリケーションでは通用しないことが多いのです。

AppleScript は、操作するアプリケーションによって利用できる命令やオブジェクト、または、結果が異なるから難しいという声を聞きますが、少なくとも Mac OS X 以前は基本的な部分はおおむね共通していたように思えます。しかし、Cocoa アプリケーションとなると基本的な部分すら異なるのです。これが、よけいに混乱に拍車をかけているような気がします。

例えば、save 命令。TextEdit では使えても Mail では使えない。というか、実装されていない。Safari では使えるが、表示されるシートのキャンセルボタンを押すとエラーになる。TextEdit の場合は、何も起きない。

tell application "TextEdit" -- Safari や Mail に変えてみると...
    save front document
end tell

save 命令は、Standard Suites に含まれている命令です。Standard Suites に含まれている命令は、全て AppleScript Language Guide でも解説されている基本的なコマンドばかりです。なのに、利用できたり、できなかったり。使えないものなら、用語説明に載せるな、と言いたい。

結果的に、Cocoa アプリケーションでは、基本的なクラスや命令でも使えるかどうかはいちいち確認しないといけないのです。また、利用できても結果がどうなるかはアプリケーション次第なのです。今までの経験を過信してはいけません。

また、以下のようにできそうでできないことも、なぜかできてしまいます。

Script Editor で開く

tell application "Mail"
    selection
    properties of attribute runs of content of item 1 of result
end tell

結果が返ってくるからといって、その結果が正しいものとは限りません。上記の結果は、明らかに間違っています。

Cocoa アプリケーションの場合、Apple より Apple 以外のソフトウェアの方がスクリプティング対応に時間をかけていたりします。それもこれも Apple 純正のソフトウェアが以上のようなむちゃくちゃな実装を行っているからです。Apple のソフトウェアが一定の基準をもって AppleScript に対応していれば、それを参考にして基本機能を実装することもできるのですが...。

りんご競争 (3)

一日のアクセス数が少し増えてます。なぜか?アクセス解析などしていないので何が原因か分からないんですが。

さて、りんご競争 (1) は導入編でした。りんご競争 (2) で、ゲームの初期化の部分を少し調べたのですね。文字属性の変更で終わっています。

少し話はそれてしまうのですが、ここで TextEdit とスクリプティング対応エディタとの差異、TextEdit での文字列の操作についてみておきます。まず、 TextEdit とスクリプティング対応エディタとの一番の違いは、insertion point の有無と選択文字列の取得が可能かどうかにあります。TextEdit は、insertion point も select 命令も selection も利用できません。ですので、選択している文字列の前後に HTML タグをつけるなどという操作がとてつもなく難しい作業だったりします。しかし、UI Scripting を使えば以下のように可能です。

Script Editor で開く

tell application "TextEdit" to activate

tell application "System Events"
    tell process "textedit"
        keystroke "c" using {command down}
        my addtag("P")
        keystroke "v" using command down
    end tell
end tell

on addtag(tagStr)
    set clipContent to «class ktxt» of ((the clipboard as text) as record)
    set clipContent to "<" & tagStr & ">" & clipContent & "</" & tagStr & ">"
    set the clipboard to clipContent
end addtag

この場合、タグで囲みたい文字列を TextEdit で選択しておく必要があります。選択範囲をどうにかしたいというときは、このように clipboard 経由で文字列を加工するのが最も簡単だと思われます。UI Scripting を利用しなくても、clipboard 経由なら文字列をコピーしておくことで操作が可能です(コピー/ペーストは手作業で)。他のアプリケーションなどとの連携も clipboard を中継させることがよくあるので、調べておくと何かの役に立つかもしれません。

選択されている文字列以外ではどうでしょうか?例えば、全部の行に何らかの処理を行いたい、というとき。TextEdit は、character と word、paragraph が利用できます。用語辞書を見ると分かりますが、これらのオブジェクトは、document クラスの text 属性を通して操作します。

Script Editor で開く

tell application "TextEdit"
    last character of paragraphs of text of front document
end tell

このスクリプトを実行すると分かりますが、行の最後の文字は改行です。ですので、単純に次のようなスクリプトを実行すると改行は失われてしまいます。

Script Editor で開く

tell application "TextEdit"
    set paragraph 1 of text of front document to "何らかの文字列"
end tell

これを踏まえて各行にタグを追加するとなると、以下のようにします。

Script Editor で開く

property LF : ASCII character 10

tell application "TextEdit"
    tell text of front document
        set last character of paragraphs of it to "<br />" & LF
    end tell
end tell

最後の改行文字を変更しているだけです。では、各行の最初に追加するときは?これが少し面倒です。間違っても以下のようにしてはいけません。

Script Editor で開く

tell application "TextEdit"
    tell text of front document
        set first character of paragraphs of it to "<p>" & first character of paragraphs of it
    end tell
end tell

これだと、各行の先頭に <p> + 各行数分だけ先頭の文字が追加されてしまいます。ちなみに以下のようにしても動きません。

Script Editor で開く

tell application "TextEdit"
    tell text of front document
        set paragraphList to a reference to paragraphs of it
        repeat with thisParagraph in paragraphList
            set contents of thisParagraph to "<p>" & thisParagraph
        end repeat
    end tell
end tell

ふむ、なかなか難しいです。次のようなスクリプトもエラーになります。

Script Editor で開く

tell application "TextEdit"
    tell text of front document
        set before first character of paragraph 1 of it to "<p>"
    end tell
end tell

単純に一文字目を置き換えるだけなら簡単なんですが。なら、make 命令で作ってしまいましょう。

Script Editor で開く

property LF : ASCII character 10

tell application "TextEdit"
    tell text of front document
        set n to count paragraphs of it
        repeat with i from 1 to n
            make new word at before first character of paragraph i of it with data "<p>"
            if i is n then
                make new word at after last character of paragraph i of it with data "</p>"
                return
            end if
            make new word at after character -2 of paragraph i of it with data "</p>"
        end repeat
    end tell
end tell

この他にもスクリプトで文字列を作って、最後に text 属性を全て置き換えてしまう、という方法もありますね。

Script Editor で開く

tell application "TextEdit"
    tell text of front document
        set paragraphList to paragraphs of it
        set tmp to ""
        repeat with i from 1 to length of paragraphList
            set thisParagraph to item i of paragraphList
            set tmp to tmp & (i as Unicode text) & " : " & thisParagraph
        end repeat
    end tell

    set text of front document to tmp
end tell

こちらの方が細かい変更ができていいかも知れません。もちろん、これら以外にも text item delimiters を使う方法もありますし、do shell script 経由で Perl 等に処理してもらう方法もあります。

では、文字列中の特定の文字を変更したい場合はどうでしょう?いわゆる検索、置き換えなんですが。TextEdit のようなスクリプタブル Cocoa アプリケーションは、ほとんどのクラスでフィルタ参照が利用できます。

Script Editor で開く

tell application "TextEdit"
    tell text of front document
        characters of paragraphs of it whose it is "文"
    end tell
end tell

このようにすることで各行中の「文」の文字だけを抜き出すことができます。もちろん、以下のようにして置き換えることも可能です。

Script Editor で開く

tell application "TextEdit"
    tell text of front document
        set characters of paragraphs of it whose it is "文" to "愛"
    end tell
end tell

ほぼ一瞬で置き換えは終わります。しかし、落とし穴があります。この例の場合は、「文」を「愛」に置き換えました。例えば、「文」を「学校」のように一文字を二文字に置き換えたりすると、位置の参照が変わってしまい(最初の文字を置き換えたところで以降の文字は一文字ずつてしまい)正しく置き換えることができなくなります。では、このようなフィルタ参照は使えないのか?というと、そうではなくてこのフィルタ参照は文字の属性を変更するときに大いに役立ちます。

例えば、普通のテキストを RTF にして見栄えを整えたいとき。RTF で文字に属性がついているときに文字サイズが 24 ポイントのものを 18 ポイントに変更したいとき。こういうときにフィルタ参照を利用すると効率よく処理できます。フィルタ参照の条件指定を大いに利用すれば、かなり細かい指定ができます。まぁ、リストや行間、スタイルやセンタリング等の位置指定が利用できないのはどうにもなりませんが。

スクリプタブル Cocoa アプリケーションの特徴は、このフィルタ参照です。繰り返しを行うより、フィルタ参照で一括処理を行う方が速いです。もう一つの特徴は、a reference to 〜 による参照です。

Script Editor で開く

tell application "TextEdit"
    set attrList to attribute runs of text of front document
    repeat with thisAttr in attrList
        if font of thisAttr is "Osaka" then
            set font of thisAttr to "Optima-Regular"
            set size of thisAttr to 24
            set color of thisAttr to {12548, 5874, 35694}
        end if
    end repeat
end tell

このようにして文字の属性を変更しようとしてもエラーになります。これは、attrList におさめられているのがドキュメント内の文字列に対する参照ではないからです。以下のように参照にすることできちんと動くようになります。

set attrList to a reference to attribute runs of text of front document

ほとんどの場合スクリプタブル Cocoa アプリケーションで複数の項目を取得すると適切な参照になります。

Script Editor で開く

tell application "Mail"
    messages of mailbox 1
    properties of item 1 of result
end tell

tell application "iCal"
    todos of calendar 1
    properties of item 1 of result
end tell

tell application "TextEdit"
    paragraphs of text of front document
    properties of item 1 of result --エラーになる
end tell

Mail、iCal は、それぞれ id 参照と番号参照で項目を返しますが、TextEdit は、中身そのもの(つまり、文字列)を返します。アプリケーションにより異なるのが困るのですが、このように a reference to 〜 を用いて参照を利用する場合が時々あります。しかし、だからといってどんなオブジェクトも a reference to 〜 で取得するといいのかというとそうではありません。オブジェクトによってはその後の処理が著しく遅くなることがあります。

Script Editor で開く

tell application "Mail"
    set messagesRef to a reference to messages of mailbox 1

    repeat with thisMessage in messagesRef
        subject of thisMessage
    end repeat
end tell

このようにするよりも、次のように素直(?)に書く方が速いです。

Script Editor で開く

tell application "Mail"
    set messagesRef to messages of mailbox 1

    repeat with thisMessage in messagesRef
        subject of thisMessage
    end repeat
end tell

処理速度を稼ぐために次のようなスクリプトを書くことがあります。

Script Editor で開く

tell application "Mail"
    set messageList to messages of mailbox 1

    set theList to {}
    set theListRef to a reference to theList
    repeat with thisMessage in messageList
        set end of theListRef to subject of thisMessage
    end repeat
end tell

しかし、これは速度的に違いはありません。a reference to 〜 は、使う場面さえ間違えなかったら速度の向上が見込めますが、多くの場合 、普通にスクリプトを書くのとそれほど変わりがなかったりします。

Script Editor で開く

tell application "System Events"
    set prefsFolder to preferences folder of user domain
    set prefsFile to path of (disk item "com.apple.itunes.plist" of prefsFolder)
    set prefsFile to property list file prefsFile

    set propList to a reference to property list items of prefsFile
    --set propList to property list items of prefsFile
    set theList to {}
    set theListRef to a reference to theList
    repeat with thisItem in propList
        set end of theListRef to name of thisItem
        --set end of theList to name of thisItem
    end repeat
end tell

コメントアウトしている部分のコメントを外しても a reference to 〜 を使った場合と比べて、遜色はないです。不必要に a reference to 〜 を使うより、他の方法を模索する方が結果的に速い場合があります。

と、ここまで書いてきて Intel Mac ではどうなるんだろうと思ってしまいました。...気分、萎えたので終わり。

りんご競争 (2)

ちょっと手違いがあり、前回、あいだの文章が抜けていました。修正しました。すいませんでした。

さて、前回は導入部でした。今回からちょっと鬱陶しいぐらいに詳しく見ていきます。

まず、オリジナルのりんご競争は、賭け金の入力やユーザーの状態をチェックする部分など、直接エディタを操作する以外のゲームとしての機能があります。しかし、これらは今回の検証に関係がないので省きます。AppleScript でスクリプティング対応エディタを操作している部分。この辺りを中心に見ていきます。話の都合上、TextEdit は、RTF 編集モードになっていることとします。

りんご競争の描画は、すべてエディタのウィンドウに対して行います。そのため、オリジナルのりんご競争は、最初にウィンドウの初期化を行っています。その部分だけを抜き出すと、次のようになります。

tell application "スクリプティング対応エディタ"
    launch
    activate
    try
        close window 1 saving no
    end try
    run
    set font of text of window 1 to "Courier"
    set size of text of window 1 to 14
    set name of window 1 to "A Day At The Races"
end tell

例えば、このスクリプトの tell application 〜 の部分を TextEdit に変更するとどうなるか?

構文確認は通ります。実行すると、フォントの指定の部分でエラーになると思います。Cocoa アプリケーションでは、この部分は document を対象にしないといけません。Mac OS X 以前では、window クラスと document クラスは、イコールの関係だった(ことが多かった)のですが、Cocoa アプリケーションではそうではありません。document(テキスト編集部分)クラスと window クラスは、明確に別物です。

別物だということは、フォント指定の前、ウィンドウを閉じている部分で分かります。

close window 1 saving no

この部分は、

close document 1 saving no

としないといけません。TextEdit で何らかの編集を行ってから上記の close window 〜 を実行すると saving no としていても必ず保存するかどうかを尋ねられます。これは、編集した結果を保存をすべきかどうかということは document クラスが責任を持って行うことだからです(言い切りましたが、多分、そういうことだろうという感じです...)。

つまり、window ではなく、document に「保存しなくていいよ」と伝えてあげなくてはだめなのです。こういったことを考慮に入れてスクリプトを修正すると、以下のようになります。

Script Editor で開く

tell application "TextEdit"
    activate
    try
        close every document saving no -- すべてのドキュメントを閉じる
    end try
    run -- 新規ドキュメントが開く
    set gameWindow to front document
    tell gameWindow
        set font of text of it to "Courier"
        set size of text of it to 14
    end tell
    set name of front window to "A Day At The Races"
end tell

しかし、これでフォントと文字のサイズが実際に変更されるわけではありません。document 内の text 属性が空だからです。対象となる文字がないことにはフォントも大きさも変えれるわけがありません。

やりたいことは、初期設定のフォントとサイズを変えたいということなのですが、この方法では駄目なんですね。すでに文字が入力さているなら別ですが。

りんご競争 (1)

りんご競争、好きなんです。

りんご競争とはなんぞや?という人のために概略を。その昔、昔、Mac OS のバージョンが X になる以前。AppleScript 対応のテキストエディタがありました。そのテキストエディタの名前は「スクリプティング対応エディタ」。このエディタと AppleScript を組み合わせたサンプルスクリプトを Apple は、配布していました。現在でもここからダウンロードできますし、クラシック環境で動作させることもできます。ただし、ダウンロードしたファイルを Mac OS X で解凍し、ディスクイメージをマウントさせてもファイル名が文字化けします。この文字化けを解消するには、Apple が配布している File Name Encoding Repair Utility を利用するか、UNi*fIX というフリーのソフトを利用します。

マウントしたディスクイメージ(ダウンロードするディスクイメージは、2 つあります。両方必要です)の中にスクリプティング対応エディタとサンプルスクリプトが入っています。クラシック環境で動作するので、クラシック環境は必須です。スクリプトを見るだけなら Mac OS X の Script Editor でも見れますが。

余談ですが、これらのディスクイメージの中には、他にもテキストファイルが入っています。今となっては懐かしい HyperCard のスタックも。スタックの方はともかく、テキストファイルには AppleScript の リファレンスが入っています。AppleScript を覚えたいという人には有用だと思います。内容は、現在でも通用します。

J-009-1199-A AppleScript 2 というディスクイメージの方にサンプルスクリプトが入ったフォルダがあります。このフォルダの中にゲームというフォルダがあり、その中にりんご競争があります。もう一方のディスクイメージの中にスクリプティング対応エディタがあります。

りんご競争は、一種の競馬ゲームです。1 から 5 までレーンがあり、どのレーンのりんごが勝つか決定します。次に賭け金を決めるとレースが始まります。所持金の増減によりイベントが発生します。お金を借りたり、腕をへし折られたり...なかなか楽しませてくれます。

ご存知のように TextEdit は、スクリプティングに対応しています。では、りんご競争を TextEdit で再現できるのか?

はっきり言ってしまえば、りんご競争をそのままコピーして TextEdit 用に書き直しても動きません。変更する部分が多々あります。同じスクリプティング対応の Apple 製のエディタなのに、なぜか?

この差異を知ることは、つまり、Mac OS X で AppleScript を利用するときの注意点になってきます。また、同じようにスクリプタブルでも Cocoa と Carbon との違いということも含まれています。

と、大袈裟なことを書いていますが、あてになる内容かどうかは保証できません。では、また、次回。