In this simple guessing game, using a standard deck of 52 playing cards, the goal is to correctly guess, or at least guess as close to the correct card as possible. The player first makes a preliminary guess, then is told whether the actual card is higher or lower than their guess, and makes a final guess. The player’s error is the absolute distance of their final guess’s card value and the actual card value. For instance, if a player’s final guess is 5 and the actual card is 8, the player’s error is 3.
The player’s goals are two-fold: guess the correct card, and minimize error. The following simulations will test several strategies to determine the optimal strategy for each goal. It is not necessarily the case that one stragtegy will optimize both goals.
Always Guess the Median
Guess the Median, Then Guess the Highest Probability (Random Tiebreaker)
Note: Actual probabilities are not calculated; since the probability of each remaining value equals the count of each card (variable) divided by total number of remaining cards (constant), computation time can be saved by simply using the raw counts instead of probability.
Guess the Median, Then Guess the Highest Probability (Closest to Median Tiebreaker)
For the secondary guess, the card with the highest probability is chosen.
Tiebreaker: If more than one card have the same probability (count), the tie is broken by selecting the card whose value is closest to the median of the remaining cards. If two cards are equidistant from the median, one is selected at random.
Guess the Highest Probability (Random Tiebreaker)
Guess the Highest Probability (Closest to Median Tiebreaker)
Random Guessing
Each method was designed into an algorithm that systematically progresses through an entire simulated deck of 52 cards, recording the guess error (distance from actual card) and whether turn resulted in a correct guess on either the first or second try. Each method was tested 5,000 times, and the average error and correct number of guesses were calculated for each iteration.
Note: Code for all simulations can be found in the appendix
In the Method description in the table below, Median, Prob, and Random refer to the type of guess and type of tiebreaker when outside and inside parentheses, respectively.
| Method | Average_Error | SD_Error | Average_Correct | SD_Correct | CE_Ratio |
|---|---|---|---|---|---|
| Method 1: Median, Median | 1.168 | 0.102 | 18.110 | 2.588 | 15.505 |
| Method 2a: Median, Prob (Random) | 1.424 | 0.196 | 20.794 | 2.599 | 14.603 |
| Method 2b: Median, Prob (Med) | 1.253 | 0.149 | 20.856 | 2.663 | 16.645 |
| Method 3a: Prob, Prob (Random) | 1.997 | 0.408 | 20.200 | 2.855 | 10.115 |
| Method 3b: Prob, Prob (Med) | 1.397 | 0.278 | 21.352 | 2.723 | 15.284 |
| Method 4: Random, Random | 2.158 | 0.327 | 17.458 | 3.065 | 8.090 |
# Generate 'error' and 'correct' dataframe template
set.seed(622)
rn <- rep("error", times=52)
rn <- paste0(rn, 1:52)
m1_error_results <- data.frame(t(1:52))
names(m1_error_results) <- rn
rn <- rep("correct", times=52)
rn <- paste0(rn, 1:52)
m1_correct_results <- data.frame(t(rep(FALSE, times=52)))
names(m1_correct_results) <- rn
rm(rn)
# Begin simulation
for ( i in 1:5000 ) { # Simulation iteration (1,000 trials)
deck <- rep(1:13, each=4) # create card deck
deck <- sample(deck) # randomly shuffle the deck
this_error <- integer()
this_correct <- logical()
for ( j in 1:52 ) { # Turn iteration for each game
actual <- deck[1]
med1 <- median(deck)
guess_1 <- sample(c(floor(med1), ceiling(med1)), 1)
if ( guess_1 == actual ) { # If first guess is correct
this_error[j] <- 0
this_correct[j] <- TRUE
deck <- deck[-1] # Remove 'actual' card from deck
next
} else if ( actual > guess_1 ) { # If the actual card is greater than guess_1
choices <- deck[deck > guess_1] # Subset deck into possible choices
med2 <- median(choices)
# If the median is not an integer, need to randomly choose between lower and upper value
guess_2 <- sample(c(floor(med2), ceiling(med2)), 1)
this_error[j] <- abs(guess_2 - actual) # Get absloute difference in guessing error
this_correct[j] <- guess_2 == actual # TRUE if guess_2 is correct, FALSE otherwise
deck <- deck[-1] # Remove top card from deck
next
} else if ( actual < guess_1 ) { # If the actual card is less than guess_1, same logic applies
choices <- deck[deck < guess_1]
med2 <- median(choices)
guess_2 <- sample(c(floor(med2), ceiling(med2)), 1)
this_error[j] <- abs(guess_2 - actual)
this_correct[j] <- guess_2 == actual
deck <- deck[-1]
}
}
m1_error_results <- rbind(m1_error_results, this_error)
m1_correct_results <- rbind(m1_correct_results, this_correct)
}
m1_error_results <- m1_error_results[-1, ]
m1_error_results$mean_error <- rowMeans(m1_error_results)
m1_correct_results <- m1_correct_results[-1, ]
m1_correct_results$num_correct <- rowSums(m1_correct_results)
if(!file.exists("m1_error_results")) { saveRDS(m1_error_results, "m1_error_results.rds") }
if(!file.exists("m1_correct_results")) { saveRDS(m1_correct_results, "m1_correct_results.rds") }
# Generate 'error' and 'correct' dataframe template
set.seed(892)
rn <- rep("error", times=52)
rn <- paste0(rn, 1:52)
m2a_error_results <- data.frame(t(1:52))
names(m2a_error_results) <- rn
rn <- rep("correct", times=52)
rn <- paste0(rn, 1:52)
m2a_correct_results <- data.frame(t(rep(FALSE, times=52)))
names(m2a_correct_results) <- rn
rm(rn)
# Begin simulation
for ( i in 1:5000 ) { # Simulation iteration (1,000 trials)
deck <- rep(1:13, each=4) # create card deck
deck <- sample(deck) # randomly shuffle the deck
this_error <- integer()
this_correct <- logical()
for ( j in 1:52 ) { # Turn iteration for each game
actual <- deck[1]
med1 <- median(deck)
# Choose the median, or randomly select between lower and upper bound of median
guess_1 <- sample(c(floor(med1), ceiling(med1)), 1)
if ( guess_1 == actual ) { # If first guess is correct
this_error[j] <- 0
this_correct[j] <- TRUE
deck <- deck[-1] # Remove 'actual' card from deck
next
} else if ( actual > guess_1 ) { # If the actual card is greater than guess_1
choices <- deck[deck > guess_1] # Subset deck into possible choices
df <- as.data.frame(table(choices)) # Create frequency table of possible choices
df$choices <- as.numeric(levels(df$choices))
options <- df$choices[which(df$Freq==max(df$Freq))] # Find which cards have the greatest frequency
# If there is only one option, choose it for guess_2, otherwise randomly select from options
guess_2 <- ifelse(length(options)==1, options, sample(options, 1))
this_error[j] <- abs(guess_2 - actual) # Get absloute difference in guessing error
this_correct[j] <- guess_2 == actual # TRUE if guess_2 is correct, FALSE otherwise
deck <- deck[-1]
next
} else if ( actual < guess_1 ) { # If the actual card is less than guess_1, same logic applies
choices <- deck[deck < guess_1]
df <- as.data.frame(table(choices))
df$choices <- as.numeric(levels(df$choices))
options <- df$choices[which(df$Freq==max(df$Freq))]
guess_2 <- ifelse(length(options)==1, options, sample(options, 1))
this_error[j] <- abs(guess_2 - actual)
this_correct[j] <- guess_2 == actual
deck <- deck[-1]
}
}
m2a_error_results <- rbind(m2a_error_results, this_error)
m2a_correct_results <- rbind(m2a_correct_results, this_correct)
}
m2a_error_results <- m2a_error_results[-1, ]
m2a_error_results$mean_error <- rowMeans(m2a_error_results)
m2a_correct_results <- m2a_correct_results[-1, ]
m2a_correct_results$num_correct <- rowSums(m2a_correct_results)
if(!file.exists("m2a_error_results")) { saveRDS(m2a_error_results, "m2a_error_results.rds") }
if(!file.exists("m2a_correct_results")) { saveRDS(m2a_correct_results, "m2a_correct_results.rds") }
# Generate 'error' and 'correct' dataframe template
set.seed(845)
rn <- rep("error", times=52)
rn <- paste0(rn, 1:52)
m2b_error_results <- data.frame(t(1:52))
names(m2b_error_results) <- rn
rn <- rep("correct", times=52)
rn <- paste0(rn, 1:52)
m2b_correct_results <- data.frame(t(rep(FALSE, times=52)))
names(m2b_correct_results) <- rn
rm(rn)
# Begin simulation
for ( i in 1:5000 ) { # Simulation iteration (1,000 trials)
deck <- rep(1:13, each=4) # create card deck
deck <- sample(deck) # randomly shuffle the deck
this_error <- integer()
this_correct <- logical()
for ( j in 1:52 ) { # Turn iteration for each game
actual <- deck[1]
med1 <- median(deck)
# Choose the median, or randomly select between lower and upper bound of median
guess_1 <- sample(c(floor(med1), ceiling(med1)), 1)
if ( guess_1 == actual ) { # If first guess is correct
this_error[j] <- 0
this_correct[j] <- TRUE
deck <- deck[-1] # Remove 'actual' card from deck
next
} else if ( actual > guess_1 ) { # If the actual card is greater than guess_1
choices <- deck[deck > guess_1] # Subset deck into possible choices
df <- as.data.frame(table(choices)) # Create frequency table of possible choices
df$choices <- as.numeric(levels(df$choices))
options <- df$choices[which(df$Freq==max(df$Freq))] # Find which cards have the greatest frequency
if ( length(options) == 1 ) { # If there is only one option, choose it for guess_2
guess_2 <- options
} else {
med2 <- median(choices)
# Find absolute distance of possible choices from the median of possible choices
df$dist <- abs(df$choices-med2)
# Subset choices by cards that have the greatest frequency
df <- df[which(df$Freq==max(df$Freq)),]
# Subset by cards that have minimum distance from median
options <- df$choices[which(df$dist==min(df$dist))]
# If only one option, choose it. Otherwise randomly choose between two options.
guess_2 <- ifelse(length(options)==1, options, sample(options, 1))
}
this_error[j] <- abs(guess_2 - actual) # Get absloute difference in guessing error
this_correct[j] <- guess_2 == actual # TRUE if guess_2 is correct, FALSE otherwise
deck <- deck[-1]
next
} else if ( actual < guess_1 ) { # If the actual card is less than guess_1, same logic applies
choices <- deck[deck < guess_1]
df <- as.data.frame(table(choices))
df$choices <- as.numeric(levels(df$choices))
options <- df$choices[which(df$Freq==max(df$Freq))]
if ( length(options) == 1 ) {
guess_2 <- options
} else {
med2 <- median(choices)
df$dist <- abs(df$choices-med2)
df <- df[which(df$Freq==max(df$Freq)),]
options <- df$choices[which(df$dist==min(df$dist))]
guess_2 <- ifelse(length(options)==1, options, sample(options, 1))
}
this_error[j] <- abs(guess_2 - actual) # Get absloute difference in guessing error
this_correct[j] <- guess_2 == actual # TRUE if guess_2 is correct, FALSE otherwise
deck <- deck[-1]
}
}
m2b_error_results <- rbind(m2b_error_results, this_error)
m2b_correct_results <- rbind(m2b_correct_results, this_correct)
}
m2b_error_results <- m2b_error_results[-1, ]
m2b_error_results$mean_error <- rowMeans(m2b_error_results)
m2b_correct_results <- m2b_correct_results[-1, ]
m2b_correct_results$num_correct <- rowSums(m2b_correct_results)
if(!file.exists("m2b_error_results")) { saveRDS(m2b_error_results, "m2b_error_results.rds") }
if(!file.exists("m2b_correct_results")) { saveRDS(m2b_correct_results, "m2b_correct_results.rds") }
# Generate 'error' and 'correct' dataframe template
set.seed(735)
rn <- rep("error", times=52)
rn <- paste0(rn, 1:52)
m3a_error_results <- data.frame(t(1:52))
names(m3a_error_results) <- rn
rn <- rep("correct", times=52)
rn <- paste0(rn, 1:52)
m3a_correct_results <- data.frame(t(rep(FALSE, times=52)))
names(m3a_correct_results) <- rn
rm(rn)
# Begin simulation
for ( i in 1:5000 ) { # Simulation iteration (1,000 trials)
deck <- rep(1:13, each=4) # create card deck
deck <- sample(deck) # randomly shuffle the deck
this_error <- integer()
this_correct <- logical()
#####
# guess_1 and guess_2 follow the same logic and structure as guess_2 in Method 2a
#####
for ( j in 1:52 ) { # Turn iteration for each game
actual <- deck[1]
df1 <- as.data.frame(table(deck))
df1$deck <- as.numeric(levels(df1$deck))
options <- df1$deck[which(df1$Freq==max(df1$Freq))]
guess_1 <- ifelse(length(options)==1, options, sample(options, 1))
if ( guess_1 == actual ) { # If first guess is correct
this_error[j] <- 0
this_correct[j] <- TRUE
deck <- deck[-1]
next
} else if ( actual > guess_1 ) { # If the actual card is greater than guess_1
choices <- deck[deck > guess_1]
df <- as.data.frame(table(choices))
df$choices <- as.numeric(levels(df$choices))
options <- df$choices[which(df$Freq==max(df$Freq))]
guess_2 <- ifelse(length(options)==1, options, sample(options, 1))
this_error[j] <- abs(guess_2 - actual)
this_correct[j] <- guess_2 == actual
deck <- deck[-1]
next
} else if ( actual < guess_1 ) { # If the actual card is less than guess_1, same logic applies
choices <- deck[deck < guess_1]
df <- as.data.frame(table(choices))
df$choices <- as.numeric(levels(df$choices))
options <- df$choices[which(df$Freq==max(df$Freq))]
guess_2 <- ifelse(length(options)==1, options, sample(options, 1))
this_error[j] <- abs(guess_2 - actual)
this_correct[j] <- guess_2 == actual
deck <- deck[-1]
}
}
m3a_error_results <- rbind(m3a_error_results, this_error)
m3a_correct_results <- rbind(m3a_correct_results, this_correct)
}
m3a_error_results <- m3a_error_results[-1, ]
m3a_error_results$mean_error <- rowMeans(m3a_error_results)
m3a_correct_results <- m3a_correct_results[-1, ]
m3a_correct_results$num_correct <- rowSums(m3a_correct_results)
if(!file.exists("m3a_error_results")) { saveRDS(m3a_error_results, "m3a_error_results.rds") }
if(!file.exists("m3a_correct_results")) { saveRDS(m3a_correct_results, "m3a_correct_results.rds") }
# Generate 'error' and 'correct' dataframe template
set.seed(470)
rn <- rep("error", times=52)
rn <- paste0(rn, 1:52)
m3b_error_results <- data.frame(t(1:52))
names(m3b_error_results) <- rn
rn <- rep("correct", times=52)
rn <- paste0(rn, 1:52)
m3b_correct_results <- data.frame(t(rep(FALSE, times=52)))
names(m3b_correct_results) <- rn
rm(rn)
# Begin simulation
for ( i in 1:5000 ) { # Simulation iteration (1,000 trials)
deck <- rep(1:13, each=4) # create card deck
deck <- sample(deck) # randomly shuffle the deck
this_error <- integer()
this_correct <- logical()
#####
# guess_1 and guess_2 follow the same logic and structure as guess_2 in Method 2b
#####
for ( j in 1:52 ) { # Turn iteration for each game
actual <- deck[1]
df1 <- as.data.frame(table(deck))
df1$deck <- as.numeric(levels(df1$deck))
med1 <- median(deck)
df1$dist <- abs(df1$deck-med1)
df1 <- df1[which(df1$Freq==max(df1$Freq)),]
options <- df1$deck[which(df1$dist==min(df1$dist))]
guess_1 <- ifelse(length(options)==1, options, sample(options, 1))
if ( guess_1 == actual ) { # If first guess is correct
this_error[j] <- 0
this_correct[j] <- TRUE
deck <- deck[-1]
next
} else if ( actual > guess_1 ) { # If the actual card is greater than guess_1
choices <- deck[deck > guess_1]
df2 <- as.data.frame(table(choices))
df2$choices <- as.numeric(levels(df2$choices))
options <- df2$choices[which(df2$Freq==max(df2$Freq))]
if ( length(options) == 1 ) {
guess_2 <- options
} else {
med2 <- median(choices)
df2$dist <- abs(df2$choices-med2)
df2 <- df2[which(df2$Freq==max(df2$Freq)),]
options <- df2$choices[which(df2$dist==min(df2$dist))]
guess_2 <- ifelse(length(options)==1, options, sample(options, 1))
}
this_error[j] <- abs(guess_2 - actual)
this_correct[j] <- guess_2 == actual
deck <- deck[-1]
next
} else if ( actual < guess_1 ) { # If the actual card is less than guess_1, same logic applies
choices <- deck[deck < guess_1]
df2 <- as.data.frame(table(choices))
df2$choices <- as.numeric(levels(df2$choices))
options <- df2$choices[which(df2$Freq==max(df2$Freq))]
if ( length(options) == 1 ) {
guess_2 <- options
} else {
med2 <- median(choices)
df2$dist <- abs(df2$choices-med2)
df2 <- df2[which(df2$Freq==max(df2$Freq)),]
options <- df2$choices[which(df2$dist==min(df2$dist))]
guess_2 <- ifelse(length(options)==1, options, sample(options, 1))
}
this_error[j] <- abs(guess_2 - actual)
this_correct[j] <- guess_2 == actual
deck <- deck[-1]
}
}
m3b_error_results <- rbind(m3b_error_results, this_error)
m3b_correct_results <- rbind(m3b_correct_results, this_correct)
}
m3b_error_results <- m3b_error_results[-1, ]
m3b_error_results$mean_error <- rowMeans(m3b_error_results)
m3b_correct_results <- m3b_correct_results[-1, ]
m3b_correct_results$num_correct <- rowSums(m3b_correct_results)
if(!file.exists("m3b_error_results")) { saveRDS(m3b_error_results, "m3b_error_results.rds") }
if(!file.exists("m3b_correct_results")) { saveRDS(m3b_correct_results, "m3b_correct_results.rds") }
# Generate 'error' and 'correct' dataframe template
set.seed(624)
rn <- rep("error", times=52)
rn <- paste0(rn, 1:52)
m4_error_results <- data.frame(t(1:52))
names(m4_error_results) <- rn
rn <- rep("correct", times=52)
rn <- paste0(rn, 1:52)
m4_correct_results <- data.frame(t(rep(FALSE, times=52)))
names(m4_correct_results) <- rn
rm(rn)
# Begin simulation
for ( i in 1:5000 ) { # Simulation iteration (1,000 trials)
deck <- rep(1:13, each=4) # create card deck
deck <- sample(deck) # randomly shuffle the deck
this_error <- integer()
this_correct <- logical()
for ( j in 1:52 ) { # Turn iteration for each game
actual <- deck[1]
# Random guess from remaining cards in deck
guess_1 <- ifelse(length(deck)==1, deck, sample(deck, 1))
if ( guess_1 == actual ) { # If first guess is correct
this_error[j] <- 0
this_correct[j] <- TRUE
deck <- deck[-1]
next
} else if ( actual > guess_1 ) { # If the actual card is greater than guess_1
choices <- deck[deck > guess_1]
# Random guess from remaining choices
guess_2 <- ifelse(length(choices)==1, choices, sample(choices, 1))
this_error[j] <- abs(guess_2 - actual)
this_correct[j] <- guess_2 == actual
deck <- deck[-1]
next
} else if ( actual < guess_1 ) { # If the actual card is less than guess_1, same logic applies
choices <- deck[deck < guess_1]
guess_2 <- ifelse(length(choices)==1, choices, sample(choices, 1))
this_error[j] <- abs(guess_2 - actual)
this_correct[j] <- guess_2 == actual
deck <- deck[-1]
}
}
m4_error_results <- rbind(m4_error_results, this_error)
m4_correct_results <- rbind(m4_correct_results, this_correct)
}
m4_error_results <- m4_error_results[-1, ]
m4_error_results$mean_error <- rowMeans(m4_error_results)
m4_correct_results <- m4_correct_results[-1, ]
m4_correct_results$num_correct <- rowSums(m4_correct_results)
if(!file.exists("m4_error_results")) { saveRDS(m4_error_results, "m4_error_results.rds") }
if(!file.exists("m4_correct_results")) { saveRDS(m4_correct_results, "m4_correct_results.rds") }
methods <- c("Method 1", "Method 2a", "Method 2b", "Method 3a", "Method 3b", "Method 4")
er <- list(m1_error_results, m2a_error_results, m2b_error_results, m3a_error_results,
m3b_error_results, m4_error_results)
cr <- list(m1_correct_results, m2a_correct_results, m2b_correct_results, m3a_correct_results,
m3b_correct_results, m4_correct_results)
results <- data.frame(Method = paste0(methods, ": ",
c("Median, Median", "Median, Prob (Random)",
"Median, Prob (Med)", "Prob, Prob (Random)",
"Prob, Prob (Med)", "Random, Random")),
Average_Error = sapply(er, function(x) round(mean(x$mean_error), 3)),
SD_Error = sapply(er, function(x) round(sd(x$mean_error), 3)),
Average_Correct = sapply(cr, function(x) round(mean(x$num_correct), 3)),
SD_Correct = sapply(cr, function(x) round(sd(x$num_correct), 3))
)
results$CE_Ratio <- round(results$Average_Correct / results$Average_Error, 3)
saveRDS(results, "results_5000.rds")
kable(results)
par(mfrow=c(1,2), mar=c(4,6,2,1), las=1)
boxplot(er[[6]]$mean_error, er[[5]]$mean_error, er[[4]]$mean_error,
er[[3]]$mean_error, er[[2]]$mean_error, er[[1]]$mean_error,
names = rev(methods), horizontal = TRUE,
main="Average Error", sub="(Figure 1)")
mtext("(Figure 1a)", 1, 3)
boxplot(cr[[6]]$num_correct, cr[[5]]$num_correct, cr[[4]]$num_correct,
cr[[3]]$num_correct, cr[[2]]$num_correct, cr[[1]]$num_correct,
names = rev(methods), horizontal = TRUE,
main="Average Correct Guesses", sub="(Figure 2)")
mtext("(Figure 1b)", 1, 3)
par(mfrow=c(1,2), mar=c(6,4,2,1), cex.lab=1.2)
# Plot of average change in error by turn
plot(1:52, (1:52)/(52/3), ylim = c(0,3), type = 'n', frame.plot = FALSE,
main = "Change in Average Error as Game Progresses",
ylab = "Average Error", xlab = "Number of Turns")
mtext("(Figure 2a)", 1, 5)
pchs <- c(1, 0, 15, 2, 17, 19)
for( i in 1:6 ) {
z <- colMeans(er[[i]][,1:52])
points(z, pch=pchs[i], cex=0.8)
lines(z)
}
legend(2, 1, methods, lty = rep(1, 6), pch = pchs, bty = "n")
# Plot of average number of correct guesses by turn
plot(1:52, (1:52)/52, ylim = c(0.2,1), type = 'n', frame.plot = FALSE,
main = "Varying Rates of Correct Guess Percentage by Turn",
xlab = "Number of Turns", ylab = "Percent Correct")
mtext("(Figure 2b)", 1, 5)
pchs <- c(1, 0, 15, 2, 17, 19)
for( i in 1:6 ) {
z <- colMeans(cr[[i]][,1:52])
points(z, pch=pchs[i], cex=0.8)
}
abline(h=seq(0.2, 1, 0.1), lty=2, col="gray40")
legend(2, 0.95, methods, pch = pchs, box.col = "white", bg="white")
par(mfrow=c(1,1), mar=c(6,4,4,2))
plot(1:51, (1:51)/(51/1.3), type = 'n', frame.plot = FALSE,
main = "Difference in Correct/Error Ratio by Turn",
xlab = "Number of Turns", ylab = "Correct/Error Ratio")
mtext("(Figure 2c)", 1, 5, cex = 0.8)
for(i in 1:6){
cc <- colMeans(cr[[i]])
ee <- colMeans(er[[i]])
ccrr <- cc/ee
points(ccrr, pch=pchs[i], cex=0.7)
}
legend(2, 1.1, c("Method 1", "Method 2a", "Method 2b", "Method 3a", "Method 3b", "Method 4"),
pch = pchs, bty = "n")