Spaces:
Paused
Paused
BPEL Bot commited on
Commit ·
e40dce0
0
Parent(s):
Initial Hugging Face deployment
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +13 -0
- .gitignore +14 -0
- Dockerfile +33 -0
- EXPORTS.md +40 -0
- LICENSE +26 -0
- Procfile +2 -0
- README.md +76 -0
- TESTING.md +72 -0
- _rooms/econ101.txt +3 -0
- _rooms/lab_demo.txt +10 -0
- _static/custom.css +47 -0
- _static/global/empty.css +0 -0
- _templates/global/Page.html +8 -0
- bargaining/Introduction.html +7 -0
- bargaining/Request.html +12 -0
- bargaining/Results.html +26 -0
- bargaining/__init__.py +75 -0
- bargaining/instructions.html +20 -0
- bertrand/Decide.html +11 -0
- bertrand/Introduction.html +7 -0
- bertrand/Results.html +26 -0
- bertrand/__init__.py +73 -0
- bertrand/instructions.html +19 -0
- common_value_auction/Bid.html +19 -0
- common_value_auction/Introduction.html +10 -0
- common_value_auction/Results.html +37 -0
- common_value_auction/__init__.py +122 -0
- common_value_auction/instructions.html +35 -0
- cournot/Decide.html +10 -0
- cournot/Introduction.html +10 -0
- cournot/Results.html +39 -0
- cournot/__init__.py +73 -0
- cournot/instructions.html +42 -0
- dictator/Introduction.html +7 -0
- dictator/Offer.html +15 -0
- dictator/Results.html +15 -0
- dictator/__init__.py +73 -0
- dictator/instructions.html +20 -0
- docker-entrypoint.sh +33 -0
- guess_two_thirds/Guess.html +16 -0
- guess_two_thirds/Introduction.html +10 -0
- guess_two_thirds/Results.html +35 -0
- guess_two_thirds/__init__.py +86 -0
- guess_two_thirds/instructions.html +27 -0
- matching_pennies/Choice.html +43 -0
- matching_pennies/ResultsSummary.html +25 -0
- matching_pennies/__init__.py +100 -0
- payment_info/PaymentInfo.html +30 -0
- payment_info/__init__.py +38 -0
- 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 |
+
|