|
|
library(shiny) |
|
|
library(shinyjs) |
|
|
|
|
|
|
|
|
CONFIG <- list( |
|
|
tile_size = 150, |
|
|
shuffle_moves = 100 |
|
|
) |
|
|
|
|
|
source("minimal_global.R") |
|
|
|
|
|
|
|
|
ui <- fluidPage( |
|
|
useShinyjs(), |
|
|
titlePanel(title = NULL, windowTitle = "Puzzle8"), |
|
|
tags$head( |
|
|
tags$meta(name = "viewport", content = "width=device-width, initial-scale=1"), |
|
|
tags$style(HTML(" |
|
|
:root { |
|
|
--tile-size: 150px; |
|
|
--primary-color: #2196F3; |
|
|
--success-color: #4CAF50; |
|
|
--warning-color: #FF9800; |
|
|
--border-radius: 8px; |
|
|
--shadow-light: 0 2px 4px rgba(0,0,0,0.1); |
|
|
--shadow-medium: 0 4px 8px rgba(0,0,0,0.15); |
|
|
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
min-height: 100vh; |
|
|
margin: 0; |
|
|
padding: 20px 0; |
|
|
} |
|
|
|
|
|
.container-fluid { |
|
|
max-width: 800px; |
|
|
margin: 0 auto; |
|
|
} |
|
|
|
|
|
.game-container { |
|
|
background: rgba(255, 255, 255, 0.95); |
|
|
border-radius: 16px; |
|
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1); |
|
|
backdrop-filter: blur(10px); |
|
|
padding: 30px; |
|
|
margin: 20px; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.puzzle-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(3, var(--tile-size)); |
|
|
grid-template-rows: repeat(3, var(--tile-size)); |
|
|
gap: 8px; |
|
|
justify-content: center; |
|
|
margin: 30px auto; |
|
|
padding: 20px; |
|
|
background: rgba(0,0,0,0.05); |
|
|
border-radius: var(--border-radius); |
|
|
} |
|
|
|
|
|
.puzzle-tile { |
|
|
width: var(--tile-size); |
|
|
height: var(--tile-size); |
|
|
border: none; |
|
|
border-radius: var(--border-radius); |
|
|
background: linear-gradient(145deg, var(--primary-color), #1976D2); |
|
|
color: white; |
|
|
font-size: 32px; |
|
|
font-weight: 700; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
cursor: pointer; |
|
|
transition: var(--transition); |
|
|
box-shadow: var(--shadow-light); |
|
|
position: relative; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.puzzle-tile::before { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
top: 0; |
|
|
left: -100%; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); |
|
|
transition: left 0.5s; |
|
|
} |
|
|
|
|
|
.puzzle-tile:hover::before { |
|
|
left: 100%; |
|
|
} |
|
|
|
|
|
.puzzle-tile:hover { |
|
|
transform: translateY(-2px) scale(1.02); |
|
|
box-shadow: var(--shadow-medium); |
|
|
} |
|
|
|
|
|
.puzzle-tile:active { |
|
|
transform: translateY(0) scale(0.98); |
|
|
} |
|
|
|
|
|
.empty-tile { |
|
|
background: rgba(255,255,255,0.8); |
|
|
border: 2px dashed #ccc; |
|
|
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1); |
|
|
} |
|
|
|
|
|
.moves-counter { |
|
|
background: white; |
|
|
padding: 20px; |
|
|
border-radius: var(--border-radius); |
|
|
margin: 20px 0; |
|
|
box-shadow: var(--shadow-light); |
|
|
border-left: 4px solid var(--primary-color); |
|
|
} |
|
|
|
|
|
.congratulations-card { |
|
|
background: linear-gradient(145deg, #4CAF50, #388E3C); |
|
|
color: white; |
|
|
padding: 25px; |
|
|
border-radius: var(--border-radius); |
|
|
margin: 20px 0; |
|
|
box-shadow: var(--shadow-medium); |
|
|
animation: celebration 0.6s ease; |
|
|
border: none; |
|
|
} |
|
|
|
|
|
@keyframes celebration { |
|
|
0% { |
|
|
opacity: 0; |
|
|
transform: translateY(-20px) scale(0.9); |
|
|
} |
|
|
50% { |
|
|
transform: translateY(-5px) scale(1.05); |
|
|
} |
|
|
100% { |
|
|
opacity: 1; |
|
|
transform: translateY(0) scale(1); |
|
|
} |
|
|
} |
|
|
|
|
|
.moves-value { |
|
|
font-size: 36px; |
|
|
font-weight: bold; |
|
|
color: var(--primary-color); |
|
|
margin-bottom: 5px; |
|
|
} |
|
|
|
|
|
.moves-label { |
|
|
font-size: 14px; |
|
|
color: #666; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 1px; |
|
|
} |
|
|
|
|
|
.congratulations-title { |
|
|
font-size: 28px; |
|
|
font-weight: bold; |
|
|
margin-bottom: 10px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.congratulations-subtitle { |
|
|
font-size: 18px; |
|
|
font-weight: 600; |
|
|
opacity: 0.9; |
|
|
} |
|
|
|
|
|
.btn-game { |
|
|
padding: 15px 30px; |
|
|
border: none; |
|
|
border-radius: var(--border-radius); |
|
|
font-weight: 600; |
|
|
font-size: 16px; |
|
|
cursor: pointer; |
|
|
transition: var(--transition); |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.5px; |
|
|
margin: 10px; |
|
|
min-width: 140px; |
|
|
} |
|
|
|
|
|
.btn-success { |
|
|
background: linear-gradient(145deg, var(--success-color), #388E3C); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-warning { |
|
|
background: linear-gradient(145deg, var(--warning-color), #F57C00); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-game:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: var(--shadow-medium); |
|
|
} |
|
|
|
|
|
.victory-message { |
|
|
background: linear-gradient(145deg, #4CAF50, #388E3C); |
|
|
color: white; |
|
|
padding: 25px; |
|
|
border-radius: var(--border-radius); |
|
|
margin: 20px 0; |
|
|
font-size: 18px; |
|
|
font-weight: 600; |
|
|
box-shadow: var(--shadow-medium); |
|
|
animation: slideIn 0.5s ease; |
|
|
} |
|
|
|
|
|
@keyframes slideIn { |
|
|
from { |
|
|
opacity: 0; |
|
|
transform: translateY(-20px); |
|
|
} |
|
|
to { |
|
|
opacity: 1; |
|
|
transform: translateY(0); |
|
|
} |
|
|
} |
|
|
|
|
|
.instructions { |
|
|
background: white; |
|
|
border-radius: var(--border-radius); |
|
|
padding: 25px; |
|
|
box-shadow: var(--shadow-light); |
|
|
text-align: left; |
|
|
margin: 20px 0; |
|
|
} |
|
|
|
|
|
.instructions h3 { |
|
|
color: var(--primary-color); |
|
|
margin-bottom: 15px; |
|
|
font-weight: 600; |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.instructions ul { |
|
|
padding-left: 20px; |
|
|
} |
|
|
|
|
|
.instructions li { |
|
|
margin-bottom: 8px; |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
.goal-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(3, 50px); |
|
|
gap: 5px; |
|
|
justify-content: center; |
|
|
margin: 15px 0; |
|
|
} |
|
|
|
|
|
.goal-tile { |
|
|
width: 50px; |
|
|
height: 50px; |
|
|
background: var(--primary-color); |
|
|
color: white; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
border-radius: 4px; |
|
|
font-weight: bold; |
|
|
font-size: 16px; |
|
|
} |
|
|
|
|
|
.goal-empty { |
|
|
background: #f0f0f0; |
|
|
border: 2px dashed #ccc; |
|
|
} |
|
|
|
|
|
/* Responsive Design */ |
|
|
@media (max-width: 768px) { |
|
|
:root { |
|
|
--tile-size: 100px; |
|
|
} |
|
|
|
|
|
.game-container { |
|
|
margin: 10px; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
.puzzle-tile { |
|
|
font-size: 24px; |
|
|
} |
|
|
|
|
|
.moves-value { |
|
|
font-size: 28px; |
|
|
} |
|
|
|
|
|
.congratulations-title { |
|
|
font-size: 24px; |
|
|
} |
|
|
|
|
|
.congratulations-subtitle { |
|
|
font-size: 16px; |
|
|
} |
|
|
|
|
|
.btn-game { |
|
|
min-width: auto; |
|
|
padding: 12px 20px; |
|
|
font-size: 14px; |
|
|
} |
|
|
} |
|
|
|
|
|
@media (max-width: 480px) { |
|
|
:root { |
|
|
--tile-size: 80px; |
|
|
} |
|
|
|
|
|
.puzzle-tile { |
|
|
font-size: 20px; |
|
|
} |
|
|
|
|
|
.moves-value { |
|
|
font-size: 24px; |
|
|
} |
|
|
|
|
|
.congratulations-title { |
|
|
font-size: 20px; |
|
|
} |
|
|
|
|
|
.congratulations-subtitle { |
|
|
font-size: 14px; |
|
|
} |
|
|
} |
|
|
")) |
|
|
), |
|
|
|
|
|
div(class = "container-fluid", |
|
|
div(class = "game-container", |
|
|
|
|
|
h1("π§© Puzzle8", |
|
|
style = "background: linear-gradient(145deg, #2196F3, #1976D2); |
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent; |
|
|
font-weight: 800; font-size: 3rem; margin-bottom: 30px;"), |
|
|
|
|
|
|
|
|
uiOutput("status_card"), |
|
|
|
|
|
|
|
|
div(class = "puzzle-grid", |
|
|
lapply(1:9, function(i) { |
|
|
div(id = paste0("tile_container_", i), uiOutput(paste0("tile_", i))) |
|
|
}) |
|
|
), |
|
|
|
|
|
|
|
|
div(class = "text-center", |
|
|
actionButton("new_game", "New Game", class = "btn-game btn-success"), |
|
|
actionButton("reset_game", "Reset", class = "btn-game btn-warning") |
|
|
), |
|
|
|
|
|
|
|
|
div(class = "instructions", |
|
|
h3("π― How to Play"), |
|
|
p("Arrange numbers 1-8 in order with the empty space in the bottom-right corner."), |
|
|
tags$ul( |
|
|
tags$li("Click on tiles adjacent to the empty space to move them"), |
|
|
tags$li("Only tiles next to the empty space can be moved"), |
|
|
tags$li("Try to solve the puzzle in as few moves as possible!") |
|
|
), |
|
|
|
|
|
div(style = "text-align: center; margin-top: 20px;", |
|
|
h4("π Goal:"), |
|
|
div(class = "goal-grid", |
|
|
lapply(1:8, function(i) { |
|
|
div(class = "goal-tile", i) |
|
|
}), |
|
|
div(class = "goal-tile goal-empty") |
|
|
) |
|
|
) |
|
|
) |
|
|
) |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
server <- function(input, output, session) { |
|
|
|
|
|
game_state <- reactiveValues( |
|
|
puzzle = NULL, |
|
|
initial_puzzle = NULL, |
|
|
moves = 0, |
|
|
game_won = FALSE |
|
|
) |
|
|
|
|
|
|
|
|
observe({ |
|
|
if(is.null(game_state$puzzle)) { |
|
|
new_puzzle <- create_solvable_puzzle() |
|
|
game_state$puzzle <- new_puzzle |
|
|
game_state$initial_puzzle <- new_puzzle |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
observe({ |
|
|
lapply(1:9, function(i) { |
|
|
observeEvent(input[[paste0("tile_", i)]], { |
|
|
if(!game_state$game_won && !is.null(game_state$puzzle)) { |
|
|
result <- move_tile(game_state$puzzle, i) |
|
|
|
|
|
if(result$moved) { |
|
|
game_state$puzzle <- result$puzzle |
|
|
game_state$moves <- game_state$moves + 1 |
|
|
|
|
|
|
|
|
if(is_solved(game_state$puzzle)) { |
|
|
game_state$game_won <- TRUE |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$new_game, { |
|
|
new_puzzle <- create_solvable_puzzle() |
|
|
game_state$puzzle <- new_puzzle |
|
|
game_state$initial_puzzle <- new_puzzle |
|
|
game_state$moves <- 0 |
|
|
game_state$game_won <- FALSE |
|
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$reset_game, { |
|
|
if(!is.null(game_state$initial_puzzle)) { |
|
|
game_state$puzzle <- game_state$initial_puzzle |
|
|
game_state$moves <- 0 |
|
|
game_state$game_won <- FALSE |
|
|
|
|
|
|
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
lapply(1:9, function(i) { |
|
|
output[[paste0("tile_", i)]] <- renderUI({ |
|
|
req(game_state$puzzle) |
|
|
|
|
|
value <- game_state$puzzle[i] |
|
|
|
|
|
if(value == 0) { |
|
|
div(class = "empty-tile") |
|
|
} else { |
|
|
actionButton( |
|
|
paste0("tile_", i), |
|
|
label = as.character(value), |
|
|
class = "puzzle-tile" |
|
|
) |
|
|
} |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
output$status_card <- renderUI({ |
|
|
if(game_state$game_won) { |
|
|
|
|
|
div(class = "congratulations-card", |
|
|
div(class = "congratulations-title", |
|
|
"π Congratulations!" |
|
|
), |
|
|
div(class = "congratulations-subtitle", |
|
|
paste("Puzzle solved in", game_state$moves, "moves!") |
|
|
) |
|
|
) |
|
|
} else { |
|
|
|
|
|
div(class = "moves-counter", |
|
|
div(class = "moves-value", game_state$moves), |
|
|
div(class = "moves-label", "Moves") |
|
|
) |
|
|
} |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
shinyApp(ui = ui, server = server) |