)
import random
+
from amazeing.config.config_parser import Config
-from amazeing.maze_class.maze_walls import Cardinal, CellCoord
+from amazeing.maze_class.maze_coords import Cardinal, CellCoord
from amazeing.maze_display.TTYdisplay import TileCycle, TileMaps, extract_pairs
from amazeing.maze_display.backend import CloseRequested, IVec2
+from amazeing.utils import AVLTree
+
+tree = AVLTree()
+
+keys = {i: tree.append(i) for i in range(25)}
+
+for i in range(1, 5):
+ keys[i].remove()
+for i in range(5, 15, 2):
+ keys[i].remove()
+
+tree2 = AVLTree()
+
+keys2 = {i: tree2.append(i) for i in range(25)}
+
+for i in range(1, 10, 3):
+ keys2[i].remove()
+
+tree.join(tree2)
+
+print(tree)
+
+
+exit(0)
config = Config.parse(open("./example.conf").read())
e
for wall in maze.walls_dirty()
for e in wall.neighbours()
- if maze._check_coord(e) and maze.get_wall(e).is_full()
+ if maze.check_coord(e) and maze.get_wall(e).is_full()
}
for wall in rewrites:
pathfind()
-while False:
+while True:
maze_make_perfect(maze, callback=display_maze)
# poll_events(200)
maze_make_pacman(maze, walls_const, callback=display_maze)
from .maze import Maze
from .maze_pattern import Pattern
-from .maze_walls import (MazeWall, NetworkID, Orientation,
- WallCoord)
+from .maze_coords import Cardinal, Orientation, WallCoord, CellCoord
-__all__ = ["Maze",
- "Pattern",
- "MazeWall",
- "NetworkID",
- "Orientation",
- "WallCoord",]
+__all__ = [
+ "Maze",
+ "Pattern",
+ "Cardinal",
+ "Orientation",
+ "WallCoord",
+ "CellCoord",
+]
from typing import Callable, Generator, Iterable, cast
from amazeing.maze_display.backend import IVec2
-from .maze_walls import (
- Cardinal,
+from .maze_coords import (
CellCoord,
- MazeWall,
- NetworkID,
Orientation,
WallCoord,
- WallNetwork,
)
+type MazeObserver = Callable[[WallCoord], None]
+
class Maze:
def __init__(self, dims: IVec2) -> None:
self.__dims = dims
- self.__dirty: set[WallCoord] = set()
- self._clear()
-
- def _clear(self) -> None:
- if hasattr(self, "horizontal") and hasattr(self, "vertical"):
- self.__dirty ^= {wall for wall in self.walls_full()}
+ self.observers: set[MazeObserver] = set()
# list of lines
- self.horizontal: list[list[MazeWall]] = [
- [MazeWall() for _ in range(0, self.__dims.x)]
+ self.horizontal: list[list[bool]] = [
+ [False for _ in range(0, self.__dims.x)]
for _ in range(0, self.__dims.y + 1)
]
# list of lines
- self.vertical: list[list[MazeWall]] = [
- [MazeWall() for _ in range(0, self.__dims.y)]
+ self.vertical: list[list[bool]] = [
+ [False for _ in range(0, self.__dims.y)]
for _ in range(0, self.__dims.x + 1)
]
- self.networks: dict[NetworkID, WallNetwork] = {}
- def _rebuild(self) -> None:
- """
- rebuilds the maze to recompute proper connectivity values
- """
- walls = {wall for wall in self.walls_full()}
- self._clear()
- for wall in walls:
- self.fill_wall(wall)
-
- def get_wall(self, coord: WallCoord) -> MazeWall:
+ def get_wall(self, coord: WallCoord) -> bool:
if coord.orientation == Orientation.HORIZONTAL:
return self.horizontal[coord.a][coord.b]
return self.vertical[coord.a][coord.b]
- def _remove_wall(self, coord: WallCoord) -> None:
- """
- removes the wall, without updating network connectivity
- """
+ def set_wall(self, coord: WallCoord, value: bool) -> None:
wall = self.get_wall(coord)
- if wall.network_id is not None:
- self.networks[wall.network_id].remove_wall(coord)
- self.__dirty ^= {coord}
- wall.network_id = None
+ if wall != value:
+ if coord.orientation == Orientation.HORIZONTAL:
+ self.horizontal[coord.a][coord.b] = value
+ self.vertical[coord.a][coord.b] = value
+
+ for observer in self.observers:
+ observer(coord)
def all_walls(self) -> Generator[WallCoord]:
for orientation, a_count, b_count in [
for b in range(0, b_count):
yield WallCoord(orientation, a, b)
- def _check_coord(self, coord: WallCoord) -> bool:
+ def all_cells(self) -> Iterable[CellCoord]:
+ return CellCoord(self.__dims).all_up_to()
+
+ def check_coord(self, coord: WallCoord) -> bool:
if coord.a < 0 or coord.b < 0:
return False
(a_max, b_max) = (
return False
return True
- def get_walls_checked(self, ids: list[WallCoord]) -> list[MazeWall]:
- return [self.get_wall(id) for id in ids if self._check_coord(id)]
+ def get_walls_checked(self, ids: list[WallCoord]) -> list[bool]:
+ return [self.get_wall(id) for id in ids if self.check_coord(id)]
- def get_neighbours(self, id: WallCoord) -> list[MazeWall]:
+ def get_neighbours(self, id: WallCoord) -> list[bool]:
return self.get_walls_checked(id.neighbours())
- def _fill_wall_alone(self, id: WallCoord, wall: MazeWall) -> None:
- network_id = NetworkID()
- wall.network_id = network_id
- network = WallNetwork()
- network.add_wall(id)
- self.networks[network_id] = network
-
- def fill_wall(self, coord: WallCoord) -> None:
- wall = self.get_wall(coord)
-
- if wall.is_full():
- return
-
- self.__dirty ^= {coord}
-
- networks = {
- cast(NetworkID, neighbour.network_id)
- for neighbour in self.get_neighbours(coord)
- if neighbour.is_full()
- }
-
- if len(networks) == 0:
- return self._fill_wall_alone(coord, wall)
-
- dest_id = max(networks, key=lambda n: self.networks[n].size())
- dest = self.networks[dest_id]
-
- wall.network_id = dest_id
- dest.add_wall(coord)
-
- for to_merge in filter(lambda n: n != dest_id, networks):
- for curr in self.networks[to_merge].walls:
- self.get_wall(curr).network_id = dest_id
- dest.add_wall(curr)
-
- del self.networks[to_merge]
-
def outline(self) -> None:
if self.__dims.x < 1 or self.__dims.y < 1:
return
]:
for a in a_iter:
for b in b_iter:
- self.fill_wall(WallCoord(orientation, a, b))
+ self.set_wall(WallCoord(orientation, a, b), True)
def walls_full(self) -> Iterable[WallCoord]:
- return filter(lambda w: self.get_wall(w).is_full(), self.all_walls())
-
- def walls_dirty(self) -> set[WallCoord]:
- return self.__dirty
+ return filter(lambda w: self.get_wall(w), self.all_walls())
def walls_empty(self) -> Iterable[WallCoord]:
- return filter(
- lambda w: not self.get_wall(w).is_full(), self.all_walls()
- )
-
- def wall_bisects(self, wall: WallCoord) -> bool:
- a = {
- cast(NetworkID, neighbour.network_id)
- for neighbour in self.get_walls_checked(wall.a_neighbours())
- if neighbour.is_full()
- }
- b = {
- cast(NetworkID, neighbour.network_id)
- for neighbour in self.get_walls_checked(wall.b_neighbours())
- if neighbour.is_full()
- }
- return len(a & b) != 0
-
- def wall_cuts_cycle(self, wall: WallCoord) -> bool:
- return any(
- (
- len(
- [
- ()
- for wall in self.get_walls_checked(list(cell.walls()))
- if wall.is_full()
- ]
- )
- >= (3 if self.get_wall(wall).is_full() else 2)
- )
- for cell in wall.neighbour_cells()
- )
-
- def wall_leaf_neighbours(self, coord: WallCoord) -> list[WallCoord]:
- leaf_f: Callable[
- [Callable[[WallCoord], list[WallCoord]]], list[WallCoord]
- ] = lambda f: (
- list(filter(lambda c: self._check_coord(c), f(coord)))
- if all(
- not wall.is_full() for wall in self.get_walls_checked(f(coord))
- )
- else []
- )
- return leaf_f(WallCoord.a_neighbours) + leaf_f(WallCoord.b_neighbours)
-
- def clear_dirty(self) -> None:
- self.__dirty = set()
-
- def pathfind(
- self, src: CellCoord, dst: CellCoord
- ) -> list[Cardinal] | None:
- class Path:
- def __init__(self, prev: tuple["Path", Cardinal] | None) -> None:
- self.prev: tuple["Path", Cardinal] | None = prev
-
- def to_list(self) -> list[Cardinal]:
- if self.prev is None:
- return []
- prev, direction = self.prev
- prev_list = prev.to_list()
- prev_list.append(direction)
- return prev_list
-
- def __add__(self, value: Cardinal) -> "Path":
- return Path((self, value))
-
- walls_empty = set(self.walls_empty())
- visited = set()
- border = {src: Path(None)}
- while len(border) != 0:
- border_next = {}
- for pos, path in border.items():
- if pos == dst:
- return path.to_list()
- visited.add(pos)
- for direction in Cardinal.all():
- if pos.get_wall(direction) not in walls_empty:
- continue
- neighbour = pos.get_neighbour(direction)
- if neighbour in visited:
- continue
- if neighbour in border or neighbour in border_next:
- continue
- border_next[neighbour] = path + direction
- border = border_next
-
- return None
+ return filter(lambda w: not self.get_wall(w), self.all_walls())
-from collections.abc import Generator
from enum import Enum, auto
-from typing import Iterable, Optional, cast, overload
+from typing import Iterable, cast, overload
from ..maze_display import IVec2
-class NetworkID:
- __uuid_gen: int = 0
-
- def __init__(self) -> None:
- self.uuid: int = NetworkID.__uuid_gen
- NetworkID.__uuid_gen += 1
-
-
-class WallNetwork:
- def __init__(self) -> None:
- from .maze_walls import WallCoord
-
- self.walls: set[WallCoord] = set()
-
- def size(self) -> int:
- return len(self.walls)
-
- def add_wall(self, id: "WallCoord") -> None:
- self.walls.add(id)
-
- def remove_wall(self, id: "WallCoord") -> None:
- self.walls.remove(id)
-
-
-class MazeWall:
- def __init__(self, network_id: Optional[NetworkID] = None) -> None:
- self.network_id: Optional[NetworkID] = network_id
-
- def is_full(self) -> bool:
- return self.network_id is not None
-
-
class Orientation(Enum):
HORIZONTAL = auto()
VERTICAL = auto()
--- /dev/null
+from collections.abc import Iterable
+from amazeing.maze_class.maze import Maze
+from amazeing.maze_class.maze_coords import WallCoord
+
+
+class MazeDirtyTracker:
+ def __init__(self, maze: Maze) -> None:
+ self.__maze: Maze = maze
+ self.__dirty: set[WallCoord] = set()
+ maze.observers.add(self.__observer)
+
+ def __del__(self):
+ self.__maze.observers.discard(self.__observer)
+
+ def __observer(self, wall: WallCoord) -> None:
+ self.__dirty ^= {wall}
+
+ def end(self):
+ self.__maze.observers.discard(self.__observer)
+
+ def clear(self) -> set[WallCoord]:
+ res = self.__dirty
+ self.__dirty = set()
+ return res
+
+ def curr_dirty(self) -> Iterable[WallCoord]:
+ return list(self.__dirty)
--- /dev/null
+from amazeing.maze_class.maze import Maze
+from amazeing.maze_class.maze_coords import CellCoord, WallCoord
+from amazeing.utils import BiMap
+from amazeing.utils import AVLTree, AVLLeaf
+
+
+class NetworkID:
+ pass
+
+
+class DualForest[T]:
+ """
+ A forest of trees that contour networks
+ AVL trees are used to represent the contours, such that split and
+ merge operations are of complexity O(log n), each tree is a cycle
+ of each connex graph boundary
+ """
+
+ def __init__(
+ self,
+ ) -> None:
+ self.__trees: set[AVLTree[T]] = set()
+ self.__revmap: dict[T, set[AVLLeaf[T]]] = {}
+
+
+class MazeNetworkTracker:
+ def __init__(self, maze: Maze) -> None:
+ self.__maze: Maze = maze
+ self.__networks_wall: BiMap[NetworkID, WallCoord] = BiMap()
+ self.__networks_cell: BiMap[NetworkID, CellCoord] = BiMap()
+
+ netid = NetworkID()
+ for cell in maze.all_cells():
+ self.__networks_cell.add(netid, cell)
+
+ maze.observers.add(self.__observer)
+ for wall in maze.walls_full():
+ self.__observer(wall)
+
+ def __observer(self, wall: WallCoord) -> None:
+ return
+
+ def end(self):
+ self.__maze.observers.discard(self.__observer)
from collections.abc import Iterable
from amazeing.maze_display.backend import IVec2
from .maze import Maze
-from .maze_walls import CellCoord
+from .maze_coords import CellCoord
from typing import Callable
def fill(self, maze: "Maze") -> None:
for cell in self.__cells:
for wall in cell.walls():
- maze.fill_wall(wall)
+ maze.set_wall(wall, True)
from collections.abc import Callable, Generator, Iterable
-from ..config.config_parser import Color, Config, ColoredLine, ColorPair
+from amazeing.utils import BiMap
+from amazeing.config.config_parser import Color, Config, ColoredLine, ColorPair
from amazeing.maze_display.layout import (
BInt,
Box,
self.__filler: None | int = None
- self.__style_mapping: dict[int, set[IVec2]] = {}
- self.__style_revmapping: dict[IVec2, int] = {}
+ self.__style_bimap: BiMap[int, IVec2] = BiMap()
def __del__(self):
curses.curs_set(1)
box.mark_dirty()
def get_styled(self, style: int) -> Iterable[IVec2]:
- return set(
- self.__style_mapping[style]
- if style in self.__style_mapping
- else []
- )
+ return self.__style_bimap.get(style)
def map_style_cb(self) -> Callable[[int], None]:
curr: int | None = None
def draw_tile(self, pos: IVec2) -> None:
style = self.__style
- mapping = self.__style_mapping
- revmapping = self.__style_revmapping
-
- if pos in revmapping:
- mapping[revmapping[pos]].remove(pos)
- revmapping[pos] = style
- if style not in mapping:
- mapping[style] = set()
- mapping[style].add(pos)
-
+ self.__style_bimap.add(style, pos)
self.__tilemap.draw_at(pos, style, self.__pad.pad)
def set_style(self, style: int) -> None:
from collections.abc import Callable
from amazeing.maze_class.maze import Maze
-from amazeing.maze_class.maze_walls import WallCoord
+from amazeing.maze_class.maze_coords import WallCoord
import random
walls = [wall for wall in maze.walls_full() if wall not in walls_const]
random.shuffle(walls)
for wall in walls:
- maze._remove_wall(wall)
+ maze.set_wall(wall)
callback(maze)
if not maze.wall_cuts_cycle(wall):
continue
if len(leaf_neighbours) == 0:
- maze._remove_wall(wall)
+ maze.set_wall(wall)
else:
- maze._remove_wall(wall)
+ maze.set_wall(wall)
maze.fill_wall(random.choice(leaf_neighbours))
n += 1
callback(maze)
--- /dev/null
+from .bi_map import BiMap
+from .avl import Tree as AVLTree
+from .avl import Leaf as AVLLeaf
+
+__all__ = ["BiMap", "AVLTree", "AVLLeaf"]
--- /dev/null
+from collections.abc import Callable
+from typing import cast
+import textwrap
+
+
+class Tree[T]:
+ def __init__(self) -> None:
+ self.root: Node[T] | None = None
+
+ def __repr__(self) -> str:
+ return f"{self.root}" if self.root is not None else "(empty)"
+
+ def append(self, value: T) -> "Leaf[T]":
+ if self.root is None:
+ leaf = Leaf(self, value)
+ self.root = leaf
+ return leaf
+ if isinstance(self.root, Branch):
+ return self.root.append(value)
+ self.root = Branch(
+ self,
+ self.root.with_parent,
+ lambda parent: Leaf(parent, value),
+ )
+ return cast(Leaf, self.root.rhs)
+
+ def height(self) -> int:
+ return 0 if self.root is None else self.root.height
+
+ def is_empty(self) -> bool:
+ return self.root is None
+
+ def replace(self, node: "Node[T]", by: "Node[T]") -> None:
+ if node is not self.root:
+ raise Exception("Replace operation with unknown node")
+ self.root = by
+ by.parent = self
+
+ def balance_one_propagate(self) -> None:
+ return
+
+ def exchange(self, other: "Tree[T]") -> None:
+ a = self.root
+ b = other.root
+ if a is not None:
+ a = a.with_parent(other)
+ if b is not None:
+ b = b.with_parent(self)
+ other.root = a
+ self.root = b
+
+ def join(self, rhs: "Tree[T]") -> None:
+ if self.height() >= rhs.height():
+ self.rjoin(rhs)
+ else:
+ rhs.ljoin(self)
+ self.exchange(rhs)
+
+ def ljoin(self, lhs: "Tree[T]") -> None:
+ if self.root is None:
+ self.exchange(lhs)
+ if self.root is None or lhs.root is None:
+ return
+ curr = self.root
+ insert = lhs.root
+ lhs.root = None
+ while isinstance(curr, Branch) and curr.height > insert.height + 1:
+ curr = curr.lhs
+ parent = curr.parent
+ new = Branch(curr.parent, insert.with_parent, curr.with_parent)
+ parent.replace(curr, new)
+ new.update_height()
+ new.parent.balance_one_propagate()
+
+ def rjoin(self, rhs: "Tree[T]") -> None:
+ if self.root is None:
+ self.exchange(rhs)
+ if self.root is None or rhs.root is None:
+ return
+ curr = self.root
+ insert = rhs.root
+ rhs.root = None
+ while isinstance(curr, Branch) and curr.height > insert.height + 1:
+ curr = curr.lhs
+ parent = curr.parent
+ new = Branch(curr.parent, curr.with_parent, insert.with_parent)
+ parent.replace(curr, new)
+ new.update_height()
+ new.parent.balance_one_propagate()
+
+
+class Node[T]:
+ def __init__(self, parent: "Branch[T] | Tree[T]") -> None:
+ self.parent: Branch[T] | Tree[T] = parent
+ self.height: int = 1
+
+ def with_parent(self, parent: "Branch[T] | Tree[T]") -> "Node[T]":
+ self.parent = parent
+ return self
+
+ def root(self) -> Tree[T]:
+ if isinstance(self.parent, Tree):
+ return self.parent
+ return self.parent.root()
+
+
+class Branch[T](Node[T]):
+ def __init__(
+ self,
+ parent: "Branch[T] | Tree[T]",
+ lhs: Callable[["Branch[T]"], Node[T]],
+ rhs: Callable[["Branch[T]"], Node[T]],
+ ) -> None:
+ super().__init__(parent)
+ self.lhs: Node[T] = lhs(self)
+ self.rhs: Node[T] = rhs(self)
+ self.update_height()
+
+ def __repr__(self) -> str:
+ return (
+ f"lhs ({self.lhs.height}):\n"
+ + textwrap.indent(str(self.lhs), "| ")
+ + f"\nrhs ({self.rhs.height}):\n"
+ + textwrap.indent(str(self.rhs), " ")
+ )
+
+ def replace(self, node: Node[T], by: Node[T]) -> None:
+ if self.lhs is node:
+ self.lhs = by
+ elif self.rhs is node:
+ self.rhs = by
+ else:
+ raise Exception("Replace operation with unknown node")
+ by.parent = self
+
+ def get_other(self, node: Node[T]) -> Node[T]:
+ if self.lhs is node:
+ return self.rhs
+ elif self.rhs is node:
+ return self.lhs
+ else:
+ raise Exception("Get other operation with unknown node")
+
+ def update_height(self) -> None:
+ self.height = max(self.rhs.height, self.lhs.height) + 1
+
+ def get_balance(self) -> int:
+ return self.rhs.height - self.lhs.height
+
+ def rotate_rr(self) -> None:
+ # Simple AVL rotate:
+ #
+ # self --> self
+ # / \ / \
+ # a n n c
+ # / \ / \
+ # b c a b
+ n = self.rhs
+ if not isinstance(n, Branch):
+ return
+ a = self.lhs
+ b = n.lhs
+ c = n.rhs
+ n.lhs = a
+ n.rhs = b
+ self.rhs = c
+ self.lhs = n
+ a.parent = n
+ b.parent = n
+ c.parent = self
+ n.parent = self
+ n.update_height()
+ self.update_height()
+
+ def rotate_ll(self) -> None:
+ # Simple AVL rotate:
+ #
+ # self --> self
+ # / \ / \
+ # n c a n
+ # / \ / \
+ # a b b c
+ n = self.lhs
+ if not isinstance(n, Branch):
+ return
+ a = n.lhs
+ b = n.rhs
+ c = self.rhs
+ self.lhs = a
+ n.lhs = b
+ n.rhs = c
+ self.rhs = n
+ a.parent = self
+ b.parent = n
+ c.parent = n
+ n.parent = self
+ n.update_height()
+ self.update_height()
+
+ def rotate_rl(self) -> None:
+ # Double AVL rotate:
+ #
+ # self --> self
+ # / \ / \
+ # a n n m
+ # / \ / \ / \
+ # m d a b c d
+ # / \
+ # b c
+ n = self.lhs
+ if not isinstance(n, Branch):
+ return
+ m = n.lhs
+ if not isinstance(m, Branch):
+ return
+ a = self.lhs
+ b = m.lhs
+ c = m.rhs
+ d = n.rhs
+ n.lhs = a
+ n.rhs = b
+ m.lhs = c
+ m.rhs = d
+ self.lhs = n
+ self.rhs = m
+ a.parent = n
+ b.parent = n
+ c.parent = m
+ d.parent = m
+ n.parent = self
+ m.parent = self
+ n.update_height()
+ m.update_height()
+ self.update_height()
+
+ def rotate_lr(self) -> None:
+ # Double AVL rotate:
+ #
+ # self --> self
+ # / \ / \
+ # n d n m
+ # / \ / \ / \
+ # a m a b c d
+ # / \
+ # b c
+ n = self.lhs
+ if not isinstance(n, Branch):
+ return
+ m = n.rhs
+ if not isinstance(m, Branch):
+ return
+ a = n.lhs
+ b = m.lhs
+ c = m.rhs
+ d = self.rhs
+ n.lhs = a
+ n.rhs = b
+ m.lhs = c
+ m.rhs = d
+ self.lhs = n
+ self.rhs = m
+ a.parent = n
+ b.parent = n
+ c.parent = m
+ d.parent = m
+ n.parent = self
+ m.parent = self
+ n.update_height()
+ m.update_height()
+ self.update_height()
+
+ def append(self, value: T) -> "Leaf[T]":
+ if self.rhs is None:
+ leaf = Leaf[T](self, value)
+ self.rhs = leaf
+ self.balance_one_propagate()
+ return leaf
+ if isinstance(self.rhs, Branch):
+ return self.rhs.append(value)
+ new = Branch[T](
+ self,
+ self.rhs.with_parent,
+ lambda parent: Leaf[T](parent, value),
+ )
+ self.rhs = new
+ new_leaf = cast(Leaf[T], new.rhs)
+ self.balance_one_propagate()
+ return new_leaf
+
+ def balance_one(self):
+ if abs(self.get_balance()) <= 1:
+ return
+ if self.get_balance() > 0:
+ # right is taller
+ if not isinstance(self.rhs, Branch):
+ raise Exception("Invalid tree state")
+ if self.rhs.get_balance() >= 0:
+ self.rotate_rr()
+ else:
+ self.rotate_rl()
+ else:
+ # left is taller
+ if not isinstance(self.lhs, Branch):
+ raise Exception("Invalid tree state")
+ if self.lhs.get_balance() >= 0:
+ self.rotate_lr()
+ else:
+ self.rotate_ll()
+
+ def balance_one_propagate(self) -> None:
+ init_height = self.height
+ self.update_height()
+ self.balance_one()
+ if init_height != self.height:
+ self.parent.balance_one_propagate()
+
+
+class Leaf[T](Node[T]):
+ def __init__(
+ self,
+ parent: Branch[T] | Tree[T],
+ value: T,
+ ) -> None:
+ super().__init__(parent)
+ self.value: T = value
+
+ def __repr__(self) -> str:
+ return f"leaf: {self.value}"
+
+ def remove(self) -> None:
+ if isinstance(self.parent, Tree):
+ self.parent.root = None
+ return
+ other = self.parent.get_other(self)
+ self.parent.parent.replace(self.parent, other)
+ other.parent.balance_one_propagate()
--- /dev/null
+from collections.abc import Iterable
+
+
+class BiMap[K, R]:
+ def __init__(self) -> None:
+ self.__map: dict[K, set[R]] = {}
+ self.__revmap: dict[R, K] = {}
+
+ def add(self, key: K, revkey: R) -> None:
+ if self.revcontains(revkey):
+ self.revremove(revkey)
+ if not self.contains(key):
+ self.__map[key] = set()
+ self.__revmap[revkey] = key
+ self.__map[key].add(revkey)
+
+ def remove(self, key: K) -> None:
+ for revkey in self.__map[key]:
+ self.__revmap.pop(revkey)
+ self.__map.pop(key)
+
+ def revremove(self, revkey: R) -> None:
+ key = self.__revmap.pop(revkey)
+ self.__map[key].remove(revkey)
+ if len(self.__map[key]) == 0:
+ self.__map.pop(key)
+
+ def get(self, key: K) -> Iterable[R]:
+ return list(self.__map[key] if self.contains(key) else [])
+
+ def revget(self, revkey: R) -> K:
+ return self.__revmap[revkey]
+
+ def contains(self, key: K) -> bool:
+ return key in self.__map
+
+ def revcontains(self, revkey: R) -> bool:
+ return revkey in self.__revmap
-WIDTH=100
-HEIGHT=100
+WIDTH=25
+HEIGHT=25
ENTRY=0,0
-EXIT=99,99
+EXIT=24,24
OUTPUT_FILE=test
PERFECT=False
SEED=111