After the last post building on feedback from readers, the blog is back to the regular program of recycling old Github repos. Today’s project was waiting for its turn here and will involve a Catan card game. Nearly a year ago, I played Catan with my husband who was kind enough to accept our monitoring all rounds. My goal? Producing a nice animated visualization of our game.
I’ll first reckon I don’t even really like card games, board games, you name it. I don’t hate them, but there are thousands other things that I’d rather do even when socializing, like talking or listening. Well in any case we received a game of Catan cards as a present, and it’s actually not a bad game. If you’re curious you can read this page, for this article you only need to know that the game is about getting riches (lumber, grain, etc.) for your territory, which you gain by cards drawn from the market or exchanged with other players. The more riches your territory has, the bigger is your score. You stop playing when one of the players’ score is higher than a limit, or after 20 rounds because you’re too tired to monitor any more round.
Collected data
This is how the data we got looks like:
market <- readr::read_csv("data/2017-01-27-catan_market.csv")
damien <- readr::read_csv("data/2017-01-27-catan_damien.csv")
maelle <- readr::read_csv("data/2017-01-27-catan_maelle.csv")
knitr::kable(head(damien))
victory | round | lumber | brick | grain | wool | ore | settlement | city | knight | city_hall | development | road |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 1 | 3 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
2 | 2 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 2 |
2 | 3 | 1 | 2 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 2 |
2 | 4 | 1 | 2 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 2 |
2 | 5 | 1 | 1 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 2 |
2 | 6 | 0 | 1 | 0 | 2 | 1 | 1 | 0 | 0 | 0 | 0 | 3 |
knitr::kable(head(maelle))
victory | round | lumber | brick | grain | wool | ore | settlement | city | knight | city_hall | development | road |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 1 | 0 | 0 | 0 | 0 | 3 | 1 | 0 | 0 | 0 | 0 | 1 |
1 | 2 | 0 | 0 | 0 | 0 | 3 | 1 | 0 | 0 | 0 | 0 | 1 |
1 | 3 | 0 | 2 | 0 | 0 | 3 | 1 | 0 | 0 | 0 | 0 | 1 |
1 | 4 | 0 | 2 | 0 | 0 | 3 | 1 | 0 | 0 | 0 | 0 | 1 |
1 | 5 | 0 | 2 | 0 | 1 | 3 | 1 | 0 | 0 | 0 | 0 | 1 |
1 | 6 | 0 | 2 | 0 | 1 | 3 | 1 | 0 | 0 | 0 | 0 | 1 |
For players for each turn one has a score which I called victory and a number of cards for each type of riches.
knitr::kable(head(market))
round | lumber | brick | grain | wool | ore |
---|---|---|---|---|---|
1 | 1 | 2 | 0 | 0 | 2 |
2 | 2 | 1 | 0 | 0 | 2 |
3 | 2 | 1 | 0 | 0 | 2 |
4 | 2 | 1 | 0 | 0 | 2 |
5 | 2 | 1 | 0 | 0 | 2 |
6 | 1 | 1 | 0 | 0 | 3 |
The market has the same variables except victory. I even entered the number of cards of each types in tables but chose not to check the total numbers of our collected data made sense because I didn’t have any desire to monitor again, since it makes playing a bit too serious. I promise that this is not how I deal with scientific data.
Design of the visualization
The title of this section sounds fancier than my actual creative process, and well even creative process sounds fancy… Let’s say I had fun making decisions about my future viz. In my mind right from the beginning it was clear that I wanted to create a map of the territories where stuff would change over time to show the evolution of the scores.
Choosing a representation of riches
I am a big fan of the emojifonts
package so I decided to assign an emoji to each type of riches. It was not easy, sometimes emojis wouldn’t render, so I ended up with this list.
emojis <- readr::read_csv("data/2017-01-27-catan_emojis.csv")
knitr::kable(emojis)
name | emoji |
---|---|
lumber | evergreen_tree |
brick | construction |
grain | icecream |
wool | sheep |
ore | lemon |
settlement | tent |
city | house |
knight | horse |
city_hall | european_castle |
development | japanese_castle |
road | truck |
I’m in particular a bit ashamed of lemons as ore but I did not have a better idea. Lemons are not ore but they are a cool thing to own for your territory, okay?
Which territories?
I wanted to draw locations for the riches on a map. I did not want to draw my own territories, so I decided to use existing countries, actually three countries where my husband and I lived together: France, Germany, Spain. I got a file with many cities in the world from this website which I transformed using the following code.
library("dplyr")
cities <- readr::read_csv("data/worldcities1.csv")
names(cities) <- gsub(" ", "_", names(cities))
names(cities) <- gsub("-", "", names(cities))
cities %>%
filter(language_script %in% c("latin", "english")) %>%
select(ISO_31661_country_code,
name,
latitude, longitude) %>%
write_csv(path = "data/2017-01-27-catan_.csv")
In this dataset for each city I get the country and longitudinal coordinates so it will be easy to get cities from the chosen countries.
From the raw data to ready-to-plot data
Since I was going to transform three similar tables, I decided to write functions. Well at the time I first made the viz I copy-pasted code three times but I decided it’d be better not to make my clumsiness public. Instead, I even started using the dplyr
and tidyr
functions with “_”, which I think looks professional.
The first step is to get from the wide to the long data, and to add emojis to each line. We thus have one line per round and riches type, with a number of cards for, say, lumber.
library("tidyr")
library("dplyr")
add_emojis <- function(df, emojis){
.dots <- names(df)[(which(names(df) == "round") + 1):ncol(df)]
df %>% gather_("what", "count",
.dots) %>%
left_join(emojis, by = c("what" = "name")) %>%
filter_(~ count > 0)
}
The second step is to get one line per card of riches, because each of them will be plotted separately, and with an index number for cards of the same type in each round. Why? Well because if a territory had 1 card of lumber and then 2 cards of lumber, at the second round I want the card of lumber from the first round to stay at the same position.
get_one_row_per_riches_unit <- function(df){
df <- df[rep(seq_len(nrow(df)), df$count),]
df %>%
select_(quote(- count)) %>%
group_by_(.dots = lapply(c("round", "what", "emoji"), as.symbol)) %>%
mutate_(which = ~1:n()) %>%
ungroup()
}
Then I draw cities from the territory for each card of lumber, card of ore, etc.
cities <- readr::read_csv("data/2017-01-27-catan_worldcities.csv")
add_location <- function(df, country, cities){
# get cities to put the things
tobelocated <- df %>%
select_(.dots = list("what", "emoji", "which")) %>%
unique()
country <- filter_(cities, ~ISO_31661_country_code == country)
set.seed(3)
where <- sample_n(country, nrow(tobelocated),
replace = FALSE)
tobelocated <- cbind(where, tobelocated)
df <- left_join(df, tobelocated,
by = c("emoji", "which", "what"))
df <- select_(df, ~round, ~what, ~emoji,
~which, ~latitude, ~longitude)
return(df)
}
I also need to add the name of the participant.
add_name <- function(df, name){
mutate_(df, who = ~name)
}
Once we have defined the functions, we can write a pretty purrr
pipeline.
library("purrr")
map_data <- list(market, damien, maelle) %>%
map(add_emojis, emojis = emojis) %>%
map(get_one_row_per_riches_unit) %>%
map2(list("FR", "DE", "ES"),
add_location, cities = cities) %>%
map2(list("Market", "Damien", "Maëlle"),
add_name) %>%
bind_rows()
knitr::kable(head(map_data))
round | what | emoji | which | latitude | longitude | who |
---|---|---|---|---|---|---|
1 | lumber | evergreen_tree | 1 | 49.77894 | 3.213335 | Market |
2 | lumber | evergreen_tree | 1 | 49.77894 | 3.213335 | Market |
2 | lumber | evergreen_tree | 2 | 49.61031 | 0.354705 | Market |
3 | lumber | evergreen_tree | 1 | 49.77894 | 3.213335 | Market |
3 | lumber | evergreen_tree | 2 | 49.61031 | 0.354705 | Market |
4 | lumber | evergreen_tree | 1 | 49.77894 | 3.213335 | Market |
I won’t lie, getting until this point was not all easy. When I read the code from so many months ago I was surprised to still understand it and quite proud of myself if I may say so.
Draw the map!
That’s where the emojifont
and gganimate
magic happen. I don’t only plot riches, but also a crown for each territory whose size depends on the score of the territory.
library("ggplot2")
library("emojifont")
## load selected emoji font
load.emojifont('OpenSansEmoji.ttf')
library("ggmap")
library("gganimate")
library(animation)
map <- get_map(location = "France", maptype = "watercolor",
zoom = 5)
p = ggmap(map, extent = "device") +
geom_text(x = - 9, y = 44,
label = "Maëlland") +
geom_text(x = 7, y = 54,
label = "Damienland") +
geom_text(x = 0, y = 50.5,
label = "Market") +
geom_text(data = damien,
aes(x = 7, y = 54.5,
size = victory, frame = round),
label = emoji("crown"), family='OpenSansEmoji') +
geom_text(data = maelle,
aes(x = - 9, y = 45,
size = victory, frame = round),
label = emoji("crown"), family='OpenSansEmoji') +
geom_text(data = map_data,
aes(x = longitude,
y = latitude,
label = emoji(emoji), family='OpenSansEmoji',
col = who,
frame = round),
size = 5)+
theme(text = element_text(size=20),
legend.position = "none") +
ggtitle("Round")
ani.options(interval = 1, ani.width = 400, ani.height = 400)
gg_animate(p, "catan.gif")
And now, sit back and enjoy!
When I told Miles McBain about this visualization he told me it would be useful for post-game debriefing. So you’ll have to excuse me, I’ll now ask my husband if he wants to debrief the game we played 10 months ago.