BPEL Bot commited on
Commit
e40dce0
·
0 Parent(s):

Initial Hugging Face deployment

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +13 -0
  2. .gitignore +14 -0
  3. Dockerfile +33 -0
  4. EXPORTS.md +40 -0
  5. LICENSE +26 -0
  6. Procfile +2 -0
  7. README.md +76 -0
  8. TESTING.md +72 -0
  9. _rooms/econ101.txt +3 -0
  10. _rooms/lab_demo.txt +10 -0
  11. _static/custom.css +47 -0
  12. _static/global/empty.css +0 -0
  13. _templates/global/Page.html +8 -0
  14. bargaining/Introduction.html +7 -0
  15. bargaining/Request.html +12 -0
  16. bargaining/Results.html +26 -0
  17. bargaining/__init__.py +75 -0
  18. bargaining/instructions.html +20 -0
  19. bertrand/Decide.html +11 -0
  20. bertrand/Introduction.html +7 -0
  21. bertrand/Results.html +26 -0
  22. bertrand/__init__.py +73 -0
  23. bertrand/instructions.html +19 -0
  24. common_value_auction/Bid.html +19 -0
  25. common_value_auction/Introduction.html +10 -0
  26. common_value_auction/Results.html +37 -0
  27. common_value_auction/__init__.py +122 -0
  28. common_value_auction/instructions.html +35 -0
  29. cournot/Decide.html +10 -0
  30. cournot/Introduction.html +10 -0
  31. cournot/Results.html +39 -0
  32. cournot/__init__.py +73 -0
  33. cournot/instructions.html +42 -0
  34. dictator/Introduction.html +7 -0
  35. dictator/Offer.html +15 -0
  36. dictator/Results.html +15 -0
  37. dictator/__init__.py +73 -0
  38. dictator/instructions.html +20 -0
  39. docker-entrypoint.sh +33 -0
  40. guess_two_thirds/Guess.html +16 -0
  41. guess_two_thirds/Introduction.html +10 -0
  42. guess_two_thirds/Results.html +35 -0
  43. guess_two_thirds/__init__.py +86 -0
  44. guess_two_thirds/instructions.html +27 -0
  45. matching_pennies/Choice.html +43 -0
  46. matching_pennies/ResultsSummary.html +25 -0
  47. matching_pennies/__init__.py +100 -0
  48. payment_info/PaymentInfo.html +30 -0
  49. payment_info/__init__.py +38 -0
  50. payment_info/tests.py +8 -0
.dockerignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.log
5
+ _db.sqlite3
6
+ /build
7
+ /tmp
8
+ node_modules
9
+ .idea
10
+ .vscode
11
+ .DS_Store
12
+ db.sqlite3
13
+ policy_guess_anchoring.log
.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ venv
2
+ staticfiles
3
+ ./db.sqlite3
4
+ .idea
5
+ *~
6
+ *.sqlite3
7
+ _static_root
8
+ _bots*s
9
+ __temp*
10
+ __pycache__/
11
+ *.py[cod]
12
+ .DS_Store
13
+ merge.ps1
14
+ *.otreezip
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONUNBUFFERED=1 \
4
+ PYTHONDONTWRITEBYTECODE=1 \
5
+ PIP_NO_CACHE_DIR=1
6
+
7
+ WORKDIR /app
8
+
9
+ COPY requirements.txt /app/requirements.txt
10
+
11
+ RUN apt-get update \
12
+ && apt-get install -y --no-install-recommends build-essential \
13
+ && pip install --no-cache-dir -r requirements.txt \
14
+ && apt-get purge -y build-essential \
15
+ && apt-get autoremove -y \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ COPY . /app
19
+
20
+ ENV OTREE_PRODUCTION=1 \
21
+ OTREE_ADMIN_PASSWORD=changeme \
22
+ OTREE_DATABASE_URL=sqlite:////data/otree.sqlite3 \
23
+ PYTHONPATH=/app
24
+
25
+ RUN mkdir -p /data \
26
+ && otree collectstatic --noinput
27
+
28
+ COPY docker-entrypoint.sh /app/entrypoint.sh
29
+ RUN chmod +x /app/entrypoint.sh
30
+
31
+ EXPOSE 8000
32
+
33
+ CMD ["/app/entrypoint.sh"]
EXPORTS.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Exports Guide (ibe-pp)
2
+
3
+ This note explains the key fields you’ll see in CSV/Excel exports from the demo sessions and how to interpret them in class.
4
+
5
+ ## Common fields
6
+ - session_code, participant_code: Internal identifiers for linking records.
7
+ - app_name, round_number, page_name: Where a row came from.
8
+ - payoff: Participant payoff in the app (points or currency units).
9
+
10
+ ## Classic/Policy Games
11
+ - Prisoner’s Dilemma: `cooperate` (True/False), `payoff`.
12
+ - Trust (simple/framed): `sent_amount`, `sent_back_amount`, `payoff`.
13
+ - Dictator (norms): `kept` (amount P1 kept), `payoff`.
14
+ - Public Goods (base/defaults): `contribution`, group totals (if included), `payoff`.
15
+
16
+ Interpretation tips
17
+ - Trust: reciprocity shows in `sent_back_amount` relative to `sent_amount`×multiplier.
18
+ - Public Goods: highlight total vs. private trade-off driving `payoff`.
19
+ - Dictator: distribution between P1 and P2 indicates fairness preferences; note framing.
20
+
21
+ ## Survey Biases (policy_survey_biases)
22
+ - Overconfidence (90% CI): `oc1_low`, `oc1_high`, `oc2_low`, `oc2_high`, `oc3_low`, `oc3_high`.
23
+ - In class: discuss calibration by checking how often intervals would contain the true value (avoid revealing “correct” answers live).
24
+ - Overplacement: `self_rank_policy_knowledge` (1–7).
25
+ - In class: look at distribution vs. mid-point 4.
26
+ - Confirmation: `agree_statement_before`, `agree_statement_after` (1–7).
27
+ - In class: look at whether responses moved after the snippet; both change and no-change are informative.
28
+ - Availability: `recall_event` (free text), `priority_energy`, `priority_public_health` (1–7).
29
+ - In class: discuss how salient events might shift priorities.
30
+
31
+ ## Exporting with bots (optional)
32
+ - Use Make targets to run bots and then export from the admin. For quick bot runs:
33
+ - `make otree-test-classic`
34
+ - `make otree-test-policy`
35
+ - `make otree-test-anchoring`
36
+ - `make otree-test-survey`
37
+
38
+ ## Notes
39
+ - Avoid showing “correct” answers for overconfidence during class; focus on calibration concepts.
40
+ - Frame findings as illustrations; behavior varies by group, context, and incentives.
LICENSE ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2014 Daniel Li Chen, Martin Walter Schonger, Christopher Wickens.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
20
+
21
+ The Licensee undertakes to mention the name oTree, the names of the licensors
22
+ (Daniel L. Chen, Martin Schonger and Christopher Wickens) and to cite the
23
+ following article in all publications in which results of experiments conducted
24
+ with the Software are published: Chen, Daniel L., Martin Schonger, and Chris Wickens.
25
+ 2016. "oTree - An open-source platform for laboratory, online, and field experiments."
26
+ Journal of Behavioral and Experimental Finance, vol 9: 88-97.
Procfile ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ web: otree prodserver1of2
2
+ worker: otree prodserver2of2
README.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # oTree HF Production Template
2
+
3
+ This directory packages the IBE public policy oTree demos for deployment on a Hugging Face Space using the Docker runtime. It was cloned from `otree-projects/ibe-pp` and augmented with container and deployment assets.
4
+
5
+ ## Directory Overview
6
+
7
+ - `Dockerfile` – builds the production image expected by Hugging Face Spaces.
8
+ - `docker-entrypoint.sh` – runtime bootstrap that ensures SQLite initialization, collects static files, and launches the ASGI server.
9
+ - `requirements.txt` – locked dependencies for the production image.
10
+ - `.dockerignore` – keeps dev artifacts (`db.sqlite3`, logs, caches) out of the build context.
11
+ - `settings.py`, apps/, tests/ – unchanged oTree project source.
12
+
13
+ ## Deploying to a Hugging Face Space
14
+
15
+ 1. **Create the Space**
16
+ - Runtime: `Docker`.
17
+ - Visibility: choose `Private` or `Public` per your needs.
18
+ 2. **Add secrets / environment variables** (Settings → Repository secrets):
19
+
20
+ | Key | Purpose | Example |
21
+ | --- | --- | --- |
22
+ | `OTREE_ADMIN_PASSWORD` | Protects the oTree admin interface | `set-a-unique-password` |
23
+ | `OTREE_AUTH_LEVEL` | Optional access control (`STUDY`, `DEMO`, `BASELINE`) | `STUDY` |
24
+ | `OTREE_DATABASE_URL` | Override DB (defaults to SQLite under `/data`) | `postgres://user:pass@host:5432/dbname` |
25
+
26
+ Additional variables such as `SENTRY_DSN`, `OTREE_SECRET_KEY`, or mail settings can also be defined here.
27
+
28
+ 3. **Populate the repository**
29
+ ```bash
30
+ # from repository root
31
+ cd otree-projects/hf-prod-ibe-pp
32
+ git init # if pushing standalone
33
+ git add .
34
+ git commit -m "Initial oTree HF deployment"
35
+ git remote add origin https://huggingface.co/spaces/<org>/<space-name>
36
+ git push origin main
37
+ ```
38
+
39
+ 4. **Space build & launch**
40
+ - Hugging Face automatically builds the Docker image.
41
+ - The container starts `/app/entrypoint.sh`, which:
42
+ - Creates the SQLite database at `/data/otree.sqlite3` if `OTREE_DATABASE_URL` points to SQLite.
43
+ - Runs `otree collectstatic --noinput` so static assets are up to date.
44
+ - Launches `otree prodserver1of2 0.0.0.0:$PORT` (HF sets `$PORT`).
45
+ - The app becomes available at `https://<org>-<space-name>.hf.space/`.
46
+
47
+ ## Local Verification
48
+
49
+ To build and run the same image locally before pushing:
50
+
51
+ ```bash
52
+ cd otree-projects/hf-prod-ibe-pp
53
+ DOCKER_BUILDKIT=1 docker build -t ibe-pp-hf .
54
+ docker run --rm -p 8000:8000 \
55
+ -e OTREE_ADMIN_PASSWORD=changeme \
56
+ -e OTREE_AUTH_LEVEL=STUDY \
57
+ ibe-pp-hf
58
+ # open http://localhost:8000/
59
+ ```
60
+
61
+ For persistence, mount a volume:
62
+ ```bash
63
+ docker run --rm -p 8000:8000 \
64
+ -v $(pwd)/data:/data \
65
+ -e OTREE_ADMIN_PASSWORD=changeme \
66
+ ibe-pp-hf
67
+ ```
68
+
69
+ ## Notes & Customization
70
+
71
+ - **Database**: The default `OTREE_DATABASE_URL=sqlite:////data/otree.sqlite3` provides demo-ready storage but resets whenever the container restarts on Hugging Face. Point to an external Postgres instance for durable experiments.
72
+ - **Static assets**: Adjust `SKIP_COLLECTSTATIC=1` in the Space secrets to skip the runtime collect step if you pre-build assets during CI.
73
+ - **Session configs**: Update `settings.py` or add new apps as usual; rebuild the Space to apply changes.
74
+ - **Logging / monitoring**: Set `SENTRY_DSN`, `OTREE_PRODUCTION=1` (already set), or add custom logging handlers as desired.
75
+
76
+ Once pushed, you can administer the server via `https://<space-url>/otree/admin` using the password stored in `OTREE_ADMIN_PASSWORD`.
TESTING.md ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Testing Guide (ibe-pp)
2
+
3
+ This project ships with a complete test suite covering cloned apps and full-session flows. Tests never modify oTree core and run against the local project only.
4
+
5
+ ## Test Types
6
+ - Unit-lite: config and import sanity checks (no DB writes).
7
+ - App bots (oTree): app-level flows in each cloned app (`tests.py` inside the app).
8
+ - E2E sessions (pytest): spins up full sessions via `otree test <session>` using fresh SQLite DBs.
9
+ - Matrix/infra checks: participant-count matrix, rooms/labels, static assets.
10
+
11
+ ## Layout
12
+ - `policy_*/tests.py` – Bot specs per app:
13
+ - `policy_public_goods_defaults`, `policy_trust_framed`, `policy_dictator_norms`, `policy_guess_anchoring`, `policy_survey_biases`
14
+ - Patterns used: `expect()` to assert payoffs and `SubmissionMustFail(...)` to test validation failures (see `policy_survey_biases`).
15
+ - `payment_info/tests.py` – No-op bot to satisfy multi-app sessions.
16
+ - `tests/test_integration_settings.py` – Verifies session configs and app folders.
17
+ - `tests/test_e2e_bots.py` – Runs E2E bots for configured sessions (`policy_nudges`, `anchoring_demo`, `survey_biases_full`).
18
+
19
+ Survey Biases (policy_survey_biases) modules and tests
20
+ - Module A (Information and Views): randomized order `order_a ∈ {info_before, info_after}` with a comprehension check. Bots cover both branches and include a negative case via `SubmissionMustFail` for a wrong comprehension answer.
21
+ - Module B (Statements and Evidence): randomized order `order_b ∈ {support_first, eval_first}`. Bots cover both branches.
22
+ - Module C (Topics and News): randomized order `order_c ∈ {recall_first, opinion_first}` with validation requiring ~20 characters for each recall text. Bots assert negative cases (short input) then submit valid responses.
23
+ - Results page safely handles optional fields using `field_maybe_none(...)` to avoid null access during rendering.
24
+
25
+ Running just the survey biases suite
26
+ - `make otree-test-survey` (fresh SQLite DB) or:
27
+ - `source scripts/activate_venv.sh && cd otree-projects/ibe-pp && OTREE_DATABASE_URL=sqlite:///tmp_survey.sqlite3 otree test survey_biases_full`
28
+ - `tests/test_session_matrix_and_rooms.py` – Participant-count matrix (valid/invalid), rooms label file presence, and static file presence.
29
+ - `tests/test_static_http.py` – Optional HTTP check for `/_static/custom.css` when `RUN_OTREE_HTTP_TESTS=1`.
30
+
31
+ ## How To Run
32
+ - Quick run (recommended):
33
+ - `make test-ibe-pp` (pytest over `tests/`)
34
+ - `make test-otree-e2e` (serial bot runs across key sessions)
35
+ - Manual (from project root):
36
+ - `source scripts/activate_venv.sh`
37
+ - `cd otree-projects/ibe-pp && pytest -q tests`
38
+ - Single E2E:
39
+ - `cd otree-projects/ibe-pp && pytest -q tests/test_e2e_bots.py::test_bots_policy_nudges_sequence`
40
+ - Optional HTTP static check:
41
+ - `RUN_OTREE_HTTP_TESTS=1 cd otree-projects/ibe-pp && pytest -q tests/test_static_http.py`
42
+
43
+ ## Dependencies
44
+ - Added to root `requirements.txt`: `pytest`, `requests`, `httpx`, `starlette==0.14.1`.
45
+ - Pin rationale: oTree 5.11.4 bots expect Starlette 0.14.x; newer Starlette breaks bot TestClient.
46
+ - If running FastAPI tooling simultaneously, consider a dedicated test requirements file or separate venv.
47
+
48
+ ## E2E Strategy
49
+ - Tests set `OTREE_DATABASE_URL=sqlite:///test_db_<name>.sqlite3` to isolate DBs.
50
+ - If you run `otree test` manually and see “delete your database”, remove `db.sqlite3` or set `OTREE_DATABASE_URL`.
51
+ - Participant-count matrix asserts success/failure for group-size compatibility.
52
+
53
+ ## Adding Tests
54
+ - New app: add `tests.py` alongside `__init__.py` with a `PlayerBot(Bot)` and yields for each page.
55
+ - Use `expect()` to assert key state (e.g., payoffs), and `SubmissionMustFail(...)` for invalid input branches.
56
+ - New session: add a test in `tests/test_e2e_bots.py` invoking `run_otree_test('<session_name>')`.
57
+
58
+ ## Common Issues
59
+ - Missing dependencies for bots: install `requests` and `httpx` (already in `requirements.txt`).
60
+ - Name collisions: cloned apps set unique `NAME_IN_URL` values; keep this when adding apps.
61
+ - Timeouts: `prisoner.Introduction` now uses `get_timeout_seconds`; override with `session.config['prisoner_intro_timeout']` in `settings.py` if needed, for example:
62
+
63
+ ```python
64
+ SESSION_CONFIGS = [
65
+ dict(
66
+ name='classic_baseline',
67
+ app_sequence=['sequence_welcome', 'prisoner', 'trust_simple', 'public_goods_simple', 'payment_info'],
68
+ num_demo_participants=6,
69
+ prisoner_intro_timeout=30, # seconds (optional)
70
+ ),
71
+ ]
72
+ ```
_rooms/econ101.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Alice
2
+ Bob
3
+ Charlie
_rooms/lab_demo.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Optional participant labels for lab demo (one per line).
2
+ # You can leave this file empty to allow unlabeled participants.
3
+ alpha
4
+ bravo
5
+ charlie
6
+ delta
7
+ echo
8
+ foxtrot
9
+ golf
10
+ hotel
_static/custom.css ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Accessibility & readability tweaks for oTree pages */
2
+
3
+ html, body {
4
+ font-size: 18px; /* larger base for readability */
5
+ line-height: 1.5;
6
+ }
7
+
8
+ .container, .otree-body {
9
+ max-width: 900px;
10
+ }
11
+
12
+ /* Buttons */
13
+ .btn, button, input[type=submit] {
14
+ padding: 0.6rem 1rem;
15
+ font-size: 1rem;
16
+ border-radius: 6px;
17
+ }
18
+
19
+ .btn-primary {
20
+ background-color: #0a58ca; /* higher contrast */
21
+ border-color: #0a58ca;
22
+ }
23
+
24
+ .btn-primary:hover, .btn-primary:focus {
25
+ background-color: #084298;
26
+ border-color: #084298;
27
+ }
28
+
29
+ /* Inputs */
30
+ input[type=number], input[type=text], textarea, select {
31
+ padding: 0.5rem 0.75rem;
32
+ font-size: 1rem;
33
+ }
34
+
35
+ /* Focus visibility */
36
+ a:focus, button:focus, .btn:focus, input:focus, select:focus, textarea:focus {
37
+ outline: 3px solid #ffbf47; /* distinct focus ring */
38
+ outline-offset: 2px;
39
+ }
40
+
41
+ /* Cards spacing */
42
+ .card.bg-light.m-3 .card-body { font-size: 0.98rem; }
43
+
44
+ /* Lists in instructions */
45
+ ul { margin-top: 0.25rem; }
46
+ li { margin-bottom: 0.25rem; }
47
+
_static/global/empty.css ADDED
File without changes
_templates/global/Page.html ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {{ extends "otree/Page.html" }}
2
+ {{ load otree static }}
3
+
4
+ {{ block global_styles }}
5
+ <link rel="stylesheet" href="{{ static 'custom.css' }}">
6
+ {{ endblock }}
7
+ {{ block global_scripts }}
8
+ {{ endblock }}
bargaining/Introduction.html ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {{ block title }}Introduction{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <p class="alert alert-info small" role="note"><strong>Note:</strong> Points convert to a bonus at the end of the study.</p>
5
+ {{ include_sibling 'instructions.html' }}
6
+ {{ next_button }}
7
+ {{ endblock }}
bargaining/Request.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Request{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <p>How much will you demand for yourself?</p>
5
+
6
+ {{ formfields }}
7
+
8
+ <p>{{ next_button }}</p>
9
+
10
+ {{ include_sibling 'instructions.html' }}
11
+
12
+ {{ endblock }}
bargaining/Results.html ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Results{{ endblock }}
2
+ {{ block content }}
3
+ <table class=table style="width: auto">
4
+ <tr>
5
+ <th>You demanded</th>
6
+ <td>{{ player.request }}</td>
7
+ </tr>
8
+ <tr>
9
+ <th>The other participant demanded</th>
10
+ <td>{{ other_player_request }}</td>
11
+ </tr>
12
+ <tr>
13
+ <th>Sum of your demands</th>
14
+ <td>{{ group.total_requests }}</td>
15
+ </tr>
16
+ <tr>
17
+ <th>Thus you earn</th>
18
+ <td>{{ player.payoff }}</td>
19
+ </tr>
20
+ </table>
21
+
22
+ <p>{{ next_button }}</p>
23
+
24
+ {{ include_sibling 'instructions.html' }}
25
+
26
+ {{ endblock }}
bargaining/__init__.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from otree.api import *
2
+
3
+
4
+
5
+
6
+ doc = """
7
+ This bargaining game involves 2 players. Each demands for a portion of some
8
+ available amount. If the sum of demands is no larger than the available
9
+ amount, both players get demanded portions. Otherwise, both get nothing.
10
+ """
11
+
12
+
13
+ class C(BaseConstants):
14
+ NAME_IN_URL = 'bargaining'
15
+ PLAYERS_PER_GROUP = 2
16
+ NUM_ROUNDS = 1
17
+ AMOUNT_SHARED = cu(100)
18
+
19
+
20
+ class Subsession(BaseSubsession):
21
+ pass
22
+
23
+
24
+ class Group(BaseGroup):
25
+ total_requests = models.CurrencyField()
26
+
27
+
28
+ class Player(BasePlayer):
29
+ request = models.CurrencyField(
30
+ doc="""
31
+ Amount requested by this player.
32
+ """,
33
+ min=0,
34
+ max=C.AMOUNT_SHARED,
35
+ label="Please enter an amount from 0 to 100",
36
+ )
37
+
38
+
39
+ # FUNCTIONS
40
+ def set_payoffs(group: Group):
41
+ players = group.get_players()
42
+ group.total_requests = sum([p.request for p in players])
43
+ if group.total_requests <= C.AMOUNT_SHARED:
44
+ for p in players:
45
+ p.payoff = p.request
46
+ else:
47
+ for p in players:
48
+ p.payoff = cu(0)
49
+
50
+
51
+ def other_player(player: Player):
52
+ return player.get_others_in_group()[0]
53
+
54
+
55
+ # PAGES
56
+ class Introduction(Page):
57
+ pass
58
+
59
+
60
+ class Request(Page):
61
+ form_model = 'player'
62
+ form_fields = ['request']
63
+
64
+
65
+ class ResultsWaitPage(WaitPage):
66
+ after_all_players_arrive = set_payoffs
67
+
68
+
69
+ class Results(Page):
70
+ @staticmethod
71
+ def vars_for_template(player: Player):
72
+ return dict(other_player_request=other_player(player).request)
73
+
74
+
75
+ page_sequence = [Introduction, Request, ResultsWaitPage, Results]
bargaining/instructions.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <div class="card bg-light m-3">
3
+
4
+ <div class="card-body">
5
+ <h3>
6
+ Instructions
7
+ </h3>
8
+
9
+ <p>
10
+ You have been randomly and anonymously paired with another participant.
11
+ There is {{ C.AMOUNT_SHARED }} for you to divide.
12
+ Both of you have to simultaneously and independently demand a portion
13
+ of the {{ C.AMOUNT_SHARED }} for yourselves. If the sum of your
14
+ demands is smaller or equal to {{ C.AMOUNT_SHARED }}, both of
15
+ you get what you demanded. If the sum of your demands is larger
16
+ than {{ C.AMOUNT_SHARED }},
17
+ both of you get nothing.
18
+ </p>
19
+ </div>
20
+ </div>
bertrand/Decide.html ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Set Your Price{{ endblock }}
2
+ {{ block content }}
3
+
4
+ {{ formfields }}
5
+
6
+
7
+ <p>{{ next_button }}</p>
8
+
9
+ {{ include_sibling 'instructions.html' }}
10
+
11
+ {{ endblock }}
bertrand/Introduction.html ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {{ block title }}Introduction{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <p class="alert alert-info small" role="note"><strong>Note:</strong> Points convert to a bonus at the end of the study.</p>
5
+ {{ include_sibling 'instructions.html' }}
6
+ {{ next_button }}
7
+ {{ endblock }}
bertrand/Results.html ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Results{{ endblock }}
2
+ {{ block content }}
3
+ <table class="table">
4
+ <tr>
5
+ <th>Your price</th>
6
+ <td>{{ player.price }}</td>
7
+ </tr>
8
+ <tr>
9
+ <th>Lowest price</th>
10
+ <td>{{ group.winning_price }}</td>
11
+ </tr>
12
+ <tr>
13
+ <th>Was your product sold?</th>
14
+ <td>{{ if player.is_winner }} Yes {{ else }} No {{ endif }}</td>
15
+ </tr>
16
+ <tr>
17
+ <th>Your payoff</th>
18
+ <td>{{ player.payoff }}</td>
19
+ </tr>
20
+ </table>
21
+
22
+ {{ next_button }}
23
+
24
+ {{ include_sibling 'instructions.html' }}
25
+
26
+ {{ endblock }}
bertrand/__init__.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from otree.api import *
2
+
3
+
4
+
5
+ doc = """
6
+ 2 firms complete in a market by setting prices for homogenous goods.
7
+ See "Kruse, J. B., Rassenti, S., Reynolds, S. S., & Smith, V. L. (1994).
8
+ Bertrand-Edgeworth competition in experimental markets.
9
+ Econometrica: Journal of the Econometric Society, 343-371."
10
+ """
11
+
12
+
13
+ class C(BaseConstants):
14
+ PLAYERS_PER_GROUP = 2
15
+ NAME_IN_URL = 'bertrand'
16
+ NUM_ROUNDS = 1
17
+ MAXIMUM_PRICE = cu(100)
18
+
19
+
20
+ class Subsession(BaseSubsession):
21
+ pass
22
+
23
+
24
+ class Group(BaseGroup):
25
+ winning_price = models.CurrencyField()
26
+
27
+
28
+ class Player(BasePlayer):
29
+ price = models.CurrencyField(
30
+ min=0,
31
+ max=C.MAXIMUM_PRICE,
32
+ doc="""Price player offers to sell product for""",
33
+ label="Please enter an amount from 0 to 100 as your price",
34
+ )
35
+ is_winner = models.BooleanField()
36
+
37
+
38
+ # FUNCTIONS
39
+ def set_payoffs(group: Group):
40
+ import random
41
+
42
+ players = group.get_players()
43
+ group.winning_price = min([p.price for p in players])
44
+ winners = [p for p in players if p.price == group.winning_price]
45
+ winner = random.choice(winners)
46
+ for p in players:
47
+ if p == winner:
48
+ p.is_winner = True
49
+ p.payoff = p.price
50
+ else:
51
+ p.is_winner = False
52
+ p.payoff = cu(0)
53
+
54
+
55
+ # PAGES
56
+ class Introduction(Page):
57
+ pass
58
+
59
+
60
+ class Decide(Page):
61
+ form_model = 'player'
62
+ form_fields = ['price']
63
+
64
+
65
+ class ResultsWaitPage(WaitPage):
66
+ after_all_players_arrive = set_payoffs
67
+
68
+
69
+ class Results(Page):
70
+ pass
71
+
72
+
73
+ page_sequence = [Introduction, Decide, ResultsWaitPage, Results]
bertrand/instructions.html ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <div class="instructions well well-lg" style="">
3
+
4
+ <h3>
5
+ Instructions
6
+ </h3>
7
+ <p>
8
+ You have been randomly and anonymously paired with another participant.
9
+ Each of you will represent a firm. Each firm manufactures one unit of
10
+ the same product at no cost.
11
+ </p>
12
+ <p>
13
+ Each of you privately sets your price, anything from 0 to {{ C.MAXIMUM_PRICE }}.
14
+ The buyer in the market will always buy one unit of the product at the
15
+ lower price. In case of a tie, the buyer will buy from one of you at
16
+ random. Your profit is your price if your product is sold and zero
17
+ otherwise.
18
+ </p>
19
+ </div>
common_value_auction/Bid.html ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Bid{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <p>
5
+ The value of the item is estimated to be {{ player.item_value_estimate }}.
6
+ This estimate may deviate from the actual value by at most {{ C.BID_NOISE }}.
7
+ </p>
8
+
9
+ <p>
10
+ Please make your bid now. The amount can be between {{ C.BID_MIN }} and {{ C.BID_MAX }}, inclusive.
11
+ </p>
12
+
13
+ {{ formfields }}
14
+
15
+ {{ next_button }}
16
+
17
+ {{ include_sibling 'instructions.html' }}
18
+
19
+ {{ endblock }}
common_value_auction/Introduction.html ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Introduction{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <p class="alert alert-info small" role="note"><strong>Note:</strong> Points convert to a bonus at the end of the study.</p>
5
+
6
+ {{ include_sibling 'instructions.html' }}
7
+
8
+ {{ next_button }}
9
+
10
+ {{ endblock }}
common_value_auction/Results.html ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Results{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <p>
5
+ {{ if player.is_winner }}
6
+ You won the auction!
7
+ {{ if is_greedy }}
8
+ However, your bid amount was higher than the actual value of the item. Your payoff is therefore zero.
9
+ {{ elif player.payoff == 0 }}
10
+ Your payoff, however, is zero.
11
+ {{ endif }}
12
+ {{ else }}
13
+ You did not win the auction.
14
+ {{ endif }}
15
+ </p>
16
+
17
+ <table class="table" style="width:400px">
18
+
19
+ <tr>
20
+ <th>Your bid</th>
21
+ <th>Winning bid</th>
22
+ <th>Actual value</th>
23
+ <th>Your payoff</th>
24
+ </tr>
25
+
26
+ <tr>
27
+ <td>{{ player.bid_amount }}</td>
28
+ <td>{{ group.highest_bid }}</td>
29
+ <td>{{ group.item_value }}</td>
30
+ <td>{{ player.payoff }}</td>
31
+ </tr>
32
+
33
+ </table>
34
+
35
+ {{ next_button }}
36
+
37
+ {{ endblock }}
common_value_auction/__init__.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from otree.api import *
2
+
3
+
4
+ doc = """
5
+ In a common value auction game, players simultaneously bid on the item being
6
+ auctioned.<br/>
7
+ Prior to bidding, they are given an estimate of the actual value of the item.
8
+ This actual value is revealed after the bidding.<br/>
9
+ Bids are private. The player with the highest bid wins the auction, but
10
+ payoff depends on the bid amount and the actual value.<br/>
11
+ """
12
+
13
+
14
+ class C(BaseConstants):
15
+ NAME_IN_URL = 'common_value_auction'
16
+ PLAYERS_PER_GROUP = None
17
+ NUM_ROUNDS = 1
18
+ BID_MIN = cu(0)
19
+ BID_MAX = cu(10)
20
+ # Error margin for the value estimates shown to the players
21
+ BID_NOISE = cu(1)
22
+
23
+
24
+ class Subsession(BaseSubsession):
25
+ pass
26
+
27
+
28
+ class Group(BaseGroup):
29
+ item_value = models.CurrencyField(
30
+ doc="""Common value of the item to be auctioned, random for treatment"""
31
+ )
32
+ highest_bid = models.CurrencyField()
33
+
34
+
35
+ class Player(BasePlayer):
36
+ item_value_estimate = models.CurrencyField(
37
+ doc="""Estimate of the common value, may be different for each player"""
38
+ )
39
+ bid_amount = models.CurrencyField(
40
+ min=C.BID_MIN,
41
+ max=C.BID_MAX,
42
+ doc="""Amount bidded by the player""",
43
+ label="Bid amount",
44
+ )
45
+ is_winner = models.BooleanField(
46
+ initial=False, doc="""Indicates whether the player is the winner"""
47
+ )
48
+
49
+
50
+ # FUNCTIONS
51
+ def creating_session(subsession: Subsession):
52
+ for g in subsession.get_groups():
53
+ import random
54
+
55
+ item_value = random.uniform(C.BID_MIN, C.BID_MAX)
56
+ g.item_value = round(item_value, 1)
57
+
58
+
59
+ def set_winner(group: Group):
60
+ import random
61
+
62
+ players = group.get_players()
63
+ group.highest_bid = max([p.bid_amount for p in players])
64
+ players_with_highest_bid = [p for p in players if p.bid_amount == group.highest_bid]
65
+ winner = random.choice(
66
+ players_with_highest_bid
67
+ ) # if tie, winner is chosen at random
68
+ winner.is_winner = True
69
+ for p in players:
70
+ set_payoff(p)
71
+
72
+
73
+ def generate_value_estimate(group: Group):
74
+ import random
75
+
76
+ estimate = group.item_value + random.uniform(-C.BID_NOISE, C.BID_NOISE)
77
+ estimate = round(estimate, 1)
78
+ if estimate < C.BID_MIN:
79
+ estimate = C.BID_MIN
80
+ if estimate > C.BID_MAX:
81
+ estimate = C.BID_MAX
82
+ return estimate
83
+
84
+
85
+ def set_payoff(player: Player):
86
+ group = player.group
87
+
88
+ if player.is_winner:
89
+ player.payoff = group.item_value - player.bid_amount
90
+ if player.payoff < 0:
91
+ player.payoff = 0
92
+ else:
93
+ player.payoff = 0
94
+
95
+
96
+ # PAGES
97
+ class Introduction(Page):
98
+ @staticmethod
99
+ def before_next_page(player: Player, timeout_happened):
100
+ group = player.group
101
+
102
+ player.item_value_estimate = generate_value_estimate(group)
103
+
104
+
105
+ class Bid(Page):
106
+ form_model = 'player'
107
+ form_fields = ['bid_amount']
108
+
109
+
110
+ class ResultsWaitPage(WaitPage):
111
+ after_all_players_arrive = set_winner
112
+
113
+
114
+ class Results(Page):
115
+ @staticmethod
116
+ def vars_for_template(player: Player):
117
+ group = player.group
118
+
119
+ return dict(is_greedy=group.item_value - player.bid_amount < 0)
120
+
121
+
122
+ page_sequence = [Introduction, Bid, ResultsWaitPage, Results]
common_value_auction/instructions.html ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <div class="card bg-light m-3">
3
+ <div class="card-body">
4
+ <h3>
5
+ Instructions
6
+ </h3>
7
+ <p>
8
+ You have been randomly and anonymously grouped with other players.
9
+ All players see these same instructions.
10
+ </p>
11
+
12
+ <p>
13
+ Your task will be to bid for an item that is being auctioned.
14
+ Prior to bidding, each player will be given an estimate of the actual
15
+ value of the item. The estimates may be different between players.
16
+ The actual value of the item, which is common to all players, will be
17
+ revealed after the bidding has taken place.
18
+ </p>
19
+
20
+ <p>
21
+ Based on the value estimate, each player will submit a single bid
22
+ within a given range.
23
+ All bids are private and submitted at the same time.
24
+ </p>
25
+
26
+ <p>
27
+ The highest bidder will receive the actual value of the item as payoff
28
+ minus their own bid amount.
29
+ If the winner's bid amount is higher than the actual value of the item,
30
+ the payoff will be zero.
31
+ In the event of a tie between two or more players, the winner will be
32
+ chosen at random. Other players will receive nothing.
33
+ </p>
34
+ </div>
35
+ </div>
cournot/Decide.html ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Production{{ endblock }}
2
+ {{ block content }}
3
+
4
+ {{ formfields }}
5
+
6
+ {{ next_button }}
7
+
8
+ {{ include_sibling 'instructions.html' }}
9
+
10
+ {{ endblock }}
cournot/Introduction.html ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Introduction{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <p class="alert alert-info small" role="note"><strong>Note:</strong> Points convert to a bonus at the end of the study.</p>
5
+
6
+ {{ include_sibling 'instructions.html' }}
7
+
8
+ {{ next_button }}
9
+
10
+ {{ endblock }}
cournot/Results.html ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Results{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <p>The results are shown in the following table.</p>
5
+
6
+ <table class="table">
7
+
8
+ <tr>
9
+ <td>Your firm produced:</td>
10
+ <td>{{ player.units }} units</td>
11
+ </tr>
12
+
13
+ <tr>
14
+ <td>The other firm produced:</td>
15
+ <td>{{ other_player_units }} units</td>
16
+ </tr>
17
+
18
+ <tr>
19
+ <td>Total production:</td>
20
+ <td>{{ group.total_units }} units</td>
21
+ </tr>
22
+
23
+ <tr>
24
+ <td style="border-top-width:4px">Unit selling price:</td>
25
+ <td style="border-top-width:4px">{{ C.TOTAL_CAPACITY }} – {{ group.total_units }} = {{ group.unit_price }}</td>
26
+ </tr>
27
+
28
+ <tr>
29
+ <td>Your profit:</td>
30
+ <td>{{ player.payoff }}</td>
31
+ </tr>
32
+
33
+ </table>
34
+
35
+ {{ next_button }}
36
+
37
+ {{ include_sibling 'instructions.html' }}
38
+
39
+ {{ endblock }}
cournot/__init__.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from otree.api import *
2
+
3
+
4
+
5
+ doc = """
6
+ In Cournot competition, firms simultaneously decide the units of products to
7
+ manufacture. The unit selling price depends on the total units produced. In
8
+ this implementation, there are 2 firms competing for 1 period.
9
+ """
10
+
11
+
12
+ class C(BaseConstants):
13
+ NAME_IN_URL = 'cournot'
14
+ PLAYERS_PER_GROUP = 2
15
+ NUM_ROUNDS = 1
16
+ # Total production capacity of all players
17
+ TOTAL_CAPACITY = 60
18
+ MAX_UNITS_PER_PLAYER = int(TOTAL_CAPACITY / PLAYERS_PER_GROUP)
19
+
20
+
21
+ class Subsession(BaseSubsession):
22
+ pass
23
+
24
+
25
+ class Group(BaseGroup):
26
+ unit_price = models.CurrencyField()
27
+ total_units = models.IntegerField(doc="""Total units produced by all players""")
28
+
29
+
30
+ class Player(BasePlayer):
31
+ units = models.IntegerField(
32
+ min=0,
33
+ max=C.MAX_UNITS_PER_PLAYER,
34
+ doc="""Quantity of units to produce""",
35
+ label="How many units will you produce (from 0 to 30)?",
36
+ )
37
+
38
+
39
+ # FUNCTIONS
40
+ def set_payoffs(group: Group):
41
+ players = group.get_players()
42
+ group.total_units = sum([p.units for p in players])
43
+ group.unit_price = C.TOTAL_CAPACITY - group.total_units
44
+ for p in players:
45
+ p.payoff = group.unit_price * p.units
46
+
47
+
48
+ def other_player(player: Player):
49
+ return player.get_others_in_group()[0]
50
+
51
+
52
+ # PAGES
53
+ class Introduction(Page):
54
+ pass
55
+
56
+
57
+ class Decide(Page):
58
+ form_model = 'player'
59
+ form_fields = ['units']
60
+
61
+
62
+ class ResultsWaitPage(WaitPage):
63
+ body_text = "Waiting for the other participant to decide."
64
+ after_all_players_arrive = set_payoffs
65
+
66
+
67
+ class Results(Page):
68
+ @staticmethod
69
+ def vars_for_template(player: Player):
70
+ return dict(other_player_units=other_player(player).units)
71
+
72
+
73
+ page_sequence = [Introduction, Decide, ResultsWaitPage, Results]
cournot/instructions.html ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <div class="card bg-light m-3">
3
+ <div class="card-body">
4
+
5
+ <h3>
6
+ Instructions
7
+ </h3>
8
+
9
+ <p>
10
+ You have been randomly and anonymously paired with another participant.
11
+ Each of you will represent a firm. Both firms manufacture the same product.
12
+ </p>
13
+
14
+ <p>
15
+ The two of you decide simultaneously and independently how many units to manufacture.
16
+ Your choices can be any number from 0 to {{ C.MAX_UNITS_PER_PLAYER }}.
17
+ All produced units will be sold, but the more is produced, the lower the unit selling price will be.
18
+ </p>
19
+
20
+
21
+ <p>
22
+ The unit selling price is:
23
+ </p>
24
+
25
+ <ul>
26
+ <strong>Unit selling price = {{ C.TOTAL_CAPACITY }} – Units produced by your firm – Units produced by the other firm</strong>
27
+ </ul>
28
+
29
+ <p>
30
+ Your profit is therefore:
31
+ </p>
32
+
33
+ <ul>
34
+ <strong>Your profit = Unit selling price × Units produced by your firm</strong>
35
+ </ul>
36
+
37
+ <p>
38
+ For your convenience, these instructions will remain available to you on all subsequent screens of this study.
39
+ </p>
40
+
41
+ </div>
42
+ </div>
dictator/Introduction.html ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {{ block title }}Introduction{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <p class="alert alert-info small" role="note"><strong>Note:</strong> Points convert to a bonus at the end of the study.</p>
5
+ {{ include_sibling 'instructions.html' }}
6
+ {{ next_button }}
7
+ {{ endblock }}
dictator/Offer.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Your Decision{{ endblock }}
2
+ {{ block content }}
3
+ <p>
4
+ You are <strong>Participant 1</strong>.
5
+ Please decide how much of the {{ C.ENDOWMENT }}
6
+ you will keep for yourself.
7
+ </p>
8
+
9
+ {{ formfields }}
10
+
11
+ {{ next_button }}
12
+
13
+ {{ include_sibling 'instructions.html' }}
14
+
15
+ {{ endblock }}
dictator/Results.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Results{{ endblock }}
2
+ {{ block content }}
3
+ <p>
4
+
5
+ {{ if player.id_in_group == 1 }}
6
+ You decided to keep <strong>{{ group.kept }}</strong> for yourself.
7
+ {{ else }}
8
+ Participant 1 decided to keep <strong>{{ group.kept }}</strong>, so
9
+ you got <strong>{{ offer }}</strong>.
10
+ {{ endif }}
11
+
12
+ {{ next_button }}
13
+ </p>
14
+ {{ include_sibling 'instructions.html' }}
15
+ {{ endblock }}
dictator/__init__.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from otree.api import *
2
+
3
+
4
+
5
+ doc = """
6
+ One player decides how to divide a certain amount between himself and the other
7
+ player.
8
+ See: Kahneman, Daniel, Jack L. Knetsch, and Richard H. Thaler. "Fairness
9
+ and the assumptions of economics." Journal of business (1986):
10
+ S285-S300.
11
+ """
12
+
13
+
14
+ class C(BaseConstants):
15
+ NAME_IN_URL = 'dictator'
16
+ PLAYERS_PER_GROUP = 2
17
+ NUM_ROUNDS = 1
18
+ # Initial amount allocated to the dictator
19
+ ENDOWMENT = cu(100)
20
+
21
+
22
+ class Subsession(BaseSubsession):
23
+ pass
24
+
25
+
26
+ class Group(BaseGroup):
27
+ kept = models.CurrencyField(
28
+ doc="""Amount dictator decided to keep for himself""",
29
+ min=0,
30
+ max=C.ENDOWMENT,
31
+ label="I will keep",
32
+ )
33
+
34
+
35
+ class Player(BasePlayer):
36
+ pass
37
+
38
+
39
+ # FUNCTIONS
40
+ def set_payoffs(group: Group):
41
+ p1 = group.get_player_by_id(1)
42
+ p2 = group.get_player_by_id(2)
43
+ p1.payoff = group.kept
44
+ p2.payoff = C.ENDOWMENT - group.kept
45
+
46
+
47
+ # PAGES
48
+ class Introduction(Page):
49
+ pass
50
+
51
+
52
+ class Offer(Page):
53
+ form_model = 'group'
54
+ form_fields = ['kept']
55
+
56
+ @staticmethod
57
+ def is_displayed(player: Player):
58
+ return player.id_in_group == 1
59
+
60
+
61
+ class ResultsWaitPage(WaitPage):
62
+ after_all_players_arrive = set_payoffs
63
+
64
+
65
+ class Results(Page):
66
+ @staticmethod
67
+ def vars_for_template(player: Player):
68
+ group = player.group
69
+
70
+ return dict(offer=C.ENDOWMENT - group.kept)
71
+
72
+
73
+ page_sequence = [Introduction, Offer, ResultsWaitPage, Results]
dictator/instructions.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <div class="card bg-light m-3">
3
+ <div class="card-body">
4
+ <h3>
5
+ Instructions
6
+ </h3>
7
+
8
+ <p>
9
+ You will be paired randomly and anonymously with another participant.
10
+ In this study, one of you will be Participant 1 and the other
11
+ Participant 2. Prior to making a decision, you will learn your role,
12
+ which will be randomly assigned.
13
+ </p>
14
+ <p>
15
+ There is {{ C.ENDOWMENT }} to split. Participant 1 will
16
+ decide how much she or he will retain. Then the rest will go to
17
+ Participant 2.
18
+ </p>
19
+ </div>
20
+ </div>
docker-entrypoint.sh ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ log() {
5
+ echo "[entrypoint] $*"
6
+ }
7
+
8
+ DB_URL="${OTREE_DATABASE_URL:-sqlite:////data/otree.sqlite3}"
9
+ DB_PATH=""
10
+
11
+ if [[ "$DB_URL" == sqlite:////* ]]; then
12
+ DB_PATH="/${DB_URL#sqlite:////}"
13
+ elif [[ "$DB_URL" == sqlite:///* ]]; then
14
+ DB_PATH="${DB_URL#sqlite:///}"
15
+ fi
16
+
17
+ if [[ -n "$DB_PATH" ]]; then
18
+ DB_DIR=$(dirname "$DB_PATH")
19
+ mkdir -p "$DB_DIR"
20
+ if [[ ! -f "$DB_PATH" ]]; then
21
+ log "Initializing SQLite database at $DB_PATH"
22
+ otree resetdb --noinput
23
+ fi
24
+ fi
25
+
26
+ if [[ "${SKIP_COLLECTSTATIC:-0}" != "1" ]]; then
27
+ log "Collecting static assets"
28
+ otree collectstatic --noinput
29
+ fi
30
+
31
+ PORT="${PORT:-8000}"
32
+ log "Starting oTree prodserver on 0.0.0.0:${PORT}"
33
+ exec otree prodserver1of2 0.0.0.0:${PORT}
guess_two_thirds/Guess.html ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Your Guess{{ endblock }}
2
+ {{ block content }}
3
+
4
+ {{ if player.round_number > 1 }}
5
+ <p>
6
+ Here were the two-thirds-average values in previous rounds:
7
+ {{ two_thirds_avg_history }}
8
+ </p>
9
+ {{ endif }}
10
+
11
+ {{ formfields }}
12
+ {{ next_button }}
13
+
14
+ {{ include_sibling 'instructions.html' }}
15
+
16
+ {{ endblock }}
guess_two_thirds/Introduction.html ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Introduction{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <p class="alert alert-info small" role="note"><strong>Note:</strong> Points convert to a bonus at the end of the study.</p>
5
+
6
+ {{ include_sibling 'instructions.html' }}
7
+
8
+ {{ next_button }}
9
+
10
+ {{ endblock }}
guess_two_thirds/Results.html ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Results{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <p>Here were the numbers guessed:</p>
5
+
6
+ <p>
7
+ {{ sorted_guesses }}
8
+ </p>
9
+
10
+ <p>
11
+ Two-thirds of the average of these numbers is {{ group.two_thirds_avg }};
12
+ the closest guess was {{ group.best_guess }}.
13
+ </p>
14
+
15
+ <p>Your guess was {{ player.guess }}.</p>
16
+
17
+ <p>
18
+ {{ if player.is_winner }}
19
+ {{ if group.num_winners > 1 }}
20
+ Therefore, you are one of the {{ group.num_winners }} winners
21
+ who tied for the best guess.
22
+ {{ else }}
23
+ Therefore, you win!
24
+ {{ endif }}
25
+ {{ else }}
26
+ Therefore, you did not win.
27
+ {{ endif }}
28
+ Your payoff is {{ player.payoff }}.
29
+ </p>
30
+
31
+ {{ next_button }}
32
+
33
+ {{ include_sibling 'instructions.html' }}
34
+
35
+ {{ endblock }}
guess_two_thirds/__init__.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from otree.api import *
2
+
3
+
4
+ doc = """
5
+ a.k.a. Keynesian beauty contest.
6
+ Players all guess a number; whoever guesses closest to
7
+ 2/3 of the average wins.
8
+ See https://en.wikipedia.org/wiki/Guess_2/3_of_the_average
9
+ """
10
+
11
+
12
+ class C(BaseConstants):
13
+ PLAYERS_PER_GROUP = 3
14
+ NUM_ROUNDS = 3
15
+ NAME_IN_URL = 'guess_two_thirds'
16
+ JACKPOT = cu(100)
17
+ GUESS_MAX = 100
18
+
19
+
20
+ class Subsession(BaseSubsession):
21
+ pass
22
+
23
+
24
+ class Group(BaseGroup):
25
+ two_thirds_avg = models.FloatField()
26
+ best_guess = models.IntegerField()
27
+ num_winners = models.IntegerField()
28
+
29
+
30
+ class Player(BasePlayer):
31
+ guess = models.IntegerField(
32
+ min=0, max=C.GUESS_MAX, label="Please pick a number from 0 to 100:"
33
+ )
34
+ is_winner = models.BooleanField(initial=False)
35
+
36
+
37
+ # FUNCTIONS
38
+ def set_payoffs(group: Group):
39
+ players = group.get_players()
40
+ guesses = [p.guess for p in players]
41
+ two_thirds_avg = (2 / 3) * sum(guesses) / len(players)
42
+ group.two_thirds_avg = round(two_thirds_avg, 2)
43
+ group.best_guess = min(guesses, key=lambda guess: abs(guess - group.two_thirds_avg))
44
+ winners = [p for p in players if p.guess == group.best_guess]
45
+ group.num_winners = len(winners)
46
+ for p in winners:
47
+ p.is_winner = True
48
+ p.payoff = C.JACKPOT / group.num_winners
49
+
50
+
51
+ def two_thirds_avg_history(group: Group):
52
+ return [g.two_thirds_avg for g in group.in_previous_rounds()]
53
+
54
+
55
+ # PAGES
56
+ class Introduction(Page):
57
+ @staticmethod
58
+ def is_displayed(player: Player):
59
+ return player.round_number == 1
60
+
61
+
62
+ class Guess(Page):
63
+ form_model = 'player'
64
+ form_fields = ['guess']
65
+
66
+ @staticmethod
67
+ def vars_for_template(player: Player):
68
+ group = player.group
69
+
70
+ return dict(two_thirds_avg_history=two_thirds_avg_history(group))
71
+
72
+
73
+ class ResultsWaitPage(WaitPage):
74
+ after_all_players_arrive = set_payoffs
75
+
76
+
77
+ class Results(Page):
78
+ @staticmethod
79
+ def vars_for_template(player: Player):
80
+ group = player.group
81
+
82
+ sorted_guesses = sorted(p.guess for p in group.get_players())
83
+ return dict(sorted_guesses=sorted_guesses)
84
+
85
+
86
+ page_sequence = [Introduction, Guess, ResultsWaitPage, Results]
guess_two_thirds/instructions.html ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <div class="card bg-light m-3">
3
+ <div class="card-body">
4
+
5
+ <h3>
6
+ Instructions
7
+ </h3>
8
+
9
+ <p>
10
+ You are in a group of {{ C.PLAYERS_PER_GROUP }} people.
11
+ Each of you will be asked to choose a
12
+ number between 0 and {{ C.GUESS_MAX }}.
13
+ The winner will be the participant whose
14
+ number is closest to 2/3 of the
15
+ average of all chosen numbers.
16
+ </p>
17
+
18
+ <p>
19
+ The winner will receive {{ C.JACKPOT }}.
20
+ In case of a tie, the {{ C.JACKPOT }}
21
+ will be equally divided among winners.
22
+ </p>
23
+
24
+ <p>This game will be played for {{ C.NUM_ROUNDS }} rounds.</p>
25
+
26
+ </div>
27
+ </div>
matching_pennies/Choice.html ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Round {{ subsession.round_number }} of {{ C.NUM_ROUNDS }}{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <h4>Instructions</h4>
5
+ <p>
6
+ This is a matching pennies game.
7
+ Player 1 is the 'Mismatcher' and wins if the choices mismatch;
8
+ Player 2 is the 'Matcher' and wins if they match.
9
+
10
+ </p>
11
+
12
+ <p>
13
+ At the end, a random round will be chosen for payment.
14
+ </p>
15
+
16
+ <p>
17
+
18
+ <h4>Round history</h4>
19
+ <table class="table">
20
+ <tr>
21
+ <th>Round</th>
22
+ <th>Player and outcome</th>
23
+ </tr>
24
+ {{ for p in player_in_previous_rounds }}
25
+ <tr>
26
+ <td>{{ p.round_number }}</td>
27
+ <td>
28
+ You were the {{ p.role }}
29
+ and {{ if p.is_winner }} won {{ else }} lost {{ endif }}
30
+ </td>
31
+ </tr>
32
+ {{ endfor }}
33
+ </table>
34
+
35
+ <p>
36
+ In this round, you are the {{ player.role }}.
37
+ </p>
38
+
39
+ {{ formfields }}
40
+
41
+ {{ next_button }}
42
+
43
+ {{ endblock }}
matching_pennies/ResultsSummary.html ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Final results{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <table class="table">
5
+ <tr>
6
+ <th>Round</th>
7
+ <th>Player and outcome</th>
8
+ </tr>
9
+ {{ for p in player_in_all_rounds }}
10
+ <tr>
11
+ <td>{{ p.round_number }}</td>
12
+ <td>
13
+ You were the {{ p.role }}
14
+ and {{ if p.is_winner }} won {{ else }} lost {{ endif }}
15
+ </td>
16
+ </tr>
17
+ {{ endfor }}
18
+ </table>
19
+
20
+ <p>
21
+ The paying round was {{ paying_round }}.
22
+ Your total payoff is therefore {{ total_payoff }}.
23
+ </p>
24
+
25
+ {{ endblock }}
matching_pennies/__init__.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from otree.api import *
2
+
3
+
4
+ doc = """
5
+ A demo of how rounds work in oTree, in the context of 'matching pennies'
6
+ """
7
+
8
+
9
+ class C(BaseConstants):
10
+ NAME_IN_URL = 'matching_pennies'
11
+ PLAYERS_PER_GROUP = 2
12
+ NUM_ROUNDS = 4
13
+ STAKES = cu(100)
14
+
15
+ MATCHER_ROLE = 'Matcher'
16
+ MISMATCHER_ROLE = 'Mismatcher'
17
+
18
+
19
+ class Subsession(BaseSubsession):
20
+ pass
21
+
22
+
23
+ class Group(BaseGroup):
24
+ pass
25
+
26
+
27
+ class Player(BasePlayer):
28
+ penny_side = models.StringField(
29
+ choices=[['Heads', 'Heads'], ['Tails', 'Tails']],
30
+ widget=widgets.RadioSelect,
31
+ label="I choose:",
32
+ )
33
+ is_winner = models.BooleanField()
34
+
35
+
36
+ # FUNCTIONS
37
+ def creating_session(subsession: Subsession):
38
+ session = subsession.session
39
+ import random
40
+
41
+ if subsession.round_number == 1:
42
+ paying_round = random.randint(1, C.NUM_ROUNDS)
43
+ session.vars['paying_round'] = paying_round
44
+ if subsession.round_number == 3:
45
+ # reverse the roles
46
+ matrix = subsession.get_group_matrix()
47
+ for row in matrix:
48
+ row.reverse()
49
+ subsession.set_group_matrix(matrix)
50
+ if subsession.round_number > 3:
51
+ subsession.group_like_round(3)
52
+
53
+
54
+ def set_payoffs(group: Group):
55
+ subsession = group.subsession
56
+ session = group.session
57
+
58
+ p1 = group.get_player_by_id(1)
59
+ p2 = group.get_player_by_id(2)
60
+ for p in [p1, p2]:
61
+ is_matcher = p.role == C.MATCHER_ROLE
62
+ p.is_winner = (p1.penny_side == p2.penny_side) == is_matcher
63
+ if subsession.round_number == session.vars['paying_round'] and p.is_winner:
64
+ p.payoff = C.STAKES
65
+ else:
66
+ p.payoff = cu(0)
67
+
68
+
69
+ # PAGES
70
+ class Choice(Page):
71
+ form_model = 'player'
72
+ form_fields = ['penny_side']
73
+
74
+ @staticmethod
75
+ def vars_for_template(player: Player):
76
+ return dict(player_in_previous_rounds=player.in_previous_rounds())
77
+
78
+
79
+ class ResultsWaitPage(WaitPage):
80
+ after_all_players_arrive = set_payoffs
81
+
82
+
83
+ class ResultsSummary(Page):
84
+ @staticmethod
85
+ def is_displayed(player: Player):
86
+ return player.round_number == C.NUM_ROUNDS
87
+
88
+ @staticmethod
89
+ def vars_for_template(player: Player):
90
+ session = player.session
91
+
92
+ player_in_all_rounds = player.in_all_rounds()
93
+ return dict(
94
+ total_payoff=sum([p.payoff for p in player_in_all_rounds]),
95
+ paying_round=session.vars['paying_round'],
96
+ player_in_all_rounds=player_in_all_rounds,
97
+ )
98
+
99
+
100
+ page_sequence = [Choice, ResultsWaitPage, ResultsSummary]
payment_info/PaymentInfo.html ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ block title }}Thank you{{ endblock }}
2
+ {{ block content }}
3
+
4
+ <p>
5
+ <em>Below are examples of messages that could be displayed for different experimental settings.</em>
6
+ </p>
7
+
8
+ <div class="panel panel-default" style="margin-bottom:10px">
9
+ <div class="panel-body">
10
+ <p><b>Laboratory:</b></p>
11
+ <p>Please remain seated until your number is called. Then take your number card, and proceed to the cashier.</p>
12
+ <p><em>Note: For the cashier in the laboratory, oTree can print a list of payments for all participants as a PDF.</em></p>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="panel panel-default" style="margin-bottom:10px">
17
+ <div class="panel-body">
18
+ <p><b>Classroom:</b></p>
19
+ <p><em>If you want to keep track of how students did, the easiest thing is to assign the starting links to students by name.
20
+ It is even possible to give each student a single permanent link for a whole semester using Rooms;
21
+ so no need to waste time in each lecture with handing out new links and keeping track of which student uses which link.
22
+ Alternatively, you may just give
23
+ students anonymous links or secret nicknames.</em>
24
+ </p>
25
+ </div>
26
+ </div>
27
+
28
+
29
+ {{ endblock }}
30
+
payment_info/__init__.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from otree.api import *
2
+
3
+
4
+
5
+ doc = """
6
+ This application provides a webpage instructing participants how to get paid.
7
+ Examples are given for the lab and Amazon Mechanical Turk (AMT).
8
+ """
9
+
10
+
11
+ class C(BaseConstants):
12
+ NAME_IN_URL = 'payment_info'
13
+ PLAYERS_PER_GROUP = None
14
+ NUM_ROUNDS = 1
15
+
16
+
17
+ class Subsession(BaseSubsession):
18
+ pass
19
+
20
+
21
+ class Group(BaseGroup):
22
+ pass
23
+
24
+
25
+ class Player(BasePlayer):
26
+ pass
27
+
28
+
29
+ # FUNCTIONS
30
+ # PAGES
31
+ class PaymentInfo(Page):
32
+ @staticmethod
33
+ def vars_for_template(player: Player):
34
+ participant = player.participant
35
+ return dict(redemption_code=participant.label or participant.code)
36
+
37
+
38
+ page_sequence = [PaymentInfo]
payment_info/tests.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from otree.api import Bot
2
+
3
+
4
+ class PlayerBot(Bot):
5
+ def play_round(self):
6
+ # No inputs; placeholder bot to satisfy session testing
7
+ pass
8
+