diff --git a/DESCRIPTION b/DESCRIPTION
index 6742181..17a6aa7 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -21,7 +21,7 @@ RoxygenNote: 7.3.3
Imports:
checkmate (>= 2.3.4),
shiny (>= 1.13.0)
-Suggests:
+Suggests:
knitr,
rmarkdown,
shinytest2,
diff --git a/NAMESPACE b/NAMESPACE
index 1dae0e7..fb2bd28 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -1,9 +1,11 @@
# Generated by roxygen2: do not edit by hand
+export(convert_manual_input)
export(generate_sequence)
export(get_card_states)
export(is_winner)
-export(run_app)
+export(play_googol)
+export(shuffle_manual_sequence)
export(validate_manual_sequence)
import(shiny)
importFrom(checkmate,assert_numeric)
diff --git a/R/app.R b/R/app.R
index 9437ca1..3fcdef7 100644
--- a/R/app.R
+++ b/R/app.R
@@ -1,194 +1,350 @@
-#' Run the Googol Game app
+#' Play the Googol Game
#'
#' @details Launches an interactive Shiny app for the Googol Game. The app has
#' three stages: setup (choose a random or manual sequence), game (reveal
#' cards one at a time and pick the highest), and result (win or loss message
#' with an option to play again).
#' @examples
-#' if (interactive()) run_app()
+#' if (interactive()) play_googol()
#' @import shiny
#' @export
-run_app <- function() {
+play_googol <- function() {
+ # Launch the Shiny app
+ shinyApp(ui, server)
+}
+
+# --- UI ---
+
+# The entire UI is driven by a single page switcher in the server.
+# output$page renders setup, game, or result depending on game state.
+ui <- fluidPage(
+ # Add a title: "Googol Game"
+ titlePanel(
+ div(
+ # Bold and center the title, with the Gs of Googol and
+ # Game highlighted in blue
+ HTML('Googol
+ Game'),
+ style = "text-align: center; font-weight: bold;"
+ )
+ ),
+ uiOutput("page")
+)
+
+# --- Card Helper ---
- # The entire UI is driven by a single page switcher in the server.
- # output$page renders setup, game, or result depending on game state.
- ui <- fluidPage(
- titlePanel("The Googol Game"),
- uiOutput("page")
+# Renders a styled card div based on state.
+# Hidden cards show a solid blue face-down card.
+# Passed cards are greyed out to signal they are no longer selectable.
+# The current card is highlighted in green to draw the player's attention.
+card_div <- function(state, number = "", multiplier = "") {
+ style <- switch(state,
+ hidden = "background-color: #2c7be5; color: #2c7be5;",
+ passed = "background-color: #d3d3d3; color: #888;",
+ current = "background-color: #28a745; color: white;"
)
+ # Use flexbox to stack number and multiplier vertically in the center
+ div(
+ style = paste(
+ "width: 100px; height: 140px; border-radius: 8px;
+ border: 2px solid #333; display: inline-flex;
+ flex-direction: column; align-items: center;
+ justify-content: center; vertical-align: top;
+ font-size: 16px; font-weight: bold; margin: 6px;",
+ style
+ ),
+ div(number),
+ div(multiplier)
+ )
+}
- server <- function(input, output, session) {
+# --- Server ---
- # Tracks how many number rows the player has added in manual mode.
- # Starts at 1 so there is always at least one row visible.
- n_rows <- reactiveVal(1)
+server <- function(input, output, session) {
+ # Tracks how many number rows the player has added in manual mode.
+ # Starts at 1 so there is always at least one row visible.
+ n_rows <- reactiveVal(1)
- # Holds the finalized game sequence once the player clicks Start.
- # NULL until Start is clicked, which signals the game has not yet begun.
- sequence <- reactiveVal(NULL)
+ # Holds the finalized game sequence once the player clicks Start.
+ # NULL until Start is clicked, which signals the game has not yet begun.
+ sequence <- reactiveVal(NULL)
- # Tracks which card is currently being revealed. Resets to 1 on Start.
- current_index <- reactiveVal(1)
+ # Tracks which card is currently being revealed. Starts at 0 (all hidden)
+ # and increments each time the player reveals a card.
+ current_index <- reactiveVal(0)
- # Stores the value the player picked, NULL until they pick
- picked <- reactiveVal(NULL)
+ # Stores the value the player picked, NULL until they pick
+ picked <- reactiveVal(NULL)
- # Switch between setup, game, and result pages based on game state.
- # sequence() == NULL: setup; picked() == NULL: game; otherwise: result.
- output$page <- renderUI({
- if (is.null(sequence())) {
- uiOutput("setup")
- } else if (is.null(picked())) {
- tagList(uiOutput("cards"), uiOutput("game_buttons"))
- } else {
+ # Stores display labels for each card (e.g. "3 Million", "42").
+ # Set in both random and manual mode. NULL until Start is clicked.
+ labels <- reactiveVal(NULL)
+
+ # Switch between setup, game, and result pages based on game state.
+ # If sequence() == NULL: setup; if picked() == NULL: game; otherwise: result.
+ output$page <- renderUI({
+ if (is.null(sequence())) {
+ # Setup page
+ uiOutput("setup")
+ } else if (is.null(picked())) {
+ # Game page: display cards and buttons in the middle of the page
+ div(
+ style = "display: flex; flex-direction: column; align-items: center;
+ justify-content: center; height: 80vh;",
+ uiOutput("cards"),
+ uiOutput("game_buttons")
+ )
+ } else {
+ # Result page: display win or loss message and "Play again" button in the
+ # middle of the page
+ div(
+ style = "display: flex; flex-direction: column; align-items: center;
+ justify-content: center; height: 80vh;",
+ uiOutput("cards"),
uiOutput("result")
- }
- })
+ )
+ }
+ })
- # --- Setup ---
+ # --- Setup ---
- # Append a new row each time the player clicks "Add number"
- observeEvent(input$add_row, {
- n_rows(n_rows() + 1)
- })
+ # Append a new row each time the player clicks "Add number"
+ observeEvent(input$add_row, {
+ n_rows(n_rows() + 1)
+ })
- # Render the setup page with instructions, sequence type selector, and inputs
- output$setup <- renderUI({
- tagList(
- p("Choose how to generate the sequence of numbers (i.e. Random or Manual)."),
- tags$ul(
- tags$li("Random: the app generates a sequence of random numbers for you.
- Choose how many cards you want."),
- tags$li("Manual: enter your own numbers one at a time. Use the multiplier
- dropdown to enter large numbers (e.g. 2 x Googol). You need at
- least 2 numbers.")
- ),
- # Let the player choose between a randomly generated or manually entered sequence
- radioButtons("mode", "Sequence type",
- choices = c("Random" = "random", "Manual" = "manual")
+ # Render the setup page with instructions, "Random" or "Manual" choice,
+ # and inputs
+ output$setup <- renderUI({
+ tagList(
+ p(
+ "Choose how to generate the sequence of numbers
+ (i.e. Random or Manual)."
+ ),
+ tags$ul(
+ tags$li(
+ "Random: the app generates a sequence of random numbers for you.
+ Choose how many cards you want."
),
+ tags$li(
+ "Manual: enter your own numbers one at a time. Use the multiplier
+ dropdown to enter large numbers (e.g. 2 x Googol).
+ You need at least 2 numbers."
+ )
+ ),
+
+ # Let the player choose between a randomly generated or
+ # manually entered sequence
+ radioButtons(
+ "mode",
+ "Sequence type",
+ choices = c("Random" = "random", "Manual" = "manual")
+ ),
+
+ # Center all inputs and buttons below the sequence type selector
+ div(
+ style = "display: flex; flex-direction: column; align-items: center;",
+
# Show number-of-cards input only when random mode is selected
- conditionalPanel("input.mode == 'random'",
- numericInput("n_cards", "Number of cards", value = 3, min = 2)
+ conditionalPanel(
+ "input.mode == 'random'",
+ numericInput(
+ "n_cards",
+ "Number of cards",
+ value = 3,
+ min = 2
+ )
),
+
# Show manual entry controls only when manual mode is selected.
- # Each number gets its own row with a base value and multiplier dropdown.
- # The player clicks "Add number" to append a new row.
- conditionalPanel("input.mode == 'manual'",
+ # Each number gets its own row with a base value and
+ # multiplier dropdown.
+ # The player clicks "Add number" to add a new row.
+ conditionalPanel(
+ "input.mode == 'manual'",
uiOutput("manual_inputs"),
actionButton("add_row", "Add number")
),
actionButton("start", "Start game")
)
- })
+ )
+ })
- # Render one numericInput + selectInput pair per row.
- # Input IDs are indexed (e.g. number_1, multiplier_1) so each row
- # can be read independently when building the sequence.
- # Existing input values are read with isolate() to preserve what the
- # player entered when a new row is added.
- output$manual_inputs <- renderUI({
- lapply(seq_len(n_rows()), function(i) {
- num_val <- isolate(input[[paste0("number_", i)]])
- mul_val <- isolate(input[[paste0("multiplier_", i)]])
- fluidRow(
- column(6, numericInput(paste0("number_", i), paste("Number", i),
- value = if (is.null(num_val)) 0 else num_val, min = 0)),
- column(6, selectInput(paste0("multiplier_", i), "Multiplier",
- choices = c("None" = 1, "Million" = 1e6, "Billion" = 1e9,
- "Trillion" = 1e12, "Googol" = 1e100),
- selected = if (is.null(mul_val)) 1 else mul_val))
+ # Render table split into two columns that shows the entered numbers
+ # and multipliers.
+ # Input IDs are indexed (e.g. number_1, multiplier_1) so each row
+ # can be read independently when building the sequence.
+ # Existing input values are read with isolate() to preserve what the
+ # player entered when a new row is added.
+ output$manual_inputs <- renderUI({
+ lapply(seq_len(n_rows()), function(i) {
+ num_val <- isolate(input[[paste0("number_", i)]])
+ mul_val <- isolate(input[[paste0("multiplier_", i)]])
+ fluidRow(column(
+ 6,
+ numericInput(
+ paste0("number_", i),
+ paste("Number", i),
+ value = if (is.null(num_val))
+ 0
+ else
+ num_val,
+ min = 0
)
- })
+ ), column(
+ 6,
+ # Choices are names (e.g. "Million" or "Trillion"), so
+ # input returns the label directly (e.g. "Million" or "Trillion")
+ # making it easy to build display labels that players can understand
+ # (e.g. "1 Trillion" instead of "1,000,000,000,000").
+ selectInput(
+ paste0("multiplier_", i),
+ "Multiplier",
+ choices = c("None", "Million", "Billion", "Trillion", "Googol"),
+ selected = if (is.null(mul_val)) {
+ "None"
+ } else {
+ mul_val
+ }
+ )
+ ))
})
+ })
+
+ # Next, the player clicks "Start".
+ # If the player chose "Random", build the sequence with generate_sequence.
+ # If the player chose "Manual", read each row's number and multiplier,
+ # multiply them, then pass the resulting vector to validate_manual_sequence()
+ # for validation.
+ observeEvent(input$start, {
+
+ # --- Random mode ---
+ # Number generation with generate_sequence
+ if (input$mode == "random") {
+ # generate_sequence() returns a list with values and labels
+ result <- generate_sequence(input$n_cards)
+ # Store labels so cards display e.g. "3 Million" instead of 3000000
+ labels(result$labels)
+ # Store numeric values for game logic (comparisons, is_winner)
+ sequence(result$values)
+ } else {
+
+ # --- Manual Mode ---
+ # Save base numbers from manual input
+ base_numbers <- sapply(seq_len(n_rows()),
+ function(i) input[[paste0("number_", i)]])
+
+ # Save multiplier names from manual input
+ multipliers <- sapply(seq_len(n_rows()),
+ function(i) input[[paste0("multiplier_", i)]])
+
+ # Use convert_manual_input() to convert the manually entered numbers
+ # into numeric values for the game logic and display labels for the
+ # players to read
+ vals <- convert_manual_input(base_numbers, multipliers, "number")
+ label_vals <- convert_manual_input(base_numbers, multipliers, "label")
- # Build the sequence using game_logic functions when the player clicks Start.
- # In manual mode, read each row's number and multiplier, multiply them,
- # then pass the resulting vector to validate_manual_sequence() for validation.
- # Resets current_index and picked so a new game always starts fresh.
- observeEvent(input$start, {
- current_index(1)
- picked(NULL)
- if (input$mode == "random") {
- sequence(generate_sequence(input$n_cards))
+ # Shuffle manual values and labels
+ shuffled <- shuffle_manual_sequence(vals, label_vals)
+ vals <- shuffled$values
+ labels(shuffled$labels)
+
+ # Catch validation errors with validate_manual_sequence() and show a
+ # notification instead of crashing the app.
+ # And assign numeric values to sequence.
+ tryCatch(
+ sequence(validate_manual_sequence(vals)),
+ error = function(e)
+ showNotification(conditionMessage(e), type = "error")
+ )
+ }
+ })
+
+ # --- Game ---
+
+ # Render one card per number in the sequence once the game has started.
+ # get_card_states() assigns each card a state ("passed", "current", "hidden")
+ # which determines what the player sees: hidden cards are blue. Passed and
+ # current cards are grey and green, respectively.
+ output$cards <- renderUI({
+ req(sequence())
+ states <- get_card_states(sequence(), current_index())
+ lapply(seq_along(sequence()), function(i) {
+ if (states[i] == "hidden") {
+ # Show hidden cards as blue
+ card_div("hidden")
} else {
- vals <- sapply(seq_len(n_rows()), function(i) {
- input[[paste0("number_", i)]] *
- as.numeric(input[[paste0("multiplier_", i)]])
- })
- # Catch validation errors from validate_manual_sequence() and show a
- # notification instead of crashing the app
- tryCatch(
- sequence(validate_manual_sequence(vals)),
- error = function(e) showNotification(conditionMessage(e), type = "error")
- )
+ # Split the label by space, e.g. "3 Million" -> c("3", "Million")
+ parts <- strsplit(labels()[i], " ")[[1]]
+ # Pass number and multiplier separately so card_div displays them
+ # on separate lines. Plain numbers (e.g. "42") have no multiplier.
+ card_div(states[i], number = parts[1],
+ multiplier = if (length(parts) > 1) parts[2] else "")
}
})
+ })
- # --- Game ---
-
- # Render one card per number in the sequence once the game has started.
- # get_card_states() assigns each card a state ("passed", "current", "hidden")
- # which determines what the player sees: passed and current cards show their
- # number, hidden cards show a placeholder.
- output$cards <- renderUI({
- req(sequence())
- states <- get_card_states(sequence(), current_index())
- lapply(seq_along(sequence()), function(i) {
- if (states[i] == "hidden") {
- div("?")
- } else {
- div(sequence()[i])
- }
- })
- })
-
- # Render Pick and Next buttons once the game has started.
- # Next is disabled on the last card, forcing the player to pick.
- output$game_buttons <- renderUI({
- req(sequence())
+ # Render game buttons once the game has started.
+ # Before any card is revealed (index 0), show only "Reveal first card".
+ # During the game, show Pick and Next. Next is hidden on the last card,
+ # forcing the player to pick.
+ output$game_buttons <- renderUI({
+ req(sequence())
+ if (current_index() == 0) {
+ actionButton("next_card", "Reveal first card")
+ } else {
is_last <- current_index() == length(sequence())
tagList(
actionButton("pick", "Pick this number"),
if (!is_last) actionButton("next_card", "Next")
)
- })
+ }
+ })
- # Advance to the next card when the player clicks Next
- observeEvent(input$next_card, {
- current_index(current_index() + 1)
- })
+ # Advance to the next card when the player clicks Next
+ observeEvent(input$next_card, {
+ current_index(current_index() + 1)
+ })
- # --- Result ---
+ # --- Result ---
- # Record the current number as the player's pick when they click Pick
- observeEvent(input$pick, {
- picked(sequence()[current_index()])
- })
+ # Record the current number as the player's pick when they click Pick,
+ # and increase current_index to 1 larger than sequence to reveal all cards
+ # on the results page
+ observeEvent(input$pick, {
+ picked(sequence()[current_index()])
+ current_index(length(sequence()) + 1)
+ })
- # Show win or loss message after the player picks, with a restart button.
- # is_winner() compares the picked value against the full sequence.
- output$result <- renderUI({
- req(picked())
- tagList(
- if (is_winner(picked(), sequence())) {
- p("You won! That was the highest number.")
- } else {
- p(paste("You lost. The highest number was", max(sequence())))
- },
- actionButton("restart", "Play again")
- )
- })
+ # Show win or loss message after the player picks.
+ # is_winner() compares the picked value against the full sequence and
+ # determines whether the player lost or won.
+ output$result <- renderUI({
+ req(picked())
+ # Find the label of the highest number to show in the loss message
+ max_index <- which.max(sequence())
+ max_label <- labels()[max_index]
- # Reset all game state when the player clicks Play again, returning to setup
- observeEvent(input$restart, {
- sequence(NULL)
- picked(NULL)
- current_index(1)
- n_rows(1)
- })
- }
+ # Print win or loss message in large bold text
+ tagList(if (is_winner(picked(), sequence())) {
+ p("You won! That was the highest number.",
+ style = "font-size: 20px; font-weight: bold; color: #28a745;")
+ } else {
+ p(paste("You lost! The highest number was", max_label),
+ style = "font-size: 20px; font-weight: bold; color: #dc3545;")
+ },
+ # Add action button to restart the game
+ div(style = "text-align: center;", actionButton("restart", "Play again")))
+ })
- # Launch the Shiny app
- shinyApp(ui, server)
+ # Reset all game state when the player clicks Play again, returning to setup.
+ observeEvent(input$restart, {
+ sequence(NULL)
+ picked(NULL)
+ current_index(0)
+ n_rows(1)
+ labels(NULL)
+ updateNumericInput(session, "number_1", value = 0)
+ updateSelectInput(session, "multiplier_1", selected = "None")
+ })
}
diff --git a/R/convert_manual_input.R b/R/convert_manual_input.R
new file mode 100644
index 0000000..7de1312
--- /dev/null
+++ b/R/convert_manual_input.R
@@ -0,0 +1,30 @@
+#' Convert manually entered base numbers and multipliers to numeric values or
+#' display labels
+#'
+#' @param base_numbers A numeric vector of base values entered by the player
+#' @param multipliers A character vector of multiplier names (e.g. "Million",
+#' "Googol"). Must be one of "None", "Million", "Billion", "Trillion",
+#' "Googol".
+#' @param output Either "number" to return numeric values for game logic, or
+#' "label" to return display labels for the UI (e.g. "2 Googol").
+#' @return A numeric vector if output is "number", a character vector if output
+#' is "label".
+#' @details Multiplier values: None = 1, Million = 1e6, Billion = 1e9,
+#' Trillion = 1e12, Googol = 1e100.
+#' @examples
+#' convert_manual_input(c(2, 4), c("Googol", "Million"), "number")
+#' convert_manual_input(c(2, 4), c("Googol", "Million"), "label")
+#' @export
+convert_manual_input <- function(base_numbers, multipliers, output) {
+ multiplier_values <- c(
+ "None" = 1, "Million" = 1e6, "Billion" = 1e9,
+ "Trillion" = 1e12, "Googol" = 1e100
+ )
+ if (output == "number") {
+ # unname() strips the multiplier names carried over from subsetting
+ unname(base_numbers * multiplier_values[multipliers])
+ } else {
+ ifelse(multipliers == "None", as.character(base_numbers),
+ paste(base_numbers, multipliers))
+ }
+}
diff --git a/R/game_logic.R b/R/game_logic.R
deleted file mode 100644
index 25c6667..0000000
--- a/R/game_logic.R
+++ /dev/null
@@ -1,75 +0,0 @@
-#' Generate a sequence of random numbers
-#'
-#' @param n Number of values to draw
-#' @return A numeric vector of length n
-#' @details Samples n unique integers from 0 to 1000 without replacement.
-#' @examples
-#' generate_sequence()
-#' generate_sequence(n = 5)
-#' @export
-generate_sequence <- function(n = 3) {
- sample(0:1000, n)
-}
-
-#' Validate and return a sequence of numbers entered by
-#' the player in the Shiny app
-#'
-#' @param numbers A numeric vector of at least 2 values
-#' @return A numeric vector
-#' @details Player can manually enter numbers. Validates that the input contains
-#' at least 2 unique non-negative numeric values, then returns them as the
-#' game sequence. Uses checkmate package for validation and error messages.
-#' @examples
-#' validate_manual_sequence(c(100, 5000, 42))
-#' validate_manual_sequence(c(0, 1e100, 12345, 678))
-#' @importFrom checkmate assert_numeric
-#' @export
-validate_manual_sequence <- function(numbers) {
- assert_numeric(
- numbers,
- lower = 0,
- any.missing = FALSE,
- min.len = 2,
- unique = TRUE
- )
- numbers
-}
-
-#' Get the display state of each card
-#'
-#' @param sequence A numeric vector
-#' @param current_index The index of the card currently revealed
-#' @return A character vector with one entry per card: "passed", "current",
-#' or "hidden"
-#' @details Determines the state of each card based on how far the player has
-#' progressed. Cards before the current index are "passed", the current card
-#' is "current", and unrevealed cards are "hidden". In the Shiny UI, each
-#' card needs to render differently depending on its state (i.e. passed cards
-#' greyed out, current card highlighted in blue, hidden cards face-down); this
-#' function provides the state labels to drive that styling.
-#' @examples
-#' get_card_states(c(42, 7, 198), 1)
-#' get_card_states(c(42, 7, 198), 2)
-#' @export
-get_card_states <- function(sequence, current_index) {
- sapply(seq_along(sequence), function(i) {
- if (i < current_index) "passed"
- else if (i == current_index) "current"
- else "hidden"
- })
-}
-
-#' Check if the player picked the highest number
-#'
-#' @param pick The value the player selected
-#' @param sequence The full sequence of numbers
-#' @return Logical, TRUE if pick is the maximum
-#' @details The player wins only if their chosen number is strictly the largest
-#' in the full sequence, including numbers they never saw.
-#' @examples
-#' is_winner(198, c(42, 7, 198))
-#' is_winner(42, c(42, 7, 198))
-#' @export
-is_winner <- function(pick, sequence) {
- pick == max(sequence)
-}
diff --git a/R/generate_sequence.R b/R/generate_sequence.R
new file mode 100644
index 0000000..a8d2984
--- /dev/null
+++ b/R/generate_sequence.R
@@ -0,0 +1,56 @@
+#' Generate a sequence of random numbers across different scales
+#'
+#' @param n Number of values to draw
+#' @return A list with elements \code{values} (numeric vector) and
+#' \code{labels} (character vector). Small numbers are shown as plain
+#' integers; numbers from a million onwards are shown with a multiplier
+#' label (e.g. "3 Million", "7 Googol").
+#' @details Samples n numbers by randomly selecting from eight ranges:
+#' 0-20, 21-100, 101-1000, 1k-100k, 1-100 Million, 1-100 Billion,
+#' 1-100 Trillion, and 1-100 Googol. Numbers from million onwards are
+#' expressed as a base number times a named multiplier for readability.
+#' Resamples until all values are unique.
+#' @examples
+#' generate_sequence()
+#' generate_sequence(n = 5)
+#' @export
+generate_sequence <- function(n = 3) {
+
+ # Small ranges: sample a plain integer and use it directly as the label
+ small_ranges <- list(c(0, 20), c(21, 100), c(101, 1000), c(1000, 100000))
+
+ # Large ranges: sample a base number (1-100) and multiply by a named
+ # multiplier to produce a human-readable label (e.g. "3 Million")
+ large_ranges <- list(
+ list(name = "Million", value = 1e6),
+ list(name = "Billion", value = 1e9),
+ list(name = "Trillion", value = 1e12),
+ list(name = "Googol", value = 1e100)
+ )
+
+ all_ranges <- c(small_ranges, large_ranges)
+
+ # Sample one number from a given range and return its value and label
+ sample_range <- function(range) {
+ if (is.list(range)) {
+ # Large range: combine base number with multiplier name for the label
+ base <- sample(1:100, 1)
+ list(value = base * range$value, label = paste(base, range$name))
+ } else {
+ # Small range: use the sampled integer as both value and label
+ val <- sample(range[1]:range[2], 1)
+ list(value = val, label = as.character(val))
+ }
+ }
+
+ # Keep resampling until all n values are unique to avoid duplicate cards
+ repeat {
+ selected <- sample(length(all_ranges), n, replace = TRUE)
+ draws <- lapply(all_ranges[selected], sample_range)
+ values <- sapply(draws, `[[`, "value")
+ labels <- sapply(draws, `[[`, "label")
+ if (length(unique(values)) == n) break
+ }
+
+ list(values = values, labels = labels)
+}
diff --git a/R/get_card_states.R b/R/get_card_states.R
new file mode 100644
index 0000000..c091175
--- /dev/null
+++ b/R/get_card_states.R
@@ -0,0 +1,23 @@
+#' Get the display state of each card
+#'
+#' @param sequence A numeric vector
+#' @param current_index The index of the card currently revealed
+#' @return A character vector with one entry per card: "passed", "current",
+#' or "hidden"
+#' @details Determines the state of each card based on how far the player has
+#' progressed. Cards before the current index are "passed", the current card
+#' is "current", and unrevealed cards are "hidden". In the Shiny UI, each
+#' card needs to render differently depending on its state (i.e. passed cards
+#' greyed out, current card highlighted in blue, hidden cards face-down); this
+#' function provides the state labels to drive that styling.
+#' @examples
+#' get_card_states(c(42, 7, 198), 1)
+#' get_card_states(c(42, 7, 198), 2)
+#' @export
+get_card_states <- function(sequence, current_index) {
+ sapply(seq_along(sequence), function(i) {
+ if (i < current_index) "passed"
+ else if (i == current_index) "current"
+ else "hidden"
+ })
+}
diff --git a/R/is_winner.R b/R/is_winner.R
new file mode 100644
index 0000000..18c4c45
--- /dev/null
+++ b/R/is_winner.R
@@ -0,0 +1,14 @@
+#' Check if the player picked the highest number
+#'
+#' @param pick The value the player selected
+#' @param sequence The full sequence of numbers
+#' @return Logical, TRUE if pick is the maximum
+#' @details The player wins only if their chosen number is strictly the largest
+#' in the full sequence, including numbers they never saw.
+#' @examples
+#' is_winner(198, c(42, 7, 198))
+#' is_winner(42, c(42, 7, 198))
+#' @export
+is_winner <- function(pick, sequence) {
+ pick == max(sequence)
+}
diff --git a/R/shuffle_manual_sequence.R b/R/shuffle_manual_sequence.R
new file mode 100644
index 0000000..7cdf6d2
--- /dev/null
+++ b/R/shuffle_manual_sequence.R
@@ -0,0 +1,18 @@
+#' Shuffle a manually entered sequence and its display labels together
+#'
+#' @param values A numeric vector of sequence values
+#' @param labels A character vector of display labels corresponding to values
+#' @return A list with elements \code{values} and \code{labels}, both shuffled
+#' in the same random order so they remain in sync.
+#' @details Shuffling the sequence before the game starts prevents the player
+#' from predicting the order of numbers entered by a second person.
+#' @examples
+#' shuffle_manual_sequence(
+#' c(100, 2e6, 1e100),
+#' c("100", "2 Million", "1 Googol")
+#' )
+#' @export
+shuffle_manual_sequence <- function(values, labels) {
+ index <- sample(length(values))
+ list(values = values[index], labels = labels[index])
+}
diff --git a/R/validate_manual_sequence.R b/R/validate_manual_sequence.R
new file mode 100644
index 0000000..362951b
--- /dev/null
+++ b/R/validate_manual_sequence.R
@@ -0,0 +1,23 @@
+#' Validate and return a sequence of numbers entered by
+#' the player in the Shiny app
+#'
+#' @param numbers A numeric vector of at least 2 values
+#' @return A numeric vector
+#' @details Player can manually enter numbers. Validates that the input contains
+#' at least 2 unique non-negative numeric values, then returns them as the
+#' game sequence. Uses checkmate package for validation and error messages.
+#' @examples
+#' validate_manual_sequence(c(100, 5000, 42))
+#' validate_manual_sequence(c(0, 1e100, 12345, 678))
+#' @importFrom checkmate assert_numeric
+#' @export
+validate_manual_sequence <- function(numbers) {
+ assert_numeric(
+ numbers,
+ lower = 0,
+ any.missing = FALSE,
+ min.len = 2,
+ unique = TRUE
+ )
+ numbers
+}
diff --git a/README.md b/README.md
index a7bbf90..4f9129e 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,44 @@
# GoogolGame
-Play the googol game to explore the theory of optimal stopping or use it as a task in your research!
-
-In the coming weeks I want to create an application that allows users to play the googol game (also known as secretary problem, marriage problem, and more).
-The googol game demonstrates a scenario involving optimal stopping theory.
-
-The basic idea is simple: a set of cards (e.g. 10) each contains a random number between zero and infinity. The cards are shuffled, placed face down in a line, and revealed one at a time. After each reveal, the player must decide whether to stop or continue searching for a higher number. Once a card is passed, the player cannot return to it later. The goal is to stop on the highest number in the deck.
-
-My application will allow players to use card sets of different sizes, with values either entered manually or generated randomly.
-The game can be used to explore optimal stopping theory and may also have applications in research or education.
[](https://github.com/Programming-The-Next-Step-2026/GoogolGame/actions/workflows/R-CMD-check.yaml)
+
+## Do You Know This Problem?
+
+You just finished your workout and it's time to shower. But first — that email you've been putting off all day. Then the dishes, and the floor isn't going to vacuum itself. After all that housework you deserve a little reward, so you sit down to finish your favourite series. By now the sweat has long dried, so it barely matters anyway. More importantly, it's late and your roommate is asleep, so it would be rude to shower! And you were planning to work out again tomorrow anyways — so what's the point of showering at all? You could go on like this forever.
+
+The question is: **when is the right moment**? To stop and say: now I decide to do it.
+
+To take the shower, to take out the trash, or to pick the card with the highest number…
+
+The card with the highest number, you ask? The idea comes from the American writer Martin Gardner who first described a gambling game called Googol.
+
+## What is the Googol Game?
+
+The Googol Game demonstrates optimal stopping theory. The rules are simple: Player 1 writes any numbers they like on a set of cards, then turns them face down and shuffles them. Player 2 turns over one card at a time and must decide when to stop — betting that the current card is the highest. Crucially, you cannot go back. If you pass a card, it's gone.
+
+Is there an optimal point to stop?
+
+Explore this question with the Googol Game app!
+
+## Installation
+
+```r
+# install.packages("devtools")
+devtools::install_github("Programming-The-Next-Step-2026/GoogolGame")
+```
+
+## Usage
+
+```r
+library(GoogolGame)
+play_googol()
+```
+
+## Features
+
+- **Random mode**: the app generates a sequence of numbers across a wide range of scales — from small integers up to Googol-sized values (e.g. "3 Million", "7 Googol").
+- **Manual mode**: enter your own numbers one at a time using a base value and a multiplier (None, Million, Billion, Trillion, Googol).
+- Players can reveal cards one at a time and decide to stop if they think they found the highest number.
+- Win and loss messages are displayed, with an option to play again.
diff --git a/man/convert_manual_input.Rd b/man/convert_manual_input.Rd
new file mode 100644
index 0000000..0f65608
--- /dev/null
+++ b/man/convert_manual_input.Rd
@@ -0,0 +1,35 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/convert_manual_input.R
+\name{convert_manual_input}
+\alias{convert_manual_input}
+\title{Convert manually entered base numbers and multipliers to numeric values or
+display labels}
+\usage{
+convert_manual_input(base_numbers, multipliers, output)
+}
+\arguments{
+\item{base_numbers}{A numeric vector of base values entered by the player}
+
+\item{multipliers}{A character vector of multiplier names (e.g. "Million",
+"Googol"). Must be one of "None", "Million", "Billion", "Trillion",
+"Googol".}
+
+\item{output}{Either "number" to return numeric values for game logic, or
+"label" to return display labels for the UI (e.g. "2 Googol").}
+}
+\value{
+A numeric vector if output is "number", a character vector if output
+ is "label".
+}
+\description{
+Convert manually entered base numbers and multipliers to numeric values or
+display labels
+}
+\details{
+Multiplier values: None = 1, Million = 1e6, Billion = 1e9,
+ Trillion = 1e12, Googol = 1e100.
+}
+\examples{
+convert_manual_input(c(2, 4), c("Googol", "Million"), "number")
+convert_manual_input(c(2, 4), c("Googol", "Million"), "label")
+}
diff --git a/man/generate_sequence.Rd b/man/generate_sequence.Rd
index ab0d77c..5855a5b 100644
--- a/man/generate_sequence.Rd
+++ b/man/generate_sequence.Rd
@@ -1,8 +1,8 @@
% Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/game_logic.R
+% Please edit documentation in R/generate_sequence.R
\name{generate_sequence}
\alias{generate_sequence}
-\title{Generate a sequence of random numbers}
+\title{Generate a sequence of random numbers across different scales}
\usage{
generate_sequence(n = 3)
}
@@ -10,13 +10,20 @@ generate_sequence(n = 3)
\item{n}{Number of values to draw}
}
\value{
-A numeric vector of length n
+A list with elements \code{values} (numeric vector) and
+ \code{labels} (character vector). Small numbers are shown as plain
+ integers; numbers from a million onwards are shown with a multiplier
+ label (e.g. "3 Million", "7 Googol").
}
\description{
-Generate a sequence of random numbers
+Generate a sequence of random numbers across different scales
}
\details{
-Samples n unique integers from 0 to 1000 without replacement.
+Samples n numbers by randomly selecting from eight ranges:
+ 0-20, 21-100, 101-1000, 1k-100k, 1-100 Million, 1-100 Billion,
+ 1-100 Trillion, and 1-100 Googol. Numbers from million onwards are
+ expressed as a base number times a named multiplier for readability.
+ Resamples until all values are unique.
}
\examples{
generate_sequence()
diff --git a/man/get_card_states.Rd b/man/get_card_states.Rd
index 1cf651c..17b931a 100644
--- a/man/get_card_states.Rd
+++ b/man/get_card_states.Rd
@@ -1,5 +1,5 @@
% Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/game_logic.R
+% Please edit documentation in R/get_card_states.R
\name{get_card_states}
\alias{get_card_states}
\title{Get the display state of each card}
diff --git a/man/is_winner.Rd b/man/is_winner.Rd
index bce7cc7..02661fd 100644
--- a/man/is_winner.Rd
+++ b/man/is_winner.Rd
@@ -1,5 +1,5 @@
% Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/game_logic.R
+% Please edit documentation in R/is_winner.R
\name{is_winner}
\alias{is_winner}
\title{Check if the player picked the highest number}
diff --git a/man/run_app.Rd b/man/play_googol.Rd
similarity index 73%
rename from man/run_app.Rd
rename to man/play_googol.Rd
index 634d7b2..3e88ac6 100644
--- a/man/run_app.Rd
+++ b/man/play_googol.Rd
@@ -1,13 +1,13 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/app.R
-\name{run_app}
-\alias{run_app}
-\title{Run the Googol Game app}
+\name{play_googol}
+\alias{play_googol}
+\title{Play the Googol Game}
\usage{
-run_app()
+play_googol()
}
\description{
-Run the Googol Game app
+Play the Googol Game
}
\details{
Launches an interactive Shiny app for the Googol Game. The app has
@@ -16,5 +16,5 @@ Launches an interactive Shiny app for the Googol Game. The app has
with an option to play again).
}
\examples{
-if (interactive()) run_app()
+if (interactive()) play_googol()
}
diff --git a/man/shuffle_manual_sequence.Rd b/man/shuffle_manual_sequence.Rd
new file mode 100644
index 0000000..2d01db9
--- /dev/null
+++ b/man/shuffle_manual_sequence.Rd
@@ -0,0 +1,30 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/shuffle_manual_sequence.R
+\name{shuffle_manual_sequence}
+\alias{shuffle_manual_sequence}
+\title{Shuffle a manually entered sequence and its display labels together}
+\usage{
+shuffle_manual_sequence(values, labels)
+}
+\arguments{
+\item{values}{A numeric vector of sequence values}
+
+\item{labels}{A character vector of display labels corresponding to values}
+}
+\value{
+A list with elements \code{values} and \code{labels}, both shuffled
+ in the same random order so they remain in sync.
+}
+\description{
+Shuffle a manually entered sequence and its display labels together
+}
+\details{
+Shuffling the sequence before the game starts prevents the player
+ from predicting the order of numbers entered by a second person.
+}
+\examples{
+shuffle_manual_sequence(
+c(100, 2e6, 1e100),
+c("100", "2 Million", "1 Googol")
+)
+}
diff --git a/man/validate_manual_sequence.Rd b/man/validate_manual_sequence.Rd
index 1de2035..8a8a525 100644
--- a/man/validate_manual_sequence.Rd
+++ b/man/validate_manual_sequence.Rd
@@ -1,5 +1,5 @@
% Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/game_logic.R
+% Please edit documentation in R/validate_manual_sequence.R
\name{validate_manual_sequence}
\alias{validate_manual_sequence}
\title{Validate and return a sequence of numbers entered by
diff --git a/tests/testthat/test-app.R b/tests/testthat/test-app.R
index a017fde..4d0b9f1 100644
--- a/tests/testthat/test-app.R
+++ b/tests/testthat/test-app.R
@@ -2,28 +2,39 @@ library(shinytest2)
# 1. App loads on setup page
test_that("app loads on setup page", {
- app <- AppDriver$new(run_app())
+ app <- AppDriver$new(play_googol())
app$wait_for_idle()
expect_true("start" %in% names(app$get_values()$input))
app$stop()
})
-# 2. Random mode: Start switches to game page
-test_that("random mode Start switches to game page", {
- app <- AppDriver$new(run_app())
- app$set_inputs(mode = "random", n_cards = 3)
+# 2. Start switches to game page
+test_that("Start switches to game page", {
+ app <- AppDriver$new(play_googol())
+ app$set_inputs(mode = "manual", number_1 = 100, multiplier_1 = "None")
+ app$click("add_row")
+ app$wait_for_idle()
+ app$set_inputs(number_2 = 500, multiplier_2 = "None")
app$click("start")
app$wait_for_idle()
+ # Reveal first card so the Pick button appears
+ app$click("next_card")
+ app$wait_for_idle()
expect_true("pick" %in% names(app$get_values()$input))
app$stop()
})
# 3. Pick switches to result page
test_that("Pick switches to result page", {
- app <- AppDriver$new(run_app())
- app$set_inputs(mode = "random", n_cards = 3)
+ app <- AppDriver$new(play_googol())
+ app$set_inputs(mode = "manual", number_1 = 100, multiplier_1 = "None")
+ app$click("add_row")
+ app$wait_for_idle()
+ app$set_inputs(number_2 = 500, multiplier_2 = "None")
app$click("start")
app$wait_for_idle()
+ app$click("next_card")
+ app$wait_for_idle()
app$click("pick")
app$wait_for_idle()
expect_true("restart" %in% names(app$get_values()$input))
@@ -32,10 +43,15 @@ test_that("Pick switches to result page", {
# 4. Play again returns to setup
test_that("Play again returns to setup page", {
- app <- AppDriver$new(run_app())
- app$set_inputs(mode = "random", n_cards = 3)
+ app <- AppDriver$new(play_googol())
+ app$set_inputs(mode = "manual", number_1 = 100, multiplier_1 = "None")
+ app$click("add_row")
+ app$wait_for_idle()
+ app$set_inputs(number_2 = 500, multiplier_2 = "None")
app$click("start")
app$wait_for_idle()
+ app$click("next_card")
+ app$wait_for_idle()
app$click("pick")
app$wait_for_idle()
app$click("restart")
@@ -44,23 +60,10 @@ test_that("Play again returns to setup page", {
app$stop()
})
-# 5. Manual mode with valid numbers starts game
-test_that("manual mode with valid numbers starts game", {
- app <- AppDriver$new(run_app())
- app$set_inputs(mode = "manual", number_1 = 100, multiplier_1 = 1)
- app$click("add_row")
- app$wait_for_idle()
- app$set_inputs(number_2 = 500, multiplier_2 = 1)
- app$click("start")
- app$wait_for_idle()
- expect_true("pick" %in% names(app$get_values()$input))
- app$stop()
-})
-
-# 6. Manual mode with only 1 number shows an error and stays on setup
-test_that("manual mode with 1 number shows error and stays on setup page", {
- app <- AppDriver$new(run_app())
- app$set_inputs(mode = "manual", number_1 = 100, multiplier_1 = 1)
+# 5. Manual mode with invalid entry (i.e. only 1 number) shows an error and stays on setup
+test_that("manual mode with invalid entry shows error and stays on setup page", {
+ app <- AppDriver$new(play_googol())
+ app$set_inputs(mode = "manual", number_1 = 100, multiplier_1 = "None")
app$click("start")
app$wait_for_idle()
expect_true("start" %in% names(app$get_values()$input))
diff --git a/tests/testthat/test-convert_manual_input.R b/tests/testthat/test-convert_manual_input.R
new file mode 100644
index 0000000..b85c70c
--- /dev/null
+++ b/tests/testthat/test-convert_manual_input.R
@@ -0,0 +1,39 @@
+# 1. Returns correct numeric values for "number" output
+test_that("convert_manual_input returns correct numeric values", {
+ expect_equal(
+ convert_manual_input(c(2, 4), c("Googol", "Million"), "number"),
+ c(2e100, 4e6)
+ )
+})
+
+# 2. Returns correct display labels for "label" output
+test_that("convert_manual_input returns correct display labels", {
+ expect_equal(
+ convert_manual_input(c(2, 4), c("Googol", "Million"), "label"),
+ c("2 Googol", "4 Million")
+ )
+})
+
+# 3. Returns plain number as string when multiplier is "None"
+test_that("convert_manual_input returns plain number when multiplier is None", {
+ expect_equal(
+ convert_manual_input(c(42), c("None"), "label"),
+ c("42")
+ )
+})
+
+# 4. Returns unnamed numeric vector for "number" output
+test_that("convert_manual_input returns unnamed numeric vector", {
+ result <- convert_manual_input(c(1, 2), c("Million", "Billion"), "number")
+ expect_null(names(result))
+})
+
+# 5. Handles all multipliers correctly for "number" output
+test_that("convert_manual_input handles all multipliers correctly", {
+ expect_equal(
+ convert_manual_input(c(1, 1, 1, 1, 1),
+ c("None", "Million", "Billion", "Trillion", "Googol"),
+ "number"),
+ c(1, 1e6, 1e9, 1e12, 1e100)
+ )
+})
diff --git a/tests/testthat/test-generate_sequence.R b/tests/testthat/test-generate_sequence.R
index 27ee688..6c77326 100644
--- a/tests/testthat/test-generate_sequence.R
+++ b/tests/testthat/test-generate_sequence.R
@@ -1,18 +1,30 @@
-# 1. Returns a vector of the correct length
-test_that("generate_sequence returns a vector of the correct length", {
- expect_length(generate_sequence(3), 3)
- expect_length(generate_sequence(5), 5)
+# 1. Returns a list with values and labels of the correct length
+test_that("generate_sequence returns values and labels of correct length", {
+ result <- generate_sequence(3)
+ expect_length(result$values, 3)
+ expect_length(result$labels, 3)
})
-# 2. Returns values within range 0 to 1000
-test_that("generate_sequence returns values within range 0 to 1000", {
- result <- generate_sequence(500)
- expect_true(all(result >= 0))
- expect_true(all(result <= 1000))
+# 2. Returns unique values
+test_that("generate_sequence returns unique values", {
+ result <- generate_sequence(5)
+ expect_equal(length(unique(result$values)), 5)
})
-# 3. Returns unique values
-test_that("generate_sequence returns unique values", {
- result <- generate_sequence(50)
- expect_equal(length(result), length(unique(result)))
+# 3. Returns positive numeric values
+test_that("generate_sequence returns positive numeric values", {
+ result <- generate_sequence(5)
+ expect_true(all(result$values >= 0))
+})
+
+# 4. Returns character labels
+test_that("generate_sequence returns character labels", {
+ result <- generate_sequence(5)
+ expect_type(result$labels, "character")
+})
+
+# 5. Labels and values are in sync
+test_that("generate_sequence labels and values are in sync", {
+ result <- generate_sequence(5)
+ expect_equal(length(result$values), length(result$labels))
})
diff --git a/tests/testthat/test-shuffle_manual_sequence.R b/tests/testthat/test-shuffle_manual_sequence.R
new file mode 100644
index 0000000..2df3f02
--- /dev/null
+++ b/tests/testthat/test-shuffle_manual_sequence.R
@@ -0,0 +1,32 @@
+# 1. Returns a list with values and labels
+test_that("shuffle_manual_sequence returns a list with values and labels", {
+ result <- shuffle_manual_sequence(c(100, 2e6, 1e100), c("100", "2 Million", "1 Googol"))
+ expect_type(result, "list")
+ expect_named(result, c("values", "labels"))
+})
+
+# 2. Returns the same number of elements
+test_that("shuffle_manual_sequence preserves length", {
+ result <- shuffle_manual_sequence(c(100, 2e6, 1e100), c("100", "2 Million", "1 Googol"))
+ expect_length(result$values, 3)
+ expect_length(result$labels, 3)
+})
+
+# 3. Values and labels remain in sync after shuffling
+test_that("shuffle_manual_sequence keeps values and labels in sync", {
+ values <- c(100, 2e6, 1e100)
+ labels <- c("100", "2 Million", "1 Googol")
+ result <- shuffle_manual_sequence(values, labels)
+ # Each label should still correspond to its value
+ for (i in seq_along(result$values)) {
+ original_index <- which(values == result$values[i])
+ expect_equal(result$labels[i], labels[original_index])
+ }
+})
+
+# 4. Contains the same values after shuffling
+test_that("shuffle_manual_sequence contains the same values", {
+ values <- c(100, 2e6, 1e100)
+ result <- shuffle_manual_sequence(values, c("100", "2 Million", "1 Googol"))
+ expect_setequal(result$values, values)
+})
diff --git a/vignettes/how-to-use-the-googol-game-app.Rmd b/vignettes/how-to-use-the-googol-game-app.Rmd
index f45f516..1443e48 100644
--- a/vignettes/how-to-use-the-googol-game-app.Rmd
+++ b/vignettes/how-to-use-the-googol-game-app.Rmd
@@ -14,56 +14,80 @@ knitr::opts_chunk$set(
)
```
-```{r setup}
-library(GoogolGame)
-```
-
## What is the Googol Game?
-The Googol Game is based on the mathematical Secretary Problem. A sequence of
-numbers is revealed one at a time. Your goal is to pick the highest number in
-the sequence — but once you pass a number, you cannot go back. You win only if
-the number you pick turns out to be the highest in the entire sequence,
-including numbers you never saw.
+The Googol Game is a gambling scenario first described by the American writer
+Martin Gardner. A sequence of numbers is revealed one at a time. Your goal is
+to pick the highest number in the sequence — but once you pass a number, you
+cannot go back. You win only if the number you pick turns out to be the highest
+in the entire sequence, including numbers you never saw.
+
+## How to install the app
+
+```{r, eval = FALSE}
+# install.packages("devtools")
+devtools::install_github("Programming-The-Next-Step-2026/GoogolGame")
+```
## How to launch the app
```{r, eval = FALSE}
-run_app()
+library(GoogolGame)
+play_googol()
```
-## Step-by-step instructions
+## Scenario
+
+**Purpose**: scenario that describes how to use the GoogolGame app.
+
+**User**: everybody that is interested in the Googol Game or optimal stopping theory.
+It is especially fun if you play it together with a second person!
+
+**Equipment**: any computer with a supported browser. You must have installed R. All
+other dependencies will be installed automatically when downloading the package.
-### Step 1: Choose a sequence type
+**Scenario**:
+
+**Step 1: Choose a sequence type**
When the app opens, choose how to generate the sequence of numbers:
- **Random**: the app generates a sequence of random numbers for you. Enter
- how many cards you want and click **Start game**.
+ how many cards you want (e.g. 3) and click **Start game**.
+
+
+
- **Manual**: enter your own numbers one at a time. For each number, type a
value and optionally select a multiplier (e.g. select `2` and `Googol` to
enter 2 Googol). Click **Add number** to add another number. You need at
- least 2 numbers and you can add as many numbers as you want.
+ least 2 numbers and you can add as many numbers as you want.
+
+
+
**Note**: manual entry requires a second person — one who enters the numbers
and one who plays the game! Hand the keyboard to a friend to enter the
- numbers and don't peek at the screen ;)
+ numbers and don't peek at the screen.
+
+**Step 2: Play the game**
-### Step 2: Play the game
+After clicking **Start game**, all cards are shown face-down in blue. Click
+**Reveal first card** to turn over the first card.
-After clicking **Start game**, the cards are revealed one at a time:
+
-- Cards you have already seen are shown with their number.
-- The current card shows its number and is the one you can pick.
-- Cards not yet revealed are shown as `?`.
+From there, after each reveal you have two choices:
-At each card you have two choices:
+- Click **Pick this number** if you think the current card (highlighted in
+ green) is the highest.
+- Click **Next** to pass it and reveal the next card. Passed cards turn grey.
-- Click **Pick this number** to select the current card as your answer.
-- Click **Next** to pass and reveal the next card.
+
On the last card, only **Pick this number** is available — you must pick.
-### Step 3: See the result
+
+
+**Step 3: See the result**
After picking, the app shows whether you won or lost:
@@ -71,4 +95,33 @@ After picking, the app shows whether you won or lost:
- **Loss**: a higher number existed elsewhere in the sequence, along with what
that number was.
+
+
Click **Play again** to return to the setup page and start a new game.
+
+## Flowchart
+
+The flowchart below shows the full flow of the app, distinguishing between
+user actions and the software functions triggered behind the scenes.
+
+**Colours**: ■ blue = user action,
+■ green = software action.
+
+
+
+## Extra: optimal stopping theory
+
+Is there a strategy that maximises your chances of picking the highest number?
+Yes — and the answer comes from mathematics.
+
+The optimal strategy is to let the first 37% of cards pass without
+picking, no matter how high they are. After that, pick the next card that is
+higher than all the cards you have already seen. If no such card appears, pick
+the last card.
+
+This strategy gives you approximately a 37% chance of picking the highest
+number, regardless of how many cards there are. Remarkably, no other strategy
+can do better in the long run.
+
+Try it out: with 10 cards, skip the first 3 or 4 and then pick the next card
+that beats them all.
diff --git a/vignettes/images/flowchart.png b/vignettes/images/flowchart.png
new file mode 100644
index 0000000..c640e0c
Binary files /dev/null and b/vignettes/images/flowchart.png differ
diff --git a/vignettes/images/manual_generation.png b/vignettes/images/manual_generation.png
new file mode 100644
index 0000000..fb1828a
Binary files /dev/null and b/vignettes/images/manual_generation.png differ
diff --git a/vignettes/images/next_or_pick.png b/vignettes/images/next_or_pick.png
new file mode 100644
index 0000000..3d083fe
Binary files /dev/null and b/vignettes/images/next_or_pick.png differ
diff --git a/vignettes/images/pick_this_number.png b/vignettes/images/pick_this_number.png
new file mode 100644
index 0000000..cb7830b
Binary files /dev/null and b/vignettes/images/pick_this_number.png differ
diff --git a/vignettes/images/random_generation.png b/vignettes/images/random_generation.png
new file mode 100644
index 0000000..36b3077
Binary files /dev/null and b/vignettes/images/random_generation.png differ
diff --git a/vignettes/images/reveal_first_card.png b/vignettes/images/reveal_first_card.png
new file mode 100644
index 0000000..3aecdb1
Binary files /dev/null and b/vignettes/images/reveal_first_card.png differ
diff --git a/vignettes/images/win_or_loss.png b/vignettes/images/win_or_loss.png
new file mode 100644
index 0000000..b9be6a6
Binary files /dev/null and b/vignettes/images/win_or_loss.png differ