oTree

_images/splash.png

中文 | 日本語

トップページ

http://www.otree.org/

oTreeとは?

oTree は Python で書かれたフレームワークで、次のようなものを作ることができます:

  • 囚人のジレンマ、公共財供給ゲーム、オークションのような、複数プレイヤーの戦略ゲーム
  • 経済学、心理学、およびその他の分野における、制御された行動実験
  • アンケート調査やクイズ

サポート

For help, post to our forum.

目次:

oTreeのインストール

Important note

If you publish research done with oTree, you are required to cite this paper. (Citation: Chen, D.L., Schonger, M., Wickens, C., 2016. oTree - An open-source platform for laboratory, online and field experiments. Journal of Behavioral and Experimental Finance, vol 9: 88-97)

Installation

If you will use oTree Studio (easiest option), go to otreehub.com.

Read: why you should use oTree Studio.

If you are an advanced programmer, you can use oTree with a text editor.

Pythonについて

以下は、oTreeを使うために知っておくべきPythonの基礎知識をまとめたチュートリアルです。

このファイルを実行する最も簡単な方法は、IDLE(通常、Pythonのインストールにバンドルされている)で実行することです。

他にも良いpythonチュートリアルがたくさんありますが、それらのチュートリアルでカバーされている内容のいくつかは、特にoTreeプログラミングには必要ないことに注意してください。

チュートリアルファイル

ダウンロード可能なバージョンは here です。

# Comments start with a # symbol.

####################################################
## 1. Basics
####################################################

# integer
3

# float (floating-point number)
3.14

# Math is what you would expect
1 + 1  # => 2
8 - 1  # => 7
10 * 2  # => 20
35 / 5  # => 7.0

# Enforce precedence with parentheses
(1 + 3) * 2  # => 8

# Boolean Operators
# Note they are
True and False # => False
False or True # => True

# negate with not
not True  # => False
not False  # => True

# Equality is ==
1 == 1  # => True
2 == 1  # => False

# Inequality is !=
1 != 1  # => False
2 != 1  # => True

# More comparisons
1 < 10  # => True
1 > 10  # => False
2 <= 2  # => True
2 >= 2  # => True

# A string (text) is created with " or '
"This is a string."
'This is also a string.'

# Strings can be added too!
"Hello " + "world!"  # => "Hello world!"

# None means an empty/nonexistent value
None  # => None


####################################################
## 2. Variables, lists, and dicts
####################################################

# print() displays the value in your command prompt window
print("I'm Python. Nice to meet you!") # => I'm Python. Nice to meet you!

# Variables
some_var = 5
some_var  # => 5

# Lists store sequences
li = []

# Add stuff to the end of a list with append
li.append(1)    # li is now [1]
li.append(2)    # li is now [1, 2]
li.append(3)    # li is now [1, 2, 3]

# Access a list like you would any array
# in Python, the first list index is 0, not 1.
li[0]  # => 1
# Assign new values to indexes that have already been initialized with =
li[0] = 42
li # => [42, 2, 3]


# You can add lists
other_li = [4, 5, 6]
li + other_li   # => [42, 2, 3, 4, 5, 6]

# Get the length with "len()"
len(li)   # => 6

# Here is a prefilled dictionary
filled_dict = dict(name='Lancelot', quest="To find the holy grail", favorite_color="Blue")

# Look up values with []
filled_dict['name']   # => 'Lancelot'

# Check for existence of keys in a dictionary with "in"
'name' in filled_dict   # => True
'age' in filled_dict   # => False

# set the value of a key with a syntax similar to lists
filled_dict["age"] = 30  # now, filled_dict["age"] => 30

####################################################
## 3. Control Flow
####################################################

# Let's just make a variable
some_var = 5

# Here is an if statement.
# prints "some_var is smaller than 10"
if some_var > 10:
    print("some_var is totally bigger than 10.")
elif some_var < 10:    # This elif clause is optional.
    print("some_var is smaller than 10.")
else:           # This is optional too.
    print("some_var is indeed 10.")

"""
SPECIAL NOTE ABOUT INDENTING
In Python, you must indent your code correctly, or it will not work.
All lines in a block of code must be aligned along the left edge.
When you're inside a code block (e.g. "if", "for", "def"; see below),
you need to indent by 4 spaces.

Examples of wrong indentation:

if some_var > 10:
print("bigger than 10." # error, this line needs to be indented by 4 spaces


if some_var > 10:
    print("bigger than 10.")
 else: # error, this line needs to be unindented by 1 space
    print("less than 10")

"""


"""
For loops iterate over lists
prints:
    1
    4
    9
"""
for x in [1, 2, 3]:
    print(x*x)

"""
"range(number)" returns a list of numbers
from zero to the given number MINUS ONE

the following code prints:
    0
    1
    2
    3
"""
for i in range(4):
    print(i)


####################################################
## 4. Functions
####################################################

# Use "def" to create new functions
def add(x, y):
    print('x is', x)
    print('y is', y)
    return x + y

# Calling functions with parameters
add(5, 6)   # => prints out "x is 5 and y is 6" and returns 11


####################################################
## 5. List comprehensions
####################################################

# We can use list comprehensions to loop or filter
numbers = [3,4,5,6,7]
[x*x for x in numbers]  # => [9, 16, 25, 36, 49]

numbers = [3, 4, 5, 6, 7]
[x for x in numbers if x > 5]   # => [6, 7]

####################################################
## 6. Modules
####################################################

# You can import modules
import random
print(random.random()) # random real between 0 and 1

Credits: このページのチュートリアルは Learn Python in Y Minutes からの引用であり、同じ license で公開されています。

チュートリアル

このチュートリアルではいくつかのアプリの作り方を紹介します。

はじめに、Pythonに慣れる必要があります。簡単なチュートリアルをコチラ Pythonについて に用意しました。

注釈

チュートリアルを終えた後は、oTree Hubの featured apps にアクセスしてみてください。あなたがこれから作ろうとしている実験課題の作例を見つけられるかもしれません。

Part 1: 簡単なアンケート

YouTube のビデオチュートリアル(英語)も参照してください。

oTree Studioを使い、まずは簡単なアンケートを作りましょう。最初のページでは実験参加者に氏名と年齢を尋ねます。次のページでその入力結果を表示しましょう。まずはサイドバーから Apps に入り、 + App を押してアプリを追加します。 アプリの名前は my_survey としましょう。

プレイヤー モデル (記録すべき実験データの定義)

サイドバーから Model Player に入り、2つのフィールドを追加しましょう。

  • 氏名データを記録するために、 StringField (文字列型のフィールドを意味します)を追加し、フィールドの名前を name としましょう。
  • 年齢データを記録するために、 IntegerField (整数型のフィールドを意味します)を追加し、フィールドの名前を age としましょう。

ページ

このアンケートでは2つのページを用意します。

  • 1ページ目: プレイヤーが名前と年齢を入力します。
  • 2ページ目: プレイヤーが1ページ目に入力した内容を確認します。

サイドバーから page_sequence に入り、 + Page を押してページを2つ追加しましょう。それぞれ SurveyResults と名前をつけましょう。

1ページ目

まず、 Survey ページの中身を設定をしましょう。このページには名前と年齢を入力するフォームが必要です。+ form_fields を押した後、form_modelplayer を選び、 form_fields の一覧から(先ほど PlayerModel で定義した) nameage を選択してください。

次に、 Survey ページ内で実際に表示するタイトルを設定します。 {{ block title }}Enter your information と入力します。Survey ページの本文は、 {{ block content }} において以下のように設定します。

Please enter the following information.

{{ formfields }}

{{ next_button }}
2ページ目

続いて、 結果を表示する Results ページの中身を設定しましょう。

タイトルは、 {{ block title }} において Results と入力します。Results ページの本文は、 {{ block content }} において以下のように設定します。

<p>Your name is {{ player.name }} and your age is {{ player.age }}.</p>

{{ next_button }}

セッション構成の定義

実験セッションで使うアプリを定義します。サイドバーから Session configs に入り、+ Session config を押します。app_sequence にアプリの候補の一覧が表示されるので、my_survey アプリを選んで app_sequence に追加します。

ダウンロードと実行

サイドバーから Download に入り、 otreezipファイルをダウンロードします。その後、指示に従ってoTreeをインストールした上で、otreezipファイルを解凍し、プログラムを起動してください。

問題がある場合は、 oTreeのグループ で質問できます。

Part 2: 公共財ゲーム

YouTube のビデオチュートリアル(英語)も参照してください。

ここでは、 公共財ゲーム を作成します。公共財ゲームは経済学の古典的なゲームです。

3人1組でプレイされる公共財ゲームを考えましょう。各プレイヤーには初期保有として1000ポイントが与えられます。プレイヤーは、グループに貢献したいポイント数を個別に決定します。3人のプレイヤーによる貢献額の合計は2倍された後、3人に均等に分割され、プレイヤーに再分配されます。

以下で解説する公共財ゲームの完全なコードは ここ にあります。

アプリの作成

新たにアプリを作成し、名前を my_public_goods とします。

定数

定数を定義するクラス class C に移動します。(詳細については、 Constants を参照してください。)

  • 埋め込み変数 PLAYERS_PER_GROUP を3に設定します。oTreeは自動的にプレイヤーを3人1組にグループ分けします。
  • 各プレイヤーへの初期保有は1000ポイントです。新たに ENDOWMENT なる変数を定義してその値を 1000 に設定しましょう。
  • プレイヤーの貢献額は2倍されます。倍率として、新たに MULTIPLIER なる変数を定義してその値を 2 に設定しましょう。

class C の中身は以下のようになります。

PLAYERS_PER_GROUP = 3
NUM_ROUNDS = 1
ENDOWMENT = cu(1000)
MULTIPLIER = 2

モデル (記録すべき実験データの定義)

実験参加者の行動を後々分析するために、各プレイヤーが貢献したポイント数を記録しなければなりません。データベースに記録する変数(フィールド)でプレイヤー一人ひとりについて定義されるものは class Player (プレイヤーモデルと呼ばれます)の中で定義します。class Player に移動し、 contribution なるフィールドを以下のように定義します。

class Player(BasePlayer):
    contribution = models.CurrencyField(
        min=0,
        max=C.ENDOWMENT,
        label="How much will you contribute?"
    )

実験参加者への謝金の支払いのためには、各プレイヤーの利得を記録する必要がありますが、oTreeではあらかじめプレイヤーモデルで payoff なるフィールドが定義されているため、改めてフィールドを明示的に定義する必要はありません。

各プレイヤーの意思決定データだけでなく、各グループについて、グループへの貢献額の合計と、各プレイヤーに分配されるポイント数を記録したい場合は、グループモデル class Group でこれら2つのフィールドを定義します。

class Group(BaseGroup):
    total_contribution = models.CurrencyField()
    individual_share = models.CurrencyField()

ページ

このゲームでは3つのページを表示します。

  • ページ1: プレイヤーはグループへの貢献額を決定し入力します。
  • ページ2: 待機ページ。プレイヤーはグループ内の他の参加者の操作が終わるまで待機します。
  • ページ3: プレイヤーに結果が通知されます。
ページ1: Contribute

貢献額を入力するページの名前は Contribute としましょう。まず、 クラス class Contribute を定義します。Contribute ページには貢献額の入力フォームが含まれているため、 form_modelform_fields を定義する必要があります。貢献額 contribution はプレイヤーモデルのフィールドなので、 form_modelplayer と設定します。form_fields には具体的なフィールド( contribution )をリストで渡します。(詳細については、 フォーム を参照してください。)

class Contribute(Page):

    form_model = 'player'
    form_fields = ['contribution']

次に、HTMLテンプレートを作成します。

{{ title block }} ブロック内にタイトルとして Contribute と入力し、 {{ content block }} ブロック内を以下のように設定します。

<p>
    This is a public goods game with
    {{ C.PLAYERS_PER_GROUP }} players per group,
    an endowment of {{ C.ENDOWMENT }},
    and a multiplier of {{ C.MULTIPLIER }}.
</p>

{{ formfields }}

{{ next_button }}
ページ2: ResultsWaitPage

すべてのプレイヤーが Contribute ページを完了した後、各プレイヤーの利得を計算します。利得の計算は、set_payoffs なる関数を定義して、そこで実行することにしましょう。関数 set_payoffs はグループモデルの変数や関数を利用するため、引数として group なるオブジェクトを受け取ることにします。関数 set_payoffs は具体的に以下のように定義します。

def set_payoffs(group):
    players = group.get_players()
    contributions = [p.contribution for p in players]
    group.total_contribution = sum(contributions)
    group.individual_share = group.total_contribution * C.MULTIPLIER / C.PLAYERS_PER_GROUP
    for player in players:
        player.payoff = C.ENDOWMENT - player.contribution + group.individual_share

プレイヤーが貢献額を意思決定した後、他のプレイヤーが意思決定し終わるのを待ってから関数 set_payoffs を呼び出す必要があります。グループ内の他のプレイヤーが意思決定を完了するまで待機するページ WaitPage を追加します。ここでは WaitPage の名前を ResultsWaitPage としましょう( class ResultsWaitPage (WaitPage): とクラスを定義する)。プレイヤーが ResultsWaitPage ページに到達すると、グループ内の他のすべてのプレイヤーが ResultsWaitPage ページに到達するまで待機します(詳細は、 Wait page を参照してください)。

class ResultsWaitPage 内で、動かしたい関数の名前( set_payoffs )を組み込み変数 after_all_players_arrive に渡します。この設定により、すべてのプレイヤーが ResultsWaitPage ページに到達したことをトリガーとして関数 set_payoffs が呼び出されます。

after_all_players_arrive = 'set_payoffs'
ページ3: Results

次に、 結果を表示する Results という名前のページを作成します(クラス class Results を定義する)。テンプレートの {{ content block }} ブロックを以下のように設定します。

<p>
    You started with an endowment of {{ C.ENDOWMENT }},
    of which you contributed {{ player.contribution }}.
    Your group contributed {{ group.total_contribution }},
    resulting in an individual share of {{ group.individual_share }}.
    Your profit is therefore {{ player.payoff }}.
</p>

{{ next_button }}

ページの順番の定義

表示するページの順番を以下のようにして設定します。

page_sequence = [
    Contribute,
    ResultsWaitPage,
    Results
]

セッション構成の定義

settings.py の SESSION_CONFIGS に 新しいセッションを追加し、 app_sequence にアプリ名 my_public_goods をリストとして渡します。

プログラムの実行

oTreeを(再)起動し、ブラウザを開いて http://localhost:8000 に接続してください。

print()を利用したトラブルシューティング

プログラミングフォーラムで、「プログラムが機能していません。コードを何時間も見ても、間違いを見つけることができません」などのメッセージがしばしば投稿されます。

エラーが見つかるまでコードをじっくり目で追っていてはいけません。プログラムをインタラクティブにテストすることが肝心です。

最も簡単な方法は、 print() を使用することです。このテクニックを学ばないと、ゲームを効果的にプログラムすることができません。

たとえば、利得を計算する関数 set_payoffs の中で、次のような print 文を挿入してみましょう。

print('group.total_contribution is', group.total_contribution)

機能していないコードの部分にこの print 文を配置します。ブラウザでゲームをプレイし、そのコードが実行されると、 print コマンドによる出力がコンソールログに表示されます(Webブラウザでは表示されません)。

print 文はいくらでも挿入することができます。

print('in payoff function')
contributions = [p.contribution for p in players]
print('contributions:', contributions)
group.total_contribution = sum(contributions)
group.individual_share = group.total_contribution * C.MULTIPLIER / C.PLAYERS_PER_GROUP
print('individual share', group.individual_share)
if group.individual_share > 100:
    print('inside if statement')
    for player in players:
        player.payoff = C.ENDOWMENT - p.contribution + group.individual_share
        print('payoff after', p.payoff)

Part 3: 信頼ゲーム

ここでは、2人でプレイされる 信頼ゲーム を作成し、oTreeの機能をさらに学びましょう。

まず、プレイヤー1(P1)は10ポイントを獲得します。この時点でプレイヤー2(P2)は何も受け取りません。P1は、ポイントの一部またはすべてをP2に渡すことができます。P2は、P1が渡したポイントの3倍を受け取ります。P2がポイントを受け取った後、受け取ったポイントの一部またはすべてをP1に渡すことができます。

以下で解説する信頼ゲームの完全なコードは ここ にあります。

アプリの作成

新たにアプリを作成し、名前を my_trust とします。

定数

定数を定義するクラス class C に移動します。

初期保有は10ポイントです。 ENDOWMENT なる変数を定義してその値を 10 に設定しましょう。P1からP2へポイントを渡すときの倍増率は3です。 MULTIPLICATION_FACTOR なる変数を定義してその値を 3 に設定しましょう。class C の中身は以下のようになります。

class C(BaseConstants):
    NAME_IN_URL = 'my_trust'
    PLAYERS_PER_GROUP = 2
    NUM_ROUNDS = 1

    ENDOWMENT = cu(10)
    MULTIPLICATION_FACTOR = 3

モデル (記録すべき実験データの定義)

次に、プレイヤーとグループにフィールドを定義します。分析のために必要な意思決定データは、P1が渡したポイント数( sent_amount )とP2が返したポイント数( sent_back_amount )です。この2つのデータを記録するためにフィールドを定義しなければなりません。

あなたは直観的に、次のようにプレイヤーのフィールドを定義してしまうかもしれません。

# Don't copy paste this
class Player(BasePlayer):

    sent_amount = models.CurrencyField()
    sent_back_amount = models.CurrencyField()

上のコードは賢い実装ではありません。sent_amount は、P1にのみ適用され、P2の場合には空白となります。sent_back_amount はP2にのみ適用され、P1の場合には空白となります。データモデルをより正確に記述するためにはどうすればよいのでしょうか?

sent_amountsent_back_amount のフィールドはグループのレベルで定義すると良いでしょう。以下のように、グループモデル( class Group )でこれら2つのフィールドを定義します。

class Group(BaseGroup):

    sent_amount = models.CurrencyField(
        label="How much do you want to send to participant B?"
    )
    sent_back_amount = models.CurrencyField(
        label="How much do you want to send back?"
    )

sent_back_amount をドロップダウンメニューで選択する形式で回答させる実装を考えましょう。oTreeには、選択肢を動的に生成するための関数が用意されています。これは {field_name}_choices と呼ばれる機能であり、 動的なフォームの検証 で説明されています。ここでは以下のように sent_back_amount_choices なる関数を定義します。使われている currency_range はポイント数の等差数列を返すoTreeの組み込み関数です。

def sent_back_amount_choices(group):
    return currency_range(
        0,
        group.sent_amount * C.MULTIPLICATION_FACTOR,
        1
    )

ページ

このゲームでは3つのページを表示します。

  • ページ1: P1がP2に渡すポイント数( sent_amount )を回答します。
  • ページ2: P2がP1に返すポイント数( sent_back_amount )を回答します。
  • ページ3: P1とP2の両方に結果が通知されます。
ページ1: Send
class Send(Page):

    form_model = 'group'
    form_fields = ['sent_amount']

    @staticmethod
    def is_displayed(player):
        return player.id_in_group == 1

クラス class Send を定義します。 form_fields には sent_amount を指定します。Send ページをP1にのみ表示するために、組み込みの is_displayed() を使用します。P2は Send ページをスキップします。id_in_group の詳細については グループ を参照してください。

HTMLテンプレートファイルを設定します。{{ title block }} ブロック内にタイトルとして Trust Game: Your Choice と入力します。本文は {{ content block }} ブロック内を以下のように設定します。

<p>
You are Participant A. Now you have {{C.ENDOWMENT}}.
</p>

{{ formfields }}

{{ next_button }}
ページ2: SendBack

HTMLテンプレートファイルを設定します。{{ title block }} ブロック内にタイトルとして Trust Game: Your Choice と入力します。本文は {{ content block }} ブロック内を以下のように設定します。

<p>
    You are Participant B. Participant A sent you {{group.sent_amount}}
    and you received {{tripled_amount}}.
</p>

{{ formfields }}

{{ next_button }}

クラス class SendBack でこれまでと同様に form_fieldsis_displayed などを設定します。テンプレートの中で、P2が受け取るポイント数(P1が渡したポイントの3倍)を {{tripled_amount}} として表示していることに注目してください。変数 tripled_amountclass SendBack の中で定義する必要があります。

  • テンプレートに渡す変数 tripled_amountvars_for_template() を使用して設定します。HTMLコードでは、ポイント数を3倍する、というような計算を直接行えないため、Pythonコードで計算してからテンプレートに渡す必要があります。
class SendBack(Page):

    form_model = 'group'
    form_fields = ['sent_back_amount']

    @staticmethod
    def is_displayed(player):
        return player.id_in_group == 2

    @staticmethod
    def vars_for_template(player):
        group = player.group

        return dict(
            tripled_amount=group.sent_amount * C.MULTIPLICATION_FACTOR
        )
ページ3: Results

結果を表示する Results ページのHTMLテンプレートファイルを設定します。{{ title block }} ブロック内にタイトルとして Results と入力します。本文は {{ content block }} ブロック内を設定します。P1とP2で表示する内容を変える必要があるため、 {{ if }} 文を使用して、 id_in_group による条件分岐を行います。

{{ if player.id_in_group == 1 }}
    <p>
        You sent Participant B {{ group.sent_amount }}.
        Participant B returned {{ group.sent_back_amount }}.
    </p>
{{ else }}
    <p>
        Participant A sent you {{ group.sent_amount }}.
        You returned {{ group.sent_back_amount }}.
    </p>

{{ endif }}

<p>
Therefore, your total payoff is {{ player.payoff }}.
</p>
class Results(Page):
    pass
WaitPageとページの順番

2つの待機ページを追加します。

  • WaitForP1 : P1が意思決定している間、P2は待機します。
  • ResultsWaitPage : P2が意思決定している間、P1は待機します。

2番目のWaitPage ( ResultsWaitPage ) の後 Results ページへ遷移する前に、利得を計算する必要があります。利得を計算する関数 set_payoffs を定義します。

def set_payoffs(group):
    p1 = group.get_player_by_id(1)
    p2 = group.get_player_by_id(2)
    p1.payoff = C.ENDOWMENT - group.sent_amount + group.sent_back_amount
    p2.payoff = group.sent_amount * C.MULTIPLICATION_FACTOR - group.sent_back_amount

関数 set_payoffs をP2の意思決定が完了したタイミングで呼び出すために、 class ResultsWaitPage の中で、 after_all_players_arrive を設定します。

after_all_players_arrive = set_payoffs

ページの順番は page_sequence にページ名のリストを渡すことで設定してください。

page_sequence = [
    Send,
    WaitForP1,
    SendBack,
    ResultsWaitPage,
    Results,
]

SESSION_CONFIGS をエントリーに追加

settings.py の SESSION_CONFIGS に 新しいセッションを追加し、 app_sequence にアプリ名 my_trust をリストとして渡します。

プログラムの実行

oTreeを(再)起動し、ブラウザを開いて http://localhost:8000 に接続してください。

コンセプト

Sessions

oTreeにおけるセッションとは、複数の参加者が一連のタスクやゲームに参加するイベントのことです。セッションの例は以下の通りです。

“多数の参加者が研究室に来て、公共財ゲームを行った後、アンケートを行います。参加者には、実験参加費として10ユーロが支払われ、それに加えてゲームの結果に応じた収益が支払われます。”

Subsessions

セッションは一連のサブセッションの集まりであり、サブセッションとはセッションを構成する「セクション」または「モジュール」のことです。例えば、セッションが公共財ゲームとアンケートで構成されている場合、公共財ゲームがサブセッション1、アンケートがサブセッション2となります。また、各サブセッションは、一連のページで構成されています。例えば、4ページの公共財ゲームの後に2ページのアンケートがあったとします。

_images/session_subsession.png

ゲームが複数ラウンドに渡る繰り返しゲームである場合、各ラウンドはサブセッションとなります。

Groups

各サブセッションは、さらにプレイヤーのグループに分けることができます。例えば、30人のプレイヤーがいるサブセッションを2人ずつの15のグループに分けることができます。(注: サブセッション間でグループを入れ替えることができます)。

オブジェクトの階層性

oTreeの構造は次のような階層に分けられます。

Session
    Subsession
        Group
            Player
  • セッションとは、一連のサブセッションのことです。
  • サブセッションには、複数のグループが含まれます。
  • グループには、複数のプレイヤーが含まれます。
  • 各プレイヤーは複数のページを進めることになります。

下位のオブジェクトから任意の上位のオブジェクトにアクセスできます。

player.participant
player.group
player.subsession
player.session
group.subsession
group.session
subsession.session

Participant

oTreeでは、”player “と “participant “では異なる意味があります。”player “と “participant “の関係は、セッションとサブセッションの関係と同じです。

_images/participant_player.png

“player”とは、ある特定のサブセッションの”participant”のインスタンスのことあり、”participant”が演じる一時的な「役割」のようなものです。”participant”は、初のサブセッションではプレイヤー2、次のサブセッションではプレイヤー1、といった具合です。

モデル

oTree アプリには 3 つのデータモデル「サブセッション(Subsession)、グループ(Group)、プレイヤー(Player)」があります。

プレイヤーはグループの一部であり、グループはサブセッションの一部です。詳しくは コンセプト を参照してください。

例えば、次のようなデータを生成する実験を考えてみましょう:

name    age is_student
John    30  False
Alice   22  True
Bob     35  False
...

上記のテーブル構造を定義する方法は以下の通りです:

class Player(BasePlayer):
    name = models.StringField()
    age = models.IntegerField()
    is_student = models.BooleanField()

つまり、 モデル とは基本的にデータベースのテーブルです。そして、 フィールド とはテーブルの列のことです。

フィールド

フィールドタイプ

  • BooleanField (true/false や yes/no の値用)
  • CurrencyField (通貨額用); 詳しくは 通貨 (Currency) を参照
  • IntegerField
  • FloatField (実数用)
  • StringField (文字列用)
  • LongStringField (長い文字列用; このフィールドのフォームウィジェットは複数行のテキストエリアになります)

初期値 / デフォルト値

initial= で設定しない限り、フィールドの初期値は None になります:

class Player(BasePlayer):
    some_number = models.IntegerField(initial=0)

最小値、最大値、選択肢

フィールドの最小値 (min), 最大値 (max), 選択肢 (choices) を設定する方法は シンプルなフォームの検証 を参照してください。

組み込みのフィールドとメソッド

プレイヤー、グループ、サブセッションには、いくつかの定義済みフィールドがあります。たとえば、 Player には payoffid_in_group というフィールドがあり、 in_all_rounds()get_others_in_group() などのメソッドがあります。

これらの組み込みフィールドとメソッドを以下に示します。

Subsession

round_number

現在のラウンド数を返します。アプリが複数のラウンドを持っている場合 (C.NUM_ROUNDS で設定) にのみ関係します。詳しくは ラウンド を参照してください。

get_groups()

サブセッション内のすべてのグループをリストで返します。

get_players()

サブセッション内のすべてのプレイヤーをリストで返します。

Player

id_in_group

自動的に割り当てられる1から始まる整数。マルチプレイヤーゲームでは、このフィールドがプレイヤー1なのか、プレイヤー2なのか、などを示す。

payoff

このラウンドでのプレイヤーの利得。詳しくは 利得 (payoff) を参照。

round_number

現在のラウンド数を返します。

Session

num_participants

セッションの参加者数

vars

session fields を参照

Participant

id_in_session

セッション内の参加者のID。このフィールドは、プレイヤーの id_in_subsession と同じです。

その他の参加者の属性とメソッド

Constants

C は、アプリのパラメータやプレイヤーごとに変化することがない定数を定義するために推奨される場所です。

組み込みの定数を以下に示します:

アプリの実名を URL に表示したくない場合は、文字列定数 NAME_IN_URL に任意の名前を定義します。

定数には、数字、文字列、ブーリアン、リストなどを定義できます。しかし、辞書型や辞書型のリストなど、より複雑なデータタイプの場合は、代わりに関数の中で定義する必要があります。例えば、 my_dict という定数を定義する代わりに、次のようにします:

def my_dict(subsession):
    return dict(a=[1,2], b=[3,4])

ページ

参加者に表示される各ページは、 Page によって定義されます。

ページの順序は page_sequence で指定します。

ゲームに複数のラウンドがある場合は、この順序が繰り返されます (ラウンド 参照)。

Page は以下のようなメソッドや属性を持つことができます。

is_displayed()

ページを表示すべき場合には True を、ページをスキップすべき場合には False を返すようにこの関数を定義します。この関数が省略された場合は、そのページは表示されます。

例えば、各グループの P2 にだけページを表示する場合は以下のようになります:

@staticmethod
def is_displayed(player):
    return player.id_in_group == 2

また、ラウンド 1 だけページを表示する場合は以下のようになります:

@staticmethod
def is_displayed(player):
    return player.round_number == 1

多くのページで同じルールを繰り返す必要がある場合は、 app_after_this_page を使用してください。

vars_for_template()

テンプレートに変数を渡す場合に使用します。例えば、次のようにします:

@staticmethod
def vars_for_template(player):
    a = player.num_apples * 10
    return dict(
        a=a,
        b=1 + 1,
    )

このとき、テンプレートでは次のように ab にアクセスできます:

Variables {{ a }} and {{ b }} ...

oTree automatically passes the following objects to the template: player, group, subsession, participant, session, and C. You can access them in the template like this: {{ C.BLAH }} or {{ player.blah }}.

ユーザーがページを更新すると、 vars_for_template は再実行されます。

before_next_page()

ここでは、フォームの検証後、プレイヤーが次のページに進む前に実行されるコードを定義します。

is_displayed でページがスキップされた場合は、 before_next_page もスキップされます。

例:

@staticmethod
def before_next_page(player, timeout_happened):
    player.tripled_apples = player.num_apples * 3

timeout_seconds

タイムアウト を参照

Wait pages

Wait page を参照

ページ順序のランダム化

ラウンドを使ってページの順番をランダムにすることができます。 こちら で例を確認できます。

app_after_this_page

アプリ全体をスキップするには、 app_after_this_page を定義します。例えば、次のアプリにスキップするには、次のようにします:

@staticmethod
def app_after_this_page(player, upcoming_apps):
    if player.whatever:
        return upcoming_apps[0]

upcoming_apps は、 app_sequence (文字列のリスト) の残りの部分です。したがって、最後のアプリにスキップするには、 upcoming_apps[-1] を返すようにします。また、ハードコードされた文字列を返すこともできます (ただし、その文字列が upcoming_apps に含まれている必要があります):

@staticmethod
def app_after_this_page(player, upcoming_apps):
    print('upcoming_apps is', upcoming_apps)
    if player.whatever:
        return "public_goods"

この関数が何も返さなかった場合、プレイヤーは通常通り次のページに進みます。

テンプレート

テンプレートの構文

変数

以下ようにして変数を表示することができます:

Your payoff is {{ player.payoff }}.

テンプレートでは、以下の変数が使用できます:

  • player: 現在ページを見ているプレイヤー
  • group: 現在のプレイヤーが所属しているグループ
  • subsession: 現在のプレイヤーが所属しているサブセッション
  • participant: 現在のプレイヤーが所属している参加者
  • session: 現在のセッション
  • C
  • vars_for_template() で渡した変数

条件文 ("if")

注釈

oTree 3.x では、テンプレートに {{ }}{% %} の 2 種類のタグを使用していました。しかし、 oTree 5 からは、 {% %} を使用する必要はなく、 {{ }} をどこでも使用することができます。以前の形式でも動作します。

'else' を付けた例:

Complex example:

ループ ("for")

{{ for item in some_list }}
    {{ item }}
{{ endfor }}

辞書型内のアイテムへのアクセス

Python のコードでは my_dict['foo'] でアクセスしますが、テンプレートでは {{ my_dict.foo }} でアクセスします。

コメント

{# this is a comment #}

{#
    this is a
    multiline comment
#}

できないこと

テンプレート言語は値を表示するだけのものです。演算 (+, *, /, -) や数値、リスト、文字列などの変更はできません。そのようなことをしたい場合には、 vars_for_template() を使用してください。

テンプレートの仕組み: 例

oTree のテンプレートは 2 つの言語が混在しています:

  • HTML (<this></this> のような山括弧を使います)
  • テンプレートタグ({{ this }} のような中括弧を使用します)

この例では、テンプレートが次のようになっているとします:

<p>Your payoff this round was {{ player.payoff }}.</p>

{{ if subsession.round_number > 1 }}
    <p>
        Your payoff in the previous round was {{ last_round_payoff }}.
    </p>
{{ endif }}

{{ next_button }}

ステップ 1: oTree はテンプレートタグをスキャンし、 HTML を生成します ("サーバサイド")

oTree は、変数の現在の値を使って、上記のテンプレートタグを次のようなプレーンな HTML に変換します:

<p>Your payoff this round was $10.</p>

    <p>
        Your payoff in the previous round was $5.
    </p>

<button class="otree-btn-next btn btn-primary">Next</button>

ステップ 2: ブラウザが HTML タグをスキャンし、ウェブページを作成する ("クライアントサイド")

oTree サーバは、この HTML をユーザのコンピュータに送信し、ユーザのウェブブラウザがコードを読み取って、フォーマットされたウェブページとして表示します:

_images/template-example.png

そのため、ブラウザはテンプレートタグを直接読み取ることはありません。

キーポイント

もし、あるページが思い通りに表示されない場合は、上記のどのステップで間違っていたのか切り分けることができます。ブラウザで右クリックして "ページのソースを表示" を選択してください。 (注: 画面分割モードでは "ページのソースを表示" が機能しない場合があります。)

すると、生成された HTML (JavaScript や CSS も含む) を見ることができます。

  • HTML コードが期待通りに表示されない場合は、サーバ側で何か問題が発生しています。 vars_for_template やテンプレートタグに間違いがないか確認してください。
  • HTML コードの生成にエラーがなかった場合は、 HTML (または JavaScript) の構文に問題があると考えられます。問題のある部分の HTML をテンプレートタグなしでテンプレートに貼り直し、正しい出力が得られるまで編集してみてください。その後、テンプレートタグを元に戻して、再び動的にしてください。

画像 (静的ファイル)

画像、動画、サードパーティの JS/CSS ライブラリ、その他の静的ファイルをプロジェクトに使用する最も簡単な方法は、 Dropbox, Imgur, YouTube などのオンラインサービスを利用することです。

そして、その URL を <img> や <video> タグでテンプレートに記述します:

<img src="https://i.imgur.com/gM5yeyS.jpg" width="500px" />

また、プロジェクトに直接画像を保存することもできます。 (ただし、ファイルサイズが大きいとパフォーマンスに影響するので注意が必要です)。 oTree Studio には、画像アップロードツールがあります。 (テキストエディタを使用している場合は、 こちら を参照してください。) 画像を保存したら、次のようにすることで表示することができます:

<img src="{{ static 'folder_name/puppy.jpg' }}"/>

動的な画像表示

文脈に応じて異なる画像を表示する必要がある場合 (ラウンド毎に異なる画像を表示するなど)、 vars_for_template で使用する画像のパスを構築し、テンプレートに渡すことができます:

@staticmethod
def vars_for_template(player):
    return dict(
        image_path='my_app/{}.png'.format(player.round_number)
    )

そして、テンプレートの中では次のようにします:

<img src="{{ static image_path }}"/>

インクルード可能なテンプレート

多くのテンプレートに同じ内容をコピーペーストする場合は、インクルード可能なテンプレートを作成し、 {{ include_sibling }} で再利用するのがよいでしょう。

例えば、ゲームの説明がすべてのページで繰り返し表示される必要がある場合、 instructions.html というテンプレートを作成し、そこに説明を書く方法があります:

<div class="card bg-light">
    <div class="card-body">

    <h3>
        Instructions
    </h3>
    <p>
        These are the instructions for the game....
    </p>
    </div>
</div>

Then use {{ include_sibling 'instructions.html' }} to insert it anywhere you want.

注釈

{{ include_sibling }} is a new alternative to {{ include }}. The advantage is that you can omit the name of the app: {{ include_sibling 'xyz.html' }} instead of {{ include 'my_app/xyz.html' }}. However, if the includable template is in a different folder, you must use {{ include }}.

JavaScript, CSS

JavaScript/CSS コードの設置場所

JavaScript や CSS は <script></script><style></style> を使うことで、テンプレート内のどこにでも置くことができます。

たくさんのスクリプトやスタイルシートがある場合は、 content ブロックの外にある scriptsstyles ブロックに入れることができます。これは必須ではありませんが、コードを整理し、正しい順序で読み込まれるようにすることができます (CSS、ページコンテンツ、JavaScriptの順)。

テーマのカスタマイズ

oTree 要素の見た目をカスタマイズしたい場合、以下のリストにある CSS セレクタをカスタマイズします:

要素 CSS/jQuery セレクタ
ページ本文 .otree-body
ページタイトル .otree-title
待機ページ (ダイアログ全体) .otree-wait-page
待機ページのダイアログタイトル .otree-wait-page__title (note: __, not _)
待機ページのダイアログ本文 .otree-wait-page__body
タイマー .otree-timer
次へボタン .otree-btn-next
フォームエラーの警告 .otree-form-errors

例えば、ページの幅を変更するには、ベースとなるテンプレートに以下のような CSS を記述します:

<style>
    .otree-body {
        max-width:800px
    }
</style>

より詳細な情報を得るためには、ブラウザで修正したい要素を右クリックし、 "検証" を選択します。すると、さまざまな要素が表示され、そのスタイルを変更することができます:

_images/dom-inspector.png

可能な限り、上記の公式セレクタのいずれかを使用してください。 _otree で始まるセレクタは使わないでください。また、 btn-primarycard のような Bootstrap のクラスは選択しないでください。

Python から JavaScript へのデータの受け渡し (js_vars)

テンプレート内の JavaScript コードにデータを渡すには、Pageに js_vars 関数を定義します:

@staticmethod
def js_vars(player):
    return dict(
        payoff=player.payoff,
    )

そして、テンプレートの中で、これらの変数を参照することができます:

<script>
    let x = js_vars.payoff;
    // etc...
</script>

Bootstrap

oTreeには、ウェブサイトのユーザーインターフェースをカスタマイズするための人気ライブラリである Bootstrap が搭載されています。

カスタムスタイル や、テーブル、アラート、プログレスバー、ラベルなどの 特定のコンポーネント を必要な場合に使用することができます。 ポップオーバー, モーダル, 折り畳み可能なテキスト などの要素を使って、動的なページを作ることもできます。

Bootstrap を使用するには、HTML の要素に class= 属性を追加します。

例えば、以下の HTML は "成功" のアラートを作成します:

<div class="alert alert-success">Great job!</div>

モバイルデバイス

Bootstrap は、スマートフォンやタブレットで見たときにモバイル対応のバージョンを表示しようとします。

Best way to test on mobile is to use Heroku. otree zipserver doesn't accept a 'port' argument. Also, devserver/zipserver seem to have issues with shutdown/reloading and freeing up the port.

グラフ

アプリにグラフを追加するには、任意の HTML/JavaScript ライブラリを使用できます。円グラフ、折れ線グラフ、棒グラフ、時系列グラフなどを描画するには、 HighCharts が適しています。

まず、HighCharts の JavaScript をインクルードします:

<script src="https://code.highcharts.com/highcharts.js"></script>

HighCharts の デモサイト にアクセスし、作りたい種類のグラフを探します。そして、 "Edit in JSFiddle" をクリックして、ソースコードのデータを書き換えて編集します。

そして、その JS と HTML をテンプレートにコピーペーストして、ページを読み込みます。グラフが表示されない場合は、JSコードがチャートを挿入しようとする <div> がHTMLに含まれていないことが原因かもしれません。

グラフが正しく読み込まれたら、 seriescategories などのソースコードに書き込まれたデータを、動的に生成された変数に置き換えることができます。

例えば、以下のコードがあるとします:

series: [{
    name: 'Tokyo',
    data: [7.0, 6.9, 9.5, 14.5, 18.2, 21.5, 25.2, 26.5, 23.3, 18.3, 13.9, 9.6]
}, {
    name: 'New York',
    data: [-0.2, 0.8, 5.7, 11.3, 17.0, 22.0, 24.8, 24.1, 20.1, 14.1, 8.6, 2.5]
}]

これを以下のように変更できます:

series: js_vars.highcharts_series

ここで highcharts_seriesjs_vars で定義した変数です。

グラフが読み込まれない場合は、ブラウザの "ページのソースを表示" をクリックして、動的に生成したデータに何か問題がないか確認してください。

その他

to2, to1, or to0 フィルタを使って、数字を丸めることができます。例えば {{ 0.1234|to2}} とすれば 0.12 が表示されます。

フォーム

oTreeの各ページはフォームを持ち、プレイヤーはフォームに入力し、 "Next" ボタンを押すことでフォームに入力したデータを送信できます。フォームを作成するためにまず、Playerクラスにフィールドを作成する必要があります。例えば、

class Player(BasePlayer):
    name = models.StringField(label="Your name:")
    age = models.IntegerField(label="Your age:")

次に、Pageクラスで form_modelform_fields を設定します。

class Page1(Page):
    form_model = 'player'
    form_fields = ['name', 'age'] # this means player.name, player.age

ユーザがフォームデータを送信したとき、送信されたデータは、Playerクラス内の対応するフィールドに自動的に保存されます。

テンプレート中のフォーム

テンプレートにおいて、次の形式でフォームを表示できます。

{{ formfields }}

シンプルなフォームの検証

最大値、最小値

例: 12から24までの整数が必要な時

offer = models.IntegerField(min=12, max=24)

フォームで設定される最大/最小値を動的に設定したい場合は、 {field_name}_max() を使用する必要があります。

選択肢

フォームを選択肢付きのドロップダウンメニューにしたい場合は、 choices= を次のように設定します。

level = models.IntegerField(
    choices=[1, 2, 3],
)

ダ六腑ダウンの代わりにラジオボタンを使いたい場合は、 widgetRadioSelectRadioSelectHorizontal を設定して下さい。:

level = models.IntegerField(
    choices=[1, 2, 3],
    widget=widgets.RadioSelect
)

選択肢を動的に設定したい場合は、 {field_name}_choices() を使う必要があります。

[value, display]のペアをリストに設定することで、選択肢に対応する表示名を設定することができます。:

level = models.IntegerField(
    choices=[
        [1, 'Low'],
        [2, 'Medium'],
        [3, 'High'],
    ]
)

上記のコードを記述した場合、 "Low" , "Medium, "High" と共にメニューが表示されます。ただし、1,2,3として選択は保存されます。

BooleanFieldStringField 等を使いたい場合は、下記のように記述します。:

cooperated = models.BooleanField(
    choices=[
        [False, 'Defect'],
        [True, 'Cooperate'],
    ]
)

次のようにすることで、人間にとって読みやすい形でユーザの選択を取得することができます。

player.cooperated  # returns e.g. False
player.field_display('cooperated')  # returns e.g. 'Defect'

注釈

field_display はoTree 5 (2021年3月) からの新機能です。

任意の入力フォーム

フォームへの入力が任意である場合、 blank=True を下記のように使います。:

offer = models.IntegerField(blank=True)

動的なフォームの検証

min 、 maxchoices は固定値限定です。

動的に検証したい場合(例えば、プレイヤーごとに入力できる値の範囲が異なる)は、代わりに下記の関数を定義してください。

{field_name}_choices()

choices= で設定することで、フォームの選択肢を設定できます。

例:

class Player(BasePlayer):
    fruit = models.StringField()


def fruit_choices(player):
    import random
    choices = ['apple', 'kiwi', 'mango']
    random.shuffle(choices)
    return choices

{field_name}_max()

max= を動的に設定したい場合の手段として用いることができます。例:

class Player(BasePlayer):
    offer = models.CurrencyField()
    budget = models.CurrencyField()


def offer_max(player):
    return player.budget

{field_name}_min()

min= を動的に設定したい場合の手段として用いることができます。

{field_name}_error_message()

これは最も柔軟にフォームの検証ができる方法です。

class Player(BasePlayer):
    offer = models.CurrencyField()
    budget = models.CurrencyField()

def offer_error_message(player, value):
    print('value is', value)
    if value > player.budget:
        return 'Cannot offer more than your remaining budget'

複数フォームの検証

3つの数値を入力するフォームがあり、その値の合計が100にならなければならないとします。その場合、ページクラスに error_message 関数を下記のように設定することで検証できます。

class MyPage(Page):

    form_model = 'player'
    form_fields = ['int1', 'int2', 'int3']

    @staticmethod
    def error_message(player, values):
        print('values is', values)
        if values['int1'] + values['int2'] + values['int3'] != 100:
            return 'The numbers must add up to 100'

注意

  • フォームが空欄でもよい場合(blank=True に設定した場合)、値は None となります。
  • この関数はフォームに他のエラーがない場合にのみ実行されます。
  • エラーメッセージにフィールドをマップすることもでき、FIELD_error_message methods メソッドを このように 繰り返す必要がありません。

動的なフォームフィールドの決定

動的なフォームフィールドのリストが必要な時、 form_fields の代わりに get_form_fields 関数を定義できます。

@staticmethod
def get_form_fields(player):
    if player.num_bids == 3:
        return ['bid_1', 'bid_2', 'bid_3']
    else:
        return ['bid_1', 'bid_2']

Widgets

ドロップダウンメニューの代わりに、ラジオボタンを使いたい場合は、modelの widgetRadioSelect または RadioSelectHorizontal に設定して下さい。

{{ formfield }}

フォームを個別に配置したい場合は、 {{ formfields }} の代わりに、 {{ formfield }} を使うことができます。:

{{ formfield 'bid' }}

テンプレート内で直接 label を配置することもできます。

{{ formfield 'bid' label="How much do you want to contribute?" }}

過去のバージョンでの構文 {% formfield player.bid %} は引き続きサポートされます。

フォームの外観のカスタマイズ

自動的にフォームフィールドに必要なすべてのパーツ(入力、ラベル、エラーメッセージ)を出力できるので、 {{ formfields }}{{ formfield }} が使いやすいです。

ただし、外観やレイアウトを変更したい場合、手動で設定したフォームのレンダリングを使用できます。{{ formfield 'my_field' }} の代わりに、 {{ form.my_field }} を実行することで、入力要素のみを取得することができます。また、 {{ formfield_errors 'my_field' }} を含むことも忘れないでください。

Example: Radio buttons arranged like a slider

pizza = models.IntegerField(
    widget=widgets.RadioSelect,
    choices=[-3, -2, -1, 0, 1, 2, 3]
)
<p>Choose the point on the scale that represents how much you like pizza:</p>
<p>
    Least &nbsp;
    {{ for choice in form.pizza }}
        {{ choice }}
    {{ endfor }}
    &nbsp; Most
</p>

例: 表やそのほかのカスタムレイアウト中のラジオボタン

model 内に IntegerField のセットがあるとする。

class Player(BasePlayer):
    offer_1 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])
    offer_2 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])
    offer_3 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])
    offer_4 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])
    offer_5 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])

各選択肢が別々の列にある、リッカート尺度としてそれらを提示します。

(まず、 多くのフィールドを作成する方法 の手順に従い、model内のコードの重複を減らしてください。)

選択肢は表のセルで区切られており、通常の RadioSelectHorizontal は機能しません。

代わりに、次のようにフィールドの選択肢をループさせてください。

<tr>
    <td>{{ form.offer_1.label }}</td>
    {{ for choice in form.offer_1 }}
        <td>{{ choice }}</td>
    {{ endfor }}
</tr>

同数の選択肢を持つフィールドが多くある場合は、それらを表に配置できます。

<table class="table">
    {{ for field in form }}
        <tr>
            <th>{{ field.label }}</th>
            {{ for choice in field }}
                <td>{{ choice }}</td>
            {{ endfor }}
        </tr>
    {{ endfor }}
</table>

独自のHTMLウィジェット

{{ formfields }}手動のフィールドレンダリング を利用しても理想の外観にならないとき、HTMLの生コード上でウィジェットを記述できます。ただし、oTreeによって提供される便利な機能を利用することはできません。例えば、フォームにエラーが発生したり、ページがリロードされたときに、ユーザが入力した情報は消去されてしまう可能性があります。

まず、 <input> を加えます。例えば、 form_fieldsmy_field を含むのであれば、 <input name="my_field" type="checkbox" /> を実行することができます。(他にも、 radiotextnumberrange がああります。)

次に、参加者が誤った値を入力してしまった場合に、エラーメッセージが表示されるように、 {{ formfield_errors 'xyz' }} を含める必要があります。

HTMLコードの例: JavaScriptを用いたカスタムユーザインタフェース

ユーザがフォームフィールに入力するのではなく、チャートのクリックやグラフィカルなゲームの操作等、ある種のビジュアルアプリを操作するとします。または、ページの一部に費やした時間やクリックした回数等、データを追加で記録したいとします。

まず、HTMLやJavaScriptを利用するインタフェースを構成してください。次に、JavaScriptを利用して非表示のフォームに結果を書き出します。例:

# Player class
contribution = models.IntegerField()

# page
form_fields = ['contribution']

# HTML
<input type="hidden" name="contribution" id="contribution" />

# JavaScript
document.getElementById('contribution').value = 42;

ページが送信されると、非表示のフォームに書き出された値が、ほかのフォームのようにoTreeに記録されます。

これが機能しない場合は、利用しているブラウザのJavaScriptコンソールを開き、エラーが出ていないか確認してください。そして、 console.log()print() に相当する命令)を利用して、コードの実行をトレースすることをおすすめします。

ボタン

フォームを送信するボタン

{{ next_button }} を省略し、代わりにいくつかのボタンの一つをクリックして、次のページへ遷移させることができます。

例えば、Playerクラスが offer_accepted = models.BooleanField() を持っており、ラジオボタンではなく、下記のようなボタンを表示したいとします。

_images/yes-no-buttons.png

まず、ページの form_fieldsoffer_accepted を設定して下さい。次に、テンプレートにこのコードを記入してください。

<p>Do you wish to accept the offer?</p>
<button name="offer_accepted" value="True">Yes</button>
<button name="offer_accepted" value="False">No</button>

この手法は BooleanField だけでなく、任意のタイプのフィールドに利用できます。

フォームの送信以外の目的で利用するボタン

フォームの送信以外の目的があるとき、 type="button" を追加します。

<button>
    Clicking this will submit the form
</button>

<button type="button">
    Clicking this will not submit the form
</button>

その他、高度な設定

動的ラベル付きフォームフィールド

"ラベルに変数が含まれる場合、ページに文字列を作成できます

class Contribute(Page):
    form_model = 'player'
    form_fields = ['contribution']

    @staticmethod
    def vars_for_template(player):
        return dict(
            contribution_label='How much of your {} do you want to contribute?'.format(player.endowment)
        )

次に、テンプレートで:

{{ formfield 'contribution' label=contribution_label }}

この手法を利用する場合は、 動的なフォームの検証 の利用もおすすめします。

JavaScript access to form inputs

注釈

New beta feature as of oTree 5.9 (July 2022)

In your JavaScript code you can use forminputs.xyz to access the <input> element of form field xyz. For example, you can do:

// get the value of an input
forminputs.xyz.value; // returns '42' or '' etc.

// set the value of a field.
forminputs.xyz.value = '42';

// dynamically set a field's properties -- readonly, size, step, pattern, etc.
forminputs.xyz.minlength = '10';

// do live calculations on inputs
function myFunction() {
    let sum = parseInt(forminputs.aaa.value) + parseInt(forminputs.bbb.value);
    alert(`Your total is ${sum}`);
}

// set an event handler (for oninput/onchange/etc)
forminputs.aaa.oninput = myFunction;

// call methods on an input
forminputs.xyz.focus();
forminputs.xyz.reportValidity();

Radio widgets work a bit differently:

my_radio = models.IntegerField(
    widget=widgets.RadioSelect,
    choices=[1, 2, 3]
)
// forminputs.my_radio is a RadioNodeList, not a single <input>
// so you need to loop over all 3 options:
for (let radio of forminputs.my_radio) {
    radio.required = false;
}

for (let radio of forminputs.my_radio) {
    radio.onclick = function() { alert("radio button changed"); };
}

// but the 'value' attribute works the same way as non-radio widgets
forminputs.my_radio.value = 2;

マルチプレイヤーゲーム

グループ

マルチプレイヤーゲームでは、プレイヤーをグループに分けることができます。(プレイヤーが互いに対応しない "treatment groups" の意味でのグループが必要な場合は、処理の割り当て を参照してください。)

グループサイズを設定するには、Constants の PLAYERS_PER_GROUP を設定します。たとえば、2人用のゲームの場合、 PLAYERS_PER_GROUP = 2 を設定します。

すべてのプレイヤーを同じグループに含める必要がある場合、またはシングルプレイヤーのゲームの場合は、 None に設定します。

各プレイヤーには属性 id_in_group を持っており、プレイヤー1であるか、プレイヤー2であるかなどを示します。

プレイヤーを取得

グループオブジェクトには、次のメソッドがあります。

get_players()

グループ内のプレイヤーのリストを返します。( id_in_group によって順序付けされます)

get_player_by_id(n)

id_in_group で指定されたグループ内のプレイヤーを返します。

他のプレイヤーを取得

プレイヤーオブジェクトは get_others_in_group()get_others_in_subsession() メソッドを持ち、グループおよびサブセッション内の他のプレイヤーのリストを返します。

Roles

各グループに購入者/販売者、プリンシパル/エージェントなどの複数の役割がある場合は、定数でそれらを定義できます。名前を _ROLE で終わらせます:

class C(BaseConstants):
    ...

    PRINCIPAL_ROLE = 'Principal'
    AGENT_ROLE = 'Agent

次に、oTreeはそれぞれの role を異なるプレイヤーに自動的に割り当てます。( id_in_group による順番に従う。)これを使用して、各役割にさまざまなコンテンツを表示できます。例:

class AgentPage(Page):

    @staticmethod
    def is_displayed(player):
        return player.role == C.AGENT_ROLE

テンプレートの場合:

You are the {{ player.role }}.

get_player_by_id() のように group.get_player_by_role() を使用することもできます。

def set_payoffs(group):
    principal = group.get_player_by_role(C.PRINCIPAL_ROLE)
    agent = group.get_player_by_role(C.AGENT_ROLE)
    # ...

プレイヤーの役割を切り替えたい場合は、 group.set_players()subsession.group_randomly() 等を使用して、グループを再配置する必要があります。

グループマッチング

固定マッチング

デフォルトでは、各ラウンドで、プレイヤーは C.PLAYERS_PER_GROUP 人で構成されたグループに分割されます。それらは順番にグループ化されます。たとえば、グループごとに2人のプレイヤーがいる場合、P1とP2は一緒にグループ化され、P3とP4も同様にグループ化されます。 また、 id_in_group は、各グループ内で順番に割り当てられます。

これは、デフォルトではグループは各ラウンドや、同じ PLAYERS_PER_GROUP を持っているアプリ間でも同じであることを意味します。

グループを再構成する場合は、以下の手法を使用できます。

group_randomly()

サブセッションには、プレイヤーをランダムにシャッフルする group_randomly() メソッドがあり、プレイヤーは任意のグループ、およびグループ内の任意の位置に配置できます。

グループ間でプレイヤーをシャッフルしたいが、プレイヤーの役割は固定したい場合は、 group_randomly(fixed_id_in_group=True) を使用します。

たとえば、これにより、ラウンドごとにプレイヤーがランダムにグループ化されます。

def creating_session(subsession):
    subsession.group_randomly()

これにより、プレイヤーはラウンドごとにランダムにグループ化されますが、 id_in_group は固定されたままになります。

def creating_session(subsession):
    subsession.group_randomly(fixed_id_in_group=True)

次の例では、セッションに12人の参加者がおり、 PLAYERS_PER_GROUP = 3 であると仮定します。

def creating_session(subsession):
    print(subsession.get_group_matrix()) # outputs the following:
    # [[1, 2, 3],
    #  [4, 5, 6],
    #  [7, 8, 9],
    #  [10, 11, 12]]

    subsession.group_randomly(fixed_id_in_group=True)
    print(subsession.get_group_matrix()) # outputs the following:
    # [[1, 8, 12],
    #  [10, 5, 3],
    #  [4, 2, 6],
    #  [7, 11, 9]]

    subsession.group_randomly()
    print(subsession.get_group_matrix()) # outputs the following:
    # [[8, 10, 3],
    #  [4, 11, 2],
    #  [9, 1, 6],
    #  [12, 5, 7]]
group_like_round()

グループの構成をあるラウンドから別のラウンドにコピーするには、 group_like_round(n) メソッドを使用します。このメソッドの引数は、グループ構成をコピーするラウンド数です。

以下の例では、グループはラウンド1でシャッフルされ、その後のラウンドはラウンド1のグループ構成をコピーします。

def creating_session(subsession):
    if subsession.round_number == 1:
        # <some shuffling code here>
    else:
        subsession.group_like_round(1)
get_group_matrix()

サブセッションには、グループの構造を行列として返す get_group_matrix() というメソッドがあります。次に例を示します。

[[1, 3, 5],
 [7, 9, 11],
 [2, 4, 6],
 [8, 10, 12]]
set_group_matrix()

set_group_matrix() は、グループ構造を任意の方法で変更できます。まず、 get_players() を使用して、プレイヤーのリストを取得するか、 get_group_matrix() を使用して既存のグループ行列を取得します。行列を作成し、それを set_group_matrix() に渡します。

def creating_session(subsession):
    matrix = subsession.get_group_matrix()

    for row in matrix:
        row.reverse()

    # now the 'matrix' variable looks like this,
    # but it hasn't been saved yet!
    # [[3, 2, 1],
    #  [6, 5, 4],
    #  [9, 8, 7],
    #  [12, 11, 10]]

    # save it
    subsession.set_group_matrix(matrix)

整数の行列を渡すこともできます。1からサブセッションのプレイヤー数までのすべての整数が含まれている必要があります。各整数は、プレイヤーの id_in_subsession を表します。例えば:

def creating_session(subsession):

    new_structure = [[1,3,5], [7,9,11], [2,4,6], [8,10,12]]
    subsession.set_group_matrix(new_structure)

    print(subsession.get_group_matrix()) # will output this:

    # [[1, 3, 5],
    #  [7, 9, 11],
    #  [2, 4, 6],
    #  [8, 10, 12]]

グループシャッフルが正しく機能したかどうかを確認するには、ブラウザを開いてセッションの "結果" タブを開き、各ラウンドの列 groupid_in_group を確認してください。

group.set_players()

これは set_group_matrix に似ていますが、グループ内のプレイヤーをシャッフルするだけです。たとえば、プレイヤーにさまざまな役割を与えることができます。

セッション中のシャッフル

creating_session は、通常はグループをシャッフルするのに適した場所ですが、 creating_session はセッションが作成され、プレイヤーがプレイを開始する前に実行されることを忘れないでください。そのため、シャッフルがセッションの開始後に発生する何かをもとに行いたい場合は、代わりにWaitPageでシャッフルを実行する必要があります。

wait_for_all_groups=True にした WaitPage を作成し、 after_all_players_arrive にシャッフルのためのコードを挿入する必要があります。

class ShuffleWaitPage(WaitPage):

    wait_for_all_groups = True

    @staticmethod
    def after_all_players_arrive(subsession):
        subsession.group_randomly()
        # etc...
到着時間によるグループ化

group_by_arrival_time を参照してください。

Wait page

WaitPageは、1人のプレイヤーが先に進む前に、他のプレイヤーの実行を待つ必要がある場合に必要です。たとえば、最後通牒ゲームでは、プレイヤー2は、プレイヤー1のオファーを見る前に受け入れたり拒否したりすることはできません。

ページのシーケンスに WaitPage が含まれている場合、oTreeは、グループ内のすべてのプレイヤーが WaitPage に到着するまで待機します。

サブセッションで複数のグループが同時に作業をしており、すべてのグループ(サブセッション内のすべてのプレイヤー)を待機させるWaitPageが必要な場合は、wait_for_all_groups = True にする必要があります。

グループの詳細については、 グループ を参照してください。

after_all_players_arrive

after_all_players_arrive はすべてのプレイヤーがWaitPageに到着したときに実行される関数です。プレイヤーの利得の設定や、勝者を決定したりするのに適した関数です。最初に、必要な計算を行うグループ関数を定義します。例えば:

def set_payoffs(group):
    for p in group.get_players():
        p.payoff = 100

そして、次のようにしてこの関数を呼び出します。

class MyWaitPage(WaitPage):
    after_all_players_arrive = set_payoffs

wait_for_all_groups = True に設定した場合は、 after_all_players_arrive は、サブセッションの関数である必要があります。

テキストエディタを使用している場合、 after_all_players_arrive は、WaitPageで直接定義することもできます。:

class MyWaitPage(WaitPage):
    @staticmethod
    def after_all_players_arrive(group: Group):
        for p in group.get_players():
            p.payoff = 100

文字列で指定することもできます。

class MyWaitPage(WaitPage):
    after_all_players_arrive = 'set_payoffs'

is_displayed()

通常のページと同じように機能します。

group_by_arrival_time

WaitPageで group_by_arrival_time = True に設定すると、プレイヤーはWaitPageに到着した順序でグループ化されます。

class MyWaitPage(WaitPage):
    group_by_arrival_time = True

たとえば、 PLAYERS_PER_GROUP = 2 の場合、待機ページに到着した最初の2人のプレイヤーがグループ化され、次に到着した2人のプレイヤーが別のグループに編成されます。

これは、一部の参加者が脱落する可能性のあるセッション(たとえば、オンライン実験、または参加者を早期に終了させる同意ページを使用した実験)、一部の参加者が他の参加者よりもはるかに時間がかかるセッションで役立ちます。

group_by_arrival_time の一般的な使用方法は、参加者を除外するアプリの後に配置することです。たとえば、実験に参加するかどうかの同意ページがセッションにある場合、同意ページのみを含む "consent" アプリを作成し、 ['consent', 'my_game'] ような app_sequence を作成します。なお、my_gamegroup_by_arrival_time 利用します。これは、誰かが同意しなかった場合、 my_game のグループ化から除外されることを意味します。

ゲームに複数のラウンドがある場合は、ラウンド1の到着時間のみでグループ化することをお勧めします。

class MyWaitPage(WaitPage):
    group_by_arrival_time = True

    @staticmethod
    def is_displayed(player):
        return player.round_number == 1

これを行うと、後続のラウンドはラウンド1と同じグループ構造を維持します。それ以外の場合、プレイヤーは各ラウンドの到着時間によって再びグループ化されます。( group_by_arrival_time は、グループ構造を将来のラウンドにコピーします。)

注意

  • If a participant arrives at the wait page but subsequently switches to a different window or browser tab, they will be excluded from grouping after a short period of time.
  • id_in_group は、プレイヤーがページに到着した順序で必ずしも割り当てられるとは限りません。
  • group_by_arrival_time``は、 ``page_sequence において、WaitPageが 最初のページである場合にのみ使用できます
  • group_by_arrival_time を持つページで is_displayed を使う場合、ラウンド数のみに基づく必要があります。一部のプレイヤーにだけページを表示するために使用しないでください。
  • group_by_arrival_time = True の場合、すべてのプレイヤーが最初は同じグループに属します。グループは、プレイヤーがWaitPageに到着したときに "on the fly" で作成されます。

プレイヤーをグループに配置することをさらに制御する必要がある場合は、 group_by_arrival_time_method() を使用します。

group_by_arrival_time_method()

group_by_arrival_time を使用していて、どのプレイヤーを一緒に割り当てるかをより細かく制御したい場合は、 group_by_arrival_time_method() を使用することもできます。

到着時間によるグループ化に加えて、各グループが2人の男性と2人の女性で構成される必要があるとします。

group_by_arrival_time_method と呼ばれる関数を定義すると、新しいプレイヤーがWaitPageに到達するたびに呼び出されます。関数の2番目の引数は、WaitPageで現在待機しているプレイヤーのリストです。これらのプレイヤーの一部を選択してリストとして返すと、それらのプレイヤーはグループに割り当てられ、先に進みます。何も返さない場合、グループ化のための処理は発生しません。

これは、各グループに2人の男性と2人の女性がいる例です。各参加者に participant.category が割り当てられていることを前提としています。

# note: this function goes at the module level, not inside the WaitPage.
def group_by_arrival_time_method(subsession, waiting_players):
    print('in group_by_arrival_time_method')
    m_players = [p for p in waiting_players if p.participant.category == 'M']
    f_players = [p for p in waiting_players if p.participant.category == 'F']

    if len(m_players) >= 2 and len(f_players) >= 2:
        print('about to create a group')
        return [m_players[0], m_players[1], f_players[0], f_players[1]]
    print('not enough players yet to create a group')
WaitPageのタイムアウト

WaitPageにタイムアウトを設定するために group_by_arrival_time_method を使用することもできます。たとえば、参加者が5分以上待機している場合に、個別に続行できるようにすることができます。まず、アプリの前の最後のページの group_by_arrival_time 内で time.time() を利用して時間を記録しておき、それを participant field に保管してください。

次に、Player関数を定義します。

def waiting_too_long(player):
    participant = player.participant

    import time
    # assumes you set wait_page_arrival in PARTICIPANT_FIELDS.
    return time.time() - participant.wait_page_arrival > 5*60

そして、これを使用してください:

def group_by_arrival_time_method(subsession, waiting_players):
    if len(waiting_players) >= 3:
        return waiting_players[:3]
    for player in waiting_players:
        if waiting_too_long(player):
            # make a single-player group.
            return [player]

これが機能するのは、WaitPageが1分に1〜2回自動的に更新され、 group_by_arrival_time_method が再実行されるためです。

プレイヤーがWaitPageで待機し続けるのを防ぐ

特にオンライン実験でよくある問題は、グループ内の別のプレイヤーが脱落したり、遅すぎたりするのを待っているプレイヤーが待機し続けることになることです。

この問題を軽減するためにできることがいくつかあります。

group_by_arrival_time の利用

上記のように、同じ時間にアクティブにプレイしているプレイヤーだけがグループ化されるように group_by_arrival_time を使用できます。

group_by_arrival_time は、 "lock-in" タスクの後に使用するとうまく機能します。つまり、マルチプレイヤーゲームの前に、単体のプレイヤーによるタスクを実行できます。参加者はこの最初のタスクを完了するために作業を行うため、その時点以降に脱落する可能性は低くなります。

ページタイムアウトの利用

各ページで timeout_seconds を使用して、プレイヤーが遅れているか非アクティブの場合に、ページが自動的に進むようにします。または、管理画面の "Advance slowest participants" ボタンをクリックして、手動でタイムアウトを強制することもできます。

timeout_happenedの確認

timeout_seconds の前にそのページでの作業を完了する必要があり、そうしないと脱落してしまうことをユーザーに伝えるという方法もあります。また、 "次のボタンをクリックして、まだプレイしていることを確認してください" というページを作ることも考えられます。そして、 timeout_happened がTrueの場合、そのプレイヤー/グループに脱落を示すフィールドを設定したり、ラウンドの残りのページをスキップしたりするなど、さまざまな操作を実行できます。

脱落したプレイヤーをボットに置き換える

上記のテクニックのいくつかを組み合わせ、プレイヤーが脱落した場合でも、ボットのように自動再生が継続されるような処理の例を次に示します。まず、 is_dropout と呼ばれる participant field を定義し、 creating_session で、その初期値を False に設定します。次に、すべてのページで get_timeout_secondsbefore_next_page を使用します。:

class Page1(Page):
    form_model = 'player'
    form_fields = ['contribution']

    @staticmethod
    def get_timeout_seconds(player):
        participant = player.participant

        if participant.is_dropout:
            return 1  # instant timeout, 1 second
        else:
            return 5*60

    @staticmethod
    def before_next_page(player, timeout_happened):
        participant = player.participant

        if timeout_happened:
            player.contribution = cu(100)
            participant.is_dropout = True

注意

  • プレイヤーが時間どおりにページを送信できない場合は、 is_dropoutTrue に設定します。
  • 一度 is_dropout が設定されると、各ページは、自動的に送信するようになります。
  • ページが自動送信される場合、 timeout_happened は、ユーザーに代わって送信される値を決定するために使用します。

WaitPageの外観のカスタマイズ

title_text および body_text 属性を設定することにより、WaitPageに表示されるテキストをカスタマイズできます。例:

class MyWaitPage(WaitPage):
    title_text = "Custom title text"
    body_text = "Custom body text"

カスタム wait page テンプレート を参照してください。

チャット

参加者が互いに交流できるように、ページにチャットルームを追加できます。

基本的な使い方

テンプレートのHTMLに、次のように入力します。

{{ chat }}

これにより、同じグループ内のプレイヤー間でチャットルームが作成されます。各プレイヤーのニックネームは "プレイヤー1" 、 "プレイヤー2" など id_in_group に基づいて表示されます。

ニックネームとチャットルームのメンバーをカスタマイズする

あなたは channel および/または nickname をこのように指定できます:

{{ chat nickname="abc" channel="123" }}
ニックネーム

nickname はチャットでそのユーザーに表示されるニックネームです。 {{ chat nickname=player.role }} の形で指定されることが多いです。

チャンネル

channel は、チャットルームの名前です。2人のプレイヤーが同じ channel にいる場合に、お互いにチャットできます。また、 channel はユーザーインターフェイスには表示されず、内部で使用されるだけです。デフォルト値は group.id となっています。この場合、グループ内のすべてのプレイヤーが一緒にチャットすることができます。チャットを現在のページまたはグループのサブディビジョンなどにスコープする代わりに、 channel を使用できます(以下の例を参照)。 channel の値に関係なく、チャットは少なくとも同じセッションと同じアプリのプレイヤーにスコープされます。

例: 役割ごとのチャット

これは、グループ内のコミュニケーションの代わりに、役割に基づいてグループ間でコミュニケーションをとる例です。たとえば、すべての買い手が互いに話し、すべての売り手が互いに話すことができます。

def chat_nickname(player):
    group = player.group

    return 'Group {} player {}'.format(group.id_in_subsession, player.id_in_group)

ページ内:

class MyPage(Page):

    @staticmethod
    def vars_for_template(player):
        return dict(
            nickname=chat_nickname(player)
        )

テンプレート内:

{{ chat nickname=nickname channel=player.id_in_group }}
例: ラウンドの違いに関係なくチャットする

現在別のラウンドにいるプレイヤーとチャットする必要がある場合は、次のことができます。

{{ chat channel=group.id_in_subsession }}
例: すべてのラウンドのすべてのグループ間でチャット

セッションの全員がお互いに自由にチャットできるようにしたい場合は、次のようにします。

{{ chat channel=1 }}

(番号1は重要ではありません。重要なのは、それがすべての人にとって同じ値であるということです。)

高度なカスタマイズ

ブラウザのインス​​ペクタでページのソースコードを見ると、 otree-chat__ で始まるクラスがたくさんあります。

CSSまたはJSを使用して、これらの要素の外観や動作を変更できます(または完全に非表示にできます)。

<div> の中にコードを挿入し、その親 の``<div>`` をスタイリングすることで、外観をカスタマイズすることもできます。たとえば、幅を設定するには:

<div style="width: 400px">
    {{ chat }}
</div>

1ページに複数のチャット

各ページに複数の {{ chat }} を配置することで、プレイヤーは複数のチャネルに同時に参加することができます。

チャットログのCSVをエクスポートする

チャットログのダウンロードリンクは、oTreeの通常のデータエクスポートページに表示されます。

参加者と実験者間のチャット を参照してください。

アプリケーションとラウンド

アプリケーション

oTreeアプリケーションはPythonおよびHTMLコードを含むフォルダとして構成されます。一つのプロジェクト内で複数のアプリケーションを含むことができます。セッションは基本的に、次々に実行される一連のアプリケーションを表します。

アプリケーションの組合わせ

セッション構成 app_sequence を設定することで、アプリケーションを組み合わせることができます。

アプリケーション間のデータ受け渡し

Participant fieldssession fields を参照してください。

ラウンド

C.NUM_ROUNDS に値を設定することで、実行するラウンド数を設定できます。例えば、 app_sequence['app1', 'app2'] として設定されており、 app1NUM_ROUNDS = 3 に、app2NUM_ROUNDS = 1 に設定されている場合は、合わせて4つのセッションが実行されます。

ラウンド数

player.round_number で現在のラウンド数を参照できます。(この要素はsubsession、group、playerオブジェクトが保持しています。)ラウンド数は1から数えられます。

ラウンドとアプリケーション間のデータの受け渡し

それぞれのラウンドは個別の GroupPlayer オブジェクトを保持します。例えば、ラウンド1で player.my_field = True に設定した場合、ラウンド2で player.my_field の値を参照しようとしても None が返ってきます。これはラウンドによって個別に Player オブジェクトが作成されるためです。

過去のラウンドやアプリケーションからのデータにアクセスするために、以下のいずれかの方法でアクセスすることができます。

in_rounds、 in_previous_rounds、 in_round など

Player、Group、subsessionオブジェクトは次のメソッドを持ちます。

  • in_previous_rounds()
  • in_all_rounds()
  • in_rounds()
  • in_round()

例えば、もし10ラウンドのゲームの最後に、 player.in_previous_rounds() を呼び出すと、過去の9ラウンド分の同じ参加者のPlayerオブジェクトのデータが返されます。

player.in_all_rounds() はほとんど同じですが、現在のラウンドも含めて10このオブジェクトを返します。

player.in_rounds(m, n) はラウンド m からラウンド n までの同じ参加者のPlayerのリストを返します。

player.in_round(m) はラウンド m のPlayerデータを返します。例えば、前のラウンドの参加者の利得が知りたい場合は、次のようにします。

prev_player = player.in_round(player.round_number - 1)
print(prev_player.payoff)

これらの関数はsubsessionでも同じように機能します。(例: subsession.in_all_rounds()

これらはGroupに対しても同じように機能しますが、ラウンド間でグループがシャッフルされる場合は、使用しても正しい結果を得ることはできません。

Participant fields

前のアプリケーションの参加者のデータにアクセスしたいのなら、participantオブジェクトにアクセスしたいデータをあらかじめ保存しておく必要があります。このオブジェクトはアプリケーション間で保存されます。( Participant )( in_all_rounds() は、同じアプリケーションの前のラウンドのデータにアクセスする必要があるときのみ有効です。)

実験の参加者に定義したいフィールドの名前のリストである、 PARTICIPANT_FIELDS を定義し、設定してください。

すると、コード上でこれらのフィールドに任意のタイプのデータの取得や設定ができます。

participant.mylist = [1, 2, 3]

内部的には、すべての参加者のデータは participant.vars という辞書型リストに保存されています。また、 participant.xyzparticipant.vars['xyz'] と同等です。

session fields

セッション中のすべての参加者で共通のグローバル変数を利用するために、 SESSION_FIELDS を追加してください。それは、 PARTICIPANT_FIELDS と同じように働きます。内部的には、全ての session fields は session.vars に保存されます。

ラウンド数を可変にする

もし、ラウンドを可変にしたいなら、ライブページ の利用を検討してください。

代わりに、 NUM_ROUNDS を高い値に設定してから、プログラム内である条件で {{ next_button }} を非表示にして、先のラウンドへ進めなくするか、 app_after_this_page を使う方法もあります。ただし、ラウンド数が増えると(例えば、100以上)、パフォーマンスに問題が生じる可能性があります。注意して、テストしてください。

処理の割り当て

参加者ごとに異なる処理を割り当てるには、 creating_session を使用します。例えば、次のようにします:

def creating_session(subsession):
    import random
    for player in subsession.get_players():
        player.time_pressure = random.choice([True, False])
        print('set time_pressure to', player.time_pressure)

グループレベルで異なる処理を割り当てることもできます (BooleanFieldGroup に入れて、上記のコードを get_groups()group.time_pressure を使うように変更します)。

creating_session は、アプリが app_sequence の先頭でなくても、 "create session" ボタンをクリックするとすぐに実行されます。

複数ラウンドにまたがる処理

ゲームに複数のラウンドがある場合、 creating_session が各ラウンドで独立して実行されるため、プレイヤーはラウンドごとに異なる処理を受ける可能性があります。これを防ぐには、 player ではなく、 participant に設定します:

def creating_session(subsession):
    if subsession.round_number == 1:
        for player in subsession.get_players():
            participant = player.participant
            participant.time_pressure = random.choice([True, False])

バランスの取れた処理

上記のコードでは、プレイヤーごとに独立して無作為抽出を行っているため、バランスが悪くなってしまうことがあります。これを解決するためには、 itertools.cycle を使用します:

def creating_session(subsession):
    import itertools
    pressures = itertools.cycle([True, False])
    for player in subsession.get_players():
        player.time_pressure = next(pressures)

実行する処理の選択

ライブ実験では、プレイヤーにランダムな処理を施したいことがよくあります。しかし、ゲームをテストする際には、どちらの処理を実行するかを明示的に選択することが有用な場合があります。例えば、上記の例でゲームを開発していて、同僚に両方の処理を見せたいとします。 1つのパラメータを除いて、同じ内容の2つのセッションコンフィグを作成することができます (oTree Studio では、 "custom parameter" を追加します):

SESSION_CONFIGS = [
    dict(
        name='my_game_primed',
        app_sequence=['my_game'],
        num_demo_participants=1,
        time_pressure=True,
    ),
    dict(
        name='my_game_noprime',
        app_sequence=['my_game'],
        num_demo_participants=1,
        time_pressure=False,
    ),
]

そうすれば、コードの中で、現在のセッションの処理を次のようにして取得することができます:

session.config['time_pressure']

これを無作為化の方法と組み合わせることもできます。 if 'time_pressure' in session.config: をチェックし、含まれていればそれを使用し、含まれていなければランダムに選択します。

セッションの設定

管理画面でゲームのパラメータを調整できるように、セッションを設定できます。

_images/edit-config.png

例えば、 num_apples というパラメータがあるとします。通常の方法では、 C.NUM_APPLES のように C に定義します。しかし、これをセッションごとに変更可能にするために、代わりにセッションコンフィグで定義することができます。例えば、以下のようになります:

dict(
    name='my_session_config',
    display_name='My Session Config',
    num_demo_participants=2,
    app_sequence=['my_app_1', 'my_app_2'],
    num_apples=10
),

管理画面でセッションを作成すると、この数値を変更するためのテキストボックスが表示されます。また、 'doc' でヘルプテキストを追加することもできます:

dict(
    name='my_session_config',
    display_name='My Session Config',
    num_demo_participants=2,
    app_sequence=['my_app_1', 'my_app_2'],
    num_apples=10,
    doc="""
    Edit the 'num_apples' parameter to change the factor by which
    contributions to the group are multiplied.
    """
),

アプリのコードでは、 session.config['num_apples'] でアクセスできます。

注意:

  • パラメータをセッションコンフィグで定義するには、その値が数値、ブール値、文字列のいずれかでなければなりません。
  • 管理画面の "Demo" セクションでは、セッションの設定はできません。 "Sessions" もしくは "Rooms" でセッションを作成するときにのみ有効です。

タイムアウト

基本

timeout_seconds

ページに時間制限を設定するには、次のように timeout_seconds を追加します。:

class Page1(Page):
    timeout_seconds = 60

設定した時間が経過すると、自動的にページが遷移します。

本番サーバー( prodserver )で実行している場合、ユーザーがブラウザーウィンドウを閉じたとしてもページは常に送信されます。ただし、開発サーバー( zipserverdevserver )を実行している場合、これは発生しません。

タイムアウト時間を動的に設定したい場合は、 get_timeout_seconds を使用します。

timeout_happened

ページがタイムアウトまでに送信されたかどうかを確認できます。

class Page1(Page):
    form_model = 'player'
    form_fields = ['xyz']
    timeout_seconds = 60

    @staticmethod
    def before_next_page(player, timeout_happened):
        if timeout_happened:
            # you may want to fill a default value for any form fields,
            # because otherwise they may be left null.
            player.xyz = False

get_timeout_seconds

timeout_seconds よりも柔軟に設定できます。例えば、タイムアウト時間が playerplayer.session に依存する場合等に有効です。

例:

class MyPage(Page):

    @staticmethod
    def get_timeout_seconds(player):
        return player.my_page_timeout_seconds

また、カスタムセッション構成パラメーターを使用することもできます。( 実行する処理の選択 を参照)。

def get_timeout_seconds(player):
    session = player.session

    return session.config['my_page_timeout_seconds']

高度な技術

タイムアウトによって送信されたフォーム

タイムアウトのためにフォームが自動送信された場合、oTreeは送信時に入力されたフィールドを保存しようとします。ただし、フォームへの入力が終わっておらず、内容が欠落しているか無効であるためにエラーが発生した場合、数値フィールドには 0 、ブールフィールドには False 、および文字列フィールドには空の文字列 '' が設定されます。

タイムアウトによって自動送信された値を破棄したい場合は、 timeout_happened を確認することによって破棄するかどうかを決定できます。

error_message() 関数が失敗した場合、フォームの情報が無効である可能性があるため、そのフォームの情報は破棄されます。

複数のページにまたがるタイムアウト

複数のページ、またはセッション全体にまたがるタイムアウト処理を設定するために get_timeout_seconds を使用できます。方法は、固定の "有効期限" を定義し、各ページで get_timeout_seconds を作成し、その有効期限までの秒数を返すようにすることです。

まず、タイマーを開始する場所を選択します。これは、 "開始する準備ができたらボタンを押してください" などのテキストを表示するページが好ましいです。ユーザーが「次へ」ボタンをクリックすると、 before_next_page が実行されます。

class Start(Page):

    @staticmethod
    def before_next_page(player, timeout_happened):
        participant = player.participant
        import time

        # remember to add 'expiry' to PARTICIPANT_FIELDS.
        participant.expiry = time.time() + 5*60

after_all_players_arrive または creating_session でタイマーを開始することもできます。セッションの全員で同じ場合は、セッションフィールドに保存できます。)

次に、各ページの get_timeout_seconds は、その有効期限までの秒数である必要があります。

class Page1(Page):

    @staticmethod
    def get_timeout_seconds(player):
        participant = player.participant
        import time
        return participant.expiry - time.time()

タイムアウトになると、 get_timeout_seconds は0または負の値が返します。これにより、ページが読み込まれ、すぐに自動送信されます。これは、残りのすべてのページが参加者の画面で自動ですばやく遷移していくことを意味しますが、これは画面がちかちかするので通常は望ましくありません。したがって、参加者がページ全体を実際に読むのに十分な時間がない場合は、 is_displayed を使用してページをスキップするのが望ましいです。

def get_timeout_seconds(player):
    participant = player.participant
    import time
    return participant.expiry - time.time()

class Page1(Page):
    get_timeout_seconds = get_timeout_seconds

    @staticmethod
    def is_displayed(player):
        return get_timeout_seconds(player) > 3

タイマーのデフォルトのテキストには、「このページを完了するまでの残り時間: 」と表示されます。ただし、タイムアウトが複数のページにまたがる場合は、 timer_text を次のように設定して、より正確に表現する必要があります。

class Page1(Page):

    timer_text = 'Time left to complete this section:'

    @staticmethod
    def get_timeout_seconds(player):
        ...

タイマーのカスタマイズ

タイマーを隠す

タイマーを非表示にする場合は、次のCSSを使用します。

.otree-timer {
    display: none;
}
タイマーの動作を変更する

タイマーの機能は、 jQuery Countdown によって提供されます。あなたはjQueryの .on()off() にイベントハンドラを新しくアタッチすることにより、その動作を変更することができます。

oTreeは update.countdownfinish.countdown イベントのハンドラーを設定するため、それらを変更する場合は、 off() でハンドラを取り除いたり、 on() で独自のハンドラーを追加したりできます。カウントダウン要素は .otree-timer__time-left です。

たとえば、残り10秒になるまでタイマーを非表示にするには、

<style>
    .otree-timer {
        display: none;
    }
</style>

<script>
    document.addEventListener("DOMContentLoaded", function (event) {
        $('.otree-timer__time-left').on('update.countdown', function (event) {
            if (event.offset.totalSeconds === 10) {
                $('.otree-timer').show();
            }
        });
    });
</script>

このコードがすべてのページに適用されることを避けるには、 includable template にコードを配置します。

Note: even if you turn off the finish.countdown event handler, the page will still be submitted on the server side. So, instead you should use the technique described in ページを送信しないタイムアウト.

ページを送信しないタイムアウト

ページを送信しないタイムアウトが必要な場合は、組み込みのタイマーを使用する必要はまったくありません。代わりに、JavaScriptを使用して独自のものを作成します。次に例を示します。

setTimeout(
    function () {
        alert("Time has run out. Please make your decision.");
    },
    60*1000 // 60 seconds
);

ボット

ボットはアプリへの参加者をシミュレートします。ボットが、各ページをクリックしたり、フォームに入力したりすることで、あなたのアプリが正常に動作することを確認できます。

この機能を使うことで、oTreeがアプリを自動的にテストしてくれます。また、oTree Studioでは、ボットのコードを設計することもできますので、ボットの作成から実行まで、ほとんど手間がかかりません。

ボットの実行

  • ボットを追加します。 (下記参照)
  • セッションコンフィグで、 use_browser_bots=True を設定します。
  • サーバを起動し、セッションを作成します。スタートリンクが開かれると、ブラウザ上でボットが自動的にページを実行します。

テストの記述

oTree Studioで、アプリの「テスト」セクションに移動します。ボタンをクリックすると、ボットのコードが自動生成されます。生成されたコードを改良したい場合(例えば、 expect() ステートメントの追加など)は、以下のセクションをお読みください。

テキストエディタをお使いの場合は、tests.py にアクセスしてください。 tests.py の例は こちら

ページの送信

ページ毎に yield を記述する必要があります。例えば、以下のようになります:

yield pages.Start
yield pages.Survey, dict(name="Bob", age=20)

ここでは、まず、フォームを含まない Start ページを送信しています。次のページには2つのフォームフィールドがあるので、辞書型を送信しています。

このテストのシステムでは、ボットがページに対して無効な入力をした場合や、間違った順序でページを送信した場合には、エラーが発生します。

if 文を使い、任意のプレイヤーやラウンドを指定して実行できます。例えば、以下のようになります:

if self.round_number == 1:
    yield pages.Introduction
if self.player.id_in_group == 1:
    yield pages.Offer, dict(offer=30)
else:
    yield pages.Accept, dict(offer_accepted=True)

if 文では self.player, self.group, self.round_number などを使用することができます。

ボットを記述する際には、WaitPage は無視します。

ラウンド

ボットのコードは、一度に1ラウンドだけ実行するように記述します。oTreeはそのコードに書かれた動作を NUM_ROUNDS 回自動的に実行します。

expect()

expect() 関数を使用することで、コードが期待通りに動作していることを確認できます。

例えば、以下のように使用できます:

expect(self.player.num_apples, 100)
yield pages.Eat, dict(apples_eaten=1)
expect(self.player.num_apples, 99)
yield pages.SomeOtherPage

self.player.num_apples が99でない場合は、エラーで警告されます。

expect(self.player.budget, '<', 100) のように、3つの引数を指定できます。この例では、 self.player.budget が100より小さいことを検証しています。次の演算子を使うことができます: '<', '<=', '==', '>=', '>', '!=', 'in', 'not in'

フォーム検証のテスト

フォームの検証 を使用している場合には、 SubmissionMustFail() を使用して、アプリがユーザーからの無効な入力を正しく拒否しているかどうかをテストする必要があります。

例えば、このようなページがあるとします:

class MyPage(Page):

    form_model = 'player'
    form_fields = ['int1', 'int2']

    @staticmethod
    def error_message(player, values):
        if values["int1"] + values["int2"] != 100:
            return 'The numbers must add up to 100'

以下のようにして、正常に動作しているかどうかを確認することができます:

yield SubmissionMustFail(pages.MyPage, dict(int1=99, int2=0))
yield pages.MyPage, dict(int1=99, int2=1)

このボットは MyPage を2回送信します。1回目の送信が 成功 してしまった場合、エラーが発生するようになっています。

HTML のチェック

self.html には、これから送信しようとしているページの HTML が格納されています。これは expect() と一緒に使用することができます:

if self.player.id_in_group == 1:
    expect(self.player.is_winner, True)
    print(self.html)
    expect('you won the game', 'in', self.html)
else:
    expect(self.player.is_winner, False)
    expect('you did not win', 'in', self.html)
yield pages.Results
# etc...

self.html は、 yield 文の実行後に次のページの HTML で更新されます。改行や余分なスペースは無視されます。

HTML の自動チェック

ボットが、ページの HTML に実際にはないフォームを送信しようとしたり、ページの HTML に送信ボタンがないのに送信しようとする場合、エラーが発生します。

ただし、JavaScriptで動的に追加されたフィールドやボタンは、ボットシステムでは確認できません。このような場合には、Submissioncheck_html=False を指定して、HTML チェックを無効にしてください。例えば、以下を:

yield pages.MyPage, dict(foo=99)

次のように変更します:

yield Submission(pages.MyPage, dict(foo=99), check_html=False)

( check_html=False を指定せずに Submission を使用した場合、2つのコードサンプルは同等のものになります。)

ページタイムアウトのシミュレート

Submission では timeout_happened=True を指定できます:

yield Submission(pages.MyPage, dict(foo=99), timeout_happened=True)

高度な機能

ボット: 高度な機能 を参照してください。

ライブページ

ライブページは、サーバーと継続的に通信し、リアルタイムに更新されるため、連続した時間のゲームが可能です。ライブページは、ユーザー同士のやりとりが多いゲームに向いています。

こちら で多くの例を確認できます。

サーバーへのデータ送信

テンプレートのJavaScriptコードでは、サーバーにデータを送信したいときに liveSend() 関数を呼び出します。例えば、ユーザーに代わって 99 の入札を行う場合は、次のように呼び出します:

liveSend(99);

このメッセージを受信する関数を定義します。この関数の引数は、送信されたデータです。

class MyPage(Page):
    @staticmethod
    def live_method(player, data):
        print('received a bid from', player.id_in_group, ':', data)

oTree Studio を使用している場合は、名前が live_ で始まるプレイヤー関数を定義する必要があります。(なお、 WaitPagelive_method はまだサポートされていません。)

ページへのデータ送信

データを送り返すには、メッセージを受け取るプレイヤーの ID をキーにした辞書型を返します。例えば、メッセージを送ってきた人に "thanks" と送るメソッドは以下の通りです:

def live_method(player, data):
    return {player.id_in_group: 'thanks'}

複数のプレイヤーに送信するには、そのプレイヤーの id_in_group を使用します。例えば、このメソッドはすべてのメッセージをプレイヤー2とプレイヤー3に転送します:

def live_method(player, data):
    return {2: data, 3: data}

グループ全体に送信する場合は、 0 を使用します(実際の id_in_group ではないため、特殊なケースです)。

def live_method(player, data):
    return {0: data}

JavaScript で、 liveRecv 関数を定義します。この関数は、サーバーからメッセージを受信するたびに自動的に呼び出されます。

function liveRecv(data) {
    console.log('received a message!', data);
    // your code goes here
}

例: オークション

class Group(BaseGroup):
    highest_bidder = models.IntegerField()
    highest_bid = models.CurrencyField(initial=0)

class Player(BasePlayer):
    pass
def live_method(player, bid):
    group = player.group
    my_id = player.id_in_group
    if bid > group.highest_bid:
        group.highest_bid = bid
        group.highest_bidder = my_id
        response = dict(id_in_group=my_id, bid=bid)
        return {0: response}
<table id="history" class="table">
<tr>
  <th>Player</th>
  <th>Bid</th>
</tr>
</table>
<input id="inputbox" type="number">
<button type="button" onclick="sendValue()">Send</button>

<script>

  let history = document.getElementById('history');
  let inputbox = document.getElementById('inputbox');

  function liveRecv(data) {
      history.innerHTML += '<tr><td>' + data.id_in_group + '</td><td>' + data.bid + '</td></tr>';
  }

  function sendValue() {
    liveSend(parseInt(inputbox.value));
  }

</script>

(注: JavaScriptでは data.id_in_group == data['id_in_group'] となります。)

データ

送受信するデータは、どのようなデータタイプでも問題ありません。(JSONにシリアライズできるものであれば) 例えば、これらはすべて有効です:

liveSend(99);
liveSend('hello world');
liveSend([4, 5, 6]);
liveSend({'type': 'bid', 'value': 10.5});

最も汎用性の高いデータタイプは辞書型です。複数のメタデータ、特にメッセージの種類を含めることができるからです:

liveSend({'type': 'offer', 'value': 99.9, 'to': 3})
liveSend({'type': 'response', 'accepted': true, 'to': 3})

if 文を使って異なるタイプのメッセージを処理することができます:

def live_method(player, data):
    t = data['type']
    if t == 'offer':
        other_player = data['to']
        response = {
            'type': 'offer',
            'from': player.id_in_group,
            'value': data['value']
        }
        return {other_player: response}
    if t == 'response':
        # etc
        ...

履歴

デフォルトでは、参加者がページに到着する前に送信されたメッセージは表示されません。(また、ページを更新してもデータは再送信されません。)履歴を保存したい場合は、データベースに保存する必要があります。プレイヤーがページを読み込んだときに、JavaScript で liveSend({}) のように呼び出し、データベースからゲームの履歴を取得するように live_method を設計することができます。

エクストラモデル

ライブページは、個々のメッセージやアクションをデータベースに保存することができる エクストラモデル と一緒に使用されることが多いです。

ユーザをページに留めておく方法

ユーザーが次のページに進む前に、10通のメッセージを送信する必要があるとします。

まず、 {{ next_button }} を削除します。(または JS を使ってタスクが完了するまで非表示にします。)

タスクが完了したら、メッセージを送信します:

class Group(BaseGroup):
    num_messages = models.IntegerField()
    game_finished = models.BooleanField()


class MyPage(Page):
    def live_method(player, data):
        group = player.group
        group.num_messages += 1
        if group.num_messages >= 10:
            group.game_finished = True
            response = dict(type='game_finished')
            return {0: response}

そして、テンプレートでは、JavaScript でページを自動送信します:

function liveRecv(data) {
    console.log('received', data);
    let type = data.type;
    if (type === 'game_finished') {
        document.getElementById("form").submit();
    }
    // handle other types of messages here..
}

また、同様のテクニックを使って、独自の待ち受けページを実装することができます。例えば、一定のタイムアウト後に、すべてのプレイヤーが到着していなくても、続行できるようなページです。

ライブページに関する一般的なアドバイス

ここでは、一般的なアドバイスを紹介します (すべての状況に当てはまるわけではありません)。ロジックのほとんどを Python で実装し、JavaScript はページの HTML を更新するためにを使用することをお勧めします。理由は以下の通りです:

  • JavaScript は適切に使用するのが難しい言語である。
  • Python のコードはサーバー上で実行され、集中管理されていて信頼性が高い。JavaScript はクライアント上で実行されるが、クライアントは互いに同期が取れず、ページを閉じたりリロードしたりするとデータが失われる可能性がある。
  • Python のコードはサーバー上で実行されるため、より安全で、参加者が見たり変更したりすることができない。

例: 三目並べ

三目並べのゲームを実装するとしましょう。live_method が受け取るメッセージには2種類あります:

  1. ユーザーがマスをマークしたときに、他のプレイヤーに通知する必要がある
  2. ユーザーがページをロード (またはリロード) したときに、現在のボードレイアウトを送信する必要がある

1. については、 onclick などの JavaScript のイベントハンドラを使用して、ユーザが 3 のマスをクリックすると、その動きがサーバに送られるようにします:

liveSend({square: 3});

2. については、テンプレートに次のようなコードを入れて、ページが読み込まれたときに、サーバーに空のメッセージを送るのが良いでしょう:

document.addEventListener("DOMContentLoaded", (event) => {
    liveSend({});
});

サーバーはこの2つの状況を if 文で処理します:

def live_method(player, data):
    group = player.group

    if 'square' in data:
        # SITUATION 1
        square = data['square']

        # save_move should save the move into a group field.
        # for example, if player 1 modifies square 3,
        # that changes group.board from 'X O XX  O' to 'X OOXX  O'
        save_move(group, square, player.id_in_group)
        # so that we can highlight the square (and maybe say who made the move)
        news = {'square': square, 'id_in_group': player.id_in_group}
    else:
        # SITUATION 2
        news = {}
    # get_state should contain the current state of the game, for example:
    # {'board': 'X O XX  O', 'whose_turn': 2}
    payload = get_state(group)
    # .update just combines 2 dicts
    payload.update(news)
    return {0: payload}

2. (プレイヤーがページを読み込む状況) では、クライアントは次のようなメッセージを受け取ります:

{'board': 'X OOXX  O', 'whose_turn': 2}

1. では、プレイヤーは先ほどの動きに関する更新情報と、現在の状態を取得します。

{'board': 'X OOXX  O', 'whose_turn': 2, 'square': square, 'id_in_group': player.id_in_group}

JavaScript のコードは、誰の一手なのかを把握する必要はなく、サーバーから受け取った情報を信頼するだけでいいのです。メッセージを受け取るたびに盤面を描き直すこともできます。

あなたのコードは、ユーザーの入力を検証する必要もあります。例えば、実際にはプレイヤー2の番なのにプレイヤー1が動こうとした場合、それをブロックする必要があります。前のセクションで述べた理由から、 JavaScript のコードではなく、 live_method で実装するのが良いでしょう。

要約

これまで説明してきたように、 live_method の典型的なパターンは次のようなものです:

if the user made an action:
    state = (get the current state of the game)
    if (action is illegal/invalid):
        return
    update the models based on the move.
    news = (produce the feedback to send back to the user, or onward to other users)
else:
    news = (nothing)
state = (get the current state of the game)
payload = (state combined with news)
return payload

ゲームの状態を2回取得していることに注意してください。モデルを更新するとゲームの状態が変化するため、再度ゲームの状態を取得しています。

トラブルシューティング

ページの読み込みが終わる前に liveSend を呼び出すと、 liveSend is not defined などのエラーが発生します。そのため、 DOMContentLoaded (または jQuery の document.ready など) を待つようにします:

window.addEventListener('DOMContentLoaded', (event) => {
    // your code goes here...
});

ユーザーが 「次へ」 ボタンをクリックしたときには、 liveSend を実行しないでください。ページを離れると、 liveSend が中断される可能性があります。代わりに、ユーザーに liveSend を実行する通常のボタンをクリックしてもらい、liveRecvdocument.getElementById("form").submit(); を実行するようにします。

サーバ設定

個人のパソコンでアプリをテストするだけなら、 otree devserver を使うことができます。完全なサーバ設定は必要ありません。

しかし、自分のアプリを誰かと共有したい場合は、ウェブサーバを使用する必要があります。

必要なオプションを選択してください:

インターネット上のユーザにアプリを公開したい場合

Heroku を使用します。

最も簡単な設定を使用したい場合

ここでも、 Heroku をお勧めします。

専用のLinuxサーバーを立ち上げたい場合

Ubuntu Linux に説明があります。

サーバの基本設定 (Heroku)

Heroku は商用のクラウドホスティングプロバイダです。oTree を導入する最もシンプルな方法です。

Heroku の無料プランは、アプリのテストには十分ですが、研究を開始する準備ができたら、より多くのトラフィックを処理できる有料サーバにアップグレードする必要があります。しかし、 Heroku は、実際に使用した時間分だけ支払うので、かなり安価です。1 日だけ試験を行う場合、 dyno やアドオンをオフにすれば、月額費用の 1/30 で済みます。多くの場合、わずか数ドルで試験を行うことができるのです。

Heroku のセットアップ

To deploy to Heroku, you should use oTree Hub, which automates your server setup and ensures your server is correctly configured.

oTree Hub は、エラーやパフォーマンスの監視や Sentry サービスも提供しています。

サーバのパフォーマンス

Heroku では、 dyno やデータベースなどのリソースに対して、さまざまなパフォーマンス層を提供しています。どの層が必要かは、アプリのトラフィック量やコードの書き方によって異なります。

パフォーマンスに影響を与える要因は多岐にわたるため、パフォーマンスは複雑なテーマです。oTree Hub の Pro プランには、パフォーマンスの問題を特定するためにログを分析する "monitor" セクションがあります。

一般的なヒント:

  • oTree を最新バージョンにアップグレードする
  • ブラウザボットを使用して、アプリのストレステストを行う
  • 高い層の dyno では、Heroku は "Metrics" タブを提供しています。 "Dyno load" を見てください。ユーザがページの読み込みに時間がかかっているのに、 dyno load が 1 以上を維持している場合は、より高速な dyno を導入する必要があります。 (ただし、1 つ以上の Web dyno を実行しないでください。)
  • dyno load が 1 未満でもページロード時間が遅い場合、ボトルネックは Postgres データベースなど他にある可能性があります。

最も負荷の高いセッションは、 (1)ラウンド数が多い、(2)プレイヤーが各ページに数秒しか滞在しない、(3)多くのプレイヤーが同時にプレイしている、が組み合わさったセッションです。これらのセッションでは、1秒あたりのページリクエスト数が多いため、サーバーに負荷がかかります。このようなゲームでは ライブページ を使用することで、より高速なパフォーマンスを実現することができます。

Ubuntu Linux サーバ

We typically recommend newcomers to oTree to deploy to Heroku (see instructions here).

しかし、oTree を Linux サーバで運用したい場合もあります。理由は以下のようなものが考えられます:

  • 実験室にインターネットがない
  • サーバの設定を完全にコントロールしたい
  • パフォーマンスを向上させたい (ローカルサーバは遅延が少ない)

apt-get パッケージのインストール

以下を実行します:

sudo apt-get install python3-pip git

virtualenv の作成

virtualenv を使用するには以下を実行します:

python3 -m venv venv_otree

シェルを起動するたびにこの venv を有効にするには、 .bashrc もしくは .profile に以下を記述します:

source ~/venv_otree/bin/activate

virtualenv が有効になると、プロンプトの先頭に (venv_otree) と表示されます。

データベース (Postgres)

Postgres と psycopg2 をインストールして、新しいデータベースを作成し、 DATABASE_URL 環境変数を設定してください (例: postgres://postgres@localhost/django_db` )。

サーバ上のデータベースをリセットする

oTree プロジェクトが入っているフォルダに cd で移動します。必要なものをインストールし、データベースをリセットします。

pip3 install -r requirements.txt
otree resetdb

サーバの実行

本番用サーバのテスト

プロジェクトフォルダから、以下を実行します:

otree prodserver 8000

そして、サーバの IP アドレスもしくはホスト名に :8000 を付けてブラウザに入力します。

Nginx や Apache のようなリバースプロキシを使用していない場合は、ポート 80 で直接 oTree を実行したいと思うでしょう。この場合、スーパーユーザの権限が必要なので、 sudo を使用します。ただし、 PATHDATABASE_URL などの環境変数を保持するために、いくつかの引数を追加する必要があります:

sudo -E env "PATH=$PATH" otree prodserver 80

もう一度、ブラウザで開いてみてください。今回は URL に :80 を追加する必要はありません。これはデフォルトの HTTP ポートだからです。

prodserver は、 devserver と異なり、ファイルが変更されても自動的に再起動しません。

残りの環境変数の設定

DATABASE_URL を設定したのと同じ場所に以下をを追加します:

export OTREE_ADMIN_PASSWORD=my_password
#export OTREE_PRODUCTION=1 # uncomment this line to enable production mode
export OTREE_AUTH_LEVEL=DEMO
(オプション) プロセスコントロールシステム

上記の説明に従って、サーバーが動作するようになったら、 Supervisord や Circus のようなプロセス制御システムを使用することをお勧めします。これは、プロセスがクラッシュした場合に再起動したり、ログアウトしても実行し続けるなどの機能を持っています。

Circus

Circus をインストールして、プロジェクトフォルダに以下の内容の circus.ini を作成します:

[watcher:webapp]
cmd = otree
args = prodserver 80
use_sockets = True
copy_env = True

そして、以下を実行します:

sudo -E env "PATH=$PATH" circusd circus.ini

これが正常に動作していれば、デーモンとして起動することができます:

sudo -E env "PATH=$PATH" circusd --daemon circus.ini --log-output=circus-logs.txt

Circus を停止するには、以下を実行します:

circusctl stop
(オプション) Apache, Nginx など

oTree は ASGIサーバで動作させる必要があるため、 Apache や Nginx をプライマリWebサーバとして使用することはできません。しかし、以下のような理由で、 Apache/Nginx をリバースプロキシとして使用したい場合があります:

  • 静的ファイルの提供を最適化しようとしている (ただし、oTree は Whitenoise を使用しており、すでにかなり効率的である)
  • 同じサーバで他の Web サイトをホストする必要がある
  • SSL やプロキシ・バッファリングなどの機能が必要である

リバースプロキシを設定する場合は、 HTTP トラフィックだけでなく、ウェブソケットも有効にするようにしてください。

トラブルシューティング

ページをリロードするたびにランダムに変更されるなど、奇妙な動作が発生する場合、シャットダウンしなかった別の oTree インスタンスが原因である可能性があります。 oTree を停止して再度リロードしてみてください。

他の oTree ユーザとサーバを共有する

他の oTree ユーザとサーバを共有することができます。ただし、コードとデータベースがお互いに競合しないように、別々に管理する必要があります。

サーバ上では、oTree を使用する人ごとに異なる Unix ユーザを作成する必要があります。その後、各人は上述と同じ手順を踏むことになりますが、クラッシュを回避するために場合によっては異なる名前をつけることになります:

  • 各人のホームディレクトリに virtualenv を作成
  • 先に述べたように、異なる Postgres データベースを作成し、これを DATABASE_URL 環境変数に設定

これらの手順が完了したら、2人目のユーザはコードをサーバにアップロードしてから otree resetdb を実行します。

複数の人が同時に実験を行う必要がない場合は、 otree prodserver 80 を使って、各ユーザが交代で 80 番ポートでサーバを動かすことができます。しかし、複数の人が同時に実験を行う必要がある場合は、 80008001 など複数のポートでサーバを動かす必要があります。

Windows Server (高度な設定)

個人のコンピュータでアプリをテストするだけなら、 otree zipserverotree devserver を使うことができます。アプリを公開するために必要な、後述のような完全なサーバ設定は必要ありません。

ここでは、Web サーバの設定に慣れている方を対象としています。もっと簡単に早く済ませたいという方には、 Heroku の利用をお勧めします。

サーバソフトをインストールする必要がある理由

oTree の開発用セットアップ (devserver) は、実際の研究を行うためのものではありません。

データベース (Postgres)

Postgres と psycopg2 をインストールして、新しいデータベースを作成し、 DATABASE_URL 環境変数を設定してください (例: postgres://postgres@localhost/django_db` )。

resetdb

上記の手順がすべてうまくいっていれば、 otree resetdb を実行できるはずです。

本番用サーバの実行

以下を実行します:

otree prodserver 80

詳しい手順は こちら を参照してください。手順は基本的に Linux と同じです。

環境変数の設定

OTREE_ADMIN_PASSWORD, OTREE_PRODUCTION, OTREE_AUTH_LEVEL を設定します。

管理者

oTreeの管理画面を使用すると、セッションからデータを作成、監視、およびエクスポートができます。

ブラウザを開いて、``localhost:8000``またはあなたのサーバーのURLを入力します。

パスワード保護

oTreeを最初にインストールしたときは、パスワードなしで管理画面全体にアクセスできます。しかし、実験を実施する準備ができたら、管理者をパスワードで保護する必要があります。

実験を行う際、訪問者にスタートリンクを提供した場合にのみアプリをプレイできるようにしたい場合は、環境変数 OTREE_AUTH_LEVELSTUDY に設定してください。

誰でもゲームのデモ版をプレイできる公開デモモードにしたい場合は、 OTREE_AUTH_LEVELDEMO に設定してください。

通常の管理者ユーザー名は「admin」です。パスワードは環境変数 OTREE_ADMIN_PASSWORD に設定してください(Herokuでは、Herokuのダッシュボードにログインして、config varとして定義してください)。

もし管理者のユーザ名やパスワードを変更したい場合には、データベースをリセットする必要があります。

参加者ラベル

room の使用にかかわらず、 participant_label を識別子として各参加者の開始URLに加えることができます。例えば、名前や、ID、コンピュータ名等です。例:

http://localhost:8000/room/my_room_name/?participant_label=John

oTreeはこの参加者ラベルを記録し、oTreeの管理画面や支払いページ等で参加者を識別できるようにします。また、コード上で participant_label としてアクセスすることができます。

また、参加者ラベルを利用することで、セッションワイドリンクを同じ参加者が2回開いてしまっても、重複して参加することを防ぐことができます。

到着順序

oTreeは、単体リンクを利用している場合を除き、最初にページを開いた人からP1、P2と参加者にIDを割り当てていきます。

管理画面のカスタマイズ(管理レポート)

セッションの管理画面で、必要なコンテンツを含むページを追加できます。例:

  • ゲームの結果を示すチャート/グラフ
  • 独自にカスタマイズした支払いページ

スクリーンショット

_images/admin-report.png

ここで、特定のラウンドの利得のソート済みのリストを表示する管理レポートを追加する簡単な例を示します。

まず、関数 vars_for_template() と同様の働きをする関数 vars_for_admin_report を定義します。例:

def vars_for_admin_report(subsession):
    payoffs = sorted([p.payoff for p in subsession.get_players()])
    return dict(payoffs=payoffs)

次に、アプリ内に組み込み可能なテンプレート admin_report.html を作成します。そして、 vars_for_admin_report で渡された変数を表示します。例:

<p>Here is the sorted list of payoffs in round {{ subsession.round_number }}</p>

<ul>
    {{ for payoff in payoffs }}
        <li>{{ payoff }}</li>
    {{ endfor }}
</ul>

注意

  • subsessionsessionC は自動的にテンプレートに渡されます。
  • admin_report.html において、 {{ block }} を使用する必要はありません。上記の例は、 admin_report.html の完全な内容として有効です。

セッション内の1つ以上のアプリが admin_report.html を持つ場合、管理画面に "レポート" タブが表示されます。メニューからアプリとラウンドを指定し、サブセッションのレポートを表示してください。

データのエクスポート

管理画面で、 "Data" をクリックすることで、CSVまたはExcelデータとしてダウンロードできます。

これは、ユーザーがすべてのページを完了した正確な時間である "page timesもエクスポートできます。このPythonスクリプト は各ページに費やされた時間を表にすることができます。このスクリプトを変更して、各参加者が待機ページに費やした合計時間などを計算することもできます。

カスタムデータのエクスポート

アプリ用に独自にカスタムしたデータのエクスポートを作成できます。oTree Studioで、 "Player" モデルに移動し、下部にある "custom_export" をクリックします。(テキストエディタを使用する場合は、以下の関数を定義します。)引数 players は、データベース内のすべてのプレイヤーのクエリセットです。データの各行について、yield を使用していください。

def custom_export(players):
    # header row
    yield ['session', 'participant_code', 'round_number', 'id_in_group', 'payoff']
    for p in players:
        participant = p.participant
        session = p.session
        yield [session.code, participant.code, p.round_number, p.id_in_group, p.payoff]

Or, you can ignore the players argument and export some other data instead, e.g.:

def custom_export(players):
    # Export an ExtraModel called "Trial"

    yield ['session', 'participant', 'round_number', 'response', 'response_msec']

    # 'filter' without any args returns everything
    trials = Trial.filter()
    for trial in trials:
        player = trial.player
        participant = player.participant
        session = player.session
        yield [session.code, participant.code, player.round_number, trial.response, trial.response_msec]

この関数を定義すると、カスタムデータのエクスポートが、通常のデータエクスポートページで利用できるようになります。

デバッグ情報

oTreeが DEBUG モードで実行されている場合(環境変数 OTREE_PRODUCTION が設定されていない場合 )、デバッグ情報がすべての画面の下部に表示されます。

支払い

もし、あなたが finished という 参加者フィールド を定義するのであれば、参加者がセッションを終えるときに、 participant.finished = True に設定することでできます。そして、これは支払いページのようなさまざまな場所に表示されます。

参加者と実験者間のチャット

参加者がチャットメッセージを送信できるようにするには、 Papercups のようなソフトウェアの使用を検討してください。Papercupsサーバーをワンクリックでセットアップするには、 "[Deploy toHeroku]" ボタンをクリックし、必要な構成変数のみを入力してください。そして、oTreeサイトではなく、Papercupsのサイトである、 BACKEND_URLREACT_APP_URL を参照してください。サイトにログインし、papercups.htmlという名前のテンプレートにHTML埋め込みコードをコピーします。また、このページ に"実験者とチャット" を行うことができるアプリの例があります。

ルーム

oTreeでは "ルーム" を設定することができます。次の機能を提供します:

  • 参加者や実験室のコンピューターに割り当てることができるリンク (セッションを跨いで同じリンクを使用できます)
  • セッション開始を待っている参加者を確認できる "待機部屋"
  • 参加者が簡単に入力できる短いリンク (短時間のライブデモに適しています)

以下がスクリーンショットです:

_images/room-combined.png

ルームの作成

複数の部屋を作成することができます。例えば、教えるクラスごとや管理する実験室ごとに部屋を作成することができます。

oTree Studio を使用している場合

サイドバーの "Settings" にアクセスし、一番下にある部屋を追加します。

PyCharm を使用している場合

settings.pyROOMS を設定してください。

例:

ROOMS = [
    dict(
        name='econ101',
        display_name='Econ 101 class',
        participant_label_file='_rooms/econ101.txt',
        use_secure_urls=True
    ),
    dict(
        name='econ_lab',
        display_name='Experimental Economics Lab'
    ),
]

参加者ラベル (下記参照) を使用している場合は、参加者ラベルのテキストファイルへの相対パス (もしくは絶対パス) を participant_label_file に設定する必要があります。

部屋の設定

参加者ラベル

参加者ラベルはルームの "ゲストリスト" です。1行に1つの参加者ラベルが含まれています。例えば、以下のようになります:

LAB1
LAB2
LAB3
LAB4
LAB5
LAB6
LAB7
LAB8
LAB9
LAB10

参加者ラベルを指定していない場合は、ルーム URL を知っていれば誰でも参加できます。詳しくは participant_label_file がない場合 を参照してください。

use_secure_urls (オプション)

この設定は participant_label_file に追加のセキュリティを提供します。例えば、セキュア URL を使用しない場合、開始 URL は以下のようになります:

http://localhost:8000/room/econ101/?participant_label=Student1
http://localhost:8000/room/econ101/?participant_label=Student2

もし Student1 に悪意があるなら、自分の URL の participant_label を "Student1" から "Student2" に変更して、Student2 になりすますかもしれません。しかし、 use_secure_urls を使用すると、各URL には以下のようなユニークなコードが付与されます:

http://localhost:8000/room/econ101/?participant_label=Student1&hash=29cd655f
http://localhost:8000/room/econ101/?participant_label=Student2&hash=46d9f31d

このとき、Student1 はユニークコードを知らなければ Student2 になりすますことができません。

ルームの使用

管理画面のヘッダーバーにある "Rooms" をクリックして、作成したルームをクリックします。参加者の URL が記載されているセクションまでスクロールダウンします。

participant_label_file がある場合

ルームの管理画面で、どの参加者が参加しているかをモニターし、準備ができたら希望の人数分のセッションを作成します。

参加者専用 URL を使うか、ルーム URL のどちらかを使うことができます。

参加者専用の URL には、すでに参加者ラベルが含まれています。例えば、以下のようになります:

http://localhost:8000/room/econ101/?participant_label=Student2

ルーム URL には参加者ラベルは含まれていません:

http://localhost:8000/room/econ101/

そのため、ルーム URL を使用する場合、参加者は参加者ラベルを入力する必要があります:

_images/room-combined.png

participant_label_file がない場合

各参加者にルームの URL を開いてもらいます。その後、ルームの管理ページで、何人の参加者がいるかを確認し、希望の人数分のセッションを作成してください。

このオプションは簡単ですが、参加者ラベルを使用するよりも信頼性が低くなります。理由は、誰かが 2 つの異なるブラウザで URL を開くことで 2 回プレイできるからです。

複数セッションへの再利用

ルームのURLは、セッション間で再利用できるように設計されています。実験室では、ブラウザのホームページとして設定することができます (ルーム URL または 参加者専用 URL を使用できます)。

教室での実験では、各生徒に固有の URL を与えて、それを学期を通して使用することができます。

参加者全員が来ない場合

実験室での実験で、参加者の数が予測できない場合は、ルーム URL を使用し、参加者に参加者ラベルを手動で入力してもらうことを検討しましょう。参加者は、参加者ラベルを入力して初めて出席者としてカウントされます。

あるいは、ブラウザで参加者専用 URL を開きますが、セッションを作成する前に、無人のコンピュータのブラウザを閉じておくこともできます。

参加者は、セッションが作成された後でも、参加枠が残っていれば参加することができます。

参加者ラベルへの事前割り当て

oTree では、到着時間に基づいて参加者を割り当てています。しかし、状況によっては、これが望ましくない場合があります。例えば、次のような場合です:

  • 参加者ラベルを oTree の ID と同じ順序で並べたい場合。例えば、LAB29 が常に参加者 29 になるようにしたい場合。
  • Alice/Bob/Charlie が常に参加者 1/2/3 になるようにグループ化して一緒にプレイさせたい場合

参加者ラベルを creating_session に割り当ててください:

def creating_session(subsession):
    labels = ['alice', 'bob', 'charlie']
    for player, label in zip(subsession.get_players(), labels):
        player.participant.label = label

誰かが participant_label=alice と書かれた開始リンクを開いた場合、oTree はそのセッションの参加者が既にそのラベルを持っているかどうかをチェックします。 (これにより、開始リンクを再びクリックすると同じ参加者として戻ることができます。)

参加者に関するデータをoTreeに渡す方法

"Participant vars for room" エンドポイント を参照

通貨 (Currency)

多くの実験では、参加者は通貨を求めてプレイします。oTree では通貨として、実世界の通貨、もしくはポイントを設定することができます。設定で USE_POINTS = False と設定することで、ポイントから実世界の通貨に切り替えることができます。

cu(42) は "42 通貨単位" を表します。これは数字と同じように扱うことができます (例: cu(0.1) + cu(0.2) == cu(0.3))。この機能の利点は、ユーザーに表示されるときに REAL_WORLD_CURRENCY_CODELANGUAGE_CODE の設定に応じて、自動的に $0.300,30 などの形式に変換されることです。

注釈

cu() は oTree 5 の新機能です。これまでは、通貨を表すために c() を使用していました。すでに c() を使用しているコードは引き続き使用できます。詳細は こちら をご覧ください。

データベースに通貨を格納するには CurrencyField を使用します。例えば、以下のようになります:

class Player(BasePlayer):
    random_bonus = models.CurrencyField()

通貨の金額のリストを作るには currency_range を使用します:

currency_range(0, 0.10, 0.02)
# this gives:
# [$0.00, $0.02, $0.04, $0.06, $0.08, $0.10]

テンプレートでは cu() 関数を使う代わりに、 |cu フィルターを使用します。例えば、 {{ 20|cu }}20 ポイント と表示されます。

利得 (payoff)

各プレイヤーは、 payoff フィールドを持っています。プレイヤーがお金を稼いだ場合は、このフィールドに格納する必要があります。participant.payoff には、すべてのサブセッションからの利得の合計が自動的に格納されます。最終的な利得を整数に丸めるなどしたい場合には、 participant.payoff を直接変更することができます。

実験の最後に、ある参加者の利益の合計にアクセスするには、 participant.payoff_plus_participation_fee() を使用します。この関数は、 participant.payoff を実世界の通貨単位に変換し (USE_POINTSTrue の場合)、session.config['participation_fee'] を加えた金額を計算します。

ポイント ("実験用通貨")

USE_POINTS = True と設定すると、通貨の金額がドルやユーロなどではなく、ポイントになります。例えば、 cu(10)10 ポイント と表示されます。

real_world_currency_per_point エントリをセッションコンフィグに追加することで、ポイントから実世界の通貨への変換率を決めることができます。

ポイントから実世界の通貨への変換

ポイントを実世界の通貨に変換するには、 .to_real_world_currency メソッドを使用します。例えば、以下のようになります:

cu(10).to_real_world_currency(session)

(セッションが異なると変換率も異なるため、 session が必要になります。)

小数点以下の表示

金額は小数点以下2桁で表示されます。

一方、ポイントは整数です。つまり、 10÷33 になるように、金額は整数に丸められます。そのため、丸め誤差が気にならない程度にポイントの規模を大きくすることをお勧めします。例えば、ゲームの資金を 100 ポイントではなく、1000 ポイントに設定します。

MTurk & Prolific

MTurk

Overview

oTree provides integration with Amazon Mechanical Turk (MTurk):

  1. From oTree's admin interface, you publish your session to MTurk.
  2. Workers on Mechanical Turk participate in your session.
  3. From oTree's admin interface, you send each participant their participation fee and bonus (payoff).

Installation

MTurk template

Put the following inside your mturk_template.html:

<script src="https://assets.crowd.aws/crowd-html-elements.js"></script>

<crowd-form>
  <div style="padding: 20px">
    <p>
      This HIT is an academic experiment on decision making from XYZ University....
      After completing this HIT, you will receive your reward plus a bonus payment....
    </p>

    <p>After
      you have accepted this HIT, the URL to the study will appear here: <b><a class="otree-link">link</a></b>.
    </p>
    <p>
      On the last page, you will be given a completion code.
      Please copy/paste that code below.
    </p>

    <crowd-input name="completion_code" label="Enter your completion code here" required></crowd-input>
    <br>
  </div>
</crowd-form>

You can easily test out the appearance by putting it in an .html file on your desktop, then double-clicking the HTML file to open it in your browser. Modify the content inside the <crowd-form> as you wish, but make sure it has the following:

  1. The link to the study, which should look like <a class="otree-link">Link text</a>. Once the user has accepted the assignment, oTree will automatically add the href to those links to make them point to your study.
  2. If you want the completion code to be displayed in the oTree Admin interface (Payments tab), you need a <crowd-input> named completion_code.

Making your session work on MTurk

On the last page of your study, give the user a completion code. For example, you can simply display: "You have completed the study. Your completion code is TRUST2020." If you like, you can generate unique completion codes. You don't need to worry too much about completion codes, because oTree tracks each worker by their MTurk ID and displays that in the admin interface and shows whether they arrived on the last page. The completion code is just an extra layer of verification, and it gives workers a specific objective which they are used to having.

Extra steps for non-Studio users

If you are not using oTree Studio, you need to additionally follow the steps here.

Local Sandbox testing

Before launching a study, you must create an employer account with MTurk, to get your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.

You can obtain these credentials in your AWS Management Console.

To test in the MTurk Sandbox locally, and see how it will appear to workers, you need to store these credentials onto your computer.

If using Windows, search for "environment variables" in the control panel, and create 2 environment variables so it looks like this:

_images/env-vars.png

On Mac, put your credentials into your ~/.bash_profile file like this:

export AWS_ACCESS_KEY_ID=AKIASOMETHINGSOMETHING
export AWS_SECRET_ACCESS_KEY=yoursecretaccesskeyhere

Restart your command prompt and run oTree. From the oTree admin interface, click on "Sessions" and then, on the button that says "Create New Session", select "For MTurk":

_images/create-mturk-session.png

Set environment variables on your web server

If using Heroku, go to your App Dashboard's "settings", and set the config vars AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.

Qualification requirements

oTree uses boto3 syntax for qualification requirements. Here is an example with 2 qualification requirements that you can paste into your qualification_requirements setting:

[
    {
        'QualificationTypeId': "3AWO4KN9YO3JRSN25G0KTXS4AQW9I6",
        'Comparator': "DoesNotExist",
    },
    {
        'QualificationTypeId': "4AMO4KN9YO3JRSN25G0KTXS4AQW9I7",
        'Comparator': "DoesNotExist",
    },
]

Here is how you would require workers from the US. (00000000000000000071 is the code for a location-based qualification.)

[
    {
        'QualificationTypeId': "00000000000000000071",
        'Comparator': "EqualTo",
        'LocaleValues': [{'Country': "US"}]
    },
]

See the MTurk API reference. (However, note that the code examples there are in JavaScript, so you would need to modify the syntax to make it work in Python, e.g. adding quotes around dictionary keys.)

Note: when you are in sandbox mode, oTree ignores qualification requirements.

Preventing retakes (repeat workers)

To prevent a worker from participating twice, you can grant a Qualification to each worker in your study, and then block people who already have this Qualification.

Login to your MTurk requester account and create a qualification. Go to your oTree MTurk settings and paste that qualification ID into grant_qualification_id. Then, add an entry to qualification_requirements:

{
    'QualificationTypeId': "YOUR_QUALIFICATION_ID_HERE",
    'Comparator': "DoesNotExist",
},

Multiplayer games & dropouts

Games that involve wait pages are difficult on Mechanical Turk, because some participants drop out or delay starting the game until some time after accepting the assignment.

To mitigate this, see the recommendations in プレイヤーがWaitPageで待機し続けるのを防ぐ.

When you create a session with N participants for MTurk, oTree actually creates (N x 2) participants, because spares are needed in case some MTurk workers start but then return the assignment.

Managing your HITs

oTree provides the ability to approve/reject assignments, send bonuses, and expire HITs early.

If you want to do anything beyond this, (e.g. extend expiration date, interact with workers, send custom bonuses, etc), you will need to install the MTurk command-line tools.

Misc notes

If you are publishing to MTurk using another service like TurkPrime, you may not need to follow the steps on this page.

Prolific

If you're using Prolific, we recommend setting up oTree HR, which will automatically handle start links, completion URLs, and payments.

その他

REST

oTreeには、外部プログラム(他のWebサイトなど)がoTreeと通信できるようにする REST API があります。

REST APIは、プログラムからアクセスできるように設計されたサーバー上の単なるURLです。

REST APIを頻繁に使用するプロジェクトの1つは、 oTree HR です。

セットアップ

注釈

「このコードはどこに配置すればいいですか」

このコードは、oTreeプロジェクトフォルダー内に配置する必要はありません。REST APIのポイントは、外部プログラムとサーバーがインターネットを介してoTreeと通信できるようにすることであるため、このコードを他のプログラムに配置する必要があります。これは、他のサーバーが使用する言語を使用しなければならないことも意味します。このページの例ではPythonを使用していますが、WebhookやcURLなどのツールの利用や他の言語によってHTTPリクエストを作成するのも簡単です。

import requests  # pip3 install requests
from pprint import pprint


GET = requests.get
POST = requests.post

# if using Heroku, change this to https://YOURAPP.herokuapp.com
SERVER_URL = 'http://localhost:8000'
REST_KEY = ''  # fill this later

def call_api(method, *path_parts, **params) -> dict:
    path_parts = '/'.join(path_parts)
    url = f'{SERVER_URL}/api/{path_parts}/'
    resp = method(url, json=params, headers={'otree-rest-key': REST_KEY})
    if not resp.ok:
        msg = (
            f'Request to "{url}" failed '
            f'with status code {resp.status_code}: {resp.text}'
        )
        raise Exception(msg)
    return resp.json()

"oTree version" エンドポイント

注釈

2021年3月現在の新しいベータ機能。

GET URL: /api/otree_version/

例:
data = call_api(GET, 'otree_version')
# returns: {'version': '5.0.0'}

"Session configs" エンドポイント

注釈

2021年3月現在の新しいベータ機能。

GET URL: /api/session_configs/

すべてのセッション構成のリストをすべてのプロパティを含む辞書型として返します。

例:
data = call_api(GET, 'session_configs')
pprint(data)

"Rooms" エンドポイント

注釈

2021年3月現在の新しいベータ機能。

GET URL: /api/rooms/

例:
data = call_api(GET, 'rooms')
pprint(data)

出力例(現在、roomにセッションがある場合、 session_code が含まれているかについて注意してください):

[{'name': 'my_room',
  'session_code': 'lq3cxfn2',
  'url': 'http://localhost:8000/room/my_room'},
 {'name': 'live_demo',
  'session_code': None,
  'url': 'http://localhost:8000/room/live_demo'}]

"Create sessions" エンドポイント

POST URL: /api/sessions/

"create sessions" エンドポイントの使用例を次に示します。

  • 他のウェブサイトはoTreeセッションの自動生成
  • oTreeの セッションの設定 インターフェイスの代替手段の作成(例: スライダーやビジュアルウィジェットなどの使用)
  • 一定期間ごとの新しいoTreeセッションの作成
  • カスタマイズされたセッションを作成するためのコマンドラインスクリプト( otree create_session で十分でなければ)
例:
data = call_api(
    POST,
    'sessions',
    session_config_name='trust',
    room_name='econ101',
    num_participants=4,
    modified_session_config_fields=dict(num_apples=10, abc=[1, 2, 3]),
)
pprint(data)
パラメーター
  • session_config_name (必須)
  • num_participants (必須)
  • modified_session_config_fields: セッションの設定 で説明されているように、セッション構成パラメーターのオプションの辞書型リスト 。
  • roomでセッションを作成する場合は、 room_name

"Get session data" エンドポイント

注釈

2021年3月現在の新機能。十分なユーザーフィードバックが得られるまでベータ版です。

GET URL: /api/sessions/{code}

このAPIは、セッションとその参加者に関するデータを取得します。 participant_labels を省略した場合、すべての参加者のデータを返します。

例:
data = call_api(GET, 'sessions', 'vfyqlw1q', participant_labels=['Alice'])
pprint(data)

"Session vars" エンドポイント

注釈

2021年4月の時点で、このエンドポイントでは、パスのパラメータとしてセッションコードを渡す必要があります。room内にセッションがある場合は、 rooms エンドポイントでセッションコードを取得できます。

POST URL: /api/session_vars/{session_code}

このエンドポイントでは、 session.vars を設定できます。1つの用途は実験者の入力です。例えば、実験の途中で抽選を行った場合、以下のようなスクリプトを実行して結果を入力することができます。

例:
call_api(POST, 'session_vars', 'vfyqlw1q', vars=dict(dice_roll=4))

"Participant vars" エンドポイント

POST URL: /api/participant_vars/{participant_code}

WebサービスやWebhookを介して、参加者に関する情報をoTreeに渡します。

例:
call_api(POST, 'participant_vars', 'vfyqlw1q', vars=dict(birth_year='1995', gender='F'))

"Participant vars for room" エンドポイント

POST URL: /api/participant_vars/

他の "participant vars" エンドポイントと同様ですが、参加者のコードがない場合にも使用できます。参加者のコードの代わりに、room名と参加者ラベルで参加者を識別します。

例:
call_api(
    POST,
    'participant_vars',
    room_name='qualtrics_study',
    participant_label='albert_e',
    vars=dict(age=25, is_male=True, x=[3, 6, 9]),
)
パラメーター
  • room_name (必須)
  • participant_label (必須)
  • vars (必須): 追加するparticipant varsの辞書型。値はネストされた辞書型やリストであっても、JSONでシリアル化可能な任意のデータ型にすることができます。

participant_label_file から取得する必要はありませんが、参加者に participant_label とともにリンクを与える必要があります。

認証

認証レベルをDEMOまたはSTUDYに設定している場合は、REST APIリクエストを認証する必要があります。

サーバー上に環境変数(つまり、Heroku config var) OTREE_REST_KEY を作成してください。値を秘密の値として設定して下さい。

リクエストを行うときは、そのキーを otree-rest-key と呼ばれるHTTPヘッダーとして追加してください。上記の 設定例 に従う場合は、 REST_KEY 変数を設定します。

デモとテスト

開発を快適にするために、偽の変数を生成し、実際のセッションでREST APIから取得されるデータをシミュレートできます。

セッション構成で、パラメーター mock_exogenous_data=True を追加してください。(oTreeの外部で発生するため、「exogenous」データと呼んでいます。)

次に、プロジェクトのshared_out.pyで 同じ名前( mock_exogenous_data )の関数を定義します。(テキストエディターを使用している場合は、そのファイルを作成する必要があるかもしれません。)

例:

def mock_exogenous_data(session):
    participants = session.get_participants()
    for pp in participants:
        pp.vars.update(age=20, is_male=True) # or make it random

ここで参加者ラベルを設定することもできます。

デモモードまたはボットを使用してセッションを実行すると、 mock_exogenous_data()creating_session の後、自動的に実行されます。ただし、セッションがroomで作成されている場合は実行されません。

異なるexogenousデータを必要とする複数のセッション構成がある場合は、次のように分岐できます。

def mock_exogenous_data(session):
    if session.config['name'] == 'whatever':
        ...
    if 'xyz' in session.config['app_sequence']:
        ...

Localization

Changing the language setting

Go to your settings and change LANGUAGE_CODE:.

For example:

LANGUAGE_CODE = 'fr' # French
LANGUAGE_CODE = 'zh-hans' # Chinese (simplified)

This will customize certain things such validation messages and formatting of numbers.

Writing your app in multiple languages

You may want your own app to work in multiple languages. For example, let's say you want to run the same experiment with English, French, and Chinese participants.

For an example, see the "multi_language" app here.

ヒントとコツ

コードの重複の防止

可能な限り、同じコードを複数の場所にコピーして貼り付けるべきではありません。工夫が必要な場合もありますが、コードを1か所にまとめることで、コードのデザインの変更やバグの修正をしたりする必要があるときに、通常は多くの労力を節約できます。

以下は、コードの再利用を実現するためのいくつかの手法です。

アプリのコピーを複数作成しないでください

可能であれば、アプリのフォルダーをコピーしてわずかに異なるバージョンを作成することは避けてください。コードが重複しているため、保守が困難になります。

複数のラウンドを用意したいだけなら、 NUM_ROUNDS を設定することで解決できますまた、わずかに異なるバージョン(たとえば、異なる処理)が必要な場合は、異なる 'treatment' パラメーターを持つ2つのセッション構成を作成し、コード上で session.config['treatment'] についてチェックするなど、 処理の割り当て で説明されている手法を使用する必要があります。

多くのフィールドを作成する方法

アプリに、次のようなほぼ同じフィールドが多数あるとします。

class Player(BasePlayer):

    f1 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f2 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f3 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f4 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f5 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f6 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f7 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f8 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f9 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f10 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )

    # etc...

これは非常に複雑です。単純化する方法を探す必要があります。

フィールドはすべて別々のページに表示されていますか?そうであれば、1つのフィールドだけ用意し、10ラウンドのゲームに変換することを検討してください。

それが不可能な場合は、フィールドを返す関数を定義することで、繰り返されるコードの量を減らすことができます。

def make_field(label):
    return models.IntegerField(
        choices=[1,2,3,4,5],
        label=label,
        widget=widgets.RadioSelect,
    )

class Player(BasePlayer):

    q1 = make_field('I am quick to understand things.')
    q2 = make_field('I use difficult words.')
    q3 = make_field('I am full of ideas.')
    q4 = make_field('I have excellent ideas.')
複数のラウンドを使用してページの重複を防止

ほぼ同じページが多数ある場合は、1ページだけを作成し、それを複数のラウンドでループさせることを検討してください。次のような場合、コードを簡略化できる可能性があります。

# [pages 1 through 7....]

class Decision8(Page):
    form_model = 'player'
    form_fields = ['decision8']

class Decision9(Page):
    form_model = 'player'
    form_fields = ['decision9']

# etc...
検証方法の重複を避ける

多くの繰り返される FIELD_error_message メソッドがある場合は、それらを単一の error_message 関数に置き換えることができます。例:

def quiz1_error_message(player, value):
    if value != 42:
        return 'Wrong answer'

def quiz2_error_message(player, value):
    if value != 'Ottawa':
        return 'Wrong answer'

def quiz3_error_message(player, value):
    if value != 3.14:
        return 'Wrong answer'

def quiz4_error_message(player, value):
    if value != 'George Washington':
        return 'Wrong answer'

代わりに、ページでこの関数を定義することもできます。

@staticmethod
def error_message(player, values):
    solutions = dict(
        quiz1=42,
        quiz2='Ottawa',
        quiz3='3.14',
        quiz4='George Washington'
    )

    error_messages = dict()

    for field_name in solutions:
        if values[field_name] != solutions[field_name]:
            error_messages[field_name] = 'Wrong answer'

    return error_messages

(通常 error_message は、単一のエラーメッセージを文字列として返すために使用されますが、辞書型を返すこともできます。)

ページ関数の重複を避ける

すべてのページ関数は、ページクラスから最上位の関数に移動できます。これは、同じ関数を複数のページで共有するための便利な方法です。たとえば、多くのページに次の2つの関数が必要だとします。:

class Page1(Page):
    @staticmethod
    def is_displayed(player: Player):
        participant = player.participant

        return participant.expiry - time.time() > 0

    @staticmethod
    def get_timeout_seconds(player):
        participant = player.participant
        import time
        return participant.expiry - time.time()

これらの関数をすべてのページの前に移動させ( @staticmethod を削除)、使用する必要があるどこからでも参照することができます。

def is_displayed1(player: Player):
    participant = player.participant

    return participant.expiry - time.time() > 0


def get_timeout_seconds1(player: Player):
    participant = player.participant
    import time

    return participant.expiry - time.time()


class Page1(Page):
    is_displayed = is_displayed1
    get_timeout_seconds = get_timeout_seconds1


class Page2(Page):
    is_displayed = is_displayed1
    get_timeout_seconds = get_timeout_seconds1

(サンプルゲームで、 after_all_players_arrivelive_method は、このように定義されることが多いです。)

コードパフォーマンスの向上

get_players()get_player_by_id()in_*_rounds()get_others_in_group() のようなプレイヤーのリストやプレイヤーを返す関数の過剰な使用を避けるべきですこれらのメソッドはすべてデータベースクエリを必要としますが、処理に時間がかかる場合があります。

たとえば、このコードはデータベースにまったく同じプレイヤーを5回要求するため、冗長なクエリが発生してしまいます。

@staticmethod
def vars_for_template(player):
    return dict(
        a=player.in_round(1).a,
        b=player.in_round(1).b,
        c=player.in_round(1).c,
        d=player.in_round(1).d,
        e=player.in_round(1).e
    )

これは次のように簡略化する必要があります。

@staticmethod
def vars_for_template(player):
    round_1_player = player.in_round(1)
    return dict(
        a=round_1_player.a,
        b=round_1_player.b,
        c=round_1_player.c,
        d=round_1_player.d,
        e=round_1_player.e
    )

さらに、コードをより読みやすくすることも期待できます。

可能であれば、StringFieldの代わりにBooleanFieldを使用してください

特に、2つの異なる値しかない場合は、多くの StringFieldsBooleanFields に分解されるべきです。

treatment という次のフィールドがあるとします。

treatment = models.StringField()

そして、 treatment は4つの異なる値しか持っていないとします。

  • high_income_high_tax
  • high_income_low_tax
  • low_income_high_tax
  • low_income_low_tax

そのままの場合、ページでは次のように記述しなければなりません。

class HighIncome(Page):
    @staticmethod
    def is_displayed(player):
        return player.treatment == 'high_income_high_tax' or player.treatment == 'high_income_low_tax'

class HighTax(Page):
    @staticmethod
    def is_displayed(player):
        return player.treatment == 'high_income_high_tax' or player.treatment == 'low_income_high_tax'

そして、フィールドを2つの別々のBooleanFieldsに分割することを推奨します:

high_income = models.BooleanField()
high_tax = models.BooleanField()

最終的に、ページを次のように簡略化できます。

class HighIncome(Page):
    @staticmethod
    def is_displayed(player):
        return player.high_income

class HighTax(Page):
    @staticmethod
    def is_displayed(player):
        return player.high_tax

field_maybe_none

If you access a Player/Group/Subsession field whose value is None, oTree will raise a TypeError. This is designed to catch situations where a user forgot to assign a value to that field, or forgot to include it in form_fields.

However, sometimes you need to intentionally access a field whose value may be None. To do this, use field_maybe_none, which will suppress the error:

# instead of player.abc, do:
abc = player.field_maybe_none('abc')
# also works on group and subsession

注釈

field_maybe_none is new in oTree 5.4 (August 2021).

An alternative solution is to assign an initial value to the field so that its value is never None:

abc = models.BooleanField(initial=False)
xyz = models.StringField(initial='')

高度な機能

これらの機能の多くはは oTree スタジオにおいて、サポートされていません。

ExtraModel

ExtraModel は1人の参加者に関して多くのデータを保存する必要がある場合に役立ちます。たとえば、入札のリストや反応時間のリスト等です。それらは ライブページ とともに頻繁に使用されます。

ここに多くの例があります。

ExtraModel は他のモデルとリンクする必要があります。

class Bid(ExtraModel):
    player = models.Link(Player)
    amount = models.CurrencyField()

参加者が入札するたびに、データベースに保存されます。

Bid.create(player=player, amount=500)

そして、参加者の入札のリストを取得できます。

bids = Bid.filter(player=player)

ExtraModel は複数のリンクを保持することができます。

class Offer(ExtraModel):
    sender = models.Link(Player)
    receiver = models.Link(Player)
    group = models.Link(Group)
    amount = models.CurrencyField()
    accepted = models.BooleanField()

そして、さまざまな方法でクエリを実行できます。

this_group_offers = Offer.filter(group=group)
offers_i_accepted = Offer.filter(receiver=player, accepted=True)

より複雑なフィルタやソートのためには、リスト操作を使う必要があります。

offers_over_500 = [o for o in Offer.filter(group=group) if o.amount > 500]

CSVスプレッドシートの各行からExtraModelデータを生成する方法を示している、Stroopタスクなどの心理学ゲームの例を参照してください。

To export your ExtraModel data to CSV/Excel, use カスタムデータのエクスポート.

Reading CSV files

注釈

This feature is in beta (new in oTree 5.8.2)

To read a CSV file (which can be produced by Excel or any other spreadsheet app), you can use read_csv(). For example, if you have a CSV file like this:

name,price,is_organic
Apple,0.99,TRUE
Mango,3.79,FALSE

read_csv() will output a list of dicts, like:

[dict(name='Apple', price=0.99, is_organic=True),
 dict(name='Mango', price=3.79, is_organic=False)]

You call the function like this:

rows = read_csv('my_app/my_data.csv', Product)

The second argument is a class that specifies the datatype of each column:

class Product(ExtraModel):
    name = models.StringField()
    price = models.FloatField()
    is_organic = models.BooleanField()

(Without this info, it would be ambiguous whether TRUE is supposed to be a bool, or the string 'TRUE', etc.)

read_csv() does not actually create any instances of that class. If you want that, you must use .create() additionally:

rows = read_csv('my_app/my_data.csv', Product)
for row in rows:
    Product.create(
        name=row['name'],
        price=row['price'],
        is_organic=row['is_organic'],
        # any other args:
        player=player,
    )

The model can be an ExtraModel, Player, Group, or Subsession. It's fine if it also contains other fields; they will be ignored by read_csv().

テンプレート

template_name

テンプレートの名前をページクラスとは異なるものにする必要がある場合(たとえば、複数のページで同じテンプレートを共有している場合)、 template_name を設定します。例:

class Page1(Page):
    template_name = 'app_name/MyPage.html'
CSS/JS とベーステンプレート

アプリのすべてのページに同じ JS / CSS を含めるには、 静的ファイル にテンプレートを配置するか、includable template にテンプレートを配置する必要があります。

静的ファイル

ページに画像(または.css、.jsなどの他の静的ファイル)を含める方法は次のとおりです。

oTreeプロジェクトのルートには、 _static/ フォルダがあります。たとえば、そこに puppy.jpg のようなファイルを配置します。テンプレートで、 {{ static 'puppy.jpg' }} を使用してそのファイルへのURLを取得することができます。

画像を表示するには、次のような <img> タグを使用します。

<img src="{{ static 'puppy.jpg' }}"/>

上記で画像を _static/puppy.jpg に保存しましたが、実際には、ファイル名の競合を防ぐために、アプリの名前でサブフォルダーを作成し、 _static/アプリ名/puppy.jpg として保存することをおすすめします。

次に、HTMLコードは次のようになります。

<img src="{{ static 'your_app_name/puppy.jpg }}"/>

(必要に応じて、静的ファイルをアプリフォルダー内のサブフォルダー static/your_app_name に配置することもできます)

静的ファイルを変更しても更新されない場合は、ブラウザがファイルをキャッシュしていることが考えられます。ページ全体をリロードすることで解決する可能性があります。(通常はCtrl + F5)

ビデオや高解像度の画像がある場合は、オンライン上に保存し、URLで参照することをお勧めします。ファイルサイズが大きいと .otreezip ファイルのアップロードが非常に遅くなる可能性があるためです。

Wait page

カスタム wait page テンプレート

You can make a custom wait page template. For example, save this to your_app_name/MyWaitPage.html:

{{ extends 'otree/WaitPage.html' }}
{{ block title }}{{ title_text }}{{ endblock }}
{{ block content }}
    {{ body_text }}
    <p>
        My custom content here.
    </p>
{{ endblock }}

次に、wait page にこのテンプレートを使用するように指示します。

class MyWaitPage(WaitPage):
    template_name = 'your_app_name/MyWaitPage.html'

そして、通常の方法で vars_for_template を使用できます。実際、 body_texttitle_text 属性は、 vars_for_template の設定のための省略形にすぎません。次の2つのコードは同等です。

class MyWaitPage(WaitPage):
    body_text = "foo"
class MyWaitPage(WaitPage):

    @staticmethod
    def vars_for_template(player):
        return dict(body_text="foo")

カスタム wait page テンプレートをグローバルに適用する場合は、 _templates/global/WaitPage.html テンプレートを保存することで、oTreeは組み込みの wait page ではなく、カスタム wait page を使うように設定されます。

通貨

"points" の名前を "tokens" や "credits" のような他の名前にカスタマイズするためには、 POINTS_CUSTOM_NAME を設定します。例えば、 POINTS_CUSTOM_NAME = 'tokens'

REAL_WORLD_CURRENCY_DECIMAL_PLACES を設定すると、実際の通貨金額の小数点以下の桁数を変更できます。小数点以下の桁数が表示されても、常に0である場合は、データベースをリセットする必要があります。

ボット: 高度な機能

これらの機能の多くはは oTree スタジオにおいて、サポートされていません。

コマンドラインボット

Webブラウザーでボットを実行する代わりに、コマンドラインでボットを実行することもできます。コマンドラインボットはより高速に実行され、セットアップも少なくて済みます。

これを実行します。

otree test mysession

特定の数の参加者でテストするには(それ以外の場合はデフォルトで num_demo_participants ):

otree test mysession 6

すべてのセッション構成のテストを実行するには:

otree test
データのエクスポート

--export フラグを使用して、結果をCSVファイルにエクスポートします。

otree test mysession --export

データが保存されるフォルダを指定するには、次の手順を実行します。

otree test mysession --export=myfolder

コマンドラインブラウザボット

otree browser_bots を使用して、コマンドラインからブラウザボットを起動できます。

  • Google Chromeがインストールされていることを確認するか、 BROWSER_COMMANDsettings.py で設定してください。(下記詳細)。

  • REST の説明に従って、環境変数 OTREE_REST_KEY を設定します。

  • サーバを実行する。

  • すべてのChromeウィンドウを閉じます。

  • これを実行します。

    otree browser_bots mysession
    

これにより、いくつかのChromeタブが起動し、ボットが実行されます。終了すると、タブが閉じ、ターミナルにレポートが表示されます。

Chromeでウィンドウが正しく閉じられない場合は、コマンドを起動する前に、必ずすべてのChromeウィンドウを閉じてください。

リモートサーバー(Herokuなど)上のコマンドラインブラウザボット

サーバーが通常の http://localhost:8000 以外のホスト/ポートで実行されている場合は、 --server-url を渡す必要があります。例えば、Herokuにある場合は、次のようにします。

otree browser_bots mysession --server-url=https://YOUR-SITE.herokuapp.com
セッションの構成とサイズの選択

参加者の数を指定できます。

otree browser_bots mysession 6

次のコマンドを実行することですべてのセッション構成をテストすることができます。

otree browser_bots
ブラウザボット: その他の注意

settings.pyBROWSER_COMMAND を設定することで 、Chrome以外のブラウザを使用できます。そして、oTreeは subprocess.Popen(settings.BROWSER_COMMAND) のようなコマンドを実行してブラウザを開きます。

テストケース

さまざまなテストケースのリストである、属性 cases をPlayerBotクラスに定義できます。たとえば、公共財ゲームでは、次の3つのシナリオをテストすることができます。

  • すべてのプレイヤーが財産の半分を寄付するケース
  • すべてのプレイヤーが何も寄付しないケース
  • すべてのプレイヤーが全財産(100ポイント)を寄付するケース

これらの3つのテストケースをそれぞれ "basic" 、 "min" 、 "max" と呼び、 cases に格納します。次に、oTreeは各テストケースに対して1回ずつ、計3回ボットを実行します。毎回 、 cases から様々な値がボットの self.case に割り当てられます。

例:

class PlayerBot(Bot):

    cases = ['basic', 'min', 'max']

    def play_round(self):
        yield (pages.Introduction)

        if self.case == 'basic':
            assert self.player.payoff == None

        if self.case == 'basic':
            if self.player.id_in_group == 1:
                for invalid_contribution in [-1, 101]:
                    yield SubmissionMustFail(pages.Contribute, {'contribution': invalid_contribution})
        contribution = {
            'min': 0,
            'max': 100,
            'basic': 50,
        }[self.case]

        yield (pages.Contribute, {"contribution": contribution})
        yield (pages.Results)

        if self.player.id_in_group == 1:

            if self.case == 'min':
                expected_payoff = 110
            elif self.case == 'max':
                expected_payoff = 190
            else:
                expected_payoff = 150
            assert self.player.payoff == expected_payoff

注釈

ユースケースを使用する場合、ブラウザボットは単一のケースしか実行しないため、 コマンドラインボット を使用することをおすすめします。

cases はリストである必要がありますが、文字列、整数、さらには辞書など、任意のデータ型を格納することができます。下記のコードは、辞書をケースとして使用する、 trust ゲームのためのボットです。

class PlayerBot(Bot):

    cases = [
        {'offer': 0, 'return': 0, 'p1_payoff': 10, 'p2_payoff': 0},
        {'offer': 5, 'return': 10, 'p1_payoff': 15, 'p2_payoff': 5},
        {'offer': 10, 'return': 30, 'p1_payoff': 30, 'p2_payoff': 0}
    ]

    def play_round(self):
        case = self.case
        if self.player.id_in_group == 1:
            yield (pages.Send, {"sent_amount": case['offer']})

        else:
            for invalid_return in [-1, case['offer'] * C.MULTIPLICATION_FACTOR + 1]:
                yield SubmissionMustFail(pages.SendBack, {'sent_back_amount': invalid_return})
            yield (pages.SendBack, {'sent_back_amount': case['return']})

        yield (pages.Results)


        if self.player.id_in_group == 1:
            expected_payoff = case['p1_payoff']
        else:
            expected_payoff = case['p2_payoff']

        assert self.player.payoff == expected_payoff

error_fields

複数のフィールドを持つフォームで SubmissionMustFail を使用する場合は、 error_fields を使用することができます。

例えば、 age で有効な送信がなされ、 weightheight が無効な送信であったとします。

yield SubmissionMustFail(
    pages.Survey,
    dict(
        age=20,
        weight=-1,
        height=-1,
    )
)

ボットシステムは送信が失敗する理由を正確に教えてくれません。weightheight のどちらが無効なのか、またはその両方が無効であるのか、 error_fields はそのあいまいさを解決できます:

yield SubmissionMustFail(
    pages.Survey,
    dict(
        age=20,
        weight=-1,
        height=-1,
    ),
    error_fields=['weight', 'height']
)

これにより`` weight`` と height にエラーが含まれていることが確認されますが、 age は含まれていません。

error_message がエラーを返す場合、 error_fields['__all__'] になります。

その他の注意

ボットでは、その種のコードが他の場所で推奨されている場合でも、player = self.player (または、 participant = self.participant 等)を割り当てるのは危険です

yield の間にある場合、データが古くなる可能性があるためです。

player = self.player
expect(player.money_left, cu(10))
yield pages.Contribute, dict(contribution=cu(1))
# don't do this!
# "player" variable still has the data from BEFORE pages.Contribute was submitted.
expect(player.money_left, cu(9))

実行中の self.player がデータベースから最新のデータを取得するため、 self.player.money_left を直接使用する方が安全です。

ライブページ

ボットでライブページをテストするために、 tests.py で最上位関数として call_live_method を定義します。(oTree Studioでは使用できません。)この関数は live_method への呼び出しシーケンスをシミュレートします。引数 method は、Playerクラスのライブメソッドをシミュレートします。例えば、 method(3, 'hello') プレイヤー3のliveメソッドを呼び出し、data'hello' を設定します。例:

def call_live_method(method, **kwargs):
    method(1, {"offer": 50})
    method(2, {"accepted": False})
    method(1, {"offer": 60})
    retval = method(2, {"accepted": True})
    # you can do asserts on retval

kwargs には少なくとも以下のパラメータが含まれます。

  • case テストケース を参照。
  • page_class : 現在のページクラス。例えば、 pages.MyPage
  • round_number

call_live_method はグループ内で最速のボットが live_method を持つページに遷移すると自動的に実行されます。(WaitPage でこれを制限しない限り、他のボットはその時点で前のページにある可能性があります。)

oTree Lite

oTree 5 is based on oTree Lite, a new implementation of oTree that runs as a self-contained framework, not dependent on Django.

oTree Lite's codebase is simpler and more self-contained. This makes it easier for me to add new features, investigate bug reports, and keep oTree simple to use.

Django comes with many features "out of the box", so being based on Django initially helped oTree add new features and iterate quickly. However, Django brings complexity and restrictions. In the long run, the "framework inside a framework" approach becomes more of a liability.

Other advantages of oTree Lite:

  • Simpler error messages
  • Fewer dependencies such as Twisted that cause installation problems for some people
  • Compatible with more versions of Python
  • No need for Redis or second dyno
  • Better performance

For the curious people who want to delve into oTree's internal source code, you will have an easier time navigating oTree Lite.

How can I ensure I stay on oTree 3.x?

To ensure that you don't install oTree Lite, you can specify <5 when you upgrade:

pip3 install -U "otree<5"

For Heroku, use one of the following formats in your requirements.txt (replace 3.3.7 with whatever 3.x version you want):

otree<5
# or:
otree>=3.3.7,<5
# or:
otree==3.3.7

Upgrading

注釈

I have set up a live chat on Discord to assist people upgrading from previous versions of oTree to oTree Lite.

oTree Lite is generally compatible with previous oTree apps. However, you will probably see small things that changed, especially in how forms and templates are rendered. This is somewhat inevitable as oTree has undergone a "brain transplant". Please send any feedback to chris@otree.org.

Here are the most important differences:

テンプレート

The template system is basically compatible with Django templates, except that only the basic tags & filters have been implemented:

  • Tags: {{ if }}, {{ for }}, {{ block }}
  • Filters: {{ x|json }}, {{ x|escape }}, {{ x|c }}, {{ x|default("something") }}

There is no floatformat filter, but there are new rounding filters that replace it. For example:

{{ pi|floatformat:0 }} -> {{ pi|to0 }}
{{ pi|floatformat:1 }} -> {{ pi|to1 }}
{{ pi|floatformat:2 }} -> {{ pi|to2 }}

The |safe filter and mark_safe are not needed anymore, because the new template system does not autoescape content. However, if you want to escape content (e.g. displaying an untrusted string to a different player), you should use the |escape filter.

Method calls must be at the end of the expression, and not followed by more dots. For example, if you have a Player method called other_player(), you can do:

Your partner is {{ player.other_player }}

But you cannot do:

Your partner's decision was {{ player.other_player.decision }}
Forms

In templates, if you are doing manual form rendering, you should change {% form.my_field.errors %} to {{ formfield_errors 'my_field' }}.

Older oTree formats

oTree Lite does not implement support for certain features found in older oTree projects. To check you should run otree update_my_code, which will tell you the changes you need to make before your code can run on oTree Lite. (It will also fix a few things automatically.)

A few common issues:

Bootstrap

Since bootstrap 5 beta just got released, I included it in this package. Certain things are different from bootstrap 4; consult the bootstrap migration docs. In my experience the main things that differed are:

  • data-* attributes are renamed to data-bs-*
  • form-group no longer exists
Misc
  • In get_group_matrix returns a matrix of integers, rather than a matrix of player objects. To preserve the previous behavior, you should pass objects=True, like .get_group_matrix(objects=True).
  • Translating an app to multiple languages works differently. See Localization.
  • If you try to access a Player/Group/Subsession field whose value is still None, oTree will raise an error. More details here: field_maybe_none.
Django

This new implementation does not use Django or Channels in any way. So, it will not run any code you got from Django documentation, such as Django views, ModelForms, ORM, etc.

Version history

Version 5.10

For IntegerField/FloatField/CurrencyField, if min is not specified, it will be assumed to be 0. If you need a form field to accept negative values, set min= to a negative value (or None).

Benefits of this change:

  • Most numeric inputs on mobile can now use the numeric keypad
  • Prevents unintended negative inputs from users. For example, if you forgot to specify min=0 for your "contribution" field, then a user could 'hack' the game by entering a negative contribution.

Other changes:

  • MTurk integration works even on Python >= 3.10 (removed dependency on the boto3 library)
  • Python 3.11 support
  • bots: better error message when bot is on the wrong page

Version 5.9

  • Improved dropout detection
  • Renamed formInputs (JavaScript variable) to forminputs
  • 5.9.5: fix bug that points inputs allow decimal numbers when they should be whole numbers.

Version 5.8

  • Better dropout detection with group_by_arrival_time; see here.
  • Python 3.10 support
  • Fix various websocket-related errors such as ConnectionClosedOK, IncompleteReadError, ClientDisconnect that tend to happen intermittently, especially with browser bots.

Version 5.6

  • Added access to form inputs through JavaScript.

Version 5.4

  • PARTICIPANT_FIELDS are now included in data export
  • field_maybe_none
  • Radio buttons can now be accessed by numeric index, e.g. {{ form.my_field.0 }}.
  • Bugfix with numpy data types assigned to model fields
  • Misc improvements and fixes

Version 5.3

  • Bugfix to deleting sessions in devserver
  • {{ static }} tag checks that the file exists
  • In SessionData tab, fix the "next round"/"previous round" icons on Mac
  • Fix to currency formatting in Japanese/Korean/Turkish currency (numbers were displayed with a decimal when there should be none)
  • allow error_message to be run on non-form pages (e.g. live pages)
  • Better error reporting when an invalid value is passed to js_vars
  • Minor fixes & improvements

Version 5.2

  • For compatibility with oTree 3.x, formfield <input> elements now prefix their id attribute with id_. If you use getElementById/querySelector/etc. to select any formfield inputs, you might need to update your selectors.
  • The data export now outputs "time started" as UTC.
  • "Time spent" data export has a column name change. If you have been using the pagetimes.py script, you should download the new version.

Version 5.1

  • Breaking changes to REST API

Version 5.0

  • oTree Lite
  • The no-self format
  • The beta method Player.start() has been removed.
  • cu() is now available as an alias for Currency. c() will still work as long as you have from otree.api import Currency as c at the top of your file. More details here.
  • oTree 3.x used two types of tags in templates: {{ }} and {% %}. Starting in oTree 5, however, you can forget about {% %} and just use {{ }} everywhere if you want. More details here.
  • All REST API calls now return JSON

Version 3.3

  • BooleanField now uses radio buttons by default (instead of dropdown)
  • otree zip can now keep your requirements.txt up to date.
  • oTree no longer installs sentry-sdk. If you need Sentry on Heroku, you should add it to your requirements.txt manually.
  • Faster server
  • Faster startup time
  • Faster installation
  • Data export page no longer outputs XLSX files. Instead it outputs CSV files formatted for Excel
  • Admin UI improvements, especially session data tab

Version 3.2

  • Should use less memory and have fewer memory spikes.
  • Enhancements to SessionData and SessionMonitor.

Version 3.1

  • New way to define Roles
  • You can pass a string to formfield, for example {{ formfield 'contribution' }}.

Version 3.0

ライブページ

See ライブページ.

REST API

See REST

Other things
  • Python 3.8 is now supported.
  • Speed improvements to devserver & zipserver
  • You can now download a single session's data as Excel or CSV (through session's Data tab)
  • When browser bots complete, they keep the last page open
  • group_by_arrival_time: quicker detection if a participant goes offline
  • Browser bots use the REST API to create sessions (see REST).
  • Instead of runprodserver you can now use prodserver (that will be the preferred name going forward).
  • "Page time" data export now has more details such as whether it is a wait page.
  • devserver and zipserver now must use db.sqlite3 as the database.

Version 2.5

  • Removed old runserver command.
  • Deprecated non-oTree widgets and model fields. See here.

Version 2.4

  • zipserver command
  • New MTurk format
  • oTree no longer records participants' IP addresses.

Version 2.3

  • Various improvements to performance, stability, and ease of use.
  • oTree now requires Python 3.7
  • oTree now uses Django 2.2.
  • Chinese/Japanese/Korean currencies are displayed as 元/円/원 instead of ¥/₩.
  • On Windows, prodserver just launches 1 worker process. If you want more processes, you should use a process manager. (This is due to a limitation of the ASGI server)
  • prodserver uses Uvicorn/Hypercorn instead of Daphne
  • update_my_code has been removed

Version 2.2

  • support for the otreezip format (otree zip, otree unzip)
  • MTurk: in sandbox mode, don't grant qualifications or check qualification requirements
  • MTurk: before paying participants, check if there is adequate account balance.
  • "next button" is disabled after clicking, to prevent congesting the server with duplicate page loads.
  • Upgrade to the latest version of Sentry
  • Form validation methods should go on the model, not the page. See 動的なフォームの検証
  • app_after_this_page
  • Various performance and stability improvements

Version 2.1

  • oTree now raises an error if you use an undefined variable in your template. This will help catch typos like {{ Player.payoff }} or {{ if player.id_in_gruop }}. This means that apps that previously worked may now get a template error (previously, it failed silently). If you can't remove the offending variable, you can apply the |default filter, like: {{ my_undefined_variable|default:None }}
  • oTree now warns you if you use an invalid attribute on a Page/WaitPage.
  • CSV/Excel data export is done asynchronously, which will fix timeout issues for large files on Heroku.
  • Better performance, especially for "Monitor" and "Data" tab in admin interface

The new no-self format

Since 2021, there has been a new optional format for oTree apps. It replaces models.py and pages.py with a single __init__.py.

The new format unifies oTree's syntax. For example, before, you needed to write either player.payoff, self.payoff, or self.player.payoff, depending on what part of the code you were in. Now, you can always write player.payoff. In fact, the self keyword has been eliminated entirely

If you use oTree Studio, your code has already been upgraded to the no-self format.

If you use a text editor, you can keep using the existing format, or use the new one if you wish. They both have access to the same features. The models.py format will continue to be fully supported and get access to the newest features.

注釈

In January 2022, constants changed format also. See 2022 Constants format change

About the new format

  1. "self" is totally gone from your app's code.
  2. Whenever you want to refer to the player, you write player. Same for group and subsession.
  3. Each method in oTree is changed to a function.
  4. There is no more models.py and pages.py. The whole game fits into one file (__init__.py).
  5. Everything else stays the same. All functions and features do the same thing as before.

Here is an example of an __init__.py in the "no self" format (with the dictator game):

class Subsession(BaseSubsession):
    pass


class Group(BaseGroup):
    pass

class Player(BasePlayer):
    kept = models.CurrencyField(
        min=0,
        max=C.ENDOWMENT,
        label="I will keep",
    )


# FUNCTIONS
def set_payoffs(group):
    player1 = group.get_player_by_id(1)
    player2 = group.get_player_by_id(2)
    player1.payoff = group.kept
    player2.payoff = C.ENDOWMENT - group.kept


# PAGES
class Introduction(Page):
    pass


class Offer(Page):
    form_model = 'group'
    form_fields = ['kept']

    def is_displayed(player):
        return player.id_in_group == 1


class ResultsWaitPage(WaitPage):
    after_all_players_arrive = 'set_payoffs'


class Results(Page):
    @staticmethod
    def vars_for_template(player):
        group = player.group

        return dict(payoff=player.payoff, offer=C.ENDOWMENT - group.kept)

So, what has changed?

  1. As you see, set_payoffs has changed from a group method to a regular function that takes "group" as its argument. This should be clearer to most people.
  2. is_displayed and vars_for_template are no longer page methods that take an argument 'self', but direct functions of the player. Now you can directly write 'player' without needing 'self.' in front of it. (If you are using a text editor like PyCharm, you should add @staticmethod before vars_for_template and is_displayed to indicate that they are not regular methods.)
  3. There is no longer any distinction between page methods and model methods. The is_displayed and vars_for_template can freely be moved up into the "FUNCTIONS" section, and reused between pages, or put inside a page class if they only pertain to that class.
  4. The app folder is simplified from this:

To this:

dictator/
    __init__.py
    Decide.html
    Results.html

Also, the "import" section at the top is simplified.

Before:

# models.py
from otree.api import (
    models,
    widgets,
    BaseConstants,
    BaseSubsession,
    BaseGroup,
    BasePlayer,
    Currency as c,
    currency_range
)

# pages.py
from otree.api import Currency as c, currency_range
from ._builtin import Page, WaitPage
from .models import Constants

After:

# __init__.py
from otree.api import *

You can see the sample games in the new format here: here.

How does this affect you?

This no-self format is only available with oTree Lite. oTree Lite supports both formats. Within the same project, you can have some apps that use the models.py format, and some that use the no-self format.

There is a command "otree remove_self" that can automatically convert the models.py format to the no-self format. This is for people who are curious what their app would look like in the no-self format. Later, I will describe this command and how to use it.

FAQ

Q: Do I need to change my existing apps? A: No, you can keep them as is. The "no-self" format is optional.

Q: Will I have to re-learn oTree for this new format? A: No, you don't really need to relearn anything. Every function, from creating_session, to before_next_page, etc, does the same thing as before. And there are no changes to other parts of oTree like templates or settings.py.

Q: Why didn't you implement it this way originally? A: The first reason is that oTree got its structure from Django. But now that I made oTree Lite which is not based on Django, I have more freedom to design the app structure the way I see fit. The second reason is that this is quite a tailored design. It was necessary to wait and see how oTree evolved and how people use oTree before I could come up with the most appropriate design.

How to use it

First, ensure that you are using oTree Lite:

pip3 install -U otree

Then do one of the following:

  1. Convert your existing apps using otree remove_self, as described in this page.
  2. Create a new project.

There are now 2 branches of the documentation. These docs you are reading now are based on the no-self format (see the note at the top of the page).

Try it out and send me any feedback!

The "otree remove_self" command

If you prefer the no-self format, or are curious what your app would look like in this format, follow these steps. First, then install oTree Lite:

pip3 install -U otree

Run:

otree remove_self
otree upcase_constants

Note this command pretty aggressively converts all your model methods to functions, e.g. changing player.foo() to foo(player). If you have a lot of custom methods, you should check that your method calls still work.

Misc notes

  • before_next_page now takes a second arg timeout_happened.
  • You can optionally add a type hint to your function signatures. For example, change def xyz(player) to def xyz(player: Player). If you use PyCharm or VS Code, that will mean you get better autocompletion.

Indices and tables

(Thank you to contributors)