7. Main Effects: Condition → Behavioral Outcomes
outcomes_beh <- list(
"Total Aggression" = "total_player_agg",
"Non-Instr. Stuns" = "noninstr_aggression",
"Instr. Aggression" = "instr_aggression",
"Shakes" = "shake_count",
"Score" = "player_score",
"Score Diff" = "score_diff"
)
beh_anova_table <- map_dfr(names(outcomes_beh), function(nm) {
formula <- as.formula(paste(outcomes_beh[[nm]], "~ hostile * capable"))
m <- aov(formula, data = df)
s <- summary(m)[[1]]
tibble(
Outcome = nm,
`Hostile F` = round(s$`F value`[1], 3),
`Hostile p` = round(s$`Pr(>F)`[1], 3),
`Capable F` = round(s$`F value`[2], 3),
`Capable p` = round(s$`Pr(>F)`[2], 3),
`Interact. F` = round(s$`F value`[3], 3),
`Interact. p` = round(s$`Pr(>F)`[3], 3)
)
})
kable(beh_anova_table,
caption = "2×2 ANOVA results: Condition → Behavioral Outcomes") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
| Outcome | Hostile F | Hostile p | Capable F | Capable p | Interact. F | Interact. p |
|---|---|---|---|---|---|---|
| Total Aggression | 120.238 | 0.000 | 11.334 | 0.001 | 6.712 | 0.010 |
| Non-Instr. Stuns | 186.808 | 0.000 | 4.696 | 0.031 | 2.066 | 0.151 |
| Instr. Aggression | 2.055 | 0.153 | 7.141 | 0.008 | 5.335 | 0.021 |
| Shakes | 11.319 | 0.001 | 1.304 | 0.254 | 4.641 | 0.032 |
| Score | 386.432 | 0.000 | 9.393 | 0.002 | 12.062 | 0.001 |
| Score Diff | 55.326 | 0.000 | 39.170 | 0.000 | 1.254 | 0.264 |
Interpretation:
Total Aggression: both main effects and the interaction are significant. Hostility drives aggression (F = 120, p < .001), capability amplifies it (F = 11, p = .001), and the interaction confirms capability matters more when the NPC is hostile (F = 6.7, p = .010).
Non-Instrumental Stuns: hostility is the dominant predictor (F = 187, p < .001). Capability also adds a small independent effect (F = 4.7, p = .031) but no interaction — capable opponents get stunned more regardless of their hostility level, possibly because they’re more threatening even when benign.
Instrumental Aggression: Hostility does NOT predict instrumental aggression (F = 2.1, p = .153) — players don’t steal/raid more just because the NPC is hostile. But capability does (F = 7.1, p = .008), and there’s a significant interaction (F = 5.3, p = .021). This suggests instrumental aggression is a strategic response to a capable opponent rather than a retaliatory response to a hostile one — players steal and raid when it’s worth doing, not just when they’re angry.
Shakes: hostility significantly reduces shaking (F = 11.3, p = .001) — players produce less fruit when under attack, which makes sense since they’re busy retaliating. The interaction (F = 4.6, p = .032) suggests this suppression of shaking is especially pronounced in one specific condition.
Score: massive effects of both hostility (F = 386, p < .001) and capability (F = 9.4, p = .002), plus a significant interaction (F = 12.1, p = .001). Score is heavily determined by what the NPC does to you.
Score Diff: both main effects significant, no interaction. Hostile and capable NPCs both close the gap independently without amplifying each other for the margin.
7.1 Figures: Behavioral Outcomes by Condition
plot_2x2 <- function(var, label) {
df %>%
group_by(hostile, capable) %>%
summarise(M = mean(as.numeric(.data[[var]]), na.rm = TRUE),
SE = sd(as.numeric(.data[[var]]), na.rm = TRUE) / sqrt(n()),
.groups = "drop") %>%
ggplot(aes(x = capable, y = M, fill = hostile, group = hostile)) +
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("low" = "#5B8DB8","high" = "#E07B54"),
name = "NPC Hostile") +
labs(title = label, x = "NPC Capable", y = "Mean") +
theme_minimal(base_size = 10) +
theme(legend.position = "bottom")
}
p1 <- plot_2x2("total_player_agg", "Total Aggression")
p2 <- plot_2x2("noninstr_aggression", "Non-Instr. Stuns")
p3 <- plot_2x2("instr_aggression", "Instr. Aggression (Steals + Raids)")
p4 <- plot_2x2("shake_count", "Shakes")
p5 <- plot_2x2("player_score", "Player Score")
grid.arrange(p1, p2, p3, p4, p5, ncol = 3)