Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transitions requiring multiple actors #263

Merged
merged 13 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dlgr/griduniverse/config.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ window_rows = 20
window_columns = 20
use_identicons = true
show_chatroom = true
recruiter = hotair

[HIT Configuration]
title = Griduniverse
Expand All @@ -38,3 +39,6 @@ num_dynos_worker = 1
host = 0.0.0.0
clock_on = false
logfile = -

[docker]
docker_image_base_name = ghcr.io/dallinger/griduniverse
28 changes: 28 additions & 0 deletions dlgr/griduniverse/constraints.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# dallinger generate-constraints
#
# Compiled from a requirement.txt file with md5sum: 17238cf158a7db2d1abade14af6da593
#
faker==19.2.0
# via
# -c /home/silvio/Jazkarta/Dallinger/dev-requirements.txt
# -r /tmp/tmpv0n8930i/requirements.txt
numpy==1.24.4
# via
# -c /home/silvio/Jazkarta/Dallinger/dev-requirements.txt
# -r /tmp/tmpv0n8930i/requirements.txt
python-dateutil==2.8.2
# via
# -c /home/silvio/Jazkarta/Dallinger/dev-requirements.txt
# faker
pyyaml==6.0.1
# via
# -c /home/silvio/Jazkarta/Dallinger/dev-requirements.txt
# -r /tmp/tmpv0n8930i/requirements.txt
six==1.16.0
# via
# -c /home/silvio/Jazkarta/Dallinger/dev-requirements.txt
# python-dateutil
15 changes: 14 additions & 1 deletion dlgr/griduniverse/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -1705,7 +1705,11 @@ def handle_item_transition(self, msg):
if transition is None:
transition = self.transition_config.get(transition_key)

if transition is None:
required_actors = transition and transition.get("required_actors", 0)
neighbors = player.neighbors()
if (transition is None) or (
required_actors and len(neighbors) + 1 < required_actors
):
error_msg = {
"type": "action_error",
"player_id": player.id,
Expand Down Expand Up @@ -1756,6 +1760,15 @@ def handle_item_transition(self, msg):
self.grid.item_locations[position] = new_target_item
self.grid.items_updated = True

# Possibly distribute calories to participating players
transition_calories = transition.get("calories")
if transition_calories:
per_player = transition_calories // (len(neighbors) + 1)
for other_player in neighbors:
other_player.score += per_player
player.score += per_player
player.score += transition_calories % (len(neighbors) + 1)

def handle_item_drop(self, msg):
player = self.grid.players[msg["player_id"]]
player_item = player.current_item
Expand Down
33 changes: 33 additions & 0 deletions dlgr/griduniverse/game_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,26 @@ items:
item_count: 0
sprite: "emoji:🌴"

- name: Stag
item_id: stag
n_uses: 1
sprite: "emoji:🦌"
spawn_rate: 0.05
item_count: 5
portable: false
crossable: true
interactive: true

- name: Fallen Stag
item_id: fallen_stag
n_uses: 1
sprite: "emoji:👜"
silviot marked this conversation as resolved.
Show resolved Hide resolved
interactive: false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't really matter now that @jessesnyder has fixed the client side bug in handling zero-calorie non-interactive items, but I think this should probably be interactive: true, since the fallen stag is likely to be a useful item and is not immediately edible. I believe that interactive: false is primarily intended for Pac Man™ style legacy food-type items.

spawn_rate: 0
item_count: 0
portable: false
crossable: true

transitions:
- actor_start: stone
actor_end: sharp_stone
Expand Down Expand Up @@ -328,3 +348,16 @@ transitions:
- -1
target_end: empty_gooseberry_bush
target_start: gooseberry_bush

# At least two players can use a sharp stone to consume a stag
- actor_start: sharp_stone
actor_end: sharp_stone
required_actors: 2
target_start: stag
target_end: fallen_stag
visible: always
last_use: false
calories: 20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea here is that consuming the stag provides a total of 20 calories, split evenly among the participants (with the initiator getting any remainder), and what's left is a fallen stag which may have separate uses?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should have another example multi-actor transition with no calories, but results in an object with multiple available uses?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alecpm You're correct about what the idea is.

Items added to this file are effective in the game. I can add one more example, but it should probably be a "real" one. Or maybe I misunderstood your suggestion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it would be good to have a "real" example of such a transition, but such transitions are the second checkbox on #235. So I think we'd ideally have such an example available. Maybe best to ask Natalia for one?

modify_uses:
- 0
- -1
1 change: 0 additions & 1 deletion dlgr/griduniverse/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
dallinger
numpy
faker
PyYAML
33 changes: 31 additions & 2 deletions dlgr/griduniverse/static/scripts/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,29 @@ var playerSet = (function () {
return distances[0].player;
};

PlayerSet.prototype.getAdjacentPlayers = function () {
/* Return a list of players adjacent to the ego player */
adjacentPlayers = [];
let egoPostiion = this._players[ego].position;
for (id in this._players) {
silviot marked this conversation as resolved.
Show resolved Hide resolved
if (id === ego) {
continue;
}
if (this._players.hasOwnProperty(id)) {
let player = this._players[id];
if (player.hasOwnProperty('position')) {
let position = player.position;
let distanceX = Math.abs(position[0] - egoPostiion[0]);
let distanceY = Math.abs(position[1] - egoPostiion[1])
if (distanceX <= 1 && distanceY <= 1) {
adjacentPlayers.push(player);
}
}
}
}
return adjacentPlayers;
};

PlayerSet.prototype.ego = function () {
return this.get(this.ego_id);
};
Expand Down Expand Up @@ -1048,8 +1071,14 @@ function renderTransition(transition) {
aEndItemString = `✋${aEndItem ? aEndItem.name: '⬜'}`;
tEndItemString = tEndItem ? tEndItem.name: '⬜';
}
return `${aStartItemString} + ${tStartItemString} → ${aEndItemString} + ${tEndItemString}`;

var actors_info = "";
const required_actors = transition.transition.required_actors
// The total number of actors is the number of adjacent players plus one for ego (the current player)
const neighbouringActors = players.getAdjacentPlayers().length + 1;
silviot marked this conversation as resolved.
Show resolved Hide resolved
if (neighbouringActors < required_actors) {
actors_info = ` - not available: ${required_actors - neighbouringActors} more players needed`;
}
return `${aStartItemString} + ${tStartItemString} → ${aEndItemString} + ${tEndItemString}${actors_info}`;
}
/**
* If the current player is sharing a grid position with an interactive
Expand Down
33 changes: 31 additions & 2 deletions dlgr/griduniverse/static/scripts/dist/bundle.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dlgr/griduniverse/static/scripts/dist/bundle.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dlgr/griduniverse/static/scripts/dist/difi.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dlgr/griduniverse/static/scripts/dist/questionnaire.js.map

Large diffs are not rendered by default.

128 changes: 125 additions & 3 deletions test/test_transitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def test_handle_item_transition_combine_items(self, mocked_exp, player):
assert player.current_item.name == "Sharp Stone"
assert len(mocked_exp.grid.items_consumed) == 1
assert len(mocked_exp.grid.item_locations) == 1
list(mocked_exp.grid.item_locations.values())[0].name == "Big Hard Rock"
assert list(mocked_exp.grid.item_locations.values())[0].name == "Big Hard Rock"

def test_handle_item_transition_reduces_items_remaining(self, mocked_exp, player):
gooseberry_bush = create_item(**mocked_exp.item_config["gooseberry_bush"])
Expand All @@ -98,7 +98,9 @@ def test_handle_item_transition_reduces_items_remaining(self, mocked_exp, player
assert gooseberry_bush.remaining_uses == 5
assert len(mocked_exp.grid.items_consumed) == 0
assert len(mocked_exp.grid.item_locations) == 1
list(mocked_exp.grid.item_locations.values())[0].name == "Gooseberry Bush"
assert (
list(mocked_exp.grid.item_locations.values())[0].name == "Gooseberry Bush"
)

def test_handle_last_item_transition(self, mocked_exp, player):
gooseberry_bush = create_item(**mocked_exp.item_config["gooseberry_bush"])
Expand All @@ -117,7 +119,60 @@ def test_handle_last_item_transition(self, mocked_exp, player):
# The bush is gone and replaced with an empty one
assert len(mocked_exp.grid.items_consumed) == 1
assert len(mocked_exp.grid.item_locations) == 1
list(mocked_exp.grid.item_locations.values())[0].name == "Empty Gooseberry Bush"
assert (
list(mocked_exp.grid.item_locations.values())[0].name
== "Empty Gooseberry Bush"
)

def test_handle_item_transition_multiple_actors_error(self, mocked_exp, player):
stone = create_item(**mocked_exp.item_config["stone"])
mocked_exp.grid.item_locations[(2, 2)] = stone
mocked_exp.grid.players[player.id].position = [2, 2]
mocked_exp.transition_config = TRANSITION_CONFIG

mocked_exp.handle_item_transition(
msg={"player_id": player.id, "position": (2, 2)}
)
# Only one player was present. The transition did not happen, since it requires 2
assert list(mocked_exp.grid.item_locations.values())[0].name == "Stone"

def test_handle_item_transition_multiple_actors_success(
self, mocked_exp, a, player
):
mocked_exp.transition_config = TRANSITION_CONFIG
other_player = create_player(mocked_exp, a)
stone = create_item(**mocked_exp.item_config["stone"])
mocked_exp.grid.item_locations[(2, 2)] = stone
player.position = [2, 2]
mocked_exp.handle_item_transition(
msg={"player_id": player.id, "position": (2, 2)}
)
# The second player was not close enough. The transition did not happen
assert list(mocked_exp.grid.item_locations.values())[0].name == "Stone"

other_player.position = [2, 1]
mocked_exp.handle_item_transition(
msg={"player_id": player.id, "position": (2, 2)}
)
assert list(mocked_exp.grid.item_locations.values())[0].name == "Sharp Stone"

def test_handle_item_transition_multiple_actors_distribute_calories(
self, mocked_exp, a, player
):
mocked_exp.transition_config = TRANSITION_CONFIG
other_player = create_player(mocked_exp, a)
player.current_item = create_item(**mocked_exp.item_config["sharp_stone"])
stag = create_item(**mocked_exp.item_config["stag"])
mocked_exp.grid.item_locations[(2, 2)] = stag
player.position = [2, 2]
other_player.position = [2, 1]
mocked_exp.handle_item_transition(
msg={"player_id": player.id, "position": (2, 2)}
)
assert list(mocked_exp.grid.item_locations.values())[0].name == "Fallen Stag"
# The 25 total calories should be diveded evenly, but the initiator gets the reminder if any
assert player.score == 13
assert other_player.score == 12


class TestHandleItemConsume(object):
Expand Down Expand Up @@ -187,6 +242,10 @@ def item(exp):

@pytest.fixture
def player(exp, a):
return create_player(exp, a)


def create_player(exp, a):
from dlgr.griduniverse.experiment import Player

participant = a.participant()
Expand Down Expand Up @@ -215,3 +274,66 @@ def create_item(**kwargs):
}
item_data.update(kwargs)
return Item(item_data)


# Configuration of transitions: tests need stable transitions,
# so we use a fixed configuration here.
TRANSITION_CONFIG = {
("last", None, "gooseberry_bush"): {
"actor_end": "gooseberry",
"actor_start": None,
"last_use": True,
"modify_uses": [0, -1],
"target_end": "empty_gooseberry_bush",
"target_start": "gooseberry_bush",
"visible": "always",
},
("sharp_stone", "wild_carrot_plant"): {
"actor_end": "sharp_stone",
"actor_start": "sharp_stone",
"last_use": False,
"modify_uses": [0, 0],
"target_end": "wild_carrot",
"target_start": "wild_carrot_plant",
"visible": "seen",
},
(None, "gooseberry_bush"): {
"actor_end": "gooseberry",
"actor_start": None,
"last_use": False,
"modify_uses": [0, -1],
"target_end": "gooseberry_bush",
"target_start": "gooseberry_bush",
"visible": "never",
},
(None, "stone"): {
"actor_end": None,
"actor_start": None,
"last_use": False,
"modify_uses": [0, 0],
"required_actors": 2,
"target_end": "sharp_stone",
"target_start": "stone",
"visible": "always",
},
("stone", "big_hard_rock"): {
"actor_end": "sharp_stone",
"actor_start": "stone",
"last_use": False,
"modify_uses": [0, 0],
"target_end": "big_hard_rock",
"target_start": "big_hard_rock",
"visible": "always",
},
("sharp_stone", "stag"): {
"actor_end": "sharp_stone",
"actor_start": "sharp_stone",
"last_use": False,
"modify_uses": [0, -1],
"required_actors": 2,
"target_end": "fallen_stag",
"target_start": "stag",
"visible": "always",
"calories": 25,
},
}
Loading