puzzle8 / app.R
aephiday's picture
Update app.R
bf579e8 verified
library(shiny)
library(shinyjs)
# Simple configuration
CONFIG <- list(
tile_size = 150,
shuffle_moves = 100
)
source("minimal_global.R")
# UI
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",
# Title
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;"),
# Dynamic Counter/Congratulations Card
uiOutput("status_card"),
# Puzzle Grid
div(class = "puzzle-grid",
lapply(1:9, function(i) {
div(id = paste0("tile_container_", i), uiOutput(paste0("tile_", i)))
})
),
# Control Buttons
div(class = "text-center",
actionButton("new_game", "New Game", class = "btn-game btn-success"),
actionButton("reset_game", "Reset", class = "btn-game btn-warning")
),
# Instructions
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
server <- function(input, output, session) {
# Simple game state
game_state <- reactiveValues(
puzzle = NULL,
initial_puzzle = NULL,
moves = 0,
game_won = FALSE
)
# Initialize game
observe({
if(is.null(game_state$puzzle)) {
new_puzzle <- create_solvable_puzzle()
game_state$puzzle <- new_puzzle
game_state$initial_puzzle <- new_puzzle
}
})
# Tile click handlers
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
# Check win condition
if(is_solved(game_state$puzzle)) {
game_state$game_won <- TRUE
# showNotification(
# paste("πŸŽ‰ Congratulations! Solved in", game_state$moves, "moves!"),
# type = "message", duration = 5
# )
}
}
}
})
})
})
# New Game button
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
# showNotification("New game started!", type = "message")
})
# Reset button
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
# showNotification("Game reset!", type = "message")
}
})
# Render tiles
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"
)
}
})
})
# Dynamic Status Card (Moves Counter or Congratulations)
output$status_card <- renderUI({
if(game_state$game_won) {
# Congratulations Card
div(class = "congratulations-card",
div(class = "congratulations-title",
"πŸ† Congratulations!"
),
div(class = "congratulations-subtitle",
paste("Puzzle solved in", game_state$moves, "moves!")
)
)
} else {
# Moves Counter Card
div(class = "moves-counter",
div(class = "moves-value", game_state$moves),
div(class = "moves-label", "Moves")
)
}
})
}
# Run the application
shinyApp(ui = ui, server = server)