commit bca920c62cae08a0c6bd7542a2b48f09e1427a66 Author: Jens Tirsvad Nielsen Date: Sun Oct 4 12:07:00 2026 +0800 Add project structure with maze solver, simulator, and tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6272c1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Secrets +.env + +# Byte-compiled / cache +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ + +# Virtual environments +.venv/ +venv/ +env/ + +# Packaging +build/ +dist/ +*.egg-info/ +.eggs/ + +# Coverage +.coverage +htmlcov/ + +# Doxygen output +docs/html/ +docs/latex/ + +# IDEs / OS +.idea/ +.vscode/ +.DS_Store +Thumbs.db diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..6d8a616 --- /dev/null +++ b/Doxyfile @@ -0,0 +1,14 @@ +# Minimal Doxygen configuration; unspecified options use Doxygen defaults. +# Generate docs with: doxygen Doxyfile (output: docs/html/index.html) + +PROJECT_NAME = "Reeborg Maze" +PROJECT_BRIEF = "Right-wall-following maze solver for Reeborg's world" +OUTPUT_DIRECTORY = docs +INPUT = src +FILE_PATTERNS = *.py +RECURSIVE = YES +OPTIMIZE_OUTPUT_JAVA = YES +EXTRACT_ALL = YES +GENERATE_HTML = YES +GENERATE_LATEX = NO +QUIET = YES diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6c0a786 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +Copyright (C) 2026 Jens Tirsvad Nielsen + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU Affero General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +details. + +You should have received a copy of the GNU Affero General Public License along +with this program. If not, see . + +NOTE: This file holds the standard AGPL-3.0 notice only. Replace it with the +full license text from https://www.gnu.org/licenses/agpl-3.0.txt before release. diff --git a/README.md b/README.md new file mode 100644 index 0000000..281c547 --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# πŸš€ Reeborg Maze + +A beginner-friendly Python project that guides a robot through a maze in Reeborg's world by following the right wall, whatever its random start position and direction. + +## πŸ“š Table of Contents + +- [Overview](#-overview) +- [Requirements](#-requirements) +- [Setup](#-setup) +- [Run](#-run) +- [Tests](#-tests) +- [License](#-license) +- [Links](#-links) + +## 🧭 Overview + +This is the final project of day 6 of Udemy's *100 Days of Code: The Complete Python Pro Bootcamp*. The maze is fixed, but the robot starts at a random position and heading, so the program must work from anywhere. + +The robot follows the right wall: + +1. Walk forward until a wall is hit, then turn left (this puts a wall on the right and avoids infinite loops). +2. Until the goal is reached: + - if the right side is clear: turn right and move, + - else if the front is clear: move, + - else: turn left. + +The project contains: + +- `src/reeborg_maze/reeborg_script.py` – the script to paste into [Reeborg's world](https://reeborg.ca/reeborg.html?lang=en&mode=python&menu=worlds%2Fmenus%2Freeborg_intro_en.json&name=Maze&url=worlds%2Ftutorial_en%2Fmaze1.json). +- `src/reeborg_maze/maze_solver.py` – the same algorithm as a testable function. +- `src/reeborg_maze/simulator.py` – a small offline simulator that reads Reeborg world JSON files. +- `src/reeborg_maze/visualizer.py` – draws the world as text and animates the robot. +- `tests/` – unit tests and three test worlds with different start headings. + +## πŸ“‹ Requirements + +- Python 3.13 or newer +- No runtime dependencies (pytest is installed as a dev dependency for the tests) +- Optional: [Doxygen](https://www.doxygen.nl/) to build the API documentation + +## πŸ› οΈ Setup + +Create and activate a local virtual environment, upgrade pip and install the project in editable mode. + +Windows (PowerShell): + +```powershell +python -m venv .venv +.venv\Scripts\Activate.ps1 +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +Linux / macOS: + +```bash +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +## ▢️ Run + +In Reeborg's world: open the maze link above, choose Python mode, paste the contents of `src/reeborg_maze/reeborg_script.py` into the editor and press run. + +Offline, against a world file: + +```bash +python -m reeborg_maze tests/worlds/problem_world.json +``` + +Watch the robot move in the terminal (optionally pick a start with `X Y HEADING`, where heading is 0=east, 1=north, 2=west, 3=south): + +```bash +python -m reeborg_maze tests/worlds/problem_world.json --animate +python -m reeborg_maze tests/worlds/problem_world.json --animate --delay 0.05 --start 1 1 0 +``` + +![Animated run of the robot solving the maze in the terminal](docs/demo.webp) + +The robot is drawn as `>` `^` `<` `v`, the goal as `G` and mud as `~~~`. + +Build the documentation (written to `docs/html`): + +```bash +doxygen Doxyfile +``` + +## πŸ§ͺ Tests + +The tests use [pytest](https://pytest.org); each world is solved from every free cell and all four headings. + +```bash +python -m pytest -v +``` + +## πŸ“„ License + +[GNU AGPL v3 or later](LICENSE) + +## πŸ”— Links + +- [Repository](https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/006-a_robot_in_Reeborg_s_world) +- [Documentation](https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/006-a_robot_in_Reeborg_s_world#readme) +- [Issue tracker](https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/006-a_robot_in_Reeborg_s_world/issues) diff --git a/docs/demo.webp b/docs/demo.webp new file mode 100644 index 0000000..18e7c50 Binary files /dev/null and b/docs/demo.webp differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5cec70f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "reeborg-maze" +version = "0.1.0" +description = "Right-wall-following maze solver for Reeborg's world (100 Days of Code, day 6)." +readme = "README.md" +requires-python = ">=3.13" +license = "AGPL-3.0-or-later" +license-files = ["LICENSE"] +dependencies = [] + +[project.optional-dependencies] +dev = ["pytest>=8"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[project.urls] +Repository = "https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/006-a_robot_in_Reeborg_s_world" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +reeborg_maze = ["py.typed"] diff --git a/src/reeborg_maze/__init__.py b/src/reeborg_maze/__init__.py new file mode 100644 index 0000000..a5a4d84 --- /dev/null +++ b/src/reeborg_maze/__init__.py @@ -0,0 +1,9 @@ +"""! +@file __init__.py +@brief Right-wall-following maze solver for Reeborg's world. +""" + +from .maze_solver import solve_maze +from .simulator import SimulatedRobot, load_world + +__all__ = ["SimulatedRobot", "load_world", "solve_maze"] diff --git a/src/reeborg_maze/__main__.py b/src/reeborg_maze/__main__.py new file mode 100644 index 0000000..b207d42 --- /dev/null +++ b/src/reeborg_maze/__main__.py @@ -0,0 +1,41 @@ +"""! +@file __main__.py +@brief Command line entry point: python -m reeborg_maze WORLD.json +""" + +import argparse + +from .constants import DEFAULT_FRAME_DELAY +from .maze_solver import solve_maze +from .simulator import SimulatedRobot, load_world +from .visualizer import make_frame_printer, render + + +def main() -> None: + """! + @brief Solve a world file offline and report the final position. + """ + parser = argparse.ArgumentParser(description="Solve a Reeborg maze offline.") + parser.add_argument("world", help="path to a Reeborg world JSON file") + parser.add_argument("--animate", action="store_true", + help="show the robot moving in the terminal") + parser.add_argument("--delay", type=float, default=DEFAULT_FRAME_DELAY, + help="seconds between animation frames (default: %(default)s)") + parser.add_argument("--start", nargs=3, type=int, metavar=("X", "Y", "HEADING"), + help="override start; heading 0=E 1=N 2=W 3=S") + args = parser.parse_args() + + world = load_world(args.world) + x, y, heading = args.start if args.start else (None, None, None) + robot = SimulatedRobot(world, x, y, heading) + if args.animate: + robot.on_action = make_frame_printer(args.delay) + robot.on_action(robot) + solve_maze(robot) + if not args.animate: + print(render(robot)) + print(f"Goal reached at ({robot.x}, {robot.y}) in {robot.actions} actions.") + + +if __name__ == "__main__": + main() diff --git a/src/reeborg_maze/constants.py b/src/reeborg_maze/constants.py new file mode 100644 index 0000000..0e15dcd --- /dev/null +++ b/src/reeborg_maze/constants.py @@ -0,0 +1,50 @@ +"""! +@file constants.py +@brief Constants shared by the maze solver and the local world simulator. +""" + +## Compass headings in Reeborg's world, counter-clockwise: east, north, west, south. +EAST: int = 0 +NORTH: int = 1 +WEST: int = 2 +SOUTH: int = 3 + +## Number of headings; used for modular turning. +HEADING_COUNT: int = 4 + +## Unit step (dx, dy) for each heading. Reeborg's y axis grows northwards. +HEADING_STEP: dict[int, tuple[int, int]] = { + EAST: (1, 0), + NORTH: (0, 1), + WEST: (-1, 0), + SOUTH: (0, -1), +} + +## Wall names used in Reeborg world JSON files, keyed by heading. +HEADING_WALL_NAME: dict[int, str] = { + EAST: "east", + NORTH: "north", + WEST: "west", + SOUTH: "south", +} + +## Tiles the robot cannot enter. +BLOCKING_TILES: frozenset[str] = frozenset({"mud", "water"}) + +## Safety limit on simulator actions, so a bad algorithm cannot loop forever. +MAX_ACTIONS: int = 10_000 + +## Characters that show the robot's heading in the text visualisation. +HEADING_SYMBOL: dict[int, str] = { + EAST: ">", + NORTH: "^", + WEST: "<", + SOUTH: "v", +} + +## Default pause between animation frames, in seconds. +DEFAULT_FRAME_DELAY: float = 0.15 + +## ANSI escape sequences: clear the screen, and move the cursor to the top left. +ANSI_CLEAR: str = "\x1b[2J" +ANSI_HOME: str = "\x1b[H" diff --git a/src/reeborg_maze/maze_solver.py b/src/reeborg_maze/maze_solver.py new file mode 100644 index 0000000..9f3d3b5 --- /dev/null +++ b/src/reeborg_maze/maze_solver.py @@ -0,0 +1,54 @@ +"""! +@file maze_solver.py +@brief Right-wall-following algorithm that works on any robot-like object. +""" + +from typing import Protocol + + +class Robot(Protocol): + """! + @brief The subset of Reeborg's API the solver needs. + """ + + def move(self) -> None: ... + def turn_left(self) -> None: ... + def front_is_clear(self) -> bool: ... + def right_is_clear(self) -> bool: ... + def at_goal(self) -> bool: ... + + +def turn_right(robot: Robot) -> None: + """! + @brief Turn the robot 90 degrees clockwise (three left turns). + @param robot The robot to turn. + """ + for _ in range(3): + robot.turn_left() + + +def solve_maze(robot: Robot) -> None: + """! + @brief Drive the robot to the goal by following the wall on its right. + @param robot Robot with a random start position and heading. + + The robot first walks straight until it hits a wall, then turns left so + that the wall is on its right. This avoids the infinite loop that happens + when the robot starts in open space and the right side is never blocked. + Then, until the goal is reached: + - right is clear: turn right and move, + - else front is clear: move, + - else: turn left. + """ + while robot.front_is_clear(): + robot.move() + robot.turn_left() + + while not robot.at_goal(): + if robot.right_is_clear(): + turn_right(robot) + robot.move() + elif robot.front_is_clear(): + robot.move() + else: + robot.turn_left() diff --git a/src/reeborg_maze/py.typed b/src/reeborg_maze/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/reeborg_maze/reeborg_script.py b/src/reeborg_maze/reeborg_script.py new file mode 100644 index 0000000..ebcfc9e --- /dev/null +++ b/src/reeborg_maze/reeborg_script.py @@ -0,0 +1,29 @@ +"""! +@file reeborg_script.py +@brief Self-contained script to paste into https://reeborg.ca (Python mode). + +It uses only Reeborg's built-in functions, so it has no imports. +""" + + +def turn_right(): + """! + @brief Turn clockwise by doing three left turns. + """ + turn_left() + turn_left() + turn_left() + + +while front_is_clear(): + move() +turn_left() + +while not at_goal(): + if right_is_clear(): + turn_right() + move() + elif front_is_clear(): + move() + else: + turn_left() diff --git a/src/reeborg_maze/simulator.py b/src/reeborg_maze/simulator.py new file mode 100644 index 0000000..be07c09 --- /dev/null +++ b/src/reeborg_maze/simulator.py @@ -0,0 +1,122 @@ +"""! +@file simulator.py +@brief Minimal offline model of Reeborg's world, used to test the solver. +""" + +import json +from collections.abc import Callable +from pathlib import Path + +from .constants import ( + BLOCKING_TILES, + HEADING_COUNT, + HEADING_STEP, + HEADING_WALL_NAME, + MAX_ACTIONS, +) + + +class SimulationError(RuntimeError): + """! + @brief Raised when the robot hits a wall or exceeds the action limit. + """ + + +class SimulatedRobot: + """! + @brief A robot on a walled grid that mimics Reeborg's built-in API. + """ + + def __init__(self, world: dict, x: int | None = None, y: int | None = None, + heading: int | None = None, + on_action: Callable[["SimulatedRobot"], None] | None = None) -> None: + """! + @param world Parsed Reeborg world JSON. + @param x Start column; defaults to the robot in the file. + @param y Start row; defaults to the robot in the file. + @param heading Start heading (0=E, 1=N, 2=W, 3=S); defaults to the file's. + @param on_action Optional callback invoked after every turn or move. + """ + start = world["robots"][0] + self.cols: int = world["cols"] + self.rows: int = world["rows"] + self.x: int = start["x"] if x is None else x + self.y: int = start["y"] if y is None else y + self.heading: int = start["_orientation"] if heading is None else heading + self.walls: dict[str, list[str]] = world.get("walls", {}) + self.tiles: dict[str, list[str]] = world.get("tiles", {}) + goal = world["goal"]["position"] + self.goal: tuple[int, int] = (goal["x"], goal["y"]) + self.actions: int = 0 + self.on_action = on_action + + def _tick(self) -> None: + self.actions += 1 + if self.actions > MAX_ACTIONS: + raise SimulationError("Action limit exceeded: probable infinite loop") + + def _notify(self) -> None: + if self.on_action is not None: + self.on_action(self) + + def wall_between(self, x: int, y: int, heading: int) -> bool: + """! + @brief Whether a wall or the world edge blocks leaving (x, y) toward heading. + """ + dx, dy = HEADING_STEP[heading] + nx, ny = x + dx, y + dy + if not (1 <= nx <= self.cols and 1 <= ny <= self.rows): + return True + name = HEADING_WALL_NAME[heading] + opposite = HEADING_WALL_NAME[(heading + 2) % HEADING_COUNT] + return ( + name in self.walls.get(f"{x},{y}", []) + or opposite in self.walls.get(f"{nx},{ny}", []) + ) + + def _clear(self, heading: int) -> bool: + if self.wall_between(self.x, self.y, heading): + return False + dx, dy = HEADING_STEP[heading] + target = self.tiles.get(f"{self.x + dx},{self.y + dy}", []) + return not BLOCKING_TILES.intersection(target) + + def front_is_clear(self) -> bool: + """!@brief True if the robot can move forward.""" + return self._clear(self.heading) + + def right_is_clear(self) -> bool: + """!@brief True if the robot could move after turning right.""" + return self._clear((self.heading - 1) % HEADING_COUNT) + + def at_goal(self) -> bool: + """!@brief True if the robot stands on the goal.""" + return (self.x, self.y) == self.goal + + def turn_left(self) -> None: + """!@brief Rotate 90 degrees counter-clockwise.""" + self._tick() + self.heading = (self.heading + 1) % HEADING_COUNT + self._notify() + + def move(self) -> None: + """! + @brief Move one cell forward. + @throws SimulationError if blocked. + """ + self._tick() + if not self.front_is_clear(): + raise SimulationError(f"Blocked at ({self.x}, {self.y}) heading {self.heading}") + dx, dy = HEADING_STEP[self.heading] + self.x += dx + self.y += dy + self._notify() + + +def load_world(path: str | Path) -> dict: + """! + @brief Read a Reeborg world JSON file. + @param path File to read. + @return Parsed world dictionary. + """ + return json.loads(Path(path).read_text(encoding="utf-8")) diff --git a/src/reeborg_maze/visualizer.py b/src/reeborg_maze/visualizer.py new file mode 100644 index 0000000..34153d6 --- /dev/null +++ b/src/reeborg_maze/visualizer.py @@ -0,0 +1,69 @@ +"""! +@file visualizer.py +@brief Text rendering and animation of a SimulatedRobot in its world. +""" + +import time + +from .constants import ( + ANSI_CLEAR, + ANSI_HOME, + BLOCKING_TILES, + EAST, + HEADING_SYMBOL, + NORTH, + SOUTH, + WEST, +) +from .simulator import SimulatedRobot + + +def render(robot: SimulatedRobot) -> str: + """! + @brief Draw the world as text: walls, mud, goal (G) and the robot (> ^ < v). + @param robot Robot whose world and position are drawn. + @return Multi-line string. + """ + lines = [] + for y in range(robot.rows, 0, -1): + top = "".join( + "+" + ("---" if robot.wall_between(x, y, NORTH) else " ") + for x in range(1, robot.cols + 1) + ) + lines.append(top + "+") + + row = "" + for x in range(1, robot.cols + 1): + row += "|" if robot.wall_between(x, y, WEST) else " " + if (x, y) == (robot.x, robot.y): + row += f" {HEADING_SYMBOL[robot.heading]} " + elif (x, y) == robot.goal: + row += " G " + elif BLOCKING_TILES.intersection(robot.tiles.get(f"{x},{y}", [])): + row += "~~~" + else: + row += " " + row += "|" if robot.wall_between(robot.cols, y, EAST) else " " + lines.append(row) + + lines.append("".join( + "+" + ("---" if robot.wall_between(x, 1, SOUTH) else " ") + for x in range(1, robot.cols + 1) + ) + "+") + lines.append(f"actions: {robot.actions} position: ({robot.x}, {robot.y})") + return "\n".join(lines) + + +def make_frame_printer(delay: float): + """! + @brief Build an on_action callback that redraws the world in place. + @param delay Seconds to pause after each frame. + @return Callback accepting a SimulatedRobot. + """ + print(ANSI_CLEAR, end="") + + def show(robot: SimulatedRobot) -> None: + print(ANSI_HOME + render(robot), flush=True) + time.sleep(delay) + + return show diff --git a/tests/test_maze_solver.py b/tests/test_maze_solver.py new file mode 100644 index 0000000..749e51b --- /dev/null +++ b/tests/test_maze_solver.py @@ -0,0 +1,49 @@ +"""! +@file test_maze_solver.py +@brief Runs the solver on every provided world from all four start headings. +""" + +from pathlib import Path + +import pytest + +from reeborg_maze import SimulatedRobot, load_world, solve_maze +from reeborg_maze.constants import BLOCKING_TILES, HEADING_COUNT + +WORLDS = sorted((Path(__file__).parent / "worlds").glob("*.json")) + + +def free_starts() -> list[tuple[str, int, int, int]]: + """! + @brief Every (world, x, y, heading) where the robot can start. + """ + starts = [] + for path in WORLDS: + world = load_world(path) + for x in range(1, world["cols"] + 1): + for y in range(1, world["rows"] + 1): + if BLOCKING_TILES.intersection(world["tiles"].get(f"{x},{y}", [])): + continue + for heading in range(HEADING_COUNT): + starts.append((path.name, x, y, heading)) + return starts + + +def test_worlds_exist() -> None: + assert WORLDS + + +@pytest.mark.parametrize("path", WORLDS, ids=lambda p: p.name) +def test_reaches_goal_from_file_start(path: Path) -> None: + robot = SimulatedRobot(load_world(path)) + solve_maze(robot) + assert robot.at_goal() + + +@pytest.mark.parametrize(("name", "x", "y", "heading"), free_starts()) +def test_reaches_goal_from_every_free_cell_and_heading( + name: str, x: int, y: int, heading: int +) -> None: + robot = SimulatedRobot(load_world(Path(__file__).parent / "worlds" / name), x, y, heading) + solve_maze(robot) + assert robot.at_goal() diff --git a/tests/worlds/problem_world.json b/tests/worlds/problem_world.json new file mode 100644 index 0000000..00738f4 --- /dev/null +++ b/tests/worlds/problem_world.json @@ -0,0 +1,248 @@ +{ + "walls": { + "1,1": [ + "east" + ], + "1,2": [ + "east" + ], + "1,3": [ + "north" + ], + "2,5": [ + "north" + ], + "1,5": [ + "north" + ], + "3,5": [ + "north" + ], + "4,5": [ + "north" + ], + "5,5": [ + "north", + "east" + ], + "2,3": [ + "north", + "east" + ], + "2,2": [ + "east" + ], + "3,2": [ + "east" + ], + "5,1": [ + "east" + ], + "5,3": [ + "east", + "north" + ], + "4,4": [ + "east", + "north" + ], + "4,2": [ + "east" + ], + "1,4": [ + "east" + ], + "5,2": [ + "east", + "north" + ], + "3,1": [ + "north" + ] + }, + "small_tiles": false, + "rows": 6, + "cols": 6, + "tiles": { + "6,1": [ + "mud" + ], + "6,2": [ + "mud" + ], + "6,3": [ + "mud" + ], + "6,5": [ + "mud" + ], + "6,6": [ + "mud" + ], + "5,6": [ + "mud" + ], + "4,6": [ + "mud" + ], + "3,6": [ + "mud" + ], + "2,6": [ + "mud" + ], + "1,6": [ + "mud" + ], + "6,4": [ + "grass" + ], + "5,4": [ + "#000000" + ], + "5,5": [ + "#000000" + ], + "4,5": [ + "#000000" + ], + "3,5": [ + "#000000" + ], + "3,4": [ + "#000000" + ], + "4,4": [ + "#000000" + ], + "4,3": [ + "#000000" + ], + "3,3": [ + "#000000" + ], + "3,2": [ + "#000000" + ], + "4,2": [ + "#000000" + ], + "5,3": [ + "#000000" + ], + "5,2": [ + "#000000" + ], + "5,1": [ + "#000000" + ], + "4,1": [ + "#000000" + ], + "3,1": [ + "#000000" + ], + "2,1": [ + "#000000" + ], + "2,2": [ + "#000000" + ], + "2,3": [ + "#000000" + ], + "2,4": [ + "#000000" + ], + "2,5": [ + "#000000" + ], + "1,5": [ + "#000000" + ], + "1,4": [ + "#000000" + ], + "1,3": [ + "#000000" + ], + "1,2": [ + "#000000" + ], + "1,1": [ + "#000000" + ] + }, + "goal": { + "possible_final_positions": [ + [ + 6, + 4 + ] + ], + "position": { + "image": "racing_flag", + "x": 6, + "y": 4 + } + }, + "robots": [ + { + "x": 3, + "y": 4, + "objects": {}, + "model": "classic", + "_orientation": 3, + "_is_leaky": true, + "_prev_x": 3, + "_prev_y": 3, + "_prev_orientation": 2, + "_trace_history": [ + { + "color": "seagreen", + "thickness": 2, + "prev_x": 140, + "x": 140, + "prev_y": 170, + "y": 210, + "grid_x": 3, + "grid_y": 3 + }, + { + "color": "seagreen", + "thickness": 2, + "prev_x": 140, + "x": 140, + "prev_y": 200, + "y": 210, + "grid_x": 3, + "grid_y": 3 + } + ], + "_trace_style": "default", + "_trace_color": "seagreen", + "__id": 80, + "initial_position": [ + 3, + 3 + ] + } + ], + "description": [ + "", + "

Lost in a maze

", + "

Reeborg was exploring a dark maze and the battery in its flashlight ran out.

", + "

Write a program using an if/elif/else statement so Reeborg can find the exit. ", + "The secret is to have Reeborg follow along the right edge of the maze, ", + "turning right if it can, going straight ahead if it ", + "can’t turn right, or turning left as a last resort.

", + "

What you need to know

", + "", + "DIFFICULTY4", + "" + ] +} diff --git a/tests/worlds/problem_world2.json b/tests/worlds/problem_world2.json new file mode 100644 index 0000000..46d6679 --- /dev/null +++ b/tests/worlds/problem_world2.json @@ -0,0 +1,248 @@ +{ + "walls": { + "1,1": [ + "east" + ], + "1,2": [ + "east" + ], + "1,3": [ + "north" + ], + "2,5": [ + "north" + ], + "1,5": [ + "north" + ], + "3,5": [ + "north" + ], + "4,5": [ + "north" + ], + "5,5": [ + "north", + "east" + ], + "2,3": [ + "north", + "east" + ], + "2,2": [ + "east" + ], + "3,2": [ + "east" + ], + "5,1": [ + "east" + ], + "5,3": [ + "east", + "north" + ], + "4,4": [ + "east", + "north" + ], + "4,2": [ + "east" + ], + "1,4": [ + "east" + ], + "5,2": [ + "east", + "north" + ], + "3,1": [ + "north" + ] + }, + "small_tiles": false, + "rows": 6, + "cols": 6, + "tiles": { + "6,1": [ + "mud" + ], + "6,2": [ + "mud" + ], + "6,3": [ + "mud" + ], + "6,5": [ + "mud" + ], + "6,6": [ + "mud" + ], + "5,6": [ + "mud" + ], + "4,6": [ + "mud" + ], + "3,6": [ + "mud" + ], + "2,6": [ + "mud" + ], + "1,6": [ + "mud" + ], + "6,4": [ + "grass" + ], + "5,4": [ + "#000000" + ], + "5,5": [ + "#000000" + ], + "4,5": [ + "#000000" + ], + "3,5": [ + "#000000" + ], + "3,4": [ + "#000000" + ], + "4,4": [ + "#000000" + ], + "4,3": [ + "#000000" + ], + "3,3": [ + "#000000" + ], + "3,2": [ + "#000000" + ], + "4,2": [ + "#000000" + ], + "5,3": [ + "#000000" + ], + "5,2": [ + "#000000" + ], + "5,1": [ + "#000000" + ], + "4,1": [ + "#000000" + ], + "3,1": [ + "#000000" + ], + "2,1": [ + "#000000" + ], + "2,2": [ + "#000000" + ], + "2,3": [ + "#000000" + ], + "2,4": [ + "#000000" + ], + "2,5": [ + "#000000" + ], + "1,5": [ + "#000000" + ], + "1,4": [ + "#000000" + ], + "1,3": [ + "#000000" + ], + "1,2": [ + "#000000" + ], + "1,1": [ + "#000000" + ] + }, + "goal": { + "possible_final_positions": [ + [ + 6, + 4 + ] + ], + "position": { + "image": "racing_flag", + "x": 6, + "y": 4 + } + }, + "robots": [ + { + "x": 3, + "y": 4, + "objects": {}, + "model": "classic", + "_orientation": 1, + "_is_leaky": true, + "_prev_x": 3, + "_prev_y": 3, + "_prev_orientation": 2, + "_trace_history": [ + { + "color": "seagreen", + "thickness": 2, + "prev_x": 140, + "x": 140, + "prev_y": 170, + "y": 210, + "grid_x": 3, + "grid_y": 3 + }, + { + "color": "seagreen", + "thickness": 2, + "prev_x": 140, + "x": 140, + "prev_y": 200, + "y": 210, + "grid_x": 3, + "grid_y": 3 + } + ], + "_trace_style": "default", + "_trace_color": "seagreen", + "__id": 80, + "initial_position": [ + 3, + 3 + ] + } + ], + "description": [ + "", + "

Lost in a maze

", + "

Reeborg was exploring a dark maze and the battery in its flashlight ran out.

", + "

Write a program using an if/elif/else statement so Reeborg can find the exit. ", + "The secret is to have Reeborg follow along the right edge of the maze, ", + "turning right if it can, going straight ahead if it ", + "can’t turn right, or turning left as a last resort.

", + "

What you need to know

", + "", + "DIFFICULTY4", + "" + ] +} diff --git a/tests/worlds/problem_world3.json b/tests/worlds/problem_world3.json new file mode 100644 index 0000000..b080679 --- /dev/null +++ b/tests/worlds/problem_world3.json @@ -0,0 +1,248 @@ +{ + "walls": { + "1,1": [ + "east" + ], + "1,2": [ + "east" + ], + "1,3": [ + "north" + ], + "2,5": [ + "north" + ], + "1,5": [ + "north" + ], + "3,5": [ + "north" + ], + "4,5": [ + "north" + ], + "5,5": [ + "north", + "east" + ], + "2,3": [ + "north", + "east" + ], + "2,2": [ + "east" + ], + "3,2": [ + "east" + ], + "5,1": [ + "east" + ], + "5,3": [ + "east", + "north" + ], + "4,4": [ + "east", + "north" + ], + "4,2": [ + "east" + ], + "1,4": [ + "east" + ], + "5,2": [ + "east", + "north" + ], + "3,1": [ + "north" + ] + }, + "small_tiles": false, + "rows": 6, + "cols": 6, + "tiles": { + "6,1": [ + "mud" + ], + "6,2": [ + "mud" + ], + "6,3": [ + "mud" + ], + "6,5": [ + "mud" + ], + "6,6": [ + "mud" + ], + "5,6": [ + "mud" + ], + "4,6": [ + "mud" + ], + "3,6": [ + "mud" + ], + "2,6": [ + "mud" + ], + "1,6": [ + "mud" + ], + "6,4": [ + "grass" + ], + "5,4": [ + "#000000" + ], + "5,5": [ + "#000000" + ], + "4,5": [ + "#000000" + ], + "3,5": [ + "#000000" + ], + "3,4": [ + "#000000" + ], + "4,4": [ + "#000000" + ], + "4,3": [ + "#000000" + ], + "3,3": [ + "#000000" + ], + "3,2": [ + "#000000" + ], + "4,2": [ + "#000000" + ], + "5,3": [ + "#000000" + ], + "5,2": [ + "#000000" + ], + "5,1": [ + "#000000" + ], + "4,1": [ + "#000000" + ], + "3,1": [ + "#000000" + ], + "2,1": [ + "#000000" + ], + "2,2": [ + "#000000" + ], + "2,3": [ + "#000000" + ], + "2,4": [ + "#000000" + ], + "2,5": [ + "#000000" + ], + "1,5": [ + "#000000" + ], + "1,4": [ + "#000000" + ], + "1,3": [ + "#000000" + ], + "1,2": [ + "#000000" + ], + "1,1": [ + "#000000" + ] + }, + "goal": { + "possible_final_positions": [ + [ + 6, + 4 + ] + ], + "position": { + "image": "racing_flag", + "x": 6, + "y": 4 + } + }, + "robots": [ + { + "x": 3, + "y": 4, + "objects": {}, + "model": "classic", + "_orientation": 2, + "_is_leaky": true, + "_prev_x": 3, + "_prev_y": 3, + "_prev_orientation": 2, + "_trace_history": [ + { + "color": "seagreen", + "thickness": 2, + "prev_x": 140, + "x": 140, + "prev_y": 170, + "y": 210, + "grid_x": 3, + "grid_y": 3 + }, + { + "color": "seagreen", + "thickness": 2, + "prev_x": 140, + "x": 140, + "prev_y": 200, + "y": 210, + "grid_x": 3, + "grid_y": 3 + } + ], + "_trace_style": "default", + "_trace_color": "seagreen", + "__id": 80, + "initial_position": [ + 3, + 3 + ] + } + ], + "description": [ + "", + "

Lost in a maze

", + "

Reeborg was exploring a dark maze and the battery in its flashlight ran out.

", + "

Write a program using an if/elif/else statement so Reeborg can find the exit. ", + "The secret is to have Reeborg follow along the right edge of the maze, ", + "turning right if it can, going straight ahead if it ", + "can’t turn right, or turning left as a last resort.

", + "

What you need to know

", + "", + "DIFFICULTY4", + "" + ] +}