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.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/_static/live_bargaining.js b/_static/live_bargaining.js index be60151..b72bf68 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'); @@ -67,8 +68,12 @@ 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'); + 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) { @@ -184,7 +189,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..8d98390 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'); @@ -90,8 +91,12 @@ function sendOffer() { 'allocations': allocations, }; - if (totalSharedValue > 0 && !members[0]) { - openPopup('Invalid proposal: the budget is zero when Player 1 is not included in the group', 'error'); + 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) { @@ -151,7 +156,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/Coalitions.html b/introduction/Coalitions.html index 7a80213..d57fa7b 100644 --- a/introduction/Coalitions.html +++ b/introduction/Coalitions.html @@ -28,7 +28,7 @@

Group formation

All other players get 0. If there is no such agreement, all three players get 0.

-

Note that because Player 1 has to be included for a group to receive a budget, there will be one group at most.

+

Note that because Player {{P1}} has to be included for a group to receive a budget, there will be one group at most.


@@ -67,9 +67,9 @@

All proposals

ID From - P1 - P2 - P3 + {{P1}} + {{P2}} + {{P3}} @@ -84,9 +84,9 @@

Choose a proposal

- Player 1 - Player 2 - Player 3 + Player {{P1}} + Player {{P2}} + Player {{P3}} Accepted proposal ID @@ -149,9 +149,9 @@

Currently accepted proposal

- Player 1 - Player 2 - Player 3 + Player {{P1}} + Player {{P2}} + Player {{P3}} Accept proposal ID diff --git a/introduction/Proposal.html b/introduction/Proposal.html index b74b200..cc4d653 100644 --- a/introduction/Proposal.html +++ b/introduction/Proposal.html @@ -14,18 +14,18 @@

Group budgets

- You and the two other players will each be randomly assigned a player number ("Player 1/2/3"). - The player numbers will be reshuffled each round. + You and the two other players will each be randomly assigned a player role ({{P1}}, {{P2}}, {{P3}}). + The player roles will be reshuffled each round.

- A group needs to include Player 1 to receive any budget. + A group needs to include Player {{P1}} to receive any budget. The more members a group has, the bigger the budget. For the exact size of the budget in CHF, you will see a table and a corresponding graph like the one below ("Group budgets").

-

For example, if Player 1 and one other player form a group together, they have a budget of 50 CHF. If all players form a group together, they have a budget of 100 CHF. (The exact numbers might be different in the game.)

+

For example, if Player {{P1}} and one other player form a group together, they have a budget of 50 CHF. If all players form a group together, they have a budget of 100 CHF. (The exact numbers might be different in the game.)

@@ -113,9 +113,9 @@

Past proposals

ID From - P1 - P2 - P3 + {{P1}} + {{P2}} + {{P3}} @@ -132,9 +132,9 @@

Make a proposal

- Player 1 - Player 2 - Player 3 + Player {{P1}} + Player {{P2}} + Player {{P3}} Total diff --git a/introduction/__init__.py b/introduction/__init__.py index 5d97b1e..c753778 100644 --- a/introduction/__init__.py +++ b/introduction/__init__.py @@ -39,16 +39,26 @@ 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 + def vars_for_template(player: Player): + return player.session.config["player_names"] + class Coalitions(Page): @staticmethod def js_vars(player: Player): return dict( my_id=1, + player_names=player.session.config["player_names"], ) + @staticmethod + def vars_for_template(player: Player): + return player.session.config["player_names"] + class Payment(Page): pass diff --git a/live_bargaining/Bargain.html b/live_bargaining/Bargain.html index 93f792b..8de1049 100644 --- a/live_bargaining/Bargain.html +++ b/live_bargaining/Bargain.html @@ -15,13 +15,13 @@ - This is round {{ actual_round_number }}. You are Player {{ player.id_in_group }}. + This is round {{ actual_round_number }}. You are Player {{ player_name }}. {{ if subsession.round_number == 1 }} @@ -74,7 +74,7 @@

Chat

- {{ chat nickname=player.role }} + {{ chat nickname=player_name }}
@@ -85,9 +85,9 @@

Past proposals

ID From - P1 - P2 - P3 + {{P1}} + {{P2}} + {{P3}} @@ -104,9 +104,9 @@

Make a proposal

- Player 1 - Player 2 - Player 3 + Player {{P1}} + Player {{P2}} + Player {{P3}} Total @@ -142,9 +142,9 @@

Currently accepted proposal

- Player 1 - Player 2 - Player 3 + Player {{P1}} + Player {{P2}} + Player {{P3}} Accepted proposal ID diff --git a/live_bargaining/BargainingResults.html b/live_bargaining/BargainingResults.html index b3589a7..d140251 100644 --- a/live_bargaining/BargainingResults.html +++ b/live_bargaining/BargainingResults.html @@ -17,9 +17,9 @@

Accepted proposals

- Player 1 - Player 2 - Player 3 + Player {{P1}} + Player {{P2}} + Player {{P3}} Accepted proposal ID @@ -45,9 +45,9 @@

All proposals

ID From - P1 - P2 - P3 + {{P1}} + {{P2}} + {{P3}} diff --git a/live_bargaining/Info.html b/live_bargaining/Info.html index 05ed712..d048c12 100644 --- a/live_bargaining/Info.html +++ b/live_bargaining/Info.html @@ -20,7 +20,7 @@

In this round, unlike in previous rounds, adding Player 5 has no impact on the budget.

{{ endif }} -

In this round you are Player {{ player.id_in_group }}.

+

In this round you are Player {{ player_name }}.

Remember, payoffs are determined as follows:
diff --git a/live_bargaining/__init__.py b/live_bargaining/__init__.py index 6dccf97..c10a50a 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 @@ -179,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": "Invalid allocation: allocation has to be zero when Player 1 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]: @@ -246,7 +263,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): @@ -279,6 +299,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 @@ -286,7 +307,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 @@ -377,12 +400,14 @@ 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 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/live_bargaining/tests.py b/live_bargaining/tests.py index cc904a6..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=False): +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=False): 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=False): "allocations": [0, 50, 50], }, ) - expect( - p1_not_included, - { - 3: { - "type": "error", - "content": "Invalid allocation: allocation has to be zero when Player 1 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,12 +190,13 @@ def call_live_method(method, **kwargs): ) prod_fct = kwargs["group"].session.config["prod_fct"] + 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) + test_invalid_input(method, Y, dummy_player, player_names) if kwargs["round_number"] == 2: print("Testing grand coalition") 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", ),