R/r4fun.R

Defines functions r4fun .r4fun_app .r4fun_fortune_ui .r4fun_wheel_ui .r4fun_emoji_ui .r4fun_mastermind_ui .r4fun_lights_ui .r4fun_maze_ui .r4fun_tf_ui .r4fun_higher_ui .r4fun_odd_ui .r4fun_cards_ui .r4fun_memory_ui .r4fun_life_ui .r4fun_home_ui .r4fun_js .r4fun_css .r4fun_mastermind_score .r4fun_light_toggle .r4fun_lights_generate .r4fun_maze_generate .r4fun_fortunes .r4fun_emoji_bank .r4fun_tf_bank .r4fun_higher_bank .r4fun_odd_pairs .r4fun_cards_symbols .r4fun_memory_sequence .r4fun_birthday_progress .r4fun_age_components .r4fun_fmt .r4fun_trick .r4fun_game_labels .r4fun_or .r4fun_need

Documented in r4fun

# R4VN Playground ---------------------------------------------------------
# This source file is intentionally ASCII-only for package portability.
# Vietnamese UI text and emoji are represented with Unicode escapes.

.r4fun_need <- function(pkg) {
  if (!requireNamespace(pkg, quietly = TRUE)) {
    stop(
      sprintf("Package '%s' is required. Install it with install.packages('%s').", pkg, pkg),
      call. = FALSE
    )
  }
}

.r4fun_or <- function(x, y) {
  if (is.null(x) || length(x) == 0L || is.na(x[1L])) y else x
}

.r4fun_text <- local({
  dict <- list(
    app_title = c(en = "R4VN Playground", vi = "R4VN Playground"),
    app_subtitle = c(en = "Take a short break. Play, relax, and come back refreshed.", vi = "Ngh\u1EC9 m\u1ED9t ch\u00FAt. Ch\u01A1i, th\u01B0 gi\u00E3n, r\u1ED3i quay l\u1EA1i v\u1EDBi tinh th\u1EA7n m\u1EDBi."),
    language = c(en = "Language", vi = "Ng\u00F4n ng\u1EEF"),
    game = c(en = "Activity", vi = "Ho\u1EA1t \u0111\u1ED9ng"),
    surprise = c(en = "Surprise me", vi = "Ch\u1ECDn ng\u1EABu nhi\u00EAn"),
    home = c(en = "Home", vi = "Trang ch\u1EE7"),
    reset = c(en = "Reset", vi = "L\u00E0m l\u1EA1i"),
    start = c(en = "Start", vi = "B\u1EAFt \u0111\u1EA7u"),
    "next" = c(en = "Next", vi = "Ti\u1EBFp"),
    submit = c(en = "Submit", vi = "Tr\u1EA3 l\u1EDDi"),
    correct = c(en = "Correct!", vi = "Ch\u00EDnh x\u00E1c!"),
    wrong = c(en = "Not quite.", vi = "Ch\u01B0a \u0111\u00FAng."),
    score = c(en = "Score", vi = "\u0110i\u1EC3m"),
    streak = c(en = "Streak", vi = "Chu\u1ED7i \u0111\u00FAng"),
    best = c(en = "Best", vi = "T\u1ED1t nh\u1EA5t"),
    level = c(en = "Level", vi = "C\u1EA5p \u0111\u1ED9"),
    moves = c(en = "Moves", vi = "S\u1ED1 b\u01B0\u1EDBc"),
    time = c(en = "Time", vi = "Th\u1EDDi gian"),
    seconds = c(en = "seconds", vi = "gi\u00E2y"),
    easy = c(en = "Easy", vi = "D\u1EC5"),
    medium = c(en = "Medium", vi = "V\u1EEBa"),
    hard = c(en = "Hard", vi = "Kh\u00F3"),
    new_game = c(en = "New game", vi = "V\u00E1n m\u1EDBi"),
    play_again = c(en = "Play again", vi = "Ch\u01A1i l\u1EA1i"),
    finished = c(en = "Finished!", vi = "Ho\u00E0n th\u00E0nh!"),
    fun_estimate = c(en = "Fun estimates use simple population assumptions and are not personal medical predictions.", vi = "C\u00E1c con s\u1ED1 vui ch\u1EC9 d\u00F9ng gi\u1EA3 \u0111\u1ECBnh d\u00E2n s\u1ED1 \u0111\u01A1n gi\u1EA3n, kh\u00F4ng ph\u1EA3i d\u1EF1 b\u00E1o y khoa cho c\u00E1 nh\u00E2n."),

    life = c(en = "Life Clock", vi = "\u0110\u1ED3ng h\u1ED3 cu\u1ED9c s\u1ED1ng"),
    life_desc = c(en = "See your age in real time and a few playful estimates about your journey around the Sun.", vi = "Xem tu\u1ED5i theo th\u1EDDi gian th\u1EF1c v\u00E0 v\u00E0i \u01B0\u1EDBc t\u00EDnh vui v\u1EC1 h\u00E0nh tr\u00ECnh quanh M\u1EB7t Tr\u1EDDi."),
    birthday = c(en = "Date of birth", vi = "Ng\u00E0y sinh"),
    life_expectancy = c(en = "Reference life expectancy (years)", vi = "K\u1EF3 v\u1ECDng s\u1ED1ng tham chi\u1EBFu (n\u0103m)"),
    calculate = c(en = "Calculate", vi = "T\u00EDnh"),
    age_now = c(en = "You have been alive for", vi = "B\u1EA1n \u0111\u00E3 s\u1ED1ng \u0111\u01B0\u1EE3c"),
    years = c(en = "years", vi = "n\u0103m"),
    months = c(en = "months", vi = "th\u00E1ng"),
    days = c(en = "days", vi = "ng\u00E0y"),
    hours = c(en = "hours", vi = "gi\u1EDD"),
    minutes = c(en = "minutes", vi = "ph\u00FAt"),
    orbit = c(en = "Trips around the Sun", vi = "V\u00F2ng quanh M\u1EB7t Tr\u1EDDi"),
    orbit_note = c(en = "Earth has carried you around the Sun about this many times.", vi = "Tr\u00E1i \u0110\u1EA5t \u0111\u00E3 \u0111\u01B0a b\u1EA1n \u0111i quanh M\u1EB7t Tr\u1EDDi x\u1EA5p x\u1EC9 b\u1EA5y nhi\u00EAu v\u00F2ng."),
    orbital_distance = c(en = "Orbital distance travelled", vi = "Qu\u00E3ng \u0111\u01B0\u1EDDng qu\u1EF9 \u0111\u1EA1o \u0111\u00E3 \u0111i"),
    orbital_note = c(en = "Approximate distance carried by Earth's orbit around the Sun.", vi = "Kho\u1EA3ng c\u00E1ch x\u1EA5p x\u1EC9 do Tr\u00E1i \u0110\u1EA5t mang b\u1EA1n theo qu\u1EF9 \u0111\u1EA1o quanh M\u1EB7t Tr\u1EDDi."),
    sunrises = c(en = "Sunrises experienced", vi = "S\u1ED1 b\u00ECnh minh \u0111\u00E3 tr\u1EA3i qua"),
    weekends = c(en = "Weekends experienced", vi = "S\u1ED1 cu\u1ED1i tu\u1EA7n \u0111\u00E3 tr\u1EA3i qua"),
    heartbeats = c(en = "Estimated heartbeats", vi = "Nh\u1ECBp tim \u01B0\u1EDBc t\u00EDnh"),
    sleep_time = c(en = "Estimated time asleep", vi = "Th\u1EDDi gian ng\u1EE7 \u01B0\u1EDBc t\u00EDnh"),
    next_orbit = c(en = "Current trip around the Sun", vi = "V\u00F2ng quanh M\u1EB7t Tr\u1EDDi hi\u1EC7n t\u1EA1i"),
    reference_remaining = c(en = "Reference life-expectancy benchmark", vi = "M\u1ED1c k\u1EF3 v\u1ECDng s\u1ED1ng tham chi\u1EBFu"),
    reference_left = c(en = "About %s years remain to this population benchmark.", vi = "C\u00F2n kho\u1EA3ng %s n\u0103m \u0111\u1EBFn m\u1ED1c tham chi\u1EBFu d\u00E2n s\u1ED1 n\u00E0y."),
    reference_passed = c(en = "You have already passed this population benchmark.", vi = "B\u1EA1n \u0111\u00E3 v\u01B0\u1EE3t qua m\u1ED1c tham chi\u1EBFu d\u00E2n s\u1ED1 n\u00E0y."),
    life_remaining_intro = c(en = "Based on this reference, you have about", vi = "Theo m\u1ED1c tham chi\u1EBFu n\u00E0y, b\u1EA1n c\u00F2n kho\u1EA3ng"),
    life_remaining_end = c(en = "until the reference benchmark.", vi = "\u0111\u1EBFn m\u1ED1c k\u1EF3 v\u1ECDng s\u1ED1ng tham chi\u1EBFu."),
    life_percent_line = c(en = "You have lived %s%% of the selected reference life expectancy.", vi = "B\u1EA1n \u0111\u00E3 tr\u1EA3i qua %s%% k\u1EF3 v\u1ECDng s\u1ED1ng tham chi\u1EBFu \u0111\u00E3 ch\u1ECDn."),
    life_cherish = c(en = "Treasure and enjoy every moment of life.", vi = "H\u00E3y tr\u00E2n tr\u1ECDng v\u00E0 t\u1EADn h\u01B0\u1EDFng t\u1EEBng kho\u1EA3nh kh\u1EAFc c\u1EE7a cu\u1ED9c s\u1ED1ng."),
    life_beyond_message = c(en = "You have already lived beyond the selected reference life expectancy.", vi = "B\u1EA1n \u0111\u00E3 s\u1ED1ng v\u01B0\u1EE3t m\u1ED1c k\u1EF3 v\u1ECDng s\u1ED1ng tham chi\u1EBFu \u0111\u00E3 ch\u1ECDn."),
    life_reference_note = c(en = "Life expectancy is a population average, not a deadline and not a personal prediction.", vi = "K\u1EF3 v\u1ECDng s\u1ED1ng l\u00E0 m\u1EE9c trung b\u00ECnh c\u1EE7a d\u00E2n s\u1ED1, kh\u00F4ng ph\u1EA3i th\u1EDDi h\u1EA1n v\u00E0 kh\u00F4ng ph\u1EA3i d\u1EF1 b\u00E1o tu\u1ED5i th\u1ECD c\u00E1 nh\u00E2n."),
    life_reference_progress = c(en = "Share of reference life expectancy lived", vi = "T\u1EF7 l\u1EC7 k\u1EF3 v\u1ECDng s\u1ED1ng tham chi\u1EBFu \u0111\u00E3 tr\u1EA3i qua"),
    life_fun_title = c(en = "A few fun ways to look at your journey", vi = "M\u1ED9t v\u00E0i c\u00E1ch vui \u0111\u1EC3 nh\u00ECn l\u1EA1i h\u00E0nh tr\u00ECnh"),
    life_interpret_title = c(en = "What do these numbers mean?", vi = "Nh\u1EEFng con s\u1ED1 n\u00E0y c\u00F3 ngh\u0129a g\u00EC?"),
    sunrises_note = c(en = "Approximately one sunrise for each day lived; this is a playful count, not the number of sunrises you literally saw.", vi = "X\u1EA5p x\u1EC9 m\u1ED9t b\u00ECnh minh cho m\u1ED7i ng\u00E0y \u0111\u00E3 s\u1ED1ng; \u0111\u00E2y l\u00E0 c\u00E1ch \u0111\u1EBFm vui, kh\u00F4ng ph\u1EA3i s\u1ED1 l\u1EA7n b\u1EA1n th\u1EF1c s\u1EF1 nh\u00ECn th\u1EA5y b\u00ECnh minh."),
    weekends_note = c(en = "A playful estimate based on one weekend per seven days.", vi = "\u01AF\u1EDBc t\u00EDnh vui d\u1EF1a tr\u00EAn m\u1ED7i b\u1EA3y ng\u00E0y c\u00F3 m\u1ED9t cu\u1ED1i tu\u1EA7n."),
    heartbeats_note = c(en = "Estimated using an average heart rate of 72 beats per minute.", vi = "\u01AF\u1EDBc t\u00EDnh v\u1EDBi nh\u1ECBp tim trung b\u00ECnh 72 l\u1EA7n m\u1ED7i ph\u00FAt."),
    sleep_note = c(en = "Estimated by assuming about eight hours of sleep per day.", vi = "\u01AF\u1EDBc t\u00EDnh v\u1EDBi gi\u1EA3 \u0111\u1ECBnh ng\u1EE7 kho\u1EA3ng t\u00E1m gi\u1EDD m\u1ED7i ng\u00E0y."),

    trick = c(en = "Trick", vi = "M\u1EB9o"),
    trick_memory = c(en = "Chunk the sequence into groups of 2-4 characters instead of memorizing one character at a time. Silently say each chunk as one unit; at higher levels, look for repeats and small patterns.", vi = "H\u00E3y chia chu\u1ED7i th\u00E0nh t\u1EEBng nh\u00F3m 2-4 k\u00FD t\u1EF1 thay v\u00EC nh\u1EDB t\u1EEBng k\u00FD t\u1EF1. Nh\u1EA9m m\u1ED7i nh\u00F3m nh\u01B0 m\u1ED9t \u0111\u01A1n v\u1ECB; \u1EDF c\u1EA5p cao h\u00E3y t\u00ECm k\u00FD t\u1EF1 l\u1EB7p l\u1EA1i v\u00E0 c\u00E1c m\u1EABu nh\u1ECF."),
    trick_cards = c(en = "Open cards systematically, row by row. Once you know the locations of a matching pair, collect that pair before revealing more unknown cards.", vi = "L\u1EADt th\u1EBB c\u00F3 h\u1EC7 th\u1ED1ng theo t\u1EEBng h\u00E0ng. Khi \u0111\u00E3 nh\u1EDB v\u1ECB tr\u00ED c\u1EE7a m\u1ED9t c\u1EB7p, h\u00E3y gh\u00E9p c\u1EB7p \u0111\u00F3 tr\u01B0\u1EDBc khi m\u1EDF th\u00EAm th\u1EBB ch\u01B0a bi\u1EBFt."),
    trick_odd = c(en = "Do not read every symbol. Relax your focus and scan the grid in blocks; the different shape often pops out faster in peripheral vision.", vi = "\u0110\u1EEBng \u0111\u1ECDc t\u1EEBng k\u00FD t\u1EF1. H\u00E3y th\u1EA3 l\u1ECFng \u00E1nh nh\u00ECn v\u00E0 qu\u00E9t theo t\u1EEBng kh\u1ED1i; h\u00ECnh kh\u00E1c bi\u1EC7t th\u01B0\u1EDDng b\u1EADt ra nhanh h\u01A1n trong t\u1EA7m nh\u00ECn ngo\u1EA1i vi."),
    trick_higher = c(en = "Estimate the order of magnitude, not the exact number. Anchor on one familiar fact, then eliminate the side that is physically implausible.", vi = "H\u00E3y \u01B0\u1EDBc l\u01B0\u1EE3ng b\u1EADc l\u1EDBn thay v\u00EC c\u1ED1 nh\u1EDB con s\u1ED1 ch\u00EDnh x\u00E1c. D\u00F9ng m\u1ED9t s\u1EF1 ki\u1EC7n quen thu\u1ED9c l\u00E0m m\u1ED1c r\u1ED3i lo\u1EA1i ph\u01B0\u01A1ng \u00E1n kh\u00F3 h\u1EE3p l\u00FD."),
    trick_tf = c(en = "Answer obvious facts immediately and keep moving. When uncertain, absolute words such as 'always' or 'never' can be clues, but they are not guarantees.", vi = "Tr\u1EA3 l\u1EDDi ngay nh\u1EEFng c\u00E2u ch\u1EAFc ch\u1EAFn v\u00E0 ti\u1EBFp t\u1EE5c th\u1EADt nhanh. Khi ph\u00E2n v\u00E2n, c\u00E1c t\u1EEB tuy\u1EC7t \u0111\u1ED1i nh\u01B0 'lu\u00F4n lu\u00F4n' hay 'kh\u00F4ng bao gi\u1EDD' c\u00F3 th\u1EC3 l\u00E0 g\u1EE3i \u00FD, nh\u01B0ng kh\u00F4ng ph\u1EA3i l\u00FAc n\u00E0o c\u0169ng \u0111\u00FAng."),
    trick_maze = c(en = "Keep one hand on one wall: always follow the right wall or always follow the left wall. This maze is generated as a perfect maze, so wall-following will eventually reach the goal, although it may not be the shortest route.", vi = "H\u00E3y t\u01B0\u1EDFng t\u01B0\u1EE3ng lu\u00F4n \u0111\u1EB7t m\u1ED9t tay l\u00EAn t\u01B0\u1EDDng: lu\u00F4n b\u00E1m t\u01B0\u1EDDng b\u00EAn ph\u1EA3i ho\u1EB7c lu\u00F4n b\u00E1m t\u01B0\u1EDDng b\u00EAn tr\u00E1i. M\u00EA cung n\u00E0y \u0111\u01B0\u1EE3c sinh theo d\u1EA1ng perfect maze n\u00EAn c\u00E1ch n\u00E0y cu\u1ED1i c\u00F9ng s\u1EBD \u0111\u1EBFn \u0111\u00EDch, d\u00F9 c\u00F3 th\u1EC3 kh\u00F4ng ph\u1EA3i \u0111\u01B0\u1EDDng ng\u1EAFn nh\u1EA5t."),
    trick_lights = c(en = "Pressing the same cell twice cancels itself, so each cell only needs to be considered once. A powerful method is row chasing: after choosing presses on the first row, press a cell in each next row whenever the light directly above it is on.", vi = "B\u1EA5m c\u00F9ng m\u1ED9t \u00F4 hai l\u1EA7n s\u1EBD tri\u1EC7t ti\u00EAu nhau, v\u00EC v\u1EADy m\u1ED7i \u00F4 ch\u1EC9 c\u1EA7n x\u00E9t m\u1ED9t l\u1EA7n. M\u1ED9t c\u00E1ch m\u1EA1nh l\u00E0 'row chasing': sau khi quy\u1EBFt \u0111\u1ECBnh h\u00E0ng \u0111\u1EA7u, \u1EDF m\u1ED7i h\u00E0ng sau h\u00E3y b\u1EA5m \u00F4 n\u1EBFu \u0111\u00E8n ngay ph\u00EDa tr\u00EAn n\u00F3 c\u00F2n s\u00E1ng."),
    trick_mastermind = c(en = "Start with guesses containing several different digits so you learn more about which symbols are present. Then keep known symbols and move them systematically using the Exact and Misplaced counts.", vi = "H\u00E3y b\u1EAFt \u0111\u1EA7u b\u1EB1ng m\u1ED9t d\u00E3y c\u00F3 nhi\u1EC1u ch\u1EEF s\u1ED1 kh\u00E1c nhau \u0111\u1EC3 bi\u1EBFt nhanh nh\u1EEFng s\u1ED1 n\u00E0o c\u00F3 trong m\u00E3. Sau \u0111\u00F3 gi\u1EEF l\u1EA1i c\u00E1c s\u1ED1 \u0111\u00E3 bi\u1EBFt v\u00E0 di chuy\u1EC3n ch\u00FAng c\u00F3 h\u1EC7 th\u1ED1ng d\u1EF1a v\u00E0o s\u1ED1 \u0110\u00FAng v\u1ECB tr\u00ED v\u00E0 Sai v\u1ECB tr\u00ED."),
    trick_emoji = c(en = "Name each emoji literally first, then combine the words into a phrase, title, place, or idea. The simplest literal combination is often the intended answer.", vi = "Tr\u01B0\u1EDBc h\u1EBFt h\u00E3y g\u1ECDi t\u00EAn t\u1EEBng emoji theo ngh\u0129a \u0111en, sau \u0111\u00F3 gh\u00E9p c\u00E1c t\u1EEB th\u00E0nh c\u1EE5m t\u1EEB, t\u00EAn, \u0111\u1ECBa danh ho\u1EB7c \u00FD t\u01B0\u1EDFng. C\u00E1ch gh\u00E9p \u0111\u01A1n gi\u1EA3n nh\u1EA5t th\u01B0\u1EDDng l\u00E0 \u0111\u00E1p \u00E1n."),
    trick_wheel = c(en = "If you keep spinning until you like the result, you may already know what you really wanted. Use the first spin when you truly want a random choice.", vi = "N\u1EBFu b\u1EA1n c\u1EE9 quay l\u1EA1i cho \u0111\u1EBFn khi th\u00EDch k\u1EBFt qu\u1EA3, c\u00F3 l\u1EBD b\u1EA1n \u0111\u00E3 bi\u1EBFt m\u00ECnh th\u1EF1c s\u1EF1 mu\u1ED1n g\u00EC. Khi th\u1EF1c s\u1EF1 c\u1EA7n ng\u1EABu nhi\u00EAn, h\u00E3y t\u00F4n tr\u1ECDng l\u1EA7n quay \u0111\u1EA7u ti\u00EAn."),
    trick_fortune = c(en = "There is no winning here. Open another cookie until one makes you smile, then keep that message for the day.", vi = "Tr\u00F2 n\u00E0y kh\u00F4ng c\u00F3 th\u1EAFng thua. H\u00E3y m\u1EDF th\u00EAm cho \u0111\u1EBFn khi c\u00F3 m\u1ED9t c\u00E2u l\u00E0m b\u1EA1n m\u1EC9m c\u01B0\u1EDDi, r\u1ED3i gi\u1EEF c\u00E2u \u0111\u00F3 cho ng\u00E0y h\u00F4m nay."),

    memory = c(en = "Memory Sequence", vi = "Chu\u1ED7i ghi nh\u1EDB"),
    memory_desc = c(en = "Memorize a sequence before it disappears. Every correct answer makes the next level harder.", vi = "Ghi nh\u1EDB chu\u1ED7i tr\u01B0\u1EDBc khi n\u00F3 bi\u1EBFn m\u1EA5t. M\u1ED7i c\u00E2u \u0111\u00FAng s\u1EBD l\u00E0m c\u1EA5p ti\u1EBFp theo kh\u00F3 h\u01A1n."),
    memory_start = c(en = "Show sequence", vi = "Hi\u1EC7n chu\u1ED7i"),
    memory_wait = c(en = "Memorize this...", vi = "H\u00E3y ghi nh\u1EDB..."),
    memory_hidden = c(en = "Now type the sequence", vi = "B\u00E2y gi\u1EDD h\u00E3y nh\u1EADp l\u1EA1i chu\u1ED7i"),
    memory_answer = c(en = "Your answer", vi = "C\u00E2u tr\u1EA3 l\u1EDDi"),
    memory_hint = c(en = "Spaces are optional. Higher levels may include letters.", vi = "C\u00F3 th\u1EC3 c\u00F3 ho\u1EB7c kh\u00F4ng c\u00F3 kho\u1EA3ng tr\u1EAFng. C\u1EA5p cao c\u00F3 th\u1EC3 c\u00F3 ch\u1EEF c\u00E1i."),

    cards = c(en = "Memory Cards", vi = "L\u1EADt th\u1EBB ghi nh\u1EDB"),
    cards_desc = c(en = "Flip two cards at a time and find every matching pair.", vi = "L\u1EADt hai th\u1EBB m\u1ED7i l\u1EA7n v\u00E0 t\u00ECm t\u1EA5t c\u1EA3 c\u00E1c c\u1EB7p gi\u1ED1ng nhau."),
    difficulty = c(en = "Difficulty", vi = "\u0110\u1ED9 kh\u00F3"),
    pairs = c(en = "Pairs", vi = "C\u1EB7p"),

    odd = c(en = "Odd One Out", vi = "T\u00ECm k\u00FD t\u1EF1 kh\u00E1c"),
    odd_desc = c(en = "Find the one symbol that is different. The grid becomes denser as your streak grows.", vi = "T\u00ECm m\u1ED9t k\u00FD t\u1EF1 kh\u00E1c bi\u1EC7t. L\u01B0\u1EDBi s\u1EBD d\u00E0y h\u01A1n khi chu\u1ED7i \u0111\u00FAng t\u0103ng."),
    find_odd = c(en = "Find the different one", vi = "H\u00E3y t\u00ECm k\u00FD t\u1EF1 kh\u00E1c"),

    higher = c(en = "Higher or Lower", vi = "Cao h\u01A1n hay th\u1EA5p h\u01A1n"),
    higher_desc = c(en = "Choose which side has the larger value. Build the longest streak you can.", vi = "Ch\u1ECDn ph\u00EDa c\u00F3 gi\u00E1 tr\u1ECB l\u1EDBn h\u01A1n v\u00E0 c\u1ED1 g\u1EAFng t\u1EA1o chu\u1ED7i \u0111\u00FAng d\u00E0i nh\u1EA5t."),
    which_larger = c(en = "Which is larger?", vi = "C\u00E1i n\u00E0o l\u1EDBn h\u01A1n?"),

    tf = c(en = "True or False Rush", vi = "\u0110\u00FAng hay Sai si\u00EAu t\u1ED1c"),
    tf_desc = c(en = "Answer as many general-knowledge questions as you can before the clock runs out.", vi = "Tr\u1EA3 l\u1EDDi c\u00E0ng nhi\u1EC1u c\u00E2u ki\u1EBFn th\u1EE9c ph\u1ED5 th\u00F4ng c\u00E0ng t\u1ED1t tr\u01B0\u1EDBc khi h\u1EBFt gi\u1EDD."),
    true = c(en = "TRUE", vi = "\u0110\u00DANG"),
    false = c(en = "FALSE", vi = "SAI"),
    start_60 = c(en = "Start 60-second round", vi = "B\u1EAFt \u0111\u1EA7u v\u00F2ng 60 gi\u00E2y"),
    time_left = c(en = "Time left", vi = "Th\u1EDDi gian c\u00F2n l\u1EA1i"),

    maze = c(en = "Maze", vi = "M\u00EA cung"),
    maze_desc = c(en = "Find your way from the start to the goal. A new maze is generated every time.", vi = "T\u00ECm \u0111\u01B0\u1EDDng t\u1EEB \u0111i\u1EC3m b\u1EAFt \u0111\u1EA7u \u0111\u1EBFn \u0111\u00EDch. M\u1ED7i v\u00E1n l\u00E0 m\u1ED9t m\u00EA cung m\u1EDBi."),
    up = c(en = "Up", vi = "L\u00EAn"),
    down = c(en = "Down", vi = "Xu\u1ED1ng"),
    left = c(en = "Left", vi = "Tr\u00E1i"),
    right = c(en = "Right", vi = "Ph\u1EA3i"),
    goal = c(en = "Goal", vi = "\u0110\u00EDch"),

    lights = c(en = "Lights Out", vi = "T\u1EAFt h\u1EBFt \u0111\u00E8n"),
    lights_desc = c(en = "Turn every light off. Each click toggles that light and its direct neighbors.", vi = "T\u1EAFt t\u1EA5t c\u1EA3 \u0111\u00E8n. M\u1ED7i l\u1EA7n b\u1EA5m s\u1EBD \u0111\u1ED5i tr\u1EA1ng th\u00E1i \u00F4 \u0111\u00F3 v\u00E0 c\u00E1c \u00F4 k\u1EC1 b\u00EAn."),
    lights_left = c(en = "Lights on", vi = "\u0110\u00E8n c\u00F2n s\u00E1ng"),

    mastermind = c(en = "Mastermind", vi = "Mastermind"),
    mastermind_desc = c(en = "Crack a four-symbol secret code. Exact means right symbol and position; misplaced means right symbol, wrong position.", vi = "Gi\u1EA3i m\u00E3 chu\u1ED7i b\u1ED1n k\u00FD hi\u1EC7u. \u0110\u00FAng v\u1ECB tr\u00ED ngh\u0129a l\u00E0 \u0111\u00FAng k\u00FD hi\u1EC7u v\u00E0 v\u1ECB tr\u00ED; sai v\u1ECB tr\u00ED ngh\u0129a l\u00E0 \u0111\u00FAng k\u00FD hi\u1EC7u nh\u01B0ng kh\u00E1c v\u1ECB tr\u00ED."),
    guess = c(en = "Guess", vi = "D\u1EF1 \u0111o\u00E1n"),
    mastermind_hint = c(en = "Use four digits from 1 to 6, for example 1234.", vi = "D\u00F9ng b\u1ED1n ch\u1EEF s\u1ED1 t\u1EEB 1 \u0111\u1EBFn 6, v\u00ED d\u1EE5 1234."),
    exact = c(en = "Exact", vi = "\u0110\u00FAng v\u1ECB tr\u00ED"),
    misplaced = c(en = "Misplaced", vi = "Sai v\u1ECB tr\u00ED"),
    attempts = c(en = "Attempts", vi = "L\u1EA7n th\u1EED"),
    code_was = c(en = "The code was", vi = "M\u00E3 \u0111\u00FAng l\u00E0"),

    emoji = c(en = "Emoji Quiz", vi = "\u0110o\u00E1n \u00FD qua emoji"),
    emoji_desc = c(en = "Guess the word or idea represented by the emoji clue.", vi = "\u0110o\u00E1n t\u1EEB ho\u1EB7c \u00FD t\u01B0\u1EDFng \u0111\u01B0\u1EE3c bi\u1EC3u di\u1EC5n b\u1EB1ng emoji."),
    choose_answer = c(en = "Choose an answer", vi = "Ch\u1ECDn c\u00E2u tr\u1EA3 l\u1EDDi"),

    wheel = c(en = "Decision Wheel", vi = "V\u00F2ng quay quy\u1EBFt \u0111\u1ECBnh"),
    wheel_desc = c(en = "Cannot decide? Add your options and let the wheel choose.", vi = "Kh\u00F3 quy\u1EBFt \u0111\u1ECBnh? Nh\u1EADp c\u00E1c l\u1EF1a ch\u1ECDn v\u00E0 \u0111\u1EC3 v\u00F2ng quay ch\u1ECDn gi\u00FAp."),
    options = c(en = "Options (one per line)", vi = "C\u00E1c l\u1EF1a ch\u1ECDn (m\u1ED7i d\u00F2ng m\u1ED9t l\u1EF1a ch\u1ECDn)"),
    spin = c(en = "Spin", vi = "Quay"),
    winner = c(en = "Selected", vi = "\u0110\u01B0\u1EE3c ch\u1ECDn"),
    need_two = c(en = "Enter at least two options.", vi = "H\u00E3y nh\u1EADp \u00EDt nh\u1EA5t hai l\u1EF1a ch\u1ECDn."),

    fortune = c(en = "Fortune Cookie", vi = "B\u00E1nh may m\u1EAFn"),
    fortune_desc = c(en = "Open a tiny message for a quick smile or a gentle reminder.", vi = "M\u1EDF m\u1ED9t th\u00F4ng \u0111i\u1EC7p nh\u1ECF \u0111\u1EC3 m\u1EC9m c\u01B0\u1EDDi ho\u1EB7c nh\u1EAFc m\u00ECnh ngh\u1EC9 m\u1ED9t ch\u00FAt."),
    open_cookie = c(en = "Open a cookie", vi = "M\u1EDF b\u00E1nh"),

    quick_fun = c(en = "Quick fun", vi = "Ch\u01A1i nhanh"),
    puzzles = c(en = "Puzzles", vi = "C\u00E2u \u0111\u1ED1"),
    chill = c(en = "Chill", vi = "Th\u01B0 gi\u00E3n"),
    choose_activity = c(en = "Choose something to do", vi = "Ch\u1ECDn m\u1ED9t ho\u1EA1t \u0111\u1ED9ng"),
    back_home = c(en = "Back to Playground", vi = "V\u1EC1 Playground")
  )

  function(key, lang = "en") {
    z <- dict[[key]]
    if (is.null(z)) return(key)
    out <- unname(z[[lang]])
    if (is.null(out) || is.na(out)) unname(z[["en"]]) else out
  }
})

.r4fun_game_labels <- function(lang = "en") {
  keys <- c("home", "life", "memory", "cards", "odd", "higher", "tf", "maze", "lights", "mastermind", "emoji", "wheel", "fortune")
  ids <- c("home", "life", "memory", "cards", "odd", "higher", "tf", "maze", "lights", "mastermind", "emoji", "wheel", "fortune")
  stats::setNames(ids, vapply(keys, .r4fun_text, character(1), lang = lang))
}

.r4fun_trick <- function(key, lang = "en") {
  shiny::tags$details(
    class = "r4-trick",
    shiny::tags$summary(paste0("\U0001F3AF ", .r4fun_text("trick", lang))),
    shiny::div(.r4fun_text(key, lang))
  )
}

.r4fun_fmt <- function(x, digits = 0L) {
  format(round(x, digits), big.mark = ",", scientific = FALSE, trim = TRUE, nsmall = digits)
}

.r4fun_age_components <- function(birthday, now = Sys.time()) {
  birth_time <- as.POSIXct(as.Date(birthday), tz = "UTC")
  now_utc <- as.POSIXct(now, tz = "UTC")
  sec <- max(0, as.numeric(difftime(now_utc, birth_time, units = "secs")))
  year_sec <- 365.2425 * 24 * 3600
  month_sec <- 30.436875 * 24 * 3600
  years <- floor(sec / year_sec)
  sec <- sec - years * year_sec
  months <- floor(sec / month_sec)
  sec <- sec - months * month_sec
  days <- floor(sec / (24 * 3600))
  sec <- sec - days * 24 * 3600
  hours <- floor(sec / 3600)
  sec <- sec - hours * 3600
  minutes <- floor(sec / 60)
  seconds <- floor(sec - minutes * 60)
  list(years = years, months = months, days = days, hours = hours, minutes = minutes, seconds = seconds)
}

.r4fun_birthday_progress <- function(birthday, today = Sys.Date()) {
  b <- as.Date(birthday)
  by <- as.integer(format(b, "%Y"))
  ty <- as.integer(format(today, "%Y"))
  md <- format(b, "%m-%d")
  safe_date <- function(year, md) {
    x <- as.Date(paste0(year, "-", md))
    if (is.na(x) && identical(md, "02-29")) x <- as.Date(paste0(year, "-02-28"))
    x
  }
  last <- safe_date(ty, md)
  if (today < last) last <- safe_date(ty - 1L, md)
  nextb <- safe_date(as.integer(format(last, "%Y")) + 1L, md)
  den <- as.numeric(nextb - last)
  num <- as.numeric(today - last)
  if (!is.finite(den) || den <= 0) return(0)
  max(0, min(100, 100 * num / den))
}

.r4fun_memory_sequence <- function(level) {
  n <- min(14L, 2L + as.integer(level))
  alphabet <- if (level <= 5L) as.character(0:9) else c(as.character(0:9), LETTERS[1:8])
  sample(alphabet, n, replace = TRUE)
}

.r4fun_cards_symbols <- function(n_pairs) {
  pool <- c(
    "\U0001F34E", "\U0001F431", "\U0001F436", "\U0001F680", "\U0001F31F", "\U0001F319",
    "\U0001F33B", "\U0001F3B5", "\U0001F355", "\U0001F98B", "\U0001F422", "\U0001F3C0"
  )
  sample(pool, n_pairs, replace = FALSE)
}

.r4fun_odd_pairs <- function() {
  list(
    c("O", "0"), c("8", "B"), c("C", "G"), c("5", "S"), c("1", "I"),
    c("M", "N"), c("X", "K"), c("6", "G"), c("P", "R"), c("Q", "O")
  )
}

.r4fun_higher_bank <- function() {
  data.frame(
    left = c("Earth", "Mars", "Jupiter", "Mount Everest", "Pacific Ocean", "Blue whale", "Cheetah", "Moon", "Saturn", "Sahara Desert", "Amazon River", "Ostrich", "Venus", "Mercury", "Greenland", "Elephant", "Giraffe", "Eagle", "Python", "Great White Shark"),
    right = c("Mars", "Mercury", "Saturn", "K2", "Atlantic Ocean", "African elephant", "Horse", "Pluto", "Neptune", "Arabian Desert", "Nile River", "Emu", "Earth", "Moon", "Madagascar", "Hippo", "Camel", "Falcon", "Cobra", "Dolphin"),
    left_value = c(12742, 6779, 139820, 8849, 165.25, 150000, 120, 3474, 116460, 9.2, 6400, 70, 12104, 4879, 2166086, 6000, 5.5, 240, 6, 1100),
    right_value = c(6779, 4879, 116460, 8611, 106.46, 6000, 88, 2377, 49244, 2.3, 6650, 50, 12742, 3474, 587041, 3200, 2.1, 390, 4, 200),
    unit = c("diameter km", "diameter km", "diameter km", "height m", "area million km2", "typical mass kg", "top speed km/h", "diameter km", "diameter km", "area million km2", "approx. length km", "top speed km/h", "diameter km", "diameter km", "area km2", "typical mass kg", "height m", "dive speed km/h", "approx. length m", "approx. mass kg"),
    stringsAsFactors = FALSE
  )
}

.r4fun_tf_bank <- function() {
  data.frame(
    en = c(
      "Octopuses have three hearts.",
      "Venus is the closest planet to the Sun.",
      "Adult humans usually have 206 bones.",
      "A group of flamingos is called a flamboyance.",
      "Sound travels faster in water than in air.",
      "The Great Wall of China is visible from the Moon with the naked eye.",
      "Bananas are berries in botanical terms.",
      "Sharks are mammals.",
      "The Pacific Ocean is larger than the Atlantic Ocean.",
      "Lightning can be hotter than the surface of the Sun.",
      "Penguins live naturally at the North Pole.",
      "A day on Venus is longer than a year on Venus.",
      "The human body has four lungs.",
      "Honey can remain edible for a very long time when sealed properly.",
      "The chemical symbol for gold is Au.",
      "An ostrich can fly long distances.",
      "Water expands when it freezes.",
      "The Moon produces its own visible light.",
      "Some turtles can breathe partly through their cloaca.",
      "Mount Everest is the tallest mountain above sea level."
    ),
    vi = c(
      "B\u1EA1ch tu\u1ED9c c\u00F3 ba tr\u00E1i tim.",
      "Sao Kim l\u00E0 h\u00E0nh tinh g\u1EA7n M\u1EB7t Tr\u1EDDi nh\u1EA5t.",
      "Ng\u01B0\u1EDDi tr\u01B0\u1EDFng th\u00E0nh th\u01B0\u1EDDng c\u00F3 206 x\u01B0\u01A1ng.",
      "M\u1ED9t nh\u00F3m h\u1ED3ng h\u1EA1c trong ti\u1EBFng Anh c\u00F3 th\u1EC3 \u0111\u01B0\u1EE3c g\u1ECDi l\u00E0 a flamboyance.",
      "\u00C2m thanh truy\u1EC1n trong n\u01B0\u1EDBc nhanh h\u01A1n trong kh\u00F4ng kh\u00ED.",
      "C\u00F3 th\u1EC3 nh\u00ECn th\u1EA5y V\u1EA1n L\u00FD Tr\u01B0\u1EDDng Th\u00E0nh t\u1EEB M\u1EB7t Tr\u0103ng b\u1EB1ng m\u1EAFt th\u01B0\u1EDDng.",
      "Theo th\u1EF1c v\u1EADt h\u1ECDc, chu\u1ED1i l\u00E0 m\u1ED9t lo\u1EA1i qu\u1EA3 m\u1ECDng.",
      "C\u00E1 m\u1EADp l\u00E0 \u0111\u1ED9ng v\u1EADt c\u00F3 v\u00FA.",
      "Th\u00E1i B\u00ECnh D\u01B0\u01A1ng l\u1EDBn h\u01A1n \u0110\u1EA1i T\u00E2y D\u01B0\u01A1ng.",
      "Tia s\u00E9t c\u00F3 th\u1EC3 n\u00F3ng h\u01A1n b\u1EC1 m\u1EB7t M\u1EB7t Tr\u1EDDi.",
      "Chim c\u00E1nh c\u1EE5t s\u1ED1ng t\u1EF1 nhi\u00EAn \u1EDF B\u1EAFc C\u1EF1c.",
      "M\u1ED9t ng\u00E0y tr\u00EAn Sao Kim d\u00E0i h\u01A1n m\u1ED9t n\u0103m tr\u00EAn Sao Kim.",
      "C\u01A1 th\u1EC3 ng\u01B0\u1EDDi c\u00F3 b\u1ED1n l\u00E1 ph\u1ED5i.",
      "M\u1EADt ong c\u00F3 th\u1EC3 gi\u1EEF \u0111\u01B0\u1EE3c r\u1EA5t l\u00E2u n\u1EBFu b\u1EA3o qu\u1EA3n k\u00EDn \u0111\u00FAng c\u00E1ch.",
      "K\u00FD hi\u1EC7u h\u00F3a h\u1ECDc c\u1EE7a v\u00E0ng l\u00E0 Au.",
      "\u0110\u00E0 \u0111i\u1EC3u c\u00F3 th\u1EC3 bay \u0111\u01B0\u1EDDng d\u00E0i.",
      "N\u01B0\u1EDBc n\u1EDF ra khi \u0111\u00F4ng b\u0103ng.",
      "M\u1EB7t Tr\u0103ng t\u1EF1 ph\u00E1t ra \u00E1nh s\u00E1ng nh\u00ECn th\u1EA5y.",
      "M\u1ED9t s\u1ED1 lo\u00E0i r\u00F9a c\u00F3 th\u1EC3 h\u00F4 h\u1EA5p m\u1ED9t ph\u1EA7n qua huy\u1EC7t.",
      "Everest l\u00E0 ng\u1ECDn n\u00FAi cao nh\u1EA5t so v\u1EDBi m\u1EF1c n\u01B0\u1EDBc bi\u1EC3n."
    ),
    answer = c(TRUE, FALSE, TRUE, TRUE, TRUE, FALSE, TRUE, FALSE, TRUE, TRUE, FALSE, TRUE, FALSE, TRUE, TRUE, FALSE, TRUE, FALSE, TRUE, TRUE),
    stringsAsFactors = FALSE
  )
}

.r4fun_emoji_bank <- function() {
  list(
    list(clue = "\U0001F981 \U0001F451", en = "The Lion King", vi = "Vua S\u01B0 T\u1EED", wrong_en = c("Jungle Book", "King Kong", "Toy Story"), wrong_vi = c("C\u1EADu b\u00E9 r\u1EEBng xanh", "King Kong", "C\u00E2u chuy\u1EC7n \u0111\u1ED3 ch\u01A1i")),
    list(clue = "\U0001F30D \U0001F4A7", en = "Water world", vi = "Th\u1EBF gi\u1EDBi n\u01B0\u1EDBc", wrong_en = c("Dry planet", "Fire world", "Moon walk"), wrong_vi = c("H\u00E0nh tinh kh\u00F4", "Th\u1EBF gi\u1EDBi l\u1EEDa", "D\u1EA1o b\u01B0\u1EDBc M\u1EB7t Tr\u0103ng")),
    list(clue = "\U0001F355 \U0001F1EE\U0001F1F9", en = "Italian pizza", vi = "Pizza \u00DD", wrong_en = c("French bread", "Japanese sushi", "Mexican taco"), wrong_vi = c("B\u00E1nh m\u00EC Ph\u00E1p", "Sushi Nh\u1EADt", "Taco Mexico")),
    list(clue = "\U0001F319 \U0001F6CC", en = "Good night", vi = "Ch\u00FAc ng\u1EE7 ngon", wrong_en = c("Good morning", "Late lunch", "Long walk"), wrong_vi = c("Ch\u00E0o bu\u1ED5i s\u00E1ng", "B\u1EEFa tr\u01B0a mu\u1ED9n", "\u0110i b\u1ED9 d\u00E0i")),
    list(clue = "\U0001F4A1 \U0001F9E0", en = "Bright idea", vi = "\u00DD t\u01B0\u1EDFng hay", wrong_en = c("Dark room", "Heavy thinking", "Brain freeze"), wrong_vi = c("Ph\u00F2ng t\u1ED1i", "Suy ngh\u0129 n\u1EB7ng n\u1EC1", "\u0110\u01A1 n\u00E3o")),
    list(clue = "\U0001F4DA \U00002615", en = "Study break", vi = "Ngh\u1EC9 gi\u1EA3i lao khi h\u1ECDc", wrong_en = c("Coffee shop", "Book sale", "Morning class"), wrong_vi = c("Qu\u00E1n c\u00E0 ph\u00EA", "B\u00E1n s\u00E1ch", "L\u1EDBp bu\u1ED5i s\u00E1ng")),
    list(clue = "\U0001F3C3 \U0001F4A8", en = "Run fast", vi = "Ch\u1EA1y nhanh", wrong_en = c("Sleep well", "Drive slowly", "Stand still"), wrong_vi = c("Ng\u1EE7 ngon", "L\u00E1i ch\u1EADm", "\u0110\u1EE9ng y\u00EAn")),
    list(clue = "\U0001F30A \U0001F3C4", en = "Surfing", vi = "L\u01B0\u1EDBt s\u00F3ng", wrong_en = c("Swimming pool", "Fishing", "Sailing"), wrong_vi = c("H\u1ED3 b\u01A1i", "C\u00E2u c\u00E1", "\u0110i thuy\u1EC1n")),
    list(clue = "\U0001F308 \U0001F327", en = "Rainbow after rain", vi = "C\u1EA7u v\u1ED3ng sau m\u01B0a", wrong_en = c("Sunny night", "Snow storm", "Cloudy desert"), wrong_vi = c("\u0110\u00EAm n\u1EAFng", "B\u00E3o tuy\u1EBFt", "Sa m\u1EA1c nhi\u1EC1u m\u00E2y")),
    list(clue = "\U0001F4F7 \U0001F31F", en = "Picture perfect", vi = "B\u1EE9c \u1EA3nh ho\u00E0n h\u1EA3o", wrong_en = c("Broken camera", "Night shift", "Star map"), wrong_vi = c("M\u00E1y \u1EA3nh h\u1ECFng", "Ca \u0111\u00EAm", "B\u1EA3n \u0111\u1ED3 sao"))
  )
}

.r4fun_fortunes <- function(lang = "en") {
  if (lang == "vi") {
    return(c(
      "Ngh\u1EC9 n\u0103m ph\u00FAt c\u00F3 th\u1EC3 gi\u1EA3i quy\u1EBFt m\u1ED9t v\u1EA5n \u0111\u1EC1 m\u00E0 nh\u00ECn m\u00E0n h\u00ECnh m\u1ED9t gi\u1EDD ch\u01B0a ch\u1EAFc gi\u1EA3i \u0111\u01B0\u1EE3c.",
      "M\u1ED9t ly n\u01B0\u1EDBc c\u00F3 th\u1EC3 l\u00E0 b\u01B0\u1EDBc ph\u00E2n t\u00EDch d\u1EEF li\u1EC7u quan tr\u1ECDng nh\u1EA5t l\u00FAc n\u00E0y.",
      "N\u1EBFu code kh\u00F4ng ch\u1EA1y, c\u00F3 th\u1EC3 code c\u0169ng \u0111ang c\u1EA7n ngh\u1EC9 gi\u1EA3i lao.",
      "H\u00F4m nay l\u00E0 m\u1ED9t ng\u00E0y t\u1ED1t \u0111\u1EC3 \u0111\u00F3ng m\u1ED9t tab tr\u00ECnh duy\u1EC7t.",
      "Kh\u00F4ng ph\u1EA3i m\u1ECDi quy\u1EBFt \u0111\u1ECBnh \u0111\u1EC1u c\u1EA7n p-value.",
      "\u0110\u00F4i khi k\u1EBFt qu\u1EA3 t\u1ED1t nh\u1EA5t xu\u1EA5t hi\u1EC7n sau khi b\u1EA1n \u0111i b\u1ED9 m\u1ED9t v\u00F2ng.",
      "H\u00E3y l\u01B0u file tr\u01B0\u1EDBc khi v\u0169 tr\u1EE5 quy\u1EBFt \u0111\u1ECBnh th\u1EED b\u1EA1n.",
      "B\u1EA1n kh\u00F4ng c\u1EA7n t\u1ED1i \u01B0u m\u1ECDi ph\u00FAt trong ng\u00E0y.",
      "M\u1ED9t vi\u1EC7c nh\u1ECF ho\u00E0n th\u00E0nh v\u1EABn l\u00E0 ti\u1EBFn b\u1ED9.",
      "N\u1EBFu kh\u00F4ng bi\u1EBFt ch\u1ECDn g\u00EC, h\u00E3y th\u1EED n\u00E9m m\u1ED9t \u0111\u1ED3ng xu - r\u1ED3i xem b\u1EA1n hy v\u1ECDng m\u1EB7t n\u00E0o xu\u1EA5t hi\u1EC7n."
    ))
  }
  c(
    "A five-minute break may solve a problem an hour of staring cannot.",
    "A glass of water may be the most important analysis step right now.",
    "If the code will not run, perhaps the code also needs a short break.",
    "Today is a good day to close one browser tab.",
    "Not every decision needs a p-value.",
    "Some of the best solutions arrive after a short walk.",
    "Save your file before the universe decides to test you.",
    "You do not need to optimize every minute of the day.",
    "Finishing one small thing still counts as progress.",
    "If you cannot decide, flip a coin - then notice which result you secretly hoped for."
  )
}

.r4fun_maze_generate <- function(n = 8L) {
  n <- as.integer(n)
  north <- matrix(TRUE, n, n)
  east <- matrix(TRUE, n, n)
  south <- matrix(TRUE, n, n)
  west <- matrix(TRUE, n, n)
  visited <- matrix(FALSE, n, n)
  stack_r <- integer(n * n)
  stack_c <- integer(n * n)
  top <- 1L
  stack_r[top] <- 1L
  stack_c[top] <- 1L
  visited[1, 1] <- TRUE

  while (top > 0L) {
    r <- stack_r[top]
    c <- stack_c[top]
    dirs <- character()
    if (r > 1L && !visited[r - 1L, c]) dirs <- c(dirs, "N")
    if (c < n && !visited[r, c + 1L]) dirs <- c(dirs, "E")
    if (r < n && !visited[r + 1L, c]) dirs <- c(dirs, "S")
    if (c > 1L && !visited[r, c - 1L]) dirs <- c(dirs, "W")

    if (!length(dirs)) {
      top <- top - 1L
    } else {
      d <- sample(dirs, 1L)
      nr <- r
      nc <- c
      if (d == "N") {
        north[r, c] <- FALSE
        south[r - 1L, c] <- FALSE
        nr <- r - 1L
      } else if (d == "E") {
        east[r, c] <- FALSE
        west[r, c + 1L] <- FALSE
        nc <- c + 1L
      } else if (d == "S") {
        south[r, c] <- FALSE
        north[r + 1L, c] <- FALSE
        nr <- r + 1L
      } else {
        west[r, c] <- FALSE
        east[r, c - 1L] <- FALSE
        nc <- c - 1L
      }
      visited[nr, nc] <- TRUE
      top <- top + 1L
      stack_r[top] <- nr
      stack_c[top] <- nc
    }
  }

  list(n = n, north = north, east = east, south = south, west = west)
}

.r4fun_lights_generate <- function(n) {
  board <- matrix(FALSE, n, n)
  toggle <- function(b, r, c) {
    pts <- rbind(c(r, c), c(r - 1L, c), c(r + 1L, c), c(r, c - 1L), c(r, c + 1L))
    for (k in seq_len(nrow(pts))) {
      rr <- pts[k, 1L]
      cc <- pts[k, 2L]
      if (rr >= 1L && rr <= n && cc >= 1L && cc <= n) b[rr, cc] <- !b[rr, cc]
    }
    b
  }
  n_moves <- max(3L, ceiling(n * n * 0.35))
  for (k in seq_len(n_moves)) {
    r <- sample.int(n, 1L)
    c <- sample.int(n, 1L)
    board <- toggle(board, r, c)
  }
  if (!any(board)) board <- toggle(board, 1L, 1L)
  board
}

.r4fun_light_toggle <- function(board, r, c) {
  n <- nrow(board)
  pts <- rbind(c(r, c), c(r - 1L, c), c(r + 1L, c), c(r, c - 1L), c(r, c + 1L))
  for (k in seq_len(nrow(pts))) {
    rr <- pts[k, 1L]
    cc <- pts[k, 2L]
    if (rr >= 1L && rr <= n && cc >= 1L && cc <= n) board[rr, cc] <- !board[rr, cc]
  }
  board
}

.r4fun_mastermind_score <- function(code, guess) {
  exact <- sum(code == guess)
  code_left <- code[code != guess]
  guess_left <- guess[code != guess]
  misplaced <- 0L
  for (d in unique(guess_left)) {
    misplaced <- misplaced + min(sum(code_left == d), sum(guess_left == d))
  }
  c(exact = exact, misplaced = misplaced)
}

.r4fun_css <- function() {
  paste0(
    ":root{--r4-bg:#f6f8fb;--r4-card:#ffffff;--r4-text:#172033;--r4-muted:#667085;--r4-border:#e4e7ec;--r4-accent:#2563eb;--r4-accent2:#eaf1ff;--r4-good:#15803d;--r4-bad:#b42318;}",
    "body{background:var(--r4-bg);color:var(--r4-text);font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;}",
    ".container-fluid{max-width:1180px;margin:0 auto;padding:0 18px 40px;}",
    ".r4-header{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding:22px 0 14px;border-bottom:1px solid var(--r4-border);margin-bottom:18px;}",
    ".r4-brand h1{font-size:28px;margin:0 0 5px;font-weight:750;letter-spacing:-.02em;}",
    ".r4-brand p{margin:0;color:var(--r4-muted);font-size:14px;}",
    ".r4-top-controls{display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;justify-content:flex-end;}",
    ".r4-top-controls .form-group{margin:0;min-width:160px;}",
    ".r4-card{background:var(--r4-card);border:1px solid var(--r4-border);border-radius:18px;padding:20px;margin-bottom:16px;box-shadow:0 1px 2px rgba(16,24,40,.03);}",
    ".r4-hero{padding:26px;}",
    ".r4-hero h2{font-size:24px;margin:0 0 8px;}",
    ".r4-hero p{margin:0;color:var(--r4-muted);}",
    ".r4-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;}",
    ".r4-tile{background:var(--r4-card);border:1px solid var(--r4-border);border-radius:16px;padding:18px;min-height:126px;display:flex;flex-direction:column;justify-content:space-between;}",
    ".r4-tile:hover{border-color:#b8c7e6;transform:translateY(-1px);transition:.15s ease;}",
    ".r4-tile-title{font-size:17px;font-weight:700;margin:8px 0 4px;}",
    ".r4-tile-text{font-size:13px;color:var(--r4-muted);margin-bottom:12px;}",
    ".r4-tile .btn{width:100%;}",
    ".r4-section-title{font-size:15px;text-transform:uppercase;letter-spacing:.08em;color:var(--r4-muted);margin:22px 0 10px;font-weight:750;}",
    ".r4-metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-top:14px;}",
    ".r4-metric{background:#fbfcfe;border:1px solid var(--r4-border);border-radius:14px;padding:14px;}",
    ".r4-metric-label{font-size:12px;color:var(--r4-muted);margin-bottom:5px;}",
    ".r4-metric-value{font-size:21px;font-weight:760;line-height:1.15;}",
    ".r4-metric-note{font-size:12px;color:var(--r4-muted);margin-top:6px;}",
    ".r4-big{font-size:34px;font-weight:800;letter-spacing:.06em;text-align:center;padding:24px 10px;min-height:90px;display:flex;align-items:center;justify-content:center;}",
    ".r4-status{text-align:center;padding:10px;font-weight:650;}",
    ".r4-good{color:var(--r4-good);}",
    ".r4-bad{color:var(--r4-bad);}",
    ".r4-note{font-size:12px;color:var(--r4-muted);margin-top:10px;}",
    ".r4-life-story{padding:12px 4px 4px;text-align:center;}",
    ".r4-life-line{font-size:22px;line-height:1.45;font-weight:700;margin:7px 0;}",
    ".r4-life-cherish{font-size:20px;line-height:1.45;font-weight:750;margin:12px 0 4px;color:#9f1239;}",
    ".r4-life-section{margin-top:20px;padding-top:4px;}",
    ".r4-life-section h3{font-size:18px;margin:0 0 10px;}",
    ".r4-life-explain{margin:12px 0 0;padding-left:22px;color:var(--r4-muted);}",
    ".r4-life-explain li{margin:7px 0;line-height:1.45;}",
    ".r4-trick{margin:10px 0 16px;border:1px solid #d9e4f5;border-radius:12px;background:#f8fbff;padding:0 12px;}",
    ".r4-trick summary{cursor:pointer;font-weight:700;padding:10px 0;color:#1e3a8a;}",
    ".r4-trick>div{padding:0 0 12px;color:var(--r4-muted);line-height:1.5;}",
    ".r4-progress{height:10px;background:#eaecf0;border-radius:999px;overflow:hidden;margin-top:8px;}",
    ".r4-progress>div{height:100%;background:var(--r4-accent);}",
    ".r4-statline{display:flex;gap:18px;flex-wrap:wrap;color:var(--r4-muted);font-size:13px;margin:8px 0 14px;}",
    ".r4-statline strong{color:var(--r4-text);}",
    ".r4-center{text-align:center;}",
    ".r4-btn-row{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;margin-top:12px;}",
    ".r4-card-grid{display:grid;gap:8px;justify-content:center;}",
    ".r4-memory-card{height:68px;font-size:28px;font-weight:700;border-radius:12px!important;}",
    ".r4-odd-grid{display:grid;gap:5px;justify-content:center;margin:12px auto;}",
    ".r4-odd-btn{width:42px;height:42px;padding:0!important;font-size:20px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}",
    ".r4-choice{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px;}",
    ".r4-choice .btn{min-height:92px;font-size:18px;white-space:normal;}",
    ".r4-maze{display:grid;max-width:620px;margin:15px auto;background:#fff;}",
    ".r4-maze-cell{aspect-ratio:1/1;display:flex;align-items:center;justify-content:center;font-size:15px;box-sizing:border-box;}",
    ".r4-player{width:56%;height:56%;border-radius:50%;background:var(--r4-accent);}",
    ".r4-goal{width:46%;height:46%;border-radius:6px;background:#16a34a;}",
    ".r4-light-grid{display:grid;gap:7px;justify-content:center;margin:15px auto;}",
    ".r4-light{width:54px;height:54px;padding:0!important;border-radius:12px!important;}",
    ".r4-light.on{background:#facc15!important;border-color:#eab308!important;box-shadow:0 0 0 3px rgba(250,204,21,.18);}",
    ".r4-light.off{background:#e5e7eb!important;border-color:#d1d5db!important;}",
    ".r4-history{max-height:270px;overflow:auto;border-top:1px solid var(--r4-border);margin-top:14px;padding-top:10px;}",
    ".r4-history-row{display:grid;grid-template-columns:90px 1fr 1fr;gap:8px;font-size:13px;padding:6px 0;border-bottom:1px solid #f2f4f7;}",
    ".r4-emoji{font-size:58px;text-align:center;padding:18px;}",
    ".r4-wheel-wrap{display:flex;flex-direction:column;align-items:center;gap:10px;}",
    "#r4fun_wheel_canvas{width:min(420px,88vw);height:min(420px,88vw);max-width:420px;max-height:420px;}",
    ".r4-fortune{font-size:22px;line-height:1.5;text-align:center;padding:26px;}",
    ".btn-primary{background:var(--r4-accent);border-color:var(--r4-accent);}",
    "@media(max-width:720px){.r4-header{flex-direction:column}.r4-top-controls{width:100%;justify-content:flex-start}.r4-top-controls .form-group{min-width:0;flex:1}.r4-choice{grid-template-columns:1fr}.r4-big{font-size:28px}.r4-light{width:44px;height:44px}.r4-odd-btn{width:36px;height:36px}.r4-history-row{grid-template-columns:70px 1fr 1fr}}"
  )
}

.r4fun_js <- function() {
  paste0(
    "(function(){",
    "if(window.__r4funWheelReady)return;window.__r4funWheelReady=true;",
    "function drawWheel(items,rotation){var c=document.getElementById('r4fun_wheel_canvas');if(!c)return;var dpr=window.devicePixelRatio||1;var size=Math.min(420,Math.max(260,c.clientWidth||360));c.width=size*dpr;c.height=size*dpr;var x=c.getContext('2d');x.setTransform(dpr,0,0,dpr,0,0);x.clearRect(0,0,size,size);var cx=size/2,cy=size/2,r=size*.44,n=items.length;if(!n)return;var base=rotation||0;for(var i=0;i<n;i++){var a0=base+i*2*Math.PI/n-Math.PI/2;var a1=base+(i+1)*2*Math.PI/n-Math.PI/2;x.beginPath();x.moveTo(cx,cy);x.arc(cx,cy,r,a0,a1);x.closePath();x.fillStyle=(i%2===0)?'#dbeafe':'#eff6ff';x.fill();x.strokeStyle='#93c5fd';x.stroke();x.save();x.translate(cx,cy);x.rotate((a0+a1)/2);x.textAlign='right';x.fillStyle='#1e3a8a';x.font='600 13px system-ui';var t=items[i];if(t.length>18)t=t.slice(0,17)+'...';x.fillText(t,r-12,4);x.restore();}x.beginPath();x.arc(cx,cy,r*.11,0,Math.PI*2);x.fillStyle='#2563eb';x.fill();x.fillStyle='#fff';x.font='700 12px system-ui';x.textAlign='center';x.fillText('R4VN',cx,cy+4);x.beginPath();x.moveTo(cx,10);x.lineTo(cx-12,34);x.lineTo(cx+12,34);x.closePath();x.fillStyle='#111827';x.fill();}",
    "Shiny.addCustomMessageHandler('r4fun-wheel-draw',function(m){drawWheel(m.items||[],0);});",
    "Shiny.addCustomMessageHandler('r4fun-wheel-spin',function(m){var items=m.items||[];var winner=m.winner||0;var start=performance.now();var dur=2600;var n=Math.max(1,items.length);var target=8*Math.PI-(winner+.5)*2*Math.PI/n;function f(now){var p=Math.min(1,(now-start)/dur);var e=1-Math.pow(1-p,3);drawWheel(items,target*e);if(p<1)requestAnimationFrame(f);}requestAnimationFrame(f);});",
    "})();"
  )
}

.r4fun_home_ui <- function(lang) {
  tile <- function(id, icon, title_key, desc_key) {
    shiny::div(
      class = "r4-tile",
      shiny::div(
        shiny::div(style = "font-size:30px;", icon),
        shiny::div(class = "r4-tile-title", .r4fun_text(title_key, lang)),
        shiny::div(class = "r4-tile-text", .r4fun_text(desc_key, lang))
      ),
      shiny::actionButton(paste0("go_", id), .r4fun_text("start", lang), class = "btn btn-default")
    )
  }

  shiny::tagList(
    shiny::div(class = "r4-card r4-hero", shiny::h2(.r4fun_text("choose_activity", lang)), shiny::p(.r4fun_text("app_subtitle", lang))),
    shiny::div(class = "r4-section-title", .r4fun_text("quick_fun", lang)),
    shiny::div(class = "r4-grid",
      tile("memory", "\U0001F9E0", "memory", "memory_desc"),
      tile("cards", "\U0001F0CF", "cards", "cards_desc"),
      tile("odd", "\U0001F50D", "odd", "odd_desc"),
      tile("higher", "\U00002195", "higher", "higher_desc"),
      tile("tf", "\U000026A1", "tf", "tf_desc"),
      tile("emoji", "\U0001F60E", "emoji", "emoji_desc")
    ),
    shiny::div(class = "r4-section-title", .r4fun_text("puzzles", lang)),
    shiny::div(class = "r4-grid",
      tile("maze", "\U0001F9E9", "maze", "maze_desc"),
      tile("lights", "\U0001F4A1", "lights", "lights_desc"),
      tile("mastermind", "\U0001F512", "mastermind", "mastermind_desc")
    ),
    shiny::div(class = "r4-section-title", .r4fun_text("chill", lang)),
    shiny::div(class = "r4-grid",
      tile("life", "\U0001F382", "life", "life_desc"),
      tile("wheel", "\U0001F3A1", "wheel", "wheel_desc"),
      tile("fortune", "\U0001F960", "fortune", "fortune_desc")
    )
  )
}

.r4fun_life_ui <- function(lang) {
  shiny::div(
    class = "r4-card",
    shiny::h2(.r4fun_text("life", lang)),
    shiny::p(.r4fun_text("life_desc", lang)),
    shiny::fluidRow(
      shiny::column(6, shiny::dateInput("life_birthday", .r4fun_text("birthday", lang), value = Sys.Date() - 365.2425 * 30, format = "dd/mm/yyyy")),
      shiny::column(6, shiny::numericInput("life_expectancy", .r4fun_text("life_expectancy", lang), value = 74.7, min = 1, max = 130, step = 0.1))
    ),
    shiny::uiOutput("life_result")
  )
}

.r4fun_memory_ui <- function(lang, level, best) {
  shiny::div(
    class = "r4-card",
    shiny::h2(.r4fun_text("memory", lang)),
    shiny::p(.r4fun_text("memory_desc", lang)),
    .r4fun_trick("trick_memory", lang),
    shiny::div(class = "r4-statline",
      shiny::span(.r4fun_text("level", lang), ": ", shiny::strong(level)),
      shiny::span(.r4fun_text("best", lang), ": ", shiny::strong(best))
    ),
    shiny::actionButton("memory_start", .r4fun_text("memory_start", lang), class = "btn btn-primary"),
    shiny::uiOutput("memory_display"),
    shiny::uiOutput("memory_answer_ui"),
    shiny::uiOutput("memory_feedback")
  )
}

.r4fun_cards_ui <- function(lang) {
  shiny::div(
    class = "r4-card",
    shiny::h2(.r4fun_text("cards", lang)),
    shiny::p(.r4fun_text("cards_desc", lang)),
    .r4fun_trick("trick_cards", lang),
    shiny::fluidRow(
      shiny::column(6, shiny::selectInput("cards_difficulty", .r4fun_text("difficulty", lang), choices = stats::setNames(c("easy", "medium", "hard"), c(.r4fun_text("easy", lang), .r4fun_text("medium", lang), .r4fun_text("hard", lang))), selected = "easy")),
      shiny::column(6, shiny::br(), shiny::actionButton("cards_new", .r4fun_text("new_game", lang), class = "btn btn-primary"))
    ),
    shiny::uiOutput("cards_stats"),
    shiny::uiOutput("cards_board")
  )
}

.r4fun_odd_ui <- function(lang) {
  shiny::div(
    class = "r4-card",
    shiny::h2(.r4fun_text("odd", lang)),
    shiny::p(.r4fun_text("odd_desc", lang)),
    .r4fun_trick("trick_odd", lang),
    shiny::div(class = "r4-center", shiny::strong(.r4fun_text("find_odd", lang))),
    shiny::uiOutput("odd_stats"),
    shiny::uiOutput("odd_board"),
    shiny::uiOutput("odd_feedback")
  )
}

.r4fun_higher_ui <- function(lang) {
  shiny::div(
    class = "r4-card",
    shiny::h2(.r4fun_text("higher", lang)),
    shiny::p(.r4fun_text("higher_desc", lang)),
    .r4fun_trick("trick_higher", lang),
    shiny::uiOutput("higher_stats"),
    shiny::uiOutput("higher_question")
  )
}

.r4fun_tf_ui <- function(lang) {
  shiny::div(
    class = "r4-card",
    shiny::h2(.r4fun_text("tf", lang)),
    shiny::p(.r4fun_text("tf_desc", lang)),
    .r4fun_trick("trick_tf", lang),
    shiny::actionButton("tf_start", .r4fun_text("start_60", lang), class = "btn btn-primary"),
    shiny::uiOutput("tf_stats"),
    shiny::uiOutput("tf_question"),
    shiny::uiOutput("tf_feedback")
  )
}

.r4fun_maze_ui <- function(lang) {
  shiny::div(
    class = "r4-card",
    shiny::h2(.r4fun_text("maze", lang)),
    shiny::p(.r4fun_text("maze_desc", lang)),
    .r4fun_trick("trick_maze", lang),
    shiny::fluidRow(
      shiny::column(6, shiny::selectInput("maze_difficulty", .r4fun_text("difficulty", lang), choices = stats::setNames(c("easy", "medium", "hard"), c(.r4fun_text("easy", lang), .r4fun_text("medium", lang), .r4fun_text("hard", lang))), selected = "easy")),
      shiny::column(6, shiny::br(), shiny::actionButton("maze_new", .r4fun_text("new_game", lang), class = "btn btn-primary"))
    ),
    shiny::uiOutput("maze_stats"),
    shiny::uiOutput("maze_board"),
    shiny::div(class = "r4-btn-row",
      shiny::actionButton("maze_up", .r4fun_text("up", lang)),
      shiny::actionButton("maze_left", .r4fun_text("left", lang)),
      shiny::actionButton("maze_down", .r4fun_text("down", lang)),
      shiny::actionButton("maze_right", .r4fun_text("right", lang))
    ),
    shiny::uiOutput("maze_feedback")
  )
}

.r4fun_lights_ui <- function(lang) {
  shiny::div(
    class = "r4-card",
    shiny::h2(.r4fun_text("lights", lang)),
    shiny::p(.r4fun_text("lights_desc", lang)),
    .r4fun_trick("trick_lights", lang),
    shiny::fluidRow(
      shiny::column(6, shiny::selectInput("lights_difficulty", .r4fun_text("difficulty", lang), choices = stats::setNames(c("easy", "medium", "hard"), c(.r4fun_text("easy", lang), .r4fun_text("medium", lang), .r4fun_text("hard", lang))), selected = "easy")),
      shiny::column(6, shiny::br(), shiny::actionButton("lights_new", .r4fun_text("new_game", lang), class = "btn btn-primary"))
    ),
    shiny::uiOutput("lights_stats"),
    shiny::uiOutput("lights_board"),
    shiny::uiOutput("lights_feedback")
  )
}

.r4fun_mastermind_ui <- function(lang) {
  shiny::div(
    class = "r4-card",
    shiny::h2(.r4fun_text("mastermind", lang)),
    shiny::p(.r4fun_text("mastermind_desc", lang)),
    shiny::div(class = "r4-note", .r4fun_text("mastermind_hint", lang)),
    .r4fun_trick("trick_mastermind", lang),
    shiny::textInput("mm_guess", .r4fun_text("guess", lang), placeholder = "1234"),
    shiny::div(class = "r4-btn-row",
      shiny::actionButton("mm_submit", .r4fun_text("submit", lang), class = "btn btn-primary"),
      shiny::actionButton("mm_new", .r4fun_text("new_game", lang))
    ),
    shiny::uiOutput("mm_status"),
    shiny::uiOutput("mm_history")
  )
}

.r4fun_emoji_ui <- function(lang) {
  shiny::div(
    class = "r4-card",
    shiny::h2(.r4fun_text("emoji", lang)),
    shiny::p(.r4fun_text("emoji_desc", lang)),
    .r4fun_trick("trick_emoji", lang),
    shiny::uiOutput("emoji_stats"),
    shiny::uiOutput("emoji_question"),
    shiny::uiOutput("emoji_feedback")
  )
}

.r4fun_wheel_ui <- function(lang) {
  shiny::div(
    class = "r4-card",
    shiny::h2(.r4fun_text("wheel", lang)),
    shiny::p(.r4fun_text("wheel_desc", lang)),
    .r4fun_trick("trick_wheel", lang),
    shiny::fluidRow(
      shiny::column(5,
        shiny::textAreaInput("wheel_options", .r4fun_text("options", lang), rows = 8, value = if (lang == "vi") "Ph\u1EDF\nPizza\nSushi\nB\u00E1nh m\u00EC" else "Pizza\nPho\nSushi\nBurger"),
        shiny::actionButton("wheel_spin", .r4fun_text("spin", lang), class = "btn btn-primary")
      ),
      shiny::column(7,
        shiny::div(class = "r4-wheel-wrap", shiny::tags$canvas(id = "r4fun_wheel_canvas"), shiny::uiOutput("wheel_result"))
      )
    )
  )
}

.r4fun_fortune_ui <- function(lang) {
  shiny::div(
    class = "r4-card",
    shiny::h2(.r4fun_text("fortune", lang)),
    shiny::p(.r4fun_text("fortune_desc", lang)),
    .r4fun_trick("trick_fortune", lang),
    shiny::div(class = "r4-center", shiny::actionButton("fortune_open", .r4fun_text("open_cookie", lang), class = "btn btn-primary")),
    shiny::uiOutput("fortune_text")
  )
}

.r4fun_app <- function(default_language = "en") {
  .r4fun_need("shiny")
  default_language <- match.arg(default_language, c("en", "vi"))

  ui <- shiny::fluidPage(
    shiny::tags$head(
      shiny::tags$meta(charset = "UTF-8"),
      shiny::tags$style(shiny::HTML(.r4fun_css())),
      shiny::tags$script(shiny::HTML(.r4fun_js()))
    ),
    shiny::div(
      class = "r4-header",
      shiny::div(class = "r4-brand", shiny::h1("R4VN Playground"), shiny::uiOutput("header_subtitle")),
      shiny::div(
        class = "r4-top-controls",
        shiny::selectInput("language", "Language", choices = c("English" = "en", "Ti\u1EBFng Vi\u1EC7t" = "vi"), selected = default_language, width = "170px"),
        shiny::selectInput("game", "Activity", choices = .r4fun_game_labels(default_language), selected = "home", width = "230px"),
        shiny::actionButton("surprise", "Surprise me")
      )
    ),
    shiny::uiOutput("main")
  )

  server <- function(input, output, session) {
    lang <- shiny::reactive(.r4fun_or(input$language, default_language))

    output$header_subtitle <- shiny::renderUI({
      shiny::p(.r4fun_text("app_subtitle", lang()))
    })

    shiny::observeEvent(input$language, {
      current <- .r4fun_or(input$game, "home")
      shiny::updateSelectInput(session, "game", label = .r4fun_text("game", lang()), choices = .r4fun_game_labels(lang()), selected = current)
      shiny::updateActionButton(session, "surprise", label = .r4fun_text("surprise", lang()))
    }, ignoreInit = FALSE)

    game_ids <- c("life", "memory", "cards", "odd", "higher", "tf", "maze", "lights", "mastermind", "emoji", "wheel", "fortune")
    shiny::observeEvent(input$surprise, {
      shiny::updateSelectInput(session, "game", selected = sample(game_ids, 1L))
    })

    for (id in game_ids) {
      local({
        game_id <- id
        shiny::observeEvent(input[[paste0("go_", game_id)]], {
          shiny::updateSelectInput(session, "game", selected = game_id)
        })
      })
    }

    # Life Clock -----------------------------------------------------------
    output$life_result <- shiny::renderUI({
      shiny::invalidateLater(1000, session)
      shiny::req(input$life_birthday, input$life_expectancy)
      now <- Sys.time()
      birthday <- as.Date(input$life_birthday)
      if (is.na(birthday) || birthday > Sys.Date()) {
        return(shiny::div(class = "r4-status r4-bad", if (lang() == "vi") "Ng\u00E0y sinh kh\u00F4ng h\u1EE3p l\u1EC7" else "Invalid date"))
      }

      age_days <- as.numeric(difftime(now, as.POSIXct(birthday), units = "days"))
      age_years <- age_days / 365.2425
      comp <- .r4fun_age_components(birthday, now)
      orbit_progress <- .r4fun_birthday_progress(birthday, Sys.Date())
      orbit_distance_km <- age_years * 939900000
      hb <- age_days * 24 * 60 * 72
      sleep_years <- age_years / 3
      weekends <- age_days / 7
      remain <- input$life_expectancy - age_years
      life_pct <- 100 * age_years / input$life_expectancy
      life_bar <- max(0, min(100, life_pct))

      age_line <- paste(
        comp$years, .r4fun_text("years", lang()),
        comp$months, .r4fun_text("months", lang()),
        comp$days, .r4fun_text("days", lang()),
        comp$hours, .r4fun_text("hours", lang()),
        comp$minutes, .r4fun_text("minutes", lang()),
        comp$seconds, .r4fun_text("seconds", lang())
      )

      if (remain > 0) {
        remain_days <- remain * 365.2425
        remain_year <- floor(remain_days / 365.2425)
        remain_days <- remain_days - remain_year * 365.2425
        remain_month <- floor(remain_days / 30.436875)
        remain_days <- remain_days - remain_month * 30.436875
        remain_day <- floor(remain_days)
        remaining_line <- paste(
          .r4fun_text("life_remaining_intro", lang()),
          remain_year, .r4fun_text("years", lang()),
          remain_month, .r4fun_text("months", lang()),
          remain_day, .r4fun_text("days", lang()),
          .r4fun_text("life_remaining_end", lang())
        )
      } else {
        remaining_line <- .r4fun_text("life_beyond_message", lang())
      }

      metric <- function(label, value) {
        shiny::div(
          class = "r4-metric",
          shiny::div(class = "r4-metric-label", label),
          shiny::div(class = "r4-metric-value", value)
        )
      }

      shiny::tagList(
        shiny::div(
          class = "r4-life-story",
          shiny::div(class = "r4-life-line", "\U0001F382 ", .r4fun_text("age_now", lang()), " ", age_line, "."),
          shiny::div(class = "r4-life-line", "\u231B ", remaining_line),
          shiny::div(class = "r4-life-line", "\U0001F4CA ", sprintf(.r4fun_text("life_percent_line", lang()), .r4fun_fmt(life_pct, 1))),
          shiny::div(class = "r4-life-cherish", "\u2764\uFE0F ", .r4fun_text("life_cherish", lang()))
        ),

        shiny::div(
          class = "r4-card",
          style = "margin-top:12px;background:#fbfcfe;",
          shiny::strong(.r4fun_text("life_reference_progress", lang()), ": ", .r4fun_fmt(life_pct, 1), "%"),
          shiny::div(class = "r4-progress", shiny::div(style = paste0("width:", life_bar, "%"))),
          shiny::div(class = "r4-note", .r4fun_text("life_reference_note", lang()))
        ),

        shiny::div(
          class = "r4-life-section",
          shiny::h3(.r4fun_text("life_fun_title", lang())),
          shiny::div(
            class = "r4-metrics",
            metric(.r4fun_text("orbit", lang()), .r4fun_fmt(age_years, 2)),
            metric(.r4fun_text("orbital_distance", lang()), paste0(.r4fun_fmt(orbit_distance_km / 1e9, 1), " billion km")),
            metric(.r4fun_text("sunrises", lang()), .r4fun_fmt(age_days, 0)),
            metric(.r4fun_text("weekends", lang()), .r4fun_fmt(weekends, 0)),
            metric(.r4fun_text("heartbeats", lang()), paste0(.r4fun_fmt(hb / 1e9, 2), " billion")),
            metric(.r4fun_text("sleep_time", lang()), paste0(.r4fun_fmt(sleep_years, 1), " ", .r4fun_text("years", lang())))
          ),
          shiny::div(
            class = "r4-card",
            style = "margin-top:14px;background:#fbfcfe;",
            shiny::strong(.r4fun_text("next_orbit", lang()), ": ", .r4fun_fmt(orbit_progress, 1), "%"),
            shiny::div(class = "r4-progress", shiny::div(style = paste0("width:", orbit_progress, "%")))
          )
        ),

        shiny::div(
          class = "r4-life-section",
          shiny::h3(.r4fun_text("life_interpret_title", lang())),
          shiny::tags$ul(
            class = "r4-life-explain",
            shiny::tags$li(shiny::strong(.r4fun_text("orbit", lang()), ": "), .r4fun_text("orbit_note", lang())),
            shiny::tags$li(shiny::strong(.r4fun_text("orbital_distance", lang()), ": "), .r4fun_text("orbital_note", lang())),
            shiny::tags$li(shiny::strong(.r4fun_text("sunrises", lang()), ": "), .r4fun_text("sunrises_note", lang())),
            shiny::tags$li(shiny::strong(.r4fun_text("weekends", lang()), ": "), .r4fun_text("weekends_note", lang())),
            shiny::tags$li(shiny::strong(.r4fun_text("heartbeats", lang()), ": "), .r4fun_text("heartbeats_note", lang())),
            shiny::tags$li(shiny::strong(.r4fun_text("sleep_time", lang()), ": "), .r4fun_text("sleep_note", lang()))
          ),
          shiny::div(class = "r4-note", .r4fun_text("fun_estimate", lang()))
        )
      )
    })

    # Memory Sequence ------------------------------------------------------
    memory_level <- shiny::reactiveVal(1L)
    memory_best <- shiny::reactiveVal(1L)
    memory_seq <- shiny::reactiveVal(character())
    memory_show_until <- shiny::reactiveVal(as.POSIXct(NA))
    memory_feedback <- shiny::reactiveVal(NULL)
    memory_ready <- shiny::reactiveVal(FALSE)

    shiny::observeEvent(input$memory_start, {
      lev <- memory_level()
      z <- .r4fun_memory_sequence(lev)
      memory_seq(z)
      show_sec <- max(0.9, 2.8 - 0.12 * lev)
      memory_show_until(Sys.time() + show_sec)
      memory_feedback(NULL)
      memory_ready(FALSE)
      shiny::updateTextInput(session, "memory_answer", value = "")
    })

    output$memory_display <- shiny::renderUI({
      shiny::invalidateLater(100, session)
      z <- memory_seq()
      if (!length(z)) return(shiny::div(class = "r4-big", "..."))
      until <- memory_show_until()
      if (is.finite(as.numeric(until)) && Sys.time() <= until) {
        memory_ready(FALSE)
        shiny::div(class = "r4-big", paste(z, collapse = "  "), shiny::div(class = "r4-note", .r4fun_text("memory_wait", lang())))
      } else {
        memory_ready(TRUE)
        shiny::div(class = "r4-big", "? ? ?", shiny::div(class = "r4-note", .r4fun_text("memory_hidden", lang())))
      }
    })
    shiny::outputOptions(output, "memory_display", suspendWhenHidden = FALSE)

    output$memory_answer_ui <- shiny::renderUI({
      if (!isTRUE(memory_ready())) return(NULL)
      shiny::tagList(
        shiny::textInput("memory_answer", .r4fun_text("memory_answer", lang()), placeholder = "12345"),
        shiny::actionButton("memory_submit", .r4fun_text("submit", lang()), class = "btn btn-primary"),
        shiny::div(class = "r4-note", .r4fun_text("memory_hint", lang()))
      )
    })

    shiny::observeEvent(input$memory_submit, {
      shiny::req(memory_ready(), length(memory_seq()))
      ans <- toupper(gsub("[^A-Za-z0-9]", "", .r4fun_or(input$memory_answer, "")))
      target <- paste(memory_seq(), collapse = "")
      if (identical(ans, target)) {
        new_level <- memory_level() + 1L
        memory_level(new_level)
        memory_best(max(memory_best(), new_level))
        memory_feedback(list(ok = TRUE, text = .r4fun_text("correct", lang())))
        memory_seq(character())
        memory_ready(FALSE)
      } else {
        memory_feedback(list(ok = FALSE, text = paste(.r4fun_text("wrong", lang()), target)))
        memory_level(1L)
        memory_seq(character())
        memory_ready(FALSE)
      }
    })

    output$memory_feedback <- shiny::renderUI({
      z <- memory_feedback()
      if (is.null(z)) return(NULL)
      shiny::div(class = paste("r4-status", if (z$ok) "r4-good" else "r4-bad"), z$text)
    })

    # Memory Cards ---------------------------------------------------------
    cards_state <- shiny::reactiveValues(symbols = character(), revealed = logical(), matched = logical(), first = NA_integer_, second = NA_integer_, mismatch_until = as.POSIXct(NA), moves = 0L, started = Sys.time(), n = 12L)

    cards_new_game <- function(difficulty = "easy") {
      n <- switch(difficulty, easy = 12L, medium = 16L, hard = 24L, 12L)
      sy <- .r4fun_cards_symbols(n / 2L)
      cards_state$symbols <- sample(rep(sy, each = 2L))
      cards_state$revealed <- rep(FALSE, n)
      cards_state$matched <- rep(FALSE, n)
      cards_state$first <- NA_integer_
      cards_state$second <- NA_integer_
      cards_state$mismatch_until <- as.POSIXct(NA)
      cards_state$moves <- 0L
      cards_state$started <- Sys.time()
      cards_state$n <- n
    }

    shiny::observeEvent(input$cards_new, cards_new_game(.r4fun_or(input$cards_difficulty, "easy")))
    shiny::observeEvent(input$cards_difficulty, cards_new_game(input$cards_difficulty), ignoreInit = TRUE)
    shiny::observe({ if (!length(cards_state$symbols)) cards_new_game("easy") })

    shiny::observe({
      shiny::invalidateLater(150, session)
      if (is.finite(as.numeric(cards_state$mismatch_until)) && Sys.time() >= cards_state$mismatch_until) {
        ids <- c(cards_state$first, cards_state$second)
        ids <- ids[is.finite(ids)]
        if (length(ids)) cards_state$revealed[ids] <- FALSE
        cards_state$first <- NA_integer_
        cards_state$second <- NA_integer_
        cards_state$mismatch_until <- as.POSIXct(NA)
      }
    })

    for (i in seq_len(24L)) {
      local({
        ii <- i
        shiny::observeEvent(input[[paste0("mc_card_", ii)]], {
          if (ii > cards_state$n || cards_state$matched[ii] || cards_state$revealed[ii]) return()
          if (is.finite(as.numeric(cards_state$mismatch_until))) return()
          cards_state$revealed[ii] <- TRUE
          if (!is.finite(cards_state$first)) {
            cards_state$first <- ii
          } else {
            cards_state$second <- ii
            cards_state$moves <- cards_state$moves + 1L
            a <- cards_state$first
            b <- cards_state$second
            if (identical(cards_state$symbols[a], cards_state$symbols[b])) {
              cards_state$matched[c(a, b)] <- TRUE
              cards_state$first <- NA_integer_
              cards_state$second <- NA_integer_
            } else {
              cards_state$mismatch_until <- Sys.time() + 0.75
            }
          }
        }, ignoreInit = TRUE)
      })
    }

    output$cards_stats <- shiny::renderUI({
      matched_pairs <- sum(cards_state$matched) / 2L
      shiny::div(class = "r4-statline",
        shiny::span(.r4fun_text("pairs", lang()), ": ", shiny::strong(paste0(matched_pairs, "/", cards_state$n / 2L))),
        shiny::span(.r4fun_text("moves", lang()), ": ", shiny::strong(cards_state$moves)),
        if (cards_state$n > 0L && all(cards_state$matched)) shiny::span(class = "r4-good", shiny::strong(.r4fun_text("finished", lang())))
      )
    })

    output$cards_board <- shiny::renderUI({
      n <- cards_state$n
      cols <- if (n == 12L) 4L else if (n == 16L) 4L else 6L
      btns <- lapply(seq_len(n), function(i) {
        visible <- cards_state$revealed[i] || cards_state$matched[i]
        shiny::actionButton(paste0("mc_card_", i), if (visible) cards_state$symbols[i] else "?", class = "r4-memory-card")
      })
      shiny::div(class = "r4-card-grid", style = paste0("grid-template-columns:repeat(", cols, ",68px);"), btns)
    })

    # Odd One Out ----------------------------------------------------------
    odd_state <- shiny::reactiveValues(level = 1L, streak = 0L, best = 0L, symbols = character(), correct = 1L, n = 25L, feedback = NULL)
    odd_new_round <- function() {
      pair <- sample(.r4fun_odd_pairs(), 1L)[[1L]]
      side <- sample(c(TRUE, FALSE), 1L)
      base <- if (side) pair[1L] else pair[2L]
      odd <- if (side) pair[2L] else pair[1L]
      size <- min(10L, 5L + floor(odd_state$streak / 3L))
      total <- size * size
      correct <- sample.int(total, 1L)
      z <- rep(base, total)
      z[correct] <- odd
      odd_state$symbols <- z
      odd_state$correct <- correct
      odd_state$n <- total
      odd_state$level <- 1L + floor(odd_state$streak / 3L)
      odd_state$feedback <- NULL
    }
    shiny::observe({ if (!length(odd_state$symbols)) odd_new_round() })

    for (i in seq_len(100L)) {
      local({
        ii <- i
        shiny::observeEvent(input[[paste0("odd_cell_", ii)]], {
          if (ii > odd_state$n) return()
          if (ii == odd_state$correct) {
            odd_state$streak <- odd_state$streak + 1L
            odd_state$best <- max(odd_state$best, odd_state$streak)
            odd_state$feedback <- list(ok = TRUE, text = .r4fun_text("correct", lang()))
            odd_new_round()
          } else {
            odd_state$feedback <- list(ok = FALSE, text = .r4fun_text("wrong", lang()))
            odd_state$streak <- 0L
            odd_new_round()
          }
        }, ignoreInit = TRUE)
      })
    }

    output$odd_stats <- shiny::renderUI({
      shiny::div(class = "r4-statline",
        shiny::span(.r4fun_text("level", lang()), ": ", shiny::strong(odd_state$level)),
        shiny::span(.r4fun_text("streak", lang()), ": ", shiny::strong(odd_state$streak)),
        shiny::span(.r4fun_text("best", lang()), ": ", shiny::strong(odd_state$best))
      )
    })
    output$odd_board <- shiny::renderUI({
      size <- as.integer(round(sqrt(odd_state$n)))
      cells <- lapply(seq_len(odd_state$n), function(i) shiny::actionButton(paste0("odd_cell_", i), odd_state$symbols[i], class = "r4-odd-btn"))
      shiny::div(class = "r4-odd-grid", style = paste0("grid-template-columns:repeat(", size, ",42px);"), cells)
    })
    output$odd_feedback <- shiny::renderUI(NULL)

    # Higher or Lower ------------------------------------------------------
    higher_bank <- .r4fun_higher_bank()
    higher_state <- shiny::reactiveValues(index = sample.int(nrow(higher_bank), 1L), streak = 0L, best = 0L, score = 0L, total = 0L, feedback = NULL)
    higher_answer <- function(side) {
      q <- higher_bank[higher_state$index, ]
      correct_side <- if (q$left_value >= q$right_value) "left" else "right"
      ok <- identical(side, correct_side)
      higher_state$total <- higher_state$total + 1L
      if (ok) {
        higher_state$score <- higher_state$score + 1L
        higher_state$streak <- higher_state$streak + 1L
        higher_state$best <- max(higher_state$best, higher_state$streak)
      } else {
        higher_state$streak <- 0L
      }
      higher_state$feedback <- list(ok = ok, text = paste0(q$left, " = ", .r4fun_fmt(q$left_value, 2), "; ", q$right, " = ", .r4fun_fmt(q$right_value, 2), " (", q$unit, ")"))
      choices <- setdiff(seq_len(nrow(higher_bank)), higher_state$index)
      higher_state$index <- sample(choices, 1L)
    }
    shiny::observeEvent(input$higher_left, higher_answer("left"))
    shiny::observeEvent(input$higher_right, higher_answer("right"))

    output$higher_stats <- shiny::renderUI({
      shiny::div(class = "r4-statline",
        shiny::span(.r4fun_text("score", lang()), ": ", shiny::strong(paste0(higher_state$score, "/", higher_state$total))),
        shiny::span(.r4fun_text("streak", lang()), ": ", shiny::strong(higher_state$streak)),
        shiny::span(.r4fun_text("best", lang()), ": ", shiny::strong(higher_state$best))
      )
    })
    output$higher_question <- shiny::renderUI({
      q <- higher_bank[higher_state$index, ]
      shiny::tagList(
        if (!is.null(higher_state$feedback)) shiny::div(class = paste("r4-status", if (higher_state$feedback$ok) "r4-good" else "r4-bad"), higher_state$feedback$text),
        shiny::h3(class = "r4-center", .r4fun_text("which_larger", lang())),
        shiny::div(class = "r4-note r4-center", q$unit),
        shiny::div(class = "r4-choice",
          shiny::actionButton("higher_left", q$left, class = "btn btn-default"),
          shiny::actionButton("higher_right", q$right, class = "btn btn-default")
        )
      )
    })

    # True or False --------------------------------------------------------
    tf_bank <- .r4fun_tf_bank()
    tf_state <- shiny::reactiveValues(active = FALSE, end_time = as.POSIXct(NA), queue = integer(), index = NA_integer_, score = 0L, total = 0L, streak = 0L, best = 0L, feedback = NULL)
    tf_next <- function() {
      if (!length(tf_state$queue)) tf_state$queue <- sample(seq_len(nrow(tf_bank)))
      tf_state$index <- tf_state$queue[1L]
      tf_state$queue <- tf_state$queue[-1L]
    }
    shiny::observeEvent(input$tf_start, {
      tf_state$active <- TRUE
      tf_state$end_time <- Sys.time() + 60
      tf_state$queue <- sample(seq_len(nrow(tf_bank)))
      tf_state$score <- 0L
      tf_state$total <- 0L
      tf_state$streak <- 0L
      tf_state$best <- 0L
      tf_state$feedback <- NULL
      tf_next()
    })
    tf_answer <- function(ans) {
      if (!isTRUE(tf_state$active) || !is.finite(tf_state$index)) return()
      if (Sys.time() >= tf_state$end_time) { tf_state$active <- FALSE; return() }
      ok <- identical(isTRUE(ans), isTRUE(tf_bank$answer[tf_state$index]))
      tf_state$total <- tf_state$total + 1L
      if (ok) {
        tf_state$score <- tf_state$score + 1L
        tf_state$streak <- tf_state$streak + 1L
        tf_state$best <- max(tf_state$best, tf_state$streak)
      } else tf_state$streak <- 0L
      tf_state$feedback <- list(ok = ok, text = if (ok) .r4fun_text("correct", lang()) else .r4fun_text("wrong", lang()))
      tf_next()
    }
    shiny::observeEvent(input$tf_true, tf_answer(TRUE))
    shiny::observeEvent(input$tf_false, tf_answer(FALSE))

    output$tf_stats <- shiny::renderUI({
      shiny::invalidateLater(250, session)
      remain <- if (isTRUE(tf_state$active)) max(0, ceiling(as.numeric(difftime(tf_state$end_time, Sys.time(), units = "secs")))) else 0L
      if (isTRUE(tf_state$active) && remain <= 0L) tf_state$active <- FALSE
      shiny::div(class = "r4-statline",
        shiny::span(.r4fun_text("time_left", lang()), ": ", shiny::strong(remain), " s"),
        shiny::span(.r4fun_text("score", lang()), ": ", shiny::strong(paste0(tf_state$score, "/", tf_state$total))),
        shiny::span(.r4fun_text("best", lang()), " ", .r4fun_text("streak", lang()), ": ", shiny::strong(tf_state$best))
      )
    })
    output$tf_question <- shiny::renderUI({
      if (!isTRUE(tf_state$active) || !is.finite(tf_state$index)) return(NULL)
      statement <- if (lang() == "vi") tf_bank$vi[tf_state$index] else tf_bank$en[tf_state$index]
      shiny::tagList(
        shiny::div(class = "r4-big", style = "font-size:25px;letter-spacing:0;", statement),
        shiny::div(class = "r4-choice",
          shiny::actionButton("tf_true", .r4fun_text("true", lang()), class = "btn btn-default"),
          shiny::actionButton("tf_false", .r4fun_text("false", lang()), class = "btn btn-default")
        )
      )
    })
    output$tf_feedback <- shiny::renderUI({
      z <- tf_state$feedback
      if (is.null(z)) return(NULL)
      shiny::div(class = paste("r4-status", if (z$ok) "r4-good" else "r4-bad"), z$text)
    })

    # Maze -----------------------------------------------------------------
    maze_state <- shiny::reactiveValues(maze = NULL, r = 1L, c = 1L, moves = 0L, solved = FALSE)
    maze_new <- function(difficulty = "easy") {
      n <- switch(difficulty, easy = 7L, medium = 10L, hard = 13L, 7L)
      maze_state$maze <- .r4fun_maze_generate(n)
      maze_state$r <- 1L
      maze_state$c <- 1L
      maze_state$moves <- 0L
      maze_state$solved <- FALSE
    }
    shiny::observe({ if (is.null(maze_state$maze)) maze_new("easy") })
    shiny::observeEvent(input$maze_new, maze_new(.r4fun_or(input$maze_difficulty, "easy")))
    shiny::observeEvent(input$maze_difficulty, maze_new(input$maze_difficulty), ignoreInit = TRUE)
    maze_move <- function(dir) {
      m <- maze_state$maze
      if (is.null(m) || maze_state$solved) return()
      r <- maze_state$r
      c <- maze_state$c
      moved <- FALSE
      if (dir == "N" && !m$north[r, c]) { r <- r - 1L; moved <- TRUE }
      if (dir == "E" && !m$east[r, c]) { c <- c + 1L; moved <- TRUE }
      if (dir == "S" && !m$south[r, c]) { r <- r + 1L; moved <- TRUE }
      if (dir == "W" && !m$west[r, c]) { c <- c - 1L; moved <- TRUE }
      if (moved) {
        maze_state$r <- r
        maze_state$c <- c
        maze_state$moves <- maze_state$moves + 1L
        if (r == m$n && c == m$n) maze_state$solved <- TRUE
      }
    }
    shiny::observeEvent(input$maze_up, maze_move("N"))
    shiny::observeEvent(input$maze_right, maze_move("E"))
    shiny::observeEvent(input$maze_down, maze_move("S"))
    shiny::observeEvent(input$maze_left, maze_move("W"))

    output$maze_stats <- shiny::renderUI(shiny::div(class = "r4-statline", shiny::span(.r4fun_text("moves", lang()), ": ", shiny::strong(maze_state$moves))))
    output$maze_board <- shiny::renderUI({
      m <- maze_state$maze
      shiny::req(m)
      cells <- vector("list", m$n * m$n)
      k <- 0L
      for (r in seq_len(m$n)) for (c in seq_len(m$n)) {
        k <- k + 1L
        borders <- c(
          if (m$north[r, c]) "border-top:2px solid #344054" else "border-top:2px solid transparent",
          if (m$east[r, c]) "border-right:2px solid #344054" else "border-right:2px solid transparent",
          if (m$south[r, c]) "border-bottom:2px solid #344054" else "border-bottom:2px solid transparent",
          if (m$west[r, c]) "border-left:2px solid #344054" else "border-left:2px solid transparent"
        )
        content <- NULL
        if (r == maze_state$r && c == maze_state$c) content <- shiny::div(class = "r4-player")
        if (r == m$n && c == m$n && !(r == maze_state$r && c == maze_state$c)) content <- shiny::div(class = "r4-goal")
        cells[[k]] <- shiny::div(class = "r4-maze-cell", style = paste(borders, collapse = ";"), content)
      }
      shiny::div(class = "r4-maze", style = paste0("grid-template-columns:repeat(", m$n, ",1fr);"), cells)
    })
    output$maze_feedback <- shiny::renderUI({ if (maze_state$solved) shiny::div(class = "r4-status r4-good", .r4fun_text("finished", lang())) else NULL })

    # Lights Out -----------------------------------------------------------
    lights_state <- shiny::reactiveValues(board = NULL, moves = 0L, solved = FALSE)
    lights_new <- function(difficulty = "easy") {
      n <- switch(difficulty, easy = 4L, medium = 5L, hard = 6L, 4L)
      lights_state$board <- .r4fun_lights_generate(n)
      lights_state$moves <- 0L
      lights_state$solved <- FALSE
    }
    shiny::observe({ if (is.null(lights_state$board)) lights_new("easy") })
    shiny::observeEvent(input$lights_new, lights_new(.r4fun_or(input$lights_difficulty, "easy")))
    shiny::observeEvent(input$lights_difficulty, lights_new(input$lights_difficulty), ignoreInit = TRUE)

    for (i in seq_len(36L)) {
      local({
        ii <- i
        shiny::observeEvent(input[[paste0("light_", ii)]], {
          b <- lights_state$board
          if (is.null(b) || lights_state$solved || ii > length(b)) return()
          n <- nrow(b)
          r <- ((ii - 1L) %/% n) + 1L
          c <- ((ii - 1L) %% n) + 1L
          lights_state$board <- .r4fun_light_toggle(b, r, c)
          lights_state$moves <- lights_state$moves + 1L
          lights_state$solved <- !any(lights_state$board)
        }, ignoreInit = TRUE)
      })
    }
    output$lights_stats <- shiny::renderUI({
      b <- lights_state$board
      shiny::div(class = "r4-statline",
        shiny::span(.r4fun_text("moves", lang()), ": ", shiny::strong(lights_state$moves)),
        shiny::span(.r4fun_text("lights_left", lang()), ": ", shiny::strong(if (is.null(b)) 0L else sum(b)))
      )
    })
    output$lights_board <- shiny::renderUI({
      b <- lights_state$board
      shiny::req(b)
      n <- nrow(b)
      btns <- lapply(seq_len(length(b)), function(i) {
        r <- ((i - 1L) %/% n) + 1L
        c <- ((i - 1L) %% n) + 1L
        shiny::actionButton(paste0("light_", i), "", class = paste("r4-light", if (b[r, c]) "on" else "off"))
      })
      shiny::div(class = "r4-light-grid", style = paste0("grid-template-columns:repeat(", n, ",54px);"), btns)
    })
    output$lights_feedback <- shiny::renderUI({ if (lights_state$solved) shiny::div(class = "r4-status r4-good", .r4fun_text("finished", lang())) else NULL })

    # Mastermind -----------------------------------------------------------
    mm_state <- shiny::reactiveValues(code = sample(1:6, 4L, replace = TRUE), history = data.frame(), won = FALSE, over = FALSE)
    mm_new <- function() {
      mm_state$code <- sample(1:6, 4L, replace = TRUE)
      mm_state$history <- data.frame(guess = character(), exact = integer(), misplaced = integer(), stringsAsFactors = FALSE)
      mm_state$won <- FALSE
      mm_state$over <- FALSE
      shiny::updateTextInput(session, "mm_guess", value = "")
    }
    shiny::observeEvent(input$mm_new, mm_new())
    shiny::observeEvent(input$mm_submit, {
      if (mm_state$over) return()
      txt <- gsub("[^1-6]", "", .r4fun_or(input$mm_guess, ""))
      if (nchar(txt) != 4L) {
        shiny::showNotification(.r4fun_text("mastermind_hint", lang()), type = "warning")
        return()
      }
      guess <- as.integer(strsplit(txt, "", fixed = TRUE)[[1L]])
      sc <- .r4fun_mastermind_score(mm_state$code, guess)
      mm_state$history <- rbind(mm_state$history, data.frame(guess = txt, exact = unname(sc["exact"]), misplaced = unname(sc["misplaced"]), stringsAsFactors = FALSE))
      if (sc["exact"] == 4L) { mm_state$won <- TRUE; mm_state$over <- TRUE }
      if (nrow(mm_state$history) >= 8L && !mm_state$won) mm_state$over <- TRUE
      shiny::updateTextInput(session, "mm_guess", value = "")
    })
    output$mm_status <- shiny::renderUI({
      if (mm_state$won) return(shiny::div(class = "r4-status r4-good", .r4fun_text("correct", lang())))
      if (mm_state$over) return(shiny::div(class = "r4-status r4-bad", paste(.r4fun_text("code_was", lang()), paste(mm_state$code, collapse = ""))))
      shiny::div(class = "r4-statline", shiny::span(.r4fun_text("attempts", lang()), ": ", shiny::strong(paste0(nrow(mm_state$history), "/8"))))
    })
    output$mm_history <- shiny::renderUI({
      h <- mm_state$history
      if (!nrow(h)) return(NULL)
      rows <- lapply(seq_len(nrow(h)), function(i) shiny::div(class = "r4-history-row", shiny::strong(h$guess[i]), paste(.r4fun_text("exact", lang()), h$exact[i]), paste(.r4fun_text("misplaced", lang()), h$misplaced[i])))
      shiny::div(class = "r4-history", rows)
    })

    # Emoji Quiz -----------------------------------------------------------
    emoji_bank <- .r4fun_emoji_bank()
    emoji_state <- shiny::reactiveValues(index = sample.int(length(emoji_bank), 1L), options = character(), correct = 1L, score = 0L, total = 0L, streak = 0L, best = 0L, feedback = NULL)
    emoji_new <- function() {
      q <- emoji_bank[[emoji_state$index]]
      correct_text <- if (lang() == "vi") q$vi else q$en
      wrong <- if (lang() == "vi") q$wrong_vi else q$wrong_en
      opts <- sample(c(correct_text, wrong))
      emoji_state$options <- opts
      emoji_state$correct <- match(correct_text, opts)
    }
    shiny::observe({ if (!length(emoji_state$options)) emoji_new() })
    shiny::observeEvent(input$language, emoji_new(), ignoreInit = TRUE)
    emoji_answer <- function(k) {
      ok <- identical(k, emoji_state$correct)
      emoji_state$total <- emoji_state$total + 1L
      if (ok) {
        emoji_state$score <- emoji_state$score + 1L
        emoji_state$streak <- emoji_state$streak + 1L
        emoji_state$best <- max(emoji_state$best, emoji_state$streak)
      } else emoji_state$streak <- 0L
      emoji_state$feedback <- list(ok = ok, text = if (ok) .r4fun_text("correct", lang()) else .r4fun_text("wrong", lang()))
      choices <- setdiff(seq_along(emoji_bank), emoji_state$index)
      emoji_state$index <- sample(choices, 1L)
      emoji_new()
    }
    for (k in 1:4) local({ kk <- k; shiny::observeEvent(input[[paste0("emoji_opt_", kk)]], emoji_answer(kk), ignoreInit = TRUE) })
    output$emoji_stats <- shiny::renderUI(shiny::div(class = "r4-statline",
      shiny::span(.r4fun_text("score", lang()), ": ", shiny::strong(paste0(emoji_state$score, "/", emoji_state$total))),
      shiny::span(.r4fun_text("streak", lang()), ": ", shiny::strong(emoji_state$streak)),
      shiny::span(.r4fun_text("best", lang()), ": ", shiny::strong(emoji_state$best))
    ))
    output$emoji_question <- shiny::renderUI({
      q <- emoji_bank[[emoji_state$index]]
      shiny::tagList(
        shiny::div(class = "r4-emoji", q$clue),
        shiny::div(class = "r4-note r4-center", .r4fun_text("choose_answer", lang())),
        shiny::div(class = "r4-choice",
          lapply(1:4, function(k) shiny::actionButton(paste0("emoji_opt_", k), emoji_state$options[k], class = "btn btn-default"))
        )
      )
    })
    output$emoji_feedback <- shiny::renderUI({
      z <- emoji_state$feedback
      if (is.null(z)) return(NULL)
      shiny::div(class = paste("r4-status", if (z$ok) "r4-good" else "r4-bad"), z$text)
    })

    # Decision Wheel -------------------------------------------------------
    wheel_winner <- shiny::reactiveVal(NULL)
    wheel_items <- shiny::reactive({
      z <- trimws(unlist(strsplit(.r4fun_or(input$wheel_options, ""), "\\r?\\n")))
      z[nzchar(z)]
    })
    shiny::observe({
      if (.r4fun_or(input$game, "home") != "wheel") return()
      shiny::invalidateLater(500, session)
      items <- wheel_items()
      if (length(items) >= 2L) session$sendCustomMessage("r4fun-wheel-draw", list(items = items))
    })
    shiny::observeEvent(input$wheel_spin, {
      items <- wheel_items()
      if (length(items) < 2L) {
        shiny::showNotification(.r4fun_text("need_two", lang()), type = "warning")
        return()
      }
      win <- sample.int(length(items), 1L)
      wheel_winner(items[win])
      session$sendCustomMessage("r4fun-wheel-spin", list(items = items, winner = win - 1L))
    })
    output$wheel_result <- shiny::renderUI({
      z <- wheel_winner()
      if (is.null(z)) return(NULL)
      shiny::div(class = "r4-status r4-good", shiny::strong(.r4fun_text("winner", lang()), ": "), " ", z)
    })

    # Fortune Cookie -------------------------------------------------------
    fortune_current <- shiny::reactiveVal(NULL)
    shiny::observeEvent(input$fortune_open, fortune_current(sample(.r4fun_fortunes(lang()), 1L)))
    shiny::observeEvent(input$language, fortune_current(NULL), ignoreInit = TRUE)
    output$fortune_text <- shiny::renderUI({
      z <- fortune_current()
      if (is.null(z)) return(shiny::div(class = "r4-fortune", "\U0001F960"))
      shiny::div(class = "r4-fortune", "\u201C", z, "\u201D")
    })

    # Main router ----------------------------------------------------------
    output$main <- shiny::renderUI({
      g <- .r4fun_or(input$game, "home")
      l <- lang()
      if (g == "home") return(.r4fun_home_ui(l))
      back <- shiny::div(style = "margin-bottom:12px;", shiny::actionButton("back_home", paste0("<- ", .r4fun_text("back_home", l))))
      body <- switch(g,
        life = .r4fun_life_ui(l),
        memory = .r4fun_memory_ui(l, memory_level(), memory_best()),
        cards = .r4fun_cards_ui(l),
        odd = .r4fun_odd_ui(l),
        higher = .r4fun_higher_ui(l),
        tf = .r4fun_tf_ui(l),
        maze = .r4fun_maze_ui(l),
        lights = .r4fun_lights_ui(l),
        mastermind = .r4fun_mastermind_ui(l),
        emoji = .r4fun_emoji_ui(l),
        wheel = .r4fun_wheel_ui(l),
        fortune = .r4fun_fortune_ui(l),
        .r4fun_home_ui(l)
      )
      shiny::tagList(back, body)
    })

    shiny::observeEvent(input$back_home, shiny::updateSelectInput(session, "game", selected = "home"))
  }

  shiny::shinyApp(ui = ui, server = server)
}

#' Launch R4VN Playground
#'
#' Starts a lightweight collection of short games, puzzles, and relaxation
#' activities bundled with R4VN. The interface can be switched between English
#' and Vietnamese. The function is intentionally independent of active data and
#' statistical workflows.
#'
#' @param language Initial interface language. Use `"en"` for English or
#'   `"vi"` for Vietnamese.
#' @param launch.browser Logical or a browser function passed to
#'   [shiny::runApp()].
#' @param host Host passed to [shiny::runApp()]. The default `"127.0.0.1"`
#'   keeps the app local to the user's computer.
#' @param port Optional port. `NULL` lets Shiny choose an available port.
#' @param ... Additional arguments passed to [shiny::runApp()].
#'
#' @details
#' The Playground uses only the optional package `shiny`. Vietnamese interface
#' strings are stored in Unicode escape form so the R source file remains
#' ASCII-only and portable across locales. Games include a collapsible Trick
#' section with a practical strategy or shortcut for easier play.
#'
#' @return Invisibly returns the value from [shiny::runApp()].
#' @export
#'
#' @examples
#' if (interactive() && requireNamespace("shiny", quietly = TRUE)) {
#'   r4fun()
#' }
r4fun <- function(language = c("en", "vi"), launch.browser = TRUE,
                  host = "127.0.0.1", port = NULL, ...) {
  .r4fun_need("shiny")
  language <- match.arg(language)
  app <- .r4fun_app(default_language = language)
  invisible(shiny::runApp(
    app,
    host = host,
    port = port,
    launch.browser = launch.browser,
    ...
  ))
}

Try the R4VN package in your browser

Any scripts or data that you put into this service are public.

R4VN documentation built on Sept. 30, 2026, 5:13 p.m.