下面介绍的是信任博弈的编写例子,一些已在前面的例子中介绍的内容不再赘述;新建一个信任博弈的app文件夹,与前面的博弈不同的是,信任博弈不仅有分组,还是一个序贯博弈,需要区分角色和决策的先后;信任博弈的两个参数是初始禀赋和投资的放大倍数,同样这两个参数写在C
类下
class C(BaseConstants):
NAME_IN_URL = 'trust_game'
PLAYERS_PER_GROUP = 2
NUM_ROUNDS = 5
ENDOWMENT = cu(100)
MULTIPLIER = 3
信任博弈的决策是角色A决定发送的点数,角色B决定的是返还的点数,这两个决策都是在两人配对的小组Group
层面做出的(同样,另一种写法是将这个决策写在Player
类下面)。在Group
类下添加两个字段:
class Group(BaseGroup):
sent_amount = models.CurrencyField(
min=cu(0),
max=C.ENDOWMENT,
label="请输入您要发送的点数",
)
sent_back_amount = models.CurrencyField(
min=cu(0),
label="请输入您要返还的点数:"
)
对sent_amount
的范围限定是显然的,但是sent_back_amount
的上限则是不确定的,需要根据发送额确定。这种属于动态的验证。这里只需要确定返还额的上限,因此使用的是{{field_name}}_max()
的函数:
def sent_back_amount_max(group: Group):
return group.sent_amount * C.MULTIPLIER
{{field_name}}\_max()
这种格式定义的函数会被自动执行,用于实验中动态确定上限,类似的函数还有{{field_name}}\_min()
等(更多验证方法参考官方文档Forms一节下面的Dynamic form field validation的内容)。
下面要定义的是收益计算函数:
def set_payoffs(group: 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.MULTIPLIER - group.sent_back_amount
这里使用了group内置的get_player_by_id()的方法,这个方法用于在组内寻找某个有id_in_group的角色,这里的player1是先行动的角色A,player2是后行动的角色B,角色的分配由oTree根据分组情况随机设定,因此可以不手写角色分配代码(关于角色分配,参考(五)的group和role的设定这部分)
最后确定分组方式,采用随机分组方式的同时,又需要保证参加者的角色固定,因此设定如下参数:
def creating_session(subsession):
subsession.group_randomly(fixed_id_in_group=True)
fixed_id_in_group
取值True
即可保证在随机分组时id_in_group
即角色不变
下面是PAGES
部分的内容,考虑博弈时序,需要决策的页面有发送和返还两个,而小组内不同角色还需要等待对方决策,因此还要有两个WaitPage
,最后还要加上结果报告的页面。除此以外,我们还想在实验开始前提供简短的实验说明并且告知参加者他们的角色。
我们首先考虑作为实验介绍的开始页,在这一页上提供简短的实验说明以及告知角色,另外,这一页仅需要在第一轮出现,在第二轮重复的时候,这一页不再出现,因此还需要设定显示条件:
class Introduction(Page):
@staticmethod
def vars_for_template(player: Player):
if player.id_in_group == 1:
role_text = '角色A'
else:
role_text = '角色B'
return dict(
role_text = role_text
)
@staticmethod
def is_displayed(player: Player):
return player.round_number == 1
设定显示条件用到的方法是is_displayed
,return后的判断条件满足才会显示页面,这里设定仅在第一轮显示
Introduction页面需要着重介绍的在html页面中,首先这里给出示例代码:
{{ block title }}
实验介绍
{{ endblock }}
{{ block content }}
{{ include_sibling 'instructions.html' }} <!-- 外部插入实验说明 -->
<p><b>您的角色是: {{ role_text }}</b></p>
<button class="otree-btn-next btn btn-primary" id="btn" style='float:right'>请阅读实验说明(20)</button>
<!-- 使用了oTree内置的button的class -->
<script>
var btn = document.getElementById('btn'); //获取元素
var secs = 20; //设定了倒计时为20秒
btn.disabled=true; //禁止点击
for (var i=1; i<=secs; i++) {
window.setTimeout("update(" + i + ")", i * 1000);
} //每1秒执行一次update函数
function update(num) {
if (num==secs) { //倒计时结束,按键的文本改变,并且可以点击
btn.textContent = "开始实验";
btn.disabled = false;
}
else { //倒计时中,实时更新文本里面的数字
var printnr = secs - num;
btn.textContent = "请阅读任务说明 (" + printnr + ")";
}
}
</script>
{{ endblock }}
可以看到,这里并没有出现实验说明,而是使用了插入html的方式在这个页面上插入了实验说明,格式是{{ include_sibling 'instructions.html' }}
,这是oTree自带的一个写法,可以方便地在页面上插入其他内容,特别是那些需要重复出现的内容
一个好处是,如果需要修改实验说明等需要重复出现的内容,只需要修改一个文件即可,不用修改多个文件,大大降低了代码重复度
这种方式插入的html内容和主页面的html文件放在同一个文件夹即可
有的时候CSS和JS的代码会作为单独的文件保存在_static文件夹中,这些文件的读取方式参考官方文档Miscellaneous→Advanced features→Static files的内容,例子:
<link rel="stylesheet" href="{{ static 'mystyle.css' }}">
<script src="{{ static 'myscript.js' }}"></script>
第二个需要说明的是这里使用了一个有点复杂的自定义按键,这个按键在20秒之内是无法点击的,只有20秒之后才能点击进入下一页,这强制要求参加者停留一段事件阅读说明
参考代码解释理解,JavaScript的作用就是获取页面上的任一个元素并进行动态实时的更新,包括这些元素的内容、属性、样式等等
倒计时未结束:

倒计时结束:

第一个决策页面是发送决策,需要限定只有角色A(id_in_group==1
)才会显示,而角色B则等待:
class Send(Page):
form_model = 'group'
form_fields = ['sent_amount']
@staticmethod
def is_displayed(player: Player):
return player.id_in_group == 1
class SendBackWaitPage(WaitPage):
title_text = '请耐心等待!'
body_text = '请保持安静,如果有问题,请询问实验员。'
对应的发送决策页面,这里在页面下方插入了实验说明:
{{ block title }}
角色A决策-第{{ player.round_number }}轮
{{ endblock }}
{{ block content }}
<p>
您是角色A。您有实验点{{C.ENDOWMENT}}。您会发送多少给角色B?
</p>
{{ formfields }}
<p>
{{ next_button }}
</p>
{{ include_sibling 'instructions.html' }}
{{ endblock }}
显示页面如下:

Trust Send
第二个决策是返还决策,这个页面只显示给角色B“(id_in_group==2
),角色A进入等待界面,在角色B完成决策后,计算最后的收益:
class SendBack(Page):
form_model = 'group'
form_fields = ['sent_back_amount']
@staticmethod
def is_displayed(player: Player):
return player.id_in_group == 2
@staticmethod
def vars_for_template(player: Player):
group = player.group
tripled_amount = group.sent_amount * C.MULTIPLIER
return dict(tripled_amount=tripled_amount)
class ResultsWaitPage(WaitPage):
title_text = '请耐心等待!'
body_text = '请保持安静,如果有问题,请询问实验员。'
after_all_players_arrive = set_payoffs
这里的after_all_players_arrive = set_payoffs
是一种简写形式,和上一例子中的写法是一样的作用,注意这里没有设定wait_for_all_groups
,默认为False
,只需要当前轮次的小组成员到达即可
对应的返还决策页面如下:
{{ block title }}
角色B决策-第{{ player.round_number }}轮
{{ endblock }}
{{ block content }}
<p>
您是角色B。
角色A发送了实验点{{ group.sent_amount }},因此你获得实验点{{ tripled_amount }}。
您将会返还多少实验点给角色A?
</p>
{{ formfields }}
<p>
{{ next_button }}
</p>
{{ include_sibling 'instructions.html' }}
{{ endblock }}
显示如下:

Trust Senback
最后是收益报告的页面:
class Results(Page):
@staticmethod
def vars_for_template(player: Player):
group = player.group
return dict(tripled_amount=group.sent_amount * C.MULTIPLIER)
对应的html:
{{ block title }}
收益报告-第{{ player.round_number }}轮
{{ endblock }}
{{ block content }}
{{ if player.id_in_group == 1 }}
<p>
本轮您有实验点{{ C.ENDOWMENT }},
您发送给角色B {{ group.sent_amount }},
角色B返还 {{ group.sent_back_amount }}。
</p>
<p>
本轮您的收益点数: {{ C.ENDOWMENT }}-{{ group.sent_amount }}+{{ group.sent_back_amount }}=<strong>{{ player.payoff }}</strong>
</p>
{{ else }}
<p>
角色A发送给您 {{ group.sent_amount }},
放大后您的点数是 {{ tripled_amount }},
您返还了 {{ group.sent_back_amount }}。
</p>
<p>
本轮您的收益点数: ({{ tripled_amount }})-({{ group.sent_back_amount }})=<strong>{{ player.payoff }}</strong>
</p>
{{ endif }}
{{ if player.round_number == C.NUM_ROUNDS }}
<p>
在{{ C.NUM_ROUNDS }}轮中,您一共获得实验点{{ participant.payoff }}
</p>
{{ endif }}
<p>{{ next_button }}</p>
{{ endblock }}
两个角色的收益报告:


页面顺序如下:Introduction仅在第一轮显示
page_sequence = [
Introduction,
Send,
SendBackWaitPage,
SendBack,
ResultsWaitPage,
Results,
]
至此完成简单的信任博弈的编写。
原文地址: https://github.com/MarvinLuoGS/otree-crash-tutorial 和 https://slides-otree-tutorial.netlify.app/1 感谢 罗干松 (ZJU) 范徐航(Duke) 同学,如果内容涉及侵权,告知我后会立即删除。
No responses yet