ooRexx logo
   1: #!/usr/bin/env rexx
   2: /*============================================================
   3:  *  puzzle15.rex  —  The 15-puzzle (sliding tile puzzle)
   4:  *
   5:  *  Layout: 4-column x 7-row GTK4Grid
   6:  *    Rows 0-3  : 4x4 puzzle tiles (15 GTK4Buttons + 1 empty cell)
   7:  *    Row  4    : spacer GTK4Label
   8:  *    Row  5    : Undo / Redo / Save / Restore control buttons
   9:  *    Row  6    : GTK4Box (horizontal)
  10:  *                  ├── 🌙/☀ theme toggle button (left)
  11:  *                  └── status GTK4Label (fills remainder)
  12:  *
  13:  *  Features:
  14:  *    - Guaranteed-solvable shuffle via parity check
  15:  *    - Unlimited in-session undo/redo
  16:  *    - Save/Restore: snapshots current board, resets undo/redo
  17:  *    - Win detection with congratulations message
  18:  *    - Dark/Light theme toggle via ::resource CSS blocks
  19:  *    - arg1 in callbacks is the live GTK4Button (v2.0 ORXOBJ)
  20:  *
  21:  *  Theming:
  22:  *    All colours/fonts/sizes live in the ::resource CSS_dark
  23:  *    and ::resource CSS_light blocks at the bottom of this file.
  24:  *    Edit either block freely — no application logic to touch.
  25:  *    Window title/size are in the CONSTANTS section below.
  26:  *
  27:  *  Run:   rexx puzzle15.rex
  28:  *============================================================*/
  29: 
  30: /*------------------------------------------------------------
  31:  * CONSTANTS — edit here to change window title / size
  32:  *------------------------------------------------------------*/
  33: WIN_TITLE  = "15-Puzzle"
  34: WIN_WIDTH  = 380
  35: WIN_HEIGHT = 660
  36: 
  37: /*------------------------------------------------------------
  38:  * Load initial theme (dark) from ::resource CSS_dark
  39:  *------------------------------------------------------------*/
  40: .local~puzzle_theme = 'dark'
  41: .GTK4CSS~loadString(.resources~CSS_dark)
  42: 
  43: /*------------------------------------------------------------
  44:  * Initialise application state in .local
  45:  *
  46:  *  puzzle_board   : stem, indices 0-15 (1-15 = tile, 0 = empty)
  47:  *  puzzle_empty   : current index of the empty cell
  48:  *  puzzle_buttons : stem, indices 0-15, GTK4Button objects
  49:  *  puzzle_undo    : array of board snapshots for undo
  50:  *  puzzle_redo    : array of board snapshots for redo
  51:  *  puzzle_saved   : saved board snapshot (or .nil)
  52:  *  puzzle_solved  : 1 if puzzle is in solved state
  53:  *  puzzle_theme   : 'dark' or 'light'
  54:  *  ctrl_undo      : GTK4Button Undo
  55:  *  ctrl_redo      : GTK4Button Redo
  56:  *  ctrl_save      : GTK4Button Save
  57:  *  ctrl_restore   : GTK4Button Restore
  58:  *  ctrl_theme     : GTK4Button theme toggle
  59:  *  puzzle_status  : GTK4Label status message
  60:  *------------------------------------------------------------*/
  61: .local~puzzle_board   = .stem~new
  62: .local~puzzle_buttons = .stem~new
  63: .local~puzzle_undo    = .array~new
  64: .local~puzzle_redo    = .array~new
  65: .local~puzzle_saved   = .nil
  66: .local~puzzle_solved  = 0
  67: 
  68: /* Solved state: positions 0-14 = tiles 1-15, position 15 = empty */
  69: do i = 0 to 14
  70:     .local~puzzle_board[i] = i + 1
  71: end
  72: .local~puzzle_board[15] = 0
  73: .local~puzzle_empty      = 15
  74: 
  75: /* Shuffle to a guaranteed-solvable position */
  76: call ShuffleBoard
  77: 
  78: /*------------------------------------------------------------
  79:  * Build the window
  80:  *------------------------------------------------------------*/
  81: app = .GTK4App~new
  82: win = .GTK4Window~new(WIN_TITLE, WIN_WIDTH, WIN_HEIGHT)
  83: win~connect("close-request", .routines~OnClose)
  84: 
  85: grid = .GTK4Grid~new
  86: grid~setRowSpacing(0)
  87: grid~setColumnSpacing(0)
  88: grid~setMargin(12, 12, 12, 12)
  89: 
  90: /*------------------------------------------------------------
  91:  * Rows 0-3: puzzle tile buttons
  92:  * All 16 buttons get the signal connected at creation time —
  93:  * including the initial empty cell.  setSensitive(0) suppresses
  94:  * clicks while empty; setSensitive(1) in RefreshTiles re-enables
  95:  * when a tile slides in (with the connection already in place).
  96:  *------------------------------------------------------------*/
  97: do idx = 0 to 15
  98:     tile = .local~puzzle_board[idx]
  99:     btn  = .GTK4Button~new("")
 100:     btn~connect("clicked", .routines~OnTileClick)
 101:     if tile = 0 then do
 102:         btn~addCSSClass("empty-cell")
 103:         btn~setSensitive(0)
 104:     end
 105:     else do
 106:         btn~setLabel(tile~string)
 107:         btn~addCSSClass("tile")
 108:     end
 109:     btn~setHExpand(1)
 110:     btn~setVExpand(1)
 111:     col = idx // 4
 112:     row = idx %  4
 113:     grid~attach(btn, col, row, 1, 1)
 114:     .local~puzzle_buttons[idx] = btn
 115: end
 116: 
 117: /*------------------------------------------------------------
 118:  * Row 4: spacer
 119:  *------------------------------------------------------------*/
 120: spacer = .GTK4Label~new("")
 121: spacer~setVExpand(0)
 122: grid~attach(spacer, 0, 4, 4, 1)
 123: 
 124: /*------------------------------------------------------------
 125:  * Row 5: control buttons
 126:  *------------------------------------------------------------*/
 127: btnUndo    = .GTK4Button~new("⟵ Undo")
 128: btnRedo    = .GTK4Button~new("Redo ⟶")
 129: btnSave    = .GTK4Button~new("💾 Save")
 130: btnRestore = .GTK4Button~new("↺ Restore")
 131: 
 132: btnUndo~addCSSClass("ctrl-btn")
 133: btnRedo~addCSSClass("ctrl-btn")
 134: btnSave~addCSSClass("ctrl-btn")
 135: btnRestore~addCSSClass("ctrl-btn")
 136: 
 137: btnUndo~setHExpand(1)
 138: btnRedo~setHExpand(1)
 139: btnSave~setHExpand(1)
 140: btnRestore~setHExpand(1)
 141: 
 142: btnUndo~connect("clicked",    .routines~OnUndo)
 143: btnRedo~connect("clicked",    .routines~OnRedo)
 144: btnSave~connect("clicked",    .routines~OnSave)
 145: btnRestore~connect("clicked", .routines~OnRestore)
 146: 
 147: btnUndo~setSensitive(0)     /* nothing to undo yet  */
 148: btnRedo~setSensitive(0)     /* nothing to redo yet  */
 149: btnRestore~setSensitive(0)  /* no save done yet     */
 150: 
 151: grid~attach(btnUndo,    0, 5, 1, 1)
 152: grid~attach(btnRedo,    1, 5, 1, 1)
 153: grid~attach(btnSave,    2, 5, 1, 1)
 154: grid~attach(btnRestore, 3, 5, 1, 1)
 155: 
 156: .local~ctrl_undo    = btnUndo
 157: .local~ctrl_redo    = btnRedo
 158: .local~ctrl_save    = btnSave
 159: .local~ctrl_restore = btnRestore
 160: 
 161: /*------------------------------------------------------------
 162:  * Row 6: horizontal box
 163:  *   [☀ Light]  [status label ...........................]
 164:  *
 165:  * The theme toggle sits on the left; the status label fills
 166:  * the rest of the row via setHExpand(1).
 167:  * Toggle label shows the *other* theme (next action):
 168:  *   dark  active → button shows "☀ Light"
 169:  *   light active → button shows "🌙 Dark"
 170:  *------------------------------------------------------------*/
 171: statusBox = .GTK4Box~new('H', 6)
 172: statusBox~setMargin(0, 0, 4, 4)
 173: 
 174: btnTheme = .GTK4Button~new("☀ Light")
 175: btnTheme~addCSSClass("theme-btn")
 176: btnTheme~connect("clicked", .routines~OnThemeToggle)
 177: .local~ctrl_theme = btnTheme
 178: 
 179: statusLbl = .GTK4Label~new("Slide a tile to begin.")
 180: statusLbl~addCSSClass("status")
 181: statusLbl~setXAlign(0.0)
 182: statusLbl~setHExpand(1)
 183: 
 184: statusBox~append(btnTheme)
 185: statusBox~append(statusLbl)
 186: 
 187: grid~attach(statusBox, 0, 6, 4, 1)
 188: .local~puzzle_status = statusLbl
 189: 
 190: /*------------------------------------------------------------
 191:  * Show window and run
 192:  *------------------------------------------------------------*/
 193: win~setChild(grid)
 194: win~present
 195: app~run
 196: exit
 197: 
 198: 
 199: /*============================================================
 200:  * ShuffleBoard
 201:  *============================================================*/
 202: ::routine ShuffleBoard public
 203:     board = .local~puzzle_board
 204: 
 205:     do i = 15 to 1 by -1
 206:         j = random(0, i)
 207:         tmp      = board[i]
 208:         board[i] = board[j]
 209:         board[j] = tmp
 210:     end
 211: 
 212:     emptyIdx = 0
 213:     do i = 0 to 15
 214:         if board[i] = 0 then emptyIdx = i
 215:     end
 216:     .local~puzzle_empty = emptyIdx
 217: 
 218:     inversions = 0
 219:     do i = 0 to 14
 220:         if board[i] = 0 then iterate
 221:         do j = i+1 to 15
 222:             if board[j] = 0 then iterate
 223:             if board[i] > board[j] then inversions = inversions + 1
 224:         end
 225:     end
 226: 
 227:     emptyRowFromBottom = 4 - (emptyIdx % 4)
 228: 
 229:     if (inversions + emptyRowFromBottom) // 2 \= 0 then do
 230:         p1 = 0; p2 = 0; found = 0
 231:         do i = 0 to 15 while found < 2
 232:             if board[i] \= 0 then do
 233:                 found = found + 1
 234:                 if found = 1 then p1 = i
 235:                 else p2 = i
 236:             end
 237:         end
 238:         tmp       = board[p1]
 239:         board[p1] = board[p2]
 240:         board[p2] = tmp
 241:     end
 242:     return
 243: 
 244: 
 245: /*============================================================
 246:  * BoardToArray
 247:  *============================================================*/
 248: ::routine BoardToArray public
 249:     snap = .array~new(16)
 250:     do i = 0 to 15
 251:         snap[i+1] = .local~puzzle_board[i]
 252:     end
 253:     return snap
 254: 
 255: 
 256: /*============================================================
 257:  * ArrayToBoard
 258:  *============================================================*/
 259: ::routine ArrayToBoard public
 260:     use arg snap
 261:     do i = 0 to 15
 262:         .local~puzzle_board[i] = snap[i+1]
 263:         if snap[i+1] = 0 then .local~puzzle_empty = i
 264:     end
 265:     return
 266: 
 267: 
 268: /*============================================================
 269:  * RefreshTiles
 270:  *============================================================*/
 271: ::routine RefreshTiles public
 272:     board   = .local~puzzle_board
 273:     buttons = .local~puzzle_buttons
 274:     do idx = 0 to 15
 275:         btn  = buttons[idx]
 276:         tile = board[idx]
 277:         if tile = 0 then do
 278:             btn~setLabel("")
 279:             btn~setSensitive(0)
 280:             btn~removeCSSClass("tile")
 281:             btn~addCSSClass("empty-cell")
 282:         end
 283:         else do
 284:             btn~setLabel(tile~string)
 285:             btn~setSensitive(1)
 286:             btn~removeCSSClass("empty-cell")
 287:             btn~addCSSClass("tile")
 288:         end
 289:     end
 290:     return
 291: 
 292: 
 293: /*============================================================
 294:  * UpdateControls
 295:  *============================================================*/
 296: ::routine UpdateControls public
 297:     .local~ctrl_undo~setSensitive(.local~puzzle_undo~items > 0)
 298:     .local~ctrl_redo~setSensitive(.local~puzzle_redo~items > 0)
 299:     .local~ctrl_restore~setSensitive(.local~puzzle_saved \= .nil)
 300:     return
 301: 
 302: 
 303: /*============================================================
 304:  * CheckWin
 305:  *============================================================*/
 306: ::routine CheckWin public
 307:     board = .local~puzzle_board
 308:     do i = 0 to 14
 309:         if board[i] \= i+1 then return 0
 310:     end
 311:     if board[15] \= 0 then return 0
 312:     return 1
 313: 
 314: 
 315: /*============================================================
 316:  * OnTileClick
 317:  *============================================================*/
 318: ::routine OnTileClick public
 319:     use arg btn, signalName
 320: 
 321:     if .local~puzzle_solved then return
 322: 
 323:     buttons  = .local~puzzle_buttons
 324:     clickIdx = -1
 325:     do i = 0 to 15
 326:         if buttons[i] = btn then do
 327:             clickIdx = i
 328:             leave
 329:         end
 330:     end
 331:     if clickIdx = -1 then return
 332: 
 333:     emptyIdx = .local~puzzle_empty
 334: 
 335:     colClick = clickIdx // 4
 336:     colEmpty = emptyIdx // 4
 337:     rowClick = clickIdx %  4
 338:     rowEmpty = emptyIdx %  4
 339: 
 340:     adjacent = 0
 341:     if rowClick = rowEmpty & abs(colClick - colEmpty) = 1 then adjacent = 1
 342:     if colClick = colEmpty & abs(rowClick - rowEmpty) = 1 then adjacent = 1
 343: 
 344:     if \adjacent then do
 345:         .local~puzzle_status~setText("That tile can't move there.")
 346:         return
 347:     end
 348: 
 349:     .local~puzzle_undo~append(BoardToArray())
 350:     .local~puzzle_redo = .array~new
 351: 
 352:     .local~puzzle_board[emptyIdx] = .local~puzzle_board[clickIdx]
 353:     .local~puzzle_board[clickIdx] = 0
 354:     .local~puzzle_empty           = clickIdx
 355: 
 356:     call RefreshTiles
 357:     call UpdateControls
 358: 
 359:     if CheckWin() then do
 360:         .local~puzzle_solved = 1
 361:         .local~puzzle_status~setMarkup( -
 362:             '' || -
 363:             '🎉 Congratulations — Puzzle Solved! 🎉')
 364:         do i = 0 to 15
 365:             .local~puzzle_buttons[i]~setSensitive(0)
 366:         end
 367:     end
 368:     else
 369:         .local~puzzle_status~setText("Keep going!")
 370:     return
 371: 
 372: 
 373: /*============================================================
 374:  * OnUndo
 375:  *============================================================*/
 376: ::routine OnUndo public
 377:     use arg btn, signalName
 378:     if .local~puzzle_undo~items = 0 then return
 379:     .local~puzzle_redo~append(BoardToArray())
 380:     snap = .local~puzzle_undo~remove(.local~puzzle_undo~items)
 381:     call ArrayToBoard snap
 382:     .local~puzzle_solved = 0
 383:     call RefreshTiles
 384:     call UpdateControls
 385:     .local~puzzle_status~setText("Undo.")
 386:     return
 387: 
 388: 
 389: /*============================================================
 390:  * OnRedo
 391:  *============================================================*/
 392: ::routine OnRedo public
 393:     use arg btn, signalName
 394:     if .local~puzzle_redo~items = 0 then return
 395:     .local~puzzle_undo~append(BoardToArray())
 396:     snap = .local~puzzle_redo~remove(.local~puzzle_redo~items)
 397:     call ArrayToBoard snap
 398:     .local~puzzle_solved = 0
 399:     call RefreshTiles
 400:     call UpdateControls
 401:     .local~puzzle_status~setText("Redo.")
 402:     return
 403: 
 404: 
 405: /*============================================================
 406:  * OnSave
 407:  *============================================================*/
 408: ::routine OnSave public
 409:     use arg btn, signalName
 410:     .local~puzzle_saved = BoardToArray()
 411:     .local~puzzle_undo  = .array~new
 412:     .local~puzzle_redo  = .array~new
 413:     call UpdateControls
 414:     .local~puzzle_status~setText("Position saved.  Undo/Redo history cleared.")
 415:     return
 416: 
 417: 
 418: /*============================================================
 419:  * OnRestore
 420:  *============================================================*/
 421: ::routine OnRestore public
 422:     use arg btn, signalName
 423:     if .local~puzzle_saved = .nil then return
 424:     call ArrayToBoard .local~puzzle_saved
 425:     .local~puzzle_undo   = .array~new
 426:     .local~puzzle_redo   = .array~new
 427:     .local~puzzle_solved = 0
 428:     call RefreshTiles
 429:     call UpdateControls
 430:     .local~puzzle_status~setText("Position restored.  Undo/Redo history cleared.")
 431:     return
 432: 
 433: 
 434: /*============================================================
 435:  * OnThemeToggle — switch between dark and light themes
 436:  * Loads the appropriate ::resource CSS block and updates the
 437:  * toggle button label to show the *other* theme (next action).
 438:  *============================================================*/
 439: ::routine OnThemeToggle public
 440:     use arg btn, signalName
 441:     if .local~puzzle_theme = 'dark' then do
 442:         .GTK4CSS~loadString(.resources~CSS_light)
 443:         .local~puzzle_theme = 'light'
 444:         .local~ctrl_theme~setLabel("🌙 Dark")
 445:     end
 446:     else do
 447:         .GTK4CSS~loadString(.resources~CSS_dark)
 448:         .local~puzzle_theme = 'dark'
 449:         .local~ctrl_theme~setLabel("☀ Light")
 450:     end
 451:     return
 452: 
 453: 
 454: /*============================================================
 455:  * OnClose
 456:  *============================================================*/
 457: ::routine OnClose public
 458:     call GTK4Quit
 459: 
 460: 
 461: ::requires 'gtk4.cls'
 462: 
 463: 
 464: /*============================================================
 465:  * CSS_dark — deep navy / soft purple tile theme
 466:  *
 467:  * Edit this block to restyle the dark theme.
 468:  * Classes: .tile  .empty-cell  .ctrl-btn  .theme-btn  .status
 469:  *============================================================*/
 470: ::resource CSS_dark
 471: window {
 472:   background-color: #1a1a2e;
 473: }
 474: .tile {
 475:   min-width:   80px;
 476:   min-height:  80px;
 477:   font-size:   28px;
 478:   font-weight: bold;
 479:   color:       #1a1a2e;
 480:   background:  linear-gradient(135deg, #e0e0ff 0%, #a0a0dd 100%);
 481:   border-radius: 8px;
 482:   border:      2px solid #6060bb;
 483:   margin:      4px;
 484: }
 485: .tile:hover {
 486:   background:  linear-gradient(135deg, #f0f0ff 0%, #c0c0ff 100%);
 487: }
 488: .empty-cell {
 489:   min-width:   80px;
 490:   min-height:  80px;
 491:   background:  #0d0d1a;
 492:   border-radius: 8px;
 493:   border:      2px solid #2a2a4a;
 494:   margin:      4px;
 495: }
 496: .ctrl-btn {
 497:   min-height:  36px;
 498:   font-size:   13px;
 499:   font-weight: bold;
 500:   color:       #e0e0ff;
 501:   background:  #16213e;
 502:   border-radius: 6px;
 503:   border:      1px solid #4040aa;
 504:   margin:      4px 4px 0px 4px;
 505: }
 506: .ctrl-btn:hover    { background: #0f3460; }
 507: .ctrl-btn:disabled { opacity: 0.35; }
 508: .theme-btn {
 509:   min-height:  28px;
 510:   font-size:   12px;
 511:   color:       #c0c0ff;
 512:   background:  #16213e;
 513:   border-radius: 6px;
 514:   border:      1px solid #4040aa;
 515:   margin:      2px 4px 2px 0px;
 516: }
 517: .theme-btn:hover { background: #0f3460; }
 518: .status {
 519:   font-size:   14px;
 520:   color:       #a0a0dd;
 521:   margin-top:  4px;
 522: }
 523: ::END
 524: 
 525: 
 526: /*============================================================
 527:  * CSS_light — warm white / teal tile theme
 528:  *
 529:  * Edit this block to restyle the light theme.
 530:  * Same classes as CSS_dark.
 531:  *============================================================*/
 532: ::resource CSS_light
 533: window {
 534:   background-color: #f0f4f8;
 535: }
 536: .tile {
 537:   min-width:   80px;
 538:   min-height:  80px;
 539:   font-size:   28px;
 540:   font-weight: bold;
 541:   color:       #1a3a4a;
 542:   background:  linear-gradient(135deg, #ffffff 0%, #b2e0ec 100%);
 543:   border-radius: 8px;
 544:   border:      2px solid #4a9ab5;
 545:   margin:      4px;
 546: }
 547: .tile:hover {
 548:   background:  linear-gradient(135deg, #ffffff 0%, #d0f0ff 100%);
 549: }
 550: .empty-cell {
 551:   min-width:   80px;
 552:   min-height:  80px;
 553:   background:  #d8e8f0;
 554:   border-radius: 8px;
 555:   border:      2px solid #a0c8d8;
 556:   margin:      4px;
 557: }
 558: .ctrl-btn {
 559:   min-height:  36px;
 560:   font-size:   13px;
 561:   font-weight: bold;
 562:   color:       #1a3a4a;
 563:   background:  #cce8f4;
 564:   border-radius: 6px;
 565:   border:      1px solid #4a9ab5;
 566:   margin:      4px 4px 0px 4px;
 567: }
 568: .ctrl-btn:hover    { background: #a8d8ee; }
 569: .ctrl-btn:disabled { opacity: 0.35; }
 570: .theme-btn {
 571:   min-height:  28px;
 572:   font-size:   12px;
 573:   color:       #1a3a4a;
 574:   background:  #cce8f4;
 575:   border-radius: 6px;
 576:   border:      1px solid #4a9ab5;
 577:   margin:      2px 4px 2px 0px;
 578: }
 579: .theme-btn:hover { background: #a8d8ee; }
 580: .status {
 581:   font-size:   14px;
 582:   color:       #2a6a8a;
 583:   margin-top:  4px;
 584: }
 585: ::END
All content © Ruurd Idenburg, 2007–2026, except where marked otherwise. All rights reserved. This page is primarily for non-commercial use only. The Idenburg website records no personal information and sets no ‘cookies’. This site is hosted on my on server at my home, falling under Dutch (privacy) laws.

This page updated on Thu, 30 Apr 2026 23:37:59 +0200.