library(qualtRics)
library(psych)
library(scales)
library(ggplot2)
library(knitr)
library(kableExtra)
library(interactions)   # for interact_plot()
library(mediation)      # for mediation analysis
library(gridExtra)
library(tidyverse)

Key Takeaways — Pilot 4

1. Narcissism is the strongest individual difference finding in the study, though this should be interpreted cautiously. High narcissists aggressed significantly more in easy mode but pulled back in hard mode (Narcissism × Condition → Total Aggression: b = −3.04, p = .004), and showed more guilt specifically when winning (Narcissism × Condition → Felt Guilt: b = −0.61, p < .001). This suggests narcissists aggress opportunistically — maximizing dominance when they have the upper hand — and experience some form of self-focused discomfort when doing so, rather than empathy-based guilt. That said, narcissism was measured as a single item and its placement in the survey flow (after playing the game) may have primed certain response patterns, so these effects should prob be replicated. Narcissism was also very low in our sample.

2. Stuns and raids dissociate by condition, suggesting two distinct forms of aggression. Easy mode players stunned more (M = 7.86 vs 6.17, p = .025) while hard mode players raided more (M = 3.67 vs 2.46, p = .001). Stunning appears to be offensive, power-driven aggression available only when winning; raiding may reflect a desperation resource strategy when losing. This distinction was absent in Pilot 3, where stuns showed no condition difference at all.

3. Guilt follows power, not perception of opponents. Easy mode players felt significantly more post-game guilt than hard mode players (M = 1.88 vs 1.46, p = .006) — the opposite direction from what a frustration-aggression account would predict — because easy mode players were the ones doing the aggressing. Believing you were playing real people (vs. bots), even with high confidence, did not predict felt guilt, and GP did not moderate this relationship. The guilt-as-moral-brake hypothesis is not supported in this paradigm. DANG!!!!

4. Guilt proneness fails to replicate as a moderator. In Pilot 3, GP showed a trending interaction with condition on intent to aggress (p = .083), suggesting low GP people were more willing to attack when winning. In Pilot 4, the same interaction is essentially zero across every outcome — aggression, stuns, raids, felt guilt, and moral outrage (all p > .38).

5. Hard mode players are consistently more negative in affect and less willing to re-engage. Condition predicted disappointment (hard > easy), moral outrage (hard > easy), and desire to play again (easy M = 3.90 vs hard M = 2.28, b = −1.62, p < .001, R² = .15) — the largest effect size in the study. Neither felt guilt nor GP explained the play-again effect; it is driven entirely by the experience of losing. The play-again condition gap widened from Pilot 3 (Δ = 1.25) to Pilot 4 (Δ = 1.62).


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/pilot4_data.csv")

cat("Raw N =", nrow(raw), "\n")
## Raw N = 322
cat("Condition split (raw):\n")
## Condition split (raw):
print(table(raw$cond))
## 
## easy hard 
##  157  158

1.2 Apply Exclusions

# Read exclusion list
exclusions <- read_csv("~/Downloads/Pilot 4 List of Exclusions (260630) - Sheet1.csv")

cat("Raw N =", nrow(raw), "\n")
## Raw N = 322
cat("Exclusions N =", nrow(exclusions), "\n")
## Exclusions N = 57
## 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. 
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 = 246
cat("Condition split:\n")
## Condition split:
print(table(df$cond))
## 
## easy hard 
##  129  117

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 ──────────────────────────────────────────────────────
    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,

    # ── GP-5 composite (1–5 scale) ───────────────────────────────────────────
    gp    = rowMeans(pick(GP_1, GP_2, GP_3, GP_4, GP_5), na.rm = TRUE),
    gp_c  = as.numeric(scale(gp)),

    # ── Everyday Sadism composite (1–7 scale) ────────────────────────────────
    sadism   = rowMeans(pick(sadism_1, sadism_2, sadism_3, sadism_4,
                              sadism_5, sadism_6, sadism_7, sadism_8),
                         na.rm = TRUE),
    sadism_c = as.numeric(scale(sadism)),

    # ── Self-esteem & Narcissism (single items, 1–7) ─────────────────────────
    selfesteem_c = as.numeric(scale(selfesteem)),
    narcissism_c = as.numeric(scale(narcissism)),

    # ── Moral outrage composite ──────────────────────────────────────────────
    moral_outrage = rowMeans(pick(outrage_1, outrage_2, outrage_3), na.rm = TRUE),

    # ── Subjective behavioral intent ─────────────────────────────────────────
    # Aggression = mean of intent to steal and intent to stun
    intent_aggression = rowMeans(pick(intent_steal, intent_stun), na.rm = TRUE),

    # ── Behavioral aggression composites ─────────────────────────────────────
    total_stuns = rowSums(
      pick(Player_stuns_Attacker, Player_stuns_Thief, Player_stuns_Freerider),
      na.rm = TRUE
    ),
    total_aggression  = total_stuns + Player_raids_hut,
    log_aggression    = log(total_aggression + 1),

    # ── People or Bot ────────────────────────────────────────────────────────
    # 1 = Real people, 2 = Computer characters, 3 = Unsure
    people_or_bot_label = case_when(
      people_or_bot == 1 ~ "Real people",
      people_or_bot == 2 ~ "Computer/bots",
      people_or_bot == 3 ~ "Unsure",
      TRUE               ~ NA_character_
    ),
    people_or_bot_label = factor(
      people_or_bot_label,
      levels = c("Real people", "Unsure", "Computer/bots")
    ),
    # Dummy: TRUE if participant believed opponents were real humans
    believed_real = (people_or_bot == 1),
    # Continuous humanness: recode so higher = more human (flip scale)
    humanness = case_when(
      people_or_bot == 1 ~ 2,
      people_or_bot == 3 ~ 1,
      people_or_bot == 2 ~ 0,
      TRUE               ~ NA_real_
    ),

    # ── 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 == 1 ~ "Never",
      game_frequency == 2 ~ "< Once/month",
      game_frequency == 3 ~ "Few times/month",
      game_frequency == 4 ~ "Few times/week",
      game_frequency == 5 ~ "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")
    )
  )

2. Demographics & Gaming Experience

2.1 Sample Overview

demo_summary <- df %>%
  summarise(
    N          = n(),
    Age_M      = round(mean(as.numeric(age), na.rm = TRUE), 1),
    Age_SD     = round(sd(as.numeric(age),   na.rm = TRUE), 1),
    Age_Range  = paste0(min(as.numeric(age), na.rm = TRUE), "–",
                        max(as.numeric(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
246 41.8 12.2 19–78 54.9% 43.9% 1.2%

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. 177 72%
Black/Afr. Am. 25 10%
Latino/Hisp. 13 5%
NA 11 4%
E. Asian/Am. 8 3%
Biracial/Multi. 7 3%
S. Asian/Am. 2 1%
Middle Eastern 1 0%
Native Am. 1 0%
Other 1 0%

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()

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()

grid.arrange(p_freq, p_skill, ncol = 2)


3. Scale Reliability

alphas <- list(
  "Moral Outrage" = df[, c("outrage_1", "outrage_2", "outrage_3")],
  "GP-5"          = df[, c("GP_1", "GP_2", "GP_3", "GP_4", "GP_5")],
  "Sadism (BCTS)" = df[, c("sadism_1", "sadism_2", "sadism_3", "sadism_4",
                             "sadism_5", "sadism_6", "sadism_7", "sadism_8")]
)

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.916 3
GP-5 0.771 5
Sadism (BCTS) 0.898 8

Note: Self-esteem and narcissism are single items and do not have reliability estimates.


4. Descriptives by Condition

desc_vars <- c("moral_outrage", "surprised", "disappointed", "felt_guilt",
               "selfesteem", "narcissism", "sadism", "gp",
               "people_or_bot", "confidence",
               "Player_score", "total_stuns", "total_aggression", "intent_aggression", "Player_raids_hut",
               "Player_produces_fruit", "difficulty",
               "positive", "play_again")

desc_table <- df %>%
  group_by(cond) %>%
  summarise(across(
    all_of(desc_vars),
    list(M  = ~round(mean(as.numeric(.x), na.rm = TRUE), 2),
         SD = ~round(sd(as.numeric(.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.57 1.85 3.46 1.95
surprised 2.93 1.93 3.52 2.20
disappointed 3.00 2.17 4.39 2.13
felt_guilt 1.88 1.31 1.46 0.97
selfesteem 4.60 1.67 4.27 1.64
narcissism 1.43 0.74 1.77 1.34
sadism 1.54 0.80 1.73 0.93
gp 4.00 0.83 3.98 0.69
people_or_bot 1.90 0.62 1.96 0.56
confidence 4.67 1.77 5.27 1.71
Player_score 3.91 2.85 0.31 0.53
total_stuns 7.86 6.93 6.17 4.73
total_aggression 10.32 7.68 9.84 6.39
intent_aggression 4.35 1.56 3.83 1.56
Player_raids_hut 2.46 2.23 3.67 3.27
Player_produces_fruit 3.95 2.09 3.68 2.30
difficulty 4.04 1.93 6.40 0.91
positive 3.32 1.20 2.07 0.94
play_again 3.90 2.12 2.28 1.69

4.1 Trait Distributions

df %>%
  dplyr::select(gp, sadism, selfesteem, narcissism) %>%
  pivot_longer(everything(), names_to = "Trait", values_to = "Score") %>%
  mutate(Trait = recode(Trait,
                        "gp"          = "Guilt Proneness (1–5)",
                        "sadism"      = "Everyday Sadism (1–7)",
                        "selfesteem"  = "Self-Esteem (1–7)",
                        "narcissism"  = "Narcissism (1–7)")) %>%
  ggplot(aes(x = as.numeric(Score))) +
  geom_histogram(bins = 20, fill = "#5B8DB8", color = "white") +
  facet_wrap(~Trait, scales = "free") +
  labs(title = "Distribution of Traits", x = "Score", y = "Count") +
  theme_minimal()

mean(df$gp)
## [1] 3.99187
sd(df$gp)
## [1] 0.7649029

5. Manipulation Checks

5.1 Perceived Difficulty

t_diff <- t.test(as.numeric(difficulty) ~ cond, data = df)
print(t_diff)
## 
##  Welch Two Sample t-test
## 
## data:  as.numeric(difficulty) by cond
## t = -12.482, df = 186.34, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
##  -2.736412 -1.989488
## sample estimates:
## mean in group easy mean in group hard 
##           4.038760           6.401709
df %>%
  group_by(cond) %>%
  summarise(M  = mean(as.numeric(difficulty), na.rm = TRUE),
            SE = sd(as.numeric(difficulty),   na.rm = TRUE) / sqrt(n())) %>%
  ggplot(aes(x = cond, y = M, fill = cond)) +
  geom_col(width = 0.5, show.legend = FALSE) +
  geom_errorbar(aes(ymin = M - SE, ymax = M + SE), width = 0.15) +
  scale_fill_manual(values = c("easy" = "#5B8DB8", "hard" = "#E07B54")) +
  scale_y_continuous(limits = c(0, 7)) +
  labs(title = "Perceived Difficulty by Condition",
       x = "Condition", y = "Mean Difficulty (1–7)") +
  theme_minimal()

5.2 Player Score

t_score <- t.test(as.numeric(Player_score) ~ cond, data = df)
print(t_score)
## 
##  Welch Two Sample t-test
## 
## data:  as.numeric(Player_score) by cond
## t = 14.067, df = 137.82, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
##  3.093368 4.105201
## sample estimates:
## mean in group easy mean in group hard 
##          3.9069767          0.3076923
df %>%
  group_by(cond) %>%
  summarise(M  = mean(as.numeric(Player_score), na.rm = TRUE),
            SE = sd(as.numeric(Player_score),   na.rm = TRUE) / sqrt(n())) %>%
  ggplot(aes(x = cond, y = M, fill = cond)) +
  geom_col(width = 0.5, show.legend = FALSE) +
  geom_errorbar(aes(ymin = M - SE, ymax = M + SE), width = 0.15) +
  scale_fill_manual(values = c("easy" = "#5B8DB8", "hard" = "#E07B54")) +
  labs(title = "Player Score by Condition",
       x = "Condition", y = "Mean Score") +
  theme_minimal()


6. Distribution of Aggression

summary(df$total_aggression)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    0.00    5.00    9.00   10.09   14.00   49.00
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()

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()

cat("Proportion of zeros:", round(mean(df$total_aggression == 0, na.rm = TRUE), 3), "\n")
## Proportion of zeros: 0.065
cat("Max:", max(df$total_aggression, na.rm = TRUE), "\n")
## Max: 49
cat("Mean:", round(mean(df$total_aggression, na.rm = TRUE), 2), "\n")
## Mean: 10.09
cat("Variance/Mean ratio:", round(var(df$total_aggression, na.rm = TRUE) /
                                   mean(df$total_aggression, na.rm = TRUE), 2),
    "(>1 = overdispersion)\n")
## Variance/Mean ratio: 4.98 (>1 = overdispersion)
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()


7. Correlations

7.1 Overall Correlation Matrix

cor_vars <- df %>%
  dplyr::select(
    moral_outrage, surprised, disappointed, felt_guilt,
    gp, sadism, selfesteem, narcissism,
    tipi_agreeableness, tipi_openness,
    people_or_bot, confidence,
    Player_score, total_stuns, Player_raids_hut,
    Player_produces_fruit, intent_aggression,
    difficulty, positive, play_again
  ) %>%
  mutate(across(everything(), as.numeric))

names(cor_vars) <- c(
  "Moral Outrage", "Surprised", "Disappointed", "Felt Guilt",
  "Guilt Proneness", "Sadism", "Self-Esteem", "Narcissism",
  "Agreeableness", "Openness",
  "People/Bot", "Confidence",
  "Score", "Stuns", "Raids", "Produces Fruit", "Intent Aggress",
  "Difficulty", "Positive", "Play Again"
)

cor(cor_vars, use = "pairwise.complete.obs") %>%
  round(2) %>%
  kable(caption = "Pairwise correlations among key variables") %>%
  kable_styling(bootstrap_options = c("striped", "condensed"),
                font_size = 10, full_width = TRUE) %>%
  scroll_box(width = "100%")
Pairwise correlations among key variables
Moral Outrage Surprised Disappointed Felt Guilt Guilt Proneness Sadism Self-Esteem Narcissism Agreeableness Openness People/Bot Confidence Score Stuns Raids Produces Fruit Intent Aggress Difficulty Positive Play Again
Moral Outrage 1.00 0.56 0.80 0.09 0.07 0.03 -0.03 0.16 0.03 -0.09 0.07 0.14 -0.26 -0.10 0.02 0.09 -0.07 0.30 -0.47 -0.35
Surprised 0.56 1.00 0.63 0.09 0.15 0.01 0.05 0.10 0.09 -0.03 -0.07 0.00 -0.15 -0.11 0.00 0.09 -0.10 0.22 -0.24 -0.10
Disappointed 0.80 0.63 1.00 0.07 0.07 0.02 -0.05 0.14 0.03 -0.07 0.04 0.08 -0.30 -0.12 0.08 0.08 -0.12 0.36 -0.49 -0.34
Felt Guilt 0.09 0.09 0.07 1.00 0.11 0.09 -0.06 0.17 -0.03 -0.03 0.08 -0.02 0.11 0.11 -0.08 -0.05 0.01 -0.09 -0.04 0.02
Guilt Proneness 0.07 0.15 0.07 0.11 1.00 -0.31 0.16 -0.07 0.35 0.03 0.00 0.08 -0.05 -0.09 -0.03 -0.04 -0.07 0.09 0.01 0.01
Sadism 0.03 0.01 0.02 0.09 -0.31 1.00 -0.10 0.43 -0.49 -0.11 0.00 0.05 -0.01 0.01 -0.07 0.11 0.04 -0.02 0.00 -0.02
Self-Esteem -0.03 0.05 -0.05 -0.06 0.16 -0.10 1.00 -0.12 0.28 0.16 0.00 0.06 0.04 -0.02 0.04 -0.11 0.06 -0.17 0.16 0.07
Narcissism 0.16 0.10 0.14 0.17 -0.07 0.43 -0.12 1.00 -0.25 -0.22 0.11 0.08 -0.02 -0.02 -0.07 0.05 0.05 0.07 -0.14 -0.06
Agreeableness 0.03 0.09 0.03 -0.03 0.35 -0.49 0.28 -0.25 1.00 0.20 -0.12 0.03 -0.08 -0.04 0.12 -0.14 0.06 0.00 0.04 -0.01
Openness -0.09 -0.03 -0.07 -0.03 0.03 -0.11 0.16 -0.22 0.20 1.00 0.01 0.03 0.10 0.05 -0.03 0.01 0.11 -0.10 0.15 0.12
People/Bot 0.07 -0.07 0.04 0.08 0.00 0.00 0.00 0.11 -0.12 0.01 1.00 -0.10 -0.06 -0.03 -0.08 0.03 -0.05 0.09 -0.16 -0.19
Confidence 0.14 0.00 0.08 -0.02 0.08 0.05 0.06 0.08 0.03 0.03 -0.10 1.00 -0.10 -0.12 -0.05 0.02 -0.08 0.10 -0.13 -0.15
Score -0.26 -0.15 -0.30 0.11 -0.05 -0.01 0.04 -0.02 -0.08 0.10 -0.06 -0.10 1.00 0.21 -0.01 0.40 0.32 -0.69 0.56 0.47
Stuns -0.10 -0.11 -0.12 0.11 -0.09 0.01 -0.02 -0.02 -0.04 0.05 -0.03 -0.12 0.21 1.00 0.17 -0.14 0.39 -0.26 0.24 0.17
Raids 0.02 0.00 0.08 -0.08 -0.03 -0.07 0.04 -0.07 0.12 -0.03 -0.08 -0.05 -0.01 0.17 1.00 -0.28 0.30 0.05 0.05 0.06
Produces Fruit 0.09 0.09 0.08 -0.05 -0.04 0.11 -0.11 0.05 -0.14 0.01 0.03 0.02 0.40 -0.14 -0.28 1.00 -0.16 -0.11 -0.01 0.03
Intent Aggress -0.07 -0.10 -0.12 0.01 -0.07 0.04 0.06 0.05 0.06 0.11 -0.05 -0.08 0.32 0.39 0.30 -0.16 1.00 -0.29 0.36 0.31
Difficulty 0.30 0.22 0.36 -0.09 0.09 -0.02 -0.17 0.07 0.00 -0.10 0.09 0.10 -0.69 -0.26 0.05 -0.11 -0.29 1.00 -0.66 -0.52
Positive -0.47 -0.24 -0.49 -0.04 0.01 0.00 0.16 -0.14 0.04 0.15 -0.16 -0.13 0.56 0.24 0.05 -0.01 0.36 -0.66 1.00 0.78
Play Again -0.35 -0.10 -0.34 0.02 0.01 -0.02 0.07 -0.06 -0.01 0.12 -0.19 -0.15 0.47 0.17 0.06 0.03 0.31 -0.52 0.78 1.00

7.2 Correlations — Easy Mode Only

cor_easy <- df %>%
  filter(cond == "easy") %>%
  dplyr::select(names(df)[names(df) %in% c(
    "moral_outrage", "surprised", "disappointed", "felt_guilt",
    "gp", "sadism", "selfesteem", "narcissism",
    "tipi_agreeableness", "tipi_openness",
    "people_or_bot", "confidence",
    "Player_score", "total_stuns", "Player_raids_hut",
    "Player_produces_fruit", "intent_aggression",
    "difficulty", "positive", "play_again"
  )]) %>%
  mutate(across(everything(), as.numeric))

names(cor_easy) <- names(cor_vars)

cor(cor_easy, use = "pairwise.complete.obs") %>%
  round(2) %>%
  kable(caption = "Pairwise correlations — Easy mode only") %>%
  kable_styling(bootstrap_options = c("striped", "condensed"),
                font_size = 10, full_width = TRUE) %>%
  scroll_box(width = "100%")
Pairwise correlations — Easy mode only
Moral Outrage Surprised Disappointed Felt Guilt Guilt Proneness Sadism Self-Esteem Narcissism Agreeableness Openness People/Bot Confidence Score Stuns Raids Produces Fruit Intent Aggress Difficulty Positive Play Again
Moral Outrage 1.00 0.19 0.27 0.06 -0.15 -0.06 -0.55 -0.42 0.08 -0.04 -0.54 -0.21 -0.25 0.01 -0.06 0.14 -0.10 0.22 -0.33 -0.27
Surprised 0.19 1.00 0.69 0.22 -0.04 0.09 -0.29 -0.16 -0.07 -0.02 -0.10 -0.01 -0.05 0.11 -0.10 0.16 -0.03 0.60 -0.11 -0.12
Disappointed 0.27 0.69 1.00 0.22 -0.07 0.01 -0.48 -0.31 -0.02 0.03 -0.17 0.08 -0.08 0.04 -0.10 0.08 -0.09 0.84 -0.15 -0.10
Felt Guilt 0.06 0.22 0.22 1.00 -0.07 0.36 -0.20 -0.14 0.09 0.12 0.01 0.01 -0.07 -0.02 -0.10 0.07 0.06 0.20 -0.05 0.14
Guilt Proneness -0.15 -0.04 -0.07 -0.07 1.00 -0.08 0.09 0.09 -0.07 0.12 -0.06 -0.01 -0.08 0.31 0.15 0.22 -0.10 -0.05 -0.04 -0.13
Sadism -0.06 0.09 0.01 0.36 -0.08 1.00 0.01 0.04 0.16 -0.09 0.19 0.01 0.07 -0.27 -0.22 -0.03 0.37 -0.03 -0.01 0.19
Self-Esteem -0.55 -0.29 -0.48 -0.20 0.09 0.01 1.00 0.80 -0.15 0.04 0.41 0.23 0.14 0.08 0.07 0.01 0.11 -0.46 0.46 0.23
Narcissism -0.42 -0.16 -0.31 -0.14 0.09 0.04 0.80 1.00 -0.15 -0.04 0.37 0.23 0.14 0.04 0.04 -0.01 -0.02 -0.34 0.37 0.19
Agreeableness 0.08 -0.07 -0.02 0.09 -0.07 0.16 -0.15 -0.15 1.00 -0.23 -0.03 -0.04 0.03 -0.19 -0.08 0.00 -0.11 0.03 -0.03 -0.03
Openness -0.04 -0.02 0.03 0.12 0.12 -0.09 0.04 -0.04 -0.23 1.00 0.02 0.07 -0.03 0.11 0.11 0.17 0.03 0.03 0.01 0.00
People/Bot -0.54 -0.10 -0.17 0.01 -0.06 0.19 0.41 0.37 -0.03 0.02 1.00 0.31 0.69 -0.13 0.06 -0.10 0.13 -0.19 0.40 0.17
Confidence -0.21 -0.01 0.08 0.01 -0.01 0.01 0.23 0.23 -0.04 0.07 0.31 1.00 -0.09 0.15 -0.12 -0.03 0.01 0.08 0.39 0.19
Score -0.25 -0.05 -0.08 -0.07 -0.08 0.07 0.14 0.14 0.03 -0.03 0.69 -0.09 1.00 -0.14 0.10 -0.10 0.12 -0.11 0.04 -0.09
Stuns 0.01 0.11 0.04 -0.02 0.31 -0.27 0.08 0.04 -0.19 0.11 -0.13 0.15 -0.14 1.00 0.30 0.35 -0.44 0.06 0.04 -0.08
Raids -0.06 -0.10 -0.10 -0.10 0.15 -0.22 0.07 0.04 -0.08 0.11 0.06 -0.12 0.10 0.30 1.00 0.02 -0.16 -0.10 0.07 -0.06
Produces Fruit 0.14 0.16 0.08 0.07 0.22 -0.03 0.01 -0.01 0.00 0.17 -0.10 -0.03 -0.10 0.35 0.02 1.00 -0.29 0.11 -0.05 -0.12
Intent Aggress -0.10 -0.03 -0.09 0.06 -0.10 0.37 0.11 -0.02 -0.11 0.03 0.13 0.01 0.12 -0.44 -0.16 -0.29 1.00 -0.09 0.00 0.04
Difficulty 0.22 0.60 0.84 0.20 -0.05 -0.03 -0.46 -0.34 0.03 0.03 -0.19 0.08 -0.11 0.06 -0.10 0.11 -0.09 1.00 -0.08 -0.05
Positive -0.33 -0.11 -0.15 -0.05 -0.04 -0.01 0.46 0.37 -0.03 0.01 0.40 0.39 0.04 0.04 0.07 -0.05 0.00 -0.08 1.00 0.40
Play Again -0.27 -0.12 -0.10 0.14 -0.13 0.19 0.23 0.19 -0.03 0.00 0.17 0.19 -0.09 -0.08 -0.06 -0.12 0.04 -0.05 0.40 1.00

7.3 Correlations — Hard Mode Only

cor_hard <- df %>%
  filter(cond == "hard") %>%
  dplyr::select(names(df)[names(df) %in% c(
    "moral_outrage", "surprised", "disappointed", "felt_guilt",
    "gp", "sadism", "selfesteem", "narcissism",
    "tipi_agreeableness", "tipi_openness",
    "people_or_bot", "confidence",
    "Player_score", "total_stuns", "Player_raids_hut",
    "Player_produces_fruit", "intent_aggression",
    "difficulty", "positive", "play_again"
  )]) %>%
  mutate(across(everything(), as.numeric))

names(cor_hard) <- names(cor_vars)

cor(cor_hard, use = "pairwise.complete.obs") %>%
  round(2) %>%
  kable(caption = "Pairwise correlations — Hard mode only") %>%
  kable_styling(bootstrap_options = c("striped", "condensed"),
                font_size = 10, full_width = TRUE) %>%
  scroll_box(width = "100%")
Pairwise correlations — Hard mode only
Moral Outrage Surprised Disappointed Felt Guilt Guilt Proneness Sadism Self-Esteem Narcissism Agreeableness Openness People/Bot Confidence Score Stuns Raids Produces Fruit Intent Aggress Difficulty Positive Play Again
Moral Outrage 1.00 0.15 0.15 -0.12 -0.16 -0.02 -0.46 -0.35 0.05 0.08 0.03 0.03 0.21 -0.02 0.00 0.10 -0.15 0.21 -0.08 -0.09
Surprised 0.15 1.00 0.56 -0.02 0.17 0.08 -0.08 0.09 -0.09 -0.02 -0.04 -0.05 0.23 0.07 0.06 0.14 0.02 0.49 -0.05 -0.06
Disappointed 0.15 0.56 1.00 0.01 0.03 0.16 -0.32 -0.19 0.08 0.04 -0.11 -0.04 0.28 0.03 0.01 0.08 0.06 0.73 0.01 -0.05
Felt Guilt -0.12 -0.02 0.01 1.00 -0.10 0.10 -0.04 0.12 0.10 -0.14 -0.18 -0.09 -0.05 -0.05 0.01 0.17 0.19 0.05 0.03 0.00
Guilt Proneness -0.16 0.17 0.03 -0.10 1.00 -0.13 0.20 -0.05 0.09 0.04 0.06 0.12 -0.15 0.24 0.16 0.07 -0.09 0.03 0.13 0.12
Sadism -0.02 0.08 0.16 0.10 -0.13 1.00 -0.15 -0.03 0.07 0.15 0.05 -0.15 0.06 -0.26 -0.21 -0.10 0.46 0.23 0.15 -0.18
Self-Esteem -0.46 -0.08 -0.32 -0.04 0.20 -0.15 1.00 0.61 -0.19 -0.18 0.29 0.18 -0.28 0.01 0.15 0.01 0.01 -0.39 0.14 0.16
Narcissism -0.35 0.09 -0.19 0.12 -0.05 -0.03 0.61 1.00 -0.23 -0.17 0.24 0.11 -0.16 -0.09 0.13 0.05 0.08 -0.24 0.15 0.00
Agreeableness 0.05 -0.09 0.08 0.10 0.09 0.07 -0.19 -0.23 1.00 0.04 -0.13 -0.15 0.04 -0.04 0.11 0.02 0.11 0.09 -0.06 -0.03
Openness 0.08 -0.02 0.04 -0.14 0.04 0.15 -0.18 -0.17 0.04 1.00 0.06 -0.23 0.10 -0.05 -0.01 -0.04 0.04 0.19 -0.13 -0.26
People/Bot 0.03 -0.04 -0.11 -0.18 0.06 0.05 0.29 0.24 -0.13 0.06 1.00 -0.01 0.03 -0.16 -0.02 0.02 0.02 -0.14 0.06 0.22
Confidence 0.03 -0.05 -0.04 -0.09 0.12 -0.15 0.18 0.11 -0.15 -0.23 -0.01 1.00 -0.40 0.10 0.07 -0.03 -0.16 -0.11 0.32 0.25
Score 0.21 0.23 0.28 -0.05 -0.15 0.06 -0.28 -0.16 0.04 0.10 0.03 -0.40 1.00 -0.14 -0.09 0.02 0.12 0.32 -0.39 -0.24
Stuns -0.02 0.07 0.03 -0.05 0.24 -0.26 0.01 -0.09 -0.04 -0.05 -0.16 0.10 -0.14 1.00 0.11 0.35 -0.55 0.00 0.08 0.02
Raids 0.00 0.06 0.01 0.01 0.16 -0.21 0.15 0.13 0.11 -0.01 -0.02 0.07 -0.09 0.11 1.00 0.03 -0.06 -0.04 0.12 0.18
Produces Fruit 0.10 0.14 0.08 0.17 0.07 -0.10 0.01 0.05 0.02 -0.04 0.02 -0.03 0.02 0.35 0.03 1.00 -0.36 0.02 -0.09 -0.02
Intent Aggress -0.15 0.02 0.06 0.19 -0.09 0.46 0.01 0.08 0.11 0.04 0.02 -0.16 0.12 -0.55 -0.06 -0.36 1.00 0.10 0.11 0.00
Difficulty 0.21 0.49 0.73 0.05 0.03 0.23 -0.39 -0.24 0.09 0.19 -0.14 -0.11 0.32 0.00 -0.04 0.02 0.10 1.00 0.01 -0.11
Positive -0.08 -0.05 0.01 0.03 0.13 0.15 0.14 0.15 -0.06 -0.13 0.06 0.32 -0.39 0.08 0.12 -0.09 0.11 0.01 1.00 0.35
Play Again -0.09 -0.06 -0.05 0.00 0.12 -0.18 0.16 0.00 -0.03 -0.26 0.22 0.25 -0.24 0.02 0.18 -0.02 0.00 -0.11 0.35 1.00

8. Main Analysis: Condition & Emotional Responses

8.1 Does Condition Predict Moral Outrage?

m_out <- lm(moral_outrage ~ cond, data = df)
summary(m_out)
## 
## Call:
## lm(formula = moral_outrage ~ cond, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -2.4558 -1.5736 -0.5736  1.4264  4.4264 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   2.5736     0.1672  15.395  < 2e-16 ***
## condhard      0.8822     0.2424   3.639 0.000334 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.899 on 244 degrees of freedom
## Multiple R-squared:  0.05149,    Adjusted R-squared:  0.0476 
## F-statistic: 13.24 on 1 and 244 DF,  p-value: 0.0003337

People were more outraged in hard mode.

8.2 Does Condition Predict Felt Guilt?

m_guilt_cond <- lm(as.numeric(felt_guilt) ~ cond, data = df)
summary(m_guilt_cond)
## 
## Call:
## lm(formula = as.numeric(felt_guilt) ~ cond, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -0.8760 -0.8760 -0.4615  0.1240  5.5385 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   1.8760     0.1023  18.347   <2e-16 ***
## condhard     -0.4144     0.1483  -2.795   0.0056 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.161 on 244 degrees of freedom
## Multiple R-squared:  0.03103,    Adjusted R-squared:  0.02706 
## F-statistic: 7.813 on 1 and 244 DF,  p-value: 0.0056
df %>%
  group_by(cond) %>%
  summarise(M  = round(mean(as.numeric(felt_guilt), na.rm = TRUE), 2),
            SD = round(sd(as.numeric(felt_guilt),   na.rm = TRUE), 2)) %>%
  kable(col.names = c("Condition", "Felt Guilt M", "SD"),
        caption = "Felt guilt by condition") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Felt guilt by condition
Condition Felt Guilt M SD
easy 1.88 1.31
hard 1.46 0.97

People felt less guilt in hard mode.

8.3 Emotion Profile by Condition

df %>%
  mutate(across(c(moral_outrage, surprised, disappointed,
                  felt_guilt, positive), as.numeric)) %>%
  dplyr::select(cond, moral_outrage, surprised, disappointed, felt_guilt, positive) %>%
  pivot_longer(-cond, names_to = "Emotion", values_to = "Score") %>%
  mutate(Emotion = recode(Emotion,
                          "moral_outrage" = "Moral Outrage",
                          "surprised"     = "Surprised",
                          "disappointed"  = "Disappointed",
                          "felt_guilt"    = "Felt Guilt",
                          "positive"      = "Positive")) %>%
  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 = "Emotional Responses by Condition",
       subtitle = "Error bars = ±1 SE",
       x = NULL, y = "Mean (1–7)") +
  theme_minimal()


9. Guilt Proneness as Moderator

9.1 GP Distribution

ggplot(df, aes(x = gp)) +
  geom_histogram(bins = 20, fill = "#6BAE75", color = "white") +
  geom_vline(xintercept = mean(df$gp, na.rm = TRUE),
             linetype = "dashed", color = "#E07B54", linewidth = 0.8) +
  annotate("text",
           x    = mean(df$gp, na.rm = TRUE) + 0.1,
           y    = Inf, vjust = 1.5,
           label = paste0("M = ", round(mean(df$gp, na.rm = TRUE), 2)),
           color = "#E07B54", size = 3.5) +
  labs(title = "Distribution of GP-5", x = "Guilt Proneness (1–5)", y = "Count") +
  theme_minimal()

# Balance check
t.test(gp ~ cond, data = df)
## 
##  Welch Two Sample t-test
## 
## data:  gp by cond
## t = 0.1426, df = 242.49, p-value = 0.8867
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
##  -0.1772573  0.2049257
## sample estimates:
## mean in group easy mean in group hard 
##           3.998450           3.984615

9.2 GP × Condition → Behavioral Aggression (Total)

m_gp_agg <- lm(total_aggression ~ cond * gp_c, data = df)
summary(m_gp_agg)
## 
## Call:
## lm(formula = total_aggression ~ cond * gp_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -10.981  -5.261  -0.776   3.796  38.906 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    10.3251     0.6246  16.531   <2e-16 ***
## condhard       -0.4896     0.9057  -0.541    0.589    
## gp_c           -0.8478     0.5796  -1.463    0.145    
## condhard:gp_c   0.6295     0.9298   0.677    0.499    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 7.094 on 242 degrees of freedom
## Multiple R-squared:  0.01027,    Adjusted R-squared:  -0.002001 
## F-statistic: 0.8369 on 3 and 242 DF,  p-value: 0.4747
interact_plot(m_gp_agg,
              pred    = cond,
              modx    = gp_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low GP (−1 SD)", "Mean GP", "High GP (+1 SD)"),
              x.label = "Condition",
              y.label = "Total Aggression (stuns + raids)",
              main.title  = "GP × Condition on Total Aggression",
              legend.main = "Guilt Proneness") +
  theme_minimal()

9.3 GP × Condition → Stuns (Aggressive Acts Only)

m_gp_stuns <- lm(total_stuns ~ cond * gp_c, data = df)
summary(m_gp_stuns)
## 
## Call:
## lm(formula = total_stuns ~ cond * gp_c, data = df)
## 
## Residuals:
##    Min     1Q Median     3Q    Max 
## -8.481 -4.227 -0.841  3.587 39.348 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)     7.8673     0.5266  14.941   <2e-16 ***
## condhard       -1.6973     0.7635  -2.223   0.0271 *  
## gp_c           -0.7927     0.4886  -1.622   0.1060    
## condhard:gp_c   0.6871     0.7839   0.877   0.3816    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 5.98 on 242 degrees of freedom
## Multiple R-squared:  0.03029,    Adjusted R-squared:  0.01827 
## F-statistic:  2.52 on 3 and 242 DF,  p-value: 0.05864
interact_plot(m_gp_stuns,
              pred    = cond,
              modx    = gp_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low GP (−1 SD)", "Mean GP", "High GP (+1 SD)"),
              x.label = "Condition",
              y.label = "Total Stuns",
              main.title  = "GP × Condition on Stuns",
              legend.main = "Guilt Proneness") +
  theme_minimal()

9.4 GP × Condition → Raids (Stealing)

m_gp_raids <- lm(as.numeric(Player_raids_hut) ~ cond * gp_c, data = df)
summary(m_gp_raids)
## 
## Call:
## lm(formula = as.numeric(Player_raids_hut) ~ cond * gp_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -3.8705 -2.3961 -0.4501  1.5427 11.3945 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    2.45784    0.24517  10.025  < 2e-16 ***
## condhard       1.20776    0.35551   3.397 0.000796 ***
## gp_c          -0.05506    0.22751  -0.242 0.808980    
## condhard:gp_c -0.05756    0.36499  -0.158 0.874816    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 2.785 on 242 degrees of freedom
## Multiple R-squared:  0.04644,    Adjusted R-squared:  0.03462 
## F-statistic: 3.929 on 3 and 242 DF,  p-value: 0.009171
interact_plot(m_gp_raids,
              pred    = cond,
              modx    = gp_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low GP (−1 SD)", "Mean GP", "High GP (+1 SD)"),
              x.label = "Condition",
              y.label = "Raids (hut steals)",
              main.title  = "GP × Condition on Raids",
              legend.main = "Guilt Proneness") +
  theme_minimal()

9.5 GP × Condition → Subjective Intent to Aggress

m_gp_intent <- lm(intent_aggression ~ cond * gp_c, data = df)
summary(m_gp_intent)
## 
## Call:
## lm(formula = intent_aggression ~ cond * gp_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -3.4098 -1.1077 -0.1247  1.1469  3.2948 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    4.34951    0.13762  31.605  < 2e-16 ***
## condhard      -0.52191    0.19956  -2.615  0.00947 ** 
## gp_c          -0.07794    0.12771  -0.610  0.54223    
## condhard:gp_c -0.07599    0.20488  -0.371  0.71103    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.563 on 242 degrees of freedom
## Multiple R-squared:  0.03231,    Adjusted R-squared:  0.02032 
## F-statistic: 2.694 on 3 and 242 DF,  p-value: 0.04674
interact_plot(m_gp_intent,
              pred    = cond,
              modx    = gp_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low GP (−1 SD)", "Mean GP", "High GP (+1 SD)"),
              x.label = "Condition",
              y.label = "Intent to Aggress (self-report)",
              main.title  = "GP × Condition on Intent to Aggress",
              legend.main = "Guilt Proneness") +
  theme_minimal()

Damn it!!!

9.6 GP × Condition → Felt Guilt (Post-game)

m_gp_fglt <- lm(as.numeric(felt_guilt) ~ cond * gp_c, data = df)
summary(m_gp_fglt)
## 
## Call:
## lm(formula = as.numeric(felt_guilt) ~ cond * gp_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -0.9945 -0.7814 -0.4170  0.2541  5.3901 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    1.87519    0.10198  18.388  < 2e-16 ***
## condhard      -0.41190    0.14787  -2.786  0.00577 ** 
## gp_c           0.09055    0.09463   0.957  0.33958    
## condhard:gp_c  0.09391    0.15181   0.619  0.53677    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.158 on 242 degrees of freedom
## Multiple R-squared:  0.04418,    Adjusted R-squared:  0.03233 
## F-statistic: 3.729 on 3 and 242 DF,  p-value: 0.01196
interact_plot(m_gp_fglt,
              pred    = cond,
              modx    = gp_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low GP (−1 SD)", "Mean GP", "High GP (+1 SD)"),
              x.label = "Condition",
              y.label = "Felt Guilt (post-game)",
              main.title  = "GP × Condition on Felt Guilt",
              legend.main = "Guilt Proneness") +
  theme_minimal()

9.7 GP × Condition → Moral Outrage

m_gp_out <- lm(moral_outrage ~ cond * gp_c, data = df)
summary(m_gp_out)
## 
## Call:
## lm(formula = moral_outrage ~ cond * gp_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -2.5170 -1.5022 -0.4728  1.3810  5.0840 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)     2.5720     0.1673  15.372  < 2e-16 ***
## condhard        0.8843     0.2426   3.645 0.000327 ***
## gp_c            0.1936     0.1553   1.247 0.213648    
## condhard:gp_c  -0.1475     0.2491  -0.592 0.554203    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.9 on 242 degrees of freedom
## Multiple R-squared:  0.05776,    Adjusted R-squared:  0.04608 
## F-statistic: 4.945 on 3 and 242 DF,  p-value: 0.002378
interact_plot(m_gp_out,
              pred    = cond,
              modx    = gp_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low GP (−1 SD)", "Mean GP", "High GP (+1 SD)"),
              x.label = "Condition",
              y.label = "Moral Outrage",
              main.title  = "GP × Condition on Moral Outrage",
              legend.main = "Guilt Proneness") +
  theme_minimal()

9.8 Mediation: Condition → Felt Guilt → Aggression (GP as Moderator)

First, does felt guilt mediate the condition → intent aggression relationship?

df <- df %>% mutate(felt_guilt_n = as.numeric(felt_guilt))

med_guilt_agg <- mediate(
  model.m = lm(felt_guilt_n   ~ cond + gp_c, data = df),
  model.y = lm(intent_aggression ~ felt_guilt_n + cond + gp_c, data = df),
  treat    = "cond",
  mediator = "felt_guilt_n",
  boot     = TRUE,
  sims     = 500
)
summary(med_guilt_agg)
## 
## Causal Mediation Analysis 
## 
## Nonparametric Bootstrap Confidence Intervals with the Percentile Method
## 
##                  Estimate 95% CI Lower 95% CI Upper p-value  
## ACME            0.0067727   -0.0588652    0.0836163   0.804  
## ADE            -0.5284938   -1.0057321   -0.1147968   0.016 *
## Total Effect   -0.5217211   -0.9641871   -0.1131955   0.012 *
## Prop. Mediated -0.0129815   -0.2454289    0.1406753   0.808  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Sample Size Used: 246 
## 
## 
## Simulations: 500

Mediation with behavioral aggression as outcome:

med_guilt_beh <- mediate(
  model.m = lm(felt_guilt_n    ~ cond + gp_c, data = df),
  model.y = lm(total_aggression ~ felt_guilt_n + cond + gp_c, data = df),
  treat    = "cond",
  mediator = "felt_guilt_n",
  boot     = TRUE,
  sims     = 500
)
summary(med_guilt_beh)
## 
## Causal Mediation Analysis 
## 
## Nonparametric Bootstrap Confidence Intervals with the Percentile Method
## 
##                Estimate 95% CI Lower 95% CI Upper p-value
## ACME           -0.18332     -0.71970      0.24116   0.364
## ADE            -0.30781     -2.15136      1.33800   0.704
## Total Effect   -0.49113     -2.32445      1.17724   0.560
## Prop. Mediated  0.37327     -3.28735      4.17948   0.628
## 
## Sample Size Used: 246 
## 
## 
## Simulations: 500

10. People or Bot: Belief About Opponents

10.1 Overall Breakdown

df %>%
  filter(!is.na(people_or_bot_label)) %>%
  count(people_or_bot_label) %>%
  mutate(pct = paste0(round(n / sum(n) * 100, 1), "%")) %>%
  kable(col.names = c("Response", "N", "%"),
        caption = "Did participants think they were playing real people?") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Did participants think they were playing real people?
Response N %
Real people 53 21.5%
Unsure 35 14.2%
Computer/bots 158 64.2%
df %>%
  filter(!is.na(people_or_bot_label)) %>%
  count(people_or_bot_label) %>%
  mutate(pct = n / sum(n)) %>%
  ggplot(aes(x = people_or_bot_label, y = pct, fill = people_or_bot_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, 0.8)) +
  scale_fill_manual(values = c("Real people"   = "#6BAE75",
                                "Unsure"        = "#F0C274",
                                "Computer/bots" = "#5B8DB8")) +
  labs(title = "Belief About Game Opponents", x = NULL, y = "Proportion") +
  theme_minimal()

10.2 By Condition

df %>%
  filter(!is.na(people_or_bot_label)) %>%
  count(cond, people_or_bot_label) %>%
  group_by(cond) %>%
  mutate(pct = paste0(round(n / sum(n) * 100, 1), "%")) %>%
  kable(col.names = c("Condition", "Response", "N", "%"),
        caption = "Belief about opponents by condition") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Belief about opponents by condition
Condition Response N %
easy Real people 32 24.8%
easy Unsure 19 14.7%
easy Computer/bots 78 60.5%
hard Real people 21 17.9%
hard Unsure 16 13.7%
hard Computer/bots 80 68.4%
# Chi-square test: is belief independent of condition?
chisq.test(table(df$cond, df$people_or_bot_label))
## 
##  Pearson's Chi-squared test
## 
## data:  table(df$cond, df$people_or_bot_label)
## X-squared = 1.9848, df = 2, p-value = 0.3707

10.3 Belief → Felt Guilt

Hypothesis: believing you’re playing real people should increase felt guilt.

m_pob_guilt <- lm(as.numeric(felt_guilt) ~ humanness + cond, data = df)
summary(m_pob_guilt)
## 
## Call:
## lm(formula = as.numeric(felt_guilt) ~ humanness + cond, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -0.9520 -0.7157 -0.5201  0.2843  5.5980 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  1.95198    0.11748  16.615  < 2e-16 ***
## humanness   -0.11814    0.09032  -1.308  0.19212    
## condhard    -0.43188    0.14865  -2.905  0.00401 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.16 on 243 degrees of freedom
## Multiple R-squared:  0.0378, Adjusted R-squared:  0.02988 
## F-statistic: 4.773 on 2 and 243 DF,  p-value: 0.009262
# Visualize: felt guilt by belief category
df %>%
  filter(!is.na(people_or_bot_label)) %>%
  group_by(people_or_bot_label, cond) %>%
  summarise(M  = mean(as.numeric(felt_guilt), na.rm = TRUE),
            SE = sd(as.numeric(felt_guilt),   na.rm = TRUE) / sqrt(n()),
            .groups = "drop") %>%
  ggplot(aes(x = people_or_bot_label, 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 = "Felt Guilt by Opponent Belief and Condition",
       x = "Believed opponents were...", y = "Felt Guilt (1–7)") +
  theme_minimal()

10.4 Belief → Aggression

Do people who think they’re playing real people aggress less?

m_pob_agg_intent <- lm(intent_aggression       ~ humanness + cond, data = df)
m_pob_agg_behav  <- lm(total_aggression         ~ humanness + cond, data = df)

summary(m_pob_agg_intent)
## 
## Call:
## lm(formula = intent_aggression ~ humanness + cond, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -3.3823 -0.9761 -0.1293  1.2174  3.2174 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  4.28849    0.15825  27.100   <2e-16 ***
## humanness    0.09379    0.12166   0.771   0.4415    
## condhard    -0.50593    0.20023  -2.527   0.0122 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.562 on 243 degrees of freedom
## Multiple R-squared:  0.0295, Adjusted R-squared:  0.02152 
## F-statistic: 3.694 on 2 and 243 DF,  p-value: 0.02629
summary(m_pob_agg_behav)
## 
## Call:
## lm(formula = total_aggression ~ humanness + cond, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -11.462  -4.775  -0.775   3.698  39.225 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   9.7751     0.7170  13.633   <2e-16 ***
## humanness     0.8435     0.5513   1.530    0.127    
## condhard     -0.3557     0.9072  -0.392    0.695    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 7.078 on 243 degrees of freedom
## Multiple R-squared:  0.01068,    Adjusted R-squared:  0.002539 
## F-statistic: 1.312 on 2 and 243 DF,  p-value: 0.2712
# Visualize behavioral aggression by belief
df %>%
  filter(!is.na(people_or_bot_label)) %>%
  group_by(people_or_bot_label) %>%
  summarise(M  = mean(total_aggression, na.rm = TRUE),
            SE = sd(total_aggression,   na.rm = TRUE) / sqrt(n()),
            .groups = "drop") %>%
  ggplot(aes(x = people_or_bot_label, y = M, fill = people_or_bot_label)) +
  geom_col(width = 0.5, show.legend = FALSE) +
  geom_errorbar(aes(ymin = M - SE, ymax = M + SE), width = 0.15) +
  scale_fill_manual(values = c("Real people"   = "#6BAE75",
                                "Unsure"        = "#F0C274",
                                "Computer/bots" = "#5B8DB8")) +
  labs(title = "Total Aggression by Belief About Opponents",
       x = "Believed opponents were...", y = "Total Aggression") +
  theme_minimal()

10.5 Mediation: Belief → Felt Guilt → Aggression

Hypothesis: believing you’re playing real people reduces aggression via increased guilt.

df_pob <- df %>% filter(!is.na(humanness))

med_pob_intent <- mediate(
  model.m = lm(felt_guilt_n    ~ humanness + cond, data = df_pob),
  model.y = lm(intent_aggression ~ felt_guilt_n + humanness + cond, data = df_pob),
  treat    = "humanness",
  mediator = "felt_guilt_n",
  boot     = TRUE,
  sims     = 500
)
summary(med_pob_intent)
## 
## Causal Mediation Analysis 
## 
## Nonparametric Bootstrap Confidence Intervals with the Percentile Method
## 
##                  Estimate 95% CI Lower 95% CI Upper p-value
## ACME            0.0024804   -0.0252343    0.0282911   0.868
## ADE             0.0913111   -0.1192316    0.3156334   0.444
## Total Effect    0.0937915   -0.1211722    0.3097212   0.428
## Prop. Mediated  0.0264463   -0.8302523    0.7844726   0.904
## 
## Sample Size Used: 246 
## 
## 
## Simulations: 500
med_pob_behav <- mediate(
  model.m = lm(felt_guilt_n    ~ humanness+ cond, data = df_pob),
  model.y = lm(total_aggression ~ felt_guilt_n + humanness + cond, data = df_pob),
  treat    = "humanness",
  mediator = "felt_guilt_n",
  boot     = TRUE,
  sims     = 500
)
summary(med_pob_behav)
## 
## Causal Mediation Analysis 
## 
## Nonparametric Bootstrap Confidence Intervals with the Percentile Method
## 
##                 Estimate 95% CI Lower 95% CI Upper p-value  
## ACME           -0.051436    -0.269468     0.060586   0.352  
## ADE             0.894890    -0.051596     1.911735   0.064 .
## Total Effect    0.843454    -0.120246     1.831118   0.080 .
## Prop. Mediated -0.060983    -0.831093     0.193431   0.424  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Sample Size Used: 246 
## 
## 
## Simulations: 500

10.6 GP Moderates Belief → Felt Guilt

High GP individuals may be especially sensitive to believing they hurt real people.

m_pob_gp <- lm(as.numeric(felt_guilt) ~ humanness * gp_c + cond, data = df)
summary(m_pob_gp)
## 
## Call:
## lm(formula = as.numeric(felt_guilt) ~ humanness * gp_c + cond, 
##     data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -1.2120 -0.7015 -0.3660  0.2930  5.5344 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)     1.93504    0.11705  16.531  < 2e-16 ***
## humanness      -0.11534    0.08978  -1.285  0.20010    
## gp_c            0.21012    0.09250   2.272  0.02400 *  
## condhard       -0.40648    0.14842  -2.739  0.00663 ** 
## humanness:gp_c -0.14420    0.09415  -1.532  0.12694    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.152 on 241 degrees of freedom
## Multiple R-squared:  0.05812,    Adjusted R-squared:  0.04249 
## F-statistic: 3.718 on 4 and 241 DF,  p-value: 0.005886
interact_plot(m_pob_gp,
              pred    = humanness,
              modx    = gp_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low GP (−1 SD)", "Mean GP", "High GP (+1 SD)"),
              x.label = "Humanness (0=Bot, 1=Unsure, 2=Real)",
              y.label = "Felt Guilt",
              main.title  = "GP Moderates Belief → Felt Guilt",
              legend.main = "Guilt Proneness") +
  theme_minimal()

10.7 Including confience in their judgement

# Confidence in people-or-bot judgment
df %>%
  filter(!is.na(people_or_bot_label)) %>%
  group_by(people_or_bot_label) %>%
  summarise(
    N             = n(),
    Confidence_M  = round(mean(as.numeric(confidence), na.rm = TRUE), 2),
    Confidence_SD = round(sd(as.numeric(confidence),   na.rm = TRUE), 2)
  ) %>%
  kable(col.names = c("Belief", "N", "Confidence M", "Confidence SD"),
        caption = "Confidence in opponent belief by response category (1–7)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Confidence in opponent belief by response category (1–7)
Belief N Confidence M Confidence SD
Real people 53 4.40 1.68
Unsure 35 3.37 1.94
Computer/bots 158 5.50 1.46
# Visualize confidence by belief category
df %>%
  filter(!is.na(people_or_bot_label)) %>%
  group_by(people_or_bot_label) %>%
  summarise(M  = mean(as.numeric(confidence), na.rm = TRUE),
            SE = sd(as.numeric(confidence),   na.rm = TRUE) / sqrt(n())) %>%
  ggplot(aes(x = people_or_bot_label, y = M, fill = people_or_bot_label)) +
  geom_col(width = 0.5, show.legend = FALSE) +
  geom_errorbar(aes(ymin = M - SE, ymax = M + SE), width = 0.15) +
  scale_fill_manual(values = c("Real people"   = "#6BAE75",
                                "Unsure"        = "#F0C274",
                                "Computer/bots" = "#5B8DB8")) +
  scale_y_continuous(limits = c(0, 7)) +
  labs(title = "Confidence in Opponent Belief by Category",
       subtitle = "Higher = more certain in their judgment",
       x = NULL, y = "Mean Confidence (1–7)") +
  theme_minimal()

# Create combined belief x confidence variable
# Ranges from -7 (very confident bots) to +7 (very confident real people)
# Unsure anchored at 0 regardless of confidence
df <- df %>%
  mutate(
    confidence_n = as.numeric(confidence),
    conf_c       = as.numeric(scale(confidence_n)),
    humanness_certain = case_when(
      people_or_bot == 1 ~  confidence_n,
      people_or_bot == 2 ~ -confidence_n,
      people_or_bot == 3 ~  0,
      TRUE               ~  NA_real_
    ),
    humanness_certain_c = as.numeric(scale(humanness_certain))
  )

# Distribution of the combined variable
ggplot(df, aes(x = humanness_certain)) +
  geom_histogram(bins = 25, fill = "#6BAE75", color = "white") +
  geom_vline(xintercept = 0, linetype = "dashed", color = "#E07B54") +
  annotate("text", x = -5, y = Inf, vjust = 1.5,
           label = "Confident\nit's bots", color = "#E07B54", size = 3.5) +
  annotate("text", x = 5, y = Inf, vjust = 1.5,
           label = "Confident\nreal people", color = "#6BAE75", size = 3.5) +
  labs(title = "Belief x Confidence Combined Variable",
       subtitle = "-7 = very confident bots, 0 = unsure, +7 = very confident real people",
       x = "Humanness (confidence-weighted)", y = "Count") +
  theme_minimal()

# Does humanness_certain predict felt guilt?
m_hc_guilt <- lm(felt_guilt_n ~ humanness_certain_c + cond, data = df)
summary(m_hc_guilt)
## 
## Call:
## lm(formula = felt_guilt_n ~ humanness_certain_c + cond, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -0.9405 -0.7965 -0.4726  0.2297  5.5797 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          1.88272    0.10272  18.328  < 2e-16 ***
## humanness_certain_c -0.05697    0.07484  -0.761  0.44729    
## condhard            -0.42862    0.14956  -2.866  0.00452 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.162 on 243 degrees of freedom
## Multiple R-squared:  0.03333,    Adjusted R-squared:  0.02538 
## F-statistic: 4.189 on 2 and 243 DF,  p-value: 0.01626
# Does humanness_certain predict aggression?
m_hc_intent <- lm(intent_aggression ~ humanness_certain_c + cond, data = df)
m_hc_behav  <- lm(total_aggression  ~ humanness_certain_c + cond, data = df)
summary(m_hc_intent)
## 
## Call:
## lm(formula = intent_aggression ~ humanness_certain_c + cond, 
##     data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -3.3982 -1.0593 -0.1513  1.2434  3.2627 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)           4.3365     0.1379  31.444   <2e-16 ***
## humanness_certain_c   0.1039     0.1005   1.034   0.3022    
## condhard             -0.4939     0.2008  -2.460   0.0146 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.561 on 243 degrees of freedom
## Multiple R-squared:  0.03139,    Adjusted R-squared:  0.02342 
## F-statistic: 3.938 on 2 and 243 DF,  p-value: 0.02075
summary(m_hc_behav)
## 
## Call:
## lm(formula = total_aggression ~ humanness_certain_c + cond, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -11.342  -5.129  -0.848   3.811  39.514 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          10.2308     0.6252  16.365   <2e-16 ***
## humanness_certain_c   0.7343     0.4555   1.612    0.108    
## condhard             -0.2973     0.9102  -0.327    0.744    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 7.074 on 243 degrees of freedom
## Multiple R-squared:  0.01172,    Adjusted R-squared:  0.003587 
## F-statistic: 1.441 on 2 and 243 DF,  p-value: 0.2387
# Does GP moderate the humanness_certain -> felt guilt path?
m_hc_gp <- lm(felt_guilt_n ~ humanness_certain_c * gp_c + cond, data = df)
summary(m_hc_gp)
## 
## Call:
## lm(formula = felt_guilt_n ~ humanness_certain_c * gp_c + cond, 
##     data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -1.2192 -0.7470 -0.3960  0.2514  5.5096 
## 
## Coefficients:
##                          Estimate Std. Error t value Pr(>|t|)    
## (Intercept)               1.86408    0.10289  18.118  < 2e-16 ***
## humanness_certain_c      -0.05024    0.07455  -0.674  0.50102    
## gp_c                      0.13041    0.07411   1.760  0.07971 .  
## condhard                 -0.40080    0.14976  -2.676  0.00796 ** 
## humanness_certain_c:gp_c -0.09896    0.07206  -1.373  0.17092    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.156 on 241 degrees of freedom
## Multiple R-squared:  0.05185,    Adjusted R-squared:  0.03611 
## F-statistic: 3.295 on 4 and 241 DF,  p-value: 0.01185
interact_plot(m_hc_gp,
              pred        = humanness_certain_c,
              modx        = gp_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low GP (-1 SD)", "Mean GP", "High GP (+1 SD)"),
              x.label     = "Humanness x Confidence (centered)",
              y.label     = "Felt Guilt",
              main.title  = "GP Moderates Belief Certainty -> Felt Guilt",
              legend.main = "Guilt Proneness") +
  theme_minimal()

Believing you’re playing real people doesn’t amplify guilt, even among those dispositionally prone to it — condition (who’s winning) is what drives guilt, not perception of opponents.


11. Sadism, Narcissism, and Self-Esteem

11.1 Descriptives and Intercorrelations

df %>%
  summarise(
    Sadism_M     = round(mean(sadism,      na.rm = TRUE), 2),
    Sadism_SD    = round(sd(sadism,        na.rm = TRUE), 2),
    Narciss_M    = round(mean(as.numeric(narcissism), na.rm = TRUE), 2),
    Narciss_SD   = round(sd(as.numeric(narcissism),   na.rm = TRUE), 2),
    SelfEst_M    = round(mean(as.numeric(selfesteem),  na.rm = TRUE), 2),
    SelfEst_SD   = round(sd(as.numeric(selfesteem),    na.rm = TRUE), 2),
    GP_M         = round(mean(gp,          na.rm = TRUE), 2),
    GP_SD        = round(sd(gp,            na.rm = TRUE), 2)
  ) %>%
  kable(caption = "Personality trait descriptives") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Personality trait descriptives
Sadism_M Sadism_SD Narciss_M Narciss_SD SelfEst_M SelfEst_SD GP_M GP_SD
1.63 0.86 1.59 1.08 4.45 1.66 3.99 0.76
# Intercorrelation table
trait_cors <- df %>%
  dplyr::select(gp, sadism, selfesteem, narcissism) %>%
  mutate(across(everything(), as.numeric))

names(trait_cors) <- c("GP", "Sadism", "Self-Esteem", "Narcissism")

cor(trait_cors, use = "pairwise.complete.obs") %>%
  round(2) %>%
  kable(caption = "Intercorrelations among personality traits") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Intercorrelations among personality traits
GP Sadism Self-Esteem Narcissism
GP 1.00 -0.31 0.16 -0.07
Sadism -0.31 1.00 -0.10 0.43
Self-Esteem 0.16 -0.10 1.00 -0.12
Narcissism -0.07 0.43 -0.12 1.00

11.2 Sadism × Condition → Aggression

Sadism should predict higher aggression, especially when winning (easy mode).

m_sad_agg_intent <- lm(intent_aggression  ~ cond * sadism_c, data = df)
m_sad_agg_behav  <- lm(total_aggression   ~ cond * sadism_c, data = df)
m_sad_stuns      <- lm(total_stuns        ~ cond * sadism_c, data = df)

summary(m_sad_agg_intent)
## 
## Call:
## lm(formula = intent_aggression ~ cond * sadism_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -3.3582 -1.0670 -0.1991  1.1503  3.3009 
## 
## Coefficients:
##                    Estimate Std. Error t value Pr(>|t|)    
## (Intercept)        4.349714   0.138488  31.409  < 2e-16 ***
## condhard          -0.538233   0.200739  -2.681  0.00784 ** 
## sadism_c           0.008474   0.150156   0.056  0.95504    
## condhard:sadism_c  0.145700   0.202020   0.721  0.47147    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.563 on 242 degrees of freedom
## Multiple R-squared:  0.03235,    Adjusted R-squared:  0.02035 
## F-statistic: 2.697 on 3 and 242 DF,  p-value: 0.04656
summary(m_sad_agg_behav)
## 
## Call:
## lm(formula = total_aggression ~ cond * sadism_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -10.694  -5.263  -1.137   3.891  38.894 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)        10.3528     0.6302  16.427   <2e-16 ***
## condhard           -0.4578     0.9135  -0.501    0.617    
## sadism_c            0.3386     0.6833   0.495    0.621    
## condhard:sadism_c  -0.8427     0.9193  -0.917    0.360    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 7.113 on 242 degrees of freedom
## Multiple R-squared:  0.004922,   Adjusted R-squared:  -0.007413 
## F-statistic: 0.399 on 3 and 242 DF,  p-value: 0.7538
summary(m_sad_stuns)
## 
## Call:
## lm(formula = total_stuns ~ cond * sadism_c, data = df)
## 
## Residuals:
##    Min     1Q Median     3Q    Max 
## -8.207 -4.172 -0.959  3.335 39.335 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)         7.8927     0.5325  14.823   <2e-16 ***
## condhard           -1.7216     0.7718  -2.231   0.0266 *  
## sadism_c            0.3121     0.5773   0.541   0.5892    
## condhard:sadism_c  -0.3139     0.7767  -0.404   0.6865    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 6.009 on 242 degrees of freedom
## Multiple R-squared:  0.02081,    Adjusted R-squared:  0.008667 
## F-statistic: 1.714 on 3 and 242 DF,  p-value: 0.1647
interact_plot(m_sad_agg_intent,
              pred    = cond,
              modx    = sadism_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low Sadism (−1 SD)", "Mean Sadism", "High Sadism (+1 SD)"),
              x.label = "Condition",
              y.label = "Intent to Aggress",
              main.title  = "Sadism × Condition on Intent to Aggress",
              legend.main = "Everyday Sadism") +
  theme_minimal()

interact_plot(m_sad_stuns,
              pred    = cond,
              modx    = sadism_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low Sadism (−1 SD)", "Mean Sadism", "High Sadism (+1 SD)"),
              x.label = "Condition",
              y.label = "Total Stuns",
              main.title  = "Sadism × Condition on Stuns",
              legend.main = "Everyday Sadism") +
  theme_minimal()

11.3 Sadism × Condition → Felt Guilt

Sadists may feel less guilt after aggressing, especially against perceived real opponents.

m_sad_guilt <- lm(as.numeric(felt_guilt) ~ cond * sadism_c, data = df)
summary(m_sad_guilt)
## 
## Call:
## lm(formula = as.numeric(felt_guilt) ~ cond * sadism_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -1.2790 -0.8257 -0.3436  0.1743  4.9968 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)        1.88428    0.10261  18.363  < 2e-16 ***
## condhard          -0.44200    0.14874  -2.972  0.00326 ** 
## sadism_c           0.08037    0.11126   0.722  0.47079    
## condhard:sadism_c  0.08857    0.14969   0.592  0.55461    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.158 on 242 degrees of freedom
## Multiple R-squared:  0.04433,    Adjusted R-squared:  0.03248 
## F-statistic: 3.741 on 3 and 242 DF,  p-value: 0.01176
interact_plot(m_sad_guilt,
              pred    = cond,
              modx    = sadism_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low Sadism (−1 SD)", "Mean Sadism", "High Sadism (+1 SD)"),
              x.label = "Condition",
              y.label = "Felt Guilt",
              main.title  = "Sadism × Condition on Felt Guilt",
              legend.main = "Everyday Sadism") +
  theme_minimal()

11.4 Narcissism × Condition → Aggression

m_narc_intent <- lm(intent_aggression ~ cond * narcissism_c, data = df)
m_narc_behav  <- lm(total_aggression  ~ cond * narcissism_c, data = df)

summary(m_narc_intent)
## 
## Call:
## lm(formula = intent_aggression ~ cond * narcissism_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -3.3615 -0.8885 -0.2195  1.1677  3.2805 
## 
## Coefficients:
##                       Estimate Std. Error t value Pr(>|t|)    
## (Intercept)            4.34423    0.14005  31.019  < 2e-16 ***
## condhard              -0.52473    0.20206  -2.597  0.00998 ** 
## narcissism_c          -0.03146    0.20071  -0.157  0.87559    
## condhard:narcissism_c  0.21348    0.23209   0.920  0.35858    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.555 on 241 degrees of freedom
##   (1 observation deleted due to missingness)
## Multiple R-squared:  0.03511,    Adjusted R-squared:  0.0231 
## F-statistic: 2.923 on 3 and 241 DF,  p-value: 0.03462
summary(m_narc_behav)
## 
## Call:
## lm(formula = total_aggression ~ cond * narcissism_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -13.176  -4.681  -1.351   3.649  39.475 
## 
## Coefficients:
##                       Estimate Std. Error t value Pr(>|t|)    
## (Intercept)            10.6057     0.6294  16.851  < 2e-16 ***
## condhard               -0.5169     0.9080  -0.569  0.56975    
## narcissism_c            1.9664     0.9020   2.180  0.03022 *  
## condhard:narcissism_c  -3.0416     1.0430  -2.916  0.00388 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 6.989 on 241 degrees of freedom
##   (1 observation deleted due to missingness)
## Multiple R-squared:  0.03666,    Adjusted R-squared:  0.02467 
## F-statistic: 3.058 on 3 and 241 DF,  p-value: 0.02901
interact_plot(m_narc_intent,
              pred    = cond,
              modx    = narcissism_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low Narc. (−1 SD)", "Mean Narc.", "High Narc. (+1 SD)"),
              x.label = "Condition",
              y.label = "Intent to Aggress",
              main.title  = "Narcissism × Condition on Intent to Aggress",
              legend.main = "Narcissism") +
  theme_minimal()

11.5 Narcissism × Condition → Felt Guilt

m_narc_guilt <- lm(as.numeric(felt_guilt) ~ cond * narcissism_c, data = df)
summary(m_narc_guilt)
## 
## Call:
## lm(formula = as.numeric(felt_guilt) ~ cond * narcissism_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -2.1758 -0.5960 -0.4077  0.4040  5.4416 
## 
## Coefficients:
##                       Estimate Std. Error t value Pr(>|t|)    
## (Intercept)             1.9777     0.1003  19.727  < 2e-16 ***
## condhard               -0.5254     0.1446  -3.632 0.000343 ***
## narcissism_c            0.6948     0.1437   4.836 2.36e-06 ***
## condhard:narcissism_c  -0.6137     0.1661  -3.694 0.000273 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.113 on 241 degrees of freedom
##   (1 observation deleted due to missingness)
## Multiple R-squared:  0.1193, Adjusted R-squared:  0.1083 
## F-statistic: 10.88 on 3 and 241 DF,  p-value: 9.983e-07
interact_plot(m_narc_guilt,
              pred    = cond,
              modx    = narcissism_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low Narc. (−1 SD)", "Mean Narc.", "High Narc. (+1 SD)"),
              x.label = "Condition",
              y.label = "Felt Guilt",
              main.title  = "Narcissism × Condition on Felt Guilt",
              legend.main = "Narcissism") +
  theme_minimal()

11.6 Self-Esteem × Condition → Aggression

m_se_intent <- lm(intent_aggression ~ cond * selfesteem_c, data = df)
m_se_behav  <- lm(total_aggression  ~ cond * selfesteem_c, data = df)

summary(m_se_intent)
## 
## Call:
## lm(formula = intent_aggression ~ cond * selfesteem_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -3.4037 -1.0554 -0.1063  1.1305  3.4446 
## 
## Coefficients:
##                       Estimate Std. Error t value Pr(>|t|)    
## (Intercept)            4.35423    0.13803  31.546   <2e-16 ***
## condhard              -0.50426    0.20026  -2.518   0.0124 *  
## selfesteem_c          -0.05678    0.13712  -0.414   0.6791    
## condhard:selfesteem_c  0.25658    0.20089   1.277   0.2027    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.561 on 242 degrees of freedom
## Multiple R-squared:  0.0352, Adjusted R-squared:  0.02324 
## F-statistic: 2.943 on 3 and 242 DF,  p-value: 0.03372
summary(m_se_behav)
## 
## Call:
## lm(formula = total_aggression ~ cond * selfesteem_c, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -11.180  -5.643  -0.977   3.895  36.745 
## 
## Coefficients:
##                       Estimate Std. Error t value Pr(>|t|)    
## (Intercept)            10.4025     0.6248  16.648   <2e-16 ***
## condhard               -0.4632     0.9066  -0.511   0.6099    
## selfesteem_c           -0.8920     0.6207  -1.437   0.1520    
## condhard:selfesteem_c   1.8635     0.9094   2.049   0.0415 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 7.065 on 242 degrees of freedom
## Multiple R-squared:  0.0182, Adjusted R-squared:  0.006025 
## F-statistic: 1.495 on 3 and 242 DF,  p-value: 0.2165
interact_plot(m_se_intent,
              pred    = cond,
              modx    = selfesteem_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low SE (−1 SD)", "Mean SE", "High SE (+1 SD)"),
              x.label = "Condition",
              y.label = "Intent to Aggress",
              main.title  = "Self-Esteem × Condition on Intent to Aggress",
              legend.main = "Self-Esteem") +
  theme_minimal()

11.7 Personality Trait × Belief: Does Sadism Amplify Dehumanization Effects?

Sadists who know they’re playing bots may aggress more freely (no moral constraint); those who think they’re playing real people may actually enjoy it more.

m_sad_pob_agg <- lm(intent_aggression ~ humanness * sadism_c + cond, data = df)
summary(m_sad_pob_agg)
## 
## Call:
## lm(formula = intent_aggression ~ humanness * sadism_c + cond, 
##     data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -3.4333 -1.1697 -0.1801  1.1619  3.2336 
## 
## Coefficients:
##                    Estimate Std. Error t value Pr(>|t|)    
## (Intercept)         4.29455    0.15893  27.021   <2e-16 ***
## humanness           0.10011    0.12231   0.819    0.414    
## sadism_c            0.10080    0.12575   0.802    0.424    
## condhard           -0.52754    0.20322  -2.596    0.010 *  
## humanness:sadism_c -0.01056    0.11611  -0.091    0.928    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.566 on 241 degrees of freedom
## Multiple R-squared:  0.03302,    Adjusted R-squared:  0.01697 
## F-statistic: 2.058 on 4 and 241 DF,  p-value: 0.08705
interact_plot(m_sad_pob_agg,
              pred    = humanness,
              modx    = sadism_c,
              modx.values = c(-1, 0, 1),
              modx.labels = c("Low Sadism (−1 SD)", "Mean Sadism", "High Sadism (+1 SD)"),
              x.label = "Humanness of Opponents",
              y.label = "Intent to Aggress",
              main.title  = "Sadism × Humanness Belief on Intent to Aggress",
              legend.main = "Everyday Sadism") +
  theme_minimal()

11.8 Summary Comparison Table: Moderator Interactions on Aggression

# Collect key interaction terms across models for quick comparison
make_row <- function(mod, label, term) {
  coefs <- summary(mod)$coefficients
  if (!term %in% rownames(coefs)) return(tibble(Model = label, b = NA, p = NA))
  tibble(Model = label,
         b = round(coefs[term, "Estimate"], 3),
         p = round(coefs[term, "Pr(>|t|)"], 3))
}

bind_rows(
  make_row(m_gp_intent,     "GP × Cond → Intent Aggress",         "condhard:gp_c"),
  make_row(m_gp_agg,        "GP × Cond → Total Aggression",       "condhard:gp_c"),
  make_row(m_gp_stuns,      "GP × Cond → Stuns",                  "condhard:gp_c"),
  make_row(m_gp_raids,      "GP × Cond → Raids",                  "condhard:gp_c"),
  make_row(m_gp_fglt,       "GP × Cond → Felt Guilt",             "condhard:gp_c"),
  make_row(m_gp_out,        "GP × Cond → Moral Outrage",          "condhard:gp_c"),
  make_row(m_sad_agg_intent,"Sadism × Cond → Intent Aggress",     "condhard:sadism_c"),
  make_row(m_sad_agg_behav, "Sadism × Cond → Total Aggression",   "condhard:sadism_c"),
  make_row(m_sad_stuns,     "Sadism × Cond → Stuns",              "condhard:sadism_c"),
  make_row(m_sad_guilt,     "Sadism × Cond → Felt Guilt",         "condhard:sadism_c"),
  make_row(m_narc_intent,   "Narcissism × Cond → Intent Aggress", "condhard:narcissism_c"),
  make_row(m_narc_behav,    "Narcissism × Cond → Total Aggression","condhard:narcissism_c"),
  make_row(m_narc_guilt,    "Narcissism × Cond → Felt Guilt",     "condhard:narcissism_c"),
  make_row(m_se_intent,     "Self-Esteem × Cond → Intent Aggress","condhard:selfesteem_c"),
  make_row(m_se_behav,      "Self-Esteem × Cond → Total Aggression","condhard:selfesteem_c")
) %>%
  kable(col.names = c("Model", "β (interaction)", "p"),
        caption = "Personality moderator interaction terms across key models") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Personality moderator interaction terms across key models
Model β (interaction) p
GP × Cond → Intent Aggress -0.076 0.711
GP × Cond → Total Aggression 0.630 0.499
GP × Cond → Stuns 0.687 0.382
GP × Cond → Raids -0.058 0.875
GP × Cond → Felt Guilt 0.094 0.537
GP × Cond → Moral Outrage -0.148 0.554
Sadism × Cond → Intent Aggress 0.146 0.471
Sadism × Cond → Total Aggression -0.843 0.360
Sadism × Cond → Stuns -0.314 0.687
Sadism × Cond → Felt Guilt 0.089 0.555
Narcissism × Cond → Intent Aggress 0.213 0.359
Narcissism × Cond → Total Aggression -3.042 0.004
Narcissism × Cond → Felt Guilt -0.614 0.000
Self-Esteem × Cond → Intent Aggress 0.257 0.203
Self-Esteem × Cond → Total Aggression 1.864 0.042

12. 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(as.numeric(Player_raids_hut),  na.rm = TRUE), 2),
    Raids_SD = round(sd(as.numeric(Player_raids_hut),    na.rm = TRUE), 2),
    Fruit_M  = round(mean(as.numeric(Player_produces_fruit), na.rm = TRUE), 2),
    Fruit_SD = round(sd(as.numeric(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 7.86 6.93 2.46 2.23 3.95 2.09
hard 6.17 4.73 3.67 3.27 3.68 2.30
cat("--- Stuns ---\n");  print(t.test(total_stuns                     ~ cond, data = df))
## --- Stuns ---
## 
##  Welch Two Sample t-test
## 
## data:  total_stuns by cond
## t = 2.2498, df = 227.05, p-value = 0.02542
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
##  0.2097575 3.1692924
## sample estimates:
## mean in group easy mean in group hard 
##           7.860465           6.170940
cat("--- Raids ---\n");  print(t.test(as.numeric(Player_raids_hut)    ~ cond, data = df))
## --- Raids ---
## 
##  Welch Two Sample t-test
## 
## data:  as.numeric(Player_raids_hut) by cond
## t = -3.3536, df = 201.8, p-value = 0.0009526
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
##  -1.9203239 -0.4982808
## sample estimates:
## mean in group easy mean in group hard 
##           2.457364           3.666667
cat("--- Fruit ---\n");  print(t.test(as.numeric(Player_produces_fruit) ~ cond, data = df))
## --- Fruit ---
## 
##  Welch Two Sample t-test
## 
## data:  as.numeric(Player_produces_fruit) by cond
## t = 0.96204, df = 234.9, p-value = 0.337
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
##  -0.2834698  0.8245153
## sample estimates:
## mean in group easy mean in group hard 
##           3.945736           3.675214
# Figure: all three outcomes
df %>%
  mutate(Stuns = total_stuns,
         Raids = as.numeric(Player_raids_hut),
         Fruit = as.numeric(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")


13. Subjective Intent by Condition

intent_vars <- c("intent_steal", "intent_protect", "intent_stun",
                 "intent_others_score", "intent_produce")

df %>%
  dplyr::select(cond, all_of(intent_vars)) %>%
  mutate(across(-cond, as.numeric)) %>%
  pivot_longer(-cond, names_to = "Intent", values_to = "Score") %>%
  mutate(Intent = recode(Intent,
                         "intent_steal"        = "Steal",
                         "intent_protect"      = "Protect",
                         "intent_stun"         = "Stun",
                         "intent_others_score" = "Let score",
                         "intent_produce"      = "Produce")) %>%
  group_by(cond, Intent) %>%
  summarise(M  = mean(Score, na.rm = TRUE),
            SE = sd(Score, na.rm = TRUE) / sqrt(n()), .groups = "drop") %>%
  ggplot(aes(x = Intent, 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 = "Subjective Behavioral Intent by Condition",
       x = NULL, y = "Mean (1–7)") +
  theme_minimal()

# t-tests
cat("--- Intent Steal ---\n"); print(t.test(as.numeric(intent_steal) ~ cond, data = df))
## --- Intent Steal ---
## 
##  Welch Two Sample t-test
## 
## data:  as.numeric(intent_steal) by cond
## t = 2.1697, df = 240.96, p-value = 0.03101
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
##  0.0469693 0.9731062
## sample estimates:
## mean in group easy mean in group hard 
##           3.860465           3.350427
cat("--- Intent Stun  ---\n"); print(t.test(as.numeric(intent_stun)  ~ cond, data = df))
## --- Intent Stun  ---
## 
##  Welch Two Sample t-test
## 
## data:  as.numeric(intent_stun) by cond
## t = 2.1878, df = 241.43, p-value = 0.02964
## alternative hypothesis: true difference in means between group easy and group hard is not equal to 0
## 95 percent confidence interval:
##  0.05275598 1.00627801
## sample estimates:
## mean in group easy mean in group hard 
##           4.837209           4.307692

13.1 Intent vs. Actual Behavior

df %>%
  mutate(across(c(intent_steal, intent_stun, intent_produce), as.numeric)) %>%
  group_by(cond) %>%
  summarise(
    r_steal   = round(cor(intent_steal,   as.numeric(Player_raids_hut),       use = "complete.obs"), 2),
    r_stun    = round(cor(intent_stun,    total_stuns,                        use = "complete.obs"), 2),
    r_produce = round(cor(intent_produce, as.numeric(Player_produces_fruit),  use = "complete.obs"), 2)
  ) %>%
  kable(col.names = c("Condition",
                      "r(Intent Steal, Raids)",
                      "r(Intent Stun, Stuns)",
                      "r(Intent Produce, Fruit)"),
        caption = "Intent-behavior correlations by condition") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Intent-behavior correlations by condition
Condition r(Intent Steal, Raids) r(Intent Stun, Stuns) r(Intent Produce, Fruit)
easy 0.46 0.55 0.40
hard 0.43 0.50 0.52

14. Play Again

m_play <- lm(as.numeric(play_again) ~ cond, data = df)
summary(m_play)
## 
## Call:
## lm(formula = as.numeric(play_again) ~ cond, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -2.8992 -1.2821 -0.2821  1.1008  4.7179 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   3.8992     0.1696  22.991  < 2e-16 ***
## condhard     -1.6172     0.2459  -6.576 2.92e-10 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.926 on 244 degrees of freedom
## Multiple R-squared:  0.1505, Adjusted R-squared:  0.1471 
## F-statistic: 43.24 on 1 and 244 DF,  p-value: 2.916e-10
df %>%
  group_by(cond) %>%
  summarise(M  = round(mean(as.numeric(play_again), na.rm = TRUE), 2),
            SD = round(sd(as.numeric(play_again),   na.rm = TRUE), 2)) %>%
  kable(col.names = c("Condition", "M", "SD"),
        caption = "Desire to play again by condition (1–7)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Desire to play again by condition (1–7)
Condition M SD
easy 3.90 2.12
hard 2.28 1.69
# Does felt guilt predict not wanting to play again?
m_guilt_play <- lm(as.numeric(play_again) ~ as.numeric(felt_guilt) + cond, data = df)
summary(m_guilt_play)
## 
## Call:
## lm(formula = as.numeric(play_again) ~ as.numeric(felt_guilt) + 
##     cond, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -2.9733 -1.3211 -0.3211  1.3228  4.6789 
## 
## Coefficients:
##                        Estimate Std. Error t value Pr(>|t|)    
## (Intercept)             4.05794    0.26182  15.499  < 2e-16 ***
## as.numeric(felt_guilt) -0.08461    0.10626  -0.796    0.427    
## condhard               -1.65224    0.25002  -6.608 2.44e-10 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.928 on 243 degrees of freedom
## Multiple R-squared:  0.1528, Adjusted R-squared:  0.1458 
## F-statistic: 21.91 on 2 and 243 DF,  p-value: 1.791e-09
# Does GP predict wanting to play again?
m_gp_play <- lm(as.numeric(play_again) ~ gp_c + cond, data = df)
summary(m_gp_play)
## 
## Call:
## lm(formula = as.numeric(play_again) ~ gp_c + cond, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -2.9264 -1.2933 -0.2825  1.1509  4.7338 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  3.89905    0.16994  22.943  < 2e-16 ***
## gp_c         0.02075    0.12332   0.168    0.867    
## condhard    -1.61680    0.24643  -6.561  3.2e-10 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.93 on 243 degrees of freedom
## Multiple R-squared:  0.1506, Adjusted R-squared:  0.1437 
## F-statistic: 21.55 on 2 and 243 DF,  p-value: 2.424e-09

15. Session Info

sessionInfo()
## R version 4.6.0 (2026-04-24)
## Platform: aarch64-apple-darwin23
## Running under: macOS Ventura 13.3
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib 
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
## 
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
## 
## time zone: America/New_York
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] lubridate_1.9.5    forcats_1.0.1      stringr_1.6.0      dplyr_1.2.1       
##  [5] purrr_1.2.2        readr_2.2.0        tidyr_1.3.2        tibble_3.3.1      
##  [9] tidyverse_2.0.0    gridExtra_2.3      mediation_4.5.1    sandwich_3.1-1    
## [13] mvtnorm_1.4-1      Matrix_1.7-5       MASS_7.3-65        interactions_1.2.0
## [17] kableExtra_1.4.0   knitr_1.51         ggplot2_4.0.3      scales_1.4.0      
## [21] psych_2.6.5        qualtRics_3.2.2   
## 
## loaded via a namespace (and not attached):
##  [1] Rdpack_2.6.6        mnormt_2.1.2        rlang_1.2.0        
##  [4] magrittr_2.0.5      furrr_0.4.0         otel_0.2.0         
##  [7] compiler_4.6.0      systemfonts_1.3.2   vctrs_0.7.3        
## [10] pkgconfig_2.0.3     crayon_1.5.3        fastmap_1.2.0      
## [13] backports_1.5.1     labeling_0.4.3      pander_0.6.6       
## [16] rmarkdown_2.31      tzdb_0.5.0          nloptr_2.2.1       
## [19] bit_4.6.0           xfun_0.58           cachem_1.1.0       
## [22] jsonlite_2.0.0      broom_1.0.13        parallel_4.6.0     
## [25] cluster_2.1.8.2     R6_2.6.1            bslib_0.11.0       
## [28] stringi_1.8.7       RColorBrewer_1.1-3  parallelly_1.47.0  
## [31] boot_1.3-32         rpart_4.1.27        jquerylib_0.1.4    
## [34] Rcpp_1.1.1-1.1      zoo_1.8-15          base64enc_0.1-6    
## [37] splines_4.6.0       nnet_7.3-20         timechange_0.4.0   
## [40] tidyselect_1.2.1    rstudioapi_0.19.0   yaml_2.3.12        
## [43] codetools_0.2-20    sjlabelled_1.2.0    listenv_0.10.1     
## [46] lattice_0.22-9      withr_3.0.2         S7_0.2.2           
## [49] evaluate_1.0.5      foreign_0.8-91      future_1.70.0      
## [52] xml2_1.5.2          lpSolve_5.6.23      jtools_2.3.1       
## [55] pillar_1.11.1       checkmate_2.3.4     reformulas_0.4.4   
## [58] insight_1.5.1       generics_0.1.4      vroom_1.7.1        
## [61] hms_1.1.4           minqa_1.2.8         globals_0.19.1     
## [64] glue_1.8.1          Hmisc_5.2-6         tools_4.6.0        
## [67] data.table_1.18.4   lme4_2.0-1          grid_4.6.0         
## [70] rbibutils_2.4.1     colorspace_2.1-2    nlme_3.1-169       
## [73] htmlTable_2.5.0     Formula_1.2-5       cli_3.6.6          
## [76] textshaping_1.0.5   viridisLite_0.4.3   svglite_2.2.2      
## [79] gtable_0.3.6        broom.mixed_0.2.9.7 sass_0.4.10        
## [82] digest_0.6.39       htmlwidgets_1.6.4   farver_2.1.2       
## [85] htmltools_0.5.9     lifecycle_1.0.5     bit64_4.8.2