Skip to content

Instantly share code, notes, and snippets.

@HacKanCuBa
Last active July 17, 2023 20:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HacKanCuBa/68d457f9ecdf8c89efcde555bf9f0731 to your computer and use it in GitHub Desktop.
Save HacKanCuBa/68d457f9ecdf8c89efcde555bf9f0731 to your computer and use it in GitHub Desktop.
Logo (programming language) turtle excercise
"""Logo (programming language) turtle exercise."""
import typing
import unittest
from dataclasses import dataclass
from enum import Enum
from itertools import zip_longest
from unittest import TestCase
class OutOfBoundsError(ValueError):
"""A turtle is out of bounds of the map."""
class CollisionError(ValueError):
"""A turtle collided against another."""
class Direction(str, Enum):
N = "N"
S = "S"
E = "E"
W = "W"
@dataclass(frozen=True)
class Coordinate:
x: int
y: int
def __add__(self, other: "Coordinate") -> "Coordinate":
x = self.x + other.x
y = self.y + other.y
return Coordinate(x, y)
class Turn(str, Enum):
L = "L"
R = "R"
class Instruction(str, Enum):
L = "L"
R = "R"
M = "M"
@dataclass(frozen=True)
class Position:
coord: Coordinate
direction: Direction
class Map(typing.TypedDict):
N: int # +
S: int # -
E: int # +
W: int # -
class Turtle:
def __init__(
self,
initial_position: Position = Position(Coordinate(0, 0), Direction.N),
*,
map_: typing.Optional[Map] = None,
name: str = "",
) -> None:
self._initial = initial_position
self._map = map_
self._name = name
self._coord = Coordinate(self._initial.coord.x, self._initial.coord.y)
self._direction = self._initial.direction
def __repr__(self) -> str:
return (
f"{type(self).__name__}"
+ f"(name={self.name}, map={self._map}, position={self.position!r})"
)
def __str__(self):
return self.name or repr(self)
@property
def position(self) -> Position:
return Position(self._coord, self._direction)
@property
def location(self) -> Coordinate:
return self._coord
@property
def direction(self) -> Direction:
return self._direction
@property
def name(self) -> str:
return self._name
def _validate_position(self) -> None:
if self._map is None:
return
out_of_bounds = any(
(
self._coord.x > self._map["E"],
self._coord.x < self._map["W"],
self._coord.y > self._map["N"],
self._coord.y < self._map["S"],
)
)
if out_of_bounds:
raise OutOfBoundsError(
f"current position is out of bounds: {self.position} (bounds: {self._map})"
)
def move(self) -> None:
"""Move turtle forward."""
direction_to_movement = {
Direction.N: Coordinate(0, 1),
Direction.S: Coordinate(0, -1),
Direction.E: Coordinate(1, 0),
Direction.W: Coordinate(-1, 0),
}
self._coord = self._coord + direction_to_movement[self._direction]
self._validate_position()
def turn(self, to: Turn) -> None:
"""Turn to given direction, without displacement."""
turn_to_direction = {
Turn.L: {
Direction.N: Direction.W,
Direction.S: Direction.E,
Direction.E: Direction.N,
Direction.W: Direction.S,
},
Turn.R: {
Direction.N: Direction.E,
Direction.S: Direction.W,
Direction.E: Direction.S,
Direction.W: Direction.N,
},
}
self._direction = turn_to_direction[to][self._direction]
def do(self, instruction: Instruction) -> None:
"""Do a single action."""
if instruction == Instruction.M:
self.move()
else:
self.turn(Turn(instruction))
def execute(self, instructions: typing.Sequence[typing.Union[Instruction, str]]) -> None:
"""Execute given instructions, one after the other."""
for instruction in instructions:
self.do(Instruction(instruction))
def reset(self) -> None:
# Copy coords because it is mutable!
self._coord = Coordinate(self._initial.coord.x, self._initial.coord.y)
self._direction = self._initial.direction
@dataclass
class Turtles:
turtles: typing.Tuple[Turtle, ...]
@property
def positions(self) -> typing.Tuple[Position, ...]:
return tuple(turtle.position for turtle in self.turtles)
def check_collision(self) -> bool:
"""Return True if there's a collision, False otherwise."""
locations: typing.Set[Coordinate] = {turtle.location for turtle in self.turtles}
return len(self.turtles) != len(locations)
def execute(
self,
instructions: typing.Sequence[typing.Sequence[typing.Union[Instruction, str]]],
) -> typing.Tuple[Position, ...]:
"""Execute instructions on every turtle, one after the other.
Note that collisions are not considered.
"""
for steps in zip_longest(*instructions):
for turtle, step in zip(self.turtles, steps):
if step is None:
continue
turtle.do(Instruction(step))
if self.check_collision():
raise CollisionError(f"the turtle {turtle} has collided")
return self.positions
class TurtleTests(TestCase):
def test_movement_works(self) -> None:
initial = Position(Coordinate(0, 0), Direction.N)
turtle = Turtle(initial)
turtle.move()
turtle.move()
self.assertEqual(Position(Coordinate(0, 2), Direction.N), turtle.position)
def test_execute_works(self) -> None:
initial = Position(Coordinate(0, 0), Direction.N)
turtle = Turtle(initial)
turtle.execute("MMLLM")
self.assertEqual(Position(Coordinate(0, 1), Direction.S), turtle.position)
def test_out_of_bounds(self) -> None:
initial = Position(Coordinate(0, 0), Direction.N)
map_ = Map(
N=2,
S=0,
E=2,
W=0,
)
turtle = Turtle(initial, map_=map_)
instructions_to_raise = {
"N": "MMM",
"S": "LLM",
"E": "RMMM",
"W": "LM",
}
for direction in map_:
with self.subTest("Out of bound raises exception", direction=direction):
turtle.reset()
with self.assertRaises(OutOfBoundsError):
turtle.execute(instructions_to_raise[direction])
class TurtlesTests(TestCase):
def test_several_turtles_moving(self) -> None:
map_ = Map(
N=3,
S=-3,
E=3,
W=-3,
)
initial_positions = (
Position(Coordinate(0, 0), Direction.N),
Position(Coordinate(-2, 1), Direction.E),
Position(Coordinate(1, 1), Direction.W),
Position(Coordinate(3, -1), Direction.S),
)
instructions = (
"RMMR",
"LMML",
"MMLLM",
"RMLMLRL",
)
expected = (
Position(Coordinate(2, 0), Direction.S),
Position(Coordinate(-2, 3), Direction.W),
Position(Coordinate(0, 1), Direction.E),
Position(Coordinate(2, -2), Direction.E),
)
turtles = Turtles(
tuple(Turtle(position, map_=map_) for position in initial_positions),
)
result = turtles.execute(instructions)
self.assertEqual(expected, result)
def test_several_turtles_out_of_bounds(self) -> None:
map_ = Map(
N=2,
S=-2,
E=2,
W=-2,
)
initial_positions = (
Position(Coordinate(1, 1), Direction.S), # ends in 1,1,N
Position(Coordinate(1, 2), Direction.E), # ends in 3,2,E
)
instructions = (
"LL",
"MM",
)
turtles = Turtles(
tuple(Turtle(position, map_=map_) for position in initial_positions),
)
with self.assertRaises(OutOfBoundsError):
turtles.execute(instructions)
def test_several_turtles_colliding(self) -> None:
map_ = Map(
N=3,
S=-3,
E=3,
W=-3,
)
initial_positions = (
Position(Coordinate(0, 0), Direction.N),
Position(Coordinate(-2, 1), Direction.E),
)
instructions = (
"M",
"MM",
)
turtles = Turtles(
tuple(Turtle(position, map_=map_) for position in initial_positions),
)
with self.assertRaises(CollisionError):
turtles.execute(instructions)
if __name__ == "__main__":
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment