```python
"""
Dungeon Crawler & Labyrinth Generator
=======================================
Standard-library-only terminal dungeon crawler with procedural
generation and a dynamic raycasting fog-of-war / line-of-sight system.

Features
--------
* 21x21 procedurally generated dungeon (rooms + corridors).
* Raycasting line-of-sight with a 5-tile radius (shadowcasting-style).
* Autonomous turn-based simulation (`run_automated_dungeon_simulation`).
* No blocking I/O — the simulation runs fully headless.
"""

import random
import math
from collections import deque, defaultdict


# ---------------------------------------------------------------------------
# Tile constants
# ---------------------------------------------------------------------------
WALL = 0
FLOOR = 1
STAIRS = 2

# ASCII glyphs
GLYPHS = {
    WALL: "#",
    FLOOR: ".",
    STAIRS: ">",
    PLAYER: "@",
    ORC: "o",
    GHOUL: "g",
    POTION: "!",
    SCROLL: "?",
}

# Entities that block movement
BLOCKING = {ORC, GHOUL}


class Entity:
    """A movable/interactive entity on the dungeon grid."""

    def __init__(self, kind, x, y):
        self.kind = kind
        self.x = x
        self.y = y
        self.hp = 10 if kind in (ORC, GHOUL) else 0

    def __repr__(self):
        return f"{GLYPHS[self.kind]}({self.x},{self.y})"


# ---------------------------------------------------------------------------
# Dungeon generator
# ---------------------------------------------------------------------------
class Dungeon:
    """Generates and holds a 21x21 procedural dungeon."""

    GRID = 21

    def __init__(self, seed=None):
        self.rng = random.Random(seed)
        self.grid = [[WALL for _ in range(self.GRID)] for _ in range(self.GRID)]
        self.entities = []          # all entities (player + monsters + items)
        self.explored = [[False] * self.GRID for _ in range(self.GRID)]
        self.visible = [[False] * self.GRID for _ in range(self.GRID)]
        self.rooms = []
        self._generate()

    # ---- generation ------------------------------------------------------
    def _generate(self):
        """Carve rooms and connect them with corridors."""
        n_rooms = self.rng.randint(7, 11)
        for _ in range(n_rooms):
            rw = self.rng.randint(3, 6)
            rh = self.rng.randint(3, 6)
            rx = self.rng.randint(1, self.GRID - rw - 1)
            ry = self.rng.randint(1, self.GRID - rh - 1)
            room = (rx, ry, rw, rh)
            if any(self._overlaps(room, r) for r in self.rooms):
                continue
            self._carve_room(room)
            if self.rooms:
                cx, cy = self._room_center(room)
                px, py = self._room_center(self.rooms[-1])
                self._carve_corridor((cx, cy), (px, py))
            self.rooms.append(room)

        # Place the stairs in the last room.
        sx, sy = self._room_center(self.rooms[-1])
        self.grid[sy][sx] = STAIRS

        # Place the player in the first room.
        px, py = self._room_center(self.rooms[0])
        self.entities.append(Entity(PLAYER, px, py))

        # Scatter monsters and items.
        for _ in range(self.rng.randint(5, 8)):
            self._spawn_entity(ORC)
        for _ in range(self.rng.randint(3, 5)):
            self._spawn_entity(GHOUL)
        for _ in range(self.rng.randint(3, 6)):
            self._spawn_entity(POTION)
        for _ in range(self.rng.randint(2, 4)):
            self._spawn_entity(SCROLL)

    def _overlaps(self, a, b):
        ax, ay, aw, ah = a
        bx, by, bw, bh = b
        return not (bx >= ax + aw or ax >= bx + bw or
                    by >= ay + ah or ay >= by + bh)

    def _carve_room(self, room):
        x, y, w, h = room
        for ry in range(y, y + h):
            for rx in range(x, x + w):
                self.grid[ry][rx] = FLOOR

    def _room_center(self, room):
        x, y, w, h = room
        return (x + w) // 2, (y + h) // 2

    def _carve_corridor(self, a, b):
        x1, y1 = a
        x2, y2 = b
        if self.rng.random() < 0.5:
            for x in range(min(x1, x2), max(x1, x2) + 1):
                self.grid[y1][x] = FLOOR
            for y in range(min(y1, y2), max(y1, y2) + 1):
                self.grid[y][x2] = FLOOR
        else:
            for y in range(min(y1, y2), max(y1, y2) + 1):
                self.grid[y][x1] = FLOOR
            for x in range(min(x1, x2), max(x1, x2) + 1):
                self.grid[y2][x] = FLOOR

    def _spawn_entity(self, kind):
        for _ in range(200):
            x = self.rng.randint(1, self.GRID - 2)
            y = self.rng.randint(1, self.GRID - 2)
            if self.grid[y][x] == FLOOR and not self._occupied(x, y):
                self.entities.append(Entity(kind, x, y))
                return

    def _occupied(self, x, y):
        return any(e.x == x and e.y == y for e in self.entities)

    # ---- queries ---------------------------------------------------------
    def in_bounds(self, x, y):
        return 0 <= x < self.GRID and 0 <= y < self.GRID

    def is_walkable(self, x, y):
        return self.in_bounds(x, y) and self.grid[y][x] != WALL

    def entity_at(self, x, y):
        for e in self.entities:
            if e.x == x and e.y == y:
                return e
        return None

    def player(self):
        for e in self.entities:
            if e.kind == PLAYER:
                return e
        return None

    # ---- fog of war (ray