Eu tenho jogado Gwent: the Witcher Card Game há algum tempo, e é impressionante a quantidade de combos e sinergias que podem haver entre as cartas de acordo com o deck que você monta. Neste post, eu tento identificar as combinações de cartas que aparecem com maior frequência através de uma análise das regras de associação entre elas.
Gwent é um jogo de cartas do universo de The Witcher no qual dois jogadores se enfrentam em busca da maior pontuação em pelo menos 2 de 3 rodadas, cada uma com no máximo uns 10 turnos para cada jogador. Esta pontuação é dada pelo poder de cada carta e, também, através da forma que elas interagem entre si. A tabela abaixo traz um exemplo disto para 4 cartas pertencentes aos decks da facção Scoia’tael - uma das 6 facções existentes no jogo.
# carrega os pacotes
library(tidyverse) # core
library(reactable) # para tabelas interativas
library(reactablefmtr) # para embedar imagens no reactable
# carrega o exemplo
read_rds(file = 'data/decks.rds') %>%
# pegando quatro cartas de exemplo
filter(localizedName %in% c('Malena', 'Brigada Vrihedd',
'Dol Blathanna: Guarda' , 'Bruxo Gato')) %>%
# extraindo um exemplo único de cada carta
distinct(small, localizedName, power, texto) %>%
# juntando o prefixo do link da imagem
mutate(small = paste0('https://www.playgwent.com/', small)) %>%
# organiza as cartas em ordem alfabetica
arrange(localizedName) %>%
# colocando os exemplos em um reactable
reactable(
compact = TRUE, borderless = TRUE, defaultColDef = colDef(align = 'left'),
style = list(fontFamily = "Fira Sans", fontSize = "12px"),
columns = list(
small = colDef(name = '', cell = embed_img(height = 80, width = 60), maxWidth = 80),
localizedName = colDef(name = 'Carta', maxWidth = 140),
power = colDef(name = 'Poder', maxWidth = 50),
texto = colDef(name = 'Descrição')
)
)
Se utilizarmos um exemplar de cada uma destas cartas em uma rodada, temos um poder total de 16 (i.e., cada uma das 4 cartas tem 4 de poder). Todavia, a cada turno dentro desta rodada, podemos tomar vantagem de cada uma das cartas:
Malena
para movimentar a Brigada Vrihedd
para outra parte do tabuleiro, causando 2 de dano ao oponente (i.e., removendo 2 pontos do oponente);Dol Blathanna: Guarda
, fazendo com que a Brigada Vrihedd
ganhe mais 1 de poder;Bruxo Gato
, causando mais 1 ponto de dano ao oponente (ou 2 pontos de dano, se a rodada estiver próxima do fim);Bruxo Gato
se movimentou no tabuleiro, acionamos novamente a habilidade da carta Dol Blathanna: Guarda
, adicionando 1 ponto de poder à ela.O combo destas 4 cartas é capaz criar uma diferença de 5 pontos entre nós e o oponente à cada turno em uma rodada (i.e., removendo um total de 3 pontos de poder dele e adicionando 2 pontos de poder à nós mesmos). Se você contar os 10 turnos da rodada, e o fato de que precisamos de 4 turnos para baixar estas cartas, este combo deve girar por uns 6 turnos - nos dando uma vantagem de 30 pontos ao final dele, caso o oponente não tome nenhuma contra-medida. Além disso, ainda existem os pontos que vamos abrindo de vantagem enquanto baixamos o combo, e que dependem muito mais da estratégia de jogo (i.e., que cartas baixar em que turnos) do que da estratégia de montagem do deck em si (i.e., que cartas incluir em um deck). De toda forma, sem as cartas certas no deck não há estratégia de jogo que segure…portanto, acredito que é muito importante saber identificar estas cartas e combiná-las da melhor forma possível.
Uma forma de identificarmos estas combinações é olhar os próprios decks existentes e tentar mapear os padrões de co-ocorrência das cartas. Neste contexto, se duas ou mais cartas tendem a aparecer juntas com grande frequência entre os decks é porquê, possivelmente, esta combinação pode estar envolvida em um combo. Assim, se conseguirmos identificar os padrões de co-ocorrência entre as cartas, poderíamos usar esta informação para desenhar decks que cujas cartas tenham maior sinergia entre si - garantindo que as contra-medidas não anulem totalmente a nossa estratégia.
Com isto em mente, vou utilizar o scrapper da biblioteca de decks de Gwent que desenvolvi em outro post para analisar os padrões de co-ocorrência entre cartas de Gwent. Meu intuito aqui vai ser preparar a base de dados que já havíamos obtido anteriormente, fazer uma breve análise exploratória dos padrões existentes e, então, utilizar um algoritmo para minear regras de associações para: (1) identificar os conjuntos de cartas que ocorrem com uma frequência maior do que àquela esperada ao acaso, e (2) determinar quão diferentes são as regras de associação detectadas. Eu não espero que ter acesso à essas informações vá me dar uma vantagem competitiva frente aos outros jogadores, até porquê talento para estratégia de jogo eu não tenho, mas acredito que elas possam acabar me ajudando a montar decks mais efetivos do que aqueles horríveis que monto atualmente.
Antes de começar a olhar os dados e tudo o mais, vou fazer um breve resumo sobre as peculiaridades e regras-padrão que devem ser seguidas para montagem de um deck de um Gwent. Isto vai facilitar bastante o entendimento dos dados que teremos à nossa disposição, bem como o contexto da análise exploratória dos dados.
Monsters
, Nilfgaard
, Northern Realms
, Scoia'tael
, Skellige
ou Syndicate
;Syndicate
podem ser usadas junto dos decks de outras facções (mas essas são a minoria das cartas do Syndicate
);Vantagem Tática
;O scrapper da biblioteca de decks de Gwent nos fornece a lista de decks contribuídos pela comunidade no site oficial do jogo, bem como a composição de cartas associadas à cada um deles. Como existem atualizações relativamente frequentes do jogo, algumas cartas sofrem nerfs e outras podem acabar recebendo um boost, o que faz com que decks que tenham sido editados há muito tempo possam estar meio defasados com relação à sua performance. Além disso, os decks podem receber likes e dislikes da comunidade, o que funciona como um termômetro do quão bom aquele deck deve ser. Assim, resolvi focar apenas nos decks que tivessem sido editados no ano de 2021 e que receberam pelo menos um voto. Com isso, é mais provável que vamos usar decks que estejam mais atualizados e validados de alguma forma pela comunidade. O pedaço de código abaixo carrega a lista de decks disponíveis no site oficial do jogo quando escrevi este post, e os apresenta em uma tabela.
# carregando os pacotes
library(tidytext) # para ajudar a trabalhar com texto
library(ggridges) # para os ridge plots
library(plotly) # para visualizacao com interatividade
library(igraph) # para plotar grafos
library(fs) # para manipular os paths
# carregando os metadados de cada deck
metadados <- read_rds(file = 'data/lista_de_decks.rds')
# fazendo alguns ajustes à base dos metadados
metadados <- metadados %>%
# selecionando apenas as colunas desejadas
select(deck = id, name, faccao = slug, ano = modified, language, votes, craftingCost) %>%
# implementando pequenos ajustes aos dados
mutate(
# ajustando a coluna de data
ano = lubridate::as_datetime(x = ano),
ano = lubridate::year(x = ano),
# ajustando coluna de slug
faccao = str_to_title(string = faccao),
faccao = case_when(faccao == 'Northernrealms' ~ 'Northern Realms',
faccao == 'Scoiatael' ~ "Scoia'tael",
TRUE ~ faccao)
)
# printando a tabela como um reactable
metadados %>%
reactable(
sortable = TRUE, filterable = TRUE, compact = TRUE,
highlight = TRUE, borderless = TRUE, showPageSizeOptions = TRUE,
defaultColDef = colDef(align = 'center'), defaultPageSize = 5,
style = list(fontFamily = "Fira Sans", fontSize = "12px"),
columns = list(
deck = colDef(name = 'Deck'),
name = colDef(name = 'Nome'),
ano = colDef(name = 'Ano de Edição'),
faccao = colDef(name = 'Facção'),
language = colDef(name = 'Origem'),
votes = colDef(name = 'Votos'),
craftingCost = colDef(name = 'Custo de Criação')
)
)
A segunda parte do scrapper usa o código identificador único de cada deck (i.e., coluna Deck
na tabela acima) para obter a sua composição de cartas e os metadados das mesmas. Eu raspei todos os decks atenderam aos dois requisitos descritos acima, compilei os resultados para um único dataframe
e fiz alguns pequenos ajustes à base, só para fins de entendimento e clareza mesmo. O código abaixo carrega a base de dados com as cartas encontradas em cada deck, e cria uma tabela para visualizarmos todas as informações existentes ao nível do deck, apenas para fins de entendimento da estrutura de dados1.
# carregando os dados dos decks
decks <- read_rds(file = 'data/decks.rds')
# fazendo alguns ajustes aos dados dos decks
decks <- decks %>%
# removendo algumas informacoes que nao precisamos
select(-small, -big, -fluff, -ownable, -short, -categoryName, -primaryCategoryId, -name) %>%
# ajustando as colunas
mutate(
# passando o id do deck para um inteiro, para bater com os metadados
deck = as.integer(deck),
# ajustando coluna do slug
slug = str_to_title(string = slug),
slug = case_when(slug == 'Northernrealms' ~ 'Northern Realms',
slug == 'Scoiatael' ~ "Scoia'tael",
TRUE ~ slug),
# ajustando coluna do repeat count - quantidade daquela carta no deck
repeatCount = repeatCount + 1,
# contando quantidade de habilidade de cada carta
habilidades = case_when(is.na(keywords) ~ 0,
TRUE ~ str_count(string = keywords, pattern = ';') + 1)
) %>%
# passando os outros strings para maiusculo
mutate(across(.cols = c(rarity, cardGroup, type), .fns = ~ str_to_title(string = .x))) %>%
# juntando com id da faccao
left_join(y = select(metadados, deck, faccao), by = 'deck')
# printando a tabela
decks %>%
# selecionando apenas as colunas cujas informações estejam no nível do deck
select(deck, faccao, card_in_seq, localizedName, repeatCount, slug, type) %>%
# passando para o reactable
reactable(
sortable = TRUE, filterable = TRUE, compact = TRUE,
highlight = TRUE, borderless = TRUE, showPageSizeOptions = TRUE,
defaultColDef = colDef(align = 'center'), defaultPageSize = 5,
style = list(fontFamily = "Fira Sans", fontSize = "12px"),
columns = list(
deck = colDef(name = 'Deck'),
faccao = colDef(name = 'Facção'),
card_in_seq = colDef(name = 'Sequência'),
localizedName = colDef(name = 'Carta'),
repeatCount = colDef(name = 'Unidades'),
slug = colDef(name = 'Facção da Carta'),
type = colDef(name = 'Tipo')
)
)