4.2 Tutorial: the Prisoner’s Dilemma
The Prisoner’s Dilemma is a classic problem in game theory that demonstrates how two rational individuals might not cooperate, even if it is in their best interest to do so. In its standard form, two suspects are arrested and interrogated separately. If both remain silent (that is, if they cooperate with each other), they each receive a light sentence. If one betrays the other (defects) while the other stays silent, the defector goes free, and the silent prisoner gets the maximum sentence. If both betray each other, they each receive a moderate sentence. The dilemma arises because betrayal is the dominant strategy for both, leading to a worse collective outcome than mutual cooperation.
In this tutorial, we’ll write a study that matches each participant with one other participant, and pits the two against each other in a “Prisoner’s Dilemma”.
Until now, our tutorial studies have only recorded and considered the responses of individual participants in isolation. But in the Prisoner’s Dilemma that’s not enough: each participant’s response will need to be compared with that of one other participant.
In doing so, you’ll learn how to design studies that match people into groups, and that incorporate responses of multiple people when calculating results.
To follow along: open DrRacket on your computer and create a new file that starts with #lang conscript.
4.2.1 Defining our variables
As with all studies, we start by defining variables to keep track of responses and results of the study.
#lang conscript (require conscript/survey-tools racket/match) (defvar my-choice) (defvar their-choice) (defvar prison-sentence)
We’ll use my-choice to record the current participant’s choice, their-choice to store the choice made by the person they were paired with, and prison-sentence to record the final result (the length of the prison sentence they end up with).
Because all three variables are declared with defvar, they will have participant scope — that is, each participant will have their own separate values for these variables.
We also require two modules we’ll need later: conscript/survey-tools provides refresh-every, and racket/match provides the match* form we’ll use to determine the outcome based on both players’ choices.
4.2.2 The introduction step
The first step in our study will be a simple one explaining what is about to happen:
(defstep (intro) @md{# Prisoner's Dilemma You are a suspect in a crime investigation. Your accomplice (another study participant) has also been arrested and is being held separately. Each of you has a choice: will you attempt to **cooperate** with your accomplice by staying silent, or will you **defect** and admit everything to the police, betraying your partner? * If you both choose to cooperate with each other, you’ll each get a 1-year prison sentence. * If you choose to defect and your partner tries to cooperate, you’ll go free and your partner will get a 20-year prison sentence. * If you both try to betray each other, you’ll each receive a 5-year prison sentence. @button{Continue...}})
In concrete programming terms, this study step is implemented as a function with no arguments (here named intro) that returns a study page.
The @ and surrounding curly-style {} indicate that we’re calling the md function using “Scribble syntax” in Conscript, to make it easier to intermingle form elements and text.
The md function can take any mix of Markdown and form controls and produce a study page for us.
By adding a button, we give the user a way to move on to the next step.
4.2.3 Pairing up prisoners
(defstep (waiter) @md{# Please Wait Please wait while another participant joins the queue. @refresh-every[5]}) (define matchmaker (make-matchmaker 2)) (defstep (pair-with-someone) (matchmaker waiter))
Conscript gives you functions that handle the messy work of matching up people into groups.
make-matchmaker gives you a function that takes 1 argument, which should be a procedure that produces a study step/page. When you call this matchmaker function, it will put the current participant into a group, and display the page you provided. Every time that page is refreshed, it checks to see if the group has been filled up by other participants; if not, it shows the same page again. If the group is filled, the matchmaker function will automatically move participants to the next step in the study.
Here we’re creating a matchmaker function that keeps group sizes to 2 members. Inside the pair-with-someone step we call that function, giving it the waiter step that refreshes the page every 5 seconds. That refresh will in turn kick back to the pair-with-someone step, keeping the loop going until another participant fills the other slot in the current group.
4.2.4 Making and storing choices
Now comes the heart of our study: presenting choices to participants and storing them in a way that participants in the same group can access each other’s responses.
(define (store-my-choice! val) (set! my-choice val) (store-my-result-in-group! 'choice val)) (defstep (make-choice) (define (cooperate) (store-my-choice! 'cooperate)) (define (defect) (store-my-choice! 'defect)) @md{# Make Your Choice @button[#:id "cooperate" cooperate]{Cooperate} @button[#:id "defect" defect]{Defect}})
Let’s break down what’s happening here:
4.2.4.1 The store-my-choice! helper function
This function does two things when a participant makes their choice:
(set! my-choice val) — Stores the choice in the current participant’s my-choice variable for later reference.
(store-my-result-in-group! 'choice val) — Stores the choice in a special shared data structure that other members of the current group can access. The first argument, 'choice, is a lookup key that we’ll use later to retrieve this value. Think of it as a label or tag for this piece of information.
The store-my-result-in-group! function is provided by Conscript specifically for multi-participant studies. It handles all the complexity of managing shared data within groups, allowing each participant to store values that their group members can later retrieve.
4.2.4.2 The make-choice step
The page shows you two buttons: you can cooperate or defect. Buttons normally just move you to the next step, but these also call functions that record the choice that was made.
We define two small helper functions (cooperate and defect) inside this defstep because they won’t be used anywhere else. Each one calls our store-my-choice! function with the appropriate value: 'cooperate or 'defect.
4.2.5 Waiting for your partner
Even after you’ve made your choice, you can’t really move on until the other person in your group has made theirs. This step handles that waiting:
(defstep (wait) (if (= (current-group-results-count 'choice) 0) @md{# Please Wait Please wait for the other participant to make their choice... @refresh-every[5]} (skip)))
The current-group-results-count function counts how many other members of your group have stored a result under the given lookup key (in this case, 'choice). Note that by default, this function does not count the current participant’s own result.
If the count is 0, that means the other participant hasn’t made their choice yet. In this case, we display a waiting page that refreshes every 5 seconds. Each refresh triggers this step again, giving us another chance to check.
If the count is not 0 (meaning the other participant has made their choice), we call (skip) to automatically move to the next step.
4.2.6 Calculating and displaying the result
Once both participants have made their choices, we can determine the outcome:
(defstep (display-result) (define their-choice (first (current-group-member-results 'choice))) (set! prison-sentence (match* (my-choice their-choice) [('cooperate 'cooperate) 1] [('cooperate 'defect) 20] [('defect 'defect) 5] [('defect 'cooperate) 0])) @md{# Result The other person chose to @~a[their-choice], while you chose to @~a[my-choice]. You get @~a[prison-sentence] years of prison.})
4.2.6.1 Retrieving the partner’s choice
(current-group-member-results 'choice) returns a list containing the results that other members of the current group have stored under the lookup key 'choice. Since we’re in a group of 2, this list will contain exactly one element: our partner’s choice.
We use first to extract that single value and store it in their-choice.
4.2.6.2 Determining the outcome
The match* form lets us pattern-match against multiple values at once. We provide it with two values (my-choice and their-choice) and then list all possible combinations:
If both cooperated: 1 year each
If I cooperated and they defected: I get 20 years (the sucker’s payoff)
If both defected: 5 years each
If I defected and they cooperated: I go free (0 years)
We store this result in prison-sentence, which records it in the database for later analysis.
4.2.6.3 Displaying the result
Finally, we display both choices and the outcome to the participant. The ~a function converts values to strings so they can be displayed in the Markdown text.
4.2.7 Putting it all together
Now we tie all the steps together into a complete study:
(defstudy prisoners-dilemma [intro --> pair-with-someone --> make-choice --> wait --> display-result] [display-result --> display-result])
This defstudy form defines our study’s transition graph: participants move through intro, then pair-with-someone, then make-choice, then wait, and finally display-result. The last line [display-result --> display-result] indicates that display-result is a terminal step — it transitions to itself, so participants remain on that page once they reach it.
4.2.8 Testing the Prisoner’s Dilemma
To test your study, you’ll need to simulate two participants:
Upload your study to your local Congame server (the Docker container) by clicking the Upload Study button in DrRacket.
Create a new instance of your study.
In your main browser window, enroll in the study as the first participant. You should progress through the introduction and then see the “Please wait while another participant joins the queue” page.
- Open a new private/incognito browser window and navigate to your local Congame server (usually http://localhost:5100/_anon-login/[INSTANCENAME]). This will create a new anonymous participant and enroll it in the study.
In the second browser window, progress through the study as the second participant. At the matchmaking step, both participants should be matched together and can proceed to make their choices.
Make a choice in each browser window and observe how the waiting and result steps work.
4.2.9 Key concepts recap
This tutorial introduced several important concepts for multi-participant studies:
Matchmaking (make-matchmaker): Automatically groups participants and manages the waiting process until groups are filled.
Storing group results (store-my-result-in-group!): Allows participants to store data that other members of their group can access, using lookup keys to organize different pieces of information.
Retrieving group results (current-group-member-results): Retrieves data that other group members have stored under a specific lookup key.
Counting group results (current-group-results-count): Checks how many other group members have stored data under a specific lookup key, useful for wait conditions.
Pattern matching (match*): A powerful way to handle different combinations of values and determine outcomes.
4.2.10 Next Steps
Now that you’ve built a basic multi-participant study, you might want to explore:
Experiment with different group sizes. This would involve changing the argument to make-matchmaker, the checking of other group members’ results in the wait step, and the match* comparison of results in the display-result step. What would a three-person Prisoner’s Dilemma look like?
Try adding multiple rounds to your Prisoner’s Dilemma. See congame-example-study/prisoners-dilemma.rkt for an example of how to implement a repeated game.
Consult the Conscript Cookbook for more recipes and patterns for common study tasks.
Review the reference documentation for make-matchmaker, store-my-result-in-group!, and related functions to understand all their options and capabilities.