aephiday commited on
Commit
658e353
·
verified ·
1 Parent(s): 3ba10fb

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +387 -548
app.R CHANGED
@@ -1,656 +1,495 @@
1
  library(shiny)
2
- # library(shinyjs)
3
- # library(shinydashboard)
4
- # library(future)
5
- # library(promises)
6
 
7
- # Enable future support
8
- # plan(multisession)
9
-
10
- max_hint <- 300
11
- size <- 100
12
-
13
- # Fungsi untuk membuat puzzle awal yang dapat diselesaikan
14
- create_solvable_puzzle <- function() {
15
- # Puzzle solved
16
- solved <- c(1, 2, 3, 4, 5, 6, 7, 8, 0)
17
-
18
- # Acak puzzle dengan langkah-langkah valid
19
- puzzle <- solved
20
- moves <- 0
21
- blank_pos <- 9
22
-
23
- while(moves < 100) {
24
- # Tentukan kemungkinan gerakan
25
- possible_moves <- c()
26
- if(blank_pos %% 3 != 1) possible_moves <- c(possible_moves, blank_pos - 1) # Kiri
27
- if(blank_pos %% 3 != 0) possible_moves <- c(possible_moves, blank_pos + 1) # Kanan
28
- if(blank_pos > 3) possible_moves <- c(possible_moves, blank_pos - 3) # Atas
29
- if(blank_pos < 7) possible_moves <- c(possible_moves, blank_pos + 3) # Bawah
30
-
31
- # Pilih gerakan acak
32
- move <- sample(possible_moves, 1)
33
-
34
- # Tukar posisi
35
- temp <- puzzle[blank_pos]
36
- puzzle[blank_pos] <- puzzle[move]
37
- puzzle[move] <- temp
38
-
39
- blank_pos <- move
40
- moves <- moves + 1
41
- }
42
-
43
- return(puzzle)
44
- }
45
-
46
- # Fungsi untuk mengecek apakah puzzle sudah selesai
47
- is_solved <- function(puzzle) {
48
- target <- c(1, 2, 3, 4, 5, 6, 7, 8, 0)
49
- return(identical(puzzle, target))
50
- }
51
-
52
- # Fungsi untuk mendapatkan posisi kotak kosong
53
- get_blank_position <- function(puzzle) {
54
- return(which(puzzle == 0))
55
- }
56
-
57
- # Fungsi untuk memindahkan kotak - dimodifikasi untuk return status pergerakan
58
- move_tile <- function(puzzle, tile_position) {
59
- blank_pos <- get_blank_position(puzzle)
60
-
61
- # Cek apakah gerakan valid
62
- valid_moves <- c()
63
- if(blank_pos %% 3 != 1) valid_moves <- c(valid_moves, blank_pos - 1) # Kiri
64
- if(blank_pos %% 3 != 0) valid_moves <- c(valid_moves, blank_pos + 1) # Kanan
65
- if(blank_pos > 3) valid_moves <- c(valid_moves, blank_pos - 3) # Atas
66
- if(blank_pos < 7) valid_moves <- c(valid_moves, blank_pos + 3) # Bawah
67
-
68
- # Jika gerakan valid, lakukan pertukaran
69
- if(tile_position %in% valid_moves) {
70
- # Lakukan pertukaran
71
- temp <- puzzle[blank_pos]
72
- puzzle[blank_pos] <- puzzle[tile_position]
73
- puzzle[tile_position] <- temp
74
- return(list(puzzle = puzzle, moved = TRUE)) # Return dengan status moved TRUE
75
- }
76
-
77
- # Jika tidak valid, return puzzle tanpa perubahan dan status moved FALSE
78
- return(list(puzzle = puzzle, moved = FALSE))
79
- }
80
-
81
- # Fungsi untuk menghitung jarak Manhattan antara dua posisi
82
- manhattan_distance <- function(pos1, pos2) {
83
- row1 <- (pos1 - 1) %/% 3 + 1
84
- col1 <- (pos1 - 1) %% 3 + 1
85
- row2 <- (pos2 - 1) %/% 3 + 1
86
- col2 <- (pos2 - 1) %% 3 + 1
87
- return(abs(row1 - row2) + abs(col1 - col2))
88
- }
89
-
90
- # Fungsi untuk menghitung heuristik (jumlah jarak Manhattan semua kotak ke posisi target)
91
- calculate_heuristic <- function(puzzle) {
92
- target_positions <- c(1, 2, 3, 4, 5, 6, 7, 8, 9) # posisi target untuk angka 1-8, 0 di posisi 9
93
- heuristic <- 0
94
-
95
- for(i in 1:8) {
96
- current_pos <- which(puzzle == i)
97
- target_pos <- i
98
- heuristic <- heuristic + manhattan_distance(current_pos, target_pos)
99
- }
100
-
101
- return(heuristic)
102
- }
103
-
104
- # Fungsi A* untuk mencari solusi optimal
105
- solve_puzzle <- function(initial_puzzle) {
106
- # Queue untuk menyimpan state yang akan dieksplorasi
107
- # Setiap elemen: list(puzzle = ..., cost = ..., path = ...)
108
- queue <- list(list(puzzle = initial_puzzle, cost = 0, path = c()))
109
-
110
- # Set untuk menyimpan state yang sudah dikunjungi
111
- visited <- list()
112
-
113
- # Target state
114
- target <- c(1, 2, 3, 4, 5, 6, 7, 8, 0)
115
-
116
- while(length(queue) > 0) {
117
- # Temukan state dengan cost terkecil
118
- costs <- sapply(queue, function(x) x$cost)
119
- min_index <- which.min(costs)[1]
120
- current <- queue[[min_index]]
121
-
122
- # Hapus dari queue
123
- queue <- queue[-min_index]
124
-
125
- # Cek apakah sudah mencapai target
126
- if(identical(current$puzzle, target)) {
127
- return(current$path)
128
- }
129
-
130
- # Buat key untuk state ini
131
- state_key <- paste(current$puzzle, collapse = ",")
132
-
133
- # Jika sudah dikunjungi, lewati
134
- if(state_key %in% visited) {
135
- next
136
- }
137
-
138
- # Tandai sebagai sudah dikunjungi
139
- visited <- c(visited, state_key)
140
-
141
- # Dapatkan posisi blank
142
- blank_pos <- get_blank_position(current$puzzle)
143
-
144
- # Dapatkan semua gerakan valid
145
- valid_moves <- c()
146
- if(blank_pos %% 3 != 1) valid_moves <- c(valid_moves, blank_pos - 1) # Kiri
147
- if(blank_pos %% 3 != 0) valid_moves <- c(valid_moves, blank_pos + 1) # Kanan
148
- if(blank_pos > 3) valid_moves <- c(valid_moves, blank_pos - 3) # Atas
149
- if(blank_pos < 7) valid_moves <- c(valid_moves, blank_pos + 3) # Bawah
150
-
151
- # Untuk setiap gerakan valid
152
- for(move in valid_moves) {
153
- # Buat salinan puzzle dan coba gerakan
154
- new_puzzle <- current$puzzle
155
- temp <- new_puzzle[blank_pos]
156
- new_puzzle[blank_pos] <- new_puzzle[move]
157
- new_puzzle[move] <- temp
158
-
159
- # Buat key untuk state baru
160
- new_state_key <- paste(new_puzzle, collapse = ",")
161
-
162
- # Jika belum dikunjungi
163
- if(!(new_state_key %in% visited)) {
164
- # Hitung heuristik
165
- heuristic <- calculate_heuristic(new_puzzle)
166
- # Hitung cost (langkah + heuristik)
167
- new_cost <- length(current$path) + 1 + heuristic
168
-
169
- # Tambahkan ke queue
170
- new_path <- c(current$path, move)
171
- queue <- c(queue, list(list(puzzle = new_puzzle, cost = new_cost, path = new_path)))
172
- }
173
- }
174
- }
175
-
176
- # Jika tidak ditemukan solusi
177
- return(NULL)
178
- }
179
-
180
- # Fungsi untuk mendapatkan hint berdasarkan solusi yang disimpan
181
- get_hint_from_solution <- function(puzzle, solution, current_step) {
182
- # Jika solusi tidak tersedia atau sudah selesai
183
- if(is.null(solution) || current_step >= length(solution)) {
184
- return(NULL)
185
- }
186
-
187
- # Kembalikan langkah berikutnya dalam solusi
188
- return(solution[current_step + 1])
189
- }
190
-
191
-
192
- # Fungsi BFS untuk mencari solusi optimal
193
- solve_puzzle_bfs <- function(initial_puzzle) {
194
- # Jika sudah solved, return 0
195
- if(is_solved(initial_puzzle)) {
196
- return(0)
197
- }
198
-
199
- # Queue untuk menyimpan state yang akan dieksplorasi
200
- # Setiap elemen: list(puzzle = ..., depth = ...)
201
- queue <- list(list(puzzle = initial_puzzle, depth = 0))
202
-
203
- # Set untuk menyimpan state yang sudah dikunjungi
204
- visited <- c()
205
- visited[[paste(initial_puzzle, collapse = ",")]] <- TRUE
206
-
207
- while(length(queue) > 0) {
208
- # Ambil elemen pertama dari queue
209
- current <- queue[[1]]
210
- queue <- queue[-1]
211
-
212
- # Dapatkan posisi blank
213
- blank_pos <- get_blank_position(current$puzzle)
214
-
215
- # Dapatkan semua gerakan valid
216
- valid_moves <- c()
217
- if(blank_pos %% 3 != 1) valid_moves <- c(valid_moves, blank_pos - 1) # Kiri
218
- if(blank_pos %% 3 != 0) valid_moves <- c(valid_moves, blank_pos + 1) # Kanan
219
- if(blank_pos > 3) valid_moves <- c(valid_moves, blank_pos - 3) # Atas
220
- if(blank_pos < 7) valid_moves <- c(valid_moves, blank_pos + 3) # Bawah
221
-
222
- # Untuk setiap gerakan valid
223
- for(move in valid_moves) {
224
- # Buat salinan puzzle dan coba gerakan
225
- new_puzzle <- current$puzzle
226
- temp <- new_puzzle[blank_pos]
227
- new_puzzle[blank_pos] <- new_puzzle[move]
228
- new_puzzle[move] <- temp
229
-
230
- # Cek apakah sudah mencapai target
231
- if(is_solved(new_puzzle)) {
232
- return(current$depth + 1)
233
- }
234
-
235
- # Buat key untuk state baru
236
- state_key <- paste(new_puzzle, collapse = ",")
237
-
238
- # Jika belum dikunjungi
239
- if(is.null(visited[[state_key]])) {
240
- # Tandai sebagai sudah dikunjungi
241
- visited[[state_key]] <- TRUE
242
-
243
- # Tambahkan ke queue
244
- queue <- c(queue, list(list(puzzle = new_puzzle, depth = current$depth + 1)))
245
- }
246
- }
247
- }
248
-
249
- # Jika tidak ditemukan solusi (seharusnya tidak terjadi untuk puzzle yang solvable)
250
- return(-1)
251
- }
252
 
 
253
 
254
  # UI
255
  ui <- fluidPage(
256
- # CSS untuk desain responsif
 
257
  tags$head(
 
258
  tags$style(HTML("
259
- /* Base styles */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  .puzzle-tile {
261
- border: 2px solid #333;
262
- background-color: #4CAF50;
 
 
 
263
  color: white;
264
- font-weight: bold;
 
265
  display: flex;
266
  align-items: center;
267
  justify-content: center;
268
  cursor: pointer;
269
- transition: all 0.2s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  }
271
 
272
  .puzzle-tile:hover {
273
- opacity: 0.9;
274
- transform: scale(0.95);
275
  }
276
 
277
- .puzzle-tile:disabled {
278
- cursor: not-allowed;
279
  }
280
 
281
  .empty-tile {
282
- background-color: #f0f0f0;
283
- border: 2px solid #333;
 
284
  }
285
 
286
- .hint-highlight {
287
- box-shadow: 0 0 15px 5px orange;
288
- border: 3px solid orange !important;
289
- z-index: 10;
 
 
 
290
  }
291
 
292
- /* Responsive grid */
293
- .puzzle-grid {
294
- display: grid;
295
- grid-template-columns: repeat(3, 1fr);
296
- grid-gap: 8px;
297
- max-width: 500px;
298
- margin: 0 auto;
299
- padding: 10px;
 
300
  }
301
 
302
- .puzzle-cell {
303
- aspect-ratio: 1;
304
- width: 100%;
 
 
 
 
 
 
 
 
 
305
  }
306
 
307
- /* Responsive button sizes */
308
- .action-button {
309
- width: 100%;
310
- padding: 8px 12px;
311
- font-size: clamp(12px, 2vw, 16px);
312
  margin-bottom: 5px;
313
  }
314
 
315
- /* Stats table */
316
- .stats-table {
317
- width: 100%;
318
- margin: 15px auto;
319
- text-align: center;
320
- font-size: clamp(14px, 2vw, 18px);
321
  }
322
 
323
- .stats-table td {
324
- padding: 5px;
 
 
 
 
 
 
325
  }
326
 
327
- /* Large screens */
328
- @media (min-width: 992px) {
329
- .puzzle-grid {
330
- max-width: 400px;
331
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  }
333
 
334
- /* Medium screens */
335
- @media (min-width: 768px) and (max-width: 991px) {
336
- .puzzle-grid {
337
- max-width: 350px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  }
340
 
341
- /* Small screens */
342
- @media (max-width: 767px) {
343
- .puzzle-grid {
344
- max-width: 90vw;
345
- grid-gap: 5px;
 
 
 
 
 
 
 
 
 
 
 
 
346
  }
347
 
348
- .stats-table {
349
- font-size: clamp(12px, 3vw, 16px);
 
 
 
 
 
 
 
 
 
 
350
  }
351
  }
352
 
353
- /* Very small screens */
354
  @media (max-width: 480px) {
355
- .puzzle-grid {
356
- max-width: 95vw;
357
- grid-gap: 3px;
358
  }
359
 
360
- .action-button {
361
- padding: 6px 8px;
362
- font-size: clamp(10px, 2.5vw, 14px);
 
 
 
 
 
 
 
 
 
 
 
363
  }
364
  }
365
  "))
366
  ),
367
- titlePanel(h1("Puzzle8", style='font-weight: bold;'), windowTitle = "Puzzle8"),
368
 
369
- fluidRow(
370
- column(5,
371
- wellPanel(
372
- tags$table(
373
- style = "margin: 0 auto; border-collapse: separate; border-spacing: 25px 0; text-align: center;",
374
- tags$tr(
375
- tags$td("Langkah"),
376
- tags$td(""),
377
- tags$td("Hint Digunakan"),
378
- ),
379
- tags$tr(
380
- tags$td(textOutput("move_count")),
381
- tags$td(""),
382
- tags$td(textOutput("hint_count"))
383
- )
384
- ),
385
- # Grid 3x3 menggunakan table untuk memastikan layout yang benar
386
- tags$table(
387
- style = "margin: 0 auto; border-collapse: separate; border-spacing: 5px;",
388
- tags$tr(
389
- tags$td(uiOutput("tile_1")),
390
- tags$td(uiOutput("tile_2")),
391
- tags$td(uiOutput("tile_3"))
392
- ),
393
- tags$tr(
394
- tags$td(uiOutput("tile_4")),
395
- tags$td(uiOutput("tile_5")),
396
- tags$td(uiOutput("tile_6"))
397
- ),
398
- tags$tr(
399
- tags$td(uiOutput("tile_7")),
400
- tags$td(uiOutput("tile_8")),
401
- tags$td(uiOutput("tile_9"))
402
- ),
403
- tags$tr(
404
- tags$td(""),
405
- tags$td(""),
406
- tags$td(""),
407
- ),
408
- tags$tr(
409
- tags$td(actionButton("new_game", "New Game", class = "btn-success btn-block")),
410
- tags$td(actionButton("reset_game", "Reset", class = "btn-warning btn-block")),
411
- tags$td(actionButton("hint_button", "Hint", class = "btn-info btn-block"))
412
- )
413
- ),
414
- br(),
415
- uiOutput('game_state'),
416
- uiOutput("final_moves_count"),
417
- br(),
418
- uiOutput('hint_message')
419
- )
420
- ),
421
- column(7,
422
- wellPanel(
423
- h3("Cara Bermain"),
424
- p("Tujuan: Susun angka 1-8 secara berurutan dengan kotak kosong di pojok kanan bawah."),
425
  tags$ul(
426
- tags$li("Hanya angka yang bersebelahan langsung dengan kotak kosong yang dapat dipindahkan"),
427
- tags$li("Anda bisa memindahkan angka ke atas, bawah, kiri, atau kanan ke kotak kosong"),
428
- tags$li("Klik pada angka yang ingin Anda pindahkan (harus bersebelahan dengan kotak kosong)")
429
  ),
430
- br(),
431
- h4("Petunjuk:"),
432
- p("Klik kotak angka yang bersebelahan dengan kotak kosong untuk memindahkannya."),
433
- p("Susun angka 1-8 secara berurutan dengan kotak kosong di pojok kanan bawah.")
 
 
 
 
 
 
434
  )
435
  )
436
- ),
437
- br()
438
  )
439
 
440
  # Server
441
  server <- function(input, output, session) {
442
- # Inisialisasi state permainan
443
  game_state <- reactiveValues(
444
- puzzle = create_solvable_puzzle(),
445
  initial_puzzle = NULL,
446
  moves = 0,
447
- game_won = FALSE,
448
- hints_used = 0,
449
- hint_position = NULL,
450
- solution = NULL,
451
- solution_step = 0
452
  )
453
 
454
- # Simpan puzzle awal saat aplikasi dimulai
455
- observe({
456
- if(is.null(game_state$initial_puzzle)) {
457
- game_state$initial_puzzle <- game_state$puzzle
458
- # Hitung ulang langkah optimal (seharusnya sama dengan yang disimpan)
459
- game_state$optimal_steps <- solve_puzzle_bfs(game_state$initial_puzzle)
460
- }
461
- })
462
-
463
- # Observer untuk menghitung solusi setiap kali puzzle berubah
464
  observe({
465
- # Hanya hitung solusi jika game belum selesai
466
- if(!game_state$game_won) {
467
- # Hitung solusi menggunakan A* algorithm
468
- solution <- solve_puzzle(game_state$puzzle)
469
- game_state$solution <- solution
470
- game_state$solution_step <- 0 # Reset langkah solusi
471
  }
472
  })
473
 
474
- # Tangani klik pada kotak puzzle (melalui observer global)
475
  observe({
476
- # Cek semua kemungkinan input tile
477
- for(i in 1:9) {
478
- local({
479
- tile_id <- i
480
- input_name <- paste0("tile_", tile_id)
481
-
482
- observeEvent(input[[input_name]], {
483
- if(!game_state$game_won) {
484
- # Panggil move_tile dan dapatkan hasil dengan status pergerakan
485
- result <- move_tile(game_state$puzzle, tile_id)
486
 
487
- # Hanya update puzzle dan increment moves jika pergerakan valid
488
- if(result$moved) {
489
- game_state$puzzle <- result$puzzle
490
- game_state$moves <- game_state$moves + 1
491
- game_state$hint_position <- NULL # Reset hint saat pemain bergerak
492
- game_state$solution_step <- game_state$solution_step + 1 # Increment langkah solusi
493
-
494
- # Cek apakah puzzle sudah selesai
495
- if(is_solved(game_state$puzzle)) {
496
- game_state$game_won <- TRUE
497
- }
498
  }
499
  }
500
- })
501
  })
502
- }
503
  })
504
 
505
- # Tombol permainan baru
506
  observeEvent(input$new_game, {
507
- message("New Game created")
508
  new_puzzle <- create_solvable_puzzle()
509
  game_state$puzzle <- new_puzzle
510
- game_state$initial_puzzle <- new_puzzle # Update initial puzzle
511
  game_state$moves <- 0
512
  game_state$game_won <- FALSE
513
- game_state$hints_used <- 0
514
- game_state$hint_position <- NULL
515
- game_state$solution <- NULL
516
- game_state$solution_step <- 0
517
- # Hitung ulang langkah optimal (seharusnya sama dengan yang disimpan)
518
- game_state$optimal_steps <- solve_puzzle_bfs(game_state$initial_puzzle)
519
  })
520
 
521
- # Tombol reset
522
  observeEvent(input$reset_game, {
523
- message("Game reset")
524
  if(!is.null(game_state$initial_puzzle)) {
525
  game_state$puzzle <- game_state$initial_puzzle
526
  game_state$moves <- 0
527
  game_state$game_won <- FALSE
528
- game_state$hints_used <- 0
529
- game_state$hint_position <- NULL
530
- game_state$solution_step <- 0
531
- # Hitung ulang langkah optimal (seharusnya sama dengan yang disimpan)
532
- game_state$optimal_steps <- solve_puzzle_bfs(game_state$initial_puzzle)
533
- }
534
- })
535
-
536
- # Tombol hint
537
- observeEvent(input$hint_button, {
538
- if(!game_state$game_won & game_state$hints_used <= max_hint) {
539
- # Dapatkan hint dari solusi yang disimpan
540
- hint_pos <- get_hint_from_solution(game_state$puzzle, game_state$solution, game_state$solution_step)
541
 
542
- if(!is.null(hint_pos)) {
543
- game_state$hint_position <- hint_pos
544
- game_state$hints_used <- game_state$hints_used + 1
545
- }
546
  }
547
  })
548
 
549
- # Render setiap tile secara individual
550
- for(i in 1:9) {
551
- local({
552
- tile_index <- i
553
 
554
- output[[paste0("tile_", tile_index)]] <- renderUI({
555
- value <- game_state$puzzle[tile_index]
556
-
557
- # Tentukan style berdasarkan apakah ini adalah hint
558
- is_hint <- !is.null(game_state$hint_position) && game_state$hint_position == tile_index
559
- hint_style <- if(is_hint) "box-shadow: 0 0 10px 3px orange; border: 3px solid orange;" else ""
560
-
561
- if(value == 0) {
562
- # Kotak kosong
563
- div(
564
- style = paste0("width: ", size, "px; height: ", size, "px; border: 2px solid #333;
565
- background-color: #f0f0f0; display: flex; align-items: center;
566
- justify-content: center; font-size: 24px; font-weight: bold;", hint_style),
567
- ""
568
- )
569
- } else {
570
- # Kotak dengan angka
571
- actionButton(
572
- paste0("tile_", tile_index),
573
- label = as.character(value),
574
- style = paste0("width: ", size, "px; height: ", size, "px; border: 2px solid #333;
575
- background-color: #4CAF50; color: white; font-size: 24px;
576
- font-weight: bold;", hint_style)
577
- )
578
- }
579
- })
580
  })
581
- }
582
-
583
- # Status permainan
584
- output$game_status <- renderText({
585
- blank_pos <- get_blank_position(game_state$puzzle)
586
- return(blank_pos)
587
- })
588
- # Jumlah langkah
589
- output$move_count <- renderText({
590
- if(!is.null(game_state$optimal_steps) && game_state$optimal_steps >= 0) {
591
- optimal <- game_state$optimal_steps
592
- } else {
593
- optimal <- "Menghitung..."
594
- }
595
- paste0(game_state$moves, " / ", optimal)
596
  })
597
 
598
- output$final_moves_count <- renderUI({
 
599
  if(game_state$game_won) {
600
- div(
601
- style = "text-align: center; font-size: 16px;",
602
- paste("Selesai dalam", game_state$moves, "langkah!")
 
 
 
 
 
603
  )
604
- }
605
- })
606
-
607
- # Jumlah hint yang digunakan
608
- output$hint_count <- renderText({
609
- paste(game_state$hints_used, "/ ", max_hint)
610
- })
611
-
612
- # Langkah optimal
613
- output$optimal_steps <- renderText({
614
- if(!is.null(game_state$optimal_steps) && game_state$optimal_steps >= 0) {
615
- game_state$optimal_steps
616
  } else {
617
- "Menghitung..."
618
- }
619
- })
620
-
621
- # Pesan hint
622
- output$hint_message <- renderUI({
623
- if(!is.null(game_state$hint_position) & game_state$hints_used < max_hint & !game_state$game_won) {
624
- div(
625
- style = "text-align: center; color: orange; font-size: 16px; font-weight: bold;",
626
- icon("lightbulb"),
627
- paste("Hint: Klik angka", game_state$puzzle[game_state$hint_position])
628
- )
629
- } else if(game_state$hints_used >= max_hint & !game_state$game_won) {
630
- div(
631
- style = "text-align: center; color: red; font-size: 16px; font-weight: bold;",
632
- paste0("Anda telah menggunakan semua hint (", game_state$hints_used, " / ", max_hint, ")")
633
- )
634
- }
635
- })
636
-
637
- # Status kemenangan
638
- output$game_won <- reactive({
639
- return(game_state$game_won)
640
- })
641
-
642
- # Untuk conditionalPanel
643
- output$game_state <- renderUI({
644
- req(game_state$game_won)
645
- if(game_state$game_won){
646
- div(
647
- style = "text-align: center; color: green; font-size: 20px; font-weight: bold;",
648
- icon("trophy"),
649
- "Selamat! Anda Menyelesaikan Puzzle!"
650
  )
651
  }
652
  })
653
  }
654
 
655
- # Jalankan aplikasi
656
  shinyApp(ui = ui, server = server)
 
1
  library(shiny)
2
+ library(shinyjs)
 
 
 
3
 
4
+ # Simple configuration
5
+ CONFIG <- list(
6
+ tile_size = 150,
7
+ shuffle_moves = 100
8
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ source("minimal_global.R")
11
 
12
  # UI
13
  ui <- fluidPage(
14
+ useShinyjs(),
15
+
16
  tags$head(
17
+ tags$meta(name = "viewport", content = "width=device-width, initial-scale=1"),
18
  tags$style(HTML("
19
+ :root {
20
+ --tile-size: 150px;
21
+ --primary-color: #2196F3;
22
+ --success-color: #4CAF50;
23
+ --warning-color: #FF9800;
24
+ --border-radius: 8px;
25
+ --shadow-light: 0 2px 4px rgba(0,0,0,0.1);
26
+ --shadow-medium: 0 4px 8px rgba(0,0,0,0.15);
27
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
28
+ }
29
+
30
+ body {
31
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
32
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
33
+ min-height: 100vh;
34
+ margin: 0;
35
+ padding: 20px 0;
36
+ }
37
+
38
+ .container-fluid {
39
+ max-width: 800px;
40
+ margin: 0 auto;
41
+ }
42
+
43
+ .game-container {
44
+ background: rgba(255, 255, 255, 0.95);
45
+ border-radius: 16px;
46
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
47
+ backdrop-filter: blur(10px);
48
+ padding: 30px;
49
+ margin: 20px;
50
+ text-align: center;
51
+ }
52
+
53
+ .puzzle-grid {
54
+ display: grid;
55
+ grid-template-columns: repeat(3, var(--tile-size));
56
+ grid-template-rows: repeat(3, var(--tile-size));
57
+ gap: 8px;
58
+ justify-content: center;
59
+ margin: 30px auto;
60
+ padding: 20px;
61
+ background: rgba(0,0,0,0.05);
62
+ border-radius: var(--border-radius);
63
+ }
64
+
65
  .puzzle-tile {
66
+ width: var(--tile-size);
67
+ height: var(--tile-size);
68
+ border: none;
69
+ border-radius: var(--border-radius);
70
+ background: linear-gradient(145deg, var(--primary-color), #1976D2);
71
  color: white;
72
+ font-size: 32px;
73
+ font-weight: 700;
74
  display: flex;
75
  align-items: center;
76
  justify-content: center;
77
  cursor: pointer;
78
+ transition: var(--transition);
79
+ box-shadow: var(--shadow-light);
80
+ position: relative;
81
+ overflow: hidden;
82
+ }
83
+
84
+ .puzzle-tile::before {
85
+ content: '';
86
+ position: absolute;
87
+ top: 0;
88
+ left: -100%;
89
+ width: 100%;
90
+ height: 100%;
91
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
92
+ transition: left 0.5s;
93
+ }
94
+
95
+ .puzzle-tile:hover::before {
96
+ left: 100%;
97
  }
98
 
99
  .puzzle-tile:hover {
100
+ transform: translateY(-2px) scale(1.02);
101
+ box-shadow: var(--shadow-medium);
102
  }
103
 
104
+ .puzzle-tile:active {
105
+ transform: translateY(0) scale(0.98);
106
  }
107
 
108
  .empty-tile {
109
+ background: rgba(255,255,255,0.8);
110
+ border: 2px dashed #ccc;
111
+ box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
112
  }
113
 
114
+ .moves-counter {
115
+ background: white;
116
+ padding: 20px;
117
+ border-radius: var(--border-radius);
118
+ margin: 20px 0;
119
+ box-shadow: var(--shadow-light);
120
+ border-left: 4px solid var(--primary-color);
121
  }
122
 
123
+ .congratulations-card {
124
+ background: linear-gradient(145deg, #4CAF50, #388E3C);
125
+ color: white;
126
+ padding: 25px;
127
+ border-radius: var(--border-radius);
128
+ margin: 20px 0;
129
+ box-shadow: var(--shadow-medium);
130
+ animation: celebration 0.6s ease;
131
+ border: none;
132
  }
133
 
134
+ @keyframes celebration {
135
+ 0% {
136
+ opacity: 0;
137
+ transform: translateY(-20px) scale(0.9);
138
+ }
139
+ 50% {
140
+ transform: translateY(-5px) scale(1.05);
141
+ }
142
+ 100% {
143
+ opacity: 1;
144
+ transform: translateY(0) scale(1);
145
+ }
146
  }
147
 
148
+ .moves-value {
149
+ font-size: 36px;
150
+ font-weight: bold;
151
+ color: var(--primary-color);
 
152
  margin-bottom: 5px;
153
  }
154
 
155
+ .moves-label {
156
+ font-size: 14px;
157
+ color: #666;
158
+ text-transform: uppercase;
159
+ letter-spacing: 1px;
 
160
  }
161
 
162
+ .congratulations-title {
163
+ font-size: 28px;
164
+ font-weight: bold;
165
+ margin-bottom: 10px;
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ gap: 10px;
170
  }
171
 
172
+ .congratulations-subtitle {
173
+ font-size: 18px;
174
+ font-weight: 600;
175
+ opacity: 0.9;
176
+ }
177
+
178
+ .btn-game {
179
+ padding: 15px 30px;
180
+ border: none;
181
+ border-radius: var(--border-radius);
182
+ font-weight: 600;
183
+ font-size: 16px;
184
+ cursor: pointer;
185
+ transition: var(--transition);
186
+ text-transform: uppercase;
187
+ letter-spacing: 0.5px;
188
+ margin: 10px;
189
+ min-width: 140px;
190
  }
191
 
192
+ .btn-success {
193
+ background: linear-gradient(145deg, var(--success-color), #388E3C);
194
+ color: white;
195
+ }
196
+
197
+ .btn-warning {
198
+ background: linear-gradient(145deg, var(--warning-color), #F57C00);
199
+ color: white;
200
+ }
201
+
202
+ .btn-game:hover {
203
+ transform: translateY(-2px);
204
+ box-shadow: var(--shadow-medium);
205
+ }
206
+
207
+ .victory-message {
208
+ background: linear-gradient(145deg, #4CAF50, #388E3C);
209
+ color: white;
210
+ padding: 25px;
211
+ border-radius: var(--border-radius);
212
+ margin: 20px 0;
213
+ font-size: 18px;
214
+ font-weight: 600;
215
+ box-shadow: var(--shadow-medium);
216
+ animation: slideIn 0.5s ease;
217
+ }
218
+
219
+ @keyframes slideIn {
220
+ from {
221
+ opacity: 0;
222
+ transform: translateY(-20px);
223
  }
224
+ to {
225
+ opacity: 1;
226
+ transform: translateY(0);
227
+ }
228
+ }
229
+
230
+ .instructions {
231
+ background: white;
232
+ border-radius: var(--border-radius);
233
+ padding: 25px;
234
+ box-shadow: var(--shadow-light);
235
+ text-align: left;
236
+ margin: 20px 0;
237
+ }
238
+
239
+ .instructions h3 {
240
+ color: var(--primary-color);
241
+ margin-bottom: 15px;
242
+ font-weight: 600;
243
+ text-align: center;
244
+ }
245
+
246
+ .instructions ul {
247
+ padding-left: 20px;
248
+ }
249
+
250
+ .instructions li {
251
+ margin-bottom: 8px;
252
+ line-height: 1.5;
253
+ }
254
+
255
+ .goal-grid {
256
+ display: grid;
257
+ grid-template-columns: repeat(3, 50px);
258
+ gap: 5px;
259
+ justify-content: center;
260
+ margin: 15px 0;
261
+ }
262
+
263
+ .goal-tile {
264
+ width: 50px;
265
+ height: 50px;
266
+ background: var(--primary-color);
267
+ color: white;
268
+ display: flex;
269
+ align-items: center;
270
+ justify-content: center;
271
+ border-radius: 4px;
272
+ font-weight: bold;
273
+ font-size: 16px;
274
+ }
275
+
276
+ .goal-empty {
277
+ background: #f0f0f0;
278
+ border: 2px dashed #ccc;
279
  }
280
 
281
+ /* Responsive Design */
282
+ @media (max-width: 768px) {
283
+ :root {
284
+ --tile-size: 100px;
285
+ }
286
+
287
+ .game-container {
288
+ margin: 10px;
289
+ padding: 20px;
290
+ }
291
+
292
+ .puzzle-tile {
293
+ font-size: 24px;
294
+ }
295
+
296
+ .moves-value {
297
+ font-size: 28px;
298
  }
299
 
300
+ .congratulations-title {
301
+ font-size: 24px;
302
+ }
303
+
304
+ .congratulations-subtitle {
305
+ font-size: 16px;
306
+ }
307
+
308
+ .btn-game {
309
+ min-width: auto;
310
+ padding: 12px 20px;
311
+ font-size: 14px;
312
  }
313
  }
314
 
 
315
  @media (max-width: 480px) {
316
+ :root {
317
+ --tile-size: 80px;
 
318
  }
319
 
320
+ .puzzle-tile {
321
+ font-size: 20px;
322
+ }
323
+
324
+ .moves-value {
325
+ font-size: 24px;
326
+ }
327
+
328
+ .congratulations-title {
329
+ font-size: 20px;
330
+ }
331
+
332
+ .congratulations-subtitle {
333
+ font-size: 14px;
334
  }
335
  }
336
  "))
337
  ),
 
338
 
339
+ div(class = "container-fluid",
340
+ div(class = "game-container",
341
+ # Title
342
+ h1("🧩 Puzzle8",
343
+ style = "background: linear-gradient(145deg, #2196F3, #1976D2);
344
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
345
+ font-weight: 800; font-size: 3rem; margin-bottom: 30px;"),
346
+
347
+ # Dynamic Counter/Congratulations Card
348
+ uiOutput("status_card"),
349
+
350
+ # Puzzle Grid
351
+ div(class = "puzzle-grid",
352
+ lapply(1:9, function(i) {
353
+ div(id = paste0("tile_container_", i), uiOutput(paste0("tile_", i)))
354
+ })
355
+ ),
356
+
357
+ # Control Buttons
358
+ div(class = "text-center",
359
+ actionButton("new_game", "New Game", class = "btn-game btn-success"),
360
+ actionButton("reset_game", "Reset", class = "btn-game btn-warning")
361
+ ),
362
+
363
+ # Instructions
364
+ div(class = "instructions",
365
+ h3("🎯 How to Play"),
366
+ p("Arrange numbers 1-8 in order with the empty space in the bottom-right corner."),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  tags$ul(
368
+ tags$li("Click on tiles adjacent to the empty space to move them"),
369
+ tags$li("Only tiles next to the empty space can be moved"),
370
+ tags$li("Try to solve the puzzle in as few moves as possible!")
371
  ),
372
+
373
+ div(style = "text-align: center; margin-top: 20px;",
374
+ h4("🏆 Goal:"),
375
+ div(class = "goal-grid",
376
+ lapply(1:8, function(i) {
377
+ div(class = "goal-tile", i)
378
+ }),
379
+ div(class = "goal-tile goal-empty")
380
+ )
381
+ )
382
  )
383
  )
384
+ )
 
385
  )
386
 
387
  # Server
388
  server <- function(input, output, session) {
389
+ # Simple game state
390
  game_state <- reactiveValues(
391
+ puzzle = NULL,
392
  initial_puzzle = NULL,
393
  moves = 0,
394
+ game_won = FALSE
 
 
 
 
395
  )
396
 
397
+ # Initialize game
 
 
 
 
 
 
 
 
 
398
  observe({
399
+ if(is.null(game_state$puzzle)) {
400
+ new_puzzle <- create_solvable_puzzle()
401
+ game_state$puzzle <- new_puzzle
402
+ game_state$initial_puzzle <- new_puzzle
 
 
403
  }
404
  })
405
 
406
+ # Tile click handlers
407
  observe({
408
+ lapply(1:9, function(i) {
409
+ observeEvent(input[[paste0("tile_", i)]], {
410
+ if(!game_state$game_won && !is.null(game_state$puzzle)) {
411
+ result <- move_tile(game_state$puzzle, i)
412
+
413
+ if(result$moved) {
414
+ game_state$puzzle <- result$puzzle
415
+ game_state$moves <- game_state$moves + 1
 
 
416
 
417
+ # Check win condition
418
+ if(is_solved(game_state$puzzle)) {
419
+ game_state$game_won <- TRUE
420
+ showNotification(
421
+ paste("🎉 Congratulations! Solved in", game_state$moves, "moves!"),
422
+ type = "message", duration = 5
423
+ )
 
 
 
 
424
  }
425
  }
426
+ }
427
  })
428
+ })
429
  })
430
 
431
+ # New Game button
432
  observeEvent(input$new_game, {
 
433
  new_puzzle <- create_solvable_puzzle()
434
  game_state$puzzle <- new_puzzle
435
+ game_state$initial_puzzle <- new_puzzle
436
  game_state$moves <- 0
437
  game_state$game_won <- FALSE
438
+
439
+ showNotification("New game started!", type = "message")
 
 
 
 
440
  })
441
 
442
+ # Reset button
443
  observeEvent(input$reset_game, {
 
444
  if(!is.null(game_state$initial_puzzle)) {
445
  game_state$puzzle <- game_state$initial_puzzle
446
  game_state$moves <- 0
447
  game_state$game_won <- FALSE
 
 
 
 
 
 
 
 
 
 
 
 
 
448
 
449
+ showNotification("Game reset!", type = "message")
 
 
 
450
  }
451
  })
452
 
453
+ # Render tiles
454
+ lapply(1:9, function(i) {
455
+ output[[paste0("tile_", i)]] <- renderUI({
456
+ req(game_state$puzzle)
457
 
458
+ value <- game_state$puzzle[i]
459
+
460
+ if(value == 0) {
461
+ div(class = "empty-tile")
462
+ } else {
463
+ actionButton(
464
+ paste0("tile_", i),
465
+ label = as.character(value),
466
+ class = "puzzle-tile"
467
+ )
468
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  })
471
 
472
+ # Dynamic Status Card (Moves Counter or Congratulations)
473
+ output$status_card <- renderUI({
474
  if(game_state$game_won) {
475
+ # Congratulations Card
476
+ div(class = "congratulations-card",
477
+ div(class = "congratulations-title",
478
+ "🏆 Congratulations!"
479
+ ),
480
+ div(class = "congratulations-subtitle",
481
+ paste("Puzzle solved in", game_state$moves, "moves!")
482
+ )
483
  )
 
 
 
 
 
 
 
 
 
 
 
 
484
  } else {
485
+ # Moves Counter Card
486
+ div(class = "moves-counter",
487
+ div(class = "moves-value", game_state$moves),
488
+ div(class = "moves-label", "Moves")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  )
490
  }
491
  })
492
  }
493
 
494
+ # Run the application
495
  shinyApp(ui = ui, server = server)