1. Data Import & Cleaning
1.1 Load Data
# Read raw Qualtrics export (skip the two label/importId rows)
raw <- read_survey("~/Google drive/My Drive/YEAR 3/PROJECTS/DANIEL/Competitive Jungle/CWV x Game/pilot2_data.csv")
# Read exclusion list
exclusions <- read_csv("~/Downloads/Pilot 2 List of Exclusions (260623) - Sheet1.csv")
cat("Raw N =", nrow(raw), "\n")
## Raw N = 218
cat("Exclusions N =", nrow(exclusions), "\n")
## Exclusions N = 43
## Note: there are a lot of exclusions. People wrote in free response that they did not understand the game, many players made no moves, and some left the game early. All were excluded.
1.2 Apply Exclusions
df <- raw %>%
filter(!participantId %in% exclusions$ID) %>%
filter(Finished == 1) %>%
filter(Q_RecaptchaScore > 0.5)
cat("N after exclusions =", nrow(df), "\n")
## N after exclusions = 155
cat("Condition split:\n")
## Condition split:
print(table(df$cond))
##
## easy hard
## 79 76
1.3 Reverse-Score & Compute Composites
df <- df %>%
mutate(
# ── Condition ──────────────────────────────────────────────
cond = factor(cond, levels = c("easy", "hard")),
cond_num = if_else(cond == "hard", 1, 0), # hard = 1, easy = 0
# ── TIPI reverse scores ────────────────────────────────────
Extraversion_6R_r = 8 - Extraversion_6R,
Agreeable_2R_r = 8 - Agreeable_2R,
Conscientious_8R_r = 8 - Conscientious_8R,
EmoStability_4R_r = 8 - EmoStability_4R,
Open_10R_r = 8 - Open_10R,
# ── TIPI composites (each = mean of 2 items) ───────────────
tipi_extraversion = (Extraversion_1 + Extraversion_6R_r) / 2,
tipi_agreeableness = (Agreeable_7 + Agreeable_2R_r) / 2,
tipi_conscientiousness= (Conscientious_3 + Conscientious_8R_r) / 2,
tipi_emo_stability = (EmoStability_9 + EmoStability_4R_r) / 2,
tipi_openness = (Open_5 + Open_10R_r) / 2,
# ── CWV reverse scores & composite ────────────────────────
CWV_2R_r = 8 - CWV_2R,
CWV_5R_r = 8 - CWV_5R,
CWV_7R_r = 8 - CWV_7R,
CWV_9R_r = 8 - CWV_9R,
CWV_10R_r = 8 - CWV_10R,
cwv = rowMeans(
pick(CWV_1, CWV_2R_r, CWV_3, CWV_4, CWV_5R_r,
CWV_6, CWV_7R_r, CWV_8, CWV_9R_r, CWV_10R_r),
na.rm = TRUE
),
# ── Moral outrage composite ────────────────────────────────
moral_outrage = rowMeans(pick(outrage_1, outrage_2, outrage_3), na.rm = TRUE),
# ── Sympathy composite ─────────────────────────────────────
sympathy = rowMeans(pick(sympathy_1, sympathy_2, sympathy_3, sympathy_4), na.rm = TRUE),
# ── SDT subscales ──────────────────────────────────────────
sdt_autonomy = rowMeans(pick(autonomy_1, autonomy_2, autonomy_3), na.rm = TRUE),
sdt_competence = rowMeans(pick(competence_1, competence_2, competence_3), na.rm = TRUE),
sdt_relatedness = rowMeans(pick(relate_1, relate_2, relate_3), na.rm = TRUE),
# ── Perceived strength (reverse strength_2R) ───────────────
strength_2R_r = 8 - strength_2R,
perceived_strength = rowMeans(pick(strength_1, strength_2R_r), na.rm = TRUE),
# ── Perceived opponent unfairness composite ─────────────────
opponent_perception = rowMeans(
pick(opponent_1, opponent_2, opponent_3, opponent_4), na.rm = TRUE
),
# ── Aggression composite (behavioral) ─────────────────────
# stuns = aggressive acts toward opponents; raids = stealing
total_stuns = rowSums(
pick(Player_stuns_Attacker, Player_stuns_Thief, Player_stuns_Freerider),
na.rm = TRUE
),
total_aggression = total_stuns + Player_raids_hut,
# ── Recode demographics ────────────────────────────────────
gender_label = case_when(
gender == 1 ~ "Male",
gender == 2 ~ "Female",
gender == 3 ~ "Non-binary",
TRUE ~ "Other/NR"
),
game_freq_label = case_when(
game_frequency == 9 ~ "Never",
game_frequency == 10 ~ "< Once/month",
game_frequency == 11 ~ "Few times/month",
game_frequency == 12 ~ "Few times/week",
game_frequency == 13 ~ "Daily/almost daily",
TRUE ~ NA_character_
),
game_freq_label = factor(game_freq_label,
levels = c("Never","< Once/month","Few times/month",
"Few times/week","Daily/almost daily"))
)
1.4 What does the distribution of aggression look like
# Distribution of total aggression
summary(df$total_aggression)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.000 5.000 9.000 9.897 13.000 45.000
# Histogram
ggplot(df, aes(x = total_aggression)) +
geom_histogram(bins = 30, fill = "#5B8DB8", color = "white") +
labs(title = "Distribution of Total Aggression",
x = "Total Aggression (stuns + raids)", y = "Count") +
theme_minimal()

# By condition
ggplot(df, aes(x = total_aggression, fill = cond)) +
geom_histogram(bins = 30, alpha = 0.7, position = "identity") +
scale_fill_manual(values = c("easy" = "#5B8DB8", "hard" = "#E07B54"),
name = "Condition") +
facet_wrap(~cond) +
labs(title = "Distribution of Total Aggression by Condition",
x = "Total Aggression", y = "Count") +
theme_minimal()

# Check zero inflation
cat("Proportion of zeros:", round(mean(df$total_aggression == 0, na.rm = TRUE), 3), "\n")
## Proportion of zeros: 0.097
cat("Max value:", max(df$total_aggression, na.rm = TRUE), "\n")
## Max value: 45
cat("Mean:", round(mean(df$total_aggression, na.rm = TRUE), 2), "\n")
## Mean: 9.9
cat("Variance:", round(var(df$total_aggression, na.rm = TRUE), 2), "\n")
## Variance: 63.87
cat("Variance/Mean ratio:", round(var(df$total_aggression, na.rm = TRUE) /
mean(df$total_aggression, na.rm = TRUE), 2),
"(>1 suggests overdispersion)\n")
## Variance/Mean ratio: 6.45 (>1 suggests overdispersion)
# Log transform check (adding 1 to handle zeros)
df <- df %>% mutate(log_aggression = log(total_aggression + 1))
ggplot(df, aes(x = log_aggression)) +
geom_histogram(bins = 30, fill = "#6BAE75", color = "white") +
labs(title = "Distribution of Log(Total Aggression + 1)",
x = "Log Aggression", y = "Count") +
theme_minimal()

2. Demographics & Gaming Experience
2.1 Sample Overview
demo_summary <- df %>%
summarise(
N = n(),
Age_M = round(mean(age, na.rm = TRUE), 1),
Age_SD = round(sd(age, na.rm = TRUE), 1),
Age_Range = paste0(min(age, na.rm = TRUE), "–", max(age, na.rm = TRUE)),
Pct_Female = paste0(round(mean(gender == 2, na.rm = TRUE) * 100, 1), "%"),
Pct_Male = paste0(round(mean(gender == 1, na.rm = TRUE) * 100, 1), "%"),
Pct_NonBin = paste0(round(mean(gender == 3, na.rm = TRUE) * 100, 1), "%")
)
kable(demo_summary,
col.names = c("N","Age M","Age SD","Age Range",
"% Female","% Male","% Non-binary"),
caption = "Sample demographics") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Sample demographics
|
N
|
Age M
|
Age SD
|
Age Range
|
% Female
|
% Male
|
% Non-binary
|
|
155
|
41.7
|
12
|
20–73
|
43.9%
|
54.2%
|
1.9%
|
2.2 Gender Distribution
df %>%
count(gender_label) %>%
mutate(pct = n / sum(n)) %>%
ggplot(aes(x = reorder(gender_label, -n), y = pct, fill = gender_label)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label = paste0(n, "\n(", percent(pct, 1), ")")),
vjust = -0.3, size = 3.5) +
scale_y_continuous(labels = percent_format(), limits = c(0, .7)) +
scale_fill_brewer(palette = "Set2") +
labs(title = "Gender Distribution", x = NULL, y = "Proportion") +
theme_minimal()

2.3 Race/Ethnicity
race_labels <- c(
"1" = "White/Eur. Am.", "2" = "Black/Afr. Am.",
"3" = "E. Asian/Am.", "4" = "S. Asian/Am.",
"5" = "Latino/Hisp.", "6" = "Native Am.",
"7" = "Middle Eastern", "8" = "Biracial/Multi.",
"9" = "Other"
)
df %>%
mutate(race_label = race_labels[as.character(race)]) %>%
count(race_label) %>%
arrange(desc(n)) %>%
mutate(pct = percent(n / sum(n), 1)) %>%
kable(col.names = c("Race/Ethnicity", "N", "%"),
caption = "Race/ethnicity breakdown") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Race/ethnicity breakdown
|
Race/Ethnicity
|
N
|
%
|
|
White/Eur. Am.
|
110
|
71%
|
|
E. Asian/Am.
|
16
|
10%
|
|
Black/Afr. Am.
|
12
|
8%
|
|
NA
|
9
|
6%
|
|
Latino/Hisp.
|
4
|
3%
|
|
S. Asian/Am.
|
4
|
3%
|
2.4 Education & SES
edu_labels <- c(
"1" = "Some HS", "2" = "HS", "3" = "Some College",
"4" = "College", "5" = "Some Grad", "6" = "MA",
"7" = "PhD", "8" = "MD", "9" = "MBA",
"10" = "JD", "11" = "Other"
)
p_edu <- df %>%
mutate(edu_label = edu_labels[as.character(edu)]) %>%
count(edu_label) %>%
mutate(edu_label = fct_reorder(edu_label, n)) %>%
ggplot(aes(x = edu_label, y = n)) +
geom_col(fill = "#5B8DB8") +
coord_flip() +
labs(title = "Education", x = NULL, y = "N") +
theme_minimal()
ses_labels <- c(
"1" = "Upper", "2" = "Upper Middle", "3" = "Middle",
"4" = "Lower Middle", "5" = "Working", "6" = "Lower"
)
p_ses <- df %>%
mutate(ses_label = ses_labels[as.character(ses)]) %>%
count(ses_label) %>%
mutate(ses_label = fct_reorder(ses_label, n)) %>%
ggplot(aes(x = ses_label, y = n)) +
geom_col(fill = "#E07B54") +
coord_flip() +
labs(title = "Socioeconomic Status", x = NULL, y = "N") +
theme_minimal()
gridExtra::grid.arrange(p_edu, p_ses, ncol = 2)

2.5 Gaming Experience
p_freq <- df %>%
filter(!is.na(game_freq_label)) %>%
count(game_freq_label) %>%
ggplot(aes(x = game_freq_label, y = n, fill = game_freq_label)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label = n), vjust = -0.3, size = 3.5) +
scale_fill_brewer(palette = "Blues", direction = 1) +
labs(title = "Gaming Frequency", x = NULL, y = "N") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 30, hjust = 1))
p_skill <- df %>%
count(skill_level) %>%
ggplot(aes(x = factor(skill_level), y = n)) +
geom_col(fill = "#6BAE75") +
scale_x_discrete(labels = c("Far below\navg","Below\navg","Slightly\nbelow","Average",
"Slightly\nabove","Above\navg","Far above\navg")) +
labs(title = "Self-Rated Skill Level", x = NULL, y = "N") +
theme_minimal()
gridExtra::grid.arrange(p_freq, p_skill, ncol = 2)

df %>%
summarise(
Genre_Exp_M = round(mean(genre_frequency, na.rm = TRUE), 2),
Genre_Exp_SD = round(sd(genre_frequency, na.rm = TRUE), 2),
Skill_M = round(mean(skill_level, na.rm = TRUE), 2),
Skill_SD = round(sd(skill_level, na.rm = TRUE), 2)
) %>%
kable(col.names = c("Genre Exp. M","Genre Exp. SD","Skill M","Skill SD"),
caption = "Gaming experience descriptives (1–7 scales)") %>%
kable_styling(bootstrap_options = "striped", full_width = FALSE)
Gaming experience descriptives (1–7 scales)
|
Genre Exp. M
|
Genre Exp. SD
|
Skill M
|
Skill SD
|
|
3.74
|
1.76
|
4.02
|
1.6
|
3. Scale Reliability
alphas <- list(
"Moral Outrage" = df[, c("outrage_1", "outrage_2", "outrage_3")],
"Sympathy" = df[, c("sympathy_1", "sympathy_2", "sympathy_3", "sympathy_4")],
"CWV" = df[, c("CWV_1", "CWV_2R_r", "CWV_3", "CWV_4", "CWV_5R_r",
"CWV_6", "CWV_7R_r", "CWV_8", "CWV_9R_r", "CWV_10R_r")],
"SDT: Autonomy" = df[, c("autonomy_1", "autonomy_2", "autonomy_3")],
"SDT: Competence" = df[, c("competence_1", "competence_2", "competence_3")],
"SDT: Relatedness" = df[, c("relate_1", "relate_2", "relate_3")],
"Opponent Perception" = df[, c("opponent_1", "opponent_2", "opponent_3", "opponent_4")]
)
alpha_table <- map_dfr(alphas, function(items) {
a <- psych::alpha(items, warnings = FALSE)
tibble(alpha = round(a$total$raw_alpha, 3), n_items = ncol(items))
}, .id = "Scale")
kable(alpha_table,
col.names = c("Scale", "Cronbach's α", "N Items"),
caption = "Internal consistency of composites") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Internal consistency of composites
|
Scale
|
Cronbach’s α
|
N Items
|
|
Moral Outrage
|
0.927
|
3
|
|
Sympathy
|
0.924
|
4
|
|
CWV
|
0.802
|
10
|
|
SDT: Autonomy
|
0.751
|
3
|
|
SDT: Competence
|
0.880
|
3
|
|
SDT: Relatedness
|
0.898
|
3
|
|
Opponent Perception
|
0.765
|
4
|
4. Descriptives by Condition
desc_vars <- c("moral_outrage", "sympathy", "cwv",
"sdt_autonomy", "sdt_competence", "sdt_relatedness",
"perceived_strength", "opponent_perception",
"Player_score", "total_aggression", "difficulty")
desc_table <- df %>%
group_by(cond) %>%
summarise(across(all_of(desc_vars),
list(M = ~round(mean(.x, na.rm = TRUE), 2),
SD = ~round(sd(.x, na.rm = TRUE), 2)),
.names = "{.col}_{.fn}")) %>%
pivot_longer(-cond, names_to = c("Variable", "stat"), names_sep = "_(?=[MS])") %>%
pivot_wider(names_from = c(cond, stat), values_from = value)
desc_table <- desc_table[, c("Variable", "easy_M", "easy_SD", "hard_M", "hard_SD")]
kable(desc_table,
col.names = c("Variable", "Easy M", "Easy SD", "Hard M", "Hard SD"),
caption = "Descriptive statistics by condition") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Descriptive statistics by condition
|
Variable
|
Easy M
|
Easy SD
|
Hard M
|
Hard SD
|
|
moral_outrage
|
2.76
|
1.72
|
4.46
|
1.97
|
|
sympathy
|
1.54
|
0.91
|
1.42
|
0.97
|
|
cwv
|
2.81
|
0.89
|
2.55
|
0.86
|
|
sdt_autonomy
|
4.57
|
1.37
|
3.56
|
1.69
|
|
sdt_competence
|
4.53
|
1.45
|
2.70
|
1.62
|
|
sdt_relatedness
|
3.06
|
1.64
|
2.12
|
1.48
|
|
perceived_strength
|
4.73
|
1.39
|
2.30
|
1.49
|
|
opponent_perception
|
3.71
|
1.30
|
4.88
|
1.32
|
|
Player_score
|
3.43
|
2.64
|
0.24
|
0.46
|
|
total_aggression
|
10.10
|
8.27
|
9.68
|
7.74
|
|
difficulty
|
4.33
|
1.71
|
6.34
|
1.23
|
4.1 Distribution of CWV
df %>%
ggplot(aes(x = cwv)) +
geom_histogram(bins = 20, fill = "#5B8DB8", color = "white") +
geom_vline(xintercept = mean(df$cwv, na.rm = TRUE),
linetype = "dashed", color = "#E07B54", linewidth = 0.8) +
annotate("text",
x = mean(df$cwv, na.rm = TRUE) + 0.15,
y = Inf, vjust = 1.5,
label = paste0("M = ", round(mean(df$cwv, na.rm = TRUE), 2)),
color = "#E07B54", size = 3.5) +
labs(title = "Distribution of CWV",
x = "CWV (1–7)", y = "Count") +
theme_minimal()

df %>%
group_by(cond) %>%
summarise(M = round(mean(cwv, na.rm = TRUE), 2),
SD = round(sd(cwv, na.rm = TRUE), 2),
Min = round(min(cwv, na.rm = TRUE), 2),
Max = round(max(cwv, na.rm = TRUE), 2)) %>%
kable(col.names = c("Condition", "M", "SD", "Min", "Max"),
caption = "CWV by condition") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
CWV by condition
|
Condition
|
M
|
SD
|
Min
|
Max
|
|
easy
|
2.81
|
0.89
|
1.2
|
4.9
|
|
hard
|
2.55
|
0.86
|
1.0
|
4.5
|
# Check CWV is balanced across conditions
t.test(cwv ~ cond, data = df)
##
## Welch Two Sample t-test
##
## data: cwv by cond
## t = 1.8259, df = 152.98, p-value = 0.06981
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
## -0.02101342 0.53377158
## sample estimates:
## mean in group easy mean in group hard
## 2.805063 2.548684
6. Model 1: Condition → Emotion → Aggression (Moderated by CWV &
TIPI)
6.1 Does Condition Predict Moral Outrage?
m1a <- lm(moral_outrage ~ cond, data = df)
summary(m1a)
##
## Call:
## lm(formula = moral_outrage ~ cond, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -3.4605 -1.7637 -0.1272 1.5395 3.9030
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.7637 0.2074 13.324 < 2e-16 ***
## condhard 1.6968 0.2962 5.728 5.22e-08 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.844 on 153 degrees of freedom
## Multiple R-squared: 0.1766, Adjusted R-squared: 0.1712
## F-statistic: 32.81 on 1 and 153 DF, p-value: 5.222e-08
People in the hard condition felt more moral outrage.
6.2 Does Condition Predict Sympathy?
m1b <- lm(sympathy ~ cond, data = df)
summary(m1b)
##
## Call:
## lm(formula = sympathy ~ cond, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.5411 -0.5411 -0.4211 0.2089 4.5789
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.5411 0.1056 14.599 <2e-16 ***
## condhard -0.1201 0.1508 -0.797 0.427
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.9383 on 153 degrees of freedom
## Multiple R-squared: 0.00413, Adjusted R-squared: -0.002379
## F-statistic: 0.6345 on 1 and 153 DF, p-value: 0.427
Condition does not predict sympathy.
6.3 Visualize Emotion by Condition
df[, c("cond", "moral_outrage", "sympathy")] %>%
pivot_longer(c(moral_outrage, sympathy), names_to = "Emotion", values_to = "Score") %>%
mutate(Emotion = recode(Emotion,
"moral_outrage" = "Moral Outrage",
"sympathy" = "Sympathy")) %>%
group_by(cond, Emotion) %>%
summarise(M = mean(Score, na.rm = TRUE),
SE = sd(Score, na.rm = TRUE) / sqrt(n()), .groups = "drop") %>%
ggplot(aes(x = Emotion, y = M, fill = cond)) +
geom_col(position = position_dodge(0.6), width = 0.5) +
geom_errorbar(aes(ymin = M - SE, ymax = M + SE),
position = position_dodge(0.6), width = 0.2) +
scale_fill_manual(values = c("easy" = "#5B8DB8", "hard" = "#E07B54"),
name = "Condition") +
scale_y_continuous(limits = c(0, 7)) +
labs(title = "Moral Outrage and Sympathy by Condition",
x = NULL, y = "Mean (1–7)") +
theme_minimal()

6.4 Do Emotions Predict Aggression?
m1c <- lm(total_aggression ~ moral_outrage + sympathy + cond, data = df)
summary(m1c)
##
## Call:
## lm(formula = total_aggression ~ moral_outrage + sympathy + cond,
## data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -11.440 -5.210 -1.441 2.926 34.759
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 13.0105 1.7413 7.472 5.93e-12 ***
## moral_outrage -0.3998 0.3504 -1.141 0.2558
## sympathy -1.1708 0.6886 -1.700 0.0911 .
## condhard 0.1207 1.4109 0.086 0.9319
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 7.966 on 151 degrees of freedom
## Multiple R-squared: 0.02589, Adjusted R-squared: 0.006533
## F-statistic: 1.338 on 3 and 151 DF, p-value: 0.2644
m1c_2 <- lm(total_stuns ~ moral_outrage + sympathy + cond, data = df)
summary(m1c_2)
##
## Call:
## lm(formula = total_stuns ~ moral_outrage + sympathy + cond, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -9.013 -4.266 -1.265 2.534 33.034
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 9.9788 1.4131 7.062 5.61e-11 ***
## moral_outrage -0.3491 0.2844 -1.227 0.222
## sympathy -0.6169 0.5588 -1.104 0.271
## condhard -1.5845 1.1450 -1.384 0.168
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 6.464 on 151 degrees of freedom
## Multiple R-squared: 0.04202, Adjusted R-squared: 0.02299
## F-statistic: 2.208 on 3 and 151 DF, p-value: 0.08954
6.5 CWV as Moderator of Condition → Moral Outrage
df <- df %>% mutate(cwv_c = scale(cwv, center = TRUE, scale = TRUE)[,1])
m1d <- lm(moral_outrage ~ cond * cwv_c, data = df)
summary(m1d)
##
## Call:
## lm(formula = moral_outrage ~ cond * cwv_c, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -3.4773 -1.6232 0.0864 1.5571 4.0574
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.7461 0.2093 13.119 < 2e-16 ***
## condhard 1.6695 0.2992 5.581 1.08e-07 ***
## cwv_c 0.1231 0.2073 0.594 0.553
## condhard:cwv_c -0.4257 0.3004 -1.417 0.158
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.842 on 151 degrees of freedom
## Multiple R-squared: 0.1889, Adjusted R-squared: 0.1728
## F-statistic: 11.72 on 3 and 151 DF, p-value: 6.008e-07
interact_plot(m1d,
pred = cond,
modx = cwv_c,
modx.values = c(-1, 0, 1),
modx.labels = c("Low CWV (-1 SD)", "Mean CWV", "High CWV (+1 SD)"),
x.label = "Condition (0 = Easy, 1 = Hard)",
y.label = "Moral Outrage",
main.title = "CWV × Condition Interaction on Moral Outrage",
legend.main = "CWV") +
theme_minimal()

The interaction is in the predicted direction — the negative
coefficient means high CWV people show less outrage in hard mode
compared to low CWV people, consistent with your theoretical model
(“just the usual jungle”). However it doesn’t reach significance at p =
.158.
6.6 CWV as Moderator of Condition → Sympathy
m1e <- lm(sympathy ~ cond * cwv_c, data = df)
summary(m1e)
##
## Call:
## lm(formula = sympathy ~ cond * cwv_c, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.9899 -0.5414 -0.4043 0.2051 4.3201
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.541686 0.104674 14.728 <2e-16 ***
## condhard -0.075617 0.149594 -0.505 0.6140
## cwv_c -0.003832 0.103667 -0.037 0.9706
## condhard:cwv_c 0.307318 0.150213 2.046 0.0425 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.921 on 151 degrees of freedom
## Multiple R-squared: 0.05302, Adjusted R-squared: 0.0342
## F-statistic: 2.818 on 3 and 151 DF, p-value: 0.04107
interact_plot(m1e,
pred = cond,
modx = cwv_c,
modx.values = c(-1, 0, 1),
modx.labels = c("Low CWV (-1 SD)", "Mean CWV", "High CWV (+1 SD)"),
x.label = "Condition (0 = Easy, 1 = Hard)",
y.label = "Sympathy",
main.title = "CWV × Condition Interaction on Sympathy",
legend.main = "CWV") +
theme_minimal()

Weird!!! The positive coefficient means high CWV people actually feel
more sympathy in hard mode than low CWV people do. This is the opposite
of what your model predicted. High CWV people in hard mode may be
experiencing something more like competitive respect or admiration for
the opponents beating them, which loads onto sympathy items.
6.7 CWV as Moderator of Condition → Aggression
m1f <- lm(total_aggression ~ cond * cwv_c, data = df)
summary(m1f)
##
## Call:
## lm(formula = total_aggression ~ cond * cwv_c, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -12.748 -5.453 -0.869 3.471 32.252
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 10.3774 0.9030 11.492 <2e-16 ***
## condhard -0.7179 1.2905 -0.556 0.5789
## cwv_c -1.9348 0.8943 -2.164 0.0321 *
## condhard:cwv_c 1.7683 1.2958 1.365 0.1744
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 7.945 on 151 degrees of freedom
## Multiple R-squared: 0.03093, Adjusted R-squared: 0.01167
## F-statistic: 1.606 on 3 and 151 DF, p-value: 0.1903
interact_plot(m1f,
pred = cond,
modx = cwv_c,
modx.values = c(-1, 0, 1),
modx.labels = c("Low CWV (-1 SD)", "Mean CWV", "High CWV (+1 SD)"),
x.label = "Condition (0 = Easy, 1 = Hard)",
y.label = "Total Aggression",
main.title = "CWV × Condition Interaction on Aggression",
legend.main = "CWV") +
theme_minimal()

m1f_2 <- lm(total_stuns ~ cond * cwv_c, data = df)
summary(m1f_2)
##
## Call:
## lm(formula = total_stuns ~ cond * cwv_c, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -10.523 -4.226 -1.196 3.123 30.477
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 8.3199 0.7258 11.463 <2e-16 ***
## condhard -2.3358 1.0373 -2.252 0.0258 *
## cwv_c -1.7980 0.7188 -2.501 0.0134 *
## condhard:cwv_c 1.9568 1.0416 1.879 0.0622 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 6.386 on 151 degrees of freedom
## Multiple R-squared: 0.06502, Adjusted R-squared: 0.04644
## F-statistic: 3.5 on 3 and 151 DF, p-value: 0.01706
interact_plot(m1f_2,
pred = cond,
modx = cwv_c,
modx.values = c(-1, 0, 1),
modx.labels = c("Low CWV (-1 SD)", "Mean CWV", "High CWV (+1 SD)"),
x.label = "Condition (0 = Easy, 1 = Hard)",
y.label = "Total Aggression",
main.title = "CWV × Condition Interaction on Aggression",
legend.main = "CWV") +
theme_minimal()
Stuns: This is trending toward significance and is in the predicted
direction. In easy mode high CWV people stun less, but in hard mode that
gap narrows or reverses. With more power this could reach
significance?
6.8 TIPI Agreeableness as Moderator
df <- df %>%
mutate(agree_c = scale(tipi_agreeableness, center = TRUE, scale = TRUE)[,1])
m1g <- lm(moral_outrage ~ cond * agree_c, data = df)
summary(m1g)
##
## Call:
## lm(formula = moral_outrage ~ cond * agree_c, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -3.5868 -1.6096 -0.0758 1.5409 3.9685
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.75556 0.20918 13.173 < 2e-16 ***
## condhard 1.69718 0.29883 5.679 6.73e-08 ***
## agree_c -0.09964 0.20510 -0.486 0.628
## condhard:agree_c 0.19116 0.30042 0.636 0.526
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.853 on 151 degrees of freedom
## Multiple R-squared: 0.1788, Adjusted R-squared: 0.1625
## F-statistic: 10.96 on 3 and 151 DF, p-value: 1.487e-06
m1h <- lm(sympathy ~ cond * agree_c, data = df)
summary(m1h)
##
## Call:
## lm(formula = sympathy ~ cond * agree_c, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.7406 -0.4830 -0.4214 0.2084 4.5817
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.53683 0.10652 14.428 <2e-16 ***
## condhard -0.11637 0.15217 -0.765 0.446
## agree_c -0.05271 0.10444 -0.505 0.615
## condhard:agree_c 0.05968 0.15298 0.390 0.697
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.9437 on 151 degrees of freedom
## Multiple R-squared: 0.005832, Adjusted R-squared: -0.01392
## F-statistic: 0.2953 on 3 and 151 DF, p-value: 0.8288
m1i <- lm(total_aggression ~ cond * agree_c, data = df)
summary(m1i)
##
## Call:
## lm(formula = total_aggression ~ cond * agree_c, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -10.264 -5.117 -1.078 3.156 34.916
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 10.10743 0.91008 11.106 <2e-16 ***
## condhard -0.45900 1.30010 -0.353 0.725
## agree_c 0.07535 0.89232 0.084 0.933
## condhard:agree_c 0.34521 1.30704 0.264 0.792
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 8.063 on 151 degrees of freedom
## Multiple R-squared: 0.002014, Adjusted R-squared: -0.01781
## F-statistic: 0.1016 on 3 and 151 DF, p-value: 0.959
6.11 Within easy mode, are people being magnanimous
7. Model 2: Score & Perceptions → SDT Facets
This model examines how objective performance (score) and subjective
opponent perceptions predict the three SDT needs — autonomy, competence,
and relatedness.
7.1 Predictors of SDT Competence
m2a <- lm(sdt_competence ~ Player_score + opponent_perception + cond,
data = df)
summary(m2a)
##
## Call:
## lm(formula = sdt_competence ~ Player_score + opponent_perception +
## cond, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -3.7389 -1.3338 0.0259 1.0856 4.2033
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 4.36936 0.44109 9.906 < 2e-16 ***
## Player_score 0.15413 0.06400 2.408 0.017239 *
## opponent_perception -0.09881 0.09356 -1.056 0.292606
## condhard -1.22682 0.33752 -3.635 0.000381 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.516 on 151 degrees of freedom
## Multiple R-squared: 0.2962, Adjusted R-squared: 0.2822
## F-statistic: 21.18 on 3 and 151 DF, p-value: 1.662e-11
7.2 Predictors of SDT Autonomy
m2b <- lm(sdt_autonomy ~ Player_score + opponent_perception + cond,
data = df)
summary(m2b)
##
## Call:
## lm(formula = sdt_autonomy ~ Player_score + opponent_perception +
## cond, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -3.7992 -1.1573 -0.0419 1.0478 3.4548
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 4.83697 0.44336 10.910 <2e-16 ***
## Player_score 0.08606 0.06433 1.338 0.183
## opponent_perception -0.15282 0.09404 -1.625 0.106
## condhard -0.55532 0.33925 -1.637 0.104
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.524 on 151 degrees of freedom
## Multiple R-squared: 0.1237, Adjusted R-squared: 0.1063
## F-statistic: 7.103 on 3 and 151 DF, p-value: 0.0001698
7.4 SDT Facets by Condition
df[, c("cond", "sdt_autonomy", "sdt_competence", "sdt_relatedness")] %>%
pivot_longer(-cond, names_to = "Facet", values_to = "Score") %>%
mutate(Facet = recode(Facet,
"sdt_autonomy" = "Autonomy",
"sdt_competence" = "Competence",
"sdt_relatedness" = "Relatedness")) %>%
group_by(cond, Facet) %>%
summarise(M = mean(Score, na.rm = TRUE),
SE = sd(Score, na.rm = TRUE) / sqrt(n()), .groups = "drop") %>%
ggplot(aes(x = Facet, y = M, fill = cond)) +
geom_col(position = position_dodge(0.6), width = 0.5) +
geom_errorbar(aes(ymin = M - SE, ymax = M + SE),
position = position_dodge(0.6), width = 0.2) +
scale_fill_manual(values = c("easy" = "#5B8DB8", "hard" = "#E07B54"),
name = "Condition") +
scale_y_continuous(limits = c(0, 7)) +
labs(title = "SDT Facets by Condition",
x = NULL, y = "Mean (1–7)") +
theme_minimal()

7.5 Score Predicts Competence & Autonomy (Scatter)
df[, c("Player_score", "sdt_competence", "sdt_autonomy", "cond")] %>%
pivot_longer(c(sdt_competence, sdt_autonomy),
names_to = "Facet", values_to = "Score") %>%
mutate(Facet = recode(Facet,
"sdt_competence" = "Competence",
"sdt_autonomy" = "Autonomy")) %>%
ggplot(aes(x = Player_score, y = Score, color = cond)) +
geom_point(alpha = 0.4) +
geom_smooth(method = "lm", se = TRUE) +
scale_color_manual(values = c("easy" = "#5B8DB8", "hard" = "#E07B54"),
name = "Condition") +
facet_wrap(~Facet) +
labs(title = "Player Score → SDT Facets",
x = "Player Score", y = "SDT Facet (1–7)") +
theme_minimal()

8. Correlations
cor_vars <- df[, c("moral_outrage", "sympathy", "cwv", "tipi_agreeableness",
"sdt_autonomy", "sdt_competence", "sdt_relatedness",
"Player_score", "total_stuns", "opponent_perception",
"perceived_strength", "difficulty", "tipi_openness")]
names(cor_vars) <- c("Moral Outrage", "Sympathy", "CWV", "Agreeableness",
"Autonomy", "Competence", "Relatedness",
"Score", "Stuns", "Opp. Perception",
"Perc. Strength", "Difficulty", "Openness")
cor_matrix <- cor(cor_vars, use = "pairwise.complete.obs")
cor_matrix %>%
round(2) %>%
kable(caption = "Pairwise correlations among key variables") %>%
kable_styling(bootstrap_options = c("striped", "condensed"),
font_size = 11, full_width = TRUE) %>%
scroll_box(width = "100%")
Pairwise correlations among key variables
|
|
Moral Outrage
|
Sympathy
|
CWV
|
Agreeableness
|
Autonomy
|
Competence
|
Relatedness
|
Score
|
Stuns
|
Opp. Perception
|
Perc. Strength
|
Difficulty
|
Openness
|
|
Moral Outrage
|
1.00
|
-0.10
|
-0.10
|
0.03
|
-0.25
|
-0.28
|
-0.26
|
-0.36
|
-0.15
|
0.76
|
-0.37
|
0.37
|
0.10
|
|
Sympathy
|
-0.10
|
1.00
|
0.16
|
-0.03
|
0.09
|
0.21
|
0.40
|
0.01
|
-0.07
|
-0.18
|
0.14
|
0.01
|
0.08
|
|
CWV
|
-0.10
|
0.16
|
1.00
|
-0.24
|
0.14
|
0.13
|
0.21
|
0.07
|
-0.11
|
-0.09
|
0.19
|
-0.06
|
-0.23
|
|
Agreeableness
|
0.03
|
-0.03
|
-0.24
|
1.00
|
0.02
|
0.12
|
0.00
|
-0.26
|
0.02
|
-0.14
|
-0.03
|
0.14
|
0.38
|
|
Autonomy
|
-0.25
|
0.09
|
0.14
|
0.02
|
1.00
|
0.64
|
0.44
|
0.28
|
-0.16
|
-0.24
|
0.47
|
-0.39
|
-0.11
|
|
Competence
|
-0.28
|
0.21
|
0.13
|
0.12
|
0.64
|
1.00
|
0.55
|
0.46
|
0.05
|
-0.27
|
0.67
|
-0.53
|
0.06
|
|
Relatedness
|
-0.26
|
0.40
|
0.21
|
0.00
|
0.44
|
0.55
|
1.00
|
0.15
|
-0.10
|
-0.31
|
0.39
|
-0.30
|
-0.01
|
|
Score
|
-0.36
|
0.01
|
0.07
|
-0.26
|
0.28
|
0.46
|
0.15
|
1.00
|
0.26
|
-0.25
|
0.56
|
-0.67
|
-0.13
|
|
Stuns
|
-0.15
|
-0.07
|
-0.11
|
0.02
|
-0.16
|
0.05
|
-0.10
|
0.26
|
1.00
|
-0.08
|
0.13
|
-0.23
|
0.17
|
|
Opp. Perception
|
0.76
|
-0.18
|
-0.09
|
-0.14
|
-0.24
|
-0.27
|
-0.31
|
-0.25
|
-0.08
|
1.00
|
-0.45
|
0.35
|
-0.03
|
|
Perc. Strength
|
-0.37
|
0.14
|
0.19
|
-0.03
|
0.47
|
0.67
|
0.39
|
0.56
|
0.13
|
-0.45
|
1.00
|
-0.67
|
0.06
|
|
Difficulty
|
0.37
|
0.01
|
-0.06
|
0.14
|
-0.39
|
-0.53
|
-0.30
|
-0.67
|
-0.23
|
0.35
|
-0.67
|
1.00
|
0.05
|
|
Openness
|
0.10
|
0.08
|
-0.23
|
0.38
|
-0.11
|
0.06
|
-0.01
|
-0.13
|
0.17
|
-0.03
|
0.06
|
0.05
|
1.00
|
8.1 Easy mode correlations
vars <- c("moral_outrage", "sympathy", "cwv", "tipi_agreeableness",
"sdt_autonomy", "sdt_competence", "sdt_relatedness",
"Player_score", "total_stuns", "Player_raids_hut","Player_produces_fruit","Enemy_score", "opponent_perception",
"perceived_strength", "difficulty", "tipi_openness")
nice_names <- c("Moral Outrage", "Sympathy", "CWV", "Agreeableness",
"Autonomy", "Competence", "Relatedness",
"Score", "Stuns", "Raids_hut" , "Produces_fruit", "Enemy_score","Opp. Perception",
"Perc. Strength", "Difficulty", "Openness")
## 8.1 Correlations Within Easy Mode
cor_vars_easy <- df[df$cond == "easy", vars]
names(cor_vars_easy) <- nice_names
cor(cor_vars_easy, use = "pairwise.complete.obs") %>%
round(2) %>%
kable(caption = "Pairwise correlations — Easy mode only") %>%
kable_styling(bootstrap_options = c("striped", "condensed"),
font_size = 11, full_width = TRUE) %>%
scroll_box(width = "100%")
Pairwise correlations — Easy mode only
|
|
Moral Outrage
|
Sympathy
|
CWV
|
Agreeableness
|
Autonomy
|
Competence
|
Relatedness
|
Score
|
Stuns
|
Raids_hut
|
Produces_fruit
|
Enemy_score
|
Opp. Perception
|
Perc. Strength
|
Difficulty
|
Openness
|
|
Moral Outrage
|
1.00
|
-0.13
|
0.07
|
-0.06
|
-0.09
|
-0.09
|
-0.26
|
-0.17
|
-0.06
|
-0.02
|
-0.10
|
0.05
|
0.75
|
0.05
|
0.23
|
0.15
|
|
Sympathy
|
-0.13
|
1.00
|
0.00
|
-0.06
|
-0.07
|
0.28
|
0.32
|
-0.02
|
0.01
|
-0.09
|
0.02
|
0.03
|
-0.11
|
-0.09
|
0.10
|
0.04
|
|
CWV
|
0.07
|
0.00
|
1.00
|
-0.24
|
0.16
|
0.05
|
0.22
|
-0.06
|
-0.24
|
-0.07
|
-0.13
|
-0.19
|
0.06
|
0.05
|
-0.01
|
-0.33
|
|
Agreeableness
|
-0.06
|
-0.06
|
-0.24
|
1.00
|
0.00
|
0.16
|
-0.05
|
-0.36
|
0.05
|
-0.15
|
-0.20
|
0.08
|
-0.29
|
-0.11
|
0.31
|
0.38
|
|
Autonomy
|
-0.09
|
-0.07
|
0.16
|
0.00
|
1.00
|
0.61
|
0.40
|
0.17
|
-0.18
|
0.03
|
0.17
|
0.08
|
-0.06
|
0.32
|
-0.30
|
-0.14
|
|
Competence
|
-0.09
|
0.28
|
0.05
|
0.16
|
0.61
|
1.00
|
0.48
|
0.26
|
0.07
|
0.17
|
0.14
|
-0.04
|
-0.10
|
0.48
|
-0.32
|
0.06
|
|
Relatedness
|
-0.26
|
0.32
|
0.22
|
-0.05
|
0.40
|
0.48
|
1.00
|
-0.05
|
-0.09
|
0.01
|
0.11
|
0.12
|
-0.18
|
0.04
|
-0.05
|
-0.19
|
|
Score
|
-0.17
|
-0.02
|
-0.06
|
-0.36
|
0.17
|
0.26
|
-0.05
|
1.00
|
0.24
|
0.43
|
0.60
|
-0.04
|
0.05
|
0.37
|
-0.58
|
-0.19
|
|
Stuns
|
-0.06
|
0.01
|
-0.24
|
0.05
|
-0.18
|
0.07
|
-0.09
|
0.24
|
1.00
|
0.23
|
-0.17
|
-0.40
|
0.01
|
0.11
|
-0.29
|
0.21
|
|
Raids_hut
|
-0.02
|
-0.09
|
-0.07
|
-0.15
|
0.03
|
0.17
|
0.01
|
0.43
|
0.23
|
1.00
|
-0.05
|
-0.37
|
0.02
|
0.27
|
-0.34
|
-0.14
|
|
Produces_fruit
|
-0.10
|
0.02
|
-0.13
|
-0.20
|
0.17
|
0.14
|
0.11
|
0.60
|
-0.17
|
-0.05
|
1.00
|
0.75
|
0.15
|
-0.01
|
-0.15
|
-0.09
|
|
Enemy_score
|
0.05
|
0.03
|
-0.19
|
0.08
|
0.08
|
-0.04
|
0.12
|
-0.04
|
-0.40
|
-0.37
|
0.75
|
1.00
|
0.17
|
-0.33
|
0.26
|
0.05
|
|
Opp. Perception
|
0.75
|
-0.11
|
0.06
|
-0.29
|
-0.06
|
-0.10
|
-0.18
|
0.05
|
0.01
|
0.02
|
0.15
|
0.17
|
1.00
|
-0.05
|
0.07
|
-0.02
|
|
Perc. Strength
|
0.05
|
-0.09
|
0.05
|
-0.11
|
0.32
|
0.48
|
0.04
|
0.37
|
0.11
|
0.27
|
-0.01
|
-0.33
|
-0.05
|
1.00
|
-0.46
|
0.11
|
|
Difficulty
|
0.23
|
0.10
|
-0.01
|
0.31
|
-0.30
|
-0.32
|
-0.05
|
-0.58
|
-0.29
|
-0.34
|
-0.15
|
0.26
|
0.07
|
-0.46
|
1.00
|
0.13
|
|
Openness
|
0.15
|
0.04
|
-0.33
|
0.38
|
-0.14
|
0.06
|
-0.19
|
-0.19
|
0.21
|
-0.14
|
-0.09
|
0.05
|
-0.02
|
0.11
|
0.13
|
1.00
|
8.2 Hard mode correlations
## 8.2 Correlations Within Hard Mode
cor_vars_hard <- df[df$cond == "hard", vars]
names(cor_vars_hard) <- nice_names
cor(cor_vars_hard, use = "pairwise.complete.obs") %>%
round(2) %>%
kable(caption = "Pairwise correlations — Hard mode only") %>%
kable_styling(bootstrap_options = c("striped", "condensed"),
font_size = 11, full_width = TRUE) %>%
scroll_box(width = "100%")
Pairwise correlations — Hard mode only
|
|
Moral Outrage
|
Sympathy
|
CWV
|
Agreeableness
|
Autonomy
|
Competence
|
Relatedness
|
Score
|
Stuns
|
Raids_hut
|
Produces_fruit
|
Enemy_score
|
Opp. Perception
|
Perc. Strength
|
Difficulty
|
Openness
|
|
Moral Outrage
|
1.00
|
-0.04
|
-0.15
|
0.05
|
-0.17
|
-0.08
|
-0.07
|
-0.15
|
-0.15
|
-0.01
|
0.04
|
0.04
|
0.67
|
-0.30
|
0.11
|
0.04
|
|
Sympathy
|
-0.04
|
1.00
|
0.31
|
0.01
|
0.18
|
0.14
|
0.50
|
-0.20
|
-0.22
|
-0.19
|
-0.30
|
-0.27
|
-0.22
|
0.34
|
0.00
|
0.13
|
|
CWV
|
-0.15
|
0.31
|
1.00
|
-0.23
|
0.06
|
0.09
|
0.13
|
0.03
|
0.03
|
-0.07
|
-0.12
|
-0.11
|
-0.14
|
0.21
|
0.07
|
-0.12
|
|
Agreeableness
|
0.05
|
0.01
|
-0.23
|
1.00
|
0.10
|
0.22
|
0.13
|
-0.11
|
0.00
|
0.09
|
-0.12
|
-0.11
|
-0.09
|
0.17
|
-0.17
|
0.38
|
|
Autonomy
|
-0.17
|
0.18
|
0.06
|
0.10
|
1.00
|
0.58
|
0.39
|
-0.03
|
-0.31
|
-0.24
|
-0.12
|
-0.10
|
-0.18
|
0.41
|
-0.25
|
-0.07
|
|
Competence
|
-0.08
|
0.14
|
0.09
|
0.22
|
0.58
|
1.00
|
0.51
|
0.12
|
-0.21
|
-0.16
|
-0.23
|
-0.25
|
-0.07
|
0.56
|
-0.40
|
0.15
|
|
Relatedness
|
-0.07
|
0.50
|
0.13
|
0.13
|
0.39
|
0.51
|
1.00
|
-0.11
|
-0.28
|
-0.15
|
-0.19
|
-0.18
|
-0.26
|
0.52
|
-0.37
|
0.22
|
|
Score
|
-0.15
|
-0.20
|
0.03
|
-0.11
|
-0.03
|
0.12
|
-0.11
|
1.00
|
0.12
|
-0.05
|
0.02
|
-0.04
|
-0.14
|
0.02
|
-0.12
|
0.09
|
|
Stuns
|
-0.15
|
-0.22
|
0.03
|
0.00
|
-0.31
|
-0.21
|
-0.28
|
0.12
|
1.00
|
0.38
|
-0.12
|
-0.16
|
-0.07
|
-0.08
|
0.10
|
0.15
|
|
Raids_hut
|
-0.01
|
-0.19
|
-0.07
|
0.09
|
-0.24
|
-0.16
|
-0.15
|
-0.05
|
0.38
|
1.00
|
-0.22
|
-0.23
|
-0.12
|
-0.10
|
-0.06
|
-0.07
|
|
Produces_fruit
|
0.04
|
-0.30
|
-0.12
|
-0.12
|
-0.12
|
-0.23
|
-0.19
|
0.02
|
-0.12
|
-0.22
|
1.00
|
0.98
|
0.46
|
-0.50
|
0.28
|
-0.13
|
|
Enemy_score
|
0.04
|
-0.27
|
-0.11
|
-0.11
|
-0.10
|
-0.25
|
-0.18
|
-0.04
|
-0.16
|
-0.23
|
0.98
|
1.00
|
0.45
|
-0.51
|
0.30
|
-0.14
|
|
Opp. Perception
|
0.67
|
-0.22
|
-0.14
|
-0.09
|
-0.18
|
-0.07
|
-0.26
|
-0.14
|
-0.07
|
-0.12
|
0.46
|
0.45
|
1.00
|
-0.47
|
0.30
|
-0.08
|
|
Perc. Strength
|
-0.30
|
0.34
|
0.21
|
0.17
|
0.41
|
0.56
|
0.52
|
0.02
|
-0.08
|
-0.10
|
-0.50
|
-0.51
|
-0.47
|
1.00
|
-0.55
|
0.13
|
|
Difficulty
|
0.11
|
0.00
|
0.07
|
-0.17
|
-0.25
|
-0.40
|
-0.37
|
-0.12
|
0.10
|
-0.06
|
0.28
|
0.30
|
0.30
|
-0.55
|
1.00
|
-0.13
|
|
Openness
|
0.04
|
0.13
|
-0.12
|
0.38
|
-0.07
|
0.15
|
0.22
|
0.09
|
0.15
|
-0.07
|
-0.13
|
-0.14
|
-0.08
|
0.13
|
-0.13
|
1.00
|
9. Behavioral outcomes by condition
## 9. Behavioral Outcomes by Condition
# Summarise all three behavioral outcomes by condition
behavioral_summary <- df %>%
group_by(cond) %>%
summarise(
Stuns_M = round(mean(total_stuns, na.rm = TRUE), 2),
Stuns_SD = round(sd(total_stuns, na.rm = TRUE), 2),
Raids_M = round(mean(Player_raids_hut, na.rm = TRUE), 2),
Raids_SD = round(sd(Player_raids_hut, na.rm = TRUE), 2),
Fruit_M = round(mean(Player_produces_fruit, na.rm = TRUE), 2),
Fruit_SD = round(sd(Player_produces_fruit, na.rm = TRUE), 2)
)
kable(behavioral_summary,
col.names = c("Condition",
"Stuns M", "Stuns SD",
"Raids M", "Raids SD",
"Fruit M", "Fruit SD"),
caption = "Behavioral outcomes by condition") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Behavioral outcomes by condition
|
Condition
|
Stuns M
|
Stuns SD
|
Raids M
|
Raids SD
|
Fruit M
|
Fruit SD
|
|
easy
|
8.06
|
7.62
|
2.04
|
1.92
|
3.82
|
2.19
|
|
hard
|
5.96
|
5.01
|
3.72
|
4.31
|
3.26
|
2.09
|
# t-tests for each outcome
cat("--- Stuns ---\n"); print(t.test(total_stuns ~ cond, data = df))
## --- Stuns ---
##
## Welch Two Sample t-test
##
## data: total_stuns by cond
## t = 2.0367, df = 135.45, p-value = 0.04363
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
## 0.06096684 4.14456281
## sample estimates:
## mean in group easy mean in group hard
## 8.063291 5.960526
cat("--- Raids ---\n"); print(t.test(Player_raids_hut ~ cond, data = df))
## --- Raids ---
##
## Welch Two Sample t-test
##
## data: Player_raids_hut by cond
## t = -3.125, df = 102.71, p-value = 0.002312
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
## -2.755565 -0.615854
## sample estimates:
## mean in group easy mean in group hard
## 2.037975 3.723684
cat("--- Fruit ---\n"); print(t.test(Player_produces_fruit ~ cond, data = df))
## --- Fruit ---
##
## Welch Two Sample t-test
##
## data: Player_produces_fruit by cond
## t = 1.6296, df = 152.99, p-value = 0.1053
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
## -0.1188349 1.2380888
## sample estimates:
## mean in group easy mean in group hard
## 3.822785 3.263158
# Figure: all three outcomes side by side
df %>%
rename(Stuns = total_stuns,
Raids = Player_raids_hut,
Fruit = Player_produces_fruit) %>%
pivot_longer(c(Stuns, Raids, Fruit),
names_to = "Behavior", values_to = "Count") %>%
group_by(cond, Behavior) %>%
summarise(M = mean(Count, na.rm = TRUE),
SE = sd(Count, na.rm = TRUE) / sqrt(n()),
.groups = "drop") %>%
mutate(Behavior = factor(Behavior, levels = c("Stuns", "Raids", "Fruit"))) %>%
ggplot(aes(x = Behavior, y = M, fill = cond)) +
geom_col(position = position_dodge(0.6), width = 0.5) +
geom_errorbar(aes(ymin = M - SE, ymax = M + SE),
position = position_dodge(0.6), width = 0.2) +
scale_fill_manual(values = c("easy" = "#5B8DB8", "hard" = "#E07B54"),
name = "Condition") +
labs(title = "Behavioral Outcomes by Condition",
subtitle = "Error bars = ±1 SE",
x = NULL, y = "Mean Count") +
theme_minimal() +
theme(legend.position = "top")

9.1 Behavioral Diversity by Condition
For each player, compute how many distinct behaviors they engaged in
(>0) and a Shannon entropy score as a continuous diversity
measure.
df <- df %>%
mutate(
# Binary indicators of whether each behavior occurred at all
did_stun = as.integer(total_stuns > 0),
did_raid = as.integer(Player_raids_hut > 0),
did_fruit = as.integer(Player_produces_fruit > 0),
# Count of distinct behaviors engaged in (0-3)
behavior_variety = did_stun + did_raid + did_fruit,
# Shannon entropy: higher = more evenly spread across behaviors
# (avoids 0 log 0 issues by adding small constant)
total_actions = total_stuns + Player_raids_hut + Player_produces_fruit + 0.001,
p_stun = (total_stuns + 0.001) / total_actions,
p_raid = (Player_raids_hut + 0.001) / total_actions,
p_fruit = (Player_produces_fruit + 0.001) / total_actions,
shannon_entropy = -(p_stun * log(p_stun) +
p_raid * log(p_raid) +
p_fruit * log(p_fruit))
)
# Summary table
df %>%
group_by(cond) %>%
summarise(
Variety_M = round(mean(behavior_variety, na.rm = TRUE), 2),
Variety_SD = round(sd(behavior_variety, na.rm = TRUE), 2),
Entropy_M = round(mean(shannon_entropy, na.rm = TRUE), 3),
Entropy_SD = round(sd(shannon_entropy, na.rm = TRUE), 3),
Pct_did_all_3 = paste0(round(mean(behavior_variety == 3,
na.rm = TRUE) * 100, 1), "%"),
Pct_did_1_or_fewer = paste0(round(mean(behavior_variety <= 1,
na.rm = TRUE) * 100, 1), "%")
) %>%
kable(col.names = c("Condition", "Variety M", "Variety SD",
"Entropy M", "Entropy SD",
"% Did All 3", "% Did ≤1"),
caption = "Behavioral diversity by condition") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Behavioral diversity by condition
|
Condition
|
Variety M
|
Variety SD
|
Entropy M
|
Entropy SD
|
% Did All 3
|
% Did ≤1
|
|
easy
|
2.56
|
0.67
|
0.731
|
0.325
|
65.8%
|
10.1%
|
|
hard
|
2.54
|
0.74
|
0.753
|
0.329
|
67.1%
|
11.8%
|
# t-tests
cat("--- Behavior variety ---\n")
## --- Behavior variety ---
print(t.test(behavior_variety ~ cond, data = df))
##
## Welch Two Sample t-test
##
## data: behavior_variety by cond
## t = 0.15378, df = 150.5, p-value = 0.878
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
## -0.2072060 0.2421827
## sample estimates:
## mean in group easy mean in group hard
## 2.556962 2.539474
cat("--- Shannon entropy ---\n")
## --- Shannon entropy ---
print(t.test(shannon_entropy ~ cond, data = df))
##
## Welch Two Sample t-test
##
## data: shannon_entropy by cond
## t = -0.40429, df = 152.59, p-value = 0.6866
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
## -0.12501302 0.08254002
## sample estimates:
## mean in group easy mean in group hard
## 0.7313866 0.7526231
# Figure 1: Distribution of variety scores
df %>%
count(cond, behavior_variety) %>%
group_by(cond) %>%
mutate(pct = n / sum(n)) %>%
ggplot(aes(x = factor(behavior_variety), y = pct, fill = cond)) +
geom_col(position = position_dodge(0.6), width = 0.5) +
geom_text(aes(label = paste0(round(pct * 100), "%")),
position = position_dodge(0.6), vjust = -0.4, size = 3.5) +
scale_fill_manual(values = c("easy" = "#5B8DB8", "hard" = "#E07B54"),
name = "Condition") +
scale_y_continuous(labels = percent_format(), limits = c(0, .6)) +
labs(title = "Number of Distinct Behaviors by Condition",
subtitle = "0 = did none, 3 = engaged in all three behaviors",
x = "Number of Distinct Behaviors", y = "Proportion of Players") +
theme_minimal() +
theme(legend.position = "top")

# Figure 2: Shannon entropy distribution
ggplot(df, aes(x = shannon_entropy, fill = cond)) +
geom_density(alpha = 0.6) +
scale_fill_manual(values = c("easy" = "#5B8DB8", "hard" = "#E07B54"),
name = "Condition") +
labs(title = "Behavioral Diversity (Shannon Entropy) by Condition",
subtitle = "Higher entropy = more evenly distributed across behaviors",
x = "Shannon Entropy", y = "Density") +
theme_minimal() +
theme(legend.position = "top")

# Figure 3: Stacked proportion chart showing behavioral mix
df %>%
group_by(cond) %>%
summarise(
Stuns = mean(total_stuns, na.rm = TRUE),
Raids = mean(Player_raids_hut, na.rm = TRUE),
Fruit = mean(Player_produces_fruit, na.rm = TRUE)
) %>%
pivot_longer(-cond, names_to = "Behavior", values_to = "Mean") %>%
group_by(cond) %>%
mutate(
Total = sum(Mean),
Pct = Mean / Total,
Behavior = factor(Behavior, levels = c("Fruit", "Raids", "Stuns"))
) %>%
ggplot(aes(x = cond, y = Pct, fill = Behavior)) +
geom_col(width = 0.5) +
geom_text(aes(label = paste0(round(Pct * 100), "%")),
position = position_stack(vjust = 0.5), size = 3.5, color = "white") +
scale_fill_manual(values = c("Stuns" = "#E07B54",
"Raids" = "#5B8DB8",
"Fruit" = "#6BAE75")) +
scale_y_continuous(labels = percent_format()) +
labs(title = "Behavioral Mix by Condition",
subtitle = "Proportion of total actions spent on each behavior",
x = "Condition", y = "Proportion of Actions") +
theme_minimal() +
theme(legend.position = "top")
