diff --git a/.gitignore b/.gitignore index 852ea75..c1ebc8a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,12 @@ build/ venv/ env/ +# IDE files +.vscode/ +.idea/ +*.sublime-project +*.sublime-workspace + # Test / tooling caches .pytest_cache/ .mypy_cache/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0f82088 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ogbench"] + path = ogbench + url = ./ogbench diff --git a/mazes/exp_maze_images/M1/10x10_corridor_kr_0.png b/mazes/exp_maze_images/M1/10x10_corridor_kr_0.png new file mode 100644 index 0000000..a460588 Binary files /dev/null and b/mazes/exp_maze_images/M1/10x10_corridor_kr_0.png differ diff --git a/mazes/exp_maze_images/README.md b/mazes/exp_maze_images/README.md new file mode 100644 index 0000000..a3ad0c0 --- /dev/null +++ b/mazes/exp_maze_images/README.md @@ -0,0 +1,60 @@ +# Experimental Maze Images + +This directory contains example visualizations of the experimental maze sets. Each folder represents a different configuration or scenario type. + +## Folder Overview + +### M1 - Multi-Agent/Mechanism Maze (Key Required) +- **Example**: `10x10_corridor_kr_0.png` +- **Characteristics**: Contains key-required mechanics (kr variant) +- **Sizes**: 8×8, 10×10, 14×14 +- **Layouts**: Corridor-like and dense arrangements +- **Purpose**: Tests agent navigation with interactive mechanisms (keys, doors, switches, etc.) + +### S1 - Empty Room Scenario +- **Example**: `8x8_empty_room_0.png` +- **Characteristics**: No walls, simple open space +- **Purpose**: Baseline test for agent movement in unrestricted environment + +### S2 - Simple Corridor (8×8) +- **Example**: `8x8_corridor_0.png` +- **Characteristics**: Straight corridors with walls +- **Size**: 8×8 +- **Purpose**: Tests navigation in simple linear layouts + +### S3 - Medium Corridor (10×10) +- **Example**: `10x10_corridor_0.png` +- **Characteristics**: More complex corridor layout with more variation +- **Size**: 10×10 +- **Purpose**: Tests navigation in moderately complex layouts + +### S4 - Dense Medium Maze (10×10) +- **Example**: `10x10_dense_0.png` +- **Characteristics**: High wall density, many branching paths +- **Size**: 10×10 +- **Purpose**: Tests pathfinding and decision-making in complex space + +### S5 - Large Corridor (14×14) +- **Example**: `14x14_corridor_0.png` +- **Characteristics**: Corridor-style layout at larger scale +- **Size**: 14×14 +- **Purpose**: Tests navigation in larger but structured environments + +### S6 - Dense Large Maze (14×14) +- **Example**: `14x14_dense_0.png` +- **Characteristics**: High wall density in large space, most complex +- **Size**: 14×14 +- **Purpose**: Tests pathfinding under maximum complexity + +## Key Differences + +| Category | M1 | S1-S6 | +|----------|----|----| +| **Mechanics** | Contains keys, doors, switches, gates | Simple movement + goal | +| **Interaction** | Requires mechanism solving | Direct pathfinding | +| **Complexity** | Variable (kr variants) | Structured progression | +| **Sizes** | Multi-scale (8-14×14) | Per-scenario (S1: 8×8, S2-S3: sizes vary, S4-S6: increase) | + +## Usage + +These example images correspond to JSON specifications in `../exp_maze_jsons/`. Each PNG is a rendered visualization of the corresponding maze layout. diff --git a/mazes/exp_maze_images/S1/8x8_empty_room_0.png b/mazes/exp_maze_images/S1/8x8_empty_room_0.png new file mode 100644 index 0000000..f2cd778 Binary files /dev/null and b/mazes/exp_maze_images/S1/8x8_empty_room_0.png differ diff --git a/mazes/exp_maze_images/S2/8x8_corridor_0.png b/mazes/exp_maze_images/S2/8x8_corridor_0.png new file mode 100644 index 0000000..da6dd39 Binary files /dev/null and b/mazes/exp_maze_images/S2/8x8_corridor_0.png differ diff --git a/mazes/exp_maze_images/S3/10x10_corridor_0.png b/mazes/exp_maze_images/S3/10x10_corridor_0.png new file mode 100644 index 0000000..8f08231 Binary files /dev/null and b/mazes/exp_maze_images/S3/10x10_corridor_0.png differ diff --git a/mazes/exp_maze_images/S4/10x10_dense_0.png b/mazes/exp_maze_images/S4/10x10_dense_0.png new file mode 100644 index 0000000..cec12d7 Binary files /dev/null and b/mazes/exp_maze_images/S4/10x10_dense_0.png differ diff --git a/mazes/exp_maze_images/S5/14x14_corridor_0.png b/mazes/exp_maze_images/S5/14x14_corridor_0.png new file mode 100644 index 0000000..0fe5c12 Binary files /dev/null and b/mazes/exp_maze_images/S5/14x14_corridor_0.png differ diff --git a/mazes/exp_maze_images/S6/14x14_dense_0.png b/mazes/exp_maze_images/S6/14x14_dense_0.png new file mode 100644 index 0000000..2a0677f Binary files /dev/null and b/mazes/exp_maze_images/S6/14x14_dense_0.png differ diff --git a/mazes/exp_maze_jsons/M1/10x10_corridor_kr_0.json b/mazes/exp_maze_jsons/M1/10x10_corridor_kr_0.json new file mode 100644 index 0000000..251c26a --- /dev/null +++ b/mazes/exp_maze_jsons/M1/10x10_corridor_kr_0.json @@ -0,0 +1,56 @@ +{ + "task_id": "10x10_corridor_kr_0", + "version": "1.0", + "seed": 0, + "difficulty_tier": 3, + "description": "10x10 corridor with turns and a single key-door mechanism.", + "maze": { + "dimensions": [10, 10], + "walls": [ + [1, 2], [2, 2], [3, 2], [4, 2], [5, 2], [6, 2], [7, 2], + [2, 4], [3, 4], [4, 4], [5, 4], [6, 4], [7, 4], [8, 4], + [1, 6], [2, 6], [3, 6], [4, 6], [5, 6], [6, 6], [7, 6], + [2, 8], [3, 8], [4, 8], [5, 8], [6, 8], [7, 8], [8, 8] + ], + "start": [1, 1], + "goal": [1, 8] + }, + "mechanisms": { + "keys": [ + { + "id": "kR", + "position": [3, 1], + "color": "red" + } + ], + "doors": [ + { + "id": "DR", + "position": [6, 3], + "color": "red", + "requires_key": "red", + "initial_state": "locked" + } + ], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [1, 8], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "single_key_door", + "tiling": "square", + "wall_topology": "winding" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/M1/10x10_corridor_kr_1.json b/mazes/exp_maze_jsons/M1/10x10_corridor_kr_1.json new file mode 100644 index 0000000..c2201ec --- /dev/null +++ b/mazes/exp_maze_jsons/M1/10x10_corridor_kr_1.json @@ -0,0 +1,60 @@ +{ + "task_id": "10x10_corridor_kr_1", + "version": "1.0", + "seed": 0, + "difficulty_tier": 3, + "description": "10x10 corridor with turns, a vertical layout, and a single key-door mechanism.", + "maze": { + "dimensions": [10, 10], + "walls": [ + [2, 1], [6, 1], + [2, 2], [4, 2], [6, 2], [8, 2], + [2, 3], [4, 3], [6, 3], [8, 3], + [2, 4], [4, 4], [6, 4], [8, 4], + [2, 5], [4, 5], [6, 5], [8, 5], + [2, 6], [4, 6], [6, 6], [8, 6], + [2, 7], [4, 7], [6, 7], [8, 7], + [4, 8], [8, 8] + ], + "start": [1, 1], + "goal": [8, 1] + }, + "mechanisms": { + "keys": [ + { + "id": "kR", + "position": [1, 3], + "color": "red" + } + ], + "doors": [ + { + "id": "DR", + "position": [1, 7], + "color": "red", + "requires_key": "red", + "initial_state": "locked" + } + ], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [8, 1], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "single_key_door", + "tiling": "square", + "wall_topology": "winding" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/M1/10x10_dense_kr_0.json b/mazes/exp_maze_jsons/M1/10x10_dense_kr_0.json new file mode 100644 index 0000000..956d2d7 --- /dev/null +++ b/mazes/exp_maze_jsons/M1/10x10_dense_kr_0.json @@ -0,0 +1,59 @@ +{ + "task_id": "10x10_dense_kr_0", + "version": "1.0", + "seed": 0, + "difficulty_tier": 4, + "description": "10x10 dense maze with dead ends and a single key-door mechanism.", + "maze": { + "dimensions": [10, 10], + "walls": [ + [2, 1], [8, 1], + [2, 2], [3, 2], [4, 2], [6, 2], [7, 2], [8, 2], + [4, 3], [8, 3], + [1, 4], [2, 4], [4, 4], [6, 4], [8, 4], + [2, 5], [4, 5], [6, 5], [8, 5], + [2, 6], [4, 6], [5, 6], [6, 6], [8, 6], + [1, 8], [2, 8], [3, 8], [4, 8], [5, 8], [6, 8], [7, 8] + ], + "start": [1, 1], + "goal": [7, 1] + }, + "mechanisms": { + "keys": [ + { + "id": "kR", + "position": [1, 5], + "color": "red" + } + ], + "doors": [ + { + "id": "DR", + "position": [4, 7], + "color": "red", + "requires_key": "red", + "initial_state": "locked" + } + ], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [7, 1], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "single_key_door", + "tiling": "square", + "wall_topology": "dense_dead_ends" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/M1/10x10_dense_kr_1.json b/mazes/exp_maze_jsons/M1/10x10_dense_kr_1.json new file mode 100644 index 0000000..1006779 --- /dev/null +++ b/mazes/exp_maze_jsons/M1/10x10_dense_kr_1.json @@ -0,0 +1,60 @@ +{ + "task_id": "10x10_dense_kr_1", + "version": "1.0", + "seed": 0, + "difficulty_tier": 4, + "description": "10x10 dense maze with dead ends and a single key-door mechanism.", + "maze": { + "dimensions": [10, 10], + "walls": [ + [3, 1], + [1, 2], [3, 2], [4, 2], [5, 2], [7, 2], [8, 2], + [1, 3], [4, 3], [8, 3], + [1, 4], [2, 4], [4, 4], [6, 4], [8, 4], + [4, 5], [6, 5], + [1, 6], [3, 6], [4, 6], [6, 6], [7, 6], [8, 6], + [1, 7], + [1, 8], [2, 8], [3, 8], [4, 8], [6, 8], [7, 8] + ], + "start": [1, 1], + "goal": [8, 1] + }, + "mechanisms": { + "keys": [ + { + "id": "kR", + "position": [1, 5], + "color": "red" + } + ], + "doors": [ + { + "id": "DR", + "position": [2, 6], + "color": "red", + "requires_key": "red", + "initial_state": "locked" + } + ], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [8, 1], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "single_key_door", + "tiling": "square", + "wall_topology": "dense_dead_ends" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/M1/14x14_corridor_kr_0.json b/mazes/exp_maze_jsons/M1/14x14_corridor_kr_0.json new file mode 100644 index 0000000..6ee5754 --- /dev/null +++ b/mazes/exp_maze_jsons/M1/14x14_corridor_kr_0.json @@ -0,0 +1,58 @@ +{ + "task_id": "14x14_corridor_kr_0", + "version": "1.0", + "seed": 0, + "difficulty_tier": 5, + "description": "14x14 corridor with turns and a single key-door mechanism.", + "maze": { + "dimensions": [14, 14], + "walls": [ + [2, 2], [3, 2], [4, 2], [5, 2], [6, 2], [7, 2], [8, 2], [9, 2], [10, 2], [11, 2], [12, 2], + [1, 4], [2, 4], [3, 4], [4, 4], [5, 4], [6, 4], [7, 4], [8, 4], [9, 4], [10, 4], [11, 4], + [2, 6], [3, 6], [4, 6], [5, 6], [6, 6], [7, 6], [8, 6], [9, 6], [10, 6], [11, 6], [12, 6], + [1, 8], [2, 8], [3, 8], [4, 8], [5, 8], [6, 8], [7, 8], [8, 8], [9, 8], [10, 8], [11, 8], + [2, 10], [3, 10], [4, 10], [5, 10], [6, 10], [7, 10], [8, 10], [9, 10], [10, 10], [11, 10], [12, 10], + [1, 12], [2, 12], [3, 12], [4, 12], [5, 12], [6, 12], [7, 12], [8, 12], [9, 12], [10, 12], [11, 12] + ], + "start": [1, 1], + "goal": [12, 12] + }, + "mechanisms": { + "keys": [ + { + "id": "kR", + "position": [3, 1], + "color": "red" + } + ], + "doors": [ + { + "id": "DR", + "position": [7, 7], + "color": "red", + "requires_key": "red", + "initial_state": "locked" + } + ], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [12, 12], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "single_key_door", + "tiling": "square", + "wall_topology": "winding" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/M1/14x14_corridor_kr_1.json b/mazes/exp_maze_jsons/M1/14x14_corridor_kr_1.json new file mode 100644 index 0000000..d7fae66 --- /dev/null +++ b/mazes/exp_maze_jsons/M1/14x14_corridor_kr_1.json @@ -0,0 +1,64 @@ +{ + "task_id": "14x14_corridor_kr_1", + "version": "1.0", + "seed": 0, + "difficulty_tier": 5, + "description": "14x14 corridor with turns, a vertical layout, and a single key-door mechanism.", + "maze": { + "dimensions": [14, 14], + "walls": [ + [4, 1], [8, 1], [12, 1], + [2, 2], [4, 2], [6, 2], [8, 2], [10, 2], [12, 2], + [2, 3], [4, 3], [6, 3], [8, 3], [10, 3], [12, 3], + [2, 4], [4, 4], [6, 4], [8, 4], [10, 4], [12, 4], + [2, 5], [4, 5], [6, 5], [8, 5], [10, 5], [12, 5], + [2, 6], [4, 6], [6, 6], [8, 6], [10, 6], [12, 6], + [2, 7], [4, 7], [6, 7], [8, 7], [10, 7], [12, 7], + [2, 8], [4, 8], [6, 8], [8, 8], [10, 8], [12, 8], + [2, 9], [4, 9], [6, 9], [8, 9], [10, 9], [12, 9], + [2, 10], [4, 10], [6, 10], [8, 10], [10, 10], [12, 10], + [2, 11], [4, 11], [6, 11], [8, 11], [10, 11], [12, 11], + [2, 12], [6, 12], [10, 12] + ], + "start": [1, 12], + "goal": [12, 12] + }, + "mechanisms": { + "keys": [ + { + "id": "kR", + "position": [1, 1], + "color": "red" + } + ], + "doors": [ + { + "id": "DR", + "position": [3, 11], + "color": "red", + "requires_key": "red", + "initial_state": "locked" + } + ], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [12, 12], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "single_key_door", + "tiling": "square", + "wall_topology": "winding" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/M1/14x14_dense_kr_0.json b/mazes/exp_maze_jsons/M1/14x14_dense_kr_0.json new file mode 100644 index 0000000..173aa69 --- /dev/null +++ b/mazes/exp_maze_jsons/M1/14x14_dense_kr_0.json @@ -0,0 +1,64 @@ +{ + "task_id": "14x14_dense_kr_0", + "version": "1.0", + "seed": 0, + "difficulty_tier": 6, + "description": "14x14 dense maze with dead ends and a single key-door mechanism.", + "maze": { + "dimensions": [14, 14], + "walls": [ + [4, 1], [12, 1], + [1, 2], [2, 2], [4, 2], [5, 2], [6, 2], [7, 2], [8, 2], [9, 2], [10, 2], [12, 2], + [2, 3], [10, 3], [12, 3], + [2, 4], [3, 4], [4, 4], [5, 4], [6, 4], [7, 4], [8, 4], [10, 4], [12, 4], + [6, 5], [10, 5], [12, 5], + [2, 6], [4, 6], [5, 6], [6, 6], [8, 6], [9, 6], [10, 6], [12, 6], + [2, 7], [8, 7], [12, 7], + [2, 8], [3, 8], [4, 8], [5, 8], [6, 8], [7, 8], [8, 8], [9, 8], [10, 8], [12, 8], + [4, 9], [12, 9], + [1, 10], [2, 10], [4, 10], [5, 10], [6, 10], [8, 10], [12, 10], + [8, 11], + [1, 12], [2, 12], [3, 12], [4, 12], [5, 12], [6, 12], [7, 12], [8, 12], [9, 12], [10, 12], [11, 12] + ], + "start": [1, 1], + "goal": [5, 1] + }, + "mechanisms": { + "keys": [ + { + "id": "kR", + "position": [5, 5], + "color": "red" + } + ], + "doors": [ + { + "id": "DR", + "position": [1, 7], + "color": "red", + "requires_key": "red", + "initial_state": "locked" + } + ], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [5, 1], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "single_key_door", + "tiling": "square", + "wall_topology": "dense_dead_ends" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/M1/14x14_dense_kr_1.json b/mazes/exp_maze_jsons/M1/14x14_dense_kr_1.json new file mode 100644 index 0000000..17093aa --- /dev/null +++ b/mazes/exp_maze_jsons/M1/14x14_dense_kr_1.json @@ -0,0 +1,64 @@ +{ + "task_id": "14x14_dense_kr_1", + "version": "1.0", + "seed": 0, + "difficulty_tier": 6, + "description": "14x14 dense maze with dead ends and a single key-door mechanism.", + "maze": { + "dimensions": [14, 14], + "walls": [ + [3, 1], + [1, 2], [3, 2], [4, 2], [5, 2], [7, 2], [8, 2], [9, 2], [11, 2], [12, 2], + [1, 3], [5, 3], [9, 3], [12, 3], + [1, 4], [2, 4], [3, 4], [5, 4], [6, 4], [7, 4], [9, 4], [10, 4], [12, 4], + [3, 5], [7, 5], [10, 5], [12, 5], + [1, 6], [3, 6], [4, 6], [5, 6], [7, 6], [8, 6], [10, 6], [12, 6], + [1, 7], [8, 7], [12, 7], + [1, 8], [2, 8], [3, 8], [4, 8], [5, 8], [7, 8], [8, 8], [9, 8], [10, 8], [12, 8], + [3, 9], [10, 9], [12, 9], + [1, 10], [3, 10], [4, 10], [5, 10], [7, 10], [8, 10], [10, 10], [12, 10], + [1, 11], [7, 11], + [1, 12], [2, 12], [3, 12], [4, 12], [5, 12], [6, 12], [7, 12], [8, 12], [9, 12], [10, 12], [11, 12] + ], + "start": [1, 1], + "goal": [12, 1] + }, + "mechanisms": { + "keys": [ + { + "id": "kR", + "position": [1, 5], + "color": "red" + } + ], + "doors": [ + { + "id": "DR", + "position": [6, 8], + "color": "red", + "requires_key": "red", + "initial_state": "locked" + } + ], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [12, 1], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "single_key_door", + "tiling": "square", + "wall_topology": "dense_dead_ends" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/M1/8x8_corridor_kr_0.json b/mazes/exp_maze_jsons/M1/8x8_corridor_kr_0.json new file mode 100644 index 0000000..cbe7ce3 --- /dev/null +++ b/mazes/exp_maze_jsons/M1/8x8_corridor_kr_0.json @@ -0,0 +1,55 @@ +{ + "task_id": "8x8_corridor_kr_0", + "version": "1.0", + "seed": 0, + "difficulty_tier": 3, + "description": "8x8 corridor with turns and a key-door mechanism.", + "maze": { + "dimensions": [8, 8], + "walls": [ + [1, 2], [2, 2], [3, 2], [4, 2], [5, 2], + [2, 4], [3, 4], [4, 4], [5, 4], [6, 4], + [1, 6], [2, 6], [3, 6], [4, 6], [5, 6] + ], + "start": [1, 1], + "goal": [6, 6] + }, + "mechanisms": { + "keys": [ + { + "id": "kR", + "position": [3, 1], + "color": "red" + } + ], + "doors": [ + { + "id": "DR", + "position": [3, 3], + "color": "red", + "requires_key": "red", + "initial_state": "locked" + } + ], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [6, 6], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "key_door", + "tiling": "square", + "wall_topology": "winding" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/M1/8x8_corridor_kr_1.json b/mazes/exp_maze_jsons/M1/8x8_corridor_kr_1.json new file mode 100644 index 0000000..c06bb7a --- /dev/null +++ b/mazes/exp_maze_jsons/M1/8x8_corridor_kr_1.json @@ -0,0 +1,58 @@ +{ + "task_id": "8x8_corridor_kr_1", + "version": "1.0", + "seed": 0, + "difficulty_tier": 3, + "description": "8x8 corridor with turns and a key-door mechanism.", + "maze": { + "dimensions": [8, 8], + "walls": [ + [2, 1], [6, 1], + [2, 2], [4, 2], [6, 2], + [2, 3], [4, 3], [6, 3], + [2, 4], [4, 4], [6, 4], + [2, 5], [4, 5], [6, 5], + [4, 6] + ], + "start": [1, 1], + "goal": [6, 6] + }, + "mechanisms": { + "keys": [ + { + "id": "kR", + "position": [1, 3], + "color": "red" + } + ], + "doors": [ + { + "id": "DR", + "position": [2, 6], + "color": "red", + "requires_key": "red", + "initial_state": "locked" + } + ], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [6, 6], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "key_door", + "tiling": "square", + "wall_topology": "winding" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/S1/8x8_empty_room_0.json b/mazes/exp_maze_jsons/S1/8x8_empty_room_0.json new file mode 100644 index 0000000..19ecce3 --- /dev/null +++ b/mazes/exp_maze_jsons/S1/8x8_empty_room_0.json @@ -0,0 +1,37 @@ +{ + "task_id": "8x8_empty_room_0", + "version": "1.0", + "seed": 0, + "difficulty_tier": 1, + "description": "8x8 empty room with implicit perimeter walls.", + "maze": { + "dimensions": [8, 8], + "walls": [], + "start": [1, 1], + "goal": [6, 6] + }, + "mechanisms": { + "keys": [], + "doors": [], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [6, 6], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "none", + "tiling": "square", + "wall_topology": "open" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/S1/8x8_empty_room_1.json b/mazes/exp_maze_jsons/S1/8x8_empty_room_1.json new file mode 100644 index 0000000..39d2548 --- /dev/null +++ b/mazes/exp_maze_jsons/S1/8x8_empty_room_1.json @@ -0,0 +1,37 @@ +{ + "task_id": "8x8_empty_room_1", + "version": "1.0", + "seed": 0, + "difficulty_tier": 1, + "description": "8x8 empty room with implicit perimeter walls.", + "maze": { + "dimensions": [8, 8], + "walls": [], + "start": [1, 1], + "goal": [4, 6] + }, + "mechanisms": { + "keys": [], + "doors": [], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [4, 6], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "none", + "tiling": "square", + "wall_topology": "open" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/S2/8x8_corridor_0.json b/mazes/exp_maze_jsons/S2/8x8_corridor_0.json new file mode 100644 index 0000000..49109d9 --- /dev/null +++ b/mazes/exp_maze_jsons/S2/8x8_corridor_0.json @@ -0,0 +1,41 @@ +{ + "task_id": "8x8_corridor_0", + "version": "1.0", + "seed": 0, + "difficulty_tier": 2, + "description": "8x8 corridor with turns.", + "maze": { + "dimensions": [8, 8], + "walls": [ + [1, 2], [2, 2], [3, 2], [4, 2], [5, 2], + [2, 4], [3, 4], [4, 4], [5, 4], [6, 4], + [1, 6], [2, 6], [3, 6], [4, 6], [5, 6] + ], + "start": [1, 1], + "goal": [6, 6] + }, + "mechanisms": { + "keys": [], + "doors": [], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [6, 6], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "none", + "tiling": "square", + "wall_topology": "winding" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/S2/8x8_corridor_1.json b/mazes/exp_maze_jsons/S2/8x8_corridor_1.json new file mode 100644 index 0000000..bcfe417 --- /dev/null +++ b/mazes/exp_maze_jsons/S2/8x8_corridor_1.json @@ -0,0 +1,44 @@ +{ + "task_id": "8x8_corridor_1", + "version": "1.0", + "seed": 0, + "difficulty_tier": 2, + "description": "8x8 corridor with turns and a vertical layout.", + "maze": { + "dimensions": [8, 8], + "walls": [ + [2, 1], [6, 1], + [2, 2], [4, 2], [6, 2], + [2, 3], [4, 3], [6, 3], + [2, 4], [4, 4], [6, 4], + [2, 5], [4, 5], [6, 5], + [4, 6] + ], + "start": [1, 1], + "goal": [6, 6] + }, + "mechanisms": { + "keys": [], + "doors": [], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [6, 6], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "none", + "tiling": "square", + "wall_topology": "winding" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/S3/10x10_corridor_0.json b/mazes/exp_maze_jsons/S3/10x10_corridor_0.json new file mode 100644 index 0000000..d151ee4 --- /dev/null +++ b/mazes/exp_maze_jsons/S3/10x10_corridor_0.json @@ -0,0 +1,42 @@ +{ + "task_id": "10x10_corridor_0", + "version": "1.0", + "seed": 0, + "difficulty_tier": 3, + "description": "10x10 corridor with turns.", + "maze": { + "dimensions": [10, 10], + "walls": [ + [1, 2], [2, 2], [3, 2], [4, 2], [5, 2], [6, 2], [7, 2], + [2, 4], [3, 4], [4, 4], [5, 4], [6, 4], [7, 4], [8, 4], + [1, 6], [2, 6], [3, 6], [4, 6], [5, 6], [6, 6], [7, 6], + [2, 8], [3, 8], [4, 8], [5, 8], [6, 8], [7, 8], [8, 8] + ], + "start": [1, 1], + "goal": [1, 8] + }, + "mechanisms": { + "keys": [], + "doors": [], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [1, 8], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "none", + "tiling": "square", + "wall_topology": "winding" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/S3/10x10_corridor_1.json b/mazes/exp_maze_jsons/S3/10x10_corridor_1.json new file mode 100644 index 0000000..fe30f0f --- /dev/null +++ b/mazes/exp_maze_jsons/S3/10x10_corridor_1.json @@ -0,0 +1,46 @@ +{ + "task_id": "10x10_corridor_1", + "version": "1.0", + "seed": 0, + "difficulty_tier": 3, + "description": "10x10 corridor with turns and a vertical layout.", + "maze": { + "dimensions": [10, 10], + "walls": [ + [2, 1], [6, 1], + [2, 2], [4, 2], [6, 2], [8, 2], + [2, 3], [4, 3], [6, 3], [8, 3], + [2, 4], [4, 4], [6, 4], [8, 4], + [2, 5], [4, 5], [6, 5], [8, 5], + [2, 6], [4, 6], [6, 6], [8, 6], + [2, 7], [4, 7], [6, 7], [8, 7], + [4, 8], [8, 8] + ], + "start": [1, 1], + "goal": [8, 1] + }, + "mechanisms": { + "keys": [], + "doors": [], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [8, 1], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "none", + "tiling": "square", + "wall_topology": "winding" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/S4/10x10_dense_0.json b/mazes/exp_maze_jsons/S4/10x10_dense_0.json new file mode 100644 index 0000000..a172f7c --- /dev/null +++ b/mazes/exp_maze_jsons/S4/10x10_dense_0.json @@ -0,0 +1,45 @@ +{ + "task_id": "10x10_dense_0", + "version": "1.0", + "seed": 0, + "difficulty_tier": 4, + "description": "10x10 dense maze with dead ends.", + "maze": { + "dimensions": [10, 10], + "walls": [ + [2, 1], [8, 1], + [2, 2], [3, 2], [4, 2], [6, 2], [7, 2], [8, 2], + [4, 3], [8, 3], + [1, 4], [2, 4], [4, 4], [6, 4], [8, 4], + [2, 5], [4, 5], [6, 5], [8, 5], + [2, 6], [4, 6], [5, 6], [6, 6], [8, 6], + [1, 8], [2, 8], [3, 8], [4, 8], [5, 8], [6, 8], [7, 8] + ], + "start": [1, 1], + "goal": [7, 1] + }, + "mechanisms": { + "keys": [], + "doors": [], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [7, 1], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "none", + "tiling": "square", + "wall_topology": "dense_dead_ends" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/S4/10x10_dense_1.json b/mazes/exp_maze_jsons/S4/10x10_dense_1.json new file mode 100644 index 0000000..4c6be73 --- /dev/null +++ b/mazes/exp_maze_jsons/S4/10x10_dense_1.json @@ -0,0 +1,46 @@ +{ + "task_id": "10x10_dense_1", + "version": "1.0", + "seed": 0, + "difficulty_tier": 4, + "description": "10x10 dense maze variant with dead ends.", + "maze": { + "dimensions": [10, 10], + "walls": [ + [3, 1], + [1, 2], [3, 2], [4, 2], [5, 2], [7, 2], [8, 2], + [1, 3], [4, 3], [8, 3], + [1, 4], [2, 4], [4, 4], [6, 4], [8, 4], + [4, 5], [6, 5], + [1, 6], [3, 6], [4, 6], [6, 6], [7, 6], [8, 6], + [1, 7], + [1, 8], [2, 8], [3, 8], [4, 8], [6, 8], [7, 8] + ], + "start": [1, 1], + "goal": [8, 1] + }, + "mechanisms": { + "keys": [], + "doors": [], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [8, 1], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "none", + "tiling": "square", + "wall_topology": "dense_dead_ends" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/S5/14x14_corridor_0.json b/mazes/exp_maze_jsons/S5/14x14_corridor_0.json new file mode 100644 index 0000000..ed05176 --- /dev/null +++ b/mazes/exp_maze_jsons/S5/14x14_corridor_0.json @@ -0,0 +1,44 @@ +{ + "task_id": "14x14_corridor_0", + "version": "1.0", + "seed": 0, + "difficulty_tier": 5, + "description": "14x14 corridor with turns.", + "maze": { + "dimensions": [14, 14], + "walls": [ + [2, 2], [3, 2], [4, 2], [5, 2], [6, 2], [7, 2], [8, 2], [9, 2], [10, 2], [11, 2], [12, 2], + [1, 4], [2, 4], [3, 4], [4, 4], [5, 4], [6, 4], [7, 4], [8, 4], [9, 4], [10, 4], [11, 4], + [2, 6], [3, 6], [4, 6], [5, 6], [6, 6], [7, 6], [8, 6], [9, 6], [10, 6], [11, 6], [12, 6], + [1, 8], [2, 8], [3, 8], [4, 8], [5, 8], [6, 8], [7, 8], [8, 8], [9, 8], [10, 8], [11, 8], + [2, 10], [3, 10], [4, 10], [5, 10], [6, 10], [7, 10], [8, 10], [9, 10], [10, 10], [11, 10], [12, 10], + [1, 12], [2, 12], [3, 12], [4, 12], [5, 12], [6, 12], [7, 12], [8, 12], [9, 12], [10, 12], [11, 12] + ], + "start": [1, 1], + "goal": [12, 12] + }, + "mechanisms": { + "keys": [], + "doors": [], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [12, 12], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "none", + "tiling": "square", + "wall_topology": "winding" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/S5/14x14_corridor_1.json b/mazes/exp_maze_jsons/S5/14x14_corridor_1.json new file mode 100644 index 0000000..0e221f5 --- /dev/null +++ b/mazes/exp_maze_jsons/S5/14x14_corridor_1.json @@ -0,0 +1,50 @@ +{ + "task_id": "14x14_corridor_1", + "version": "1.0", + "seed": 0, + "difficulty_tier": 5, + "description": "14x14 vertical corridor with turns.", + "maze": { + "dimensions": [14, 14], + "walls": [ + [4, 1], [8, 1], [12, 1], + [2, 2], [4, 2], [6, 2], [8, 2], [10, 2], [12, 2], + [2, 3], [4, 3], [6, 3], [8, 3], [10, 3], [12, 3], + [2, 4], [4, 4], [6, 4], [8, 4], [10, 4], [12, 4], + [2, 5], [4, 5], [6, 5], [8, 5], [10, 5], [12, 5], + [2, 6], [4, 6], [6, 6], [8, 6], [10, 6], [12, 6], + [2, 7], [4, 7], [6, 7], [8, 7], [10, 7], [12, 7], + [2, 8], [4, 8], [6, 8], [8, 8], [10, 8], [12, 8], + [2, 9], [4, 9], [6, 9], [8, 9], [10, 9], [12, 9], + [2, 10], [4, 10], [6, 10], [8, 10], [10, 10], [12, 10], + [2, 11], [4, 11], [6, 11], [8, 11], [10, 11], [12, 11], + [2, 12], [6, 12], [10, 12] + ], + "start": [1, 12], + "goal": [12, 12] + }, + "mechanisms": { + "keys": [], + "doors": [], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [12, 12], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "none", + "tiling": "square", + "wall_topology": "winding" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/S6/14x14_dense_0.json b/mazes/exp_maze_jsons/S6/14x14_dense_0.json new file mode 100644 index 0000000..9421c2c --- /dev/null +++ b/mazes/exp_maze_jsons/S6/14x14_dense_0.json @@ -0,0 +1,50 @@ +{ + "task_id": "14x14_dense_0", + "version": "1.0", + "seed": 0, + "difficulty_tier": 6, + "description": "14x14 dense maze with many walls and dead ends.", + "maze": { + "dimensions": [14, 14], + "walls": [ + [4, 1], [12, 1], + [1, 2], [2, 2], [4, 2], [5, 2], [6, 2], [7, 2], [8, 2], [9, 2], [10, 2], [12, 2], + [2, 3], [10, 3], [12, 3], + [2, 4], [3, 4], [4, 4], [5, 4], [6, 4], [7, 4], [8, 4], [10, 4], [12, 4], + [6, 5], [10, 5], [12, 5], + [2, 6], [4, 6], [5, 6], [6, 6], [8, 6], [9, 6], [10, 6], [12, 6], + [2, 7], [8, 7], [12, 7], + [2, 8], [3, 8], [4, 8], [5, 8], [6, 8], [7, 8], [8, 8], [9, 8], [10, 8], [12, 8], + [4, 9], [12, 9], + [1, 10], [2, 10], [4, 10], [5, 10], [6, 10], [8, 10], [12, 10], + [8, 11], + [1, 12], [2, 12], [3, 12], [4, 12], [5, 12], [6, 12], [7, 12], [8, 12], [9, 12], [10, 12], [11, 12] + ], + "start": [1, 1], + "goal": [5, 1] + }, + "mechanisms": { + "keys": [], + "doors": [], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [5, 1], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "none", + "tiling": "square", + "wall_topology": "dense_dead_ends" + }, + "max_steps": 1000 +} diff --git a/mazes/exp_maze_jsons/S6/14x14_dense_1.json b/mazes/exp_maze_jsons/S6/14x14_dense_1.json new file mode 100644 index 0000000..f218b56 --- /dev/null +++ b/mazes/exp_maze_jsons/S6/14x14_dense_1.json @@ -0,0 +1,50 @@ +{ + "task_id": "14x14_dense_1", + "version": "1.0", + "seed": 0, + "difficulty_tier": 6, + "description": "14x14 dense maze variant with many walls and dead ends.", + "maze": { + "dimensions": [14, 14], + "walls": [ + [3, 1], + [1, 2], [3, 2], [4, 2], [5, 2], [7, 2], [8, 2], [9, 2], [11, 2], [12, 2], + [1, 3], [5, 3], [9, 3], [12, 3], + [1, 4], [2, 4], [3, 4], [5, 4], [6, 4], [7, 4], [9, 4], [10, 4], [12, 4], + [3, 5], [7, 5], [10, 5], [12, 5], + [1, 6], [3, 6], [4, 6], [5, 6], [7, 6], [8, 6], [10, 6], [12, 6], + [1, 7], [8, 7], [12, 7], + [1, 8], [2, 8], [3, 8], [4, 8], [5, 8], [7, 8], [8, 8], [9, 8], [10, 8], [12, 8], + [3, 9], [10, 9], [12, 9], + [1, 10], [3, 10], [4, 10], [5, 10], [7, 10], [8, 10], [10, 10], [12, 10], + [1, 11], [7, 11], + [1, 12], [2, 12], [3, 12], [4, 12], [5, 12], [6, 12], [7, 12], [8, 12], [9, 12], [10, 12], [11, 12] + ], + "start": [1, 1], + "goal": [12, 1] + }, + "mechanisms": { + "keys": [], + "doors": [], + "switches": [], + "gates": [] + }, + "rules": { + "key_consumption": true, + "switch_type": "toggle", + "hidden_mechanisms": [], + "observability": "full", + "view_size": 7 + }, + "goal": { + "type": "reach_position", + "target": [12, 1], + "auxiliary_conditions": [] + }, + "metadata": { + "chain_pattern": "none", + "tiling": "square", + "wall_topology": "dense_dead_ends" + }, + "max_steps": 1000 +} diff --git a/ogbench b/ogbench new file mode 160000 index 0000000..84b5770 --- /dev/null +++ b/ogbench @@ -0,0 +1 @@ +Subproject commit 84b5770c8fba35d13a2693180b9f524980c3b2fc diff --git a/tests/BFS_solver.py b/tests/BFS_solver.py new file mode 100644 index 0000000..74b55a6 --- /dev/null +++ b/tests/BFS_solver.py @@ -0,0 +1,85 @@ +"""BFS test helper for experimental maze JSON specs. + +This module preserves the legacy ``solve``/``find_all_paths`` API used by the +maze tests while delegating planning to the gridworld baseline BFS solver. +""" + +from __future__ import annotations + +from gridworld.actions import MiniGridActions +from gridworld.baselines import TaskPlanningContext, _shortest_plan, _successors +from gridworld.task_spec import TaskSpecification + + +def _to_task_spec(spec: dict | TaskSpecification) -> TaskSpecification: + if isinstance(spec, TaskSpecification): + return spec + return TaskSpecification.from_dict(spec) + + +def _interaction_label(label: str) -> str | None: + if label.startswith("pickup:"): + return label + if label.startswith("open_door:"): + return f"open:{label.split(':', 1)[1]}" + if label.startswith("toggle:"): + return label + return None + + +def solve(spec): + """Return a shortest path result for a maze JSON spec.""" + task_spec = _to_task_spec(spec) + ctx = TaskPlanningContext(task_spec) + start_state = ctx.initial_state() + actions, final_state = _shortest_plan( + ctx, + start_state, + lambda state: state.agent_pos == ctx.goal, + ) + + if final_state is None: + return { + "is_solvable": False, + "path": [], + "interactions": [], + "optimal_cost": None, + } + + state = start_state + path = [state.agent_pos] + interactions = [] + include_pickup_positions = ctx.goal in ctx.doors_by_pos + + for action in actions: + transition = next( + candidate + for candidate in _successors(ctx, state) + if candidate.action == action + ) + label = _interaction_label(transition.label) + if label is not None: + interactions.append(label) + if include_pickup_positions and transition.label.startswith("pickup:"): + key_id = transition.label.split(":", 1)[1] + key_pos = ctx.keys_by_id[key_id]["position"] + if path[-1] != key_pos: + path.append(key_pos) + state = transition.next_state + if action == int(MiniGridActions.MOVE_FORWARD): + path.append(state.agent_pos) + + return { + "is_solvable": True, + "path": path, + "interactions": interactions, + "optimal_cost": len(path) - 1, + } + + +def find_all_paths(spec): + """Return the BFS solver path in the legacy list-of-paths test-helper shape.""" + result = solve(spec) + if not result["is_solvable"]: + return [] + return [result["path"]] diff --git a/tests/maze_test_utils.py b/tests/maze_test_utils.py new file mode 100644 index 0000000..74f859a --- /dev/null +++ b/tests/maze_test_utils.py @@ -0,0 +1,131 @@ +import json +from pathlib import Path + +from BFS_solver import solve + + +MAZE_JSON_DIR = Path(__file__).resolve().parent.parent / 'mazes' / 'exp_maze_jsons' +MECHANISM_KEYS = ('keys', 'doors', 'switches', 'gates') + + +def load_maze_specs(maze_type, *, include_file_name=False): + specs = [] + for path in sorted((MAZE_JSON_DIR / maze_type).glob('*.json')): + spec = json.loads(path.read_text(encoding='utf-8')) + if include_file_name: + specs.append((path.name, spec)) + else: + specs.append(spec) + return specs + + +def assert_navigation_contract(test_case, spec): + maze = spec['maze'] + width, height = maze['dimensions'] + start = maze['start'] + goal = maze['goal'] + walls = {tuple(wall) for wall in maze['walls']} + + test_case.assertEqual(spec['goal']['type'], 'reach_position') + if spec['goal'].get('target') is not None: + test_case.assertEqual(spec['goal']['target'], goal) + test_case.assertEqual(len(start), 2) + test_case.assertEqual(len(goal), 2) + test_case.assertNotEqual(start, goal) + for label, point in (('start', start), ('goal', goal)): + x, y = point + test_case.assertGreaterEqual(x, 0, label) + test_case.assertLess(x, width, label) + test_case.assertGreaterEqual(y, 0, label) + test_case.assertLess(y, height, label) + test_case.assertNotIn(tuple(point), walls, label) + x, y = point + test_case.assertGreaterEqual(x, 0, label) + test_case.assertLess(x, width, label) + test_case.assertGreaterEqual(y, 0, label) + test_case.assertLess(y, height, label) + test_case.assertNotIn(tuple(point), walls, label) + + +def assert_goal_target_matches_maze_goal(test_case, spec): + test_case.assertEqual(spec['goal']['target'], spec['maze']['goal']) + + +def assert_bfs_solver_finds_path_to_goal(test_case, spec): + result = solve(spec) + test_case.assertTrue(result['is_solvable']) + test_case.assertEqual(result['path'][0], tuple(spec['maze']['start'])) + test_case.assertEqual(result['path'][-1], tuple(spec['maze']['goal'])) + test_case.assertEqual(result['optimal_cost'], len(result['path']) - 1) + + walls = {tuple(wall) for wall in spec['maze']['walls']} + for current, next_cell in zip(result['path'], result['path'][1:]): + test_case.assertNotIn(current, walls) + test_case.assertEqual( + abs(current[0] - next_cell[0]) + abs(current[1] - next_cell[1]), + 1, + ) + test_case.assertNotIn(result['path'][-1], walls) + return result + + +def assert_no_mechanisms(test_case, spec): + mechanisms = spec['mechanisms'] + for key in MECHANISM_KEYS: + test_case.assertEqual(mechanisms[key], [], key) + for key, value in mechanisms.items(): + test_case.assertEqual(value, [], key) + test_case.assertEqual(spec['rules']['hidden_mechanisms'], []) + test_case.assertEqual(spec['metadata']['chain_pattern'], 'none') + + +def assert_standard_mechanism_groups(test_case, spec): + test_case.assertEqual( + set(spec['mechanisms']), + {'keys', 'doors', 'switches', 'gates'}, + ) + + +def assert_no_hidden_or_auxiliary_mechanisms(test_case, spec): + test_case.assertEqual(spec['rules']['hidden_mechanisms'], []) + test_case.assertEqual(spec['goal']['auxiliary_conditions'], []) + + +def assert_key_door_chain_on_path(test_case, spec, *, key_id, door_id): + mechanisms = spec['mechanisms'] + key = next(key for key in mechanisms['keys'] if key['id'] == key_id) + door = next(door for door in mechanisms['doors'] if door['id'] == door_id) + + to_door = { + **spec, + 'maze': { + **spec['maze'], + 'goal': door['position'], + }, + 'goal': { + **spec['goal'], + 'target': door['position'], + }, + } + to_door_result = solve(to_door) + test_case.assertTrue(to_door_result['is_solvable']) + test_case.assertEqual(to_door_result['path'][-1], tuple(door['position'])) + test_case.assertIn(f'pickup:{key_id}', to_door_result['interactions']) + test_case.assertIn(f'open:{door_id}', to_door_result['interactions']) + test_case.assertIn(tuple(key['position']), to_door_result['path']) + + from_door = { + **spec, + 'maze': { + **spec['maze'], + 'start': door['position'], + }, + 'mechanisms': { + **mechanisms, + 'doors': [item for item in mechanisms['doors'] if item['id'] != door_id], + }, + } + from_door_result = solve(from_door) + test_case.assertTrue(from_door_result['is_solvable']) + test_case.assertEqual(from_door_result['path'][0], tuple(door['position'])) + test_case.assertEqual(from_door_result['path'][-1], tuple(spec['maze']['goal'])) diff --git a/tests/test_M1_maze_types.py b/tests/test_M1_maze_types.py new file mode 100644 index 0000000..b5f5c7d --- /dev/null +++ b/tests/test_M1_maze_types.py @@ -0,0 +1,153 @@ +import sys +import unittest +from pathlib import Path + +_REPO_ROOT = str(Path(__file__).resolve().parent.parent) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) + +from maze_test_utils import ( + MECHANISM_KEYS, + assert_bfs_solver_finds_path_to_goal, + assert_goal_target_matches_maze_goal, + assert_key_door_chain_on_path, + assert_navigation_contract, + assert_no_hidden_or_auxiliary_mechanisms, + assert_standard_mechanism_groups, + load_maze_specs, +) + + +S_MAZE_TYPE_BY_BASE_NAME = { + '8x8_corridor': 'S2', + '10x10_corridor': 'S3', + '10x10_dense': 'S4', + '14x14_corridor': 'S5', + '14x14_dense': 'S6', +} + + +def _parse_m1_name(file_name): + stem = Path(file_name).stem + parts = stem.split('_') + if len(parts) < 4: + raise ValueError(f'Unexpected M1 filename: {file_name}') + size, structure_type, mechanism, variant = parts + return size, structure_type, mechanism, variant + + +def _load_s_counterparts(): + counterparts = {} + for base_name, maze_type in S_MAZE_TYPE_BY_BASE_NAME.items(): + for spec in load_maze_specs(maze_type): + variant = spec['task_id'].rsplit('_', 1)[-1] + counterparts[f'{base_name}_{variant}'] = spec + return counterparts + + +class TestM1MazeTypes(unittest.TestCase): + """In addition to passing the S maze tests, M1 mazes should also have one and only one mechanism chain. + + The chain must be on the path to reaching the goal. i.e. it's impossible to solve the maze without going through the mechanism chain. + """ + + @classmethod + def setUpClass(cls): + cls.specs = load_maze_specs('M1', include_file_name=True) + cls.s_counterparts = _load_s_counterparts() + + def test_expected_number_of_variants(self): + """Tests that M1 contains the expected number of maze variants.""" + self.assertEqual(len(self.specs), 10) + + def test_naming_scheme(self): + """Tests that each M1 filename matches its dimensions and kr mechanism type.""" + for file_name, spec in self.specs: + with self.subTest(file_name=file_name): + size, structure_type, mechanism, variant = _parse_m1_name(file_name) + width, height = spec['maze']['dimensions'] + + self.assertEqual(size, f'{width}x{height}') + self.assertIn(structure_type, {'corridor', 'dense'}) + self.assertEqual(mechanism, 'kr') + self.assertIn(variant, {'0', '1'}) + + def test_navigation_contract(self): + """Tests that each M1 maze has valid start and goal navigation fields.""" + for file_name, spec in self.specs: + with self.subTest(file_name=file_name): + assert_navigation_contract(self, spec) + assert_goal_target_matches_maze_goal(self, spec) + + def test_structure_matches_s_maze_counterpart(self): + """Tests that M1 walls, start, and goal match the corresponding S maze.""" + for file_name, spec in self.specs: + with self.subTest(file_name=file_name): + size, structure_type, _, variant = _parse_m1_name(file_name) + counterpart_key = f'{size}_{structure_type}_{variant}' + s_spec = self.s_counterparts[counterpart_key] + + self.assertEqual(spec['maze']['dimensions'], s_spec['maze']['dimensions']) + self.assertEqual(spec['maze']['walls'], s_spec['maze']['walls']) + self.assertEqual(spec['maze']['start'], s_spec['maze']['start']) + self.assertEqual(spec['maze']['goal'], s_spec['maze']['goal']) + + def test_bfs_solver_finds_path_to_goal(self): + """Tests that each M1 maze is solvable by the BFS solver.""" + for file_name, spec in self.specs: + with self.subTest(file_name=file_name): + assert_bfs_solver_finds_path_to_goal(self, spec) + + def test_has_one_red_key_and_one_red_door(self): + """Tests that each M1 maze has exactly one red key and one red door.""" + for file_name, spec in self.specs: + with self.subTest(file_name=file_name): + mechanisms = spec['mechanisms'] + self.assertEqual(mechanisms['switches'], []) + self.assertEqual(mechanisms['gates'], []) + self.assertEqual(len(mechanisms['keys']), 1) + self.assertEqual(len(mechanisms['doors']), 1) + + self.assertEqual( + mechanisms['keys'], + [ + { + 'id': 'kR', + 'position': mechanisms['keys'][0]['position'], + 'color': 'red', + } + ], + ) + self.assertEqual( + mechanisms['doors'], + [ + { + 'id': 'DR', + 'position': mechanisms['doors'][0]['position'], + 'color': 'red', + 'requires_key': 'red', + 'initial_state': 'locked', + } + ], + ) + + def test_has_one_and_only_one_mechanism_chain(self): + """Tests that M1 mazes contain one key-door chain with the door on a valid path to the goal.""" + for file_name, spec in self.specs: + with self.subTest(file_name=file_name): + mechanisms = spec['mechanisms'] + key = mechanisms['keys'][0] + door = mechanisms['doors'][0] + chain = [key['id'], door['id']] + + self.assertEqual(chain, ['kR', 'DR']) + self.assertIn(spec['metadata']['chain_pattern'], {'key_door', 'single_key_door'}) + assert_no_hidden_or_auxiliary_mechanisms(self, spec) + self.assertEqual(sum(len(mechanisms[key]) for key in MECHANISM_KEYS), 2) + assert_key_door_chain_on_path(self, spec, key_id=key['id'], door_id=door['id']) + + def test_has_no_extra_mechanism_groups(self): + """Tests that M1 mechanisms only use the standard mechanism groups.""" + for file_name, spec in self.specs: + with self.subTest(file_name=file_name): + assert_standard_mechanism_groups(self, spec) diff --git a/tests/test_S_maze_types.py b/tests/test_S_maze_types.py new file mode 100644 index 0000000..d2d3054 --- /dev/null +++ b/tests/test_S_maze_types.py @@ -0,0 +1,133 @@ +import sys +import unittest +from pathlib import Path + +_REPO_ROOT = str(Path(__file__).resolve().parent.parent) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) + +from maze_test_utils import ( + assert_bfs_solver_finds_path_to_goal, + assert_navigation_contract, + assert_no_mechanisms, + load_maze_specs, +) + + +class SimpleMazeAssertions: + maze_type = None + expected_dimensions = None + expected_difficulty_tier = None + expected_wall_topology = None + + @classmethod + def setUpClass(cls): + cls.specs = load_maze_specs(cls.maze_type) + + def test_expected_number_of_variants(self): + """Tests that each simple maze type has exactly two JSON variants.""" + self.assertEqual(len(self.specs), 2) + + def test_navigation_contract(self): + """Tests that each simple maze has valid start and goal navigation fields.""" + for spec in self.specs: + with self.subTest(task_id=spec['task_id']): + assert_navigation_contract(self, spec) + + def test_has_no_mechanisms(self): + """Tests that simple mazes do not define keys, doors, switches, or gates.""" + for spec in self.specs: + with self.subTest(task_id=spec['task_id']): + assert_no_mechanisms(self, spec) + + def test_matches_maze_type_metadata(self): + """Tests that each simple maze matches its expected dimensions and metadata.""" + for spec in self.specs: + with self.subTest(task_id=spec['task_id']): + self.assertEqual(spec['maze']['dimensions'], self.expected_dimensions) + self.assertEqual(spec['difficulty_tier'], self.expected_difficulty_tier) + self.assertEqual(spec['metadata']['wall_topology'], self.expected_wall_topology) + + def test_bfs_solver_finds_path_to_goal(self): + """Tests that each simple maze is solvable by the BFS solver.""" + for spec in self.specs: + with self.subTest(task_id=spec['task_id']): + assert_bfs_solver_finds_path_to_goal(self, spec) + + +class TestS1DSMazes(SimpleMazeAssertions, unittest.TestCase): + maze_type = 'S1' + expected_dimensions = [8, 8] + expected_difficulty_tier = 1 + expected_wall_topology = 'open' + + def test_has_no_interior_walls(self): + """Tests that S1 open-room mazes have no interior wall cells.""" + for spec in self.specs: + with self.subTest(task_id=spec['task_id']): + self.assertEqual(spec['maze']['walls'], []) + + +class TestS2SmallCorridorMazes(SimpleMazeAssertions, unittest.TestCase): + maze_type = 'S2' + expected_dimensions = [8, 8] + expected_difficulty_tier = 2 + expected_wall_topology = 'winding' + + def test_has_corridor_walls(self): + """Tests that S2 small corridor mazes define interior wall cells.""" + for spec in self.specs: + with self.subTest(task_id=spec['task_id']): + self.assertGreater(len(spec['maze']['walls']), 0) + + +class TestS3MediumCorridorMazes(SimpleMazeAssertions, unittest.TestCase): + maze_type = 'S3' + expected_dimensions = [10, 10] + expected_difficulty_tier = 3 + expected_wall_topology = 'winding' + + def test_has_corridor_walls(self): + """Tests that S3 medium corridor mazes define interior wall cells.""" + for spec in self.specs: + with self.subTest(task_id=spec['task_id']): + self.assertGreater(len(spec['maze']['walls']), 0) + + +class TestS4MediumDenseMazes(SimpleMazeAssertions, unittest.TestCase): + maze_type = 'S4' + expected_dimensions = [10, 10] + expected_difficulty_tier = 4 + expected_wall_topology = 'dense_dead_ends' + + def test_has_dense_walls(self): + """Tests that S4 medium dense mazes have the expected dense wall count.""" + for spec in self.specs: + with self.subTest(task_id=spec['task_id']): + self.assertGreaterEqual(len(spec['maze']['walls']), 30) + + +class TestS5LargeCorridorMazes(SimpleMazeAssertions, unittest.TestCase): + maze_type = 'S5' + expected_dimensions = [14, 14] + expected_difficulty_tier = 5 + expected_wall_topology = 'winding' + + def test_has_corridor_walls(self): + """Tests that S5 large corridor mazes define interior wall cells.""" + for spec in self.specs: + with self.subTest(task_id=spec['task_id']): + self.assertGreater(len(spec['maze']['walls']), 0) + + +class TestS6LargeDenseMazes(SimpleMazeAssertions, unittest.TestCase): + maze_type = 'S6' + expected_dimensions = [14, 14] + expected_difficulty_tier = 6 + expected_wall_topology = 'dense_dead_ends' + + def test_has_dense_walls(self): + """Tests that S6 large dense mazes have the expected dense wall count.""" + for spec in self.specs: + with self.subTest(task_id=spec['task_id']): + self.assertGreaterEqual(len(spec['maze']['walls']), 60)