DrawFiddle▶ RunEdit in Fiddle
// GAME ETUDE
SkiaLabel movesLabel = null;
SkiaControl winLabel = null; //game over dlg

var rnd = new Random();
var board = new int[4, 4];         // value in each cell; 0 = the empty space (no tile there)
var tiles = new SkiaShape[16];     // 15 physical tiles indexed by value 1..15 — there is NO tile 0
int moves = 0;
bool won = false;
const double pitch = 68;           // tile 66 + 2 gap between tiles

(int r, int c) Cell(int v)
{
    for (int r = 0; r < 4; r++)
        for (int c = 0; c < 4; c++)
            if (board[r, c] == v) return (r, c);
    return (3, 3);
}

// Snap every tile to its slot with no animation, and refresh the HUD.
void Snap()
{
    for (int v = 1; v < 16; v++)
    {
        var (r, c) = Cell(v);
        tiles[v].Left = c * pitch;   // Left/Top are the tile's absolute position
        tiles[v].Top = r * pitch;
        tiles[v].TranslationX = 0;   // translation is only a transient slide offset
        tiles[v].TranslationY = 0;
    }
    movesLabel.Text = $"MOVES {moves}";
    winLabel.IsVisible = won;
}

bool Solved()
{
    int n = 1;
    for (int r = 0; r < 4; r++)
        for (int c = 0; c < 4; c++)
        {
            int expect = (r == 3 && c == 3) ? 0 : n++;
            if (board[r, c] != expect) return false;
        }
    return true;
}

// Move the tile in cell (fr,fc) into the empty cell (tr,tc). Its own translation slides.
void MoveTile(int fr, int fc, int tr, int tc, bool animate)
{
    int v = board[fr, fc];
    board[tr, tc] = v;
    board[fr, fc] = 0;
    tiles[v].Left = tc * pitch;    // final position via Left/Top
    tiles[v].Top = tr * pitch;
    if (animate)
    {
        // start visually at the old slot (translation offset) and slide the offset to 0
        tiles[v].AnimateTranslationX((fc - tc) * pitch, 0, 0.11, easing: Easing.Linear);
        tiles[v].AnimateTranslationY((fr - tr) * pitch, 0, 0.11, easing: Easing.Linear);
    }
    else
    {
        tiles[v].TranslationX = 0;
        tiles[v].TranslationY = 0;
    }
}

// Arrow: pull the neighbour on that side into the empty cell. dir 0 left,1 right,2 up,3 down.
bool Slide(int dir, bool animate)
{
    var (gr, gc) = Cell(0);
    int nr = gr, nc = gc;
    if (dir == 0) nc = gc + 1;
    else if (dir == 1) nc = gc - 1;
    else if (dir == 2) nr = gr + 1;
    else nr = gr - 1;
    if (nr < 0 || nr > 3 || nc < 0 || nc > 3) return false;
    MoveTile(nr, nc, gr, gc, animate);
    return true;
}

// Tap a tile by its value: if it sits next to the empty space, slide it in.
void Tap(int v)
{
    if (won) return;
    var (tr, tc) = Cell(v);
    var (gr, gc) = Cell(0);
    bool adjacent = (Math.Abs(gr - tr) == 1 && gc == tc) || (Math.Abs(gc - tc) == 1 && gr == tr);
    if (!adjacent) return;
    MoveTile(tr, tc, gr, gc, true);
    moves++;
    if (Solved()) won = true;
    movesLabel.Text = $"MOVES {moves}";
    winLabel.IsVisible = won;
}

void Reset()
{
    // 15 evenly-spaced hues (every tile a distinct hue), one sane lightness, shuffled onto values.
    var hue = new int[15];
    for (int i = 0; i < 15; i++) hue[i] = i;
    for (int i = 14; i > 0; i--) { int j = rnd.Next(i + 1); (hue[i], hue[j]) = (hue[j], hue[i]); }
    for (int v = 1; v < 16; v++) tiles[v].BackgroundColor = Color.FromHsla(hue[v - 1] / 15f, 0.6f, 0.6f, 1f);
    int n = 1;
    for (int r = 0; r < 4; r++)
        for (int c = 0; c < 4; c++)
            board[r, c] = (r == 3 && c == 3) ? 0 : n++;
    for (int i = 0; i < 400; i++) Slide(rnd.Next(4), false);   // legal moves keep it solvable
    if (Solved()) Slide(rnd.Next(4), false);                   // never start already solved
    moves = 0;
    won = false;
    Snap();
}

// Build the 15 physical tiles once, absolute-positioned by their own TranslationX/Y.
// The 16th slot is simply never occupied — a real hole in the absolute layout.
var tileViews = new List<SkiaControl>();
for (int v = 1; v < 16; v++)
{
    int val = v;                   // copy loop var so each tap closure keeps its own value
    tileViews.Add(new SkiaShape
    {
        UseCache = SkiaCacheType.GPU,
        AnimationTapped = SkiaTouchAnimation.Shimmer,
        TouchEffectColor = Colors.White.WithAlpha(0.5),
        AnimationTappedSpeed = 200,    
        Type = ShapeType.Rectangle, 
        CornerRadius = 8,
        WidthRequest = 66, HeightRequest = 66,
        BevelType = BevelType.Bevel,   // raised candy tile: light top-left, shadow bottom-right
        Bevel = new SkiaBevel 
        { 
            Depth = 3, 
            LightColor = Color.FromHex("#F0F0F0"), 
            ShadowColor = Color.FromHex("#202020"), 
            Opacity = 0.45 
        },
        Children =
        {
            new SkiaLabel(val.ToString())
            {
                FontSize = 27, FontAttributes = FontAttributes.Bold,
                TextColor = Color.FromHex("#CC000000"),
                // engraved/embossed digits: soft light offset under the dark glyph
                DropShadowColor = Color.FromHex("#55FFFFFF"),
                DropShadowOffsetX = 1, DropShadowOffsetY = 1, DropShadowSize = 1,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Center,
            },
        },
    }
    .Assign(out tiles[val])
    .OnTapped(me => 
    {
        //sync with shimmer
        Tasks.StartDelayed(TimeSpan.FromMilliseconds(10),()=>
        {
            Tap(val);
        });
    }));   
}

return new SkiaLayout
{
    UseCache = SkiaCacheType.Operations,
    Type = LayoutType.Column,
    Spacing = 12,
    HorizontalOptions = LayoutOptions.Center,
    VerticalOptions = LayoutOptions.Center,
    Children =
    {
        new SkiaLabel("15 Puzzle")
        {
            FontSize = 30, FontAttributes = FontAttributes.Bold,
            TextColor = Color.FromHex("#E2E8F0"),
            HorizontalOptions = LayoutOptions.Center,
        },
        new SkiaLabel("MOVES 0") { FontSize = 14, TextColor = Color.FromHex("#94A3B8"), HorizontalOptions = LayoutOptions.Center }.Assign(out movesLabel),
        new SkiaLayout
        {
            HorizontalOptions = LayoutOptions.Center,
            Children = 
            {
                new SkiaShape
                {
                    UseCache = SkiaCacheType.Image,
                    BevelType = BevelType.Emboss,
                    Bevel = new SkiaBevel 
                    { 
                        Depth = 3, 
                        ShadowColor = Colors.Black, 
                        LightColor = Colors.White, 
                        Opacity = 0.35 
                    },
                    Type = ShapeType.Rectangle, 
                    CornerRadius = 8,
                    BackgroundColor = Color.FromHex("#F3E5D9"),
                    Padding = 4,
                    HorizontalOptions = LayoutOptions.Fill,
                    VerticalOptions = LayoutOptions.Fill,
                    Children =
                    {

                    }
                },
                // Absolute playfield: tiles place themselves by translation.
                new SkiaLayout
                {
                    Margin=4,
                    UseCache = SkiaCacheType.Operations,
                    Type = LayoutType.Absolute,
                    WidthRequest = 269, 
                    LockRatio = 1,
                    HorizontalOptions = LayoutOptions.Center,
                    VerticalOptions = LayoutOptions.Center,
                    Children = tileViews,
                },
                new SkiaShape()
                {
                    IsVisible = false,
                    UseCache = SkiaCacheType.Operations,
                    HorizontalOptions = LayoutOptions.Center,
                    VerticalOptions = LayoutOptions.Center,
                    BackgroundColor = Color.FromHex("#99000000"),
                    CornerRadius = 8,
                    Children = 
                    {
                        new SkiaLabel("SOLVED!!!")
                        {
                            Padding = 16,
                            FontSize = 24, FontAttributes = FontAttributes.Bold,
                            TextColor = Color.FromHex("#FFD9D0"),
                            HorizontalTextAlignment = DrawTextAlignment.Center,
                        },
                    }
            }.Assign(out winLabel).OnTapped(me => Reset()),                
    
            }
        },
        new SkiaRichLabel("← → ↑ ↓  slide  ·  tap a tile  ·  Space shuffle")
        {
            FontSize = 12, TextColor = Color.FromHex("#64748B"),
            HorizontalOptions = LayoutOptions.Center,
        },
    }
}
.OnKeyDown((me, key) =>
{
    if (won)
    {
        if (key == InputKey.Space || key == InputKey.Enter) Reset();
        return;
    }
    int dir = key switch
    {
        InputKey.ArrowLeft => 0, InputKey.ArrowRight => 1,
        InputKey.ArrowUp => 2, InputKey.ArrowDown => 3, _ => -1,
    };
    if (dir < 0) return;
    if (Slide(dir, true))
    {
        moves++;
        if (Solved()) won = true;
        movesLabel.Text = $"MOVES {moves}";
        winLabel.IsVisible = won;
    }
})
.Adapt(me => Reset());