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