はじめに
COVID-19感染拡大防止のため、社会心理学実験の授業がオンライン開講になりました。そこで、例年グループでトランプを使って行っていた廃棄物処理ゲーム ( Hirose et al., 2004) の授業回をオンラインでもできるよう、ゲームをoTreeで作りました。コードを全て載せています。
廃棄物処理ゲーム( Hirose et al., 2004)
廃棄物処理ゲームとは、社会的ジレンマ状況および二次のジレンマ状況をシミュレートするゲームです。
プレイヤーが工場の社長になったという設定で、グループの各プレイヤーが産業廃棄物 (配られたトランプ4枚) の処理 (一斉に場に出すこと) を行います。配られたトランプのマークがダイヤ以外であれば「普通ごみ」として、ダイヤであれば「有害ごみ」としての処理を行います。「普通ごみ」は裏返しで無料で場に出すことができますが、「有害ごみ」は表を向けたうえでお金を払い、適正処理をして)場に出します。
そのとき、「有害ごみ」をこっそり裏返しで不法投棄することができますが、誰かが「監視する」と表明すると全てのカードを裏返しにされ、不法投棄が発覚して罰金が科せられます。が、監視そのものにお金がかかります。
これを「4シーズン × 数年」 ピリオド 行い、元手 – 使ったお金が最も多い人が勝ちです。1年ごとに清算が行われ、誰も監視をせず発覚しなかった不法投棄の数ぶんのコストが全員にかかります。
oTreeで作ってみた
上記のゲームは、
・3/4の確率で「普通ごみ」、1/4の確率で「有害ごみ」がプレイヤーに割り当てられる
・「有害ごみ」が割り当てられたプレイヤーのみ、お金を払って適正処理をするか、払わずに不法投棄するか選ぶ
・「普通ごみ」が割り当てられたプレイヤーのみ、お金を払って監視するか、払わずに監視しないか選ぶ
・誰かひとりでも監視をすれば、有害ごみを不法投棄したプレイヤーが罰される
・4ピリオドに1回、4ピリオド中に発覚しなかった不法投棄数ぶんのコストが全員にかかる
・決められた元手から、ピリオドが経過するごとに利得が減っていく
特徴を持っています。これを、oTreeで作ってみました。
説明の画面 (introduction.html)

現在の年数と季節、残金が表示されます。

普通ごみの選択画面 (Page1.html)

監視するかどうかを決定する場面(Page3.html)

不法投棄するかどうか決める画面 (Page2.html)

結果を表示する画面 (Page4.html)
自身は監視を行わなかったが、グループの誰かが監視した場合

監視によって罰金が科された場合

1年の結果が表示される画面 (Page5.html)

models.py
def set_gomi(self)で普通ごみと有害ごみの割り当てを行っています。
また、class Group(BaseGroup)で色々条件分けをしています。
from otree.api import (
models,
widgets,
BaseConstants,
BaseSubsession,
BaseGroup,
BasePlayer,
Currency as c,
currency_range,
)
import random
author = 'Mizuno'
doc = """
"""
class Constants(BaseConstants):
name_in_url = 'iwg'
players_per_group = 4 #グループ人数 (何人でもOK)
num_rounds = 16 #ラウンド数 (4の倍数)
years = num_rounds//4
initial = c(800) #元手
proper = c(80) #適正処理にかかるコスト
cost = c(40) #年末の生産にかかるコスト
punishment_endowment = c(40) #監視にかかるコスト
punishment = c(200) #罰の大きさ
class Subsession(BaseSubsession):
pass
class Group(BaseGroup):
group_inspection = models.CurrencyField()
group_ild = models.IntegerField()
group_cost = models.CurrencyField()
group_detection = models.IntegerField()
def season(self):
inspections = [p.inspection for p in self.get_players()]
self.group_inspection = sum(inspections)
for p in self.get_players():
if p.toxicgomi == c(0) and p.gomi == 1:
p.ild = 1
else:
p.ild = 0
ilds = [p.ild for p in self.get_players()]
self.group_ild = sum(ilds)
if sum(inspections) >= Constants.punishment_endowment:
self.group_inspection = Constants.punishment
self.group_detection = 0
else:
self.group_inspection = c(0)
self.group_detection = self.group_ild
self.group_cost = self.group_detection * Constants.cost
for p in self.get_players():
if p.toxicgomi == c(0) and p.gomi == 1:
p.individual_punishment = self.group_inspection
else:
p.individual_punishment = c(0)
p.payoff = p.individual_punishment + p.toxicgomi + p.normal + p.inspection
class Player(BasePlayer):
gomi = models.IntegerField()
individual_punishment = models.CurrencyField()
ild = models.IntegerField()
def set_gomi(self):
self.gomi = random.choice([0, 0, 0, 1])
toxicgomi = models.CurrencyField(
choices=[
[Constants.proper,'お金を払って適正処理する'],
[c(0), '無料で不法投棄する']
],
label="有害ごみをどのように処理しますか?",
widget = widgets.RadioSelect
)
normal = models.CurrencyField(
choices=[
[c(0), '処理する']
],
label="普通ごみを処理してください",
widget=widgets.RadioSelect
)
inspection = models.CurrencyField(
choices=[
[Constants.punishment_endowment, '監視する (お金がかかります)'],
[c(0), '監視しない']
],
label="監視しますか?",
widget=widgets.RadioSelect
)
Pages.py
特定の条件のときにだけページを表示させたり変数を作ったり、計算のタイミングを工夫したり色々頑張ってます。
from otree.api import Currency as c, currency_range
from ._builtin import Page, WaitPage
from .models import Constants
class introduction(Page):
def before_next_page(self):
self.player.set_gomi() #ごみの割り当て (models.py参照)
def vars_for_template(self):
balances = [p.payoff for p in self.player.in_previous_rounds()]
balance = sum(balances)
year = self.player.round_number//4 + 1
if self.player.round_number % 4 == 3:
season = "秋"
elif self.player.round_number % 4 == 2:
season = "夏"
elif self.player.round_number % 4 == 1:
season = "春"
elif self.player.round_number % 4 == 0:
season = "冬"
return dict(
balance = Constants.initial - balance,
season = season,
year = year
)
class Page1(Page):
form_model = 'player'
form_fields = ['normal']
#普通ごみの場合のみ表示
def is_displayed(self):
return self.player.gomi != 1
def before_next_page(self):
self.player.toxicgomi = 0
class Page2(Page):
form_model = 'player'
form_fields = ['toxicgomi']
#有害ごみの場合のみ表示
def is_displayed(self):
return self.player.gomi == 1
def before_next_page(self):
self.player.normal = 0
self.player.inspection = 0
class Page3(Page):
form_model = 'player'
form_fields = ['inspection']
#普通ごみの場合のみ表示
def is_displayed(self):
return self.player.gomi != 1
class ResultsWaitPage(WaitPage):
def after_all_players_arrive(self):
self.group.season()
class Page4(Page):
pass
class Page5(Page):
#4ピリオドに1回表示
def is_displayed(self):
return self.player.round_number % 4 == 0
def vars_for_template(self):
year_ild = [self.player.in_round(self.player.round_number - 3).group.group_detection,
self.player.in_round(self.player.round_number - 2).group.group_detection,
self.player.in_round(self.player.round_number - 1).group.group_detection,
self.player.in_round(self.player.round_number - 0).group.group_detection]
year_payoff = [self.player.in_round(self.player.round_number - 3).payoff,
self.player.in_round(self.player.round_number - 2).payoff,
self.player.in_round(self.player.round_number - 1).payoff,
self.player.in_round(self.player.round_number - 0).payoff]
year_cost = [self.player.in_round(self.player.round_number - 3).group.group_cost,
self.player.in_round(self.player.round_number - 2).group.group_cost,
self.player.in_round(self.player.round_number - 1).group.group_cost,
self.player.in_round(self.player.round_number - 0).group.group_cost]
spring = self.player.in_round(self.player.round_number - 3)
summer = self.player.in_round(self.player.round_number - 2)
autumn = self.player.in_round(self.player.round_number - 1)
winter = self.player.in_round(self.player.round_number - 0)
return dict(
spring = spring,
summer = summer,
autumn = autumn,
winter = winter,
year_ild = sum(year_ild),
year_payoff = sum(year_payoff),
year_cost = sum(year_cost)
)
#発覚しなかった不法投棄ごみを清算
def before_next_page(self):
year_costs = [self.player.in_round(self.player.round_number - 3).group.group_cost,
self.player.in_round(self.player.round_number - 2).group.group_cost,
self.player.in_round(self.player.round_number - 1).group.group_cost,
self.player.in_round(self.player.round_number - 0).group.group_cost]
year_cost = sum(year_costs)
self.player.in_round(self.player.round_number - 0).payoff = self.player.in_round(self.player.round_number - 0).payoff + year_cost
class finish(Page):
#ゲーム終了時のみ表示
def is_displayed(self):
return self.player.round_number == Constants.num_rounds
def vars_for_template(self):
balances = [p.payoff for p in self.player.in_previous_rounds()]
balance = sum(balances)
return dict(
balance=Constants.initial - balance
)
page_sequence = [introduction,Page1,Page2,Page3,ResultsWaitPage,Page4,Page5,finish]
Templates
introduction.html
{% extends "global/Page.html" %}
{% load otree static %}
{% block title %}
説明
{% endblock %}
{% block content %}
<p>
現在、<strong>{{year}}年目{{season}}</strong>です。<br><br>
あなたは工場の社長です。<br>
年に4回(春・夏・秋・冬) ゴミだしをしなければなりません。<br>
できるだけ多くのお金を残し、 {{Constants.years}}年間(計{{Constants.num_rounds}}回)ゴミを処理してください。<br>
一番多くお金を残したプレイヤーが勝利です。<br><br>
現在あなたは<strong>{{balance}}</strong>を持っています。<br><br>
上記が確認できたら、「次へ」を押してください。
<br><br>
</p>
{% next_button %}
{% endblock %}
Page1.html
{% extends "global/Page.html" %}
{% load otree static %}
{% block title %}
ゴミ出し(普通ごみ)
{% endblock %}
{% block content %}
<p>
あなたは、<strong>普通ごみ</strong>を処理しなくてはなりません。<br>
普通ごみの処理は<strong>無料</strong>です。<br><br>
「処理する」にチェックを入れて、次のページに進んでください。<br><br>
{% formfield player.normal %}
</p>
{% next_button %}
{% endblock %}
Page2.html
{% extends "global/Page.html" %}
{% load otree static %}
{% block title %}
ゴミ出し(有害ごみ)
{% endblock %}
{% block content %}
<p>
あなたは、<strong>有害ごみ</strong>を処理しなくてはなりません。<br>
<strong>{{ Constants.proper }}</strong>払って適正処理するか、<br>
<strong>無料</strong>で不法投棄するか選んで次のページに進んでください。<br><br>
もし監視が行われて不法投棄が発覚すると、<strong>{{ Constants.punishment }}</strong>が
罰金として科されます。<br><br>
{% formfield player.toxicgomi %}
</p>
{% next_button %}
{% endblock %}
Page3.html
{% extends "global/Page.html" %}
{% load otree static %}
{% block title %}
監視の決定
{% endblock %}
{% block content %}
<p>
また、任意に監視を行うことができます。<br>
監視には<strong>{{ Constants.punishment_endowment }}</strong>かかります。<br>
監視が行われると、不法投棄した人には<strong>{{ Constants.punishment }}</strong>の罰金が科せられます。<br><br>
</p>
{% formfield player.inspection %}
{% next_button %}
{% endblock %}
Page4.html
{% extends "global/Page.html" %}
{% load otree static %}
{% block title %}
今シーズンの結果
{% endblock %}
{% block content %}
<p>
<b>あなた自身のごみの処理</b><br><br>
{% if player.gomi == 1 and player.toxicgomi == 0 %}
あなたは、有害ごみを無料で不法投棄しました。<br><br><br>
{% elif player.gomi == 1 and player.toxicgomi == Constants.proper %}
あなたは、有害ごみを{{ Constants.proper }}払って適正処理しました。<br><br><br>
{% else %}
あなたは、無料で普通ごみを処理しました。<br><br><br>
{% endif %}
{% if player.gomi != 1 and player.inspection == Constants.punishment_endowment %}
<b>あなた自身の監視の決定</b><br><br>
あなたは、{{ Constants.punishment_endowment }}を払って監視を行いました。<br><br><br>
{% elif player.gomi != 1 and player.inspection != Constants.punishment_endowment %}
<b>あなた自身の監視の決定</b><br><br>
あなたは、監視を行いませんでした。<br><br><br>
{% else %}
{% endif %}
<b>グループの監視</b><br><br>
{% if group.group_inspection >= Constants.punishment %}
グループでは、監視が行われました。<br>
その結果、{{ group.group_ild }}個の不法投棄が見つかりました。<br><br><br>
{% else %}
誰も監視を行いませんでした。<br><br><br>
{% endif %}
{% if group.group_inspection >= Constants.punishment and player.gomi == 1 and player.toxicgomi == 0 %}
<b>監視による罰金</b><br><br>
あなたには、罰金{{ Constants.punishment }}が科されました。<br><br><br>
{% else %}
{% endif %}
<b>結果</b><br><br>
あなたはこのシーズンで<strong>{{ player.payoff }}</strong>を使いました。<br><br><br>
</p>
{% next_button %}
{% endblock %}
Page5.html
{% extends "global/Page.html" %}
{% load otree static %}
{% block title %}
年末の清算
{% endblock %}
{% block content %}
<p>
あなたはこの1年で、<br><br>
<strong>
春: {{spring.payoff}}<br>
夏: {{summer.payoff}}<br>
秋: {{autumn.payoff}}<br>
冬: {{winter.payoff}}<br>
--<br>
計: {{year_payoff}}<br>
</strong>
<br>
を使いました。<br><br>
また、発覚しなかった不法投棄が<strong>{{year_ild}}個</strong>あり、<br>
全員に<strong>{{year_ild}}個 × {{Constants.cost}} = {{year_cost}}</strong>の費用がかかりました。
</p>
{% next_button %}
{% endblock %}
finish.html
最終結果を出すためのページです。
{% extends "global/Page.html" %}
{% load otree static %}
{% block title %}
最終結果
{% endblock %}
{% block content %}
<p>
<br><br>
あなたの最終的な金額は<strong>{{balance}}</strong>でした。
<br><br>
</p>
{% next_button %}
{% endblock %}
単位を「万円」にする
settings.pyでポイントを使う設定にして、ポイントの名前を「万円」にしました。
USE_POINTS = True POINTS_CUSTOM_NAME = '万円'
おわりに
それぞれのファイルにコピペすれば使えると思います。
作った実験をサーバーにアップして使う手順については、こちらを参考にしていただければと存じます。
引用文献
Hirose, Y., Sugiura, J., & Shimomoto, K. (2004). Industrial waste management simulation game and its educational effect. Journal of Material Cycles and Waste Management, 6(1), 58-63.