#Load packages
rm(list=ls()) # Clear all objects in the global environment
library(tidyverse) # For data manipulation + graphing
library(gtsummary) # Summary statistics
library(knitr) # To compile HTML document
library(readr) # To read and import survey data
library(ggh4x) # Advanced layout options
library(gridExtra) # Advanced layout options
library(huxtable) # Tables
library(lubridate) # To work with dates and times
#Import Data
public <- read_csv("Data/TT Survey 5.csv")
admin <- read_csv("Data/TT Survey 5 Admins.csv")
reddit <- read_csv("Data/TT Survey 5 Reddit.csv")
#Source identifier
public <- public %>% mutate(admin=0, reddit = 0)
admin <- admin %>% mutate(admin=1, reddit = 0)
reddit <- reddit %>% mutate(admin=0, reddit = 1)
#Merge and drop previous
tt<- bind_rows(public, admin, reddit)
rm(public, admin, reddit)
#Cleaning
df <- tt[, 2:73] #Creating a separate work dataframe.
#Clean var names
colnames(df) <- c('sqtime', 'ttime', 'regular', 'sl',
'slserver', 'clan', 'tzone', 'admresp', 'admprof',
'admfair', 'admoverall', 'twork', 'learn',
'balance', 'skill', 'toxic', 'basecamp','comms',
'asset', 'maprot', 'Basrah', 'Anvil', 'Belaya', 'Black Coast',
'Chora', 'Fallujah', 'Fools', 'Goose Bay', 'Gorodok', 'Harju',
'Kamdesh', 'Kohat', 'Kokan','Lashkar', 'Logar', 'Manic',
'Mestia','Mutaha','Narva', 'Pacific', 'Sanxian',
'Skorpo', 'Sumari', 'Tallil','Yehorivka', 'moderot',
'raas', 'aas', 'fraas', 'tc', 'invasion', 'skirm',
'unitsat', 'light', 'medium', 'heavy', 'assym',
'unconv', 'tlf', 'ins', 'imf', 'wpmc', 'votesat',
'votefreq', 'fogless', 'fogsat', 'tcevent',
'vconnect', 'modcomment', 'gencomment', 'admin', 'reddit')
#Remove comments from analysis frame
df <- df %>% subset(select = -c(modcomment, gencomment))
#Designate certain variables as ordered factor variables
#sqtime
df$sqtime <- ordered(df$sqtime, levels = c("Fewer than 100 hours",
"Between 100 - 600 hours",
"Between 600 - 1000 hours",
"1000+ hours"))
levels(df$sqtime) <- c("Fewer than 100 hours",
"Between 100 - 600 hours",
"Between 600 - 1,000 hours",
"1,000+ hours")
#ttime
df$ttime <- ordered(df$ttime, levels = c("Fewer than 6 months",
"Between 6 months - 1 year",
"Between 1 - 2 years", "Over 2 years"))
#regular, sl, ext.clan, fogless awareness
df <- df %>% mutate(across(c(regular, sl, clan, fogless), ~ ordered(., levels = c("Yes", "No"))))
#timezones
df$tzone <- ordered(df$tzone, levels = c("US East",
"US Central",
"US Pacific",
"UK/Continental Europe","Other"))
#Map specific scored variables
df <- df %>%
mutate(across(c('Basrah':'Yehorivka'), ~ case_when(
. == "Much less" ~ 1,
. == "Slighty less" ~ 2,
. == "About the same" ~ 3,
. == "Slightly more" ~ 4,
. == "Much more" ~ 5,
. == "" ~ NA_real_,
is.na(.) ~ NA_real_)))
#Create dummy variables
df <- df %>% mutate(over1k = if_else(sqtime == "1,000+ hours", "Yes", "No"))
df <- df %>% mutate(over1tt = if_else(ttime %in% c("Between 1 - 2 years", "Over 2 years"), "Yes", "No"))
#Other likert variables
df <- df %>%
mutate(across(c(raas:skirm, light:assym, tlf:wpmc, votefreq), ~ case_when(. == "Much less" ~ 1,
. == "Slightly less" ~ 2,
. == "About the same" ~ 3,
. == "Slightly more" ~ 4,
. == "Much more" ~ 5,
. == "" ~ NA_real_,
is.na(.) ~ NA_real_)))
#Graph logic and themes
#Create blank theme to use in all pie charts
blank_theme <- theme_minimal()+
theme(
axis.title.x = element_blank(),
axis.title.y = element_blank(),
panel.border = element_blank(),
panel.grid=element_blank(),
axis.ticks = element_blank(),
plot.title=element_text(size=14, face="bold"))
#Table logic and themes
#Categorical variable tables
cat_table_fmt <-function(htable) {
htable %>% as_hux_table() %>% set_width(1) %>%
set_all_padding(4) %>%
set_outer_padding(0) %>%
theme_article() %>% .[-2, ] %>%
add_footnote("* N is the observation count. ** CI stands for confidence interval") %>%
set_italic(final(1), 1, TRUE)
}
#Continious variable tables
cont_table_fmt <-function(htable) {
htable %>% as_hux_table() %>% set_width(1) %>%
set_all_padding(4) %>%
set_outer_padding(0) %>%
theme_article() %>%
add_footnote("* N is the observation count. ** CI stands for confidence interval") %>%
set_italic(final(1), 1, TRUE)
}
#Difference tables
#Continuous
diff_table_fmt <-function(diff_table) {
diff_table %>% set_width(1) %>%
set_all_padding(4) %>%
set_outer_padding(0) %>%
theme_article() %>%
map_background_color(everywhere, 1:6,
by_cases(diff_table[, 5]<0.05 &
diff_table[, 5]!= ">0.9" &
diff_table[, 4] < 0 ~ "#e06666")) %>%
map_background_color(everywhere, 1:6,
by_cases(diff_table[, 6]<0.05 &
diff_table[, 6]!= ">0.9" &
diff_table[, 4] < 0 ~ "#D7261E")) %>%
map_background_color(everywhere, 1:6,
by_cases(diff_table[, 5]<0.05 &
diff_table[, 5]!= ">0.9" &
diff_table[, 4] > 0 ~ "#6aa84f")) %>%
map_background_color(everywhere, 1:6,
by_cases(diff_table[, 6]<0.05 &
diff_table[, 6]!= ">0.9" &
diff_table[, 4] > 0 ~ "#38761d")) %>%
map_text_color(everywhere, 1:6,
by_cases(diff_table[, 5]<0.05 &
diff_table[, 5]!= ">0.9" &
diff_table[, 4] < 0 ~ "white")) %>%
map_text_color(everywhere, 1:6,
by_cases(diff_table[, 6]<0.05 &
diff_table[, 6]!= ">0.9" &
diff_table[, 4] < 0 ~ "white")) %>%
map_text_color(everywhere, 1:6,
by_cases(diff_table[, 5]<0.05 &
diff_table[, 5]!= ">0.9" &
diff_table[, 4] > 0 ~ "white")) %>%
map_text_color(everywhere, 1:6,
by_cases(diff_table[, 6]<0.05 &
diff_table[, 6]!= ">0.9" &
diff_table[, 4] > 0 ~ "white")) %>%
set_background_color(1, everywhere, "white") %>%
set_text_color(1, everywhere, "black") %>%
add_footnote("* P-value calculated using Welch's T-test. ** Q-value calculated using Bonferroni's correction") %>%
set_italic(final(1), 1, TRUE)
}
# Dichotomous
dich_table_fmt <- function(dich_table) {
dich_table %>% set_width(1) %>%
set_all_padding(4) %>%
set_outer_padding(0) %>%
theme_article() %>%
map_background_color(everywhere, 1:4,
by_cases((dich_table[, 4]<0.05 | dich_table[, 4]=="<0.001") & dich_table[, 3] > dich_table[, 2] & dich_table[, 4]!=">0.9" ~ "#e06666")) %>%
map_background_color(everywhere, 1:4,
by_cases((dich_table[, 4]<0.05 | dich_table[, 4]=="<0.001") & dich_table[, 3] < dich_table[, 2] & dich_table[, 4]!=">0.9" ~ "#6aa84f")) %>%
map_text_color(everywhere, 1:4,
by_cases(dich_table[, 4]<0.05 & dich_table[, 4]!=">0.9" ~ "white")) %>% set_background_color(1, everywhere, "white") %>%
set_text_color(1, everywhere, "black") %>%
add_footnote("* P-value calculated using Pearson's Chi-squared test") %>%
set_italic(final(1), 1, TRUE)
}
#Read and clean server data
mapsdf <- read_csv("Data/DBLog_Matches_2025-05-06.csv")
#Deal with dates (Start analysis on Sept 28, 2024 - WPMC/Asymmetry update)
mapsdf <- mapsdf %>%
mutate(
start_utc = ymd_hms(startTime, tz = "UTC"),
end_utc = ymd_hms(endTime, tz = "UTC")) %>%
mutate(
start_est = with_tz(start_utc, tz = "America/New_York"),
end_est = with_tz(end_utc, tz = "America/New_York")) %>%
filter(start_est >= ymd_hms("2024-09-28 00:00:00", tz = "America/New_York"))
#Clean up map names
#Separate into maps, modes and version
mapsdf <- mapsdf %>% separate(layerClassname,
into = c("map", "mode", "version"),
sep = "_",
remove = FALSE) %>% select(-layer) %>% rename(Layer = layerClassname)
#Clean up errant names and remove seeding/jensens/voiceconnect layers/draws
mapsdf <- mapsdf %>%
mutate(map = case_when(
map == "AlBasrah" ~ "Basrah",
map == "BlackCoast" ~ "Black Coast",
map == "FoolsRoad" ~ "Fools",
map == "GooseBay" ~ "Goose Bay",
map == "Kamdesh Highlands" ~ "Kamdesh",
map == "Manicouagan" ~ "Manic",
map == "PacificProvingGrounds" ~ "Pacific",
TRUE ~ map)) %>%
filter(!map %in% c("L", "VoiceConnect", "WOPFL", "Jensens")) %>%
filter(!mode %in% c("Seed")) %>% filter(isDraw==0)
#Manually examing edge case rounds - made the determination that these are good to throw out
#garbage <- mapsdf %>% filter(dur<=15 | dur>=120)
#Create durations and remove more invalid rounds
mapsdf <- mapsdf %>% mutate(dur = (end_est - start_est)) %>%
filter(dur>=15) %>% filter(dur<=120)
# Extract Day, time of day, Week, month and year based on US EST
mapsdf <- mapsdf %>%
mutate(dow = wday(start_est, label = TRUE, abbr = FALSE)) %>%
mutate(day= lubridate::as_date(start_est)) %>%
mutate(time = hms::as_hms(start_est)) %>%
mutate(month = month(start_est, label = TRUE, abbr = FALSE)) %>%
mutate(year = year(start_est)) %>%
mutate(week = week(start_est)) %>%
unite("ym", month:year, sep = " ", remove = FALSE)
##Generate dummy variables for prime time, pre-prime, late-night
mapsdf <- mapsdf %>%
mutate(prime = ifelse(time >= hms::as_hms('17:30:00') , 1, 0))
mapsdf <- mapsdf %>%
mutate(pre_prime = ifelse(time < hms::as_hms('17:30:00') &
time >= hms::as_hms('12:00:00') , 1, 0))
mapsdf <- mapsdf %>%
mutate(gremlins = ifelse(time > hms::as_hms('00:00:00') &
time < hms::as_hms('12:00:00') , 1, 0))
mapsdf <- mapsdf %>% mutate(tod = case_when(pre_prime==1 ~ "Pre-Prime",
prime==1 ~ "Prime Time",
gremlins==1 ~ "Gremlins"))
mapsdf$tod <- ordered(mapsdf$tod, levels = c("Pre-Prime", "Prime Time", "Gremlins"))
# Weekends
mapsdf <- mapsdf %>%
mutate(weekend = case_when(dow=="Friday" & prime == 1 ~ 1,
dow=="Saturday" ~ 1,
dow=="Sunday" ~ 1,
TRUE ~ 0))
#Merge in Layer size thanks to Zero
layers <- read_csv("Data/Layer_Size.csv")
#Fix logar
layers <- layers %>%
mutate(Size = case_when(
grepl("Logar", Layer) & Layer != "Logar_Seed_v1" ~ "Small",
TRUE ~ Size
))
# Merge
mapsdf <- mapsdf %>%
left_join(layers, by = "Layer") %>%
mutate(Size = ordered(Size, levels = c("Large", "Medium", "Small")))
rm(layers)
#Clean subfactions
mapsdf <- mapsdf %>%
rename(subfac_1 = subFactionShortTeam1,
subfac_2 = subFactionShortTeam2) %>%
mutate(subfac_1 = ifelse(is.na(subfac_1), "CombinedArms", subfac_1),
subfac_2 = ifelse(is.na(subfac_2), "CombinedArms", subfac_2)) %>%
mutate(subfac_1 = ifelse(Size != "Large", "CombinedArms", subfac_1),
subfac_2 = ifelse(Size != "Large", "CombinedArms", subfac_2))
#Prepare merge with Zero's scores
facscore <- read_csv("Data/facscore.csv")
#Create subfaction keys
mapsdf <- mapsdf %>%
rename(fac1 = team1Short, fac2 = team2Short) %>%
mutate(fac1_key = paste(fac1, subfac_1, Size, sep = "_")) %>%
mutate(fac2_key = paste(fac2, subfac_2, Size, sep = "_"))
#Create keys in facscore
facscore <- facscore %>% mutate(fac_key = paste(Faction, Subfaction, Category, sep = "_"))
#Merge then clean
#Faction 1
mapsdf <- mapsdf %>%
left_join(
facscore %>%
select(
fac_key,
logistics_1 = Logistics,
transport_1 = Transportation,
anti_inf_1 = 'Anti-Infantry',
armour_1 = Armor),
by = c("fac1_key" = "fac_key"))
#Faction 2
mapsdf <- mapsdf %>%
left_join(
facscore %>%
select(
fac_key,
logistics_2 = Logistics,
transport_2 = Transportation,
anti_inf_2 = 'Anti-Infantry',
armour_2 = Armor),
by = c("fac2_key" = "fac_key"))
#Clean dumb admin map mis-sets which default to CA (26 cases)
#Verify
# garbage <- mapsdf %>% filter(is.na(logistics_1) | is.na(logistics_2)) %>%
# select(start_est, end_est, dur, layer, fac1_key, fac2_key, tickets, winner,
# t1_long, t2_long, transport_1, transport_2)
#Fix subfactions for unmatched scores
mapsdf <- mapsdf %>%
mutate(
subfac_1 = ifelse(is.na(logistics_1), "CombinedArms", subfac_1),
subfac_2 = ifelse(is.na(logistics_2), "CombinedArms", subfac_2)
)
#Undo the first merge
mapsdf <- mapsdf %>% select(start_est, end_est, start_utc, end_utc, dur, layer= Layer,
map, mode, version, size=Size, tickets, fac1, fac2, subfac_1, subfac_2,
winner = winnerTeamID, dow:tod, t1_long = subFactionTeam1, t2_long = subFactionTeam2)
#Re-create keys
mapsdf <- mapsdf %>%
mutate(fac1_key = paste(fac1, subfac_1, size, sep = "_")) %>%
mutate(fac2_key = paste(fac2, subfac_2, size, sep = "_"))
# Re-do the merge
#Faction 1
mapsdf <- mapsdf %>%
left_join(
facscore %>%
select(
fac_key,
logistics_1 = Logistics,
transport_1 = Transportation,
anti_inf_1 = 'Anti-Infantry',
armour_1 = Armor),
by = c("fac1_key" = "fac_key"))
#Faction 2
mapsdf <- mapsdf %>%
left_join(
facscore %>%
select(
fac_key,
logistics_2 = Logistics,
transport_2 = Transportation,
anti_inf_2 = 'Anti-Infantry',
armour_2 = Armor),
by = c("fac2_key" = "fac_key"))
#Final selection and variable order
mapsdf <- mapsdf %>% select(start_est, end_est, start_utc, end_utc, dur, layer,
map, mode, version, size, tickets, fac1, fac2, subfac_1, subfac_2,
winner, logistics_1:armour_2, dow:tod, fac1_key, fac2_key, t1_long, t2_long)
rm(facscore)
In this document, we present the results of Tactical Triggernometry’s fifth annual survey. The results of our last annual survey can be found here. Readers may access the R-code used to generate the results by clicking the collapsible “code” buttons adjacent to all the tables, statistical tests, charts, and plots. The ability to peer review this work ensures the analysis presented is transparent and reproducible. I encourage readers to examine the code, find any mistakes, and run the code themselves. Most of the relevant datasets the material used to generate the report can be found here. Some datasets used to calculate the regression models in the Appendix have been held back as they contain proprietary information.
The main findings from each section are summarised below:
Section | Assessment | Year on Year | Key Observations |
---|---|---|---|
Experienced | More experienced players and external clan members | Most respondents have over 1,000 hours in Squad, have been at TT for longer than a year, and consider TT their primary server. There are a lot more players with over 1,000 hours, and external clan members compared to the previous survey. | |
High | Slightly worse | We score approximately 8/10 across all four questions relating to Administration (response time, fairness, professionalism and overall quality). Fairness and Overall Admin Quality are rated slightly worse compared to the previous survey, but they are still rated at approximately 8/10. | |
High* | No change | Respondents rate us highly on Gameplay Level and Teamwork (over 8/10). They rate us well on “Overall Server Environment” and “Environment for SLs” (roughly 7-7.5/10). As always, the main areas of concern are the learning environment and team balance. In these categories, we received ratings of roughly 6.5/10. | |
High | No change | We score approximately 8/10 across all three questions relating to rules and enforcement (base camping enforcement, comms enforcement, and the asset claim system). | |
High | Not applicable | We adhere remarkably well to the current balance policy. We are almost always in compliance of our stated goals with only a handful of violations over thousands of rounds. When we define balance thresholds in other ways, we still do quite well. There is a strong case to be made for tightening existing balance policy as the current policy requiring balance after two consecutive losses of 200 is outdated. Based on these results, we shall be lowering the balance threshold in our policy from '2 rounds of 200' to '2 rounds of 150.' However, it must be stressed that almost no matter how one looks at the data, the server is remarkably well balanced at least in terms of ticket differentials. | |
* Excluding Balance & Learning environment |
Maps, Modes, Factions, & Events:
Survey data on map preferences were compared to actual data from the server covering over 3,000 valid rounds (i.e. not seeding layers, training range, map rolls, etc.).
The top 3 underplayed maps are Mestia, Sanxian, and Basrah while the top 3 most overplayed maps are Fallujah, Goose Bay, and Sumari.
Only a handful of maps are played more than once per day.
RAAS, Fogless RAAS, and AAS make up more than 90% of what we play. The majority of respondents are satisfied with this. Respondents do want slightly less Skirmish and slightly more Fogless RAAS.
Respondents are satisfied with the frequency of unconventional units played on the server and would like to see fewer “Heavy” units. We largely adhere to these preferences already.
Respondents are satisfied with Fogless Friday events while TC focused and Voice Connect mod events are somewhat polarising.
Non-admins rate us the same or marginally lower across most categories. Admins are more likely to be experienced players (1,000+ hours) and more likely to prefer TC and Skirmish, while non-admins prefer RAAS.
Experienced players (1,000+ hours) rate us the same across most categories. Experienced players favour more AAS and Fogless RAAS, and TC. Newer players prefer more RAAS and Invasion.
External clan members are less satisfied with some admin categories. They are marginally less satisfied with Base Camping Enforcement, the Overall Server Environment, and the map rotation. External clan members are far more likely to be experienced players and marginally less likely to be server regulars.
Compared to Survey 4, we have a lot more experienced players, and external clan members. The scores of common questions across the two surveys were nearly identical, with the exception of the Overall Server Environment score, which was slightly worse.
As usual there is a Responses to Comments section where Affinity responds to select comments. There is also an Appendix which attempts to evaluate how well our internal layer viability algorithm performs at its task.
Before we can delve into the analysis, a basic glossary of statistical terms may be useful in understanding this report. For ease of reference, we have highlighted these concepts as they appear in the report. Clicking the highlighted terms will lead you back to the glossary.
Term | Definition |
---|---|
N | The number of observations being used in the calculation. Sometimes different across tables because of missing values (people leaving questions blank). |
Frequency Distribution | A frequency distribution is just a count of the number of times an observation occurs in the data. In a scoring metric of 1-10, if 50 people gave us a 9/10 in a certain category, the frequency of '9' would be 50. |
Standard deviation | Standard deviation/Variance is a common statistical measure of the amount of dispersion in the scores. A low standard deviation suggests that observations are clustered close to the mean. A high standard deviation indicates greater dispersion. |
Confidence interval | When dealing with sample data (the survey) we, as readers, want to make inferences about the population (the full TT player base) from a limited sample. A confidence interval is a range of estimates for an unknown parameter computed at a confidence level (commonly 95%). The straightforward way to interpret the confidence interval is that 'we are 95% confident that the true population parameter is between the lower and the upper bounds.' |
Histogram | A common visual tool used to represent a distribution which allows readers to easily observe patterns, such as the spread or central tendency of the data. |
Statistical test | A test for whether the data at hand supports a certain hypothesis. Each statistical test (such as a T-test) is a hypothesis test. In the case of the T-test, the hypothesis is that 'the 2 groups have identical means.' Rejecting that hypothesis means that the two groups have different means. |
P-value | The outcome of each statistical test is a number known as the test statistic. Each 'test statistic' is associated with a p-value (probability value). The p-value has a straightforward interpretation. Conventionally, if a p-value is lower than 0.05, we conclude that there is enough evidence to reject a null hypothesis in a statistical test. This means that differences we are seeing between the groups are probably real (non-zero) and not due to random chance. |
Statistical significance | Statistically significant is just a formal way of saying non-zero. It is really that simple — something is statistically significant if it is different from zero. We calculate p-values after statistical tests to ascertain the probability of statistical significance. Normally, we consider something significant if the p-value associated with it is less than 0.05. It is important to remember that just because something is significant (non-zero) does not mean that the effect is large. Sometimes, we also calculate q-values too which are adjusted p-values. Q-values are just p-values adjusted for things like multiple comparisons. They have the same interpretation. In this report, I consider something to be 'strongly' significant if both the associated p and q values are less than 0.05 and 'weakly' significant if only the p-value is less than 0.05. |
Likert Scale | A Likert scale is a psychometric scale (typically 5 or 7 points) used to measure attitudes or agreement, ranging from 'Strongly Disagree' to 'Strongly Agree' (or similar bipolar adjectives). Example: 1 = Much less → 3 = About the same → 5 = Much more. |
Chi-squared test | A standard statistical test commonly used for frequency counts. More information can be gleaned from this video on YouTube. |
Welch's T-test | A standard statistical test commonly used to test for differences in means between categories. See here for more information. |
The survey was conducted from March 9 to May 31, 2025. We received a total of 312 responses, including admins. Critical readers may point out that the responses we receive are not from a truly random sample of players. The survey was disproportionately filled out by players that are more engaged than the random player. I have deliberately not adjusted for this bias using any weighting techniques, because it is better to show the results as they are. Rather than a random sample, readers should understand the results as representing the view of the “average regular.” I have presented confidence intervals where appropriate so that the reader can get an idea of where the “true parameter” lies and draw their own conclusion.
To start, we asked general questions such as “How long have you played Squad?” and “Do you SL often on the server?” Below, I present the findings of these questions. First, let us look at the breakdown of how long respondents have played Squad:
#Remove previous
rm(gloss, chi2test, ci, hist, fdistr, nn, pval, statsig, stdev, stest, weltest, likert)
#Chart 1
chart1 <- df %>% select(sqtime) %>% drop_na() %>% ggplot(aes(x = factor(1), fill = sqtime)) +
geom_bar(width = 1)+ coord_polar("y") + blank_theme +
theme(axis.text.x=element_blank()) +
scale_fill_brewer("How long have you played Squad?", palette = "Set1") +
theme(axis.text.y=element_blank()) +
labs(title = "Chart 1: Time spent playing Squad") +
theme(plot.title = element_text(hjust = 0.5))
#Display
chart1
#Remove previous
rm(chart1)
#Table 1 - Sqtime
table1 <- df %>%
mutate(sqtime = as.character(sqtime) %>% factor()) %>%
select(sqtime) %>%
tbl_summary(
type = list(sqtime ~ "categorical"),
label = list(sqtime ~ "Hours in Squad"), sort = list(sqtime ~ "frequency"),
statistic = list(all_categorical() ~ "{n} ({p}%)")) %>%
add_ci() %>%
modify_footnote(everything() ~ NA) %>% remove_abbreviation("CI = Confidence Interval") %>%
modify_header(label = "**Hours in Squad**")
#Save inline text results
inline.t1 <- as_tibble(table1[["table_body"]][["stat_0"]])
#Apply theme
table1 <- cat_table_fmt(table1) %>% set_caption("Table 1: Hours played in Squad - Full Sample")
#Display
table1
Hours in Squad | N = 312 | 95% CI |
---|---|---|
1,000+ hours | 243 (78%) | 73%, 82% |
Between 100 - 600 hours | 38 (12%) | 8.9%, 16% |
Between 600 - 1,000 hours | 26 (8.3%) | 5.6%, 12% |
Fewer than 100 hours | 5 (1.6%) | 0.59%, 3.9% |
* N is the observation count. ** CI stands for confidence interval |
As expected, both the chart and the table indicate that players with over 1,000 hours are overrepresented both at TT and in the pool of respondents. We can see that most respondents — approximately 78% — have over 1,000 hours in game, significantly higher than our previous survey. Readers can see the difference between Survey 4 and Survey 5 (this survey) in Tables 22A and 22B.
The table above also reports confidence intervals for each category. Please check the glossary for a better understanding of this and other terms!
The chart below shows how long people have been playing at TT:
#Remove previous
rm(table1, inline.t1)
#Chart 2
chart2 <- df %>% select(ttime) %>% drop_na() %>% ggplot(aes(x = factor(1), fill = ttime)) +
geom_bar(width = 1)+ coord_polar("y") + blank_theme +
theme(axis.text.x=element_blank()) +
scale_fill_brewer("1+ Year at TT?", palette = "Set1") +
theme(axis.text.y=element_blank()) +
labs(title = "Chart 2: Time spent at TT") +
theme(plot.title = element_text(hjust = 0.5))
#Display
chart2
#Remove previous
rm(chart2)
#Table 2 - ttime
table2 <- df %>% mutate(ttime = as.character(ttime) %>% factor()) %>% select(ttime) %>%
tbl_summary(type = ttime ~ "categorical",
label = ttime ~ "Time at TT", sort = list(ttime ~ "frequency")) %>%
add_ci() %>% modify_footnote(everything() ~ NA) %>%
modify_header(label = "Time at TT") %>% remove_abbreviation("CI = Confidence Interval")
#Save inline text results
inline.t2 <- as_tibble(table2[["table_body"]][["stat_0"]])
#Apply theme
table2 <- cat_table_fmt(table2) %>% set_caption("Table 2: Time spent at TT - Full sample")
#Display
table2
Time at TT | N = 312 | 95% CI |
---|---|---|
Over 2 years | 154 (49%) | 44%, 55% |
Between 1 - 2 years | 79 (25%) | 21%, 31% |
Fewer than 6 months | 42 (13%) | 10%, 18% |
Between 6 months - 1 year | 37 (12%) | 8.6%, 16% |
* N is the observation count. ** CI stands for confidence interval |
As before, confidence intervals are presented so that the reader can make their own inferences.
#Remove previous
rm(table2, inline.t2)
#Table 3 - regulars
table3 <- df %>% mutate(regular = as.character(regular) %>% factor()) %>% select(regular) %>%
tbl_summary(type = regular ~ "categorical",
label = regular ~ "Do you consider TT to be your primary server?",
missing_text = "Did not answer", sort = list(regular ~ "frequency")) %>%
add_ci() %>% modify_footnote(everything() ~ NA) %>%
modify_header(label = "Do you consider TT to be your primary server?") %>%
modify_footnote(ci_stat_0 ~ NA, abbreviation = FALSE) %>% remove_abbreviation("CI = Confidence Interval")
#Save inline text results
inline.t3 <- as_tibble(table3[["table_body"]][["stat_0"]])
#Apply theme
table3 <- cat_table_fmt(table3) %>% set_caption("Table 3: Yes/No question on whether people consider TT their primary server - Full sample")
#Display
table3
Do you consider TT to be your primary server? | N = 312 | 95% CI |
---|---|---|
Yes | 248 (80%) | 75%, 84% |
No | 61 (20%) | 16%, 25% |
Did not answer | 3 | |
* N is the observation count. ** CI stands for confidence interval |
This means that the sample is not representative of the general server population but can be considered a good representation of server regulars and admins, who are the most invested and influential groups driving the server.
A wide variety of external clans and communities have made TT their home server. Let’s check what percentage of our player base is affiliated with outside clans and/or communities:
#Remove previous
rm(table3, inline.t3)
#Chart 3
chart3 <- df %>% select(clan) %>% drop_na() %>% ggplot(aes(x = factor(1), fill = clan)) +
geom_bar(width = 1)+ coord_polar("y") + blank_theme +
theme(axis.text.x=element_blank()) +
scale_fill_brewer("Are you affiliated with an outside clan?", palette = "Set1") +
theme(axis.text.y=element_blank()) +
labs(title = "Chart 3: Outside clans") +
theme(plot.title = element_text(hjust = 0.5))
#Display
chart3
#Remove previous
rm(chart3)
#Table 4 - clan
table4 <- df %>% mutate(clan = as.character(clan) %>% factor()) %>% select(clan) %>%
tbl_summary(type = clan ~ "categorical",
label = clan ~ "Are you affiliated with an outside clan?",
missing_text = "Did not answer", sort = list(clan ~ "frequency")) %>%
add_ci() %>% modify_footnote(everything() ~ NA) %>%
modify_header(label = "Are you affiliated with an outside clan?") %>%
modify_footnote(ci_stat_0 ~ NA, abbreviation = FALSE) %>% remove_abbreviation("CI = Confidence Interval")
#Save inline text results
inline.t4 <- as_tibble(table4[["table_body"]])
#Apply theme
table4 <- cat_table_fmt(table4) %>% set_caption("Table 4: Outside clan affiliation - Full sample")
#Display
table4
Are you affiliated with an outside clan? | N = 312 | 95% CI |
---|---|---|
No | 179 (58%) | 52%, 63% |
Yes | 132 (42%) | 37%, 48% |
Did not answer | 1 | |
* N is the observation count. ** CI stands for confidence interval |
Because clan members are the sort of players who are more likely to respond to a survey, their views are probably overrepresented in the sample. It is my belief that the true proportion of external clan members on the server lies closer to the lower bound of the confidence interval (around 37%), although that is still quite substantial.
#Remove previous
rm(table4, inline.t4)
#Chart 4
chart4 <- df %>% select(sl) %>% drop_na() %>% ggplot(aes(x = factor(1), fill = sl)) +
geom_bar(width = 1)+ coord_polar("y") + blank_theme +
theme(axis.text.x=element_blank()) +
scale_fill_brewer("Do you squad lead open squads often?", palette = "Set1") +
theme(axis.text.y=element_blank()) +
labs(title = "Chart 4: Squad leaders") +
theme(plot.title = element_text(hjust = 0.5))
#Display
chart4
#Remove previous
rm(chart4)
#Table 5 - Squad leaders
table5 <- df %>% mutate(sl = as.character(sl) %>% factor()) %>% select(sl) %>%
tbl_summary(type = sl ~ "categorical",
label = sl ~ "Do you squad lead open squads often?", missing_text = "Did not answer", sort = list(sl ~ "frequency")) %>%
add_ci() %>% modify_footnote(everything() ~ NA) %>%
modify_header(label = "Do you squad lead open squads often?") %>%
modify_footnote(ci_stat_0 ~ NA, abbreviation = FALSE) %>% remove_abbreviation("CI = Confidence Interval")
#Save inline text results
inline.t5 <- as_tibble(table5[["table_body"]])
#Apply theme
table5 <- cat_table_fmt(table5) %>% set_caption("Table 5: Do you squad lead open squads often? - Full sample")
#Display
table5
Do you squad lead open squads often? | N = 312 | 95% CI |
---|---|---|
No | 208 (67%) | 61%, 72% |
Yes | 103 (33%) | 28%, 39% |
Did not answer | 1 | |
* N is the observation count. ** CI stands for confidence interval |
Next, we look at the time zones in which players are located:
#Remove previous
rm(table5, inline.t5)
#Chart 5
chart5 <- df %>% select(tzone) %>% drop_na() %>% ggplot(aes(x = factor(1), fill = tzone)) +
geom_bar(width = 1)+ coord_polar("y") + blank_theme +
theme(axis.text.x=element_blank()) +
scale_fill_brewer("What time zone are you in?", palette = "Set1") +
theme(axis.text.y=element_blank()) +
labs(title = "Chart 5: Time zones") +
theme(plot.title = element_text(hjust = 0.5))
#Display
chart5
#Remove previous
rm(chart5)
#Table 6 - Squad leaders
table6 <- df %>%
mutate(tzone = as.character(tzone) %>% factor()) %>%
select(tzone) %>%
tbl_summary(
type = tzone ~ "categorical",
label = tzone ~ "What time zone are you in?",
missing_text = "Did not answer",
sort = list(tzone ~ "frequency")) %>%
add_ci() %>%
modify_footnote(everything() ~ NA) %>%
modify_header(label = "What time zone are you in?") %>%
modify_footnote(ci_stat_0 ~ NA) %>%
remove_abbreviation("CI = Confidence Interval")
#Save inline text results
inline.t6 <- as_tibble(table6[["table_body"]])
#Apply theme
table6 <- cat_table_fmt(table6) %>% set_caption("Table 6: Time zones - Full sample")
#Display
table6
What time zone are you in? | N = 312 | 95% CI |
---|---|---|
US East | 121 (39%) | 34%, 45% |
US Pacific | 70 (23%) | 18%, 28% |
US Central | 67 (22%) | 17%, 27% |
UK/Continental Europe | 36 (12%) | 8.3%, 16% |
Other | 17 (5.5%) | 3.3%, 8.8% |
Did not answer | 1 | |
* N is the observation count. ** CI stands for confidence interval |
The figures above show that the plurality of TT players are from the US East time zone. Central and Pacific time zones are less represented, but combined they make up a slightly greater proportion of our players than the US Eastern time zone. The US makes up around 80% of our player base with the remainder originating from outside the United States.
Below, I present the results for the admin categories over the full sample. Admin responses are included. Readers who want to check how admins differ from non-admins should check the relevant section.
#Remove previous
rm(table6, inline.t6)
#Build
table7 <- df %>% select('admresp':'admoverall') %>%
tbl_summary(type = everything() ~ "continuous",
statistic = all_continuous() ~ "{mean} ({sd})",
missing = "no", label = list('admresp' ~ 'Admin response time',
'admprof' ~ 'Admin professionalism',
'admfair' ~ 'Admin fairness',
'admoverall' ~ 'Overall quality of administration')) %>% add_ci() %>%
modify_header(stat_0 ~ "**Mean (SD)**") %>%
modify_footnote(everything() ~ NA) %>%
modify_footnote(ci_stat_0 ~ NA, abbreviation = FALSE) %>% add_n() %>% remove_abbreviation("CI = Confidence Interval")
#Format
table7 <- cont_table_fmt(table7) %>% set_caption("Table 7: Summary statistics for Administration categories - Full Sample")
#Display
table7
Characteristic | N | Mean (SD) | 95% CI |
---|---|---|---|
Admin response time | 295 | 8.36 (1.68) | 8.2, 8.6 |
Admin professionalism | 299 | 7.96 (2.09) | 7.7, 8.2 |
Admin fairness | 301 | 8.01 (2.22) | 7.8, 8.3 |
Overall quality of administration | 301 | 8.25 (1.97) | 8.0, 8.5 |
* N is the observation count. ** CI stands for confidence interval |
We can visualise the data better using some histograms:
#Remove previous
rm(table7)
#Pivot the category scores to long
tempdf <- df %>% select(admresp, admprof, admfair, admoverall) %>%
tidyr::pivot_longer(cols = c("admresp", "admprof", "admfair", "admoverall"),
names_to = "Category", values_to = "Score")
tempdf$Category <- ordered(tempdf$Category, levels = c("admresp", "admprof", "admfair", "admoverall"))
levels(tempdf$Category) <- c("Admin response time", "Admin professionalism", "Admin fairness", "Overall quality of administration" )
#Histograms
chart6 <- ggplot(data = tempdf, aes(x = Score)) +
geom_histogram(
fill = "#99ccff",
color = "white",
binwidth = 1,
boundary = 0.5,
linewidth = 0.5
) +
theme_classic() +
theme(legend.position = "none") +
facet_wrap(~ Category, scales = "free_y", ncol = 2) +
xlab("Score (1-10)") +
ylab("Count") +
labs(title = "Chart 6: Histograms - Administration categories") +
theme(plot.title = element_text(hjust = 0.5)) +
scale_x_continuous(
limits = c(0.5, 10.5),
breaks = 1:10,
expand = c(0, 0)
) +
scale_y_continuous(expand = expansion(mult = c(0, 0.05)))
chart6
In this section, we examine the questions related to server environment (teamwork, balance, etc.).
#Remove previous
rm(chart6)
#Build
table8 <- df %>% select('twork':'toxic', 'slserver') %>%
tbl_summary(type = everything() ~ "continuous",
statistic = all_continuous() ~ "{mean} ({sd})",
missing = "no",label =
list("twork" ~ "Teamwork",
"learn" ~ "Learning Environment",
"balance" ~ "Team Balance",
"skill" ~ "Gameplay level",
"slserver" ~ "Environment for SLs",
"toxic" ~ "Overall server environment"),
digits = everything() ~ 2) %>% add_ci() %>%
modify_header(stat_0 ~ "**Mean (SD)**") %>%
modify_footnote(everything() ~ NA) %>%
modify_footnote(ci_stat_0 ~ NA, abbreviation = FALSE) %>% add_n() %>% remove_abbreviation("CI = Confidence Interval")
#Format
table8 <- cont_table_fmt(table8) %>% set_caption("Table 8: Summary statistics for Server Environment categories - Full Sample")
#Display
table8
Characteristic | N | Mean (SD) | 95% CI |
---|---|---|---|
Teamwork | 304 | 8.37 (1.62) | 8.2, 8.6 |
Learning Environment | 303 | 6.37 (2.37) | 6.1, 6.6 |
Team Balance | 305 | 6.58 (2.18) | 6.3, 6.8 |
Gameplay level | 305 | 8.63 (1.62) | 8.4, 8.8 |
Overall server environment | 304 | 7.00 (2.14) | 6.8, 7.2 |
Environment for SLs | 297 | 7.64 (1.96) | 7.4, 7.9 |
* N is the observation count. ** CI stands for confidence interval |
As before, I show histograms to illustrate the frequency distributions of the variables above:
#Remove previous
rm(table8)
#Pivot the category scores to long
tempdf <- df %>% select(twork, learn, balance, skill, toxic, slserver) %>%
tidyr::pivot_longer(cols = c("twork", "learn", "balance", "skill", "toxic", "slserver"),
names_to = "Category", values_to = "Score")
tempdf$Category <- ordered(tempdf$Category, levels = c("twork", "learn", "balance", "skill", "toxic", "slserver"))
levels(tempdf$Category) <- c("Teamwork", "Learning environment", "Team balance", "Gameplay level", "Overall server environment", "Environment for SLs")
#Build
chart7 <- ggplot(data = tempdf, aes(x = Score)) +
geom_histogram(
fill = "#99ccff",
color = "white",
binwidth = 1,
boundary = 0.5,
linewidth = 0.5
) +
theme_classic() +
theme(legend.position = "none") +
facet_wrap(~ Category, scales = "free_y", ncol = 2) +
xlab("Score (1-10)") +
ylab("Count") +
labs(title = "Chart 7: Histograms - Server Environment categories") +
theme(plot.title = element_text(hjust = 0.5)) +
scale_x_continuous(
limits = c(0.5, 10.5),
breaks = 1:10,
expand = c(0, 0)
) +
scale_y_continuous(expand = expansion(mult = c(0, 0.05)))
#Display
chart7
Regardless, the verdict is clear: We at TT should make a greater effort to create a welcoming environment. This is something in which both admins and regulars should play a part. As we can see in Table 19, admins agree with non-admins when it comes to the learning environment.
With regards to balance, this is always a tricky topic. TT is one of a few servers with explicitly stated balance guidelines. We track all rounds and ticket counts in Discord and tend to stick by our guidelines. We still feel that guidelines are the best way to balance the server. Squad is a game where a great number of events can punish an otherwise good team (a logistics truck getting wiped during “rollout”, a bad RAAS lane, etc.) Given all the variables involved, including ever changing teams, it would be untenable to commit balance actions after every round. Perhaps what players do not see, but admins do, is the ongoing discussions about balance behind the scenes. These discussions occur both in admin chat and in the admin channels in Discord. Now, 6.5/10 isn’t a terrible score for balance overall, and it is the same as last survey, but we could stand to do better.
The balance guidelines, and how well we adhere to these guidelines is discussed in great detail in the balance section. Questions such as “how well we adhere to our stated balance policy?” and “is our balance policy up to the task?” are all discussed in detail using data from the rounds played on the server.
In this section, we present the results of the questions pertaining to maps, modes, and events. First, we look at the overall satisfaction with the maps, modes, and voting on TT. With regard to voting frequency, respondents were asked whether they would like to see more or less voting and their responses were coded on a scale of one to five on a simple Likert Scale.
#Remove previous
rm(chart7, tempdf)
#Build
table9 <- df %>% select('maprot','moderot', 'unitsat', 'unconv', 'votesat', 'votefreq') %>%
tbl_summary(type = everything() ~ "continuous",
statistic = all_continuous() ~ "{mean} ({sd})",
missing = "no",label =
list("maprot" ~ "Satisfaction with maps played (1-10)",
"moderot" ~ "Satisfaction with modes played (1-10)",
"unitsat" ~ "Satisfaction with unit types (1-10)",
"unconv" ~ "Satisfaction with frequency of unconv. units (1-10)",
"votesat" ~ "Satisfaction with voting system (1-10)",
"votefreq" ~ "More or less voting on the server? (1-5)"),
digits = everything() ~ 2) %>% add_ci() %>%
modify_header(stat_0 ~ "**Mean (SD)**") %>%
modify_footnote(everything() ~ NA) %>%
modify_footnote(ci_stat_0 ~ NA, abbreviation = FALSE) %>% add_n() %>%
remove_abbreviation("CI = Confidence Interval")
#Format
table9 <- cont_table_fmt(table9) %>% set_caption("Table 9: Satisfaction maps, modes, unit types, & voting - Full Sample")
#Display
table9
Characteristic | N | Mean (SD) | 95% CI |
---|---|---|---|
Satisfaction with maps played (1-10) | 296 | 7.04 (2.33) | 6.8, 7.3 |
Satisfaction with modes played (1-10) | 300 | 7.68 (2.07) | 7.4, 7.9 |
Satisfaction with unit types (1-10) | 296 | 7.36 (2.24) | 7.1, 7.6 |
Satisfaction with frequency of unconv. units (1-10) | 301 | 6.51 (2.45) | 6.2, 6.8 |
Satisfaction with voting system (1-10) | 301 | 7.68 (2.63) | 7.4, 8.0 |
More or less voting on the server? (1-5) | 303 | 3.71 (1.00) | 3.6, 3.8 |
* N is the observation count. ** CI stands for confidence interval |
#Remove previous
rm(table9)
#Pivot the category scores to long
tempdf <- df %>% select('maprot','moderot', 'unitsat', 'unconv', 'votesat') %>%
tidyr::pivot_longer(cols = c('maprot','moderot', 'unitsat', 'unconv', 'votesat'),
names_to = "Category", values_to = "Score")
tempdf$Category <- ordered(tempdf$Category, levels = c('maprot','moderot', 'unitsat', 'unconv', 'votesat'))
levels(tempdf$Category) <- c("Satisfaction with Maps",
"Satisfaction with Modes",
"Satisfaction with unit types",
"Satisfaction with frequency of unconv. units",
"Satisfaction with voting system")
# Create faceted histograms
chart8 <- ggplot(data = tempdf, aes(x = Score)) +
geom_histogram(
fill = "#99ccff",
color = "white",
binwidth = 1,
boundary = 0.5,
linewidth = 0.5
) +
theme_classic() +
theme(legend.position = "none") +
facet_wrap(~ Category, scales = "free_y", ncol = 2) +
xlab("Score (1-10)") +
ylab("Count") +
labs(title = "Chart 8: Histograms - Map, modes, unit types, & voting") +
theme(plot.title = element_text(hjust = 0.5)) +
scale_x_continuous(
limits = c(0.5, 10.5),
breaks = 1:10,
expand = c(0, 0)
) +
scale_y_continuous(expand = expansion(mult = c(0, 0.05)))
#Layout
design <- c("AABB
CCDD
EEEE")
#display
chart8 + ggh4x::facet_manual(~Category, scales = "free_y", design=design)
Given that we cannot satisfy everyone, and that each map, mode and unit choice has an opportunity cost (for each map played, there is some map or mode that someone likes that is not played), an approximate satisfaction rating of 7/10 for maps and units along with approximate 8/10 satisfaction rating for modes played on the server is not too bad. The density plots above for maps, modes, and unit types show local peaks around their averages and a large rightwards skew suggesting that a sizeable majority of respondents are satisfied on these fronts. A score of approximately 6.5/10 regarding the frequency of unconventional units reflects a desire for more unconventional units on the server. This question is explored further in Table 14.
People are quite happy with the voting system (respondents rated us approximately 7.7/10 on this front and the density plot shows a pronounced rightwards skew) and with the frequency of voting on the server. A rating of 3.7/5 in Table 9 above does suggest that respondents want to see somewhere between “about the same amount of” and “slightly more” voting on the server. Different groups also have different opinions about maps and game modes, which we will explore below in the subgroup analysis section.
Overall, nothing in these results suggests that radical changes need to be made to any of these variables.
The figures below show map ratings. In the survey, we asked people to rate the maps in Squad on a simple five point Likert scale depending on whether they would like to see more or less of a given map on the server. The resulting scores can be interpreted as “Map favourability ratings.”
The chart below shows these scores for each map for the full sample:
#Remove previous
rm(chart8, tempdf)
# Map ratings
maps <- df %>%
summarise(across(Basrah:Yehorivka,
list(overall = ~ mean(., na.rm = TRUE),
admin = ~ mean(.[admin == 1], na.rm = TRUE),
non_admin = ~ mean(.[admin == 0], na.rm = TRUE))) %>%
pivot_longer(everything(),
names_to = c("column", "group"),
names_sep = "_",
values_to = "average") %>%
pivot_wider(names_from = "group", values_from = "average")) %>% mutate(diffs = non-admin)
# Create new color scheme (same as before but works with centered data)
colour_full <- ifelse(maps$overall < 3, "#ff9999", "#b3ffb3")
#Center data at 3
maps$centered <- maps$overall - 3
# Create the modified chart
chart9a <- ggplot(data = maps, aes(x = reorder(column, overall),
y = centered)) +
geom_bar(stat = "identity",
show.legend = FALSE,
fill = colour_full,
color = "white",
width = 0.7) +
geom_vline(xintercept = 0, color = "black", lwd = 0.2) +
geom_text(
aes(label = round(overall, 2)),
position = position_dodge(width = 1),
vjust = 0.5,
hjust = ifelse(maps$centered < 0, 1.1, -0.1), # Adjusted hjust for new orientation
size = 3
) +
ylab("Rating (3 = About the same)") +
xlab("") +
coord_flip() +
theme_classic() +
labs(title = "Chart 9A - Map favourability ratings (Full sample)") +
theme(plot.title = element_text(hjust = 0.5)) +
scale_y_continuous(
breaks = seq(-0.35, 0.5, by = 0.1),
labels = seq(2.65, 3.5, by = 0.1),
limits = c(-0.35, 0.5)
) + geom_hline(yintercept = 0, color = "black", linewidth = 0.5)
# Display
chart9a
Chart 9 above shows that the usual suspects have the highest ratings. At the top of the pecking order is Narva which is our highest rated and most played map. This is followed by the classic duo of Yehorivka and Gorodok. Harju and Mutaha round out the top 5. The bottom 5 in chart 9 above are very similar to the bottom 5 in chart 10, which uses server data to show what the server plays and how often. When comparing the two charts, we can say that the maps that people want “more of” are the ones that we already play quite often, and the maps that people want “less of” are maps we already do not play very much at all.
All scores are between 2.5/5 and 3.5/5 suggesting that there is not any one map that people want drastically more or less of. These results suggest we need to make some minor adjustments, but nothing radical.
The table below is divided into three parts — non-admins, admins, and the differences between the two. This has been done so that the map preferences of admins, who have control of what the server plays or votes on, can be compared against those of non-admins.
#Slice
c3 <- maps %>% arrange(desc(diffs)) %>% slice(1:5) %>% select(column, diffs)
c2 <- maps %>% arrange(desc(admin)) %>% slice(1:5) %>% select(column, admin)
c1 <- maps %>% arrange(desc(non)) %>% slice(1:5) %>% select(column, non)
c6 <- maps %>% arrange(diffs) %>% slice(1:5) %>% select(column, diffs)
c5 <- maps %>% arrange(admin) %>% slice(1:5) %>% select(column, admin)
c4 <- maps %>% arrange(non) %>% slice(1:5) %>% select(column, non)
#Merge
table10_temp <- cbind(c1, c2, c3)
table10_temp2 <- cbind(c4, c5, c6)
table10<- rbind(table10_temp, table10_temp2)
#Remove slices
rm(c1,c2,c3, c4, c5, c6, table10_temp, table10_temp2)
colnames(table10) <- c('Non-Admins', 'Map Rating', 'Admins',
'Map Rating', 'Largest differences', 'Non-Admin - Admin')
#Format
table10<- as_hux(table10) %>% set_bold(1,everywhere) %>%
insert_row("Most Favourable", "", "", "", "", "", after = 1) %>% set_align(everywhere, everywhere, "center") %>%
merge_cells(2, 1:6) %>% set_align(2, everywhere, "center") %>% set_bold(2, everywhere) %>%
insert_row("Least favourable", "", "", "", "", "", after = 7) %>% merge_cells(8, 1:6) %>% set_align(8, everywhere, "center") %>% set_bold(8, everywhere) %>%
set_background_color(evens, everywhere, "grey95") %>% set_width(1) %>% set_bottom_border(row = 1, col = everywhere) %>% set_caption("Table 10 - Map favourability")
table10
Non-Admins | Map Rating | Admins | Map Rating | Largest differences | Non-Admin - Admin |
---|---|---|---|---|---|
Most Favourable | |||||
Narva | 3.52 | Mestia | 3.19 | Yehorivka | 0.407 |
Yehorivka | 3.42 | Mutaha | 3.19 | Gorodok | 0.361 |
Gorodok | 3.38 | Narva | 3.19 | Narva | 0.329 |
Harju | 3.27 | Kamdesh | 3.17 | Black Coast | 0.326 |
Black Coast | 3.25 | Sumari | 3.11 | Manic | 0.248 |
Least favourable | |||||
Pacific | 2.6 | Goose Bay | 2.66 | Pacific | -0.437 |
Sumari | 2.73 | Tallil | 2.79 | Kamdesh | -0.395 |
Kamdesh | 2.78 | Black Coast | 2.92 | Sumari | -0.382 |
Lashkar | 2.78 | Anvil | 2.94 | Fools | -0.258 |
Tallil | 2.78 | Sanxian | 2.95 | Kohat | -0.237 |
In the table above, we can see that the top 5 picks of non-admins and admins are quite similar. The only map that the two groups jointly disapprove of is Tallil. The differences column shows that non-admins like Yehorivka, Gorodok, Narva, Black Coast, and Manic more than admins do. Non-Admins dislike Pacific, Kamdesh, Sumari, Fools, and Kohat more than admins do.
The charts below illustrate the differences in favourability ratings between admins and non-admins across all maps, visually representing the data in the table above:
#Remove Previous
rm(table10)
# Create new color scheme (same as before but works with centered data)
colour_full <- ifelse(maps$admin < 3, "#ff9999", "#b3ffb3")
#Center data at 3
maps$centered <- maps$admin - 3
# Create the modified chart
chart9b <- ggplot(data = maps, aes(x = reorder(column, admin),
y = centered)) +
geom_bar(stat = "identity",
show.legend = FALSE,
fill = colour_full,
color = "white",
width = 0.7) +
geom_vline(xintercept = 0, color = "black", lwd = 0.2) +
geom_text(
aes(label = round(admin, 2)),
position = position_dodge(width = 1),
vjust = 0.5,
hjust = ifelse(maps$centered < 0, 1.1, -0.1), # Adjusted hjust for new orientation
size = 3
) +
ylab("Rating (3 = About the same)") +
xlab("") +
coord_flip() +
theme_classic() +
labs(title = "Chart 9B - Map favourability ratings (admin)") +
theme(plot.title = element_text(hjust = 0.5)) +
scale_y_continuous(
breaks = seq(-0.4, 0.2, by = 0.1), # Centered breaks (-0.4=2.6, 0=3, 0.2=3.2)
labels = c(2.6, 2.7, 2.8, 2.9, 3.0, 3.1, 3.2), # Manual labels to match breaks
limits = c(-0.4, 0.2) # 2.6-3.2 range when centered
)+ geom_hline(yintercept = 0, color = "black", linewidth = 0.5)
# Display
chart9b
# Create new color scheme (same as before but works with centered data)
colour_full <- ifelse(maps$non < 3, "#ff9999", "#b3ffb3")
#Center data at 3
maps$centered <- maps$non - 3
# Create the modified chart
chart9c <- ggplot(data = maps, aes(x = reorder(column, non),
y = centered)) +
geom_bar(stat = "identity",
show.legend = FALSE,
fill = colour_full,
color = "white",
width = 0.7) +
geom_vline(xintercept = 0, color = "black", lwd = 0.2) + # Vertical line at 3 (now 0)
geom_text(
aes(label = round(non, 2)),
position = position_dodge(width = 1),
vjust = 0.5,
hjust = ifelse(maps$centered < 0, 1.1, -0.1), # Adjusted hjust for new orientation
size = 3
) +
ylab("Rating (3 = About the same)") +
xlab("") +
coord_flip() +
theme_classic() +
labs(title = "Chart 9C - Map favourability ratings (Non-Admins)") +
theme(plot.title = element_text(hjust = 0.5)) +
scale_y_continuous(
breaks = seq(-0.45, 0.55, by = 0.1), # Centered breaks (-0.45=2.55, 0=3, 0.55=3.55)
labels = seq(2.55, 3.55, by = 0.1), # 11 labels: 2.55, 2.65, ..., 3.55
limits = c(-0.45, 0.55) # 2.55-3.55 range when centered
) + geom_hline(yintercept = 0, color = "black", linewidth = 0.5)
# Display
chart9c
In this section, we present actual data from the server regarding the maps that are played using data collected from thousands of rounds played on the server. After filtering out extraneous rounds (training range, skirmish layers, seed layers, map rolls, etc.), we are left with a sample of 3436 rounds covering the period from September 28, 2024 to May 6, 2025.
#Remove previous
rm(chart9a, chart9b, chart9c, colour_full)
#chart 10
#Create dataset
dfc10 <- mapsdf %>% select(map) %>% count(map, sort = TRUE)
total <- sum(dfc10$n)
title <- paste0("Chart 10: Maps played: 28 Sept 24 - 6 May 25"," (Total matches: ", total, ")")
#Build Chart
chart10 <- ggplot(
dfc10, aes(x = reorder(map, n), y = n, label = n)) +
geom_col(fill="#99ccff", position = "dodge") +
theme_classic() +
scale_y_continuous(limits = c(0, 360)) +
xlab("") +
ylab("Frequency count") +
labs(title = title) +
theme(plot.title = element_text(hjust = 0.5)) +
geom_text(position = position_dodge(width= 1), vjust= 0.5, hjust = -0.5, size = 3) +
coord_flip()
#Display
chart10
It is a testament to player narrow-mindedness that Narva, despite being the most played map on the server, is also the map that people want us to play even more of.
In the full sample, we can see that maps played are roughly distributed according to a power law. The long tail is made up of generally unpopular maps, such as Tallil, Lashkar, Anvil, etc. The two most played maps apart from Narva are Mutaha and Fallujah. Based on the chart above, and Chart 9A, we can get some idea of what maps are the most over or underplayed relative to player preferences:
#Remove previous
rm(chart10)
#Gen pref rank
maps <- maps %>% arrange(desc(overall)) %>%
mutate(rank_pref = row_number())
#Gen played rank
dfc10 <- dfc10 %>% arrange(desc(n)) %>%
mutate(rank_actual = row_number()) %>% rename(Maps=map)
#
# Merge and rank diff
df11 <- left_join(dfc10, maps, by = c('Maps' = 'column')) %>%
select(Maps, rank_actual, rank_pref) %>%
mutate(rank_diff = rank_actual - rank_pref)
#Nice colnames
colnames(df11) <- c("Map", "Rank (Actual)", "Rank (Survey)", "Difference")
# #Slice
overplayed <- df11 %>% arrange(Difference) %>% slice(1:5)
underplayed <- df11 %>% arrange(desc(Difference)) %>% slice(1:5)
#Merge table
table11<- rbind(overplayed, underplayed)
#Format
table11_fmt <- as_hux(table11) %>% set_bold(1,everywhere) %>%
insert_row("Most Overplayed", "", "", "", after = 1) %>%
merge_cells(2, 1:4) %>% set_align(2, everywhere, "center") %>%
set_bold(2, everywhere) %>%
insert_row("Most Underplayed", "", "", "", after = 7) %>%
merge_cells(8, 1:4) %>% set_align(8, everywhere, "center") %>%
set_bold(8, everywhere) %>% set_background_color(evens, everywhere, "grey95") %>%
set_width(1) %>% set_bottom_border(row = 1, col = everywhere) %>% set_caption("Table 11 - Most Over/Underplayed maps")
table11_fmt
Map | Rank (Actual) | Rank (Survey) | Difference |
---|---|---|---|
Most Overplayed | |||
Fallujah | 3 | 16 | -13 |
Goose Bay | 10 | 20 | -10 |
Sumari | 16 | 23 | -7 |
Chora | 8 | 13 | -5 |
Kamdesh | 14 | 18 | -4 |
Most Underplayed | |||
Mestia | 21 | 10 | 11 |
Sanxian | 15 | 8 | 7 |
Basrah | 18 | 11 | 7 |
Manic | 12 | 6 | 6 |
Harju | 9 | 4 | 5 |
The table above shows the difference in rank between what is played on the server (Chart 10). and what people said they wanted in the survey (Chart 9A). According to these results we should probably play less Fallujah and Goose Bay; we should play more Mestia and Sanxian. Even if we look at the map rating for the most overplayed map, Fallujah, it is rated at approximately 2.9 in Chart 9A suggesting that players do not want to see a large reduction in the amount we play this map. We should be cautious when interpreting these results and not assume that players want drastic changes; they clearly do not judging by the fact that all the scores in Chart 9A range between 2.5 to 3.5 (or “Slightly less” to “Slightly More.”)
Over the years, a number of players have complained that TacTrig plays the same maps “all the time.” The charts below show “average repeats per day.” I present two versions:
The two charts separate data to avoid skewing averages, as internal policy restricts operations to safer layers after midnight EST to maximize server longevity. To accurately assess variety, we should focus on hours when policy allows greater flexibility.
While people can draw their own conclusions by looking at the charts below, a few things stand out:
Outside of late night (past 12AM EST) hours, we do not repeat any map more than twice in a day on average.
Only a handful of maps are played every day (average greater than 1) during the main 12 hour window that the server is active.
Even if one looks at the full picture which includes all hours, no map is played over twice a day on average.
A handful of popular maps are repeated more than once a day.
Map preferences vary widely. By comparing survey results with data from over 3,000 rounds played on the server, we can identify which maps to prioritise or reduce. Overall, the survey results align closely with actual maps played data.
#Remove Previous
rm(dfc10, df11, chart10, overplayed, underplayed, table11, table11_fmt, title, total)
#Create dataset for chart 11a
df11a <- mapsdf %>% mutate(lvl = as.factor(map)) %>%
filter(prime==1 | pre_prime==1) %>%
select(lvl, day) %>% group_by(day) %>% count(lvl, .drop= FALSE) %>%
group_by(lvl) %>% summarise(mean = round(mean(n), 2))
total <- mapsdf %>% filter(prime==1 | pre_prime==1) %>% nrow()
title <- paste0("Chart 11A: Average per day excluding late night"," (Total matches: ", total, ")")
#Build Chart
chart11a <- ggplot(
df11a, aes(x = reorder(lvl, mean), y = mean, label = mean)) +
geom_col(fill="#99ccff", position = "dodge", width = 0.3) +
theme_classic() +
scale_y_continuous(limits = c(0, 1.3)) +
xlab("") +
ylab("Daily mean") +
labs(title = title) +
theme(plot.title = element_text(hjust = 0.5)) +
geom_text(position = position_dodge(width= 1), vjust= 0.5, hjust = -0.5, size = 2.75) +
geom_hline(yintercept = 1 , linetype="dotted", color = "red", size=1) +
coord_flip()
#Display
chart11a
#Remove Previous
rm(chart11a, df11a)
#Create dataset for chart 11b
df11b <- mapsdf %>% mutate(lvl = as.factor(map)) %>%
select(lvl, day) %>% group_by(day) %>% count(lvl, .drop= FALSE) %>%
group_by(lvl) %>% summarise(mean = round(mean(n), 2))
total <- mapsdf %>% nrow()
title <- paste0("Chart 11B: Average per day - Full Sample"," (Total matches: ", total, ")")
#Build Chart
chart11b <- ggplot(
df11b, aes(x = reorder(lvl, mean), y = mean, label = mean)) +
geom_col(fill="#99ccff", position = "dodge", width = 0.3) +
theme_classic() +
scale_y_continuous(limits = c(0, 1.8)) +
xlab("") +
ylab("Daily mean") +
labs(title = title) +
theme(plot.title = element_text(hjust = 0.5)) +
geom_text(position = position_dodge(width= 1), vjust= 0.5, hjust = -0.5, size = 2.75) +
geom_hline(yintercept = 1 , linetype="dotted", color = "red", size=1) +
coord_flip()
#Display
chart11b
The table below shows the survey responses to the question about game modes. Similar to previous questions about maps, respondents were asked to rate what game modes they would like to see more, less, or about the same of on the server on a five point Likert Scale.
#Remove previous
rm(df11b, chart11b, title, total)
#Table 12 modes
table12 <- df %>% select('raas':'skirm' ) %>%
tbl_summary(type = everything() ~ "continuous",
statistic = all_continuous() ~ "{mean} ({sd})",
missing = "no",
label =list(raas ~ "RAAS",
aas ~ "AAS",
fraas ~ "Fogless RAAS",
invasion ~ "Invasion",
tc ~ "Territory Control",
skirm ~ "Skirmish")) %>% add_ci() %>% add_n() %>% modify_footnote(everything() ~ NA) %>%
modify_header(label = "What modes should TT play more/less of?") %>% modify_header(stat_0 ~ "**Mean (SD)**") %>%
modify_footnote(ci_stat_0 ~ NA, abbreviation = FALSE) %>% remove_abbreviation("CI = Confidence Interval")
#Apply theme
table12 <- cont_table_fmt(table12) %>% set_caption("Table 12: Mode preferences - Survey results")
#Display
table12
What modes should TT play more/less of? | N | Mean (SD) | 95% CI |
---|---|---|---|
RAAS | 292 | 3.13 (1.05) | 3.0, 3.3 |
AAS | 290 | 3.19 (1.11) | 3.1, 3.3 |
Fogless RAAS | 286 | 3.48 (1.18) | 3.3, 3.6 |
Territory Control | 287 | 3.24 (1.29) | 3.1, 3.4 |
Invasion | 292 | 3.18 (1.31) | 3.0, 3.3 |
Skirmish | 282 | 2.87 (1.26) | 2.7, 3.0 |
* N is the observation count. ** CI stands for confidence interval |
The results above suggest that respondents are largely satisfied with the modes player on the server. Respondents want slightly more Fogless RAAS, while Skirmish (mean 2.90) is slightly disfavoured. Other modes are near neutral with scores near 3 — or “about the same.” There are differences between different groups and these are explored in the subgroup analysis section.
In this section, we look at what modes are actually played on the server using data from the server logs. This is the same dataset that was used to generate Chart 10. This dataset contains information on 3436 rounds, covering the period from September 28, 2024 to May 6, 2025.
#Remove Previous
rm(table12)
#Create chart data
dfc12 <- mapsdf %>% select(layer, mode, pre_prime, prime, gremlins, dow) %>%
mutate(mode = ifelse(dow == "Friday" & mode=="RAAS", "Fogless RAAS", mode))
dfc12$mode <- ordered(dfc12$mode, levels = c("RAAS","AAS",
"Fogless RAAS", "TC",
"Skirmish","Invasion", "Destruction", "Insurgency"))
#Create chart
chart12 <- dfc12 %>% select(mode) %>%
ggplot(aes(x = factor(1), fill = mode)) +
geom_bar(width = 1)+ coord_polar("y") + blank_theme +
theme(axis.text.x=element_blank()) +
scale_fill_brewer("Game modes", palette = "Set1") +
theme(axis.text.y=element_blank()) +
labs(title = "Chart 12: Game modes (Actual data)") +
theme(plot.title = element_text(hjust = 0.5))
#Display
chart12
#Remove previous
rm(chart12)
#Table 13
table13 <- dfc12 %>% select(mode) %>%
tbl_summary(type = mode ~ "categorical",label = mode ~ "Game mode") %>%
modify_footnote(everything() ~ NA) %>%
modify_header(label = 'Mode')
#Format
#Copy function with amended footnote
cat_table_fmt_foot <-function(htable) {
htable %>% as_hux_table() %>% set_width(1) %>%
set_all_padding(4) %>%
set_outer_padding(0) %>%
theme_article() %>% .[-2, ] %>%
add_footnote("* N is the observation count. ** Fogless RAAS counted as RAAS") %>%
set_italic(final(1), 1, TRUE)
}
#Apply amended format function
table13 <- cat_table_fmt_foot(table13) %>% set_caption("Table 13: Game modes - Actual data")
#Display
table13
Mode | N = 3,436 |
---|---|
RAAS | 2,172 (63%) |
AAS | 771 (22%) |
Fogless RAAS | 304 (8.8%) |
TC | 96 (2.8%) |
Skirmish | 64 (1.9%) |
Invasion | 24 (0.7%) |
Destruction | 4 (0.1%) |
Insurgency | 1 (<0.1%) |
* N is the observation count. ** Fogless RAAS counted as RAAS |
The table above shows a stark reality — we are overwhelmingly an AAS and RAAS server. Together, RAAS, Fogless RAAS, and AAS make up over 90% of all rounds played on the server. We saw in Table 12 above that respondents desire more Fogless RAAS, and slightly less Skirmish. We play a negligible amount of Skirmish anyway and unless we want to eliminate all Skirmish, this is not feasible. More Fogless RAAS rounds can only realistically come at the expense of regular RAAS rounds on days that are not designated “Fogless Fridays.” We occasionally run Fogless RAAS outside of Fridays, and this practice can be modestly expanded.
In the survey, we specifically asked respondents about whether they would like to see more/less/about the same amount of unconventional factions on the server. As before, responses were recorded on a five point Likert scale.The results are below:
#Remove previous
rm(table13, dfc12)
#Table 14
table14 <- df %>% select('light':'assym', 'tlf':'wpmc' ) %>%
tbl_summary(type = everything() ~ "continuous",
statistic = all_continuous() ~ "{mean} ({sd})", missing = "no",
label =list(
light ~ "Light units (Air Assault, Support, Light infantry)",
medium ~ "Medium units (Motorised/Combined Arms/Wheeled logi Mechanised)",
heavy ~ "Heavy units (Tracked logi Mechanised/Armour)",
assym ~ "Asymetrical factions",
tlf ~ "Turkish forces",
ins ~ "Insurgents",
imf ~ "Irregular Militia",
wpmc ~ "Western PMC")) %>%
add_ci() %>% add_n() %>%
modify_footnote(everything() ~ NA) %>%
modify_header(label = "What unit types/unconventional factions should TT play more or less of?") %>%
modify_header(stat_0 ~ "**Mean (SD)**") %>%
modify_footnote(ci_stat_0 ~ NA, abbreviation = FALSE) %>% remove_abbreviation("CI = Confidence Interval")
#Apply theme
table14 <- cont_table_fmt(table14) %>% set_caption("Table 14: Faction type preferences - Survey results")
#Display
table14
What unit types/unconventional factions should TT play more or less of? | N | Mean (SD) | 95% CI |
---|---|---|---|
Light units (Air Assault, Support, Light infantry) | 286 | 3.30 (1.15) | 3.2, 3.4 |
Medium units (Motorised/Combined Arms/Wheeled logi Mechanised) | 286 | 3.56 (0.86) | 3.5, 3.7 |
Heavy units (Tracked logi Mechanised/Armour) | 287 | 2.82 (1.14) | 2.7, 3.0 |
Asymetrical factions | 301 | 3.11 (1.04) | 3.0, 3.2 |
Turkish forces | 285 | 2.99 (1.11) | 2.9, 3.1 |
Insurgents | 287 | 3.38 (1.10) | 3.3, 3.5 |
Irregular Militia | 284 | 3.45 (1.03) | 3.3, 3.6 |
Western PMC | 285 | 3.05 (1.20) | 2.9, 3.2 |
* N is the observation count. ** CI stands for confidence interval |
#Remove previous
rm(table14)
#Pivot the category scores to long
tempdf <- df %>% select('light':'assym', 'tlf':'wpmc') %>%
tidyr::pivot_longer(cols = c('light':'assym', 'tlf':'wpmc'),
names_to = "Category", values_to = "Score")
tempdf$Category <- ordered(tempdf$Category, levels = c('light', 'medium', 'heavy', 'assym', 'tlf', 'ins', 'imf', 'wpmc'))
levels(tempdf$Category) <- c(
"Light units",
"Medium units",
"Heavy units",
"Asymetrical factions",
"Turkish forces",
"Insurgents",
"Irregular Militia",
"Western PMC")
#Histograms
chart13 <- ggplot(data = tempdf, aes(x = Score)) +
geom_histogram(
color = "white",
fill = "#99ccff",
binwidth = 1,
boundary = 0.5, # Centres bins on whole numbers (1,2,3,4,5)
linewidth = 0.5
) +
theme_classic() +
theme(legend.position = "none") +
facet_wrap(~ Category, scales = "free_y", ncol = 2) +
xlab("Score (1-5)") +
ylab("Count") +
labs(title = "Chart 13: Histograms - Unit Types and Unconventional factions") +
theme(plot.title = element_text(hjust = 0.5)) +
scale_x_continuous(
limits = c(0.5, 5.5),
breaks = 1:5,
expand = c(0, 0)
) +
scale_y_continuous(expand = expansion(mult = c(0, 0.05)))
#Display
chart13
The scores for each variable generally hover around 3/5, indicating overall satisfaction with the frequency of these unit types and factions on the server. However, the results suggest minor adjustments: we should slightly reduce the use of “heavy units” while increasing the presence of “light” and “medium” units. For unconventional factions, players prefer TLF and WPMC to be played at current rates, and desire a slight increase in rounds featuring the Insurgents and Militia.
The histograms above reveal a fuller picture. For example, almost no one wants fewer medium units; all respondents want “about the same” (indicated by a score of 3) or “slightly/much more” (indicated by scores of 4 and 5). However, people do want fewer heavy units suggesting that the increase in medium units should come at the expense of heavy units. With regard to unconventional factions, it seems the WPMC and TLF are quite well balanced as it is — the distributions roughly resemble a standard bell curve. Respondents seem to want more militia and insurgents and very few people want to see less of these factions. This suggests that the increase in IMF and INS has to come at the expense of conventional factions.
The results call for only slight adjustments to these variables, not drastic ones. In the results below where we look at actual server data, we shall see that TT already adheres to these preferences. For example, we already play very few “heavy” factions and there isn’t much scope for further decreases.
Below, I present some figures on the actual unit types played on the server so that we can compare them to the preferences expressed in the survey results.
#Remove Previous
rm(chart13, tempdf)
#Chart data
df15 <- mapsdf %>% select(fac1, fac2) %>%
pivot_longer(cols = c(fac1, fac2), names_to = "team_type", values_to = "unit_type")
#Table 15
table15 <- df15 %>%
mutate(unit_type = as.character(unit_type) %>% factor()) %>%
select(unit_type) %>%
tbl_summary(
type = list(unit_type ~ "categorical"),
label = list(unit_type ~ "Faction play count"), sort = list(unit_type ~ "frequency"),
statistic = list(all_categorical() ~ "{n} ({p}%)")) %>%
add_ci() %>%
modify_footnote(everything() ~ NA) %>% remove_abbreviation("CI = Confidence Interval") %>%
modify_header(label = "**Faction play counts**")
#Apply theme
table15 <- cat_table_fmt(table15) %>% set_caption("Table 15: Faction play counts - Full Sample")
#Display
table15
Faction play counts | N = 6,872 | 95% CI |
---|---|---|
RGF | 1,238 (18%) | 17%, 19% |
USA | 744 (11%) | 10%, 12% |
PLA | 665 (9.7%) | 9.0%, 10% |
CAF | 657 (9.6%) | 8.9%, 10% |
USMC | 638 (9.3%) | 8.6%, 10% |
WPMC | 556 (8.1%) | 7.5%, 8.8% |
ADF | 392 (5.7%) | 5.2%, 6.3% |
MEA | 364 (5.3%) | 4.8%, 5.9% |
VDV | 340 (4.9%) | 4.5%, 5.5% |
BAF | 301 (4.4%) | 3.9%, 4.9% |
IMF | 299 (4.4%) | 3.9%, 4.9% |
INS | 291 (4.2%) | 3.8%, 4.7% |
TLF | 278 (4.0%) | 3.6%, 4.5% |
PLANMC | 79 (1.1%) | 0.92%, 1.4% |
PLAAGF | 30 (0.4%) | 0.30%, 0.63% |
* N is the observation count. ** CI stands for confidence interval |
As expected, the US and RGF are the most played factions due to their availability across nearly all maps. RGF stands out significantly, though the combined total of US Army and USMC slightly exceeds RGF’s percentage. WPMC is the only unconventional faction that rivals some major conventional factions in play frequency. Meanwhile, conventional factions like BAF and VDV are played at levels comparable to unconventional factions like INS. PLANMC and PLAAGF see limited action, as they are restricted to a few maps, with PLAAGF further limited by its tracked logistics focus.
The table below looks at sub-faction types and unit categories.
#Remove Previous
rm(table15, df15)
#Create chart data
mapsdf <- mapsdf %>%
mutate(
type_1 = case_when(
size %in% c("Medium", "Small") ~ "S/M Combined Arms",
#str_detect(fac1_key, "WPMC_CombinedArms_Large") ~ "Light Infantry",
str_detect(fac1_key, "CombinedArms_Large") ~ "Combined Arms Large",
str_detect(fac1_key, "Motorized") ~ "Motorised",
str_detect(fac1_key, "Mechanized") ~ "Mechanised",
str_detect(fac1_key, "LightInf") ~ "Light Infantry",
str_detect(fac1_key, "Support") ~ "Support",
str_detect(fac1_key, "AirAssault") ~ "Air Assault",
str_detect(fac1_key, "Armored") ~ "Armoured",
TRUE ~ NA_character_),
type_2 = case_when(
size %in% c("Medium", "Small") ~ "S/M Combined Arms",
#str_detect(fac2_key, "WPMC_CombinedArms_Large") ~ "Light Infantry",
str_detect(fac2_key, "CombinedArms_Large") ~ "Combined Arms Large",
str_detect(fac2_key, "Motorized") ~ "Motorised",
str_detect(fac2_key, "Mechanized") ~ "Mechanised",
str_detect(fac2_key, "LightInf") ~ "Light Infantry",
str_detect(fac2_key, "Support") ~ "Support",
str_detect(fac2_key, "AirAssault") ~ "Air Assault",
str_detect(fac2_key, "Armored") ~ "Armoured",
TRUE ~ NA_character_))
#Heavy Medium Light
mapsdf <- mapsdf %>%
mutate(
type_1a = case_when(
type_1 == "S/M Combined Arms" ~ NA_character_,
type_1 %in% c("Combined Arms Large", "Motorised") ~ "Medium",
type_1 %in% c("Air Assault", "Support", "Light Infantry") ~ "Light",
type_1 %in% c("Armoured", "Mechanised") ~ "Heavy"
),
type_2a = case_when(
type_2 == "S/M Combined Arms" ~ NA_character_,
type_2 %in% c("Combined Arms Large", "Motorised") ~ "Medium",
type_2 %in% c("Air Assault", "Support", "Light Infantry") ~ "Light",
type_2 %in% c("Armoured", "Mechanised") ~ "Heavy"
)
)
# Moving away from OWI definitions
mapsdf <- mapsdf %>%
mutate(
type_1a = case_when(
str_detect(fac1_key, "WPMC_CombinedArms_Large") ~ "Light",
str_detect(fac1_key, "BAF_Mechanized|ADF_Mechanised|CAF_Mechanized|IMF_Mechanized|INS_Mechanized|MEA_Support|USMC_Support") ~ "Medium",
TRUE ~ type_1a
),
type_2a = case_when(
str_detect(fac2_key, "WPMC_CombinedArms_Large") ~ "Light",
str_detect(fac2_key, "BAF_Mechanized|ADF_Mechanised|CAF_Mechanized|IMF_Mechanized|INS_Mechanized|MEA_Support|USMC_Support") ~ "Medium",
TRUE ~ type_2a
)
)
df16 <- mapsdf %>% select(type_1, type_2) %>%
pivot_longer(cols = c(type_1, type_2), names_to = "team_type", values_to = "unit_type")
df16b <- mapsdf %>% select(type_1a, type_2a) %>%
pivot_longer(cols = c(type_1a, type_2a), names_to = "team_type", values_to = "unit_type") %>% filter(!is.na(unit_type))
#Table 16
table16a <- df16 %>%
mutate(unit_type = as.character(unit_type) %>% factor()) %>%
select(unit_type) %>%
tbl_summary(
type = list(unit_type ~ "categorical"),
label = list(unit_type ~ "Unit Types"), sort = list(unit_type ~ "frequency"),
statistic = list(all_categorical() ~ "{n} ({p}%)")) %>%
add_ci() %>%
modify_footnote(everything() ~ NA) %>% remove_abbreviation("CI = Confidence Interval") %>%
modify_header(label = "**Unit Types**")
#16B
table16b <- df16b %>%
mutate(unit_type = as.character(unit_type) %>% factor()) %>%
select(unit_type) %>%
tbl_summary(
type = list(unit_type ~ "categorical"),
label = list(unit_type ~ "Unit Types"), sort = list(unit_type ~ "frequency"),
statistic = list(all_categorical() ~ "{n} ({p}%)")) %>%
add_ci() %>%
modify_footnote(everything() ~ NA) %>% remove_abbreviation("CI = Confidence Interval") %>%
modify_header(label = "**Light/Medium/Heavy**")
stacked_table <- tbl_stack(tbls = list(table16a, table16b))
#Apply theme
table16 <- cat_table_fmt(stacked_table) %>% set_caption("Table 16: Unit Types - Full Sample") %>%
set_contents(row = 10, col = everywhere, value = "Light/Medium/Heavy classification on large layers") %>%
set_bold(row = 10, col = everywhere, value = TRUE) %>%
set_align(row = 10, col = everywhere, value = "left") %>%
set_colspan(row = 10, col = 1, value = 3) %>% set_bottom_border(row = 9, col = everywhere, value = 1) %>%
set_top_border(row = 11, col = everywhere, value = 1)
#Display
table16
Unit Types | N = 6,872 | 95% CI |
---|---|---|
Combined Arms Large | 2,292 (33%) | 32%, 34% |
S/M Combined Arms | 1,358 (20%) | 19%, 21% |
Motorised | 971 (14%) | 13%, 15% |
Support | 860 (13%) | 12%, 13% |
Mechanised | 559 (8.1%) | 7.5%, 8.8% |
Light Infantry | 414 (6.0%) | 5.5%, 6.6% |
Air Assault | 238 (3.5%) | 3.0%, 3.9% |
Armoured | 180 (2.6%) | 2.3%, 3.0% |
Light/Medium/Heavy classification on large layers | ||
Medium | 3,499 (63%) | 62%, 65% |
Light | 1,502 (27%) | 26%, 28% |
Heavy | 513 (9.3%) | 8.6%, 10% |
* N is the observation count. ** CI stands for confidence interval |
The category “S/M Combined Arms” denotes a special category of Combined Arms factions that are the only sub-faction type available on “Small” and “Medium” maps as defined by OWI. Readers can check this official OWI spreadsheet for details. Medium maps include maps such as Chora, Fools Road, and Mestia, while there are only three “small” maps: Sumari, Pacific, and Logar. It is only “Large” layers that allow for distinct sub-faction choices. Even maps that we may consider “Medium,” such as Narva, are actually classified as “Large” maps by OWI.
Combined Arms Large is our most played faction type. This is classified as a “Medium” sub-faction, alongside motorised sub-factions. Support, air assault, and light infantry sub-factions are categorised as “Light,” while armour and mechanised sub-factions are classified as “Heavy.” There are some exceptions: for instance, British, Canadian, and Australian mechanised factions are classified as “Medium” because they feature wheeled logistics and resemble motorised factions. Similarly, the MEA and USMC support factions, which feature a significant number of vehicles, are also classified as “Medium.”
The bottom half of Table 16 displays the frequency of these sub-faction types, categorised by “Light,” “Medium,” and “Heavy” classifications. Since this classification applies only to “Large” layers, only those are considered. In Table 14, respondents expressed a preference for fewer “Heavy” sub-factions; however, the data in the table above shows that such factions account for only about 9% of sub-faction types played. Medium sub-factions, which include wheeled logistics, some IFVs, and typically one tank per side (except for Turkish factions, which receive two tanks to compensate for the M-60’s weaknesses), are by far our most played faction type, comprising over 60% of all matches on “Large” layers.
As I am sure everyone knows, we host an event called “Fogless Friday” every week, during which we disable fog on RAAS maps and follow a fixed rotation created by an admin or community member. Over the years, we have also experimented with various mods. Most recently, TT hosted the “voice connect” mod, which primarily includes tweaks to gunplay and movement and reverses some aspects of the ICO. We asked survey respondents for their opinions on these events and the types of events they would like to see in the future.
#Remove previous
rm(table16, df16, table16a, table16b, df16b, stacked_table)
#Table 17
table17 <- df %>% select(fogless, fogsat, tcevent, vconnect) %>%
tbl_summary(
type = list(
fogless ~ "dichotomous",
fogsat ~ "continuous",
tcevent ~ "continuous",
vconnect ~ "continuous"
),
statistic = list(
all_continuous() ~ "{mean} ({sd})",
all_dichotomous() ~ "{p}%"
),
label = list(
fogless ~ "Are you aware of fogless Friday? (%)",
fogsat ~ "How satisfied are you with fogless Friday? (1-10)",
tcevent ~ "How interested are you in a TC focused event? (1-10)",
vconnect ~ "How interested are you in voice connect events? (1-10)"
),
missing = "no",
) %>%
add_ci() %>% add_n() %>%
modify_header(label = "Fogless Friday and other events") %>%
modify_footnote(
everything() ~ NA,
ci_stat_0 ~ NA,
abbreviation = FALSE
) %>% modify_header(stat_0 ~ "**Mean (SD)**") %>%
remove_abbreviation("CI = Confidence Interval")
#Apply theme
table17 <- cont_table_fmt(table17) %>% set_caption("Table 17: Event preferences - Survey results")
#Display
table17
Fogless Friday and other events | N | Mean (SD) | 95% CI |
---|---|---|---|
Are you aware of fogless Friday? (%) | 309 | 94% | 91%, 97% |
How satisfied are you with fogless Friday? (1-10) | 296 | 7.48 (2.63) | 7.2, 7.8 |
How interested are you in a TC focused event? (1-10) | 304 | 6.15 (3.43) | 5.8, 6.5 |
How interested are you in voice connect events? (1-10) | 306 | 6.41 (3.44) | 6.0, 6.8 |
* N is the observation count. ** CI stands for confidence interval |
#Remove previous
rm(table17)
#Pivot the category scores to long
tempdf <- df %>% select(fogsat, tcevent, vconnect) %>%
tidyr::pivot_longer(cols = c(fogsat, tcevent, vconnect),
names_to = "Category", values_to = "Score")
tempdf$Category <- ordered(tempdf$Category, levels = c('fogsat', 'tcevent', 'vconnect'))
levels(tempdf$Category) <- c("Satisfaction with Fogless Friday",
"Interest in a TC focused event",
"Interest in Voice Connect events")
#Histograms
chart14 <- ggplot(data = tempdf, aes(x = Score)) +
geom_histogram(
fill = "#99ccff",
color = "white",
binwidth = 1,
boundary = 0.5,
linewidth = 0.5
) +
theme_classic() +
theme(legend.position = "none") +
facet_wrap(~ Category, scales = "free_y", ncol = 2) +
xlab("Score") +
ylab("Count") +
labs(title = "Chart 14: Histograms - Event preferences") +
theme(plot.title = element_text(hjust = 0.5)) +
scale_x_continuous(
limits = c(0.5, 10.5),
breaks = 1:10,
expand = c(0, 0)
) +
scale_y_continuous(expand = expansion(mult = c(0, 0.05)))
#Layout
design <- c("AABB
CCCC")
#display
chart14 + ggh4x::facet_manual(~Category, scales = "free_y", design=design)
We can see from Table 17 that the vast majority of respondents, around 94%, are aware of Fogless Fridays and are largely satisfied with the event. Interest in TC and Voice Connect events exhibits similar distributions. Although the mean interest in these events is moderate, approximately 6/10, the histograms reveal that this average is influenced by polarisation. Peaks at both 1 and 10 for TC and Voice Connect events indicate that significant minorities either strongly support or strongly oppose these events, with an ambivalent group in the middle. These questions will be explored further in the subgroup analysis section below.
This section covers the questions related to rules & enforcement in the survey.
#Remove previous
rm(chart14, tempdf)
#Build
table18 <- df %>% select(basecamp, comms, asset) %>%
tbl_summary(statistic = all_continuous() ~ "{mean} ({sd})", missing = "no",
label = list("basecamp" ~ "Base camping enforcement",
"comms" ~ "Comms enforcement",
"asset" ~ "Asset claim system")) %>%
add_ci() %>% add_n() %>%
modify_footnote(
everything() ~ NA,
ci_stat_0 ~ NA,
abbreviation = FALSE
) %>% modify_header(stat_0 ~ "**Mean (SD)**") %>%
remove_abbreviation("CI = Confidence Interval")
#Apply theme
table18 <- cont_table_fmt(table18) %>% set_caption("Table 18: Satisfaction with server rules & enforcement - Full Sample")
#Display
table18
Characteristic | N | Mean (SD) | 95% CI |
---|---|---|---|
Base camping enforcement | 303 | 7.77 (2.44) | 7.5, 8.0 |
Comms enforcement | 304 | 8.21 (2.09) | 8.0, 8.4 |
Asset claim system | 308 | 8.45 (2.46) | 8.2, 8.7 |
* N is the observation count. ** CI stands for confidence interval |
#Remove previous
rm(table18)
# Pivot the category scores to long
tempdf <- df %>% select('basecamp','comms', 'asset') %>%
tidyr::pivot_longer(cols = c('basecamp','comms', 'asset'),
names_to = "Category", values_to = "Score")
tempdf$Category <- ordered(tempdf$Category, levels = c('basecamp','comms', 'asset'))
levels(tempdf$Category) <- c("Base camping enforcement", "Comms enforcement", "Asset claim")
# Histograms
chart15 <- ggplot(data = tempdf, aes(x = Score)) +
geom_histogram(
fill = "#99ccff",
color = "white",
binwidth = 1,
boundary = 0.5,
linewidth = 0.5
) +
theme_classic() +
theme(legend.position = "none") +
facet_wrap(~ Category, scales = "free_y", ncol = 2) +
xlab("Score (1-10)") +
ylab("Count") +
labs(title = "Chart 15: Histograms - Basecamp, communications, & assets") +
theme(plot.title = element_text(hjust = 0.5)) +
scale_x_continuous(
limits = c(0.5, 10.5),
breaks = 1:10,
expand = c(0, 0)
) +
scale_y_continuous(expand = expansion(mult = c(0, 0.05)))
#Display
chart15 + ggh4x::facet_manual(~Category, scales = "free_y", design=design)
The histograms in the chart above show no pronounced “peaks” at lower scores. We can see that people are most happy with our asset claim system. Respondents are also satisfied with the scores for basecamping enforcement and comms enforcement, rating both at approximately 8/10.
There are no major issues to address here.
Next, I compare various subgroups of respondents to see whether their opinions differ from others not part of the subgroup. The subgroups compared are:
I did look at other subgroups such as US EST time zone against non-EST, as well as long-term (1+ year) TT players against newer arrivals, but did not find any interesting differences. The tables below make liberal use of statistical tests and these need to be explained.
For each subgroup, there are two tables. The first table looks at continuous (1-10) score variables such as admin professionalism, teamplay, etc. The second table looks at dichotomous variables which are binary “Yes/No” questions such as “Do you SL on the server often?” The two blue boxes below describe how to read each type of table:
Here’s how to read tables comparing continuous variables (Tables 19A - 22A)
Rows highlighted in dark red/green show strong statistical differences between the two groups (i.e., both P and Q values are below 0.05).
Rows highlighted in light red/green show weak statistical differences between the two groups (i.e., only the P-value is below 0.05).
Rows without colour should be treated as not statistically significant (i.e., any difference is statistically indistinguishable from zero).
Statistically significant simply means that the difference between the two groups is non-zero. It is not a statement about the magnitude of the difference.
Here’s how to read tables comparing dichotomous variables (Tables 19B - 22B)
Percentages shown in the table refer to what percentage of respondents answered “Yes” to the particular questions. For example, in Table 19B, 73% of non-admins have over 1,000 hours in Squad and so on.
Rows highlighted in green show statistically significant differences between the two groups (i.e., P-value below 0.05), and the subgroup percentage is higher than the comparison percentage.
Rows highlighted in red show statistically significant differences between the two groups (i.e., P-value below 0.05), and the subgroup percentage is lower than the comparison percentage.
Rows without colour should be treated as not statistically significant (i.e., any difference is statistically indistinguishable from zero).
Statistically significant simply means that the difference between the two groups (Admins and Non-Admins) is non-zero. It is not a statement about the magnitude of the difference.
This section compares the scores of admins and non-admins across the various questions in the survey. The aim is to check for statistical differences.
#Remove previous
rm(chart15, tempdf)
#Build
diff_table <- df %>%
select(admresp, admprof, admfair, admoverall, twork, learn, balance, skill, slserver,
toxic, maprot, moderot, votesat, fogsat, tcevent, vconnect,
basecamp, comms, asset, unitsat, unconv, light, medium, heavy, assym, tlf, ins, imf, wpmc,
raas, aas, fraas, tc, invasion, skirm, admin) %>%
tbl_summary(
by = 'admin',
type = everything() ~ "continuous",
statistic = all_continuous() ~ "{mean}",
missing = "no",
label = list(
'admresp' ~ 'Admin response time',
'admprof' ~ 'Admin professionalism',
'admfair' ~ 'Admin fairness',
'admoverall' ~ 'Overall admin quality',
"twork" ~ "Teamwork",
"learn" ~ "Learning Environment",
"balance" ~ "Team Balance",
"skill" ~ "Gameplay level",
"slserver" ~ "Environment for SLs",
"toxic" ~ "Overall server environment",
'maprot' ~ "Satisfaction with map rotation",
'moderot' ~ "Satisfaction with mode types",
'votesat' ~ "Satisfaction with voting system",
'fogsat' ~ "Satisfaction with fogless friday",
'tcevent' ~ "Interest in a TC focused event",
'vconnect' ~ "Interest in Voice connect mod",
"basecamp" ~ "Base camping enforcement",
"comms" ~ "Comms enforcement",
"asset" ~ "Asset claim system",
"unitsat" ~ "Satisfaction with unit types",
"unconv" ~ "Satisfaction with frequency of unconv. units",
"light" ~ "Light Units",
"medium" ~ "Medium Units",
"heavy" ~ "Heavy Units",
"assym" ~ "Asymtery frequency",
"tlf" ~ "Turkish forces",
"ins" ~ "Insurgents",
"imf" ~ "Irregular Militia",
"wpmc" ~ "Western PMC",
"raas" ~ "RAAS",
"aas" ~ "AAS",
"fraas" ~ "Fogless RAAS",
"tc" ~ "Territory control",
"invasion" ~ "Invasion",
"skirm" ~ "Skirmish"
),
digits = everything() ~ 2
) %>%
add_difference() %>%
add_q(method = "bonferroni") %>%
modify_footnote(everything() ~ NA) %>%
modify_header(
stat_1 = "Non-Admin (N={n})",
stat_2 = "Admin (N={n})",
label = "Continuous (1-10) Variables") %>%
remove_abbreviation("CI = Confidence Interval") %>%
as_hux_table() %>% {.[, -5]}
#Format
table19a <- diff_table_fmt(diff_table) %>% set_caption("Table 19A - Admins versus non-Admins (Continuous variables)") %>%
insert_row("Likert Scale variables (3 = About the same)", "", "", "", "", "", after = 22) %>% merge_cells(23, 1:6) %>% set_align(23, everywhere, "left") %>%
set_bold(23, everywhere) %>% set_text_color(23, everywhere, "black") %>%
set_background_color(23, everywhere, "grey95")
#Display
table19a
Continuous (1-10) Variables | Non-Admin (N=243) | Admin (N=69) | Difference | p-value | q-value |
---|---|---|---|---|---|
Admin response time | 8.31 | 8.54 | -0.23 | 0.2 | >0.9 |
Admin professionalism | 7.84 | 8.37 | -0.53 | 0.015 | 0.5 |
Admin fairness | 7.85 | 8.58 | -0.73 | <0.001 | 0.022 |
Overall admin quality | 8.09 | 8.81 | -0.71 | <0.001 | 0.005 |
Teamwork | 8.30 | 8.60 | -0.29 | 0.12 | >0.9 |
Learning Environment | 6.31 | 6.61 | -0.31 | 0.3 | >0.9 |
Team Balance | 6.42 | 7.16 | -0.74 | 0.005 | 0.2 |
Gameplay level | 8.50 | 9.06 | -0.56 | <0.001 | 0.019 |
Environment for SLs | 7.47 | 8.26 | -0.79 | <0.001 | 0.008 |
Overall server environment | 7.09 | 6.69 | 0.40 | 0.10 | >0.9 |
Satisfaction with map rotation | 6.85 | 7.67 | -0.82 | 0.002 | 0.070 |
Satisfaction with mode types | 7.61 | 7.91 | -0.30 | 0.2 | >0.9 |
Satisfaction with voting system | 7.50 | 8.32 | -0.83 | 0.008 | 0.3 |
Satisfaction with fogless friday | 7.20 | 8.45 | -1.3 | <0.001 | <0.001 |
Interest in a TC focused event | 5.89 | 7.06 | -1.2 | 0.009 | 0.3 |
Interest in Voice connect mod | 6.17 | 7.22 | -1.0 | 0.024 | 0.8 |
Base camping enforcement | 7.64 | 8.24 | -0.60 | 0.043 | >0.9 |
Comms enforcement | 8.14 | 8.48 | -0.34 | 0.2 | >0.9 |
Asset claim system | 8.25 | 9.17 | -0.93 | <0.001 | 0.011 |
Satisfaction with unit types | 7.28 | 7.62 | -0.34 | 0.2 | >0.9 |
Satisfaction with frequency of unconv. units | 6.37 | 7.00 | -0.63 | 0.039 | >0.9 |
Likert Scale variables (3 = About the same) | |||||
Light Units | 3.30 | 3.31 | -0.01 | >0.9 | >0.9 |
Medium Units | 3.65 | 3.25 | 0.40 | <0.001 | 0.033 |
Heavy Units | 2.81 | 2.86 | -0.05 | 0.8 | >0.9 |
Asymtery frequency | 3.03 | 3.38 | -0.34 | 0.005 | 0.2 |
Turkish forces | 2.99 | 2.98 | 0.00 | >0.9 | >0.9 |
Insurgents | 3.35 | 3.48 | -0.13 | 0.3 | >0.9 |
Irregular Militia | 3.47 | 3.41 | 0.06 | 0.6 | >0.9 |
Western PMC | 3.08 | 2.92 | 0.16 | 0.3 | >0.9 |
RAAS | 3.26 | 2.68 | 0.58 | <0.001 | <0.001 |
AAS | 3.18 | 3.23 | -0.04 | 0.7 | >0.9 |
Fogless RAAS | 3.42 | 3.68 | -0.26 | 0.064 | >0.9 |
Territory control | 3.11 | 3.68 | -0.57 | <0.001 | 0.011 |
Invasion | 3.19 | 3.14 | 0.06 | 0.8 | >0.9 |
Skirmish | 2.75 | 3.27 | -0.52 | 0.001 | 0.047 |
* P-value calculated using Welch's T-test. ** Q-value calculated using Bonferroni's correction |
#Remove previous
rm(diff_table, table19a)
#Build
dich_table <- df %>% select('over1k', 'over1tt', 'sl', 'regular', 'clan', 'admin') %>% tbl_summary(by= 'admin',
type = everything() ~ "dichotomous", statistic = all_dichotomous() ~ "{p}%",
missing = "no", label = list('over1k' ~ 'Over 1,000 hours?',
'over1tt' ~ 'Over 1 year at TT?',
'sl' ~ 'Do you SL open squads often?',
'regular' ~ 'Server Regular',
'clan' ~ 'External clan member'),
digits = everything() ~ 0) %>% add_p(test = everything() ~ "chisq.test") %>%
modify_footnote(everything() ~ NA) %>%
modify_header(stat_1 = "Non-Admin (N={n})", stat_2="Admin (N={n})", label = "Dichotomous (Y/N) Variables") %>% as_hux_table()
#Format
table19b <- dich_table_fmt(dich_table) %>% set_caption("Table 19B - Admins versus non-Admins (Dichotomous variables)") %>%
set_background_color(row = 5, col = everywhere, value = "#e06666")
#Display
table19b
Dichotomous (Y/N) Variables | Non-Admin (N=243) | Admin (N=69) | p-value |
---|---|---|---|
Over 1,000 hours? | 73% | 94% | <0.001 |
Over 1 year at TT? | 68% | 97% | <0.001 |
Do you SL open squads often? | 31% | 41% | 0.2 |
Server Regular | 75% | 99% | <0.001 |
External clan member | 45% | 35% | 0.2 |
* P-value calculated using Pearson's Chi-squared test |
Key results from tables 19A and 19B:
Check how to read the tables above here.
Unsurprisingly, admins tend to hold more positive views about the server than non-admins. Red indicates that the Non-admin score is lower than the Admin score for that category.
Strong statistical differences in Admin fairness, Overall admin quality, Gameplay level, Environment for Sls, satisfaction with Fogless Friday, and satisfaction with the Asset Claim system.
Weak statistical differences in many areas: Admin professionalism, Team Balance, satisfaction with maps and the map voting system, interest TC in Voice Connect mod events, Base camping enforcement, and differences in game mode and unit preferences. There are also some demographic differences seen in Table 19B.
Despite seeing a lot of red, most scores are very much in line with each other. For example, both admins and non-admins rate “Overall admin quality” between 8-9/10 despite their being a strong statistical differences between the groups in this area.
The tables above highlight several differences between regular players and admins. Non-admins consistently rate the server slightly lower than admins across all server quality categories, which is not unexpected. In admin-related categories, although non-admins give lower ratings, the scores remain high at around 8/10. This trend also applies to satisfaction with the Asset claim system and Gameplay level. Non-admins are less satisfied with Basecamping enforcement and slightly less content with the maps played and voting systems. This is unsurprising as admins know how much nuance goes into Basecamping enforcement and have much more control over the maps that are voted on and played at TT.
Non-admins also show less satisfaction with Fogless Fridays, though they still rate the event at approximately 7/10. They express a stronger preference for RAAS and Medium units compared to admins. The difference is particularly notable with RAAS: non-admins prefer slightly more RAAS, while admins favour slightly less. Admins, on the other hand, show a greater preference for TC and Skirmish. Non-admins prefer slightly less Skirmish, while admins favour slightly more. Both groups desire slightly more Fogless RAAS which is interesting and suggests that we should incorporate more Fogless RAAS on a day to day basis.
From Table 19B above, we can see that admins are more likely to be regulars, have over 1,000 hours of playtime, and have been at TT for over a year. Admins are equally likely to be external clan members as not (see last row of Table 19B), indicating that external clans are proportionally represented on the admin team.
According to Chart 1/Table 1, a sizeable majority of survey respondents have over 1,000 hours in Squad. We want to check whether these relatively more experienced players hold different views from people who are newer to the game.
#Remove previous
rm(table19b, dich_table)
#Build
diff_table <- df %>% mutate(over1k = ordered(recode(over1k,
`1` = "Yes",
`0` = "No"),
levels = c("Yes", "No"))) %>%
select(admresp, admprof, admfair, admoverall, twork, learn, balance, skill, slserver,
toxic, maprot, moderot, votesat, fogsat, tcevent, vconnect,
basecamp, comms, asset, unitsat, unconv, light, medium, heavy, assym, tlf, ins, imf, wpmc,
raas, aas, fraas, tc, invasion, skirm, over1k) %>%
tbl_summary(
by = 'over1k',
type = everything() ~ "continuous",
statistic = all_continuous() ~ "{mean}",
missing = "no",
label = list(
'admresp' ~ 'Admin response time',
'admprof' ~ 'Admin professionalism',
'admfair' ~ 'Admin fairness',
'admoverall' ~ 'Overall admin quality',
"twork" ~ "Teamwork",
"learn" ~ "Learning Environment",
"balance" ~ "Team Balance",
"skill" ~ "Gameplay level",
"slserver" ~ "Environment for SLs",
"toxic" ~ "Overall server environment",
'maprot' ~ "Satisfaction with map rotation",
'moderot' ~ "Satisfaction with mode types",
'votesat' ~ "Satisfaction with voting system",
'fogsat' ~ "Satisfaction with fogless friday",
'tcevent' ~ "Interest in a TC focused event",
'vconnect' ~ "Interest in Voice connect mod",
"basecamp" ~ "Base camping enforcement",
"comms" ~ "Comms enforcement",
"asset" ~ "Asset claim system",
"unitsat" ~ "Satisfaction with unit types",
"unconv" ~ "Satisfaction with frequency of unconv. units",
"light" ~ "Light Units",
"medium" ~ "Medium Units",
"heavy" ~ "Heavy Units",
"assym" ~ "Asymtery frequency",
"tlf" ~ "Turkish forces",
"ins" ~ "Insurgents",
"imf" ~ "Irregular Militia",
"wpmc" ~ "Western PMC",
"raas" ~ "RAAS",
"aas" ~ "AAS",
"fraas" ~ "Fogless RAAS",
"tc" ~ "Territory control",
"invasion" ~ "Invasion",
"skirm" ~ "Skirmish"
),
digits = everything() ~ 2
) %>%
add_difference() %>%
add_q(method = "bonferroni") %>%
modify_footnote(everything() ~ NA) %>%
modify_header(stat_1 = "1,000+ Hours (N={n})", stat_2="< 1,000 Hours (N={n})",
label = "Continuous (1-10) Variables") %>%
remove_abbreviation("CI = Confidence Interval") %>%
as_hux_table() %>% {.[, -5]}
#Format
table20a <- diff_table_fmt(diff_table) %>% set_caption("Table 20A - Experienced vs Newer players (Continuous variables)") %>%
insert_row("Likert Scale variables (3 = About the same)", "", "", "", "", "", after = 22) %>% merge_cells(23, 1:6) %>% set_align(23, everywhere, "left") %>%
set_bold(23, everywhere) %>% set_text_color(23, everywhere, "black") %>%
set_background_color(23, everywhere, "grey95")
#Display
table20a
Continuous (1-10) Variables | 1,000+ Hours (N=243) | < 1,000 Hours (N=69) | Difference | p-value | q-value |
---|---|---|---|---|---|
Admin response time | 8.39 | 8.28 | 0.10 | 0.7 | >0.9 |
Admin professionalism | 7.85 | 8.34 | -0.49 | 0.086 | >0.9 |
Admin fairness | 7.94 | 8.26 | -0.32 | 0.3 | >0.9 |
Overall admin quality | 8.20 | 8.42 | -0.22 | 0.4 | >0.9 |
Teamwork | 8.40 | 8.26 | 0.13 | 0.6 | >0.9 |
Learning Environment | 6.25 | 6.79 | -0.54 | 0.13 | >0.9 |
Team Balance | 6.61 | 6.48 | 0.14 | 0.7 | >0.9 |
Gameplay level | 8.75 | 8.22 | 0.53 | 0.024 | 0.9 |
Environment for SLs | 7.65 | 7.62 | 0.03 | >0.9 | >0.9 |
Overall server environment | 6.86 | 7.47 | -0.61 | 0.043 | >0.9 |
Satisfaction with map rotation | 6.97 | 7.29 | -0.32 | 0.3 | >0.9 |
Satisfaction with mode types | 7.66 | 7.77 | -0.11 | 0.7 | >0.9 |
Satisfaction with voting system | 7.72 | 7.54 | 0.18 | 0.6 | >0.9 |
Satisfaction with fogless friday | 7.62 | 6.94 | 0.69 | 0.078 | >0.9 |
Interest in a TC focused event | 6.35 | 5.41 | 0.94 | 0.050 | >0.9 |
Interest in Voice connect mod | 6.45 | 6.25 | 0.20 | 0.7 | >0.9 |
Base camping enforcement | 7.59 | 8.38 | -0.78 | 0.015 | 0.5 |
Comms enforcement | 8.11 | 8.55 | -0.44 | 0.11 | >0.9 |
Asset claim system | 8.59 | 8.00 | 0.59 | 0.090 | >0.9 |
Satisfaction with unit types | 7.28 | 7.63 | -0.34 | 0.3 | >0.9 |
Satisfaction with frequency of unconv. units | 6.43 | 6.80 | -0.37 | 0.3 | >0.9 |
Likert Scale variables (3 = About the same) | |||||
Light Units | 3.29 | 3.35 | -0.06 | 0.7 | >0.9 |
Medium Units | 3.60 | 3.43 | 0.17 | 0.2 | >0.9 |
Heavy Units | 2.79 | 2.97 | -0.18 | 0.3 | >0.9 |
Asymtery frequency | 3.15 | 2.98 | 0.16 | 0.2 | >0.9 |
Turkish forces | 2.91 | 3.28 | -0.36 | 0.015 | 0.5 |
Insurgents | 3.33 | 3.58 | -0.26 | 0.10 | >0.9 |
Irregular Militia | 3.46 | 3.43 | 0.03 | 0.9 | >0.9 |
Western PMC | 3.00 | 3.20 | -0.20 | 0.3 | >0.9 |
RAAS | 3.05 | 3.45 | -0.40 | 0.005 | 0.2 |
AAS | 3.30 | 2.80 | 0.50 | 0.002 | 0.070 |
Fogless RAAS | 3.59 | 3.04 | 0.55 | <0.001 | 0.026 |
Territory control | 3.37 | 2.75 | 0.62 | <0.001 | 0.025 |
Invasion | 3.09 | 3.52 | -0.44 | 0.020 | 0.7 |
Skirmish | 2.91 | 2.70 | 0.21 | 0.2 | >0.9 |
* P-value calculated using Welch's T-test. ** Q-value calculated using Bonferroni's correction |
#Remove previous
rm(diff_table, table20a)
#Build
dich_table <- df %>% mutate(over1k = ordered(recode(over1k,
`1` = "Yes",
`0` = "No"),
levels = c("Yes", "No"))) %>%
select('over1k', 'over1tt', 'sl', 'regular', 'clan') %>% tbl_summary(by= 'over1k',
type = everything() ~ "dichotomous", statistic = all_dichotomous() ~ "{p}%",
missing = "no", label = list('over1tt' ~ 'Over 1 year at TT?',
'sl' ~ 'Do you SL open squads often?',
'regular' ~ 'Server Regular',
'clan' ~ 'External clan member'),
digits = everything() ~ 0) %>% add_p(test = everything() ~ "chisq.test") %>%
modify_footnote(everything() ~ NA) %>%
modify_header(stat_1 = "1,000+ Hours (N={n})", stat_2="< 1,000 Hours (N={n})", label = "Dichotomous (Y/N) Variables") %>% as_hux_table()
#Format
table20b <- dich_table_fmt(dich_table) %>% set_caption("Table 20B - Experienced vs Newer players (Dichotomous variables)")
#Display
table20b
Dichotomous (Y/N) Variables | 1,000+ Hours (N=243) | < 1,000 Hours (N=69) | p-value |
---|---|---|---|
Over 1 year at TT? | 81% | 52% | <0.001 |
Do you SL open squads often? | 35% | 26% | 0.2 |
Server Regular | 80% | 81% | >0.9 |
External clan member | 51% | 13% | <0.001 |
* P-value calculated using Pearson's Chi-squared test |
Key results from tables 20A and 20B:
Check how to read the tables above here.
Strong statistical differences in preferences for Fogless RAAS and Territory control.
Weak statistical differences in Gameplay level, Overall server environment, and Basecamping enforcement. Experienced players also have a weaker preference for TLF, RAAS, and Invasion compared to newer players. Experienced players also have a slightly stronger preference for AAS.
Experienced players are more likely to be long term TT players and external clan members.
Experienced players demonstrate a stronger preference for Fogless RAAS, TC, and AAS when compared to newer players. Newer players show a slightly greater interest in Invasion compared to their experienced counterparts. Additionally, newer players prefer a slight increase in RAAS, while experienced players are content with the current frequency of RAAS on the server. These preferences are not unexpected. AAS, TC, and Fogless RAAS tend to reward experience, whereas RAAS is considered more accessible to new players. Indeed, the raison d’être of RAAS was to prevent backcap rushes, thereby supporting newer players. This is not to suggest that RAAS lacks strategy; on the contrary, experienced players who understand flag lanes gain a significant advantage over newer players who may not yet grasp the placement of flags on a given layer.
Experienced players report slightly lower satisfaction with the overall server environment while expressing greater contentment with the gameplay level. This is unsurprising, as experienced players often face the frustration in command chat when rounds go awry. Experienced players also have a deeper understanding of gameplay variation across servers, leading them to rate TT higher on this metric.
As we saw in Chart 3/Table 4, a significant minority of TT’s player base belong to external clans. The tables below examine whether this minority holds views that are different from the unaffiliated majority.
#Remove previous
rm(table20b, dich_table)
#Build
diff_table <- df %>% mutate(clan = ordered(recode(clan,
`1` = "Yes",
`0` = "No"),
levels = c("Yes", "No"))) %>%
select(admresp, admprof, admfair, admoverall, twork, learn, balance, skill, slserver,
toxic, maprot, moderot, votesat, fogsat, tcevent, vconnect,
basecamp, comms, asset, unitsat, unconv, light, medium, heavy, assym, tlf, ins, imf, wpmc,
raas, aas, fraas, tc, invasion, skirm, clan) %>%
tbl_summary(
by = 'clan',
type = everything() ~ "continuous",
statistic = all_continuous() ~ "{mean}",
missing = "no",
label = list(
'admresp' ~ 'Admin response time',
'admprof' ~ 'Admin professionalism',
'admfair' ~ 'Admin fairness',
'admoverall' ~ 'Overall admin quality',
"twork" ~ "Teamwork",
"learn" ~ "Learning Environment",
"balance" ~ "Team Balance",
"skill" ~ "Gameplay level",
"slserver" ~ "Environment for SLs",
"toxic" ~ "Overall server environment",
'maprot' ~ "Satisfaction with map rotation",
'moderot' ~ "Satisfaction with mode types",
'votesat' ~ "Satisfaction with voting system",
'fogsat' ~ "Satisfaction with fogless friday",
'tcevent' ~ "Interest in a TC focused event",
'vconnect' ~ "Interest in Voice connect mod",
"basecamp" ~ "Base camping enforcement",
"comms" ~ "Comms enforcement",
"asset" ~ "Asset claim system",
"unitsat" ~ "Satisfaction with unit types",
"unconv" ~ "Satisfaction with frequency of unconv. units",
"light" ~ "Light Units",
"medium" ~ "Medium Units",
"heavy" ~ "Heavy Units",
"assym" ~ "Asymtery frequency",
"tlf" ~ "Turkish forces",
"ins" ~ "Insurgents",
"imf" ~ "Irregular Militia",
"wpmc" ~ "Western PMC",
"raas" ~ "RAAS",
"aas" ~ "AAS",
"fraas" ~ "Fogless RAAS",
"tc" ~ "Territory control",
"invasion" ~ "Invasion",
"skirm" ~ "Skirmish"
),
digits = everything() ~ 2
) %>%
add_difference() %>%
add_q(method = "bonferroni") %>%
modify_footnote(everything() ~ NA) %>%
modify_header(stat_1 = "External clan member (N={n})", stat_2="Unaffiliated (N={n})",
label = "Continuous (1-10) Variables") %>%
remove_abbreviation("CI = Confidence Interval") %>%
as_hux_table() %>% {.[, -5]}
#Format
table21a <- diff_table_fmt(diff_table) %>% set_caption("Table 21A - External clans vs unaffiliated players (Continuous variables)") %>%
insert_row("Likert Scale variables (3 = About the same)", "", "", "", "", "", after = 22) %>% merge_cells(23, 1:6) %>% set_align(23, everywhere, "left") %>%
set_bold(23, everywhere) %>% set_text_color(23, everywhere, "black") %>%
set_background_color(23, everywhere, "grey95")
#Display
table21a
Continuous (1-10) Variables | External clan member (N=132) | Unaffiliated (N=179) | Difference | p-value | q-value |
---|---|---|---|---|---|
Admin response time | 8.33 | 8.39 | -0.07 | 0.7 | >0.9 |
Admin professionalism | 7.57 | 8.25 | -0.68 | 0.007 | 0.2 |
Admin fairness | 7.69 | 8.26 | -0.57 | 0.034 | >0.9 |
Overall admin quality | 8.02 | 8.43 | -0.41 | 0.077 | >0.9 |
Teamwork | 8.25 | 8.45 | -0.20 | 0.3 | >0.9 |
Learning Environment | 6.19 | 6.51 | -0.32 | 0.2 | >0.9 |
Team Balance | 6.48 | 6.66 | -0.19 | 0.5 | >0.9 |
Gameplay level | 8.60 | 8.65 | -0.05 | 0.8 | >0.9 |
Environment for SLs | 7.48 | 7.77 | -0.30 | 0.2 | >0.9 |
Overall server environment | 6.67 | 7.25 | -0.58 | 0.023 | 0.8 |
Satisfaction with map rotation | 6.71 | 7.28 | -0.57 | 0.042 | >0.9 |
Satisfaction with mode types | 7.57 | 7.76 | -0.18 | 0.5 | >0.9 |
Satisfaction with voting system | 7.55 | 7.79 | -0.24 | 0.4 | >0.9 |
Satisfaction with fogless friday | 7.18 | 7.72 | -0.54 | 0.083 | >0.9 |
Interest in a TC focused event | 5.98 | 6.28 | -0.30 | 0.5 | >0.9 |
Interest in Voice connect mod | 6.44 | 6.38 | 0.06 | 0.9 | >0.9 |
Base camping enforcement | 7.27 | 8.14 | -0.86 | 0.003 | 0.11 |
Comms enforcement | 8.10 | 8.30 | -0.20 | 0.4 | >0.9 |
Asset claim system | 8.26 | 8.60 | -0.34 | 0.2 | >0.9 |
Satisfaction with unit types | 7.06 | 7.58 | -0.53 | 0.052 | >0.9 |
Satisfaction with frequency of unconv. units | 6.18 | 6.76 | -0.58 | 0.042 | >0.9 |
Likert Scale variables (3 = About the same) | |||||
Light Units | 3.12 | 3.44 | -0.32 | 0.020 | 0.7 |
Medium Units | 3.69 | 3.46 | 0.22 | 0.029 | >0.9 |
Heavy Units | 2.79 | 2.85 | -0.05 | 0.7 | >0.9 |
Asymtery frequency | 3.02 | 3.18 | -0.17 | 0.2 | >0.9 |
Turkish forces | 2.77 | 3.16 | -0.39 | 0.003 | 0.12 |
Insurgents | 3.17 | 3.54 | -0.37 | 0.006 | 0.2 |
Irregular Militia | 3.43 | 3.47 | -0.04 | 0.7 | >0.9 |
Western PMC | 3.04 | 3.05 | -0.01 | >0.9 | >0.9 |
RAAS | 3.21 | 3.07 | 0.15 | 0.3 | >0.9 |
AAS | 3.27 | 3.13 | 0.14 | 0.3 | >0.9 |
Fogless RAAS | 3.56 | 3.41 | 0.14 | 0.3 | >0.9 |
Territory control | 3.28 | 3.21 | 0.07 | 0.7 | >0.9 |
Invasion | 2.93 | 3.37 | -0.44 | 0.004 | 0.15 |
Skirmish | 2.69 | 3.01 | -0.32 | 0.034 | >0.9 |
* P-value calculated using Welch's T-test. ** Q-value calculated using Bonferroni's correction |
#Remove previous
rm(diff_table, table21a)
#Build
dich_table <- df %>% mutate(clan = ordered(recode(clan,
`1` = "Yes",
`0` = "No"),
levels = c("Yes", "No"))) %>%
select('over1k', 'over1tt', 'sl', 'regular', 'clan') %>% tbl_summary(by= 'clan',
type = everything() ~ "dichotomous", statistic = all_dichotomous() ~ "{p}%",
missing = "no", label = list('over1k' ~ 'Over 1,000 hours?',
'over1tt' ~ 'Over 1 year at TT?',
'sl' ~ 'Do you SL open squads often?',
'regular' ~ 'Server Regular'),
digits = everything() ~ 0) %>% add_p(test = everything() ~ "chisq.test") %>%
modify_footnote(everything() ~ NA) %>%
modify_header(stat_1 = "External clan member (N={n})", stat_2="Unaffiliated (N={n})", label = "Dichotomous (Y/N) Variables") %>% as_hux_table()
#Format
table21b <- dich_table_fmt(dich_table) %>% set_caption("Table 21B - External clans vs unaffiliated players (Dichotomous variables)")
#Display
table21b
Dichotomous (Y/N) Variables | External clan member (N=132) | Unaffiliated (N=179) | p-value |
---|---|---|---|
Over 1,000 hours? | 93% | 66% | <0.001 |
Over 1 year at TT? | 77% | 73% | 0.4 |
Do you SL open squads often? | 34% | 32% | 0.8 |
Server Regular | 72% | 87% | 0.002 |
* P-value calculated using Pearson's Chi-squared test |
Key results from table 21A and 21B:
Check how to read the tables above here.
External clan members tend to have slightly less favourable views of the server. This is in line with historical norms (check Surveys 4 and 3).
There are no strong statistical differences in Table 21A.
Weak statistical differences in Admin professionalism and fairness, Overall server environment, satisfaction with map rotation, satisfaction with the frequency of unconventional units, and Basecamping enforcement. External clan members also show a less pronounced preference for light units, TLF, Insurgents, Invasion, and Skirmish.
External clan members are much more likely to be experienced players (1,000+ hours) and slightly less likely to be server regulars, though around 70% of them are.
There are minor differences in the preferences and opinions of external clan members compared to their unaffiliated counterparts. Clan members rate us slightly lower on Admin professionalism and fairness — though still around 7.5/10 — as well as in a few other categories, such as overall server environment and satisfaction with map rotation. The most significant differences lie in Basecamping enforcement and even that is less than a point. Anecdotally, some comments suggest that the lower ratings stem from disagreements with basecamping rules in general.
However, widespread disagreement with Basecamping rules is unlikely, as there is general satisfaction with the rule. Even external clan members, who are marginally less satisfied than unaffiliated players, rate Basecamping enforcement quite highly, at approximately 7/10. Other differences between the two groups are even smaller. Both external clan members and their unaffiliated counterparts express a desire for slightly more light units on the server, though clan members show a less pronounced preference; in both cases, scores are above 3, indicating a slight preference for more light units. This pattern also applies to the Insurgent faction, where both groups score above 3, but external clan members exhibit a less marked preference. External clan members prefer fewer rounds featuring TLF and Invasion, though these differences are slight, with all scores ranging around 2.5-3.5/5.
We did a survey last year that asked many of the same questions. The tables below examine whether our scores have changed from last year.
#Remove previous
rm(dich_table, table21b)
# Load Survey 4 data (Create temp env and load s4 data into containerised env)
temp_env <- new.env()
load("Data/s4.RData", envir = temp_env)
# Move to main env
s4 <- as.data.frame(temp_env$df)
# Remove temp env
rm(temp_env)
#Combine
merge_df <- df %>%
mutate(current = 1) %>%
bind_rows(s4 %>% mutate(current = 0)) %>%
select(admresp, admfair, admoverall, admprof, twork,
learn, balance, skill, toxic, maprot, moderot, slserver, basecamp, asset, comms,
clan, over1k, over1tt, sl, regular,
current)
#Remove old data
rm(s4)
#Build
diff_table <- merge_df %>% select(admresp, admfair, admoverall, admprof, twork,
learn, balance, skill, toxic, maprot, moderot, slserver, basecamp, asset, comms,
current) %>%
mutate(current = factor(current, levels = c(1, 0), labels = c("1", "0"))) %>%
tbl_summary(
by = 'current',
type = everything() ~ "continuous",
statistic = all_continuous() ~ "{mean}",
missing = "no",
label = list(
'admresp' ~ 'Admin response time',
'admprof' ~ 'Admin professionalism',
'admfair' ~ 'Admin fairness',
'admoverall' ~ 'Overall admin quality',
"twork" ~ "Teamwork",
"learn" ~ "Learning Environment",
"balance" ~ "Team Balance",
"skill" ~ "Gameplay level",
"slserver" ~ "Environment for SLs",
"toxic" ~ "Overall server environment",
'maprot' ~ "Satisfaction with map rotation",
'moderot' ~ "Satisfaction with modes played",
"basecamp" ~ "Base camping enforcement",
"comms" ~ "Comms enforcement",
"asset" ~ "Asset claim system"),
digits = everything() ~ 2) %>%
add_difference() %>%
add_q(method = "bonferroni") %>%
modify_footnote(everything() ~ NA) %>%
modify_column_hide(ci) %>%
modify_header(
stat_1 = "Current (N={n})",
stat_2 = "Previous (N={n})",
label = "Continuous (0-10) Variables") %>%
remove_abbreviation("CI = Confidence Interval") %>%
as_hux_table() %>% {.[, -5]}
#Format
table22a <- diff_table_fmt(diff_table) %>% set_caption("Table 22A - Survey 5 vs Survey 4 (Continious variables)")
#Display
table22a
Continuous (0-10) Variables | Current (N=312) | Previous (N=330) | Difference | p-value | q-value |
---|---|---|---|---|---|
Admin response time | 8.36 | 8.42 | -0.06 | 0.7 | >0.9 |
Admin fairness | 8.01 | 8.35 | -0.34 | 0.036 | 0.5 |
Overall admin quality | 8.25 | 8.63 | -0.37 | 0.011 | 0.2 |
Admin professionalism | 7.96 | 8.25 | -0.29 | 0.066 | >0.9 |
Teamwork | 8.37 | 8.13 | 0.24 | 0.061 | >0.9 |
Learning Environment | 6.37 | 6.43 | -0.05 | 0.8 | >0.9 |
Team Balance | 6.58 | 6.47 | 0.11 | 0.5 | >0.9 |
Gameplay level | 8.63 | 8.50 | 0.13 | 0.3 | >0.9 |
Overall server environment | 7.00 | 7.53 | -0.53 | 0.002 | 0.025 |
Satisfaction with map rotation | 7.04 | 7.02 | 0.02 | >0.9 | >0.9 |
Satisfaction with modes played | 7.68 | 7.29 | 0.39 | 0.021 | 0.3 |
Environment for SLs | 7.64 | 7.62 | 0.03 | 0.9 | >0.9 |
Base camping enforcement | 7.77 | 8.08 | -0.31 | 0.11 | >0.9 |
Asset claim system | 8.45 | 8.63 | -0.17 | 0.4 | >0.9 |
Comms enforcement | 8.21 | 8.36 | -0.14 | 0.4 | >0.9 |
* P-value calculated using Welch's T-test. ** Q-value calculated using Bonferroni's correction |
#Remove previous
rm(diff_table, table22a)
#Build
dich_table <- merge_df %>%
mutate(current = factor(current, levels = c(1, 0), labels = c("1", "0"))) %>%
select('over1k', 'over1tt', 'sl', 'regular', 'clan', 'current') %>% tbl_summary(by= 'current',
type = everything() ~ "dichotomous", statistic = all_dichotomous() ~ "{p}%",
missing = "no", label = list('over1k' ~ 'Over 1,000 hours?',
'over1tt' ~ 'Over 1 year at TT?',
'sl' ~ 'Do you SL open squads often?',
'regular' ~ 'Server Regular',
'clan' ~ 'External clan member'),
digits = everything() ~ 0) %>% add_p(test = everything() ~ "chisq.test") %>%
modify_footnote(everything() ~ NA) %>%
modify_header(stat_1 = "Survey 5 (N={n})", stat_2="Survey 4 (N={n})", label = "Dichotomous (Y/N) Variables") %>% as_hux_table()
#Format
table22b <- dich_table_fmt(dich_table) %>% set_caption("Table 22B - Survey 5 vs Survey 4 (Dichotomous variables)")
#Display
table22b
Dichotomous (Y/N) Variables | Survey 5 (N=312) | Survey 4 (N=330) | p-value |
---|---|---|---|
Over 1,000 hours? | 78% | 59% | <0.001 |
Over 1 year at TT? | 75% | 68% | 0.10 |
Do you SL open squads often? | 33% | 36% | 0.5 |
Server Regular | 80% | 89% | 0.005 |
External clan member | 42% | 34% | 0.031 |
* P-value calculated using Pearson's Chi-squared test |
Key results from Tables 22A and 22B:
Check how to read the tables above here.
The server has becomes markedly more experienced and has a much higher proportion of external clan members.
Strong statistical difference in Overall server environment.
Weak statistical differences in Admin fairness, Overall admin quality, and satisfaction with game modes played.
On most questions, the results are very similar to Survey 4.
There are strong demographic differences in in Table 22B. We see an increased amount of experienced players and external clan members.
Table 22A reveals minor statistical differences in Admin fairness and Overall admin quality; however, scores for all admin categories in both Survey 4 and Survey 5 remain approximately 8/10. Similarly, slight variations appear in Overall server environment and Satisfaction with game modes, yet scores for both categories hover between 7-8/10, showing little divergence. The most striking findings emerge in Table 22B, where the proportion of players with over 1,000 hours of gameplay at TT has increased from an already high 59% in the previous survey to nearly 80% in the current one. This shift is likely influenced by a rise in external clan members, from 34% to 43%, alongside an increase in experienced unaffiliated regulars.
While these trends are not entirely unwelcome, they raise concerns, particularly regarding whether TT is becoming less welcoming to newer players. Our learning environment has historically been less than ideal (see Table 8), but we have never before had such a high proportion of experienced players. The slight decline in server regulars is primarily driven by approximately a dozen responses from Reddit and an increase in external clan members, who are marginally less likely to be regulars (see Table 21B), though 80% of respondents remain regulars. This growing presence of experienced players and external clans calls for self-reflection. No server can thrive if it alienates newer players. TT’s mission has never been to serve as an “experienced player haven.” We have always aimed for a higher level of gameplay, but this should not translate to catering exclusively to experienced players. The lifeblood of the server lies in training newer players to achieve a higher standard. At its best, TT acts as a crucible where skilled players are forged. We must not lose sight of this essential mission.
Balance is a complex topic in squad. Mountains of digital ink have been spilt upon it. At TT, we collect a variety of data looking at unit types, unit loadouts, map types, and so on to come up with a complex internal system to determine what factions are viable on each map. This system is described in some detail in the Appendix. From a macro perspective, we have determined via an internal algorithm that roughly 14,000 layer and faction combinations are viable out of a possible universe of over 200,000 layer and faction combinations. These viable layers are the ones played on the server the vast majority of the time. We do stray out of the main pool from time to time, especially on Fogless Fridays and sometimes when an HQ (read: senior) admin deems it appropriate.
We also have a public facing balance policy listed in our rules based on ticket differentials over the past two rounds. After some triggers are reached, we typically look at the teams/factions and attempt to remedy balance on the server. This section will look at these two aspects of balance. In the first section, we will look at unit asymmetry and how this affects balance on the server, and in the second section, we shall look at ticket differentials and judge how well TT lives up to its publicly stated balance policies.
The dataset used in this section is the same as before covering rounds from September 28, 2024 to May 6, 2025.
The units played on the server can and do affect who wins and who loses. The table below tries to look at the question of unit asymmetry to try and quantify some of these effects.
# #Remove previous
rm(dich_table, table22b, merge_df)
#create data
mapsdf <- mapsdf %>%
mutate(
assy = case_when(
is.na(type_1a) & is.na(type_2a) ~ 0,
type_1a == type_2a ~ 0,
type_1a != type_2a ~ 1),
t1_win = ifelse(winner == 1, 1, 0),
t2_win = ifelse(winner == 2, 1, 0))
#Build table
table23 <-mapsdf %>% filter(!is.na(type_1a) & !is.na(type_2a)) %>% tbl_cross(
row = t2_win,
col = assy,
percent = "cell",
margin = c("row", "column"), label = list('assy' ~ 'Asymmetric Match-ups', 't2_win' ~ 'Team 1 Victory')) %>%
add_p('chisq.test') %>% modify_footnote(everything() ~ NA) %>%
as_huxtable()
#Format
table23 <- table23 %>% theme_article() %>% set_contents(1, 2, "Symmetric") %>%
set_bold(2,1) %>% set_bold(5,1) %>%
set_contents(1, 3, "Asymmetric") %>%
set_contents(3,1, "Yes") %>% set_contents(4,1, "No") %>% #Flip team 1 team 2
add_footnote("P-values calculated using Pearson's Chi-squared test.") %>%
set_width(1) %>% set_top_border(row = 5, col = everywhere, value = 1) %>%
set_caption("Table 23 - Win rates of Asymmetric match ups on large maps")
#Display
table23
Symmetric | Asymmetric | Total | p-value | |
---|---|---|---|---|
Team 1 Victory | 0.086 | |||
Yes | 1,104 (40%) | 297 (11%) | 1,401 (51%) | |
No | 1,104 (40%) | 252 (9.1%) | 1,356 (49%) | |
Total | 2,208 (80%) | 549 (20%) | 2,757 (100%) | |
P-values calculated using Pearson's Chi-squared test. |
Key results from Table 23:
As expected, the split between Teams 1 and 2 is almost exactly 50/50 (see totals column).
Approximately 80% of matches on large layers are symmetric (see totals row). By definition, all matches on small and medium layers (e.g., maps like Chora, Kokan, Mestia) are symmetric, as the only unit type available on those layers is combined arms. Notably, respondents in Table 14 expressed satisfaction with the frequency of asymmetric match-ups on the server, indicating approval of the 80/20 symmetric/asymmetric mix.
On applicable maps (i.e., “large” layers where unit types other than “combined arms” can be selected, as noted in Table 16), the win percentages for symmetric and asymmetric match-ups show no statistically significant difference.
The table above only looks at “Large” layers (see Table 16) but the results are no different if we include all layers. However, we only want to look at asymmetry where asymmetry is actually possible. Whether a unit is “Light”, “Medium”, or “Heavy” is defined the same way as it is in Table 16. If a “Light” unit goes up against a “Medium” or “Heavy” unit, then that match is classified as asymmetric. This definition does not wholly adhere to OWI’s definition, but it is close. For example, I choose to classify “WPMC Combined Arms Large” as a Light unit while OWI would classify all “Large Combined Arms” as “Medium” units.
These definitions, both OWI’s and mine, are somewhat arbitrary. For example, is a match between Turkish Combined Arms and USMC Combined Arms really symmetric? In the table above this match-up is classified as symmetric, however, I do not think many would agree with this classification since USMC LAVs are far and above better than their Turkish counterparts. The point is that symmetric or asymmetric is not really a binary distinction.
At TT, we use a specialised internal algorithm to give each sub-faction scores along various dimensions (armour, anti-armour, logistics, etc.) and we select match-ups on the basis of these scores. This system is described in the Appendix and the topic of asymmetry is explored further.
We can look at average ticket differentials on the server. The figure below sheds some light on this. I count only RAAS, Fogless RAAS, AAS and TC rounds.
# #Remove previous
rm(table23)
#Tick buckets
mapsdf <- mapsdf %>%
mutate(tick_bucket = factor(
case_when(
tickets <= 100 ~ "100 or below",
tickets <= 150 & tickets > 100 ~ "101 to 150",
tickets < 200 & tickets > 150 ~ "151 to 199",
tickets >= 200 ~ "Above 200",
is.na(tickets) ~ "Unknown" # Handle NA values explicitly
),
levels = c("100 or below", "101 to 150", "151 to 199", "Above 200"),
ordered = TRUE
))
#Chart
chart17 <- mapsdf %>% filter(!mode %in% c("Destruction", "Invasion", "Skirmish")) %>%
select(tick_bucket) %>%
ggplot(aes(x = factor(1), fill = tick_bucket)) +
geom_bar(width = 1)+ coord_polar("y") + blank_theme +
theme(axis.text.x=element_blank()) +
scale_fill_brewer("Ticket differentials", palette = "Set1") +
theme(axis.text.y=element_blank()) +
labs(title = "Chart 17: Ticket differentials for RAAS/AAS/FRAAS/TC") +
theme(plot.title = element_text(hjust = 0.5))
chart17
# #Remove previous
rm(chart17)
#Build
table24 <- mapsdf %>% filter(!mode %in% c("Destruction", "Invasion", "Skirmish")) %>%
select(tick_bucket, size) %>%
tbl_cross(row = 'tick_bucket', col = 'size', percent="column", margin = "column",
label = list('tick_bucket' ~ 'Ticket Differentials','size' ~ 'Layer Size')) %>%
add_p() %>% modify_footnote(everything() ~ NA) %>% as_huxtable()
#Format
table24 <- table24 %>% theme_article() %>% set_bold(2,everywhere) %>%
set_background_color(2, everywhere, "#6aa84f") %>%
set_text_color(2, everywhere, "white") %>%
add_footnote("P-values calculated using Pearson's Chi-squared test. Significant at the 99% level.") %>%
set_width(1) %>% set_caption("Table 24 - Ticket differentials by layer type (RAAS/FRAAS/AAS/TC only")
# set_bold(7,1) %>%
# set_top_border(row = 7, col = everywhere, value = 1) %>%
table24
Large | Medium | Small | Total | p-value | |
---|---|---|---|---|---|
Ticket Differentials | 0.014 | ||||
100 or below | 1,052 (38%) | 164 (34%) | 59 (48%) | 1,275 (38%) | |
101 to 150 | 518 (19%) | 91 (19%) | 27 (22%) | 636 (19%) | |
151 to 199 | 444 (16%) | 71 (15%) | 11 (8.9%) | 526 (16%) | |
Above 200 | 726 (26%) | 154 (32%) | 27 (22%) | 907 (27%) | |
P-values calculated using Pearson's Chi-squared test. Significant at the 99% level. |
We can see that the ticket differentials are statistically different depending on the layer size. Here, we are using OWI’s definitions of layer size detailed in this spreadsheet.
Roughly 40% of all matches played at TT result in a differential of less than 100 and this is also true for large layers. Slightly more than a quarter of all matches played at TT are what we would typically classify as “rolls” with ticket differentials of greater than 200. Medium layers tend to have more “rolls” (differentials greater than 200) while small layers tend to have tighter differentials probably because they are largely infantry focused with fewer tickets to begin with in the first place.
A lot of balance is about “weaker” and “stronger” teams. To look at this question, we need to look at win streaks. Before we do that, it is important to state TacTrig’s balance policy in clear terms. I quote the following directly from the server rules:
Tactical Triggernometry — Balance policy (from the rules)
At TacTrig, we take team balance seriously. As such we expect our players to abide by the following policy:
Furthermore, to promote balanced gameplay and a positive player experience:
If a team suffers two heavy defeats (200+ tickets), current team balance will be discussed in the TT discord with all players present. Players may be asked to swap teams to correct balance issues, however no players will be forced to swap. If you as a player believe there are issues with team balance, please bring them up constructively in all chat or directly with an admin so that we are aware of it and can work on the issue.
If the team balance situation is not fixed after three rounds, a team randomiser will be used. This may seem like a drastic measure, but it is necessary to ensure fair gameplay.
The guidelines above set us a clear yardstick by which to measure ourselves by. In this section, I am trying to answer three questions:
How well do we adhere to our stated balance policy?
What happens if we change the definition of a streak from “two rounds of 200+ ticket rolls” to something else?
Is our balance policy fit for purpose?
The chart below looks at balance through the lens of TT policy and shows how well we measure up to our stated goals.
#Remove previous
rm(table24)
#Overall
all_streaks <- mapsdf %>%
select(day, winner, layer, mode, map, tickets,
tick_bucket, start_est, end_est, dur, dow, day) %>%
mutate(time_diff = start_est - lag(end_est))
# Streaks (Raw)
all_streaks<- all_streaks %>%
mutate(win_count = if_else(winner!= lag(winner) & time_diff<=100, 1, 0)) %>%
mutate(win_count = ifelse(time_diff>=100, 0, win_count)) %>% mutate(win_count = replace_na(win_count, 0)) %>%
mutate(raw_streak = if_else(win_count!=0, as.numeric(sequence(rle(win_count)$lengths)), 0) + 1)
# Streaks (150 standard)
all_streaks <- all_streaks %>%
mutate(win_count_150 = if_else(winner != lag(winner) & tickets >= 150 & time_diff <= 100, 1, 0)) %>%
mutate(win_count_150 = ifelse(time_diff>=100, 0, win_count_150)) %>% mutate(win_count_150 = replace_na(win_count_150, 0)) %>%
mutate(streak_150 = if_else(win_count_150 != 0, as.numeric(sequence(rle(win_count_150)$lengths)), 0)) %>%
mutate(streak_150 = if_else(tickets <150 & streak_150 == 1, 0, streak_150)) %>%
mutate(streak_150 = if_else(tickets >=150 & streak_150 == 0, 1, streak_150))
# Streaks (TT Standard)
all_streaks <- all_streaks %>%
mutate(win_count_tt = if_else(winner != lag(winner) & tickets >= 200 & time_diff <= 100, 1, 0)) %>%
mutate(win_count_tt = ifelse(time_diff>=100, 0, win_count_tt)) %>% mutate(win_count_tt = replace_na(win_count_tt, 0)) %>%
mutate(tt_streak = if_else(win_count_tt != 0, as.numeric(sequence(rle(win_count_tt)$lengths)), 0)) %>%
mutate(tt_streak = if_else(tickets <200 & tt_streak == 1, 0, tt_streak)) %>%
mutate(tt_streak = if_else(tickets >=200 & tt_streak == 0, 1, tt_streak))
# saveRDS(all_streaks, file = "Data/all_streaks.rds")
#TT standard
dfc18 <- all_streaks %>% select(tt_streak) %>% count(tt_streak, sort= TRUE)
total <- nrow(all_streaks)
title <- paste0("Chart 18: Defined by consecutive wins of 200+ tickets"," (Total matches: ", total, ")")
#150 standard
dfc18a <- all_streaks %>% select(streak_150) %>% count(streak_150, sort= TRUE)
#Raw
dfc18b <- all_streaks %>% select(raw_streak) %>% count(raw_streak, sort= TRUE)
# 0_00_0's "weight of previous round" argument
dfc18 <- dfc18 %>% mutate(alt = ifelse(tt_streak!= nrow(dfc18)-1,
tt_streak*(n-lead(n)), tt_streak*n)) %>%
mutate(alt = ifelse(tt_streak == 0, n, alt))
dfc18a <- dfc18a %>% mutate(alt = ifelse(streak_150!= nrow(dfc18a)-1,
streak_150*(n-lead(n)), streak_150*n)) %>%
mutate(alt = ifelse(streak_150 == 0, n, alt))
#Build Chart
chart18 <- ggplot(
dfc18, aes(x = reorder(tt_streak, -n), y = n, label = n, fill = reorder(tt_streak, -n))) +
geom_col(position = "dodge", width = 0.75) +
theme_classic() +
scale_y_continuous(limits = c(0, 2600)) +
xlab("Streak Length") +
ylab("Frequency count") +
labs(title = title) +
theme(plot.title = element_text(hjust = 0.5), legend.position = "none") +
geom_text(vjust= -1, hjust = 0.5, size = 2.75) +
scale_x_discrete(labels = c("No streak", "1+", "2+", "3+", "4")) +
scale_fill_manual(values = c("#99ccff", "#99ccff", "#99ccff", "red", "red"))
#Display
chart18
Each streak of length N inherently includes streaks of length N-1. For instance, achieving a streak of 4 means the preceding round contributes to the count of streaks of 3, the round before that contributes to streaks of 2, and so forth. In simpler terms, there were 25 violations where there was a streak 3 or more consecutive rounds where the same team won by more than 200 tickets. Of those 25 violations, there were only 4 times where 2 policy violations occurred in a row. This implies that 25 - 4 = 21 violations occurred individually by themselves. There is not a single instance where a violation occurs thrice or more in a row.
Out of 3436 rounds, there were 25 + 4 = 29 violations of our policy. This translates a rate of approximately 0.84% — a negligible amount. This may come as a surprise to many people in the comments who ceaselessly complain, but the truth of the matter is that we live up to our stated goals.
Now, some might argue that our goals are too modest. Perhaps “2 rounds of 200” is too lax a standard especially since the ICO made games longer (see Survey 4 released shortly after the ICO). This is indeed a valid criticism. From the chart above we can see that 2499 or 72.73% of rounds are wins of fewer than 200 tickets (to be counted as “streak equals 1” means that a team has to win by at least 200 tickets to begin with). This means that, at least according to our standards, we would not even be talking about balance in over 70% of rounds to begin with! Policy violations only occur if a team wins two consecutive rounds by more than 200 tickets each.
Next, I look at what happens if I change the standard. In the charts below, I calculate streaks in three more ways:
Change the threshold of “roll” from 200 to 150.
Count consecutive wins, regardless of ticket count. This is equivalent to setting the threshold to 0.
Show an alternative way of counting all rounds in a given streak.
The idea of this exercise is to understand what drives perceptions of balance on the server since it is something we do spill quite a bit of ink on.
The first alternative way of measuring the state of balance on the server is to simply ask the question “what if we we lowered the threshold from 200 tickets to 150?” This is a useful exercise because total starting tickets on many maps were lowered from 300 to 250 tickets by OWI with the release of version 7.2. The chart below illustrates the result of this calculation:
#Remove previous
rm(chart18)
#150tick standard
title <- paste0("Chart 18A: Defined by consecutive wins of 150+ tickets"," (Total matches: ", total, ")")
#Build Chart
chart18a <- ggplot(
dfc18a, aes(x = reorder(streak_150, -n), y = n, label = n, fill = reorder(streak_150, -n))) +
geom_col(position = "dodge", width = 0.75) +
theme_classic() +
scale_y_continuous(limits = c(0, 2000)) +
xlab("Streak Length") +
ylab("Frequency count") +
labs(title = title) +
theme(plot.title = element_text(hjust = 0.5)) +
geom_text(vjust= -1, hjust = 0.5, size = 2.75) +
scale_x_discrete(labels = c("No streak", "1+", "2+", "3+", "4+", "5")) +
scale_fill_manual(values = c("#99ccff", "#99ccff", "#99ccff", "red", "red", "red"), guide = "none")
#Display
chart18a
If we use a threshold of 150, we can see from the chart above there would be 93 violations out of 3436 rounds that are part of a streak of three or more consecutive wins by one team of more than 150 tickets. The translates to a violation rate of approximately 2.71%.
Therefore, even if TT lowered its policy threshold to 150 tickets, we would still be in compliance 97.29% of the time.
Based on these results, we are going to lower our policy threshold to 2 losses of 150 tickets. Readers should understand that ticket differentials are not the only factor we consider when evaluating balance—we also take into account other elements, such as streaks, which group is on which side, and so on. However, while we assess balance holistically, we also strive to adhere to a publicly stated ticket standard.
If we use the simplest definition of counting consecutive wins, regardless of ticket count, things change quite a bit.
# #Remove previous
rm(chart18a)
#150tick standard
title <- paste0("Chart 18B: Defined by simple consecutive wins"," (Total matches: ", total, ")")
#Build Chart
chart18b <- ggplot(
dfc18b, aes(x = reorder(raw_streak, -n), y = n, label = n)) +
geom_col(fill="#99ccff", position = "dodge", width = 0.75) +
theme_classic() +
scale_y_continuous(limits = c(0, 1500)) +
xlab("Streak Length") +
ylab("Frequency count") +
labs(title = title) +
theme(plot.title = element_text(hjust = 0.5)) +
geom_text(vjust= -1, hjust = 0.5, size = 2.75) +
scale_x_discrete(labels = c("No streak", "2+", "3+", "4+", "5+", "6+", "7+", "8+", "9+", "10+", "11+", "12+", "13"))
#Display
chart18b
Before proceeding, it is important to note a key difference in how Chart 18 and Chart 18A handle streak counts compared to the chart above. In Chart 18 and Chart 18A, streak counts begin at “1” following “No Streak,” whereas the chart above starts its count at “2.” This discrepancy arises due to the presence of ticket thresholds in the earlier charts — 200 tickets for Chart 18 and 150 tickets for Chart 18A — which are absent in the current chart. In the previous charts, a round was only counted as “a streak of 1” if the ticket differential exceeded the respective threshold. In contrast, the chart above has no ticket threshold, meaning every round won by a team is considered “a streak of at least 1.” Consequently, if a team wins a round and then wins a subsequent round, it is recorded as “2”; if it wins another round after that, the count increases to “3,” and so forth.
We can see that there are cases of somewhat extreme streaks! There were 4 instances where a streak crossed 12 rounds and in 2 of those instances, the streak reached 13 rounds!
The chart above also answers a simple, yet important, question: What is the probability that a team will win the next round given that it has won the round before. Mathematically, this can be defined and calculated as:
\[ P(Win\;next\;round\;|\;Won\; Previous ) = \frac{P(Streak \;of\; atleast\;2 )}{P(No\;Streak)} = \frac{818}{1435} \approx 57 \% \]
As seen above, if we use the simplest definition of a streak (ie, simple consecutive wins), this probability works out to be approximately 57%. This means that a team will win the current round roughly 57% of the time if it won the prior one. Not quite 50/50: a team does have some persistence.
The more serious problem from Chart 18B is that there were 92 instances where a streak crossed five rounds. Even though only a negligible amount of rounds (see Chart 18 above) can be counted as “TT policy violations,” I would argue that these streaks account for a large portion of public perceptions of balance problems. However, without taking into account ticket differentials, it is hard to comment on this further. If a couple of rounds in a 5 round streak are differentiated by 10 tickets, I would think most players would be satisfied with that outcome in terms of balance.
To illustrate some of these issues, let us take one of the two extreme 13 round streaks to illustrate the point:
# #Remove previous
rm(chart18b)
#Build table
table25<- all_streaks %>% select(Layer=layer, "Datetime (EST)"=start_est, Winner=winner, Tickets = tickets, "Streak counter" = raw_streak,
"150 threshold"= streak_150, "200 threshold" = tt_streak) %>%
filter(row_number() %in% 142:154) %>% as_hux() %>%
set_width(1) %>% set_caption("Table 25 - Illustrating an extreme streak") %>% theme_article() %>%
set_background_color(evens, everywhere, "grey95")
#Display
table25
Layer | Datetime (EST) | Winner | Tickets | Streak counter | 150 threshold | 200 threshold |
---|---|---|---|---|---|---|
Kokan_AAS_v2 | 2024-10-05 13:16:25 | 2 | 38 | 1 | 0 | 0 |
GooseBay_RAAS_v1 | 2024-10-05 14:07:20 | 1 | 139 | 2 | 0 | 0 |
Belaya_RAAS_v1 | 2024-10-05 14:59:53 | 2 | 42 | 3 | 0 | 0 |
BlackCoast_RAAS_v2 | 2024-10-05 16:06:08 | 1 | 41 | 4 | 0 | 0 |
Yehorivka_Skirmish_v2 | 2024-10-05 16:59:21 | 2 | 6 | 5 | 0 | 0 |
Gorodok_AAS_v1 | 2024-10-05 17:31:16 | 1 | 43 | 6 | 0 | 0 |
Logar_RAAS_v1 | 2024-10-05 18:15:23 | 2 | 293 | 7 | 1 | 1 |
Sanxian_AAS_v1 | 2024-10-05 18:44:25 | 1 | 97 | 8 | 0 | 0 |
Sumari_RAAS_v1 | 2024-10-05 19:46:00 | 2 | 50 | 9 | 0 | 0 |
Manicouagan_AAS_v1 | 2024-10-05 20:24:52 | 1 | 147 | 10 | 0 | 0 |
GooseBay_RAAS_v1 | 2024-10-05 21:14:32 | 2 | 184 | 11 | 1 | 0 |
BlackCoast_RAAS_v2 | 2024-10-05 22:01:07 | 1 | 125 | 12 | 0 | 0 |
Lashkar_RAAS_v1 | 2024-10-05 22:55:08 | 2 | 133 | 13 | 0 | 0 |
This example shows the nuances involved with balance. We can see that on the 5th of October, 2024, the day opened with a match on Kokan after the server was seeded. The team that won the initial match went on to win 12 more rounds afterwards. A person who played 3-5 hours on the server during those rounds on the losing team would be correct to be somewhat upset by this outcome. However, let us consider the following:
The server experiences regular population inflow and outflow. Is the server at 9PM EST the same as it was at 2PM EST? Obviously not. Therefore, balance decisions taken at 9PM EST should not really consider the context of what was going on at 2PM EST, a full 7 hours ago.
What should have been done to break the streak? Should the admins have randomised the games after the streak crossed 5 rounds? That would be strange as the 5th round was a very close (albeit, Skirmish) 6 ticket round! What about after the 6th round? However, then the discussion would focus on the fact that the 5 out of the last 6 rounds had ticket differentials of fewer than 100 tickets!
The biggest “blowout” occurred on Logar at 6:15PM EST. However, both players and admins understand that Logar is a very momentum driven map that depends heavily on rollout. Generally, the team that wins the fight in the villages in the central valley will win the round very quickly (the round lasted 29 minutes). Given that the few rounds before Logar were reasonably close, admins would have waited before balancing. The two rounds after Logar were also reasonably close (97 tickets on Sanxian and 50 tickets on Sumari respectively), further weakening the case for drastic balancing actions.
At no point did a team win two consecutive rounds by more than 150 tickets, let alone 200 tickets. The balance policy was not violated. Yet, despite the nuances that I have described, a 13 round streak is not a particularly desirable or defensible outcome. The players on the side that lost 13 rounds in a row must have been quite miserable. This is why I propose an alternate streak counting measure below to get some idea of how many of these “bad rounds” there are where players are stuck in losing streaks.
The way that streaks are counted above means that in a three round streak, only the third round would be counted as a “3” and if the subsequent round was also a win for the streaking team, that round would be counted in “4.” However, some (specifically 0_ZERO_0) might argue that “all rounds of a streak should be counted. There are arguments for and against this. For instance, to measure policy violations, one cannot count the first two rounds because the policy allows for a two round streak.
A flaw in this method is that it implies “all rounds in a 5 (or any) round streak count equally.” This means that players in the 2nd round somehow have “foreknowledge” and can see into the future that they are about to lose the next 3. If we were going by raw streaks, this would be the equivalent of setting the “raw streak” counter to 13 for all of the 13 round streak in Table 25.
It is not really possible to say that the cumulative weight of losing previous rounds exists in the same way in the third round of a 3 round streak (when the prior 2 rounds have been heavy losses) versus the first round of that same streak (when players do not know what will happen in the next 2 rounds). However, this method of counting treats all rounds in the streak as equivalent. A counterargument is that when we count “all rounds that are a part of a streak” we get an idea of the “balance environment” on the server. From Chart 17 we already know that fewer than a third of the rounds have ticket differentials exceeding 200. Some portion of that third are going to be part of streaks of 200 or more ticket losses. The chart below finds out how many.
#Remove previous
rm(chart18b, table25)
# 0_00_0s weight argument
#Setup
title <- paste0("Chart 19: Streaks of 200+ counting all rounds"," (Total matches: ", total, ")")
#Build Chart
chart19 <- ggplot(
dfc18, aes(x = reorder(tt_streak, -alt), y = alt, label = alt, fill = reorder(tt_streak, -alt))) +
geom_col(position = "dodge", width = 0.75) +
theme_classic() +
scale_y_continuous(limits = c(0, 2600)) +
xlab("Streak Length - All rounds") +
ylab("Frequency count") +
labs(title = title) +
theme(plot.title = element_text(hjust = 0.5)) +
geom_text(vjust= -1, hjust = 0.5, size = 2.75) +
scale_x_discrete(labels = c("No streak", "1+", "2+", "3+", "4")) +
scale_fill_manual(values = c("#99ccff", "#99ccff", "red", "red", "red"), guide = "none")
#Display
chart19
Going by this measurement, we can see that a total of 144 + 63 + 16 = 223 rounds were a part of a streak greater than or equal to 2 where the ticket differentials exceeded 200 tickets. This translates to approximately 6.5% of rounds. Again, I must stress that this method of counting should be interpreted very carefully. This is not to say that we are in “violation” of our policy 6.5% of the time. Our violation rate, measured in Chart 18, is negligible. The correct way to interpret this 6.5% number is that this is the proportion of times when the balance “environment” has become “bad” even though it is not a violation of our publicly stated guidelines.
By measuring win streaks by all these additional standards and showing examples, I hope to mitigate the argument that TT is living by a lax/conservative yardstick of balance. We have seen that the balance situation, even measured by multiple different standards, is perhaps the best that can be hoped for in a public server.
Let us check whether we can identify some clusters and groups in the survey scores:
#Remove previous
rm(dfc18, dfc18a, dfc18b, chart19)
#Create a new analysis frame
df2 <- df
#Row means which handle missing values sensibly
df2$admscore <- rowMeans(df2[, 8:11], na.rm=TRUE)
df2$envscore <- rowMeans(df2[, c(12:16, 20, 46, 53, 63)], na.rm=TRUE)
#Modify new analysis frame - prepare for kmeans clustering
df2 <- df2 %>% select('admscore', 'envscore') %>% filter(!is.na(admscore))
#Seed set to random number, 2, for reproducibility
set.seed(32)
#Cluster
clust <- kmeans(df2, centers = 4)
#Map cluster ID to OBS
df2$cluster <- as.character(clust$cluster)
#df3 %>% ggplot(aes(x=admscore, y=envscore, colour = cluster)) + geom_jitter(width = 0.1, height = 0.1) + theme_bw()
########OPTIMAL CLUSTERS############
# k.max <- 10
# data <- df3
# wss <- sapply(1:k.max,
# function(k){kmeans(data, k, nstart=50,iter.max = 15 )$tot.withinss})
# wss
# plot(1:k.max, wss,
# type="b", pch = 19, frame = FALSE,
# xlab="Number of clusters K",
# ylab="Total within-clusters sum of squares")
### THE ANSWER IS 4 - YOU CAN SEE IT IF YOU RUN THIS
########OPTIMAL CLUSTERS############
df2 <- df2 %>% mutate(Group= case_when(cluster==1 ~ "Admin Good Env Avg",
cluster==2 ~ "Both Excellent",
cluster==3 ~ "Both Decent",
cluster==4 ~ "They hate us"))
df2$Group<- ordered(df2$Group, levels = c("Both Excellent", "Both Decent", "Admin Good Env Avg", "They hate us"))
#Build chart
clust_chart <- ggplot() +
geom_jitter(data = df2, aes(x=admscore, y=envscore, colour = Group), width = 0.1, height = 0.1) +
geom_point(mapping = aes_string(x = clust$centers[, "admscore"], y = clust$centers[, "envscore"]),color = "magenta", size = 4) +
theme_bw() +
scale_color_manual(values = c("black", "blue", "purple", "red" )) +
theme(panel.border = element_blank(), panel.grid.major = element_blank(), panel.grid.minor = element_blank(), axis.line = element_line(colour = "black")) +
xlab('Average score for admin categories') + ylab('Average score for environment categories') +
labs(title = "Chart 20: Why play at TT?") + theme(legend.position = "bottom") + theme(plot.title = element_text(hjust = 0.5))
#Display
clust_chart
The average scores of participants for administration categories is shown along the X-axis and the average score for the server environment categories is shown on the Y-axis. I assume that people play at TT because they either like the way it is run (the administration) or because they like the environment. I do not think people play at TT because of our asset claim system or because they like how we enforce base camping. So, these questions about rules were not considered when producing this chart.
This means that everything from teamwork, learning environment, balance, overall server environment, and satisfaction with maps, modes, units played, and voting were used to calculate the environment score.
Participants are assigned to groups using a clustering algorithm. Chart 20 above shows roughly four groups:
#Build
table26 <- df2 %>% select(admscore, envscore, Group) %>%
group_by(Group) %>%
summarise(N = n(), "Group Mean (Admin)" = round(mean(admscore), 2), " Group Mean (Environment)" = round(mean(envscore), 2))
#Format
table26 <- as_hux(table26) %>% theme_article() %>% set_caption("Table 26 - Cluster Statistics")
#Display
table26
Group | N | Group Mean (Admin) | Group Mean (Environment) |
---|---|---|---|
Both Excellent | 122 | 9.45 | 8.53 |
Both Decent | 82 | 7.78 | 7.75 |
Admin Good Env Avg | 67 | 8.17 | 6.26 |
They hate us | 31 | 3.94 | 4.67 |
Key results from table 25:
The largest group of respondents gave very high ratings to both admin and server environment categories.
The second-largest group rated both categories as “good,” with average scores of approximately 8/10 across admin and server environment categories.
A smaller group considered the server environment categories to be average, though they still rated admin categories relatively well.
A very small group of respondents rated both admin and server environment categories poorly. Alas, you cannot please everyone.
Thank you for reading the survey report!
Affinity and Chaos Muppet have responded to a selection of comments below for your perusal and enjoyment!
Comment 2
Dear Affinity,
God please vet admins better. No more 12 year olds.
Sincerely,
Oh Baby A Triple II
Response 2
Dear Oh Baby A Triple II,
I can assure you we have no 12 year olds on staff. I can provide you an incomplete list of volunteers:
14 hamsters in the IT group. Technically, this is two groups of 7 hamsters. One group spins the server’s wheels with a second–shift. Server rack terrarium made possible by community donations.
1 Russian guy whose only role is to play on Jensen’s Range. We are fairly confident he is an adult, although no one reads Russian.
1 Nondescript Finnish adult. Claims to understand the hamsters. Reports directly to them.
1 Descript Italian adult. No matter how often it is explained this man does not appear to understand we do not see him gesturing vigorously at a computer screen.
Some number of Americans and Canadians. Potentially adults. We try our best to hide from this group. We recommend you do the same.
We don’t have any 12 year olds. Yet. It might not be such a bad idea. Twelve year olds are beginning to reach peak human reaction times. That means they could potentially respond quickly to reports. Twelve year olds are familiar with Minecraft, so they could effectively fortify radio positions with buildables. That could raise the bar of gameplay. Food for thought.
Sincerely,
Affinity
Comment 3
Dear Affinity,
ADMINS! Why have we gone soft? The experience preferred principle has been diminished by an abundance of new player vibes - muddying the ideal expectancy / experience.
Be less noob-friendly. There are plenty of servers like that. It’d be great if TT would work back toward being a server known for experienced players only.
Sincerely,
Gny. Sgt. Hartman
Response 3
Dear Gunny,
What the fuck did you just fucking say about me? I’ll have you know I graduated top of my class in Squad University, and I’ve been involved in numerous online Squad tournaments. I have over 6 confirmed championship wins. I am trained in HAB v. HAB warefare. Is that hard enough for you, Gunny?!
The server is who shows up, who takes a role, and who coordinates. It has never meaningfully been about who admins decide can stay. Admins can put their thumbs on the scale in small measures, admins can track problem players, but they cannot will a player base into existence. We will continue to keep eyes open for methods to increase the level of play, but bashing the less skilled, even if done with unreasonable accuracy, is not sustainable.
Admins are happy to receive help tracking players that are problematic. Give a shout in #contact-admins, or message an admin you trust.
Sincerely,
Affinity
Comment 4
Dear Affinity,
I disagree that backcapping with a full squad is a waste of assets. It’s why half the matches end with one team back at their base- they never capped their points before the other team pounced on them. Could the tip suggest that people not roam to the other side of the map instead of not spending an entire squad on backcap?
Sincerely,
Brave Backcap Degenerate
Response 4
Dear Brave Backcap Degenerate,
Trade cap speed for position. Use position for space. Trade space for pressure. Use pressure for position. Win.
I agree the server message could be argued as too absolute. There are cases where heavier back caps are neutral or helpful given the variety of lanes. However, stacked back caps in the community with detrimental outcomes are so common that it likely justifies the propaganda.
TacTrig admin and player MyEggo published his thesis on back cap strategy here:
Part 1: https://youtu.be/1dAyA7Kzys8
Part 2: https://youtu.be/YPyJ-fvSPK4
Sincerely,
Affinity
Comment 5
Dear Affinity,
Moderation is inconsistent and tainted with nepotism and favoritism
Sincerely,
Primary Target
Response 5
Dear Primary Target,
Identify yourself as soon as possible, coward. I need to know if you deserve to be punished for this heresy or lauded for a pointed critique.
An intentional team kill is wrong, but an intentional team kill is less wrong if a person has asked to be team killed. A person that has played 500 hours on the server without issue has a different type of violation than someone that joined two minutes ago. You won’t find perfect consistency in any system that wields judgment or engages with a concept of fairness.
If you disagree the best I can do is encourage you to continue making evidenced reports. The team will receive them and they may give the report more consideration than is sane. Depends on who you are.
Sincerely,
Affinity
Comment 6
Dear Affinity,
I like the TT team balance rule, but it suffers from inconsistent enforcement, and it’s missing a stipulation about a team losing too many games in a row. Once a team has lost three or four games by over 100 tickets, it’s time for a balancing action. One issue is because the 200 ticket threshold is so well known; I’ve heard regulars try to game the system by trying to prolong a game that they’re clearly dominating, so when they win the difference will be less than 200 tickets, so no re-balance is required. This highlights the need for more flexibility in the balance rule, so people are not gaming the system.
Sincerely,
Charles Blondin
Response 6
Dear Charles Blondin,
A player trying to game the 200 ticket rule would be mistaken if they think that is a trump card. Randy discusses balance policy at length in the report above including consistency. Instead of retreading that I will hijack your comment for fun napkin arithmetic.
In late 2020, TacTrig installed a SquadJS plugin that logs all server chats to Discord. Since the day the plugin came online – approaching 1900 days – the word “balance” has been mentioned on the server ~11,500 times. If we search everything except private admin chat, then the word “balance” has been used 8,650 times. This is imperfect, as these logs also count server broadcasts (RIP “We Take Balance Very Seriously”) and also includes any admins who use the word “balance” in public. Still, it’s safe to assume many mentions are from normal players. This averages out to around 4.6 balance mentions per day outside of admin chat.
If we look at the server’s admin chat, then across the entire logged history of the TT server admins averaged 1.5 “balance” word mentions a day. That’s around a 3:1 ratio between player “balance” words and admins. Is it though? At 1.5 times a day, a nearly identical rate, admins also used the word “swap” in private. Admins used the word “switch” at a slightly lower rate, only ~1.3 times a day. For anyone familiar, yes that search excludes admin commands.
Speaking of, the most common in-game command admins use for balance actions was deployed in 2021. It is called the !switchnext command. According to Discord logs, in the year that followed admins averaged 3.08 !switchnext commands per day. In 2022, admins increased to 4.97 !switchnext commands per day. In 2023 there was a decrease to 3.25 commands sent per day. This decrease was not a trend, because in our most recent year, 2024-2025, admins set a new record average of 6.71 !switchnext commands per day. In the last 50 days, admins have averaged 7.77 !switchnext commands per day. Much higher than the total average of 4.6 !switchnext commands per day.
There’s plenty wrong with deriving conclusions from this. This isn’t the only method to swap players, it doesn’t account for number of groups, and more. It’s fun anyway, no?
Sincerely,
Affinity
Comment 7
Dear Chaos Muppet,
Add a separate tag for TT admins, like a TTa or something like that. Might make it easy for players to ask for help or something like that.
Sincerely,
Mr. Orwell
Response 7
Dear Mr. Orwell,
We recognize that there are times when it seems there may be an insufficient number of admins present, but there are multiple ways to communicate with them both in-game and via Discord; we also expect our admins to act as players the majority of the time and more weight or preference should not be given to their ideas because of a tag, which could happen.
More importantly, one of the chief pleasures we offer the TT community is that precious and joyful moment when a toxic, racist, homophobic, or sexist player makes a statement that would cause a Neaderthal to doubt their intelligence and they abruptly vanish, never to be seen again. Who hasn’t been in a squad with an admin, heard someone say something, and mentally count in their head the seconds required to type “!yeet”? Why would we deny our players that great happiness?
Sincerely,
C. Muppet
Comment 8
Dear Chaos Muppet,
I think you’ve got a power tripping admin problem with some of the admins. They should enforce rules but also embrace constructive feedback instead of vehemently defending every decision TT makes.
Sincerely,
Mah Rights!
Response 8
Dear Mah Rights!,
TT admins take a battery of psychological, cognitive, and emotional tests in order to ensure that, when exercising power, there is no tripping—TT admins power-walk, but never trip. We are also careful to remove any capacity for free-will, imagination, or independence when an individual becomes an admin; the Admin Hive Mind creates the rules and the drones enforce them.
Constructive feedback offered on Discord is always welcome, so the Admin Hive can discuss it, but within the game admins have no choice but to implement the will of the collective; resistance is futile.
Sincerely,
C. Muppet
Many people assisted in the writing of this report.
I would like to thank the following people: Grey275, PSG, and all the people who work on the technical side of TT for helping me assemble the data for this report and being patient with my inquiries throughout the process.
I would also like to thank all the admins at TT who helped me design the survey and helped proofread this report with special thanks to Affinity, Zero, MyEggo, Kirb, Chaos Muppet, Gaites, Hutchinman, Jasu, sexy mao zedong, Gangry, and the entire TT Admin and Command team. Thank you for sticking with me throughout.
Any remaining errors in this report are my responsibility alone.
Thank you for reading.
Frankly, if you are crazy enough to have read the report this far, chances are that you might just be crazy enough to go a little further. I must warn people that this section is meant for enthusiasts. However, a great deal of effort has gone into making it tractable for a lay audience. I apologise to you, dear reader, in advance for subjecting you to this.
The aim of this section is to try and ascertain how well our internal map pool algorithm does when tested against actual server data. A little bit of context is necessary here. With the release of 7.2, OWI added sub-factions and a map voting system to the game. At TT, we tried the OWI voting system for a while, but, at the end we decided to go our own way because of numerous issues with the OWI system that I will not re-litigate here. We can see in Table 9, most respondents are satisfied with the custom TT voting system.
Before we go further, it is necessary to briefly describe our algorithm and internal system. At TT, admins set layers using a custom website (developed by Grey275 and others) which looks like this:
Admins can select more than 14,000 layers in the “Main pool” out of a possible combination of over 200,000 layers overall. Senior admins, known as “Headquarters” or “HQ”, can set layers outside this 14,000 layer pool at their discretion. The vast majority of what we play on the server comes from this pool. The “Main Pool” filters out obvious nonsense such as a strong wheeled Motorised division going up against Light Infantry divisions.
Some players may desire these matchups, and we do have some of these matchups in the pool where appropriate, but, in general, these types of matchups are not available in the pool and must be set manually if desired. To come up with the main pool, we use the following process:
Brief description of the TT system for determining layer viability
Each vehicle in the game is rated by a “Layers” team (which any admin can to join) along several dimensions. For example, the BTR82A has an armour score, a transport score, a logistics score, and an anti-infantry score. The same is also true for every other vehicle in the game.
Each sub-faction is given scores for infantry (how good is the main weapon, LAT/HAT capability, etc).
These infantry and asset scores are then aggregated at the sub-faction level. For example, “British Mechanised” will have an overall “transportation,” “armour,” “anti-infantry,” and “logistics” score. The same is also true for every other sub-faction.
These scores are then weighted for map type, size, and other factors. For example, transportation scores matter a lot more on larger maps such as Manic than on smaller maps such as Fallujah.
For each map, a viable list of layers is determined based on these scores. For example, the algorithm judges most tracked logistics factions to be more viable on Fallujah and less viable on Manic because tracked logistics is quite unpleasant on large maps. However, recall that senior admins (HQ) can still set outside the main pool.
The purpose of the main pool is to simply allow admins to navigate the complex sub-faction system and see at a glance what is viable and what is not. It is a tool designed to aid judgement, not substitute for judgement. However, admins are encouraged to use the main pool and admin lites (junior admins) are restricted to the approximately 14,000 layers in the main pool.
WARNING
This section does get slightly more involved with the guts of the model. If you dislike statistics and equations (and you somehow still made it this far), you should turn away now.
Certain assumptions which underlie the model are also spelled out and explained. This is important so that the interested reader can decide for themselves if they find the model, and thus its results, credible.
At the end of the day, this exercise should be viewed as a “back of the envelope” type calculation. Results can be sensitive to assumptions and modelling choices, and thus, should be read with caution.
Now that I have set out how TT determines viable layers, it is time to put our system to the test against real world server data. To do this, I have decided to use a two-way Fixed-effects model. Readers should not be unduly intimidated by this model choice as in our case it simply means adding dummy (0,1) variables for each map and time period to account for unobserved map and time heterogeneity — in effect, controlling for maps and time.
The data used comes from the same dataset used to generate all the charts and tables in the main report. I am only looking at RAAS, AAS, Fogless RAAS, and TC rounds played from September 28, 2024 to May 7, 2025.
\[ \begin{align*} Y_{it} = \beta _{0} + \beta_{1} x_{1it} + \beta_{2} x_{2it} +...\beta_{5} x_{5it} + a_{i} + \gamma_t + \varepsilon_{it}\ \end{align*} \]
Since we have a panel dataset where the unit of observation is individual matches, \(Y_{it}\) is the ticket differential of a round on map \(i\) in month \(t\). The same \(i\) and \(t\) notation applies to all the explanatory variables on the right hand side. The explanatory variables are as follows:
\[ \begin{bmatrix} x_{1it}\\ x_{2it}\\ x_{3it}\\ x_{4it}\\ x_{5it}\\ a_{i}\\ \gamma_t\\ \varepsilon_{it}\\ \beta_{1}\ to\ \beta_{5} \end{bmatrix} = \begin{bmatrix} Logistics\ Differential\\ Transport\ Differential\\ Infantry\ Differential\\ Armour\ Differential\\ Team\ Strength\ (Won\ last\ by\ 200+\ tickets)\\ Map\ fixed\ effects \ (for \ each\ map)\\ Time\ fixed\ effects\ (for \ each\ month)\\ Idiosyncratic\ error\ term\\ Estimated\ coefficients\ on\ variables\ of\ interest\\ \end{bmatrix} \]
The model relies on the standard Gauss-Markov assumptions.Apart from these standard assumptions, the additional assumption that we have to make is that the entities \(a_{i}\) and \(\gamma_t\) capture all unobserved heterogeneity specific to maps and time periods, respectively. Recall that the vast majority of the matches played at TT are picked from a pool of “viable layers” determined by a customised algorithm. This means that the match data is not a random sample since the matchups are influenced heavily by what we allow or disallow in the pool.
The units for the dependent variable are intuitive to anyone who understands the game; ticket differentials are just tickets. The units for the independent variables, the logistics differentials, the armour differentials, and so on are much harder to intuitively understand. They are just arbitrary numbers that have been through the algorithm’s weighting process. In practice, the differences between teams are quite small because we largely restrict the server to only viable layers. Here is a summary:
#Prepare maps.rds for appendix
saveRDS(mapsdf, file = "Data/mapsdf.rds")
#Load packages
rm(list=ls()) # Clear all objects in the global environment
library(tidyverse) # For data manipulation + graphing
library(gtsummary) # Summary statistics
library(knitr) # To compile HTML document
library(readr) # To read and import survey data
library(ggh4x) # Advanced layout options
library(gridExtra) # Advanced layout options
library(huxtable) # Tables
library(lubridate) # To work with dates and times
library(lmtest) # Post estimation tests
library(sandwich) # Heteroskedasticity robust standard errors
#Import Data
mapsdf<- readRDS("Data/mapsdf.rds")
weights <- read_csv("Data/Level_Formulas.csv")
#Clean up the weights dataset
weights <- weights %>%
mutate(
map = case_when(
Level == "AlBasrah" ~ "Basrah",
Level == "BlackCoast" ~ "Black Coast",
Level == "FoolsRoad" ~ "Fools",
Level == "GooseBay" ~ "Goose Bay",
Level == "Manicouagan" ~ "Manic",
Level == "PacificProvingGrounds" ~ "Pacific",
TRUE ~ Level
)) %>% select(-Level, -Min_Transport)
#Merge weights
mapsdf <- left_join(mapsdf, weights, by = "map")
#Apply zero's weights and transformations
mapsdf <- mapsdf %>%
filter(!mode %in% c("Invasion", "Destruction", "Skirmish")) %>%
mutate(across(c(logistics_1:armour_2), ~ sqrt(.))) %>%
mutate(
logistics_1 = logistics_1 * Logistics_W,
logistics_2 = logistics_2 * Logistics_W,
transport_1 = transport_1 * Transporation_W,
transport_2 = transport_2 * Transporation_W,
armour_1 = armour_1 * Armor_W,
armour_2 = armour_2 * Armor_W,
anti_inf_1 = anti_inf_1 * `Anti-Infantry_W`,
anti_inf_2 = anti_inf_2 * `Anti-Infantry_W`) %>%
mutate(
across(c(logistics_1, logistics_2), ~ case_when(
size == "Medium" ~ . * 0.60,
size == "Small" ~ . * 0.30,
TRUE ~ .
)),
across(c(transport_1, transport_2), ~ case_when(
size == "Medium" ~ . * 0.60,
size == "Small" ~ . * 0.30,
TRUE ~ .
)),
across(c(anti_inf_1, anti_inf_2), ~ case_when(
size == "Medium" ~ . * 0.50,
size == "Small" ~ . * 0.25,
TRUE ~ .
)),
across(c(armour_1, armour_2), ~ case_when(
size == "Medium" ~ . * 0.50,
size == "Small" ~ . * 0.25,
TRUE ~ .
))
)
#Gen differences
mapsdf <- mapsdf %>%
mutate(logi_diff = logistics_1 - logistics_2) %>%
mutate(trans_diff = transport_1 - transport_2) %>%
mutate(inf_diff = anti_inf_1 - anti_inf_2) %>%
mutate(arm_diff = armour_1 - armour_2)
#Gen proper ticket diff for team 1 victory
mapsdf <- mapsdf %>% mutate(tick_diff_1 = ifelse(winner == 1, tickets, -tickets))
#won last 200?
mapsdf<- mapsdf %>%
mutate(time_diff = start_est - lag(end_est)) %>% mutate(win_200 = if_else(
lag(winner) == 2 & lag(tickets) >= 200 & time_diff < 100, 1, 0))
#Relevel the ym factor to set "September 2024" as the reference level
mapsdf$ym <- relevel(factor(mapsdf$ym), ref = "September 2024")
# Scale predictors by IQR (1 unit = IQR change in original variable)
mapsdf <- mapsdf %>%
mutate(
logi_diff_iqr = logi_diff / IQR(logi_diff, na.rm = TRUE),
trans_diff_iqr = trans_diff / IQR(trans_diff, na.rm = TRUE),
inf_diff_iqr = inf_diff / IQR(inf_diff, na.rm = TRUE),
arm_diff_iqr = arm_diff / IQR(arm_diff, na.rm = TRUE))
#Build
summtab <- mapsdf %>%
select(logi_diff, trans_diff, inf_diff, arm_diff) %>%
summarise(across(.cols = everything(),
.fns = list(mean = ~round(mean(.x), 4),
sd = ~round(sd(.x), 4),
iqr = ~round(IQR(.x), 4),
abs_max = ~round(max(abs(.x)), 4)))) %>%
pivot_longer(cols = everything(),
names_to = c("Variable", ".value"),
names_pattern = "(.*)_(mean|sd|iqr|abs_max)") %>%
mutate(Variable = recode(Variable,
logi_diff = "Logistics Differential",
trans_diff = "Transport Differential",
inf_diff = "Infantry Differential",
arm_diff = "Armour Differential")) %>%
rename("Mean" = mean,
"Standard Deviation" = sd,
"Interquartile Range" = iqr,
"Absolute Maximum" = abs_max)
#Format
summtab <- as_hux(summtab) %>% theme_article() %>% set_width(1) %>%
set_caption("Appendix table 1: Summary statistics for key variables")
#Display
summtab
Variable | Mean | Standard Deviation | Interquartile Range | Absolute Maximum |
---|---|---|---|---|
Logistics Differential | -0.0326 | 0.285 | 0.332 | 1.67 |
Transport Differential | -0.0032 | 0.173 | 0.193 | 1.09 |
Infantry Differential | 0.0909 | 1.02 | 1.14 | 4.79 |
Armour Differential | -0.0593 | 1.14 | 1.08 | 6.01 |
We can see that the average magnitude of these differences is quite small. This is why we have to scale these variables appropriately before estimating the regression model. What we should aim to achieve with this scaling is to give readers an idea of “realistic differences which occur on the server” or more simply “what is a good armour faction worth over a less good armour faction.” I have chosen to scale the differences to the interquartile range. This means that the coefficients on the differentials in the regression table can be read as the “effect of moving from the bottom quarter of sub-factions for transport/armour/logistics/infantry to the top quarter” — in other words, the effect of moving from a “bad” (bottom 25%) faction to a “good” (top 25%) faction in each of the 4 respective categories.
Before we estimate the model, we have to outline what we expect to see if our layer viability system is working as intended:
Pre-estimation hypothesis
Hypothesis 1: If the “layer viability” system is working as intended, then none of the coefficients from \(\beta_{1}\) to \(\beta_{4}\) should be statistically significant. The reason is that the differences should either be too small or differences in one area should be compensated by differences in another. For example, a faction with a relatively weak logistics or transport score should be able to compensate for it with a higher armour score.
Hypothesis 2: If balance is unchanging through time, then none of the time fixed effects should be statistically significant.
Hypothesis 3: If maps are perfectly balanced with no advantage given to either team, the map fixed effects should not be statistically significant.
Hypothesis 4: \(\beta_{5}\) should be positive and strongly significant. Common sense suggests that if a team won the previous round by 200 or more tickets, it is the stronger team on the server during that time period.
We can see below whether these hypotheses are borne out in the data. Note that two versions of the model described in the equation above are estimated. Model (1) is estimated without the fixed effect (\(a_{i}\) and \(\gamma_t\)) terms while Model (2) estimates all the terms in the equation. Showing both models allows readers to compare the results and assess the robustness of the findings. If the key coefficients remain similar in both models, it suggests the results are not heavily dependent on the fixed effects assumption.
#Regressions
mod1 <- lm(
tick_diff_1 ~ logi_diff_iqr + trans_diff_iqr + inf_diff_iqr + arm_diff_iqr +
win_200,
data = mapsdf)
mod2 <- lm(
tick_diff_1 ~ logi_diff_iqr + trans_diff_iqr + inf_diff_iqr + arm_diff_iqr +
win_200 + factor(map) + factor(ym),
data = mapsdf)
# mapsdf <- mapsdf %>% mutate(large = ifelse(size=="Large", 1, 0))
# bptest(mod1)
# bptest(mod2)
# Model 2 and fails breusch-pagan test. To be conservative, I use HC1 standard errors for both.
# bgtest(mod1, order = 1)
# bgtest(mod2, order = 1)
#Fail the auto correlation test - need to cluster at map level to account
# Create robust versions of all models with HC1 errors
mod1_robust <- coeftest(mod1, vcov = vcovHC(mod1, type = "HC1", cluster = "map"))
mod2_robust <- coeftest(mod2, vcov = vcovHC(mod2, type = "HC1", cluster = "map"))
# Create regression table
reg_table <- huxreg(
"Model (1) - Without Fixed Effects" = mod1_robust,
"Model (2) - With Fixed Effects" = mod2_robust,
statistics = c("N" = "nobs"),
stars = c(`*` = 0.1, `**` = 0.05, `***` = 0.01),
note = "Significance levels: {stars}. Huber–White standard errors in adjacent columns, clustered at the map level.",
number_format = 2,
error_pos = "right"
)
# Define F-statistics with significance stars
fmod1 <- paste0(round(as.numeric(glance(mod1)$statistic), 2), "***")
fmod2 <- paste0(round(as.numeric(glance(mod2)$statistic), 2), "***")
# Combine F-statistics into a vector, ensuring alignment with the 4-column structure
f_stats <- c("F-Statistic", fmod1, "", fmod2, "")
# Add F-statistics
reg_table <- add_rows(
reg_table,
as_huxtable(
matrix(f_stats, nrow = 1, ncol = 5)
),
after = nrow(reg_table) - 1
)
#Clean table
reg_table_raw <- reg_table
#Variable names
reg_table[1,1] <- "Coefficients"
reg_table[2,1] <- "Intercept"
reg_table[3,1] <- "Logistics Differential - β1"
reg_table[4,1] <- "Transport Differential - β2"
reg_table[5,1] <- "Infantry Differential - β3"
reg_table[6,1] <- "Armour Differential - β4"
reg_table[7,1] <- "Team Strength (Won last by 200+) - β5"
reg_table[8,1] <- "Basrah"
reg_table[9,1] <- "Belaya"
reg_table[10,1] <- "Black Coast"
reg_table[11,1] <- "Chora"
reg_table[12,1] <- "Fallujah"
reg_table[13,1] <- "Fools"
reg_table[14,1] <- "Goose Bay"
reg_table[15,1] <- "Gorodok"
reg_table[16,1] <- "Harju"
reg_table[17,1] <- "Kamdesh"
reg_table[18,1] <- "Kohat"
reg_table[19,1] <- "Kokan"
reg_table[20,1] <- "Lashkar"
reg_table[21,1] <- "Logar"
reg_table[22,1] <- "Manic"
reg_table[23,1] <- "Mestia"
reg_table[24,1] <- "Mutaha"
reg_table[25,1] <- "Narva"
reg_table[26,1] <- "Pacific"
reg_table[27,1] <- "Sanxian"
reg_table[28,1] <- "Skorpo"
reg_table[29,1] <- "Sumari"
reg_table[30,1] <- "Tallil"
reg_table[31,1] <- "Yehorivka"
reg_table[32,1] <- "Apr 2025"
reg_table[33,1] <- "Dec 2024"
reg_table[34,1] <- "Feb 2025"
reg_table[35,1] <- "Jan 2025"
reg_table[36,1] <- "Mar 2024"
reg_table[37,1] <- "May 2025"
reg_table[38,1] <- "Nov 2024"
reg_table[39,1] <- "Oct 2024"
# 'Basrah', 'Belaya', 'Black Coast',
# 'Chora', 'Fallujah', 'Fools', 'Goose Bay', 'Gorodok', 'Harju',
# 'Kamdesh', 'Kohat', 'Kokan','Lashkar', 'Logar', 'Manic',
# 'Mestia','Mutaha','Narva', 'Pacific', 'Sanxian',
# 'Skorpo', 'Sumari', 'Tallil','Yehorivka'
#Format
reg_table2 <- reg_table %>%
set_width(1) %>% set_italic(final(1), 1) %>%
set_align(everywhere, everywhere, 'center') %>% set_bold(1, everywhere) %>%
set_caption("Appendix Table 2: Regression Analysis") %>%
set_background_color(seq(3, nrow(reg_table) - 2, by = 2), everywhere, "grey95") %>%
set_bottom_border(row = 40, col = everywhere, value = 0) %>%
set_bottom_border(row = 41, col = everywhere, value = 1) %>%
set_bottom_border(row = 39, col = everywhere, value = 1) %>%
set_bottom_border(row = 1, col = everywhere, value = 1) %>%
merge_cells(40, 2:3) %>% merge_cells(40, 4:5) %>%
merge_cells(41, 2:3) %>% merge_cells(41, 4:5) %>%
set_bold(c(40,41), 1)
#Formatting for significance
reg_table2 <- reg_table2 %>%
set_background_color(c(4,6,7), 2, "#6aa84f") %>%
set_text_color(c(4,6,7), 2, "white") %>%
set_bold(c(4,6,7), 2) %>%
set_background_color(c(4,6,7,9,13,16,20,23), 4, "#6aa84f") %>%
set_text_color(c(4,6,7,9,13,16,20,23), 4, "white") %>%
set_bold(c(4,6,7,9,13,16,20,23), 4)
#Display
reg_table2
Coefficients | Model (1) - Without Fixed Effects | Model (2) - With Fixed Effects | ||
---|---|---|---|---|
Intercept | -2.51 | (3.14) | -38.11 | (35.84) |
Logistics Differential - β1 | 0.84 | (3.80) | -0.17 | (3.78) |
Transport Differential - β2 | 7.96 ** | (3.75) | 8.06 ** | (3.75) |
Infantry Differential - β3 | 2.80 | (3.62) | 3.14 | (3.67) |
Armour Differential - β4 | 6.94 ** | (3.08) | 6.91 ** | (3.10) |
Team Strength (Won last by 200+) - β5 | 80.52 *** | (8.79) | 82.41 *** | (8.82) |
Basrah | -33.85 | (36.87) | ||
Belaya | 62.25 ** | (30.16) | ||
Black Coast | 28.03 | (29.58) | ||
Chora | 17.46 | (31.18) | ||
Fallujah | 33.34 | (29.54) | ||
Fools | 68.22 ** | (34.58) | ||
Goose Bay | 1.41 | (31.32) | ||
Gorodok | 44.29 | (29.54) | ||
Harju | 56.15 * | (31.45) | ||
Kamdesh | 27.95 | (34.20) | ||
Kohat | 34.64 | (37.32) | ||
Kokan | 36.38 | (31.51) | ||
Lashkar | 99.09 *** | (36.52) | ||
Logar | 22.13 | (37.92) | ||
Manic | 40.65 | (31.81) | ||
Mestia | 59.93 * | (36.31) | ||
Mutaha | 44.90 | (29.53) | ||
Narva | 45.07 | (29.52) | ||
Pacific | 50.90 | (34.58) | ||
Sanxian | -2.76 | (35.34) | ||
Skorpo | 13.71 | (36.95) | ||
Sumari | 32.10 | (33.74) | ||
Tallil | 39.29 | (40.48) | ||
Yehorivka | 34.84 | (30.24) | ||
Apr 2025 | -4.74 | (23.89) | ||
Dec 2024 | -5.80 | (23.32) | ||
Feb 2025 | 5.61 | (23.62) | ||
Jan 2025 | 7.70 | (23.47) | ||
Mar 2024 | -3.14 | (23.68) | ||
May 2025 | 20.85 | (30.41) | ||
Nov 2024 | 0.14 | (23.70) | ||
Oct 2024 | -13.95 | (23.44) | ||
N | 3343 | 3343 | ||
F-Statistic | 17.79*** | 3.67*** | ||
Significance levels: *** p < 0.01; ** p < 0.05; * p < 0.1. Huber–White standard errors in adjacent columns, clustered at the map level. |
Here is how to read Appendix Table 2 above:
Rows highlighted in green are both statistically significant (at varying confidence levels) and variables of interest. The confidence level is shown by the asterisk on the coefficients (see note on table).
Rows without colour should be treated as statistically insignificant (i.e., any difference is statistically indistinguishable from zero). All non-green coefficients can be treated as zero.
The coefficients \(\beta_{1}\) to \(\beta_{5}\) can be viewed as the effects of moving from a “bad” sub-faction to a “good” sub-faction. For example, the coefficient on \(beta_{3}\), infantry differential, is statistically insignificant suggesting that after other factors are controlled for, the effects of moving from a sub-faction with a bad infantry score (bottom 25%) to a sub-faction with good infantry (top 25%) score are 0.
The regressions are estimated from the perspective of Team 1. Therefore, given that some coefficients on map fixed effects are statistically significant and positive, the data suggest that Team 1 has a ticket advantage on that map most likely due to map specific features (better mains, faster routes to central points, RAAS information advantage, etc.).
Robust standard errors, shown in parentheses beside each coefficient estimate, serve as a statistical indicator of the uncertainty surrounding the estimated average, reflecting the potential variability that may result from random sampling fluctuations. A smaller standard error suggests a more precise estimate, and if it is sufficiently low, the coefficient may be deemed statistically significant, meaning it is less likely to have occurred by chance. The units of the standard errors are also in terms of the dependent variable, ticket differentials.
Key results from Appendix Table 2
Transport and armour differentials are highly significant (at the 95% confidence level) across both specifications, indicating they are slightly underweighted as factors in our layer viability system. The results show that a transport advantage provides a team with an approximately 8-ticket advantage. The corresponding figure for an armour advantage is around 7 tickets. This partially violates Hypothesis 1, but the magnitudes are small.
Logistics and infantry differentials are not statistically significant suggesting that these factors are properly weighted in our layer viability system. This is in line with Hypothesis 1.
All of the time fixed effects are statistically insignificant in Model (2), the main model. This is in line with Hypothesis 2.
Some maps have a significant advantage for Team 1 judging by the positive and statistically significant coefficients. Lashkar is the map that is most heavily tilted towards Team 1. The data suggest that Team 1 has an almost 100 ticket advantage on Lashkar even when accounting for faction and team strength. The results for the map fixed effects are partially in line with Hypothesis 3, however, this is just a matter of OWI’s level design and has nothing to do with TT per se.
A team that won the previous round by 200 or more tickets has an approximately 80 ticket advantage on the next map after accounting for faction strength and map and time specific factors. This is in line with Hypothesis 4.
The hypotheses that we laid out pre-estimation were largely borne out in the results from Appendix Table 2. If we were weighting all of the sub-faction strengths and weaknesses correctly in our layer viability system, then none of the differentials should have been statistically significant. A weakness in one area should have been compensated by strength in another by our algorithm leaving chances of victory or defeat unchanged. This is true for for 2 out of the 4 variables we consider — logistics and infantry differentials are statistically insignificant and can be treated as 0.
The partial exceptions to Hypothesis 1 are the transport and armour differentials. In both models, both transport and armour differentials are highly significant (p < 0.05), indicating a robust effect. The results across both models indicate that a positive transportation differential provides a team with an approximate 8-ticket advantage in a given round, after accounting for team strength and map and time specific factors. The corresponding figure for armour is approximately 7 tickets. While these advantages cannot be considered negligible, they remain relatively small, as the loss of an IFV or tank in any round would offset them. The layer viability system, which balances numerous factors including dozens of cross-faction vehicle comparisons and map size weights, appears to be performing as effectively as can be expected.
A team is considered to be “strong” if it has won its previous match by more than 200 tickets. It is vitally important to use some proxy to capture team strength, as without controlling for this, other coefficients would be biased. The coefficient on team strength, \(\beta_{5}\), is strongly significant across both model specifications. A team that won the previous round by 200 or more tickets has an approximately 80 ticket advantage in the following around. A strong and positive coefficient here is in line with Hypothesis 4.
Moving on to the coefficients on the fixed effects variables, keen eyed readers might be wondering why Anvil is absent from Model (2). The absence of Anvil in Model (2) is due to a statistical issue known as multicollinearity. In fixed effects models, we must always designate one category — such as a specific map — as a reference point and exclude it from the model. This is necessary to prevent perfect collinearity, a situation where the included variables are so closely related that they redundantly explain the same variation, rendering the model unstable. This is the same reason why September 2024 is excluded in the time fixed effects. The effects for these omitted variables show up in the intercept. Regarding Anvil, we can reasonably conclude that neither team has a clear advantage because the intercept in Model (2) is not significant.
Interestingly, several maps exhibit a slight advantage for Team 1, with none favouring Team 2 — an unusual design choice by OWI. For some maps, this advantage is pronounced. On Lashkar, for example, Team 1 benefits significantly due to its southern starting position, which provides faster access to central points compared to Team 2’s northeastern position. Similarly, Fool’s Road and Belaya offer Team 1 a notable edge. I estimated a version of Model (2) restricted to RAAS layers (not shown here) and found that nearly all these advantages stem from RAAS layers, suggesting they are partly driven by informational advantages for Team 1 in those scenarios. Two other maps, Harju and Mestia, also display a weakly significant (p < 0.1) advantage for Team 1, equivalent to approximately 55-60 tickets. However, with standard errors around 30 tickets, these advantages are relatively modest and may not have a significant practical impact.
Comment 1
Comment 1
Dear Affinity,
Try rolling layers to Skirmish at late hours if server drops below ~60.
Generally, content. Want much less WPMC, much more SKIRMISH.
Sincerely,
Esteemed Colleagues
Response 1
Dear Esteemed colleagues,
I hope this letter finds you well. We hope to complete welfare checks on each of you by the end of the week. Please lay off the sauce or whatever crankery that leads you to believe these are reasonable suggestions.
I get it. The infantry fight is fun. We all agree, right? It is a perfect nightcap; the top-shelf palette cleanser. But, I ask, do you not read the same seething hatred that oozes through the screen as Logar rolls into view? Do you not witness the tragedy of ten thousand neurons frying in collective rage as Skirmish is sighted on the scoreboard? Must you bask and revel in the cries of the forsaken?
I wince when I see Skirmish pop out of the heads of admins. The bright, naive folly to assume an innocent preference won’t summon deep malcontent. Something different, something simple, yet something that is capable of summoning the worst horrors, including the dark stirrings of deranged hearts among adolescent milsim fans. Take’em when you get’em and enjoy’em when you can.
Sincerely,
Affinity