Interactive: Coin flip with statistics
Summary
Interactive coin flip with in-built statistics for use in the guide on introduction to probability.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 360
library(shiny)
library(bslib)
# Hard-coded hex color for the button (blue color requested)
BUTTON_COLOR <- "#3F6BB6"
ui <- page_fluid(
title = "Coin Flipper",
card(
card_header("Coin Flipper"),
card_body(
# Use a row layout for landscape orientation
layout_columns(
col_widths = c(4, 4, 4), # Equal width columns
# Left column: Description and stats
card(
card_body(
div(
style = "display: flex; flex-direction: column; height: 100%; justify-content: center;",
p("Click the button to flip a coin.", style = "font-size: 18px;"),
br(),
div(
style = "background-color: #f8f9fa; padding: 15px; border-radius: 5px;",
h5("Statistics:"),
div(
style = "display: flex; justify-content: space-between;",
div("Total flips:"),
textOutput("totalFlips", inline = TRUE)
),
div(
style = "display: flex; justify-content: space-between;",
div("Heads:"),
textOutput("headsCount", inline = TRUE)
),
div(
style = "display: flex; justify-content: space-between;",
div("Tails:"),
textOutput("tailsCount", inline = TRUE)
)
)
)
)
),
# Middle column: Coin display
card(
card_body(
div(
style = "height: 100%; display: flex; align-items: center; justify-content: center;",
div(
id = "coinDisplay",
style = "font-size: 40px; line-height: 1; width: 150px; height: 150px;
margin: 0 auto; border: 2px solid #ccc; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background-color: #f0f0f0;",
textOutput("coinResult")
)
)
)
),
# Right column: Button
card(
card_body(
div(
style = "display: flex; flex-direction: column; height: 100%;
align-items: center; justify-content: center; gap: 20px;",
actionButton("flipButton", "Flip Coin", class = "btn-lg",
style = paste0("background-color: ", BUTTON_COLOR, "; color: white;")),
actionButton("resetButton", "Reset Stats", class = "btn-sm")
)
)
)
)
)
)
)
server <- function(input, output, session) {
# Create reactive values to store the current state
flips <- reactiveValues(
current = sample(c("HEADS", "TAILS"), 1),
total = 0,
heads = 0,
tails = 0,
flipping = FALSE,
timer = NULL # Track the timer
)
# Initialize the display
output$coinResult <- renderText({
if(flips$flipping) {
return("") # Show blank state during animation
} else {
return(flips$current)
}
})
# Update the statistics displays
output$totalFlips <- renderText({
flips$total
})
output$headsCount <- renderText({
paste0(flips$heads, " (", round(ifelse(flips$total > 0, flips$heads/flips$total*100, 0), 1), "%)")
})
output$tailsCount <- renderText({
paste0(flips$tails, " (", round(ifelse(flips$total > 0, flips$tails/flips$total*100, 0), 1), "%)")
})
# Handle the coin flip
observeEvent(input$flipButton, {
# Set the flipping state to true to show blank state
flips$flipping <- TRUE
# Create a separate reactive timer that will complete the flip after delay
# This fixes the delay issue in the previous version
flips$timer <- reactiveTimer(500)
# This observer will fire when the timer triggers
observeEvent(flips$timer(), {
# Determine the result
result <- sample(c("HEADS", "TAILS"), 1)
# Update the state
flips$current <- result
flips$total <- flips$total + 1
# Update the appropriate counter
if(result == "HEADS") {
flips$heads <- flips$heads + 1
} else {
flips$tails <- flips$tails + 1
}
# End the flipping state
flips$flipping <- FALSE
}, once = TRUE) # This ensures it only fires once per button click
})
# Reset button to clear statistics
observeEvent(input$resetButton, {
flips$total <- 0
flips$heads <- 0
flips$tails <- 0
})
}
# Run the application
shinyApp(ui = ui, server = server)
Further reading
Version history
v1.0: initial version created 04/25 by Michelle Arnetta (as part of a University of St Andrews VIP project) and tdhc.