16 puzzle game

Revision as of 06:46, 23 March 2021 by Petelomax (talk | contribs) (→‎{{header|Phix}}: added syntax colouring the hard way)

Create 16 Puzzle Game.

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.


Task


See details: 16 Puzzle Game


Video: 16 Puzzle Game


Related task



Go

<lang 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.")
           }
       }
   }

}</lang>

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!!

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;
}

<lang javascript> 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(); </lang>

Julia

<lang 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) </lang>

Perl

<lang 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;
 }</lang>

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>