From 9cad056211d23503634164520d086024de3ff199 Mon Sep 17 00:00:00 2001 From: Alec Mitchell Date: Thu, 12 Oct 2023 16:29:20 -0700 Subject: [PATCH 1/8] Add support for loading map grid from CSV. The parsing is TBD. See #268 and #269 --- dlgr/griduniverse/experiment.py | 39 +++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/dlgr/griduniverse/experiment.py b/dlgr/griduniverse/experiment.py index 9792a113..36012847 100644 --- a/dlgr/griduniverse/experiment.py +++ b/dlgr/griduniverse/experiment.py @@ -1,6 +1,7 @@ """The Griduniverse.""" import collections +import csv import datetime import itertools import json @@ -94,6 +95,7 @@ "difi_group_label": unicode, "difi_group_image": unicode, "fun_survey": bool, + "map_csv": unicode, "pre_difi_question": bool, "pre_difi_group_label": unicode, "pre_difi_group_image": unicode, @@ -201,6 +203,7 @@ def __init__(self, **kwargs): self.visibility_ramp_time = kwargs.get("visibility_ramp_time", 4) self.background_animation = kwargs.get("background_animation", True) self.player_overlap = kwargs.get("player_overlap", False) + self.map_csv = kwargs.get("map_csv", None) # Motion self.motion_speed_limit = kwargs.get("motion_speed_limit", 8) @@ -507,6 +510,21 @@ def compute_payoffs(self): player.payoff *= inter_proportions[player.color_idx] player.payoff *= self.dollars_per_point + def load_map(self): + with open(self.map_csv) as csv_file: + grid_state = self.csv_to_grid_state(csv_file) + self.deserialize(grid_state) + + def csv_to_grid_state(self, csv_file): + grid_state = {} + reader = csv.reader(csv_file) + for i, row in enumerate(reader): + for j, col in enumerate(row): + # location = (i, j) + # Process each col value + continue + return grid_state + def build_labyrinth(self): if self.walls_density and not self.wall_locations: start = time.time() @@ -1353,7 +1371,7 @@ def handle_connect(self, msg): return logger.info("Client {} has connected.".format(player_id)) - client_count = len(self.grid.players) + client_count = len(self.node_by_player_id) logger.info("Grid num players: {}".format(self.grid.num_players)) if client_count < self.grid.num_players: participant = self.session.query(dallinger.models.Participant).get( @@ -1370,13 +1388,14 @@ def handle_connect(self, msg): # We use the current node id modulo the number of colours # to pick the user's colour. This ensures that players are # allocated to colours uniformly. - self.grid.spawn_player( - id=player_id, - color_name=self.grid.limited_player_color_names[ - node.id % self.grid.num_colors - ], - recruiter_id=participant.recruiter_id, - ) + if player_id not in self.grid.players: + self.grid.spawn_player( + id=player_id, + color_name=self.grid.limited_player_color_names[ + node.id % self.grid.num_colors + ], + recruiter_id=participant.recruiter_id, + ) else: logger.info("No free network found for player {}".format(player_id)) @@ -1721,7 +1740,9 @@ def send_state_thread(self): def game_loop(self): """Update the world state.""" gevent.sleep(0.1) - if not self.config.get("replay", False): + if self.config.get("map_csv", None): + self.grid.load_map() + elif not self.config.get("replay", False): self.grid.build_labyrinth() logger.info("Spawning items") for item_type in self.item_config.values(): From 429d16e6a1d8b0e9e71241570745d39dc33dbf7e Mon Sep 17 00:00:00 2001 From: Jesse Snyder Date: Thu, 12 Oct 2023 16:18:12 -0700 Subject: [PATCH 2/8] Function to transform a 2D matrix into Gridworld's serialization format --- dlgr/griduniverse/csv_gridworlds.py | 61 ++++++++++ test/test_gridworld.py | 166 ++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 dlgr/griduniverse/csv_gridworlds.py diff --git a/dlgr/griduniverse/csv_gridworlds.py b/dlgr/griduniverse/csv_gridworlds.py new file mode 100644 index 00000000..3fb39570 --- /dev/null +++ b/dlgr/griduniverse/csv_gridworlds.py @@ -0,0 +1,61 @@ +import re +from collections import defaultdict + +from dlgr.griduniverse.experiment import Gridworld + +player_regex = re.compile(r"(p\d+)(c\d+)?") +color_names = Gridworld.player_color_names + + +def matrix2gridworld(matrix): + """Transform a 2D matrix representing an initial grid state + into the serialized format used by Gridworld. + """ + result = defaultdict(list) + + result["rows"] = len(matrix) + if matrix: + result["columns"] = len(matrix[0]) + else: + result["columns"] = 0 + + for row_num, row in enumerate(matrix): + for col_num, cell in enumerate(row): + position = [col_num, row_num] + cell = cell.strip() + player_match = player_regex.match(cell) + if not cell: + # emtpy + continue + if cell == "w": + result["walls"].append(position) + elif player_match: + id_str, color_str = player_match.groups() + player_id = int(id_str.replace("p", "")) + player_data = { + "id": player_id, + "position": position, + } + if color_str is not None: + player_color_index = int(color_str.replace("c", "")) - 1 + try: + player_data["color"] = color_names[player_color_index] + except IndexError: + max_color = len(color_names) + raise ValueError( + f"Invalid player color specified in {cell}. Max color is {max_color}" + ) + + result["players"].append(player_data) + else: + # assume an Item + id_and_maybe_uses = [s.strip() for s in cell.split("|")] + item_data = { + "item_id": id_and_maybe_uses[0], + "position": position, + } + if len(id_and_maybe_uses) == 2: + item_data["remaining_uses"] = int(id_and_maybe_uses[1]) + result["items"].append(item_data) + + return dict(result) diff --git a/test/test_gridworld.py b/test/test_gridworld.py index 3630acd0..1b2d4a2a 100644 --- a/test/test_gridworld.py +++ b/test/test_gridworld.py @@ -157,3 +157,169 @@ def test_instructions(self, gridworld): # Just test something basic html = gridworld.instructions() assert "🫐 Gooseberry (3 points)" in html + + +class TestMatrix2SerializedGridworld(object): + """Tests for converting a list of lists extracted from matrix + representation of initial grid state into the format used in + Gridworld [de]serialization. + """ + + @property + def subject(self): + from dlgr.griduniverse.csv_gridworlds import matrix2gridworld + + return matrix2gridworld + + def test_transformation_one_row(self): + csv = [["w", "stone", "", "gooseberry_bush|3", "p1c2"]] + + result = self.subject(csv) + + assert result == { + "columns": 5, + "rows": 1, + "items": [ + { + "item_id": "stone", + "position": [1, 0], + }, + { + "item_id": "gooseberry_bush", + "position": [3, 0], + "remaining_uses": 3, + }, + ], + "walls": [ + [0, 0], + ], + "players": [ + { + "color": "YELLOW", + "position": [4, 0], + "id": 1, + } + ], + } + + def test_transformation_multirow(self): + csv = [ + ["w", "stone", "", "gooseberry_bush|3", "p1c1"], + ["", "w", "", "", ""], + ["", "p2c1", "w", "", ""], + ["", "", "", "w", ""], + ["", "", "", "p3c2", "w"], + ["", "", "", "", "w"], + ["gooseberry_bush|4", "", "", "", ""], + ["", "big_hard_rock", "", "", ""], + ["", "p4c2", "", "", ""], + ["", "", "p5c3", "", ""], + ] + + result = self.subject(csv) + + assert result == { + "columns": 5, + "rows": 10, + "items": [ + { + "item_id": "stone", + "position": [1, 0], + }, + { + "item_id": "gooseberry_bush", + "position": [3, 0], + "remaining_uses": 3, + }, + { + "item_id": "gooseberry_bush", + "position": [0, 6], + "remaining_uses": 4, + }, + { + "item_id": "big_hard_rock", + "position": [1, 7], + }, + ], + "walls": [ + [0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [4, 5], + ], + "players": [ + { + "color": "BLUE", + "position": [4, 0], + "id": 1, + }, + { + "color": "BLUE", + "position": [1, 2], + "id": 2, + }, + { + "color": "YELLOW", + "position": [3, 4], + "id": 3, + }, + { + "color": "YELLOW", + "position": [1, 8], + "id": 4, + }, + { + "color": "ORANGE", + "position": [2, 9], + "id": 5, + }, + ], + } + + def test_transformation_empty_matrix(self): + csv = [] + + result = self.subject(csv) + + assert result == {"rows": 0, "columns": 0} + + def test_transformation_not_confused_by_whitepace(self): + csv = [["w ", " stone", " ", " gooseberry_bush | 3 ", " p1c2"]] + + result = self.subject(csv) + + assert result == { + "columns": 5, + "rows": 1, + "items": [ + { + "item_id": "stone", + "position": [1, 0], + }, + { + "item_id": "gooseberry_bush", + "position": [3, 0], + "remaining_uses": 3, + }, + ], + "walls": [ + [0, 0], + ], + "players": [ + { + "color": "YELLOW", + "position": [4, 0], + "id": 1, + } + ], + } + + def test_transformation_explains_invalid_player_colors(self): + csv = [["p1c999"]] + + with pytest.raises(ValueError) as exc_info: + self.subject(csv) + + assert exc_info.match("Invalid player color") From 6f0ab4a8595ab0de60545b33b5348507eb65106c Mon Sep 17 00:00:00 2001 From: Jesse Snyder Date: Thu, 12 Oct 2023 16:26:19 -0700 Subject: [PATCH 3/8] Integrate CSV-to-grid state work with experiment infrastructure --- dlgr/griduniverse/csv_gridworlds.py | 5 ++- dlgr/griduniverse/experiment.py | 23 +++++------ test/conftest.py | 5 +++ test/test_griduniverse.py | 63 ++++++++++++++++++++++++++++- test/test_gridworld.py | 33 ++++++++++++--- 5 files changed, 109 insertions(+), 20 deletions(-) diff --git a/dlgr/griduniverse/csv_gridworlds.py b/dlgr/griduniverse/csv_gridworlds.py index 3fb39570..d60efecc 100644 --- a/dlgr/griduniverse/csv_gridworlds.py +++ b/dlgr/griduniverse/csv_gridworlds.py @@ -43,7 +43,9 @@ def matrix2gridworld(matrix): except IndexError: max_color = len(color_names) raise ValueError( - f"Invalid player color specified in {cell}. Max color is {max_color}" + f'Invalid player color specified in "{cell}" at postion {position}. ' + f"Max color value is {max_color}, " + f"but you specified {player_color_index + 1}." ) result["players"].append(player_data) @@ -51,6 +53,7 @@ def matrix2gridworld(matrix): # assume an Item id_and_maybe_uses = [s.strip() for s in cell.split("|")] item_data = { + "id": len(result["items"]) + 1, "item_id": id_and_maybe_uses[0], "position": position, } diff --git a/dlgr/griduniverse/experiment.py b/dlgr/griduniverse/experiment.py index 36012847..ccfd5737 100644 --- a/dlgr/griduniverse/experiment.py +++ b/dlgr/griduniverse/experiment.py @@ -203,7 +203,6 @@ def __init__(self, **kwargs): self.visibility_ramp_time = kwargs.get("visibility_ramp_time", 4) self.background_animation = kwargs.get("background_animation", True) self.player_overlap = kwargs.get("player_overlap", False) - self.map_csv = kwargs.get("map_csv", None) # Motion self.motion_speed_limit = kwargs.get("motion_speed_limit", 8) @@ -510,19 +509,18 @@ def compute_payoffs(self): player.payoff *= inter_proportions[player.color_idx] player.payoff *= self.dollars_per_point - def load_map(self): - with open(self.map_csv) as csv_file: + def load_map(self, csv_file_path): + with open(csv_file_path) as csv_file: grid_state = self.csv_to_grid_state(csv_file) self.deserialize(grid_state) def csv_to_grid_state(self, csv_file): + from .csv_gridworlds import matrix2gridworld # avoid circular import + grid_state = {} reader = csv.reader(csv_file) - for i, row in enumerate(reader): - for j, col in enumerate(row): - # location = (i, j) - # Process each col value - continue + grid_matrix = list(reader) + grid_state = matrix2gridworld(grid_matrix) return grid_state def build_labyrinth(self): @@ -579,7 +577,7 @@ def deserialize(self, state): self.columns, ) ) - self.round = state["round"] + self.round = state.get("round", 0) # @@@ can't set donation_active because it's a property # self.donation_active = state['donation_active'] @@ -875,7 +873,7 @@ class Item: """ item_config: dict - id: int = field(default_factory=lambda: uuid.uuid4()) + id: int = field(default_factory=lambda: uuid.uuid4().int) creation_timestamp: float = field(default_factory=time.time) position: tuple = (0, 0) remaining_uses: int = field(default=None) @@ -1740,8 +1738,9 @@ def send_state_thread(self): def game_loop(self): """Update the world state.""" gevent.sleep(0.1) - if self.config.get("map_csv", None): - self.grid.load_map() + map_csv_path = self.config.get("map_csv", None) + if map_csv_path is not None: + self.grid.load_map(map_csv_path) elif not self.config.get("replay", False): self.grid.build_labyrinth() logger.info("Spawning items") diff --git a/test/conftest.py b/test/conftest.py index bcc17403..dc17169b 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -85,9 +85,14 @@ def stub_config(): } from dallinger.config import Configuration, default_keys + from dlgr.griduniverse.experiment import GU_PARAMS + config = Configuration() for key in default_keys: config.register(*key) + for key in GU_PARAMS.items(): + config.register(*key) + config.extend(defaults.copy()) # Patch load() so we don't update any key/value pairs from actual files: # (Note: this is blindly cargo-culted in from dallinger's equivalent fixture. diff --git a/test/test_griduniverse.py b/test/test_griduniverse.py index 9eb3559d..6fa9b18e 100755 --- a/test/test_griduniverse.py +++ b/test/test_griduniverse.py @@ -2,9 +2,9 @@ Tests for `dlgr.griduniverse` module. """ import collections +import csv import json import time -import uuid import mock import pytest @@ -48,7 +48,7 @@ def test_initialized_with_some_default_values(self, item_config): item = self.subject(item_config) assert isinstance(item.creation_timestamp, float) - assert isinstance(item.id, uuid.UUID) + assert isinstance(item.id, int) assert item.position == (0, 0) def test_instance_specific_values_can_be_specified(self, item_config): @@ -220,6 +220,65 @@ def test_loop_spawns_items(self, loop_exp_3x): [i["item_count"] for i in exp.item_config.values()] ) + def test_builds_grid_from_csv_if_specified(self, tmpdir, loop_exp_3x): + exp = loop_exp_3x + grid_config = [["w", "stone", "", "gooseberry_bush|3", "p1c2"]] + # Grid size must match incoming data, so update the gridworlds's existing + # settings: + exp.grid.rows = len(grid_config) + exp.grid.columns = len(grid_config[0]) + + csv_file = tmpdir.join("test_grid.csv") + + with csv_file.open(mode="w") as file: + writer = csv.writer(file) + writer.writerows(grid_config) + + # active_config.extend({"map_csv": csv_file.strpath}, strict=True) + exp.config.extend({"map_csv": csv_file.strpath}, strict=True) + + exp.game_loop() + + state = exp.grid.serialize() + + def relevant_keys(dictionary): + relevant = {"id", "item_id", "position", "remaining_uses", "color"} + return {k: v for k, v in dictionary.items() if k in relevant} + + # Ignore keys added by experiment execution we don't care about and/or + # which are non-deterministic (like player names): + state["items"] = [relevant_keys(item) for item in state["items"]] + state["players"] = [relevant_keys(player) for player in state["players"]] + + assert state == { + "columns": 5, + "donation_active": False, + "items": [ + { + "id": 1, + "item_id": "stone", + "position": [1, 0], + "remaining_uses": 1, + }, + { + "id": 2, + "item_id": "gooseberry_bush", + "position": [3, 0], + "remaining_uses": 3, + }, + ], + "players": [ + { + "color": "YELLOW", + "id": 1, + "position": [4, 0], + } + ], + "round": 0, + "rows": 1, + "walls": [[0, 0]], + } + def test_loop_serialized_and_saves(self, loop_exp_3x): # Grid serialized and added to DB session once per loop exp = loop_exp_3x diff --git a/test/test_gridworld.py b/test/test_gridworld.py index 1b2d4a2a..d8a33954 100644 --- a/test/test_gridworld.py +++ b/test/test_gridworld.py @@ -171,7 +171,7 @@ def subject(self): return matrix2gridworld - def test_transformation_one_row(self): + def test_one_row(self): csv = [["w", "stone", "", "gooseberry_bush|3", "p1c2"]] result = self.subject(csv) @@ -181,10 +181,12 @@ def test_transformation_one_row(self): "rows": 1, "items": [ { + "id": 1, "item_id": "stone", "position": [1, 0], }, { + "id": 2, "item_id": "gooseberry_bush", "position": [3, 0], "remaining_uses": 3, @@ -202,7 +204,7 @@ def test_transformation_one_row(self): ], } - def test_transformation_multirow(self): + def test_multirow(self): csv = [ ["w", "stone", "", "gooseberry_bush|3", "p1c1"], ["", "w", "", "", ""], @@ -223,20 +225,24 @@ def test_transformation_multirow(self): "rows": 10, "items": [ { + "id": 1, "item_id": "stone", "position": [1, 0], }, { + "id": 2, "item_id": "gooseberry_bush", "position": [3, 0], "remaining_uses": 3, }, { + "id": 3, "item_id": "gooseberry_bush", "position": [0, 6], "remaining_uses": 4, }, { + "id": 4, "item_id": "big_hard_rock", "position": [1, 7], }, @@ -278,14 +284,14 @@ def test_transformation_multirow(self): ], } - def test_transformation_empty_matrix(self): + def test_supports_empty_matrix(self): csv = [] result = self.subject(csv) assert result == {"rows": 0, "columns": 0} - def test_transformation_not_confused_by_whitepace(self): + def test_not_confused_by_whitepace(self): csv = [["w ", " stone", " ", " gooseberry_bush | 3 ", " p1c2"]] result = self.subject(csv) @@ -295,10 +301,12 @@ def test_transformation_not_confused_by_whitepace(self): "rows": 1, "items": [ { + "id": 1, "item_id": "stone", "position": [1, 0], }, { + "id": 2, "item_id": "gooseberry_bush", "position": [3, 0], "remaining_uses": 3, @@ -316,10 +324,25 @@ def test_transformation_not_confused_by_whitepace(self): ], } - def test_transformation_explains_invalid_player_colors(self): + def test_explains_invalid_player_colors(self): csv = [["p1c999"]] with pytest.raises(ValueError) as exc_info: self.subject(csv) assert exc_info.match("Invalid player color") + + def test_preserves_empty_edge_rows_and_columns(self): + csv = [ + ["", "", "", "", ""], + ["", "", "stone", "", ""], + ["", "", "", "", ""], + ] + + result = self.subject(csv) + + assert result == { + "columns": 5, + "items": [{"id": 1, "item_id": "stone", "position": [2, 1]}], + "rows": 3, + } From e24a6c8cdb8cbde4a705672b62b21a6bfd015ea5 Mon Sep 17 00:00:00 2001 From: Jesse Snyder Date: Fri, 13 Oct 2023 13:05:20 -0700 Subject: [PATCH 4/8] Simplify --- dlgr/griduniverse/experiment.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dlgr/griduniverse/experiment.py b/dlgr/griduniverse/experiment.py index ccfd5737..4d649144 100644 --- a/dlgr/griduniverse/experiment.py +++ b/dlgr/griduniverse/experiment.py @@ -517,10 +517,8 @@ def load_map(self, csv_file_path): def csv_to_grid_state(self, csv_file): from .csv_gridworlds import matrix2gridworld # avoid circular import - grid_state = {} reader = csv.reader(csv_file) - grid_matrix = list(reader) - grid_state = matrix2gridworld(grid_matrix) + grid_state = matrix2gridworld(list(reader)) return grid_state def build_labyrinth(self): From 4084c3a8d1cb2d3f701d7e65da68e4d60b7b5183 Mon Sep 17 00:00:00 2001 From: Alec Mitchell Date: Fri, 13 Oct 2023 12:47:27 -0700 Subject: [PATCH 5/8] Add test to ensure pre-populated players are linked to participant nodes. The current implementation could cause issues if the node ids don't exactly match the imported player ids (e.g. a player drops out of the waiting room?). Not sure if that could ever happen in the real world though. --- test/test_griduniverse.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/test_griduniverse.py b/test/test_griduniverse.py index 6fa9b18e..48ce6027 100755 --- a/test/test_griduniverse.py +++ b/test/test_griduniverse.py @@ -344,6 +344,16 @@ def test_handle_connect_adds_player_to_grid(self, exp, a): exp.handle_connect({"player_id": participant.id}) assert participant.id in exp.grid.players + def test_handle_connect_uses_existing_player_on_grid(self, exp, a): + participant = a.participant() + exp.grid.players[participant.id] = Player( + id=participant.id, color=[0.50, 0.86, 1.00], location=[10, 10] + ) + exp.handle_connect({"player_id": participant.id}) + assert participant.id in exp.node_by_player_id + assert len(exp.grid.players) == 1 + assert len(exp.node_by_player_id) == 1 + def test_handle_connect_is_noop_for_spectators(self, exp): exp.handle_connect({"player_id": "spectator"}) assert exp.node_by_player_id == {} From 03985d2feb9e093508a02bde402e29c1c321a1b4 Mon Sep 17 00:00:00 2001 From: Jesse Snyder Date: Fri, 13 Oct 2023 17:13:30 -0700 Subject: [PATCH 6/8] Fix x, y vs. y, x grid layout bug, and player ID's are strings, not ints --- dlgr/griduniverse/csv_gridworlds.py | 5 +- test/test_gridworld.py | 118 +++++++++------------------- 2 files changed, 40 insertions(+), 83 deletions(-) diff --git a/dlgr/griduniverse/csv_gridworlds.py b/dlgr/griduniverse/csv_gridworlds.py index d60efecc..c21442ba 100644 --- a/dlgr/griduniverse/csv_gridworlds.py +++ b/dlgr/griduniverse/csv_gridworlds.py @@ -21,7 +21,8 @@ def matrix2gridworld(matrix): for row_num, row in enumerate(matrix): for col_num, cell in enumerate(row): - position = [col_num, row_num] + # NB: we use [y, x] format in GU!! (╯°□°)╯︵ ┻━┻ + position = [row_num, col_num] cell = cell.strip() player_match = player_regex.match(cell) if not cell: @@ -31,7 +32,7 @@ def matrix2gridworld(matrix): result["walls"].append(position) elif player_match: id_str, color_str = player_match.groups() - player_id = int(id_str.replace("p", "")) + player_id = id_str.replace("p", "") player_data = { "id": player_id, "position": position, diff --git a/test/test_gridworld.py b/test/test_gridworld.py index d8a33954..093d9dc0 100644 --- a/test/test_gridworld.py +++ b/test/test_gridworld.py @@ -178,32 +178,35 @@ def test_one_row(self): assert result == { "columns": 5, - "rows": 1, "items": [ - { - "id": 1, - "item_id": "stone", - "position": [1, 0], - }, + {"id": 1, "item_id": "stone", "position": [0, 1]}, { "id": 2, "item_id": "gooseberry_bush", - "position": [3, 0], + "position": [0, 3], "remaining_uses": 3, }, ], - "walls": [ - [0, 0], - ], - "players": [ - { - "color": "YELLOW", - "position": [4, 0], - "id": 1, - } - ], + "players": [{"color": "YELLOW", "id": "1", "position": [0, 4]}], + "rows": 1, + "walls": [[0, 0]], } + def test_to_demonstrate_orientation(self): + csv = [ + ["top-left", "top-right"], + ["bottom-left", "bottom-right"], + ] + + result = self.subject(csv) + + assert result["items"] == [ + {"id": 1, "item_id": "top-left", "position": [0, 0]}, + {"id": 2, "item_id": "top-right", "position": [0, 1]}, + {"id": 3, "item_id": "bottom-left", "position": [1, 0]}, + {"id": 4, "item_id": "bottom-right", "position": [1, 1]}, + ] + def test_multirow(self): csv = [ ["w", "stone", "", "gooseberry_bush|3", "p1c1"], @@ -222,66 +225,31 @@ def test_multirow(self): assert result == { "columns": 5, - "rows": 10, "items": [ - { - "id": 1, - "item_id": "stone", - "position": [1, 0], - }, + {"id": 1, "item_id": "stone", "position": [0, 1]}, { "id": 2, "item_id": "gooseberry_bush", - "position": [3, 0], + "position": [0, 3], "remaining_uses": 3, }, { "id": 3, "item_id": "gooseberry_bush", - "position": [0, 6], + "position": [6, 0], "remaining_uses": 4, }, - { - "id": 4, - "item_id": "big_hard_rock", - "position": [1, 7], - }, - ], - "walls": [ - [0, 0], - [1, 1], - [2, 2], - [3, 3], - [4, 4], - [4, 5], + {"id": 4, "item_id": "big_hard_rock", "position": [7, 1]}, ], "players": [ - { - "color": "BLUE", - "position": [4, 0], - "id": 1, - }, - { - "color": "BLUE", - "position": [1, 2], - "id": 2, - }, - { - "color": "YELLOW", - "position": [3, 4], - "id": 3, - }, - { - "color": "YELLOW", - "position": [1, 8], - "id": 4, - }, - { - "color": "ORANGE", - "position": [2, 9], - "id": 5, - }, + {"color": "BLUE", "id": "1", "position": [0, 4]}, + {"color": "BLUE", "id": "2", "position": [2, 1]}, + {"color": "YELLOW", "id": "3", "position": [4, 3]}, + {"color": "YELLOW", "id": "4", "position": [8, 1]}, + {"color": "ORANGE", "id": "5", "position": [9, 2]}, ], + "rows": 10, + "walls": [[0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 4]], } def test_supports_empty_matrix(self): @@ -298,30 +266,18 @@ def test_not_confused_by_whitepace(self): assert result == { "columns": 5, - "rows": 1, "items": [ - { - "id": 1, - "item_id": "stone", - "position": [1, 0], - }, + {"id": 1, "item_id": "stone", "position": [0, 1]}, { "id": 2, "item_id": "gooseberry_bush", - "position": [3, 0], + "position": [0, 3], "remaining_uses": 3, }, ], - "walls": [ - [0, 0], - ], - "players": [ - { - "color": "YELLOW", - "position": [4, 0], - "id": 1, - } - ], + "players": [{"color": "YELLOW", "id": "1", "position": [0, 4]}], + "rows": 1, + "walls": [[0, 0]], } def test_explains_invalid_player_colors(self): @@ -343,6 +299,6 @@ def test_preserves_empty_edge_rows_and_columns(self): assert result == { "columns": 5, - "items": [{"id": 1, "item_id": "stone", "position": [2, 1]}], + "items": [{"id": 1, "item_id": "stone", "position": [1, 2]}], "rows": 3, } From d43d46525608657fb10f2ff75428259f53f5ebe1 Mon Sep 17 00:00:00 2001 From: Jesse Snyder Date: Fri, 13 Oct 2023 17:23:33 -0700 Subject: [PATCH 7/8] Missed a test fix --- test/test_griduniverse.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_griduniverse.py b/test/test_griduniverse.py index 48ce6027..6af39c79 100755 --- a/test/test_griduniverse.py +++ b/test/test_griduniverse.py @@ -257,21 +257,21 @@ def relevant_keys(dictionary): { "id": 1, "item_id": "stone", - "position": [1, 0], + "position": [0, 1], "remaining_uses": 1, }, { "id": 2, "item_id": "gooseberry_bush", - "position": [3, 0], + "position": [0, 3], "remaining_uses": 3, }, ], "players": [ { "color": "YELLOW", - "id": 1, - "position": [4, 0], + "id": "1", + "position": [0, 4], } ], "round": 0, From 2d67882bef5c259ee3f630868460f82c0aaa8714 Mon Sep 17 00:00:00 2001 From: Jesse Snyder Date: Wed, 6 Nov 2024 12:44:03 -0800 Subject: [PATCH 8/8] Add example grid with explanation as docstring --- dlgr/griduniverse/csv_gridworlds.py | 19 +++++++++++++++++++ package.json | 3 ++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/dlgr/griduniverse/csv_gridworlds.py b/dlgr/griduniverse/csv_gridworlds.py index c21442ba..b5cf66bf 100644 --- a/dlgr/griduniverse/csv_gridworlds.py +++ b/dlgr/griduniverse/csv_gridworlds.py @@ -10,6 +10,25 @@ def matrix2gridworld(matrix): """Transform a 2D matrix representing an initial grid state into the serialized format used by Gridworld. + + Example: + + +---------------+---------+--------------------+ + | w | stone | gooseberry_bush|3 | + | p1c1 | w | w | + | | | p3c2 | + | | p4c2 | | + | big_hard_rock | w | p2c1 | + +---------------+---------+--------------------+ + + Explanation: + + - "w": a wall + - "stone": item defined by item_id "stone" in game_config.yml + - "gooseberry_bush|3": similar to the above, with the added detail + that the item has 3 remaining uses + - "p2c1": player ID 2, who is on team (color) 1 + - Empty cells: empty space in the grid """ result = defaultdict(list) diff --git a/package.json b/package.json index 935e8859..ac54df54 100644 --- a/package.json +++ b/package.json @@ -50,5 +50,6 @@ "bugs": { "url": "https://github.com/Dallinger/Griduniverse/issues" }, - "homepage": "https://github.com/Dallinger/Griduniverse#readme" + "homepage": "https://github.com/Dallinger/Griduniverse#readme", + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" }