Add project structure with maze solver, simulator, and tests
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Failing after 3s

This commit is contained in:
2026-10-04 12:07:00 +08:00
commit bca920c62c
18 changed files with 1367 additions and 0 deletions
+35
View File
@@ -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
+14
View File
@@ -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
+17
View File
@@ -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.
+106
View File
@@ -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)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+28
View File
@@ -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"]
+9
View File
@@ -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"]
+41
View File
@@ -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()
+50
View File
@@ -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"
+54
View File
@@ -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()
View File
+29
View File
@@ -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()
+122
View File
@@ -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"))
+69
View File
@@ -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
+49
View File
@@ -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()
+248
View File
@@ -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",
""
]
}
+248
View File
@@ -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",
""
]
}
+248
View File
@@ -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",
""
]
}