15 puzzle game in 3D

From Rosetta Code
(Redirected from 15 Puzzle Game in 3D)
15 puzzle game in 3D 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

Create either a 15 puzzle game or an 8 puzzle game in your language using cubes rather than squares to represent the tiles (including the blank tile). The 'cuboid look' may be simulated rather than using actual 3D graphics and the cubes may either rotate or remain static.

This should be a playable game as opposed to a program which automatically attempts to find a solution without user intervention.

References

Go

Library: raylib-go


Despite the name of the task, it appears that the author's Ring entry is actually producing an '8 puzzle' game and that the 3D aspect is handled by replacing the usual squares with colored, numbered cubes which rotate in space.

The following produces instead a '15 puzzle' game and the 3D aspect is again handled by using colored, numbered cubes. However, the cubes do not rotate (I found this very distracting when playing the game) and the 3D look is simulated by drawing regular hexagons with inner lines drawn between the appropriate vertices to make them look like cubes. A white numberless cube represents the blank square in a normal game.

The game is controlled by the arrow keys (mouse movement is not supported) which move the white cube left, right, up or down exchanging positions with the colored cube already occupying that position. The number of moves is continually updated and, if the player is eventually successful in assembling the cubes in order, an appropriate message is displayed, the white cube changes to dark green and displays the number 16.

package main

import (
    "fmt"
    "github.com/gen2brain/raylib-go/raylib"
    "math"
    "math/rand"
    "strconv"
    "time"
)

var palette = []rl.Color{
    rl.Blue,
    rl.Green,
    rl.Red,
    rl.SkyBlue,
    rl.Magenta,
    rl.Gray,
    rl.Lime,
    rl.Purple,
    rl.Violet,
    rl.Pink,
    rl.Gold,
    rl.Orange,
    rl.Maroon,
    rl.Beige,
    rl.Brown,
    rl.RayWhite,
}

var (
    screenWidth  = int32(960)
    screenHeight = int32(840)
    radius       = screenHeight / 14
    fontSize     = 2 * radius / 5
    angle        = math.Pi / 6
    incr         = 2 * angle
    blank        = 15
    moves        = 0
    gameOver     = false
)

var (
    centers [16]rl.Vector2
    cubes   [16]int
)

func init() {
    for i := 0; i < 16; i++ {
        cubes[i] = i
    }
}

func drawCube(n, pos int) {
    r := float32(radius)
    rl.DrawPoly(centers[pos], 6, r, 0, palette[n])
    cx, cy := centers[pos].X, centers[pos].Y
    for i := 1; i <= 5; i += 2 {
        fi := float64(i)
        vx := int32(r*float32(math.Cos(angle+fi*incr)) + cx)
        vy := int32(r*float32(math.Sin(angle+fi*incr)) + cy)
        rl.DrawLine(int32(cx), int32(cy), vx, vy, rl.Black)
    }
    ns := ""
    if n < 15 || gameOver {
        ns = strconv.Itoa(n + 1)
    }
    hr, er, tqr := r/2, r/8, 0.75*r
    rl.DrawText(ns, int32(cx+hr-er), int32(cy), fontSize, rl.RayWhite)
    rl.DrawText(ns, int32(cx-hr-er), int32(cy), fontSize, rl.RayWhite)
    rl.DrawText(ns, int32(cx-er), int32(cy-tqr), fontSize, rl.RayWhite)
}

func updateGame() {
    if gameOver {
        return
    } else if rl.IsKeyPressed(rl.KeyLeft) {
        if blank%4 != 0 {
            cubes[blank], cubes[blank-1] = cubes[blank-1], cubes[blank]
            blank--
            moves++
        }
    } else if rl.IsKeyPressed(rl.KeyRight) {
        if (blank+1)%4 != 0 {
            cubes[blank], cubes[blank+1] = cubes[blank+1], cubes[blank]
            blank++
            moves++
        }
    } else if rl.IsKeyPressed(rl.KeyUp) {
        if blank > 3 {
            cubes[blank], cubes[blank-4] = cubes[blank-4], cubes[blank]
            blank -= 4
            moves++
        }
    } else if rl.IsKeyPressed(rl.KeyDown) {
        if blank < 12 {
            cubes[blank], cubes[blank+4] = cubes[blank+4], cubes[blank]
            blank += 4
            moves++
        }
    }
}

func completed() bool {
    for i := 0; i < 16; i++ {
        if cubes[i] != i {
            return false
        }
    }
    palette[15] = rl.DarkGreen
    gameOver = true
    return true
}

func main() {
    rand.Seed(time.Now().UnixNano())
    rand.Shuffle(15, func(i, j int) {
        cubes[i], cubes[j] = cubes[j], cubes[i]
    })
    rl.InitWindow(screenWidth, screenHeight, "15-puzzle game using 3D cubes")
    rl.SetTargetFPS(60)
    var x, y = float32(screenWidth) / 10, float32(radius)
    for i := 0; i < 4; i++ {
        cx := 2 * x * float32(i+1)
        for j := 0; j < 4; j++ {
            cy := (x + y) * float32(j+1)
            centers[j*4+i] = rl.NewVector2(cx, cy)
        }
    }

    for !rl.WindowShouldClose() {
        rl.BeginDrawing()
        rl.ClearBackground(rl.Black)
        for i := 0; i < 16; i++ {
            drawCube(cubes[i], i)
        }

        if !completed() {
            m := fmt.Sprintf("Moves = %d", moves)
            rl.DrawText(m, 4*int32(x), 13*int32(y), fontSize, rl.RayWhite)
        } else {
            m := fmt.Sprintf("You've completed the puzzle in %d moves!", moves)
            rl.DrawText(m, 3*int32(x), 13*int32(y), fontSize, rl.RayWhite)
        }
        rl.EndDrawing()
        updateGame()
    }

    rl.CloseWindow()
}

Phix

Library: Phix/online

You can run this online here.

--
-- demo\rosetta\15_puzzle_game_in_3D.exw
-- =====================================
--
--  Contains big chunks of demo\rosetta\DrawRotatingCube.exw 
--                     and demo\pGUI\r3d.exw
--                     and demo\rosetta\Solve15puzzle_simple.exw
--                     (but modified to solve an 8 or 15-puzzle,
--                      and recursive ==> re-entrant iterative.)
--
--  Use the up/down/left/right keys,
--      or 's' to single-step-solve, or when solved re-scramble.
--      or '8' to toggle between the 8 and 15 games. NB: it can
--       spend a very long time just thinking about a 15-puzzle,
--       but should still be perfectly playable, and keying 's' 
--       when thinking/before it has managed to find a solution
--       should re-scramble to something hopefully a bit easier.
--
with javascript_semantics
without debug
include pGUI.e
include builtins\VM\pcmdlnN.e
include builtins\pcurrdir.e
include builtins\pgetpath.e
include builtins\VM\pcfunc.e
include builtins\pfile.e
include builtins\VM\pprntfN.e
include builtins\get_routine_info.e
include builtins\VM\pTime.e
include builtins\scanf.e
include builtins\pdir.e
include builtins\penv.e
with debug

constant title = "8 puzzle game in 3D",
         iterations_per_timeslice = 100_000 -- too low ==> too slow, but (obviously)
                                            -- too high ==> responsiveness:=sluggish
                                            -- 100K gives me around 20% CPU usage,
                                            -- that is, when in "thinking" mode.
Ihandle dlg, canvas
cdCanvas cd_canvas

-- <copied from demo\rosetta\DrawRotatingCube.exw>:
--
-- First, define 8 corners equidistant from {0,0,0}:
--
--          6-----2
--      5-----1   3
--      8-----4  
--
-- ie the right face is 1-2-3-4 clockwise, and the left face
--  is 5-6-7-8 counter-clockwise (unless using x-ray vision).
--
enum X, Y, Z
constant l = 100
constant corners = {{+l,+l,+l},     -- 1 (front top right)
                    {+l,+l,-l},     -- 2 (back top "right")
                    {+l,-l,-l},     -- 3 (back btm "right")
                    {+l,-l,+l},     -- 4 (front btm right)
                    {-l,+l,+l},     -- 5 (front top left)
                    {-l,+l,-l},     -- 6 (back top "left")
                    {-l,-l,-l},     -- 7 (back btm "left")
                    {-l,-l,+l}}     -- 8 (front btm left)
-- I put left/right in quotes for the back face as a reminder
-- those match the above diagram, but of course they would be
-- swapped were you looking "at" the face/rotated it by 180.

constant faces = {{CD_RED,          1,2,3,4},   -- right
                  {CD_YELLOW,       1,5,6,2},   -- top
                  {CD_DARK_GREEN,   1,4,8,5},   -- front
                  {CD_BLUE,         2,3,7,6},   -- back
                  {CD_WHITE,        3,4,8,7},   -- bottom
                  {CD_ORANGE,       8,7,6,5}}   -- left

-- <this bit not from DrawRotatingCube>
constant colours = {CD_DARK_CYAN,
                    CD_DARK_YELLOW,
                    CD_LIGHT_BLUE,
                    CD_INDIGO,
                    CD_ORANGE,
                    CD_PURPLE,
                    CD_MAGENTA,
                    CD_OLIVE,
                    CD_CYAN,
                    CD_YELLOW,
                    CD_BLUE,
                    CD_VIOLET,
                    CD_LIGHT_GREEN,
                    CD_DARK_BLUE,
                    CD_DARK_RED}

constant m = l/2, n = l/4, k = l/8
-- (ps: the 6 has a curved top, whereas the 9 has a flat bottom.)
constant digits = {{{n,n,l},{k,m,l},{-k,m,l},{-n,n,l},{-n,-n,l},{-k,-m,l},{k,-m,l},{n,-n,l},{n,n,l}}, --0
                   {{k,n,l},{0,m,l},{0,-m,l},{k,-m,l},{-k,-m,l}},   -- 1
                   {{n,n,l},{k,m,l},{-k,m,l},{-n,n,l},{n,-m,l},{-n,-m,l}}, -- 2
                   {{n,n,l},{k,m,l},{-k,m,l},{-n,n,l},{-k,0,l},{-n,-n,l},{-k,-m,l},{k,-m,l},{n,-n,l}}, --3
                   {{-n,0,l},{n,0,l},{0,m,l},{0,-m,l}}, -- 4
                   {{-k,m,l},{n,m,l},{n,0,l},{-k,0,l},{-n,-n,l},{-k,-m,l},{k,-m,l},{n,-n,l}}, -- 5
                   {{-n,n,l},{-k,m,l},{k,m,l},{n,n,l},{n,-n,l},{k,-m,l},{-k,-m,l},{-n,-n,l},{-k,0,l},{k,0,l},{n,-n,l}}, -- 6
                   {{n,m,l},{-n,m,l},{0,-m,l}}, -- 7
                   {{k,0,l},{n,n,l},{k,m,l},{-k,m,l},{-n,n,l},{-k,0,l},{-n,-n,l},{-k,-m,l},{k,-m,l},{n,-n,l},{k,0,l},{-k,0,l}}, --8
                   {{-n,n,l},{-k,0,l},{k,0,l},{n,n,l},{k,m,l},{-k,m,l},{-n,n,l},{-n,-n,l},{-k,-m,l},{k,-m,l}}} --9

-- initial rotation onto the required face
constant irot = {{  0,-90,0},   -- right
                 {-90,  0,0},   -- top
                 {  0,  0,0},   -- front
                 {180,  0,0},   -- back
                 { 90,  0,0},   -- bottom
                 {  0, 90,0}}   -- left

--</not from DrawRotatingCube, but the following lot is>

-- rotation angles, 0..359, on a timer
atom rx = 45,   -- initially makes cube like a H
     ry = 35,   --     "	   "    "	italic H
     rz = 0

constant naxes = {{Y,Z},    -- (rotate about the X-axis)
                  {X,Z},    -- (rotate about the Y-axis)
                  {X,Y}}    -- (rotate about the Z-axis)

function rotate(sequence points, atom angle, integer axis)
--
-- rotate points by the specified angle about the given axis
--
    atom radians = angle*CD_DEG2RAD,
         sin_t = sin(radians),
         cos_t = cos(radians)
    integer {nx,ny} = naxes[axis]
    for i=1 to length(points) do
        atom x = points[i][nx],
             y = points[i][ny]
        points[i][nx] = x*cos_t - y*sin_t
        points[i][ny] = y*cos_t + x*sin_t
    end for
    return points
end function

function projection(sequence points, atom d)
--
-- project points from {0,0,d} onto the perpendicular plane through {0,0,0}
--
    for i=1 to length(points) do
        atom {x,y,z} = points[i],
             denom = (1-z/d)
        points[i][X] = x/denom
        points[i][Y] = y/denom
    end for
    return points
end function

function nearest(sequence points)
--
-- return the index of the nearest point (highest z value)
--
    return largest(vslice(points,Z),true)
end function

-- (this has has a scale added, which DrawRotatingCube.exw does not,
--  plus the drawing of the digits on the faces is also new here.)

procedure draw_cube(atom cx, cy, scale, integer digit)
    -- {cx,cy} is the centre point of the canvas
    -- A scale of 100 should draw ~2.5"/6cm cube on a 480x480 canvas
    -- digit can (now in fact) be 0..15, 0 draws a 1..6 standard dice
    sequence points = sq_mul(corners,scale/100)
    points = rotate(points,rx,X)
    points = rotate(points,ry,Y)
    points = rotate(points,rz,Z)
    points = projection(points,1000)
    integer np = nearest(points)
    --
    -- find the three faces that contain the nearest point,
    -- then for each of those faces let diag be the point
    -- that is diagonally opposite said nearest point, and
    -- order by/draw those faces furthest diag away first.
    --
    sequence faceset = {}
    for i=1 to length(faces) do
        sequence fi = faces[i]
        integer k = find(np,fi)       -- k:=2..5, or 0
        if k then
            integer diag = mod(k,4)+2 -- {2,3,4,5} --> {4,5,2,3}
                                      -- aka swap 2<=>4 & 3<=>5
            diag = fi[diag] -- 1..8, diagonally opp. np
            faceset = append(faceset,{points[diag][Z],i})
        end if
    end for
    faceset = sort(faceset)
    for i=1 to length(faceset) do
        integer fn = faceset[i][2]
        sequence face = faces[fn]
        integer clr = iff(digit?colours[digit]:face[1])
        cdCanvasSetForeground(cd_canvas,clr)
        -- first fill sides (with bresenham edges), then
        -- redraw edges, but anti-aliased aka smoother
        sequence modes = {CD_FILL,CD_CLOSED_LINES}
        for m=1 to length(modes) do
            cdCanvasBegin(cd_canvas,modes[m])
            for fdx=2 to 5 do
                sequence pt = points[face[fdx]]
                cdCanvasVertex(cd_canvas,cx+pt[X],cy-pt[Y])
            end for
            cdCanvasEnd(cd_canvas)
            cdCanvasSetForeground(cd_canvas,CD_BLACK)
        end for
-- </DrawRotatingCube.exw>
        -- now draw the number(s) on the face
        integer d = iff(digit?digit:fn), skip_point = 0
        sequence dp, d2
        if d<=9 then
            dp = digits[d+1]
        else
            dp = deep_copy(digits[2]) -- (ie a '1')
            d2 = deep_copy(digits[remainder(d,10)+1])
            skip_point = length(dp)+1
            atom dx = iff(d=11?1.5:2.5)*k,
                 x2 = 1.5*k
            for l=1 to length(dp) do dp[l][X] += dx end for
            for l=1 to length(d2) do d2[l][X] -= x2 end for
            dp &= d2
        end if
        dp = sq_mul(dp,scale/100)
        -- rotate the digit(s) onto the required face
        dp = rotate(dp,irot[fn][X],X)
        dp = rotate(dp,irot[fn][Y],Y)
        -- then rotate to match the cube
        dp = rotate(dp,rx,X)
        dp = rotate(dp,ry,Y)
        dp = rotate(dp,rz,Z)
        dp = projection(dp,1000)
        atom {x1,y1} = dp[1]
        for l=2 to length(dp) do
            atom {x2,y2} = dp[l]
            if l!=skip_point then
                cdCanvasLine(cd_canvas,cx+x1,cy-y1,cx+x2,cy-y2)
            end if
            {x1,y1} = {x2,y2}
        end for
    end for
end procedure

-- <copied from demo\pGUI\r3d.exw>:
constant num = 90,  -- number of lines
         dist = 11000,
         eye = 1450,
         dz = 430

sequence stars = repeat({0,0,eye+1},num)

procedure draw_stars(integer w, h)
    atom w2 = w/2, h2 = h/2
    for i=1 to num do
        atom {x,y,z} = stars[i],
             px = (eye/z*x)+w2,
             py = (eye/z*y)+h2,
             px2 = (eye/(z+250)*x)+w2,
             py2 = (eye/(z+250)*y)+h2
        if z<eye or abs(px)>w or abs(py)>h then
            stars[i] = {(rnd()-0.5)*w*2,(rnd()-0.5)*h*2,rand(dist)}
        else
            stars[i][Z] = z-dz
            cdCanvasSetForeground(cd_canvas,iff(odd(i)?CD_WHITE:CD_YELLOW))
            cdCanvasLine(cd_canvas,px,py,px2,py2)
        end if
    end for
end procedure
--</r3d.exw>

--<from demo/rosetta/Solve15puzzle_simple.exw>
enum left, down, up, right  -- (nb 5-move flips it, as in down===5-up, etc)

integer N = 3,  -- use '8' to toggle between 8 and 15 games
        N2,
        space
 
sequence valid_moves,
         zero_cost,
         piecemasks,
         goal,
         board

string moves = ""

-- Based on demo/rosetta/Solve15puzzle_simple.exw, but  
--  made re-entrant iterative rather than recursive, 
--  and able to solve either an 8 or 15 puzzle, and to
--  use an entirely private copy of the board, etc.

enum RESET, THINKING, SOLVED -- (solve_state values)
integer solve_state = RESET,
        iscount = 0
enum MOVE=1, /*SKIP=2, SPACE=3, NFREE=4,*/ UNDO=5 -- (solve_stack entries)
sequence solve_stack, solve_board
string solve_time
atom solve_t0

procedure iterative_solve()
    if solve_state=RESET then
        solve_state = THINKING
        --
        -- The following is equivalent to:
        --  move := left-1 (aka no move)
        --  skip_move := 0 (aka no skip)
        --  idle_space := space
        --  nfree := 0
        --  undo := false
        --
        solve_stack = {{0,0,space,0,false}}
        solve_board = deep_copy(board)
        solve_t0 = time()
        iscount = 0
    end if

    integer nleft = iterations_per_timeslice,
            new_space       -- (scratch)

    while nleft>0 do
        integer {move,skip_move,idle_space,nfree,undo} = solve_stack[$]
        if undo then
            new_space = valid_moves[idle_space][move]
            solve_board[new_space] = solve_board[idle_space]
            solve_board[idle_space] = 0
        else
            assert(solve_board[idle_space]=0)
        end if
        move += 1
        if move>right then
            if length(solve_stack)=1 then
                -- (as per RESET above)
                assert(idle_space=space)
                nfree += 1
                solve_stack = {{0,0,idle_space,nfree,false}}
            else
                solve_stack = solve_stack[1..$-1] -- (pop)
            end if
        else
            solve_stack[$][MOVE] = move
            solve_stack[$][UNDO] = false
            if move!=skip_move then
                new_space = valid_moves[idle_space][move]
                if new_space then
                    integer piece = solve_board[new_space],
                            zcsmv = zero_cost[idle_space][move],
                            pmask = piecemasks[piece],
                            zcost = (and_bits(zcsmv,pmask)=0) -- (0==free, 1==not)
                    nfree -= zcost
                    if nfree>=0 then
                        solve_stack[$][UNDO] = true -- (set the undo flag)
                        solve_board[idle_space] = piece
                        solve_board[new_space] = 0
                        if idle_space=piece and solve_board=goal then
                            moves = repeat(' ',length(solve_stack))
                            for i=1 to length(solve_stack) do
                                move = solve_stack[i][MOVE]
                                moves[i] = "ludr"[move]
                            end for
                            solve_state = SOLVED
                            exit
                        end if
                        iscount += 1
                        solve_stack = append(solve_stack,{0,5-move,new_space,nfree,false})
                    end if
                end if
            end if
        end if
        nleft -= 1 
    end while
    solve_time = sprintf("(%s)",{elapsed(time()-solve_t0)})
end procedure

function set_title()
    if board=goal then
        moves = ""
        return sprintf("%s - solved",{title})
    elsif solve_state<=THINKING then
        return sprintf("%s - thinking (%,d tries)",{title,iscount})
    else
        return sprintf("%s - solvable in %d moves %s",{title,length(moves),solve_time})
    end if
end function

procedure move(integer d)
    if d then
        integer new_space = valid_moves[space][d]
        if new_space then
            if length(moves) and moves[1]="ludr"[d] then
                moves = moves[2..$]
                solve_time = ""
            else
                solve_state = RESET
                moves = "" -- (force re-examination)
            end if
            board[space] = board[new_space]
            board[new_space] = 0
            space = new_space
        end if
    end if
end procedure

procedure scramble()
    for i=1 to 5000 do move(rand(4)) end for
    solve_state = RESET
end procedure

sequence cubies,
         rxyz

procedure reset()
    N2 = N*N
    valid_moves = repeat(repeat(0,4),N2)
    zero_cost = repeat(repeat(0,4),N2)
    piecemasks = sq_power(2,tagset(N2-2,0))
    for square=1 to N2 do
        integer s_row = floor((square+N-1)/N),
                s_col = remainder((square-1),N)+1
        for move=left to right do -- (via up/down)
            if (move=left  and s_col>1)
            or (move=down  and s_row>1)
            or (move=up    and s_row<N)
            or (move=right and s_col<N) then
                integer origin = square+{-1,-N,+N,+1}[move],
                        o_row = floor((origin+N-1)/N),
                        o_col = remainder((origin-1),N)+1
                valid_moves[square][move] = origin
                for piece=1 to N2-1 do -- (aka target)
                    integer t_row = floor((piece+N-1)/N),
                            t_col = remainder((piece-1),N)+1,
                            p_md = abs(t_row-o_row)+abs(t_col-o_col),
                            n_md = abs(t_row-s_row)+abs(t_col-s_col)
                    if n_md<=p_md then
                        zero_cost[square][move] += piecemasks[piece]
                    end if
                end for
            end if
        end for
    end for
    goal = tagset(N2-1)&0 
    board = deep_copy(goal)
    space = find(0,board)
    moves = ""
    -- </Solve15puzzle_simple.exw>
    cubies = iff(N=3?{{-1,-1},{ 0,-1},{+1,-1},
                      {-1, 0},{ 0, 0},{+1, 0},
                      {-1,+1},{ 0,+1},{+1,+1}}
            :iff(N=4?{{-1.5,-1.5},{-0.5,-1.5},{0.5,-1.5},{1.5,-1.5},
                      {-1.5,-0.5},{-0.5,-0.5},{0.5,-0.5},{1.5,-0.5},
                      {-1.5,+0.5},{-0.5,+0.5},{0.5,+0.5},{1.5,+0.5},
                      {-1.5,+1.5},{-0.5,+1.5},{0.5,+1.5},{1.5,+1.5}}
            :9/0))
    scramble()
    rxyz = sq_rand(repeat({45,35,1,3},N2))
end procedure

function canvas_action_cb(Ihandle canvas)
    cdCanvasActivate(cd_canvas)
    cdCanvasClear(cd_canvas)
    integer {w, h} = IupGetIntInt(canvas, "DRAWSIZE")
    atom cx = w/2,
         cy = h/2,
         dxy = min(w,h)/N,
         size = dxy/4.5
    draw_stars(w,h)
    for i=1 to N2 do
        integer bi = board[i]
        if bi then
            atom {dx,dy} = cubies[i], r
            {rx,ry,rz,r} = rxyz[bi]
            rx = mod(rx+0.25*r,360)
            ry = mod(ry+0.50*r,360)
            rz = mod(rz+0.75*r,360)
            rxyz[bi] = {rx,ry,rz,r}
            draw_cube(cx+dx*dxy,cy-dy*dxy,size,bi)
        end if
    end for
    cdCanvasFlush(cd_canvas)
    IupSetStrAttribute(dlg,"TITLE",set_title())
    return IUP_DEFAULT
end function

function canvas_map_cb(Ihandle canvas)
    IupGLMakeCurrent(canvas)
    if platform()=JS then
        cd_canvas = cdCreateCanvas(CD_IUP, canvas)
    else
        atom res = IupGetDouble(NULL, "SCREENDPI")/25.4
        cd_canvas = cdCreateCanvas(CD_GL, "10x10 %g", {res})
    end if
    cdCanvasSetBackground(cd_canvas, CD_BLACK)
    return IUP_DEFAULT
end function

function canvas_resize_cb(Ihandle /*canvas*/)
    integer {canvas_width, canvas_height} = IupGetIntInt(canvas, "DRAWSIZE")
    atom res = IupGetDouble(NULL, "SCREENDPI")/25.4
    cdCanvasSetAttribute(cd_canvas, "SIZE", "%dx%d %g", {canvas_width, canvas_height, res})
    return IUP_DEFAULT
end function

function key_cb(Ihandle ih, atom c)
    if c=K_ESC then return IUP_CLOSE end if
    --
    -- aside: the precise mapping of enums/K_XXX/"ludr" is semi-ad-hoc,
    --        aka I'm not wasting any effort enforcing strict coherence
    --        when a piddling tweak or two (right here) would suffice,
    --        aka what does right (etc) really mean anyway, given that 
    --        for instance piece:up is the same as saying space:down.
    --
    integer k = 0
    if c='8' then
        -- toggle between an 8 and 15 game
        N = 7-N -- (3<==>4)
        reset()
    elsif lower(c)='s' then
        if length(moves) then
            k = find(moves[1],"ludr")
        else
            scramble()
        end if
    else
        k = find(c,{K_RIGHT,K_DOWN,K_UP,K_LEFT})
    end if
    move(k)     -- (move(0) does nowt)
    IupRedraw(ih)
    return IUP_IGNORE
end function

function timer_cb(Ihandln /*ih*/)
    if solve_state!=SOLVED then
        iterative_solve()
    end if
    IupRedraw(canvas)
    return IUP_IGNORE
end function

procedure main()
    IupOpen()
    reset()
    canvas = IupGLCanvas("RASTERSIZE=640x480")
    IupSetCallbacks(canvas, {"ACTION", Icallback("canvas_action_cb"),
                             "MAP_CB", Icallback("canvas_map_cb"),
                             "RESIZE_CB", Icallback("canvas_resize_cb"),
                             "KEY_CB", Icallback("key_cb")})
    dlg = IupDialog(canvas,`TITLE="%s"`,{title})
    IupShow(dlg)
    IupSetAttribute(canvas, "RASTERSIZE", NULL)
    Ihandle hTimer = IupTimer(Icallback("timer_cb"), 30)
    if platform()!=JS then
        IupMainLoop()
        IupClose()
    end if
end procedure

main()

Ring

/*
## Project : 15 Puzzle Game in 3D 
*/

# Load Libraries
load "gamelib.ring"		# RingAllegro Library
load "opengl21lib.ring"       # RingOpenGL  Library

butSize = 3
texture = list(9)
cube = list(9)
rnd = list(9)
rndok = 0

for n=1 to 9
     rnd[n] = 0
next 

for n=1 to 9
     while true
               rndok = 0
               ran = random(8) + 1
               for nr=1 to 9
                     if rnd[nr] = ran
                        rndok = 1
                     ok
               next  
               if rndok = 0
                  rnd[n] = ran
                  exit
               ok 
     end
next 

for n=1 to 9
     if rnd[n] = 9
        empty = n
     ok
next 

#==============================================================
# To Support MacOS X
	al_run_main()	
	func al_game_start 	# Called by al_run_main()
		main()		# Now we call our main function
#==============================================================

func main
	new TicTacToe3D {
		start()
	}

class TicTacToe3D from GameLogic

	FPS = 60
	TITLE = "CalmoSoft Fifteen Puzzle Game 3D"

	oBackground = new GameBackground
	oGameSound = new GameSound
	oGameCube = new GameCube
	oGameInterface = new GameInterface 

	func loadresources
		oGameSound.loadresources()
		oBackGround.loadresources()
		oGameCube.loadresources()

	func drawScene
		oBackground.update()
		oGameInterface.update(self)

	func MouseClickEvent
		oGameInterface.MouseClickEvent(self)

class GameInterface 

	func Update oGame
		prepare()
		cubes(oGame)

	func Prepare 
		w = 1024 h = 768
		ratio =  w / h
		glViewport(0, 0, w, h)
		glMatrixMode(GL_PROJECTION)
		glLoadIdentity()
		gluPerspective(-120,ratio,1,120)
		glMatrixMode(GL_MODELVIEW)
		glLoadIdentity()
		glEnable(GL_TEXTURE_2D)							
		glShadeModel(GL_SMOOTH)		
		glClearColor(0.0, 0.0, 0.0, 0.5)
		glClearDepth(1.0)			
		glEnable(GL_DEPTH_TEST)	
		glEnable(GL_CULL_FACE)
		glDepthFunc(GL_LEQUAL)
		glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)

	func Cubes oGame
		oGame.oGameCube {
		aGameMap = oGame.aGameMap 
                cube[1] = cube( 5  , -3 , -5  , texture[rnd[1]] )
		cube[2] = cube( 0  , -3 , -5  , texture[rnd[2]] )
		cube[3] = cube( -5 , -3 , -5  , texture[rnd[3]] )
		cube[4] = cube( 5  , 1  , -5  , texture[rnd[4]] )
		cube[5] = cube( 0  , 1  , -5  , texture[rnd[5]] )
		cube[6] = cube( -5 , 1  , -5  , texture[rnd[6]] )
		cube[7] = cube( 5  , 5  , -5  , texture[rnd[7]] )
		cube[8] = cube( 0  , 5  , -5  , texture[rnd[8]] )
		cube[9] = cube( -5 , 5  , -5  , texture[rnd[9]] )
 		rotate()
		}

	func MouseClickEvent oGame
		oGame {
			aBtn = Point2Button(Mouse_X,Mouse_Y)
                        move = 0
			nRow = aBtn[1]
			nCol = aBtn[2]
                        tile = (nRow-1)*3 + nCol
                        up = (empty = (tile - butSize))
                        down = (empty = (tile + butSize))
                        left = ((empty = (tile- 1)) and ((tile % butSize) != 1))
                        right = ((empty = (tile + 1)) and ((tile % butSize) != 0))
                        move = up or down or left  or right
                        if move = 1 
                           temp = rnd[empty]
                           rnd[empty] = rnd[tile] 
                           rnd[tile] = temp
                           empty = tile
                           oGame.oGameCube {
		                      aGameMap = oGame.aGameMap 
                                      cube[1] = cube( 5  , -3 , -5  , texture[rnd[1]] )
		                      cube[2] = cube( 0  , -3 , -5  , texture[rnd[2]] )
		                      cube[3] = cube( -5 , -3 , -5  , texture[rnd[3]] )
		                      cube[4] = cube( 5  , 1  , -5  , texture[rnd[4]] )
		                      cube[5] = cube( 0  , 1  , -5  , texture[rnd[5]] )
	                              cube[6] = cube( -5 , 1  , -5  , texture[rnd[6]] )
		                      cube[7] = cube( 5  , 5  , -5  , texture[rnd[7]] )
	                              cube[8] = cube( 0  , 5  , -5  , texture[rnd[8]] )
		                      cube[9] = cube( -5 , 5  , -5  , texture[rnd[9]] )
 		                      rotate()
		           }
                        ok
		}

Class GameLogic from GraphicsAppBase

	aGameMap = [
		[ :n , :n , :n ] ,
		[ :n , :n , :n ] ,
		[ :n , :n , :n ]
	]

	aGameButtons = [			# x1,y1,x2,y2
 		[176,88,375,261],		# [1,1]
		[423,88,591,261],		# [1,2]
		[645,88,876,261],		# [1,3]
 		[176,282,375,428],		# [2,1]
		[423,282,591,428],		# [2,2]
		[645,282,876,428],		# [2,3]
 		[176,454,375,678],		# [3,1]
		[423,454,591,678],		# [3,2]
		[645,454,876,678]		# [3,3]
	]

	cActivePlayer = :x

	func point2button x,y
		nRow = 0
		nCol = 0
		for t = 1 to len(aGameButtons) 
			rect = aGameButtons[t]
			if x >= rect[1] and x <= rect[3] and
			   y >= rect[2] and y <= rect[4] 
					switch t
						on 1  nRow = 1  nCol = 1
						on 2  nRow = 1  nCol = 2
						on 3  nRow = 1 	nCol = 3
						on 4  nRow = 2  nCol = 1
						on 5  nRow = 2  nCol = 2
						on 6  nRow = 2  nCol = 3
						on 7  nRow = 3 	nCol = 1
						on 8  nRow = 3  nCol = 2
						on 9  nRow = 3  nCol = 3
					off
					exit 
			ok
		next 
		return [nRow,nCol]

class GameCube

	bitmap bitmap2 bitmap3 
	textureX textureO textureN

	xrot = 0.0
	yrot = 0.0
	zrot = 0.0

	func loadresources
		bitmp1 = al_load_bitmap("image/n1.jpg")
		texture[1] = al_get_opengl_texture(bitmp1)
		bitmp2 = al_load_bitmap("image/n2.jpg")
		texture[2] = al_get_opengl_texture(bitmp2)
		bitmp3 = al_load_bitmap("image/n3.jpg")
		texture[3] = al_get_opengl_texture(bitmp3)
		bitmp4 = al_load_bitmap("image/n4.jpg")
		texture[4] = al_get_opengl_texture(bitmp4)
		bitmp5 = al_load_bitmap("image/n5.jpg")
		texture[5] = al_get_opengl_texture(bitmp5)
		bitmp6 = al_load_bitmap("image/n6.jpg")
		texture[6] = al_get_opengl_texture(bitmp6)
		bitmp7 = al_load_bitmap("image/n7.jpg")
		texture[7] = al_get_opengl_texture(bitmp7)
		bitmp8 = al_load_bitmap("image/n8.jpg")
		texture[8] = al_get_opengl_texture(bitmp8)
		bitmp9 = al_load_bitmap("image/empty.png")
		texture[9] = al_get_opengl_texture(bitmp9)

	func cube(x,y,z,nTexture)
		glLoadIdentity()									
		glTranslatef(x,y,z)
		glRotatef(xrot,1.0,0.0,0.0)
		glRotatef(yrot,0.0,1.0,0.0)
		glRotatef(zrot,0.0,0.0,1.0)
		setCubeTexture(nTexture)
		drawCube()

	func setCubeTexture cTexture
		glBindTexture(GL_TEXTURE_2D, cTexture)

	func Rotate 
		xrot += 0.3 * 5
		yrot += 0.2 * 5
		zrot += 0.4 * 5

	func drawcube
		glBegin(GL_QUADS)  
			// Front Face
			glTexCoord2f(0.0, 0.0) glVertex3f(-1.0, -1.0,  1.0)
			glTexCoord2f(1.0, 0.0) glVertex3f( 1.0, -1.0,  1.0)
			glTexCoord2f(1.0, 1.0) glVertex3f( 1.0,  1.0,  1.0)
			glTexCoord2f(0.0, 1.0) glVertex3f(-1.0,  1.0,  1.0)
			// Back Face
			glTexCoord2f(1.0, 0.0) glVertex3f(-1.0, -1.0, -1.0)
			glTexCoord2f(1.0, 1.0) glVertex3f(-1.0,  1.0, -1.0)
			glTexCoord2f(0.0, 1.0) glVertex3f( 1.0,  1.0, -1.0)
			glTexCoord2f(0.0, 0.0) glVertex3f( 1.0, -1.0, -1.0)
			// Top Face
			glTexCoord2f(0.0, 1.0) glVertex3f(-1.0,  1.0, -1.0)
			glTexCoord2f(0.0, 0.0) glVertex3f(-1.0,  1.0,  1.0)
			glTexCoord2f(1.0, 0.0) glVertex3f( 1.0,  1.0,  1.0)
			glTexCoord2f(1.0, 1.0) glVertex3f( 1.0,  1.0, -1.0)
			// Bottom Face
			glTexCoord2f(1.0, 1.0) glVertex3f(-1.0, -1.0, -1.0)
			glTexCoord2f(0.0, 1.0) glVertex3f( 1.0, -1.0, -1.0)
			glTexCoord2f(0.0, 0.0) glVertex3f( 1.0, -1.0,  1.0)
			glTexCoord2f(1.0, 0.0) glVertex3f(-1.0, -1.0,  1.0)
		 
			// Right face
			glTexCoord2f(1.0, 0.0) glVertex3f( 1.0, -1.0, -1.0)
			glTexCoord2f(1.0, 1.0) glVertex3f( 1.0,  1.0, -1.0)
			glTexCoord2f(0.0, 1.0) glVertex3f( 1.0,  1.0,  1.0)
			glTexCoord2f(0.0, 0.0) glVertex3f( 1.0, -1.0,  1.0)
	 
			// Left Face
			glTexCoord2f(0.0, 0.0) glVertex3f(-1.0, -1.0, -1.0)
			glTexCoord2f(1.0, 0.0) glVertex3f(-1.0, -1.0,  1.0)
			glTexCoord2f(1.0, 1.0) glVertex3f(-1.0,  1.0,  1.0)
			glTexCoord2f(0.0, 1.0) glVertex3f(-1.0,  1.0, -1.0)
		glEnd()


class GameBackground 

	nBackX = 0
	nBackY = 0
	nBackDiffx = -1
	nBackDiffy = -1
	nBackMotion = 1
	aBackMotionList = [
		[ -1, -1 ] ,		# Down - Right
		[ 0 , 1  ] ,		# Up
		[ -1, -1 ] , 		# Down - Right
		[ 0 , 1  ] ,		# Up
		[ 1 , -1 ] ,		# Down - Left
		[ 0 , 1  ] ,		# Up
		[ 1 , -1 ] , 		# Down - Left
		[ 0 , 1  ]			# Up
	]

	bitmap

	func Update 
		draw()
		motion()

	func draw
		al_draw_bitmap(bitmap,nBackX,nBackY,1)

	func motion
		nBackX += nBackDiffx
		nBackY += nBackDiffy
		if (nBackY = -350) or (nBackY = 0)
			nBackMotion++
			if nBackMotion > len(aBackMotionList)
				nBackMotion = 1
			ok
			nBackDiffx  = aBackMotionList[nBackMotion][1]
			nBackDiffy  = aBackMotionList[nBackMotion][2]
		ok

	func loadResources
		bitmap = al_load_bitmap("image/back.jpg")

class GameSound

	sample sampleid

	func loadresources
		sample = al_load_sample( "sound/music1.wav" )
		sampleid = al_new_allegro_sample_id()
		al_play_sample(sample, 1.0, 0.0,1.0,ALLEGRO_PLAYMODE_LOOP,sampleid)

class GraphicsAppBase

	display event_queue ev timeout 
	timer  
	redraw 			= true
	FPS 			= 60 
	SCREEN_W 		= 1024
	SCREEN_H 		= 700
	KEY_UP			= 1
	KEY_DOWN 		= 2
	KEY_LEFT 		= 3
	KEY_RIGHT 		= 4
	Key 			= [false,false,false,false]
	Mouse_X 		= 0
	Mouse_Y			= 0
	TITLE 			= "Graphics Application"
	PRINT_MOUSE_XY 	= False

	func start
		SetUp()
		loadResources()
		eventsLoop()
		destroy()

	func setup
		al_init()
		al_init_font_addon()
		al_init_ttf_addon()
		al_init_image_addon()
		al_install_audio()
		al_init_acodec_addon()
		al_reserve_samples(1)
		al_set_new_display_flags(ALLEGRO_OPENGL) 
		display = al_create_display(SCREEN_W,SCREEN_H)
		al_set_window_title(display,TITLE)
		al_clear_to_color(al_map_rgb(0,0,0))
		event_queue = al_create_event_queue()
		al_register_event_source(event_queue, 
		al_get_display_event_source(display))
		ev = al_new_allegro_event()
		timeout = al_new_allegro_timeout()
		al_init_timeout(timeout, 0.06)
		timer = al_create_timer(1.0 / FPS)
		al_register_event_source(event_queue, 
		al_get_timer_event_source(timer))
		al_start_timer(timer)
		al_install_mouse()
		al_register_event_source(event_queue, 
		al_get_mouse_event_source())
		al_install_keyboard()
		al_register_event_source(event_queue, 
		al_get_keyboard_event_source())

	func eventsLoop
		while true
			al_wait_for_event_until(event_queue, ev, timeout)
			switch al_get_allegro_event_type(ev)
			on ALLEGRO_EVENT_DISPLAY_CLOSE
				CloseEvent()
			on ALLEGRO_EVENT_TIMER
				redraw = true
			on ALLEGRO_EVENT_MOUSE_AXES
				mouse_x = al_get_allegro_event_mouse_x(ev)
				mouse_y = al_get_allegro_event_mouse_y(ev)
				if PRINT_MOUSE_XY
					see "x = " + mouse_x + nl
					see "y = " + mouse_y + nl
				ok
			on ALLEGRO_EVENT_MOUSE_ENTER_DISPLAY
				mouse_x = al_get_allegro_event_mouse_x(ev)
				mouse_y = al_get_allegro_event_mouse_y(ev)
			on ALLEGRO_EVENT_MOUSE_BUTTON_UP
				MouseClickEvent()
			on ALLEGRO_EVENT_KEY_DOWN
				switch al_get_allegro_event_keyboard_keycode(ev)
					on ALLEGRO_KEY_UP
						key[KEY_UP] = true
					on ALLEGRO_KEY_DOWN
						key[KEY_DOWN] = true
					on ALLEGRO_KEY_LEFT
						key[KEY_LEFT] = true
					on ALLEGRO_KEY_RIGHT
						key[KEY_RIGHT] = true
				off
			on ALLEGRO_EVENT_KEY_UP
				switch al_get_allegro_event_keyboard_keycode(ev)
					on ALLEGRO_KEY_UP
						key[KEY_UP] = false
					on ALLEGRO_KEY_DOWN
						key[KEY_DOWN] = false
					on ALLEGRO_KEY_LEFT
						key[KEY_LEFT] = false
					on ALLEGRO_KEY_RIGHT
						key[KEY_RIGHT] = false
					on ALLEGRO_KEY_ESCAPE
						exit
				off
			off
			if redraw and al_is_event_queue_empty(event_queue)
				redraw = false
				drawScene()
				al_flip_display()
			ok
			callgc()
		end

	func destroy
		al_destroy_timer(timer)
		al_destroy_allegro_event(ev)
		al_destroy_allegro_timeout(timeout)
		al_destroy_event_queue(event_queue)
		al_destroy_display(display)
		al_exit()

	func loadresources

	func drawScene

	func MouseClickEvent
		exit 			# Exit from the Events Loop 

	func CloseEvent
		exit 			# Exit from the Events Loop

Wren

Translation of: Go
Library: DOME
Library: Wren-polygon
Library: Go-fonts

The palette is somewhat different from the Go example and, if the player is eventually successful in assembling the cubes in order, the white cube changes to gold and displays the number 16.

import "dome" for Window
import "graphics" for Canvas, Color, Font
import "input" for Keyboard
import "random" for Random
import "math" for Math
import "./polygon" for Polygon

var Palette = [    
    Color.purple,
    Color.darkblue, 
    Color.darkpurple,
    Color.darkgreen,
    Color.brown,
    Color.darkgray,
    Color.lightgray,
    Color.red,
    Color.orange,
    Color.yellow,
    Color.green,
    Color.blue,
    Color.indigo,
    Color.pink,
    Color.peach,
    Color.white
]

var Rand = Random.new()

class FifteenPuzzle3d {
    construct new() {
        Window.resize(960, 840)
        Canvas.resize(960, 840)
        Window.title = "15-puzzle game using 3D cubes"
        _radius = Canvas.height / 14
        // see Go-fonts page
        Font.load("Go-Regular20", "Go-Regular.ttf", 20)
        Canvas.font = "Go-Regular20"
        _blank = 15
        _moves = 0
        _gameOver = false
    }

    init() {
        _centers = List.filled(16, null)
        _cubes = List.filled(15, 0)
        for (i in 0..14) _cubes[i] = i
        Rand.shuffle(_cubes)
        _cubes.add(15)
        var x = Canvas.width / 10
        var y = _radius
        for (i in 0..3) {
            var cx = 2 * x * (i + 1)
            for (j in 0..3) {
                var cy = (x + y) * (j + 1)
                _centers[j * 4 + i] = [cx, cy]
            }
        }
        for (i in 0..15) drawCube(_cubes[i], i)
    }

    drawCube(n, pos) {
        var cx = _centers[pos][0]
        var cy = _centers[pos][1]
        var angle = Num.pi / 6
        var incr = 2 * angle
        var hexagon = Polygon.regular(6, cx, cy, _radius, 90)
        hexagon.drawfill(Palette[n])
        for (i in [1, 3, 5]) {
            var vx = (_radius * Math.cos(angle + i*incr) + cx).floor
            var vy = (_radius * Math.sin(angle + i*incr) + cy).floor
            Canvas.line(cx, cy, vx, vy, Color.black)
        }
        var ns = (n < 15 || _gameOver) ? (n + 1).toString : ""
        var hr  = _radius * 0.5
        var er  = _radius * 0.125
        var tqr = _radius * 0.75
        Canvas.print(ns, cx + hr - er, cy, Color.white)
        Canvas.print(ns, cx - hr - er, cy, Color.white)
        Canvas.print(ns, cx - er, cy - tqr, Color.white)
    }

    completed {
        for (i in 0..15) {
            if (_cubes[i] != i) return false
        }
        Palette[15] = Color.rgb(255, 215, 0) // gold
        return true
    }

    update() {
        var changed = false
        if (Keyboard["Left"].justPressed) {
            if (_blank%4 != 0) {
                _cubes.swap(_blank, _blank-1)
                _blank = _blank - 1
                _moves =_moves + 1
                changed = true
            }
        } else if (Keyboard["Right"].justPressed) {
            if ((_blank+1)%4 != 0) {
                _cubes.swap(_blank, _blank+1)
                _blank = _blank + 1
                _moves = _moves + 1
                changed = true
            }
        } else if (Keyboard["Up"].justPressed) {
            if (_blank > 3) {
                _cubes.swap(_blank, _blank-4)
                _blank = _blank - 4
                _moves = _moves + 1
                changed = true
            }
        } else if (Keyboard["Down"].justPressed) {
            if (_blank < 12) {
                _cubes.swap(_blank, _blank+4)
                _blank = _blank + 4
                _moves = _moves + 1
                changed = true
            }
        }
        if (changed) {
            Canvas.cls(Color.black)
            _gameOver = completed
            for (i in 0..15) drawCube(_cubes[i], i)
            if (!_gameOver) {
                var m = "Moves = %(_moves)"
                Canvas.print(m, 0.4 * Canvas.width, 13 * _radius, Color.white)
            } else {
                var m = "You've completed the puzzle in %(_moves) moves!"
                Canvas.print(m, 0.3 * Canvas.width, 13 * _radius, Color.white)
            }
        }
    }

    draw(alpha) {}
}

var Game = FifteenPuzzle3d.new()