16 puzzle game

Revision as of 23:28, 25 August 2022 by Thundergnat (talk | contribs) (syntax highlighting fixup automation)

16 numbered pieces of the puzzle are placed out of order on a 4 X 4 grid. The correct order to win is to order the pieces as 1 through 16, read left to right, top to bottom:

16 puzzle game is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.
  1  2  3  4
  5  6  7  8
  9 10 11 12
 13 14 15 16

How to Play: The aim is to get the pieces back in order by clicking on the yellow arrows (choosing a location to rotate a row or column) to slide the pieces left or right, up or down.

  1 14  3  4          1  2  3  4
  5  2  7  8   -->    5  6  7  8
  9  6 11 12          9 10 11 12
 13 10 15 16         13 14 15 16
     ^

The Easy puzzle target is 3 moves, for the Hard puzzle it is 12 moves (or less!). Can it be that simple?

Task

Create 16 Puzzle Game.


See details: [1]


Video: 16 Puzzle Game


Related task



AutoHotkey

With Solver, See 16_puzzle_game/autohotkey

Go

package main

import (
    "bufio"
    "fmt"
    "log"
    "math/rand"
    "os"
    "strings"
    "time"
)

const (
    easy = 1
    hard = 4
)

var n [16]int

func initGrid() {
    for i := 0; i < 16; i++ {
        n[i] = i + 1
    }
}

func setDiff(level int) {
    moves := 3
    if level == hard {
        moves = 12
    }
    rc := make([]int, 0, 4)
    for i := 0; i < moves; i++ {
        rc = rc[:0]
        r := rand.Intn(2)
        s := rand.Intn(4)
        if r == 0 { // rotate random row
            for j := s * 4; j < (s+1)*4; j++ {
                rc = append(rc, j)
            }
        } else { // rotate random column
            for j := s; j < s+16; j += 4 {
                rc = append(rc, j)
            }
        }
        var rca [4]int
        copy(rca[:], rc)
        rotate(rca)
        if hasWon() { // do it again
            i = -1
        }
    }
    fmt.Println("Target is", moves, "moves.")
}

func drawGrid() {
    fmt.Println()
    fmt.Println("     D1   D2   D3   D4")
    fmt.Println("   ╔════╦════╦════╦════╗")
    fmt.Printf("R1 ║ %2d ║ %2d ║ %2d ║ %2d ║ L1\n", n[0], n[1], n[2], n[3])
    fmt.Println("   ╠════╬════╬════╬════╣")
    fmt.Printf("R2 ║ %2d ║ %2d ║ %2d ║ %2d ║ L2\n", n[4], n[5], n[6], n[7])
    fmt.Println("   ╠════╬════╬════╬════╣")
    fmt.Printf("R3 ║ %2d ║ %2d ║ %2d ║ %2d ║ L3\n", n[8], n[9], n[10], n[11])
    fmt.Println("   ╠════╬════╬════╬════╣")
    fmt.Printf("R4 ║ %2d ║ %2d ║ %2d ║ %2d ║ L4\n", n[12], n[13], n[14], n[15])
    fmt.Println("   ╚════╩════╩════╩════╝")
    fmt.Println("     U1   U2   U3   U4\n")
}

func rotate(ix [4]int) {
    last := n[ix[3]]
    for i := 3; i >= 1; i-- {
        n[ix[i]] = n[ix[i-1]]
    }
    n[ix[0]] = last
}

func hasWon() bool {
    for i := 0; i < 16; i++ {
        if n[i] != i+1 {
            return false
        }
    }
    return true
}

func check(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func main() {
    initGrid()
    rand.Seed(time.Now().UnixNano())
    level := easy
    scanner := bufio.NewScanner(os.Stdin)
    for {
        fmt.Print("Enter difficulty level easy or hard E/H : ")
        scanner.Scan()
        diff := strings.ToUpper(strings.TrimSpace(scanner.Text()))
        if diff != "E" && diff != "H" {
            fmt.Println("Invalid response, try again.")
        } else {
            if diff == "H" {
                level = hard
            }
            break
        }
    }
    check(scanner.Err())
    setDiff(level)
    var ix [4]int
    fmt.Println("When entering moves, you can also enter Q to quit or S to start again.")
    moves := 0
outer:
    for {
        drawGrid()
        if hasWon() {
            fmt.Println("Congratulations, you have won the game in", moves, "moves!!")
            return
        }
        for {
            fmt.Println("Moves so far =", moves, "\n")
            fmt.Print("Enter move : ")
            scanner.Scan()
            move := strings.ToUpper(strings.TrimSpace(scanner.Text()))
            check(scanner.Err())
            switch move {
            case "D1", "D2", "D3", "D4":
                c := int(move[1] - 49)
                ix[0] = 0 + c
                ix[1] = 4 + c
                ix[2] = 8 + c
                ix[3] = 12 + c
                rotate(ix)
                moves++
                continue outer
            case "L1", "L2", "L3", "L4":
                c := int(move[1] - 49)
                ix[0] = 3 + 4*c
                ix[1] = 2 + 4*c
                ix[2] = 1 + 4*c
                ix[3] = 0 + 4*c
                rotate(ix)
                moves++
                continue outer
            case "U1", "U2", "U3", "U4":
                c := int(move[1] - 49)
                ix[0] = 12 + c
                ix[1] = 8 + c
                ix[2] = 4 + c
                ix[3] = 0 + c
                rotate(ix)
                moves++
                continue outer
            case "R1", "R2", "R3", "R4":
                c := int(move[1] - 49)
                ix[0] = 0 + 4*c
                ix[1] = 1 + 4*c
                ix[2] = 2 + 4*c
                ix[3] = 3 + 4*c
                rotate(ix)
                moves++
                continue outer
            case "Q":
                return
            case "S":
                initGrid()
                setDiff(level)
                moves = 0
                continue outer
            default:
                fmt.Println("Invalid move, try again.")
            }
        }
    }
}
Output:

Sample game:

Enter difficulty level easy or hard E/H : e
Target is 3 moves.
When entering moves, you can also enter Q to quit or S to start again.

     D1   D2   D3   D4
   ╔════╦════╦════╦════╗
R1 ║  1 ║  2 ║  3 ║  4 ║ L1
   ╠════╬════╬════╬════╣
R2 ║  7 ║  8 ║  5 ║  6 ║ L2
   ╠════╬════╬════╬════╣
R3 ║  9 ║ 10 ║ 11 ║ 12 ║ L3
   ╠════╬════╬════╬════╣
R4 ║ 16 ║ 13 ║ 14 ║ 15 ║ L4
   ╚════╩════╩════╩════╝
     U1   U2   U3   U4

Moves so far = 0 

Enter move : l4

     D1   D2   D3   D4
   ╔════╦════╦════╦════╗
R1 ║  1 ║  2 ║  3 ║  4 ║ L1
   ╠════╬════╬════╬════╣
R2 ║  7 ║  8 ║  5 ║  6 ║ L2
   ╠════╬════╬════╬════╣
R3 ║  9 ║ 10 ║ 11 ║ 12 ║ L3
   ╠════╬════╬════╬════╣
R4 ║ 13 ║ 14 ║ 15 ║ 16 ║ L4
   ╚════╩════╩════╩════╝
     U1   U2   U3   U4

Moves so far = 1 

Enter move : l2

     D1   D2   D3   D4
   ╔════╦════╦════╦════╗
R1 ║  1 ║  2 ║  3 ║  4 ║ L1
   ╠════╬════╬════╬════╣
R2 ║  8 ║  5 ║  6 ║  7 ║ L2
   ╠════╬════╬════╬════╣
R3 ║  9 ║ 10 ║ 11 ║ 12 ║ L3
   ╠════╬════╬════╬════╣
R4 ║ 13 ║ 14 ║ 15 ║ 16 ║ L4
   ╚════╩════╩════╩════╝
     U1   U2   U3   U4

Moves so far = 2 

Enter move : l2

     D1   D2   D3   D4
   ╔════╦════╦════╦════╗
R1 ║  1 ║  2 ║  3 ║  4 ║ L1
   ╠════╬════╬════╬════╣
R2 ║  5 ║  6 ║  7 ║  8 ║ L2
   ╠════╬════╬════╬════╣
R3 ║  9 ║ 10 ║ 11 ║ 12 ║ L3
   ╠════╬════╬════╬════╣
R4 ║ 13 ║ 14 ║ 15 ║ 16 ║ L4
   ╚════╩════╩════╩════╝
     U1   U2   U3   U4

Congratulations, you have won the game in 3 moves!!

J

Assumes a recent release of jqt:

require'ide/qt/gl2'
coinsert'jgl2'

NB. event handlers
game_reset_button=: {{ draw COUNT=: #SETUP=: EMPTY [BOARD=: WIN }}
game_restart_button=: {{ draw COUNT=: 0 [rotate"1 SETUP [BOARD=: WIN }}
game_easy_button=: {{ setup 3 }}
game_hard_button=: {{ setup 15 }}
game_board_mbldown=: {{ 
  loc=. (20+40*i.5)I.2{._".sysdata
  if. 1=#ndx=. loc -. 0 5 do.
    side=. 2#.<:>.loc%4 NB. _2: left, _1 top, 1 bottom, 2 right
    draw rotate side, ndx
  end.
}}

NB. game logic
BOARD=: WIN=: 1+i.4 4
message=: {{
  color=. (COUNT>#SETUP){::;:'black red'
  A=. '<span style="color: ',color,'">' [Z=. '</span>'
  if. BOARD-:WIN do. A,'You win',Z return. end.
  A,(":COUNT),Z,' of ',":#SETUP
}}
setup=: {{ game_restart_button SETUP=: (_2 _1 1 2{~?y#4),.1+?y#4 }}
rotate=: {{
  COUNT=: COUNT+1
  'side ndx'=. y-0 1
  flip=. |: if. 2=|side do. flip=. ] end.
  BOARD=: flip ((*side)|.ndx{flip BOARD) ndx} flip BOARD
}}

NB. rendering
wd {{)n
  pc game closeok;
  cc board isidraw;
  set board wh 200 200;
  cc msg static center;
  bin h;
    cc easy button;
    set easy tooltip start game which can be completed in 3 moves;
    cc hard button;
    set hard tooltip start game which can be completed in 15 moves;
  bin z; bin h;
    cc restart button;
    cc reset button;
    set reset tooltip set board to initial position, ending any current game;
  pshow;
}}

draw=: {{
  glclear''
  glbrush glrgb 3#224     NB. silver
  glrect 0 0 200 200
  glbrush glrgb 0 0 255   NB. blue
  glrect T=:20 20 40 40+"1]40*4 4 1 1#:i.4 4
  glbrush glrgb 255 255 0 NB. yellow
  glpolygon (,200-])(,;"1@(_2<@|.\"1]))0 30 0 50 10 40+"1(i.4)*/6$0 40
  gltextcolor glrgb 3#255 NB. white
  glfont '"lucidia console" 16'
  BOARD {{ gltext ":x [ gltextxy y+5 0*_1^1<#":x }}"_2(30+40*4 4#:|:i.4 4)
  if. EMPTY-:SETUP do.
    wd {{)n
     set msg text <b>easy</b> or <b>hard</b> to start;
     set restart enable 0;
     set restart caption;
}} else. wd {{)n
    set msg text MESSAGE;
    set restart enable 1;
    set restart caption restart;
}} rplc 'MESSAGE';message''
  end.
  glpaint''
}}

NB. start:
game_reset_button''

JavaScript

Try it here.

You'll also need a html file:

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <link href="https://fonts.googleapis.com/css?family=Ubuntu+Mono&display=swap" rel="stylesheet">
 <link rel="stylesheet" type="text/css" media="screen" href="./css/main.css">
 <title>16 Puzzle</title>
</head>
<body>
 <div id="done">WELL DONE!</div>
 <div id="board"></div>
 <div id="moves"></div>
 <button id="shuffle">SHUFFLE</button>
 <script src="./src/index.js" type="module"></script>
</body>
</html>

And css file:

* {
 margin: 0;
 border: 0;
 text-align: center;
 font-family: 'Ubuntu Mono', monospace;
 user-select: none;
}
button {
 border-radius: 5px;
 width: 300px;
 height: 80px;
 font-size: 40px;
 margin-top: 60px;
}
#board {
 width: 410px;
 height: 410px;
 margin: 120px auto 30px auto;
}
#done {
 font-size: 140px;
 padding: 20px;
 color: #fff;
 background-color: rgba(0, 23, 56, .5);
 border: 1px solid rgb(0, 90, 220);
 width: 700px;
 position: absolute;
 top: 250px;
 left: calc(50% - 380px);
}
#moves {
 font-size: 40px;
 line-height: 80px;
 height: 80px;
 width: 300px;
 margin: auto;
 border: 1px solid #000;
 border-radius: 5px;
}
.btn,
.numbers,
.hide {
 float: left;
 width: 64px;
 height: 64px;
 line-height: 65px;
 font-size: 40px;
 border: 1px solid black;
 color: black;
 background-color: white;
 cursor: none;
 margin: 1px;
 transition: all .3s;
}
.btn:hover {
 background-color: rgba(71, 231, 71, 0.5);
 cursor: pointer;
}
.hide {
 border: 1px solid white;
 cursor: none;
}
class Puzzle {
 constructor() {
  this.moves;
  this.started;

  this.board = document.getElementById("board");
  document.getElementById("shuffle").addEventListener("click", () => {
   this.shuffle();
  });
  this.reset();
 }

 reset() {
  while (this.board.hasChildNodes()) {
   this.board.removeChild(this.board.firstChild);
  }

  this.moves = 0;
  this.started = false;
  document.getElementById("moves").innerText = this.moves;
  document.getElementById("done").style.visibility = "hidden";

  let t = 1;
  for (let y = 0; y < 6; y++) {
   for (let x = 0; x < 6; x++) {
    const d = document.createElement("div");
    d.id = `${x}_${y}`;
    if (y === 0 || x === 0 || y === 5 || x === 5) {
     if (((y === 0 || y === 5) && (x === 0 || x === 5))) {
      d.className = "hide";
     } else {
      d.className = "btn";
      if (y === 0) {
       d.innerText = "🡇";
       d.func = () => {
        this.rollDownRight(x, true);
       };
      } else if (y === 5) {
       d.innerText = "🡅";
       d.func = () => {
        this.rollUpLeft(x, true);
       };
      }
      if (x === 0) {
       d.innerText = "🡆";
       d.func = () => {
        this.rollDownRight(y, false);
       };
      } else if (x === 5) {
       d.innerText = "🡄";
       d.func = () => {
        this.rollUpLeft(y, false);
       };
      }
      d.addEventListener("click", (ev) => {
       ev.srcElement.func();
      })
     }
    } else {
     d.className = "numbers";
     d.innerText = `${t}`;
     t++;
    }
    this.board.appendChild(d);
   }
  }
  document.getElementById("shuffle").innerText = "SHUFFLE";
 }

 shuffle() {
  if (this.started) {
   this.reset();
  } else {
   this.started = true;
   const e = Math.floor(Math.random() * 30) + 30;
   for (let z = 0; z < e; z++) {
    switch (Math.floor(Math.random() * 4)) {
     case 0:
      this.rollDownRight(Math.floor(Math.random() * 4) + 1, false);
      break;
     case 1:
      this.rollUpLeft(Math.floor(Math.random() * 4) + 1, true);
      break;
     case 2:
      this.rollUpLeft(Math.floor(Math.random() * 4) + 1, false);
      break;
     case 3:
      this.rollDownRight(Math.floor(Math.random() * 4) + 1, true);
      break;
    }
   }
   this.moves = 0;
   document.getElementById("moves").innerText = this.moves;
   document.getElementById("shuffle").innerText = "RESTART";
  }
 }

 getElements(l, col) {
  const z = Array.from(document.querySelectorAll(".numbers"));
  for (let e = 15; e > -1; e--) {
   if (z[e].id[(col ? 0 : 2)] != l) {
    z.splice(e, 1)
   }
  }
  return z;
 }

 rollDownRight(x, col) {
  if (!this.started) return;
  const z = this.getElements(x, col),
   a = z[3].innerText;
  for (let r = 3; r > 0; r--) {
   z[r].innerText = z[r - 1].innerText;
  }
  z[0].innerText = a;
  this.updateMoves();
  this.checkSolved();
 }

 rollUpLeft(x, col) {
  if (!this.started) return;
  const z = this.getElements(x, col),
   a = z[0].innerText;
  for (let r = 0; r < 3; r++) {
   z[r].innerText = z[r + 1].innerText;
  }
  z[3].innerText = a;
  this.updateMoves();
  this.checkSolved();
 }

 updateMoves() {
  this.moves++;
  document.getElementById("moves").innerText = this.moves;
 }

 checkSolved() {
  function check() {
   const z = document.querySelectorAll(".numbers");
   let u = 1;
   for (let r of z) {
    if (r.innerText != u) return false;
    u++;
   }
   return true;
  }
  if (this.started && check()) {
   document.getElementById("done").style.visibility = "visible";
  }
 }
}

new Puzzle();

Julia

using Gtk, Random

function puzzle16app(bsize)
    aclock, clock = "\u27f2", "\u27f3"
    win = GtkWindow("16 Game", 300, 300) |> (GtkFrame() |> (box = GtkBox(:v)))
    toolbar = GtkToolbar()
    newgame = GtkToolButton("New Game")
    set_gtk_property!(newgame, :label, "New Game")
    set_gtk_property!(newgame, :is_important, true)
    push!(toolbar, newgame)
    grid = GtkGrid()
    map(w -> push!(box, w),[toolbar, grid])
    buttons = Array{Gtk.GtkButtonLeaf,2}(undef, bsize + 2, bsize + 2)
    for i in 1:bsize+2, j in 1:bsize+2
        grid[i,j] = buttons[i,j] = GtkButton()
        set_gtk_property!(buttons[i,j], :expand, true)
    end

    inorder = string.(reshape(1:bsize*bsize, bsize, bsize))
    puzzle = shuffle(inorder)
    rotatecol(puzzle, col, n) = puzzle[:, col] .= circshift(puzzle[:, col], n)
    rotaterow(puzzle, row, n) = puzzle[row, :] .= circshift(puzzle[row, :], n)
    iswon() = puzzle == inorder
    won = false

    function findrowcol(button)
        for i in 1:bsize+2, j in 1:bsize+2
            if buttons[i, j] == button
                return i, j
            end
        end
        return 0, 0
    end

    function playerclicked(button)
        if !won
        i, j = findrowcol(button)
            if i == 1
                rotatecol(puzzle, j - 1, 1)
            elseif i == bsize + 2
                rotatecol(puzzle, j - 1, -1)
            elseif j == 1
                rotaterow(puzzle, i - 1, 1)
            elseif j == bsize + 2
                rotaterow(puzzle, i - 1, -1)
            end
        end
        update!()
    end

    function setup!()
        for i in 1:bsize+2, j in 1:bsize+2
            if 1 < j < bsize + 2
                if i == 1
                    signal_connect(playerclicked, buttons[i, j], "clicked")
                elseif i == bsize + 2
                    signal_connect(playerclicked, buttons[i, j], "clicked")
                end
            elseif 1 < i < bsize + 2
                if j == 1
                    signal_connect(playerclicked, buttons[i, j], "clicked")
                elseif j == bsize + 2
                    signal_connect(playerclicked, buttons[i, j], "clicked")
                end
            end
        end
    end

    function update!()
        for i in 1:bsize+2, j in 1:bsize+2
            if 1 < j < bsize + 2
                if i == 1
                    set_gtk_property!(buttons[i, j], :label, clock)
                elseif i == bsize + 2
                    set_gtk_property!(buttons[i, j], :label, aclock)
                else
                    set_gtk_property!(buttons[i, j], :label, puzzle[i-1, j-1])
                end
            elseif 1 < i < bsize + 2
                if j == 1
                    set_gtk_property!(buttons[i, j], :label, clock)
                elseif j == bsize + 2
                    set_gtk_property!(buttons[i, j], :label, aclock)
                end
            end
        end
        if iswon()
            won = true
            info_dialog("Game over.\nScore: $score", win)
        end
        showall(win)
    end

    function initialize!(w)
        puzzle = shuffle(inorder)
        won = false
        update!()
    end
    
    setup!()
    condition = Condition()
    endit(w) = notify(condition)
    signal_connect(initialize!, newgame, :clicked)
    signal_connect(endit, win, :destroy)
    initialize!(win)
    showall(win)
    wait(condition)
end

puzzle16app(4)

Nim

Translation of: Julia
Library: gintro

Not a direct translation as there are a lot of differences, but, at least, the graphical interface is similar and the logic is the same.

import random, sequtils, strutils
import gintro/[glib, gobject, gtk, gio]

const

  BoardSize = 4
  GridSize = BoardSize + 2
  Clock = "\u27f2"
  AClock = "\u27f3"

type

  Value = 1..16
  Puzzle = array[BoardSize, array[BoardSize, Value]]

  PuzzleApp = ref object of Application
    inOrder: Puzzle                                     # Target grid.
    puzzle: Puzzle                                      # Current grid.
    buttons: array[GridSize, array[GridSize, Button]]   # Button grid.
    won: bool                                           # True if game won.
    moves: Natural                                      # Count of moves.

#---------------------------------------------------------------------------------------------------

proc initPuzzle(puzzle: var Puzzle; data: openArray[Value]) =
  ## Initialize the puzzle with a list of values.
  var i = 0
  for row in puzzle.mitems:
    for cell in row.mitems:
      cell = data[i]
      inc i

#---------------------------------------------------------------------------------------------------

proc showMessage(app: PuzzleApp) =
  ## As "gintro" doesn't provide "MessageDialog" yet, we will use a simple dialog.
  let dialog = newDialog()
  dialog.setModal(true)
  let label = newLabel("You won in $# move(s)".format(app.moves))
  dialog.contentArea.add(label)
  discard dialog.addButton("Ok", ord(ResponseType.ok))
  dialog.showAll()
  discard dialog.run()
  dialog.destroy()

#---------------------------------------------------------------------------------------------------

proc onQuit(button: ToolButton; window: ApplicationWindow) =
  ## Procedure called when clicking quit button.
  window.destroy()

#---------------------------------------------------------------------------------------------------

proc rotateRow(puzzle: var Puzzle; row: Natural; left: bool) =
  ## Rotate a row left or right.
  if left:
    let first = puzzle[row][0]
    for i in 1..puzzle.high: puzzle[row][i-1] = puzzle[row][i]
    puzzle[row][^1] = first
  else:
    let last = puzzle[row][^1]
    for i in countdown(puzzle.high, 1): puzzle[row][i] = puzzle[row][i-1]
    puzzle[row][0] = last

#---------------------------------------------------------------------------------------------------

proc rotateCol(puzzle: var Puzzle; col: Natural; up: bool) =
  ## Rotate a column up or down.
  if up:
    let first = puzzle[0][col]
    for i in 1..puzzle.high: puzzle[i-1][col] = puzzle[i][col]
    puzzle[^1][col] = first
  else:
    let last = puzzle[^1][col]
    for i in countdown(puzzle[0].high, 1): puzzle[i][col] =puzzle[i-1][col]
    puzzle[0][col] = last

#---------------------------------------------------------------------------------------------------

proc findRowCol(app: PuzzleApp; button: Button): (int, int) =
  ## Find the row and column of a button.
  for i in [0, BoardSize+1]:
    for j in 1..Boardsize:
      if app.buttons[i][j] == button: return (i, j)
  for j in [0, BoardSize+1]:
    for i in 1..Boardsize:
      if app.buttons[i][j] == button: return (i, j)

#---------------------------------------------------------------------------------------------------

proc update(app: PuzzleApp) =
  ## Update the grid.
  for i in 0..BoardSize+1:
    for j in 0..BoardSize+1:
      if j in 1..BoardSize:
        if i == 0:
          app.buttons[i][j].setLabel(Clock)
        elif i == BoardSize + 1:
          app.buttons[i][j].setLabel(AClock)
        else:
          app.buttons[i][j].setLabel($app.puzzle[i-1][j-1])
      elif i in 1..BoardSize:
        if j == 0:
          app.buttons[i][j].setLabel(Clock)
        elif j == BoardSize + 1:
          app.buttons[i][j].setLabel(AClock)

  if app.puzzle == app.inOrder:
    app.won = true
    app.showMessage()

#---------------------------------------------------------------------------------------------------

proc onClick(button: Button; app: PuzzleApp) =
  ## Procedure called when the user cliked a grid button.
  if not app.won:
    inc app.moves
    let (i, j) = app.findRowCol(button)
    if i == 0:
      app.puzzle.rotateCol(j - 1, true)
    elif i == BoardSize + 1:
      app.puzzle.rotateCol(j - 1, false)
    elif j == 0:
      app.puzzle.rotateRow(i - 1, true)
    elif j == BoardSize + 1:
      app.puzzle.rotateRow(i - 1, false)
    app.update()

#---------------------------------------------------------------------------------------------------

proc newGame(button: ToolButton; app: PuzzleApp) =
  ## Prepare a new game.
  var values = toSeq(Value.low..Value.high)
  values.shuffle()
  app.puzzle.initPuzzle(values)
  app.won = false
  app.update()

#---------------------------------------------------------------------------------------------------

proc activate(app: PuzzleApp) =
  ## Activate the application.

  let window = app.newApplicationWindow()
  window.setTitle("16 puzzle game")
  window.setSizeRequest(300, 340)

  let box = newBox(Orientation.vertical, 0)
  window.add box

  let toolbar = newToolbar()
  let newGameButton = newToolButton(label = "New game")
  toolbar.insert(newGameButton, 0)
  let quitButton = newToolButton(label = "Quit")
  toolbar.insert(quitButton, 1)
  box.add toolbar

  let grid = newGrid()
  box.add grid

  for i in 0..BoardSize+1:
    for j in 0..BoardSize+1:
      let button = newButton()
      button.setHexpand(true)
      button.setVexpand(true)
      app.buttons[i][j] = button
      grid.attach(button, j, i, 1, 1)

  var values = toSeq(Value.low..Value.high)
  app.inOrder.initPuzzle(values)
  values.shuffle()
  app.puzzle.initPuzzle(values)

  for i in [0, BoardSize + 1]:
    for j in 1..BoardSize:
      app.buttons[i][j].connect("clicked", onClick, app)
  for j in [0, BoardSize + 1]:
    for i in 1..BoardSize:
      app.buttons[i][j].connect("clicked", onClick, app)

  newGameButton.connect("clicked", newGame, app)
  quitButton.connect("clicked", onQuit, window)

  app.won = false
  app.update()

  window.showAll()

#———————————————————————————————————————————————————————————————————————————————————————————————————

randomize()
let app = newApplication(PuzzleApp, "Rosetta.Puzzle16Game")
discard app.connect("activate", activate)
discard app.run()

Perl

#!/usr/bin/perl

use strict; # http://www.rosettacode.org/wiki/16_Puzzle_Game
use warnings;
use List::Util qw( any );
use Tk;

my $size = $ARGV[0] // 4;
my $width = length $size ** 2;
my $message = '';
my $steps;
my @board;
my @again;
my $easy = 3;

my $mw = MainWindow->new( -title => '16 Puzzle in Tk' );
$mw->geometry( '+470+300' );
$mw->optionAdd('*font' => 'sans 14');
my $frame = $mw->Frame(-bg => 'gray', -relief => 'ridge',
  -borderwidth => 5)->pack;
my $label = $mw->Label( -textvariable => \$message, -font => 'times-bold 30',
  )->pack;
$mw->Button( -text => "Exit", -command => sub {$mw->destroy},
  )->pack(-side => 'right');
$mw->Button( -text => "Reset", -command => sub {
  @board = @again;
  show();
  $message = $steps = 0;
  $label->configure(-fg => 'black');
  },)->pack(-side => 'right');
$mw->Button( -text => "Easy", -command => sub {$easy = 3; generate()},
  )->pack(-side => 'left');
$mw->Button( -text => "Hard", -command => sub {$easy = 12; generate()},
  )->pack(-side => 'left');

my @cells = map {
  $frame->Label(-text => $_, -relief => 'sunken', -borderwidth => 2,
    -fg => 'white', -bg => 'blue', -font => 'sans 24',
    -padx => 7, -pady => 7, -width => $width,
    )->grid(-row => int( $_ / $size + 2 ), -column => $_ % $size + 2,
    -sticky => "nsew",
  ) } 0 .. $size ** 2 - 1;

for my $i (1 .. $size)
  {
  $frame->Button(-text => ">", -command => sub {move("R$i") },
    )->grid(-row => $i + 1, -column => 1, -sticky => "nsew");
  $frame->Button(-text => "<", -command => sub {move("L$i") },
    )->grid(-row => $i + 1, -column => $size + 2, -sticky => "nsew");
  $frame->Button(-text => "v", -command => sub {move("D$i") },
    )->grid(-row => 1, -column => $i + 1, -sticky => "nsew");
  $frame->Button(-text => "^", -command => sub {move("U$i") },
    )->grid(-row => $size + 2, -column => $i + 1, -sticky => "nsew");
  }

generate();

MainLoop;
-M $0 < 0 and exec $0, @ARGV; # restart if source file modified since start

sub randommove { move( qw(U D L R)[rand 4] . int 1 + rand $size ) }

sub show { $cells[$_]->configure(-text => $board[$_]) for 0 .. $size ** 2 - 1 }

sub success { not any { $_ + 1 != $board[$_] } 0 .. $size ** 2 - 1 }

sub move
  {
  my ($dir, $index) = split //, shift;
  $index--;
  my @from = map {
    $dir =~ /L|R/i ? $_ + $size * $index : $_ * $size + $index
    } 0 .. $size - 1;
  @board[@from] = (@board[@from])[ ($dir =~ /L|U/i || -1) .. $size - 1, 0 ];
  show();
  $message = ++$steps;
  $label->configure(-fg => success() ? 'red' : 'black');
  success() and $message = "Solved in $steps";
  }

sub generate
  {
  @board = 1 .. $size ** 2;
  randommove() for 1 .. 1 + int rand $easy;
  success() and randommove();
  @again = @board;
  $message = $steps = 0;
  }

Phix

NB arrow keys not tested on linux, but "UDLR" should work...

constant level = 5,
         ESC=27, UP=328, DOWN=336, LEFT=331, RIGHT=333
 
sequence board = tagset(16), solve = board
 
procedure print_board()
    printf(1,"    1  2  3  4\n")
    for r=1 to 4 do
        printf(1,"%d: %2d %2d %2d %2d\n",r&board[r*4-3..r*4])
    end for
    puts(1,"\n")
end procedure
 
procedure move(integer d,rc)
    -- d is 1..4 for up/down/left/right
    -- rc is 1..4 for row(d>=3)/column(d<=2)
    sequence idx = repeat(0,4),
             tiles = repeat(0,4)
    for i=1 to 4 do
        idx[i] = iff(d<=2?rc+(i-1)*4:(rc-1)*4+i)
        tiles[i] = board[idx[i]]
    end for
--  ?{d,rc,idx}
    idx = iff(mod(d,2)?idx[4]&idx[1..3]:idx[2..4]&idx[1])
    for i=1 to 4 do
        board[idx[i]] = tiles[i]
    end for
end procedure
 
for i=1 to level do move(rand(4),rand(4)) end for
while 1 do
    print_board()
    if board=solve then
        puts(1,"Solved!\n")
        exit
    end if
    puts(1,"Your move (escape|Up/Down|Left/Right & 1/2/3/4):")
    integer d, rc
    while true do
        while true do
            d = find(upper(wait_key()),{ESC,UP,DOWN,LEFT,RIGHT}&"UDLR")-1
            if d!=-1 then exit end if
        end while
        if d=0 then
            puts(1,"\n\nYou gave up!\n")
            exit
        end if
        if d>4 then d-=4 end if
        puts(1,"UDLR"[d])
        while true do
            rc = find(upper(wait_key()),ESC&"1234UDLR"&{UP,DOWN,LEFT,RIGHT})-1
            if rc>4 then
                if rc>