Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/mcanouil/ggpacman

A `ggplot2` and `gganimate` Version of Pac-Man
https://github.com/mcanouil/ggpacman

gganimate ggplot2 pac-man r r-package

Last synced: 3 months ago
JSON representation

A `ggplot2` and `gganimate` Version of Pac-Man

Awesome Lists containing this project

README

        

---
output: github_document
editor_options:
chunk_output_type: console
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
message = FALSE,
warning = FALSE,
collapse = TRUE,
comment = "#>",
fig.path = "man/figures/README-",
dev = "ragg_png",
dev.args = list(
background = "#ffffff00"
)
)
```

# A `ggplot2` and `gganimate` Version of Pac-Man

[![Lifecycle: maturing](https://img.shields.io/badge/lifecycle-maturing-blue.svg)](https://www.tidyverse.org/lifecycle/#maturing)
[![GitHub tag](https://img.shields.io/github/tag/mcanouil/ggpacman.svg?label=latest tag)](https://github.com/mcanouil/ggpacman)
[![R build status](https://github.com/mcanouil/ggpacman/workflows/R-CMD-check/badge.svg)](https://github.com/mcanouil/ggpacman/actions)
[![CRAN status](https://www.r-pkg.org/badges/version/ggpacman)](https://CRAN.R-project.org/package=ggpacman)

The goal of `ggpacman` is to ...
Make a GIF of the game Pac-Man (*not to develop an R version of Pac-Man ...*).

## Installation

```{r install, eval = FALSE}
# Install ggpacman from CRAN:
install.packages("ggpacman")

# Or the the development version from GitHub:
# install.packages("remotes")
remotes::install_github("mcanouil/ggpacman")
```

## Pac-Man in action

```{r pacman}
library(ggpacman)
animate_pacman(
pacman = pacman,
ghosts = list(blinky, pinky, inky, clyde),
font_family = "xkcd"
)
```

## The Story of `ggpacman`

It started on a Saturday evening ...

It was the 21st of March (*for the sake of precision*),
around 10 pm CET (*also for the sake of precision and mostly because it is not relevant*).
I was playing around with my data on 'all' the movies I have seen so far ([mcanouil/IMDbRating](https://github.com/mcanouil/IMDbRating)) and looking on possibly new ideas of visualisation on twitter using `#ggplot2` and `#gganimate` (by the way the first time I played with [`gganimate`](https://gganimate.com/) was at [useR-2018 (Brisbane, Australia)](https://www.r-project.org/conferences/useR-2018/), just before and when @thomasp85 released the actual framework).
The only thing on the feed was "contaminated/deaths and covid-19" curves made with [`ggplot2`](https://ggplot2.tidyverse.org/) and a few with [`gganimate`](https://gganimate.com/) ...
Let's say, it was not as funny and interesting as I was hoping for ...
Then, I've got an idea, what if I can do something funny and not expected with [`ggplot2`](https://ggplot2.tidyverse.org/) and [`gganimate`](https://gganimate.com/)?
My first thought, was let's draw and animate Pac-Man, that should not be that hard!

Well, it was not that easy after-all ...
But, I am going to go through my code here (you might be interested to actually look at the [commits history](https://github.com/mcanouil/ggpacman/commits/master).

1. [The packages](#the-packages)
2. [The maze layer](#the-maze-layer)
1. [The base layer](#the-base-layer)
2. [The grid layer](#the-grid-layer)
3. [The bonus points layer](#the-bonus-points-layer)
3. [Pac-Man character](#pac-man-character)
4. [The Ghosts characters](#the-ghosts-characters)
1. [Body](#body)
2. [Eyes](#eyes)
3. [Ghost shape](#ghost-shape)
5. [How Pac-Man interacts with the maze?](#how-pac-man-interacts-with-the-maze)
1. [Bonus points](#bonus-points)
2. [Ghost `"weak"` and `"eaten"` states](#ghost-weak-and-eaten-states)
6. [Plot time](#Plot-time-to-summarise-a-little-or-a-lot)

### The packages

```{r libraries, results = "hide", message = FALSE, warning = FALSE}
library("stats")
library("utils")
library("rlang")
library("magrittr")
library("dplyr")
library("tidyr")
library("purrr")
library("ggplot2")
library("ggforce")
library("gganimate")
library("ggtext")
```

### The maze layer

#### The base layer

First thing first, I needed to set-up the base layer, meaning, the maze from Pac-Man.
I did start by setting the coordinates of the maze.

```{r base-layer}
base_layer <- ggplot() +
theme_void() +
theme(
legend.position = "none",
plot.background = element_rect(fill = "black", colour = "black"),
panel.background = element_rect(fill = "black", colour = "black"),
) +
coord_fixed(xlim = c(0, 20), ylim = c(0, 26))
```

For later use, I defined some scales (actually those scales, where defined way after chronologically speaking).
I am using those to define sizes and colours for all the geometries I am going to use to achieve the Pac-Man GIF.

```{r colours-mapping}
map_colours <- c(
"READY!" = "goldenrod1",
"wall" = "dodgerblue3", "door" = "dodgerblue3",
"normal" = "goldenrod1", "big" = "goldenrod1", "eaten" = "black",
"Pac-Man" = "yellow",
"eye" = "white", "iris" = "black",
"Blinky" = "red", "Blinky_weak" = "blue", "Blinky_eaten" = "transparent",
"Pinky" = "pink", "Pinky_weak" = "blue", "Pinky_eaten" = "transparent",
"Inky" = "cyan", "Inky_weak" = "blue", "Inky_eaten" = "transparent",
"Clyde" = "orange", "Clyde_weak" = "blue", "Clyde_eaten" = "transparent"
)
```

```{r base-layer-colours}
base_layer <- base_layer +
scale_size_manual(values = c("wall" = 2.5, "door" = 1, "big" = 2.5, "normal" = 0.5, "eaten" = 3)) +
scale_fill_manual(breaks = names(map_colours), values = map_colours) +
scale_colour_manual(breaks = names(map_colours), values = map_colours)
```

```{r base-layer-plot, echo = FALSE}
base_layer
```

My `base_layer` here is not really helpful, so I temporarily added some elements to help me draw everything on it.
*Note*: I won't use it in the following.

```{r base-layer-dev}
base_layer +
scale_x_continuous(breaks = 0:21, sec.axis = dup_axis()) +
scale_y_continuous(breaks = 0:26, sec.axis = dup_axis()) +
theme(
panel.grid.major = element_line(colour = "white"),
axis.text = element_text(colour = "white")
) +
annotate("rect", xmin = 0, xmax = 21, ymin = 0, ymax = 26, fill = NA)
```

Quite better, isn't it?!

#### The grid layer

Here, I am calling "grid", the walls of the maze.
For this grid, I started drawing the vertical lines on the left side of the maze (as you may have noticed, the first level is symmetrical).

```{r left-vertical}
left_vertical_segments <- tribble(
~x, ~y, ~xend, ~yend,
0, 0, 0, 9,
0, 17, 0, 26,
2, 4, 2, 5,
2, 19, 2, 20,
2, 22, 2, 24,
4, 4, 4, 7,
4, 9, 4, 12,
4, 14, 4, 17,
4, 19, 4, 20,
4, 22, 4, 24,
6, 2, 6, 5,
6, 9, 6, 12,
6, 14, 6, 20,
6, 22, 6, 24,
8, 4, 8, 5,
8, 9, 8, 10,
8, 12, 8, 15,
8, 19, 8, 20,
8, 22, 8, 24
)
```

```{r left-vertical-plot}
base_layer +
geom_segment(
data = left_vertical_segments,
mapping = aes(x = x, y = y, xend = xend, yend = yend),
lineend = "round",
inherit.aes = FALSE,
colour = "white"
)
```

Then, I added the horizontal lines (still only on the left side of the maze)!

```{r left-horizontal}
left_horizontal_segments <- tribble(
~x, ~y, ~xend, ~yend,
0, 0, 10, 0,
2, 2, 8, 2,
0, 4, 2, 4,
8, 4, 10, 4,
0, 5, 2, 5,
8, 5, 10, 5,
2, 7, 4, 7,
6, 7, 8, 7,
0, 9, 4, 9,
8, 9, 10, 9,
8, 10, 10, 10,
0, 12, 4, 12,
8, 12, 10, 12,
0, 14, 4, 14,
8, 15, 9, 15,
0, 17, 4, 17,
6, 17, 8, 17,
2, 19, 4, 19,
8, 19, 10, 19,
2, 20, 4, 20,
8, 20, 10, 20,
2, 22, 4, 22,
6, 22, 8, 22,
2, 24, 4, 24,
6, 24, 8, 24,
0, 26, 10, 26
)

left_segments <- bind_rows(left_vertical_segments, left_horizontal_segments)
```

```{r left-plot}
base_layer +
geom_segment(
data = left_segments,
mapping = aes(x = x, y = y, xend = xend, yend = yend),
lineend = "round",
inherit.aes = FALSE,
colour = "white"
)
```

The maze is slowly appearing, but surely.
As I wrote earlier, the first level is symmetrical, so I used my left lines `left_segments` to compute all the lines on the right `right_segments`.

```{r right}
right_segments <- mutate(
.data = left_segments,
x = abs(x - 20),
xend = abs(xend - 20)
)
```

```{r right-plot}
base_layer +
geom_segment(
data = bind_rows(left_segments, right_segments),
mapping = aes(x = x, y = y, xend = xend, yend = yend),
lineend = "round",
inherit.aes = FALSE,
colour = "white"
)
```

The middle vertical lines were missing, *i.e.*, I did not want to plot them twice, which would have happen, if I added these in `left_segments`.
Also, the "door" of the ghost spawn area is missing.
I added the door and the missing vertical walls in the end.

```{r middle}
centre_vertical_segments <- tribble(
~x, ~y, ~xend, ~yend,
10, 2, 10, 4,
10, 7, 10, 9,
10, 17, 10, 19,
10, 22, 10, 26
)
door_segment <- tibble(x = 9, y = 15, xend = 11, yend = 15, type = "door")
```

Finally, I combined all the segments and drew them all.

```{r maze}
maze_walls <- bind_rows(
left_segments,
centre_vertical_segments,
right_segments
) %>%
mutate(type = "wall") %>%
bind_rows(door_segment)
```

```{r maze-plot}
base_layer +
geom_segment(
data = maze_walls,
mapping = aes(x = x, y = y, xend = xend, yend = yend),
lineend = "round",
inherit.aes = FALSE,
colour = "white"
)
```

The maze is now complete, but no-one can actually see the door, since it appears the same way as the walls.
You may have noticed, I added a column named `type`.
`type` can currently hold two values: `"wall"` and `"door"`.
I am going to use `type` as values for two aesthetics, you may already have guessed which ones.
The answer is the `colour` and `size` aesthetics.

```{r maze-plot-colour}
base_layer +
geom_segment(
data = maze_walls,
mapping = aes(x = x, y = y, xend = xend, yend = yend, colour = type, size = type),
lineend = "round",
inherit.aes = FALSE
)
```

*Note: `maze_walls` is a dataset of `ggpacman` (`data("maze_walls", package = "ggpacman")`).*

#### The bonus points layer

The strategy was quite the same as for the grid layer:

* Setting up the point coordinates for the left side and the middle.
* Compute the coordinates for the right side.
* Use a column `type` for the two types of bonus points, *i.e.*, `"normal"` and `"big"` (the one who weaken the ghosts).

```{r bonus-points}
bonus_points_coord <- function() {
left_bonus_points <- tribble(
~x, ~y, ~type,
1, c(1:3, 7:8, 18:22, 24:25), "normal",
1, c(6, 23), "big",
2, c(1, 3, 6, 8, 18, 21, 25), "normal",
3, c(1, 3:6, 8, 18, 21, 25), "normal",
4, c(1, 3, 8, 18, 21, 25), "normal",
5, c(1, 3:25), "normal",
6, c(1, 6, 8, 21, 25), "normal",
7, c(1, 3:6, 8, 18:21, 25), "normal",
8, c(1, 3, 6, 8, 18, 21, 25), "normal",
9, c(1:3, 6:8, 18, 21:25), "normal"
)

bind_rows(
left_bonus_points,
tribble(
~x, ~y, ~type,
10, c(1, 21), "normal"
),
mutate(left_bonus_points, x = abs(x - 20))
) %>%
unnest("y")
}
maze_points <- bonus_points_coord()
```

```{r maze-point-plot}
maze_layer <- base_layer +
geom_segment(
data = maze_walls,
mapping = aes(x = x, y = y, xend = xend, yend = yend, colour = type, size = type),
lineend = "round",
inherit.aes = FALSE
) +
geom_point(
data = maze_points,
mapping = aes(x = x, y = y, size = type, colour = type),
inherit.aes = FALSE
)
```

```{r maze-layer-show, echo = FALSE}
maze_layer
```

*Note: `maze_points` is a dataset of `ggpacman` (`data("maze_points", package = "ggpacman")`).*

### Pac-Man character

It is now time to draw the main character.
To draw Pac-Man, I needed few things:

* The Pac-Man moves, *i.e.*, all the coordinates where Pac-Man is supposed to be at every `step`.
```{r pacman-position}
data("pacman", package = "ggpacman")
unnest(pacman, c("x", "y"))
```
```{r pacman-position-plot}
maze_layer +
geom_point(
data = unnest(pacman, c("x", "y")),
mapping = aes(x = x, y = y, colour = colour),
size = 4
)
```

* The Pac-Man shape (open and closed mouth). Since, Pac-Man is not a complete circle shape, I used `geom_arc_bar()` (from [`ggforce`](https://ggforce.data-imaginist.com/)), and defined the properties of each state of Pac-Man based on the aesthetics required by this function.
*Note*: At first, I wanted a smooth animation/transition of Pac-Man opening and closing its mouth, this is why there are four `"close_"` states.
```{r pacman-state}
pacman_state <- tribble(
~state, ~start, ~end,
"open_right", 14 / 6 * pi, 4 / 6 * pi,
"close_right", 15 / 6 * pi, 3 / 6 * pi,
"open_up", 11 / 6 * pi, 1 / 6 * pi,
"close_up", 12 / 3 * pi, 0 / 6 * pi,
"open_left", 8 / 6 * pi, - 2 / 6 * pi,
"close_left", 9 / 6 * pi, - 3 / 6 * pi,
"open_down", 5 / 6 * pi, - 5 / 6 * pi,
"close_down", pi, - pi
)
```
```{r pacman-state-plot}
ggplot() +
geom_arc_bar(
data = pacman_state,
mapping = aes(x0 = 0, y0 = 0, r0 = 0, r = 0.5, start = start, end = end),
fill = "yellow",
inherit.aes = FALSE
) +
facet_wrap(vars(state), ncol = 4)
```

Once those things available, how to make Pac-Man look where he is headed?
Short answer, I just computed the differences between two successive positions of Pac-Man and added both open/close state to a new column `state`.

```{r pacman-position-state}
pacman %>%
unnest(c("x", "y")) %>%
mutate(
state_x = sign(x - lag(x)),
state_y = sign(y - lag(y)),
state = case_when(
(is.na(state_x) | state_x %in% 0) & (is.na(state_y) | state_y %in% 0) ~ list(c("open_right", "close_right")),
state_x == 1 & state_y == 0 ~ list(c("open_right", "close_right")),
state_x == -1 & state_y == 0 ~ list(c("open_left", "close_left")),
state_x == 0 & state_y == -1 ~ list(c("open_down", "close_down")),
state_x == 0 & state_y == 1 ~ list(c("open_up", "close_up"))
)
) %>%
unnest("state")
```

Here, in preparation for [`gganimate`](https://gganimate.com/), I also added a column `step` before merging the new upgraded `pacman` (*i.e.*, with the Pac-Man `state` column) with the `pacman_state` defined earlier.

```{r pacman-moves}
pacman_moves <- ggpacman::compute_pacman_coord(pacman)
pacman_moves
```

```{r pacman-moves-plots}
maze_layer +
geom_arc_bar(
data = pacman_moves,
mapping = aes(x0 = x, y0 = y, r0 = 0, r = 0.5, start = start, end = end, colour = colour, fill = colour, group = step),
inherit.aes = FALSE
)
```

You can't see much?!
Ok, perhaps it's time to use [`gganimate`](https://gganimate.com/).
I am going to animate Pac-Man based on the column `step`, which is, if you looked at the code above, just the line number of `pacman_moves`.

```{r pacman-animated}
animated_pacman <- maze_layer +
geom_arc_bar(
data = pacman_moves,
mapping = aes(x0 = x, y0 = y, r0 = 0, r = 0.5, start = start, end = end, colour = colour, fill = colour, group = step),
inherit.aes = FALSE
) +
transition_manual(step)
```

```{r pacman-plot-animated, echo = FALSE, message = FALSE}
animate(
plot = animated_pacman,
width = 3.7 * 2.54,
height = 4.7 * 2.54,
units = "cm",
res = 120,
bg = "black",
duration = 10,
renderer = gifski_renderer()
)
```

*Note: `pacman` is a dataset of `ggpacman` (`data("pacman", package = "ggpacman")`).*

### The Ghosts characters

Time to draw the ghosts, namely: Blinky, Pinky, Inky and Clyde.

#### Body

I started with the body, especially the top and the bottom part of the ghost which are half circle (or at least I chose this) and use again `geom_arc_bar()`.

```{r ghost-arc}
ghost_arc <- tribble(
~x0, ~y0, ~r, ~start, ~end, ~part,
0, 0, 0.5, - 1 * pi / 2, 1 * pi / 2, "top",
-0.5, -0.5 + 1/6, 1 / 6, pi / 2, 2 * pi / 2, "bottom",
-1/6, -0.5 + 1/6, 1 / 6, pi / 2, 3 * pi / 2, "bottom",
1/6, -0.5 + 1/6, 1 / 6, pi / 2, 3 * pi / 2, "bottom",
0.5, -0.5 + 1/6, 1 / 6, 3 * pi / 2, 2 * pi / 2, "bottom"
)
```

```{r ghost-top}
top <- ggplot() +
geom_arc_bar(
data = ghost_arc[1, ],
mapping = aes(x0 = x0, y0 = y0, r0 = 0, r = r, start = start, end = end)
) +
coord_fixed(xlim = c(-1, 1), ylim = c(-1, 1))
```

```{r ghost-top-plot, echo = FALSE}
top
```

I retrieved the coordinates of the created polygon, using `ggplot_build()`.

```{r ghost-top-polygon}
top_polygon <- ggplot_build(top)$data[[1]][, c("x", "y")]
```

And I proceeded the same way for the bottom part of the ghost.

```{r ghost-bottom}
bottom <- ggplot() +
geom_arc_bar(
data = ghost_arc[-1, ],
mapping = aes(x0 = x0, y0 = y0, r0 = 0, r = r, start = start, end = end)
) +
coord_fixed(xlim = c(-1, 1), ylim = c(-1, 1))
```

```{r ghost-bottom-plot, echo = FALSE}
bottom
```

```{r ghost-bottom-polygon}
bottom_polygon <- ggplot_build(bottom)$data[[1]][, c("x", "y")]
```
Then, I just added one point to "properly" link the top and the bottom part.

```{r ghost-body}
ghost_body <- dplyr::bind_rows(
top_polygon,
dplyr::tribble(
~x, ~y,
0.5, 0,
0.5, -0.5 + 1/6
),
bottom_polygon,
dplyr::tribble(
~x, ~y,
-0.5, -0.5 + 1/6,
-0.5, 0
)
)
```

I finally got the whole ghost shape I was looking for.

```{r ghost-body-plot}
ggplot() +
coord_fixed(xlim = c(-1, 1), ylim = c(-1, 1)) +
geom_polygon(
data = ghost_body,
mapping = aes(x = x, y = y),
inherit.aes = FALSE
)
```

*Note: `ghost_body` is a dataset of `ggpacman` (`data("ghost_body", package = "ggpacman")`).*
*Note: `ghost_body` definitely needs some code refactoring.*

#### Eyes

The eyes are quite easy to draw, they are just circles, but ...
As for Pac-Man before, I wanted the ghosts to look where they are headed.
This implies moving the iris one way or the other, and so I defined five states for the iris: right, down, left, up and middle.

```{r ghost-eyes}
ghost_eyes <- tribble(
~x0, ~y0, ~r, ~part, ~direction,
1/5, 1/8, 1/8, "eye", c("up", "down", "right", "left", "middle"),
-1/5, 1/8, 1/8, "eye", c("up", "down", "right", "left", "middle"),
5/20, 1/8, 1/20, "iris", "right",
-3/20, 1/8, 1/20, "iris", "right",
1/5, 1/16, 1/20, "iris", "down",
-1/5, 1/16, 1/20, "iris", "down",
3/20, 1/8, 1/20, "iris", "left",
-5/20, 1/8, 1/20, "iris", "left",
1/5, 3/16, 1/20, "iris", "up",
-1/5, 3/16, 1/20, "iris", "up",
1/5, 1/8, 1/20, "iris", "middle",
-1/5, 1/8, 1/20, "iris", "middle"
) %>%
unnest("direction")
```

```{r ghost-eyes-plot}
map_eyes <- c("eye" = "white", "iris" = "black")
ggplot() +
coord_fixed(xlim = c(-0.5, 0.5), ylim = c(-0.5, 0.5)) +
scale_fill_manual(breaks = names(map_eyes), values = map_eyes) +
scale_colour_manual(breaks = names(map_eyes), values = map_eyes) +
geom_circle(
data = ghost_eyes,
mapping = aes(x0 = x0, y0 = y0, r = r, colour = part, fill = part),
inherit.aes = FALSE,
show.legend = FALSE
) +
facet_wrap(vars(direction), ncol = 3)
```

*Note: `ghost_eyes` is a dataset of `ggpacman` (`data("ghost_eyes", package = "ggpacman")`).*

#### Ghost shape

I had the whole ghost shape and the eyes.

```{r ghost-shape-plot}
ggplot() +
coord_fixed(xlim = c(-1, 1), ylim = c(-1, 1)) +
scale_fill_manual(breaks = names(map_colours), values = map_colours) +
scale_colour_manual(breaks = names(map_colours), values = map_colours) +
geom_polygon(
data = get(data("ghost_body", package = "ggpacman")),
mapping = aes(x = x, y = y),
inherit.aes = FALSE
) +
geom_circle(
data = get(data("ghost_eyes", package = "ggpacman")),
mapping = aes(x0 = x0, y0 = y0, r = r, colour = part, fill = part),
inherit.aes = FALSE,
show.legend = FALSE
) +
facet_wrap(vars(direction), ncol = 3)
```

Again, same as for Pac-Man, in order to know where the ghosts are supposed to look, I computed the differences of each successive positions of the ghosts and I added the corresponding directions.

```{r blinky-ghost}
blinky_ghost <- tibble(x = c(0, 1, 1, 0, 0), y = c(0, 0, 1, 1, 0), colour = "Blinky") %>%
unnest(c("x", "y")) %>%
mutate(
X0 = x,
Y0 = y,
state_x = sign(round(x) - lag(round(x))),
state_y = sign(round(y) - lag(round(y))),
direction = case_when(
(is.na(state_x) | state_x %in% 0) & (is.na(state_y) | state_y %in% 0) ~ "middle",
state_x == 1 & state_y == 0 ~ "right",
state_x == -1 & state_y == 0 ~ "left",
state_x == 0 & state_y == -1 ~ "down",
state_x == 0 & state_y == 1 ~ "up"
)
) %>%
unnest("direction")
```

```{r blinky-ghost-static, echo = FALSE}
blinky_ghost
```

I also added some noise around the position, *i.e.*, four noised position at each actual position of a ghost.

```{r blinky-ghost-plot}
blinky_ghost <- blinky_ghost %>%
mutate(state = list(1:4)) %>%
unnest("state") %>%
mutate(
step = 1:n(),
noise_x = rnorm(n(), mean = 0, sd = 0.05),
noise_y = rnorm(n(), mean = 0, sd = 0.05)
)
```

```{r blinky-ghost-noise, echo = FALSE}
blinky_ghost
```

Then, I added (*in a weird way I might say*) the polygons coordinates for the body and the eyes.

```{r blinky-ghost-state}
blinky_ghost <- blinky_ghost %>%
mutate(
body = pmap(
.l = list(x, y, noise_x, noise_y),
.f = function(.x, .y, .noise_x, .noise_y) {
mutate(
.data = get(data("ghost_body")),
x = x + .x + .noise_x,
y = y + .y + .noise_y
)
}
),
eyes = pmap(
.l = list(x, y, noise_x, noise_y, direction),
.f = function(.x, .y, .noise_x, .noise_y, .direction) {
mutate(
.data = filter(get(data("ghost_eyes")), direction == .direction),
x0 = x0 + .x + .noise_x,
y0 = y0 + .y + .noise_y,
direction = NULL
)
}
),
x = NULL,
y = NULL
)
```

```{r blinky-ghost-show, echo = FALSE}
blinky_ghost
```

For ease, it is now a call to one function directly on the potion matrix of a ghost.

```{r blinky-moves}
blinky_ghost <- tibble(x = c(0, 1, 1, 0, 0), y = c(0, 0, 1, 1, 0), colour = "Blinky")
blinky_moves <- ggpacman::compute_ghost_coord(blinky_ghost)
```

```{r blinky-plot, message = FALSE}
blinky_plot <- base_layer +
coord_fixed(xlim = c(-1, 2), ylim = c(-1, 2)) +
geom_polygon(
data = unnest(blinky_moves, "body"),
mapping = aes(x = x, y = y, fill = colour, colour = colour, group = step),
inherit.aes = FALSE
) +
geom_circle(
data = unnest(blinky_moves, "eyes"),
mapping = aes(x0 = x0, y0 = y0, r = r, colour = part, fill = part, group = step),
inherit.aes = FALSE
)
```

```{r blinky-plot-static, echo = FALSE}
blinky_plot
```

Again, it is better with an animated GIF.

```{r blinky-animated}
animated_blinky <- blinky_plot + transition_manual(step)
```

```{r blinky-plot-animated, echo = FALSE, message = FALSE}
animate(
plot = animated_blinky,
width = 3.7 * 2.54,
height = 3.7 * 2.54,
units = "cm",
res = 120,
bg = "black",
duration = 10,
renderer = gifski_renderer()
)
```

### How Pac-Man interacts with the maze?

#### Bonus points

For ease, I am using some functions I defined to go quickly to the results of the first part of this readme.
The idea here is to look at all the position in common between Pac-Man (`pacman_moves`) and the bonus points (`maze_points`).
Each time Pac-Man was at the same place as a bonus point, I defined a status `"eaten"` for all values of `step` after.
I ended up with a big table with position and the state of the bonus points.

```{r points-eaten}
pacman_moves <- ggpacman::compute_pacman_coord(get(data("pacman", package = "ggpacman")))
right_join(get(data("maze_points")), pacman_moves, by = c("x", "y")) %>%
distinct(step, x, y, type) %>%
mutate(
step = map2(step, max(step), ~ seq(.x, .y, 1)),
colour = "eaten"
) %>%
unnest("step")
```

Again, for ease, I am using a function I defined to compute everything.

```{r create-data}
pacman_moves <- ggpacman::compute_pacman_coord(get(data("pacman", package = "ggpacman")))
bonus_points_eaten <- ggpacman::compute_points_eaten(get(data("maze_points")), pacman_moves)
```

If you don't recall, `maze_layer` already includes a geometry with the bonus points.

```{r reminder-maze, echo = FALSE}
maze_layer
```

I could have change this geometry (*i.e.*, `geom_point()`), but I did not, and draw a new geometry on top of the previous ones.
Do you remember the values of the scale for the size aesthetic?

```{r reminder-scale, eval = FALSE}
scale_size_manual(values = c("wall" = 2.5, "door" = 1, "big" = 2.5, "normal" = 0.5, "eaten" = 3))
```

```{r points-eaten-plot-code}
maze_layer_points <- maze_layer +
geom_point(
data = bonus_points_eaten,
mapping = aes(x = x, y = y, colour = colour, size = colour, group = step),
inherit.aes = FALSE
)
```

```{r points-eaten-plot, echo = FALSE}
maze_layer_points
```

A new animation to see, how the new geometry is overlapping the previous one as `step` increases.

```{r points-eaten-animated}
animated_points <- maze_layer_points + transition_manual(step)
```

```{r points-eaten-plot-animated, echo = FALSE, message = FALSE}
animate(
plot = animated_points,
width = 3.7 * 2.54,
height = 4.7 * 2.54,
units = "cm",
res = 120,
bg = "black",
duration = 10,
renderer = gifski_renderer()
)
```

#### Ghost `"weak"` and `"eaten"` states

The ghosts were more tricky (I know, they are ghosts ...).

I first retrieved all the positions where a `"big"` bonus point was eaten by Pac-Man.

```{r vulnerability}
ghosts_vulnerability <- bonus_points_eaten %>%
filter(type == "big") %>%
group_by(x, y) %>%
summarise(step_init = min(step)) %>%
ungroup() %>%
mutate(
step = map(step_init, ~ seq(.x, .x + 30, 1)),
vulnerability = TRUE,
x = NULL,
y = NULL
) %>%
unnest("step")
```

```{r vulnerability-values, echo = FALSE}
ghosts_vulnerability
```

This is part of a bigger function (I won't dive too deep into it).

```{r compute-function}
ggpacman::compute_ghost_status
```

The goal of this function, is to compute the different states of a ghost, according to the bonus points eaten and, of course, the current Pac-Man position at a determined `step`.

```{r ghost-moves-small}
pacman_moves <- ggpacman::compute_pacman_coord(get(data("pacman", package = "ggpacman")))
bonus_points_eaten <- ggpacman::compute_points_eaten(get(data("maze_points")), pacman_moves)
ghost_moves <- ggpacman::compute_ghost_status(
ghost = get(data("blinky", package = "ggpacman")),
pacman_moves = pacman_moves,
bonus_points_eaten = bonus_points_eaten
)
ghost_moves %>%
filter(state == 1) %>%
distinct(step, direction, colour, vulnerability) %>%
as.data.frame()
```

To simplify a little, below a small example of a ghost moving in one direction with predetermined states.

```{r blinky-moves-state}
blinky_ghost <- bind_rows(
tibble(x = 1:4, y = 0, colour = "Blinky"),
tibble(x = 5:8, y = 0, colour = "Blinky_weak"),
tibble(x = 9:12, y = 0, colour = "Blinky_eaten")
)
blinky_moves <- ggpacman::compute_ghost_coord(blinky_ghost)
```

```{r blinky-moves-small, echo = FALSE}
blinky_moves
```

```{r blinky-plot-state}
blinky_plot <- base_layer +
coord_fixed(xlim = c(0, 13), ylim = c(-1, 1)) +
geom_polygon(
data = unnest(blinky_moves, "body"),
mapping = aes(x = x, y = y, fill = colour, colour = colour, group = step),
inherit.aes = FALSE
) +
geom_circle(
data = unnest(blinky_moves, "eyes"),
mapping = aes(x0 = x0, y0 = y0, r = r, colour = part, fill = part, group = step),
inherit.aes = FALSE
)
```

```{r blinky-plot-line, echo = FALSE, fig.height = 0.5, fig.width = 3.7}
blinky_plot
```

I am sure, you remember all the colours and their mapped values from the beginning, so you probably won't need the following to understand of the ghost disappeared.

```{r reminder-colours, eval = FALSE}
"Blinky" = "red", "Blinky_weak" = "blue", "Blinky_eaten" = "transparent",
```

*Note: yes, `"transparent"` is a colour and a very handy one.*

A new animation to see our little Blinky in action?

```{r blinky-state-animated}
animated_blinky <- blinky_plot + transition_manual(step)
```

```{r blinky-state-plot-animated, echo = FALSE}
animate(
plot = animated_blinky,
width = 3.7 * 2.54,
height = 0.5 * 2.54,
units = "cm",
res = 120,
bg = "black",
duration = 10,
renderer = gifski_renderer()
)
```

### Plot time (to summarise a little (or a lot))

In the current version, nearly everything is either a dataset or a function and could be used like this.

1. Load and compute the data.
```{r plot-time-data}
data("pacman", package = "ggpacman")
data("maze_points", package = "ggpacman")
data("maze_walls", package = "ggpacman")
data("blinky", package = "ggpacman")
data("pinky", package = "ggpacman")
data("inky", package = "ggpacman")
data("clyde", package = "ggpacman")
ghosts <- list(blinky, pinky, inky, clyde)
pacman_moves <- ggpacman::compute_pacman_coord(pacman)
bonus_points_eaten <- ggpacman::compute_points_eaten(maze_points, pacman_moves)
map_colours <- c(
"READY!" = "goldenrod1",
"wall" = "dodgerblue3", "door" = "dodgerblue3",
"normal" = "goldenrod1", "big" = "goldenrod1", "eaten" = "black",
"Pac-Man" = "yellow",
"eye" = "white", "iris" = "black",
"Blinky" = "red", "Blinky_weak" = "blue", "Blinky_eaten" = "transparent",
"Pinky" = "pink", "Pinky_weak" = "blue", "Pinky_eaten" = "transparent",
"Inky" = "cyan", "Inky_weak" = "blue", "Inky_eaten" = "transparent",
"Clyde" = "orange", "Clyde_weak" = "blue", "Clyde_eaten" = "transparent"
)
```

2. Build the base layer with the maze.
```{r plot-time-base}
base_grid <- ggplot() +
theme_void() +
theme(
legend.position = "none",
plot.caption = element_textbox_simple(halign = 0.5, colour = "white"),
plot.caption.position = "plot",
plot.background = element_rect(fill = "black", colour = "black"),
panel.background = element_rect(fill = "black", colour = "black")
) +
labs(caption = "© Mickaël 'Coeos' Canouil") +
scale_size_manual(values = c("wall" = 2.5, "door" = 1, "big" = 2.5, "normal" = 0.5, "eaten" = 3)) +
scale_fill_manual(breaks = names(map_colours), values = map_colours) +
scale_colour_manual(breaks = names(map_colours), values = map_colours) +
coord_fixed(xlim = c(0, 20), ylim = c(0, 26)) +
geom_segment(
data = maze_walls,
mapping = aes(x = x, y = y, xend = xend, yend = yend, size = type, colour = type),
lineend = "round",
inherit.aes = FALSE
) +
geom_point(
data = maze_points,
mapping = aes(x = x, y = y, size = type, colour = type),
inherit.aes = FALSE
) +
geom_text(
data = tibble(x = 10, y = 11, label = "READY!", step = 1:20),
mapping = aes(x = x, y = y, label = label, colour = label, group = step),
size = 6
)
```

```{r base-grid-final}
base_grid
```

3. Draw the `"eaten"` bonus points geometry.
```{r plot-time-points}
p_points <- list(
geom_point(
data = bonus_points_eaten,
mapping = aes(x = x, y = y, colour = colour, size = colour, group = step),
inherit.aes = FALSE
)
)
```

```{r base-grid-points-final}
base_grid + p_points
```

4. Draw the main character (I am talking about Pac-Man ...)
```{r plot-time-pacman}
p_pacman <- list(
geom_arc_bar(
data = pacman_moves,
mapping = aes(
x0 = x, y0 = y,
r0 = 0, r = 0.5,
start = start, end = end,
colour = colour, fill = colour,
group = step
),
inherit.aes = FALSE
)
)
```

```{r base-grid-pacman-final}
base_grid + p_pacman
```

5. Draw the ghosts, using the trick that `+` works also on a list of geometries.
```{r plot-time-ghosts}
p_ghosts <- map(.x = ghosts, .f = function(data) {
ghost_moves <- compute_ghost_status(
ghost = data,
pacman_moves = pacman_moves,
bonus_points_eaten = bonus_points_eaten
)
list(
geom_polygon(
data = unnest(ghost_moves, "body"),
mapping = aes(
x = x, y = y,
fill = colour, colour = colour,
group = step
),
inherit.aes = FALSE
),
geom_circle(
data = unnest(ghost_moves, "eyes"),
mapping = aes(
x0 = x0, y0 = y0,
r = r,
colour = part, fill = part,
group = step
),
inherit.aes = FALSE
)
)
})
```

```{r base-grid-ghosts-final}
base_grid + p_ghosts
```

6. Draw everything.
```{r plot-time-all}
base_grid + p_points + p_pacman + p_ghosts
```

7. Animate everything.
```{r plot-time-all-transition}
PacMan <- base_grid + p_points + p_pacman + p_ghosts + transition_manual(step)
```

```{r plot-time-all-animated, echo = FALSE}
animate(
plot = PacMan,
width = 3.7 * 2.54,
height = 4.7 * 2.54,
units = "cm",
res = 120,
bg = "black",
duration = 10,
renderer = gifski_renderer()
)
```

## Getting help

If you encounter a clear bug, please file a minimal reproducible example on [github](https://github.com/mcanouil/ggpacman/issues).
For questions and other discussion, please contact the package maintainer.

---

Please note that this project is released with a [Contributor Code of Conduct](https://github.com/mcanouil/ggpacman/blob/master/.github/CODE_OF_CONDUCT.md). participating in this project you agree to abide by its terms.