diff --git a/_static/coalitions_demo.js b/_static/coalitions_demo.js index 42253bb..f949e7d 100644 --- a/_static/coalitions_demo.js +++ b/_static/coalitions_demo.js @@ -46,7 +46,7 @@ exampleOffers = [ { 'offer_id': 3, 'player': 1, - 'members': [true, true, true], + 'members': [true, true, false], 'allocations': [80, 20, 0], }, { diff --git a/_static/live_bargaining.js b/_static/live_bargaining.js index b72bf68..68558f5 100644 --- a/_static/live_bargaining.js +++ b/_static/live_bargaining.js @@ -47,7 +47,11 @@ openPopup = function (content, type) { for (let i = 0; i < numPlayers; i++) { isMemberCheckboxes[i].addEventListener('change', function () { allocationTextBoxes[i].disabled = !isMemberCheckboxes[i].checked; - allocationTextBoxes[i].value = 0; + if (!isMemberCheckboxes[i].checked) { + allocationTextBoxes[i].value = 0; + } else { + allocationTextBoxes[i].value = 1; + } updateTotalShareable(); updateTotalShared(); if (!allocationTextBoxes[i].disabled) { @@ -56,12 +60,12 @@ for (let i = 0; i < numPlayers; i++) { }); allocationTextBoxes[i].addEventListener('change', function () { - allocationTextBoxes[i].value = Math.floor(Math.max(0, allocationTextBoxes[i].value)); + allocationTextBoxes[i].value = Math.floor(Math.max(1, allocationTextBoxes[i].value)); updateTotalShared(); }); allocationTextBoxes[i].addEventListener("keyup", function (event) { if (event.key === "Enter") { - allocationTextBoxes[i].value = Math.floor(Math.max(0, allocationTextBoxes[i].value)); + allocationTextBoxes[i].value = Math.floor(Math.max(1, allocationTextBoxes[i].value)); updateTotalShared(); } }); @@ -80,6 +84,12 @@ function sendOffer() { openPopup('Invalid proposal: total amount exceeds the budget available to this group', 'error'); return; } + for (let i = 0; i < numPlayers; i++) { + if (isMemberCheckboxes[i].checked && allocationTextBoxes[i].value === '0') { + openPopup('Invalid proposal: all group members must receive a positive amount', 'error'); + return; + } + } members = isMemberCheckboxes.map(member => member.checked); allocations = allocationTextBoxes.map(alloc => alloc.value); liveSend({ 'type': 'propose', 'members': members, 'allocations': allocations }) diff --git a/_static/proposal_demo.js b/_static/proposal_demo.js index 8d98390..972ba39 100644 --- a/_static/proposal_demo.js +++ b/_static/proposal_demo.js @@ -59,7 +59,11 @@ openPopup = function (content, type) { for (let i = 0; i < numPlayers; i++) { isMemberCheckboxes[i].addEventListener('change', function () { allocationTextBoxes[i].disabled = !isMemberCheckboxes[i].checked; - allocationTextBoxes[i].value = 0; + if (!isMemberCheckboxes[i].checked) { + allocationTextBoxes[i].value = 0; + } else { + allocationTextBoxes[i].value = 1; + } updateTotalShareable(); updateTotalShared(); if (!allocationTextBoxes[i].disabled) { @@ -68,12 +72,12 @@ for (let i = 0; i < numPlayers; i++) { }); allocationTextBoxes[i].addEventListener('change', function () { - allocationTextBoxes[i].value = Math.floor(Math.max(0, allocationTextBoxes[i].value)); + allocationTextBoxes[i].value = Math.floor(Math.max(1, allocationTextBoxes[i].value)); updateTotalShared(); }); allocationTextBoxes[i].addEventListener("keyup", function (event) { if (event.key === "Enter") { - allocationTextBoxes[i].value = Math.floor(Math.max(0, allocationTextBoxes[i].value)); + allocationTextBoxes[i].value = Math.floor(Math.max(1, allocationTextBoxes[i].value)); updateTotalShared(); } }); @@ -103,6 +107,12 @@ function sendOffer() { openPopup('Invalid proposal: total amount exceeds the budget available to this group', 'error'); return; } + for (let i = 0; i < numPlayers; i++) { + if (isMemberCheckboxes[i].checked && allocationTextBoxes[i].value === '0') { + openPopup('Invalid proposal: all group members must receive a positive amount', 'error'); + return; + } + } newPastOffers.push(newOffer); updatePastOffers(newPastOffers); diff --git a/introduction/Coalitions.html b/introduction/Coalitions.html index d57fa7b..890b897 100644 --- a/introduction/Coalitions.html +++ b/introduction/Coalitions.html @@ -22,7 +22,7 @@

Group formation

Once the round ends, the final outcome and the payoffs of this round are determined as follows: Only if all players in a proposed group agree on the same proposal is that proposal successful. - Note, that players who are not included in a proposal do not have to agree to it. + Note, that players who are not included in a proposal (marked as "—") do not have to agree to it. The group is then successfully formed and its members' payoffs are then determined by the agreed-upon proposal. All other players get 0. diff --git a/live_bargaining/__init__.py b/live_bargaining/__init__.py index 3d856d9..0530c3a 100644 --- a/live_bargaining/__init__.py +++ b/live_bargaining/__init__.py @@ -220,6 +220,16 @@ def check_proposal_validity(player: Player, members, allocations): } } + if any( + member and allocation == 0 for member, allocation in zip(members, allocations) + ): + return { + player.id_in_group: { + "type": "error", + "content": "Invalid allocation: all members must receive a positive amount", # noqa: E501 + } + } + def check_acceptance_validity(player: Player, offer_id): if not isinstance(offer_id, int): diff --git a/live_bargaining/tests.py b/live_bargaining/tests.py index cb99239..c9905ca 100644 --- a/live_bargaining/tests.py +++ b/live_bargaining/tests.py @@ -10,7 +10,7 @@ def create_offers(method, Y): { "type": "propose", "members": [True, True, True], - "allocations": [100, 0, 0], + "allocations": [90, 5, 5], }, ) @@ -182,6 +182,24 @@ def test_invalid_input(method, Y, dummy_player, player_names): {3: {"type": "error", "content": "Data is incomplete"}}, ) + member_receives_zero = method( + 1, + { + "type": "propose", + "members": [True, True, True], + "allocations": [100, 0, 0], + }, + ) + expect( + member_receives_zero, + { + 1: { + "type": "error", + "content": "Invalid allocation: all members must receive a positive amount", + } + }, + ) + def call_live_method(method, **kwargs): print(