diff --git a/live_bargaining/__init__.py b/live_bargaining/__init__.py
index 6dccf97..ed38e2e 100644
--- a/live_bargaining/__init__.py
+++ b/live_bargaining/__init__.py
@@ -246,7 +246,10 @@ def create_acceptance_data(group: Group):
class Info(Page):
@staticmethod
def vars_for_template(player: Player):
- return dict(actual_round_number=player.subsession.round_number - 1)
+ return dict(
+ actual_round_number=player.subsession.round_number - 1,
+ player_name=player.session.config["player_names"][f"P{player.id_in_group}"],
+ )
@staticmethod
def js_vars(player: Player):
@@ -286,7 +289,9 @@ def vars_for_template(player: Player):
return dict(
last_player_is_dummy=len(player.session.config["prod_fct"])
== C.PLAYERS_PER_GROUP - 1,
+ player_name=player.session.config["player_names"][f"P{player.id_in_group}"],
actual_round_number=player.subsession.round_number - 1,
+ **player.session.config["player_names"],
)
@staticmethod
@@ -383,6 +388,7 @@ def js_vars(player: Player):
def vars_for_template(player: Player):
return dict(
payoff_to_display=f"CHF {player.payoff_this_round:.2f}",
+ **player.session.config["player_names"],
)
diff --git a/settings.py b/settings.py
index c706e3b..c2a47d7 100644
--- a/settings.py
+++ b/settings.py
@@ -13,10 +13,15 @@
num_demo_participants=3,
seconds_per_round=5 * 60,
prod_fct={
- "{P2,\xa0P3}": 0,
- "{P1,\xa0P2},\n{P1,\xa0P3}": 10,
+ "{B1,\xa0B2}": 0,
+ "{A,\xa0B1},\n{A,\xa0B2}": 10,
"Everyone": 100,
},
+ player_names={
+ "P1": "A",
+ "P2": "B1",
+ "P3": "B2",
+ },
doc="Treatment with production function [0, 10, 100]",
),
dict(
@@ -26,10 +31,15 @@
num_demo_participants=3,
seconds_per_round=5 * 60,
prod_fct={
- "{P2,\xa0P3}": 0,
- "{P1,\xa0P2}, {P1,\xa0P3}": 30,
+ "{B1,\xa0B2}": 0,
+ "{A,\xa0B1}, {A,\xa0B2}": 30,
"Everyone": 100,
},
+ player_names={
+ "P1": "A",
+ "P2": "B1",
+ "P3": "B2",
+ },
doc="Treatment with production function [0, 30, 100]",
),
dict(
@@ -39,10 +49,15 @@
num_demo_participants=3,
seconds_per_round=5 * 60,
prod_fct={
- "{P2,\xa0P3}": 0,
- "{P1,\xa0P2}, {P1,\xa0P3}": 90,
+ "{B1,\xa0B2}": 0,
+ "{A,\xa0B1}, {A,\xa0B2}": 90,
"Everyone": 100,
},
+ player_names={
+ "P1": "A",
+ "P2": "B1",
+ "P3": "B2",
+ },
doc="Treatment with production function [0, 90, 100]",
),
dict(
@@ -52,8 +67,13 @@
num_demo_participants=3,
seconds_per_round=5 * 60,
prod_fct={
- "{P1,\xa0P3}, {P2,\xa0P3}": 0,
- "{P1,\xa0P2}, Everyone": 100,
+ "{A1,\xa0B}, {A2,\xa0B}": 0,
+ "{A1,\xa0A2}, Everyone": 100,
+ },
+ player_names={
+ "P1": "A1",
+ "P2": "A2",
+ "P3": "B",
},
doc="Treatment with production function [0, 100] and P3 as dummy player",
),
From 3b30f9d289f65f312331e73547a5c31fe2d64c68 Mon Sep 17 00:00:00 2001
From: Martin Stancsics
Date: Tue, 23 Apr 2024 11:38:16 +0200
Subject: [PATCH 2/8] Change player names in chat
---
_static/live_bargaining.css | 2 +-
live_bargaining/Bargain.html | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/_static/live_bargaining.css b/_static/live_bargaining.css
index f4d8eb2..6b752be 100644
--- a/_static/live_bargaining.css
+++ b/_static/live_bargaining.css
@@ -118,7 +118,7 @@ table.shaded-rows tbody tr:nth-child(odd) {
}
.otree-chat__nickname {
- width: 120px;
+ width: 70px;
}
#payoff-table {
diff --git a/live_bargaining/Bargain.html b/live_bargaining/Bargain.html
index 528631b..8de1049 100644
--- a/live_bargaining/Bargain.html
+++ b/live_bargaining/Bargain.html
@@ -74,7 +74,7 @@
Chat
- {{ chat nickname=player.role }}
+ {{ chat nickname=player_name }}
From b528b1706b3c6157112340f0c26958e0850439c4 Mon Sep 17 00:00:00 2001
From: Martin Stancsics
Date: Tue, 23 Apr 2024 11:46:16 +0200
Subject: [PATCH 3/8] Change player names in JS
---
_static/accept.js | 3 ++-
_static/coalitions_demo.js | 4 +++-
_static/live_bargaining.js | 3 ++-
_static/proposal_demo.js | 3 ++-
introduction/__init__.py | 2 ++
live_bargaining/__init__.py | 2 ++
6 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/_static/accept.js b/_static/accept.js
index ea921ff..2e0c83c 100644
--- a/_static/accept.js
+++ b/_static/accept.js
@@ -1,4 +1,5 @@
let numPlayers = 3;
+let player_names = js_vars.player_names;
let pastOffersTable = document.getElementById('past-offers-table');
@@ -12,7 +13,7 @@ function populatePastOffers(pastOffers) {
let from = row.insertCell();
from.className = "offer-proposer-col";
- from.innerHTML = `P${offer.player}`;
+ from.innerHTML = player_names[`P${offer.player}`];
for (let i = 0; i < numPlayers; i++) {
let cell = row.insertCell();
diff --git a/_static/coalitions_demo.js b/_static/coalitions_demo.js
index 760f271..42253bb 100644
--- a/_static/coalitions_demo.js
+++ b/_static/coalitions_demo.js
@@ -1,5 +1,7 @@
let numPlayers = 3;
+let player_names = js_vars.player_names;
+
let totalShareable = document.getElementById('total-shareable');
let totalShared = document.getElementById('total-shared');
@@ -101,7 +103,7 @@ function updatePastOffers(newPastOffers) {
let from = row.insertCell();
from.className = "offer-proposer-col";
- from.innerHTML = `P${offer.player}`;
+ from.innerHTML = player_names[`P${offer.player}`];
for (let i = 0; i < numPlayers; i++) {
let cell = row.insertCell();
diff --git a/_static/live_bargaining.js b/_static/live_bargaining.js
index be60151..e9a0d7e 100644
--- a/_static/live_bargaining.js
+++ b/_static/live_bargaining.js
@@ -21,6 +21,7 @@ let pastOffers = []
let prod_fct = js_vars.prod_fct;
let prod_fct_labels = js_vars.prod_fct_labels;
let lastPlayerIsDummy = prod_fct.length == numPlayers - 1;
+let player_names = js_vars.player_names;
let popupFull = document.getElementById('popup-full');
let popupTitle = document.getElementById('popup-title');
@@ -184,7 +185,7 @@ function updatePastOffers(newPastOffers) {
let from = row.insertCell();
from.className = "offer-proposer-col";
- from.innerHTML = `P${offer.player}`;
+ from.innerHTML = player_names[`P${offer.player}`];
for (let i = 0; i < numPlayers; i++) {
let cell = row.insertCell();
diff --git a/_static/proposal_demo.js b/_static/proposal_demo.js
index 2e08f09..4aece75 100644
--- a/_static/proposal_demo.js
+++ b/_static/proposal_demo.js
@@ -19,6 +19,7 @@ let pastOffers = []
let prod_fct = js_vars.prod_fct;
let prod_fct_labels = js_vars.prod_fct_labels;
let lastPlayerIsDummy = prod_fct.length == numPlayers - 1;
+let player_names = js_vars.player_names;
let popupFull = document.getElementById('popup-full');
let popupTitle = document.getElementById('popup-title');
@@ -151,7 +152,7 @@ function updatePastOffers(newPastOffers) {
let from = row.insertCell();
from.className = "offer-proposer-col";
- from.innerHTML = `P${offer.player}`;
+ from.innerHTML = player_names[`P${offer.player}`];
for (let i = 0; i < numPlayers; i++) {
let cell = row.insertCell();
diff --git a/introduction/__init__.py b/introduction/__init__.py
index 5205419..a7e7375 100644
--- a/introduction/__init__.py
+++ b/introduction/__init__.py
@@ -39,6 +39,7 @@ def js_vars(player: Player):
prod_fct=list(player.session.config["prod_fct"].values()),
prod_fct_labels=list(player.session.config["prod_fct"].keys()),
my_id=1,
+ player_names=player.session.config["player_names"],
)
@staticmethod
@@ -51,6 +52,7 @@ class Coalitions(Page):
def js_vars(player: Player):
return dict(
my_id=1,
+ player_names=player.session.config["player_names"],
)
@staticmethod
diff --git a/live_bargaining/__init__.py b/live_bargaining/__init__.py
index ed38e2e..1647078 100644
--- a/live_bargaining/__init__.py
+++ b/live_bargaining/__init__.py
@@ -282,6 +282,7 @@ def js_vars(player: Player):
my_id=player.id_in_group,
prod_fct=list(player.session.config["prod_fct"].values()),
prod_fct_labels=list(player.session.config["prod_fct"].keys()),
+ player_names=player.session.config["player_names"],
)
@staticmethod
@@ -382,6 +383,7 @@ def js_vars(player: Player):
acceptances=acceptance_data["acceptances"],
coalition_members=acceptance_data["coalition_members"],
payoffs=acceptance_data["payoffs"],
+ player_names=player.session.config["player_names"],
)
@staticmethod
From 381e39b46b0d60529d2907560da498c27b5cb76b Mon Sep 17 00:00:00 2001
From: Martin Stancsics
Date: Tue, 23 Apr 2024 11:53:19 +0200
Subject: [PATCH 4/8] Make pyright happy
---
introduction/__init__.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/introduction/__init__.py b/introduction/__init__.py
index a7e7375..56067b8 100644
--- a/introduction/__init__.py
+++ b/introduction/__init__.py
@@ -43,7 +43,7 @@ def js_vars(player: Player):
)
@staticmethod
- def vars_for_template(subsession):
+ def vars_for_template(subsession: BaseSubsession): # type: ignore
return subsession.session.config["player_names"]
@@ -56,7 +56,7 @@ def js_vars(player: Player):
)
@staticmethod
- def vars_for_template(subsession):
+ def vars_for_template(subsession: BaseSubsession): # type: ignore
return subsession.session.config["player_names"]
From 5172707498f0e1b726f89c73a93588a054741fe7 Mon Sep 17 00:00:00 2001
From: Martin Stancsics
Date: Tue, 23 Apr 2024 11:59:00 +0200
Subject: [PATCH 5/8] Make pyright happier
---
introduction/__init__.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/introduction/__init__.py b/introduction/__init__.py
index 56067b8..c753778 100644
--- a/introduction/__init__.py
+++ b/introduction/__init__.py
@@ -43,8 +43,8 @@ def js_vars(player: Player):
)
@staticmethod
- def vars_for_template(subsession: BaseSubsession): # type: ignore
- return subsession.session.config["player_names"]
+ def vars_for_template(player: Player):
+ return player.session.config["player_names"]
class Coalitions(Page):
@@ -56,8 +56,8 @@ def js_vars(player: Player):
)
@staticmethod
- def vars_for_template(subsession: BaseSubsession): # type: ignore
- return subsession.session.config["player_names"]
+ def vars_for_template(player: Player):
+ return player.session.config["player_names"]
class Payment(Page):
From 3b6518ff5b82d1cddcf3d97c77b94c7173dfd140 Mon Sep 17 00:00:00 2001
From: Martin Stancsics
Date: Tue, 23 Apr 2024 12:09:42 +0200
Subject: [PATCH 6/8] Remove remaining Player 1 mentions
---
_static/live_bargaining.js | 2 +-
_static/proposal_demo.js | 2 +-
live_bargaining/__init__.py | 6 +-----
live_bargaining/tests.py | 7 ++++---
4 files changed, 7 insertions(+), 10 deletions(-)
diff --git a/_static/live_bargaining.js b/_static/live_bargaining.js
index e9a0d7e..6d0c0f4 100644
--- a/_static/live_bargaining.js
+++ b/_static/live_bargaining.js
@@ -69,7 +69,7 @@ for (let i = 0; i < numPlayers; i++) {
function sendOffer() {
if (totalSharedValue > 0 && !isMemberCheckboxes[0].checked) {
- openPopup('Invalid proposal: the budget is zero when Player 1 is not included in the group', 'error');
+ openPopup(`Invalid proposal: the budget is zero when Player ${player_names['P1']} is not included in the group`, 'error');
return;
}
if (totalSharedValue > totalShareableValue) {
diff --git a/_static/proposal_demo.js b/_static/proposal_demo.js
index 4aece75..77f9c5a 100644
--- a/_static/proposal_demo.js
+++ b/_static/proposal_demo.js
@@ -92,7 +92,7 @@ function sendOffer() {
};
if (totalSharedValue > 0 && !members[0]) {
- openPopup('Invalid proposal: the budget is zero when Player 1 is not included in the group', 'error');
+ openPopup(`Invalid proposal: the budget is zero when Player ${player_names['P1']} is not included in the group`, 'error');
return;
}
if (totalSharedValue > totalShareableValue) {
diff --git a/live_bargaining/__init__.py b/live_bargaining/__init__.py
index 1647078..9be9387 100644
--- a/live_bargaining/__init__.py
+++ b/live_bargaining/__init__.py
@@ -45,10 +45,6 @@ class C(BaseConstants):
PLAYERS_PER_GROUP = 3
NUM_ROUNDS = 5 # todo: adjust for main experiment
- BIG_ROLE = "Player 1"
- SMALL1_ROLE = "Player 2"
- SMALL2_ROLE = "Player 3"
-
class Subsession(BaseSubsession):
start_time = models.FloatField(initial=float("inf")) # type: ignore
@@ -183,7 +179,7 @@ def check_proposal_validity(player: Player, members, allocations):
return {
player.id_in_group: {
"type": "error",
- "content": "Invalid allocation: allocation has to be zero when Player 1 is not included", # noqa: E501
+ "content": f"Invalid allocation: allocation has to be zero when Player {player.session.config['player_names']['P1']} is not included", # noqa: E501
}
}
diff --git a/live_bargaining/tests.py b/live_bargaining/tests.py
index cc904a6..69ac4c4 100644
--- a/live_bargaining/tests.py
+++ b/live_bargaining/tests.py
@@ -39,7 +39,7 @@ def create_offers(method, Y):
)
-def test_invalid_input(method, Y, dummy_player=False):
+def test_invalid_input(method, Y, dummy_player, P1_name):
non_existing_offer = method(1, {"type": "accept", "offer_id": 1})
expect(
non_existing_offer,
@@ -96,7 +96,7 @@ def test_invalid_input(method, Y, dummy_player=False):
{
3: {
"type": "error",
- "content": "Invalid allocation: allocation has to be zero when Player 1 is not included",
+ "content": f"Invalid allocation: allocation has to be zero when Player {P1_name} is not included",
}
},
)
@@ -179,12 +179,13 @@ def call_live_method(method, **kwargs):
)
prod_fct = kwargs["group"].session.config["prod_fct"]
+ P1_name = kwargs["group"].session.config["player_names"]["P1"]
Y = list(prod_fct.values())[1]
dummy_player = len(prod_fct) == 2
if kwargs["round_number"] == 1:
print("Testing invalid input")
- test_invalid_input(method, Y, dummy_player)
+ test_invalid_input(method, Y, dummy_player, P1_name)
if kwargs["round_number"] == 2:
print("Testing grand coalition")
From dbbd5f130ca8e6611dd89a47a3b87acf07b35b1b Mon Sep 17 00:00:00 2001
From: Martin Stancsics
Date: Tue, 23 Apr 2024 14:28:34 +0200
Subject: [PATCH 7/8] Better error message for the dummy player case
---
_static/live_bargaining.js | 6 +++++-
_static/proposal_demo.js | 6 +++++-
live_bargaining/__init__.py | 27 ++++++++++++++++++++++---
live_bargaining/tests.py | 39 ++++++++++++++++++++++++-------------
4 files changed, 59 insertions(+), 19 deletions(-)
diff --git a/_static/live_bargaining.js b/_static/live_bargaining.js
index 6d0c0f4..b72bf68 100644
--- a/_static/live_bargaining.js
+++ b/_static/live_bargaining.js
@@ -68,10 +68,14 @@ for (let i = 0; i < numPlayers; i++) {
}
function sendOffer() {
- if (totalSharedValue > 0 && !isMemberCheckboxes[0].checked) {
+ if (!lastPlayerIsDummy && totalSharedValue > 0 && !isMemberCheckboxes[0].checked) {
openPopup(`Invalid proposal: the budget is zero when Player ${player_names['P1']} is not included in the group`, 'error');
return;
}
+ if (lastPlayerIsDummy && totalSharedValue > 0 && !(isMemberCheckboxes[0].checked && isMemberCheckboxes[1].checked)) {
+ openPopup(`Invalid proposal: the budget is zero when Players ${player_names['P1']} and ${player_names['P2']} are not included in the group`, 'error');
+ return;
+ }
if (totalSharedValue > totalShareableValue) {
openPopup('Invalid proposal: total amount exceeds the budget available to this group', 'error');
return;
diff --git a/_static/proposal_demo.js b/_static/proposal_demo.js
index 77f9c5a..8d98390 100644
--- a/_static/proposal_demo.js
+++ b/_static/proposal_demo.js
@@ -91,10 +91,14 @@ function sendOffer() {
'allocations': allocations,
};
- if (totalSharedValue > 0 && !members[0]) {
+ if (!lastPlayerIsDummy && totalSharedValue > 0 && !isMemberCheckboxes[0].checked) {
openPopup(`Invalid proposal: the budget is zero when Player ${player_names['P1']} is not included in the group`, 'error');
return;
}
+ if (lastPlayerIsDummy && totalSharedValue > 0 && !(isMemberCheckboxes[0].checked && isMemberCheckboxes[1].checked)) {
+ openPopup(`Invalid proposal: the budget is zero when Players ${player_names['P1']} and ${player_names['P2']} are not included in the group`, 'error');
+ return;
+ }
if (totalSharedValue > totalShareableValue) {
openPopup('Invalid proposal: total amount exceeds the budget available to this group', 'error');
return;
diff --git a/live_bargaining/__init__.py b/live_bargaining/__init__.py
index 9be9387..c10a50a 100644
--- a/live_bargaining/__init__.py
+++ b/live_bargaining/__init__.py
@@ -175,16 +175,37 @@ def check_proposal_validity(player: Player, members, allocations):
}
}
- if not big_player_included and sum(allocations) > 0:
+ last_player_is_dummy = len(prod_fct) == C.PLAYERS_PER_GROUP - 1
+
+ if not last_player_is_dummy and not big_player_included and sum(allocations) > 0:
+ return {
+ player.id_in_group: {
+ "type": "error",
+ "content": (
+ "Invalid allocation: allocation has to be zero when "
+ f"Player {player.session.config['player_names']['P1']} is not included"
+ ),
+ }
+ }
+
+ if (
+ last_player_is_dummy
+ and not (members[0] and members[1])
+ and sum(allocations) > 0
+ ):
return {
player.id_in_group: {
"type": "error",
- "content": f"Invalid allocation: allocation has to be zero when Player {player.session.config['player_names']['P1']} is not included", # noqa: E501
+ "content": (
+ "Invalid allocation: allocation has to be zero when Players "
+ f"{player.session.config['player_names']['P1']} and "
+ f"{player.session.config['player_names']['P2']} are not included"
+ ),
}
}
num_small_players = coalition_size - big_player_included
- if len(prod_fct) == C.PLAYERS_PER_GROUP - 1: # last player is a dummy player
+ if last_player_is_dummy: # last player is a dummy player
num_small_players -= members[-1] # the dummy player is always last
if sum(allocations) > prod_fct[num_small_players]:
diff --git a/live_bargaining/tests.py b/live_bargaining/tests.py
index 69ac4c4..cb99239 100644
--- a/live_bargaining/tests.py
+++ b/live_bargaining/tests.py
@@ -39,7 +39,7 @@ def create_offers(method, Y):
)
-def test_invalid_input(method, Y, dummy_player, P1_name):
+def test_invalid_input(method, Y, dummy_player, player_names):
non_existing_offer = method(1, {"type": "accept", "offer_id": 1})
expect(
non_existing_offer,
@@ -69,8 +69,8 @@ def test_invalid_input(method, Y, dummy_player, P1_name):
2,
{
"type": "propose",
- "members": [True, False, True],
- "allocations": [50, 0, 50],
+ "members": [True, True, True],
+ "allocations": [50, 50, 10],
},
)
expect(
@@ -91,15 +91,26 @@ def test_invalid_input(method, Y, dummy_player, P1_name):
"allocations": [0, 50, 50],
},
)
- expect(
- p1_not_included,
- {
- 3: {
- "type": "error",
- "content": f"Invalid allocation: allocation has to be zero when Player {P1_name} is not included",
- }
- },
- )
+ if not dummy_player:
+ expect(
+ p1_not_included,
+ {
+ 3: {
+ "type": "error",
+ "content": f"Invalid allocation: allocation has to be zero when Player {player_names['P1']} is not included",
+ }
+ },
+ )
+ else:
+ expect(
+ p1_not_included,
+ {
+ 3: {
+ "type": "error",
+ "content": f"Invalid allocation: allocation has to be zero when Players {player_names['P1']} and {player_names['P2']} are not included",
+ }
+ },
+ )
invalid_allocaion_negative = method(
1,
@@ -179,13 +190,13 @@ def call_live_method(method, **kwargs):
)
prod_fct = kwargs["group"].session.config["prod_fct"]
- P1_name = kwargs["group"].session.config["player_names"]["P1"]
+ player_names = kwargs["group"].session.config["player_names"]
Y = list(prod_fct.values())[1]
dummy_player = len(prod_fct) == 2
if kwargs["round_number"] == 1:
print("Testing invalid input")
- test_invalid_input(method, Y, dummy_player, P1_name)
+ test_invalid_input(method, Y, dummy_player, player_names)
if kwargs["round_number"] == 2:
print("Testing grand coalition")
From 9e4bb3afcdb3d1adc3d9ecf4bc5e22d2f48400bf Mon Sep 17 00:00:00 2001
From: Martin Stancsics
Date: Tue, 23 Apr 2024 16:10:04 +0200
Subject: [PATCH 8/8] Better wording
---
introduction/Proposal.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/introduction/Proposal.html b/introduction/Proposal.html
index cca0107..cc4d653 100644
--- a/introduction/Proposal.html
+++ b/introduction/Proposal.html
@@ -15,7 +15,7 @@ Group budgets
You and the two other players will each be randomly assigned a player role ({{P1}}, {{P2}}, {{P3}}).
- The player numbers will be reshuffled each round.
+ The player roles will be reshuffled each round.