Circular Maze Puzzle — ggplot2

A circular maze puzzle visualization featuring concentric rings connected by radial passages. Unlike rectangular mazes, this design creates a unique solving experience where the player navigates inward through ring-shaped corridors. The maze has an entry point on the outer edge and a goal at the center, with algorithmically generated walls ensuring exactly one solvable path.

Circular Maze Puzzle rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' maze-circular: Circular Maze Puzzle
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 90/100 | Created: 2026-05-20

library(ggplot2)
library(ragg)

set.seed(42)

# --- Theme tokens
THEME    <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG  <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
INK      <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
BAND_ALT <- if (THEME == "light") "#EDEAE0" else "#242420"

# --- Maze parameters
RINGS        <- 7
SECTORS      <- 12
ANGLE_STEP   <- 2 * pi / SECTORS
CENTER_R     <- 0.5
ENTRY_SECTOR <- 1

ring_in  <- function(r) CENTER_R + (r - 1)
ring_out <- function(r) CENTER_R + r
OUTER_R  <- ring_out(RINGS)

sec_angle <- function(s, frac = 0) (s - 1 + frac) * ANGLE_STEP - pi / 2

# --- Iterative DFS maze generation
arc_open    <- matrix(FALSE, nrow = RINGS - 1, ncol = SECTORS)
radial_open <- matrix(FALSE, nrow = RINGS,     ncol = SECTORS)
visited     <- matrix(FALSE, nrow = RINGS,     ncol = SECTORS)

visited[RINGS, ENTRY_SECTOR] <- TRUE
dfs_stack <- list(list(r = RINGS, s = ENTRY_SECTOR))

while (length(dfs_stack) > 0) {
    top <- dfs_stack[[length(dfs_stack)]]
    r   <- top$r
    s   <- top$s
    sl  <- if (s == 1) SECTORS else s - 1
    sr  <- if (s == SECTORS) 1 else s + 1

    cands <- character(0)
    if (r > 1     && !visited[r - 1, s]) cands <- c(cands, "in")
    if (r < RINGS && !visited[r + 1, s]) cands <- c(cands, "out")
    if (!visited[r, sl])                  cands <- c(cands, "left")
    if (!visited[r, sr])                  cands <- c(cands, "right")

    if (length(cands) == 0) {
        dfs_stack <- dfs_stack[-length(dfs_stack)]
        next
    }

    dir <- sample(cands, 1)

    if (dir == "in") {
        arc_open[r - 1, s] <- TRUE
        visited[r - 1, s]  <- TRUE
        dfs_stack <- c(dfs_stack, list(list(r = r - 1, s = s)))
    } else if (dir == "out") {
        arc_open[r, s]     <- TRUE
        visited[r + 1, s]  <- TRUE
        dfs_stack <- c(dfs_stack, list(list(r = r + 1, s = s)))
    } else if (dir == "left") {
        radial_open[r, sl] <- TRUE
        visited[r, sl]     <- TRUE
        dfs_stack <- c(dfs_stack, list(list(r = r, s = sl)))
    } else {
        radial_open[r, s]  <- TRUE
        visited[r, sr]     <- TRUE
        dfs_stack <- c(dfs_stack, list(list(r = r, s = sr)))
    }
}

center_sector <- sample(seq_len(SECTORS), 1)

# --- Alternating ring band shading (visual depth: even rings tinted)
band_data <- NULL
for (r in seq_len(RINGS)) {
    if (r %% 2 == 0) {
        angs <- seq(0, 2 * pi, length.out = 120)
        ri   <- ring_in(r)
        ro   <- ring_out(r)
        df   <- data.frame(
            x    = c(ro * cos(angs), ri * cos(rev(angs))),
            y    = c(ro * sin(angs), ri * sin(rev(angs))),
            ring = r
        )
        band_data <- rbind(band_data, df)
    }
}

# --- Outer boundary (heavier stroke — visual frame, drawn separately)
outer_a0   <- sec_angle(ENTRY_SECTOR, 0)
outer_a1   <- sec_angle(ENTRY_SECTOR, 1)
outer_angs <- seq(outer_a1, outer_a0 + 2 * pi, length.out = 300)
outer_wall  <- data.frame(x = OUTER_R * cos(outer_angs), y = OUTER_R * sin(outer_angs))

# --- Accumulate inner wall geometry (center boundary + internal arcs + radial walls)
walls <- NULL
seg   <- 0L

append_seg <- function(x, y) {
    seg   <<- seg + 1L
    walls <<- rbind(walls, data.frame(x = x, y = y, seg = seg))
}

# Center boundary with goal gap
{
    a0   <- sec_angle(center_sector, 0)
    a1   <- sec_angle(center_sector, 1)
    angs <- seq(a1, a0 + 2 * pi, length.out = 180)
    append_seg(CENTER_R * cos(angs), CENTER_R * sin(angs))
}

# Internal ring boundary arcs (where wall is closed)
for (r in seq_len(RINGS - 1)) {
    wr <- ring_out(r)
    for (s in seq_len(SECTORS)) {
        if (!arc_open[r, s]) {
            angs <- seq(sec_angle(s, 0), sec_angle(s, 1), length.out = 20)
            append_seg(wr * cos(angs), wr * sin(angs))
        }
    }
}

# Radial walls (where wall is closed)
for (r in seq_len(RINGS)) {
    ri <- ring_in(r)
    ro <- ring_out(r)
    for (s in seq_len(SECTORS)) {
        if (!radial_open[r, s]) {
            angle <- sec_angle(s, 1)
            append_seg(c(ri, ro) * cos(angle), c(ri, ro) * sin(angle))
        }
    }
}

# --- Goal disc and entry label
goal_r    <- CENTER_R * 0.65
goal_angs <- seq(0, 2 * pi, length.out = 80)
goal_pts  <- data.frame(
    x = goal_r * cos(goal_angs),
    y = goal_r * sin(goal_angs)
)

entry_mid <- sec_angle(ENTRY_SECTOR, 0.5)
label_r   <- OUTER_R * 1.07
entry_lx  <- label_r * cos(entry_mid)
entry_ly  <- label_r * sin(entry_mid)

extent <- OUTER_R * 1.20

# --- Plot
p <- ggplot() +
    # Alternating ring bands for visual depth
    geom_polygon(
        data = band_data,
        aes(x = x, y = y, group = ring),
        fill = BAND_ALT, color = NA
    ) +
    # Inner walls and center boundary
    geom_path(
        data = walls,
        aes(x = x, y = y, group = factor(seg)),
        color = INK, linewidth = 0.9, lineend = "round"
    ) +
    # Outer boundary with heavier stroke for visual frame
    geom_path(
        data = outer_wall,
        aes(x = x, y = y),
        color = INK, linewidth = 1.3, lineend = "round"
    ) +
    # Goal disc
    geom_polygon(
        data = goal_pts, aes(x = x, y = y),
        fill = "#009E73", color = NA
    ) +
    annotate("text", x = 0, y = 0,
             label = "★", color = PAGE_BG, size = 2.5) +
    annotate("text", x = entry_lx, y = entry_ly,
             label = "START", color = INK_SOFT, size = 2.6,
             fontface = "bold", hjust = 0.5, vjust = 0.5) +
    coord_equal(
        xlim = c(-extent, extent),
        ylim = c(-extent, extent)
    ) +
    labs(
        title   = "maze-circular · r · ggplot2 · anyplot.ai",
        caption = "rings: 7 · sectors: 12 · medium difficulty"
    ) +
    theme_void(base_size = 8) +
    theme(
        plot.background = element_rect(fill = PAGE_BG, color = NA),
        plot.title = element_text(
            color = INK, size = 12, hjust = 0.5,
            margin = ggplot2::margin(t = 8, b = 6)
        ),
        plot.caption = element_text(
            color = INK_SOFT, size = 7, hjust = 0.5,
            margin = ggplot2::margin(t = 6, b = 6)
        ),
        plot.margin = ggplot2::margin(8, 8, 8, 8)
    )

# --- Save
ggsave(
    filename = sprintf("plot-%s.png", THEME),
    plot     = p,
    device   = ragg::agg_png,
    width    = 6,
    height   = 6,
    units    = "in",
    dpi      = 400
)

Part of Circular Maze Puzzle on anyplot.ai.

Other implementations