Add project structure with maze solver, simulator, and tests
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Failing after 3s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Failing after 3s
This commit is contained in:
+35
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -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"]
|
||||||
@@ -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"]
|
||||||
@@ -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()
|
||||||
@@ -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"
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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"))
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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": [
|
||||||
|
"",
|
||||||
|
"<h1>Lost in a maze</h1>",
|
||||||
|
"<p>Reeborg was exploring a dark maze and the battery in its flashlight ran out.</p>",
|
||||||
|
"<p>Write a program using an <code>if/elif/else</code> 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.</p>",
|
||||||
|
"<h3>What you need to know</h3>",
|
||||||
|
"<ul><li>The functions <code>move()</code> and <code>turn_left()</code>.</li>",
|
||||||
|
"<li>Either the test <code>front_is_clear()</code> or <code>wall_in_front()</code>,",
|
||||||
|
"<code>right_is_clear()</code> or <code>wall_on_right()</code>, and <code>at_goal()</code>.</li>",
|
||||||
|
"<li>How to use a <code>while</code> loop and <code>if/elif/else</code> statements.</li>",
|
||||||
|
"<li>It might be useful to know how to use the negation of a test (<code>not</code> in Python).</li></ul>",
|
||||||
|
"DIFFICULTY4",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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": [
|
||||||
|
"",
|
||||||
|
"<h1>Lost in a maze</h1>",
|
||||||
|
"<p>Reeborg was exploring a dark maze and the battery in its flashlight ran out.</p>",
|
||||||
|
"<p>Write a program using an <code>if/elif/else</code> 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.</p>",
|
||||||
|
"<h3>What you need to know</h3>",
|
||||||
|
"<ul><li>The functions <code>move()</code> and <code>turn_left()</code>.</li>",
|
||||||
|
"<li>Either the test <code>front_is_clear()</code> or <code>wall_in_front()</code>,",
|
||||||
|
"<code>right_is_clear()</code> or <code>wall_on_right()</code>, and <code>at_goal()</code>.</li>",
|
||||||
|
"<li>How to use a <code>while</code> loop and <code>if/elif/else</code> statements.</li>",
|
||||||
|
"<li>It might be useful to know how to use the negation of a test (<code>not</code> in Python).</li></ul>",
|
||||||
|
"DIFFICULTY4",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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": [
|
||||||
|
"",
|
||||||
|
"<h1>Lost in a maze</h1>",
|
||||||
|
"<p>Reeborg was exploring a dark maze and the battery in its flashlight ran out.</p>",
|
||||||
|
"<p>Write a program using an <code>if/elif/else</code> 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.</p>",
|
||||||
|
"<h3>What you need to know</h3>",
|
||||||
|
"<ul><li>The functions <code>move()</code> and <code>turn_left()</code>.</li>",
|
||||||
|
"<li>Either the test <code>front_is_clear()</code> or <code>wall_in_front()</code>,",
|
||||||
|
"<code>right_is_clear()</code> or <code>wall_on_right()</code>, and <code>at_goal()</code>.</li>",
|
||||||
|
"<li>How to use a <code>while</code> loop and <code>if/elif/else</code> statements.</li>",
|
||||||
|
"<li>It might be useful to know how to use the negation of a test (<code>not</code> in Python).</li></ul>",
|
||||||
|
"DIFFICULTY4",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user